optimizations
This commit is contained in:
@@ -5,17 +5,23 @@ declare(strict_types=1);
|
||||
namespace App\Http\Controllers\Studio;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\Category;
|
||||
use App\Models\ContentType;
|
||||
use App\Models\ArtworkVersion;
|
||||
use App\Services\ArtworkSearchIndexer;
|
||||
use App\Services\TagService;
|
||||
use App\Services\ArtworkVersioningService;
|
||||
use App\Services\Studio\StudioArtworkQueryService;
|
||||
use App\Services\Studio\StudioBulkActionService;
|
||||
use App\Services\Tags\TagDiscoveryService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
|
||||
|
||||
/**
|
||||
@@ -29,6 +35,7 @@ final class StudioArtworksApiController extends Controller
|
||||
private readonly ArtworkVersioningService $versioningService,
|
||||
private readonly ArtworkSearchIndexer $searchIndexer,
|
||||
private readonly TagDiscoveryService $tagDiscoveryService,
|
||||
private readonly TagService $tagService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -117,21 +124,74 @@ final class StudioArtworksApiController extends Controller
|
||||
'title' => 'sometimes|string|max:255',
|
||||
'description' => 'sometimes|nullable|string|max:5000',
|
||||
'is_public' => 'sometimes|boolean',
|
||||
'visibility' => 'sometimes|string|in:public,unlisted,private',
|
||||
'mode' => 'sometimes|string|in:now,schedule',
|
||||
'publish_at' => 'sometimes|nullable|string|date',
|
||||
'timezone' => 'sometimes|nullable|string|max:64',
|
||||
'category_id' => 'sometimes|nullable|integer|exists:categories,id',
|
||||
'content_type_id' => 'sometimes|nullable|integer|exists:content_types,id',
|
||||
'tags' => 'sometimes|array|max:15',
|
||||
'tags.*' => 'string|max:64',
|
||||
'title_source' => 'sometimes|nullable|string|in:manual,ai_generated,ai_applied,mixed',
|
||||
'description_source' => 'sometimes|nullable|string|in:manual,ai_generated,ai_applied,mixed',
|
||||
'tags_source' => 'sometimes|nullable|string|in:manual,ai_generated,ai_applied,mixed',
|
||||
'category_source' => 'sometimes|nullable|string|in:manual,ai_generated,ai_applied,mixed',
|
||||
]);
|
||||
|
||||
if (isset($validated['is_public'])) {
|
||||
if ($validated['is_public'] && !$artwork->is_public) {
|
||||
$validated['published_at'] = $artwork->published_at ?? now();
|
||||
$visibility = (string) ($validated['visibility'] ?? ($artwork->visibility ?: ((bool) $artwork->is_public ? Artwork::VISIBILITY_PUBLIC : Artwork::VISIBILITY_PRIVATE)));
|
||||
$mode = (string) ($validated['mode'] ?? ($artwork->artwork_status === 'scheduled' ? 'schedule' : 'now'));
|
||||
$timezone = array_key_exists('timezone', $validated)
|
||||
? $validated['timezone']
|
||||
: $artwork->artwork_timezone;
|
||||
|
||||
$publishAt = null;
|
||||
if ($mode === 'schedule' && ! empty($validated['publish_at'])) {
|
||||
try {
|
||||
$publishAt = Carbon::parse($validated['publish_at'])->utc();
|
||||
} catch (\Throwable) {
|
||||
return response()->json(['errors' => ['publish_at' => ['Invalid publish date/time.']]], 422);
|
||||
}
|
||||
|
||||
if ($publishAt->lte(now()->addMinute())) {
|
||||
return response()->json(['errors' => ['publish_at' => ['Scheduled publish time must be at least 1 minute in the future.']]], 422);
|
||||
}
|
||||
} elseif ($mode === 'schedule') {
|
||||
return response()->json(['errors' => ['publish_at' => ['Choose a date and time for scheduled publishing.']]], 422);
|
||||
}
|
||||
|
||||
// Extract tags and category before updating core fields
|
||||
$tags = $validated['tags'] ?? null;
|
||||
$categoryId = $validated['category_id'] ?? null;
|
||||
unset($validated['tags'], $validated['category_id']);
|
||||
$contentTypeId = $validated['content_type_id'] ?? null;
|
||||
unset($validated['tags'], $validated['category_id'], $validated['content_type_id'], $validated['visibility'], $validated['mode'], $validated['publish_at'], $validated['timezone']);
|
||||
|
||||
$validated['visibility'] = $visibility;
|
||||
$validated['artwork_timezone'] = $timezone;
|
||||
|
||||
if ($mode === 'schedule' && $publishAt) {
|
||||
$validated['is_public'] = false;
|
||||
$validated['is_approved'] = true;
|
||||
$validated['publish_at'] = $publishAt;
|
||||
$validated['published_at'] = null;
|
||||
$validated['artwork_status'] = 'scheduled';
|
||||
} else {
|
||||
$validated['is_public'] = $visibility !== Artwork::VISIBILITY_PRIVATE;
|
||||
$validated['is_approved'] = true;
|
||||
$validated['publish_at'] = null;
|
||||
$validated['artwork_status'] = 'published';
|
||||
|
||||
if (($validated['is_public'] ?? false) && ! $artwork->published_at) {
|
||||
$validated['published_at'] = now();
|
||||
}
|
||||
|
||||
if ($visibility === Artwork::VISIBILITY_PRIVATE) {
|
||||
$validated['published_at'] = $artwork->published_at;
|
||||
}
|
||||
}
|
||||
|
||||
if ($categoryId === null && $contentTypeId !== null) {
|
||||
$categoryId = $this->resolveCategoryIdForContentType((int) $contentTypeId);
|
||||
}
|
||||
|
||||
$artwork->update($validated);
|
||||
|
||||
@@ -140,22 +200,26 @@ final class StudioArtworksApiController extends Controller
|
||||
$artwork->categories()->sync([(int) $categoryId]);
|
||||
}
|
||||
|
||||
// Sync tags (by slug/name)
|
||||
// Sync tags through the shared tag service so pivot source/usage rules stay valid.
|
||||
if ($tags !== null) {
|
||||
$tagIds = [];
|
||||
foreach ($tags as $tagSlug) {
|
||||
$tag = \App\Models\Tag::firstOrCreate(
|
||||
['slug' => \Illuminate\Support\Str::slug($tagSlug)],
|
||||
['name' => $tagSlug, 'is_active' => true, 'usage_count' => 0]
|
||||
try {
|
||||
$this->tagService->syncStudioTags(
|
||||
$artwork,
|
||||
$tags,
|
||||
(string) ($validated['tags_source'] ?? 'manual')
|
||||
);
|
||||
$tagIds[$tag->id] = ['source' => 'studio_edit', 'confidence' => 1.0];
|
||||
} catch (ValidationException $exception) {
|
||||
return response()->json(['errors' => $exception->errors()], 422);
|
||||
}
|
||||
$artwork->tags()->sync($tagIds);
|
||||
}
|
||||
|
||||
// Reindex in Meilisearch
|
||||
try {
|
||||
$artwork->searchable();
|
||||
if ((bool) $artwork->is_public && (bool) $artwork->is_approved && $artwork->published_at) {
|
||||
$artwork->searchable();
|
||||
} else {
|
||||
$artwork->unsearchable();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Meilisearch may be unavailable
|
||||
}
|
||||
@@ -171,14 +235,49 @@ final class StudioArtworksApiController extends Controller
|
||||
'title' => $artwork->title,
|
||||
'description' => $artwork->description,
|
||||
'is_public' => (bool) $artwork->is_public,
|
||||
'visibility' => $artwork->visibility ?: ((bool) $artwork->is_public ? Artwork::VISIBILITY_PUBLIC : Artwork::VISIBILITY_PRIVATE),
|
||||
'publish_mode' => $artwork->artwork_status === 'scheduled' ? 'schedule' : 'now',
|
||||
'publish_at' => $artwork->publish_at?->toIso8601String(),
|
||||
'artwork_status' => $artwork->artwork_status,
|
||||
'artwork_timezone' => $artwork->artwork_timezone,
|
||||
'slug' => $artwork->slug,
|
||||
'content_type_id' => $primaryCategory?->contentType?->id,
|
||||
'category_id' => $primaryCategory?->id,
|
||||
'tags' => $artwork->tags->map(fn ($t) => ['id' => $t->id, 'name' => $t->name, 'slug' => $t->slug])->values()->all(),
|
||||
'title_source' => $artwork->title_source ?: 'manual',
|
||||
'description_source' => $artwork->description_source ?: 'manual',
|
||||
'tags_source' => $artwork->tags_source ?: 'manual',
|
||||
'category_source' => $artwork->category_source ?: 'manual',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveCategoryIdForContentType(int $contentTypeId): ?int
|
||||
{
|
||||
$contentType = ContentType::query()->find($contentTypeId);
|
||||
if (! $contentType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$category = $contentType->rootCategories()
|
||||
->where('is_active', true)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->first();
|
||||
|
||||
if (! $category) {
|
||||
$category = Category::query()
|
||||
->where('content_type_id', $contentType->id)
|
||||
->where('is_active', true)
|
||||
->orderByRaw('CASE WHEN parent_id IS NULL THEN 0 ELSE 1 END')
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->first();
|
||||
}
|
||||
|
||||
return $category?->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/studio/artworks/{id}/toggle
|
||||
* Toggle publish/unpublish/archive for a single artwork.
|
||||
@@ -247,8 +346,11 @@ final class StudioArtworksApiController extends Controller
|
||||
'slug' => $artwork->slug,
|
||||
'thumb_url' => $artwork->thumbUrl('md') ?? '/images/placeholder.jpg',
|
||||
'is_public' => (bool) $artwork->is_public,
|
||||
'visibility' => $artwork->visibility ?: ((bool) $artwork->is_public ? Artwork::VISIBILITY_PUBLIC : Artwork::VISIBILITY_PRIVATE),
|
||||
'is_approved' => (bool) $artwork->is_approved,
|
||||
'published_at' => $artwork->published_at?->toIso8601String(),
|
||||
'publish_at' => $artwork->publish_at?->toIso8601String(),
|
||||
'artwork_status' => $artwork->artwork_status,
|
||||
'created_at' => $artwork->created_at?->toIso8601String(),
|
||||
'deleted_at' => $artwork->deleted_at?->toIso8601String(),
|
||||
'category' => $artwork->categories->first()?->name,
|
||||
|
||||
Reference in New Issue
Block a user