Merge branch 'main' of https://github.com/appwrite/console into feat-custom-smtp

This commit is contained in:
Damodar Lohani
2023-04-13 07:09:10 +05:45
187 changed files with 4253 additions and 1774 deletions
-1
View File
@@ -1,5 +1,4 @@
VITE_APPWRITE_ENDPOINT=
VITE_APPWRITE_GROWTH_ENDPOINT=
VITE_GA_PROJECT=
VITE_SENTRY_DSN=
VITE_CONSOLE_MODE=self-hosted
+35
View File
@@ -0,0 +1,35 @@
import { fileURLToPath } from 'url';
import { build, loadEnv } from 'vite';
import kleur from 'kleur';
const { bold, yellow } = kleur;
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const env = loadEnv('production', __dirname);
function log(text = '', prefix = '') {
console.log(`${bold().green(`# ${prefix}`)}${text}`);
}
function logEnv(key, value, fallback = 'not set') {
log(value || yellow(fallback), `${key}: `);
}
function logDelimiter() {
console.log(bold().green('#'.repeat(80)));
}
async function main() {
logDelimiter();
log();
log(bold().magenta('APPWRITE CONSOLE'));
log();
logEnv('CONSOLE MODE', env?.VITE_CONSOLE_MODE);
logEnv('APPWRITE ENDPOINT', env?.VITE_APPWRITE_ENDPOINT, 'relative');
logEnv('GROWTH ENDPOINT', env?.VITE_APPWRITE_GROWTH_ENDPOINT);
logEnv('GOOGLE ANALYTICS', env?.VITE_GA_PROJECT);
log();
logDelimiter();
await build();
}
main();
+638 -614
View File
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -5,7 +5,7 @@
},
"scripts": {
"dev": "vite dev",
"build": "vite build",
"build": "node build.js",
"preview": "vite preview",
"sync": "svelte-kit sync",
"check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning",
@@ -19,8 +19,8 @@
},
"dependencies": {
"@analytics/google-analytics": "^1.0.5",
"@appwrite.io/console": "0.1.0-preview-0.1",
"@appwrite.io/pink": "0.0.6-rc.2",
"@appwrite.io/console": "0.1.0",
"@appwrite.io/pink": "^0.0.6-rc.9",
"@popperjs/core": "^2.11.6",
"@sentry/svelte": "^7.44.2",
"@sentry/tracing": "^7.44.2",
@@ -35,9 +35,9 @@
},
"devDependencies": {
"@playwright/test": "^1.31.2",
"@sveltejs/adapter-static": "2.0.1",
"@sveltejs/kit": "1.13.0",
"@sveltejs/vite-plugin-svelte": "^2.0.3",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.15.4",
"@sveltejs/vite-plugin-svelte": "^2.0.4",
"@testing-library/dom": "^9.0.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/svelte": "^3.2.2",
@@ -50,6 +50,7 @@
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-svelte3": "^4.0.0",
"jsdom": "^21.1.1",
"kleur": "^4.1.5",
"pre-commit": "^1.2.2",
"prettier": "^2.8.6",
"prettier-plugin-svelte": "^2.9.0",
-1
View File
@@ -1,5 +1,4 @@
/// <reference types="@sveltejs/kit" />
interface Window {
VERCEL_ANALYTICS_ID: string | false;
SENTRY_DSN: string | false;
}
+2
View File
@@ -152,6 +152,7 @@ export enum Submit {
DatabaseDelete = 'submit_database_delete',
DatabaseUpdateName = 'submit_database_update_name',
AttributeCreate = 'submit_attribute_create',
AttributeUpdate = 'submit_attribute_update',
AttributeDelete = 'submit_attribute_delete',
DocumentCreate = 'submit_document_create',
DocumentDelete = 'submit_document_delete',
@@ -165,6 +166,7 @@ export enum Submit {
CollectionUpdatePermissions = 'submit_collection_update_permissions',
CollectionUpdateSecurity = 'submit_collection_update_security',
CollectionUpdateEnabled = 'submit_collection_update_enabled',
CollectionUpdateDisplayNames = 'submit_collection_update_display_names',
FunctionCreate = 'submit_function_create',
FunctionDelete = 'submit_function_delete',
FunctionUpdateName = 'submit_function_update_name',
+1
View File
@@ -43,6 +43,7 @@
class:common-section={!isTile}
class:is-border-dashed={isDashed}
class:is-danger={danger}
class:is-allowed-focus={href}
on:click
on:keyup={clickOnEnter}
{href}>
+5 -2
View File
@@ -1,11 +1,14 @@
<script lang="ts">
import { page } from '$app/stores';
import { Empty } from '$lib/components';
import { cardLimit } from '$lib/stores/layout';
import { CARD_LIMIT } from '$lib/constants';
import { preferences } from '$lib/stores/preferences';
export let offset = 0;
export let limit = $cardLimit;
export let total = 0;
export let event: string = null;
$: limit = preferences.get($page.route)?.limit ?? CARD_LIMIT;
</script>
<ul
+3
View File
@@ -0,0 +1,3 @@
<ul class="clickable-list">
<slot />
</ul>
@@ -0,0 +1,14 @@
<script lang="ts">
export let href: string;
</script>
<li class="clickable-list-item">
<a {href} class="clickable-list-button" on:click>
<h5 class="clickable-list-title u-trim-1">
<slot />
</h5>
<div class="clickable-list-desc">
<p class="text u-margin-block-start-8"><slot name="desc" /></p>
</div>
</a>
</li>
+1 -1
View File
@@ -28,7 +28,7 @@
});
</script>
<section class="box u-overflow-hidden " class:common-section={!noMargin}>
<section class="box u-overflow-hidden" class:common-section={!noMargin}>
<div
class="controls u-position-absolute u-inset-inline-end-8 u-inset-block-start-8 u-flex u-gap-8">
{#if label}
+1
View File
@@ -33,6 +33,7 @@
</script>
<span
data-private
style:cursor="pointer"
on:click|preventDefault={handleClick}
on:keyup={clickOnEnter}
+2 -1
View File
@@ -6,6 +6,7 @@
export let show = false;
export let name: string;
export let id: string;
export let autofocus = true;
$: if (!show) {
id = null;
@@ -27,7 +28,7 @@
</svelte:fragment>
<svelte:fragment slot="content">
<div class="form">
<InputId bind:value={id} />
<InputId bind:value={id} {autofocus} />
</div>
</svelte:fragment>
</InnerModal>
+28 -3
View File
@@ -4,12 +4,17 @@
<script lang="ts">
import { createPopper, type Instance } from '@popperjs/core';
import { onDestroy, onMount } from 'svelte';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
export let show = false;
export let noArrow = false;
export let placement: Placement = 'bottom-start';
export let childStart = false;
export let noStyle = false;
export let fullWidth = false;
export let fixed = false;
const dispatch = createEventDispatcher();
let element: HTMLDivElement;
let tooltip: HTMLDivElement;
@@ -19,6 +24,7 @@
onMount(() => {
instance = createPopper(element, tooltip, {
placement,
strategy: fixed ? 'fixed' : 'absolute',
modifiers: [
{
name: 'arrow',
@@ -37,6 +43,20 @@
options: {
fallbackPlacements: ['bottom-start', 'bottom-end', 'top-start', 'top-end']
}
},
{
name: 'sameWidth',
enabled: fixed,
phase: 'beforeWrite',
requires: ['computeStyles'],
fn: ({ state }) => {
state.styles.popper.width = `${state.rects.reference.width}px`;
},
effect: ({ state }) => {
state.elements.popper.style.width = `${
(state.elements.reference as HTMLElement)?.offsetWidth
}px`;
}
}
]
});
@@ -61,17 +81,22 @@
)
) {
show = false;
dispatch('blur');
}
};
</script>
<svelte:window on:click={onBlur} />
<div class="drop-wrapper" class:u-cross-child-start={childStart} bind:this={element}>
<div class:drop-wrapper={!noStyle} class:u-cross-child-start={childStart} bind:this={element}>
<slot />
</div>
<div class="drop-tooltip" bind:this={tooltip} style="z-index: 10">
<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} />
{#if show}
<slot name="list" />
+9 -6
View File
@@ -4,27 +4,30 @@
export let show = false;
export let placement: Placement = 'bottom-start';
export let fixed = false;
export let scrollable = false;
export let childStart = false;
export let noArrow = false;
export let noStyle = false;
export let width: string = null;
export let fullWidth = false;
export let position: 'relative' | 'static' = 'relative';
</script>
<Drop bind:show {placement} {childStart} {noArrow}>
<Drop bind:show {placement} {childStart} {noArrow} {noStyle} {fullWidth} {fixed} on:blur>
<slot />
<svelte:fragment slot="list">
<div
class="drop is-no-arrow"
style={`${
width
? `--drop-width-size-desktop:${width}rem; position:relative`
: ' position:relative'
class:u-max-width-100-percent={fullWidth}
style={`${width ? `--drop-width-size-desktop:${width}rem; ` : ''} ${
position === 'static' ? 'position:static' : 'position:relative'
}`}>
{#if $$slots.list}
<section
class:u-overflow-y-auto={scrollable}
class:u-max-height-200={scrollable}
class="drop-section ">
class="drop-section">
<ul class="drop-list">
<slot name="list" />
</ul>
+14 -1
View File
@@ -1,10 +1,23 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
export let disabled = false;
export let icon: string = null;
export let event: string = null;
function track() {
if (!event) {
return;
}
trackEvent(`click_${event}`, {
from: 'button'
});
}
</script>
<li class="drop-list-item">
<button class="drop-button" on:click|preventDefault {disabled}>
<button class="drop-button" on:click={track} on:click|preventDefault {disabled}>
<span class="text"><slot /></span>
{#if icon}
<span class={`icon-${icon}`} aria-hidden="true" />
+1 -1
View File
@@ -41,7 +41,7 @@
<slot>
<div class="u-text-center">
<Heading size="7" tag="h2">Create your first {target} to get started.</Heading>
<p class="body-text-2 u-margin-block-start-4">
<p class="body-text-2 u-bold u-margin-block-start-4">
Need a hand? Check out our documentation.
</p>
</div>
+7 -20
View File
@@ -10,8 +10,6 @@
import { feedback } from '$lib/stores/app';
import { addNotification } from '$lib/stores/notifications';
export let show = false;
let message: string;
let name: string;
let email: string;
@@ -29,21 +27,21 @@
message: error.message
});
} finally {
show = false;
feedback.toggleFeedback();
}
}
</script>
<section class="drop-section">
<header class="u-flex u-main-space-between u-gap-16">
<h4 class="body-text-1">How can we improve?</h4>
<h4 class="body-text-1 u-bold">How can we improve?</h4>
<button
type="button"
class="button is-text is-only-icon u-margin-inline-start-auto"
style="--button-size:1.5rem;"
aria-label="Close Modal"
title="Close Modal"
on:click={() => (show = false)}>
on:click={() => feedback.toggleFeedback()}>
<span class="icon-x" aria-hidden="true" />
</button>
</header>
@@ -53,29 +51,18 @@
<Form onSubmit={handleSubmit}>
<FormList>
<InputText
label="name"
id="name"
bind:value={name}
placeholder="Enter name"
showLabel={false} />
<InputEmail
label="email"
id="email"
bind:value={email}
placeholder="Enter email"
showLabel={false} />
<InputText label="Name" id="name" bind:value={name} placeholder="Enter name" />
<InputEmail label="Email" id="email" bind:value={email} placeholder="Enter email" />
<InputTextarea
id="feedback"
placeholder="Your message here"
showLabel={false}
label="Feedback"
label="Message"
required
bind:value={message} />
</FormList>
<div class="u-flex u-main-end u-gap-16 u-margin-block-start-24">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button text on:click={() => feedback.toggleFeedback()}>Cancel</Button>
<Button secondary submit>Submit</Button>
</div>
</Form>
+12 -18
View File
@@ -11,8 +11,6 @@
import { addNotification } from '$lib/stores/notifications';
import Evaluation from './evaluation.svelte';
export let show = false;
let value: number = null;
let message: string;
let email: string;
@@ -21,26 +19,32 @@
try {
await feedback.submitFeedback('feedback-nps', message, name, email, value);
feedback.switchType('general');
addNotification({
type: 'success',
message: 'Thank you for your feedback!'
});
} catch (error) {
feedback.switchType('general');
addNotification({
type: 'error',
message: error.message
});
} finally {
feedback.toggleFeedback();
}
}
</script>
<section class="drop-section">
<header class="u-flex u-main-space-between u-gap-16">
<h4 class="body-text-1">How are we doing?</h4>
<h4 class="body-text-1 u-bold">How are we doing?</h4>
<button
type="button"
class="button is-text is-only-icon u-margin-inline-start-auto"
style="--button-size:1.5rem;"
aria-label="Close Modal"
title="Close Modal"
on:click={() => (show = false)}>
on:click={() => feedback.toggleFeedback()}>
<span class="icon-x" aria-hidden="true" />
</button>
</header>
@@ -58,26 +62,16 @@
<InputTextarea
id="feedback"
placeholder="Your message here"
label="Feedback"
label="Message"
required
bind:value={message}
showLabel={false} />
<InputText
label="name"
id="name"
bind:value={name}
placeholder="Enter name"
showLabel={false} />
<InputEmail
label="email"
id="email"
bind:value={email}
placeholder="Enter email"
showLabel={false} />
<InputText label="Name" id="name" bind:value={name} placeholder="Enter name" />
<InputEmail label="Email" id="email" bind:value={email} placeholder="Enter email" />
</FormList>
{/if}
<div class="u-flex u-main-end u-gap-16 u-margin-block-start-24">
<Button text on:click={() => (show = false)}>No thanks</Button>
<Button text on:click={() => feedback.toggleFeedback()}>No thanks</Button>
<Button disabled={!value} secondary submit>Submit</Button>
</div>
</Form>
+67
View File
@@ -0,0 +1,67 @@
<script lang="ts">
import { Copy } from '.';
export let value: string;
export let event: string = null;
function truncateText(node: HTMLElement) {
const MAX_TRIES = 100;
let originalText = node.textContent;
function checkOverflow() {
node.textContent = originalText;
if (node.scrollWidth > node.clientWidth) {
let truncatedText = originalText;
let tries = 0;
while (
node.scrollWidth > node.clientWidth &&
truncatedText.length > 0 &&
tries < MAX_TRIES
) {
tries++;
if (truncatedText.includes('…')) {
const [left, right] = truncatedText.split('…');
truncatedText = `${left.slice(0, -1)}${right.slice(1)}`;
} else {
const left = truncatedText.slice(0, truncatedText.length / 2);
const right = truncatedText.slice(truncatedText.length / 2);
truncatedText = `${left.slice(0, -1)}${right.slice(1)}`;
}
node.textContent = truncatedText;
}
}
}
checkOverflow();
window.addEventListener('resize', checkOverflow);
return {
update() {
originalText = node.textContent;
checkOverflow();
},
destroy() {
window.removeEventListener('resize', checkOverflow);
}
};
}
</script>
<div
class="interactive-text-output is-buttons-on-top"
style:min-inline-size="0"
style:display="inline-flex">
<span
style:white-space="nowrap"
class="text u-line-height-1-5"
style:overflow="hidden"
use:truncateText>
<slot />
</span>
<div class="interactive-text-output-buttons">
<Copy {value} {event}>
<button class="interactive-text-output-button is-hidden" aria-label="copy text">
<span class="icon-duplicate" aria-hidden="true" />
</button>
</Copy>
</div>
</div>
+6 -1
View File
@@ -47,5 +47,10 @@ export { default as EventModal } from './eventModal.svelte';
export { default as Status } from './status.svelte';
export { default as Heading } from './heading.svelte';
export { default as Output } from './output.svelte';
export { default as Limit } from './limit.svelte';
export { default as ViewSelector } from './viewSelector.svelte';
export { default as LabelCard } from './labelCard.svelte';
export { default as Limit } from './limit.svelte';
export { default as PaginationWithLimit } from './paginationWithLimit.svelte';
export { default as ClickableList } from './clickableList.svelte';
export { default as ClickableListItem } from './clickableListItem.svelte';
export { default as Id } from './id.svelte';
+20 -14
View File
@@ -2,29 +2,35 @@
import { FormItem } from '$lib/elements/forms';
export let show = false;
export let closable = true;
</script>
{#if show}
<FormItem>
<section class="modal is-inner-modal">
<div class="modal-form">
<header class="modal-header u-flex-wrap">
<h4 class="modal-title body-text-1">
<slot name="title" />
</h4>
<button
type="button"
class="button is-text is-only-icon"
style="--button-size:1.5rem;"
aria-label="Close"
title="Close"
on:click={() => (show = false)}>
<span class="icon-x" aria-hidden="true" />
</button>
<p class="u-flex-basis-100-percent">
<header class="modal-header">
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
<h4 class="modal-title body-text-1 u-bold">
<slot name="title" />
</h4>
{#if closable}
<button
type="button"
class="button is-text is-only-icon"
style="--button-size:1.5rem;"
aria-label="Close"
title="Close"
on:click={() => (show = false)}>
<span class="icon-x" aria-hidden="true" />
</button>
{/if}
</div>
<p class="u-flex-basis-100-percent text u-small">
<slot name="subtitle" />
</p>
</header>
<div class="modal-content">
<slot name="content" />
</div>
+43
View File
@@ -0,0 +1,43 @@
<script lang="ts">
export let name: string;
export let group: string;
export let value: string | number | boolean;
export let disabled = false;
export let padding = 1;
export let icon: string = null;
export let fullHeight = true;
export let borderRadius: 'xsmall' | 'small' | 'medium' | 'large' = 'small';
enum Radius {
xsmall = '--border-radius-xsmall',
small = '--border-radius-small',
medium = '--border-radius-medium',
large = '--border-radius-large'
}
</script>
<label
class="card is-allow-focus u-cursor-pointer"
class:u-height-100-percent={fullHeight}
style:--card-padding={`${padding}rem`}
style:--card-border-radius={`var(${Radius[borderRadius]})`}>
<div class="u-flex u-gap-16">
<input
class="is-small u-margin-block-start-2"
type="radio"
{name}
{disabled}
{value}
bind:group
on:click />
<div class="u-flex u-flex-vertical u-gap-4">
<h4 class="body-text-2 u-bold"><slot name="title" /></h4>
<p class="u-color-text-gray u-small">
<slot />
</p>
</div>
{#if icon}
<span class={`icon-${icon} u-margin-inline-start-auto`} aria-hidden="true" />
{/if}
</div>
</label>
+37 -24
View File
@@ -6,9 +6,11 @@
export let show = false;
export let size: 'small' | 'big' = null;
export let warning = false;
export let icon: string = null;
export let state: 'success' | 'warning' | 'error' | 'info' = null;
export let error: string = null;
export let closable = true;
export let headerDivider = true;
export let onSubmit: () => Promise<void> | void = function () {
return;
};
@@ -79,33 +81,44 @@
class="modal"
class:is-small={size === 'small'}
class:is-big={size === 'big'}
bind:this={dialog}>
class:is-separate-header={headerDivider}
bind:this={dialog}
on:cancel|preventDefault>
{#if show}
<Form isModal {onSubmit}>
<header class="modal-header">
{#if warning}
<div class="avatar is-warning is-medium">
<span class="icon-exclamation" aria-hidden="true" />
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
<div class="u-flex u-cross-center u-gap-16">
{#if icon}
<div
class="avatar is-medium"
class:is-success={state === 'success'}
class:is-warning={state === 'warning'}
class:is-danger={state === 'error'}
class:is-info={state === 'info'}>
<span class={`icon-${icon}`} aria-hidden="true" />
</div>
{/if}
<h4 class="modal-title heading-level-5">
<slot name="header" />
</h4>
</div>
{/if}
<h4 class="modal-title heading-level-5">
<slot name="header" />
</h4>
{#if closable}
<button
type="button"
class="button is-text is-only-icon"
style="--button-size:1.5rem;"
aria-label="Close Modal"
title="Close Modal"
on:click={() =>
trackEvent('click_close_modal', {
from: 'button'
})}
on:click={closeModal}>
<span class="icon-x" aria-hidden="true" />
</button>
{/if}
{#if closable}
<button
type="button"
class="button is-text is-only-icon"
style="--button-size:1.5rem;"
aria-label="Close Modal"
title="Close Modal"
on:click={() =>
trackEvent('click_close_modal', {
from: 'button'
})}
on:click={closeModal}>
<span class="icon-x" aria-hidden="true" />
</button>
{/if}
</div>
</header>
<div class="modal-content">
{#if error}
@@ -0,0 +1,16 @@
<script lang="ts">
import Limit from './limit.svelte';
import Pagination from './pagination.svelte';
export let limit: number;
export let offset: number;
export let total: number;
export let name: string;
</script>
<div class="u-flex u-margin-block-start-32 u-flex-wrap">
<Limit {limit} sum={total} {name} />
<div class="u-margin-inline-start-auto">
<Pagination {limit} {offset} sum={total} />
</div>
</div>
+2 -2
View File
@@ -70,13 +70,13 @@
{@const isAnonymous = !data.email && !data.phone && isUser}
<div class="user-profile">
{#if isAnonymous}
<div class="avatar is-size-small ">
<div class="avatar is-size-small">
<span class="icon-anonymous" aria-hidden="true" />
</div>
{:else if data.name}
<AvatarInitials name={data.name} size={40} />
{:else}
<div class="avatar is-size-small ">
<div class="avatar is-size-small">
<span class="icon-minus-sm" aria-hidden="true" />
</div>
{/if}
+2 -2
View File
@@ -98,7 +98,7 @@
<label class="u-flex u-cross-center u-gap-8" for={team.$id}>
<AvatarInitials size={32} name={team.name} />
<div class="u-line-height-1-5">
<div class="body-text-2">{team.name}</div>
<div class="body-text-2 u-bold">{team.name}</div>
<div class="u-x-small">{team.$id}</div>
</div>
</label>
@@ -116,7 +116,7 @@
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<b class="body-text-2">Sorry we couldn't find "{search}"</b>
<b class="body-text-2 u-bold">Sorry we couldn't find "{search}"</b>
<p>There are no teams that match your search.</p>
</div>
<div class="u-flex u-gap-16 common-section u-main-center">
+6 -6
View File
@@ -93,28 +93,28 @@
{#if user.name}
<AvatarInitials size={32} name={user.name} />
<div class="u-line-height-1-5">
<div class="body-text-2">
<div class="body-text-2 u-bold">
{user.name}
</div>
<div class="u-x-small">{user.$id}</div>
</div>
{:else}
<div class="avatar is-size-small ">
<div class="avatar is-size-small">
<span class="icon-minus-sm" aria-hidden="true" />
</div>
<div class="u-line-height-1-5">
<div class="body-text-2">
<div class="body-text-2 u-bold">
{user.email ? user.email : user.phone}
</div>
<div class="u-x-small">{user.$id}</div>
</div>
{/if}
{:else}
<div class="avatar is-size-small ">
<div class="avatar is-size-small">
<span class="icon-anonymous" aria-hidden="true" />
</div>
<div class="u-line-height-1-5">
<div class="body-text-2">
<div class="body-text-2 u-bold">
{user.name ? user.name : '-'}
</div>
<div class="u-x-small">{user.$id}</div>
@@ -135,7 +135,7 @@
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<b class="body-text-2">Sorry we couldn't find "{search}"</b>
<b class="body-text-2 u-bold">Sorry we couldn't find "{search}"</b>
<p>There are no Users that match your search.</p>
</div>
<div class="u-flex u-gap-16 common-section u-main-center">
+37 -25
View File
@@ -15,12 +15,14 @@
import { preferences } from '$lib/stores/preferences';
import { onMount } from 'svelte';
import { View } from '$lib/helpers/load';
import { tooltip } from '$lib/actions/tooltip';
export let columns: Writable<Column[]>;
export let view: View;
export let isCustomCollection = false;
export let hideView = false;
export let hideColumns = false;
export let allowNoColumns = false;
let showSelectColumns = false;
@@ -73,32 +75,36 @@
}, 0);
</script>
{#if !hideColumns && view === View.Table}
{#if $columns?.length}
<DropList bind:show={showSelectColumns} scrollable={true}>
<Button secondary on:click={() => (showSelectColumns = !showSelectColumns)}>
<span
class="icon-view-boards u-opacity-50"
aria-hidden="true"
aria-label="columns" />
<span class="text">Columns</span>
<span class="inline-tag">{selectedColumnsNumber}</span>
</Button>
<svelte:fragment slot="list">
{#each $columns as column}
<InputChoice
id={column.id}
label={column.title}
bind:value={column.show}
disabled={selectedColumnsNumber <= 1 && column.show} />
{/each}
</svelte:fragment>
</DropList>
<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)}>
<span
class="icon-view-boards u-opacity-50"
aria-hidden="true"
aria-label="columns" />
<span class="text is-only-desktop">Columns</span>
<span class="inline-tag">{selectedColumnsNumber}</span>
</Button>
<svelte:fragment slot="list">
{#each $columns as column}
<InputChoice
id={column.id}
label={column.title}
bind:value={column.show}
disabled={allowNoColumns
? false
: selectedColumnsNumber <= 1 && column.show} />
{/each}
</svelte:fragment>
</DropList>
{/if}
{/if}
{/if}
</div>
{#if !hideView}
<div class="toggle-button">
<div class="grid-header-col-3 toggle-button">
<ul class="toggle-button-list">
{#key $page.url}
<li class="toggle-button-item">
@@ -108,7 +114,10 @@
class="toggle-button-element"
aria-label="List View"
type="button"
class:is-selected={view === View.Table}>
class:is-selected={view === View.Table}
use:tooltip={{
content: 'List View'
}}>
<span class="icon-view-list" aria-hidden="true" />
</a>
</li>
@@ -119,7 +128,10 @@
class="toggle-button-element"
aria-label="Grid View"
type="button"
class:is-selected={view === View.Grid}>
class:is-selected={view === View.Grid}
use:tooltip={{
content: 'Grid View'
}}>
<span class="icon-view-grid" aria-hidden="true" />
</a>
</li>
+1
View File
@@ -15,6 +15,7 @@ export enum Dependencies {
TEAMS = 'dependency:teams',
MEMBERSHIPS = 'dependency:memberships',
DATABASE = 'dependency:database',
DATABASES = 'dependency:databases',
COLLECTION = 'dependency:collection',
DOCUMENT = 'dependency:document',
DOCUMENTS = 'dependency:documents',
+5 -1
View File
@@ -1,7 +1,11 @@
<script lang="ts">
export let isCommonSection = false;
export let gap = 24;
</script>
<ul class="form-list" class:common-section={isCommonSection}>
<ul
class="form-list"
class:common-section={isCommonSection}
style={`--form-list-gap: ${gap / 16}rem;`}>
<slot />
</ul>
+1
View File
@@ -15,6 +15,7 @@ export { default as InputDateTime } from './inputDateTime.svelte';
export { default as InputSearch } from './inputSearch.svelte';
export { default as InputRadio } from './inputRadio.svelte';
export { default as InputSelect } from './inputSelect.svelte';
export { default as InputSelectSearch } from './inputSelectSearch.svelte';
export { default as InputCheckbox } from './inputCheckbox.svelte';
export { default as InputChoice } from './inputChoice.svelte';
export { default as InputPhone } from './inputPhone.svelte';
+23 -1
View File
@@ -1,13 +1,16 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
export let label: string;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let id: string;
export let value = '';
export let placeholder = '';
export let required = false;
export let nullable = false;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
@@ -38,10 +41,21 @@
$: if (value) {
error = null;
}
let prevValue = '';
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
if (isNull) {
prevValue = value;
value = null;
} else {
value = prevValue;
}
}
</script>
<FormItem>
<Label {required} hide={!showLabel} for={id}>
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
@@ -58,6 +72,14 @@
bind:value
bind:this={element}
on:invalid={handleInvalid} />
{#if nullable && !required}
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
</ul>
{/if}
</div>
{#if error}
<Helper type="warning">{error}</Helper>
+2 -2
View File
@@ -63,7 +63,7 @@
<p class="text">{label}</p>
{/if}
<div
class="box is-no-shadow u-padding-24"
class="box is-no-shadow u-padding-24"
style="--box-border-radius:var(--border-radius-xsmall); z-index: 1"
class:is-border-dashed={!hovering}
class:is-hover-with-file={hovering}
@@ -93,7 +93,7 @@
<div class="u-flex u-main-center u-cross-center u-gap-16 u-flex-vertical-mobile">
{#if maxSize}
{@const readableMaxSize = humanFileSize(maxSize)}
<p class="upload-file-box-info body-text-2 u-normal">
<p class="upload-file-box-info body-text-2">
Max file size: {readableMaxSize.value + readableMaxSize.unit}
</p>
{/if}
+3 -3
View File
@@ -4,11 +4,11 @@
import TextCounter from './textCounter.svelte';
export let value = '';
export let autofocus = true;
let element: HTMLInputElement;
let icon = 'info';
const pattern = String.raw`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`;
const autofocus = true;
onMount(() => {
if (element && autofocus) {
@@ -47,12 +47,12 @@
</div>
</FormItem>
<div
class="u-flex u-gap-4 u-margin-block-start-8 u-small"
class="u-flex u-gap-4 u-margin-block-start-8 u-small u-cross-center"
class:u-color-text-warning={icon === 'exclamation'}>
<span
class:icon-info={icon === 'info'}
class:icon-exclamation={icon === 'exclamation'}
class="u-cross-center u-margin-block-start-2 u-line-height-1 u-icon-small"
class="u-cross-center u-line-height-1 u-icon-small u-color-text-gray"
aria-hidden="true" />
<span class="text u-line-height-1-5">
Allowed characters: alphanumeric, non-leading hyphen, underscore, period
+23 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
export let label: string;
export let optionalText: string | undefined = undefined;
@@ -9,6 +10,7 @@
export let value: number = null;
export let placeholder = '';
export let required = false;
export let nullable = false;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
@@ -49,6 +51,17 @@
$: if (value) {
error = null;
}
let prevValue = 0;
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
if (isNull) {
prevValue = value;
value = null;
} else {
value = prevValue;
}
}
</script>
<FormItem>
@@ -70,7 +83,16 @@
class="input-text"
bind:value
bind:this={element}
on:invalid={handleInvalid} />
on:invalid={handleInvalid}
style:--amount-of-buttons={required ? 0 : 1.75} />
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
{#if nullable && !required}
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
{/if}
</ul>
</div>
{#if error}
<Helper type="warning">{error}</Helper>
+1 -1
View File
@@ -22,7 +22,7 @@
role="textbox"
{id}
aria-placeholder={placeholder}
class="input-text is-resizable"
class="input-text is-resizable"
bind:textContent={value} />
{:else}
<input
+3 -1
View File
@@ -38,6 +38,8 @@
$: if (value) {
error = null;
}
$: hasNullOption = options.some((option) => option.value === null);
</script>
<FormItem>
@@ -54,7 +56,7 @@
bind:value
on:invalid={handleInvalid}
on:change>
{#if placeholder}
{#if placeholder && !hasNullOption}
<option value={null} disabled selected hidden>{placeholder}</option>
{/if}
{#each options as option}
@@ -0,0 +1,196 @@
<script lang="ts">
import { DropList } from '$lib/components';
import { clickOnEnter } from '$lib/helpers/a11y';
import { createEventDispatcher, onMount } from 'svelte';
import { Label } from '.';
type Option = $$Generic<{
value: string | boolean | number;
label: string;
data?: string[];
}>;
type OptionArray = Option[];
export let options: OptionArray;
export let id: string;
export let label: string;
export let name = 'elements';
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let placeholder = '';
export let required = false;
export let disabled = false;
export let autofocus = false;
export let interactiveOutput = false;
// Input value
export let search = '';
// The actual selected value
export let value: Option['value'];
$: selectedOption = options.find((option) => option.value === value);
let element: HTMLInputElement;
let hasFocus = false;
let selected: number = null;
$: if (!hasFocus) {
selected = null;
}
const dispatch = createEventDispatcher();
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
function handleInput() {
hasFocus = true;
}
function handleKeydown(event: KeyboardEvent) {
event.stopImmediatePropagation();
switch (event.key) {
case 'Escape':
hasFocus = false;
break;
case 'ArrowDown':
if (selected === null) {
selected = 0;
} else {
selected = (selected + 1) % options.length;
}
break;
case 'ArrowUp':
if (selected === null) {
selected = options.length - 1;
} else {
selected = (selected - 1 + options.length) % options.length;
}
break;
case 'Enter':
if (selected !== null) {
event.preventDefault();
selectOption(options[selected]);
}
break;
}
}
function selectOption(option: Option) {
value = option.value;
search = option.label;
// It's not working without this line.
!$$slots.output && (element.value = search);
hasFocus = false;
search = '';
dispatch('select', option);
}
function clearOption() {
search = '';
value = null;
!$$slots.output && (element.value = search);
hasFocus = false;
dispatch('reset');
}
$: showClearBtn = (hasFocus && search) || value;
</script>
<li class="u-position-relative form-item u-stretch">
<DropList
bind:show={hasFocus}
noStyle
noArrow
scrollable
placement="bottom-end"
position="static"
fullWidth={true}
fixed>
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
<div class="custom-select">
<div class="input-text-wrapper" style="--amount-of-buttons:2">
{#if $$slots.output && selectedOption}
<div
on:keyup={clickOnEnter}
on:click={() => {
if (interactiveOutput) hasFocus = !hasFocus;
}}>
<slot name="output" option={selectedOption} />
</div>
{:else}
<input
type="text"
class="input-text"
{placeholder}
{disabled}
{required}
bind:value={search}
bind:this={element}
on:focus={() => (hasFocus = true)}
on:click={() => (hasFocus = true)}
on:input={handleInput}
on:keydown={handleKeydown} />
{/if}
<div class="options-list">
{#if showClearBtn}
<button
class="options-list-button"
aria-label="clear field"
type="button"
disabled={!interactiveOutput}
on:click|preventDefault={clearOption}>
<span class="icon-x" aria-hidden="true" />
</button>
{/if}
<button
class="options-list-button"
type="button"
disabled={!interactiveOutput}
on:click={() => (hasFocus = !hasFocus)}>
<span class="icon-cheveron-down" aria-hidden="true" />
</button>
</div>
</div>
</div>
<svelte:fragment slot="list">
{#each options as option, i}
<li class="drop-list-item">
<button
class="drop-button"
class:is-selected={selected === i}
type="button"
on:click|preventDefault={() => {
selectOption(option);
}}>
<slot {option}>
<span class="text" data-private>{option.label}</span>
</slot>
</button>
</li>
{:else}
<li class="drop-list-item">
<span class="text">There are no {name} that match your search</span>
</li>
{/each}
</svelte:fragment>
</DropList>
</li>
<style>
.form-item :global(.drop) {
translate: 0 4px;
}
.form-item :global(.drop-section) {
width: 100%;
margin-inline: initial;
max-inline-size: initial;
}
</style>
+26 -3
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
import TextCounter from './textCounter.svelte';
export let label: string;
@@ -10,6 +11,7 @@
export let value = '';
export let placeholder = '';
export let required = false;
export let nullable = false;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
@@ -40,6 +42,17 @@
$: if (value) {
error = null;
}
let prevValue = '';
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
if (isNull) {
prevValue = value;
value = null;
} else {
value = prevValue;
}
}
</script>
<FormItem>
@@ -62,9 +75,19 @@
class:u-padding-inline-end-56={typeof maxlength === 'number'}
bind:this={element}
on:invalid={handleInvalid} />
{#if maxlength}
<TextCounter max={maxlength} count={value?.length ?? 0} />
{/if}
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
{#if maxlength}
<li class="buttons-list-item">
<TextCounter max={maxlength} count={value?.length ?? 0} />
</li>
{/if}
{#if nullable && !required}
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
{/if}
</ul>
</div>
{#if error}
<Helper type="warning">{error}</Helper>
+32 -4
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
import TextCounter from './textCounter.svelte';
export let label: string;
@@ -9,6 +10,7 @@
export let value = '';
export let placeholder = '';
export let required = false;
export let nullable = false;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
@@ -37,6 +39,17 @@
$: if (value) {
error = null;
}
let prevValue = null;
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
if (isNull) {
prevValue = value;
value = null;
} else {
value = prevValue;
}
}
</script>
<FormItem>
@@ -53,13 +66,28 @@
{required}
{maxlength}
class="input-text"
class:u-padding-block-end-32={maxlength}
style={maxlength ? '--amount-of-buttons: 0.25' : ''}
bind:value
bind:this={element}
on:invalid={handleInvalid} />
{#if maxlength}
<TextCounter max={maxlength} count={value?.length ?? 0} />
{/if}
on:invalid={handleInvalid}
style:--amount-of-buttons={required ? undefined : 0.25} />
<ul
class="buttons-list u-gap-8 u-cross-center u-position-absolute d u-inset-block-end-1 u-inset-inline-end-1 u-padding-block-8 u-padding-inline-12"
style="border-end-end-radius:0.0625rem;">
{#if maxlength}
<li class="buttons-list-item">
<TextCounter max={maxlength} count={value?.length ?? 0} />
</li>
{/if}
{#if nullable && !required}
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
{/if}
</ul>
</div>
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
+2 -1
View File
@@ -3,6 +3,7 @@
import { FormItem, Helper, Label } from '.';
export let label: string;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let id: string;
export let value = '';
@@ -50,7 +51,7 @@
</script>
<FormItem>
<Label {required} hide={!showLabel} for={id}>
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
+1 -1
View File
@@ -18,7 +18,7 @@
</label>
{#if optionalText}
<span class="optional">{optionalText}</span>
<span class="optional u-margin-inline-start-8">{optionalText}</span>
{/if}
{#if tooltip}
@@ -0,0 +1,22 @@
<script lang="ts" context="module">
</script>
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let checked = false;
const dispatch = createEventDispatcher<{
change: boolean;
}>();
const handleChange = (e: Event) => {
const input = e.target as HTMLInputElement;
dispatch('change', !!input.checked);
};
</script>
<label class="u-flex u-cross-center u-gap-8 u-margin-inline-start-8">
<input bind:checked on:change={handleChange} type="checkbox" class="is-small" />
<span class="eyebrow-heading-3 u-x-small u-color-text-gray"> NULL </span>
</label>
@@ -0,0 +1,15 @@
<script lang="ts">
export let id: string = null;
export let text: string[] = [];
</script>
<div class="u-flex u-cross-baseline u-gap-12">
<span
class="u-flex-basis-140 u-flex-shrink-0 u-text-start u-trim-start u-x-small u-color-text-gray"
>{id}</span>
<span class="u-flex-1 u-trim-1">
{#each text as t, i}
{t}{i < text.length - 1 ? ', ' : ''}
{/each}
</span>
</div>
+1
View File
@@ -1 +1,2 @@
export { default as Pill } from './pill.svelte';
export { default as SelectSearchItem } from './selectSearchItem.svelte';
-29
View File
@@ -9,34 +9,6 @@
export let submit = false;
export let external = false;
export let href: string = null;
export let trim = false;
let element: HTMLButtonElement;
const isOverflowing = (elem: HTMLButtonElement, iterator = 1) => {
let parent = elem?.parentElement;
if (
trim &&
elem &&
elem.scrollWidth + 10 >
(parent.clientWidth ? parent.clientWidth : parent.parentElement.clientWidth)
)
trimText(iterator);
else return;
};
function trimText(iterator: number) {
if (iterator > 3) {
element.childNodes[element.childElementCount].textContent = '...';
} else {
let text = element.childNodes[element.childElementCount].textContent;
element.childNodes[element.childElementCount].textContent =
text.slice(0, 3) + '...' + text.slice(-9 + iterator * 2);
isOverflowing(element, iterator + 1);
}
}
$: isOverflowing(element);
</script>
{#if href}
@@ -55,7 +27,6 @@
</a>
{:else if button}
<button
bind:this={element}
on:click
{disabled}
type={submit ? 'submit' : 'button'}
+27
View File
@@ -0,0 +1,27 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
export let data: string[] = [];
</script>
<div class="u-flex u-cross-baseline u-gap-12">
<span
class="u-flex-basis-140 u-flex-shrink-0 u-text-start u-trim-start u-x-small u-color-text-gray u-font-code"
data-private>
<slot />
</span>
{#if data?.length}
<span
class="u-flex u-flex-1 u-trim-1 u-gap-4"
use:tooltip={{
content: data.map((e) => e ?? 'null').join(' | ')
}}>
{#each data as item, i}
{#if i}
<span class="u-text-color-gray">|</span>
{/if}
<span class="text" style="white-space: nowrap" data-private>{item}</span>
{/each}
</span>
{/if}
</div>
+2 -2
View File
@@ -5,11 +5,11 @@
</script>
<div
style={width ? `--p-col-width:${width?.toString()}` : ''}
style:--p-col-width={width?.toString()}
class:is-only-desktop={onlyDesktop}
class="table-thead-col"
role="columnheader">
<span class={eyebrow ? 'eyebrow-heading-3' : 'body-text-2'}>
<span class={eyebrow ? 'eyebrow-heading-3' : 'body-text-2 u-bold'}>
<slot />
</span>
</div>
+2
View File
@@ -3,10 +3,12 @@
export let title: string;
export let showOverflow = false;
export let onlyDesktop = false;
export let width: number = null;
</script>
<div
class="table-col"
style:--p-col-width={width?.toString()}
class:is-only-desktop={onlyDesktop}
class:u-overflow-visible={showOverflow}
data-title={title}
-1
View File
@@ -1,4 +1,3 @@
<!-- TODO: is 100% height needed? -->
<div class="table-with-scroll" data-private>
<div class="table-wrapper">
<div class="table is-remove-outer-styles">
+5 -2
View File
@@ -5,9 +5,12 @@ export function intersection(arr1: unknown[], arr2: unknown[]) {
}
export function difference(arr1: unknown[], arr2: unknown[]) {
if (!Array.isArray(arr1) || !Array.isArray(arr2)) {
return Array.isArray(arr1) ? arr1 : Array.isArray(arr2) ? arr2 : [];
}
const set = new Set(arr2);
const intersection = new Set(arr1.filter((elem) => !set.has(elem)));
return Array.from(intersection);
const difference = new Set(arr1.filter((elem) => !set.has(elem)));
return Array.from(difference);
}
export function symmetricDifference(arr1: unknown[], arr2: unknown[]) {
+29
View File
@@ -7,3 +7,32 @@
export function objectEntries<T extends object>(obj: T) {
return Object.entries(obj) as Array<[keyof T, T[keyof T]]>;
}
/**
* Recursively compares two objects to see if they are equal. Returns true if they are equal, false otherwise.
* @param obj1 the first object
* @param obj2 the second object
* @returns true if the objects are equal, false otherwise
*/
export function deepEqual<T>(obj1: T, obj2: T): boolean {
if (obj1 === obj2) return true;
if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null)
return false;
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (
!keys2.includes(key) ||
!Object.prototype.hasOwnProperty.call(obj2, key) ||
!deepEqual(obj1[key], obj2[key])
)
return false;
}
return true;
}
+20
View File
@@ -20,3 +20,23 @@ export function capitalize(str: string): string {
export function singular(str: string): string {
return str.replace(/s$/, '');
}
/**
* Convert a dash/underscore/space separated string to camelCase.
*
* @export
* @param {string} str - The string to convert.
* @returns {string} The camelized string.
*/
export function camelize(str: string): string {
if (!str) {
return '';
}
return str
.replace(/[-_\s]+(.)?/g, (_, char: string) => {
return char ? char.toUpperCase() : '';
})
.replace(/^(.)/, (firstChar) => {
return firstChar.toLowerCase();
});
}
+11 -6
View File
@@ -1,5 +1,11 @@
<script lang="ts">
import { AvatarInitials, EmptySearch, Limit, Pagination, Trim } from '$lib/components';
import {
AvatarInitials,
EmptySearch,
Heading,
PaginationWithLimit,
Trim
} from '$lib/components';
import {
TableBody,
TableHeader,
@@ -19,6 +25,7 @@
</script>
<Container>
<Heading tag="h2" size="5">Activity</Heading>
{#if logs.total}
<TableScroll>
<TableHeader>
@@ -42,7 +49,7 @@
<Trim>{log.userEmail}</Trim>
{/if}
{:else}
<div class="avatar is-size-small ">
<div class="avatar is-size-small">
<span class="icon-anonymous" aria-hidden="true" />
</div>
<span class="text u-trim">{log.userName ?? 'Anonymous'}</span>
@@ -64,10 +71,8 @@
{/each}
</TableBody>
</TableScroll>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<Limit {limit} sum={logs.total} name="Logs" />
<Pagination {limit} {offset} sum={logs.total} />
</div>
<PaginationWithLimit name="Logs" {limit} {offset} total={logs.total} />
{:else}
<EmptySearch>
<div class="u-flex u-flex-vertical u-cross-center">
+2 -3
View File
@@ -1,6 +1,5 @@
<script>
import { Mode, MODE } from '$lib/system';
import { isCloud } from '$lib/system';
import { version } from '$routes/console/store';
const currentYear = new Date().getFullYear();
@@ -11,7 +10,7 @@
<ul class="inline-links is-no-padding-first-and-last u-x-small">
<li class="inline-links-item">
<div class="u-flex u-cross-center u-gap-8">
{#if MODE !== Mode.CLOUD}
{#if isCloud}
<span class="icon-cloud" />
{/if}
{#if $version}
+30
View File
@@ -0,0 +1,30 @@
<script lang="ts">
import { Heading, ViewSelector } from '$lib/components';
import type { View } from '$lib/helpers/load';
import type { Column } from '$lib/components/viewSelector.svelte';
import type { Writable } from 'svelte/store';
export let title: string;
export let columns: Writable<Column[]>;
export let view: View;
export let isCustomCollection = false;
export let hideView = false;
export let hideColumns = false;
export let allowNoColumns = false;
</script>
<header class="grid-header">
<Heading tag="h2" size="5">{title}</Heading>
<div class="u-flex u-gap-16 u-contents-mobile">
<ViewSelector
{view}
{columns}
{isCustomCollection}
{hideView}
{hideColumns}
{allowNoColumns} />
<div class="grid-header-col-2">
<slot />
</div>
</div>
</header>
+4 -5
View File
@@ -16,7 +16,6 @@
import SystemMode from '$lib/images/mode/system-mode.svg';
import { FeedbackNPS } from '$lib/components';
let showFeedback = false;
import { slide } from 'svelte/transition';
import { page } from '$app/stores';
import { Submit, trackEvent } from '$lib/actions/analytics';
@@ -27,7 +26,7 @@
let droplistElement: HTMLDivElement;
function toggleFeedback() {
showFeedback = !showFeedback;
feedback.toggleFeedback();
if ($feedback.notification) {
feedback.toggleNotification();
feedback.addVisualization();
@@ -73,15 +72,15 @@
<div class="pulse-notification" />
</div>
{/if}
<DropList width="28" bind:show={showFeedback} scrollable={true}>
<DropList show={$feedback.show} scrollable on:blur={toggleFeedback}>
<button class="button is-small is-text" on:click={toggleFeedback}>
<span class="text">Feedback</span>
</button>
<svelte:fragment slot="other">
{#if $feedback.type === 'nps'}
<FeedbackNPS bind:show={showFeedback} />
<FeedbackNPS />
{:else}
<FeedbackGeneral bind:show={showFeedback} />
<FeedbackGeneral />
{/if}
</svelte:fragment>
</DropList>
+1
View File
@@ -15,3 +15,4 @@ export { default as Unauthenticated } from './unauthenticated.svelte';
export { default as Usage, type UsagePeriods } from './usage.svelte';
export { default as Activity } from './activity.svelte';
export { default as Progress } from './progress.svelte';
export { default as GridHeader } from './gridHeader.svelte';
+3 -3
View File
@@ -49,7 +49,7 @@
{#if $log.data}
<section class="cover-frame" data-private>
<header class="cover-frame-header u-flex u-gap-16 u-main-space-between u-cross-center">
<h1 class="body-text-1">Function ID: {$log.func.$id}</h1>
<h1 class="body-text-1 u-bold">Function ID: {$log.func.$id}</h1>
<button
on:click={() => ($log.show = false)}
class="button is-text is-only-icon"
@@ -73,7 +73,7 @@
</div>
<div>
<div class="u-flex u-gap-12 u-cross-center">
<h2 class="body-text-2">Deployment ID:</h2>
<h2 class="body-text-2 u-bold">Deployment ID:</h2>
<Output value={$log.data.$id}>
{$log.data.$id}
</Output>
@@ -145,7 +145,7 @@
</div>
<div>
<div class="u-flex u-gap-12 u-cross-center">
<h2 class="body-text-2">Execution ID:</h2>
<h2 class="body-text-2 u-bold">Execution ID:</h2>
<Output value={$log.data.$id}>
{$log.data.$id}
</Output>
+8 -4
View File
@@ -4,6 +4,7 @@
import type { Notification } from '../stores/notifications';
export let type: Notification['type'] = 'info';
export let icon: Notification['icon'] = null;
export let title: Notification['title'];
export let buttons: Notification['buttons'];
@@ -26,10 +27,11 @@
</button>
<div class="alert-sticky-image">
<span
class:icon-check-circle={type === 'success'}
class:icon-exclamation={type === 'warning'}
class:icon-exclamation-circle={type === 'error'}
class:icon-check-circle={type === 'success' && !icon}
class:icon-exclamation={type === 'warning' && !icon}
class:icon-exclamation-circle={type === 'error' && !icon}
class:icon-info={type === 'info'}
class={icon ? `icon-${icon}` : ''}
aria-hidden="true" />
</div>
<div class="alert-sticky-content">
@@ -41,7 +43,9 @@
{#if buttons}
<div class="alert-sticky-buttons u-flex">
{#each buttons as button}
<button class="button is-text is-small" on:click={button.method}>
<button
class="button is-text is-small"
on:click|preventDefault|stopPropagation={button.method}>
<span class="text">{button.name}</span>
</button>
{/each}
+1
View File
@@ -12,6 +12,7 @@
<Notification
type={notification.type}
title={notification.title}
icon={notification.icon}
on:dismiss={() => dismissNotification(notification.id)}
buttons={notification?.buttons}>
{notification.message}
+1 -5
View File
@@ -64,8 +64,8 @@
{/if}
<slot />
<slot name="footer" />
</section>
<slot name="footer" />
</main>
<style lang="scss">
@@ -92,8 +92,4 @@
grid-template-columns: auto 1fr !important;
}
}
.main-content {
overflow: hidden;
}
</style>
+2 -2
View File
@@ -95,11 +95,11 @@
<div class="wizard-end-bg" />
<header class="wizard-header">
<div class="body-text-1">{title}</div>
<div class="body-text-1 u-bold">{title}</div>
<slot name="header" />
<button
class="button is-text is-only-icon"
class="button is-text is-only-icon u-margin-inline-start-auto"
style="--button-size:1.5rem;"
aria-label="close wizard"
on:click={handleExit}>
+1 -1
View File
@@ -11,7 +11,7 @@
}
</script>
<Modal bind:show onSubmit={handleSubmit} warning>
<Modal bind:show onSubmit={handleSubmit} icon="exclamation" state="warning">
<svelte:fragment slot="header">Exit Process</svelte:fragment>
<p>
Are you sure you want to exit from <slot />? All data will be deleted. This action is
+10 -2
View File
@@ -12,6 +12,7 @@ export type Feedback = {
visualized: number;
notification: boolean;
type: 'nps' | 'general';
show: boolean;
};
export const app = writable<AppStore>({
@@ -24,17 +25,24 @@ function createFeedbackStore() {
elapsed: browser ? parseInt(localStorage.getItem('feedbackElapsed')) : 0,
visualized: browser ? parseInt(localStorage.getItem('feedbackVisualized')) : 0,
notification: false,
type: 'general'
type: 'general',
show: false
});
return {
subscribe,
update,
toggleFeedback: () => {
update((feedback) => {
feedback.show = !feedback.show;
return feedback;
});
},
toggleNotification: () =>
update((feedback) => {
feedback.notification = !feedback.notification;
return feedback;
}),
switchType: (feedType: 'nps' | 'general') =>
switchType: (feedType: Feedback['type']) =>
update((feedback) => {
feedback.type = feedType;
return feedback;
+3 -4
View File
@@ -1,4 +1,4 @@
import { writable, readable } from 'svelte/store';
import { writable } from 'svelte/store';
import type { SvelteComponent } from 'svelte';
export type Tab = {
@@ -11,6 +11,8 @@ export type Breadcrumb = {
title: string;
};
export type View = 'list' | 'grid';
export type updateLayoutArguments = {
header?: typeof SvelteComponent;
breadcrumb?: typeof SvelteComponent;
@@ -23,6 +25,3 @@ export function updateLayout(args: updateLayoutArguments) {
header.set(args.header ?? null);
breadcrumb.set(args.breadcrumb ?? null);
}
export const pageLimit = readable(12); // default page limit
export const cardLimit = readable(6); // default card limit
+5
View File
@@ -7,6 +7,7 @@ export type Notification = {
timeout?: number;
message: string;
title?: string;
icon?: string;
buttons?: Buttons[];
};
@@ -23,6 +24,10 @@ export const dismissNotification = (id: number) => {
notifications.update((all) => all.filter((t) => t.id !== id));
};
export const dismissAllNotifications = () => {
notifications.set([]);
};
export const addNotification = (notification: Omit<Notification, 'id'>) => {
const defaults = {
id: counter++,
+43 -1
View File
@@ -4,6 +4,8 @@ import type { View } from '$lib/helpers/load';
import type { Page } from '@sveltejs/kit';
import { get, writable } from 'svelte/store';
import { sdk } from './sdk';
import type { Models } from '@appwrite.io/console';
import { organization } from './organization';
type Preferences = {
limit?: number;
@@ -11,12 +13,19 @@ type Preferences = {
columns?: string[];
};
type TeamPreferences = {
names?: string[];
};
type PreferencesStore = {
[key: string]: {
[key: string]: Preferences;
collections?: {
[key: string]: Preferences['columns'];
};
displayNames?: {
[key: string]: TeamPreferences['names'];
};
};
};
@@ -100,7 +109,40 @@ function createPreferences() {
n[project].collections[collection] = columns;
return n;
})
}),
loadTeamPrefs: async (id: string) => {
const teamPrefs = await sdk.forConsole.teams.getPrefs(id);
update((n) => {
n[id] = teamPrefs;
return n;
});
return teamPrefs;
},
getDisplayNames: () => {
const id = get(organization)?.$id;
if (!id) return {};
let preferences: PreferencesStore;
subscribe((n) => (preferences = n))();
return preferences?.[id]?.displayNames ?? {};
},
setDisplayNames: async (collectionId: string, names: TeamPreferences['names']) => {
const id = get(organization).$id;
let teamPrefs: Models.Preferences;
update((n) => {
if (!n[id]?.displayNames) {
n[id] ??= {};
n[id].displayNames ??= {};
}
teamPrefs = n[id];
n[id].displayNames[collectionId] = names;
return n;
});
await sdk.forConsole.teams.updatePrefs(id, teamPrefs);
}
};
}
+1 -1
View File
@@ -4,5 +4,5 @@ import type { Models } from '@appwrite.io/console';
export const user = derived(
page,
($page) => $page.data.account as Models.Account<Record<string, string>>
($page) => $page.data.account as Models.User<Record<string, string>>
);
+6 -1
View File
@@ -29,7 +29,12 @@
}
</script>
<Modal bind:show={showDelete} onSubmit={deleteAccount} warning>
<Modal
bind:show={showDelete}
onSubmit={deleteAccount}
icon="exclamation"
state="warning"
headerDivider={false}>
<svelte:fragment slot="header">Delete Account</svelte:fragment>
<p>Are you sure you want to delete your account?</p>
<svelte:fragment slot="footer">
@@ -3,11 +3,10 @@
import {
GridItem1,
Empty,
Pagination,
AvatarGroup,
CardContainer,
Heading,
Limit
PaginationWithLimit
} from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
@@ -51,7 +50,7 @@
{organization.name}
</svelte:fragment>
{#await avatarList}
<span class="avatar is-color-empty" />
<span class="avatar is-color-empty" />
{:then avatars}
<AvatarGroup size={40} {avatars} />
{/await}
@@ -66,10 +65,12 @@
<p>Create a new organization</p>
</Empty>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<Limit limit={data.limit} sum={data.organizations.total} name="Organizations" />
<Pagination limit={data.limit} offset={data.offset} sum={data.organizations.total} />
</div>
<PaginationWithLimit
name="Organizations"
limit={data.limit}
offset={data.offset}
total={data.organizations.total} />
</Container>
<CreateOrganization bind:show={addOrganization} />
@@ -80,7 +80,7 @@
{session.osVersion}
</Trim>
{:else}
<span class="avatar is-color-empty" />
<span class="avatar is-color-empty" />
<p class="text u-trim">Unknown</p>
{/if}
<div class="is-only-desktop">
@@ -1,7 +1,7 @@
<script lang="ts">
import { base } from '$app/paths';
import { Pill } from '$lib/elements';
import { GridItem1, Heading, Empty, CardContainer, Pagination, Limit } from '$lib/components';
import { GridItem1, Heading, Empty, CardContainer, PaginationWithLimit } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import CreateProject from './createProject.svelte';
@@ -94,10 +94,12 @@
<p>Create a new project</p>
</Empty>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<Limit limit={data.limit} sum={data.projects.total} name="Projects" />
<Pagination limit={data.limit} offset={data.offset} sum={data.projects.total} />
</div>
<PaginationWithLimit
name="Projects"
limit={data.limit}
offset={data.offset}
total={data.projects.total} />
</Container>
<CreateOrganization bind:show={addOrganization} />
@@ -43,7 +43,12 @@
$: isUser = selectedMember?.userId === $user?.$id;
</script>
<Modal bind:show={showDelete} onSubmit={deleteMembership} warning>
<Modal
bind:show={showDelete}
onSubmit={deleteMembership}
icon="exclamation"
state="warning"
headerDivider={false}>
<svelte:fragment slot="header">
{isUser ? 'Leave Organization' : 'Delete Member'}
</svelte:fragment>
@@ -36,7 +36,12 @@
}
</script>
<Modal onSubmit={deleteOrg} bind:show={showDelete} warning>
<Modal
onSubmit={deleteOrg}
bind:show={showDelete}
icon="exclamation"
state="warning"
headerDivider={false}>
<svelte:fragment slot="header">Delete Organization</svelte:fragment>
<p>
Are you sure you want to delete <b>{$organization.name}</b>? All projects ({$organization.total})
@@ -2,7 +2,7 @@
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { AvatarInitials, Heading, Limit, Pagination } from '$lib/components';
import { AvatarInitials, Heading, PaginationWithLimit } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
@@ -112,10 +112,12 @@
{/each}
</TableBody>
</TableScroll>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<Limit limit={data.limit} sum={data.members.total} name="Members" />
<Pagination limit={data.limit} offset={data.offset} sum={data.members.total} />
</div>
<PaginationWithLimit
name="Members"
limit={data.limit}
offset={data.offset}
total={data.members.total} />
{/if}
</Container>
@@ -6,5 +6,5 @@
<Container>
<Heading size="1" tag="h3">{$page.status}</Heading>
<p class="body-text-2 u-margin-block-start-4">{$page.error.message}</p>
<p class="body-text-2 u-bold u-margin-block-start-4">{$page.error.message}</p>
</Container>
@@ -2,6 +2,7 @@ import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { error } from '@sveltejs/kit';
import type { LayoutLoad } from './$types';
import { preferences } from '$lib/stores/preferences';
export const load: LayoutLoad = async ({ params, depends }) => {
depends(Dependencies.PROJECT);
@@ -10,6 +11,7 @@ export const load: LayoutLoad = async ({ params, depends }) => {
const project = await sdk.forConsole.projects.get(params.project);
localStorage.setItem('project', project.$id);
localStorage.setItem('organization', project.teamId);
preferences.loadTeamPrefs(project.teamId);
return {
project,
@@ -3,11 +3,10 @@
import {
Empty,
EmptySearch,
Pagination,
Copy,
SearchQuery,
AvatarInitials,
Limit
PaginationWithLimit
} from '$lib/components';
import { Button } from '$lib/elements/forms';
import {
@@ -63,12 +62,12 @@
<AvatarInitials size={32} name={user.name} />
<span class="text u-trim">{user.name}</span>
{:else}
<div class="avatar is-size-small ">
<div class="avatar is-size-small">
<span class="icon-minus-sm" aria-hidden="true" />
</div>
{/if}
{:else}
<div class="avatar is-size-small ">
<div class="avatar is-size-small">
<span class="icon-anonymous" aria-hidden="true" />
</div>
<span class="text u-trim">{user.name}</span>
@@ -110,10 +109,12 @@
{/each}
</TableBody>
</Table>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<Limit limit={data.limit} sum={data.users.total} name="Users" />
<Pagination limit={data.limit} offset={data.offset} sum={data.users.total} />
</div>
<PaginationWithLimit
name="Users"
limit={data.limit}
offset={data.offset}
total={data.users.total} />
{:else if data.search}
<EmptySearch>
<div class="u-text-center">
@@ -64,7 +64,7 @@
type="radio"
bind:group={isLimited}
value={false} />
<div class="choice-item-content u-cross-child-center">
<div class="choice-item-content u-cross-child-center">
<div class="choice-item-title">Unlimited</div>
</div>
<Pill>recommended</Pill>
@@ -14,9 +14,8 @@
Empty,
EmptySearch,
AvatarInitials,
Pagination,
SearchQuery,
Limit
PaginationWithLimit
} from '$lib/components';
import Create from '../createTeam.svelte';
import { goto } from '$app/navigation';
@@ -68,10 +67,12 @@
{/each}
</TableBody>
</Table>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<Limit limit={data.limit} sum={data.teams.total} name="Teams" />
<Pagination limit={data.limit} offset={data.offset} sum={data.teams.total} />
</div>
<PaginationWithLimit
name="Teams"
limit={data.limit}
offset={data.offset}
total={data.teams.total} />
{:else if data.search}
<EmptySearch>
<div class="u-text-center">
@@ -37,7 +37,12 @@
};
</script>
<Modal bind:show={showDelete} onSubmit={deleteMembership} warning>
<Modal
bind:show={showDelete}
onSubmit={deleteMembership}
icon="exclamation"
state="warning"
headerDivider={false}>
<svelte:fragment slot="header">Delete Member</svelte:fragment>
<p data-private>
Are you sure you want to delete <b>{selectedMembership.userName}</b> from '{selectedMembership.teamName}'?
@@ -28,7 +28,12 @@
};
</script>
<Modal bind:show={showDelete} onSubmit={deleteTeam} warning>
<Modal
bind:show={showDelete}
onSubmit={deleteTeam}
icon="exclamation"
state="warning"
headerDivider={false}>
<svelte:fragment slot="header">Delete Team</svelte:fragment>
<p data-private>
Are you sure you want to delete <b>{team.name}</b>?
@@ -1,7 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { Copy, Tab, Tabs } from '$lib/components';
import { Pill } from '$lib/elements';
import { Id, Tab, Tabs } from '$lib/components';
import { isTabSelected } from '$lib/helpers/load';
import { Cover, CoverTitle } from '$lib/layout';
import { team } from './store';
@@ -35,12 +34,7 @@
<CoverTitle href={`/console/project-${projectId}/auth/teams`}>
{$team?.name}
</CoverTitle>
<Copy value={$team?.$id} event="team">
<Pill button>
<span class="icon-duplicate" aria-hidden="true" />
Team ID
</Pill>
</Copy>
<Id value={$team?.$id} event="team">{$team?.$id}</Id>
</svelte:fragment>
<Tabs>
@@ -4,9 +4,8 @@
Empty,
EmptySearch,
AvatarInitials,
Pagination,
SearchQuery,
Limit
PaginationWithLimit
} from '$lib/components';
import {
Table,
@@ -87,10 +86,12 @@
{/each}
</TableBody>
</Table>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<Limit limit={data.limit} sum={data.memberships.total} name="Memberships" />
<Pagination limit={data.limit} offset={data.offset} sum={data.memberships.total} />
</div>
<PaginationWithLimit
name="Memberships"
limit={data.limit}
offset={data.offset}
total={data.memberships.total} />
{:else if data.search}
<EmptySearch>
<div class="u-text-center">
@@ -31,7 +31,12 @@
}
</script>
<Modal bind:show={showDeleteAll} onSubmit={deleteAllSessions} warning>
<Modal
bind:show={showDeleteAll}
onSubmit={deleteAllSessions}
icon="exclamation"
state="warning"
headerDivider={false}>
<svelte:fragment slot="header">Delete All Sessions</svelte:fragment>
<p data-private>
Are you sure you want to delete <b>all of {$user.name}'s sessions?</b>
@@ -39,7 +39,12 @@
}
</script>
<Modal bind:show={showDelete} onSubmit={deleteMembership} warning>
<Modal
bind:show={showDelete}
onSubmit={deleteMembership}
icon="exclamation"
state="warning"
headerDivider={false}>
<svelte:fragment slot="header">Delete Member</svelte:fragment>
{#if selectedMembership}
<p data-private>
@@ -30,7 +30,12 @@
}
</script>
<Modal bind:show={showDelete} onSubmit={deleteSession} warning>
<Modal
bind:show={showDelete}
onSubmit={deleteSession}
icon="exclamation"
state="warning"
headerDivider={false}>
<svelte:fragment slot="header">Delete Sessions</svelte:fragment>
<p>Are you sure you want to delete this session?</p>
@@ -32,7 +32,12 @@
};
</script>
<Modal bind:show={showDelete} onSubmit={deleteUser} warning>
<Modal
bind:show={showDelete}
onSubmit={deleteUser}
icon="exclamation"
state="warning"
headerDivider={false}>
<svelte:fragment slot="header">Delete User</svelte:fragment>
<p data-private>Are you sure you want to delete <b>{$user.name}</b> from '{$project.name}'?</p>
<svelte:fragment slot="footer">
@@ -1,7 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { Copy, Tab, Tabs } from '$lib/components';
import { Pill } from '$lib/elements';
import { Id, Tab, Tabs } from '$lib/components';
import { isTabSelected } from '$lib/helpers/load';
import { Cover, CoverTitle } from '$lib/layout';
import { user } from './store';
@@ -39,12 +38,7 @@
<CoverTitle href={`/console/project-${projectId}/auth`}>
{$user.name ? $user.name : '-'}
</CoverTitle>
<Copy value={$user.$id} event="user">
<Pill button>
<span class="icon-duplicate" aria-hidden="true" />
User ID
</Pill>
</Copy>
<Id value={$user.$id} event="user">{$user.$id}</Id>
</svelte:fragment>
<Tabs>
@@ -29,7 +29,7 @@
<Container>
{#if data.sessions.total}
<div class="u-flex u-main-end common-section">
<div class="u-flex u-main-end common-section">
<Button secondary on:click={() => (showDeleteAll = true)}>
<span class="text">Delete All</span>
</Button>
@@ -2,8 +2,8 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { Button } from '$lib/elements/forms';
import { Empty, Heading, Pagination, Limit, ViewSelector } from '$lib/components';
import { Container } from '$lib/layout';
import { Empty, PaginationWithLimit } from '$lib/components';
import { Container, GridHeader } from '$lib/layout';
import { base } from '$app/paths';
import Create from './create.svelte';
import Grid from './grid.svelte';
@@ -24,18 +24,12 @@
</script>
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Databases</Heading>
<div class="u-flex u-gap-16">
<ViewSelector view={data.view} {columns} />
<Button on:click={() => (showCreate = true)} event="create_database">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create database</span>
</Button>
</div>
</div>
<GridHeader title="Databases" {columns} view={data.view}>
<Button on:click={() => (showCreate = true)} event="create_database">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create database</span>
</Button>
</GridHeader>
{#if data.databases.total}
{#if data.view === 'grid'}
@@ -44,10 +38,11 @@
<Table {data} />
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<Limit limit={data.limit} sum={data.databases.total} name="Databases" />
<Pagination limit={data.limit} offset={data.offset} sum={data.databases.total} />
</div>
<PaginationWithLimit
name="Databases"
limit={data.limit}
offset={data.offset}
total={data.databases.total} />
{:else}
<Empty
single
@@ -14,7 +14,7 @@
let name = '';
let id: string = null;
let showCustomId = false;
let showCustomId = true;
const create = async () => {
try {
@@ -58,7 +58,7 @@
</span></Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Database" bind:id />
<CustomId bind:show={showCustomId} name="Database" bind:id autofocus={false} />
{/if}
</FormList>
<svelte:fragment slot="footer">
@@ -1,7 +1,7 @@
<script lang="ts">
import { Empty, Heading, Limit, Pagination, ViewSelector } from '$lib/components';
import { Empty, PaginationWithLimit } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { Container, GridHeader } from '$lib/layout';
import { columns, showCreate } from './store';
import Table from './table.svelte';
import Grid from './grid.svelte';
@@ -11,17 +11,12 @@
</script>
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Collections</Heading>
<div class="u-flex u-gap-16">
<ViewSelector view={data.view} {columns} />
<Button on:click={() => ($showCreate = true)} event="create_collection">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create collection</span>
</Button>
</div>
</div>
<GridHeader title="Collections" {columns} view={data.view}>
<Button on:click={() => ($showCreate = true)} event="create_collection">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create collection</span>
</Button>
</GridHeader>
{#if data.collections.total}
{#if data.view === 'grid'}
@@ -30,10 +25,11 @@
<Table {data} />
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<Limit limit={data.limit} sum={data.collections.total} name="Collections" />
<Pagination limit={data.limit} offset={data.offset} sum={data.collections.total} />
</div>
<PaginationWithLimit
name="Collections"
limit={data.limit}
offset={data.offset}
total={data.collections.total} />
{:else}
<Empty
single
@@ -1,8 +1,8 @@
import { Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { getLimit, getPage, getView, pageToOffset, View } from '$lib/helpers/load';
import { CARD_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
import { CARD_LIMIT } from '$lib/constants';
export const load: PageLoad = async ({ params, url, route }) => {
const page = getPage(url);
@@ -0,0 +1,15 @@
import { writable } from 'svelte/store';
export type Column = {
id: string;
title: string;
show: boolean;
width?: number;
};
export const columns = writable<Column[]>([
{ id: '$id', title: 'Database ID', show: true, width: 150 },
{ id: 'name', title: 'Name', show: true, width: 120 },
{ id: '$createdAt', title: 'Created', show: true, width: 120 },
{ id: '$updatedAt', title: 'Updated', show: true, width: 120 }
]);
@@ -1,6 +1,6 @@
<script lang="ts">
import { Empty, Heading, Pagination, Limit, ViewSelector } from '$lib/components';
import { Container } from '$lib/layout';
import { Empty, Heading, PaginationWithLimit } from '$lib/components';
import { Container, GridHeader } from '$lib/layout';
import { Button } from '$lib/elements/forms';
import { wizard } from '$lib/stores/wizard';
import Create from './createDocument.svelte';
@@ -10,10 +10,13 @@
import Table from './table.svelte';
import { preferences } from '$lib/stores/preferences';
import { page } from '$app/stores';
import CreateAttributeDropdown from './attributes/createAttributeDropdown.svelte';
export let data: PageData;
let showCreateAttribute = false;
let showCreateDropdown = false;
let selectedAttribute: string = null;
$: selected = preferences.getCustomCollectionColumns($page.params.collection);
$: columns.set(
@@ -28,31 +31,38 @@
function openWizard() {
wizard.start(Create);
}
$: hasAttributes = !!$collection.attributes.length;
$: hasValidAttributes = $collection?.attributes?.some((attr) => attr.status === 'available');
</script>
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Documents</Heading>
<div class="u-flex u-gap-16">
<ViewSelector view={data.view} {columns} hideView isCustomCollection />
<Button
disabled={!$collection?.attributes?.length}
on:click={openWizard}
event="create_document">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create document</span>
</Button>
</div>
</div>
<GridHeader
title="Documents"
{columns}
view={data.view}
hideView
isCustomCollection
allowNoColumns>
<Button
disabled={!(hasAttributes && hasValidAttributes)}
on:click={openWizard}
event="create_document">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create document</span>
</Button>
</GridHeader>
{#if $collection?.attributes?.length}
{#if hasAttributes && hasValidAttributes}
{#if data.documents.total}
<Table {data} />
<div class="u-flex common-section u-main-space-between">
<Limit limit={data.limit} sum={data.documents.total} name="Documents" />
<Pagination limit={data.limit} offset={data.offset} sum={data.documents.total} />
</div>
<PaginationWithLimit
name="Documents"
limit={data.limit}
offset={data.offset}
total={data.documents.total} />
{:else}
<Empty
single
@@ -61,12 +71,36 @@
on:click={openWizard} />
{/if}
{:else}
<Empty
single
href="https://appwrite.io/docs/databases#attributes"
target="attribute"
on:click={() => (showCreateAttribute = true)} />
<Empty single target="attribute" on:click={() => (showCreateDropdown = true)}>
<div class="u-text-center">
<Heading size="7" tag="h2">Create your first attribute to get started.</Heading>
<p class="body-text-2 u-bold u-margin-block-start-4">
Need a hand? Check out our documentation.
</p>
</div>
<div class="u-flex u-gap-16 u-main-center">
<Button
external
href="https://appwrite.io/docs/databases#attributes"
text
event="empty_documentation"
ariaLabel={`create {target}`}>Documentation</Button>
<CreateAttributeDropdown
bind:showCreateDropdown
bind:showCreate={showCreateAttribute}
bind:selectedOption={selectedAttribute}>
<Button
secondary
event="create_attribute"
on:click={() => {
showCreateDropdown = !showCreateDropdown;
}}>
Create attribute
</Button>
</CreateAttributeDropdown>
</div>
</Empty>
{/if}
</Container>
<CreateAttribute bind:showCreate={showCreateAttribute} />
<CreateAttribute bind:showCreate={showCreateAttribute} selectedOption={selectedAttribute} />
@@ -1,39 +1,54 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { DropList, DropListItem, Empty, Heading } from '$lib/components';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
Table,
TableHeader,
TableBody,
TableRow,
TableCell,
TableCellHead,
TableCellText,
TableCell
TableHeader,
TableRow
} from '$lib/elements/table';
import { Button } from '$lib/elements/forms';
import { DropList, DropListItem, Empty, Heading } from '$lib/components';
import { attributes, type Attributes } from '../store';
import { Container } from '$lib/layout';
import { Pill } from '$lib/elements';
import Create from '../createAttribute.svelte';
import { isRelationship } from '../document-[document]/attributes/store';
import CreateIndex from '../indexes/createIndex.svelte';
import { attributes, type Attributes } from '../store';
import CreateAttributeDropdown from './createAttributeDropdown.svelte';
import Delete from './deleteAttribute.svelte';
import Overview from './overview.svelte';
import Edit from './edit.svelte';
import { options } from './store';
const projectId = $page.params.project;
const databaseId = $page.params.database;
let showCreateDropdown = false;
let showEmptyCreateDropdown = false;
let showDropdown = [];
let selectedOption: string = null;
let selectedAttribute: Attributes = null;
let showCreate = false;
let showDelete = false;
let showOverview = false;
let showEdit = false;
let showCreateIndex = false;
enum attributeFormatIcon {
ip = 'location-marker',
url = 'link',
email = 'mail',
enum = 'list'
}
</script>
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Attributes</Heading>
<Button on:click={() => (showCreate = true)} event="create_attribute">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create attribute</span>
</Button>
<CreateAttributeDropdown bind:showCreateDropdown bind:selectedOption bind:showCreate />
</div>
{#if $attributes.length}
@@ -46,10 +61,31 @@
</TableHeader>
<TableBody>
{#each $attributes as attribute, index}
{@const option = options.find((option) => option.type === attribute.type)}
<TableRow>
<TableCell title="Key">
<div class="u-flex u-main-space-between">
<span class="text u-trim">{attribute.key}</span>
<div class="u-flex u-main-space-between u-cross-center">
<div class="u-flex u-cross-center u-gap-16">
<div class="avatar is-size-small u-color-text-gray">
{#if isRelationship(attribute)}
<span
class={`icon-${
attribute?.twoWay
? 'switch-horizontal'
: 'arrow-sm-right'
}`}
aria-hidden="true" />
{:else if 'format' in attribute && attribute.format}
{@const icon = attributeFormatIcon[attribute?.format]}
<span class={`icon-${icon}`} aria-hidden="true" />
{:else}
<span
class={`icon-${option.icon}`}
aria-hidden="true" />
{/if}
</div>
<span class="text u-trim" data-private>{attribute.key}</span>
</div>
{#if attribute.status !== 'available'}
<Pill
warning={attribute.status === 'processing'}
@@ -64,10 +100,26 @@
</div>
</TableCell>
<TableCellText onlyDesktop title="Type">
{`${attribute.type}${attribute.array ? '[]' : ''}`}
{#if 'format' in attribute && attribute.format}
<span class="u-capitalize">{attribute.format}</span>
{:else}
<span class="u-capitalize">{attribute.type}</span>
{#if isRelationship(attribute)}
<span>
with <a
href={`${base}/console/project-${projectId}/databases/database-${databaseId}/collection-${attribute?.relatedCollection}`}
><b data-private>{attribute?.key}</b></a>
</span>
{/if}
{/if}
<span>
{attribute.array ? '[]' : ''}
</span>
</TableCellText>
<TableCellText onlyDesktop title="Default Value">
{attribute.default !== null ? attribute.default : '-'}
{attribute?.default !== null && attribute?.default !== undefined
? attribute?.default
: '-'}
</TableCellText>
<TableCell showOverflow>
<DropList
@@ -84,25 +136,35 @@
</button>
<svelte:fragment slot="list">
<DropListItem
icon="eye"
event="edit_attribute"
icon="pencil"
on:click={() => {
selectedAttribute = attribute;
showOverview = true;
showEdit = true;
showDropdown[index] = false;
}}>Overview</DropListItem>
<DropListItem
icon="plus"
on:click={() => {
selectedAttribute = attribute;
showCreateIndex = true;
showDropdown[index] = false;
}}>Create Index</DropListItem>
}}>
Edit
</DropListItem>
{#if !isRelationship(attribute)}
<DropListItem
icon="plus"
on:click={() => {
selectedAttribute = attribute;
showCreateIndex = true;
showDropdown[index] = false;
}}>
Create Index
</DropListItem>
{/if}
<DropListItem
icon="trash"
on:click={() => {
selectedAttribute = attribute;
showDelete = true;
}}>Delete</DropListItem>
showDropdown[index] = false;
}}>
Delete
</DropListItem>
</svelte:fragment>
</DropList>
</TableCell>
@@ -114,15 +176,39 @@
<p class="text">Total results: {$attributes.length}</p>
</div>
{:else}
<Empty
single
href="https://appwrite.io/docs/databases#attributes"
target="attribute"
on:click={() => (showCreate = true)} />
<Empty single target="attribute" on:click={() => (showEmptyCreateDropdown = true)}>
<div class="u-text-center">
<Heading size="7" tag="h2">Create your first attribute to get started.</Heading>
<p class="body-text-2 u-bold u-margin-block-start-4">
Need a hand? Check out our documentation.
</p>
</div>
<div class="u-flex u-gap-16 u-main-center">
<Button
external
href="https://appwrite.io/docs/databases#attributes"
text
event="empty_documentation"
ariaLabel={`create {target}`}>Documentation</Button>
<CreateAttributeDropdown
bind:showCreateDropdown={showEmptyCreateDropdown}
bind:selectedOption
bind:showCreate>
<Button
secondary
event="create_attribute"
on:click={() => {
showEmptyCreateDropdown = !showEmptyCreateDropdown;
}}>
Create attribute
</Button>
</CreateAttributeDropdown>
</div>
</Empty>
{/if}
</Container>
<Create bind:showCreate />
<Create bind:showCreate bind:selectedOption />
<Delete bind:showDelete {selectedAttribute} />
<Overview bind:showOverview {selectedAttribute} />
<Edit bind:showEdit {selectedAttribute} />
<CreateIndex bind:showCreateIndex externalAttribute={selectedAttribute} />
@@ -0,0 +1,3 @@
<svg width="55" height="9" viewBox="0 0 55 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M54.8536 4.85355C55.0488 4.65829 55.0488 4.34171 54.8536 4.14645L51.6716 0.964466C51.4763 0.769204 51.1597 0.769204 50.9645 0.964466C50.7692 1.15973 50.7692 1.47631 50.9645 1.67157L53.7929 4.5L50.9645 7.32843C50.7692 7.52369 50.7692 7.84027 50.9645 8.03553C51.1597 8.2308 51.4763 8.2308 51.6716 8.03553L54.8536 4.85355ZM0.5 5H54.5V4H0.5V5Z" fill="#868EA3"/>
</svg>

After

Width:  |  Height:  |  Size: 468 B

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