Fix feed form so user can only choose from dropdown

This commit is contained in:
myrmidex 2025-08-09 17:05:11 +02:00
parent 696e2b5235
commit e495f49481
8 changed files with 195 additions and 76 deletions

View file

@ -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(

View file

@ -35,7 +35,7 @@ public function status(): JsonResponse
$hasFeed = Feed::where('is_active', true)->exists();
$hasChannel = PlatformChannel::where('is_active', true)->exists();
$hasRoute = Route::where('is_active', true)->exists();
// Check if onboarding was explicitly skipped
$onboardingSkipped = Setting::where('key', 'onboarding_skipped')->value('value') === 'true';
@ -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.');
}
@ -166,19 +173,19 @@ public function createPlatform(Request $request): JsonResponse
if (str_contains($e->getMessage(), 'Rate limited by')) {
return $this->sendError($e->getMessage(), [], 429);
}
return $this->sendError('Invalid username or password. Please check your credentials and try again.', [], 422);
} catch (\Exception $e) {
$message = 'Unable to connect to the Lemmy instance. Please check the URL and try again.';
// If it's a network/connection issue, provide a more specific message
if (str_contains(strtolower($e->getMessage()), 'connection') ||
if (str_contains(strtolower($e->getMessage()), 'connection') ||
str_contains(strtolower($e->getMessage()), 'network') ||
str_contains(strtolower($e->getMessage()), 'timeout')) {
$message = 'Connection failed. Please check the instance URL and your internet connection.';
}
return $this->sendError($message, [], 422);
}
}
@ -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,
@ -292,7 +313,7 @@ public function complete(): JsonResponse
// or create a setting that tracks onboarding completion
// For now, we'll just return success since the onboarding status
// is determined by the existence of platform accounts, feeds, and channels
return $this->sendResponse(
['completed' => true],
'Onboarding completed successfully.'
@ -308,7 +329,7 @@ public function skip(): JsonResponse
['key' => 'onboarding_skipped'],
['value' => 'true']
);
return $this->sendResponse(
['skipped' => true],
'Onboarding skipped successfully.'
@ -321,10 +342,10 @@ public function skip(): JsonResponse
public function resetSkip(): JsonResponse
{
Setting::where('key', 'onboarding_skipped')->delete();
return $this->sendResponse(
['reset' => true],
'Onboarding skip status reset successfully.'
);
}
}
}

View file

@ -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'

View file

@ -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,
]);
}
}
}

View file

@ -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

View file

@ -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', []);

View file

@ -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;
}

View file

@ -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>