mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
parent
7c2f7d6c51
commit
f55b3add80
242 changed files with 12794 additions and 13426 deletions
|
|
@ -1,80 +1,78 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api, ApiError } from '@api';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SettingInputField, {
|
||||
SettingInputFieldType
|
||||
} from '../admin-page/settings/setting-input-field.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api, ApiError } from '@api';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SettingInputField, { SettingInputFieldType } from '../admin-page/settings/setting-input-field.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
|
||||
let password = '';
|
||||
let newPassword = '';
|
||||
let confirmPassword = '';
|
||||
let password = '';
|
||||
let newPassword = '';
|
||||
let confirmPassword = '';
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
try {
|
||||
await api.authenticationApi.changePassword({
|
||||
changePasswordDto: {
|
||||
password,
|
||||
newPassword
|
||||
}
|
||||
});
|
||||
const handleChangePassword = async () => {
|
||||
try {
|
||||
await api.authenticationApi.changePassword({
|
||||
changePasswordDto: {
|
||||
password,
|
||||
newPassword,
|
||||
},
|
||||
});
|
||||
|
||||
notificationController.show({
|
||||
message: 'Updated password',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
notificationController.show({
|
||||
message: 'Updated password',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
|
||||
password = '';
|
||||
newPassword = '';
|
||||
confirmPassword = '';
|
||||
} catch (error) {
|
||||
console.error('Error [user-profile] [changePassword]', error);
|
||||
notificationController.show({
|
||||
message: (error as ApiError)?.response?.data?.message || 'Unable to change password',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
};
|
||||
password = '';
|
||||
newPassword = '';
|
||||
confirmPassword = '';
|
||||
} catch (error) {
|
||||
console.error('Error [user-profile] [changePassword]', error);
|
||||
notificationController.show({
|
||||
message: (error as ApiError)?.response?.data?.message || 'Unable to change password',
|
||||
type: NotificationType.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="my-4">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="flex flex-col gap-4 ml-4 mt-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.PASSWORD}
|
||||
label="PASSWORD"
|
||||
bind:value={password}
|
||||
required={true}
|
||||
/>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="flex flex-col gap-4 ml-4 mt-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.PASSWORD}
|
||||
label="PASSWORD"
|
||||
bind:value={password}
|
||||
required={true}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.PASSWORD}
|
||||
label="NEW PASSWORD"
|
||||
bind:value={newPassword}
|
||||
required={true}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.PASSWORD}
|
||||
label="NEW PASSWORD"
|
||||
bind:value={newPassword}
|
||||
required={true}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.PASSWORD}
|
||||
label="CONFIRM PASSWORD"
|
||||
bind:value={confirmPassword}
|
||||
required={true}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.PASSWORD}
|
||||
label="CONFIRM PASSWORD"
|
||||
bind:value={confirmPassword}
|
||||
required={true}
|
||||
/>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={!(password && newPassword && newPassword === confirmPassword)}
|
||||
on:click={() => handleChangePassword()}>Save</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={!(password && newPassword && newPassword === confirmPassword)}
|
||||
on:click={() => handleChangePassword()}>Save</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,72 +1,70 @@
|
|||
<script lang="ts">
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { AuthDeviceResponseDto } from '@api';
|
||||
import { DateTime, ToRelativeCalendarOptions } from 'luxon';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Android from 'svelte-material-icons/Android.svelte';
|
||||
import Apple from 'svelte-material-icons/Apple.svelte';
|
||||
import AppleSafari from 'svelte-material-icons/AppleSafari.svelte';
|
||||
import GoogleChrome from 'svelte-material-icons/GoogleChrome.svelte';
|
||||
import Help from 'svelte-material-icons/Help.svelte';
|
||||
import Linux from 'svelte-material-icons/Linux.svelte';
|
||||
import MicrosoftWindows from 'svelte-material-icons/MicrosoftWindows.svelte';
|
||||
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { AuthDeviceResponseDto } from '@api';
|
||||
import { DateTime, ToRelativeCalendarOptions } from 'luxon';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Android from 'svelte-material-icons/Android.svelte';
|
||||
import Apple from 'svelte-material-icons/Apple.svelte';
|
||||
import AppleSafari from 'svelte-material-icons/AppleSafari.svelte';
|
||||
import GoogleChrome from 'svelte-material-icons/GoogleChrome.svelte';
|
||||
import Help from 'svelte-material-icons/Help.svelte';
|
||||
import Linux from 'svelte-material-icons/Linux.svelte';
|
||||
import MicrosoftWindows from 'svelte-material-icons/MicrosoftWindows.svelte';
|
||||
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||
|
||||
export let device: AuthDeviceResponseDto;
|
||||
export let device: AuthDeviceResponseDto;
|
||||
|
||||
const dispatcher = createEventDispatcher();
|
||||
const dispatcher = createEventDispatcher();
|
||||
|
||||
const options: ToRelativeCalendarOptions = {
|
||||
unit: 'days',
|
||||
locale: $locale
|
||||
};
|
||||
const options: ToRelativeCalendarOptions = {
|
||||
unit: 'days',
|
||||
locale: $locale,
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row w-full">
|
||||
<!-- TODO: Device Image -->
|
||||
<div
|
||||
class="hidden sm:flex pr-2 justify-center items-center text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
{#if device.deviceOS === 'Android'}
|
||||
<Android size="40" />
|
||||
{:else if device.deviceOS === 'iOS' || device.deviceOS === 'Mac OS'}
|
||||
<Apple size="40" />
|
||||
{:else if device.deviceOS.indexOf('Safari') !== -1}
|
||||
<AppleSafari size="40" />
|
||||
{:else if device.deviceOS.indexOf('Windows') !== -1}
|
||||
<MicrosoftWindows size="40" />
|
||||
{:else if device.deviceOS === 'Linux'}
|
||||
<Linux size="40" />
|
||||
{:else if device.deviceOS === 'Chromium OS' || device.deviceType === 'Chrome' || device.deviceType === 'Chromium'}
|
||||
<GoogleChrome size="40" />
|
||||
{:else}
|
||||
<Help size="40" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="pl-4 sm:pl-0 flex flex-row grow justify-between gap-1">
|
||||
<div class="flex flex-col gap-1 justify-center dark:text-white">
|
||||
<span class="text-sm">
|
||||
{#if device.deviceType || device.deviceOS}
|
||||
<span>{device.deviceOS || 'Unknown'} • {device.deviceType || 'Unknown'}</span>
|
||||
{:else}
|
||||
<span>Unknown</span>
|
||||
{/if}
|
||||
</span>
|
||||
<div class="text-sm">
|
||||
<span class="">Last seen</span>
|
||||
<span>{DateTime.fromISO(device.updatedAt).toRelativeCalendar(options)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{#if !device.current}
|
||||
<div class="text-sm flex flex-col justify-center">
|
||||
<button
|
||||
on:click={() => dispatcher('delete')}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
title="Log out"
|
||||
>
|
||||
<TrashCanOutline size="16" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- TODO: Device Image -->
|
||||
<div class="hidden sm:flex pr-2 justify-center items-center text-immich-primary dark:text-immich-dark-primary">
|
||||
{#if device.deviceOS === 'Android'}
|
||||
<Android size="40" />
|
||||
{:else if device.deviceOS === 'iOS' || device.deviceOS === 'Mac OS'}
|
||||
<Apple size="40" />
|
||||
{:else if device.deviceOS.indexOf('Safari') !== -1}
|
||||
<AppleSafari size="40" />
|
||||
{:else if device.deviceOS.indexOf('Windows') !== -1}
|
||||
<MicrosoftWindows size="40" />
|
||||
{:else if device.deviceOS === 'Linux'}
|
||||
<Linux size="40" />
|
||||
{:else if device.deviceOS === 'Chromium OS' || device.deviceType === 'Chrome' || device.deviceType === 'Chromium'}
|
||||
<GoogleChrome size="40" />
|
||||
{:else}
|
||||
<Help size="40" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="pl-4 sm:pl-0 flex flex-row grow justify-between gap-1">
|
||||
<div class="flex flex-col gap-1 justify-center dark:text-white">
|
||||
<span class="text-sm">
|
||||
{#if device.deviceType || device.deviceOS}
|
||||
<span>{device.deviceOS || 'Unknown'} • {device.deviceType || 'Unknown'}</span>
|
||||
{:else}
|
||||
<span>Unknown</span>
|
||||
{/if}
|
||||
</span>
|
||||
<div class="text-sm">
|
||||
<span class="">Last seen</span>
|
||||
<span>{DateTime.fromISO(device.updatedAt).toRelativeCalendar(options)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{#if !device.current}
|
||||
<div class="text-sm flex flex-col justify-center">
|
||||
<button
|
||||
on:click={() => dispatcher('delete')}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
title="Log out"
|
||||
>
|
||||
<TrashCanOutline size="16" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,102 +1,93 @@
|
|||
<script lang="ts">
|
||||
import { api, AuthDeviceResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
import DeviceCard from './device-card.svelte';
|
||||
import { api, AuthDeviceResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
|
||||
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||
import DeviceCard from './device-card.svelte';
|
||||
|
||||
let devices: AuthDeviceResponseDto[] = [];
|
||||
let deleteDevice: AuthDeviceResponseDto | null = null;
|
||||
let deleteAll = false;
|
||||
let devices: AuthDeviceResponseDto[] = [];
|
||||
let deleteDevice: AuthDeviceResponseDto | null = null;
|
||||
let deleteAll = false;
|
||||
|
||||
const refresh = () => api.authenticationApi.getAuthDevices().then(({ data }) => (devices = data));
|
||||
const refresh = () => api.authenticationApi.getAuthDevices().then(({ data }) => (devices = data));
|
||||
|
||||
onMount(() => {
|
||||
refresh();
|
||||
});
|
||||
onMount(() => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
$: currentDevice = devices.find((device) => device.current);
|
||||
$: otherDevices = devices.filter((device) => !device.current);
|
||||
$: currentDevice = devices.find((device) => device.current);
|
||||
$: otherDevices = devices.filter((device) => !device.current);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteDevice) {
|
||||
return;
|
||||
}
|
||||
const handleDelete = async () => {
|
||||
if (!deleteDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.authenticationApi.logoutAuthDevice({ id: deleteDevice.id });
|
||||
notificationController.show({ message: `Logged out device`, type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to log out device');
|
||||
} finally {
|
||||
await refresh();
|
||||
deleteDevice = null;
|
||||
}
|
||||
};
|
||||
try {
|
||||
await api.authenticationApi.logoutAuthDevice({ id: deleteDevice.id });
|
||||
notificationController.show({ message: `Logged out device`, type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to log out device');
|
||||
} finally {
|
||||
await refresh();
|
||||
deleteDevice = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAll = async () => {
|
||||
try {
|
||||
await api.authenticationApi.logoutAuthDevices();
|
||||
notificationController.show({
|
||||
message: `Logged out all devices`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to log out all devices');
|
||||
} finally {
|
||||
await refresh();
|
||||
deleteAll = false;
|
||||
}
|
||||
};
|
||||
const handleDeleteAll = async () => {
|
||||
try {
|
||||
await api.authenticationApi.logoutAuthDevices();
|
||||
notificationController.show({
|
||||
message: `Logged out all devices`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to log out all devices');
|
||||
} finally {
|
||||
await refresh();
|
||||
deleteAll = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if deleteDevice}
|
||||
<ConfirmDialogue
|
||||
prompt="Are you sure you want to log out this device?"
|
||||
on:confirm={() => handleDelete()}
|
||||
on:cancel={() => (deleteDevice = null)}
|
||||
/>
|
||||
<ConfirmDialogue
|
||||
prompt="Are you sure you want to log out this device?"
|
||||
on:confirm={() => handleDelete()}
|
||||
on:cancel={() => (deleteDevice = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if deleteAll}
|
||||
<ConfirmDialogue
|
||||
prompt="Are you sure you want to log out all devices?"
|
||||
on:confirm={() => handleDeleteAll()}
|
||||
on:cancel={() => (deleteAll = false)}
|
||||
/>
|
||||
<ConfirmDialogue
|
||||
prompt="Are you sure you want to log out all devices?"
|
||||
on:confirm={() => handleDeleteAll()}
|
||||
on:cancel={() => (deleteAll = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<section class="my-4">
|
||||
{#if currentDevice}
|
||||
<div class="mb-6">
|
||||
<h3 class="font-medium text-xs mb-2 text-immich-primary dark:text-immich-dark-primary">
|
||||
CURRENT DEVICE
|
||||
</h3>
|
||||
<DeviceCard device={currentDevice} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if otherDevices.length > 0}
|
||||
<div class="mb-6">
|
||||
<h3 class="font-medium text-xs mb-2 text-immich-primary dark:text-immich-dark-primary">
|
||||
OTHER DEVICES
|
||||
</h3>
|
||||
{#each otherDevices as device, i}
|
||||
<DeviceCard {device} on:delete={() => (deleteDevice = device)} />
|
||||
{#if i !== otherDevices.length - 1}
|
||||
<hr class="my-3" />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<h3 class="font-medium text-xs mb-2 text-immich-primary dark:text-immich-dark-primary">
|
||||
LOG OUT ALL DEVICES
|
||||
</h3>
|
||||
<div class="flex justify-end">
|
||||
<Button color="red" size="sm" on:click={() => (deleteAll = true)}>Log Out All Devices</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if currentDevice}
|
||||
<div class="mb-6">
|
||||
<h3 class="font-medium text-xs mb-2 text-immich-primary dark:text-immich-dark-primary">CURRENT DEVICE</h3>
|
||||
<DeviceCard device={currentDevice} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if otherDevices.length > 0}
|
||||
<div class="mb-6">
|
||||
<h3 class="font-medium text-xs mb-2 text-immich-primary dark:text-immich-dark-primary">OTHER DEVICES</h3>
|
||||
{#each otherDevices as device, i}
|
||||
<DeviceCard {device} on:delete={() => (deleteDevice = device)} />
|
||||
{#if i !== otherDevices.length - 1}
|
||||
<hr class="my-3" />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<h3 class="font-medium text-xs mb-2 text-immich-primary dark:text-immich-dark-primary">LOG OUT ALL DEVICES</h3>
|
||||
<div class="flex justify-end">
|
||||
<Button color="red" size="sm" on:click={() => (deleteAll = true)}>Log Out All Devices</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,80 +1,77 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { oauth, OAuthConfigResponseDto, UserResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { oauth, OAuthConfigResponseDto, UserResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
export let user: UserResponseDto;
|
||||
|
||||
let config: OAuthConfigResponseDto = { enabled: false, passwordLoginEnabled: true };
|
||||
let loading = true;
|
||||
let config: OAuthConfigResponseDto = { enabled: false, passwordLoginEnabled: true };
|
||||
let loading = true;
|
||||
|
||||
onMount(async () => {
|
||||
if (oauth.isCallback(window.location)) {
|
||||
try {
|
||||
loading = true;
|
||||
onMount(async () => {
|
||||
if (oauth.isCallback(window.location)) {
|
||||
try {
|
||||
loading = true;
|
||||
|
||||
const { data } = await oauth.link(window.location);
|
||||
user = data;
|
||||
const { data } = await oauth.link(window.location);
|
||||
user = data;
|
||||
|
||||
notificationController.show({
|
||||
message: 'Linked OAuth account',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to link OAuth account');
|
||||
} finally {
|
||||
goto('?open=oauth');
|
||||
}
|
||||
}
|
||||
notificationController.show({
|
||||
message: 'Linked OAuth account',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to link OAuth account');
|
||||
} finally {
|
||||
goto('?open=oauth');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await oauth.getConfig(window.location);
|
||||
config = data;
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to load OAuth config');
|
||||
}
|
||||
try {
|
||||
const { data } = await oauth.getConfig(window.location);
|
||||
config = data;
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to load OAuth config');
|
||||
}
|
||||
|
||||
loading = false;
|
||||
});
|
||||
loading = false;
|
||||
});
|
||||
|
||||
const handleUnlink = async () => {
|
||||
try {
|
||||
const { data } = await oauth.unlink();
|
||||
user = data;
|
||||
notificationController.show({
|
||||
message: 'Unlinked OAuth account',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to unlink account');
|
||||
}
|
||||
};
|
||||
const handleUnlink = async () => {
|
||||
try {
|
||||
const { data } = await oauth.unlink();
|
||||
user = data;
|
||||
notificationController.show({
|
||||
message: 'Unlinked OAuth account',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to unlink account');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="my-4">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<div class="flex justify-end">
|
||||
{#if loading}
|
||||
<div class="flex place-items-center place-content-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{:else if config.enabled}
|
||||
{#if user.oauthId}
|
||||
<Button size="sm" on:click={() => handleUnlink()}>Unlink Oauth</Button>
|
||||
{:else}
|
||||
<a href={config.url}>
|
||||
<Button size="sm" on:click={() => handleUnlink()}>Link to OAuth</Button>
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<div class="flex justify-end">
|
||||
{#if loading}
|
||||
<div class="flex place-items-center place-content-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{:else if config.enabled}
|
||||
{#if user.oauthId}
|
||||
<Button size="sm" on:click={() => handleUnlink()}>Unlink Oauth</Button>
|
||||
{:else}
|
||||
<a href={config.url}>
|
||||
<Button size="sm" on:click={() => handleUnlink()}>Link to OAuth</Button>
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,87 +1,85 @@
|
|||
<script lang="ts">
|
||||
import { api, UserResponseDto } from '@api';
|
||||
import BaseModal from '../shared-components/base-modal.svelte';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { api, UserResponseDto } from '@api';
|
||||
import BaseModal from '../shared-components/base-modal.svelte';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
export let user: UserResponseDto;
|
||||
|
||||
let availableUsers: UserResponseDto[] = [];
|
||||
let selectedUsers: UserResponseDto[] = [];
|
||||
let availableUsers: UserResponseDto[] = [];
|
||||
let selectedUsers: UserResponseDto[] = [];
|
||||
|
||||
const dispatch = createEventDispatcher<{ close: void; 'add-users': UserResponseDto[] }>();
|
||||
const dispatch = createEventDispatcher<{ close: void; 'add-users': UserResponseDto[] }>();
|
||||
|
||||
onMount(async () => {
|
||||
// TODO: update endpoint to have a query param for deleted users
|
||||
let { data: users } = await api.userApi.getAllUsers({ isAll: false });
|
||||
onMount(async () => {
|
||||
// TODO: update endpoint to have a query param for deleted users
|
||||
let { data: users } = await api.userApi.getAllUsers({ isAll: false });
|
||||
|
||||
// remove invalid users
|
||||
users = users.filter((_user) => !(_user.deletedAt || _user.id === user.id));
|
||||
// remove invalid users
|
||||
users = users.filter((_user) => !(_user.deletedAt || _user.id === user.id));
|
||||
|
||||
// exclude partners from the list of users available for selection
|
||||
const { data: partners } = await api.partnerApi.getPartners({ direction: 'shared-by' });
|
||||
const partnerIds = partners.map((partner) => partner.id);
|
||||
availableUsers = users.filter((user) => !partnerIds.includes(user.id));
|
||||
});
|
||||
// exclude partners from the list of users available for selection
|
||||
const { data: partners } = await api.partnerApi.getPartners({ direction: 'shared-by' });
|
||||
const partnerIds = partners.map((partner) => partner.id);
|
||||
availableUsers = users.filter((user) => !partnerIds.includes(user.id));
|
||||
});
|
||||
|
||||
const selectUser = (user: UserResponseDto) => {
|
||||
if (selectedUsers.includes(user)) {
|
||||
selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id);
|
||||
} else {
|
||||
selectedUsers = [...selectedUsers, user];
|
||||
}
|
||||
};
|
||||
const selectUser = (user: UserResponseDto) => {
|
||||
if (selectedUsers.includes(user)) {
|
||||
selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id);
|
||||
} else {
|
||||
selectedUsers = [...selectedUsers, user];
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<BaseModal on:close={() => dispatch('close')}>
|
||||
<svelte:fragment slot="title">
|
||||
<span class="flex gap-2 place-items-center">
|
||||
<ImmichLogo width={24} />
|
||||
<p class="font-medium">Add partner</p>
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">
|
||||
<span class="flex gap-2 place-items-center">
|
||||
<ImmichLogo width={24} />
|
||||
<p class="font-medium">Add partner</p>
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="max-h-[300px] overflow-y-auto immich-scrollbar">
|
||||
{#if availableUsers.length > 0}
|
||||
{#each availableUsers as user}
|
||||
<button
|
||||
on:click={() => selectUser(user)}
|
||||
class="w-full flex place-items-center gap-4 py-4 px-5 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all"
|
||||
>
|
||||
{#if selectedUsers.includes(user)}
|
||||
<span
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-bg rounded-full w-12 h-12 border flex place-items-center place-content-center text-3xl dark:border-immich-dark-gray"
|
||||
>✓</span
|
||||
>
|
||||
{:else}
|
||||
<UserAvatar {user} size="md" autoColor />
|
||||
{/if}
|
||||
<div class="max-h-[300px] overflow-y-auto immich-scrollbar">
|
||||
{#if availableUsers.length > 0}
|
||||
{#each availableUsers as user}
|
||||
<button
|
||||
on:click={() => selectUser(user)}
|
||||
class="w-full flex place-items-center gap-4 py-4 px-5 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all"
|
||||
>
|
||||
{#if selectedUsers.includes(user)}
|
||||
<span
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-bg rounded-full w-12 h-12 border flex place-items-center place-content-center text-3xl dark:border-immich-dark-gray"
|
||||
>✓</span
|
||||
>
|
||||
{:else}
|
||||
<UserAvatar {user} size="md" autoColor />
|
||||
{/if}
|
||||
|
||||
<div class="text-left">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{user.firstName}
|
||||
{user.lastName}
|
||||
</p>
|
||||
<p class="text-xs">
|
||||
{user.email}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="text-sm p-5">
|
||||
Looks like you shared your photos with all users or you don't have any user to share with.
|
||||
</p>
|
||||
{/if}
|
||||
<div class="text-left">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{user.firstName}
|
||||
{user.lastName}
|
||||
</p>
|
||||
<p class="text-xs">
|
||||
{user.email}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="text-sm p-5">
|
||||
Looks like you shared your photos with all users or you don't have any user to share with.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if selectedUsers.length > 0}
|
||||
<div class="flex place-content-end p-5">
|
||||
<Button size="sm" rounded="lg" on:click={() => dispatch('add-users', selectedUsers)}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if selectedUsers.length > 0}
|
||||
<div class="flex place-content-end p-5">
|
||||
<Button size="sm" rounded="lg" on:click={() => dispatch('add-users', selectedUsers)}>Add</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</BaseModal>
|
||||
|
|
|
|||
|
|
@ -1,100 +1,100 @@
|
|||
<script lang="ts">
|
||||
import { UserResponseDto, api } from '@api';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import PartnerSelectionModal from './partner-selection-modal.svelte';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import { onMount } from 'svelte';
|
||||
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import { UserResponseDto, api } from '@api';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import PartnerSelectionModal from './partner-selection-modal.svelte';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import { onMount } from 'svelte';
|
||||
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
export let user: UserResponseDto;
|
||||
|
||||
let partners: UserResponseDto[] = [];
|
||||
let createPartner = false;
|
||||
let removePartner: UserResponseDto | null = null;
|
||||
let partners: UserResponseDto[] = [];
|
||||
let createPartner = false;
|
||||
let removePartner: UserResponseDto | null = null;
|
||||
|
||||
const refreshPartners = async () => {
|
||||
const { data } = await api.partnerApi.getPartners({ direction: 'shared-by' });
|
||||
partners = data;
|
||||
};
|
||||
const refreshPartners = async () => {
|
||||
const { data } = await api.partnerApi.getPartners({ direction: 'shared-by' });
|
||||
partners = data;
|
||||
};
|
||||
|
||||
const handleRemovePartner = async () => {
|
||||
if (!removePartner) {
|
||||
return;
|
||||
}
|
||||
const handleRemovePartner = async () => {
|
||||
if (!removePartner) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.partnerApi.removePartner({ id: removePartner.id });
|
||||
removePartner = null;
|
||||
await refreshPartners();
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to remove partner');
|
||||
}
|
||||
};
|
||||
try {
|
||||
await api.partnerApi.removePartner({ id: removePartner.id });
|
||||
removePartner = null;
|
||||
await refreshPartners();
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to remove partner');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreatePartners = async (users: UserResponseDto[]) => {
|
||||
try {
|
||||
for (const user of users) {
|
||||
await api.partnerApi.createPartner({ id: user.id });
|
||||
}
|
||||
const handleCreatePartners = async (users: UserResponseDto[]) => {
|
||||
try {
|
||||
for (const user of users) {
|
||||
await api.partnerApi.createPartner({ id: user.id });
|
||||
}
|
||||
|
||||
await refreshPartners();
|
||||
createPartner = false;
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to add partners');
|
||||
}
|
||||
};
|
||||
await refreshPartners();
|
||||
createPartner = false;
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to add partners');
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await refreshPartners();
|
||||
});
|
||||
onMount(async () => {
|
||||
await refreshPartners();
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="my-4">
|
||||
{#if partners.length > 0}
|
||||
<div class="flex flex-row gap-4">
|
||||
{#each partners as partner (partner.id)}
|
||||
<div class="flex rounded-lg gap-4 py-4 px-5 transition-all">
|
||||
<UserAvatar user={partner} size="md" autoColor />
|
||||
<div class="text-left">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{partner.firstName}
|
||||
{partner.lastName}
|
||||
</p>
|
||||
<p class="text-xs text-immich-fg/75 dark:text-immich-dark-fg/75">
|
||||
{partner.email}
|
||||
</p>
|
||||
</div>
|
||||
<CircleIconButton
|
||||
on:click={() => (removePartner = partner)}
|
||||
logo={Close}
|
||||
size={'16'}
|
||||
title="Remove partner"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex justify-end">
|
||||
<Button size="sm" on:click={() => (createPartner = true)}>Add partner</Button>
|
||||
</div>
|
||||
{#if partners.length > 0}
|
||||
<div class="flex flex-row gap-4">
|
||||
{#each partners as partner (partner.id)}
|
||||
<div class="flex rounded-lg gap-4 py-4 px-5 transition-all">
|
||||
<UserAvatar user={partner} size="md" autoColor />
|
||||
<div class="text-left">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{partner.firstName}
|
||||
{partner.lastName}
|
||||
</p>
|
||||
<p class="text-xs text-immich-fg/75 dark:text-immich-dark-fg/75">
|
||||
{partner.email}
|
||||
</p>
|
||||
</div>
|
||||
<CircleIconButton
|
||||
on:click={() => (removePartner = partner)}
|
||||
logo={Close}
|
||||
size={'16'}
|
||||
title="Remove partner"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex justify-end">
|
||||
<Button size="sm" on:click={() => (createPartner = true)}>Add partner</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if createPartner}
|
||||
<PartnerSelectionModal
|
||||
{user}
|
||||
on:close={() => (createPartner = false)}
|
||||
on:add-users={(event) => handleCreatePartners(event.detail)}
|
||||
/>
|
||||
<PartnerSelectionModal
|
||||
{user}
|
||||
on:close={() => (createPartner = false)}
|
||||
on:add-users={(event) => handleCreatePartners(event.detail)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if removePartner}
|
||||
<ConfirmDialogue
|
||||
title="Stop sharing your photos?"
|
||||
prompt="{removePartner.firstName} will no longer be able to access your photos."
|
||||
on:cancel={() => (removePartner = null)}
|
||||
on:confirm={() => handleRemovePartner()}
|
||||
/>
|
||||
<ConfirmDialogue
|
||||
title="Stop sharing your photos?"
|
||||
prompt="{removePartner.firstName} will no longer be able to access your photos."
|
||||
on:cancel={() => (removePartner = null)}
|
||||
on:confirm={() => handleRemovePartner()}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,177 +1,167 @@
|
|||
<script lang="ts">
|
||||
import { api, APIKeyResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
|
||||
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import APIKeyForm from '../forms/api-key-form.svelte';
|
||||
import APIKeySecret from '../forms/api-key-secret.svelte';
|
||||
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import { api, APIKeyResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
|
||||
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import APIKeyForm from '../forms/api-key-form.svelte';
|
||||
import APIKeySecret from '../forms/api-key-secret.svelte';
|
||||
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
|
||||
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
|
||||
let keys: APIKeyResponseDto[] = [];
|
||||
let keys: APIKeyResponseDto[] = [];
|
||||
|
||||
let newKey: Partial<APIKeyResponseDto> | null = null;
|
||||
let editKey: APIKeyResponseDto | null = null;
|
||||
let deleteKey: APIKeyResponseDto | null = null;
|
||||
let secret = '';
|
||||
let newKey: Partial<APIKeyResponseDto> | null = null;
|
||||
let editKey: APIKeyResponseDto | null = null;
|
||||
let deleteKey: APIKeyResponseDto | null = null;
|
||||
let secret = '';
|
||||
|
||||
const format: Intl.DateTimeFormatOptions = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
};
|
||||
const format: Intl.DateTimeFormatOptions = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
refreshKeys();
|
||||
});
|
||||
onMount(() => {
|
||||
refreshKeys();
|
||||
});
|
||||
|
||||
async function refreshKeys() {
|
||||
const { data } = await api.keyApi.getKeys();
|
||||
keys = data;
|
||||
}
|
||||
async function refreshKeys() {
|
||||
const { data } = await api.keyApi.getKeys();
|
||||
keys = data;
|
||||
}
|
||||
|
||||
const handleCreate = async (event: CustomEvent<APIKeyResponseDto>) => {
|
||||
try {
|
||||
const dto = event.detail;
|
||||
const { data } = await api.keyApi.createKey({ aPIKeyCreateDto: dto });
|
||||
secret = data.secret;
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to create a new API Key');
|
||||
} finally {
|
||||
await refreshKeys();
|
||||
newKey = null;
|
||||
}
|
||||
};
|
||||
const handleCreate = async (event: CustomEvent<APIKeyResponseDto>) => {
|
||||
try {
|
||||
const dto = event.detail;
|
||||
const { data } = await api.keyApi.createKey({ aPIKeyCreateDto: dto });
|
||||
secret = data.secret;
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to create a new API Key');
|
||||
} finally {
|
||||
await refreshKeys();
|
||||
newKey = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = async (event: CustomEvent<APIKeyResponseDto>) => {
|
||||
if (!editKey) {
|
||||
return;
|
||||
}
|
||||
const handleUpdate = async (event: CustomEvent<APIKeyResponseDto>) => {
|
||||
if (!editKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dto = event.detail;
|
||||
const dto = event.detail;
|
||||
|
||||
try {
|
||||
await api.keyApi.updateKey({ id: editKey.id, aPIKeyUpdateDto: { name: dto.name } });
|
||||
notificationController.show({
|
||||
message: `Saved API Key`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save API Key');
|
||||
} finally {
|
||||
await refreshKeys();
|
||||
editKey = null;
|
||||
}
|
||||
};
|
||||
try {
|
||||
await api.keyApi.updateKey({ id: editKey.id, aPIKeyUpdateDto: { name: dto.name } });
|
||||
notificationController.show({
|
||||
message: `Saved API Key`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save API Key');
|
||||
} finally {
|
||||
await refreshKeys();
|
||||
editKey = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteKey) {
|
||||
return;
|
||||
}
|
||||
const handleDelete = async () => {
|
||||
if (!deleteKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.keyApi.deleteKey({ id: deleteKey.id });
|
||||
notificationController.show({
|
||||
message: `Removed API Key: ${deleteKey.name}`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to remove API Key');
|
||||
} finally {
|
||||
await refreshKeys();
|
||||
deleteKey = null;
|
||||
}
|
||||
};
|
||||
try {
|
||||
await api.keyApi.deleteKey({ id: deleteKey.id });
|
||||
notificationController.show({
|
||||
message: `Removed API Key: ${deleteKey.name}`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to remove API Key');
|
||||
} finally {
|
||||
await refreshKeys();
|
||||
deleteKey = null;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if newKey}
|
||||
<APIKeyForm
|
||||
title="New API Key"
|
||||
submitText="Create"
|
||||
apiKey={newKey}
|
||||
on:submit={handleCreate}
|
||||
on:cancel={() => (newKey = null)}
|
||||
/>
|
||||
<APIKeyForm
|
||||
title="New API Key"
|
||||
submitText="Create"
|
||||
apiKey={newKey}
|
||||
on:submit={handleCreate}
|
||||
on:cancel={() => (newKey = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if secret}
|
||||
<APIKeySecret {secret} on:done={() => (secret = '')} />
|
||||
<APIKeySecret {secret} on:done={() => (secret = '')} />
|
||||
{/if}
|
||||
|
||||
{#if editKey}
|
||||
<APIKeyForm
|
||||
submitText="Save"
|
||||
apiKey={editKey}
|
||||
on:submit={handleUpdate}
|
||||
on:cancel={() => (editKey = null)}
|
||||
/>
|
||||
<APIKeyForm submitText="Save" apiKey={editKey} on:submit={handleUpdate} on:cancel={() => (editKey = null)} />
|
||||
{/if}
|
||||
|
||||
{#if deleteKey}
|
||||
<ConfirmDialogue
|
||||
prompt="Are you sure you want to delete this API Key?"
|
||||
on:confirm={() => handleDelete()}
|
||||
on:cancel={() => (deleteKey = null)}
|
||||
/>
|
||||
<ConfirmDialogue
|
||||
prompt="Are you sure you want to delete this API Key?"
|
||||
on:confirm={() => handleDelete()}
|
||||
on:cancel={() => (deleteKey = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<section class="my-4">
|
||||
<div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
|
||||
<div class="flex justify-end mb-2">
|
||||
<Button size="sm" on:click={() => (newKey = { name: 'API Key' })}>New API Key</Button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
|
||||
<div class="flex justify-end mb-2">
|
||||
<Button size="sm" on:click={() => (newKey = { name: 'API Key' })}>New API Key</Button>
|
||||
</div>
|
||||
|
||||
{#if keys.length > 0}
|
||||
<table class="text-left w-full">
|
||||
<thead
|
||||
class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full h-12 dark:bg-immich-dark-gray dark:text-immich-dark-primary dark:border-immich-dark-gray"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="text-center w-1/3 font-medium text-sm">Name</th>
|
||||
<th class="text-center w-1/3 font-medium text-sm">Created</th>
|
||||
<th class="text-center w-1/3 font-medium text-sm">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="overflow-y-auto rounded-md w-full block border dark:border-immich-dark-gray">
|
||||
{#each keys as key, i}
|
||||
{#key key.id}
|
||||
<tr
|
||||
class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-fg ${
|
||||
i % 2 == 0
|
||||
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
|
||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
>
|
||||
<td class="text-sm px-4 w-1/3 text-ellipsis">{key.name}</td>
|
||||
<td class="text-sm px-4 w-1/3 text-ellipsis"
|
||||
>{new Date(key.createdAt).toLocaleDateString($locale, format)}
|
||||
</td>
|
||||
<td class="text-sm px-4 w-1/3 text-ellipsis">
|
||||
<button
|
||||
on:click={() => (editKey = key)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<PencilOutline size="16" />
|
||||
</button>
|
||||
<button
|
||||
on:click={() => (deleteKey = key)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<TrashCanOutline size="16" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
{#if keys.length > 0}
|
||||
<table class="text-left w-full">
|
||||
<thead
|
||||
class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full h-12 dark:bg-immich-dark-gray dark:text-immich-dark-primary dark:border-immich-dark-gray"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="text-center w-1/3 font-medium text-sm">Name</th>
|
||||
<th class="text-center w-1/3 font-medium text-sm">Created</th>
|
||||
<th class="text-center w-1/3 font-medium text-sm">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="overflow-y-auto rounded-md w-full block border dark:border-immich-dark-gray">
|
||||
{#each keys as key, i}
|
||||
{#key key.id}
|
||||
<tr
|
||||
class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-fg ${
|
||||
i % 2 == 0 ? 'bg-immich-gray dark:bg-immich-dark-gray/75' : 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
>
|
||||
<td class="text-sm px-4 w-1/3 text-ellipsis">{key.name}</td>
|
||||
<td class="text-sm px-4 w-1/3 text-ellipsis"
|
||||
>{new Date(key.createdAt).toLocaleDateString($locale, format)}
|
||||
</td>
|
||||
<td class="text-sm px-4 w-1/3 text-ellipsis">
|
||||
<button
|
||||
on:click={() => (editKey = key)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<PencilOutline size="16" />
|
||||
</button>
|
||||
<button
|
||||
on:click={() => (deleteKey = key)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<TrashCanOutline size="16" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,92 +1,86 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api, UserResponseDto } from '@api';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import SettingInputField, {
|
||||
SettingInputFieldType
|
||||
} from '../admin-page/settings/setting-input-field.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api, UserResponseDto } from '@api';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import SettingInputField, { SettingInputFieldType } from '../admin-page/settings/setting-input-field.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
export let user: UserResponseDto;
|
||||
|
||||
const handleSaveProfile = async () => {
|
||||
try {
|
||||
const { data } = await api.userApi.updateUser({
|
||||
updateUserDto: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName
|
||||
}
|
||||
});
|
||||
const handleSaveProfile = async () => {
|
||||
try {
|
||||
const { data } = await api.userApi.updateUser({
|
||||
updateUserDto: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
},
|
||||
});
|
||||
|
||||
Object.assign(user, data);
|
||||
Object.assign(user, data);
|
||||
|
||||
notificationController.show({
|
||||
message: 'Saved profile',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save profile');
|
||||
}
|
||||
};
|
||||
notificationController.show({
|
||||
message: 'Saved profile',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save profile');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="my-4">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="flex flex-col gap-4 ml-4 mt-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="USER ID"
|
||||
bind:value={user.id}
|
||||
disabled={true}
|
||||
/>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="flex flex-col gap-4 ml-4 mt-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="USER ID"
|
||||
bind:value={user.id}
|
||||
disabled={true}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.EMAIL}
|
||||
label="EMAIL"
|
||||
bind:value={user.email}
|
||||
/>
|
||||
<SettingInputField inputType={SettingInputFieldType.EMAIL} label="EMAIL" bind:value={user.email} />
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="FIRST NAME"
|
||||
bind:value={user.firstName}
|
||||
required={true}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="FIRST NAME"
|
||||
bind:value={user.firstName}
|
||||
required={true}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="LAST NAME"
|
||||
bind:value={user.lastName}
|
||||
required={true}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="LAST NAME"
|
||||
bind:value={user.lastName}
|
||||
required={true}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="STORAGE LABEL"
|
||||
disabled={true}
|
||||
value={user.storageLabel || ''}
|
||||
required={false}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="STORAGE LABEL"
|
||||
disabled={true}
|
||||
value={user.storageLabel || ''}
|
||||
required={false}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="EXTERNAL PATH"
|
||||
disabled={true}
|
||||
value={user.externalPath || ''}
|
||||
required={false}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="EXTERNAL PATH"
|
||||
disabled={true}
|
||||
value={user.externalPath || ''}
|
||||
required={false}
|
||||
/>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button type="submit" size="sm" on:click={() => handleSaveProfile()}>Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<Button type="submit" size="sm" on:click={() => handleSaveProfile()}>Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,58 +1,58 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { oauth, UserResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import SettingAccordion from '../admin-page/settings/setting-accordion.svelte';
|
||||
import ChangePasswordSettings from './change-password-settings.svelte';
|
||||
import OAuthSettings from './oauth-settings.svelte';
|
||||
import UserAPIKeyList from './user-api-key-list.svelte';
|
||||
import DeviceList from './device-list.svelte';
|
||||
import PartnerSettings from './partner-settings.svelte';
|
||||
import UserProfileSettings from './user-profile-settings.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { oauth, UserResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import SettingAccordion from '../admin-page/settings/setting-accordion.svelte';
|
||||
import ChangePasswordSettings from './change-password-settings.svelte';
|
||||
import OAuthSettings from './oauth-settings.svelte';
|
||||
import UserAPIKeyList from './user-api-key-list.svelte';
|
||||
import DeviceList from './device-list.svelte';
|
||||
import PartnerSettings from './partner-settings.svelte';
|
||||
import UserProfileSettings from './user-profile-settings.svelte';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
export let user: UserResponseDto;
|
||||
|
||||
let oauthEnabled = false;
|
||||
let oauthOpen = false;
|
||||
let oauthEnabled = false;
|
||||
let oauthOpen = false;
|
||||
|
||||
onMount(async () => {
|
||||
oauthOpen = oauth.isCallback(window.location);
|
||||
onMount(async () => {
|
||||
oauthOpen = oauth.isCallback(window.location);
|
||||
|
||||
try {
|
||||
const { data } = await oauth.getConfig(window.location);
|
||||
oauthEnabled = data.enabled;
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
});
|
||||
try {
|
||||
const { data } = await oauth.getConfig(window.location);
|
||||
oauthEnabled = data.enabled;
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<SettingAccordion title="Account" subtitle="Manage your account">
|
||||
<UserProfileSettings {user} />
|
||||
<UserProfileSettings {user} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="API Keys" subtitle="Manage your API keys">
|
||||
<UserAPIKeyList />
|
||||
<UserAPIKeyList />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Authorized Devices" subtitle="Manage your logged-in devices">
|
||||
<DeviceList />
|
||||
<DeviceList />
|
||||
</SettingAccordion>
|
||||
|
||||
{#if oauthEnabled}
|
||||
<SettingAccordion
|
||||
title="OAuth"
|
||||
subtitle="Manage your OAuth connection"
|
||||
isOpen={oauthOpen || $page.url.searchParams.get('open') === 'oauth'}
|
||||
>
|
||||
<OAuthSettings {user} />
|
||||
</SettingAccordion>
|
||||
<SettingAccordion
|
||||
title="OAuth"
|
||||
subtitle="Manage your OAuth connection"
|
||||
isOpen={oauthOpen || $page.url.searchParams.get('open') === 'oauth'}
|
||||
>
|
||||
<OAuthSettings {user} />
|
||||
</SettingAccordion>
|
||||
{/if}
|
||||
|
||||
<SettingAccordion title="Password" subtitle="Change your password">
|
||||
<ChangePasswordSettings />
|
||||
<ChangePasswordSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Sharing" subtitle="Manage sharing with partners">
|
||||
<PartnerSettings {user} />
|
||||
<PartnerSettings {user} />
|
||||
</SettingAccordion>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue