Merge branch 'main' into usability-internal

This commit is contained in:
Torsten Dittmann
2022-10-06 11:05:22 +02:00
committed by GitHub
19 changed files with 399 additions and 383 deletions
+14 -14
View File
@@ -9,8 +9,8 @@
"version": "0.0.1",
"dependencies": {
"@aw-labs/appwrite-console": "^6.0.0",
"@aw-labs/icons": "0.0.0-57",
"@aw-labs/ui": "0.0.0-57",
"@aw-labs/icons": "0.0.0-58",
"@aw-labs/ui": "0.0.0-58",
"echarts": "^5.4.0",
"tippy.js": "^6.3.7",
"web-vitals": "^2.1.4"
@@ -77,14 +77,14 @@
}
},
"node_modules/@aw-labs/icons": {
"version": "0.0.0-57",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-57.tgz",
"integrity": "sha512-HmXSTSP3GEBi5awFk0APTTNdM10DDIBCTnxi3BJGlsXo+SXnpVikoMk6RZltNLH7MRfPQNx+VfJ2bOC+hlW8ZQ=="
"version": "0.0.0-58",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-58.tgz",
"integrity": "sha512-xUD5DQcYVNiKhDSpxMO24G/4l7txgAdwochK01tJA6wXhuPAcp5Yjuofjm2bGkMzCuze4Vs+SVvhsBxzVXRPrA=="
},
"node_modules/@aw-labs/ui": {
"version": "0.0.0-57",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-57.tgz",
"integrity": "sha512-a/nKqu9nHysTF7bIOLo+ZN1zKpCHfv+jHGixU0SGXchTNAmj2OXClZNk3XfMmwnPD6CiF4cG6YRdbmpx+TTXgA==",
"version": "0.0.0-58",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-58.tgz",
"integrity": "sha512-W3bTvAPX4ig5qLBvGn/VpNaDk62RqO4oDa0poube2WuElkVvaGwFAR0wCwmisdi2NeXyGJqt20g3Y/Mz4K2yMA==",
"dependencies": {
"@aw-labs/icons": "*"
}
@@ -8134,14 +8134,14 @@
}
},
"@aw-labs/icons": {
"version": "0.0.0-57",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-57.tgz",
"integrity": "sha512-HmXSTSP3GEBi5awFk0APTTNdM10DDIBCTnxi3BJGlsXo+SXnpVikoMk6RZltNLH7MRfPQNx+VfJ2bOC+hlW8ZQ=="
"version": "0.0.0-58",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-58.tgz",
"integrity": "sha512-xUD5DQcYVNiKhDSpxMO24G/4l7txgAdwochK01tJA6wXhuPAcp5Yjuofjm2bGkMzCuze4Vs+SVvhsBxzVXRPrA=="
},
"@aw-labs/ui": {
"version": "0.0.0-57",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-57.tgz",
"integrity": "sha512-a/nKqu9nHysTF7bIOLo+ZN1zKpCHfv+jHGixU0SGXchTNAmj2OXClZNk3XfMmwnPD6CiF4cG6YRdbmpx+TTXgA==",
"version": "0.0.0-58",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-58.tgz",
"integrity": "sha512-W3bTvAPX4ig5qLBvGn/VpNaDk62RqO4oDa0poube2WuElkVvaGwFAR0wCwmisdi2NeXyGJqt20g3Y/Mz4K2yMA==",
"requires": {
"@aw-labs/icons": "*"
}
+2 -2
View File
@@ -19,8 +19,8 @@
},
"dependencies": {
"@aw-labs/appwrite-console": "^6.0.0",
"@aw-labs/icons": "0.0.0-57",
"@aw-labs/ui": "0.0.0-57",
"@aw-labs/icons": "0.0.0-58",
"@aw-labs/ui": "0.0.0-58",
"echarts": "^5.4.0",
"tippy.js": "^6.3.7",
"web-vitals": "^2.1.4"
+1 -19
View File
@@ -1,21 +1,3 @@
<ul class="collapsible">
<li class="collapsible-item">
<details class="collapsible-wrapper">
<summary class="collapsible-button">
<span class="text">
<slot name="header" />
</span>
<span class="collapsible-button-optional">
<slot name="subheader" />
</span>
<div class="icon">
<span class="icon-plus" aria-hidden="true" />
<span class="icon-minus" aria-hidden="true" />
</div>
</summary>
<p class="collapsible-content">
<slot />
</p>
</details>
</li>
<slot />
</ul>
+14
View File
@@ -0,0 +1,14 @@
<li class="collapsible-item">
<details class="collapsible-wrapper">
<summary class="collapsible-button">
<span class="text"><slot name="title" /></span>
<span class="collapsible-button-optional"><slot name="subtitle" /></span>
<div class="icon">
<span class="icon-cheveron-down" aria-hidden="true" />
</div>
</summary>
<div class="collapsible-content">
<slot />
</div>
</details>
</li>
+2
View File
@@ -16,6 +16,7 @@ export { default as DropList } from './dropList.svelte';
export { default as DropListItem } from './dropListItem.svelte';
export { default as DropListLink } from './dropListLink.svelte';
export { default as Collapsible } from './collapsible.svelte';
export { default as CollapsibleItem } from './collapsibleItem.svelte';
export { default as DropTabs } from './dropTabs.svelte';
export { default as DropTabsItem } from './dropTabsItem.svelte';
export { default as Avatar } from './avatar.svelte';
@@ -27,3 +28,4 @@ export { default as GridItem1 } from './gridItem1.svelte';
export { default as Steps } from './steps.svelte';
export { default as Step } from './step.svelte';
export { default as CustomId } from './customId.svelte';
export { default as Secret } from './secret.svelte';
+28
View File
@@ -0,0 +1,28 @@
<script lang="ts">
import { Copy } from '.';
export let show = false;
export let value: string;
</script>
<div class="interactive-text-output" class:is-textarea={show}>
{#if show}
<span class="text u-line-height-1-5 u-break-word">{value}</span>
{:else}
<span class="text">••••••••••••</span>
{/if}
<div class="u-flex u-cross-child-start u-gap-8">
<button
class="interactive-text-output-button"
aria-label="show hidden text"
type="button"
on:click={() => (show = !show)}>
<span class:icon-eye-off={show} class:icon-eye={!show} aria-hidden="true" />
</button>
<Copy {value}>
<button class="interactive-text-output-button" aria-label="copy text" type="button">
<span class="icon-duplicate" aria-hidden="true" />
</button>
</Copy>
</div>
</div>
+128 -78
View File
@@ -1,80 +1,130 @@
export const scopes = [
'users.read',
'users.write',
'teams.read',
'teams.write',
'collections.read',
'collections.write',
'attributes.read',
'attributes.write',
'indexes.read',
'indexes.write',
'documents.read',
'documents.write',
'files.read',
'files.write',
'buckets.read',
'buckets.write',
'functions.read',
'functions.write',
'execution.read',
'execution.write',
'locale.read',
'avatars.read',
'health.read'
];
export const events = [
'account.create',
'account.update.email',
'account.update.name',
'account.update.password',
'users.update.email',
'users.update.name',
'users.update.password',
'account.update.prefs',
'account.recovery.create',
'account.recovery.update',
'account.verification.create',
'account.verification.update',
'account.delete',
'account.sessions.create',
'account.sessions.delete',
'account.sessions.update',
'database.collections.create',
'database.collections.update',
'database.collections.delete',
'database.attributes.create',
'database.attributes.delete',
'database.indexes.create',
'database.indexes.delete',
'database.documents.create',
'database.documents.update',
'database.documents.delete',
'functions.create',
'functions.update',
'functions.delete',
'functions.deployments.create',
'functions.deployments.update',
'functions.deployments.delete',
'functions.executions.create',
'functions.executions.update',
'storage.files.create',
'storage.files.update',
'storage.files.delete',
'storage.buckets.create',
'storage.buckets.update',
'storage.buckets.delete',
'users.create',
'users.update.prefs',
'users.update.status',
'users.delete',
'users.sessions.delete',
'teams.create',
'teams.update',
'teams.delete',
'teams.memberships.create',
'teams.memberships.update',
'teams.memberships.update.status',
'teams.memberships.delete'
{
scope: 'users.read',
description: "Access to read your project's users",
category: 'Authentication'
},
{
scope: 'users.write',
description: "Access to create, update, and delete your project's users",
category: 'Authentication'
},
{
scope: 'teams.read',
description: "Access to read your project's teams",
category: 'Authentication'
},
{
scope: 'teams.write',
description: "Access to create, update, and delete your project's teams",
category: 'Authentication'
},
{
scope: 'databases.read',
description: "Access to read your project's databases",
category: 'Database'
},
{
scope: 'databases.write',
description: "Access to create, update, and delete your project's databases",
category: 'Database'
},
{
scope: 'collections.read',
description: "Access to read your project's database collections",
category: 'Database'
},
{
scope: 'collections.write',
description: "Access to create, update, and delete your project's database collections",
category: 'Database'
},
{
scope: 'attributes.read',
description: "Access to read your project's database collection's attributes",
category: 'Database'
},
{
scope: 'attributes.write',
description:
"Access to create, update, and delete your project's database collection's attributes",
category: 'Database'
},
{
scope: 'indexes.read',
description: "Access to read your project's database collection's indexes",
category: 'Database'
},
{
scope: 'indexes.write',
description:
"Access to create, update, and delete your project's database collection's indexes",
category: 'Database'
},
{
scope: 'documents.read',
description: "Access to read your project's database documents",
category: 'Database'
},
{
scope: 'documents.write',
description: "Access to create, update, and delete your project's database documents",
category: 'Database'
},
{
scope: 'files.read',
description: "Access to read your project's storage files and preview images",
category: 'Storage'
},
{
scope: 'files.write',
description: "Access to create, update, and delete your project's storage files",
category: 'Storage'
},
{
scope: 'buckets.read',
description: "Access to read your project's storage buckets",
category: 'Storage'
},
{
scope: 'buckets.write',
description: "Access to create, update, and delete your project's storage buckets",
category: 'Storage'
},
{
scope: 'functions.read',
description: "Access to read your project's functions and code deployments",
category: 'Functions'
},
{
scope: 'functions.write',
description:
"Access to create, update, and delete your project's functions and code deployments",
category: 'Functions'
},
{
scope: 'execution.read',
description: "Access to read your project's execution logs",
category: 'Functions'
},
{
scope: 'execution.write',
description: "Access to execute your project's functions",
category: 'Functions'
},
{
scope: 'locale.read',
description: "Access to access your project's Locale service",
category: 'Other'
},
{
scope: 'avatars.read',
description: "Access to access your project's Avatars service",
category: 'Other'
},
{
scope: 'health.read',
description: "Access to read your project's health status",
category: 'Other'
}
];
+1
View File
@@ -11,6 +11,7 @@ export { default as InputSwitch } from './inputSwitch.svelte';
export { default as InputTags } from './inputTags.svelte';
export { default as InputFile } from './inputFile.svelte';
export { default as InputCustomId } from './inputCustomId.svelte';
export { default as InputDateTime } from './inputDateTime.svelte';
export { default as InputSearch } from './inputSearch.svelte';
export { default as InputRadio } from './inputRadio.svelte';
export { default as InputSelect } from './inputSelect.svelte';
@@ -0,0 +1,58 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper } from '.';
export let label: string;
export let showLabel = true;
export let id: string;
export let value = '';
export let required = false;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
export let autocomplete = false;
let element: HTMLInputElement;
let error: string;
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
const handleInvalid = (event: Event) => {
event.preventDefault();
if (element.validity.valueMissing) {
error = 'This field is required';
return;
}
error = element.validationMessage;
};
$: if (value) {
error = null;
}
</script>
<FormItem>
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
<div class="input-text-wrapper">
<input
{id}
{disabled}
{readonly}
{required}
autocomplete={autocomplete ? 'on' : 'off'}
type="datetime-local"
class="input-text"
bind:value
bind:this={element}
on:invalid={handleInvalid} />
</div>
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
</FormItem>
@@ -1,22 +0,0 @@
<script>
import { afterNavigate } from '$app/navigation';
import { updateLayout } from '$lib/stores/layout';
import { onMount } from 'svelte';
onMount(handle);
afterNavigate(handle);
function handle(event = null) {
updateLayout({
navigate: event,
title: 'API Keys',
level: 3,
breadcrumbs: {
href: 'keys',
title: 'API Keys'
}
});
}
</script>
<slot />
@@ -1,62 +0,0 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Empty } from '$lib/components';
import { Button } from '$lib/elements/forms';
import {
Table,
TableBody,
TableCellHead,
TableCellLink,
TableCellText,
TableHeader,
TableRow
} from '$lib/elements/table';
import { Container } from '$lib/layout';
import { project } from '../store';
import Create from './_create.svelte';
const projectId = $page.params.project;
let showCreate = false;
</script>
<svelte:head>
<title>Appwrite - API Keys</title>
</svelte:head>
<Container>
{#if $project}
{#if $project.keys}
<Table>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Scopes</TableCellHead>
</TableHeader>
<TableBody>
{#each $project.keys as key}
<TableRow>
<TableCellLink
href={`${base}/console/project-${projectId}/keys/key/${key.$id}`}
title="Name">
{key.name}
</TableCellLink>
<TableCellText title="Scopes">{key.scopes.length}</TableCellText>
</TableRow>
{/each}
</TableBody>
</Table>
{:else}
<Empty>
<div class="u-flex u-flex-vertical">
<div class="common-section">No API Keys Found</div>
<div class="common-section">
You haven't created any API keys for your project yet.
</div>
</div>
</Empty>
{/if}
<Button on:click={() => (showCreate = true)}>Add API Key</Button>
{/if}
</Container>
<Create bind:show={showCreate} />
@@ -1,69 +0,0 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { InputText, Button, Form, InputCheckbox } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { project } from '../store';
import { scopes } from '$lib/constants';
export let show = false;
let name: string;
const activeScopes = scopes.reduce((prev, next) => {
prev[next] = false;
return prev;
}, {});
const create = async () => {
try {
await sdkForConsole.projects.createKey(
$project.$id,
name,
scopes.filter((scope) => activeScopes[scope])
);
name = null;
for (const scope in activeScopes) {
activeScopes[scope] = false;
}
await project.load($project.$id);
show = false;
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
};
const selectAll = () => {
for (const scope in activeScopes) {
activeScopes[scope] = true;
}
};
const unselectAll = () => {
for (const scope in activeScopes) {
activeScopes[scope] = false;
}
};
</script>
<Form on:submit={create}>
<Modal bind:show>
<svelte:fragment slot="header">Add API Key</svelte:fragment>
<p>
<span class="link" on:click={selectAll}>Select All</span><span
class="link"
on:click={unselectAll}>Unselect All</span>
</p>
<InputText id="name" label="Name" bind:value={name} required />
{#each scopes as scope}
<InputCheckbox id={scope} label={scope} bind:value={activeScopes[scope]} />
{/each}
<svelte:fragment slot="footer">
<Button submit>Create</Button>
<Button secondary on:click={() => (show = false)}>Cancel</Button>
</svelte:fragment>
</Modal>
</Form>
@@ -1,35 +0,0 @@
<script lang="ts">
import { Card } from '$lib/components';
import { Container } from '$lib/layout';
import { sdkForConsole } from '$lib/stores/sdk';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { project } from '../../../store';
import { base } from '$app/paths';
const projectId = $page.params.project;
const keyId = $page.params.key;
const request = sdkForConsole.projects.getKey(projectId, keyId);
const deleteKey = async () => {
await sdkForConsole.projects.deleteKey(projectId, keyId);
addNotification({
message: 'API key deleted.',
type: 'success'
});
project.load(projectId);
await goto(`${base}/console/project-${projectId}/keys`);
};
</script>
<Container>
<Card>
{#await request}
loading
{:then response}
<p>{response.name}</p>
<Button danger on:click={deleteKey}>Delete</Button>
{/await}
</Card>
</Container>
@@ -276,7 +276,7 @@
</div>
<div
class="card is-2-columns-medium-screen is-2-columns-large-screen is-2-rows-large-screen is-location-row-2-end-large-screen">
<div class="heading-level-4">10</div>
<div class="heading-level-4">XX</div>
<div>Realtime Connections</div>
</div>
</div>
@@ -39,7 +39,7 @@
{key.name}
</TableCellText>
<TableCellText title="Last Accessed">
{toLocaleDateTime(key.$createdAt)}
{key.accessedAt ? toLocaleDateTime(key.accessedAt) : 'never'}
</TableCellText>
<TableCellText title="Expiration Date">
{toLocaleDateTime(key.$updatedAt)}
@@ -2,16 +2,10 @@
import { afterNavigate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { CardGrid } from '$lib/components';
import { scopes } from '$lib/constants';
import {
Button,
Form,
FormList,
InputCheckbox,
InputPassword,
InputText
} from '$lib/elements/forms';
import { CardGrid, Secret } from '$lib/components';
import { Button, Form, FormList, InputText } from '$lib/elements/forms';
import InputDateTime from '$lib/elements/forms/inputDateTime.svelte';
import { difference } from '$lib/helpers/array';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container } from '$lib/layout';
import { updateLayout } from '$lib/stores/layout';
@@ -19,38 +13,30 @@
import { sdkForConsole } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { project } from '../../../store';
import Scopes from '../scopes.svelte';
import Delete from './delete.svelte';
import { key } from './store';
const projectId = $page.params.project;
const keyId = $page.params.key;
const activeScopes = scopes.reduce((prev, next) => {
prev[next] = false;
return prev;
}, {});
let loaded = false;
let showDelete = false;
let name: string = null;
let secret: string = null;
let expire: string = null;
let scopes: string[] = null;
onMount(handle);
afterNavigate(handle);
async function handle(event = null) {
if ($key?.$id !== keyId) {
await key.load(projectId, keyId);
}
await key.load(projectId, keyId);
name ??= $key.name;
secret ??= $key.secret;
expire ??= $key.expire;
unselectAll();
$key.scopes.forEach((scope) => {
activeScopes[scope] = true;
});
scopes ??= $key.scopes;
updateLayout({
navigate: event,
@@ -117,12 +103,8 @@
async function updateScopes() {
try {
await sdkForConsole.projects.updateKey(
$project.$id,
$key.$id,
$key.name,
scopes.filter((scope) => activeScopes[scope])
);
await sdkForConsole.projects.updateKey($project.$id, $key.$id, $key.name, scopes);
$key.scopes = scopes;
addNotification({
type: 'success',
message: 'API Key scopes has been updated'
@@ -134,12 +116,6 @@
});
}
}
function unselectAll() {
for (const scope in activeScopes) {
activeScopes[scope] = false;
}
}
</script>
<svelte:head>
@@ -148,13 +124,14 @@
<Container>
{#if loaded}
{@const accessedAt = $key.accessedAt ? toLocaleDateTime($key.accessedAt) : 'never'}
<CardGrid>
<div>
<h6 class="heading-level-7">{$key.name}</h6>
</div>
<svelte:fragment slot="aside">
<p>
Last accessed: {toLocaleDateTime($key.$updatedAt)}<br />
Last accessed: {accessedAt}<br />
Scopes granted: {$key.scopes.length}
</p>
</svelte:fragment>
@@ -164,18 +141,9 @@
<h6 class="heading-level-7">API Key Secret</h6>
<svelte:fragment slot="aside">
<FormList>
<InputPassword
id="secret"
label="API Key Secret"
bind:value={secret}
showPasswordButton
required />
<Secret bind:value={secret} />
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updateName}>
@@ -206,18 +174,16 @@
practice to allow only the permissions you need to meet your project goals.
</p>
<svelte:fragment slot="aside">
<FormList>
{#each scopes as scope}
<InputCheckbox
id={scope}
label={scope}
bind:value={activeScopes[scope]} />
{/each}
</FormList>
<Scopes bind:scopes />
</svelte:fragment>
<svelte:fragment slot="actions">
<Button submit>Update</Button>
<Button
submit
disabled={!(
difference(scopes, $key.scopes).length !== 0 ||
difference($key.scopes, scopes).length !== 0
)}>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -227,11 +193,7 @@
<p class="text">Choose any name that will help you distinguish between API keys.</p>
<svelte:fragment slot="aside">
<FormList>
<InputText
id="expire"
label="Expiration Date"
bind:value={expire}
required />
<InputDateTime id="expire" label="Expiration Date" bind:value={expire} />
</FormList>
</svelte:fragment>
@@ -250,7 +212,7 @@
<div class="u-flex u-gap-16">
<div class="u-cross-child-center u-line-height-1-5">
<h6 class="u-bold">{$key.name}</h6>
<p>Last accessed: {toLocaleDateTime($key.$updatedAt)}</p>
<p>Last accessed: {accessedAt}</p>
</div>
</div>
</div>
@@ -1,32 +1,21 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { scopes } from '$lib/constants';
import { InputText, Button, Form, FormList, InputCheckbox } from '$lib/elements/forms';
import { InputText, Button, Form, FormList } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { project } from '../../.../../store';
import Scopes from './scopes.svelte';
export let show = false;
const activeScopes = scopes.reduce((prev, next) => {
prev[next] = false;
return prev;
}, {});
let name: string;
let scopes: string[] = [];
async function create() {
try {
await sdkForConsole.projects.createKey(
$project.$id,
name,
scopes.filter((scope) => activeScopes[scope])
);
await sdkForConsole.projects.createKey($project.$id, name, scopes);
name = null;
for (const scope in activeScopes) {
activeScopes[scope] = false;
}
scopes = [];
project.load($project.$id);
show = false;
} catch (error) {
@@ -39,13 +28,11 @@
</script>
<Form on:submit={create}>
<Modal bind:show>
<Modal bind:show size="big">
<svelte:fragment slot="header">Create API Key</svelte:fragment>
<FormList>
<InputText id="name" label="Name" bind:value={name} autofocus required />
{#each scopes as scope}
<InputCheckbox id={scope} label={scope} bind:value={activeScopes[scope]} />
{/each}
<Scopes bind:scopes />
</FormList>
<svelte:fragment slot="footer">
<Button submit>Register</Button>
@@ -0,0 +1,72 @@
<script lang="ts">
import { Collapsible, CollapsibleItem } from '$lib/components';
import { Button, FormList, InputChoice } from '$lib/elements/forms';
import { scopes as allScopes } from '$lib/constants';
import { onMount } from 'svelte';
import { difference } from '$lib/helpers/array';
export let scopes: string[];
let mounted = false;
onMount(() => {
scopes.forEach((scope) => {
activeScopes[scope] = true;
});
mounted = true;
});
function selectAll() {
for (const scope in activeScopes) {
activeScopes[scope] = true;
}
}
function unselectAll() {
for (const scope in activeScopes) {
activeScopes[scope] = false;
}
}
const activeScopes = allScopes.reduce((prev, next) => {
prev[next.scope] = false;
return prev;
}, {});
$: {
if (mounted) {
const newScopes = allScopes
.filter((scope) => activeScopes[scope.scope])
.map(({ scope }) => scope);
if (
difference(scopes, newScopes).length !== 0 ||
difference(newScopes, scopes).length !== 0
) {
scopes = newScopes;
}
}
}
</script>
<div class="u-flex u-cross-center u-main-end">
<Button text on:click={unselectAll}>Unselect all</Button>
<Button text on:click={selectAll}>Select all</Button>
</div>
<Collapsible>
{#each ['Authentication', 'Database', 'Functions', 'Storage', 'Other'] as category}
<CollapsibleItem>
<svelte:fragment slot="title">{category}</svelte:fragment>
<FormList>
{#each allScopes.filter((s) => s.category === category) as scope}
<InputChoice
id={scope.scope}
label={scope.scope}
bind:value={activeScopes[scope.scope]}>
{scope.description}
</InputChoice>
{/each}
</FormList>
</CollapsibleItem>
{/each}
</Collapsible>
+48
View File
@@ -0,0 +1,48 @@
import '@testing-library/jest-dom';
import { vi } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import { Secret } from '../../../src/lib/components';
const value = 'This is a secret';
test('shows Secret component', () => {
const { container } = render(Secret, { value });
const secret = container.querySelector('span.text');
const toggle = container.querySelector('[aria-label="show hidden text"]');
const copy = container.querySelector('[aria-label="copy text"]');
expect(secret).toBeInTheDocument();
expect(toggle).toBeInTheDocument();
expect(copy).toBeInTheDocument();
});
test('toggle secret', async () => {
const { container } = render(Secret, { value });
const toggle = container.querySelector('[aria-label="show hidden text"]');
let secret = container.querySelector('span.text');
expect(secret).not.toContainEqual(value);
await fireEvent.click(toggle);
secret = container.querySelector('span.text');
expect(secret.textContent).toEqual(value);
await fireEvent.click(toggle);
secret = container.querySelector('span.text');
expect(secret.textContent).not.toEqual(value);
});
test('copy to clipboard on click', async () => {
const { container } = render(Secret, { value });
const copy = container.querySelector('[aria-label="copy text"]');
Object.assign(window.navigator, {
clipboard: {
writeText: vi.fn().mockImplementation(() => Promise.resolve())
}
});
await fireEvent.click(copy);
expect(window.navigator.clipboard.writeText).toHaveBeenCalledWith(value);
});