mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
chore!: migrate album owner to album_user (#27467)
Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com> Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { getContextMenuPositionFromEvent, type ContextMenuPosition } from '$lib/utils/context-menu';
|
||||
import { getShortDateRange } from '$lib/utils/date-time';
|
||||
import type { AlbumResponseDto } from '@immich/sdk';
|
||||
import { type AlbumResponseDto } from '@immich/sdk';
|
||||
import { IconButton } from '@immich/ui';
|
||||
import { mdiDotsVertical } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
@@ -85,12 +85,13 @@
|
||||
{/if}
|
||||
|
||||
{#if showOwner}
|
||||
{#if authManager.user.id === album.ownerId}
|
||||
{@const owner = album.albumUsers[0].user}
|
||||
{#if owner.id === authManager.user.id}
|
||||
<p>{$t('owned')}</p>
|
||||
{:else if album.owner}
|
||||
<p>{$t('shared_by_user', { values: { user: album.owner.name } })}</p>
|
||||
{:else}
|
||||
<p>{$t('shared')}</p>
|
||||
<p>
|
||||
{$t('shared_by_user', { values: { user: owner.name } })}
|
||||
</p>
|
||||
{/if}
|
||||
{:else if album.shared}
|
||||
<p>{$t('shared')}</p>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { getSelectedAlbumGroupOption, sortAlbums, stringToSortOrder, type AlbumGroup } from '$lib/utils/album-utils';
|
||||
import type { ContextMenuPosition } from '$lib/utils/context-menu';
|
||||
import { normalizeSearchString } from '$lib/utils/string-utils';
|
||||
import { type AlbumResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { AlbumUserRole, type AlbumResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { modalManager } from '@immich/ui';
|
||||
import { mdiDeleteOutline, mdiDownload, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js';
|
||||
import { groupBy } from 'lodash-es';
|
||||
@@ -98,24 +98,26 @@
|
||||
/** Group by owner */
|
||||
[AlbumGroupBy.Owner]: (order, albums): AlbumGroup[] => {
|
||||
const currentUserId = authManager.user.id;
|
||||
const groupedByOwnerIds = groupBy(albums, 'ownerId');
|
||||
const groupedByOwnerIds = groupBy(albums, (album) => album.albumUsers[0].user.id);
|
||||
|
||||
const sortSign = order === SortOrder.Desc ? -1 : 1;
|
||||
const sortedByOwnerNames = Object.entries(groupedByOwnerIds).sort(([ownerA, albumsA], [ownerB, albumsB]) => {
|
||||
const sortedByOwnerNames = Object.entries(groupedByOwnerIds).sort(([ownerIdA, albumsA], [ownerIdB, albumsB]) => {
|
||||
// We make sure owned albums stay either at the beginning or the end
|
||||
// of the list
|
||||
if (ownerA === currentUserId) {
|
||||
if (ownerIdA === currentUserId) {
|
||||
return -sortSign;
|
||||
} else if (ownerB === currentUserId) {
|
||||
} else if (ownerIdB === currentUserId) {
|
||||
return sortSign;
|
||||
} else {
|
||||
return albumsA[0].owner.name.localeCompare(albumsB[0].owner.name, $locale) * sortSign;
|
||||
const ownerA = albumsA[0].albumUsers[0].user;
|
||||
const ownerB = albumsB[0].albumUsers[0].user;
|
||||
return ownerA.name.localeCompare(ownerB.name, $locale) * sortSign;
|
||||
}
|
||||
});
|
||||
|
||||
return sortedByOwnerNames.map(([ownerId, albums]) => ({
|
||||
id: ownerId,
|
||||
name: ownerId === currentUserId ? $t('my_albums') : albums[0].owner.name,
|
||||
name: ownerId === currentUserId ? $t('my_albums') : albums[0].albumUsers[0].user.name,
|
||||
albums,
|
||||
}));
|
||||
},
|
||||
@@ -130,7 +132,10 @@
|
||||
return sharedAlbums;
|
||||
}
|
||||
default: {
|
||||
const nonOwnedAlbums = sharedAlbums.filter((album) => album.ownerId !== authManager.user.id);
|
||||
const nonOwnedAlbums = sharedAlbums.filter(
|
||||
(album) =>
|
||||
album.albumUsers.find(({ user: { id } }) => id === authManager.user.id)?.role !== AlbumUserRole.Owner,
|
||||
);
|
||||
return nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums;
|
||||
}
|
||||
}
|
||||
@@ -167,7 +172,9 @@
|
||||
albumGroupIds = groupedAlbums.map(({ id }) => id);
|
||||
});
|
||||
|
||||
let showFullContextMenu = $derived(allowEdit && selectedAlbum && selectedAlbum.ownerId === authManager.user.id);
|
||||
let showFullContextMenu = $derived(
|
||||
allowEdit && selectedAlbum && selectedAlbum.albumUsers[0].user.id === authManager.user.id,
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
if (allowEdit) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Route } from '$lib/route';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { ContextMenuPosition } from '$lib/utils/context-menu';
|
||||
import type { AlbumResponseDto } from '@immich/sdk';
|
||||
import { AlbumUserRole, type AlbumResponseDto } from '@immich/sdk';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiShareVariantOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
@@ -43,9 +43,11 @@
|
||||
icon={mdiShareVariantOutline}
|
||||
size="16"
|
||||
class="inline ms-1 opacity-70"
|
||||
title={album.ownerId === authManager.user.id
|
||||
title={album.albumUsers.find(({ user: { id } }) => id === authManager.user.id)?.role === AlbumUserRole.Owner
|
||||
? $t('shared_by_you')
|
||||
: $t('shared_by_user', { values: { user: album.owner.name } })}
|
||||
: $t('shared_by_user', {
|
||||
values: { user: album.albumUsers[0].user.name },
|
||||
})}
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import { getAssetType } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { isTenMinutesApart } from '$lib/utils/timesince';
|
||||
import { ReactionType, type ActivityResponseDto, type AssetTypeEnum } from '@immich/sdk';
|
||||
import { ReactionType, type ActivityResponseDto, type AlbumUserResponseDto, type AssetTypeEnum } from '@immich/sdk';
|
||||
import { Icon, IconButton, LoadingSpinner, Textarea, toastManager } from '@immich/ui';
|
||||
import { mdiClose, mdiDeleteOutline, mdiDotsVertical, mdiSend, mdiThumbUp } from '@mdi/js';
|
||||
import * as luxon from 'luxon';
|
||||
@@ -43,11 +43,11 @@
|
||||
assetId?: string | undefined;
|
||||
albumId: string;
|
||||
assetType?: AssetTypeEnum | undefined;
|
||||
albumOwnerId: string;
|
||||
albumUsers: AlbumUserResponseDto[];
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
let { assetId = undefined, albumId, assetType = undefined, albumOwnerId, disabled }: Props = $props();
|
||||
let { assetId = undefined, albumId, assetType = undefined, albumUsers, disabled }: Props = $props();
|
||||
|
||||
let innerHeight: number = $state(0);
|
||||
let activityHeight: number = $state(0);
|
||||
@@ -56,6 +56,7 @@
|
||||
let previousAssetId: string | undefined = $state(assetId);
|
||||
let message = $state('');
|
||||
let isSendingMessage = $state(false);
|
||||
const isAlbumOwner = $derived(albumUsers[0].user.id === authManager.user.id);
|
||||
|
||||
const timeOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
@@ -147,7 +148,7 @@
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
{#if reaction.user.id === authManager.user.id || albumOwnerId === authManager.user.id}
|
||||
{#if reaction.user.id === authManager.user.id || isAlbumOwner}
|
||||
<div class="me-4">
|
||||
<ButtonContextMenu
|
||||
icon={mdiDotsVertical}
|
||||
@@ -200,7 +201,7 @@
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
{#if reaction.user.id === authManager.user.id || albumOwnerId === authManager.user.id}
|
||||
{#if reaction.user.id === authManager.user.id || isAlbumOwner}
|
||||
<div class="me-4">
|
||||
<ButtonContextMenu
|
||||
icon={mdiDotsVertical}
|
||||
|
||||
@@ -638,7 +638,7 @@
|
||||
<ActivityViewer
|
||||
disabled={!album.isActivityEnabled}
|
||||
assetType={asset.type}
|
||||
albumOwnerId={album.ownerId}
|
||||
albumUsers={album.albumUsers}
|
||||
albumId={album.id}
|
||||
assetId={asset.id}
|
||||
/>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
}: Props = $props();
|
||||
|
||||
const isOwner = $derived(authManager.authenticated && asset.ownerId === authManager.user.id);
|
||||
const isAlbumOwner = $derived(authManager.authenticated && album?.ownerId === authManager.user.id);
|
||||
const isAlbumOwner = $derived(authManager.authenticated && album?.albumUsers[0].user.id === authManager.user.id);
|
||||
const isLocked = $derived(asset.visibility === AssetVisibility.Locked);
|
||||
const smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
const { album, onClose }: Props = $props();
|
||||
|
||||
let users: UserResponseDto[] = $state([]);
|
||||
const excludedUserIds = $derived([album.ownerId, ...album.albumUsers.map(({ user: { id } }) => id)]);
|
||||
const excludedUserIds = $derived(album.albumUsers.map(({ user: { id } }) => id));
|
||||
const filteredUsers = $derived(
|
||||
sortBy(
|
||||
users.filter(
|
||||
|
||||
@@ -105,16 +105,6 @@
|
||||
<HeaderActionButton action={AddUsers} />
|
||||
</HStack>
|
||||
<div class="ps-2">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div>
|
||||
<UserAvatar user={album.owner} size="md" />
|
||||
</div>
|
||||
<Text class="w-full" size="small">{album.owner.name}</Text>
|
||||
<Field disabled class="w-32 shrink-0">
|
||||
<Select options={[{ label: $t('owner'), value: 'owner' }]} value="owner" />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{#each album.albumUsers as { user, role } (user.id)}
|
||||
<div class="flex items-center justify-between gap-4 py-2">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
@@ -123,12 +113,13 @@
|
||||
</div>
|
||||
<Text size="small">{user.name}</Text>
|
||||
</div>
|
||||
<Field class="w-32">
|
||||
<Field class="w-32" disabled={role === AlbumUserRole.Owner}>
|
||||
<Select
|
||||
value={role}
|
||||
options={[
|
||||
{ label: $t('role_editor'), value: AlbumUserRole.Editor },
|
||||
{ label: $t('role_viewer'), value: AlbumUserRole.Viewer },
|
||||
{ label: $t('owner'), value: AlbumUserRole.Owner },
|
||||
{ label: $t('remove_user'), value: 'none' },
|
||||
] as SelectOption<AlbumUserRole | 'none'>[]}
|
||||
onChange={(value) => handleRoleSelect(user, value)}
|
||||
|
||||
@@ -42,7 +42,7 @@ export const getAlbumsActions = ($t: MessageFormatter) => {
|
||||
};
|
||||
|
||||
export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto) => {
|
||||
const isOwned = authManager.user.id === album.ownerId;
|
||||
const isOwned = album.albumUsers[0].user.id === authManager.user.id;
|
||||
|
||||
const Share: ActionItem = {
|
||||
title: $t('share'),
|
||||
|
||||
+10
-18
@@ -210,9 +210,7 @@
|
||||
let albumId = $derived(album.id);
|
||||
|
||||
const containsEditors = $derived(album?.shared && album.albumUsers.some(({ role }) => role === AlbumUserRole.Editor));
|
||||
const albumUsers = $derived(
|
||||
showAlbumUsers && containsEditors ? [album.owner, ...album.albumUsers.map(({ user }) => user)] : [],
|
||||
);
|
||||
const albumUsers = $derived(showAlbumUsers && containsEditors ? album.albumUsers.map(({ user }) => user) : []);
|
||||
|
||||
$effect(() => {
|
||||
if (!album.isActivityEnabled && activityManager.commentCount === 0) {
|
||||
@@ -231,7 +229,7 @@
|
||||
return { albumId, order: album.order };
|
||||
});
|
||||
|
||||
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 0);
|
||||
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 1);
|
||||
|
||||
$effect(() => {
|
||||
if (assetViewerManager.isViewing || !isShared) {
|
||||
@@ -243,16 +241,15 @@
|
||||
|
||||
onDestroy(() => activityManager.reset());
|
||||
|
||||
let isOwned = $derived(authManager.user.id == album.ownerId);
|
||||
const isOwned = $derived(album.albumUsers[0].user.id === authManager.user.id);
|
||||
|
||||
let showActivityStatus = $derived(
|
||||
album.albumUsers.length > 0 &&
|
||||
album.albumUsers.length > 1 &&
|
||||
!assetViewerManager.isViewing &&
|
||||
(album.isActivityEnabled || activityManager.commentCount > 0),
|
||||
);
|
||||
let isEditor = $derived(
|
||||
album.albumUsers.find(({ user: { id } }) => id === authManager.user.id)?.role === AlbumUserRole.Editor ||
|
||||
album.ownerId === authManager.user.id,
|
||||
const isEditor = $derived(
|
||||
album.albumUsers.find(({ user: { id } }) => id === authManager.user.id)?.role === AlbumUserRole.Editor || isOwned,
|
||||
);
|
||||
|
||||
let albumHasViewers = $derived(album.albumUsers.some(({ role }) => role === AlbumUserRole.Viewer));
|
||||
@@ -374,7 +371,7 @@
|
||||
{/if}
|
||||
|
||||
<!-- ALBUM SHARING -->
|
||||
{#if album.albumUsers.length > 0 || (album.hasSharedLink && isOwned)}
|
||||
{#if album.albumUsers.length > 1 || (album.hasSharedLink && isOwned)}
|
||||
<div class="my-3 flex gap-x-1">
|
||||
<!-- link -->
|
||||
{#if album.hasSharedLink && isOwned}
|
||||
@@ -388,13 +385,8 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- owner -->
|
||||
<button type="button" onclick={() => modalManager.show(AlbumOptionsModal, { album })}>
|
||||
<UserAvatar user={album.owner} size="md" />
|
||||
</button>
|
||||
|
||||
<!-- users with write access (collaborators) -->
|
||||
{#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)}
|
||||
{#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor || role === AlbumUserRole.Owner) as { user } (user.id)}
|
||||
<button type="button" onclick={() => modalManager.show(AlbumOptionsModal, { album })}>
|
||||
<UserAvatar {user} size="md" />
|
||||
</button>
|
||||
@@ -620,7 +612,7 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if album.albumUsers.length > 0 && album && assetViewerManager.isShowActivityPanel && authManager.authenticated && !assetViewerManager.isViewing}
|
||||
{#if album.albumUsers.length > 1 && album && assetViewerManager.isShowActivityPanel && authManager.authenticated && !assetViewerManager.isViewing}
|
||||
<div class="flex">
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
@@ -628,7 +620,7 @@
|
||||
class="z-2 w-90 md:w-115 overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray"
|
||||
translate="yes"
|
||||
>
|
||||
<ActivityViewer disabled={!album.isActivityEnabled} albumOwnerId={album.ownerId} albumId={album.id} />
|
||||
<ActivityViewer disabled={!album.isActivityEnabled} albumUsers={album.albumUsers} albumId={album.id} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { AssetOrder, type AlbumResponseDto } from '@immich/sdk';
|
||||
import { Sync } from 'factory.ts';
|
||||
import { userFactory } from './user-factory';
|
||||
|
||||
export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
|
||||
albumName: Sync.each(() => faker.commerce.product()),
|
||||
@@ -11,8 +10,6 @@ export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
|
||||
createdAt: Sync.each(() => faker.date.past().toISOString()),
|
||||
updatedAt: Sync.each(() => faker.date.past().toISOString()),
|
||||
id: Sync.each(() => faker.string.uuid()),
|
||||
ownerId: Sync.each(() => faker.string.uuid()),
|
||||
owner: userFactory.build(),
|
||||
shared: false,
|
||||
albumUsers: [],
|
||||
hasSharedLink: false,
|
||||
|
||||
Reference in New Issue
Block a user