Commit workspace changes

This commit is contained in:
2026-04-05 19:42:33 +02:00
parent 148a3bbe43
commit 08ad757bcb
312 changed files with 35149 additions and 399 deletions

View File

@@ -15,6 +15,7 @@ use App\Events\Collections\CollectionUpdated;
use App\Events\Collections\SmartCollectionRulesUpdated;
use App\Models\Artwork;
use App\Models\Collection;
use App\Models\Group;
use App\Models\User;
use App\Support\AvatarUrl;
use App\Services\ThumbnailPresenter;
@@ -31,6 +32,7 @@ class CollectionService
public function __construct(
private readonly SmartCollectionService $smartCollections,
private readonly CollectionCollaborationService $collaborators,
private readonly GroupMembershipService $groupMembers,
) {
}
@@ -39,14 +41,14 @@ class CollectionService
return $this->normalizeLayoutModules(null, Collection::TYPE_PERSONAL, true, false);
}
public function makeUniqueSlugForUser(User $user, string $source, ?int $ignoreCollectionId = null): string
public function makeUniqueSlugForUser(User $user, string $source, ?int $ignoreCollectionId = null, ?Group $group = null): string
{
$base = Str::slug(Str::limit($source, 140, ''));
$base = $base !== '' ? $base : 'collection';
$slug = $base;
$suffix = 2;
while ($this->slugExistsForUser($user, $slug, $ignoreCollectionId)) {
while ($this->slugExistsForUser($user, $slug, $ignoreCollectionId, $group)) {
$slug = Str::limit($base, 132, '');
$slug = rtrim($slug, '-');
$slug .= '-' . $suffix;
@@ -70,9 +72,10 @@ class CollectionService
$collection = new Collection();
$collection->user()->associate($ownership['owner_user']);
$collection->group()->associate($ownership['group']);
$collection->managed_by_user_id = $ownership['managed_by_user_id'];
$collection->title = (string) $attributes['title'];
$collection->slug = $this->makeUniqueSlugForUser($ownership['owner_user'], (string) ($attributes['slug'] ?? $attributes['title']));
$collection->slug = $this->makeUniqueSlugForUser($ownership['owner_user'], (string) ($attributes['slug'] ?? $attributes['title']), null, $ownership['group']);
$collection->lifecycle_state = (string) ($attributes['lifecycle_state'] ?? Collection::LIFECYCLE_DRAFT);
$collection->type = $type;
$collection->editorial_owner_mode = $ownership['editorial_owner_mode'];
@@ -129,7 +132,7 @@ class CollectionService
$collection->collaborators_count = 1;
$collection->smart_rules_json = $smartRules;
$collection->layout_modules_json = $this->normalizeLayoutModules($attributes['layout_modules_json'] ?? null, $type, $allowComments, $allowSubmissions, false);
$collection->profile_order = $this->nextProfileOrder($ownership['owner_user']);
$collection->profile_order = $this->nextProfileOrder($ownership['owner_user'], $ownership['group']);
$collection->last_activity_at = now();
$collection->published_at = $this->resolvePublishedAt($attributes);
$collection->unpublished_at = $this->resolveUnpublishedAt($attributes);
@@ -179,10 +182,11 @@ class CollectionService
$allowComments = array_key_exists('allow_comments', $attributes) ? (bool) $attributes['allow_comments'] : $collection->allow_comments;
$collection->user()->associate($ownership['owner_user']);
$collection->group()->associate($ownership['group']);
$collection->fill([
'title' => (string) ($attributes['title'] ?? $collection->title),
'slug' => $this->makeUniqueSlugForUser($ownership['owner_user'], $slugSource, (int) $collection->id),
'slug' => $this->makeUniqueSlugForUser($ownership['owner_user'], $slugSource, (int) $collection->id, $ownership['group']),
'lifecycle_state' => (string) ($attributes['lifecycle_state'] ?? $collection->lifecycle_state),
'type' => $type,
'managed_by_user_id' => $ownership['managed_by_user_id'],
@@ -541,7 +545,11 @@ class CollectionService
->with(['contentType:id,slug,name']);
},
])
->whereIn('user_id', $this->contributorIds($collection))
->when(
(int) ($collection->group_id ?? 0) > 0,
fn ($builder) => $builder->where('group_id', (int) $collection->group_id),
fn ($builder) => $builder->whereIn('user_id', $this->contributorIds($collection))
)
->whereNull('deleted_at')
->whereNotIn('id', $attachedIds)
->orderByDesc('published_at')
@@ -560,17 +568,31 @@ class CollectionService
public function getCollectionOptionsForArtwork(User $owner, Artwork $artwork): array
{
if ((int) $artwork->user_id !== (int) $owner->id) {
$isPersonalArtwork = (int) ($artwork->group_id ?? 0) < 1;
$group = $artwork->group;
if ($isPersonalArtwork && (int) $artwork->user_id !== (int) $owner->id) {
throw ValidationException::withMessages([
'artwork_id' => 'You can only manage collections for your own artworks.',
]);
}
if (! $isPersonalArtwork && (! $group || ! $group->canManageCollections($owner))) {
throw ValidationException::withMessages([
'artwork_id' => 'You can only manage collections for groups you can edit.',
]);
}
$collections = Collection::query()
->ownedBy((int) $owner->id)
->with('group')
->when(
! $isPersonalArtwork,
fn ($query) => $query->where('group_id', (int) $artwork->group_id),
fn ($query) => $query->ownedBy((int) $owner->id)
)
->where('mode', Collection::MODE_MANUAL)
->orderByDesc('updated_at')
->get(['id', 'user_id', 'title', 'slug', 'visibility', 'mode', 'artworks_count', 'updated_at']);
->get(['id', 'user_id', 'group_id', 'title', 'slug', 'visibility', 'mode', 'artworks_count', 'updated_at']);
if ($collections->isEmpty()) {
return [];
@@ -586,6 +608,11 @@ class CollectionService
return $collections->map(function (Collection $collection) use ($attachedCollectionIds, $owner) {
$alreadyAttached = in_array((int) $collection->id, $attachedCollectionIds, true);
$publicUrl = route('profile.collections.show', [
'username' => strtolower((string) $owner->username),
'slug' => $collection->slug,
]);
return [
'id' => (int) $collection->id,
'title' => (string) $collection->title,
@@ -597,10 +624,7 @@ class CollectionService
'already_attached' => $alreadyAttached,
'attach_url' => route('settings.collections.artworks.attach', ['collection' => $collection->id]),
'manage_url' => route('settings.collections.show', ['collection' => $collection->id]),
'public_url' => route('profile.collections.show', [
'username' => strtolower((string) $owner->username),
'slug' => $collection->slug,
]),
'public_url' => $publicUrl,
];
})->all();
}
@@ -1319,13 +1343,19 @@ class CollectionService
private function resolveOwnershipContext(User $actor, array $attributes, ?Collection $collection, string $type): array
{
if ($type !== Collection::TYPE_EDITORIAL) {
$group = $this->resolveGroupContext($actor, $attributes, $collection);
$ownerUser = $collection && ! $collection->hasSystemEditorialOwner() && (int) $collection->user_id !== (int) $actor->id
? $collection->user
: $actor;
: ($group?->owner ?: $actor);
$managedByUserId = $group && (int) $ownerUser->id !== (int) $actor->id
? (int) $actor->id
: null;
return [
'owner_user' => $ownerUser,
'managed_by_user_id' => null,
'group' => $group,
'managed_by_user_id' => $managedByUserId,
'editorial_owner_mode' => Collection::EDITORIAL_OWNER_CREATOR,
'editorial_owner_user_id' => null,
'editorial_owner_label' => null,
@@ -1371,6 +1401,7 @@ class CollectionService
return [
'owner_user' => $ownerUser,
'group' => null,
'managed_by_user_id' => $managedByUserId,
'editorial_owner_mode' => $ownerMode,
'editorial_owner_user_id' => $editorialOwnerUserId,
@@ -1380,6 +1411,26 @@ class CollectionService
private function mapCollectionOwnerPayload(Collection $collection): array
{
if ((int) ($collection->group_id ?? 0) > 0) {
$group = $collection->relationLoaded('group') ? $collection->group : $collection->group()->with('owner.profile')->first();
return [
'name' => $group?->name ?: 'Skinbase Group',
'username' => null,
'profile_url' => $group ? $group->publicUrl() : null,
'is_system' => false,
'mode' => 'group',
'managed_by_user_id' => $collection->managed_by_user_id ? (int) $collection->managed_by_user_id : null,
'avatar_url' => $group?->avatarUrl(),
'group' => $group ? [
'id' => (int) $group->id,
'slug' => (string) $group->slug,
'name' => (string) $group->name,
'public_url' => $group->publicUrl(),
] : null,
];
}
$owner = $collection->relationLoaded('user') ? $collection->user : $collection->user()->first();
$username = $collection->displayOwnerUsername();
$avatarUrl = null;
@@ -1396,6 +1447,7 @@ class CollectionService
'mode' => $collection->editorial_owner_mode,
'managed_by_user_id' => $collection->managed_by_user_id ? (int) $collection->managed_by_user_id : null,
'avatar_url' => $avatarUrl,
'group' => null,
];
}
@@ -1406,10 +1458,13 @@ class CollectionService
return $presented['url'] ?? $artwork->thumbUrl('md');
}
private function slugExistsForUser(User $user, string $slug, ?int $ignoreCollectionId = null): bool
private function slugExistsForUser(User $user, string $slug, ?int $ignoreCollectionId = null, ?Group $group = null): bool
{
return Collection::query()
->where('user_id', $user->id)
->when($group !== null,
fn ($query) => $query->where('group_id', $group->id),
fn ($query) => $query->whereNull('group_id')->where('user_id', $user->id)
)
->where('slug', $slug)
->when($ignoreCollectionId !== null, fn ($query) => $query->where('id', '!=', $ignoreCollectionId))
->withTrashed()
@@ -1421,13 +1476,37 @@ class CollectionService
return max(1, (int) config('collections.featured_limit', 3));
}
private function nextProfileOrder(User $user): int
private function nextProfileOrder(User $user, ?Group $group = null): int
{
return (int) (Collection::query()
->ownedBy((int) $user->id)
->when($group !== null,
fn ($query) => $query->where('group_id', $group->id),
fn ($query) => $query->ownedBy((int) $user->id)
)
->max('profile_order') ?? -1) + 1;
}
private function resolveGroupContext(User $actor, array $attributes, ?Collection $collection = null): ?Group
{
$groupIdentifier = $attributes['group'] ?? $attributes['group_id'] ?? ($collection?->group_id ? (string) $collection->group_id : null);
if ($groupIdentifier === null || $groupIdentifier === '') {
return null;
}
$group = is_numeric($groupIdentifier)
? Group::query()->with('members')->findOrFail((int) $groupIdentifier)
: Group::query()->with('members')->where('slug', (string) $groupIdentifier)->firstOrFail();
if (! $group->canManageCollections($actor) && ! $actor->isAdmin()) {
throw ValidationException::withMessages([
'group' => 'You are not allowed to manage collections for this group.',
]);
}
return $group;
}
private function resolvePublishedAt(array $attributes, mixed $fallback = null): ?Carbon
{
if (! array_key_exists('published_at', $attributes)) {
@@ -1463,6 +1542,12 @@ class CollectionService
*/
private function contributorIds(Collection $collection): array
{
if ((int) ($collection->group_id ?? 0) > 0) {
$group = $collection->relationLoaded('group') ? $collection->group : $collection->group()->with('members')->first();
return $group ? $this->groupMembers->activeContributorIds($group) : [];
}
return $collection->isCollaborative()
? $this->collaborators->activeContributorIds($collection)
: [(int) $collection->user_id];