mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
feat(web): Added password field visibility toggle (#7368)
* Added password field visibility toggle * improvements to password input field * fix e2e and change tabindex * add missing name=password * remove unnecessary type prop --------- Co-authored-by: Jan108 <dasJan108@gmail.com> Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
This commit is contained in:
parent
9d3ed719e0
commit
038e69fd02
@ -4,14 +4,15 @@
|
|||||||
import { signUpAdmin } from '@immich/sdk';
|
import { signUpAdmin } from '@immich/sdk';
|
||||||
import { handleError } from '../../utils/handle-error';
|
import { handleError } from '../../utils/handle-error';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
|
import PasswordField from '../shared-components/password-field.svelte';
|
||||||
|
|
||||||
let errorMessage: string;
|
let errorMessage: string;
|
||||||
let password = '';
|
let password = '';
|
||||||
let confirmPassowrd = '';
|
let confirmPassword = '';
|
||||||
let canRegister = false;
|
let canRegister = false;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (password !== confirmPassowrd && confirmPassowrd.length > 0) {
|
if (password !== confirmPassword && confirmPassword.length > 0) {
|
||||||
errorMessage = 'Password does not match';
|
errorMessage = 'Password does not match';
|
||||||
canRegister = false;
|
canRegister = false;
|
||||||
} else {
|
} else {
|
||||||
@ -56,28 +57,12 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="password">Admin Password</label>
|
<label class="immich-form-label" for="password">Admin Password</label>
|
||||||
<input
|
<PasswordField id="password" name="password" bind:password autocomplete="new-password" />
|
||||||
class="immich-form-input"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
required
|
|
||||||
bind:value={password}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="confirmPassword">Confirm Admin Password</label>
|
<label class="immich-form-label" for="confirmPassword">Confirm Admin Password</label>
|
||||||
<input
|
<PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
|
||||||
class="immich-form-input"
|
|
||||||
id="confirmPassword"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
required
|
|
||||||
bind:value={confirmPassowrd}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
|
import PasswordField from '../shared-components/password-field.svelte';
|
||||||
import { updateUser, type UserResponseDto } from '@immich/sdk';
|
import { updateUser, type UserResponseDto } from '@immich/sdk';
|
||||||
|
|
||||||
export let user: UserResponseDto;
|
export let user: UserResponseDto;
|
||||||
@ -46,28 +47,12 @@
|
|||||||
<form on:submit|preventDefault={changePassword} method="post" class="mt-5 flex flex-col gap-5">
|
<form on:submit|preventDefault={changePassword} method="post" class="mt-5 flex flex-col gap-5">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="password">New Password</label>
|
<label class="immich-form-label" for="password">New Password</label>
|
||||||
<input
|
<PasswordField id="password" bind:password autocomplete="new-password" />
|
||||||
class="immich-form-input"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
required
|
|
||||||
bind:value={password}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
|
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
|
||||||
<input
|
<PasswordField id="confirmPassword" bind:password={passwordConfirm} autocomplete="new-password" />
|
||||||
class="immich-form-input"
|
|
||||||
id="confirmPassword"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
required
|
|
||||||
bind:value={passwordConfirm}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
|
@ -6,12 +6,13 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
||||||
|
import PasswordField from '../shared-components/password-field.svelte';
|
||||||
|
|
||||||
let error: string;
|
let error: string;
|
||||||
let success: string;
|
let success: string;
|
||||||
|
|
||||||
let password = '';
|
let password = '';
|
||||||
let confirmPassowrd = '';
|
let confirmPassword = '';
|
||||||
|
|
||||||
let canCreateUser = false;
|
let canCreateUser = false;
|
||||||
let quotaSize: number | undefined;
|
let quotaSize: number | undefined;
|
||||||
@ -20,7 +21,7 @@
|
|||||||
$: quotaSizeWarning = quotaSize && convertToBytes(Number(quotaSize), 'GiB') > $serverInfo.diskSizeRaw;
|
$: quotaSizeWarning = quotaSize && convertToBytes(Number(quotaSize), 'GiB') > $serverInfo.diskSizeRaw;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (password !== confirmPassowrd && confirmPassowrd.length > 0) {
|
if (password !== confirmPassword && confirmPassword.length > 0) {
|
||||||
error = 'Password does not match';
|
error = 'Password does not match';
|
||||||
canCreateUser = false;
|
canCreateUser = false;
|
||||||
} else {
|
} else {
|
||||||
@ -91,19 +92,12 @@
|
|||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="password">Password</label>
|
<label class="immich-form-label" for="password">Password</label>
|
||||||
<input class="immich-form-input" id="password" name="password" type="password" required bind:value={password} />
|
<PasswordField id="password" name="password" bind:password autocomplete="new-password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
|
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
|
||||||
<input
|
<PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
|
||||||
class="immich-form-input"
|
|
||||||
id="confirmPassword"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
bind:value={confirmPassowrd}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
|
import PasswordField from '../shared-components/password-field.svelte';
|
||||||
|
|
||||||
export let onSuccess: () => unknown | Promise<unknown>;
|
export let onSuccess: () => unknown | Promise<unknown>;
|
||||||
export let onFirstLogin: () => unknown | Promise<unknown>;
|
export let onFirstLogin: () => unknown | Promise<unknown>;
|
||||||
@ -112,15 +113,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="password">Password</label>
|
<label class="immich-form-label" for="password">Password</label>
|
||||||
<input
|
<PasswordField id="password" bind:password autocomplete="current-password" />
|
||||||
class="immich-form-input"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
bind:value={password}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-5 flex w-full">
|
<div class="my-5 flex w-full">
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { mdiEyeOffOutline, mdiEyeOutline } from '@mdi/js';
|
||||||
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||||
|
import Icon from '../elements/icon.svelte';
|
||||||
|
|
||||||
|
interface $$Props extends HTMLInputAttributes {
|
||||||
|
password: string;
|
||||||
|
autocomplete: string;
|
||||||
|
required?: boolean;
|
||||||
|
onInput?: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let password: $$Props['password'];
|
||||||
|
export let required = true;
|
||||||
|
export let onInput: $$Props['onInput'] = undefined;
|
||||||
|
|
||||||
|
let showPassword = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative w-full">
|
||||||
|
<input
|
||||||
|
{...$$restProps}
|
||||||
|
class="immich-form-input w-full !pr-12"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
{required}
|
||||||
|
value={password}
|
||||||
|
on:input={(e) => {
|
||||||
|
password = e.currentTarget.value;
|
||||||
|
onInput?.(password);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if password.length > 0}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
tabindex="-1"
|
||||||
|
class="absolute inset-y-0 end-0 px-4 text-gray-700 dark:text-gray-200"
|
||||||
|
on:click={() => (showPassword = !showPassword)}
|
||||||
|
title={showPassword ? 'Hide password' : 'Show password'}
|
||||||
|
>
|
||||||
|
<Icon path={showPassword ? mdiEyeOffOutline : mdiEyeOutline} size="1.25em" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input::-ms-reveal {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
@ -10,6 +10,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
|
import PasswordField from '../password-field.svelte';
|
||||||
|
|
||||||
export let inputType: SettingInputFieldType;
|
export let inputType: SettingInputFieldType;
|
||||||
export let value: string | number;
|
export let value: string | number;
|
||||||
@ -22,6 +23,7 @@
|
|||||||
export let required = false;
|
export let required = false;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let isEdited = false;
|
export let isEdited = false;
|
||||||
|
export let passwordAutocomplete: string = 'current-password';
|
||||||
|
|
||||||
const handleInput = (e: Event) => {
|
const handleInput = (e: Event) => {
|
||||||
value = (e.target as HTMLInputElement).value;
|
value = (e.target as HTMLInputElement).value;
|
||||||
@ -64,20 +66,35 @@
|
|||||||
<slot name="desc" />
|
<slot name="desc" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<input
|
{#if inputType !== SettingInputFieldType.PASSWORD}
|
||||||
class="immich-form-input w-full pb-2"
|
<input
|
||||||
aria-describedby={desc ? `${label}-desc` : undefined}
|
class="immich-form-input w-full pb-2"
|
||||||
aria-labelledby="{label}-label"
|
aria-describedby={desc ? `${label}-desc` : undefined}
|
||||||
id={label}
|
aria-labelledby="{label}-label"
|
||||||
name={label}
|
id={label}
|
||||||
type={inputType}
|
name={label}
|
||||||
min={min.toString()}
|
type={inputType}
|
||||||
max={max.toString()}
|
min={min.toString()}
|
||||||
{step}
|
max={max.toString()}
|
||||||
{required}
|
{step}
|
||||||
{value}
|
{required}
|
||||||
on:input={handleInput}
|
{value}
|
||||||
{disabled}
|
on:input={handleInput}
|
||||||
{title}
|
{disabled}
|
||||||
/>
|
{title}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<PasswordField
|
||||||
|
aria-describedby={desc ? `${label}-desc` : undefined}
|
||||||
|
aria-labelledby="{label}-label"
|
||||||
|
id={label}
|
||||||
|
name={label}
|
||||||
|
autocomplete={passwordAutocomplete}
|
||||||
|
{required}
|
||||||
|
password={value.toString()}
|
||||||
|
onInput={(passwordValue) => (value = passwordValue)}
|
||||||
|
{disabled}
|
||||||
|
{title}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
label="PASSWORD"
|
label="PASSWORD"
|
||||||
bind:value={password}
|
bind:value={password}
|
||||||
required={true}
|
required={true}
|
||||||
|
passwordAutocomplete="current-password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
@ -54,6 +55,7 @@
|
|||||||
label="NEW PASSWORD"
|
label="NEW PASSWORD"
|
||||||
bind:value={newPassword}
|
bind:value={newPassword}
|
||||||
required={true}
|
required={true}
|
||||||
|
passwordAutocomplete="new-password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
@ -61,6 +63,7 @@
|
|||||||
label="CONFIRM PASSWORD"
|
label="CONFIRM PASSWORD"
|
||||||
bind:value={confirmPassword}
|
bind:value={confirmPassword}
|
||||||
required={true}
|
required={true}
|
||||||
|
passwordAutocomplete="new-password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user