Merge branch 'main' into cloud-1.5.x

This commit is contained in:
Matej Bačo
2024-04-02 17:55:08 +02:00
378 changed files with 13146 additions and 2162 deletions
+849 -861
View File
File diff suppressed because it is too large Load Diff
+7 -8
View File
@@ -12,15 +12,15 @@
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test": "vitest run",
"test:ui": "vitest --ui",
"test:watch": "vitest watch",
"test": "TZ=EST vitest run",
"test:ui": "TZ=EST vitest --ui",
"test:watch": "TZ=EST vitest watch",
"e2e": "playwright test tests/e2e"
},
"dependencies": {
"@appwrite.io/console": "^0.5.0",
"@appwrite.io/pink": "0.3.0",
"@appwrite.io/pink-icons": "0.3.0",
"@appwrite.io/console": "^0.6.1",
"@appwrite.io/pink": "0.8.0",
"@appwrite.io/pink-icons": "0.8.0",
"@popperjs/core": "^2.11.8",
"@sentry/svelte": "^7.66.0",
"@sentry/tracing": "^7.66.0",
@@ -36,8 +36,7 @@
"pretty-bytes": "^6.1.1",
"prismjs": "^1.29.0",
"svelte-confetti": "^1.3.0",
"tippy.js": "^6.3.7",
"web-vitals": "^3.4.0"
"tippy.js": "^6.3.7"
},
"devDependencies": {
"@melt-ui/pp": "^0.1.4",
+1 -3
View File
@@ -1,4 +1,2 @@
/// <reference types="@sveltejs/kit" />
interface Window {
VERCEL_ANALYTICS_ID: string | false;
}
interface Window {}
+18 -1
View File
@@ -151,6 +151,7 @@ export enum Submit {
AccountDelete = 'submit_account_delete',
AccountDeleteSession = 'submit_account_delete_session',
AccountDeleteAllSessions = 'submit_account_delete_all_sessions',
AccountAuthenticatorDelete = 'submit_account_authenticator_delete',
UserCreate = 'submit_user_create',
UserDelete = 'submit_user_delete',
UserUpdateEmail = 'submit_user_update_email',
@@ -162,6 +163,9 @@ export enum Submit {
UserUpdateStatus = 'submit_user_update_status',
UserUpdateVerificationEmail = 'submit_user_update_verification_email',
UserUpdateVerificationPhone = 'submit_user_update_verification_phone',
UserTargetCreate = 'submit_user_target_create',
UserTargetDelete = 'submit_user_target_delete',
UserAuthenticatorDelete = 'submit_user_authenticator_delete',
OrganizationCreate = 'submit_organization_create',
OrganizationDelete = 'submit_organization_delete',
OrganizationUpdateName = 'submit_organization_update_name',
@@ -243,6 +247,7 @@ export enum Submit {
WebhookUpdateUrl = 'submit_webhook_update_url',
WebhookUpdateEvents = 'submit_webhook_update_events',
WebhookUpdateName = 'submit_webhook_update_name',
WebhookUpdateEnabled = 'submit_webhook_update_enabled',
WebhookUpdateSecurity = 'submit_webhook_update_security',
BucketCreate = 'submit_bucket_create',
BucketDelete = 'submit_bucket_delete',
@@ -290,5 +295,17 @@ export enum Submit {
SmsResetTemplate = 'submit_sms_reset_template',
SmsUpdateInviteTemplate = 'submit_sms_update_invite_template',
SmsUpdateLoginTemplate = 'submit_sms_update_login_template',
SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template'
SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template',
MessagingProviderCreate = 'submit_messaging_provider_create',
MessagingProviderDelete = 'submit_messaging_provider_delete',
MessagingProviderUpdate = 'submit_messaging_provider_update',
MessagingMessageCreate = 'submit_messaging_message_create',
MessagingMessageUpdate = 'submit_messaging_message_update',
MessagingMessageDelete = 'submit_messaging_message_delete',
MessagingTopicCreate = 'submit_messaging_topic_create',
MessagingTopicDelete = 'submit_messaging_topic_delete',
MessagingTopicUpdateName = 'submit_messaging_topic_update_name',
MessagingTopicUpdatePermissions = 'submit_messaging_topic_update_permissions',
MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add',
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete'
}
+26 -4
View File
@@ -5,6 +5,7 @@ import { onMount } from 'svelte';
import { derived, writable } from 'svelte/store';
import { nanoid } from 'nanoid/non-secure';
import { trackEvent } from '$lib/actions/analytics';
import { omit } from '$lib/helpers/omit';
const groups = [
'ungrouped',
@@ -17,6 +18,10 @@ const groups = [
'platforms',
'databases',
'functions',
'messaging',
'messages',
'providers',
'topics',
'storage',
'domains',
'webhooks',
@@ -44,6 +49,7 @@ type BaseCommand = {
forceEnable?: boolean;
group?: CommandGroup;
icon?: string;
image?: string;
rank?: number;
nested?: boolean;
keepOpen?: boolean;
@@ -69,7 +75,23 @@ export const disabledMap = writable<Map<string, boolean>>(new Map());
// Derived stores
export const commands = derived(commandMap, ($commandMap) => {
return Array.from($commandMap.values()).flat();
const res: Command[] = [];
const keys = new Set<string>();
const allCommands = Array.from($commandMap.values()).flat().toReversed();
for (const command of allCommands) {
if (isKeyedCommand(command) && !command.disabled) {
const keysString = command.keys.join('+');
if (keys.has(keysString)) {
res.push(omit(command, 'keys'));
continue;
}
keys.add(keysString);
}
res.push(command);
}
return res;
});
const commandsEnabled = derived(disabledMap, ($disabledMap) => {
@@ -109,9 +131,9 @@ function hasDisputing(command: KeyedCommand, allCommands: Command[]) {
}
export const commandCenterKeyDownHandler = derived(
[commandMap, commandsEnabled, wizard],
([$commandMap, enabled, $wizard]) => {
const commandsArr = Array.from($commandMap.values()).flat();
[commands, commandsEnabled, wizard],
([$commands, enabled, $wizard]) => {
const commandsArr = $commands;
let recentKeyCodes: number[] = [];
let validCommands: KeyedCommand[] = [];
@@ -0,0 +1,73 @@
<script lang="ts">
import { providers } from '$routes/console/project-[project]/messaging/providers/store';
import {
messageParams,
providerType,
targetsById
} from '$routes/console/project-[project]/messaging/wizard/store';
import { MessagingProviderType } from '@appwrite.io/console';
import Template from './template.svelte';
import { wizard } from '$lib/stores/wizard';
import Create from '$routes/console/project-[project]/messaging/create.svelte';
import { topicsById } from '$routes/console/project-[project]/messaging/store';
let search = '';
let options = Object.entries(providers).map(([type, option]) => {
return {
label: option.name,
icon: option.icon,
callback() {
if (
type !== MessagingProviderType.Email &&
type !== MessagingProviderType.Sms &&
type !== MessagingProviderType.Push
)
return;
$providerType = type;
$topicsById = {};
$targetsById = {};
const common = {
topics: [],
users: [],
targets: []
};
switch (type) {
case MessagingProviderType.Email:
$messageParams[$providerType] = {
...common,
subject: '',
content: ''
};
break;
case MessagingProviderType.Sms:
$messageParams[$providerType] = {
...common,
content: ''
};
break;
case MessagingProviderType.Push:
$messageParams[$providerType] = {
...common,
title: '',
body: '',
data: [['', '']]
};
break;
}
wizard.start(Create);
}
};
});
$: filteredOptions = options.filter((option) => {
return option.label.toLowerCase().includes(search.toLowerCase());
});
</script>
<Template options={filteredOptions} bind:search>
<div class="u-flex u-cross-center u-gap-8" slot="option" let:option>
<i class="icon-{option.icon}" />
<span>{option.label}</span>
</div>
</Template>
+6
View File
@@ -78,3 +78,9 @@ export const FilesPanel: SubPanel = {
name: 'Files',
component: Files
};
import CreateMessage from './createMessage.svelte';
export const CreateMessagePanel: SubPanel = {
name: 'Create Message',
component: CreateMessage
};
+9 -1
View File
@@ -2,6 +2,8 @@
This is the root command panel. It precedes all other command panels.
-->
<script lang="ts">
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
import { debounce } from '$lib/helpers/debounce';
import { isMac } from '$lib/helpers/platform';
import { commands, searchers, type Command, isKeyedCommand } from '../commands';
@@ -54,7 +56,13 @@
<Template options={results} bind:search searchPlaceholder="Search for commands or content...">
<div slot="option" class="u-flex u-main-space-between content" let:option={command}>
<div class="u-flex u-gap-8 u-cross-center">
<i class="icon-{command.icon ?? 'arrow-sm-right'}" />
{#if command.image}
<img
src={`${base}/icons/${$app.themeInUse}/color/${command.image}.svg`}
alt={command.label} />
{:else}
<i class="icon-{command.icon ?? 'arrow-sm-right'}" />
{/if}
<span>
{command.label}
</span>
@@ -0,0 +1,52 @@
import { goto } from '$app/navigation';
import { project } from '$routes/console/project-[project]/store';
import { get } from 'svelte/store';
import { type Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
import { MessagingProviderType } from '@appwrite.io/console';
const getLabel = (message) => {
switch (message.providerType) {
case MessagingProviderType.Push:
return message.data.title;
case MessagingProviderType.Sms:
return message.data.content;
case MessagingProviderType.Email:
return message.data.subject;
default:
return 'null';
}
};
const getIcon = (message) => {
switch (message.providerType) {
case MessagingProviderType.Push:
return 'device-mobile';
case MessagingProviderType.Sms:
return 'annotation';
case MessagingProviderType.Email:
return 'mail';
default:
return 'send';
}
};
export const messagesSearcher = (async (query: string) => {
const { messages } = await sdk.forProject.messaging.listMessages([], query || undefined);
const projectId = get(project).$id;
return messages
.filter((message) => getLabel(message).toLowerCase().includes(query.toLowerCase()))
.map(
(message) =>
({
group: 'messages',
label: getLabel(message),
callback: () => {
goto(`/console/project-${projectId}/messaging/message-${message.$id}`);
},
icon: getIcon(message)
}) as const
);
}) satisfies Searcher;
@@ -0,0 +1,33 @@
import { goto } from '$app/navigation';
import { project } from '$routes/console/project-[project]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
import { getProviderDisplayNameAndIcon } from '$routes/console/project-[project]/messaging/provider.svelte';
const getIcon = (provider: string) => {
const { icon } = getProviderDisplayNameAndIcon(provider);
return icon;
};
export const providersSearcher = (async (query: string) => {
const { providers } = await sdk.forProject.messaging.listProviders([], query || undefined);
const projectId = get(project).$id;
return providers
.filter((provider) => provider.name.toLowerCase().includes(query.toLowerCase()))
.map(
(provider) =>
({
group: 'providers',
label: provider.name,
callback: () => {
goto(
`/console/project-${projectId}/messaging/providers/provider-${provider.$id}`
);
},
image: getIcon(provider.provider)
}) as const
);
}) satisfies Searcher;
+25
View File
@@ -0,0 +1,25 @@
import { goto } from '$app/navigation';
import { project } from '$routes/console/project-[project]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
export const topicsSearcher = (async (query: string) => {
const { topics } = await sdk.forProject.messaging.listTopics([], query || undefined);
const projectId = get(project).$id;
return topics
.filter((topic) => topic.name.toLowerCase().includes(query.toLowerCase()))
.map(
(topic) =>
({
group: 'topics',
label: topic.name,
callback: () => {
goto(`/console/project-${projectId}/messaging/topics/topic-${topic.$id}`);
},
icon: 'send'
}) as const
);
}) satisfies Searcher;
+34 -13
View File
@@ -1,18 +1,39 @@
<script lang="ts">
export let href: string;
export let href: string = null;
export let external: boolean = false;
</script>
<li class="clickable-list-item">
<a {href} class="clickable-list-button" on:click>
{#if $$slots.default}
<h5 class="clickable-list-title u-trim-1">
<slot />
</h5>
{/if}
{#if $$slots.desc}
<div class="clickable-list-desc">
<p class="text u-margin-block-start-8"><slot name="desc" /></p>
</div>
{/if}
</a>
{#if href}
<a
{href}
class="clickable-list-button"
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}
on:click>
{#if $$slots.default}
<h5 class="clickable-list-title u-trim-1">
<slot />
</h5>
{/if}
{#if $$slots.desc}
<div class="clickable-list-desc">
<p class="text u-margin-block-start-8"><slot name="desc" /></p>
</div>
{/if}
</a>
{:else}
<button class="clickable-list-button u-width-full-line" on:click>
{#if $$slots.default}
<h5 class="clickable-list-title u-trim-1">
<slot />
</h5>
{/if}
{#if $$slots.desc}
<div class="clickable-list-desc">
<p class="text u-margin-block-start-8"><slot name="desc" /></p>
</div>
{/if}
</button>
{/if}
</li>
+31 -4
View File
@@ -3,10 +3,11 @@
export let withIndentation = false;
export let open = false;
export let disabled = false;
</script>
<li class="collapsible-item">
<details class="collapsible-wrapper" {open}>
<details class="collapsible-wrapper" class:is-disabled={disabled} bind:open>
<!-- svelte-ignore a11y-no-redundant-roles -->
<summary
class="collapsible-button u-position-relative"
@@ -16,13 +17,18 @@
tabindex="0">
<slot name="beforetitle" />
<div>
<span class="text"><slot name="title" /></span>
<span class="text" class:u-color-text-disabled={disabled}
><slot name="title" /></span>
{#if $$slots.subtitle}
<span class="collapsible-button-optional"><slot name="subtitle" /></span>
<span class="collapsible-button-optional" class:u-color-text-disabled={disabled}
><slot name="subtitle" /></span>
{/if}
</div>
<div class="icon">
<span class="icon-cheveron-down" aria-hidden="true" />
<span
class="icon-cheveron-down u-font-size-20"
class:u-color-text-disabled={disabled}
aria-hidden="true" />
</div>
</summary>
<div
@@ -33,3 +39,24 @@
</div>
</details>
</li>
<style lang="scss">
// TODO: remove once pink is updated
.collapsible-item {
.collapsible-wrapper.is-disabled {
&[open] .icon-cheveron-down {
rotate: unset;
}
cursor: not-allowed;
* {
cursor: not-allowed;
}
.collapsible {
&-content {
display: none !important;
}
}
}
}
</style>
@@ -1,12 +1,14 @@
<script lang="ts">
import { isValueOfStringEnum } from '$lib/helpers/types';
import { sdk } from '$lib/stores/sdk';
import { CreditCard } from '@appwrite.io/console';
export let brand: string;
export let width = 23;
export let height = 16;
function getCreditCardImage(brand: string, width = 46, height = 32) {
if (!brand) return '';
if (!isValueOfStringEnum(CreditCard, brand)) return '';
return sdk.forConsole.avatars.getCreditCard(brand, width, height).toString();
}
</script>
+55 -26
View File
@@ -12,7 +12,11 @@
export let childStart = false;
export let noStyle = false;
export let fullWidth = false;
export let wrapperFullWidth = false;
export let fixed = false;
export let display = 'block';
export let arrowSize = 10;
export let isPopover = false;
const dispatch = createEventDispatcher<{
blur: undefined;
@@ -31,13 +35,14 @@
{
name: 'arrow',
options: {
element: arrow
element: arrow,
padding: arrowSize * 1.75
}
},
{
name: 'offset',
options: {
offset: [0, noArrow ? 0 : 6]
offset: [noArrow ? 0 : -arrowSize, noArrow ? 0 : arrowSize / 1.5]
}
},
{
@@ -100,16 +105,26 @@
<svelte:window on:click={onBlur} on:keydown={onKeyDown} />
<div class:drop-wrapper={!noStyle} class:u-cross-child-start={childStart} bind:this={element}>
<div
class:drop-wrapper={!noStyle}
class:u-cross-child-start={childStart}
class:u-width-full-line={wrapperFullWidth}
bind:this={element}
style:display>
<slot />
</div>
<div
class="drop-tooltip"
class:u-width-full-line={fullWidth}
bind:this={tooltip}
style:z-index="10">
<div class="drop-arrow" class:u-hide={!show || (show && noArrow)} bind:this={arrow} />
class:u-width-full-line={fullWidth}
style:--arrow-size={`${arrowSize}px`}
style:z-index="1">
<div
class="drop-arrow"
class:is-popover={isPopover}
class:u-hide={!show || (show && noArrow)}
bind:this={arrow} />
{#if show}
<slot name="list" />
{/if}
@@ -117,36 +132,50 @@
<!-- svelte-ignore css-unused-selector -->
<style global lang="scss">
.drop-tooltip[data-popper-placement^='top'] > .drop-arrow {
bottom: -4px;
}
.drop-tooltip[data-popper-placement^='bottom'] > .drop-arrow {
top: -4px;
}
.drop-tooltip[data-popper-placement^='left'] > .drop-arrow {
right: -4px;
}
.drop-tooltip[data-popper-placement^='right'] > .drop-arrow {
left: -4px;
.drop-arrow.is-popover {
--drop-arrow-pop-over-bg-color: var(--color-neutral-90);
body.theme-light & {
--drop-arrow-pop-over-bg-color: var(--color-neutral-0);
}
}
.drop-arrow,
.drop-arrow::before {
position: absolute;
width: 8px;
height: 8px;
z-index: -1;
width: var(--arrow-size);
height: var(--arrow-size);
z-index: 1;
--drop-arrow-border: 1px solid hsl(var(--color-neutral-85));
--drop-arrow-bg-color: hsl(var(--drop-arrow-pop-over-bg-color, var(--color-neutral-105)));
body.theme-light & {
--drop-arrow-border: 1px solid hsl(var(--color-neutral-10));
--drop-arrow-bg-color: hsl(var(--drop-arrow-pop-over-bg-color, var(--color-neutral-0)));
}
}
.drop-arrow::before {
content: '';
transform: rotate(45deg);
background: hsl(var(--color-neutral-85));
background: var(--drop-arrow-bg-color);
}
body.theme-light & {
background: hsl(var(--color-neutral-10));
.drop-tooltip[data-popper-placement^='top'] > .drop-arrow {
bottom: calc(var(--arrow-size) / -2);
&::before {
border-bottom: var(--drop-arrow-border);
border-right: var(--drop-arrow-border);
border-bottom-right-radius: 25%;
}
}
.drop-tooltip[data-popper-placement^='bottom'] > .drop-arrow {
top: calc(var(--arrow-size) / -2);
&::before {
border-top: var(--drop-arrow-border);
border-left: var(--drop-arrow-border);
border-top-left-radius: 25%;
}
}
</style>
+11 -1
View File
@@ -11,10 +11,20 @@
export let noStyle = false;
export let width: string = null;
export let fullWidth = false;
export let wrapperFullWidth = false;
export let position: 'relative' | 'static' = 'relative';
</script>
<Drop bind:show {placement} {childStart} {noArrow} {noStyle} {fullWidth} {fixed} on:blur>
<Drop
bind:show
{placement}
{childStart}
{noArrow}
{noStyle}
{fullWidth}
{wrapperFullWidth}
{fixed}
on:blur>
<slot />
<svelte:fragment slot="list">
<div
+26
View File
@@ -0,0 +1,26 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { EmptySearch } from '.';
import { queries } from './filters';
export let resource;
</script>
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<b class="body-text-2 u-bold">Sorry, we couldn't find any {resource}.</b>
<p>There are no {resource} that match your filters.</p>
</div>
<div class="u-flex common-section u-main-center">
<Button
secondary
on:click={() => {
queries.clearAll();
queries.apply();
}}>
Clear filters
</Button>
</div>
</div>
</EmptySearch>
+1 -1
View File
@@ -332,7 +332,7 @@
</div>
{:else}
<div class="input-text-wrapper" style="--amount-of-buttons:2" bind:this={copyParent}>
<!--
<!--
This object syntax avoids TS erroring because 'type' isn't a valid HTMLDivElement attribute
(we need to set it to 'text' to add styling)
-->
+544
View File
@@ -0,0 +1,544 @@
<script lang="ts">
import { Id, ModalWrapper, Trim } from '.';
import { Button, Form } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { ID, Query, Permission, Role } from '@appwrite.io/console';
import type { Models } from '@appwrite.io/console';
import { calculateSize } from '$lib/helpers/sizeConvertion';
import { toLocaleDate } from '$lib/helpers/date';
import {
Table,
TableBody,
TableRowButton,
TableHeader,
TableCell,
TableCellText,
TableCellHead
} from '$lib/elements/table';
import InputSearch from '$lib/elements/forms/inputSearch.svelte';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
import FormList from '$lib/elements/forms/formList.svelte';
import { writable } from 'svelte/store';
import { onMount } from 'svelte';
import Heading from './heading.svelte';
import { clickOnEnter } from '$lib/helpers/a11y';
import Empty from './empty.svelte';
import { base } from '$app/paths';
import { page } from '$app/stores';
export let show: boolean;
export let mimeTypeQuery: string = 'image/';
export let selectedBucket: string = null;
export let selectedFile: string = null;
export let onSelect: (e: Models.File) => void;
let search = writable('');
let searchEnabled = false;
let fileSelector: HTMLInputElement;
let uploading = false;
let view: 'grid' | 'list' = 'list';
onMount(() => {
selectedBucket = currentBucket?.$id;
});
function submitForm() {
onSelect(currentFile);
closeModal();
}
function getPreview(bucketId: string, fileId: string, size: number = 64) {
return (
sdk.forProject.storage.getFilePreview(bucketId, fileId, size, size).toString() +
'&mode=admin'
);
}
async function uploadFile() {
try {
uploading = true;
const file = await sdk.forProject.storage.createFile(
selectedBucket,
ID.unique(),
fileSelector.files[0],
[Permission.read(Role.any())]
);
search.set($search === null ? '' : null);
selectFile(file);
} catch (e) {
console.error(e);
} finally {
uploading = false;
}
}
function selectBucket(bucket: Models.Bucket | null) {
currentBucket = bucket;
selectedBucket = bucket?.$id ?? null;
resetFile();
}
function selectFile(file: Models.File) {
currentFile = file;
selectedBucket = currentFile.bucketId;
selectedFile = currentFile.$id;
}
function resetFile() {
selectedFile = null;
}
function resetBucket() {
selectedBucket = null;
}
function closeModal() {
show = false;
resetFile();
resetBucket();
}
function handleVisibilityChange() {
if (!document.hidden) {
buckets = loadBuckets();
}
}
let currentBucket: Models.Bucket = null;
let currentFile: Models.File = null;
let buckets: Promise<Models.BucketList> = loadBuckets();
async function loadBuckets() {
const response = await sdk.forProject.storage.listBuckets();
const bucket = response.buckets[0] ?? null;
if (bucket) {
currentBucket = bucket;
selectedBucket = bucket.$id;
}
return response;
}
$: files =
currentBucket &&
sdk.forProject.storage
.listFiles(
currentBucket.$id,
[Query.startsWith('mimeType', mimeTypeQuery), Query.orderDesc('$createdAt')],
$search || undefined
)
.then((response) => {
if ($search === '') {
searchEnabled = response.total > 0;
}
return response;
});
$: if ($search) {
resetFile();
}
</script>
<svelte:document on:visibilitychange={handleVisibilityChange} />
<ModalWrapper bind:show size="huge">
<Form isModal onSubmit={submitForm} class="u-stretch">
<header class="modal-header u-margin-block-end-0">
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
<h4 class="modal-title heading-level-5">Select file</h4>
<button
type="button"
on:click={closeModal}
class="button is-text is-small is-only-icon"
aria-label="Close modal">
<span class="icon-x" aria-hidden="true"></span>
</button>
</div>
</header>
<div
class="modal-content u-stretch u-flex-vertical u-padding-0 u-margin-block-0 u-overflow-visible">
<div class="u-flex u-min-height-0 u-stretch">
<aside
class="drop-section u-width-200 u-padding-16
u-flex-vertical u-gap-8
u-flex-shrink-0 u-margin-inline-0 u-overflow-y-auto is-not-mobile">
<h6
class="eyebrow-heading-3"
style:--heading-text-color="var(--color-neutral-50)">
Buckets
</h6>
<ul class="drop-list">
{#await buckets}
<div class="u-flex u-main-center">
<div class="loader" />
</div>
{:then response}
{#each response.buckets as bucket}
{@const isSelected = bucket.$id === selectedBucket}
<li class="drop-list-item">
<button
type="button"
class="drop-button"
class:is-selected={isSelected}
on:click={() => selectBucket(bucket)}>
<span>{bucket.name}</span>
</button>
</li>
{:else}
<li class="drop-list-item">
<span class="drop-button">No buckets found</span>
</li>
{/each}
{/await}
</ul>
</aside>
<article
style:padding-inline="calc(var(--p-modal-padding))"
class="modal-content-main u-flex-vertical u-gap-24 u-sep-inline-start u-flex-basis-1000 u-padding-block-24 u-overflow-y-auto">
<div class="is-only-mobile">
{#await buckets}
loading
{:then response}
{#if currentBucket?.$id}
<FormList>
<InputSelect
wrapperTag="div"
label="Buckets"
options={response.buckets.map((n) => ({
value: n.$id,
label: n.name
}))}
bind:value={currentBucket.$id}
id="buckets" />
</FormList>
{/if}
{/await}
</div>
{#await buckets}
<div class="u-flex-vertical u-stretch u-position-relative u-main-center">
<div
class="u-position-absolute u-width-full-line u-flex u-flex-vertical u-main-center u-cross-center u-gap-16 u-margin-block-start-32"
style="inset-inline-start: 0;">
<div class="loader" />
<p class="text">Loading files...</p>
</div>
</div>
{:then response}
{#if response?.total}
{#if currentBucket}
<header class="u-flex-vertical u-gap-32">
<div class="u-flex u-gap-16">
<h5 class="heading-level-6 u-trim u-min-width-0">
{currentBucket?.name}
</h5>
<Id value={currentBucket?.$id} event="bucket">
{currentBucket?.$id}
</Id>
</div>
<div
class="u-flex u-main-space-between u-gap-16 u-flex-vertical-mobile">
<InputSearch
placeholder="Search files"
bind:value={$search}
disabled={!searchEnabled}
style="min-inline-size: 17.5rem; block-size: 100%" />
<div class="u-flex u-gap-16">
<div class="toggle-button">
<ul class="toggle-button-list">
<li class="toggle-button-item">
<button
on:click={() => (view = 'list')}
disabled={!searchEnabled}
type="button"
class="toggle-button-element"
class:is-selected={view === 'list'}
aria-label="List View">
<span
class="icon-view-list"
aria-hidden="true" />
</button>
</li>
<li class="toggle-button-item">
<button
on:click={() => (view = 'grid')}
disabled={!searchEnabled}
type="button"
class="toggle-button-element"
class:is-selected={view === 'grid'}
aria-label="Grid View">
<span
class="icon-view-grid"
aria-hidden="true" />
</button>
</li>
</ul>
</div>
<Button
secondary
class="is-full-width-in-stack-mobile u-height-100-percent"
disabled={uploading}
on:click={() => fileSelector.click()}>
<input
tabindex="-1"
type="file"
accept="image/*"
class="u-hide"
on:change={uploadFile}
bind:this={fileSelector} />
{#if uploading}
<div class="loader is-small"></div>
<span>Uploading</span>
{:else}
<span class="icon-upload" aria-hidden="true"
></span>
<span>Upload</span>
{/if}
</Button>
</div>
</div>
</header>
{#if files}
{#await files}
<div
class="u-flex-vertical u-stretch u-position-relative u-main-center">
<div
class="u-position-absolute u-width-full-line u-flex u-flex-vertical u-main-center u-cross-center u-gap-16 u-margin-block-start-32"
style="inset-inline-start: 0;">
<div class="loader" />
<p class="text">Loading files...</p>
</div>
</div>
{:then response}
<div class="u-flex-vertical u-stretch">
{#if response?.files?.length}
{#if view === 'grid'}
<ul
class="grid-box"
style="--grid-gap:40px; --grid-item-size:120px; --grid-item-size-small-screens:100px;">
{#each response?.files as file}
<li>
<div
class="u-flex-vertical u-gap-8">
<div
role="button"
style:background-size="cover"
style:background-image={`url(${getPreview(
currentBucket.$id,
file.$id,
360
)})`}
on:click={() =>
selectFile(file)}
on:keyup={clickOnEnter}
tabindex="0"
style:aspect-ratio="1/1"
style:display="flex"
style:align-items="flex-end"
style:flex-direction="row-reverse"
style:box-shadow="none"
class="card u-height-100-percent u-gap-16"
style="--card-padding:0.5rem;--card-padding-mobile:0.5rem; --card-border-radius:var(--border-radius-medium);">
<input
class="u-position-absolute is-small u-margin-block-start-2"
type="radio"
name="file"
value={file.$id}
style:pointer-events="none"
checked={selectedFile ===
file.$id} />
</div>
<span class="u-text-center"
><Trim alternativeTrim
>{file.name}</Trim
></span>
</div>
</li>
{/each}
</ul>
{/if}
{#if view === 'list'}
<Table noMargin noStyles transparent dense>
<TableHeader>
<TableCellHead
><span
class="u-margin-inline-start-8"
>Filename</span
></TableCellHead>
<TableCellHead width={140} onlyDesktop>
ID
</TableCellHead>
<TableCellHead width={100} onlyDesktop>
Type
</TableCellHead>
<TableCellHead width={100} onlyDesktop>
Size
</TableCellHead>
<TableCellHead width={120} onlyDesktop>
Created
</TableCellHead>
</TableHeader>
<TableBody>
{#each response?.files as file}
<TableRowButton
on:click={() =>
selectFile(file)}>
<TableCell title="Filename">
<div
class="u-inline-flex u-cross-center u-gap-12">
<input
type="radio"
class="is-small u-margin-inline-start-8"
name="file"
value={file.$id}
style:pointer-events="none"
checked={selectedFile ===
file.$id} />
<span class="image">
<img
class="avatar"
style:border-radius="var(--border-radius-xsmall)"
width="28"
height="28"
src={getPreview(
currentBucket.$id,
file.$id
)}
alt={file.name} />
</span>
<Trim alternativeTrim>
{file.name}
</Trim>
</div>
</TableCell>
<TableCellText
title="ID"
onlyDesktop>
<Id value={file.$id}
>{file.$id}</Id>
</TableCellText>
<TableCellText
title="Type"
onlyDesktop>
{file.mimeType}
</TableCellText>
<TableCellText
title="Size"
onlyDesktop>
{calculateSize(
file.sizeOriginal
)}
</TableCellText>
<TableCellText
title="Created"
onlyDesktop>
{toLocaleDate(
file.$createdAt
)}
</TableCellText>
</TableRowButton>
{/each}
</TableBody>
</Table>
{/if}
{:else if $search}
<article
style:--card-bg-color="transparent"
style:--shadow-small="none"
style:--color-border="var(--color-neutral-15)"
class="card u-grid u-cross-center u-width-full-line common-section is-border-dashed">
<div
class="u-flex u-flex-vertical u-cross-center u-gap-24 u-overflow-hidden">
<div class="common-section">
<div
class="u-text-center common-section">
<b class="body-text-2 u-bold"
>Sorry we couldn't find "{$search}"</b>
<p>
There are no files that match
your search.
</p>
</div>
<div
class="u-flex u-gap-16 common-section u-main-center">
<Button
secondary
on:click={() => ($search = '')}
>Clear search</Button>
</div>
</div>
</div>
</article>
{:else}
<Empty
single
noMedia
--card-bg-color="transparent"
--shadow-small="none"
--color-border="var(--color-neutral-15)">
<div class="common-section">
<div class="u-text-center common-section">
<Heading
size="7"
tag="h2"
trimmed={false}>
No files found within this bucket.
</Heading>
<p class="text u-line-height-1-5">
Need a hand? Learn more in our <a
class="link"
href="https://appwrite.io/docs/products/storage"
target="_blank"
rel="noopener noreferrer">
documentation</a
>.
</p>
</div>
</div>
</Empty>
{/if}
</div>
{/await}
{/if}
{/if}
{:else}
<Empty
single
noMedia
--card-bg-color="transparent"
--shadow-small="none"
--color-border="var(--color-neutral-15)">
<div class="u-text-center u-flex-vertical u-cross-center u-gap-24">
<Heading size="7" tag="h2" trimmed={false}>
No buckets found
</Heading>
<Button
secondary
external
href={`${base}/console/project-${$page.params.project}/storage`}>
Create bucket
</Button>
</div>
</Empty>
{/if}
{/await}
</article>
</div>
</div>
<div class="modal-footer u-margin-block-start-0">
<div class="u-flex u-main-end u-gap-16">
<Button text on:click={closeModal}>Cancel</Button>
<Button submit disabled={selectedBucket === null || selectedFile === null}
>Select</Button>
</div>
</div>
</Form>
</ModalWrapper>
<style lang="scss">
input[type='radio']:where(:indeterminate) {
--p-bg-color: var(--p-bg-color-default);
--p-border-color: var(--p-border-color-default);
}
</style>
+8 -6
View File
@@ -118,10 +118,12 @@
<ul class="selects u-flex u-gap-8 u-margin-block-start-16">
<InputSelect
id="column"
options={$columns.map((c) => ({
label: c.title,
value: c.id
}))}
options={$columns
.filter((c) => c.filter !== false)
.map((c) => ({
label: c.title,
value: c.id
}))}
placeholder="Select column"
bind:value={columnId} />
<InputSelect
@@ -132,7 +134,7 @@
bind:value={operatorKey} />
</ul>
{#if column && operator && !operator?.hideInput}
<div class="u-margin-block-start-8">
<ul class="u-margin-block-start-8">
{#if column.type === 'integer' || column.type === 'double'}
<InputNumber id="value" bind:value placeholder="Enter value" />
{:else if column.type === 'boolean'}
@@ -148,7 +150,7 @@
{:else}
<InputText id="value" bind:value placeholder="Enter value" />
{/if}
</div>
</ul>
{/if}
<Button text disabled={isDisabled} class="u-margin-block-start-4" submit>
<i class="icon-plus" />
+2 -1
View File
@@ -9,6 +9,7 @@
export let query = '[]';
export let columns: Writable<Column[]>;
export let fullWidthMobile = false;
const parsedQueries = queryParamToMap(query);
queries.set(parsedQueries);
@@ -55,7 +56,7 @@
</div>
<div class="is-only-mobile">
<Button secondary on:click={() => (showFiltersMobile = !showFiltersMobile)}>
<Button secondary on:click={() => (showFiltersMobile = !showFiltersMobile)} {fullWidthMobile}>
<i class="icon-filter u-opacity-50" />
Filters
{#if applied > 0}
+2 -1
View File
@@ -1 +1,2 @@
export { default as filters } from './filters.svelte';
export { default as Filters } from './filters.svelte';
export { hasPageQueries, queryParamToMap, queries } from '$lib/components/filters/store';
+1 -1
View File
@@ -47,7 +47,7 @@
}
</script>
<Copy {value} {event}>
<Copy {value} {event} appendTo="parent">
<div
class="interactive-text-output is-buttons-on-top"
class:u-text-center={centered}
+24
View File
@@ -0,0 +1,24 @@
<script lang="ts">
import { app } from '$lib/stores/app';
export let darkSrc: string;
export let lightSrc: string;
export let alt: string;
</script>
<a
href={$app.themeInUse === 'dark' ? darkSrc : lightSrc}
class="file-preview is-with-image"
style="inline-size: 100%; block-size: 100%;"
target="_blank"
rel="noopener noreferrer"
aria-label="open file in new window">
<div class="file-preview-image">
<img src={$app.themeInUse === 'dark' ? darkSrc : lightSrc} {alt} />
</div>
<div class="file-preview-content">
<div class="avatar">
<span class="icon-external-link" aria-hidden="true" />
</div>
</div>
</a>
+2
View File
@@ -15,6 +15,7 @@ export { default as UploadBox } from './uploadBox.svelte';
export { default as List } from './list.svelte';
export { default as ListItem } from './listItem.svelte';
export { default as Empty } from './empty.svelte';
export { default as EmptyFilter } from './emptyFilter.svelte';
export { default as EmptySearch } from './emptySearch.svelte';
export { default as Drop } from './drop.svelte';
export { default as DropList } from './dropList.svelte';
@@ -71,3 +72,4 @@ export { default as FakeModal } from './fakeModal.svelte';
export { default as RadioBoxes } from './radioBoxes.svelte';
export { default as ModalWrapper } from './modalWrapper.svelte';
export { default as ModalSideCol } from './modalSideCol.svelte';
export { default as ImagePreview } from './imagePreview.svelte';
+11
View File
@@ -1,5 +1,7 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import { app } from '$lib/stores/app';
import { base } from '$app/paths';
export let name: string;
export let group: string;
@@ -7,6 +9,7 @@
export let disabled = false;
export let padding = 1;
export let icon: string = null;
export let imageIcon: string = null;
export let fullHeight = true;
export let borderRadius: 'xsmall' | 'small' | 'medium' | 'large' = 'small';
export let backgroundColor: string = null;
@@ -57,6 +60,14 @@
{#if icon}
<span class={`icon-${icon} u-margin-inline-start-auto`} aria-hidden="true" />
{/if}
{#if imageIcon}
<img
class="u-margin-inline-start-auto"
style:max-inline-size="1.25rem"
style:max-block-size="1.25rem"
src={`${base}/icons/${$app.themeInUse}/color/${imageIcon}.svg`}
alt={imageIcon} />
{/if}
{/if}
</div>
</label>
+1 -1
View File
@@ -5,7 +5,7 @@
import { disableCommands } from '$lib/commandCenter';
export let show = false;
export let size: 'small' | 'big' = null;
export let size: 'small' | 'big' | 'huge' = null;
export let icon: string = null;
export let state: 'success' | 'warning' | 'error' | 'info' = null;
export let error: string = null;
+20 -3
View File
@@ -4,7 +4,7 @@
import { disableCommands } from '$lib/commandCenter';
export let show = false;
export let size: 'small' | 'big' = null;
export let size: 'small' | 'big' | 'huge' = null;
export let closable = true;
export let headerDivider = true;
export let style = '';
@@ -70,13 +70,30 @@
<dialog
class="modal"
class:u-hide={!show}
class:is-small={size === 'small'}
class:is-big={size === 'big'}
class:is-huge={size === 'huge'}
class:is-separate-header={headerDivider}
{style}
bind:this={dialog}
on:cancel|preventDefault>
on:cancel|preventDefault
{style}>
{#if show}
<slot close={closeModal} />
{/if}
</dialog>
<style lang="scss">
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
.modal.is-huge {
block-size: 100%;
min-block-size: 80vh;
@media #{$break1}, #{$break2} {
min-inline-size: 100%;
min-block-size: 100%;
border-radius: 0;
}
}
</style>
+2 -1
View File
@@ -12,6 +12,7 @@
export let disabled = false;
export let autofocus = false;
export let isWithEndButton = true;
export let fullWidth = false;
let element: HTMLInputElement;
let timer: ReturnType<typeof setTimeout>;
@@ -58,7 +59,7 @@
</script>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<div class="u-flex-basis-50-percent">
<div class={fullWidth ? 'u-width-full-line' : 'u-flex-basis-50-percent'}>
<div class="input-text-wrapper" class:is-with-end-button={isWithEndButton}>
<input
{placeholder}
+9 -12
View File
@@ -16,16 +16,13 @@
<svelte:window on:resize={throttle(onResize, 250)} />
<span class={`text ${alternativeTrim ? 'u-trim-1' : 'u-trim'}`} bind:this={container}>
{#if showTooltip}
<span
use:tooltip={{
content: container.innerText,
maxWidth: '30rem'
}}>
<slot />
</span>
{:else}
<span><slot /></span>
{/if}
<span
class={`text ${alternativeTrim ? 'u-trim-1' : 'u-trim'}`}
bind:this={container}
use:tooltip={{
disabled: !showTooltip,
content: container?.innerText ?? undefined,
maxWidth: '30rem'
}}>
<span><slot /></span>
</span>
+15 -8
View File
@@ -16,6 +16,7 @@
export let hideColumns = false;
export let allowNoColumns = false;
export let showColsTextMobile = false;
export let fullWidthMobile = false;
let showSelectColumns = false;
@@ -31,12 +32,15 @@
} else {
const prefs = preferences.get($page.route);
columns.set(
$columns.map((column) => {
column.show = prefs.columns?.includes(column.id) ?? true;
return column;
})
);
// Override the shown columns only if a preference was set
if (prefs?.columns) {
columns.set(
$columns.map((column) => {
column.show = prefs.columns?.includes(column.id) ?? true;
return column;
})
);
}
}
columns.subscribe((ctx) => {
@@ -71,8 +75,11 @@
<div class="grid-header-col-4">
{#if !hideColumns && view === View.Table}
{#if $columns?.length}
<DropList bind:show={showSelectColumns} scrollable>
<Button secondary on:click={() => (showSelectColumns = !showSelectColumns)}>
<DropList bind:show={showSelectColumns} scrollable wrapperFullWidth={fullWidthMobile}>
<Button
secondary
on:click={() => (showSelectColumns = !showSelectColumns)}
{fullWidthMobile}>
<span
class="icon-view-boards u-opacity-50"
aria-hidden="true"
+66 -1
View File
@@ -3,6 +3,7 @@ export const CARD_LIMIT = 6; // default card limit
export const INTERVAL = 5 * 60000; // default interval to check for feedback
export enum Dependencies {
FACTORS = 'dependency:factors',
CREDIT = 'dependency:credit',
INVOICES = 'dependency:invoices',
ADDRESS = 'dependency:address',
@@ -17,6 +18,7 @@ export enum Dependencies {
ACCOUNT_SESSIONS = 'dependency:account_sessions',
USER = 'dependency:user',
USERS = 'dependency:users',
USER_TARGETS = 'dependency:user_targets',
SESSIONS = 'dependency:sessions',
TEAM = 'dependency:team',
TEAMS = 'dependency:teams',
@@ -47,7 +49,14 @@ export enum Dependencies {
MIGRATIONS = 'dependency:migrations',
COLLECTIONS = 'dependency:collections',
RUNTIMES = 'dependency:runtimes',
CONSOLE_VARIABLES = 'dependency:console_variables'
CONSOLE_VARIABLES = 'dependency:console_variables',
MESSAGING_PROVIDERS = 'dependency:messaging_providers',
MESSAGING_PROVIDER = 'dependency:messaging_provider',
MESSAGING_MESSAGES = 'dependency:messaging_messages',
MESSAGING_MESSAGE = 'dependency:messaging_message',
MESSAGING_TOPICS = 'dependency:messaging_topics',
MESSAGING_TOPIC = 'dependency:messaging_topic',
MESSAGING_TOPIC_SUBSCRIBERS = 'dependency:messaging_topic_subscribers'
}
export const scopes: {
@@ -55,6 +64,11 @@ export const scopes: {
description: string;
category: string;
}[] = [
{
scope: 'sessions.write',
description: "Access to create, update and delete your project's sessions",
category: 'Auth'
},
{
scope: 'users.read',
description: "Access to read your project's users",
@@ -168,6 +182,57 @@ export const scopes: {
description: "Access to execute your project's functions",
category: 'Functions'
},
{
scope: 'targets.read',
description: "Access to read your project's messaging targets",
category: 'Messaging'
},
{
scope: 'targets.write',
description: "Access to create, update, and delete your project's messaging targets",
category: 'Messaging'
},
{
scope: 'providers.read',
description: "Access to read your project's messaging providers",
category: 'Messaging'
},
{
scope: 'providers.write',
description: "Access to create, update, and delete your project's messaging providers",
category: 'Messaging'
},
{
scope: 'messages.read',
description: "Access to read your project's messages",
category: 'Messaging'
},
{
scope: 'messages.write',
description: "Access to create, update, and delete your project's messages",
category: 'Messaging'
},
{
scope: 'topics.read',
description: "Access to read your project's messaging topics",
category: 'Messaging'
},
{
scope: 'topics.write',
description: "Access to create, update, and delete your project's messaging topics",
category: 'Messaging'
},
{
scope: 'subscribers.read',
description: "Access to read your project's messaging topic subscribers",
category: 'Messaging'
},
{
scope: 'subscribers.write',
description:
"Access to create, update, and delete your project's messaging topic subscribers",
category: 'Messaging'
},
{
scope: 'locale.read',
description: "Access to access your project's Locale service",
+3
View File
@@ -1,5 +1,7 @@
<script lang="ts">
import { isValueOfStringEnum } from '$lib/helpers/types';
import { sdk } from '$lib/stores/sdk';
import { Flag } from '@appwrite.io/console';
export let flag: string;
export let name: string = flag;
export let width = 40;
@@ -9,6 +11,7 @@
export { classes as class };
export function getFlag(country: string, width: number, height: number, quality: number) {
if (!isValueOfStringEnum(Flag, country)) return '';
let flag = sdk.forProject.avatars
.getFlag(country, width * 2, height * 2, quality)
?.toString();
+2
View File
@@ -15,6 +15,7 @@
export let disabled = false;
export let external = false;
export let href: string = null;
export let download: string = undefined;
export let fullWidth = false;
export let fullWidthMobile = false;
export let ariaLabel: string = null;
@@ -63,6 +64,7 @@
on:click
on:click={track}
{href}
{download}
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}
class={resolvedClasses}
+3
View File
@@ -29,3 +29,6 @@ export { default as Label } from './label.svelte';
export { default as InputProjectId } from './inputProjectId.svelte';
export { default as InputDate } from './inputDate.svelte';
export { default as InputDateRange } from './inputDateRange.svelte';
export { default as InputTime } from './inputTime.svelte';
export { default as InputDigits } from './inputDigits.svelte';
export { default as InputFilePicker } from './inputFilePicker.svelte';
+25 -27
View File
@@ -1,32 +1,27 @@
<script lang="ts">
import { FormItem, Helper, Label } from '.';
import { FormItem, Helper } from '.';
import type { FormItemTag } from './formItem.svelte';
interface $$Props extends Partial<HTMLLabelElement> {
id: string;
label?: string;
optionalText?: string;
tooltip?: string;
showLabel?: boolean;
checked?: boolean;
required?: boolean;
disabled?: boolean;
element?: HTMLInputElement | undefined;
indeterminate?: boolean;
wrapperTag?: FormItemTag;
size?: 'small' | 'medium';
}
export let id: string;
export let label: string | undefined = undefined;
export let optionalText: string | undefined = undefined;
export let tooltip: string = null;
export let showLabel = true;
export let checked = false;
export let required = false;
export let disabled = false;
export let element: HTMLInputElement | undefined = undefined;
export let wrapperTag: FormItemTag = 'li';
export let size: $$Props['size'] = 'medium';
let error: string;
const handleInvalid = (event: Event) => {
@@ -44,25 +39,28 @@
</script>
<FormItem tag={wrapperTag}>
{#if label}
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{/if}
<div class="input-text-wrapper">
<input
{id}
{disabled}
{required}
{...$$restProps}
type="checkbox"
bind:this={element}
bind:checked
on:invalid={handleInvalid}
on:click
on:change />
</div>
<label class="choice-item" for={id}>
<div class="input-text-wrapper">
<input
{id}
{disabled}
{required}
{...$$restProps}
class:is-small={size === 'small'}
type="checkbox"
bind:this={element}
bind:checked
on:invalid={handleInvalid}
on:click
on:change />
</div>
<div class="choice-item-content">
{#if label}
<div class="choice-item-title">{label}</div>
{/if}
<slot name="description" />
</div>
</label>
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
@@ -39,6 +39,7 @@
aria-checked={value}
bind:this={element}
bind:checked={value}
on:change
on:invalid={handleInvalid} />
<div class="choice-item-content" class:u-width-full-line={fullWidth}>
+8
View File
@@ -10,6 +10,8 @@
export let value = '';
export let required = false;
export let nullable = false;
export let min: string | number | undefined = undefined;
export let max: string | number | undefined = undefined;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
@@ -65,12 +67,18 @@
{readonly}
{required}
step=".001"
{min}
{max}
autocomplete={autocomplete ? 'on' : 'off'}
type="date"
style={disabled ? '' : 'cursor: pointer;'}
class="input-text"
bind:value
bind:this={element}
on:invalid={handleInvalid}
on:click={function () {
this.showPicker();
}}
style:--amount-of-buttons={isNullable ? 2.75 : 1}
style:--button-size={isNullable ? '2rem' : '1rem'} />
{#if isNullable}
+66
View File
@@ -0,0 +1,66 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem } from '.';
import { createPinInput, melt } from '@melt-ui/svelte';
export let length: number = 6;
export let value: string = '';
export let required = false;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
export let fullWidth = false;
let element: HTMLOListElement;
let autoSubmitted = false;
const {
elements: { root, input }
} = createPinInput({
placeholder: '',
defaultValue: value.split(''),
onValueChange: ({ next }) => {
value = next.join('');
if (value.length === 6 && !autoSubmitted) {
autoSubmitted = true;
const firstInputElement = element.querySelector('input');
firstInputElement?.form.requestSubmit();
}
return next;
}
});
onMount(() => {
const interval = setInterval(() => {
const input = element.querySelector('input');
if (element) {
if (input && autofocus) {
input.focus();
}
clearInterval(interval);
}
}, 10);
});
</script>
<FormItem {fullWidth}>
<ol class="u-flex u-main-center u-gap-16" use:melt={$root} bind:this={element}>
{#each Array.from({ length }) as _}
<li>
<input
type="number"
class="verification-code-input u-remove-input-number-buttons"
maxlength="1"
style:--p-input-size="3.75rem"
style:font-size="2rem"
use:melt={$input()}
{required}
{readonly}
{disabled} />
</li>
{/each}
</ol>
</FormItem>
+32 -4
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { SvelteComponent, onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import { Drop } from '$lib/components';
export let label: string;
export let showLabel = true;
@@ -13,12 +14,16 @@
export let autofocus = false;
export let autocomplete = false;
export let maxlength: number = null;
export let popover: typeof SvelteComponent<unknown> = null;
export let popoverProps: Record<string, unknown> = {};
export let fullWidth = false;
// https://www.geeksforgeeks.org/how-to-validate-a-domain-name-using-regular-expression/
const pattern = String.raw`^(?!-)[A-Za-z0-9-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}$`;
const pattern = String.raw`(?!-)[A-Za-z0-9\-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}`;
let element: HTMLInputElement;
let error: string;
let show = false;
onMount(() => {
if (element && autofocus) {
@@ -46,9 +51,32 @@
}
</script>
<FormItem>
<FormItem {fullWidth}>
<Label {required} hide={!showLabel} for={id}>
{label}
{label}{#if popover}
<Drop isPopover bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--card-border-radius="var(--border-radius-small)"
style:--p-card-padding=".75rem"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
<div class="input-text-wrapper">
+31 -3
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import { SvelteComponent, onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
import { Drop } from '$lib/components';
export let label: string;
export let optionalText: string | undefined = undefined;
@@ -16,9 +17,13 @@
export let autofocus = false;
export let autocomplete = false;
export let tooltip: string = null;
export let popover: typeof SvelteComponent<unknown> = null;
export let popoverProps: Record<string, unknown> = {};
export let fullWidth = false;
let element: HTMLInputElement;
let error: string;
let show = false;
onMount(() => {
if (element && autofocus) {
@@ -55,9 +60,32 @@
}
</script>
<FormItem>
<FormItem {fullWidth}>
<Label {required} {optionalText} {tooltip} hide={!showLabel} for={id}>
{label}
{label}{#if popover}
<Drop isPopover bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--card-border-radius="var(--border-radius-small)"
style:--p-card-padding=".75rem"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
<div
+29 -3
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { Trim } from '$lib/components';
import { Drop, Trim } from '$lib/components';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { onMount } from 'svelte';
import { SvelteComponent, onMount } from 'svelte';
import { Helper, Label } from '.';
export let label: string = null;
@@ -13,9 +13,12 @@
export let optionalText: string = null;
export let tooltip: string = null;
export let error: string = null;
export let popover: typeof SvelteComponent<unknown> = null;
export let popoverProps: Record<string, unknown> = {};
let input: HTMLInputElement;
let hovering = false;
let show = false;
function setFiles(value: FileList) {
if (!value) return;
@@ -94,7 +97,30 @@
<div>
{#if label}
<Label {required} {optionalText} {tooltip} hide={!label}>
{label}
{label}{#if popover}
<Drop isPopover bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--card-border-radius="var(--border-radius-small)"
style:--p-card-padding=".75rem"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
{/if}
<div
@@ -0,0 +1,104 @@
<script lang="ts">
import { Drop, Trim } from '$lib/components';
import FilePicker from '$lib/components/filePicker.svelte';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import type { Models } from '@appwrite.io/console';
import { Label } from '.';
export let label: string = null;
export let value: Models.File = null;
export let disabled = false;
export let optionalText: string = null;
export let tooltip: string = null;
export let isPopoverDefined = true;
let show = false;
function onSelect(file: Models.File) {
value = file;
}
</script>
<div>
{#if label}
<Label {optionalText} {tooltip} hide={!label}>
{label}{#if $$slots.popover && isPopoverDefined}
<Drop bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style="--p-card-padding: .75rem; box-shadow:var(--shadow-large);">
<slot name="popover" />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
{/if}
<div
role="region"
class="box is-no-shadow u-padding-24 is-border-dashed"
style:--box-border-radius="var(--border-radius-xsmall)">
<div class="upload-file-box">
<div class="upload-file-box-image">
<span class="icon-upload" aria-hidden="true" />
</div>
<div class="u-min-width-0 u-text-center">
<h5 class="upload-file-box-title heading-level-7 u-inline">
<span class="is-only-desktop">Select a file to upload</span>
<span class="is-not-desktop">Select a file to upload</span>
</h5>
</div>
<div class="u-flex u-main-center u-cross-center u-gap-16 u-flex-vertical-mobile">
Max file size: 1MB
<button
class="button is-secondary is-full-width-mobile"
type="button"
{disabled}
on:click={() => (show = true)}>
<span class="text">Browse</span>
</button>
</div>
{#if value}
{@const fileSize = humanFileSize(value.sizeOriginal)}
<ul class="upload-file-box-list u-min-width-0">
<li class="u-flex u-cross-center u-min-width-0">
<span class="icon-document" aria-hidden="true" />
<span class="upload-file-box-name u-min-width-0">
<Trim alternativeTrim>{value.name}</Trim>
</span>
<span
class="upload-file-box-size u-margin-inline-start-4 u-margin-inline-end-16">
{fileSize.value + fileSize.unit}
</span>
<button
on:click={() => (value = null)}
type="button"
class="button is-text is-only-icon u-margin-inline-start-auto"
aria-label="remove file"
style="--button-size:1.5rem;">
<span class="icon-x" aria-hidden="true" />
</button>
</li>
</ul>
{/if}
</div>
</div>
</div>
{#if show}
<FilePicker selectedFile={value?.$id} selectedBucket={value?.bucketId} bind:show {onSelect} />
{/if}
+31 -3
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { SvelteComponent, onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import { Drop } from '$lib/components';
export let id: string;
export let label: string;
@@ -15,10 +16,14 @@
export let showPasswordButton = false;
export let minlength = 8;
export let maxlength: number = null;
export let popover: typeof SvelteComponent<unknown> = null;
export let popoverProps: Record<string, unknown> = {};
export let fullWidth = false;
let element: HTMLInputElement;
let error: string;
let showInPlainText = false;
let showPopover = false;
onMount(() => {
if (element && autofocus) {
@@ -46,9 +51,32 @@
}
</script>
<FormItem>
<FormItem {fullWidth}>
<Label {required} hide={!showLabel} for={id}>
{label}
{label}{#if popover}
<Drop isPopover bind:show={showPopover} display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (showPopover = !showPopover)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--card-border-radius="var(--border-radius-small)"
style:--p-card-padding=".75rem"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
<div class="input-text-wrapper" style={showPasswordButton ? '--amount-of-buttons: 1' : ''}>
+31 -3
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { SvelteComponent, onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import { Drop } from '$lib/components';
export let label: string;
export let showLabel = true;
@@ -13,11 +14,15 @@
export let autofocus = false;
export let autocomplete = false;
export let maxlength: number = null;
export let popover: typeof SvelteComponent<unknown> = null;
export let popoverProps: Record<string, unknown> = {};
export let fullWidth = false;
const pattern = String.raw`^\+?[1-9]\d{1,14}$`;
let element: HTMLInputElement;
let error: string;
let show = false;
onMount(() => {
if (element && autofocus) {
@@ -45,9 +50,32 @@
}
</script>
<FormItem>
<FormItem {fullWidth}>
<Label {required} hide={!showLabel} for={id}>
{label}
{label}{#if popover}
<Drop isPopover bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--card-border-radius="var(--border-radius-small)"
style:--p-card-padding=".75rem"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
<div class="input-text-wrapper">
<input
+18
View File
@@ -47,6 +47,24 @@
<slot />
{/if}
</Label>
<!-- <label class="choice-item" for={id}>
<input
{id}
{name}
{disabled}
{required}
{value}
type="radio"
bind:group
bind:this={element}
on:invalid={handleInvalid} />
<div
class="choice-item-content u-cross-child-center"
class:u-width-full-line={fullWidth}>
<div class="choice-item-title">{label}</div>
<slot name="description" />
</div>
</label> -->
</div>
{#if error}
<Helper type="warning">{error}</Helper>
@@ -9,6 +9,7 @@
export let disabled = false;
export let autofocus = false;
export let isWithEndButton = true;
export let style: string = '';
const dispatch = createEventDispatcher();
let element: HTMLInputElement;
@@ -53,6 +54,7 @@
{placeholder}
{disabled}
{required}
{style}
type="search"
class="input-text"
bind:this={element}
@@ -4,7 +4,6 @@
export let label: string;
export let id: string;
export let value = false;
export let required = false;
export let disabled = false;
let element: HTMLInputElement;
@@ -13,10 +12,6 @@
const handleInvalid = (event: Event) => {
event.preventDefault();
if (element.validity.valueMissing) {
error = 'This field is required';
return;
}
error = element.validationMessage;
};
@@ -31,7 +26,6 @@
<input
{id}
{disabled}
{required}
type="checkbox"
class="switch"
role="switch"
+29 -2
View File
@@ -1,8 +1,9 @@
<script lang="ts">
import { onMount } from 'svelte';
import { SvelteComponent, onMount } from 'svelte';
import { FormItem, FormItemPart, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
import TextCounter from './textCounter.svelte';
import { Drop } from '$lib/components';
export let label: string = undefined;
export let optionalText: string | undefined = undefined;
@@ -22,9 +23,12 @@
export let maxlength: number = null;
export let tooltip: string = null;
export let isMultiple = false;
export let popover: typeof SvelteComponent<unknown> = null;
export let popoverProps: Record<string, unknown> = {};
let element: HTMLInputElement;
let error: string;
let show = false;
onMount(() => {
if (element && autofocus) {
@@ -74,7 +78,30 @@
<svelte:component this={wrapper} {fullWidth}>
{#if label}
<Label {required} {hideRequired} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
{label}{#if popover}
<Drop isPopover bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style:font-size="var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--p-card-padding=".75rem"
style:--card-border-radius="var(--border-radius-small)"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
{/if}
+71
View File
@@ -0,0 +1,71 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
export let label: string;
export let showLabel = true;
export let optionalText: string | undefined = undefined;
export let id: string;
export let value = '';
export let required = false;
export let min: string | number | undefined = undefined;
export let max: string | number | undefined = undefined;
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();
}
});
function 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 {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
<div class="input-text-wrapper" style="--amount-of-buttons:1; --button-size: 1rem">
<input
{id}
{disabled}
{readonly}
{required}
{min}
{max}
step="60"
autocomplete={autocomplete ? 'on' : 'off'}
type="time"
class="input-text"
style={disabled ? '' : 'cursor: pointer;'}
bind:value
bind:this={element}
on:invalid={handleInvalid}
on:click={function () {
this.showPicker();
}} />
</div>
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
</FormItem>
+2
View File
@@ -6,6 +6,7 @@
export let id: string;
export let selectedIds: string[] = [];
export let disabled: boolean = false;
let el: HTMLInputElement;
const handleClick = (e: Event) => {
@@ -35,6 +36,7 @@
id="select-{id}"
wrapperTag="div"
checked={selectedIds.includes(id)}
{disabled}
on:click={handleClick} />
</TableCell>
+2
View File
@@ -5,6 +5,7 @@
export let transparent = false;
export let isAutoLayout = false;
export let tag: 'div' | 'table' = 'div';
export let dense = false;
</script>
<svelte:element
@@ -13,6 +14,7 @@
class:is-table-layout-auto={isAutoLayout}
class:u-margin-block-start-32={!noMargin}
class:is-remove-outer-styles={noStyles}
class:is-table-row-medium-size={dense}
{style}
style:--p-table-bg-color={transparent ? 'var(--transparent)' : ''}
role="table"
@@ -6,6 +6,7 @@
export let style = '';
export let transparent = false;
export let noStyles = false;
export let dense = false;
let isOverflowing = false;
@@ -48,6 +49,7 @@
class="table"
class:is-remove-outer-styles={noStyles}
class:is-sticky-scroll={isSticky && isOverflowing}
class:is-table-row-medium-size={dense}
{style}
style:--p-table-bg-color={transparent ? 'var(--transparent)' : ''}>
<slot />
+32
View File
@@ -41,6 +41,38 @@ export const toLocaleDateTime = (datetime: string | number) => {
return date.toLocaleDateString('en', options);
};
/**
* Returns a date string usig the local timezone in ISO format (yyyy-mm-dd)
*
* @param datetime date string or milliseconds since the epoch
*/
export const toLocaleDateISO = (datetime: string | number) => {
const date = new Date(datetime);
if (isNaN(date.getTime())) {
return 'n/a';
}
// Use Sweden's locale (sv) since it matches ISO format
return date.toLocaleDateString('sv');
};
/**
* Returns a time string usig the local timezone in ISO format (hh:mm:ss)
*
* @param datetime date string or milliseconds since the epoch
*/
export const toLocaleTimeISO = (datetime: string | number) => {
const date = new Date(datetime);
if (isNaN(date.getTime())) {
return 'n/a';
}
// Use Sweden's locale (sv) since it matches ISO format
return date.toLocaleTimeString('sv');
};
export const isSameDay = (date1: Date, date2: Date) => {
return (
date1.getFullYear() === date2.getFullYear() &&
+1 -1
View File
@@ -36,7 +36,7 @@ export function getQuery(url: URL): string | undefined {
return url.searchParams.get('query') ?? undefined;
}
type TabElement = { href: string; title: string; hasChildren?: boolean };
export type TabElement = { href: string; title: string; event: string; hasChildren?: boolean };
export function isTabSelected(
tab: TabElement,
+7
View File
@@ -0,0 +1,7 @@
export function omit<T, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> {
const ret = { ...obj };
for (const key of keys) {
delete ret[key];
}
return ret;
}
+14
View File
@@ -19,6 +19,20 @@ export type Column = {
id: string;
title: string;
type: ColumnType;
/**
* Set to false to hide by default
*/
show: boolean;
width?: number;
/**
* Set to false to disable filtering for this column
*/
filter?: boolean;
};
export function isValueOfStringEnum<T extends Record<string, string>>(
enumType: T,
value: string
): value is T[keyof T] {
return Object.values<string>(enumType).includes(value);
}
-77
View File
@@ -1,77 +0,0 @@
import { page } from '$app/stores';
import { get } from 'svelte/store';
import type { Metric } from 'web-vitals';
type Options = {
params:
| {
[s: string]: string;
}
| ArrayLike<string>;
path: string;
analyticsId: string;
debug?: boolean;
};
const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals';
function getConnectionSpeed() {
return 'connection' in navigator &&
navigator['connection'] &&
'effectiveType' in (navigator['connection'] as Record<string, unknown>)
? navigator['connection']['effectiveType']
: '';
}
function sendToAnalytics(metric: Metric, options: Options) {
const page = Object.entries(options.params).reduce(
(acc, [key, value]) => acc.replace(value, `[${key}]`),
options.path
);
const body = {
dsn: options.analyticsId,
id: metric.id,
page,
href: location.href,
event_name: metric.name,
value: metric.value.toString(),
speed: getConnectionSpeed()
};
if (options.debug) {
console.debug('[Analytics]', metric.name, JSON.stringify(body, null, 2));
}
const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`
type: 'application/x-www-form-urlencoded'
});
if (navigator.sendBeacon) {
navigator.sendBeacon(vitalsUrl, blob);
} else
fetch(vitalsUrl, {
body: blob,
method: 'POST',
credentials: 'omit',
keepalive: true
});
}
export function reportWebVitals(metric: Metric) {
try {
sendToAnalytics(metric, createWebVitalsOptions());
} catch (err) {
console.error('[Analytics]', err);
}
}
function createWebVitalsOptions(): Options {
const ctx = get(page);
return {
path: ctx.url.pathname,
params: ctx.params,
analyticsId: window.VERCEL_ANALYTICS_ID || ''
};
}
+18
View File
@@ -0,0 +1,18 @@
<svg width="220" height="221" viewBox="0 0 220 221" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="path-1-inside-1_5272_24665" fill="white">
<path d="M0 1C0 0.447715 0.447715 0 1 0H32V32H0V1Z"/>
</mask>
<path d="M-1 1C-1 -0.104569 -0.104569 -1 1 -1H32V1H1H-1ZM32 32H0H32ZM-1 32V1C-1 -0.104569 -0.104569 -1 1 -1V1V32H-1ZM32 0V32V0Z" fill="#C3C3C6" mask="url(#path-1-inside-1_5272_24665)"/>
<mask id="path-3-inside-2_5272_24665" fill="white">
<path d="M219 -4.37114e-08C219.552 -1.95703e-08 220 0.447715 220 1L220 32L188 32L188 -1.39876e-06L219 -4.37114e-08Z"/>
</mask>
<path d="M219 -1C220.105 -1 221 -0.104569 221 1L221 32L219 32L219 1L219 -1ZM188 32L188 -1.39876e-06L188 32ZM188 -1L219 -1C220.105 -1 221 -0.104569 221 1L219 1L188 0.999999L188 -1ZM220 32L188 32L220 32Z" fill="#C3C3C6" mask="url(#path-3-inside-2_5272_24665)"/>
<mask id="path-5-inside-3_5272_24665" fill="white">
<path d="M1 221C0.447715 221 -1.95703e-08 220.552 -4.37114e-08 220L-1.39876e-06 189L32 189L32 221L1 221Z"/>
</mask>
<path d="M1 222C-0.104569 222 -1 221.105 -1 220L-1 189L0.999999 189L1 220L1 222ZM32 189L32 221L32 189ZM32 222L1 222C-0.104569 222 -1 221.105 -1 220L1 220L32 220L32 222ZM-1.39876e-06 189L32 189L-1.39876e-06 189Z" fill="#C3C3C6" mask="url(#path-5-inside-3_5272_24665)"/>
<mask id="path-7-inside-4_5272_24665" fill="white">
<path d="M220 220C220 220.552 219.552 221 219 221L188 221L188 189L220 189L220 220Z"/>
</mask>
<path d="M221 220C221 221.105 220.105 222 219 222L188 222L188 220L219 220L221 220ZM188 189L220 189L188 189ZM221 189L221 220C221 221.105 220.105 222 219 222L219 220L219 189L221 189ZM188 221L188 189L188 221Z" fill="#C3C3C6" mask="url(#path-7-inside-4_5272_24665)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+18
View File
@@ -0,0 +1,18 @@
<svg width="220" height="221" viewBox="0 0 220 221" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="path-1-inside-1_5602_13938" fill="white">
<path d="M0 1.00006C0 0.447776 0.447715 6.10352e-05 1 6.10352e-05H32V32.0001H0V1.00006Z"/>
</mask>
<path d="M-1 1.00006C-1 -0.104508 -0.104569 -0.999939 1 -0.999939H32V1.00006H1H-1ZM32 32.0001H0H32ZM-1 32.0001V1.00006C-1 -0.104508 -0.104569 -0.999939 1 -0.999939V1.00006V32.0001H-1ZM32 6.10352e-05V32.0001V6.10352e-05Z" fill="#C3C3C6" mask="url(#path-1-inside-1_5602_13938)"/>
<mask id="path-3-inside-2_5602_13938" fill="white">
<path d="M219 6.09914e-05C219.552 6.10156e-05 220 0.447776 220 1.00006L220 32.0001L188 32.0001L188 5.96364e-05L219 6.09914e-05Z"/>
</mask>
<path d="M219 -0.999939C220.105 -0.999939 221 -0.104508 221 1.00006L221 32.0001L219 32.0001L219 1.00006L219 -0.999939ZM188 32.0001L188 5.96364e-05L188 32.0001ZM188 -0.99994L219 -0.999939C220.105 -0.999939 221 -0.104508 221 1.00006L219 1.00006L188 1.00006L188 -0.99994ZM220 32.0001L188 32.0001L220 32.0001Z" fill="#C3C3C6" mask="url(#path-3-inside-2_5602_13938)"/>
<mask id="path-5-inside-3_5602_13938" fill="white">
<path d="M1 221C0.447715 221 -1.95703e-08 220.552 -4.37114e-08 220L-1.39876e-06 189L32 189L32 221L1 221Z"/>
</mask>
<path d="M1 222C-0.104569 222 -1 221.105 -1 220L-1 189L0.999999 189L1 220L1 222ZM32 189L32 221L32 189ZM32 222L1 222C-0.104569 222 -1 221.105 -1 220L1 220L32 220L32 222ZM-1.39876e-06 189L32 189L-1.39876e-06 189Z" fill="#C3C3C6" mask="url(#path-5-inside-3_5602_13938)"/>
<mask id="path-7-inside-4_5602_13938" fill="white">
<path d="M220 220C220 220.552 219.552 221 219 221L188 221L188 189L220 189L220 220Z"/>
</mask>
<path d="M221 220C221 221.105 220.105 222 219 222L188 222L188 220L219 220L221 220ZM188 189L220 189L188 189ZM221 189L221 220C221 221.105 220.105 222 219 222L219 220L219 189L221 189ZM188 221L188 189L188 221Z" fill="#C3C3C6" mask="url(#path-7-inside-4_5602_13938)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+25 -18
View File
@@ -1,23 +1,23 @@
<script lang="ts">
import {
tierToPlan,
getServiceLimit,
type PlanServices,
showUsageRatesModal,
checkForUsageFees,
readOnly,
checkForProjectLimitation
} from '$lib/stores/billing';
import { Alert, DropList, Heading } from '$lib/components';
import { Pill } from '$lib/elements';
import { organization } from '$lib/stores/organization';
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import { createEventDispatcher, onMount } from 'svelte';
import { wizard } from '$lib/stores/wizard';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { ContainerButton } from '.';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
checkForProjectLimitation,
checkForUsageFees,
getServiceLimit,
readOnly,
showUsageRatesModal,
tierToPlan,
type PlanServices
} from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { wizard } from '$lib/stores/wizard';
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { createEventDispatcher, onMount } from 'svelte';
import { ContainerButton } from '.';
import { trackEvent } from '$lib/actions/analytics';
export let isFlex = true;
@@ -36,7 +36,14 @@
let showDropdown = false;
const { bandwidth, documents, storage, users, executions } = $organization?.billingLimits ?? {};
// TODO: remove the default billing limits when backend is updated with billing code
const { bandwidth, documents, storage, users, executions } = $organization?.billingLimits ?? {
bandwidth: 1,
documents: 1,
storage: 1,
users: 1,
executions: 1
};
const limitedServices = [
{ name: 'bandwidth', value: bandwidth },
{ name: 'documents', value: documents },
+17
View File
@@ -124,6 +124,23 @@
<span class="text">Functions</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${projectPath}/messaging`
)}
on:click={() => trackEvent('click_menu_messaging')}
href={`${projectPath}/messaging`}
use:tooltip={{
content: 'Messaging',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-send" aria-hidden="true" />
<span class="text">Messaging</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
+41 -337
View File
@@ -11,98 +11,27 @@
</script>
<main class="grid-1-1 is-full-page" id="main">
<section class="u-flex u-flex-vertical side-bg">
<div class="logo u-flex u-gap-16 u-margin-inline-auto is-not-mobile">
<a href={$user ? '/console' : '/'}>
<section
class="u-flex u-flex-vertical"
style:--url={`url(${$app.themeInUse === 'dark' ? imgDark : imgLight})`}>
<div class="logo u-flex u-gap-16">
<a href={user ? '/console' : '/'}>
{#if $app.themeInUse === 'dark'}
<img
src={AppwriteLogoDark}
width="123"
width="160"
class="u-block u-only-dark"
alt="Appwrite Logo" />
{:else}
<img
src={AppwriteLogoLight}
width="123"
width="160"
class="u-block u-only-light"
alt="Appwrite Logo" />
{/if}
</a>
</div>
<div class="appwrite-pro">
<span class="text">APPWRITE</span>
<span class="appwrite-pro-text">
<span class="appwrite-pro-text-letter">P</span><span
class="appwrite-pro-text-letter">R</span
><span class="appwrite-pro-text-letter">O</span></span>
</div>
<div class="now-available">Now available</div>
</section>
<section class="grid-1-1-col-2 u-flex u-main-center u-cross-center _u-padding-16-mobile">
<div class="container u-flex u-flex-vertical u-cross-center u-main-center">
<div class="u-max-width-500 u-width-full-line">
<h1 class="heading-level-2 u-margin-block-start-auto">
<slot name="title" />
</h1>
<div class="u-margin-block-start-24">
<slot />
</div>
<ul class="inline-links is-center is-with-sep u-margin-block-start-32">
<slot name="links" />
</ul>
</div>
<div
class="logo u-flex u-gap-16 u-margin-inline-auto is-only-mobile u-margin-block-start-32">
<a href={user ? '/console' : '/'}>
{#if $app.themeInUse === 'dark'}
<img
src={AppwriteLogoDark}
width="93"
class="u-block u-only-dark"
alt="Appwrite Logo" />
{:else}
<img
src={AppwriteLogoLight}
width="93"
class="u-block u-only-light"
alt="Appwrite Logo" />
{/if}
</a>
</div>
</div>
</section>
</main>
<!-- OLD one -->
<!--
<main class="grid-1-1 is-full-page" id="main">
<section
class="u-flex u-flex-vertical"
style:--url={`url(${$app.themeInUse === 'dark' ? imgDark : imgLight})`}>
<div class="logo u-flex u-gap-16">
<a href={user ? '/console' : '/'}>
{#if $app.themeInUse === 'dark'}
<img
src={AppwriteLogoDark}
width="160"
class="u-block u-only-dark"
alt="Appwrite Logo" />
{:else}
<img
src={AppwriteLogoLight}
width="160"
class="u-block u-only-light"
alt="Appwrite Logo" />
{/if}
</a>
{#if isCloud}
<span class="aw-badges aw-eyebrow">Cloud Beta</span>
{/if}
</div>
<div class="u-flex u-stretch" />
<div class="tag-line is-not-mobile">
@@ -126,254 +55,10 @@
</div>
</section>
</main>
<style lang="scss">
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
/* Default (including mobile) */
#main section:first-child {
padding-block-start: 2.25rem;
padding-block-end: 2rem;
div {
padding-inline-start: 1rem;
padding-inline-end: 1rem;
}
.tag-line {
font-family: 'Aeonik Pro';
font-size: 4rem;
font-style: normal;
font-weight: 400;
line-height: 100%; /* 80px */
letter-spacing: -1.6px;
backdrop-filter: blur(0.5 rem);
.underscore {
-webkit-text-fill-color: #f02e65;
}
}
}
/* for smaller screens */
@media #{$break2open} {
#main section:first-child {
background: var(--url);
background-repeat: no-repeat;
background-position: top;
background-size: cover;
padding-block-start: 6.25rem;
padding-block-end: 6.875rem;
div {
padding-inline-start: 2.625rem;
padding-inline-end: 2rem;
}
}
}
/* for larger screens */
@media #{$break3open} {
#main section:first-child {
div {
padding-inline-start: 5.625rem;
padding-inline-end: 5rem;
}
.tag-line {
font-size: 5rem;
}
}
}
:global(.theme-dark) .tag-line {
background: linear-gradient(45deg, white, white 60%, #fd376f);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
:global(.theme-light) .tag-line {
background: linear-gradient(45deg, #19191d, #19191d 60%, #fd376f);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.aw-eyebrow {
font-family: 'Source Code Pro', monospace;
line-height: 0.9rem;
font-size: 0.78rem;
letter-spacing: 0.08em;
font-weight: 400;
text-transform: uppercase;
}
.aw-badges {
--p-badges-shadow-bg-color: #f2c8d6;
--p-badges-shadow-border-color: #f69db7;
--p-badges-shadowopacity: 0.4;
align-self: center;
color: hsl(var(--color-neutral-0));
padding-block: 0.375rem;
padding-inline: 0.75rem;
border-radius: 0.375rem;
background: radial-gradient(
98.13% 199.7% at 98.13% 100%,
#fe6947 0%,
#fd366e 41.6%,
#fd366e 100%
);
-webkit-backdrop-filter: blur(2.5rem);
backdrop-filter: blur(2.5rem);
box-shadow:
0.1875rem 0.1875rem var(--p-badges-shadow-bg-color),
0.25rem 0.1875rem var(--p-badges-shadow-border-color),
0.1875rem 0.25rem var(--p-badges-shadow-border-color),
0.125rem 0.1875rem var(--p-badges-shadow-border-color),
0.1875rem 0.125rem var(--p-badges-shadow-border-color);
:global(.theme-dark) & {
--p-badges-shadow-bg-color: #2c2c2f;
--p-badges-shadow-border-color: #39393c;
--p-badges-shadowopacity: 0.13;
}
:global(.theme-light) & {
--p-badges-shadow-bg-color: #f2c8d6;
--p-badges-shadow-border-color: #f69db7;
--p-badges-shadowopacity: 0.4;
}
}
</style>
-->
<style lang="scss">
@import '@appwrite.io/pink/src/abstract/variables/_common.scss';
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
@import '@appwrite.io/pink/src/abstract/functions/_pxToRem.scss';
/* mobile utility class */
@media #{$break1} {
._u-padding-16-mobile {
padding: pxToRem(16);
}
}
.appwrite-pro {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
align-items: baseline;
color: hsl(var(--color-neutral-100));
@media #{$break1} {
gap: pxToRem(10);
font-size: pxToRem(18);
letter-spacing: pxToRem(4);
}
@media #{$break2open} {
gap: pxToRem(24);
font-size: pxToRem(40);
letter-spacing: pxToRem(8);
line-height: 120%;
margin-block-start: pxToRem(160);
}
&-text {
padding: pxToRem(18) pxToRem(28);
border: pxToRem(2) solid hsl(343 98% 60% / 0.2);
border-radius: pxToRem(16);
background: rgba(253, 54, 110, 0.1);
box-shadow: 0 -12.173px 20.289px 0px rgba(253, 54, 110, 0.08) inset;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
&-letter {
width: 2rem;
@media #{$break1} {
width: 1rem;
}
}
@media #{$break1} {
padding: pxToRem(8) pxToRem(12);
border-radius: pxToRem(8);
}
}
}
:global(.theme-dark) .appwrite-pro {
color: hsl(var(--color-neutral-10));
}
.now-available {
position: relative;
z-index: 1;
background: linear-gradient(70deg, #fb5491 -35.72%, #19191d 79.96%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-inline: auto;
@media #{$break1} {
font-size: pxToRem(18);
margin-block-start: pxToRem(12);
}
@media #{$break2open} {
font-size: pxToRem(30);
margin-block-start: pxToRem(36);
}
}
:global(.theme-dark) .now-available {
background: linear-gradient(89deg, #fb5491 -29.25%, #fff 43.27%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.side-bg {
position: relative;
background-color: #ededf0;
overflow: hidden;
}
.side-bg::before {
position: absolute;
inset-block-start: -950px;
inset-inline-end: -650px;
content: '';
display: block;
inline-size: 100%;
block-size: 100%;
background: radial-gradient(49.55% 43.54% at 47% 50.69%, #e7f8f7 0%, #85dbd8 100%);
filter: blur(250px);
@media #{$break1} {
inset-block-start: -200px;
inset-inline-end: -400px;
filter: blur(100px);
}
}
.side-bg::after {
position: absolute;
inset-block-end: -850px;
inset-inline-start: -600px;
content: '';
display: block;
inline-size: 100%;
block-size: 100%;
background: radial-gradient(50% 46.73% at 50% 53.27%, #fe9567 28.17%, #fd366e 59.38%);
filter: blur(250px);
@media #{$break1} {
inset-block-end: -200px;
inset-inline-start: -400px;
filter: blur(100px);
}
}
:global(.theme-dark) .side-bg {
background-color: #19191d;
}
/****** OLD ******/
/* Default (including mobile) */
#main section:first-child {
padding-block-start: 2.25rem;
@@ -384,23 +69,28 @@
padding-inline-end: 1rem;
}
// .tag-line {
// font-family: 'Aeonik Pro';
// font-size: 4rem;
// font-style: normal;
// font-weight: 400;
// line-height: 100%; /* 80px */
// letter-spacing: -1.6px;
// backdrop-filter: blur(0.5 rem);
// .underscore {
// -webkit-text-fill-color: #f02e65;
// }
// }
.tag-line {
font-family: 'Aeonik Pro';
font-size: 4rem;
font-style: normal;
font-weight: 400;
line-height: 100%; /* 80px */
letter-spacing: -1.6px;
backdrop-filter: blur(0.5 rem);
.underscore {
-webkit-text-fill-color: #f02e65;
}
}
}
/* for smaller screens */
@media #{$break2open} {
#main section:first-child {
background: var(--url);
background-repeat: no-repeat;
background-position: top;
background-size: cover;
padding-block-start: 6.25rem;
padding-block-end: 6.875rem;
@@ -418,9 +108,23 @@
padding-inline-start: 5.625rem;
padding-inline-end: 5rem;
}
// .tag-line {
// font-size: 5rem;
// }
.tag-line {
font-size: 5rem;
}
}
}
:global(.theme-dark) .tag-line {
background: linear-gradient(45deg, white, white 60%, #fd376f);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
:global(.theme-light) .tag-line {
background: linear-gradient(45deg, #19191d, #19191d 60%, #fd376f);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
+4 -4
View File
@@ -4,7 +4,7 @@
export function periodToDates(period: UsagePeriods): {
start: string;
end: string;
period: '1h' | '1d';
period: ProjectUsageRange;
} {
const start = new Date();
switch (period) {
@@ -26,7 +26,7 @@
return {
start: start.toISOString(),
end: end.toISOString(),
period: period === '24h' ? '1h' : '1d'
period: period === '24h' ? ProjectUsageRange.OneHour : ProjectUsageRange.OneDay
};
}
@@ -45,8 +45,8 @@
): Array<[string, number]> {
return metrics.reduceRight(
(acc, curr) => {
acc.total -= curr.value;
acc.data.unshift([curr.date, acc.total]);
acc.total -= curr.value;
return acc;
},
{
@@ -62,7 +62,7 @@
import { BarChart } from '$lib/charts';
import { formatNumberWithCommas } from '$lib/helpers/numbers';
import { Card, SecondaryTabs, SecondaryTabsItem, Heading } from '$lib/components';
import type { Models } from '@appwrite.io/console';
import { ProjectUsageRange, type Models } from '@appwrite.io/console';
import { page } from '$app/stores';
type MetricMetadata = {
+13 -1
View File
@@ -6,6 +6,10 @@
component: typeof SvelteComponent<unknown>;
optional?: boolean;
disabled?: boolean;
actions?: {
label: string;
onClick: () => Promise<void>;
}[];
}
>;
</script>
@@ -128,6 +132,7 @@
$: sortedSteps = [...steps].sort(([a], [b]) => (a > b ? 1 : -1));
$: isLastStep = $wizard.step === steps.size;
$: currentStep = steps.get($wizard.step);
</script>
<svelte:window on:keydown={handleKeydown} />
@@ -176,7 +181,7 @@
{/each}
<div class="form-footer">
<div class="u-flex u-main-end u-gap-12">
{#if !isLastStep && sortedSteps[$wizard.step - 1]?.[1]?.optional}
{#if !isLastStep && currentStep?.optional}
<Button text on:click={() => dispatch('finish')}>
Skip optional steps
</Button>
@@ -188,6 +193,13 @@
<Button secondary on:click={previousStep}>Back</Button>
{/if}
{#if currentStep?.actions}
{#each currentStep.actions as action}
<Button secondary on:click={action.onClick}>
{action.label}</Button>
{/each}
{/if}
<Button submit disabled={$wizard.nextDisabled}>
{isLastStep ? finalAction : 'Next'}
</Button>
+2 -9
View File
@@ -1,10 +1,3 @@
<script context="module" lang="ts">
export enum ProxyTypes {
API = 'api',
FUNCTION = 'function'
}
</script>
<script lang="ts">
import { DropList, DropListItem, Empty, Heading, Modal, Trim } from '$lib/components';
import {
@@ -22,14 +15,14 @@
import { toLocaleDate } from '$lib/helpers/date';
import { wizard } from '$lib/stores/wizard';
import type { Dependencies } from '$lib/constants';
import type { Models } from '@appwrite.io/console';
import type { Models, ResourceType } from '@appwrite.io/console';
import Create from './create.svelte';
import Delete from './delete.svelte';
import Retry from './wizard/retry.svelte';
import { Pill } from '$lib/elements';
export let rules: Models.ProxyRuleList;
export let type: ProxyTypes;
export let type: ResourceType;
export let dependency: Dependencies;
let showDomainsDropdown = [];
+1 -1
View File
@@ -1,2 +1,2 @@
export { default as ProxyRulesPage, ProxyTypes } from './index.svelte';
export { default as ProxyRulesPage } from './index.svelte';
export { default as Retry } from './wizard/retry.svelte';
+3 -3
View File
@@ -6,9 +6,9 @@
import { sdk } from '$lib/stores/sdk';
import { isSelfHosted } from '$lib/system';
import { func } from '$routes/console/project-[project]/functions/function-[function]/store';
import { ProxyTypes } from '../index.svelte';
import { domain, typeStore } from './store';
import { consoleVariables } from '$routes/console/store';
import { ResourceType } from '@appwrite.io/console';
let error = null;
const isDomainsEnabled = $consoleVariables?._APP_DOMAIN_ENABLED === true;
@@ -22,7 +22,7 @@
$domain = await sdk.forProject.proxy.createRule(
$domain.domain,
$typeStore,
$typeStore === ProxyTypes.FUNCTION ? $func.$id : undefined
$typeStore === ResourceType.Function ? $func.$id : undefined
);
trackEvent(Submit.DomainCreate);
@@ -34,7 +34,7 @@
</script>
<WizardStep beforeSubmit={createDomain}>
<svelte:fragment slot="title">Add function domain</svelte:fragment>
<svelte:fragment slot="title">Domain</svelte:fragment>
<svelte:fragment slot="subtitle">
Use your self-owned domain as the endpoint of your Appwrite API. <a
href="https://appwrite.io/docs/advanced/platform/custom-domains"
+2 -3
View File
@@ -1,8 +1,7 @@
import type { Models } from '@appwrite.io/console';
import type { Models, ResourceType } from '@appwrite.io/console';
import { writable } from 'svelte/store';
import type { ProxyTypes } from '../index.svelte';
import type { Dependencies } from '$lib/constants';
export const domain = writable<Partial<Models.ProxyRule>>({ $id: '', domain: '' });
export const typeStore = writable<ProxyTypes>();
export const typeStore = writable<ResourceType>();
export const dependencyStore = writable<Dependencies>();
+4 -4
View File
@@ -1,4 +1,4 @@
import type { Client, Models, Query } from '@appwrite.io/console';
import type { Client, Models } from '@appwrite.io/console';
import type { Organization, OrganizationList } from '../stores/organization';
import type { PaymentMethod } from '@stripe/stripe-js';
import type { Tier } from '$lib/stores/billing';
@@ -275,7 +275,7 @@ export class Billing {
this.client = client;
}
async listOrganization(queries: Query[] = []): Promise<OrganizationList> {
async listOrganization(queries: string[] = []): Promise<OrganizationList> {
const path = `/organizations`;
const params = {
queries
@@ -465,7 +465,7 @@ export class Billing {
);
}
async listInvoices(organizationId: string, queries: Query[] = []): Promise<InvoiceList> {
async listInvoices(organizationId: string, queries: string[] = []): Promise<InvoiceList> {
const path = `/organizations/${organizationId}/invoices`;
const params = {
organizationId,
@@ -886,7 +886,7 @@ export class Billing {
);
}
async listAddresses(queries: Query[] = []): Promise<AddressesList> {
async listAddresses(queries: string[] = []): Promise<AddressesList> {
const path = `/account/billing-addresses`;
const params = {
queries
+13 -8
View File
@@ -1,9 +1,9 @@
import { writable } from 'svelte/store';
import type { Models } from '@appwrite.io/console';
import { type Models, AuthMethod as AuthMethodEnum } from '@appwrite.io/console';
export type AuthMethod = {
label: string;
method: string;
method: AuthMethodEnum;
value: boolean | null;
};
@@ -11,32 +11,37 @@ const setAuthMethod = (project: Models.Project): AuthMethod[] => {
return [
{
label: 'Email/Password',
method: 'email-password',
method: AuthMethodEnum.Emailpassword,
value: project?.authEmailPassword
},
{
label: 'Phone',
method: 'phone',
method: AuthMethodEnum.Phone,
value: project?.authPhone
},
{
label: 'Magic URL',
method: 'magic-url',
method: AuthMethodEnum.Magicurl,
value: project?.authUsersAuthMagicURL
},
{
label: 'Email OTP',
method: AuthMethodEnum.Emailotp,
value: project?.authEmailOtp
},
{
label: 'Anonymous',
method: 'anonymous',
method: AuthMethodEnum.Anonymous,
value: project?.authAnonymous
},
{
label: 'Team Invites',
method: 'invites',
method: AuthMethodEnum.Invites,
value: project?.authInvites
},
{
label: 'JWT',
method: 'jwt',
method: AuthMethodEnum.Jwt,
value: project?.authJWT
}
];
+58 -4
View File
@@ -4,11 +4,11 @@ type TemplateRuntime = {
};
const TemplateRuntimes = {
NODE: { name: 'node', versions: ['20.0', '19.0', '18.0', '16.0', '14.5'] },
PHP: { name: 'php', versions: ['8.2', '8.1', '8.0'] },
RUBY: { name: 'ruby', versions: ['3.2', '3.1', '3.0'] },
NODE: { name: 'node', versions: ['21.0', '20.0', '19.0', '18.0', '16.0', '14.5'] },
PHP: { name: 'php', versions: ['8.3', '8.2', '8.1', '8.0'] },
RUBY: { name: 'ruby', versions: ['3.3', '3.2', '3.1', '3.0'] },
PYTHON: { name: 'python', versions: ['3.12', '3.11', '3.10', '3.9', '3.8'] },
DART: { name: 'dart', versions: ['3.1', '3.0', '2.19', '2.18', '2.17', '2.16', '2.16'] },
DART: { name: 'dart', versions: ['3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16', '2.16'] },
BUN: { name: 'bun', versions: ['1.0'] }
};
@@ -515,6 +515,30 @@ export const marketplace: MarketplaceTemplate[] = [
'npm install',
'src/main.js',
'node/sync-with-meilisearch'
),
...getRuntimes(
TemplateRuntimes.PYTHON,
'pip install -r requirements.txt',
'src/main.py',
'python/sync-with-meilisearch'
),
...getRuntimes(
TemplateRuntimes.PHP,
'composer install',
'src/index.php',
'php/sync-with-meilisearch'
),
...getRuntimes(
TemplateRuntimes.BUN,
'bun install',
'src/main.ts',
'bun/sync-with-meilisearch'
),
...getRuntimes(
TemplateRuntimes.RUBY,
'bundle install',
'lib/main.rb',
'ruby/sync-with-meilisearch'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/sync-with-meilisearch">file</a>.`,
@@ -599,6 +623,36 @@ export const marketplace: MarketplaceTemplate[] = [
'npm install',
'src/main.js',
'node/whatsapp-with-vonage'
),
...getRuntimes(
TemplateRuntimes.PYTHON,
'pip install -r requirements.txt',
'src/main.py',
'python/whatsapp_with_vonage'
),
...getRuntimes(
TemplateRuntimes.PHP,
'composer install',
'src/index.php',
'php/whatsapp-with-vonage'
),
...getRuntimes(
TemplateRuntimes.DART,
'dart pub get',
'lib/main.dart',
'dart/whatsapp-with-vonage'
),
...getRuntimes(
TemplateRuntimes.RUBY,
'bundle install',
'lib/main.rb',
'ruby/whatsapp-with-vonage'
),
...getRuntimes(
TemplateRuntimes.BUN,
'bun install',
'src/main.ts',
'bun/whatsapp-with-vonage'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/whatsapp-with-vonage">file</a>.`,
+19 -17
View File
@@ -35,21 +35,23 @@ export const createMigrationFormStore = () => {
};
};
const resources = [
'user',
'team',
'membership',
'file',
'bucket',
'function',
'envVar',
'deployment',
'database',
'collection',
'index',
'attribute',
'document'
] as const;
export const ResourcesFriendly = {
user: { singular: 'User', plural: 'Users' },
team: { singular: 'Team', plural: 'Teams' },
membership: { singular: 'Membership', plural: 'Memberships' },
file: { singular: 'File', plural: 'Files' },
bucket: { singular: 'Bucket', plural: 'Buckets' },
function: { singular: 'Function', plural: 'Functions' },
'environment variable': { singular: 'Environment Variable', plural: 'Environment Variables' },
deployment: { singular: 'Deployment', plural: 'Deployments' },
database: { singular: 'Database', plural: 'Databases' },
collection: { singular: 'Collection', plural: 'Collections' },
index: { singular: 'Index', plural: 'Indexes' },
attribute: { singular: 'Attribute', plural: 'Attributes' },
document: { singular: 'Document', plural: 'Documents' }
};
const resources = Object.keys(ResourcesFriendly);
type Resource = (typeof resources)[number];
@@ -101,7 +103,7 @@ export const migrationFormToResources = (
addResource('function');
}
if (formData.functions.env) {
addResource('envVar');
addResource('environment variable');
}
if (formData.functions.inactive) {
addResource('deployment');
@@ -155,7 +157,7 @@ export const resourcesToMigrationForm = (
if (resources.includes('function') && isVersionAtLeast(version, '1.4.0')) {
formData.functions.root = true;
}
if (resources.includes('envVar') && isVersionAtLeast(version, '1.4.0')) {
if (resources.includes('environment variable') && isVersionAtLeast(version, '1.4.0')) {
formData.functions.env = true;
}
if (resources.includes('deployment') && isVersionAtLeast(version, '1.4.0')) {
+8 -7
View File
@@ -11,8 +11,7 @@ import Microsoft from '../../routes/console/project-[project]/auth/(providers)/m
import Oidc from '../../routes/console/project-[project]/auth/(providers)/oidcOAuth.svelte';
import Okta from '../../routes/console/project-[project]/auth/(providers)/oktaOAuth.svelte';
export type Provider = Models.Provider & {
key: string;
export type Provider = Models.AuthProvider & {
icon: string;
docs?: string;
component?: typeof SvelteComponent<unknown>;
@@ -24,13 +23,12 @@ export type Providers = {
const setProviders = (project: Models.Project): Provider[] => {
return (
project?.providers.map((n) => {
const p = n as Models.Provider & { key: string };
project?.oAuthProviders.map((n) => {
let docs: Provider['docs'];
let icon: Provider['icon'] = p.key.toLowerCase();
let icon: Provider['icon'] = n.key.toLowerCase();
let component: Provider['component'] = Main;
switch (p.key.toLowerCase()) {
switch (n.key.toLowerCase()) {
case 'amazon':
docs = 'https://developer.amazon.com/apps-and-games/services-and-apis';
break;
@@ -150,6 +148,9 @@ const setProviders = (project: Models.Project): Provider[] => {
case 'yandex':
docs = 'https://tech.yandex.com/oauth/';
break;
case 'zoho':
docs = 'https://www.zoho.com/accounts/protocol/oauth/sign-in-using-zoho.html';
break;
case 'zoom':
docs = 'https://marketplace.zoom.us/docs/guides/auth/oauth/';
break;
@@ -158,7 +159,7 @@ const setProviders = (project: Models.Project): Provider[] => {
}
return {
...p,
...n,
icon,
docs,
component
+31 -31
View File
@@ -1,9 +1,9 @@
import { writable } from 'svelte/store';
import type { Models } from '@appwrite.io/console';
import { ApiService, type Models } from '@appwrite.io/console';
export type Service = {
label: string;
method: string;
method: ApiService;
value: boolean | null;
};
@@ -12,62 +12,57 @@ function createServices() {
list: [
{
label: 'Account',
method: 'account',
method: ApiService.Account,
value: null
},
{
label: 'Avatars',
method: 'avatars',
method: ApiService.Avatars,
value: null
},
{
label: 'Databases',
method: 'databases',
method: ApiService.Databases,
value: null
},
{
label: 'Functions',
method: 'functions',
method: ApiService.Functions,
value: null
},
{
label: 'Health',
method: 'health',
method: ApiService.Health,
value: null
},
{
label: 'Locale',
method: 'locale',
method: ApiService.Locale,
value: null
},
{
label: 'Messaging',
method: ApiService.Messaging,
value: null
},
{
label: 'Storage',
method: 'storage',
method: ApiService.Storage,
value: null
},
{
label: 'Teams',
method: 'teams',
method: ApiService.Teams,
value: null
},
{
label: 'Users',
method: 'users',
method: ApiService.Users,
value: null
},
{
label: 'GraphQL',
method: 'graphql',
value: null
},
{
label: 'VCS',
method: 'vcs',
value: null
},
{
label: 'Proxy',
method: 'proxy',
method: ApiService.Graphql,
value: null
}
]
@@ -80,47 +75,52 @@ function createServices() {
const list = [
{
label: 'Account',
method: 'account',
method: ApiService.Account,
value: project.serviceStatusForAccount
},
{
label: 'Avatars',
method: 'avatars',
method: ApiService.Avatars,
value: project.serviceStatusForAvatars
},
{
label: 'Databases',
method: 'databases',
method: ApiService.Databases,
value: project.serviceStatusForDatabases
},
{
label: 'Functions',
method: 'functions',
method: ApiService.Functions,
value: project.serviceStatusForFunctions
},
{
label: 'Health',
method: 'health',
method: ApiService.Health,
value: project.serviceStatusForHealth
},
{
label: 'Locale',
method: 'locale',
method: ApiService.Locale,
value: project.serviceStatusForLocale
},
{
label: 'Messaging',
method: ApiService.Messaging,
value: project.serviceStatusForMessaging
},
{
label: 'Storage',
method: 'storage',
method: ApiService.Storage,
value: project.serviceStatusForStorage
},
{
label: 'Teams',
method: 'teams',
method: ApiService.Teams,
value: project.serviceStatusForTeams
},
{
label: 'Users',
method: 'users',
method: ApiService.Users,
value: project.serviceStatusForUsers
}
];
+2
View File
@@ -10,6 +10,7 @@ import {
Functions,
Health,
Locale,
Messaging,
Migrations,
Project,
Project as ProjectApi,
@@ -38,6 +39,7 @@ const sdkForProject = {
functions: new Functions(clientProject),
health: new Health(clientProject),
locale: new Locale(clientProject),
messaging: new Messaging(clientProject),
project: new Project(clientProject),
projectApi: new ProjectApi(clientProject),
storage: new Storage(clientProject),
@@ -11,9 +11,14 @@
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { isValueOfStringEnum } from '$lib/helpers/types';
import { Runtime } from '@appwrite.io/console';
async function createGitHubInstallation() {
try {
if (!isValueOfStringEnum(Runtime, $func.runtime)) {
throw new Error(`Invalid runtime: ${$func.runtime}`);
}
await sdk.forProject.functions.update(
$func.$id,
$func.name,
+5 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { ID } from '@appwrite.io/console';
import { ID, Runtime } from '@appwrite.io/console';
import { Wizard } from '$lib/layout';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import { sdk } from '$lib/stores/sdk';
@@ -13,9 +13,13 @@
import ExecuteAccess from './steps/executeAccess.svelte';
import GitConfiguration from './steps/gitConfiguration.svelte';
import FunctionConfiguration from './steps/functionConfiguration.svelte';
import { isValueOfStringEnum } from '$lib/helpers/types';
async function create() {
try {
if (!isValueOfStringEnum(Runtime, $createFunction.runtime)) {
throw new Error(`Invalid runtime: ${$createFunction.runtime}`);
}
const response = await sdk.forProject.functions.create(
$createFunction.$id || ID.unique(),
$createFunction.name,
@@ -1,5 +1,5 @@
<script lang="ts">
import { ID } from '@appwrite.io/console';
import { ID, Runtime } from '@appwrite.io/console';
import { Wizard } from '$lib/layout';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import { sdk } from '$lib/stores/sdk';
@@ -19,9 +19,13 @@
import Details from './steps/details.svelte';
import Configuration from './steps/configuration.svelte';
import ExecuteAccess from './steps/executeAccess.svelte';
import { isValueOfStringEnum } from '$lib/helpers/types';
async function create() {
try {
if (!isValueOfStringEnum(Runtime, $createFunction.runtime)) {
throw new Error(`Invalid runtime: ${$createFunction.runtime}`);
}
const response = await sdk.forProject.functions.create(
$createFunction.$id || ID.unique(),
$createFunction.name,
@@ -1,5 +1,5 @@
<script lang="ts">
import { ID } from '@appwrite.io/console';
import { ID, Runtime } from '@appwrite.io/console';
import { Wizard } from '$lib/layout';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import { sdk } from '$lib/stores/sdk';
@@ -16,9 +16,13 @@
import CreateRepository from './steps/createRepository.svelte';
import TemplateVariables from './steps/templateVariables.svelte';
import { scopes } from '$lib/constants';
import { isValueOfStringEnum } from '$lib/helpers/types';
async function create() {
try {
if (!isValueOfStringEnum(Runtime, $templateConfig.runtime)) {
throw new Error(`Invalid runtime: ${$templateConfig.runtime}`);
}
const runtimeDetail = $template.runtimes.find(
(r) => r.name === $templateConfig.runtime
);
@@ -5,7 +5,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Execution permissions</svelte:fragment>
<svelte:fragment slot="title">Permissions</svelte:fragment>
<svelte:fragment slot="subtitle">
Choose who can execute this function using the client API. For more information, visit our
<a
+27 -34
View File
@@ -4,18 +4,16 @@
import { base } from '$app/paths';
import { page } from '$app/stores';
import { trackPageView } from '$lib/actions/analytics';
import { reportWebVitals } from '$lib/helpers/vitals';
import { Notifications, Progress } from '$lib/layout';
import { app } from '$lib/stores/app';
import { user } from '$lib/stores/user';
import { ENV, isCloud } from '$lib/system';
import * as Sentry from '@sentry/svelte';
import { onMount } from 'svelte';
import { onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from 'web-vitals';
import Loading from './loading.svelte';
import { loading, requestedMigration } from './store';
import { parseIfString } from '$lib/helpers/object';
import Consent from '$lib/components/consent.svelte';
import type { Models } from '@appwrite.io/console';
if (browser) {
window.VERCEL_ANALYTICS_ID = import.meta.env.VERCEL_ANALYTICS_ID?.toString() ?? false;
@@ -26,17 +24,6 @@
const migrateData = $page.url.searchParams.get('migrate');
requestedMigration.set(parseIfString(migrateData) as Record<string, string>);
}
/**
* Reporting Web Vitals.
*/
if (ENV.PROD && window.VERCEL_ANALYTICS_ID) {
onCLS(reportWebVitals);
onFID(reportWebVitals);
onLCP(reportWebVitals);
onFCP(reportWebVitals);
onINP(reportWebVitals);
onTTFB(reportWebVitals);
}
if (ENV.PROD) {
/**
@@ -52,37 +39,43 @@
/**
* Handle initial load.
*/
if (!$page.url.pathname.startsWith('/auth') && !$page.url.pathname.startsWith('/git')) {
const acceptedRoutes = [
'/login',
'/register',
'/recover',
'/invite',
'/card',
'/hackathon'
];
if ($user) {
if (
!$page.url.pathname.startsWith('/console') &&
!$page.url.pathname.startsWith('/invite') &&
!$page.url.pathname.startsWith('/card') &&
!$page.url.pathname.startsWith('/hackathon')
) {
function shouldRedirect(route: string, routes: string[]) {
return !routes.some((n) => route.startsWith(n));
}
const authenticationRoutes = ['/auth', '/git'];
const acceptedUnauthenticatedRoutes = [
'/login',
'/register',
'/recover',
'/invite',
'/card',
'/hackathon',
'/mfa'
];
const acceptedAuthenticatedRoutes = ['/console', '/invite', '/card', '/hackathon'];
const pathname = $page.url.pathname;
const user = $page.data.account as Models.User<Record<string, string>>;
if (shouldRedirect(pathname, authenticationRoutes)) {
if (user?.$id) {
if (shouldRedirect(pathname, acceptedAuthenticatedRoutes)) {
await goto(`${base}/console`, {
replaceState: true
});
}
loading.set(false);
} else {
if (acceptedRoutes.some((n) => $page.url.pathname.startsWith(n))) {
await goto(`${base}${$page.url.pathname}${$page.url.search}`);
if (acceptedUnauthenticatedRoutes.some((n) => pathname.startsWith(n))) {
await goto(`${base}${pathname}${$page.url.search}`);
} else {
await goto(`${base}/login`, {
replaceState: true
});
}
loading.set(false);
}
loading.set(false);
}
});
+10 -4
View File
@@ -34,13 +34,19 @@ export const load: LayoutLoad = async ({ depends, url }) => {
'/auth/oauth2/success',
'/auth/oauth2/failure',
'/card',
'/hackathon'
'/hackathon',
'/mfa'
];
const redirectUrl = url.pathname && url.pathname !== '/' ? `redirect=${url.pathname}` : '';
const path = url.search ? `${url.search}&${redirectUrl}` : `?${redirectUrl}`;
if (error.type === 'user_more_factors_required') {
if (url.pathname === '/mfa') return;
redirect(303, `/mfa${path}`);
}
if (!acceptedRoutes.some((n) => url.pathname.startsWith(n))) {
const redirectUrl =
url.pathname && url.pathname !== '/' ? `redirect=${url.pathname}` : '';
const path = url.search ? `${url.search}&${redirectUrl}` : `?${redirectUrl}`;
redirect(303, `/login${path}`);
}
}
-21
View File
@@ -1,29 +1,8 @@
<script>
import { addSubPanel, registerCommands } from '$lib/commandCenter';
import { TeamsPanel, UsersPanel } from '$lib/commandCenter/panels';
import { Container } from '$lib/layout';
import { loading } from '../store';
loading.set(false);
$registerCommands([
{
label: 'Find users',
callback: () => {
addSubPanel(UsersPanel);
},
group: 'users',
rank: -1
},
{
label: 'Find teams',
callback: () => {
addSubPanel(TeamsPanel);
},
group: 'teams',
rank: -1
}
]);
</script>
<Container>
@@ -3,7 +3,7 @@
import { Button } from '$lib/elements/forms';
import { deepMap } from '$lib/helpers/object';
import type { WritableValue } from '$lib/helpers/types';
import { sdk, type getSdkForProject } from '$lib/stores/sdk';
import { type getSdkForProject } from '$lib/stores/sdk';
import { onMount } from 'svelte';
@@ -107,19 +107,10 @@
);
break;
case 'firebase':
if ($provider.projectId) {
// OAuth
report = await sdk.forProject.migrations.getFirebaseReportOAuth(
providerResources.firebase,
$provider.projectId
);
} else if ($provider.serviceAccount) {
// Manual auth
report = await projectSdk.migrations.getFirebaseReport(
providerResources.firebase,
$provider.serviceAccount
);
}
report = await projectSdk.migrations.getFirebaseReport(
providerResources.firebase,
$provider.serviceAccount
);
break;
case 'nhost':
@@ -353,7 +344,7 @@
<div />
<span>Import all functions and their active deployment</span>
<ul>
{#if resources?.includes('envVar')}
{#if resources?.includes('environment variable')}
<li class="checkbox-field">
<input
type="checkbox"
@@ -12,7 +12,6 @@
let selectedOrg = organizations.length ? organizations[0].$id : null;
let projects = [] as Models.ProjectList['projects'];
$: hasProjects = projects.length > 0;
let loadingProjects = false;
let projectType = 'existing';
@@ -43,9 +42,7 @@
</script>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">
{hasProjects ? 'Select project' : 'Create project'}
</svelte:fragment>
<svelte:fragment slot="title">Project</svelte:fragment>
<div class="u-flex u-flex-vertical u-gap-24">
{#if organizations.length > 1}
@@ -7,7 +7,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Select resources</svelte:fragment>
<svelte:fragment slot="title">Resources</svelte:fragment>
<svelte:fragment slot="subtitle">
Select the resources you need to migrate to Appwrite. Some resources can be migrated, but
with limitations. <a
+3 -5
View File
@@ -6,9 +6,7 @@
</script>
<Container>
<div class="u-flex-vertical u-gap-16">
<Heading size="1" tag="h3">{$page.error.status || 'Invalid Argument'}</Heading>
<Heading size="4" tag="h4" trimmed={false}>{$page.error.message}</Heading>
<Button href="/console">Back to the console</Button>
</div>
<Heading size="1" tag="h3">{$page.error.status || 'Invalid Argument'}</Heading>
<Heading size="3" tag="h4">{$page.error.message}</Heading>
<Button href="/console">Back to the console</Button>
</Container>
+1 -10
View File
@@ -58,7 +58,7 @@
$: $registerCommands([
{
label: 'Go to projects',
label: 'Go to Projects',
callback: () => {
goto('/console');
},
@@ -79,15 +79,6 @@
icon: 'sparkles',
disabled: !isAssistantEnabled
},
{
label: 'Go to account',
callback: () => {
goto('/console/account');
},
keys: ['i'],
group: 'navigation',
rank: -2
},
{
label: 'Create new organization',
callback: () => {
+6 -1
View File
@@ -1,9 +1,14 @@
import Header from './header.svelte';
import Breadcrumbs from './breadcrumbs.svelte';
import type { LayoutLoad } from './$types';
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
export const load: LayoutLoad = async ({ depends }) => {
depends(Dependencies.FACTORS);
export const load: LayoutLoad = async () => {
return {
factors: await sdk.forConsole.account.listMfaFactors(),
header: Header,
breadcrumbs: Breadcrumbs
};
+2
View File
@@ -4,11 +4,13 @@
import UpdateName from './updateName.svelte';
import UpdateEmail from './updateEmail.svelte';
import DeleteAccount from './deleteAccount.svelte';
import UpdateMfa from './updateMfa.svelte';
</script>
<Container>
<UpdateName />
<UpdateEmail />
<UpdatePassword />
<UpdateMfa />
<DeleteAccount />
</Container>
@@ -0,0 +1,62 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import FormList from '$lib/elements/forms/formList.svelte';
import InputDigits from '$lib/elements/forms/inputDigits.svelte';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { AuthenticatorType } from '@appwrite.io/console';
export let showDelete = false;
let code: string;
let error: string;
async function deleteProvider() {
try {
await sdk.forConsole.account.deleteMfaAuthenticator(AuthenticatorType.Totp, code);
await invalidate(Dependencies.ACCOUNT);
await invalidate(Dependencies.FACTORS);
showDelete = false;
addNotification({
type: 'success',
message: 'The authenticator has been removed'
});
trackEvent(Submit.AccountAuthenticatorDelete);
} catch (e) {
error = e.message;
trackError(e, Submit.AccountAuthenticatorDelete);
}
}
$: if (showDelete) {
code = '';
error = '';
}
</script>
<Modal
title="Delete authentication method"
bind:show={showDelete}
onSubmit={deleteProvider}
icon="exclamation"
state="warning"
bind:error
headerDivider={false}>
<p class="u-bold">
Deleting the authentication method will disable multi-factor authentication for your
account. To re-enable it, you'll need to add a new authentication method.
</p>
<p>Enter the 6-digit verification code generated by your authenticator app to continue.</p>
<FormList>
<InputDigits autofocus required bind:value={code} />
</FormList>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit>Delete</Button>
</svelte:fragment>
</Modal>
+194
View File
@@ -0,0 +1,194 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackError } from '$lib/actions/analytics';
import { Modal, Output, Copy, Alert } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, FormList, InputDigits } from '$lib/elements/forms';
import { Table, TableBody, TableCell, TableRow } from '$lib/elements/table';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { AuthenticatorType, type Models } from '@appwrite.io/console';
import QrFrame from '$lib/images/qr2.svg';
export let showSetup = false;
export let showRecoveryCodes = false;
let copyParent: HTMLElement;
let code: string;
let type: Models.MfaType = null;
let step = 1;
let codes: Models.MfaRecoveryCodes = null;
async function addAuthenticator(): Promise<URL> {
type = await sdk.forConsole.account.createMfaAuthenticator(AuthenticatorType.Totp);
return sdk.forConsole.avatars.getQR(type.uri, 192 * 2);
}
async function verifyAuthenticator() {
try {
await sdk.forConsole.account.updateMfaAuthenticator(AuthenticatorType.Totp, code);
codes = await sdk.forConsole.account.createMfaRecoveryCodes();
await invalidate(Dependencies.ACCOUNT);
await invalidate(Dependencies.FACTORS);
showSetup = false;
showRecoveryCodes = true;
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.AccountDelete);
}
}
</script>
<Modal
title="Scan QR code"
description="Open your authentication app and scan the QR code."
bind:show={showSetup}
onSubmit={verifyAuthenticator}>
{#key showSetup}
<p>
Install an authenticator app on your mobile device, open it and scan the provided QR
code or enter it manually.
</p>
{#if step === 1}
{#await addAuthenticator()}
<div class="loading">
<div class="loader"></div>
</div>
{:then qr}
<div
class="qr"
style:background-image={`url(${QrFrame})`}
style:background-repeat="no-repeat"
style:background-position="center"
style:background-size="contain">
<img alt="MFA QR Code" class="code" src={qr.toString()} />
</div>
<span class="with-separators eyebrow-heading-3">or</span>
<div bind:this={copyParent}>
<label class="label" for="manual-code">Manual entry code</label>
<button class="tooltip" aria-label="sec retinfo">
<span class="icon-info" aria-hidden="true"></span>
<span class="tooltip-popup" role="tooltip">
<p class="text u-margin-block-start-8">
Manually enter the following code into the authenticator app
</p>
</span>
</button>
<div class="input-text-wrapper" style="--amount-of-buttons:1">
<input id="manual-code" type="text" value={type.secret} readonly={true} />
<div class="options-list">
{#key copyParent}
<Copy value={type.secret} appendTo={copyParent}>
<span class="icon-duplicate" aria-hidden="true" />
</Copy>
{/key}
</div>
</div>
</div>
{/await}
{:else}
<FormList>
<label for="code">Enter the 6-digit one-time code generated by the app</label>
<InputDigits bind:value={code} autofocus />
</FormList>
{/if}
{/key}
<svelte:fragment slot="footer">
<Button text on:click={() => (showSetup = false)}>Cancel</Button>
{#if step === 1}
<Button submit={false} on:click={() => (step = 2)}>Continue</Button>
{:else}
<Button submit>Continue</Button>
{/if}
</svelte:fragment>
</Modal>
<Modal
title="Save recovery codes"
description="Learn more about multi-factor authentication in our documentation."
bind:show={showRecoveryCodes}
onSubmit={verifyAuthenticator}>
{#if type && codes}
{@const formattedBackupCodes = codes.recoveryCodes.join('\n')}
<Alert type="info">
<span slot="title">
It is highly recommended to securely store your recovery codes
</span>
<p>
Use security codes for emergency sign-ins in case you've lost access to your mobile
device. Each recovery code can only be used once, but you can re-generate a new set
of 6 codes anytime.
</p>
</Alert>
<div
style:flex-direction="row-reverse"
class="u-flex u-flex-vertical-mobile u-main-space-between u-gap-16">
<ul class="buttons-list">
<li class="buttons-list-item">
<Button
download="backups.txt"
href={`data:application/octet-stream;charset=utf-8,${formattedBackupCodes}`}
text>
<span class="icon-download" />
<span class="text">Download</span>
</Button>
</li>
<li class="buttons-list-item">
<Copy value={formattedBackupCodes} appendTo="parent">
<Button text>
<span class="icon-duplicate" />
<span class="text">Copy all</span>
</Button>
</Copy>
</li>
</ul>
</div>
<Table noMargin noStyles>
<TableBody>
{#each codes.recoveryCodes as code}
<TableRow>
<TableCell title="code">
<Output value={code} hideCopyIcon>{code}</Output>
</TableCell>
<TableCell title="actions" width={24}>
<Copy value={code} appendTo="parent">
<span class="icon-duplicate" aria-hidden="true" />
</Copy>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
{/if}
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showRecoveryCodes = false)}>Close</Button>
</svelte:fragment>
</Modal>
<style lang="scss">
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 351px;
}
.qr {
width: 100%;
height: 220px;
display: flex;
align-items: center;
.code {
width: 100%;
max-width: 192px;
aspect-ratio: 1;
margin: 0 auto;
display: block;
}
}
</style>
@@ -15,15 +15,18 @@
import { sdk } from '$lib/stores/sdk';
import { goto, invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import type { Models } from '@appwrite.io/console';
import { Browser, type Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import { Submit, trackEvent } from '$lib/actions/analytics';
import { base } from '$app/paths';
import { isValueOfStringEnum } from '$lib/helpers/types';
export let data: PageData;
function getBrowser(clientCode: string) {
return sdk.forConsole.avatars.getBrowser(clientCode, 40, 40);
const code = clientCode.toLowerCase();
if (!isValueOfStringEnum(Browser, code)) return '';
return sdk.forProject.avatars.getBrowser(code, 40, 40);
}
async function logout(session: Models.Session) {
@@ -62,16 +65,24 @@
</TableHeader>
<TableBody>
{#each data.sessions.sessions as session}
{@const browser = getBrowser(session.clientCode)}
<TableRow>
<TableCell title="Client">
<div class="u-flex u-gap-12 u-cross-center">
{#if session.clientName}
<div class="avatar is-small">
<img
height="20"
width="20"
src={getBrowser(session.clientCode).toString()}
alt={session.clientName} />
{#if browser}
<img
height="20"
width="20"
src={getBrowser(session.clientCode).toString()}
alt={session.clientName} />
{:else}
<span
class="icon-globe-alt"
style="font-size: 20px"
aria-hidden="true" />
{/if}
</div>
<Trim>
{session.clientName}
@@ -116,7 +127,7 @@
<Button
external
secondary
href="https://appwrite.io/docs/references/cloud/client-web/account#createEmailSession">
href="https://appwrite.io/docs/references/cloud/client-web/account#createEmailPasswordSession">
Documentation
</Button>
</div>
+5
View File
@@ -0,0 +1,5 @@
import { page } from '$app/stores';
import { derived } from 'svelte/store';
import type { Models } from '@appwrite.io/console';
export const factors = derived(page, ($page) => $page.data.factors as Models.MfaFactors);
@@ -0,0 +1,94 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading, Empty } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, FormList, InputChoice } from '$lib/elements/forms';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRow
} from '$lib/elements/table';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import Mfa from './mfa.svelte';
import DeleteMfa from './deleteMfa.svelte';
import { factors } from './store';
let showSetup: boolean = false;
let showDelete: boolean = false;
async function updateMfa() {
try {
await sdk.forConsole.account.updateMFA(!$user.mfa);
await invalidate(Dependencies.ACCOUNT);
trackEvent(Submit.AccountUpdateEmail);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.AccountUpdateEmail);
}
}
</script>
<CardGrid>
<Heading tag="h6" size="7">Multi-factor authentication</Heading>
<svelte:fragment slot="aside">
<FormList>
<InputChoice
on:change={updateMfa}
type="switchbox"
id="mfa"
label="Multi-factor authentication"
value={$user.mfa} />
</FormList>
<p>Enhance the security of your account by adding authentication factors.</p>
{#if $user.mfa}
{#if $factors.totp}
<Table noMargin noStyles transparent>
<TableHeader>
<TableCellHead>Authenticator</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
<TableRow>
<TableCellText title="Authenticator">
TOTP (One-time code)
</TableCellText>
<TableCell>
<Button
on:click={() => (showDelete = true)}
round
text
ariaLabel="Delete authenticator">
<span class="icon-trash" aria-hidden="true" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Button disabled text noMargin>
<span class="icon-plus" />
<span class="text">Add authentication factor</span>
</Button>
{:else}
<Empty on:click={() => (showSetup = true)}>
<p class="text">Add authentication factor</p>
</Empty>
{/if}
{/if}
</svelte:fragment>
</CardGrid>
{#if showSetup}
<Mfa bind:showSetup />
{/if}
<DeleteMfa bind:showDelete />
@@ -10,6 +10,7 @@ import { headerAlert } from '$lib/stores/headerAlert';
import ProjectsAtRisk from '$lib/components/billing/alerts/projectsAtRisk.svelte';
import { get } from 'svelte/store';
import { preferences } from '$lib/stores/preferences';
import type { Organization } from '$lib/stores/organization';
export const load: LayoutLoad = async ({ params, depends }) => {
depends(Dependencies.ORGANIZATION);
@@ -37,7 +38,9 @@ export const load: LayoutLoad = async ({ params, depends }) => {
return {
header: Header,
breadcrumbs: Breadcrumbs,
organization: await sdk.forConsole.teams.get(params.organization),
organization: await (sdk.forConsole.teams.get(
params.organization
) as Promise<Organization>),
members: await sdk.forConsole.teams.listMemberships(params.organization)
};
} catch (e) {
@@ -25,7 +25,7 @@
import { sdk } from '$lib/stores/sdk';
import { loading } from '$routes/store';
import type { Models } from '@appwrite.io/console';
import { ID } from '@appwrite.io/console';
import { ID, Region } from '@appwrite.io/console';
import { openImportWizard } from '../project-[project]/settings/migrations/(import)';
import { readOnly } from '$lib/stores/billing';
import type { RegionList } from '$lib/sdk/billing';
@@ -105,7 +105,7 @@
ID.unique(),
`Imported project ${new Date().toISOString()}`,
$page.params.organization,
'default'
Region.Default
);
trackEvent(Submit.ProjectCreate, {
teamId: $page.params.organization
@@ -38,7 +38,7 @@
</script>
<WizardStep beforeSubmit={validateCoupon}>
<svelte:fragment slot="title">Add credits</svelte:fragment>
<svelte:fragment slot="title">Credits</svelte:fragment>
<svelte:fragment slot="subtitle">
Add Appwrite credits to your organization. A payment method is required before credits can
be applied.
@@ -42,7 +42,7 @@
</script>
<WizardStep beforeSubmit={handleSubmit}>
<svelte:fragment slot="title">Payment details</svelte:fragment>
<svelte:fragment slot="title">Details</svelte:fragment>
<svelte:fragment slot="subtitle">
Add a payment method to your organization. You will not be charged until you upgrade to a
Pro or Scale plan.

Some files were not shown because too many files have changed in this diff Show More