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