mirror of
https://github.com/immich-app/immich.git
synced 2026-06-12 11:01:45 -07:00
refactor: settings accordion reactivity (#28281)
This commit is contained in:
@@ -11,5 +11,6 @@
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"oidc-provider": "^9.0.0",
|
||||
"tsx": "^4.20.6"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.33.1"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { accordionManager } from '$lib/managers/accordion-manager.svelte';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { onDestroy, onMount, type Snippet } from 'svelte';
|
||||
import { onDestroy, type Snippet } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { getAccordionState } from './SettingAccordionState.svelte';
|
||||
|
||||
const accordionState = getAccordionState();
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -21,7 +19,7 @@
|
||||
title,
|
||||
subtitle = '',
|
||||
key,
|
||||
isOpen = $bindable($accordionState.has(key)),
|
||||
isOpen = $bindable(false),
|
||||
autoScrollTo = false,
|
||||
icon = '',
|
||||
subtitleSnippet,
|
||||
@@ -30,9 +28,15 @@
|
||||
|
||||
let accordionElement: HTMLDivElement | undefined = $state();
|
||||
|
||||
const setIsOpen = (isOpen: boolean) => {
|
||||
$effect(() => {
|
||||
isOpen = accordionManager.isOpen(key);
|
||||
});
|
||||
|
||||
const toggleOpen = () => {
|
||||
if (isOpen) {
|
||||
$accordionState = $accordionState.add(key);
|
||||
accordionManager.close(key);
|
||||
} else {
|
||||
accordionManager.open(key);
|
||||
|
||||
if (autoScrollTo) {
|
||||
setTimeout(() => {
|
||||
@@ -42,24 +46,11 @@
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
} else {
|
||||
$accordionState.delete(key);
|
||||
// eslint-disable-next-line no-self-assign
|
||||
$accordionState = $accordionState;
|
||||
}
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
setIsOpen(false);
|
||||
});
|
||||
|
||||
const onclick = () => {
|
||||
isOpen = !isOpen;
|
||||
setIsOpen(isOpen);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
setIsOpen(isOpen);
|
||||
accordionManager.close(key);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -72,7 +63,7 @@
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isOpen}
|
||||
{onclick}
|
||||
onclick={toggleOpen}
|
||||
class="flex w-full place-items-center justify-between text-start"
|
||||
>
|
||||
<div>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<script lang="ts" module>
|
||||
export type AccordionState = Set<string>;
|
||||
|
||||
const { get: getAccordionState, set: setAccordionState } = createContext<Writable<AccordionState>>();
|
||||
export { getAccordionState };
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { createContext } from '$lib/utils/context';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
|
||||
const getParamValues = (param: string) => {
|
||||
return new Set((page.url.searchParams.get(param) || '').split(' ').filter((x) => x !== ''));
|
||||
};
|
||||
|
||||
interface Props {
|
||||
queryParam: string;
|
||||
state?: Writable<AccordionState>;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { queryParam, state = writable(getParamValues(queryParam)), children }: Props = $props();
|
||||
setAccordionState(state);
|
||||
|
||||
const searchParams = new SvelteURLSearchParams(page.url.searchParams);
|
||||
|
||||
$effect(() => {
|
||||
if ($state.size > 0) {
|
||||
searchParams.set(queryParam, [...$state].join(' '));
|
||||
} else {
|
||||
searchParams.delete(queryParam);
|
||||
}
|
||||
|
||||
handlePromiseError(goto(`?${searchParams.toString()}`, { replaceState: true, noScroll: true, keepFocus: true }));
|
||||
});
|
||||
</script>
|
||||
|
||||
{@render children?.()}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { SvelteSet, SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { QueryParameter } from '$lib/constants';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
|
||||
class AccordionManager {
|
||||
// needs to be derived since `page.url.searchParams` isn't actually initialized by the time this class gets instantiated.
|
||||
#searchParams = $derived(new SvelteURLSearchParams(page.url.searchParams));
|
||||
#state = $derived(
|
||||
new SvelteSet(
|
||||
this.#searchParams
|
||||
.get(QueryParameter.IS_OPEN)
|
||||
?.split(' ')
|
||||
.filter((x) => x !== ''),
|
||||
),
|
||||
);
|
||||
|
||||
isOpen(key: string) {
|
||||
return this.#state.has(key);
|
||||
}
|
||||
|
||||
#refreshSearchParams() {
|
||||
if (this.#state.size === 0) {
|
||||
this.#searchParams.delete(QueryParameter.IS_OPEN);
|
||||
} else {
|
||||
this.#searchParams.set(QueryParameter.IS_OPEN, [...this.#state].join(' '));
|
||||
}
|
||||
|
||||
handlePromiseError(
|
||||
goto(`?${this.#searchParams.toString()}`, { replaceState: true, noScroll: true, keepFocus: true }),
|
||||
);
|
||||
}
|
||||
|
||||
open(key: string) {
|
||||
this.#state.add(key);
|
||||
this.#refreshSearchParams();
|
||||
}
|
||||
|
||||
close(key: string) {
|
||||
this.#state.delete(key);
|
||||
this.#refreshSearchParams();
|
||||
}
|
||||
}
|
||||
|
||||
export const accordionManager = new AccordionManager();
|
||||
@@ -1,8 +0,0 @@
|
||||
import { getContext, setContext } from 'svelte';
|
||||
|
||||
export function createContext<T>(key: string | symbol = Symbol()) {
|
||||
return {
|
||||
get: () => getContext<T>(key),
|
||||
set: (context: T) => setContext<T>(key, context),
|
||||
};
|
||||
}
|
||||
@@ -26,7 +26,6 @@
|
||||
mdiTwoFactorAuthentication,
|
||||
} from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import SettingAccordionState from '$lib/components/shared-components/settings/SettingAccordionState.svelte';
|
||||
import SettingAccordion from '$lib/components/shared-components/settings/SettingAccordion.svelte';
|
||||
import AppSettings from './AppSettings.svelte';
|
||||
import ChangePasswordSettings from './ChangePasswordSettings.svelte';
|
||||
@@ -48,116 +47,114 @@
|
||||
$page.url.searchParams.get(QueryParameter.OPEN_SETTING) === OpenQueryParam.OAUTH;
|
||||
</script>
|
||||
|
||||
<SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
|
||||
<SettingAccordion
|
||||
icon={mdiCogOutline}
|
||||
key="app-settings"
|
||||
title={$t('app_settings')}
|
||||
subtitle={$t('manage_the_app_settings')}
|
||||
>
|
||||
<AppSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion icon={mdiAccountOutline} key="account" title={$t('account')} subtitle={$t('manage_your_account')}>
|
||||
<UserProfileSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiServerOutline}
|
||||
key="user-usage-info"
|
||||
title={$t('user_usage_stats')}
|
||||
subtitle={$t('user_usage_stats_description')}
|
||||
>
|
||||
<UserUsageStatistic />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion icon={mdiApi} key="api-keys" title={$t('api_keys')} subtitle={$t('manage_your_api_keys')}>
|
||||
<UserApiKeyList bind:keys />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiDevices}
|
||||
key="authorized-devices"
|
||||
title={$t('authorized_devices')}
|
||||
subtitle={$t('manage_your_devices')}
|
||||
>
|
||||
<DeviceList bind:devices={sessions} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiDownload}
|
||||
key="download-settings"
|
||||
title={$t('download_settings')}
|
||||
subtitle={$t('download_settings_description')}
|
||||
>
|
||||
<DownloadSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiFeatureSearchOutline}
|
||||
key="feature"
|
||||
title={$t('features')}
|
||||
subtitle={$t('features_setting_description')}
|
||||
>
|
||||
<FeatureSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiBellOutline}
|
||||
key={OpenQueryParam.NOTIFICATIONS}
|
||||
title={$t('notifications')}
|
||||
subtitle={$t('notifications_setting_description')}
|
||||
>
|
||||
<NotificationsSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
{#if featureFlagsManager.value.oauth}
|
||||
<SettingAccordion
|
||||
icon={mdiCogOutline}
|
||||
key="app-settings"
|
||||
title={$t('app_settings')}
|
||||
subtitle={$t('manage_the_app_settings')}
|
||||
icon={mdiTwoFactorAuthentication}
|
||||
key={OpenQueryParam.OAUTH}
|
||||
title={$t('oauth')}
|
||||
subtitle={$t('manage_your_oauth_connection')}
|
||||
isOpen={oauthOpen || undefined}
|
||||
>
|
||||
<AppSettings />
|
||||
<OauthSettings />
|
||||
</SettingAccordion>
|
||||
{/if}
|
||||
|
||||
<SettingAccordion icon={mdiAccountOutline} key="account" title={$t('account')} subtitle={$t('manage_your_account')}>
|
||||
<UserProfileSettings />
|
||||
</SettingAccordion>
|
||||
<SettingAccordion
|
||||
icon={mdiFormTextboxPassword}
|
||||
key="password"
|
||||
title={$t('password')}
|
||||
subtitle={$t('change_your_password')}
|
||||
>
|
||||
<ChangePasswordSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiServerOutline}
|
||||
key="user-usage-info"
|
||||
title={$t('user_usage_stats')}
|
||||
subtitle={$t('user_usage_stats_description')}
|
||||
>
|
||||
<UserUsageStatistic />
|
||||
</SettingAccordion>
|
||||
<SettingAccordion
|
||||
icon={mdiAccountGroupOutline}
|
||||
key="partner-sharing"
|
||||
title={$t('partner_sharing')}
|
||||
subtitle={$t('manage_sharing_with_partners')}
|
||||
>
|
||||
<PartnerSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion icon={mdiApi} key="api-keys" title={$t('api_keys')} subtitle={$t('manage_your_api_keys')}>
|
||||
<UserApiKeyList bind:keys />
|
||||
</SettingAccordion>
|
||||
<SettingAccordion
|
||||
icon={mdiLockSmart}
|
||||
key="user-pin-code-settings"
|
||||
title={$t('user_pin_code_settings')}
|
||||
subtitle={$t('user_pin_code_settings_description')}
|
||||
autoScrollTo={true}
|
||||
>
|
||||
<ChangePinCodeSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiDevices}
|
||||
key="authorized-devices"
|
||||
title={$t('authorized_devices')}
|
||||
subtitle={$t('manage_your_devices')}
|
||||
>
|
||||
<DeviceList bind:devices={sessions} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiDownload}
|
||||
key="download-settings"
|
||||
title={$t('download_settings')}
|
||||
subtitle={$t('download_settings_description')}
|
||||
>
|
||||
<DownloadSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiFeatureSearchOutline}
|
||||
key="feature"
|
||||
title={$t('features')}
|
||||
subtitle={$t('features_setting_description')}
|
||||
>
|
||||
<FeatureSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiBellOutline}
|
||||
key={OpenQueryParam.NOTIFICATIONS}
|
||||
title={$t('notifications')}
|
||||
subtitle={$t('notifications_setting_description')}
|
||||
>
|
||||
<NotificationsSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
{#if featureFlagsManager.value.oauth}
|
||||
<SettingAccordion
|
||||
icon={mdiTwoFactorAuthentication}
|
||||
key={OpenQueryParam.OAUTH}
|
||||
title={$t('oauth')}
|
||||
subtitle={$t('manage_your_oauth_connection')}
|
||||
isOpen={oauthOpen || undefined}
|
||||
>
|
||||
<OauthSettings />
|
||||
</SettingAccordion>
|
||||
{/if}
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiFormTextboxPassword}
|
||||
key="password"
|
||||
title={$t('password')}
|
||||
subtitle={$t('change_your_password')}
|
||||
>
|
||||
<ChangePasswordSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiAccountGroupOutline}
|
||||
key="partner-sharing"
|
||||
title={$t('partner_sharing')}
|
||||
subtitle={$t('manage_sharing_with_partners')}
|
||||
>
|
||||
<PartnerSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiLockSmart}
|
||||
key="user-pin-code-settings"
|
||||
title={$t('user_pin_code_settings')}
|
||||
subtitle={$t('user_pin_code_settings_description')}
|
||||
autoScrollTo={true}
|
||||
>
|
||||
<ChangePinCodeSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiKeyOutline}
|
||||
key={OpenQueryParam.PURCHASE_SETTINGS}
|
||||
title={$t('user_purchase_settings')}
|
||||
subtitle={$t('user_purchase_settings_description')}
|
||||
autoScrollTo={true}
|
||||
>
|
||||
<UserPurchaseSettings />
|
||||
</SettingAccordion>
|
||||
</SettingAccordionState>
|
||||
<SettingAccordion
|
||||
icon={mdiKeyOutline}
|
||||
key={OpenQueryParam.PURCHASE_SETTINGS}
|
||||
title={$t('user_purchase_settings')}
|
||||
subtitle={$t('user_purchase_settings_description')}
|
||||
autoScrollTo={true}
|
||||
>
|
||||
<UserPurchaseSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script lang="ts">
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import MaintenanceBackupsList from '$lib/components/maintenance/MaintenanceBackupsList.svelte';
|
||||
import SettingAccordionState from '$lib/components/shared-components/settings/SettingAccordionState.svelte';
|
||||
import SettingAccordion from '$lib/components/shared-components/settings/SettingAccordion.svelte';
|
||||
import { QueryParameter } from '$lib/constants';
|
||||
import { getMaintenanceAdminActions } from '$lib/services/maintenance.service';
|
||||
import { mdiRefresh } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
@@ -20,16 +18,14 @@
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[StartMaintenance]}>
|
||||
<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">
|
||||
<SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
|
||||
<SettingAccordion
|
||||
title={$t('admin.maintenance_restore_database_backup')}
|
||||
subtitle={$t('admin.maintenance_restore_database_backup_description')}
|
||||
icon={mdiRefresh}
|
||||
key="backups"
|
||||
>
|
||||
<MaintenanceBackupsList backups={data.backups} expectedVersion={data.expectedVersion} />
|
||||
</SettingAccordion>
|
||||
</SettingAccordionState>
|
||||
<SettingAccordion
|
||||
title={$t('admin.maintenance_restore_database_backup')}
|
||||
subtitle={$t('admin.maintenance_restore_database_backup_description')}
|
||||
icon={mdiRefresh}
|
||||
key="backups"
|
||||
>
|
||||
<MaintenanceBackupsList backups={data.backups} expectedVersion={data.expectedVersion} />
|
||||
</SettingAccordion>
|
||||
</section>
|
||||
</section>
|
||||
</AdminPageLayout>
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
import TrashSettings from './TrashSettings.svelte';
|
||||
import UserSettings from './UserSettings.svelte';
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import SettingAccordionState from '$lib/components/shared-components/settings/SettingAccordionState.svelte';
|
||||
import SettingAccordion from '$lib/components/shared-components/settings/SettingAccordion.svelte';
|
||||
import { QueryParameter } from '$lib/constants';
|
||||
import SearchBar from '$lib/elements/SearchBar.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
@@ -215,12 +213,10 @@
|
||||
<div>
|
||||
<SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
|
||||
</div>
|
||||
<SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
|
||||
{#each filteredSettings as { component: Component, title, subtitle, key, icon } (key)}
|
||||
<SettingAccordion {title} {subtitle} {key} {icon}>
|
||||
<Component />
|
||||
</SettingAccordion>
|
||||
{/each}
|
||||
</SettingAccordionState>
|
||||
{#each filteredSettings as { component: Component, title, subtitle, key, icon } (key)}
|
||||
<SettingAccordion {title} {subtitle} {key} {icon}>
|
||||
<Component />
|
||||
</SettingAccordion>
|
||||
{/each}
|
||||
</Container>
|
||||
</AdminPageLayout>
|
||||
|
||||
Reference in New Issue
Block a user