mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #3004 from appwrite/fix-mock-phone-numbers-ux
fix: mock phone numbers ux
This commit is contained in:
+204
-82
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import { InputPhone, InputOTP, Button } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getChangePlanUrl } from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
import { isCloud, isSelfHosted } from '$lib/system';
|
||||
import MockNumbersLight from './mock-numbers-light.png';
|
||||
@@ -15,7 +14,7 @@
|
||||
import { app } from '$lib/stores/app';
|
||||
import Empty from '$lib/components/empty.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Icon, Input, Layout, Link, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { Icon, Input, Layout, Link, Spinner, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus, IconRefresh, IconTrash } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
const {
|
||||
@@ -25,13 +24,20 @@
|
||||
} = $props();
|
||||
|
||||
type MockNumberForm = Pick<Models.MockNumber, 'number' | 'otp'> & {
|
||||
draftId?: string;
|
||||
initialNumber?: string;
|
||||
initialOtp?: string;
|
||||
};
|
||||
|
||||
let numbers: MockNumberForm[] = $state(getMockNumberRows(project?.authMockNumbers ?? []));
|
||||
let lastProjectId: string = $state(project?.$id ?? null);
|
||||
let savedNumbers: MockNumberForm[] = $state([]);
|
||||
let draftNumbers: MockNumberForm[] = $state([]);
|
||||
let isLoadingMockNumbers = $state(false);
|
||||
let lastProjectId: string | null = $state(null);
|
||||
let pendingRow: string | null = $state(null);
|
||||
let showDeleteConfirm = $state(false);
|
||||
let deleteTargetIndex: number | null = $state(null);
|
||||
let deleteError: string | null = $state(null);
|
||||
let draftIdCounter = $state(0);
|
||||
|
||||
const isComponentDisabled = $derived(
|
||||
isSelfHosted || (isCloud && !$currentPlan.supportsMockNumbers)
|
||||
@@ -53,7 +59,7 @@
|
||||
const id = project?.$id ?? null;
|
||||
if (id && id !== lastProjectId) {
|
||||
lastProjectId = id;
|
||||
numbers = getMockNumberRows(project?.authMockNumbers ?? []);
|
||||
void loadMockNumbers();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -66,8 +72,38 @@
|
||||
}));
|
||||
}
|
||||
|
||||
function getRowKey(number: MockNumberForm, index: number) {
|
||||
return number.initialNumber ?? `new-${index}`;
|
||||
async function loadMockNumbers() {
|
||||
try {
|
||||
if (savedNumbers.length === 0) {
|
||||
isLoadingMockNumbers = true;
|
||||
}
|
||||
const response = await sdk
|
||||
.forProject(project.region, project.$id)
|
||||
.project.listMockPhones({});
|
||||
savedNumbers = getMockNumberRows(response.mockNumbers ?? []);
|
||||
} catch (error) {
|
||||
addNotification({ type: 'error', message: error.message });
|
||||
trackError(error, Submit.AuthMockNumbersUpdate);
|
||||
} finally {
|
||||
isLoadingMockNumbers = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getSavedRowKey(number: MockNumberForm, index: number) {
|
||||
return number.initialNumber ?? number.number ?? `saved-${index}`;
|
||||
}
|
||||
|
||||
function createDraftNumber(): MockNumberForm {
|
||||
draftIdCounter += 1;
|
||||
return {
|
||||
draftId: `draft-${draftIdCounter}`,
|
||||
number: generateNumber(),
|
||||
otp: generateOTP()
|
||||
};
|
||||
}
|
||||
|
||||
function getDraftRowKey(number: MockNumberForm, index: number) {
|
||||
return number.draftId ?? `draft-${index}`;
|
||||
}
|
||||
|
||||
function isSaved(number: MockNumberForm) {
|
||||
@@ -83,40 +119,36 @@
|
||||
}
|
||||
|
||||
function isChanged(number: MockNumberForm) {
|
||||
return !isSaved(number) || number.otp !== number.initialOtp;
|
||||
}
|
||||
|
||||
function setRow(index: number, number: Models.MockNumber) {
|
||||
numbers = numbers.map((row, rowIndex) =>
|
||||
rowIndex === index
|
||||
? {
|
||||
number: number.number,
|
||||
otp: number.otp,
|
||||
initialNumber: number.number,
|
||||
initialOtp: number.otp
|
||||
}
|
||||
: row
|
||||
return (
|
||||
!isSaved(number) ||
|
||||
number.number !== number.initialNumber ||
|
||||
number.otp !== number.initialOtp
|
||||
);
|
||||
}
|
||||
|
||||
function setDraftRow(index: number, nextValue: MockNumberForm) {
|
||||
draftNumbers = draftNumbers.map((row, rowIndex) => (rowIndex === index ? nextValue : row));
|
||||
}
|
||||
|
||||
async function createPhoneNumber(number: MockNumberForm, index: number) {
|
||||
const rowKey = getRowKey(number, index);
|
||||
const rowKey = getDraftRowKey(number, index);
|
||||
try {
|
||||
const projectSdk = sdk.forProject(project.region, project.$id).project;
|
||||
pendingRow = rowKey;
|
||||
const mockNumber = await projectSdk.createMockPhone({
|
||||
await projectSdk.createMockPhone({
|
||||
number: number.number,
|
||||
otp: number.otp
|
||||
});
|
||||
|
||||
setRow(index, mockNumber);
|
||||
pendingRow = null;
|
||||
draftNumbers = draftNumbers.filter((_, rowIndex) => rowIndex !== index);
|
||||
await loadMockNumbers();
|
||||
addNotification({ type: 'success', message: 'Mock phone numbers have been updated' });
|
||||
trackEvent(Submit.AuthMockNumbersUpdate);
|
||||
} catch (error) {
|
||||
addNotification({ type: 'error', message: error.message });
|
||||
trackError(error, Submit.AuthMockNumbersUpdate);
|
||||
} finally {
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
pendingRow = null;
|
||||
}
|
||||
}
|
||||
@@ -124,53 +156,52 @@
|
||||
async function updatePhoneNumber(number: MockNumberForm, index: number) {
|
||||
if (!number.initialNumber) return;
|
||||
|
||||
const rowKey = getRowKey(number, index);
|
||||
const rowKey = getSavedRowKey(number, index);
|
||||
try {
|
||||
const projectSdk = sdk.forProject(project.region, project.$id).project;
|
||||
pendingRow = rowKey;
|
||||
const mockNumber = await projectSdk.updateMockPhone({
|
||||
await projectSdk.updateMockPhone({
|
||||
number: number.initialNumber,
|
||||
otp: number.otp
|
||||
});
|
||||
|
||||
setRow(index, mockNumber);
|
||||
await loadMockNumbers();
|
||||
addNotification({ type: 'success', message: 'Mock phone numbers have been updated' });
|
||||
trackEvent(Submit.AuthMockNumbersUpdate);
|
||||
} catch (error) {
|
||||
addNotification({ type: 'error', message: error.message });
|
||||
trackError(error, Submit.AuthMockNumbersUpdate);
|
||||
} finally {
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
pendingRow = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function deletePhoneNumber(number: MockNumberForm, index: number) {
|
||||
if (!number.initialNumber) {
|
||||
numbers = numbers.filter((_, rowIndex) => rowIndex !== index);
|
||||
draftNumbers = draftNumbers.filter((_, rowIndex) => rowIndex !== index);
|
||||
return;
|
||||
}
|
||||
|
||||
const rowKey = getRowKey(number, index);
|
||||
const rowKey = getSavedRowKey(number, index);
|
||||
try {
|
||||
const projectSdk = sdk.forProject(project.region, project.$id).project;
|
||||
pendingRow = rowKey;
|
||||
await projectSdk.deleteMockPhone({ number: number.initialNumber });
|
||||
|
||||
numbers = numbers.filter((_, rowIndex) => rowIndex !== index);
|
||||
savedNumbers = savedNumbers.filter((row) => row.initialNumber !== number.initialNumber);
|
||||
await loadMockNumbers();
|
||||
addNotification({ type: 'success', message: 'Mock phone number has been deleted' });
|
||||
trackEvent(Submit.AuthMockNumbersUpdate);
|
||||
} catch (error) {
|
||||
addNotification({ type: 'error', message: error.message });
|
||||
deleteError = error.message;
|
||||
trackError(error, Submit.AuthMockNumbersUpdate);
|
||||
} finally {
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
pendingRow = null;
|
||||
}
|
||||
}
|
||||
|
||||
function addPhoneNumber(number: MockNumberForm) {
|
||||
numbers = [...numbers, { number: number.number, otp: number.otp }];
|
||||
function addPhoneNumber() {
|
||||
draftNumbers = [...draftNumbers, createDraftNumber()];
|
||||
}
|
||||
|
||||
function generateNumber(): string {
|
||||
@@ -184,6 +215,38 @@
|
||||
function generateOTP(): string {
|
||||
return String(Math.floor(100000 + Math.random() * 900000));
|
||||
}
|
||||
|
||||
function getDeleteTargetRow() {
|
||||
return deleteTargetIndex === null ? null : (savedNumbers[deleteTargetIndex] ?? null);
|
||||
}
|
||||
|
||||
function isDeletePending() {
|
||||
const row = getDeleteTargetRow();
|
||||
return (
|
||||
row !== null &&
|
||||
deleteTargetIndex !== null &&
|
||||
pendingRow === getSavedRowKey(row, deleteTargetIndex)
|
||||
);
|
||||
}
|
||||
|
||||
function promptDelete(index: number) {
|
||||
deleteTargetIndex = index;
|
||||
deleteError = null;
|
||||
showDeleteConfirm = true;
|
||||
}
|
||||
|
||||
async function confirmDeletePhoneNumber() {
|
||||
const row = getDeleteTargetRow();
|
||||
if (row === null || deleteTargetIndex === null) return;
|
||||
|
||||
deleteError = null;
|
||||
await deletePhoneNumber(row, deleteTargetIndex);
|
||||
|
||||
if (!deleteError) {
|
||||
showDeleteConfirm = false;
|
||||
deleteTargetIndex = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardGrid hideFooter>
|
||||
@@ -250,12 +313,16 @@
|
||||
}}>{cta}</Button>
|
||||
</svelte:fragment>
|
||||
</EmptyCardImageCloud>
|
||||
{:else if numbers?.length > 0}
|
||||
{#each numbers as number, index}
|
||||
{:else if isLoadingMockNumbers}
|
||||
<Layout.Stack direction="row" justifyContent="center">
|
||||
<Spinner />
|
||||
</Layout.Stack>
|
||||
{:else if savedNumbers.length > 0 || draftNumbers.length > 0}
|
||||
{#each savedNumbers as number, index (getSavedRowKey(number, index))}
|
||||
<Layout.Stack direction="row" alignItems="flex-end">
|
||||
<Layout.Stack direction="row" alignItems="flex-end" gap="xs">
|
||||
<InputPhone
|
||||
id={`key-${index}`}
|
||||
id={`saved-key-${index}`}
|
||||
bind:value={number.number}
|
||||
placeholder="Enter phone number"
|
||||
label={index === 0 ? 'Phone number' : undefined}
|
||||
@@ -263,22 +330,10 @@
|
||||
maxlength={16}
|
||||
disabled={isSaved(number)}
|
||||
required />
|
||||
{#if !isSaved(number)}
|
||||
<Tooltip>
|
||||
<Button
|
||||
icon
|
||||
compact
|
||||
ariaLabel="Regenerate phone number"
|
||||
on:click={() => (number.number = generateNumber())}>
|
||||
<Icon icon={IconRefresh} size="s" />
|
||||
</Button>
|
||||
<span slot="tooltip">Regenerate</span>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" alignItems="flex-end" gap="xs">
|
||||
<InputOTP
|
||||
id={`value-${index}`}
|
||||
id={`saved-value-${index}`}
|
||||
bind:value={number.otp}
|
||||
placeholder="Enter value"
|
||||
label={index === 0 ? 'Verification code' : undefined}
|
||||
@@ -298,56 +353,123 @@
|
||||
compact
|
||||
disabled={!isChanged(number) ||
|
||||
!isValid(number) ||
|
||||
pendingRow === getRowKey(number, index)}
|
||||
pendingRow === getSavedRowKey(number, index)}
|
||||
on:click={() => updatePhoneNumber(number, index)}>
|
||||
Update
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
compact
|
||||
disabled={!isValid(number) ||
|
||||
pendingRow === getRowKey(number, index)}
|
||||
on:click={() => createPhoneNumber(number, index)}>
|
||||
Create
|
||||
Save
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
icon
|
||||
compact
|
||||
danger={isSaved(number)}
|
||||
disabled={pendingRow === getRowKey(number, index)}
|
||||
text
|
||||
disabled={pendingRow === getSavedRowKey(number, index)}
|
||||
ariaLabel="Delete mock phone number"
|
||||
on:click={() => {
|
||||
deletePhoneNumber(number, index);
|
||||
}}>
|
||||
on:click={() => promptDelete(index)}>
|
||||
<Icon icon={IconTrash} size="s" />
|
||||
</Button>
|
||||
{#if pendingRow === getSavedRowKey(number, index)}
|
||||
<span style:opacity="0.75">
|
||||
<Spinner size="s" />
|
||||
</span>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
{/each}
|
||||
{#if numbers?.length < 10}
|
||||
{#each draftNumbers as number, index (getDraftRowKey(number, index))}
|
||||
<Layout.Stack direction="row" alignItems="flex-end">
|
||||
<Layout.Stack direction="row" alignItems="flex-end" gap="xs">
|
||||
<InputPhone
|
||||
id={`draft-key-${index}`}
|
||||
bind:value={number.number}
|
||||
placeholder="Enter phone number"
|
||||
label={savedNumbers.length === 0 && index === 0
|
||||
? 'Phone number'
|
||||
: undefined}
|
||||
minlength={9}
|
||||
maxlength={16}
|
||||
required>
|
||||
<Tooltip slot="end">
|
||||
<Input.Action
|
||||
icon={IconRefresh}
|
||||
on:click={() =>
|
||||
setDraftRow(index, {
|
||||
...number,
|
||||
number: generateNumber()
|
||||
})} />
|
||||
<span slot="tooltip">Regenerate</span>
|
||||
</Tooltip>
|
||||
</InputPhone>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" alignItems="flex-end" gap="xs">
|
||||
<InputOTP
|
||||
id={`draft-value-${index}`}
|
||||
bind:value={number.otp}
|
||||
placeholder="Enter value"
|
||||
label={savedNumbers.length === 0 && index === 0
|
||||
? 'Verification code'
|
||||
: undefined}
|
||||
maxlength={6}
|
||||
pattern={'^[0-9]{6}$'}
|
||||
patternError="The value must contain 6 digits"
|
||||
required>
|
||||
<Tooltip slot="end">
|
||||
<Input.Action
|
||||
icon={IconRefresh}
|
||||
on:click={() =>
|
||||
setDraftRow(index, { ...number, otp: generateOTP() })} />
|
||||
<span slot="tooltip">Regenerate</span>
|
||||
</Tooltip>
|
||||
</InputOTP>
|
||||
<Button
|
||||
compact
|
||||
disabled={!isValid(number) ||
|
||||
pendingRow === getDraftRowKey(number, index)}
|
||||
on:click={() => createPhoneNumber(number, index)}>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
icon
|
||||
text
|
||||
disabled={pendingRow === getDraftRowKey(number, index)}
|
||||
ariaLabel="Delete mock phone number"
|
||||
on:click={() => deletePhoneNumber(number, index)}>
|
||||
<Icon icon={IconTrash} size="s" />
|
||||
</Button>
|
||||
{#if pendingRow === getDraftRowKey(number, index)}
|
||||
<span style:opacity="0.75">
|
||||
<Spinner size="s" />
|
||||
</span>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
{/each}
|
||||
{#if savedNumbers.length + draftNumbers.length < 10}
|
||||
<div>
|
||||
<Button
|
||||
secondary
|
||||
on:click={() =>
|
||||
addPhoneNumber({
|
||||
number: generateNumber(),
|
||||
otp: generateOTP()
|
||||
})}
|
||||
disabled={numbers.length >= 10}>
|
||||
on:click={addPhoneNumber}
|
||||
disabled={savedNumbers.length + draftNumbers.length >= 10}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Add number
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Empty
|
||||
on:click={() => {
|
||||
addPhoneNumber({
|
||||
number: generateNumber(),
|
||||
otp: generateOTP()
|
||||
});
|
||||
}}>Generate number</Empty>
|
||||
<Empty on:click={addPhoneNumber}>Add a number</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<Confirm
|
||||
bind:open={showDeleteConfirm}
|
||||
bind:error={deleteError}
|
||||
title="Delete mock phone number"
|
||||
action="Delete"
|
||||
submissionLoader
|
||||
disabled={isDeletePending()}
|
||||
onSubmit={confirmDeletePhoneNumber}>
|
||||
{#if getDeleteTargetRow()}
|
||||
<p>
|
||||
Are you sure you want to delete <b>{getDeleteTargetRow()?.initialNumber}</b>?
|
||||
</p>
|
||||
{/if}
|
||||
</Confirm>
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { InputSwitch } from '$lib/elements/forms';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import { Container } from '$lib/layout';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { authMethods, type AuthMethod } from '$lib/stores/auth-methods';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { oAuthProviders } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { MethodId, type Models } from '@appwrite.io/console';
|
||||
import { base } from '$app/paths';
|
||||
import { Avatar, Badge, Card, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Card,
|
||||
Dialog,
|
||||
Divider,
|
||||
Layout,
|
||||
Spinner,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { PageProps } from './$types';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
/** Must stay derived from `data` so OAuth/auth toggles reflect `invalidate(Dependencies.PROJECT)` without a full reload. */
|
||||
@@ -21,8 +33,29 @@
|
||||
|
||||
let showProvider = $state(false);
|
||||
let selectedProvider: Models.AuthProvider | null = $state(null);
|
||||
let isUpdatingAllAuthMethods = $state(false);
|
||||
let showUpdateAuthMethodsDialog = $state(false);
|
||||
let updateAuthMethodsEnabledMode = $state<boolean | null>(null);
|
||||
let apiAuthMethodUpdates = new SvelteSet<MethodId>();
|
||||
|
||||
const isAnyAuthMethodUpdating = $derived(apiAuthMethodUpdates.size > 0);
|
||||
const isAnyUpdateInProgress = $derived(isUpdatingAllAuthMethods || isAnyAuthMethodUpdating);
|
||||
|
||||
const allAuthMethodsEnabled = $derived.by(() => {
|
||||
if (isAnyUpdateInProgress) return false;
|
||||
return $authMethods.list.every((method) => method.value);
|
||||
});
|
||||
|
||||
const allAuthMethodsDisabled = $derived.by(() => {
|
||||
if (isAnyUpdateInProgress) return false;
|
||||
return $authMethods.list.every((method) => !method.value);
|
||||
});
|
||||
|
||||
const shouldDisableEnableAllButton = $derived(isAnyUpdateInProgress || allAuthMethodsEnabled);
|
||||
const shouldDisableDisableAllButton = $derived(isAnyUpdateInProgress || allAuthMethodsDisabled);
|
||||
|
||||
async function authUpdate(box: AuthMethod) {
|
||||
apiAuthMethodUpdates.add(box.method);
|
||||
try {
|
||||
await sdk.forProject(project.region, project.$id).project.updateAuthMethod({
|
||||
methodId: box.method,
|
||||
@@ -44,9 +77,68 @@
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AuthStatusUpdate);
|
||||
} finally {
|
||||
apiAuthMethodUpdates.delete(box.method);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleAllAuthMethods(status: boolean | null) {
|
||||
if (status === null) return;
|
||||
|
||||
isUpdatingAllAuthMethods = true;
|
||||
|
||||
try {
|
||||
const projectSdk = sdk.forProject(project.region, project.$id).project;
|
||||
for (const method of get(authMethods).list) {
|
||||
if (method.value === status) continue;
|
||||
await projectSdk.updateAuthMethod({
|
||||
methodId: method.method,
|
||||
enabled: status
|
||||
});
|
||||
}
|
||||
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message:
|
||||
'All authentication methods for ' +
|
||||
project.name +
|
||||
' have been ' +
|
||||
(status ? 'enabled.' : 'disabled.')
|
||||
});
|
||||
trackEvent(Submit.AuthStatusUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AuthStatusUpdate);
|
||||
} finally {
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
isUpdatingAllAuthMethods = false;
|
||||
showUpdateAuthMethodsDialog = false;
|
||||
updateAuthMethodsEnabledMode = null;
|
||||
}
|
||||
}
|
||||
|
||||
const dialogDetails = $derived.by(() => {
|
||||
if (updateAuthMethodsEnabledMode) {
|
||||
return {
|
||||
title: 'Enable all auth methods',
|
||||
message: 'All authentication methods will be enabled.',
|
||||
actionButton: 'Enable all'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Disable all auth methods',
|
||||
message:
|
||||
'Are you sure you want to disable all authentication methods? This will prevent users from signing in until at least one method is re-enabled.',
|
||||
actionButton: 'Disable all'
|
||||
};
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
authMethods.load(project);
|
||||
});
|
||||
@@ -59,16 +151,49 @@
|
||||
<svelte:fragment slot="title">Auth methods</svelte:fragment>
|
||||
Enable the authentication methods you wish to use.
|
||||
<svelte:fragment slot="aside">
|
||||
<Layout.Stack gap="l" direction="row" wrap="wrap">
|
||||
{#each $authMethods.list as box}
|
||||
<div style="flex-basis: 45%;">
|
||||
<InputSwitch
|
||||
label={box.label}
|
||||
id={box.method}
|
||||
bind:value={box.value}
|
||||
on:change={() => authUpdate(box)} />
|
||||
</div>
|
||||
{/each}
|
||||
<Layout.Stack gap="m" class="auth-methods-list">
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
<Button
|
||||
extraCompact
|
||||
on:click={() => {
|
||||
showUpdateAuthMethodsDialog = true;
|
||||
updateAuthMethodsEnabledMode = true;
|
||||
}}
|
||||
disabled={shouldDisableEnableAllButton}>Enable all</Button>
|
||||
<span style:height="20px">
|
||||
<Divider vertical />
|
||||
</span>
|
||||
<Button
|
||||
extraCompact
|
||||
on:click={() => {
|
||||
showUpdateAuthMethodsDialog = true;
|
||||
updateAuthMethodsEnabledMode = false;
|
||||
}}
|
||||
disabled={shouldDisableDisableAllButton}>Disable all</Button>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack gap="l">
|
||||
<Divider />
|
||||
<Layout.Stack direction="row" wrap="wrap">
|
||||
{#each $authMethods.list as box}
|
||||
<span style:flex-basis="30%">
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<InputSwitch
|
||||
label={box.label}
|
||||
id={box.method}
|
||||
bind:value={box.value}
|
||||
disabled={apiAuthMethodUpdates.has(box.method)}
|
||||
on:change={() => authUpdate(box)} />
|
||||
|
||||
{#if apiAuthMethodUpdates.has(box.method)}
|
||||
<span style:opacity="0.75">
|
||||
<Spinner size="s" />
|
||||
</span>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</span>
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -128,3 +253,21 @@
|
||||
showProvider = false;
|
||||
}} />
|
||||
{/if}
|
||||
|
||||
<Dialog title={dialogDetails.title} bind:open={showUpdateAuthMethodsDialog}>
|
||||
<p class="text" data-private>{dialogDetails.message}</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Layout.Stack direction="row" gap="s" justifyContent="flex-end">
|
||||
<Button text on:click={() => (showUpdateAuthMethodsDialog = false)}>Cancel</Button>
|
||||
|
||||
<Button
|
||||
secondary
|
||||
submissionLoader
|
||||
disabled={isUpdatingAllAuthMethods}
|
||||
forceShowLoader={isUpdatingAllAuthMethods}
|
||||
on:click={() => toggleAllAuthMethods(updateAuthMethodsEnabledMode)}>
|
||||
{dialogDetails.actionButton}
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Dialog>
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
|
||||
<style>
|
||||
.protocols-list {
|
||||
max-width: 36rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.protocol-toolbar {
|
||||
|
||||
Reference in New Issue
Block a user