-
-
{title}
- {#if description}
-
{description}
- {/if}
-
+
{@render buttons?.()}
{@render children?.()}
diff --git a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte
index b2c6cf296d..6de97e94f9 100644
--- a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte
+++ b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte
@@ -6,6 +6,7 @@
import { getSharedLinkActions } from '$lib/services/shared-link.service';
import { locale } from '$lib/stores/preferences.store';
import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
+ import { ContextMenuButton, MenuItemType } from '@immich/ui';
import { DateTime, type ToRelativeUnit } from 'luxon';
import { t } from 'svelte-i18n';
@@ -31,7 +32,7 @@
}
};
- const SharedLinkActions = $derived(getSharedLinkActions($t, sharedLink));
+ const { Edit, Copy, Delete } = $derived(getSharedLinkActions($t, sharedLink));
diff --git a/web/src/lib/services/library.service.ts b/web/src/lib/services/library.service.ts
index 415d6dae42..93cf836c82 100644
--- a/web/src/lib/services/library.service.ts
+++ b/web/src/lib/services/library.service.ts
@@ -7,7 +7,6 @@ import LibraryFolderAddModal from '$lib/modals/LibraryFolderAddModal.svelte';
import LibraryFolderEditModal from '$lib/modals/LibraryFolderEditModal.svelte';
import LibraryRenameModal from '$lib/modals/LibraryRenameModal.svelte';
import LibraryUserPickerModal from '$lib/modals/LibraryUserPickerModal.svelte';
-import type { ActionItem } from '$lib/types';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
import {
@@ -20,7 +19,7 @@ import {
updateLibrary,
type LibraryResponseDto,
} from '@immich/sdk';
-import { modalManager, toastManager } from '@immich/ui';
+import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import { mdiPencilOutline, mdiPlusBoxOutline, mdiSync, mdiTrashCanOutline } from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n';
@@ -28,13 +27,13 @@ export const getLibrariesActions = ($t: MessageFormatter) => {
const ScanAll: ActionItem = {
title: $t('scan_all_libraries'),
icon: mdiSync,
- onSelect: () => void handleScanAllLibraries(),
+ onAction: () => void handleScanAllLibraries(),
};
const Create: ActionItem = {
title: $t('create_library'),
icon: mdiPlusBoxOutline,
- onSelect: () => void handleCreateLibrary(),
+ onAction: () => void handleCreateLibrary(),
};
return { ScanAll, Create };
@@ -44,32 +43,32 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
const Rename: ActionItem = {
icon: mdiPencilOutline,
title: $t('rename'),
- onSelect: () => void modalManager.show(LibraryRenameModal, { library }),
+ onAction: () => void modalManager.show(LibraryRenameModal, { library }),
};
const Delete: ActionItem = {
icon: mdiTrashCanOutline,
title: $t('delete'),
color: 'danger',
- onSelect: () => void handleDeleteLibrary(library),
+ onAction: () => void handleDeleteLibrary(library),
};
const AddFolder: ActionItem = {
icon: mdiPlusBoxOutline,
title: $t('add'),
- onSelect: () => void modalManager.show(LibraryFolderAddModal, { library }),
+ onAction: () => void modalManager.show(LibraryFolderAddModal, { library }),
};
const AddExclusionPattern: ActionItem = {
icon: mdiPlusBoxOutline,
title: $t('add'),
- onSelect: () => void modalManager.show(LibraryExclusionPatternAddModal, { library }),
+ onAction: () => void modalManager.show(LibraryExclusionPatternAddModal, { library }),
};
const Scan: ActionItem = {
icon: mdiSync,
title: $t('scan_library'),
- onSelect: () => void handleScanLibrary(library),
+ onAction: () => void handleScanLibrary(library),
};
return { Rename, Delete, AddFolder, AddExclusionPattern, Scan };
@@ -79,13 +78,13 @@ export const getLibraryFolderActions = ($t: MessageFormatter, library: LibraryRe
const Edit: ActionItem = {
icon: mdiPencilOutline,
title: $t('edit'),
- onSelect: () => void modalManager.show(LibraryFolderEditModal, { folder, library }),
+ onAction: () => void modalManager.show(LibraryFolderEditModal, { folder, library }),
};
const Delete: ActionItem = {
icon: mdiTrashCanOutline,
title: $t('delete'),
- onSelect: () => void handleDeleteLibraryFolder(library, folder),
+ onAction: () => void handleDeleteLibraryFolder(library, folder),
};
return { Edit, Delete };
@@ -99,13 +98,13 @@ export const getLibraryExclusionPatternActions = (
const Edit: ActionItem = {
icon: mdiPencilOutline,
title: $t('edit'),
- onSelect: () => void modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }),
+ onAction: () => void modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }),
};
const Delete: ActionItem = {
icon: mdiTrashCanOutline,
title: $t('delete'),
- onSelect: () => void handleDeleteExclusionPattern(library, exclusionPattern),
+ onAction: () => void handleDeleteExclusionPattern(library, exclusionPattern),
};
return { Edit, Delete };
diff --git a/web/src/lib/services/shared-link.service.ts b/web/src/lib/services/shared-link.service.ts
index 3ce6f4222d..ea7f158db8 100644
--- a/web/src/lib/services/shared-link.service.ts
+++ b/web/src/lib/services/shared-link.service.ts
@@ -16,48 +16,37 @@ import {
type SharedLinkEditDto,
type SharedLinkResponseDto,
} from '@immich/sdk';
-import { MenuItemType, menuManager, modalManager, toastManager, type MenuItem } from '@immich/ui';
-import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiDotsVertical, mdiQrcode } from '@mdi/js';
+import { modalManager, toastManager, type ActionItem } from '@immich/ui';
+import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiQrcode } from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n';
export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLinkResponseDto) => {
- const Edit: MenuItem = {
+ const Edit: ActionItem = {
title: $t('edit_link'),
icon: mdiCircleEditOutline,
- onSelect: () => void goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`),
+ onAction: () => void goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`),
};
- const Delete: MenuItem = {
+ const Delete: ActionItem = {
title: $t('delete_link'),
icon: mdiDelete,
color: 'danger',
- onSelect: () => void handleDeleteSharedLink(sharedLink),
+ onAction: () => void handleDeleteSharedLink(sharedLink),
};
- const Copy: MenuItem = {
+ const Copy: ActionItem = {
title: $t('copy_link'),
icon: mdiContentCopy,
- onSelect: () => void copyToClipboard(asUrl(sharedLink)),
+ onAction: () => void copyToClipboard(asUrl(sharedLink)),
};
- const ViewQrCode: MenuItem = {
+ const ViewQrCode: ActionItem = {
title: $t('view_qr_code'),
icon: mdiQrcode,
- onSelect: () => void handleShowSharedLinkQrCode(sharedLink),
+ onAction: () => void handleShowSharedLinkQrCode(sharedLink),
};
- const ContextMenu: MenuItem = {
- title: $t('shared_link_options'),
- icon: mdiDotsVertical,
- onSelect: ({ event }) =>
- void menuManager.show({
- target: event.currentTarget as HTMLElement,
- position: 'top-right',
- items: [Edit, Copy, MenuItemType.Divider, Delete],
- }),
- };
-
- return { Edit, Delete, Copy, ViewQrCode, ContextMenu };
+ return { Edit, Delete, Copy, ViewQrCode };
};
const asUrl = (sharedLink: SharedLinkResponseDto) => {
diff --git a/web/src/lib/services/system-config.service.ts b/web/src/lib/services/system-config.service.ts
index b555c425ef..62034886b9 100644
--- a/web/src/lib/services/system-config.service.ts
+++ b/web/src/lib/services/system-config.service.ts
@@ -1,12 +1,11 @@
import { downloadManager } from '$lib/managers/download-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
-import type { ActionItem } from '$lib/types';
import { copyToClipboard } from '$lib/utils';
import { downloadBlob } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
import { getConfig, updateConfig, type ServerFeaturesDto, type SystemConfigDto } from '@immich/sdk';
-import { toastManager } from '@immich/ui';
+import { toastManager, type ActionItem } from '@immich/ui';
import { mdiContentCopy, mdiDownload, mdiUpload } from '@mdi/js';
import { isEqual } from 'lodash-es';
import type { MessageFormatter } from 'svelte-i18n';
@@ -19,20 +18,20 @@ export const getSystemConfigActions = (
const CopyToClipboard: ActionItem = {
title: $t('copy_to_clipboard'),
icon: mdiContentCopy,
- onSelect: () => void handleCopyToClipboard(config),
+ onAction: () => void handleCopyToClipboard(config),
};
const Download: ActionItem = {
title: $t('export_as_json'),
icon: mdiDownload,
- onSelect: () => handleDownloadConfig(config),
+ onAction: () => handleDownloadConfig(config),
};
const Upload: ActionItem = {
title: $t('import_from_json'),
icon: mdiUpload,
$if: () => !featureFlags.configFile,
- onSelect: () => handleUploadConfig(),
+ onAction: () => handleUploadConfig(),
};
return { CopyToClipboard, Download, Upload };
diff --git a/web/src/lib/services/user-admin.service.ts b/web/src/lib/services/user-admin.service.ts
index 93b8800b11..b8a4c648c1 100644
--- a/web/src/lib/services/user-admin.service.ts
+++ b/web/src/lib/services/user-admin.service.ts
@@ -1,13 +1,11 @@
import { goto } from '$app/navigation';
import { eventManager } from '$lib/managers/event-manager.svelte';
-import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte';
import UserCreateModal from '$lib/modals/UserCreateModal.svelte';
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
import UserEditModal from '$lib/modals/UserEditModal.svelte';
import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte';
import { user as authUser } from '$lib/stores/user.store';
-import type { ActionItem } from '$lib/types';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
import {
@@ -21,45 +19,33 @@ import {
type UserAdminResponseDto,
type UserAdminUpdateDto,
} from '@immich/sdk';
-import { MenuItemType, menuManager, modalManager, toastManager } from '@immich/ui';
+import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import {
mdiDeleteRestore,
- mdiDotsVertical,
- mdiEyeOutline,
mdiLockReset,
mdiLockSmart,
mdiPencilOutline,
mdiPlusBoxOutline,
mdiTrashCanOutline,
} from '@mdi/js';
-import { DateTime } from 'luxon';
import type { MessageFormatter } from 'svelte-i18n';
import { get } from 'svelte/store';
-const getDeleteDate = (deletedAt: string): Date =>
- DateTime.fromISO(deletedAt).plus({ days: serverConfigManager.value.userDeleteDelay }).toJSDate();
-
export const getUserAdminsActions = ($t: MessageFormatter) => {
const Create: ActionItem = {
title: $t('create_user'),
icon: mdiPlusBoxOutline,
- onSelect: () => void modalManager.show(UserCreateModal, {}),
+ onAction: () => void modalManager.show(UserCreateModal, {}),
};
return { Create };
};
export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminResponseDto) => {
- const View: ActionItem = {
- icon: mdiEyeOutline,
- title: $t('view'),
- onSelect: () => void goto(`/admin/users/${user.id}`),
- };
-
const Update: ActionItem = {
icon: mdiPencilOutline,
title: $t('edit'),
- onSelect: () => void modalManager.show(UserEditModal, { user }),
+ onAction: () => void modalManager.show(UserEditModal, { user }),
};
const Delete: ActionItem = {
@@ -67,7 +53,7 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
title: $t('delete'),
color: 'danger',
$if: () => get(authUser).id !== user.id && !user.deletedAt,
- onSelect: () => void modalManager.show(UserDeleteConfirmModal, { user }),
+ onAction: () => void modalManager.show(UserDeleteConfirmModal, { user }),
};
const Restore: ActionItem = {
@@ -75,47 +61,23 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
title: $t('restore'),
color: 'primary',
$if: () => !!user.deletedAt && user.status === UserStatus.Deleted,
- onSelect: () => void modalManager.show(UserRestoreConfirmModal, { user }),
- props: {
- title: $t('admin.user_restore_scheduled_removal', {
- values: { date: getDeleteDate(user.deletedAt!) },
- }),
- },
+ onAction: () => void modalManager.show(UserRestoreConfirmModal, { user }),
};
const ResetPassword: ActionItem = {
icon: mdiLockReset,
title: $t('reset_password'),
$if: () => get(authUser).id !== user.id,
- onSelect: () => void handleResetPasswordUserAdmin(user),
+ onAction: () => void handleResetPasswordUserAdmin(user),
};
const ResetPinCode: ActionItem = {
icon: mdiLockSmart,
title: $t('reset_pin_code'),
- onSelect: () => void handleResetPinCodeUserAdmin(user),
+ onAction: () => void handleResetPinCodeUserAdmin(user),
};
- const ContextMenu: ActionItem = {
- icon: mdiDotsVertical,
- title: $t('actions'),
- onSelect: ({ event }) =>
- void menuManager.show({
- target: event.currentTarget as HTMLElement,
- position: 'top-right',
- items: [
- View,
- Update,
- ResetPassword,
- ResetPinCode,
- get(authUser).id === user.id ? undefined : MenuItemType.Divider,
- Restore,
- Delete,
- ].filter(Boolean),
- }),
- };
-
- return { View, Update, Delete, Restore, ResetPassword, ResetPinCode, ContextMenu };
+ return { Update, Delete, Restore, ResetPassword, ResetPinCode };
};
export const handleCreateUserAdmin = async (dto: UserAdminCreateDto) => {
@@ -172,6 +134,10 @@ export const handleRestoreUserAdmin = async (user: UserAdminResponseDto) => {
}
};
+export const handleNavigateUserAdmin = async (user: UserAdminResponseDto) => {
+ await goto(`/admin/users/${user.id}`);
+};
+
// TODO move password reset server-side
const generatePassword = (length: number = 16) => {
let generatedPassword = '';
diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts
index 4e6e8a45f4..d95e7b7cf2 100644
--- a/web/src/lib/types.ts
+++ b/web/src/lib/types.ts
@@ -1,8 +1,4 @@
import type { ServerVersionResponseDto } from '@immich/sdk';
-import type { MenuItem } from '@immich/ui';
-import type { HTMLAttributes } from 'svelte/elements';
-
-export type ActionItem = MenuItem & { props?: Omit
, 'color'> };
export interface ReleaseEvent {
isAvailable: boolean;
diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte
index 808a5b57ca..6a3195f447 100644
--- a/web/src/routes/admin/jobs-status/+page.svelte
+++ b/web/src/routes/admin/jobs-status/+page.svelte
@@ -58,7 +58,7 @@
});
-
+
{#snippet buttons()}
{#if pausedJobs.length > 0}
diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte
index 37153d5003..6aa2b3007a 100644
--- a/web/src/routes/admin/library-management/+page.svelte
+++ b/web/src/routes/admin/library-management/+page.svelte
@@ -58,7 +58,7 @@
onLibraryDelete={handleDeleteLibrary}
/>
-
+
{#snippet buttons()}
{#if libraries.length > 0}
diff --git a/web/src/routes/admin/library-management/[id]/+page.svelte b/web/src/routes/admin/library-management/[id]/+page.svelte
index c6fffbbd95..32367e78a8 100644
--- a/web/src/routes/admin/library-management/[id]/+page.svelte
+++ b/web/src/routes/admin/library-management/[id]/+page.svelte
@@ -39,7 +39,12 @@
onLibraryDelete={({ id }) => id === library.id && goto(AppRoute.ADMIN_LIBRARY_MANAGEMENT)}
/>
-
+
{#snippet buttons()}
diff --git a/web/src/routes/admin/server-status/+page.svelte b/web/src/routes/admin/server-status/+page.svelte
index e33a792322..31b193d952 100644
--- a/web/src/routes/admin/server-status/+page.svelte
+++ b/web/src/routes/admin/server-status/+page.svelte
@@ -24,7 +24,7 @@
});
-
+
diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte
index 7fb7559be7..71035b90ea 100644
--- a/web/src/routes/admin/system-settings/+page.svelte
+++ b/web/src/routes/admin/system-settings/+page.svelte
@@ -215,7 +215,7 @@
);
-
+
{#snippet buttons()}
diff --git a/web/src/routes/admin/users/+page.svelte b/web/src/routes/admin/users/+page.svelte
index 85cff25e97..ef20a94b86 100644
--- a/web/src/routes/admin/users/+page.svelte
+++ b/web/src/routes/admin/users/+page.svelte
@@ -2,12 +2,11 @@
import HeaderButton from '$lib/components/HeaderButton.svelte';
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import OnEvents from '$lib/components/OnEvents.svelte';
- import TableButton from '$lib/components/TableButton.svelte';
- import { getUserAdminActions, getUserAdminsActions } from '$lib/services/user-admin.service';
+ import { getUserAdminsActions, handleNavigateUserAdmin } from '$lib/services/user-admin.service';
import { locale } from '$lib/stores/preferences.store';
import { getByteUnitString } from '$lib/utils/byte-units';
- import { type UserAdminResponseDto } from '@immich/sdk';
- import { HStack, Icon } from '@immich/ui';
+ import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
+ import { Button, HStack, Icon } from '@immich/ui';
import { mdiInfinity } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
@@ -20,9 +19,11 @@
let allUsers: UserAdminResponseDto[] = $state(data.allUsers);
- const onUpdate = (user: UserAdminResponseDto) => {
+ const onUpdate = async (user: UserAdminResponseDto) => {
const index = allUsers.findIndex(({ id }) => id === user.id);
- if (index !== -1) {
+ if (index === -1) {
+ allUsers = await searchUsersAdmin({ withDeleted: true });
+ } else {
allUsers[index] = user;
}
};
@@ -42,7 +43,7 @@
{onUserAdminDeleted}
/>
-
+
{#snippet buttons()}
@@ -60,12 +61,10 @@
>
{$t('name')} |
{$t('has_quota')} |
- {$t('action')} |
{#each allUsers as user (user.id)}
- {@const { View, ContextMenu } = getUserAdminActions($t, user)}
-
-
+
{/each}
diff --git a/web/src/routes/admin/users/[id]/+page.svelte b/web/src/routes/admin/users/[id]/+page.svelte
index 434d58c56c..5fa7030173 100644
--- a/web/src/routes/admin/users/[id]/+page.svelte
+++ b/web/src/routes/admin/users/[id]/+page.svelte
@@ -8,6 +8,7 @@
import DeviceCard from '$lib/components/user-settings-page/device-card.svelte';
import FeatureSetting from '$lib/components/users/FeatureSetting.svelte';
import { AppRoute } from '$lib/constants';
+ import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { getUserAdminActions } from '$lib/services/user-admin.service';
import { locale } from '$lib/stores/preferences.store';
import { createDateFormatter, findLocale } from '$lib/utils';
@@ -15,6 +16,7 @@
import { type UserAdminResponseDto } from '@immich/sdk';
import {
Alert,
+ Badge,
Card,
CardBody,
CardHeader,
@@ -39,6 +41,7 @@
mdiPlayCircle,
mdiTrashCanOutline,
} from '@mdi/js';
+ import { DateTime } from 'luxon';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
@@ -77,7 +80,7 @@
return 'bg-primary';
};
- const UserAdminActions = $derived(getUserAdminActions($t, user));
+ const { ResetPassword, ResetPinCode, Update, Delete, Restore } = $derived(getUserAdminActions($t, user));
const onUpdate = (update: UserAdminResponseDto) => {
if (update.id === user.id) {
@@ -90,6 +93,9 @@
await goto(AppRoute.ADMIN_USERS);
}
};
+
+ const getDeleteDate = (deletedAt: string): Date =>
+ DateTime.fromISO(deletedAt).plus({ days: serverConfigManager.value.userDeleteDelay }).toJSDate();
-
+
{#snippet buttons()}
-
-
-
-
-
+
+
+
+
+
{/snippet}
@@ -116,9 +127,16 @@
{/if}
-
-
-
{user.name}
+
+
+
+ {user.name}
+
+ {#if user.isAdmin}
+
+ {$t('admin.admin_user')}
+
+ {/if}