Instance communities retrieval
This commit is contained in:
parent
58848c934e
commit
25cae3c0e9
7 changed files with 1047 additions and 19 deletions
|
|
@ -5,11 +5,14 @@
|
||||||
use Domains\Platform\Resources\PlatformChannelResource;
|
use Domains\Platform\Resources\PlatformChannelResource;
|
||||||
use Domains\Platform\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use Domains\Platform\Models\PlatformAccount;
|
use Domains\Platform\Models\PlatformAccount;
|
||||||
|
use Domains\Platform\Models\PlatformInstance;
|
||||||
use Domains\Platform\Services\ChannelLanguageDetectionService;
|
use Domains\Platform\Services\ChannelLanguageDetectionService;
|
||||||
|
use Domains\Platform\Api\Lemmy\LemmyApiService;
|
||||||
use Domains\Platform\Exceptions\ChannelException;
|
use Domains\Platform\Exceptions\ChannelException;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class PlatformChannelsController extends BaseController
|
class PlatformChannelsController extends BaseController
|
||||||
{
|
{
|
||||||
|
|
@ -271,4 +274,108 @@ public function updateAccountRelation(PlatformChannel $channel, PlatformAccount
|
||||||
return $this->sendError('Failed to update platform account relationship: ' . $e->getMessage(), [], 500);
|
return $this->sendError('Failed to update platform account relationship: ' . $e->getMessage(), [], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available communities for a platform instance
|
||||||
|
*/
|
||||||
|
public function getCommunities(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'platform_instance_id' => 'required|exists:platform_instances,id',
|
||||||
|
'type' => 'sometimes|string|in:Local,All,Subscribed',
|
||||||
|
'sort' => 'sometimes|string|in:Hot,Active,New,TopDay,TopWeek,TopMonth,TopYear,TopAll',
|
||||||
|
'limit' => 'sometimes|integer|min:1|max:100',
|
||||||
|
'page' => 'sometimes|integer|min:1',
|
||||||
|
'show_nsfw' => 'sometimes|boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$platformInstance = PlatformInstance::findOrFail($validated['platform_instance_id']);
|
||||||
|
|
||||||
|
// Check if there are active platform accounts for this instance to get auth token
|
||||||
|
$activeAccount = PlatformAccount::where('instance_url', $platformInstance->url)
|
||||||
|
->where('is_active', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$activeAccount) {
|
||||||
|
return $this->sendError(
|
||||||
|
'Cannot fetch communities: No active platform accounts found for this instance. Please create a platform account first.',
|
||||||
|
[],
|
||||||
|
422
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create cache key based on instance and parameters
|
||||||
|
$cacheKey = sprintf(
|
||||||
|
'communities:%s:%s:%s:%d:%d:%s',
|
||||||
|
$platformInstance->id,
|
||||||
|
$validated['type'] ?? 'Local',
|
||||||
|
$validated['sort'] ?? 'Active',
|
||||||
|
$validated['limit'] ?? 50,
|
||||||
|
$validated['page'] ?? 1,
|
||||||
|
$validated['show_nsfw'] ?? false ? '1' : '0'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to get communities from cache first (cache for 10 minutes)
|
||||||
|
$communities = Cache::remember($cacheKey, 600, function () use ($platformInstance, $activeAccount, $validated) {
|
||||||
|
$apiService = app(LemmyApiService::class, ['instance' => $platformInstance->url]);
|
||||||
|
|
||||||
|
return $apiService->listCommunities(
|
||||||
|
$activeAccount->settings['api_token'] ?? null,
|
||||||
|
$validated['type'] ?? 'Local',
|
||||||
|
$validated['sort'] ?? 'Active',
|
||||||
|
$validated['limit'] ?? 50,
|
||||||
|
$validated['page'] ?? 1,
|
||||||
|
$validated['show_nsfw'] ?? false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transform the response to include only relevant data and add helpful fields
|
||||||
|
$transformedCommunities = collect($communities['communities'] ?? [])->map(function ($item) {
|
||||||
|
$community = $item['community'] ?? [];
|
||||||
|
return [
|
||||||
|
'id' => $community['id'] ?? null,
|
||||||
|
'name' => $community['name'] ?? null,
|
||||||
|
'title' => $community['title'] ?? null,
|
||||||
|
'description' => $community['description'] ?? null,
|
||||||
|
'nsfw' => $community['nsfw'] ?? false,
|
||||||
|
'local' => $community['local'] ?? false,
|
||||||
|
'subscribers' => $item['counts']['subscribers'] ?? 0,
|
||||||
|
'posts' => $item['counts']['posts'] ?? 0,
|
||||||
|
'display_text' => sprintf(
|
||||||
|
'%s (%s subscribers)',
|
||||||
|
$community['title'] ?? $community['name'] ?? 'Unknown',
|
||||||
|
number_format($item['counts']['subscribers'] ?? 0)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->sendResponse([
|
||||||
|
'communities' => $transformedCommunities,
|
||||||
|
'total' => $transformedCommunities->count(),
|
||||||
|
'platform_instance' => [
|
||||||
|
'id' => $platformInstance->id,
|
||||||
|
'name' => $platformInstance->name,
|
||||||
|
'url' => $platformInstance->url,
|
||||||
|
],
|
||||||
|
'parameters' => [
|
||||||
|
'type' => $validated['type'] ?? 'Local',
|
||||||
|
'sort' => $validated['sort'] ?? 'Active',
|
||||||
|
'limit' => $validated['limit'] ?? 50,
|
||||||
|
'page' => $validated['page'] ?? 1,
|
||||||
|
'show_nsfw' => $validated['show_nsfw'] ?? false,
|
||||||
|
]
|
||||||
|
], 'Communities retrieved successfully.');
|
||||||
|
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return $this->sendValidationError($e->errors());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Clear cache on error to prevent serving stale data
|
||||||
|
if (isset($cacheKey)) {
|
||||||
|
Cache::forget($cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->sendError('Failed to fetch communities: ' . $e->getMessage(), [], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +57,10 @@
|
||||||
->name('api.platform-accounts.set-active');
|
->name('api.platform-accounts.set-active');
|
||||||
|
|
||||||
// Platform Channels
|
// Platform Channels
|
||||||
|
// NOTE: Specific routes must come before resource routes to avoid parameter conflicts
|
||||||
|
Route::get('/platform-channels/communities', [PlatformChannelsController::class, 'getCommunities'])
|
||||||
|
->name('api.platform-channels.get-communities');
|
||||||
|
|
||||||
Route::apiResource('platform-channels', PlatformChannelsController::class)->names([
|
Route::apiResource('platform-channels', PlatformChannelsController::class)->names([
|
||||||
'index' => 'api.platform-channels.index',
|
'index' => 'api.platform-channels.index',
|
||||||
'store' => 'api.platform-channels.store',
|
'store' => 'api.platform-channels.store',
|
||||||
|
|
|
||||||
|
|
@ -246,4 +246,70 @@ public function getLanguages(): array
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List communities on the instance with optional filtering
|
||||||
|
*
|
||||||
|
* @param string|null $token
|
||||||
|
* @param string $type Local, All, or Subscribed
|
||||||
|
* @param string $sort Hot, Active, New, TopDay, TopWeek, TopMonth, TopYear, TopAll
|
||||||
|
* @param int $limit Maximum number of communities to return (default: 50)
|
||||||
|
* @param int $page Page number for pagination (default: 1)
|
||||||
|
* @param bool $showNsfw Whether to include NSFW communities (default: false)
|
||||||
|
* @return array<string, mixed>
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function listCommunities(
|
||||||
|
?string $token = null,
|
||||||
|
string $type = 'Local',
|
||||||
|
string $sort = 'Active',
|
||||||
|
int $limit = 50,
|
||||||
|
int $page = 1,
|
||||||
|
bool $showNsfw = false
|
||||||
|
): array {
|
||||||
|
try {
|
||||||
|
$request = new LemmyRequest($this->instance, $token);
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'type_' => $type,
|
||||||
|
'sort' => $sort,
|
||||||
|
'limit' => $limit,
|
||||||
|
'page' => $page,
|
||||||
|
'show_nsfw' => $showNsfw,
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $request->get('community/list', $params);
|
||||||
|
|
||||||
|
if (!$response->successful()) {
|
||||||
|
$statusCode = $response->status();
|
||||||
|
$responseBody = $response->body();
|
||||||
|
|
||||||
|
logger()->warning('Failed to fetch communities list', [
|
||||||
|
'status' => $statusCode,
|
||||||
|
'response' => $responseBody,
|
||||||
|
'params' => $params
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new Exception("Failed to fetch communities list: {$statusCode} - {$responseBody}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
|
||||||
|
if (!isset($data['communities'])) {
|
||||||
|
logger()->warning('Invalid communities list response format', ['response' => $data]);
|
||||||
|
return ['communities' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
logger()->error('Exception while fetching communities list', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'type' => $type,
|
||||||
|
'sort' => $sort,
|
||||||
|
'limit' => $limit,
|
||||||
|
'page' => $page
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@
|
||||||
use Domains\Platform\Models\PlatformChannel;
|
use Domains\Platform\Models\PlatformChannel;
|
||||||
use Domains\Platform\Models\PlatformInstance;
|
use Domains\Platform\Models\PlatformInstance;
|
||||||
use Domains\Platform\Services\ChannelLanguageDetectionService;
|
use Domains\Platform\Services\ChannelLanguageDetectionService;
|
||||||
|
use Domains\Platform\Api\Lemmy\LemmyApiService;
|
||||||
use Domains\Settings\Models\Language;
|
use Domains\Settings\Models\Language;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
|
|
||||||
|
|
@ -270,4 +272,390 @@ public function test_toggle_deactivates_active_channel(): void
|
||||||
'is_active' => false
|
'is_active' => false
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_get_communities_returns_successful_response(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$instance = PlatformInstance::factory()->create(['url' => 'lemmy.world']);
|
||||||
|
$account = PlatformAccount::factory()->create([
|
||||||
|
'instance_url' => $instance->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'settings' => ['api_token' => 'test-token']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock LemmyApiService
|
||||||
|
$mockApiService = Mockery::mock(LemmyApiService::class);
|
||||||
|
$mockApiService->shouldReceive('listCommunities')
|
||||||
|
->with('test-token', 'Local', 'Active', 50, 1, false)
|
||||||
|
->once()
|
||||||
|
->andReturn([
|
||||||
|
'communities' => [
|
||||||
|
[
|
||||||
|
'community' => [
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'technology',
|
||||||
|
'title' => 'Technology',
|
||||||
|
'description' => 'Tech discussions',
|
||||||
|
'nsfw' => false,
|
||||||
|
'local' => true
|
||||||
|
],
|
||||||
|
'counts' => [
|
||||||
|
'subscribers' => 1500,
|
||||||
|
'posts' => 250
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'community' => [
|
||||||
|
'id' => 2,
|
||||||
|
'name' => 'news',
|
||||||
|
'title' => 'News',
|
||||||
|
'description' => 'Latest news',
|
||||||
|
'nsfw' => false,
|
||||||
|
'local' => true
|
||||||
|
],
|
||||||
|
'counts' => [
|
||||||
|
'subscribers' => 2300,
|
||||||
|
'posts' => 450
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->app->bind(LemmyApiService::class, function () use ($mockApiService) {
|
||||||
|
return $mockApiService;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson('/api/v1/platform-channels/communities?platform_instance_id=' . $instance->id);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJsonStructure([
|
||||||
|
'success',
|
||||||
|
'message',
|
||||||
|
'data' => [
|
||||||
|
'communities' => [
|
||||||
|
'*' => [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'title',
|
||||||
|
'description',
|
||||||
|
'nsfw',
|
||||||
|
'local',
|
||||||
|
'subscribers',
|
||||||
|
'posts',
|
||||||
|
'display_text'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'total',
|
||||||
|
'platform_instance' => [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'url'
|
||||||
|
],
|
||||||
|
'parameters' => [
|
||||||
|
'type',
|
||||||
|
'sort',
|
||||||
|
'limit',
|
||||||
|
'page',
|
||||||
|
'show_nsfw'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Communities retrieved successfully.',
|
||||||
|
'data' => [
|
||||||
|
'communities' => [
|
||||||
|
[
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'technology',
|
||||||
|
'title' => 'Technology',
|
||||||
|
'subscribers' => 1500,
|
||||||
|
'display_text' => 'Technology (1,500 subscribers)'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 2,
|
||||||
|
'name' => 'news',
|
||||||
|
'title' => 'News',
|
||||||
|
'subscribers' => 2300,
|
||||||
|
'display_text' => 'News (2,300 subscribers)'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'total' => 2,
|
||||||
|
'parameters' => [
|
||||||
|
'type' => 'Local',
|
||||||
|
'sort' => 'Active',
|
||||||
|
'limit' => 50,
|
||||||
|
'page' => 1,
|
||||||
|
'show_nsfw' => false
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_communities_with_custom_parameters(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$instance = PlatformInstance::factory()->create(['url' => 'lemmy.world']);
|
||||||
|
$account = PlatformAccount::factory()->create([
|
||||||
|
'instance_url' => $instance->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'settings' => ['api_token' => 'test-token']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock LemmyApiService with custom parameters
|
||||||
|
$mockApiService = Mockery::mock(LemmyApiService::class);
|
||||||
|
$mockApiService->shouldReceive('listCommunities')
|
||||||
|
->with('test-token', 'All', 'TopMonth', 25, 2, true)
|
||||||
|
->once()
|
||||||
|
->andReturn(['communities' => []]);
|
||||||
|
|
||||||
|
$this->app->bind(LemmyApiService::class, function () use ($mockApiService) {
|
||||||
|
return $mockApiService;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson('/api/v1/platform-channels/communities?' . http_build_query([
|
||||||
|
'platform_instance_id' => $instance->id,
|
||||||
|
'type' => 'All',
|
||||||
|
'sort' => 'TopMonth',
|
||||||
|
'limit' => 25,
|
||||||
|
'page' => 2,
|
||||||
|
'show_nsfw' => true
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'parameters' => [
|
||||||
|
'type' => 'All',
|
||||||
|
'sort' => 'TopMonth',
|
||||||
|
'limit' => 25,
|
||||||
|
'page' => 2,
|
||||||
|
'show_nsfw' => true
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_communities_validates_required_platform_instance_id(): void
|
||||||
|
{
|
||||||
|
$response = $this->getJson('/api/v1/platform-channels/communities');
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['platform_instance_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_communities_validates_platform_instance_exists(): void
|
||||||
|
{
|
||||||
|
$response = $this->getJson('/api/v1/platform-channels/communities?platform_instance_id=999');
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['platform_instance_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_communities_validates_type_parameter(): void
|
||||||
|
{
|
||||||
|
$instance = PlatformInstance::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/v1/platform-channels/communities?' . http_build_query([
|
||||||
|
'platform_instance_id' => $instance->id,
|
||||||
|
'type' => 'InvalidType'
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_communities_validates_sort_parameter(): void
|
||||||
|
{
|
||||||
|
$instance = PlatformInstance::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/v1/platform-channels/communities?' . http_build_query([
|
||||||
|
'platform_instance_id' => $instance->id,
|
||||||
|
'sort' => 'InvalidSort'
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['sort']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_communities_validates_limit_parameter(): void
|
||||||
|
{
|
||||||
|
$instance = PlatformInstance::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/v1/platform-channels/communities?' . http_build_query([
|
||||||
|
'platform_instance_id' => $instance->id,
|
||||||
|
'limit' => 0 // Invalid: below minimum
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['limit']);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/v1/platform-channels/communities?' . http_build_query([
|
||||||
|
'platform_instance_id' => $instance->id,
|
||||||
|
'limit' => 101 // Invalid: above maximum
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['limit']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_communities_validates_page_parameter(): void
|
||||||
|
{
|
||||||
|
$instance = PlatformInstance::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/v1/platform-channels/communities?' . http_build_query([
|
||||||
|
'platform_instance_id' => $instance->id,
|
||||||
|
'page' => 0 // Invalid: below minimum
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['page']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_communities_fails_when_no_active_account_exists(): void
|
||||||
|
{
|
||||||
|
$instance = PlatformInstance::factory()->create(['url' => 'lemmy.world']);
|
||||||
|
|
||||||
|
// Create an inactive account
|
||||||
|
PlatformAccount::factory()->create([
|
||||||
|
'instance_url' => $instance->url,
|
||||||
|
'is_active' => false
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/v1/platform-channels/communities?platform_instance_id=' . $instance->id);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJson([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Cannot fetch communities: No active platform accounts found for this instance. Please create a platform account first.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_communities_uses_cache(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$instance = PlatformInstance::factory()->create(['url' => 'lemmy.world']);
|
||||||
|
$account = PlatformAccount::factory()->create([
|
||||||
|
'instance_url' => $instance->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'settings' => ['api_token' => 'test-token']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mockData = ['communities' => [['community' => ['id' => 1, 'name' => 'test']]]];
|
||||||
|
$cacheKey = "communities:{$instance->id}:Local:Active:50:1:0";
|
||||||
|
|
||||||
|
// Set cache data
|
||||||
|
Cache::put($cacheKey, $mockData, 600);
|
||||||
|
|
||||||
|
// Mock should not be called since cache hit
|
||||||
|
$mockApiService = Mockery::mock(LemmyApiService::class);
|
||||||
|
$mockApiService->shouldNotReceive('listCommunities');
|
||||||
|
|
||||||
|
$this->app->bind(LemmyApiService::class, function () use ($mockApiService) {
|
||||||
|
return $mockApiService;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson('/api/v1/platform-channels/communities?platform_instance_id=' . $instance->id);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_communities_clears_cache_on_error(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$instance = PlatformInstance::factory()->create(['url' => 'lemmy.world']);
|
||||||
|
$account = PlatformAccount::factory()->create([
|
||||||
|
'instance_url' => $instance->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'settings' => ['api_token' => 'test-token']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cacheKey = "communities:{$instance->id}:Local:Active:50:1:0";
|
||||||
|
|
||||||
|
// Mock LemmyApiService to throw exception
|
||||||
|
$mockApiService = Mockery::mock(LemmyApiService::class);
|
||||||
|
$mockApiService->shouldReceive('listCommunities')
|
||||||
|
->once()
|
||||||
|
->andThrow(new \Exception('API Error'));
|
||||||
|
|
||||||
|
$this->app->bind(LemmyApiService::class, function () use ($mockApiService) {
|
||||||
|
return $mockApiService;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure cache is empty initially so API service gets called
|
||||||
|
Cache::forget($cacheKey);
|
||||||
|
$this->assertFalse(Cache::has($cacheKey));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson('/api/v1/platform-channels/communities?platform_instance_id=' . $instance->id);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(500);
|
||||||
|
// Cache should still not exist since the error prevented caching
|
||||||
|
$this->assertFalse(Cache::has($cacheKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_communities_handles_missing_community_data_gracefully(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$instance = PlatformInstance::factory()->create(['url' => 'lemmy.world']);
|
||||||
|
$account = PlatformAccount::factory()->create([
|
||||||
|
'instance_url' => $instance->url,
|
||||||
|
'is_active' => true,
|
||||||
|
'settings' => ['api_token' => 'test-token']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock LemmyApiService with incomplete community data
|
||||||
|
$mockApiService = Mockery::mock(LemmyApiService::class);
|
||||||
|
$mockApiService->shouldReceive('listCommunities')
|
||||||
|
->once()
|
||||||
|
->andReturn([
|
||||||
|
'communities' => [
|
||||||
|
[
|
||||||
|
'community' => [
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'minimal',
|
||||||
|
// Missing title, description, etc.
|
||||||
|
],
|
||||||
|
// Missing counts
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->app->bind(LemmyApiService::class, function () use ($mockApiService) {
|
||||||
|
return $mockApiService;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson('/api/v1/platform-channels/communities?platform_instance_id=' . $instance->id);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'communities' => [
|
||||||
|
[
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'minimal',
|
||||||
|
'title' => null,
|
||||||
|
'description' => null,
|
||||||
|
'nsfw' => false,
|
||||||
|
'local' => false,
|
||||||
|
'subscribers' => 0,
|
||||||
|
'posts' => 0,
|
||||||
|
'display_text' => 'minimal (0 subscribers)'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -426,4 +426,260 @@ public function test_get_languages_returns_empty_when_all_languages_missing(): v
|
||||||
|
|
||||||
$this->assertEquals([], $languages);
|
$this->assertEquals([], $languages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_list_communities_success_with_default_parameters(): void
|
||||||
|
{
|
||||||
|
Http::fake([
|
||||||
|
'*' => Http::response([
|
||||||
|
'communities' => [
|
||||||
|
[
|
||||||
|
'community' => [
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'technology',
|
||||||
|
'title' => 'Technology Community',
|
||||||
|
'description' => 'All about tech',
|
||||||
|
'nsfw' => false
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'community' => [
|
||||||
|
'id' => 2,
|
||||||
|
'name' => 'news',
|
||||||
|
'title' => 'News Community',
|
||||||
|
'description' => 'Latest news',
|
||||||
|
'nsfw' => false
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], 200)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new LemmyApiService('lemmy.world');
|
||||||
|
$result = $service->listCommunities();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('communities', $result);
|
||||||
|
$this->assertCount(2, $result['communities']);
|
||||||
|
$this->assertEquals('technology', $result['communities'][0]['community']['name']);
|
||||||
|
$this->assertEquals('news', $result['communities'][1]['community']['name']);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
return str_contains($request->url(), '/api/v3/community/list')
|
||||||
|
&& str_contains($request->url(), 'type_=Local')
|
||||||
|
&& str_contains($request->url(), 'sort=Active')
|
||||||
|
&& str_contains($request->url(), 'limit=50')
|
||||||
|
&& str_contains($request->url(), 'page=1')
|
||||||
|
&& str_contains($request->url(), 'show_nsfw=');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_communities_success_with_custom_parameters(): void
|
||||||
|
{
|
||||||
|
Http::fake([
|
||||||
|
'*' => Http::response([
|
||||||
|
'communities' => [
|
||||||
|
[
|
||||||
|
'community' => [
|
||||||
|
'id' => 3,
|
||||||
|
'name' => 'gaming',
|
||||||
|
'title' => 'Gaming Community',
|
||||||
|
'description' => 'Gaming discussions',
|
||||||
|
'nsfw' => false
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], 200)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new LemmyApiService('lemmy.world');
|
||||||
|
$result = $service->listCommunities(
|
||||||
|
'test-token',
|
||||||
|
'All',
|
||||||
|
'TopMonth',
|
||||||
|
25,
|
||||||
|
2,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('communities', $result);
|
||||||
|
$this->assertCount(1, $result['communities']);
|
||||||
|
$this->assertEquals('gaming', $result['communities'][0]['community']['name']);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
return str_contains($request->url(), '/api/v3/community/list')
|
||||||
|
&& str_contains($request->url(), 'type_=All')
|
||||||
|
&& str_contains($request->url(), 'sort=TopMonth')
|
||||||
|
&& str_contains($request->url(), 'limit=25')
|
||||||
|
&& str_contains($request->url(), 'page=2')
|
||||||
|
&& str_contains($request->url(), 'show_nsfw=1')
|
||||||
|
&& $request->header('Authorization')[0] === 'Bearer test-token';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_communities_success_without_token(): void
|
||||||
|
{
|
||||||
|
Http::fake([
|
||||||
|
'*' => Http::response([
|
||||||
|
'communities' => []
|
||||||
|
], 200)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new LemmyApiService('lemmy.world');
|
||||||
|
$result = $service->listCommunities();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('communities', $result);
|
||||||
|
$this->assertEmpty($result['communities']);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
return !$request->hasHeader('Authorization');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_communities_throws_on_unsuccessful_response(): void
|
||||||
|
{
|
||||||
|
Http::fake([
|
||||||
|
'*' => Http::response('Server Error', 500)
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::shouldReceive('warning')->once()->with('Failed to fetch communities list', Mockery::any());
|
||||||
|
Log::shouldReceive('error')->once()->with('Exception while fetching communities list', Mockery::any());
|
||||||
|
|
||||||
|
$service = new LemmyApiService('lemmy.world');
|
||||||
|
|
||||||
|
$this->expectException(Exception::class);
|
||||||
|
$this->expectExceptionMessage('Failed to fetch communities list: 500 - Server Error');
|
||||||
|
|
||||||
|
$service->listCommunities('token');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_communities_throws_on_404_response(): void
|
||||||
|
{
|
||||||
|
Http::fake([
|
||||||
|
'*' => Http::response('Not Found', 404)
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::shouldReceive('warning')->once();
|
||||||
|
Log::shouldReceive('error')->once();
|
||||||
|
|
||||||
|
$service = new LemmyApiService('lemmy.world');
|
||||||
|
|
||||||
|
$this->expectException(Exception::class);
|
||||||
|
$this->expectExceptionMessage('Failed to fetch communities list: 404');
|
||||||
|
|
||||||
|
$service->listCommunities('token');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_communities_returns_empty_communities_on_invalid_response_format(): void
|
||||||
|
{
|
||||||
|
Http::fake([
|
||||||
|
'*' => Http::response([
|
||||||
|
'invalid_format' => 'data'
|
||||||
|
], 200)
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::shouldReceive('warning')->once()->with('Invalid communities list response format', Mockery::any());
|
||||||
|
|
||||||
|
$service = new LemmyApiService('lemmy.world');
|
||||||
|
$result = $service->listCommunities();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('communities', $result);
|
||||||
|
$this->assertEmpty($result['communities']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_communities_handles_exception(): void
|
||||||
|
{
|
||||||
|
Http::fake(function () {
|
||||||
|
throw new Exception('Network error');
|
||||||
|
});
|
||||||
|
|
||||||
|
Log::shouldReceive('error')->once()->with('Exception while fetching communities list', Mockery::any());
|
||||||
|
|
||||||
|
$service = new LemmyApiService('lemmy.world');
|
||||||
|
|
||||||
|
$this->expectException(Exception::class);
|
||||||
|
$this->expectExceptionMessage('Network error');
|
||||||
|
|
||||||
|
$service->listCommunities('token');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_communities_with_all_sort_options(): void
|
||||||
|
{
|
||||||
|
$sortOptions = ['Hot', 'Active', 'New', 'TopDay', 'TopWeek', 'TopMonth', 'TopYear', 'TopAll'];
|
||||||
|
|
||||||
|
foreach ($sortOptions as $sort) {
|
||||||
|
Http::fake([
|
||||||
|
'*' => Http::response(['communities' => []], 200)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new LemmyApiService('lemmy.world');
|
||||||
|
$result = $service->listCommunities(null, 'Local', $sort);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('communities', $result);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) use ($sort) {
|
||||||
|
return str_contains($request->url(), "sort={$sort}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_communities_with_all_type_options(): void
|
||||||
|
{
|
||||||
|
$typeOptions = ['Local', 'All', 'Subscribed'];
|
||||||
|
|
||||||
|
foreach ($typeOptions as $type) {
|
||||||
|
Http::fake([
|
||||||
|
'*' => Http::response(['communities' => []], 200)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new LemmyApiService('lemmy.world');
|
||||||
|
$result = $service->listCommunities(null, $type);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('communities', $result);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) use ($type) {
|
||||||
|
return str_contains($request->url(), "type_={$type}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_communities_pagination_parameters(): void
|
||||||
|
{
|
||||||
|
Http::fake([
|
||||||
|
'*' => Http::response(['communities' => []], 200)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new LemmyApiService('lemmy.world');
|
||||||
|
$result = $service->listCommunities(null, 'Local', 'Active', 100, 3);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('communities', $result);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
return str_contains($request->url(), 'limit=100')
|
||||||
|
&& str_contains($request->url(), 'page=3');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_communities_nsfw_parameter(): void
|
||||||
|
{
|
||||||
|
Http::fake([
|
||||||
|
'*' => Http::response(['communities' => []], 200)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new LemmyApiService('lemmy.world');
|
||||||
|
|
||||||
|
// Test with NSFW enabled
|
||||||
|
$result = $service->listCommunities(null, 'Local', 'Active', 50, 1, true);
|
||||||
|
$this->assertArrayHasKey('communities', $result);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
return str_contains($request->url(), 'show_nsfw=1');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test with NSFW disabled
|
||||||
|
$result = $service->listCommunities(null, 'Local', 'Active', 50, 1, false);
|
||||||
|
$this->assertArrayHasKey('communities', $result);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
return str_contains($request->url(), 'show_nsfw=');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,44 @@ export interface KeywordRequest {
|
||||||
is_active?: boolean;
|
is_active?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Community {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
title: string | null;
|
||||||
|
description: string | null;
|
||||||
|
nsfw: boolean;
|
||||||
|
local: boolean;
|
||||||
|
subscribers: number;
|
||||||
|
posts: number;
|
||||||
|
display_text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommunitiesResponse {
|
||||||
|
communities: Community[];
|
||||||
|
total: number;
|
||||||
|
platform_instance: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
parameters: {
|
||||||
|
type: string;
|
||||||
|
sort: string;
|
||||||
|
limit: number;
|
||||||
|
page: number;
|
||||||
|
show_nsfw: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommunitiesRequest {
|
||||||
|
platform_instance_id: number;
|
||||||
|
type?: 'Local' | 'All' | 'Subscribed';
|
||||||
|
sort?: 'Hot' | 'Active' | 'New' | 'TopDay' | 'TopWeek' | 'TopMonth' | 'TopYear' | 'TopAll';
|
||||||
|
limit?: number;
|
||||||
|
page?: number;
|
||||||
|
show_nsfw?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// API Client class
|
// API Client class
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -475,6 +513,14 @@ class ApiClient {
|
||||||
async deletePlatformAccount(id: number): Promise<void> {
|
async deletePlatformAccount(id: number): Promise<void> {
|
||||||
await axios.delete(`/platform-accounts/${id}`);
|
await axios.delete(`/platform-accounts/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Platform Communities endpoints
|
||||||
|
async getPlatformCommunities(params: CommunitiesRequest): Promise<CommunitiesResponse> {
|
||||||
|
const response = await axios.get<ApiResponse<CommunitiesResponse>>('/platform-channels/communities', {
|
||||||
|
params
|
||||||
|
});
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiClient = new ApiClient();
|
export const apiClient = new ApiClient();
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { apiClient, type ChannelRequest, type Language, type PlatformInstance } from '../../../lib/api';
|
import { apiClient, type ChannelRequest, type Language, type PlatformInstance, type Community } from '../../../lib/api';
|
||||||
|
|
||||||
const ChannelStep: React.FC = () => {
|
const ChannelStep: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -13,6 +13,10 @@ const ChannelStep: React.FC = () => {
|
||||||
description: ''
|
description: ''
|
||||||
});
|
});
|
||||||
const [errors, setErrors] = useState<Record<string, string[]>>({});
|
const [errors, setErrors] = useState<Record<string, string[]>>({});
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
|
const [, setSelectedCommunity] = useState<Community | null>(null);
|
||||||
|
const [manualInput, setManualInput] = useState(false);
|
||||||
|
|
||||||
// Get onboarding options (languages, platform instances)
|
// Get onboarding options (languages, platform instances)
|
||||||
const { data: options, isLoading: optionsLoading } = useQuery({
|
const { data: options, isLoading: optionsLoading } = useQuery({
|
||||||
|
|
@ -27,6 +31,31 @@ const ChannelStep: React.FC = () => {
|
||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch communities when platform instance is selected
|
||||||
|
const { data: communitiesData, isLoading: communitiesLoading } = useQuery({
|
||||||
|
queryKey: ['communities', formData.platform_instance_id],
|
||||||
|
queryFn: () => apiClient.getPlatformCommunities({
|
||||||
|
platform_instance_id: formData.platform_instance_id,
|
||||||
|
type: 'Local',
|
||||||
|
sort: 'Active',
|
||||||
|
limit: 100
|
||||||
|
}),
|
||||||
|
enabled: formData.platform_instance_id > 0 && !manualInput,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter communities based on search term
|
||||||
|
const filteredCommunities = useMemo(() => {
|
||||||
|
if (!communitiesData?.communities) return [];
|
||||||
|
if (!searchTerm) return communitiesData.communities;
|
||||||
|
|
||||||
|
return communitiesData.communities.filter(community =>
|
||||||
|
community.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
community.title?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
community.description?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [communitiesData?.communities, searchTerm]);
|
||||||
|
|
||||||
// Pre-fill form with existing data
|
// Pre-fill form with existing data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (channels && channels.length > 0) {
|
if (channels && channels.length > 0) {
|
||||||
|
|
@ -37,9 +66,34 @@ const ChannelStep: React.FC = () => {
|
||||||
language_id: firstChannel.language_id || 0,
|
language_id: firstChannel.language_id || 0,
|
||||||
description: firstChannel.description || ''
|
description: firstChannel.description || ''
|
||||||
});
|
});
|
||||||
|
setSearchTerm(firstChannel.name || '');
|
||||||
}
|
}
|
||||||
}, [channels]);
|
}, [channels]);
|
||||||
|
|
||||||
|
// Reset community selection when platform instance changes
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedCommunity(null);
|
||||||
|
setSearchTerm('');
|
||||||
|
if (formData.platform_instance_id === 0) {
|
||||||
|
setManualInput(false);
|
||||||
|
}
|
||||||
|
}, [formData.platform_instance_id]);
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (!target.closest('[data-dropdown]')) {
|
||||||
|
setShowDropdown(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showDropdown) {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('click', handleClickOutside);
|
||||||
|
}
|
||||||
|
}, [showDropdown]);
|
||||||
|
|
||||||
const createChannelMutation = useMutation({
|
const createChannelMutation = useMutation({
|
||||||
mutationFn: (data: ChannelRequest) => apiClient.createChannelForOnboarding(data),
|
mutationFn: (data: ChannelRequest) => apiClient.createChannelForOnboarding(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -47,11 +101,12 @@ const ChannelStep: React.FC = () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['onboarding-status'] });
|
queryClient.invalidateQueries({ queryKey: ['onboarding-status'] });
|
||||||
navigate('/onboarding/route');
|
navigate('/onboarding/route');
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: unknown) => {
|
||||||
if (error.response?.data?.errors) {
|
const errorData = error as { response?: { data?: { errors?: Record<string, string[]>; message?: string } } };
|
||||||
setErrors(error.response.data.errors);
|
if (errorData.response?.data?.errors) {
|
||||||
|
setErrors(errorData.response.data.errors);
|
||||||
} else {
|
} else {
|
||||||
setErrors({ general: [error.response?.data?.message || 'An error occurred'] });
|
setErrors({ general: [errorData.response?.data?.message || 'An error occurred'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -70,6 +125,34 @@ const ChannelStep: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCommunitySelect = (community: Community) => {
|
||||||
|
setSelectedCommunity(community);
|
||||||
|
setFormData(prev => ({ ...prev, name: community.name }));
|
||||||
|
setSearchTerm(community.name);
|
||||||
|
setShowDropdown(false);
|
||||||
|
if (errors.name) {
|
||||||
|
setErrors(prev => ({ ...prev, name: [] }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = (value: string) => {
|
||||||
|
setSearchTerm(value);
|
||||||
|
setFormData(prev => ({ ...prev, name: value }));
|
||||||
|
setShowDropdown(true);
|
||||||
|
setSelectedCommunity(null);
|
||||||
|
if (errors.name) {
|
||||||
|
setErrors(prev => ({ ...prev, name: [] }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleManualInput = () => {
|
||||||
|
setManualInput(prev => !prev);
|
||||||
|
setSelectedCommunity(null);
|
||||||
|
setSearchTerm('');
|
||||||
|
setFormData(prev => ({ ...prev, name: '' }));
|
||||||
|
setShowDropdown(false);
|
||||||
|
};
|
||||||
|
|
||||||
if (optionsLoading) {
|
if (optionsLoading) {
|
||||||
return <div className="text-center">Loading...</div>;
|
return <div className="text-center">Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -97,19 +180,97 @@ const ChannelStep: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
Community Name
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||||
</label>
|
Community Name
|
||||||
<input
|
</label>
|
||||||
type="text"
|
{formData.platform_instance_id > 0 && (
|
||||||
id="name"
|
<button
|
||||||
value={formData.name}
|
type="button"
|
||||||
onChange={(e) => handleChange('name', e.target.value)}
|
onClick={toggleManualInput}
|
||||||
placeholder="technology"
|
className="text-sm text-blue-600 hover:text-blue-800"
|
||||||
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
|
{manualInput ? 'Use dropdown' : 'Enter manually'}
|
||||||
/>
|
</button>
|
||||||
<p className="text-sm text-gray-500 mt-1">Enter the community name (without the @ or instance)</p>
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{manualInput || formData.platform_instance_id === 0 ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleChange('name', e.target.value)}
|
||||||
|
placeholder="technology"
|
||||||
|
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
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="relative" data-dropdown>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
|
onFocus={() => setShowDropdown(true)}
|
||||||
|
placeholder="Search communities..."
|
||||||
|
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
|
||||||
|
/>
|
||||||
|
|
||||||
|
{communitiesLoading && (
|
||||||
|
<div className="absolute right-3 top-3">
|
||||||
|
<div className="animate-spin h-4 w-4 border-2 border-blue-500 border-t-transparent rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDropdown && !communitiesLoading && filteredCommunities.length > 0 && (
|
||||||
|
<div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-y-auto">
|
||||||
|
{filteredCommunities.slice(0, 10).map((community) => (
|
||||||
|
<button
|
||||||
|
key={community.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCommunitySelect(community)}
|
||||||
|
className="w-full text-left px-3 py-2 hover:bg-gray-50 focus:bg-gray-50 focus:outline-none border-b border-gray-100 last:border-b-0"
|
||||||
|
>
|
||||||
|
<div className="font-medium text-gray-900">{community.title || community.name}</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
!{community.name} • {community.subscribers.toLocaleString()} subscribers
|
||||||
|
</div>
|
||||||
|
{community.description && (
|
||||||
|
<div className="text-xs text-gray-400 mt-1 truncate">{community.description}</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{filteredCommunities.length > 10 && (
|
||||||
|
<div className="px-3 py-2 text-sm text-gray-500 bg-gray-50">
|
||||||
|
And {filteredCommunities.length - 10} more communities...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDropdown && !communitiesLoading && searchTerm && filteredCommunities.length === 0 && (
|
||||||
|
<div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg p-3">
|
||||||
|
<div className="text-sm text-gray-500">No communities found matching "{searchTerm}"</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleManualInput}
|
||||||
|
className="text-sm text-blue-600 hover:text-blue-800 mt-1"
|
||||||
|
>
|
||||||
|
Enter community name manually
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
|
{manualInput || formData.platform_instance_id === 0
|
||||||
|
? 'Enter the community name (without the @ or instance)'
|
||||||
|
: 'Search and select from available communities, or enter manually'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
{errors.name && (
|
{errors.name && (
|
||||||
<p className="text-red-600 text-sm mt-1">{errors.name[0]}</p>
|
<p className="text-red-600 text-sm mt-1">{errors.name[0]}</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue