parent
5226898184
commit
2d5ec528d5
|
|
@ -54,7 +54,7 @@ test.describe('User Administration', () => {
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Edit' }).click();
|
await page.getByRole('button', { name: 'Edit' }).click();
|
||||||
await expect(page.getByLabel('Admin User')).not.toBeChecked();
|
await expect(page.getByLabel('Admin User')).not.toBeChecked();
|
||||||
await page.getByText('Admin User').click();
|
await page.getByLabel('Admin User').click();
|
||||||
await expect(page.getByLabel('Admin User')).toBeChecked();
|
await expect(page.getByLabel('Admin User')).toBeChecked();
|
||||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||||
|
|
||||||
|
|
@ -83,7 +83,7 @@ test.describe('User Administration', () => {
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Edit' }).click();
|
await page.getByRole('button', { name: 'Edit' }).click();
|
||||||
await expect(page.getByLabel('Admin User')).toBeChecked();
|
await expect(page.getByLabel('Admin User')).toBeChecked();
|
||||||
await page.getByText('Admin User').click();
|
await page.getByLabel('Admin User').click();
|
||||||
await expect(page.getByLabel('Admin User')).not.toBeChecked();
|
await expect(page.getByLabel('Admin User')).not.toBeChecked();
|
||||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -717,8 +717,8 @@ importers:
|
||||||
specifier: file:../open-api/typescript-sdk
|
specifier: file:../open-api/typescript-sdk
|
||||||
version: link:../open-api/typescript-sdk
|
version: link:../open-api/typescript-sdk
|
||||||
'@immich/ui':
|
'@immich/ui':
|
||||||
specifier: ^0.45.0
|
specifier: ^0.49.1
|
||||||
version: 0.45.1(svelte@5.43.12)
|
version: 0.49.1(svelte@5.43.12)
|
||||||
'@mapbox/mapbox-gl-rtl-text':
|
'@mapbox/mapbox-gl-rtl-text':
|
||||||
specifier: 0.2.3
|
specifier: 0.2.3
|
||||||
version: 0.2.3(mapbox-gl@1.13.3)
|
version: 0.2.3(mapbox-gl@1.13.3)
|
||||||
|
|
@ -2983,8 +2983,8 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
|
|
||||||
'@immich/ui@0.45.1':
|
'@immich/ui@0.49.1':
|
||||||
resolution: {integrity: sha512-LanpfRI7cJLXExRxaYd4xMRvq/SWZlHmBhSsw56l0aAI6Mltm+IcFMD6LF+jvSzGOSLGLcNxIAYsqqWAPmn8+g==}
|
resolution: {integrity: sha512-E8x3iLnGRvkso1XeG3qZGPPjX8l8CoKcrTKxDvn59OjhnK0aZDs1Fv+Nq0lyOhSsH6qyV9vjDbLmhLje6D+thg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
|
|
||||||
|
|
@ -14708,7 +14708,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
svelte: 5.43.12
|
svelte: 5.43.12
|
||||||
|
|
||||||
'@immich/ui@0.45.1(svelte@5.43.12)':
|
'@immich/ui@0.49.1(svelte@5.43.12)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.43.12)
|
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.43.12)
|
||||||
'@internationalized/date': 3.10.0
|
'@internationalized/date': 3.10.0
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||||
"@immich/justified-layout-wasm": "^0.4.3",
|
"@immich/justified-layout-wasm": "^0.4.3",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@immich/ui": "^0.45.0",
|
"@immich/ui": "^0.49.1",
|
||||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@photo-sphere-viewer/core": "^5.14.0",
|
"@photo-sphere-viewer/core": "^5.14.0",
|
||||||
|
|
|
||||||
|
|
@ -76,14 +76,6 @@
|
||||||
--immich-dark-gray: 33 33 33;
|
--immich-dark-gray: 33 33 33;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
|
||||||
::after,
|
|
||||||
::before,
|
|
||||||
::backdrop,
|
|
||||||
::file-selector-button {
|
|
||||||
border-color: rgb(var(--immich-ui-default-border));
|
|
||||||
}
|
|
||||||
|
|
||||||
button:not(:disabled),
|
button:not(:disabled),
|
||||||
[role='button']:not(:disabled) {
|
[role='button']:not(:disabled) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ActionItem } from '$lib/types';
|
import { IconButton, type ActionItem } from '@immich/ui';
|
||||||
import { IconButton, type IconButtonProps } from '@immich/ui';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
action: ActionItem;
|
action: ActionItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { action }: Props = $props();
|
const { action }: Props = $props();
|
||||||
const { title, icon, color = 'secondary', props: other = {}, onSelect } = $derived(action);
|
const { title, icon, color = 'secondary', onAction } = $derived(action);
|
||||||
const onclick = (event: Event) => onSelect?.({ event, item: action });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if action.$if?.() ?? true}
|
{#if action.$if?.() ?? true}
|
||||||
<IconButton variant="ghost" {color} shape="round" {...other as IconButtonProps} {icon} aria-label={title} {onclick} />
|
<IconButton variant="ghost" shape="round" {color} {icon} aria-label={title} onclick={() => onAction(action)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ActionItem } from '$lib/types';
|
import { type ActionItem, Button, Text } from '@immich/ui';
|
||||||
import { Button, type ButtonProps, Text } from '@immich/ui';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
action: ActionItem;
|
action: ActionItem;
|
||||||
|
title?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { action }: Props = $props();
|
const { action, title: titleAttr }: Props = $props();
|
||||||
const { title, icon, color = 'secondary', props: other = {}, onSelect } = $derived(action);
|
const { title, icon, color = 'secondary', onAction } = $derived(action);
|
||||||
const onclick = (event: Event) => onSelect?.({ event, item: action });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if action.$if?.() ?? true}
|
{#if action.$if?.() ?? true}
|
||||||
<Button variant="ghost" size="small" {color} {...other as ButtonProps} leadingIcon={icon} {onclick}>
|
<Button variant="ghost" size="small" {color} leadingIcon={icon} onclick={() => onAction(action)} title={titleAttr}>
|
||||||
<Text class="hidden md:block">{title}</Text>
|
<Text class="hidden md:block">{title}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ActionItem } from '$lib/types';
|
import { IconButton, type ActionItem } from '@immich/ui';
|
||||||
import { IconButton, type IconButtonProps } from '@immich/ui';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
action: ActionItem;
|
action: ActionItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { action }: Props = $props();
|
const { action }: Props = $props();
|
||||||
const { title, icon, props: other = {}, onSelect } = $derived(action);
|
const { title, icon, onAction } = $derived(action);
|
||||||
const onclick = (event: Event) => onSelect?.({ event, item: action });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if action.$if?.() ?? true}
|
{#if action.$if?.() ?? true}
|
||||||
<IconButton shape="round" color="primary" {...other as IconButtonProps} {icon} aria-label={title} {onclick} />
|
<IconButton shape="round" color="primary" {icon} aria-label={title} onclick={() => onAction(action)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' • ');
|
.join(' • ');
|
||||||
|
|
||||||
const SharedLinkActions = $derived(getSharedLinkActions($t, sharedLink));
|
const { ViewQrCode, Copy } = $derived(getSharedLinkActions($t, sharedLink));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
<Text size="tiny" color="muted">{getShareProperties()}</Text>
|
<Text size="tiny" color="muted">{getShareProperties()}</Text>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<ActionButton action={SharedLinkActions.ViewQrCode} />
|
<ActionButton action={ViewQrCode} />
|
||||||
<ActionButton action={SharedLinkActions.Copy} />
|
<ActionButton action={Copy} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,16 @@
|
||||||
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
||||||
import AdminSidebar from '$lib/sidebars/AdminSidebar.svelte';
|
import AdminSidebar from '$lib/sidebars/AdminSidebar.svelte';
|
||||||
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||||
import { AppShell, AppShellHeader, AppShellSidebar, Scrollable } from '@immich/ui';
|
import { AppShell, AppShellHeader, AppShellSidebar, Scrollable, type BreadcrumbItem } from '@immich/ui';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
breadcrumbs: BreadcrumbItem[];
|
||||||
buttons?: Snippet;
|
buttons?: Snippet;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { title, buttons, children }: Props = $props();
|
let { breadcrumbs, buttons, children }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppShell>
|
<AppShell>
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
<AdminSidebar />
|
<AdminSidebar />
|
||||||
</AppShellSidebar>
|
</AppShellSidebar>
|
||||||
|
|
||||||
<TitleLayout {title} {buttons}>
|
<TitleLayout {breadcrumbs} {buttons}>
|
||||||
<Scrollable class="grow">
|
<Scrollable class="grow">
|
||||||
<PageContent>
|
<PageContent>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,20 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Text } from '@immich/ui';
|
import { Breadcrumbs, type BreadcrumbItem } from '@immich/ui';
|
||||||
|
import { mdiSlashForward } from '@mdi/js';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
id?: string;
|
breadcrumbs: BreadcrumbItem[];
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
buttons?: Snippet;
|
buttons?: Snippet;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
}
|
};
|
||||||
|
|
||||||
let { id, title, description, buttons, children }: Props = $props();
|
let { breadcrumbs, buttons, children }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="h-full flex flex-col">
|
<div class="h-full flex flex-col">
|
||||||
<div class="flex h-16 w-full place-items-center justify-between border-b p-2">
|
<div class="flex h-16 w-full place-items-center justify-between border-b p-2">
|
||||||
<div class="flex gap-1">
|
<Breadcrumbs items={breadcrumbs} separator={mdiSlashForward} />
|
||||||
<div class="font-medium outline-none" tabindex="-1" {id}>{title}</div>
|
|
||||||
{#if description}
|
|
||||||
<Text color="muted">{description}</Text>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{@render buttons?.()}
|
{@render buttons?.()}
|
||||||
</div>
|
</div>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import { getSharedLinkActions } from '$lib/services/shared-link.service';
|
import { getSharedLinkActions } from '$lib/services/shared-link.service';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
|
import { ContextMenuButton, MenuItemType } from '@immich/ui';
|
||||||
import { DateTime, type ToRelativeUnit } from 'luxon';
|
import { DateTime, type ToRelativeUnit } from 'luxon';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
|
@ -31,7 +32,7 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const SharedLinkActions = $derived(getSharedLinkActions($t, sharedLink));
|
const { Edit, Copy, Delete } = $derived(getSharedLinkActions($t, sharedLink));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -95,13 +96,17 @@
|
||||||
</svelte:element>
|
</svelte:element>
|
||||||
<div class="flex flex-auto flex-col place-content-center place-items-end text-end ms-4">
|
<div class="flex flex-auto flex-col place-content-center place-items-end text-end ms-4">
|
||||||
<div class="sm:flex hidden">
|
<div class="sm:flex hidden">
|
||||||
<ActionButton action={SharedLinkActions.Edit} />
|
<ActionButton action={Edit} />
|
||||||
<ActionButton action={SharedLinkActions.Copy} />
|
<ActionButton action={Copy} />
|
||||||
<ActionButton action={SharedLinkActions.Delete} />
|
<ActionButton action={Delete} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:hidden">
|
<div class="sm:hidden">
|
||||||
<ActionButton action={SharedLinkActions.ContextMenu} />
|
<ContextMenuButton
|
||||||
|
aria-label={$t('shared_link_options')}
|
||||||
|
position="top-right"
|
||||||
|
items={[Edit, Copy, MenuItemType.Divider, Delete]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import LibraryFolderAddModal from '$lib/modals/LibraryFolderAddModal.svelte';
|
||||||
import LibraryFolderEditModal from '$lib/modals/LibraryFolderEditModal.svelte';
|
import LibraryFolderEditModal from '$lib/modals/LibraryFolderEditModal.svelte';
|
||||||
import LibraryRenameModal from '$lib/modals/LibraryRenameModal.svelte';
|
import LibraryRenameModal from '$lib/modals/LibraryRenameModal.svelte';
|
||||||
import LibraryUserPickerModal from '$lib/modals/LibraryUserPickerModal.svelte';
|
import LibraryUserPickerModal from '$lib/modals/LibraryUserPickerModal.svelte';
|
||||||
import type { ActionItem } from '$lib/types';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import {
|
import {
|
||||||
|
|
@ -20,7 +19,7 @@ import {
|
||||||
updateLibrary,
|
updateLibrary,
|
||||||
type LibraryResponseDto,
|
type LibraryResponseDto,
|
||||||
} from '@immich/sdk';
|
} 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 { mdiPencilOutline, mdiPlusBoxOutline, mdiSync, mdiTrashCanOutline } from '@mdi/js';
|
||||||
import type { MessageFormatter } from 'svelte-i18n';
|
import type { MessageFormatter } from 'svelte-i18n';
|
||||||
|
|
||||||
|
|
@ -28,13 +27,13 @@ export const getLibrariesActions = ($t: MessageFormatter) => {
|
||||||
const ScanAll: ActionItem = {
|
const ScanAll: ActionItem = {
|
||||||
title: $t('scan_all_libraries'),
|
title: $t('scan_all_libraries'),
|
||||||
icon: mdiSync,
|
icon: mdiSync,
|
||||||
onSelect: () => void handleScanAllLibraries(),
|
onAction: () => void handleScanAllLibraries(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Create: ActionItem = {
|
const Create: ActionItem = {
|
||||||
title: $t('create_library'),
|
title: $t('create_library'),
|
||||||
icon: mdiPlusBoxOutline,
|
icon: mdiPlusBoxOutline,
|
||||||
onSelect: () => void handleCreateLibrary(),
|
onAction: () => void handleCreateLibrary(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ScanAll, Create };
|
return { ScanAll, Create };
|
||||||
|
|
@ -44,32 +43,32 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
|
||||||
const Rename: ActionItem = {
|
const Rename: ActionItem = {
|
||||||
icon: mdiPencilOutline,
|
icon: mdiPencilOutline,
|
||||||
title: $t('rename'),
|
title: $t('rename'),
|
||||||
onSelect: () => void modalManager.show(LibraryRenameModal, { library }),
|
onAction: () => void modalManager.show(LibraryRenameModal, { library }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Delete: ActionItem = {
|
const Delete: ActionItem = {
|
||||||
icon: mdiTrashCanOutline,
|
icon: mdiTrashCanOutline,
|
||||||
title: $t('delete'),
|
title: $t('delete'),
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
onSelect: () => void handleDeleteLibrary(library),
|
onAction: () => void handleDeleteLibrary(library),
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddFolder: ActionItem = {
|
const AddFolder: ActionItem = {
|
||||||
icon: mdiPlusBoxOutline,
|
icon: mdiPlusBoxOutline,
|
||||||
title: $t('add'),
|
title: $t('add'),
|
||||||
onSelect: () => void modalManager.show(LibraryFolderAddModal, { library }),
|
onAction: () => void modalManager.show(LibraryFolderAddModal, { library }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddExclusionPattern: ActionItem = {
|
const AddExclusionPattern: ActionItem = {
|
||||||
icon: mdiPlusBoxOutline,
|
icon: mdiPlusBoxOutline,
|
||||||
title: $t('add'),
|
title: $t('add'),
|
||||||
onSelect: () => void modalManager.show(LibraryExclusionPatternAddModal, { library }),
|
onAction: () => void modalManager.show(LibraryExclusionPatternAddModal, { library }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Scan: ActionItem = {
|
const Scan: ActionItem = {
|
||||||
icon: mdiSync,
|
icon: mdiSync,
|
||||||
title: $t('scan_library'),
|
title: $t('scan_library'),
|
||||||
onSelect: () => void handleScanLibrary(library),
|
onAction: () => void handleScanLibrary(library),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { Rename, Delete, AddFolder, AddExclusionPattern, Scan };
|
return { Rename, Delete, AddFolder, AddExclusionPattern, Scan };
|
||||||
|
|
@ -79,13 +78,13 @@ export const getLibraryFolderActions = ($t: MessageFormatter, library: LibraryRe
|
||||||
const Edit: ActionItem = {
|
const Edit: ActionItem = {
|
||||||
icon: mdiPencilOutline,
|
icon: mdiPencilOutline,
|
||||||
title: $t('edit'),
|
title: $t('edit'),
|
||||||
onSelect: () => void modalManager.show(LibraryFolderEditModal, { folder, library }),
|
onAction: () => void modalManager.show(LibraryFolderEditModal, { folder, library }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Delete: ActionItem = {
|
const Delete: ActionItem = {
|
||||||
icon: mdiTrashCanOutline,
|
icon: mdiTrashCanOutline,
|
||||||
title: $t('delete'),
|
title: $t('delete'),
|
||||||
onSelect: () => void handleDeleteLibraryFolder(library, folder),
|
onAction: () => void handleDeleteLibraryFolder(library, folder),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { Edit, Delete };
|
return { Edit, Delete };
|
||||||
|
|
@ -99,13 +98,13 @@ export const getLibraryExclusionPatternActions = (
|
||||||
const Edit: ActionItem = {
|
const Edit: ActionItem = {
|
||||||
icon: mdiPencilOutline,
|
icon: mdiPencilOutline,
|
||||||
title: $t('edit'),
|
title: $t('edit'),
|
||||||
onSelect: () => void modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }),
|
onAction: () => void modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Delete: ActionItem = {
|
const Delete: ActionItem = {
|
||||||
icon: mdiTrashCanOutline,
|
icon: mdiTrashCanOutline,
|
||||||
title: $t('delete'),
|
title: $t('delete'),
|
||||||
onSelect: () => void handleDeleteExclusionPattern(library, exclusionPattern),
|
onAction: () => void handleDeleteExclusionPattern(library, exclusionPattern),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { Edit, Delete };
|
return { Edit, Delete };
|
||||||
|
|
|
||||||
|
|
@ -16,48 +16,37 @@ import {
|
||||||
type SharedLinkEditDto,
|
type SharedLinkEditDto,
|
||||||
type SharedLinkResponseDto,
|
type SharedLinkResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { MenuItemType, menuManager, modalManager, toastManager, type MenuItem } from '@immich/ui';
|
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||||
import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiDotsVertical, mdiQrcode } from '@mdi/js';
|
import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiQrcode } from '@mdi/js';
|
||||||
import type { MessageFormatter } from 'svelte-i18n';
|
import type { MessageFormatter } from 'svelte-i18n';
|
||||||
|
|
||||||
export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLinkResponseDto) => {
|
export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLinkResponseDto) => {
|
||||||
const Edit: MenuItem = {
|
const Edit: ActionItem = {
|
||||||
title: $t('edit_link'),
|
title: $t('edit_link'),
|
||||||
icon: mdiCircleEditOutline,
|
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'),
|
title: $t('delete_link'),
|
||||||
icon: mdiDelete,
|
icon: mdiDelete,
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
onSelect: () => void handleDeleteSharedLink(sharedLink),
|
onAction: () => void handleDeleteSharedLink(sharedLink),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Copy: MenuItem = {
|
const Copy: ActionItem = {
|
||||||
title: $t('copy_link'),
|
title: $t('copy_link'),
|
||||||
icon: mdiContentCopy,
|
icon: mdiContentCopy,
|
||||||
onSelect: () => void copyToClipboard(asUrl(sharedLink)),
|
onAction: () => void copyToClipboard(asUrl(sharedLink)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const ViewQrCode: MenuItem = {
|
const ViewQrCode: ActionItem = {
|
||||||
title: $t('view_qr_code'),
|
title: $t('view_qr_code'),
|
||||||
icon: mdiQrcode,
|
icon: mdiQrcode,
|
||||||
onSelect: () => void handleShowSharedLinkQrCode(sharedLink),
|
onAction: () => void handleShowSharedLinkQrCode(sharedLink),
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContextMenu: MenuItem = {
|
return { Edit, Delete, Copy, ViewQrCode };
|
||||||
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 };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const asUrl = (sharedLink: SharedLinkResponseDto) => {
|
const asUrl = (sharedLink: SharedLinkResponseDto) => {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
||||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
import type { ActionItem } from '$lib/types';
|
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard } from '$lib/utils';
|
||||||
import { downloadBlob } from '$lib/utils/asset-utils';
|
import { downloadBlob } from '$lib/utils/asset-utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getConfig, updateConfig, type ServerFeaturesDto, type SystemConfigDto } from '@immich/sdk';
|
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 { mdiContentCopy, mdiDownload, mdiUpload } from '@mdi/js';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import type { MessageFormatter } from 'svelte-i18n';
|
import type { MessageFormatter } from 'svelte-i18n';
|
||||||
|
|
@ -19,20 +18,20 @@ export const getSystemConfigActions = (
|
||||||
const CopyToClipboard: ActionItem = {
|
const CopyToClipboard: ActionItem = {
|
||||||
title: $t('copy_to_clipboard'),
|
title: $t('copy_to_clipboard'),
|
||||||
icon: mdiContentCopy,
|
icon: mdiContentCopy,
|
||||||
onSelect: () => void handleCopyToClipboard(config),
|
onAction: () => void handleCopyToClipboard(config),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Download: ActionItem = {
|
const Download: ActionItem = {
|
||||||
title: $t('export_as_json'),
|
title: $t('export_as_json'),
|
||||||
icon: mdiDownload,
|
icon: mdiDownload,
|
||||||
onSelect: () => handleDownloadConfig(config),
|
onAction: () => handleDownloadConfig(config),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Upload: ActionItem = {
|
const Upload: ActionItem = {
|
||||||
title: $t('import_from_json'),
|
title: $t('import_from_json'),
|
||||||
icon: mdiUpload,
|
icon: mdiUpload,
|
||||||
$if: () => !featureFlags.configFile,
|
$if: () => !featureFlags.configFile,
|
||||||
onSelect: () => handleUploadConfig(),
|
onAction: () => handleUploadConfig(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { CopyToClipboard, Download, Upload };
|
return { CopyToClipboard, Download, Upload };
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
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 PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte';
|
||||||
import UserCreateModal from '$lib/modals/UserCreateModal.svelte';
|
import UserCreateModal from '$lib/modals/UserCreateModal.svelte';
|
||||||
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
|
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
|
||||||
import UserEditModal from '$lib/modals/UserEditModal.svelte';
|
import UserEditModal from '$lib/modals/UserEditModal.svelte';
|
||||||
import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte';
|
import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte';
|
||||||
import { user as authUser } from '$lib/stores/user.store';
|
import { user as authUser } from '$lib/stores/user.store';
|
||||||
import type { ActionItem } from '$lib/types';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import {
|
import {
|
||||||
|
|
@ -21,45 +19,33 @@ import {
|
||||||
type UserAdminResponseDto,
|
type UserAdminResponseDto,
|
||||||
type UserAdminUpdateDto,
|
type UserAdminUpdateDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { MenuItemType, menuManager, modalManager, toastManager } from '@immich/ui';
|
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiDeleteRestore,
|
mdiDeleteRestore,
|
||||||
mdiDotsVertical,
|
|
||||||
mdiEyeOutline,
|
|
||||||
mdiLockReset,
|
mdiLockReset,
|
||||||
mdiLockSmart,
|
mdiLockSmart,
|
||||||
mdiPencilOutline,
|
mdiPencilOutline,
|
||||||
mdiPlusBoxOutline,
|
mdiPlusBoxOutline,
|
||||||
mdiTrashCanOutline,
|
mdiTrashCanOutline,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import type { MessageFormatter } from 'svelte-i18n';
|
import type { MessageFormatter } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
const getDeleteDate = (deletedAt: string): Date =>
|
|
||||||
DateTime.fromISO(deletedAt).plus({ days: serverConfigManager.value.userDeleteDelay }).toJSDate();
|
|
||||||
|
|
||||||
export const getUserAdminsActions = ($t: MessageFormatter) => {
|
export const getUserAdminsActions = ($t: MessageFormatter) => {
|
||||||
const Create: ActionItem = {
|
const Create: ActionItem = {
|
||||||
title: $t('create_user'),
|
title: $t('create_user'),
|
||||||
icon: mdiPlusBoxOutline,
|
icon: mdiPlusBoxOutline,
|
||||||
onSelect: () => void modalManager.show(UserCreateModal, {}),
|
onAction: () => void modalManager.show(UserCreateModal, {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { Create };
|
return { Create };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminResponseDto) => {
|
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 = {
|
const Update: ActionItem = {
|
||||||
icon: mdiPencilOutline,
|
icon: mdiPencilOutline,
|
||||||
title: $t('edit'),
|
title: $t('edit'),
|
||||||
onSelect: () => void modalManager.show(UserEditModal, { user }),
|
onAction: () => void modalManager.show(UserEditModal, { user }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Delete: ActionItem = {
|
const Delete: ActionItem = {
|
||||||
|
|
@ -67,7 +53,7 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
|
||||||
title: $t('delete'),
|
title: $t('delete'),
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
$if: () => get(authUser).id !== user.id && !user.deletedAt,
|
$if: () => get(authUser).id !== user.id && !user.deletedAt,
|
||||||
onSelect: () => void modalManager.show(UserDeleteConfirmModal, { user }),
|
onAction: () => void modalManager.show(UserDeleteConfirmModal, { user }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Restore: ActionItem = {
|
const Restore: ActionItem = {
|
||||||
|
|
@ -75,47 +61,23 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
|
||||||
title: $t('restore'),
|
title: $t('restore'),
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
$if: () => !!user.deletedAt && user.status === UserStatus.Deleted,
|
$if: () => !!user.deletedAt && user.status === UserStatus.Deleted,
|
||||||
onSelect: () => void modalManager.show(UserRestoreConfirmModal, { user }),
|
onAction: () => void modalManager.show(UserRestoreConfirmModal, { user }),
|
||||||
props: {
|
|
||||||
title: $t('admin.user_restore_scheduled_removal', {
|
|
||||||
values: { date: getDeleteDate(user.deletedAt!) },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ResetPassword: ActionItem = {
|
const ResetPassword: ActionItem = {
|
||||||
icon: mdiLockReset,
|
icon: mdiLockReset,
|
||||||
title: $t('reset_password'),
|
title: $t('reset_password'),
|
||||||
$if: () => get(authUser).id !== user.id,
|
$if: () => get(authUser).id !== user.id,
|
||||||
onSelect: () => void handleResetPasswordUserAdmin(user),
|
onAction: () => void handleResetPasswordUserAdmin(user),
|
||||||
};
|
};
|
||||||
|
|
||||||
const ResetPinCode: ActionItem = {
|
const ResetPinCode: ActionItem = {
|
||||||
icon: mdiLockSmart,
|
icon: mdiLockSmart,
|
||||||
title: $t('reset_pin_code'),
|
title: $t('reset_pin_code'),
|
||||||
onSelect: () => void handleResetPinCodeUserAdmin(user),
|
onAction: () => void handleResetPinCodeUserAdmin(user),
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContextMenu: ActionItem = {
|
return { Update, Delete, Restore, ResetPassword, ResetPinCode };
|
||||||
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 };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleCreateUserAdmin = async (dto: UserAdminCreateDto) => {
|
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
|
// TODO move password reset server-side
|
||||||
const generatePassword = (length: number = 16) => {
|
const generatePassword = (length: number = 16) => {
|
||||||
let generatedPassword = '';
|
let generatedPassword = '';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
import type { ServerVersionResponseDto } from '@immich/sdk';
|
import type { ServerVersionResponseDto } from '@immich/sdk';
|
||||||
import type { MenuItem } from '@immich/ui';
|
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
|
||||||
|
|
||||||
export type ActionItem = MenuItem & { props?: Omit<HTMLAttributes<HTMLElement>, 'color'> };
|
|
||||||
|
|
||||||
export interface ReleaseEvent {
|
export interface ReleaseEvent {
|
||||||
isAvailable: boolean;
|
isAvailable: boolean;
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AdminPageLayout title={data.meta.title}>
|
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||||
{#snippet buttons()}
|
{#snippet buttons()}
|
||||||
<HStack gap={0}>
|
<HStack gap={0}>
|
||||||
{#if pausedJobs.length > 0}
|
{#if pausedJobs.length > 0}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
onLibraryDelete={handleDeleteLibrary}
|
onLibraryDelete={handleDeleteLibrary}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminPageLayout title={data.meta.title}>
|
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||||
{#snippet buttons()}
|
{#snippet buttons()}
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
{#if libraries.length > 0}
|
{#if libraries.length > 0}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,12 @@
|
||||||
onLibraryDelete={({ id }) => id === library.id && goto(AppRoute.ADMIN_LIBRARY_MANAGEMENT)}
|
onLibraryDelete={({ id }) => id === library.id && goto(AppRoute.ADMIN_LIBRARY_MANAGEMENT)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminPageLayout title={data.meta.title}>
|
<AdminPageLayout
|
||||||
|
breadcrumbs={[
|
||||||
|
{ title: $t('admin.external_library_management'), href: AppRoute.ADMIN_LIBRARY_MANAGEMENT },
|
||||||
|
{ title: library.name },
|
||||||
|
]}
|
||||||
|
>
|
||||||
{#snippet buttons()}
|
{#snippet buttons()}
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<HeaderButton action={Scan} />
|
<HeaderButton action={Scan} />
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AdminPageLayout title={data.meta.title}>
|
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||||
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
||||||
<section class="w-full pb-28 sm:w-5/6 md:w-212.5">
|
<section class="w-full pb-28 sm:w-5/6 md:w-212.5">
|
||||||
<ServerStatisticsPanel {stats} />
|
<ServerStatisticsPanel {stats} />
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AdminPageLayout title={data.meta.title}>
|
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||||
{#snippet buttons()}
|
{#snippet buttons()}
|
||||||
<HStack gap={1}>
|
<HStack gap={1}>
|
||||||
<div class="hidden lg:block">
|
<div class="hidden lg:block">
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,11 @@
|
||||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
||||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
import TableButton from '$lib/components/TableButton.svelte';
|
import { getUserAdminsActions, handleNavigateUserAdmin } from '$lib/services/user-admin.service';
|
||||||
import { getUserAdminActions, getUserAdminsActions } from '$lib/services/user-admin.service';
|
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||||
import { type UserAdminResponseDto } from '@immich/sdk';
|
import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||||
import { HStack, Icon } from '@immich/ui';
|
import { Button, HStack, Icon } from '@immich/ui';
|
||||||
import { mdiInfinity } from '@mdi/js';
|
import { mdiInfinity } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
@ -20,9 +19,11 @@
|
||||||
|
|
||||||
let allUsers: UserAdminResponseDto[] = $state(data.allUsers);
|
let allUsers: UserAdminResponseDto[] = $state(data.allUsers);
|
||||||
|
|
||||||
const onUpdate = (user: UserAdminResponseDto) => {
|
const onUpdate = async (user: UserAdminResponseDto) => {
|
||||||
const index = allUsers.findIndex(({ id }) => id === user.id);
|
const index = allUsers.findIndex(({ id }) => id === user.id);
|
||||||
if (index !== -1) {
|
if (index === -1) {
|
||||||
|
allUsers = await searchUsersAdmin({ withDeleted: true });
|
||||||
|
} else {
|
||||||
allUsers[index] = user;
|
allUsers[index] = user;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -42,7 +43,7 @@
|
||||||
{onUserAdminDeleted}
|
{onUserAdminDeleted}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminPageLayout title={data.meta.title}>
|
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||||
{#snippet buttons()}
|
{#snippet buttons()}
|
||||||
<HStack gap={1}>
|
<HStack gap={1}>
|
||||||
<HeaderButton action={Create} />
|
<HeaderButton action={Create} />
|
||||||
|
|
@ -60,12 +61,10 @@
|
||||||
>
|
>
|
||||||
<th class="hidden sm:block w-3/12 text-center text-sm font-medium">{$t('name')}</th>
|
<th class="hidden sm:block w-3/12 text-center text-sm font-medium">{$t('name')}</th>
|
||||||
<th class="hidden xl:block w-3/12 2xl:w-2/12 text-center text-sm font-medium">{$t('has_quota')}</th>
|
<th class="hidden xl:block w-3/12 2xl:w-2/12 text-center text-sm font-medium">{$t('has_quota')}</th>
|
||||||
<th class="w-4/12 lg:w-3/12 xl:w-2/12 text-center text-sm font-medium">{$t('action')}</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||||
{#each allUsers as user (user.id)}
|
{#each allUsers as user (user.id)}
|
||||||
{@const { View, ContextMenu } = getUserAdminActions($t, user)}
|
|
||||||
<tr
|
<tr
|
||||||
class="flex h-20 overflow-hidden w-full place-items-center text-center dark:text-immich-dark-fg {user.deletedAt
|
class="flex h-20 overflow-hidden w-full place-items-center text-center dark:text-immich-dark-fg {user.deletedAt
|
||||||
? 'bg-red-300 dark:bg-red-900'
|
? 'bg-red-300 dark:bg-red-900'
|
||||||
|
|
@ -87,8 +86,7 @@
|
||||||
<td
|
<td
|
||||||
class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-4/12 lg:w-3/12 xl:w-2/12 text-ellipsis break-all text-sm"
|
class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-4/12 lg:w-3/12 xl:w-2/12 text-ellipsis break-all text-sm"
|
||||||
>
|
>
|
||||||
<TableButton action={View} />
|
<Button onclick={() => handleNavigateUserAdmin(user)}>{$t('view')}</Button>
|
||||||
<TableButton action={ContextMenu} />
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import DeviceCard from '$lib/components/user-settings-page/device-card.svelte';
|
import DeviceCard from '$lib/components/user-settings-page/device-card.svelte';
|
||||||
import FeatureSetting from '$lib/components/users/FeatureSetting.svelte';
|
import FeatureSetting from '$lib/components/users/FeatureSetting.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||||
import { getUserAdminActions } from '$lib/services/user-admin.service';
|
import { getUserAdminActions } from '$lib/services/user-admin.service';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { createDateFormatter, findLocale } from '$lib/utils';
|
import { createDateFormatter, findLocale } from '$lib/utils';
|
||||||
|
|
@ -15,6 +16,7 @@
|
||||||
import { type UserAdminResponseDto } from '@immich/sdk';
|
import { type UserAdminResponseDto } from '@immich/sdk';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Badge,
|
||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
|
|
@ -39,6 +41,7 @@
|
||||||
mdiPlayCircle,
|
mdiPlayCircle,
|
||||||
mdiTrashCanOutline,
|
mdiTrashCanOutline,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
|
@ -77,7 +80,7 @@
|
||||||
return 'bg-primary';
|
return 'bg-primary';
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserAdminActions = $derived(getUserAdminActions($t, user));
|
const { ResetPassword, ResetPinCode, Update, Delete, Restore } = $derived(getUserAdminActions($t, user));
|
||||||
|
|
||||||
const onUpdate = (update: UserAdminResponseDto) => {
|
const onUpdate = (update: UserAdminResponseDto) => {
|
||||||
if (update.id === user.id) {
|
if (update.id === user.id) {
|
||||||
|
|
@ -90,6 +93,9 @@
|
||||||
await goto(AppRoute.ADMIN_USERS);
|
await goto(AppRoute.ADMIN_USERS);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDeleteDate = (deletedAt: string): Date =>
|
||||||
|
DateTime.fromISO(deletedAt).plus({ days: serverConfigManager.value.userDeleteDelay }).toJSDate();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<OnEvents
|
<OnEvents
|
||||||
|
|
@ -99,14 +105,19 @@
|
||||||
{onUserAdminDeleted}
|
{onUserAdminDeleted}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminPageLayout title={data.meta.title}>
|
<AdminPageLayout
|
||||||
|
breadcrumbs={[{ title: $t('admin.user_management'), href: AppRoute.ADMIN_USERS }, { title: user.name }]}
|
||||||
|
>
|
||||||
{#snippet buttons()}
|
{#snippet buttons()}
|
||||||
<HStack gap={0}>
|
<HStack gap={0}>
|
||||||
<HeaderButton action={UserAdminActions.ResetPassword} />
|
<HeaderButton action={ResetPassword} />
|
||||||
<HeaderButton action={UserAdminActions.ResetPinCode} />
|
<HeaderButton action={ResetPinCode} />
|
||||||
<HeaderButton action={UserAdminActions.Update} />
|
<HeaderButton action={Update} />
|
||||||
<HeaderButton action={UserAdminActions.Restore} />
|
<HeaderButton
|
||||||
<HeaderButton action={UserAdminActions.Delete} />
|
action={Restore}
|
||||||
|
title={$t('admin.user_restore_scheduled_removal', { values: { date: getDeleteDate(user.deletedAt!) } })}
|
||||||
|
/>
|
||||||
|
<HeaderButton action={Delete} />
|
||||||
</HStack>
|
</HStack>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -116,9 +127,16 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="grid gap-4 grid-cols-1 lg:grid-cols-2 w-full">
|
<div class="grid gap-4 grid-cols-1 lg:grid-cols-2 w-full">
|
||||||
<div class="col-span-full flex gap-4 items-center my-4">
|
<div class="col-span-full flex flex-col gap-4 my-4">
|
||||||
<UserAvatar {user} size="md" />
|
<div class="flex items-center gap-4">
|
||||||
<Heading tag="h1" size="large">{user.name}</Heading>
|
<UserAvatar {user} size="md" />
|
||||||
|
<Heading tag="h1" size="large">{user.name}</Heading>
|
||||||
|
</div>
|
||||||
|
{#if user.isAdmin}
|
||||||
|
<div>
|
||||||
|
<Badge color="primary" size="small">{$t('admin.admin_user')}</Badge>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-full">
|
<div class="col-span-full">
|
||||||
<div class="flex flex-col lg:flex-row gap-4 w-full">
|
<div class="flex flex-col lg:flex-row gap-4 w-full">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue