Fix feed form so user can only choose from dropdown
This commit is contained in:
parent
696e2b5235
commit
e495f49481
8 changed files with 195 additions and 76 deletions
|
|
@ -47,6 +47,19 @@ public function store(StoreFeedRequest $request): JsonResponse
|
|||
$validated = $request->validated();
|
||||
$validated['is_active'] = $validated['is_active'] ?? true;
|
||||
|
||||
// Map provider to URL and set type
|
||||
$providers = [
|
||||
'vrt' => new \App\Services\Parsers\VrtHomepageParserAdapter(),
|
||||
'belga' => new \App\Services\Parsers\BelgaHomepageParserAdapter(),
|
||||
];
|
||||
|
||||
$adapter = $providers[$validated['provider']];
|
||||
$validated['url'] = $adapter->getHomepageUrl();
|
||||
$validated['type'] = 'website';
|
||||
|
||||
// Remove provider from validated data as it's not a database column
|
||||
unset($validated['provider']);
|
||||
|
||||
$feed = Feed::create($validated);
|
||||
|
||||
return $this->sendResponse(
|
||||
|
|
|
|||
|
|
@ -90,11 +90,18 @@ public function options(): JsonResponse
|
|||
->orderBy('name')
|
||||
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']);
|
||||
|
||||
// Expose supported feed providers so frontend can offer only VRT and Belga
|
||||
$feedProviders = [
|
||||
['code' => 'vrt', 'name' => (new \App\Services\Parsers\VrtHomepageParserAdapter())->getSourceName()],
|
||||
['code' => 'belga', 'name' => (new \App\Services\Parsers\BelgaHomepageParserAdapter())->getSourceName()],
|
||||
];
|
||||
|
||||
return $this->sendResponse([
|
||||
'languages' => $languages,
|
||||
'platform_instances' => $platformInstances,
|
||||
'feeds' => $feeds,
|
||||
'platform_channels' => $platformChannels,
|
||||
'feed_providers' => $feedProviders,
|
||||
], 'Onboarding options retrieved successfully.');
|
||||
}
|
||||
|
||||
|
|
@ -190,8 +197,7 @@ public function createFeed(Request $request): JsonResponse
|
|||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'url' => 'required|url|max:500',
|
||||
'type' => 'required|in:website,rss',
|
||||
'provider' => 'required|string|in:vrt,belga',
|
||||
'language_id' => 'required|exists:languages,id',
|
||||
'description' => 'nullable|string|max:1000',
|
||||
]);
|
||||
|
|
@ -202,10 +208,25 @@ public function createFeed(Request $request): JsonResponse
|
|||
|
||||
$validated = $validator->validated();
|
||||
|
||||
// Supported providers and their canonical definitions
|
||||
$providers = [
|
||||
'vrt' => new \App\Services\Parsers\VrtHomepageParserAdapter(),
|
||||
'belga' => new \App\Services\Parsers\BelgaHomepageParserAdapter(),
|
||||
];
|
||||
|
||||
$adapter = $providers[$validated['provider']];
|
||||
$finalUrl = $adapter->getHomepageUrl();
|
||||
$finalName = $validated['name'];
|
||||
|
||||
// Keep user provided name, but default to provider name if empty/blank
|
||||
if (trim($finalName) === '') {
|
||||
$finalName = $adapter->getSourceName();
|
||||
}
|
||||
|
||||
$feed = Feed::create([
|
||||
'name' => $validated['name'],
|
||||
'url' => $validated['url'],
|
||||
'type' => $validated['type'],
|
||||
'name' => $finalName,
|
||||
'url' => $finalUrl,
|
||||
'type' => 'website',
|
||||
'language_id' => $validated['language_id'],
|
||||
'description' => $validated['description'] ?? null,
|
||||
'is_active' => true,
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ public function rules(): array
|
|||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'url' => 'required|url|unique:feeds,url',
|
||||
'type' => 'required|in:website,rss',
|
||||
'provider' => 'required|in:vrt,belga',
|
||||
'language_id' => 'required|exists:languages,id',
|
||||
'description' => 'nullable|string',
|
||||
'is_active' => 'boolean'
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@ public function run(): void
|
|||
{
|
||||
$this->call([
|
||||
SettingsSeeder::class,
|
||||
LanguageSeeder::class,
|
||||
]);
|
||||
|
||||
// Seed languages in local/dev environment only to avoid conflicts in tests
|
||||
if (app()->environment('local')) {
|
||||
$this->call([
|
||||
LanguageSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,14 +47,13 @@ public function test_index_returns_feeds_ordered_by_active_status_then_name(): v
|
|||
$this->assertFalse($feeds[2]['is_active']);
|
||||
}
|
||||
|
||||
public function test_store_creates_feed_successfully(): void
|
||||
public function test_store_creates_vrt_feed_successfully(): void
|
||||
{
|
||||
$language = Language::factory()->create();
|
||||
|
||||
$feedData = [
|
||||
'name' => 'Test Feed',
|
||||
'url' => 'https://example.com/feed.xml',
|
||||
'type' => 'rss',
|
||||
'name' => 'VRT Test Feed',
|
||||
'provider' => 'vrt',
|
||||
'language_id' => $language->id,
|
||||
'is_active' => true,
|
||||
];
|
||||
|
|
@ -66,17 +65,49 @@ public function test_store_creates_feed_successfully(): void
|
|||
'success' => true,
|
||||
'message' => 'Feed created successfully!',
|
||||
'data' => [
|
||||
'name' => 'Test Feed',
|
||||
'url' => 'https://example.com/feed.xml',
|
||||
'type' => 'rss',
|
||||
'name' => 'VRT Test Feed',
|
||||
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||
'type' => 'website',
|
||||
'is_active' => true,
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('feeds', [
|
||||
'name' => 'Test Feed',
|
||||
'url' => 'https://example.com/feed.xml',
|
||||
'type' => 'rss',
|
||||
'name' => 'VRT Test Feed',
|
||||
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||
'type' => 'website',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_creates_belga_feed_successfully(): void
|
||||
{
|
||||
$language = Language::factory()->create();
|
||||
|
||||
$feedData = [
|
||||
'name' => 'Belga Test Feed',
|
||||
'provider' => 'belga',
|
||||
'language_id' => $language->id,
|
||||
'is_active' => true,
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/v1/feeds', $feedData);
|
||||
|
||||
$response->assertStatus(201)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'Feed created successfully!',
|
||||
'data' => [
|
||||
'name' => 'Belga Test Feed',
|
||||
'url' => 'https://www.belganewsagency.eu/',
|
||||
'type' => 'website',
|
||||
'is_active' => true,
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('feeds', [
|
||||
'name' => 'Belga Test Feed',
|
||||
'url' => 'https://www.belganewsagency.eu/',
|
||||
'type' => 'website',
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -86,8 +117,7 @@ public function test_store_sets_default_active_status(): void
|
|||
|
||||
$feedData = [
|
||||
'name' => 'Test Feed',
|
||||
'url' => 'https://example.com/feed.xml',
|
||||
'type' => 'rss',
|
||||
'provider' => 'vrt',
|
||||
'language_id' => $language->id,
|
||||
// Not setting is_active
|
||||
];
|
||||
|
|
@ -107,7 +137,23 @@ public function test_store_validates_required_fields(): void
|
|||
$response = $this->postJson('/api/v1/feeds', []);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['name', 'url', 'type']);
|
||||
->assertJsonValidationErrors(['name', 'provider', 'language_id']);
|
||||
}
|
||||
|
||||
public function test_store_rejects_invalid_provider(): void
|
||||
{
|
||||
$language = Language::factory()->create();
|
||||
|
||||
$feedData = [
|
||||
'name' => 'Invalid Feed',
|
||||
'provider' => 'invalid',
|
||||
'language_id' => $language->id,
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/v1/feeds', $feedData);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['provider']);
|
||||
}
|
||||
|
||||
public function test_show_returns_feed_successfully(): void
|
||||
|
|
|
|||
|
|
@ -191,15 +191,14 @@ public function test_create_feed_validates_required_fields()
|
|||
$response = $this->postJson('/api/v1/onboarding/feed', []);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['name', 'url', 'type', 'language_id']);
|
||||
->assertJsonValidationErrors(['name', 'provider', 'language_id']);
|
||||
}
|
||||
|
||||
public function test_create_feed_creates_feed_successfully()
|
||||
public function test_create_feed_creates_vrt_feed_successfully()
|
||||
{
|
||||
$feedData = [
|
||||
'name' => 'Test Feed',
|
||||
'url' => 'https://example.com/rss',
|
||||
'type' => 'rss',
|
||||
'name' => 'VRT Test Feed',
|
||||
'provider' => 'vrt',
|
||||
'language_id' => 1,
|
||||
'description' => 'Test description',
|
||||
];
|
||||
|
|
@ -210,22 +209,68 @@ public function test_create_feed_creates_feed_successfully()
|
|||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'name' => 'Test Feed',
|
||||
'url' => 'https://example.com/rss',
|
||||
'type' => 'rss',
|
||||
'name' => 'VRT Test Feed',
|
||||
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||
'type' => 'website',
|
||||
'is_active' => true,
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('feeds', [
|
||||
'name' => 'Test Feed',
|
||||
'url' => 'https://example.com/rss',
|
||||
'type' => 'rss',
|
||||
'name' => 'VRT Test Feed',
|
||||
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||
'type' => 'website',
|
||||
'language_id' => 1,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_create_feed_creates_belga_feed_successfully()
|
||||
{
|
||||
$feedData = [
|
||||
'name' => 'Belga Test Feed',
|
||||
'provider' => 'belga',
|
||||
'language_id' => 1,
|
||||
'description' => 'Test description',
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/v1/onboarding/feed', $feedData);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'name' => 'Belga Test Feed',
|
||||
'url' => 'https://www.belganewsagency.eu/',
|
||||
'type' => 'website',
|
||||
'is_active' => true,
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('feeds', [
|
||||
'name' => 'Belga Test Feed',
|
||||
'url' => 'https://www.belganewsagency.eu/',
|
||||
'type' => 'website',
|
||||
'language_id' => 1,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_create_feed_rejects_invalid_provider()
|
||||
{
|
||||
$feedData = [
|
||||
'name' => 'Invalid Feed',
|
||||
'provider' => 'invalid',
|
||||
'language_id' => 1,
|
||||
'description' => 'Test description',
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/v1/onboarding/feed', $feedData);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['provider']);
|
||||
}
|
||||
|
||||
public function test_create_channel_validates_required_fields()
|
||||
{
|
||||
$response = $this->postJson('/api/v1/onboarding/channel', []);
|
||||
|
|
|
|||
|
|
@ -155,11 +155,17 @@ export interface OnboardingStatus {
|
|||
onboarding_skipped: boolean;
|
||||
}
|
||||
|
||||
export interface FeedProvider {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface OnboardingOptions {
|
||||
languages: Language[];
|
||||
platform_instances: PlatformInstance[];
|
||||
feeds: Feed[];
|
||||
platform_channels: PlatformChannel[];
|
||||
feed_providers: FeedProvider[];
|
||||
}
|
||||
|
||||
export interface PlatformAccountRequest {
|
||||
|
|
@ -171,8 +177,7 @@ export interface PlatformAccountRequest {
|
|||
|
||||
export interface FeedRequest {
|
||||
name: string;
|
||||
url: string;
|
||||
type: 'rss' | 'website';
|
||||
provider: 'vrt' | 'belga';
|
||||
language_id: number;
|
||||
description?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { apiClient, type FeedRequest, type Language } from '../../../lib/api';
|
||||
import { apiClient, type FeedRequest, type Language, type FeedProvider } from '../../../lib/api';
|
||||
|
||||
const FeedStep: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [formData, setFormData] = useState<FeedRequest>({
|
||||
name: '',
|
||||
url: '',
|
||||
type: 'rss',
|
||||
provider: 'vrt',
|
||||
language_id: 0,
|
||||
description: ''
|
||||
});
|
||||
|
|
@ -56,7 +55,7 @@ const FeedStep: React.FC = () => {
|
|||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">Add Your First Feed</h1>
|
||||
<p className="text-gray-600">
|
||||
Add a RSS feed or website to monitor for new articles
|
||||
Choose from our supported news providers to monitor for new articles
|
||||
</p>
|
||||
|
||||
{/* Progress indicator */}
|
||||
|
|
@ -93,40 +92,25 @@ const FeedStep: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="url" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Feed URL
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="url"
|
||||
value={formData.url}
|
||||
onChange={(e) => handleChange('url', e.target.value)}
|
||||
placeholder="https://example.com/rss.xml"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
{errors.url && (
|
||||
<p className="text-red-600 text-sm mt-1">{errors.url[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Feed Type
|
||||
<label htmlFor="provider" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
News Provider
|
||||
</label>
|
||||
<select
|
||||
id="type"
|
||||
value={formData.type}
|
||||
onChange={(e) => handleChange('type', e.target.value)}
|
||||
id="provider"
|
||||
value={formData.provider}
|
||||
onChange={(e) => handleChange('provider', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
required
|
||||
>
|
||||
<option value="">Select feed type</option>
|
||||
<option value="rss">RSS Feed</option>
|
||||
<option value="website">Website</option>
|
||||
<option value="">Select news provider</option>
|
||||
{options?.feed_providers?.map((provider: FeedProvider) => (
|
||||
<option key={provider.code} value={provider.code}>
|
||||
{provider.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.type && (
|
||||
<p className="text-red-600 text-sm mt-1">{errors.type[0]}</p>
|
||||
{errors.provider && (
|
||||
<p className="text-red-600 text-sm mt-1">{errors.provider[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue