mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' of github.com:appwrite/console into poc-admin-panel
This commit is contained in:
+1
-2
@@ -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
|
||||
@@ -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
@@ -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:
|
||||
|
||||
|
||||
@@ -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();
|
||||
Generated
+1017
-961
File diff suppressed because it is too large
Load Diff
+27
-25
@@ -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": [
|
||||
|
||||
Vendored
-1
@@ -1,5 +1,4 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
interface Window {
|
||||
VERCEL_ANALYTICS_ID: string | false;
|
||||
SENTRY_DSN: string | false;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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'))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 +1,2 @@
|
||||
export { default as Pill } from './pill.svelte';
|
||||
export { default as SelectSearchItem } from './selectSearchItem.svelte';
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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">
|
||||
|
||||
@@ -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,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[]) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isMac(): boolean {
|
||||
return window.navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function getProjectId() {
|
||||
const pathname = window.location.pathname;
|
||||
const projectMatch = pathname.match(/\/project-(.*?)\//);
|
||||
return projectMatch?.[1] || null;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export async function sleep(ms: number): Promise<void> {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;`
|
||||
};
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user