Merge branch 'main' of github.com:appwrite/console into poc-admin-panel

This commit is contained in:
Arman
2023-05-18 13:35:24 +02:00
453 changed files with 9510 additions and 4676 deletions
+1 -2
View File
@@ -1,5 +1,4 @@
VITE_APPWRITE_ENDPOINT=
VITE_APPWRITE_ENDPOINT=http://localhost/v1
VITE_APPWRITE_GROWTH_ENDPOINT=
VITE_GA_PROJECT=
VITE_SENTRY_DSN=
VITE_CONSOLE_MODE=self-hosted
+4 -8
View File
@@ -15,11 +15,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: 16
- name: Audit dependencies
run: npm audit --audit-level low
node-version: 18
# - name: Audit dependencies
# run: npm audit --audit-level low
- name: Install dependencies
run: npm ci
- name: Build Console
@@ -30,7 +30,3 @@ jobs:
run: npm run lint
- name: Unit Tests
run: npm test
# - name: E2E Tests
# run: |
# npx playwright install
# npm run e2e
+47 -20
View File
@@ -10,30 +10,57 @@ If you are worried about or don’t know where to start, check out the next sect
```
├── src
│ ├── lib // All non-route components, accessible over "import ... from '$lib'"
│ │ ├── components // Re-usable components
│ │ ├── elements // Re-usable elements
│ │ ├── layout // Global components for the layout (Nav/Content/Container)
│ │ └── stores // Global stores (state management)
│ └─── routes
│ ├── console // Routes that need authentication
│ │ ├──[project]
│ │ │ ├── database // Database Service
│ │ │ │ ├── [collection] // Nested Route for the collection "/console/[PROJECT_ID]/database/[COLLECTION_ID]"
│ │ │ │ ├── _create.svelte // Component to Create collections
│ │ │ │ └── index.svelte // Entrypoint for "/console/[PROJECT_ID]/database"
│ │ │ ├── storage // Storage Service "/console/[PROJECT]/storage"
│ │ │ └── auth // Users Service "/console/[PROJECT]/auth"
│ │ └──...
│ ├── login.svelte // Component for Login "/console/login"
│ └── register.svelte // Component for Register "/console/register"
├── build // Compiled application
└── static // Static assets
│ ├── lib // Reusable logic (accessible with '$lib')
│ │ ├── actions // Svelte actions
│ │ ├── charts // Chart components
│ │ ├── components // Re-usable components
│ │ ├── elements // Re-usable elements
│ │ ├── helpers // Small functions used through out the console
│ │ ├── images // Images used in the console
│ │ ├── layout // Global components for the layout (Nav/Content/Container)
│ │ ├── mock // Mock components used for testing
│ │ └── stores // Global stores (state management)
│ └── routes
│ └── console // Routes that need authentication
│ │ └── project-[project]
│ │ │ └── database // Database Service
│ │ │ │ ├── +layout.svelte // Layout head and other logic like realtime events is set here
│ │ │ │ ├── +layout.ts // Layout data is set here (Header, Breadcrumbs, ...)
│ │ │ │ ├── +page.svelte // Page displayed on "/console/project-[PROJECT_ID]/database"
│ │ │ │ ├── +page.ts // Necessary data for the page is fetched here
│ │ │ │ └── create.svelte // Component to create databases
│ │ │ └── ... // Other services
│ │ └── ...
│ └── ... // Routes that don't need authentication
├── build // Compiled application
└── static // Static assets
```
## Development
Once you've created a project and installed dependencies with `npm install`, create a `.env` file using `.env.example` as a template.
### 1. Clone the repository with git
```bash
git clone https://github.com/appwrite/console.git appwrite-console
```
### 2. Install dependencies with npm
Navigate to the Appwrite Console repository and install dependencies.
```bash
cd appwrite-console && npm install
```
### 3. Install and run Appwrite locally
When you run the Appwrite Console locally, it needs to point to a backend as well. The easiest way to do this is to run an Appwrite instance locally.
Follow the [install instructions](https://appwrite.io/docs/installation) in the Appwrite docs.
### 4. Setup environment variables
Add a `.env` file by copying the `.env.example` file as a template in the project's root directory.
Finally, start a development server:
+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();
+1017 -961
View File
File diff suppressed because it is too large Load Diff
+27 -25
View File
@@ -1,12 +1,11 @@
{
"name": "@appwrite/console",
"version": "0.0.1",
"engines": {
"node": ">=16"
},
"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",
@@ -20,47 +19,50 @@
},
"dependencies": {
"@analytics/google-analytics": "^1.0.5",
"@appwrite.io/pink": "^0.0.4",
"@aw-labs/appwrite-console": "^13.1.0",
"@appwrite.io/console": "0.1.0",
"@appwrite.io/pink": "^0.0.6-rc.10",
"@popperjs/core": "^2.11.6",
"@sentry/svelte": "^7.36.0",
"@sentry/tracing": "^7.36.0",
"@sentry/svelte": "^7.44.2",
"@sentry/tracing": "^7.44.2",
"analytics": "^0.8.1",
"dotenv": "^16.0.3",
"echarts": "^5.4.1",
"logrocket": "^3.0.1",
"pretty-bytes": "^6.1.0",
"prismjs": "^1.29.0",
"tippy.js": "^6.3.7",
"web-vitals": "^3.1.1"
},
"devDependencies": {
"@playwright/test": "^1.30.0",
"@sveltejs/adapter-static": "1.0.6",
"@sveltejs/kit": "1.3.10",
"@sveltejs/vite-plugin-svelte": "^2.0.2",
"@testing-library/dom": "^8.20.0",
"@playwright/test": "^1.31.2",
"@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",
"@testing-library/user-event": "^14.4.3",
"@types/prismjs": "^1.26.0",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"@vitest/ui": "^0.28.4",
"eslint": "^8.33.0",
"eslint-config-prettier": "^8.6.0",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"@vitest/ui": "^0.29.7",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-svelte3": "^4.0.0",
"jsdom": "^21.1.0",
"jsdom": "^21.1.1",
"kleur": "^4.1.5",
"pre-commit": "^1.2.2",
"prettier": "^2.8.3",
"prettier": "^2.8.6",
"prettier-plugin-svelte": "^2.9.0",
"sass": "^1.58.0",
"svelte": "^3.55.1",
"svelte-check": "^3.0.3",
"sass": "^1.59.3",
"svelte": "^3.57.0",
"svelte-check": "^3.1.4",
"svelte-jester": "^2.3.2",
"svelte-preprocess": "^5.0.1",
"svelte-preprocess": "^5.0.3",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vite": "^4.1.1",
"vitest": "^0.28.4"
"typescript": "^5.0.2",
"vite": "^4.2.1",
"vitest": "^0.29.7"
},
"type": "module",
"pre-commit": [
-1
View File
@@ -1,5 +1,4 @@
/// <reference types="@sveltejs/kit" />
interface Window {
VERCEL_ANALYTICS_ID: string | false;
SENTRY_DSN: string | false;
}
+10 -6
View File
@@ -1,16 +1,16 @@
import Analytics from 'analytics';
import googleAnalytics from '@analytics/google-analytics';
import { get } from 'svelte/store';
import { page } from '$app/stores';
import { user } from '$lib/stores/user';
import { AppwriteException } from '@aw-labs/appwrite-console';
import { ENV, MODE, VARS } from '$lib/system';
import googleAnalytics from '@analytics/google-analytics';
import { AppwriteException } from '@appwrite.io/console';
import Analytics from 'analytics';
import { get } from 'svelte/store';
const analytics = Analytics({
app: 'appwrite',
plugins: [
googleAnalytics({
measurementIds: [VARS.GOOGLE_ANALYTICS ?? 'G-R4YJ9JN8L4']
measurementIds: [VARS.GOOGLE_ANALYTICS || 'G-R4YJ9JN8L4']
})
]
});
@@ -88,7 +88,7 @@ function sendEventToGrowth(event: string, path: string, data: object = null): vo
});
}
function isTrackingAllowed() {
export function isTrackingAllowed() {
if (ENV.TEST) {
return;
}
@@ -141,6 +141,8 @@ export enum Submit {
TeamUpdateName = 'submit_team_update_name',
AuthLimitUpdate = 'submit_auth_limit_update',
AuthStatusUpdate = 'submit_auth_status_update',
AuthPasswordHistoryUpdate = 'submit_auth_password_history_limit_update',
AuthPasswordDictionaryUpdate = 'submit_auth_password_dictionary_update',
SessionsLengthUpdate = 'submit_sessions_length_update',
SessionsLimitUpdate = 'submit_sessions_limit_update',
SessionDelete = 'submit_session_delete',
@@ -149,6 +151,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',
@@ -162,6 +165,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 -1
View File
@@ -32,7 +32,7 @@
class:icon-exclamation={type === 'warning'}
class:icon-exclamation-circle={type === 'error'}
aria-hidden="true" />
<div class="alert-content">
<div class="alert-content" data-private>
{#if $$slots.title}
<h6 class="alert-title">
<slot name="title" />
+2 -2
View File
@@ -1,11 +1,11 @@
<script lang="ts">
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import Avatar from './avatar.svelte';
export let name: string;
export let size: number;
$: src = sdkForConsole.avatars.getInitials(name, size * 2, size * 2).toString();
$: src = sdk.forConsole.avatars.getInitials(name, size * 2, size * 2).toString();
</script>
<Avatar {name} {size} {src} />
+21 -5
View File
@@ -1,16 +1,31 @@
<script lang="ts">
import { clickOnEnter } from '$lib/helpers/a11y';
type BaseProps = {
isTile?: boolean;
isDashed?: boolean;
danger?: boolean;
};
type ButtonProps = {
isButton: true;
href?: never;
};
type AnchorProps = {
href: string;
isButton?: never;
};
type $$Props = BaseProps & (ButtonProps | AnchorProps | BaseProps);
export let isTile = false;
export let isDashed = false;
export let isButton = false;
export let danger = false;
export let href: string = null;
//allows to add the href attribute without throwing an error
let attributes = { href } as Record<string, string>;
function getElement(): string {
function getElement() {
switch (true) {
case !!href:
return 'a';
@@ -28,8 +43,9 @@
class:common-section={!isTile}
class:is-border-dashed={isDashed}
class:is-danger={danger}
class:is-allowed-focus={href}
on:click
on:keyup={clickOnEnter}
{...attributes}>
{href}>
<slot />
</svelte:element>
+7 -3
View File
@@ -1,16 +1,20 @@
<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
class="grid-box common-section u-margin-block-start-32"
style={`--grid-gap:1.5rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}>
style={`--grid-gap:1.5rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}
data-private>
<slot />
{#if total > 3 ? total < limit + offset : total % 2 !== 0}
+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}
+2
View File
@@ -33,6 +33,8 @@
</script>
<span
data-private
style:cursor="pointer"
on:click|preventDefault={handleClick}
on:keyup={clickOnEnter}
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
+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 -4
View File
@@ -4,13 +4,10 @@
export let href: string;
export let icon: string = null;
export let disabled = false;
//allows to add the disabled attribute to <a> tag without throwing an error
let attributes = { disabled } as Record<string, boolean>;
</script>
<li class="drop-list-item" on:click on:keyup={clickOnEnter}>
<a {href} class="drop-button" {...attributes}>
<a {href} class="drop-button" class:is-disabled={disabled}>
<span class="text"><slot /></span>
{#if icon}
<span class={`icon-${icon}`} aria-hidden="true" />
+2 -2
View File
@@ -41,8 +41,8 @@
<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">
Need a hand? Check out our documentation.
<p class="body-text-2 u-bold u-margin-block-start-4">
Need a hand? Learn more in our documentation.
</p>
</div>
<div class="u-flex u-gap-16 u-main-center">
+1 -1
View File
@@ -245,7 +245,7 @@
}
</script>
<Modal bind:show on:submit={create} size="big">
<Modal bind:show onSubmit={create} size="big">
<svelte:fragment slot="header">Create Event</svelte:fragment>
<slot />
<div>
+8 -21
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>
@@ -51,31 +49,20 @@
Your feedback is important to us. Please be honest and tell us what you think.
</div>
<Form on:submit={handleSubmit}>
<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>
+13 -19
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>
@@ -49,7 +53,7 @@
think.
</div>
<Form on:submit={handleSubmit}>
<Form onSubmit={handleSubmit}>
<Evaluation bind:value>
How likely are you to recommend Appwrite to a friend or colleague?
</Evaluation>
@@ -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;
}
}
}
requestAnimationFrame(checkOverflow);
window.addEventListener('resize', checkOverflow);
return {
update() {
originalText = node.textContent;
checkOverflow();
},
destroy() {
window.removeEventListener('resize', checkOverflow);
}
};
}
</script>
<Copy {value} {event}>
<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">
<button class="interactive-text-output-button is-hidden" aria-label="copy text">
<span class="icon-duplicate" aria-hidden="true" />
</button>
</div>
</div>
</Copy>
+8
View File
@@ -1,6 +1,7 @@
export { default as Modal } from './modal.svelte';
export { default as Pagination } from './pagination.svelte';
export { default as PaginationInline } from './paginationInline.svelte';
export { default as Paginator } from './paginator.svelte';
export { default as Card } from './card.svelte';
export { default as CardGrid } from './cardGrid.svelte';
export { default as CardContainer } from './cardContainer.svelte';
@@ -46,3 +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 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>
+44
View File
@@ -0,0 +1,44 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page as pageStore } from '$app/stores';
import { InputSelect } from '$lib/elements/forms';
import { preferences } from '$lib/stores/preferences';
export let sum: number;
export let limit: number;
export let name: string;
const options = [
{ label: '6', value: 6 },
{ label: '12', value: 12 },
{ label: '24', value: 24 },
{ label: '48', value: 48 },
{ label: '96', value: 96 }
];
async function limitChange() {
const url = new URL($pageStore.url);
const previousLimit = Number(url.searchParams.get('limit'));
url.searchParams.set('limit', limit.toString());
preferences.setLimit(limit);
if (url.searchParams.has('page')) {
const page = Number(url.searchParams.get('page'));
const newPage = Math.floor(((page - 1) * previousLimit) / limit);
url.searchParams.set('page', newPage.toString());
}
await goto(url.toString());
}
</script>
<div class="u-flex u-gap-12 u-cross-center">
<InputSelect
id="rows"
label="Rows per page"
showLabel={false}
{options}
bind:value={limit}
on:change={limitChange} />
<p class="text">{name} per page. Total results: {sum}</p>
</div>
+43 -27
View File
@@ -2,12 +2,18 @@
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { Alert } from '$lib/components';
import { trackEvent } from '$lib/actions/analytics';
import { Form } from '$lib/elements/forms';
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;
};
let dialog: HTMLDialogElement;
let alert: HTMLElement;
@@ -75,34 +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}
<!-- svelte-ignore a11y-no-redundant-roles -->
<form class="modal-form" role="form" on:submit|preventDefault>
<Form isModal {onSubmit}>
<header class="modal-header">
{#if warning}
<div class="avatar is-color-orange 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}
@@ -127,6 +143,6 @@
</div>
</div>
{/if}
</form>
</Form>
{/if}
</dialog>
+12 -7
View File
@@ -2,16 +2,21 @@
import Copy from './copy.svelte';
export let value: string;
export let hideCopyIcon = false;
</script>
<Copy {value}>
<div class="interactive-text-output is-textarea">
<div
class="interactive-text-output is-textarea"
style:min-inline-size="0"
style:display="inline-block">
<span class="text u-line-height-1-5 u-break-all"><slot /></span>
<div class="u-flex u-cross-child-start u-gap-8">
<button class="interactive-text-output-button" aria-label="copy text">
<span class="icon-duplicate" aria-hidden="true" />
</button>
</div>
{#if !hideCopyIcon}
<div class="u-flex u-cross-child-start u-gap-8">
<button class="interactive-text-output-button" aria-label="copy text">
<span class="icon-duplicate" aria-hidden="true" />
</button>
</div>
{/if}
</div>
</Copy>
+68 -49
View File
@@ -1,10 +1,10 @@
<script lang="ts">
import { page } from '$app/stores';
import { page as pageStore } from '$app/stores';
import { Button } from '$lib/elements/forms';
export let sum: number;
export let limit: number;
export let offset: number;
export let path: string;
export let hidePages = false;
$: totalPages = Math.ceil(sum / limit);
@@ -27,68 +27,87 @@
...(end < total - 1 ? ['...', total] : end < total ? [total] : [])
];
}
function getLink(page: number): string {
const url = new URL($pageStore.url);
if (page === 1) {
url.searchParams.delete('page');
} else {
url.searchParams.set('page', page.toString());
}
return url.toString();
}
</script>
{#if totalPages > 1}
{@const search = $page.url.search}
<nav class="pagination">
<a
class:is-disabled={currentPage <= 1}
class="button is-text"
aria-label="prev page"
href={`${path}/${currentPage - 1}${search}`}>
<span class="icon-cheveron-left" aria-hidden="true" />
<span class="text">Prev</span>
</a>
{#if !hidePages}
<ol class="pagination-list is-only-desktop">
{#each pages as page}
{#if typeof page === 'number'}
<li class="pagination-item">
<a
href={`${path}/${page}${search}`}
class="button"
class:is-disabled={currentPage === page}
class:is-text={currentPage !== page}
aria-label="page">
<span class="text">{page}</span>
</a>
</li>
{:else}
<li class="li is-text">
<span class="icon">...</span>
</li>
{/if}
{/each}
</ol>
{/if}
<a
class:is-disabled={currentPage === totalPages}
class="button is-text"
href={`${path}/${currentPage + 1}${search}`}
aria-label="next page">
<span class="text">Next</span>
<span class="icon-cheveron-right" aria-hidden="true" />
</a>
</nav>
{#key $pageStore.url}
<nav class="pagination">
{#if currentPage <= 1}
<Button disabled text ariaLabel="prev page">
<span class="icon-cheveron-left" aria-hidden="true" />
<span class="text">Prev</span>
</Button>
{:else}
<Button text ariaLabel="prev page" href={getLink(currentPage - 1)}>
<span class="icon-cheveron-left" aria-hidden="true" />
<span class="text">Prev</span>
</Button>
{/if}
{#if !hidePages}
<ol class="pagination-list is-only-desktop">
{#each pages as page}
{#if typeof page === 'number'}
<li class="pagination-item">
{#if currentPage === page}
<Button ariaLabel="page" disabled>
<span class="text">{page}</span>
</Button>
{:else}
<Button text ariaLabel="page" href={getLink(page)}>
<span class="text">{page}</span>
</Button>
{/if}
</li>
{:else}
<li class="li is-text">
<span class="icon">...</span>
</li>
{/if}
{/each}
</ol>
{/if}
{#if currentPage >= totalPages}
<Button disabled text ariaLabel="next page">
<span class="text">Next</span>
<span class="icon-cheveron-right" aria-hidden="true" />
</Button>
{:else}
<Button text ariaLabel="next page" href={getLink(currentPage + 1)}>
<span class="text">Next</span>
<span class="icon-cheveron-right" aria-hidden="true" />
</Button>
{/if}
</nav>
{/key}
{:else}
<nav class="pagination">
<button type="button" class="button is-text is-disabled" aria-label="prev page">
<Button text disabled ariaLabel="prev page">
<span class="icon-cheveron-left" aria-hidden="true" />
<span class="text">Prev</span>
</button>
</Button>
{#if !hidePages}
<ol class="pagination-list is-only-desktop">
<li class="pagination-item">
<button type="button" class="button is-disabled" aria-label="page">
<Button disabled ariaLabel="page">
<span class="text">1</span>
</button>
</Button>
</li>
</ol>
{/if}
<button type="button" class="button is-text is-disabled" aria-label="next page">
<Button text disabled ariaLabel="next page">
<span class="text">Next</span>
<span class="icon-cheveron-right" aria-hidden="true" />
</button>
</Button>
</nav>
{/if}
@@ -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>
+18
View File
@@ -0,0 +1,18 @@
<script lang="ts">
import PaginationInline from './paginationInline.svelte';
export let items = [];
export let limit = 5;
let offset = 0;
$: total = items?.length;
$: paginatedItems = items.slice(offset, offset + limit);
</script>
<slot {paginatedItems} />
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {total}</p>
<PaginationInline {limit} bind:offset sum={total} hidePages />
</div>
+2 -2
View File
@@ -25,7 +25,7 @@
$: disabled = !value || $groups.has(value);
</script>
<Modal bind:show on:close={reset} on:submit={create}>
<Modal bind:show on:close={reset} onSubmit={create}>
<svelte:fragment slot="header">Custom permission</svelte:fragment>
<p class="text">
Custom permissions allow you to grant access to specific users or teams using their ID and
@@ -44,6 +44,6 @@
</FormList>
<svelte:fragment slot="footer">
<Button submit {disabled}>Create</Button>
<Button submit {disabled}>Add</Button>
</svelte:fragment>
</Modal>
+1 -1
View File
@@ -104,7 +104,7 @@
<Row {role} />
</TableCell>
<TableCellText title="Remove">
<div class="u-flex">
<div class="u-flex u-main-end">
<button
class="button is-text is-only-icon"
type="button"
+9 -7
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import { sdkForProject } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { tick } from 'svelte';
import { AvatarInitials } from '../';
import Output from '../output.svelte';
@@ -14,15 +14,17 @@
async function getData(
permission: string
): Promise<Partial<Models.User<Record<string, unknown>> & Models.Team>> {
): Promise<
Partial<Models.User<Record<string, unknown>> & Models.Team<Record<string, unknown>>>
> {
const role = permission.split(':')[0];
const id = permission.split(':')[1].split('/')[0];
if (role === 'user') {
const user = await sdkForProject.users.get(id);
const user = await sdk.forProject.users.get(id);
return user;
}
if (role === 'team') {
const team = await sdkForProject.teams.get(id);
const team = await sdk.forProject.teams.get(id);
return team;
}
}
@@ -68,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}
+12 -9
View File
@@ -1,8 +1,8 @@
<script lang="ts">
import { Button, InputSearch } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
import { sdkForProject } from '$lib/stores/sdk';
import { Query, type Models } from '@aw-labs/appwrite-console';
import { sdk } from '$lib/stores/sdk';
import { Query, type Models } from '@appwrite.io/console';
import { AvatarInitials, EmptySearch, Modal, PaginationInline } from '..';
import type { Writable } from 'svelte/store';
import type { Permission } from './permissions.svelte';
@@ -14,7 +14,7 @@
let search = '';
let offset = 0;
let results: Models.TeamList;
let results: Models.TeamList<Record<string, unknown>>;
let selected: Set<string> = new Set();
let hasSelection = false;
@@ -31,7 +31,10 @@
async function request() {
if (!show) return;
results = await sdkForProject.teams.list([Query.limit(5), Query.offset(offset)], search);
results = await sdk.forProject.teams.list(
[Query.limit(5), Query.offset(offset)],
search || undefined
);
}
function onSelection(event: Event, role: string) {
@@ -58,7 +61,7 @@
}
</script>
<Modal bind:show on:submit={create} on:close={reset} size="big">
<Modal bind:show onSubmit={create} on:close={reset} size="big">
<svelte:fragment slot="header">Select teams</svelte:fragment>
<p class="text">
Grant access to any member of a specific team. To grant access to team members with specific
@@ -95,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>
@@ -113,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">
@@ -131,7 +134,7 @@
You have no teams. Create a team to see them here.
</p>
<p class="text u-line-height-1-5">
Need a hand? Check out our <a
Need a hand? Learn more in our <a
href="https://appwrite.io/docs/client/teams"
target="_blank"
rel="noopener noreferrer">
@@ -144,6 +147,6 @@
{/if}
<svelte:fragment slot="footer">
<Button submit disabled={!hasSelection}>Create</Button>
<Button submit disabled={!hasSelection}>Add</Button>
</svelte:fragment>
</Modal>
+15 -12
View File
@@ -2,8 +2,8 @@
import { Button, InputSearch } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
import { AvatarInitials, EmptySearch, Modal, PaginationInline } from '..';
import { sdkForProject } from '$lib/stores/sdk';
import { Query, type Models } from '@aw-labs/appwrite-console';
import { sdk } from '$lib/stores/sdk';
import { Query, type Models } from '@appwrite.io/console';
import type { Writable } from 'svelte/store';
import type { Permission } from './permissions.svelte';
@@ -31,7 +31,10 @@
async function request() {
if (!show) return;
results = await sdkForProject.users.list([Query.limit(5), Query.offset(offset)], search);
results = await sdk.forProject.users.list(
[Query.limit(5), Query.offset(offset)],
search || undefined
);
}
function onSelection(event: Event, role: string) {
@@ -58,7 +61,7 @@
}
</script>
<Modal bind:show on:submit={create} on:close={reset} size="big">
<Modal bind:show onSubmit={create} on:close={reset} size="big">
<svelte:fragment slot="header">Select users</svelte:fragment>
<p class="text">Grant access to any authenticated or anonymous user.</p>
<InputSearch
@@ -90,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>
@@ -132,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">
@@ -150,7 +153,7 @@
You have no users. Create a user to see them here.
</p>
<p class="text u-line-height-1-5">
Need a hand? Check out our <a
Need a hand? Learn more in our <a
href="https://appwrite.io/docs/server/users"
target="_blank"
rel="noopener noreferrer">
@@ -163,6 +166,6 @@
{/if}
<svelte:fragment slot="footer">
<Button submit disabled={!hasSelection}>Create</Button>
<Button submit disabled={!hasSelection}>Add</Button>
</svelte:fragment>
</Modal>
+13 -8
View File
@@ -29,23 +29,28 @@
}
});
$: valueChange(search);
$: valueChange(search ?? '');
function valueChange(value: string) {
clearTimeout(timer);
timer = setTimeout(async () => {
timer = setTimeout(() => {
const url = new URL($page.url);
if (url.search === value) {
const previous = url.searchParams.get('search') ?? '';
if (previous === value) {
return;
}
/**
* Reset to first page if search changes.
*/
if ($page.data.page > 1) {
url.pathname = url.pathname.replace(new RegExp(`/${$page.data.page}*$`), '/');
url.searchParams.delete('page');
}
if (value === '') {
url.searchParams.delete('search');
} else {
url.searchParams.set('search', value);
}
url.search = value;
trackEvent('search');
goto(url, { keepFocus: true });
}, debounce);
+1 -1
View File
@@ -8,7 +8,7 @@
export let copyEvent: string = null;
</script>
<div class="interactive-text-output" class:is-textarea={show}>
<div class="interactive-text-output" class:is-textarea={show} data-private>
{#if show}
<span class="text u-line-height-1-5 u-break-all">{value}</span>
{:else}
+1 -1
View File
@@ -1,3 +1,3 @@
<ul class="grid-box common-section" style="--grid-item-size:25rem;">
<ul class="grid-box common-section" style="--grid-item-size:25rem;" data-private>
<slot />
</ul>
+2 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { uploader } from '$lib/stores/uploader';
import { Pill } from '$lib/elements';
import { sdkForProject } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { Avatar } from '$lib/components';
import { base } from '$app/paths';
import { page } from '$app/stores';
@@ -9,7 +9,7 @@
let hovering = false;
const getPreview = (fileId: string, bucketId: string) =>
sdkForProject.storage.getFilePreview(bucketId, fileId, 32, 32).toString() + '&mode=admin';
sdk.forProject.storage.getFilePreview(bucketId, fileId, 32, 32).toString() + '&mode=admin';
</script>
{#if $uploader?.isOpen}
+141
View File
@@ -0,0 +1,141 @@
<script context="module" lang="ts">
export type Column = {
id: string;
title: string;
show: boolean;
width?: number;
};
</script>
<script lang="ts">
import { Button, InputChoice } from '$lib/elements/forms';
import { DropList } from '.';
import { page } from '$app/stores';
import type { Writable } from 'svelte/store';
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;
onMount(async () => {
if (isCustomCollection) {
const prefs = preferences.getCustomCollectionColumns($page.params.collection);
columns.set(
$columns.map((column) => {
column.show = prefs?.includes(column.id) ?? true;
return column;
})
);
} else {
const prefs = preferences.get($page.route);
columns.set(
$columns.map((column) => {
column.show = prefs.columns?.includes(column.id) ?? true;
return column;
})
);
}
return columns.subscribe((ctx) => {
const columns = ctx.filter((n) => n.show).map((n) => n.id);
if (isCustomCollection) {
preferences.setCustomCollectionColumns(columns);
} else {
preferences.setColumns(columns);
}
});
});
function getViewLink(view: View): string {
const url = new URL($page.url);
url.searchParams.set('view', view);
return url.toString();
}
function updateViewPreferences(view: View) {
preferences.setView(view);
}
$: selectedColumnsNumber = $columns.reduce((acc, column) => {
if (column.show) {
acc++;
}
return acc;
}, 0);
</script>
<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}
</div>
{#if !hideView}
<div class="grid-header-col-3 toggle-button">
<ul class="toggle-button-list">
{#key $page.url}
<li class="toggle-button-item">
<a
href={getViewLink(View.Table)}
on:click={() => updateViewPreferences(View.Table)}
class="toggle-button-element"
aria-label="List View"
type="button"
class:is-selected={view === View.Table}
use:tooltip={{
content: 'List View'
}}>
<span class="icon-view-list" aria-hidden="true" />
</a>
</li>
<li class="toggle-button-item">
<a
href={getViewLink(View.Grid)}
on:click={() => updateViewPreferences(View.Grid)}
class="toggle-button-element"
aria-label="Grid View"
type="button"
class:is-selected={view === View.Grid}
use:tooltip={{
content: 'Grid View'
}}>
<span class="icon-view-grid" aria-hidden="true" />
</a>
</li>
{/key}
</ul>
</div>
{/if}
+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',
+10 -4
View File
@@ -1,5 +1,8 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
import { getContext, hasContext } from 'svelte';
import { readable } from 'svelte/store';
import type { FormContext } from './form.svelte';
export let submit = false;
export let secondary = false;
@@ -14,8 +17,11 @@
export let noMargin = false;
export let event: string = null;
//allows to add the disabled attribute to <a> tag without throwing an error
let attributes = { disabled } as Record<string, boolean>;
const isSubmitting = hasContext('form')
? getContext<FormContext>('form').isSubmitting
: readable(false);
$: internalDisabled = (submit && $isSubmitting) || disabled;
function track() {
if (!event) {
@@ -31,11 +37,11 @@
{#if href}
<a
on:click={track}
{...attributes}
{href}
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}
class="button"
class:is-disabled={disabled}
class:is-only-icon={round}
class:is-secondary={secondary}
class:is-text={text}
@@ -49,7 +55,7 @@
<button
on:click
on:click={track}
{disabled}
disabled={internalDisabled}
class="button"
class:is-only-icon={round}
class:is-secondary={secondary}
+31 -5
View File
@@ -1,9 +1,35 @@
<script lang="ts">
export let noMargin = false;
export let noStyle = false;
<script context="module" lang="ts">
import type { Writable } from 'svelte/store';
export type FormContext = {
isSubmitting: Writable<boolean>;
};
</script>
<!-- svelte-ignore a11y-no-redundant-roles -->
<form role="form" class:form={!noStyle} class:common-section={!noMargin} on:submit|preventDefault>
<script lang="ts">
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
export let noMargin = false;
export let noStyle = false;
export let isModal = false;
export let onSubmit: () => Promise<void> | void;
const { isSubmitting } = setContext<FormContext>('form', {
isSubmitting: writable(false)
});
async function submit() {
isSubmitting.set(true);
await onSubmit();
isSubmitting.set(false);
}
</script>
<form
class:form={!noStyle}
class:common-section={!noMargin}
class:modal-form={isModal}
on:submit|preventDefault={submit}>
<slot />
</form>
+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';
+1 -1
View File
@@ -15,7 +15,7 @@
export let maxlength: number = null;
// 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,6}$`;
const pattern = String.raw`^(?!-)[A-Za-z0-9-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}$`;
let element: HTMLInputElement;
let error: string;
+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>
+3 -3
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}
@@ -77,7 +77,7 @@
</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">Drag and drop files here to upload</span>
<span class="is-only-desktop">Drag and drop a file here to upload</span>
<span class="is-not-desktop">Upload a File</span>
</h5>
{#if allowedFileExtensions?.length}
@@ -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}
+5 -8
View File
@@ -1,13 +1,14 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem } from '.';
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) {
@@ -42,20 +43,16 @@
bind:value
bind:this={element}
on:invalid={handleInvalid} />
<span class="text-counter">
<span class="text-counter-count">{value?.length ?? 0}</span>
<span class="text-counter-separator" />
<span class="text-counter-max">36</span>
</span>
<TextCounter count={value?.length ?? 0} max={36} />
</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={nullable && !required ? 1.75 : 0} />
<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
+14 -9
View File
@@ -27,17 +27,21 @@
error = element.validationMessage;
};
$: if (element && required && !value) {
element.setCustomValidity('This field is required');
const isNotEmpty = (value: string | number | boolean) => {
return typeof value === 'boolean' ? true : !!value;
};
$: if (required && !isNotEmpty(value)) {
element?.setCustomValidity('This field is required');
} else {
element?.setCustomValidity('');
}
$: if (element && required && value) {
element.setCustomValidity('');
}
$: if (value) {
$: if (isNotEmpty(value)) {
error = null;
}
$: hasNullOption = options.some((option) => option.value === null);
</script>
<FormItem>
@@ -52,8 +56,9 @@
{disabled}
bind:this={element}
bind:value
on:invalid={handleInvalid}>
{#if placeholder}
on:invalid={handleInvalid}
on:change>
{#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>
+32 -6
View File
@@ -1,6 +1,8 @@
<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;
export let optionalText: string | undefined = undefined;
@@ -9,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;
@@ -39,6 +42,20 @@
$: if (value) {
error = null;
}
let prevValue = '';
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
if (isNull) {
prevValue = value;
value = null;
} else {
value = prevValue;
}
}
$: showTextCounter = !!maxlength;
$: showNullCheckbox = nullable && !required;
</script>
<FormItem>
@@ -58,14 +75,23 @@
type="text"
class="input-text"
bind:value
class:u-padding-inline-end-56={typeof maxlength === 'number'}
bind:this={element}
on:invalid={handleInvalid} />
{#if maxlength}
<span class="text-counter">
<span class="text-counter-count">{value?.length ?? 0}</span>
<span class="text-counter-separator" />
<span class="text-counter-max">{maxlength}</span>
</span>
{#if showTextCounter || showNullCheckbox}
<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 showTextCounter}
<li class="buttons-list-item">
<TextCounter max={maxlength} count={value?.length ?? 0} />
</li>
{/if}
{#if showNullCheckbox}
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
{/if}
</ul>
{/if}
</div>
{#if error}
+41 -2
View File
@@ -1,6 +1,8 @@
<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;
export let showLabel = true;
@@ -8,10 +10,13 @@
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;
export let maxlength: number = null;
export let optionalText: string | undefined = undefined;
export let tooltip: string = null;
let element: HTMLTextAreaElement;
let error: string;
@@ -34,10 +39,24 @@
$: 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;
}
}
$: showTextCounter = !!maxlength;
$: showNullCheckbox = nullable && !required;
</script>
<FormItem>
<Label {required} hide={!showLabel} for={id}>
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
@@ -50,10 +69,30 @@
{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} />
on:invalid={handleInvalid}
style:--amount-of-buttons={required ? undefined : 0.25} />
{#if showTextCounter || showNullCheckbox}
<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 showTextCounter}
<li class="buttons-list-item">
<TextCounter max={maxlength} count={value?.length ?? 0} />
</li>
{/if}
{#if showNullCheckbox}
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
{/if}
</ul>
{/if}
</div>
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
+29 -8
View File
@@ -1,13 +1,17 @@
<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;
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,19 +42,24 @@
error = element.validationMessage;
};
const handleInput = () => {
if (value === '') {
value = null;
}
};
$: 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>
@@ -65,9 +74,21 @@
type="url"
autocomplete={autocomplete ? 'on' : 'off'}
bind:value
on:input={handleInput}
bind:this={element}
on:invalid={handleInvalid} />
<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>
+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>
+28
View File
@@ -0,0 +1,28 @@
<script lang="ts">
export let count: number;
export let max: number;
</script>
<span class="text-counter" class:active={count > 0}>
<span class="text-counter-count">{count}</span>
<span class="text-counter-separator" />
<span class="text-counter-max">{max}</span>
</span>
<style lang="scss">
.text-counter {
--p-text-counter-color: var(--color-neutral-50);
&.active {
--p-text-counter-color: var(--color-neutral-70);
}
}
:global(.theme-dark) .text-counter {
--p-text-counter-color: var(--color-neutral-70);
&.active {
--p-text-counter-color: var(--color-neutral-50);
}
}
</style>
+1
View File
@@ -1 +1,2 @@
export { default as Pill } from './pill.svelte';
export { default as SelectSearchItem } from './selectSearchItem.svelte';
-33
View File
@@ -9,42 +9,10 @@
export let submit = false;
export let external = false;
export let href: string = null;
export let trim = false;
//allows to add the disabled attribute to <a> tag without throwing an error
let attributes = { disabled } as Record<string, boolean>;
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}
<a
{...attributes}
{href}
target={external ? '_blank' : '_self'}
rel={external ? 'noopener noreferrer' : ''}
@@ -59,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>
+1 -1
View File
@@ -3,6 +3,6 @@
export let title: string;
</script>
<div class="table-col" data-title={title} role="cell">
<div class="table-col" data-title={title} role="cell" data-private>
<a role="button" tabindex="0" class="link" {href}><slot /></a>
</div>
+4 -1
View File
@@ -3,13 +3,16 @@
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}
role="cell">
role="cell"
data-private>
<Trim><slot /></Trim>
</div>
+2 -1
View File
@@ -7,6 +7,7 @@
class="table is-selected-columns-mobile"
class:u-margin-block-start-32={!noMargin}
class:is-remove-outer-styles={noStyles}
role="table">
role="table"
data-private>
<slot />
</div>
+1 -2
View File
@@ -1,5 +1,4 @@
<!-- TODO: is 100% height needed? -->
<div class="table-with-scroll">
<div class="table-with-scroll" data-private>
<div class="table-wrapper">
<div class="table is-remove-outer-styles">
<ul class="table-thead">
+23 -3
View File
@@ -1,10 +1,30 @@
<script lang="ts">
import type { Action } from 'svelte/action';
export let isSticky = false;
const hasOverflow: Action<HTMLDivElement, (value: boolean) => void> = (node, callback) => {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
callback(entry.contentRect.width < entry.target.scrollWidth);
}
});
observer.observe(node);
return {
destroy() {
observer.disconnect();
}
};
};
let isOverflowing = false;
</script>
<div class="table-with-scroll u-margin-block-start-32">
<div class="table-wrapper">
<table class="table" class:is-sticky-scroll={isSticky}>
<div class="table-with-scroll u-margin-block-start-32" data-private>
<div class="table-wrapper" use:hasOverflow={(v) => (isOverflowing = v)}>
<table class="table" class:is-sticky-scroll={isSticky && isOverflowing}>
<slot />
</table>
</div>
+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[]) {
+6
View File
@@ -11,6 +11,12 @@ async function securedCopy(value: string) {
function unsecuredCopy(value: string) {
const textArea = document.createElement('textarea');
textArea.value = value;
// Avoid scrolling to bottom
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.position = 'fixed';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
+30
View File
@@ -1,7 +1,37 @@
import { preferences } from '$lib/stores/preferences';
import type { Page } from '@sveltejs/kit';
export function pageToOffset(page: number, limit: number): number {
return page ? page * limit - limit : 0;
}
export function getPage(url: URL): number {
return Number(url.searchParams.get('page'));
}
export function getLimit(url: URL, route: Page['route'], fallback: number): number {
return Number(url.searchParams.get('limit') ?? preferences.get(route).limit ?? fallback);
}
export enum View {
Table = 'table',
Grid = 'grid'
}
export function getView(url: URL, route: Page['route'], fallback: View): View {
return (url.searchParams.get('view') ?? preferences.get(route).view) === View.Grid
? View.Grid
: View.Table ?? fallback;
}
export function getColumns(route: Page['route'], fallback: string[]): string[] {
return preferences.get(route).columns ?? fallback;
}
export function getSearch(url: URL): string | undefined {
return url.searchParams.get('search') ?? undefined;
}
type TabElement = { href: string; title: string; hasChildren?: boolean };
export function isTabSelected(
+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;
}
+3
View File
@@ -0,0 +1,3 @@
export function isMac(): boolean {
return window.navigator.platform.toUpperCase().indexOf('MAC') >= 0;
}
+5
View File
@@ -0,0 +1,5 @@
export function getProjectId() {
const pathname = window.location.pathname;
const projectMatch = pathname.match(/\/project-(.*?)\//);
return projectMatch?.[1] || null;
}
+3
View File
@@ -0,0 +1,3 @@
export async function sleep(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
+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();
});
}
+48
View File
@@ -0,0 +1,48 @@
import { cubicOut } from 'svelte/easing';
import type { EasingFunction, TransitionConfig } from 'svelte/transition';
export interface SlideParams {
delay?: number;
duration?: number;
easing?: EasingFunction;
axis?: 'x' | 'y';
}
export function slide(
node: Element,
{ delay = 0, duration = 400, easing = cubicOut, axis = 'y' }: SlideParams = {}
): TransitionConfig {
const style = getComputedStyle(node);
const opacity = +style.opacity;
const primary_property = axis === 'y' ? 'height' : 'width';
const primary_property_value = parseFloat(style[primary_property]);
const secondary_properties = axis === 'y' ? ['top', 'bottom'] : ['left', 'right'];
const capitalized_secondary_properties = secondary_properties.map(
(e) => `${e[0].toUpperCase()}${e.slice(1)}`
);
const padding_start_value = parseFloat(style[`padding${capitalized_secondary_properties[0]}`]);
const padding_end_value = parseFloat(style[`padding${capitalized_secondary_properties[1]}`]);
const margin_start_value = parseFloat(style[`margin${capitalized_secondary_properties[0]}`]);
const margin_end_value = parseFloat(style[`margin${capitalized_secondary_properties[1]}`]);
const border_width_start_value = parseFloat(
style[`border${capitalized_secondary_properties[0]}Width`]
);
const border_width_end_value = parseFloat(
style[`border${capitalized_secondary_properties[1]}Width`]
);
return {
delay,
duration,
easing,
css: (t) =>
'overflow: hidden;' +
`opacity: ${Math.min(t * 20, 1) * opacity};` +
`${primary_property}: ${t * primary_property_value}px;` +
`padding-${secondary_properties[0]}: ${t * padding_start_value}px;` +
`padding-${secondary_properties[1]}: ${t * padding_end_value}px;` +
`margin-${secondary_properties[0]}: ${t * margin_start_value}px;` +
`margin-${secondary_properties[1]}: ${t * margin_end_value}px;` +
`border-${secondary_properties[0]}-width: ${t * border_width_start_value}px;` +
`border-${secondary_properties[1]}-width: ${t * border_width_end_value}px;`
};
}
+13 -9
View File
@@ -1,5 +1,11 @@
<script lang="ts">
import { AvatarInitials, EmptySearch, Pagination, Trim } from '$lib/components';
import {
AvatarInitials,
EmptySearch,
Heading,
PaginationWithLimit,
Trim
} from '$lib/components';
import {
TableBody,
TableHeader,
@@ -11,15 +17,15 @@
} from '$lib/elements/table';
import { Container } from '$lib/layout';
import { toLocaleDateTime } from '$lib/helpers/date';
import { PAGE_LIMIT } from '$lib/constants';
import type { Models } from '@aw-labs/appwrite-console';
import type { Models } from '@appwrite.io/console';
export let logs: Models.LogList;
export let path: string;
export let offset = 0;
export let limit = 0;
</script>
<Container>
<Heading tag="h2" size="5">Activity</Heading>
{#if logs.total}
<TableScroll>
<TableHeader>
@@ -43,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>
@@ -65,10 +71,8 @@
{/each}
</TableBody>
</TableScroll>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {logs.total}</p>
<Pagination limit={PAGE_LIMIT} {path} {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">
+1 -1
View File
@@ -18,7 +18,7 @@
<nav class="breadcrumbs is-only-desktop" aria-label="breadcrumb">
<ol class="breadcrumbs-list">
{#each breadcrumbs as breadcrumb}
<li class="breadcrumbs-item">
<li class="breadcrumbs-item" data-private>
{#if breadcrumb.href}
<a href={breadcrumb.href} title={breadcrumb.title} on:click={track}>
{breadcrumb.title}
+10 -8
View File
@@ -5,12 +5,14 @@
</script>
<Heading size="4" tag="h1" trimmed={false}>
{#if href}
<a class="button is-text is-only-icon" {href} aria-label="page back">
<span class="icon-cheveron-left" aria-hidden="true" />
</a>
{/if}
<span class="text u-trim-1">
<slot />
</span>
<div class="u-flex u-cross-center">
{#if href}
<a class="button is-text is-only-icon" {href} aria-label="page back">
<span class="icon-cheveron-left" aria-hidden="true" />
</a>
{/if}
<span class="text u-trim-1" data-private>
<slot />
</span>
</div>
</Heading>
+54 -34
View File
@@ -1,47 +1,67 @@
<footer class="main-footer">
<div class="container">
<ul class="inline-links is-center">
<script>
import { isCloud } from '$lib/system';
import { version } from '$routes/console/store';
const currentYear = new Date().getFullYear();
</script>
<footer class="main-footer u-cross-center">
<div class="main-footer-start">
<ul class="inline-links is-no-padding-first-and-last u-x-small">
<li class="inline-links-item">
<a
class="link"
href="https://github.com/appwrite/appwrite"
target="_blank"
rel="noopener noreferrer">
<span class="icon-github-circled" aria-hidden="true" />
<span class="text">GitHub</span>
<div class="u-flex u-cross-center u-gap-8">
{#if isCloud}
<span class="icon-cloud" />
{/if}
{#if $version}
<a
class="text"
href="https://github.com/appwrite/appwrite/releases"
target="_blank"
rel="noreferrer">
Version {$version}
</a>
{/if}
</div>
</li>
<li class="inline-links-item">
<a href="https://appwrite.io/policy/terms" target="_blank" rel="noreferrer">
<span class="text">Terms</span>
</a>
</li>
<li class="inline-links-item">
<a
class="link"
href="https://appwrite.io/discord"
target="_blank"
rel="noopener noreferrer">
<span class="icon-discord" aria-hidden="true" />
<span class="text">Discord</span>
<a href="https://appwrite.io/policy/privacy" target="_blank" rel="noreferrer">
<span class="text">Privacy</span>
</a>
</li>
</ul>
</div>
<div class="main-footer-end">
<ul class="inline-links is-no-padding-first-and-last u-x-small">
<li class="inline-links-item">
<a
class="link"
href="https://github.com/appwrite/appwrite/issues/new"
target="_blank"
rel="noopener noreferrer">
<span class="text">Open an Issue</span>
</a>
<span class="text">ⓒ {currentYear} Appwrite. All rights reserved.</span>
</li>
<li class="inline-links-item">
<a
class="link"
href="https://appwrite.io/docs"
target="_blank"
rel="noopener noreferrer">
<span class="text">Docs</span>
<li class="inline-links-item u-flex u-gap-8">
<a href="https://github.com/appwrite/appwrite" target="_blank" rel="noreferrer">
<span class="icon-github" aria-hidden="true" aria-label="Appwrite on Github" />
</a>
<a href="https://appwrite.io/discord" target="_blank" rel="noreferrer">
<span
class="icon-discord"
aria-hidden="true"
aria-label="Appwrite on Discord" />
</a>
</li>
<li class="inline-links-item">
<span class="text">API:0.13.2</span>
</li>
</ul>
</div>
</footer>
<style lang="scss">
.main-footer {
margin-block-start: auto;
}
[class^='icon-']:not(.icon-cloud):not(:hover) {
color: hsl(var(--color-neutral-50));
}
</style>
+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>
+6 -7
View File
@@ -16,18 +16,17 @@
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';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { goto } from '$app/navigation';
let showDropdown = false;
let droplistElement: HTMLDivElement;
function toggleFeedback() {
showFeedback = !showFeedback;
feedback.toggleFeedback();
if ($feedback.notification) {
feedback.toggleNotification();
feedback.addVisualization();
@@ -35,7 +34,7 @@
}
async function logout() {
await sdkForConsole.account.deleteSession('current');
await sdk.forConsole.account.deleteSession('current');
trackEvent(Submit.AccountLogout);
await goto(`${base}/login`);
}
@@ -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';
+8 -8
View File
@@ -6,10 +6,10 @@
import { Button } from '$lib/elements/forms';
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/stores';
import { calculateTime } from '$lib/helpers/timeConversion';
import type { Models } from '@aw-labs/appwrite-console';
import type { Models } from '@appwrite.io/console';
let selectedTab: string;
let rawData: string;
@@ -17,7 +17,7 @@
function isDeployment(data: Models.Deployment | Models.Execution): data is Models.Deployment {
if ('buildId' in data) {
selectedTab = 'logs';
rawData = `${sdkForConsole.client.config.endpoint}/functions/${$log.func.$id}/deployment/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
rawData = `${sdk.forConsole.client.config.endpoint}/functions/${$log.func.$id}/deployment/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
return true;
}
}
@@ -25,7 +25,7 @@
function isExecution(data: Models.Deployment | Models.Execution): data is Models.Execution {
if ('trigger' in data) {
selectedTab = 'response';
rawData = `${sdkForConsole.client.config.endpoint}/functions/${$log.func.$id}/execution/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
rawData = `${sdk.forConsole.client.config.endpoint}/functions/${$log.func.$id}/execution/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
return true;
}
}
@@ -47,9 +47,9 @@
<svelte:window on:keydown={handleKeydown} />
{#if $log.data}
<section class="cover-frame">
<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>
+160 -70
View File
@@ -2,81 +2,171 @@
import { base } from '$app/paths';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { tooltip } from '$lib/actions/tooltip';
import { isMac } from '$lib/helpers/platform';
import { slide } from '$lib/helpers/transition';
$: project = $page.params.project;
$: path = `${base}/console/project-${project}`;
$: projectPath = `${base}/console/project-${project}`;
$: subNavigation = $page.data.subNavigation;
// We need to have this second variable, because we only want narrow
// to change automatically if we change from having a second side nav to
// not having one, not when the second side nav changes to a different value.
$: hasSubNavigation = !!subNavigation;
let narrow = false;
$: {
narrow = hasSubNavigation;
}
function handleKeyDown(event: KeyboardEvent) {
// If Alt + S is pressed
if (hasSubNavigation && event.altKey && event.keyCode === 83) {
event.preventDefault();
narrow = !narrow;
}
}
</script>
<div class="side-nav">
{#if project}
<div class="side-nav-main">
<section class="drop-section">
<ul class="drop-list">
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(`${path}/overview`)}
on:click={() => trackEvent('click_menu_overview')}
href={path}>
<span class="icon-chart-bar" aria-hidden="true" />
<span class="text">Overview</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(`${path}/auth`)}
on:click={() => trackEvent('click_menu_auth')}
href={`${path}/auth`}>
<span class="icon-user-group" aria-hidden="true" />
<span class="text">Auth</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(`${path}/databases`)}
on:click={() => trackEvent('click_menu_databases')}
href={`${path}/databases`}>
<span class="icon-database" aria-hidden="true" />
<span class="text">Databases</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(`${path}/functions`)}
on:click={() => trackEvent('click_menu_functions')}
href={`${path}/functions`}>
<span class="icon-lightning-bolt" aria-hidden="true" />
<span class="text">Functions</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(`${path}/storage`)}
on:click={() => trackEvent('click_menu_storage')}
href={`${path}/storage`}>
<span class="icon-folder" aria-hidden="true" />
<span class="text">Storage</span>
</a>
</li>
</ul>
</section>
</div>
<svelte:window on:keydown={handleKeyDown} />
<div class="side-nav-bottom">
<section class="drop-section">
<a
class="drop-button"
href={`${path}/settings`}
on:click={() => trackEvent('click_menu_settings')}
class:is-selected={$page.url.pathname.startsWith(`${path}/settings`)}>
<span class="icon-cog" aria-hidden="true" />
<span class="text">Settings</span>
</a>
</section>
<div class="side-nav" class:is-open-level-2={hasSubNavigation}>
<div class="side-nav-level-1" class:is-narrow={narrow}>
{#if project}
<div class="side-nav-main">
<section class="drop-section">
<ul class="drop-list">
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${projectPath}/overview`
)}
on:click={() => trackEvent('click_menu_overview')}
href={projectPath}
use:tooltip={{
content: 'Overview',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-chart-bar" aria-hidden="true" />
<span class="text">Overview</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${projectPath}/auth`
)}
on:click={() => trackEvent('click_menu_auth')}
href={`${projectPath}/auth`}
use:tooltip={{
content: 'Auth',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-user-group" aria-hidden="true" />
<span class="text">Auth</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${projectPath}/databases`
)}
on:click={() => trackEvent('click_menu_databases')}
href={`${projectPath}/databases`}
use:tooltip={{
content: 'Databases',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-database" aria-hidden="true" />
<span class="text">Databases</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${projectPath}/functions`
)}
on:click={() => trackEvent('click_menu_functions')}
href={`${projectPath}/functions`}
use:tooltip={{
content: 'Functions',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-lightning-bolt" aria-hidden="true" />
<span class="text">Functions</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${projectPath}/storage`
)}
on:click={() => trackEvent('click_menu_storage')}
href={`${projectPath}/storage`}
use:tooltip={{
content: 'Storage',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-folder" aria-hidden="true" />
<span class="text">Storage</span>
</a>
</li>
</ul>
</section>
</div>
<div class="side-nav-bottom">
<section class="drop-section">
<a
class="drop-button"
href={`${projectPath}/settings`}
on:click={() => trackEvent('click_menu_settings')}
class:is-selected={$page.url.pathname.startsWith(`${projectPath}/settings`)}
title="Settings"
use:tooltip={{
content: 'Settings',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-cog" aria-hidden="true" />
<span class="text">Settings</span>
</a>
</section>
</div>
{/if}
{#if subNavigation}
<button
class="side-nav-button button is-small is-secondary is-not-mobile"
aria-label="resize menu"
on:click={() => (narrow = !narrow)}
use:tooltip={{
content: isMac() ? '⌥ + S' : 'Alt + S'
}}>
<span
class:icon-cheveron-right={narrow}
class:icon-cheveron-left={!narrow}
aria-hidden="true" />
</button>
{/if}
</div>
{#if subNavigation}
<div class="side-nav-level-2 is-open" transition:slide|local={{ axis: 'x', duration: 250 }}>
<div class="side-nav-main">
<svelte:component this={subNavigation} />
</div>
</div>
{/if}
</div>
+13 -7
View File
@@ -1,20 +1,23 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { fly } from 'svelte/transition';
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'];
const dispatch = createEventDispatcher();
</script>
<li
<div
class="alert-sticky"
class:is-success={type === 'success'}
class:is-warning={type === 'warning'}
class:is-danger={type === 'error'}
class:is-info={type === 'info'}>
class:is-info={type === 'info'}
transition:fly={{ x: 50 }}>
<button
class="button is-text is-only-icon"
style="--button-size:1.5rem;"
@@ -24,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">
@@ -39,10 +43,12 @@
{#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}
</div>
{/if}
</li>
</div>
+20 -9
View File
@@ -1,19 +1,23 @@
<script lang="ts">
import Notification from './notification.svelte';
import { dismissNotification, notifications } from '../stores/notifications';
import { flip } from 'svelte/animate';
</script>
{#if $notifications}
<section>
<ul class="u-flex u-flex-vertical u-gap-16">
{#each $notifications as notification (notification.id)}
<Notification
type={notification.type}
title={notification.title}
on:dismiss={() => dismissNotification(notification.id)}
buttons={notification?.buttons}>
{notification.message}
</Notification>
<li animate:flip={{ duration: 500 }}>
<Notification
type={notification.type}
title={notification.title}
icon={notification.icon}
on:dismiss={() => dismissNotification(notification.id)}
buttons={notification?.buttons}>
{notification.message}
</Notification>
</li>
{/each}
</ul>
</section>
@@ -22,8 +26,15 @@
<style lang="scss">
section {
position: fixed;
top: calc(var(--main-header-height) + 24px);
right: 24px;
top: calc(var(--main-header-height) + 12px);
right: 12px;
z-index: 1000;
}
@media (min-width: 768px) {
section {
top: calc(var(--main-header-height) + 24px);
right: 24px;
}
}
</style>
+24 -11
View File
@@ -1,17 +1,17 @@
<script lang="ts">
import { navigating, page } from '$app/stores';
import { browser } from '$app/environment';
import { wizard } from '$lib/stores/wizard';
import { log } from '$lib/stores/logs';
import { beforeNavigate } from '$app/navigation';
import { page } from '$app/stores';
import { log } from '$lib/stores/logs';
import { wizard } from '$lib/stores/wizard';
export let isOpen = false;
export let showSideNavigation = false;
let y: number;
navigating.subscribe(() => {
if (isOpen) isOpen = false;
page.subscribe(({ url }) => {
isOpen = url.searchParams.get('openNavbar') === 'true';
});
const toggleMenu = () => {
@@ -41,7 +41,6 @@
<main
class:grid-with-side={showSideNavigation}
class:grid={!showSideNavigation}
class:is-open={isOpen}
class:u-hide={$wizard.show || $log.show}>
<header class="main-header u-padding-inline-end-0">
@@ -54,29 +53,43 @@
</button>
<slot name="header" />
</header>
<nav class="main-side">
<slot name="side" />
</nav>
{#if showSideNavigation}
<nav class="main-side">
<slot name="side" />
</nav>
{/if}
<section class="main-content">
{#if $page.data?.header}
<svelte:component this={$page.data.header} />
{/if}
<slot />
<slot name="footer" />
</section>
</main>
<style>
.grid-with-side {
<style lang="scss">
main {
min-height: 100vh;
&:not(.grid-with-side) {
display: flex;
flex-direction: column;
}
}
.main-side {
z-index: 25;
}
@media (max-width: 550.99px), (min-width: 551px) and (max-width: 1198.99px) {
.main-side {
top: 4.5rem;
}
}
@media (min-width: 1199px) {
.grid-with-side {
grid-template-columns: auto 1fr !important;
}
}
</style>
+5 -2
View File
@@ -6,6 +6,9 @@
import { base } from '$app/paths';
import { user } from '$lib/stores/user';
export let imgLight = LoginLight;
export let imgDark = LoginDark;
const technologies = [
'js',
'flutter',
@@ -38,9 +41,9 @@
class="container u-margin-block-start-20 is-not-mobile"
style="--p-container-max-size: var(--container-size-large);">
{#if $app.themeInUse === 'dark'}
<img src={LoginDark} alt="" class="u-only-dark" />
<img src={imgDark} alt="" class="u-only-dark" />
{:else}
<img src={LoginLight} alt="" class="u-only-light" />
<img src={imgLight} alt="" class="u-only-light" />
{/if}
</div>
+2 -2
View File
@@ -16,7 +16,7 @@
import { BarChart, LineChart } from '$lib/charts';
import { Card, SecondaryTabs, SecondaryTabsItem, Heading, Tiles } from '$lib/components';
import { Colors } from '$lib/charts/config';
import type { Models } from '@aw-labs/appwrite-console';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/stores';
type MetricMetadata = {
@@ -58,7 +58,7 @@
</div>
<Card>
{#if count}
<Heading tag="h6" size="6">{last(count).value}</Heading>
<Heading tag="h6" size="6">{total(count)}</Heading>
<p>{countMetadata.title}</p>
<div class="u-margin-block-start-16" />
<div class="chart-container">
+3 -3
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}>
@@ -122,7 +122,7 @@
{/if}
</div>
<div class="wizard-main">
<Form noStyle on:submit={submit}>
<Form noStyle onSubmit={submit}>
{#each sortedSteps as [step, { component }]}
{#if currentStep === step}
<svelte:component this={component} />
+1 -1
View File
@@ -11,7 +11,7 @@
}
</script>
<Modal bind:show on:submit={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
+11
View File
@@ -0,0 +1,11 @@
<script lang="ts">
import Form from '../elements/forms/form.svelte';
import Button from '../elements/forms/button.svelte';
export let onSubmit: () => Promise<void> | void;
</script>
<Form {onSubmit}>
<Button submit>Submit</Button>
<Button>Button</Button>
</Form>

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