284 lines
13 KiB
PHP
284 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\NovaCards;
|
|
|
|
use App\Events\NovaCards\NovaCardAutosaved;
|
|
use App\Events\NovaCards\NovaCardCreated;
|
|
use App\Events\NovaCards\NovaCardTemplateSelected;
|
|
use App\Models\NovaCard;
|
|
use App\Models\NovaCardCategory;
|
|
use App\Models\NovaCardTemplate;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Str;
|
|
|
|
class NovaCardDraftService
|
|
{
|
|
public function __construct(
|
|
private readonly NovaCardTagService $tagService,
|
|
private readonly NovaCardProjectNormalizer $normalizer,
|
|
private readonly NovaCardVersionService $versions,
|
|
) {
|
|
}
|
|
|
|
public function createDraft(User $user, array $attributes = []): NovaCard
|
|
{
|
|
$template = $this->resolveTemplate(Arr::get($attributes, 'template_id'));
|
|
$category = $this->resolveCategory(Arr::get($attributes, 'category_id'));
|
|
$title = trim((string) Arr::get($attributes, 'title', 'Untitled card'));
|
|
$quote = trim((string) Arr::get($attributes, 'quote_text', 'Your next quote starts here.'));
|
|
$project = $this->normalizer->upgradeToV2(null, $template, $attributes);
|
|
$topLevel = $this->normalizer->syncTopLevelAttributes($project);
|
|
$originalCardId = Arr::get($attributes, 'original_card_id');
|
|
$rootCardId = Arr::get($attributes, 'root_card_id', $originalCardId);
|
|
|
|
$card = NovaCard::query()->create([
|
|
'user_id' => $user->id,
|
|
'category_id' => $category?->id,
|
|
'template_id' => $template?->id,
|
|
'title' => Str::limit($topLevel['title'] ?: $title, (int) config('nova_cards.validation.title_max', 120), ''),
|
|
'slug' => $this->generateUniqueSlug($title),
|
|
'quote_text' => Str::limit($topLevel['quote_text'] ?: $quote, (int) config('nova_cards.validation.quote_max', 420), ''),
|
|
'quote_author' => $topLevel['quote_author'],
|
|
'quote_source' => $topLevel['quote_source'],
|
|
'description' => Arr::get($attributes, 'description'),
|
|
'format' => $this->resolveFormat((string) Arr::get($attributes, 'format', NovaCard::FORMAT_SQUARE)),
|
|
'project_json' => $project,
|
|
'schema_version' => (int) $topLevel['schema_version'],
|
|
'visibility' => NovaCard::VISIBILITY_PRIVATE,
|
|
'status' => NovaCard::STATUS_DRAFT,
|
|
'moderation_status' => NovaCard::MOD_PENDING,
|
|
'background_type' => $topLevel['background_type'],
|
|
'background_image_id' => $topLevel['background_image_id'],
|
|
'allow_download' => true,
|
|
'allow_remix' => (bool) Arr::get($attributes, 'allow_remix', true),
|
|
'allow_background_reuse' => (bool) Arr::get($attributes, 'allow_background_reuse', false),
|
|
'allow_export' => (bool) Arr::get($attributes, 'allow_export', true),
|
|
'style_family' => Arr::get($attributes, 'style_family'),
|
|
'palette_family' => Arr::get($attributes, 'palette_family'),
|
|
'original_card_id' => $originalCardId,
|
|
'root_card_id' => $rootCardId,
|
|
]);
|
|
|
|
$this->tagService->syncTags($card, Arr::wrap(Arr::get($attributes, 'tags', [])));
|
|
$this->versions->snapshot($card->fresh(['template']), $user, $originalCardId ? 'Remix draft created' : 'Initial draft', true);
|
|
|
|
event(new NovaCardCreated($card->fresh()->load(['category', 'template', 'tags'])));
|
|
|
|
if ($card->template_id !== null) {
|
|
event(new NovaCardTemplateSelected($card->fresh()->load(['category', 'template', 'tags']), null, (int) $card->template_id));
|
|
}
|
|
|
|
return $card->load(['category', 'template', 'tags']);
|
|
}
|
|
|
|
public function autosave(NovaCard $card, array $payload): NovaCard
|
|
{
|
|
$currentProject = $this->normalizer->upgradeToV2(is_array($card->project_json) ? $card->project_json : [], $card->template, [], $card);
|
|
$template = $this->resolveTemplateId($payload, $card)
|
|
? NovaCardTemplate::query()->find($this->resolveTemplateId($payload, $card))
|
|
: $card->template;
|
|
$projectPatch = is_array(Arr::get($payload, 'project_json')) ? Arr::get($payload, 'project_json') : [];
|
|
$normalizedProject = $this->normalizer->upgradeToV2(
|
|
array_replace_recursive($currentProject, $projectPatch),
|
|
$template,
|
|
array_merge($payload, [
|
|
'title' => Arr::get($payload, 'title', $card->title),
|
|
'quote_text' => Arr::get($payload, 'quote_text', $card->quote_text),
|
|
'quote_author' => Arr::get($payload, 'quote_author', $card->quote_author),
|
|
'quote_source' => Arr::get($payload, 'quote_source', $card->quote_source),
|
|
'background_type' => Arr::get($payload, 'background_type', $card->background_type),
|
|
'background_image_id' => Arr::get($payload, 'background_image_id', $card->background_image_id),
|
|
'original_card_id' => $card->original_card_id,
|
|
'root_card_id' => $card->root_card_id,
|
|
]),
|
|
$card,
|
|
);
|
|
$topLevel = $this->normalizer->syncTopLevelAttributes($normalizedProject);
|
|
|
|
if (array_key_exists('title', $payload)) {
|
|
$topLevel['title'] = trim((string) $payload['title']);
|
|
}
|
|
|
|
if (array_key_exists('quote_text', $payload)) {
|
|
$topLevel['quote_text'] = trim((string) $payload['quote_text']);
|
|
}
|
|
|
|
if (array_key_exists('quote_author', $payload)) {
|
|
$topLevel['quote_author'] = (($value = trim((string) $payload['quote_author'])) !== '') ? $value : null;
|
|
}
|
|
|
|
if (array_key_exists('quote_source', $payload)) {
|
|
$topLevel['quote_source'] = (($value = trim((string) $payload['quote_source'])) !== '') ? $value : null;
|
|
}
|
|
|
|
$previousTemplateId = $card->template_id ? (int) $card->template_id : null;
|
|
|
|
$card->fill([
|
|
'title' => Str::limit($topLevel['title'], (int) config('nova_cards.validation.title_max', 120), ''),
|
|
'quote_text' => Str::limit($topLevel['quote_text'], (int) config('nova_cards.validation.quote_max', 420), ''),
|
|
'quote_author' => $topLevel['quote_author'],
|
|
'quote_source' => $topLevel['quote_source'],
|
|
'description' => Arr::get($payload, 'description', $card->description),
|
|
'format' => $this->resolveFormat((string) Arr::get($payload, 'format', $card->format)),
|
|
'project_json' => $normalizedProject,
|
|
'schema_version' => (int) $topLevel['schema_version'],
|
|
'background_type' => $topLevel['background_type'],
|
|
'background_image_id' => $topLevel['background_image_id'],
|
|
'template_id' => $this->resolveTemplateId($payload, $card),
|
|
'category_id' => $this->resolveCategoryId($payload, $card),
|
|
'visibility' => Arr::get($payload, 'visibility', $card->visibility),
|
|
'allow_download' => (bool) Arr::get($payload, 'allow_download', $card->allow_download),
|
|
'allow_remix' => (bool) Arr::get($payload, 'allow_remix', $card->allow_remix),
|
|
'allow_background_reuse' => (bool) Arr::get($payload, 'allow_background_reuse', $card->allow_background_reuse ?? false),
|
|
'allow_export' => (bool) Arr::get($payload, 'allow_export', $card->allow_export ?? true),
|
|
'style_family' => Arr::get($payload, 'style_family', $card->style_family),
|
|
'palette_family' => Arr::get($payload, 'palette_family', $card->palette_family),
|
|
'editor_mode_last_used' => Arr::get($payload, 'editor_mode_last_used', $card->editor_mode_last_used),
|
|
]);
|
|
|
|
if ($card->isDirty('title')) {
|
|
$card->slug = $this->generateUniqueSlug($card->title, $card->id);
|
|
}
|
|
|
|
$card->save();
|
|
$changes = $card->getChanges();
|
|
|
|
if (array_key_exists('tags', $payload)) {
|
|
$this->tagService->syncTags($card, Arr::wrap(Arr::get($payload, 'tags', [])));
|
|
$changes['tags'] = true;
|
|
}
|
|
|
|
if ($changes !== []) {
|
|
$fresh = $card->fresh()->load(['category', 'template', 'tags']);
|
|
$this->versions->snapshot($fresh->loadMissing('template'), $fresh->user, Arr::get($payload, 'version_label'));
|
|
event(new NovaCardAutosaved($fresh, array_keys($changes)));
|
|
|
|
$currentTemplateId = $fresh->template_id ? (int) $fresh->template_id : null;
|
|
if ($currentTemplateId !== $previousTemplateId && $currentTemplateId !== null) {
|
|
event(new NovaCardTemplateSelected($fresh, $previousTemplateId, $currentTemplateId));
|
|
}
|
|
}
|
|
|
|
return $card->refresh()->load(['category', 'template', 'tags']);
|
|
}
|
|
|
|
public function createRemix(User $user, NovaCard $source, array $attributes = []): NovaCard
|
|
{
|
|
$baseProject = $this->normalizer->normalizeForCard($source->loadMissing('template'));
|
|
$payload = array_merge([
|
|
'title' => 'Remix of ' . $source->title,
|
|
'quote_text' => $source->quote_text,
|
|
'quote_author' => $source->quote_author,
|
|
'quote_source' => $source->quote_source,
|
|
'description' => $source->description,
|
|
'format' => $source->format,
|
|
'background_type' => $source->background_type,
|
|
'background_image_id' => $source->background_image_id,
|
|
'template_id' => $source->template_id,
|
|
'category_id' => $source->category_id,
|
|
'tags' => $source->tags->pluck('name')->all(),
|
|
'project_json' => $baseProject,
|
|
'original_card_id' => $source->id,
|
|
'root_card_id' => $source->root_card_id ?: $source->id,
|
|
], $attributes);
|
|
|
|
return $this->createDraft($user, $payload);
|
|
}
|
|
|
|
public function createDuplicate(User $user, NovaCard $source, array $attributes = []): NovaCard
|
|
{
|
|
abort_unless($source->isOwnedBy($user), 403);
|
|
|
|
$baseProject = $this->normalizer->normalizeForCard($source->loadMissing('template'));
|
|
$payload = array_merge([
|
|
'title' => 'Copy of ' . $source->title,
|
|
'quote_text' => $source->quote_text,
|
|
'quote_author' => $source->quote_author,
|
|
'quote_source' => $source->quote_source,
|
|
'description' => $source->description,
|
|
'format' => $source->format,
|
|
'background_type' => $source->background_type,
|
|
'background_image_id' => $source->background_image_id,
|
|
'template_id' => $source->template_id,
|
|
'category_id' => $source->category_id,
|
|
'tags' => $source->tags->pluck('name')->all(),
|
|
'project_json' => $baseProject,
|
|
'allow_remix' => $source->allow_remix,
|
|
], $attributes);
|
|
|
|
return $this->createDraft($user, $payload);
|
|
}
|
|
|
|
private function buildProjectPayload(?NovaCardTemplate $template, array $attributes): array
|
|
{
|
|
return $this->normalizer->normalize(null, $template, $attributes);
|
|
}
|
|
|
|
private function generateUniqueSlug(string $title, ?int $ignoreId = null): string
|
|
{
|
|
$base = Str::slug($title);
|
|
if ($base === '') {
|
|
$base = 'nova-card';
|
|
}
|
|
|
|
$slug = $base;
|
|
$suffix = 2;
|
|
|
|
while (NovaCard::query()
|
|
->when($ignoreId !== null, fn ($query) => $query->where('id', '!=', $ignoreId))
|
|
->where('slug', $slug)
|
|
->exists()) {
|
|
$slug = $base . '-' . $suffix;
|
|
$suffix++;
|
|
}
|
|
|
|
return $slug;
|
|
}
|
|
|
|
private function resolveTemplate(mixed $templateId): ?NovaCardTemplate
|
|
{
|
|
if ($templateId) {
|
|
return NovaCardTemplate::query()->find($templateId);
|
|
}
|
|
|
|
return NovaCardTemplate::query()->where('active', true)->orderBy('order_num')->first();
|
|
}
|
|
|
|
private function resolveCategory(mixed $categoryId): ?NovaCardCategory
|
|
{
|
|
if ($categoryId) {
|
|
return NovaCardCategory::query()->find($categoryId);
|
|
}
|
|
|
|
return NovaCardCategory::query()->where('active', true)->orderBy('order_num')->first();
|
|
}
|
|
|
|
private function resolveFormat(string $format): string
|
|
{
|
|
$formats = array_keys((array) config('nova_cards.formats', []));
|
|
|
|
return in_array($format, $formats, true) ? $format : NovaCard::FORMAT_SQUARE;
|
|
}
|
|
|
|
private function resolveTemplateId(array $payload, NovaCard $card): ?int
|
|
{
|
|
if (! array_key_exists('template_id', $payload)) {
|
|
return $card->template_id;
|
|
}
|
|
|
|
return $this->resolveTemplate(Arr::get($payload, 'template_id'))?->id;
|
|
}
|
|
|
|
private function resolveCategoryId(array $payload, NovaCard $card): ?int
|
|
{
|
|
if (! array_key_exists('category_id', $payload)) {
|
|
return $card->category_id;
|
|
}
|
|
|
|
return $this->resolveCategory(Arr::get($payload, 'category_id'))?->id;
|
|
}
|
|
}
|