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 = $request->validated();
|
||||||
$validated['is_active'] = $validated['is_active'] ?? true;
|
$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);
|
$feed = Feed::create($validated);
|
||||||
|
|
||||||
return $this->sendResponse(
|
return $this->sendResponse(
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,18 @@ public function options(): JsonResponse
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']);
|
->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([
|
return $this->sendResponse([
|
||||||
'languages' => $languages,
|
'languages' => $languages,
|
||||||
'platform_instances' => $platformInstances,
|
'platform_instances' => $platformInstances,
|
||||||
'feeds' => $feeds,
|
'feeds' => $feeds,
|
||||||
'platform_channels' => $platformChannels,
|
'platform_channels' => $platformChannels,
|
||||||
|
'feed_providers' => $feedProviders,
|
||||||
], 'Onboarding options retrieved successfully.');
|
], 'Onboarding options retrieved successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,8 +197,7 @@ public function createFeed(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'url' => 'required|url|max:500',
|
'provider' => 'required|string|in:vrt,belga',
|
||||||
'type' => 'required|in:website,rss',
|
|
||||||
'language_id' => 'required|exists:languages,id',
|
'language_id' => 'required|exists:languages,id',
|
||||||
'description' => 'nullable|string|max:1000',
|
'description' => 'nullable|string|max:1000',
|
||||||
]);
|
]);
|
||||||
|
|
@ -202,10 +208,25 @@ public function createFeed(Request $request): JsonResponse
|
||||||
|
|
||||||
$validated = $validator->validated();
|
$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([
|
$feed = Feed::create([
|
||||||
'name' => $validated['name'],
|
'name' => $finalName,
|
||||||
'url' => $validated['url'],
|
'url' => $finalUrl,
|
||||||
'type' => $validated['type'],
|
'type' => 'website',
|
||||||
'language_id' => $validated['language_id'],
|
'language_id' => $validated['language_id'],
|
||||||
'description' => $validated['description'] ?? null,
|
'description' => $validated['description'] ?? null,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,7 @@ public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'url' => 'required|url|unique:feeds,url',
|
'provider' => 'required|in:vrt,belga',
|
||||||
'type' => 'required|in:website,rss',
|
|
||||||
'language_id' => 'required|exists:languages,id',
|
'language_id' => 'required|exists:languages,id',
|
||||||
'description' => 'nullable|string',
|
'description' => 'nullable|string',
|
||||||
'is_active' => 'boolean'
|
'is_active' => 'boolean'
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,13 @@ public function run(): void
|
||||||
{
|
{
|
||||||
$this->call([
|
$this->call([
|
||||||
SettingsSeeder::class,
|
SettingsSeeder::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Seed languages in local/dev environment only to avoid conflicts in tests
|
||||||
|
if (app()->environment('local')) {
|
||||||
|
$this->call([
|
||||||
LanguageSeeder::class,
|
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']);
|
$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();
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
$feedData = [
|
$feedData = [
|
||||||
'name' => 'Test Feed',
|
'name' => 'VRT Test Feed',
|
||||||
'url' => 'https://example.com/feed.xml',
|
'provider' => 'vrt',
|
||||||
'type' => 'rss',
|
|
||||||
'language_id' => $language->id,
|
'language_id' => $language->id,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
];
|
];
|
||||||
|
|
@ -66,17 +65,49 @@ public function test_store_creates_feed_successfully(): void
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Feed created successfully!',
|
'message' => 'Feed created successfully!',
|
||||||
'data' => [
|
'data' => [
|
||||||
'name' => 'Test Feed',
|
'name' => 'VRT Test Feed',
|
||||||
'url' => 'https://example.com/feed.xml',
|
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||||
'type' => 'rss',
|
'type' => 'website',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('feeds', [
|
$this->assertDatabaseHas('feeds', [
|
||||||
'name' => 'Test Feed',
|
'name' => 'VRT Test Feed',
|
||||||
'url' => 'https://example.com/feed.xml',
|
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||||
'type' => 'rss',
|
'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 = [
|
$feedData = [
|
||||||
'name' => 'Test Feed',
|
'name' => 'Test Feed',
|
||||||
'url' => 'https://example.com/feed.xml',
|
'provider' => 'vrt',
|
||||||
'type' => 'rss',
|
|
||||||
'language_id' => $language->id,
|
'language_id' => $language->id,
|
||||||
// Not setting is_active
|
// Not setting is_active
|
||||||
];
|
];
|
||||||
|
|
@ -107,7 +137,23 @@ public function test_store_validates_required_fields(): void
|
||||||
$response = $this->postJson('/api/v1/feeds', []);
|
$response = $this->postJson('/api/v1/feeds', []);
|
||||||
|
|
||||||
$response->assertStatus(422)
|
$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
|
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 = $this->postJson('/api/v1/onboarding/feed', []);
|
||||||
|
|
||||||
$response->assertStatus(422)
|
$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 = [
|
$feedData = [
|
||||||
'name' => 'Test Feed',
|
'name' => 'VRT Test Feed',
|
||||||
'url' => 'https://example.com/rss',
|
'provider' => 'vrt',
|
||||||
'type' => 'rss',
|
|
||||||
'language_id' => 1,
|
'language_id' => 1,
|
||||||
'description' => 'Test description',
|
'description' => 'Test description',
|
||||||
];
|
];
|
||||||
|
|
@ -210,22 +209,68 @@ public function test_create_feed_creates_feed_successfully()
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
'name' => 'Test Feed',
|
'name' => 'VRT Test Feed',
|
||||||
'url' => 'https://example.com/rss',
|
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||||
'type' => 'rss',
|
'type' => 'website',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('feeds', [
|
$this->assertDatabaseHas('feeds', [
|
||||||
'name' => 'Test Feed',
|
'name' => 'VRT Test Feed',
|
||||||
'url' => 'https://example.com/rss',
|
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||||
'type' => 'rss',
|
'type' => 'website',
|
||||||
'language_id' => 1,
|
'language_id' => 1,
|
||||||
'is_active' => true,
|
'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()
|
public function test_create_channel_validates_required_fields()
|
||||||
{
|
{
|
||||||
$response = $this->postJson('/api/v1/onboarding/channel', []);
|
$response = $this->postJson('/api/v1/onboarding/channel', []);
|
||||||
|
|
|
||||||
|
|
@ -155,11 +155,17 @@ export interface OnboardingStatus {
|
||||||
onboarding_skipped: boolean;
|
onboarding_skipped: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FeedProvider {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface OnboardingOptions {
|
export interface OnboardingOptions {
|
||||||
languages: Language[];
|
languages: Language[];
|
||||||
platform_instances: PlatformInstance[];
|
platform_instances: PlatformInstance[];
|
||||||
feeds: Feed[];
|
feeds: Feed[];
|
||||||
platform_channels: PlatformChannel[];
|
platform_channels: PlatformChannel[];
|
||||||
|
feed_providers: FeedProvider[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlatformAccountRequest {
|
export interface PlatformAccountRequest {
|
||||||
|
|
@ -171,8 +177,7 @@ export interface PlatformAccountRequest {
|
||||||
|
|
||||||
export interface FeedRequest {
|
export interface FeedRequest {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
provider: 'vrt' | 'belga';
|
||||||
type: 'rss' | 'website';
|
|
||||||
language_id: number;
|
language_id: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
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 FeedStep: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [formData, setFormData] = useState<FeedRequest>({
|
const [formData, setFormData] = useState<FeedRequest>({
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
provider: 'vrt',
|
||||||
type: 'rss',
|
|
||||||
language_id: 0,
|
language_id: 0,
|
||||||
description: ''
|
description: ''
|
||||||
});
|
});
|
||||||
|
|
@ -56,7 +55,7 @@ const FeedStep: React.FC = () => {
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">Add Your First Feed</h1>
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">Add Your First Feed</h1>
|
||||||
<p className="text-gray-600">
|
<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>
|
</p>
|
||||||
|
|
||||||
{/* Progress indicator */}
|
{/* Progress indicator */}
|
||||||
|
|
@ -93,40 +92,25 @@ const FeedStep: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="url" className="block text-sm font-medium text-gray-700 mb-2">
|
<label htmlFor="provider" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Feed URL
|
News Provider
|
||||||
</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>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="type"
|
id="provider"
|
||||||
value={formData.type}
|
value={formData.provider}
|
||||||
onChange={(e) => handleChange('type', e.target.value)}
|
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"
|
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
|
required
|
||||||
>
|
>
|
||||||
<option value="">Select feed type</option>
|
<option value="">Select news provider</option>
|
||||||
<option value="rss">RSS Feed</option>
|
{options?.feed_providers?.map((provider: FeedProvider) => (
|
||||||
<option value="website">Website</option>
|
<option key={provider.code} value={provider.code}>
|
||||||
|
{provider.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
{errors.type && (
|
{errors.provider && (
|
||||||
<p className="text-red-600 text-sm mt-1">{errors.type[0]}</p>
|
<p className="text-red-600 text-sm mt-1">{errors.provider[0]}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue