mirror of
https://github.com/immich-app/immich.git
synced 2025-01-21 00:52:43 -05:00
refactor: migrate create user form to immich ui (#15350)
* refactor: migrate create user form to immich ui * minor styling tweak * remove unintentional commit * revert formating diff --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
2d2966caa0
commit
a60da1ccab
7 changed files with 121 additions and 148 deletions
|
@ -5,10 +5,8 @@
|
|||
import { ByteUnit, convertToBytes } from '$lib/utils/byte-units';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { createUserAdmin } from '@immich/sdk';
|
||||
import { Alert, Button, Field, HelperText, Input, PasswordInput, Stack, Switch } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import Slider from '../elements/slider.svelte';
|
||||
import PasswordField from '../shared-components/password-field.svelte';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
|
@ -17,137 +15,114 @@
|
|||
oauthEnabled?: boolean;
|
||||
}
|
||||
|
||||
let { onClose, onSubmit, onCancel, oauthEnabled = false }: Props = $props();
|
||||
let { onClose, onSubmit: onDone, onCancel, oauthEnabled = false }: Props = $props();
|
||||
|
||||
let error = $state('');
|
||||
let success = $state('');
|
||||
let success = $state(false);
|
||||
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let confirmPassword = $state('');
|
||||
let passwordConfirm = $state('');
|
||||
let name = $state('');
|
||||
let shouldChangePassword = $state(true);
|
||||
let notify = $state(true);
|
||||
|
||||
let canCreateUser = $state(false);
|
||||
let quotaSize: number | undefined = $state();
|
||||
let quotaSize: string | undefined = $state();
|
||||
let isCreatingUser = $state(false);
|
||||
|
||||
let quotaSizeInBytes = $derived(quotaSize ? convertToBytes(quotaSize, ByteUnit.GiB) : null);
|
||||
let quotaSizeInBytes = $derived(quotaSize ? convertToBytes(Number(quotaSize), ByteUnit.GiB) : null);
|
||||
let quotaSizeWarning = $derived(
|
||||
quotaSizeInBytes && userInteraction.serverInfo && quotaSizeInBytes > userInteraction.serverInfo.diskSizeRaw,
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (password !== confirmPassword && confirmPassword.length > 0) {
|
||||
error = $t('password_does_not_match');
|
||||
canCreateUser = false;
|
||||
} else {
|
||||
error = '';
|
||||
canCreateUser = true;
|
||||
}
|
||||
});
|
||||
const passwordMismatch = $derived(password !== passwordConfirm && passwordConfirm.length > 0);
|
||||
const passwordMismatchMessage = $derived(passwordMismatch ? $t('password_does_not_match') : '');
|
||||
const valid = $derived(!passwordMismatch && !isCreatingUser);
|
||||
|
||||
async function registerUser() {
|
||||
if (canCreateUser && !isCreatingUser) {
|
||||
isCreatingUser = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
await createUserAdmin({
|
||||
userAdminCreateDto: {
|
||||
email,
|
||||
password,
|
||||
shouldChangePassword,
|
||||
name,
|
||||
quotaSizeInBytes,
|
||||
notify,
|
||||
},
|
||||
});
|
||||
|
||||
success = $t('new_user_created');
|
||||
|
||||
onSubmit();
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_create_user'));
|
||||
} finally {
|
||||
isCreatingUser = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onsubmit = async (event: Event) => {
|
||||
const onSubmit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
await registerUser();
|
||||
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
isCreatingUser = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
await createUserAdmin({
|
||||
userAdminCreateDto: {
|
||||
email,
|
||||
password,
|
||||
shouldChangePassword,
|
||||
name,
|
||||
quotaSizeInBytes,
|
||||
notify,
|
||||
},
|
||||
});
|
||||
|
||||
success = true;
|
||||
|
||||
onDone();
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_create_user'));
|
||||
} finally {
|
||||
isCreatingUser = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal title={$t('create_new_user')} showLogo {onClose}>
|
||||
<form {onsubmit} autocomplete="off" id="create-new-user-form">
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="email">{$t('email')}</label>
|
||||
<input class="immich-form-input" id="email" bind:value={email} type="email" required />
|
||||
</div>
|
||||
|
||||
{#if $featureFlags.email}
|
||||
<div class="my-4 flex place-items-center justify-between gap-2">
|
||||
<label class="text-sm dark:text-immich-dark-fg" for="send-welcome-email">
|
||||
{$t('admin.send_welcome_email')}
|
||||
</label>
|
||||
<Slider id="send-welcome-email" bind:checked={notify} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="password">{$t('password')}</label>
|
||||
<PasswordField id="password" bind:password autocomplete="new-password" required={!oauthEnabled} />
|
||||
</div>
|
||||
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="confirmPassword">{$t('confirm_password')}</label>
|
||||
<PasswordField
|
||||
id="confirmPassword"
|
||||
bind:password={confirmPassword}
|
||||
autocomplete="new-password"
|
||||
required={!oauthEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-4 flex place-items-center justify-between gap-2">
|
||||
<label class="text-sm dark:text-immich-dark-fg" for="require-password-change">
|
||||
{$t('admin.require_password_change_on_login')}
|
||||
</label>
|
||||
<Slider id="require-password-change" bind:checked={shouldChangePassword} />
|
||||
</div>
|
||||
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="name">{$t('name')}</label>
|
||||
<input class="immich-form-input" id="name" bind:value={name} type="text" required />
|
||||
</div>
|
||||
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="flex items-center gap-2 immich-form-label" for="quotaSize">
|
||||
{$t('admin.quota_size_gib')}
|
||||
{#if quotaSizeWarning}
|
||||
<p class="text-red-400 text-sm">{$t('errors.quota_higher_than_disk_size')}</p>
|
||||
{/if}
|
||||
</label>
|
||||
<input class="immich-form-input" id="quotaSize" type="number" min="0" bind:value={quotaSize} />
|
||||
</div>
|
||||
|
||||
<form onsubmit={onSubmit} autocomplete="off" id="create-new-user-form">
|
||||
<FullScreenModal title={$t('create_new_user')} showLogo {onClose}>
|
||||
{#if error}
|
||||
<p class="text-sm text-red-400">{error}</p>
|
||||
<Alert color="danger" size="small" title={error} closable />
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<p class="text-sm text-immich-primary">{success}</p>
|
||||
<p class="text-sm text-immich-primary">{$t('new_user_created')}</p>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
{#snippet stickyBottom()}
|
||||
<Button color="gray" fullwidth onclick={onCancel}>{$t('cancel')}</Button>
|
||||
<Button type="submit" disabled={isCreatingUser} fullwidth form="create-new-user-form">{$t('create')}</Button>
|
||||
{/snippet}
|
||||
</FullScreenModal>
|
||||
<Stack gap={4}>
|
||||
<Field label={$t('email')} required>
|
||||
<Input bind:value={email} type="email" />
|
||||
</Field>
|
||||
|
||||
{#if $featureFlags.email}
|
||||
<Field label={$t('admin.send_welcome_email')}>
|
||||
<Switch id="send-welcome-email" bind:checked={notify} class="flex justify-between text-sm" />
|
||||
</Field>
|
||||
{/if}
|
||||
|
||||
<Field label={$t('password')} required={!oauthEnabled}>
|
||||
<PasswordInput id="password" bind:value={password} autocomplete="new-password" />
|
||||
</Field>
|
||||
|
||||
<Field label={$t('confirm_password')} required={!oauthEnabled}>
|
||||
<PasswordInput id="confirmPassword" bind:value={passwordConfirm} autocomplete="new-password" />
|
||||
<HelperText color="danger">{passwordMismatchMessage}</HelperText>
|
||||
</Field>
|
||||
|
||||
<Field label={$t('admin.require_password_change_on_login')}>
|
||||
<Switch id="require-password-change" bind:checked={shouldChangePassword} class="flex justify-between text-sm" />
|
||||
</Field>
|
||||
|
||||
<Field label={$t('name')} required>
|
||||
<Input bind:value={name} />
|
||||
</Field>
|
||||
|
||||
<Field label={$t('admin.quota_size_gib')}>
|
||||
<Input bind:value={quotaSize} type="number" min="0" />
|
||||
{#if quotaSizeWarning}
|
||||
<HelperText color="danger">{$t('errors.quota_higher_than_disk_size')}</HelperText>
|
||||
{/if}
|
||||
</Field>
|
||||
</Stack>
|
||||
|
||||
{#snippet stickyBottom()}
|
||||
<Button color="secondary" fullWidth onclick={onCancel} shape="round">{$t('cancel')}</Button>
|
||||
<Button type="submit" disabled={!valid} fullWidth shape="round">{$t('create')}</Button>
|
||||
{/snippet}
|
||||
</FullScreenModal>
|
||||
</form>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
</script>
|
||||
|
||||
<section class="min-w-screen flex min-h-screen items-center justify-center">
|
||||
<Card color="secondary" class="w-full max-w-xl border m-2">
|
||||
<Card color="secondary" class="w-full max-w-lg border m-2">
|
||||
<CardHeader class="mt-6">
|
||||
<VStack>
|
||||
<Logo variant="icon" size="giant" />
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import { Button } from '@immich/ui';
|
||||
import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk';
|
||||
import SearchPeopleSection from './search-people-section.svelte';
|
||||
import SearchLocationSection from './search-location-section.svelte';
|
||||
|
@ -163,7 +163,7 @@
|
|||
</form>
|
||||
|
||||
{#snippet stickyBottom()}
|
||||
<Button type="reset" color="gray" fullwidth form={formId}>{$t('clear_all')}</Button>
|
||||
<Button type="submit" fullwidth form={formId}>{$t('search')}</Button>
|
||||
<Button shape="round" size="large" type="reset" color="secondary" fullWidth form={formId}>{$t('clear_all')}</Button>
|
||||
<Button shape="round" size="large" type="submit" fullWidth form={formId}>{$t('search')}</Button>
|
||||
{/snippet}
|
||||
</FullScreenModal>
|
||||
|
|
|
@ -2,9 +2,6 @@
|
|||
import { page } from '$app/stores';
|
||||
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialogue.svelte';
|
||||
import RestoreDialogue from '$lib/components/admin-page/restore-dialogue.svelte';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
|
||||
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
|
||||
|
@ -15,12 +12,13 @@
|
|||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { serverConfig, featureFlags } from '$lib/stores/server-config.store';
|
||||
import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { UserStatus, searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||
import { Button, Code, IconButton, Text } from '@immich/ui';
|
||||
import { mdiContentCopy, mdiDeleteRestore, mdiInfinity, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onMount } from 'svelte';
|
||||
|
@ -161,22 +159,21 @@
|
|||
>
|
||||
{#snippet promptSnippet()}
|
||||
<div class="flex flex-col gap-4">
|
||||
<p>{$t('admin.user_password_has_been_reset')}</p>
|
||||
<Text>{$t('admin.user_password_has_been_reset')}</Text>
|
||||
|
||||
<div class="flex justify-center gap-2">
|
||||
<code
|
||||
class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary dark:text-immich-dark-primary dark:bg-gray-700"
|
||||
>
|
||||
{newPassword}
|
||||
</code>
|
||||
<LinkButton onclick={() => copyToClipboard(newPassword)} title={$t('copy_password')}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiContentCopy} size="18" />
|
||||
</div>
|
||||
</LinkButton>
|
||||
<div class="flex justify-center gap-2 items-center">
|
||||
<Code color="primary">{newPassword}</Code>
|
||||
<IconButton
|
||||
icon={mdiContentCopy}
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => copyToClipboard(newPassword)}
|
||||
title={$t('copy_password')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p>{$t('admin.user_password_reset_description')}</p>
|
||||
<Text>{$t('admin.user_password_reset_description')}</Text>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ConfirmDialog>
|
||||
|
@ -222,31 +219,31 @@
|
|||
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"
|
||||
>
|
||||
{#if !immichUser.deletedAt}
|
||||
<CircleIconButton
|
||||
<IconButton
|
||||
shape="round"
|
||||
size="large"
|
||||
icon={mdiPencilOutline}
|
||||
title={$t('edit_user')}
|
||||
color="primary"
|
||||
size="16"
|
||||
onclick={() => editUserHandler(immichUser)}
|
||||
/>
|
||||
{#if immichUser.id !== $user.id}
|
||||
<CircleIconButton
|
||||
<IconButton
|
||||
shape="round"
|
||||
size="large"
|
||||
icon={mdiTrashCanOutline}
|
||||
title={$t('delete_user')}
|
||||
color="primary"
|
||||
size="16"
|
||||
onclick={() => deleteUserHandler(immichUser)}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if immichUser.deletedAt && immichUser.status === UserStatus.Deleted}
|
||||
<CircleIconButton
|
||||
<IconButton
|
||||
shape="round"
|
||||
size="large"
|
||||
icon={mdiDeleteRestore}
|
||||
title={$t('admin.user_restore_scheduled_removal', {
|
||||
values: { date: getDeleteDate(immichUser.deletedAt) },
|
||||
})}
|
||||
color="primary"
|
||||
size="16"
|
||||
onclick={() => restoreUserHandler(immichUser)}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -256,8 +253,7 @@
|
|||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Button size="sm" onclick={() => (shouldShowCreateUserForm = true)}>{$t('create_user')}</Button>
|
||||
<Button shape="round" onclick={() => (shouldShowCreateUserForm = true)}>{$t('create_user')}</Button>
|
||||
</section>
|
||||
</section>
|
||||
</UserPageLayout>
|
||||
|
|
|
@ -51,9 +51,7 @@
|
|||
|
||||
<Field label={$t('confirm_password')} required>
|
||||
<PasswordInput bind:value={passwordConfirm} autocomplete="new-password" />
|
||||
{#if errorMessage}
|
||||
<HelperText color="danger">{errorMessage}</HelperText>
|
||||
{/if}
|
||||
<HelperText color="danger">{errorMessage}</HelperText>
|
||||
</Field>
|
||||
|
||||
<div class="my-5 flex w-full">
|
||||
|
|
|
@ -131,7 +131,7 @@
|
|||
<div class="inline-flex w-full items-center justify-center mt-4">
|
||||
<hr class="my-4 h-px w-3/4 border-0 bg-gray-200 dark:bg-gray-600" />
|
||||
<span
|
||||
class="absolute left-1/2 -translate-x-1/2 bg-white px-3 font-medium text-gray-900 dark:bg-immich-dark-gray dark:text-white"
|
||||
class="absolute left-1/2 -translate-x-1/2 bg-gray-50 px-3 font-medium text-gray-900 dark:bg-neutral-900 dark:text-white"
|
||||
>
|
||||
{$t('or').toUpperCase()}
|
||||
</span>
|
||||
|
|
|
@ -2,7 +2,11 @@ import plugin from 'tailwindcss/plugin';
|
|||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}', './node_modules/@immich/ui/dist/**/*.{svelte,js}'],
|
||||
content: [
|
||||
'./src/**/*.{html,js,svelte,ts}',
|
||||
'./node_modules/@immich/ui/dist/**/*.{svelte,js}',
|
||||
'../../ui/src/**/*.{html,js,svelte,ts}',
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
|
|
Loading…
Add table
Reference in a new issue