Wire admin studio SSR and search infrastructure
This commit is contained in:
@@ -28,6 +28,19 @@ class Artwork extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, Searchable;
|
||||
|
||||
/**
|
||||
* Override Scout's bootSearchable to skip the ModelObserver (which fires MakeSearchable
|
||||
* on every save). We still register SearchableScope and Builder macros so that
|
||||
* scout:import and Builder::searchable() continue to work.
|
||||
* All indexing is managed explicitly via IndexArtworkJob.
|
||||
*/
|
||||
public static function bootSearchable(): void
|
||||
{
|
||||
static::addGlobalScope(new \Laravel\Scout\SearchableScope);
|
||||
(new static)->registerSearchableMacros();
|
||||
// ModelObserver intentionally omitted — indexing is handled by IndexArtworkJob.
|
||||
}
|
||||
|
||||
public const PUBLISHED_AS_USER = 'user';
|
||||
public const PUBLISHED_AS_GROUP = 'group';
|
||||
|
||||
@@ -254,6 +267,11 @@ class Artwork extends Model
|
||||
return $this->hasMany(WorldSubmission::class)->orderByDesc('reviewed_at')->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
public function worldRewardGrants(): HasMany
|
||||
{
|
||||
return $this->hasMany(WorldRewardGrant::class)->orderByDesc('granted_at')->orderByDesc('id');
|
||||
}
|
||||
|
||||
public function isPublishedByGroup(): bool
|
||||
{
|
||||
return $this->publishedAsType() === self::PUBLISHED_AS_GROUP;
|
||||
@@ -409,6 +427,14 @@ class Artwork extends Model
|
||||
$stat = $this->stats;
|
||||
$awardStat = $this->awardStat;
|
||||
$publishedSortAt = $this->published_at ?? $this->created_at;
|
||||
$sortedCategories = $this->categories->sortBy(
|
||||
fn ($category) => sprintf(
|
||||
'%010d|%s|%010d',
|
||||
(int) ($category->sort_order ?? 999999999),
|
||||
strtolower((string) ($category->name ?? '')),
|
||||
(int) ($category->id ?? 0)
|
||||
)
|
||||
)->values();
|
||||
|
||||
// Orientation derived from pixel dimensions
|
||||
$orientation = 'square';
|
||||
@@ -425,8 +451,22 @@ class Artwork extends Model
|
||||
? $this->width . 'x' . $this->height
|
||||
: '';
|
||||
|
||||
// Primary category slug (first attached category)
|
||||
$primaryCategory = $this->categories->first();
|
||||
// Primary category slug follows the same sort_order-first semantics used by page presenters.
|
||||
$primaryCategory = $sortedCategories->first();
|
||||
$categorySlugs = $sortedCategories
|
||||
->pluck('slug')
|
||||
->filter()
|
||||
->map(static fn ($slug) => (string) $slug)
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
$contentTypeSlugs = $sortedCategories
|
||||
->map(static fn ($category) => $category->contentType?->slug)
|
||||
->filter()
|
||||
->map(static fn ($slug) => (string) $slug)
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
$category = $primaryCategory?->slug ?? '';
|
||||
$content_type = $primaryCategory?->contentType?->slug ?? '';
|
||||
|
||||
@@ -442,7 +482,9 @@ class Artwork extends Model
|
||||
'author_name' => $this->group?->name ?? $this->user?->name ?? 'Skinbase',
|
||||
'published_as_type' => $this->publishedAsType(),
|
||||
'category' => $category,
|
||||
'categories' => $categorySlugs,
|
||||
'content_type' => $content_type,
|
||||
'content_types' => $contentTypeSlugs,
|
||||
'tags' => $tags,
|
||||
'ai_clip_tags' => collect((array) ($this->clip_tags_json ?? []))
|
||||
->map(static fn ($row) => is_array($row) ? (string) ($row['tag'] ?? '') : '')
|
||||
|
||||
@@ -83,6 +83,6 @@ final class Country extends Model
|
||||
return null;
|
||||
}
|
||||
|
||||
return '/gfx/flags/shiny/24/'.rawurlencode($iso2).'.png';
|
||||
return rtrim((string) \config('cdn.files_url', ''), '/').'/images/flags/shiny/24/'.rawurlencode($iso2).'.png';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,13 @@ final class Tag extends Model
|
||||
'name',
|
||||
'slug',
|
||||
'usage_count',
|
||||
'artworks_count',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'usage_count' => 'integer',
|
||||
'artworks_count' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
|
||||
43
app/Models/UploadBatch.php
Normal file
43
app/Models/UploadBatch.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
final class UploadBatch extends Model
|
||||
{
|
||||
public const STATUS_UPLOADING = 'uploading';
|
||||
public const STATUS_PROCESSING = 'processing';
|
||||
public const STATUS_COMPLETED = 'completed';
|
||||
public const STATUS_COMPLETED_WITH_ERRORS = 'completed_with_errors';
|
||||
public const STATUS_CANCELLED = 'cancelled';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'name',
|
||||
'status',
|
||||
'total_items',
|
||||
'processed_items',
|
||||
'failed_items',
|
||||
'published_items',
|
||||
'defaults_json',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'defaults_json' => 'array',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(UploadBatchItem::class)->orderBy('id');
|
||||
}
|
||||
}
|
||||
66
app/Models/UploadBatchItem.php
Normal file
66
app/Models/UploadBatchItem.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
final class UploadBatchItem extends Model
|
||||
{
|
||||
public const STATUS_UPLOADED = 'uploaded';
|
||||
public const STATUS_PROCESSING = 'processing';
|
||||
public const STATUS_NEEDS_METADATA = 'needs_metadata';
|
||||
public const STATUS_NEEDS_REVIEW = 'needs_review';
|
||||
public const STATUS_READY = 'ready';
|
||||
public const STATUS_FAILED = 'failed';
|
||||
public const STATUS_PUBLISHED = 'published';
|
||||
public const STATUS_DELETED = 'deleted';
|
||||
|
||||
public const STAGE_QUEUED = 'queued';
|
||||
public const STAGE_STORED = 'stored';
|
||||
public const STAGE_THUMBNAILS = 'thumbnails';
|
||||
public const STAGE_VISION_ANALYSIS = 'vision_analysis';
|
||||
public const STAGE_MATURITY_CHECK = 'maturity_check';
|
||||
public const STAGE_METADATA_SUGGESTIONS = 'metadata_suggestions';
|
||||
public const STAGE_FINALIZED = 'finalized';
|
||||
|
||||
protected $fillable = [
|
||||
'upload_batch_id',
|
||||
'user_id',
|
||||
'artwork_id',
|
||||
'original_filename',
|
||||
'status',
|
||||
'processing_stage',
|
||||
'error_code',
|
||||
'error_message',
|
||||
'metadata_completeness',
|
||||
'is_ready_to_publish',
|
||||
'uploaded_at',
|
||||
'processed_at',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_ready_to_publish' => 'boolean',
|
||||
'uploaded_at' => 'datetime',
|
||||
'processed_at' => 'datetime',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function batch(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(UploadBatch::class, 'upload_batch_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function artwork(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Artwork::class);
|
||||
}
|
||||
}
|
||||
@@ -265,6 +265,11 @@ class User extends Authenticatable
|
||||
return $this->hasMany(UserActivity::class, 'user_id');
|
||||
}
|
||||
|
||||
public function worldRewardGrants(): HasMany
|
||||
{
|
||||
return $this->hasMany(WorldRewardGrant::class, 'user_id')->orderByDesc('granted_at')->orderByDesc('id');
|
||||
}
|
||||
|
||||
public function achievements(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Achievement::class, 'user_achievements', 'user_id', 'achievement_id')
|
||||
@@ -385,6 +390,25 @@ class User extends Authenticatable
|
||||
return $this->hasRole('admin') || $this->hasLegacyPrivilegeFlag('isAdmin');
|
||||
}
|
||||
|
||||
public function isManager(): bool
|
||||
{
|
||||
return $this->hasRole('manager');
|
||||
}
|
||||
|
||||
public function isEditorial(): bool
|
||||
{
|
||||
return $this->hasRole('editorial');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true for any role that grants access to the /admin panel
|
||||
* (admin, manager, editorial).
|
||||
*/
|
||||
public function hasStaffAccess(): bool
|
||||
{
|
||||
return $this->isAdmin() || $this->isManager() || $this->isEditorial();
|
||||
}
|
||||
|
||||
public function isModerator(): bool
|
||||
{
|
||||
return $this->hasRole('moderator') || $this->hasLegacyPrivilegeFlag('isModerator');
|
||||
|
||||
@@ -23,6 +23,7 @@ class UserActivity extends Model
|
||||
public const TYPE_FAVOURITE = 'favourite';
|
||||
public const TYPE_FOLLOW = 'follow';
|
||||
public const TYPE_ACHIEVEMENT = 'achievement';
|
||||
public const TYPE_WORLD_REWARD = 'world_reward';
|
||||
public const TYPE_FORUM_POST = 'forum_post';
|
||||
public const TYPE_FORUM_REPLY = 'forum_reply';
|
||||
|
||||
@@ -30,6 +31,7 @@ class UserActivity extends Model
|
||||
public const ENTITY_ARTWORK_COMMENT = 'artwork_comment';
|
||||
public const ENTITY_USER = 'user';
|
||||
public const ENTITY_ACHIEVEMENT = 'achievement';
|
||||
public const ENTITY_WORLD_REWARD = 'world_reward';
|
||||
public const ENTITY_FORUM_THREAD = 'forum_thread';
|
||||
public const ENTITY_FORUM_POST = 'forum_post';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user