mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' of github.com:appwrite/appwrite-console-poc into feat-tag-trim-overflow
This commit is contained in:
@@ -147,7 +147,7 @@ Adding a new dependency should have vital value on the product with minimum poss
|
||||
│ │ │ │ ├── _create.svelte // Component to Create collections
|
||||
│ │ │ │ └── index.svelte // Entrypoint for "/console/[PROJECT_ID]/database"
|
||||
│ │ │ ├── storage // Storage Service "/console/[PROJECT]/storage"
|
||||
│ │ │ └── authentication // Users Service "/console/[PROJECT]/authentication"
|
||||
│ │ │ └── auth // Users Service "/console/[PROJECT]/auth"
|
||||
│ │ └──...
|
||||
│ ├── login.svelte // Component for Login "/console/login"
|
||||
│ └── register.svelte // Component for Register "/console/register"
|
||||
|
||||
Generated
+239
-707
File diff suppressed because it is too large
Load Diff
+13
-11
@@ -8,6 +8,7 @@
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"sync": "svelte-kit sync",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint .",
|
||||
@@ -18,30 +19,31 @@
|
||||
"e2e": "playwright test tests/e2e"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aw-labs/appwrite-console": "^6.0.0",
|
||||
"@aw-labs/icons": "0.0.0-64",
|
||||
"@aw-labs/ui": "0.0.0-64",
|
||||
"@aw-labs/appwrite-console": "^10.0.0",
|
||||
"@aw-labs/icons": "0.0.0-74",
|
||||
"@aw-labs/ui": "0.0.0-74",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"echarts": "^5.4.0",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"web-vitals": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@sveltejs/adapter-static": "1.0.0-next.44",
|
||||
"@sveltejs/kit": "1.0.0-next.516",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.9",
|
||||
"@sveltejs/adapter-static": "1.0.0-next.46",
|
||||
"@sveltejs/kit": "1.0.0-next.525",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.1.0",
|
||||
"@testing-library/dom": "^8.19.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/svelte": "3.1.3",
|
||||
"@testing-library/svelte": "^3.2.2",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/gtag.js": "^0.0.12",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
||||
"@typescript-eslint/parser": "^5.41.0",
|
||||
"@vitest/ui": "^0.24.3",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"pre-commit": "^1.2.2",
|
||||
@@ -54,7 +56,7 @@
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.1.8",
|
||||
"vite": "^3.2.1",
|
||||
"vitest": "^0.24.3"
|
||||
},
|
||||
"type": "module",
|
||||
|
||||
@@ -3,21 +3,23 @@ import type { Action } from 'svelte/action';
|
||||
export type AnalyticsActionParam = {
|
||||
name: string;
|
||||
action: string;
|
||||
category?: string;
|
||||
label?: string;
|
||||
parameters?: Record<string, string>;
|
||||
event?: keyof HTMLElementEventMap;
|
||||
};
|
||||
|
||||
export const event: Action<HTMLElement, AnalyticsActionParam> = (node, param) => {
|
||||
export const event: Action<HTMLElement, Partial<AnalyticsActionParam>> = (node, param) => {
|
||||
if (!isTrackingAllowed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
node.addEventListener(param.event ?? 'click', () =>
|
||||
node.addEventListener(param.event ?? 'click', () => {
|
||||
gtag('event', param.name, {
|
||||
...param.parameters,
|
||||
action: param.action
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const isTrackingAllowed = () => {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import type { BarSeriesOption } from 'echarts/charts';
|
||||
import Base from './base.svelte';
|
||||
|
||||
export let series: BarSeriesOption[];
|
||||
export let options: EChartsOption = null;
|
||||
</script>
|
||||
|
||||
<Base
|
||||
{options}
|
||||
series={series.map((s) => {
|
||||
s.type = 'bar';
|
||||
s.stack = 'total';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"borderWidth": 1
|
||||
},
|
||||
"lineStyle": {
|
||||
"width": 2
|
||||
"width": 1
|
||||
},
|
||||
"symbolSize": 4,
|
||||
"symbol": "emptyCircle",
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
import dark from './dark.json';
|
||||
import type { ECharts } from 'echarts/core';
|
||||
import type { BarSeriesOption, LineSeriesOption } from 'echarts/charts';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
|
||||
registerTheme('light', { ...base, ...light });
|
||||
registerTheme('dark', { ...base, ...dark });
|
||||
|
||||
export let options: EChartsOption;
|
||||
export let series: (BarSeriesOption | LineSeriesOption)[];
|
||||
|
||||
let chart: ECharts;
|
||||
@@ -19,6 +21,7 @@
|
||||
|
||||
$: option = {
|
||||
...defaultConfig,
|
||||
...options,
|
||||
series
|
||||
};
|
||||
|
||||
@@ -39,7 +42,7 @@
|
||||
(await import('echarts/components')).TransformComponent,
|
||||
(await import('echarts/components')).LegendComponent,
|
||||
(await import('echarts/features')).LabelLayout,
|
||||
(await import('echarts/renderers')).CanvasRenderer
|
||||
(await import('echarts/renderers')).SVGRenderer
|
||||
]);
|
||||
makeChart();
|
||||
});
|
||||
@@ -85,6 +88,7 @@
|
||||
<style>
|
||||
.echart {
|
||||
width: 100%;
|
||||
min-height: 12rem;
|
||||
min-height: 10rem;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,8 +17,8 @@ export const defaultConfig: EChartsOption = {
|
||||
containLabel: true,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 15,
|
||||
top: 15
|
||||
bottom: 0,
|
||||
top: 45
|
||||
},
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
@@ -27,6 +27,11 @@ export const defaultConfig: EChartsOption = {
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitNumber: 3,
|
||||
minInterval: 1
|
||||
minInterval: 1,
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
width: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,11 +45,11 @@
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"color": "#666666"
|
||||
"color": "#868EA3"
|
||||
},
|
||||
"splitLine": {
|
||||
"lineStyle": {
|
||||
"color": ["#333333"]
|
||||
"color": ["#4F5769"]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"splitLine": {
|
||||
"lineStyle": {
|
||||
"color": ["#E0E6F1"]
|
||||
"color": ["#E8E9F0"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { LineSeriesOption } from 'echarts/charts';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import { Colors } from './config';
|
||||
import Base from './base.svelte';
|
||||
|
||||
export let series: LineSeriesOption[];
|
||||
export let options: EChartsOption = null;
|
||||
</script>
|
||||
|
||||
<Base
|
||||
{options}
|
||||
series={series.map((s) => {
|
||||
s.type = 'line';
|
||||
s.stack = 'total';
|
||||
s.lineStyle = {
|
||||
shadowBlur: 38,
|
||||
shadowColor: Colors.Primary,
|
||||
shadowOffsetY: 15,
|
||||
shadowOffsetX: 0
|
||||
};
|
||||
s.showSymbol = false;
|
||||
|
||||
return s;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let href: string;
|
||||
</script>
|
||||
|
||||
<a {href}>
|
||||
<span class="icon-cheveron-left" aria-hidden="true" />
|
||||
<slot />
|
||||
</a>
|
||||
@@ -1,7 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
|
||||
export let isTile = false;
|
||||
export let isDashed = false;
|
||||
export let isButton = false;
|
||||
export let danger = false;
|
||||
export let href: string = null;
|
||||
|
||||
function getElement(): string {
|
||||
switch (true) {
|
||||
case !!href:
|
||||
return 'a';
|
||||
case isButton:
|
||||
return 'button';
|
||||
default:
|
||||
return 'article';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<article class="card" class:common-section={!isTile}>
|
||||
<svelte:element
|
||||
this={getElement()}
|
||||
class="card"
|
||||
class:common-section={!isTile}
|
||||
class:is-border-dashed={isDashed}
|
||||
class:is-danger={danger}
|
||||
on:click
|
||||
on:keyup={clickOnEnter}
|
||||
{href}>
|
||||
<slot />
|
||||
</article>
|
||||
</svelte:element>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
style={`--grid-gap:1.5rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}>
|
||||
<slot />
|
||||
|
||||
{#if limit + offset > total && (total % 2 !== 0 || total % 4 === 0)}
|
||||
<Empty isButton on:click>
|
||||
{#if total > 3 ? total < limit + offset : total % 2 !== 0}
|
||||
<Empty on:click>
|
||||
<slot name="empty" />
|
||||
</Empty>
|
||||
{/if}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script>
|
||||
import { Card } from './';
|
||||
|
||||
export let danger = false;
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<Card {danger}>
|
||||
<div class="common-section grid-1-2">
|
||||
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
|
||||
<slot />
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
import 'prismjs/plugins/autoloader/prism-autoloader';
|
||||
import 'prismjs/plugins/custom-class/prism-custom-class';
|
||||
import 'prismjs/plugins/line-numbers/prism-line-numbers';
|
||||
import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
|
||||
import { afterUpdate } from 'svelte';
|
||||
import { Copy } from '.';
|
||||
|
||||
@@ -49,16 +48,16 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<pre class={`language-${language}`} class:line-numbers={withLineNumbers}>
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
<pre class={`language-${language}`} class:line-numbers={withLineNumbers}><code
|
||||
>{code}</code></pre>
|
||||
</section>
|
||||
|
||||
<style lang="scss" global>
|
||||
@import 'prismjs/themes/prism.css';
|
||||
@import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
|
||||
|
||||
.box {
|
||||
--p-box-background-color: var(--color-neutral-300) !important;
|
||||
--p-box-background-color: var(--color-neutral-400) !important;
|
||||
|
||||
body.theme-light & {
|
||||
--p-box-background-color: var(--color-neutral-5) !important;
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
<script lang="ts">
|
||||
export let withIndentation = false;
|
||||
</script>
|
||||
|
||||
<li class="collapsible-item">
|
||||
<details class="collapsible-wrapper">
|
||||
<summary class="collapsible-button">
|
||||
<span class="text"><slot name="title" /></span>
|
||||
<span class="collapsible-button-optional"><slot name="subtitle" /></span>
|
||||
<slot name="beforetitle" />
|
||||
<div>
|
||||
<span class="text"><slot name="title" /></span>
|
||||
<span class="collapsible-button-optional"><slot name="subtitle" /></span>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<span class="icon-cheveron-down" aria-hidden="true" />
|
||||
</div>
|
||||
</summary>
|
||||
<div class="collapsible-content">
|
||||
<div
|
||||
class="collapsible-content"
|
||||
class:u-margin-block-start-8={withIndentation}
|
||||
class:u-padding-inline-32={withIndentation}>
|
||||
<slot />
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -22,21 +22,23 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="input-text-wrapper is-with-end-button">
|
||||
<div>
|
||||
<label class:u-hide={!showLabel} class="label" for={label}>{label}</label>
|
||||
<div class="input-text-wrapper">
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:1">
|
||||
<input {value} id={label} type="text" class="input-text" readonly />
|
||||
<button
|
||||
type="button"
|
||||
class="input-button"
|
||||
aria-label="Click to copy."
|
||||
on:click={copy}
|
||||
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
|
||||
use:tooltip={{
|
||||
content,
|
||||
hideOnClick: false
|
||||
}}>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</button>
|
||||
<div class="options-list">
|
||||
<button
|
||||
type="button"
|
||||
class="input-button"
|
||||
aria-label="Click to copy."
|
||||
on:click={copy}
|
||||
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
|
||||
use:tooltip={{
|
||||
content,
|
||||
hideOnClick: false
|
||||
}}>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,39 +11,23 @@
|
||||
type Placement = 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
|
||||
let element: HTMLDivElement;
|
||||
let tooltip: HTMLDivElement;
|
||||
let arrow: HTMLDivElement;
|
||||
let instance: Instance;
|
||||
let currentArrow: Placement;
|
||||
|
||||
$: [arrowHorizontal, arrowPosition] = applyArrow(currentArrow);
|
||||
|
||||
function applyArrow(value: Placement): [string: 'start' | 'end', string: 'top' | 'bottom'] {
|
||||
switch (value) {
|
||||
case 'bottom-start':
|
||||
return ['start', 'bottom'];
|
||||
case 'bottom-end':
|
||||
return ['end', 'bottom'];
|
||||
case 'top-start':
|
||||
return ['start', 'top'];
|
||||
case 'top-end':
|
||||
return ['end', 'top'];
|
||||
default:
|
||||
return ['start', 'bottom'];
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
instance = createPopper(element, tooltip, {
|
||||
placement,
|
||||
onFirstUpdate(state) {
|
||||
if (currentArrow !== state.placement) {
|
||||
currentArrow = state.placement as Placement;
|
||||
}
|
||||
},
|
||||
modifiers: [
|
||||
{
|
||||
name: 'arrow',
|
||||
options: {
|
||||
element: arrow
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, noArrow ? 0 : 12]
|
||||
offset: [0, noArrow ? 0 : 6]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -51,16 +35,6 @@
|
||||
options: {
|
||||
fallbackPlacements: ['top-start', 'top-end', 'bottom-start', 'bottom-end']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'placementLogger',
|
||||
enabled: true,
|
||||
phase: 'main',
|
||||
fn({ state }) {
|
||||
if (currentArrow !== state.placement) {
|
||||
currentArrow = state.placement as Placement;
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -87,16 +61,10 @@
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div bind:this={tooltip} style="z-index: 10">
|
||||
<div class="drop-tooltip" bind:this={tooltip} style="z-index: 10">
|
||||
<div class="drop-arrow" class:u-hide={!show} bind:this={arrow} />
|
||||
{#if show}
|
||||
<div
|
||||
class="drop"
|
||||
style="position: revert"
|
||||
class:is-no-arrow={noArrow}
|
||||
class:is-arrow-start={arrowHorizontal === 'start'}
|
||||
class:is-arrow-end={arrowHorizontal === 'end'}
|
||||
class:is-block-start={arrowPosition === 'top'}
|
||||
class:is-block-end={arrowPosition === 'bottom'}>
|
||||
<div class="drop is-no-arrow" style="position: revert">
|
||||
<section
|
||||
class:u-overflow-y-auto={scrollable}
|
||||
class:u-max-height-200={scrollable}
|
||||
@@ -109,3 +77,38 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style global lang="scss">
|
||||
.drop-tooltip[data-popper-placement^='top'] > .drop-arrow {
|
||||
bottom: -4px;
|
||||
}
|
||||
|
||||
.drop-tooltip[data-popper-placement^='bottom'] > .drop-arrow {
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
.drop-tooltip[data-popper-placement^='left'] > .drop-arrow {
|
||||
right: -4px;
|
||||
}
|
||||
|
||||
.drop-tooltip[data-popper-placement^='right'] > .drop-arrow {
|
||||
left: -4px;
|
||||
}
|
||||
.drop-arrow,
|
||||
.drop-arrow::before {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.drop-arrow::before {
|
||||
content: '';
|
||||
transform: rotate(45deg);
|
||||
background: hsl(var(--color-neutral-200));
|
||||
|
||||
body.theme-light & {
|
||||
background: hsl(var(--color-neutral-10));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
|
||||
export let href: string;
|
||||
export let icon: string = null;
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<li class="drop-list-item" on:click on:keyup={clickOnEnter}>
|
||||
<a {href} class="drop-button">
|
||||
<a {href} class="drop-button" {disabled}>
|
||||
<span class="text"><slot /></span>
|
||||
{#if icon}
|
||||
<span class={`icon-${icon}`} aria-hidden="true" />
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
export let href: string;
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<li class="drop-tabs-item">
|
||||
{#if disabled}
|
||||
<button class="drop-tabs-button" disabled>
|
||||
<span class="text"><slot /></span>
|
||||
</button>
|
||||
{:else}
|
||||
<a class="drop-tabs-button" {href}>
|
||||
<span class="text"><slot /></span>
|
||||
</a>
|
||||
{/if}
|
||||
</li>
|
||||
@@ -3,52 +3,44 @@
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import EmptyLight from '$lib/images/empty-light.svg';
|
||||
import EmptyDark from '$lib/images/empty-dark.svg';
|
||||
import { Heading } from '.';
|
||||
|
||||
export let isButton = false;
|
||||
export let single = false;
|
||||
export let target: string = null;
|
||||
export let href: string = null;
|
||||
</script>
|
||||
|
||||
{#if isButton}
|
||||
{#if single}
|
||||
<article class="card u-grid u-cross-center u-width-full-line common-section">
|
||||
<div class="u-flex u-flex-vertical u-cross-center u-gap-24">
|
||||
<button type="button" on:click|preventDefault>
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img src={EmptyDark} alt="create" aria-hidden="true" />
|
||||
{:else}
|
||||
<img src={EmptyLight} alt="create" aria-hidden="true" />
|
||||
{/if}
|
||||
</button>
|
||||
<slot>
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h4">Create your first {target} to get started.</Heading>
|
||||
<p class="body-text-2 u-margin-block-start-4">
|
||||
Need a hand? Check out our documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 u-main-center">
|
||||
<Button external {href} text>Documentation</Button>
|
||||
<Button secondary on:click>Create {target}</Button>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</article>
|
||||
{:else}
|
||||
<button
|
||||
on:click|preventDefault
|
||||
type="button"
|
||||
class="card u-grid u-cross-center u-width-full-line dashed"
|
||||
class:common-section={single}>
|
||||
{#if single}
|
||||
<div class="u-flex u-flex-vertical u-cross-center u-gap-24">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img src={EmptyDark} alt="create" aria-hidden="true" />
|
||||
{:else}
|
||||
<img src={EmptyLight} alt="create" aria-hidden="true" />
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
<div class="common-section">
|
||||
<Button secondary round>
|
||||
<i class="icon-plus" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="common-section">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
<article class="card u-grid u-cross-center u-width-full-line" class:common-section={single}>
|
||||
{#if single}
|
||||
<div class="u-flex u-flex-vertical u-cross-center u-gap-24">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img src={EmptyDark} alt="create" aria-hidden="true" />
|
||||
{:else}
|
||||
<img src={EmptyLight} alt="create" aria-hidden="true" />
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="common-section u-main-center u-flex">
|
||||
class="card u-grid u-cross-center u-width-full-line dashed">
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
<div class="common-section">
|
||||
<Button secondary round>
|
||||
<i class="icon-plus" />
|
||||
</Button>
|
||||
@@ -56,6 +48,6 @@
|
||||
<div class="common-section">
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import { app } from '$lib/stores/app';
|
||||
import Light from '$lib/images/search-light.svg';
|
||||
import Dark from '$lib/images/search-dark.svg';
|
||||
import Pagination from './pagination.svelte';
|
||||
import PaginationInline from './paginationInline.svelte';
|
||||
|
||||
export let hidePages = false;
|
||||
</script>
|
||||
|
||||
<article class="card u-grid u-cross-center u-width-full-line common-section">
|
||||
@@ -18,5 +20,5 @@
|
||||
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: 0</p>
|
||||
<Pagination limit={1} offset={0} sum={0} />
|
||||
<PaginationInline limit={1} offset={0} sum={0} {hidePages} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Modal, Copy } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
@@ -157,152 +157,147 @@
|
||||
//TODO: remove inline style
|
||||
</script>
|
||||
|
||||
<Form noStyle noMargin on:submit={create}>
|
||||
<Modal bind:show size="big">
|
||||
<svelte:fragment slot="header">Create Event</svelte:fragment>
|
||||
<Modal bind:show on:submit={create} size="big">
|
||||
<svelte:fragment slot="header">Create Event</svelte:fragment>
|
||||
|
||||
<div>
|
||||
<p class="u-text">Choose a service</p>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8">
|
||||
{#each services as service}
|
||||
<Pill
|
||||
disabled={showInput}
|
||||
selected={service.name === selectedService?.name}
|
||||
button
|
||||
on:click={() => {
|
||||
selectedService = service;
|
||||
inputData = null;
|
||||
}}>{service.name}</Pill>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{#if selectedService}
|
||||
<div>
|
||||
<p class="u-text">Choose a service</p>
|
||||
<p class="u-text">Choose a request (optional)</p>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8">
|
||||
{#each services as service}
|
||||
{#each selectedService.requests as request}
|
||||
<Pill
|
||||
disabled={showInput}
|
||||
selected={service.name === selectedService?.name}
|
||||
selected={request.name === selectedRequest?.name}
|
||||
button
|
||||
on:click={() => {
|
||||
selectedService = service;
|
||||
selectedRequest === request
|
||||
? (selectedRequest = null)
|
||||
: (selectedRequest = request);
|
||||
inputData = null;
|
||||
}}>{service.name}</Pill>
|
||||
}}>{request.name}</Pill>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{#if selectedService}
|
||||
<div>
|
||||
<p class="u-text">Choose a request (optional)</p>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8">
|
||||
{#each selectedService.requests as request}
|
||||
<Pill
|
||||
disabled={showInput}
|
||||
selected={request.name === selectedRequest?.name}
|
||||
button
|
||||
on:click={() => {
|
||||
selectedRequest === request
|
||||
? (selectedRequest = null)
|
||||
: (selectedRequest = request);
|
||||
inputData = null;
|
||||
}}>{request.name}</Pill>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="u-text">Choose an event (optional)</p>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8">
|
||||
{#each selectedRequest?.events ?? selectedService?.events as event}
|
||||
<Pill
|
||||
disabled={showInput}
|
||||
selected={selectedEvent === event}
|
||||
button
|
||||
on:click={() => {
|
||||
selectedEvent === event
|
||||
? (selectedEvent = null)
|
||||
: (selectedEvent = event);
|
||||
inputData = null;
|
||||
}}>{event}</Pill>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{#if selectedService?.name === 'users' && !selectedRequest?.name}
|
||||
<div>
|
||||
<p class="u-text">Choose an attribute (optional)</p>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8">
|
||||
{#each selectedService?.attributes as attribute}
|
||||
<Pill
|
||||
disabled={showInput}
|
||||
selected={selectedAttribute === attribute}
|
||||
button
|
||||
on:click={() => {
|
||||
selectedAttribute === attribute
|
||||
? (selectedAttribute = null)
|
||||
: (selectedAttribute = attribute);
|
||||
inputData = null;
|
||||
}}>{attribute}</Pill>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if showInput}
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:2">
|
||||
<input type="text" placeholder="Enter custom event" bind:value={inputData} />
|
||||
<div class="options-list">
|
||||
<button
|
||||
on:click|preventDefault={() => {
|
||||
showInput = false;
|
||||
}}
|
||||
class="options-list-button"
|
||||
aria-label="confirm">
|
||||
<span class="icon-check" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
on:click|preventDefault={() => {
|
||||
<div>
|
||||
<p class="u-text">Choose an event (optional)</p>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8">
|
||||
{#each selectedRequest?.events ?? selectedService?.events as event}
|
||||
<Pill
|
||||
disabled={showInput}
|
||||
selected={selectedEvent === event}
|
||||
button
|
||||
on:click={() => {
|
||||
selectedEvent === event
|
||||
? (selectedEvent = null)
|
||||
: (selectedEvent = event);
|
||||
inputData = null;
|
||||
showInput = false;
|
||||
}}
|
||||
class="options-list-button"
|
||||
aria-label="cancel">
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
}}>{event}</Pill>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:2">
|
||||
<div type="text" readonly style="min-height: 2.5rem;">
|
||||
{#if inputData}
|
||||
<span>{inputData}</span>
|
||||
{:else}
|
||||
{#each Array.from(eventString.values()) as route, i}
|
||||
<button
|
||||
class:u-opacity-0-5={helper !== route.description}
|
||||
on:mouseenter={() => {
|
||||
helper = route.description;
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
helper = null;
|
||||
}}>
|
||||
{route.value}
|
||||
</button>
|
||||
<span class="u-opacity-0-5">
|
||||
{i + 1 < eventString?.size ? '.' : ''}
|
||||
</span>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{#if selectedService?.name === 'users' && !selectedRequest?.name}
|
||||
<div>
|
||||
<p class="u-text">Choose an attribute (optional)</p>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8">
|
||||
{#each selectedService?.attributes as attribute}
|
||||
<Pill
|
||||
disabled={showInput}
|
||||
selected={selectedAttribute === attribute}
|
||||
button
|
||||
on:click={() => {
|
||||
selectedAttribute === attribute
|
||||
? (selectedAttribute = null)
|
||||
: (selectedAttribute = attribute);
|
||||
inputData = null;
|
||||
}}>{attribute}</Pill>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="options-list">
|
||||
<button
|
||||
on:click|preventDefault={() => {
|
||||
inputData = copyValue;
|
||||
showInput = true;
|
||||
}}
|
||||
class="options-list-button"
|
||||
aria-label="edit event">
|
||||
<span class="icon-pencil" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
disabled={!copyValue}
|
||||
class="options-list-button"
|
||||
aria-label="copy text">
|
||||
<Copy value={copyValue}>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</Copy>
|
||||
</button>
|
||||
</div>
|
||||
<p style="height: 2rem;">{helper ?? ''}</p>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if showInput}
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:2">
|
||||
<input type="text" placeholder="Enter custom event" bind:value={inputData} />
|
||||
<div class="options-list">
|
||||
<button
|
||||
on:click|preventDefault={() => {
|
||||
showInput = false;
|
||||
}}
|
||||
class="options-list-button"
|
||||
aria-label="confirm">
|
||||
<span class="icon-check" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
on:click|preventDefault={() => {
|
||||
inputData = null;
|
||||
showInput = false;
|
||||
}}
|
||||
class="options-list-button"
|
||||
aria-label="cancel">
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:2">
|
||||
<div type="text" readonly style="min-height: 2.5rem;">
|
||||
{#if inputData}
|
||||
<span>{inputData}</span>
|
||||
{:else}
|
||||
{#each Array.from(eventString.values()) as route, i}
|
||||
<button
|
||||
class:u-opacity-0-5={helper !== route.description}
|
||||
on:mouseenter={() => {
|
||||
helper = route.description;
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
helper = null;
|
||||
}}>
|
||||
{route.value}
|
||||
</button>
|
||||
<span class="u-opacity-0-5">
|
||||
{i + 1 < eventString?.size ? '.' : ''}
|
||||
</span>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="options-list">
|
||||
<button
|
||||
on:click|preventDefault={() => {
|
||||
inputData = copyValue;
|
||||
showInput = true;
|
||||
}}
|
||||
class="options-list-button"
|
||||
aria-label="edit event">
|
||||
<span class="icon-pencil" aria-hidden="true" />
|
||||
</button>
|
||||
<button disabled={!copyValue} class="options-list-button" aria-label="copy text">
|
||||
<Copy value={copyValue}>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</Copy>
|
||||
</button>
|
||||
</div>
|
||||
<p style="height: 2rem;">{helper ?? ''}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button disabled={showInput || !copyValue} submit>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Form>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button disabled={showInput || !copyValue} submit>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
export let tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||
export let size: '1' | '2' | '3' | '4' | '5' | '6' | '7';
|
||||
export let trimmed = true;
|
||||
</script>
|
||||
|
||||
<svelte:element this={tag} class={`heading-level-${size}`}>
|
||||
<svelte:element this={tag} class={`heading-level-${size}`} class:u-trim-1={trimmed}>
|
||||
<slot />
|
||||
</svelte:element>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export { default as Modal } from './modal.svelte';
|
||||
export { default as Pagination } from './pagination.svelte';
|
||||
export { default as PaginationInline } from './paginationInline.svelte';
|
||||
export { default as Card } from './card.svelte';
|
||||
export { default as CardGrid } from './cardGrid.svelte';
|
||||
export { default as CardContainer } from './cardContainer.svelte';
|
||||
export { default as InnerModal } from './innerModal.svelte';
|
||||
export { default as Tile } from './tile.svelte';
|
||||
export { default as Tiles } from './tiles.svelte';
|
||||
export { default as Back } from './back.svelte';
|
||||
export { default as Copy } from './copy.svelte';
|
||||
export { default as CopyInput } from './copyInput.svelte';
|
||||
export { default as UploadBox } from './uploadBox.svelte';
|
||||
@@ -21,12 +21,14 @@ export { default as Collapsible } from './collapsible.svelte';
|
||||
export { default as CollapsibleItem } from './collapsibleItem.svelte';
|
||||
export { default as DropTabs } from './dropTabs.svelte';
|
||||
export { default as DropTabsItem } from './dropTabsItem.svelte';
|
||||
export { default as DropTabsLink } from './dropTabsLink.svelte';
|
||||
export { default as Avatar } from './avatar.svelte';
|
||||
export { default as AvatarInitials } from './avatarInitials.svelte';
|
||||
export { default as AvatarGroup } from './avatarGroup.svelte';
|
||||
export { default as Alert } from './alert.svelte';
|
||||
export { default as Box } from './box.svelte';
|
||||
export { default as Search } from './search.svelte';
|
||||
export { default as SearchQuery } from './searchQuery.svelte';
|
||||
export { default as GridItem1 } from './gridItem1.svelte';
|
||||
export { default as Steps } from './steps.svelte';
|
||||
export { default as Step } from './step.svelte';
|
||||
@@ -40,3 +42,4 @@ export { default as Tab } from './tab.svelte';
|
||||
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';
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
<script>
|
||||
import { FormItem } from '$lib/elements/forms';
|
||||
|
||||
export let show = false;
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<section class="modal is-inner-modal">
|
||||
<header class="modal-header u-flex-wrap">
|
||||
<h4 class="modal-title body-text-1">
|
||||
<slot name="title" />
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
class="x-button"
|
||||
aria-label="Close"
|
||||
title="Close"
|
||||
on:click={() => (show = false)}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
<p class="u-flex-basis-100-percent">
|
||||
<slot name="subtitle" />
|
||||
</p>
|
||||
</header>
|
||||
<div class="modal-content">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</section>
|
||||
<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="x-button"
|
||||
aria-label="Close"
|
||||
title="Close"
|
||||
on:click={() => (show = false)}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
<p class="u-flex-basis-100-percent">
|
||||
<slot name="subtitle" />
|
||||
</p>
|
||||
</header>
|
||||
<div class="modal-content">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</FormItem>
|
||||
{/if}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { fade, fly, type FadeParams, type FlyParams } from 'svelte/transition';
|
||||
import { Alert, Heading } from '$lib/components';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { Alert } from '$lib/components';
|
||||
|
||||
export let show = false;
|
||||
export let size: 'small' | 'big' = null;
|
||||
@@ -11,44 +8,53 @@
|
||||
export let error: string = null;
|
||||
export let closable = true;
|
||||
|
||||
let dialog: HTMLDialogElement;
|
||||
let alert: HTMLElement;
|
||||
const dispatch = createEventDispatcher();
|
||||
const transitionFly: FlyParams = {
|
||||
duration: 150,
|
||||
y: 50
|
||||
};
|
||||
const transitionFade: FadeParams = {
|
||||
duration: 150
|
||||
};
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
onMount(() => {
|
||||
if (show) openModal();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (show) closeModal();
|
||||
});
|
||||
|
||||
function handleBLur(event: MouseEvent) {
|
||||
if (event.target === dialog) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
function openModal() {
|
||||
if (dialog && !dialog.open) {
|
||||
dialog.showModal();
|
||||
document.documentElement.classList.add('u-overflow-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
if (closable) {
|
||||
if (dialog && dialog.open) {
|
||||
dispatch('close');
|
||||
dialog.close();
|
||||
show = false;
|
||||
document.documentElement.classList.remove('u-overflow-hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
const handleBLur = (event: MouseEvent) => {
|
||||
const target: Partial<HTMLElement> = event.target;
|
||||
if (target.hasAttribute('data-curtain')) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
const closeModal = () => {
|
||||
if (closable) {
|
||||
dispatch('close');
|
||||
show = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround until https://github.com/sveltejs/svelte/issues/3105 is resolved.
|
||||
*/
|
||||
$: if (browser) {
|
||||
if (show) {
|
||||
document.body.classList.add('u-overflow-hidden');
|
||||
} else {
|
||||
document.body.classList.remove('u-overflow-hidden');
|
||||
}
|
||||
$: if (show) {
|
||||
openModal();
|
||||
} else {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
$: if (error) {
|
||||
@@ -56,29 +62,25 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
<svelte:window on:click={handleBLur} on:keydown={handleKeydown} />
|
||||
|
||||
{#if show}
|
||||
<div
|
||||
class="modal-curtain"
|
||||
data-curtain
|
||||
on:click={handleBLur}
|
||||
on:keyup|self={clickOnEnter}
|
||||
transition:fade={transitionFade}>
|
||||
<section
|
||||
class:is-small={size === 'small'}
|
||||
class:is-big={size === 'big'}
|
||||
class="modal"
|
||||
transition:fly={transitionFly}>
|
||||
<dialog
|
||||
class="modal"
|
||||
class:is-small={size === 'small'}
|
||||
class:is-big={size === 'big'}
|
||||
bind:this={dialog}>
|
||||
{#if show}
|
||||
<!-- svelte-ignore a11y-no-redundant-roles -->
|
||||
<form class="modal-form" role="form" on:submit|preventDefault>
|
||||
<header class="modal-header">
|
||||
{#if warning}
|
||||
<div class="avatar is-color-orange is-medium">
|
||||
<span class="icon-exclamation" aria-hidden="true" />
|
||||
</div>
|
||||
{/if}
|
||||
<Heading tag="h4" size="5">
|
||||
<h4 class="modal-title heading-level-5">
|
||||
<slot name="header" />
|
||||
</Heading>
|
||||
</h4>
|
||||
{#if closable}
|
||||
<button
|
||||
type="button"
|
||||
@@ -105,13 +107,14 @@
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{#if $$slots.footer}
|
||||
<div class="modal-footer">
|
||||
<div class="u-flex u-main-end u-gap-12">
|
||||
<div class="u-flex u-main-end u-gap-16">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
{/if}
|
||||
</dialog>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import Copy from './copy.svelte';
|
||||
|
||||
export let value: string;
|
||||
</script>
|
||||
|
||||
<Copy {value}>
|
||||
<div class="interactive-text-output is-textarea">
|
||||
<span class="text u-line-height-1-5 u-break-word"><slot /></span>
|
||||
|
||||
<div class="u-flex u-cross-child-start u-gap-8">
|
||||
<button class="interactive-text-output-button" aria-label="copy text">
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Copy>
|
||||
@@ -1,37 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let sum: number;
|
||||
export let limit: number;
|
||||
export let offset: number;
|
||||
export let path: string;
|
||||
export let hidePages = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: totalPages = Math.ceil(sum / limit);
|
||||
$: currentPage = Math.floor(offset / limit + 1);
|
||||
$: pages = pagination(currentPage, totalPages);
|
||||
|
||||
function handleOptionClick(page: number) {
|
||||
if (currentPage !== page) {
|
||||
offset = limit * (page - 1);
|
||||
currentPage = page;
|
||||
dispatch('change');
|
||||
}
|
||||
}
|
||||
|
||||
function handleButtonPage(direction: string) {
|
||||
if (direction === 'next' && currentPage < totalPages) {
|
||||
currentPage += 1;
|
||||
offset = limit * (currentPage - 1);
|
||||
dispatch('change');
|
||||
} else if (direction === 'prev' && currentPage > 1) {
|
||||
currentPage -= 1;
|
||||
offset = limit * (currentPage - 1);
|
||||
dispatch('change');
|
||||
}
|
||||
}
|
||||
|
||||
function pagination(page: number, total: number) {
|
||||
const pagesShown = 5;
|
||||
const start = Math.max(
|
||||
@@ -51,30 +29,29 @@
|
||||
</script>
|
||||
|
||||
{#if totalPages > 1}
|
||||
{@const search = $page.url.search}
|
||||
<nav class="pagination">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => handleButtonPage('prev')}
|
||||
<a
|
||||
class:is-disabled={currentPage <= 1}
|
||||
class="button is-text"
|
||||
aria-label="prev page">
|
||||
aria-label="prev page"
|
||||
href={`${path}/${currentPage - 1}${search}`}>
|
||||
<span class="icon-cheveron-left" aria-hidden="true" />
|
||||
<span class="text">Prev</span>
|
||||
</button>
|
||||
</a>
|
||||
{#if !hidePages}
|
||||
<ol class="pagination-list is-only-desktop">
|
||||
{#each pages as page}
|
||||
{#if typeof page === 'number'}
|
||||
<li class="pagination-item">
|
||||
<button
|
||||
type="button"
|
||||
<a
|
||||
href={`${path}/${page}${search}`}
|
||||
class="button"
|
||||
on:click={() => handleOptionClick(+page)}
|
||||
class:is-disabled={currentPage === page}
|
||||
class:is-text={currentPage !== page}
|
||||
aria-label="page">
|
||||
<span class="text">{page}</span>
|
||||
</button>
|
||||
</a>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="li is-text">
|
||||
@@ -84,15 +61,14 @@
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
<button
|
||||
on:click={() => handleButtonPage('next')}
|
||||
<a
|
||||
class:is-disabled={currentPage === totalPages}
|
||||
class="button is-text"
|
||||
type="button"
|
||||
href={`${path}/${currentPage + 1}${search}`}
|
||||
aria-label="next page">
|
||||
<span class="text">Next</span>
|
||||
<span class="icon-cheveron-right" aria-hidden="true" />
|
||||
</button>
|
||||
</a>
|
||||
</nav>
|
||||
{:else}
|
||||
<nav class="pagination">
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let sum: number;
|
||||
export let limit: number;
|
||||
export let offset: number;
|
||||
export let hidePages = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: totalPages = Math.ceil(sum / limit);
|
||||
$: currentPage = Math.floor(offset / limit + 1);
|
||||
$: pages = pagination(currentPage, totalPages);
|
||||
|
||||
function handleOptionClick(page: number) {
|
||||
if (currentPage !== page) {
|
||||
offset = limit * (page - 1);
|
||||
currentPage = page;
|
||||
dispatch('change');
|
||||
}
|
||||
}
|
||||
|
||||
function handleButtonPage(direction: string) {
|
||||
if (direction === 'next' && currentPage < totalPages) {
|
||||
currentPage += 1;
|
||||
offset = limit * (currentPage - 1);
|
||||
dispatch('change');
|
||||
} else if (direction === 'prev' && currentPage > 1) {
|
||||
currentPage -= 1;
|
||||
offset = limit * (currentPage - 1);
|
||||
dispatch('change');
|
||||
}
|
||||
}
|
||||
|
||||
function pagination(page: number, total: number) {
|
||||
const pagesShown = 5;
|
||||
const start = Math.max(
|
||||
1,
|
||||
Math.min(page - Math.floor((pagesShown - 3) / 2), total - pagesShown + 2)
|
||||
);
|
||||
const end = Math.min(
|
||||
total,
|
||||
Math.max(page + Math.floor((pagesShown - 2) / 2), pagesShown - 1)
|
||||
);
|
||||
return [
|
||||
...(start > 2 ? [1, '...'] : start > 1 ? [1] : []),
|
||||
...Array.from({ length: end + 1 - start }, (_, i) => i + start),
|
||||
...(end < total - 1 ? ['...', total] : end < total ? [total] : [])
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if totalPages > 1}
|
||||
<nav class="pagination">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => handleButtonPage('prev')}
|
||||
class:is-disabled={currentPage <= 1}
|
||||
class="button is-text"
|
||||
aria-label="prev page">
|
||||
<span class="icon-cheveron-left" aria-hidden="true" />
|
||||
<span class="text">Prev</span>
|
||||
</button>
|
||||
{#if !hidePages}
|
||||
<ol class="pagination-list is-only-desktop">
|
||||
{#each pages as page}
|
||||
{#if typeof page === 'number'}
|
||||
<li class="pagination-item">
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
on:click={() => handleOptionClick(+page)}
|
||||
class:is-disabled={currentPage === page}
|
||||
class:is-text={currentPage !== page}
|
||||
aria-label="page">
|
||||
<span class="text">{page}</span>
|
||||
</button>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="li is-text">
|
||||
<span class="icon">...</span>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
<button
|
||||
on:click={() => handleButtonPage('next')}
|
||||
class:is-disabled={currentPage === totalPages}
|
||||
class="button is-text"
|
||||
type="button"
|
||||
aria-label="next page">
|
||||
<span class="text">Next</span>
|
||||
<span class="icon-cheveron-right" aria-hidden="true" />
|
||||
</button>
|
||||
</nav>
|
||||
{:else}
|
||||
<nav class="pagination">
|
||||
<button type="button" class="button is-text is-disabled" aria-label="prev page">
|
||||
<span class="icon-cheveron-left" aria-hidden="true" />
|
||||
<span class="text">Prev</span>
|
||||
</button>
|
||||
{#if !hidePages}
|
||||
<ol class="pagination-list is-only-desktop">
|
||||
<li class="pagination-item">
|
||||
<button type="button" class="button is-disabled" aria-label="page">
|
||||
<span class="text">1</span>
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
{/if}
|
||||
<button type="button" class="button is-text is-disabled" aria-label="next page">
|
||||
<span class="text">Next</span>
|
||||
<span class="icon-cheveron-right" aria-hidden="true" />
|
||||
</button>
|
||||
</nav>
|
||||
{/if}
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { DropList, DropListItem } from '..';
|
||||
import Custom from './custom.svelte';
|
||||
import Team from './team.svelte';
|
||||
@@ -22,10 +21,7 @@
|
||||
</script>
|
||||
|
||||
<DropList bind:show={showDropdown} placement="bottom-end">
|
||||
<Button text noMargin on:click={() => (showDropdown = !showDropdown)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add role</span>
|
||||
</Button>
|
||||
<slot />
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem disabled={$groups.has('any')} on:click={() => dispatch('create', ['any'])}>
|
||||
Any
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Button, Form, FormList, Helper, InputText } from '$lib/elements/forms';
|
||||
import { Button, FormList, Helper, InputText } from '$lib/elements/forms';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { Modal } from '..';
|
||||
import type { Writable } from 'svelte/store';
|
||||
@@ -25,26 +25,22 @@
|
||||
$: disabled = !value || $groups.has(value);
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<Form noMargin noStyle on:submit={create}>
|
||||
<Modal bind:show on:close={reset}>
|
||||
<svelte:fragment slot="header">Custom permission</svelte:fragment>
|
||||
<Modal bind:show on:close={reset} on:submit={create}>
|
||||
<svelte:fragment slot="header">Custom permission</svelte:fragment>
|
||||
|
||||
<FormList>
|
||||
<InputText
|
||||
showLabel={false}
|
||||
id="custom-permission"
|
||||
label="Custom permission"
|
||||
placeholder="user:[USER_ID] or team:[TEAM_ID]/[ROLE]"
|
||||
bind:value />
|
||||
<Helper type="neutral">
|
||||
A permission should be formatted as: user:[USER_ID] or team:[TEAM_ID]/[ROLE]¸
|
||||
</Helper>
|
||||
</FormList>
|
||||
<FormList>
|
||||
<InputText
|
||||
showLabel={false}
|
||||
id="custom-permission"
|
||||
label="Custom permission"
|
||||
placeholder="user:[USER_ID] or team:[TEAM_ID]/[ROLE]"
|
||||
bind:value />
|
||||
<Helper type="neutral">
|
||||
A permission should be formatted as: user:[USER_ID] or team:[TEAM_ID]/[ROLE]¸
|
||||
</Helper>
|
||||
</FormList>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button submit {disabled}>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Form>
|
||||
{/if}
|
||||
<svelte:fragment slot="footer">
|
||||
<Button submit {disabled}>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as Permissions } from './permissions.svelte';
|
||||
export { default as Roles } from './roles.svelte';
|
||||
export { default as Team } from './team.svelte';
|
||||
export { default as User } from './user.svelte';
|
||||
|
||||
@@ -10,26 +10,23 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { difference } from '$lib/helpers/array';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { writable, type Unsubscriber } from 'svelte/store';
|
||||
import { Empty } from '../';
|
||||
import Actions from './actions.svelte';
|
||||
import Row from './row.svelte';
|
||||
|
||||
export let withCreate = false;
|
||||
export let withCrud = true;
|
||||
export let permissions: string[] = [];
|
||||
export let executeAccess: string[] = [];
|
||||
|
||||
let showUser = false;
|
||||
let showTeam = false;
|
||||
@@ -43,10 +40,7 @@
|
||||
permissions.forEach(fromPermissionString);
|
||||
unsubscribe = groups.subscribe(() => {
|
||||
const current = exportRoles();
|
||||
if (
|
||||
difference(current, permissions).length ||
|
||||
difference(permissions, current).length
|
||||
) {
|
||||
if (symmetricDifference(current, permissions).length) {
|
||||
permissions = current;
|
||||
}
|
||||
});
|
||||
@@ -145,76 +139,61 @@
|
||||
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
$: if (executeAccess.length && !permissions.length) {
|
||||
executeAccess.forEach((permission) => {
|
||||
addRole(permission);
|
||||
});
|
||||
}
|
||||
$: if ([...$groups]?.length && !permissions.length) {
|
||||
executeAccess = [...$groups].sort(sortRoles).map(([role]) => role);
|
||||
}
|
||||
|
||||
//TODO: click on Empty components opens the dropdown
|
||||
</script>
|
||||
|
||||
{#if [...$groups]?.length}
|
||||
<Table noMargin noStyles noMobile>
|
||||
<TableHeader>
|
||||
<TableCellHead>Role</TableCellHead>
|
||||
{#if withCrud}
|
||||
{#if withCreate}
|
||||
<TableCellHead width={70}>Create</TableCellHead>
|
||||
{/if}
|
||||
<TableCellHead width={70}>Read</TableCellHead>
|
||||
<TableCellHead width={70}>Update</TableCellHead>
|
||||
<TableCellHead width={70}>Delete</TableCellHead>
|
||||
{#if withCreate}
|
||||
<TableCellHead width={70}>Create</TableCellHead>
|
||||
{/if}
|
||||
<TableCellHead width={70}>Read</TableCellHead>
|
||||
<TableCellHead width={70}>Update</TableCellHead>
|
||||
<TableCellHead width={70}>Delete</TableCellHead>
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each [...$groups].sort(sortRoles) as [role, permission]}
|
||||
<TableRow>
|
||||
<TableCellText title="Role">
|
||||
<TableCell title="Role">
|
||||
<Row {role} />
|
||||
</TableCellText>
|
||||
</TableCell>
|
||||
|
||||
{#if withCrud}
|
||||
{#if withCreate}
|
||||
<TableCell title="Create">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="icon-check"
|
||||
aria-label="Create"
|
||||
checked={permission.create}
|
||||
on:change={() => togglePermission(role, 'create')} />
|
||||
</TableCell>
|
||||
{/if}
|
||||
<TableCell title="Read">
|
||||
{#if withCreate}
|
||||
<TableCell title="Create">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="icon-check"
|
||||
aria-label="Read"
|
||||
checked={permission.read}
|
||||
on:change={() => togglePermission(role, 'read')} />
|
||||
</TableCell>
|
||||
<TableCell title="Update">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="icon-check"
|
||||
aria-label="Update"
|
||||
checked={permission.update}
|
||||
on:change={() => togglePermission(role, 'update')} />
|
||||
</TableCell>
|
||||
<TableCell title="Delete">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="icon-check"
|
||||
aria-label="Delete"
|
||||
checked={permission.delete}
|
||||
on:change={() => togglePermission(role, 'delete')} />
|
||||
aria-label="Create"
|
||||
checked={permission.create}
|
||||
on:change={() => togglePermission(role, 'create')} />
|
||||
</TableCell>
|
||||
{/if}
|
||||
<TableCell title="Read">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="icon-check"
|
||||
aria-label="Read"
|
||||
checked={permission.read}
|
||||
on:change={() => togglePermission(role, 'read')} />
|
||||
</TableCell>
|
||||
<TableCell title="Update">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="icon-check"
|
||||
aria-label="Update"
|
||||
checked={permission.update}
|
||||
on:change={() => togglePermission(role, 'update')} />
|
||||
</TableCell>
|
||||
<TableCell title="Delete">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="icon-check"
|
||||
aria-label="Delete"
|
||||
checked={permission.delete}
|
||||
on:change={() => togglePermission(role, 'delete')} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell title="Remove" width={40}>
|
||||
<div class="u-flex">
|
||||
@@ -231,18 +210,38 @@
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Actions
|
||||
bind:showCustom
|
||||
bind:showDropdown
|
||||
bind:showTeam
|
||||
bind:showUser
|
||||
{groups}
|
||||
on:create={create}>
|
||||
<Button text noMargin on:click={() => (showDropdown = !showDropdown)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add role</span>
|
||||
</Button>
|
||||
</Actions>
|
||||
{:else}
|
||||
<span class="eyebrow-heading-3 u-sep-block-end">ROLE</span>
|
||||
|
||||
<Empty isButton on:click={() => (showDropdown = !showDropdown)}>
|
||||
Add a role to get started
|
||||
</Empty>
|
||||
<article class="card u-grid u-cross-center u-width-full-line dashed">
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
<div class="common-section">
|
||||
<Actions
|
||||
bind:showCustom
|
||||
bind:showDropdown
|
||||
bind:showTeam
|
||||
bind:showUser
|
||||
{groups}
|
||||
on:create={create}>
|
||||
<Button secondary round on:click={() => (showDropdown = !showDropdown)}>
|
||||
<i class="icon-plus" />
|
||||
</Button>
|
||||
</Actions>
|
||||
</div>
|
||||
<div class="common-section">
|
||||
<span class="text"> Add a role to get started </span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{/if}
|
||||
|
||||
<Actions
|
||||
bind:showCustom
|
||||
bind:showDropdown
|
||||
bind:showTeam
|
||||
bind:showUser
|
||||
{groups}
|
||||
on:create={create} />
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { difference } from '$lib/helpers/array';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { writable, type Unsubscriber } from 'svelte/store';
|
||||
import Actions from './actions.svelte';
|
||||
@@ -28,7 +30,7 @@
|
||||
roles.forEach(addRole);
|
||||
unsubscribe = groups.subscribe((n) => {
|
||||
const current = Array.from(n.keys());
|
||||
if (difference(current, roles).length || difference(roles, current).length) {
|
||||
if (symmetricDifference(current, roles).length) {
|
||||
roles = current;
|
||||
}
|
||||
});
|
||||
@@ -69,39 +71,84 @@
|
||||
return n;
|
||||
});
|
||||
}
|
||||
|
||||
function sortRoles(a: string, b: string) {
|
||||
if ((a === 'any') !== (b === 'any')) {
|
||||
return a === 'any' ? -1 : 1;
|
||||
}
|
||||
if ((a === 'users') !== (b === 'users')) {
|
||||
return a === 'users' ? -1 : 1;
|
||||
}
|
||||
if ((a === 'guests') !== (b === 'guests')) {
|
||||
return a === 'guests' ? -1 : 1;
|
||||
}
|
||||
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
$: if (roles) {
|
||||
roles.forEach(addRole);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Table noMargin noStyles noMobile>
|
||||
<TableHeader>
|
||||
<TableCellHead>Role</TableCellHead>
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each [...$groups.keys()] as role}
|
||||
<TableRow>
|
||||
<TableCellText title="Role">
|
||||
<Row {role} />
|
||||
</TableCellText>
|
||||
<TableCellText title="Remove">
|
||||
<div class="u-flex">
|
||||
<button
|
||||
class="button is-text is-only-icon"
|
||||
type="button"
|
||||
aria-label="delete"
|
||||
on:click={() => deleteRole(role)}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCellText>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Actions
|
||||
bind:showCustom
|
||||
bind:showDropdown
|
||||
bind:showTeam
|
||||
bind:showUser
|
||||
{groups}
|
||||
on:create={create} />
|
||||
{#if [...$groups.keys()]?.length}
|
||||
<Table noMargin noStyles noMobile>
|
||||
<TableHeader>
|
||||
<TableCellHead>Role</TableCellHead>
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each [...$groups.keys()].sort(sortRoles) as role}
|
||||
<TableRow>
|
||||
<TableCell title="Role">
|
||||
<Row {role} />
|
||||
</TableCell>
|
||||
<TableCellText title="Remove">
|
||||
<div class="u-flex">
|
||||
<button
|
||||
class="button is-text is-only-icon"
|
||||
type="button"
|
||||
aria-label="delete"
|
||||
on:click={() => deleteRole(role)}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCellText>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Actions
|
||||
bind:showCustom
|
||||
bind:showDropdown
|
||||
bind:showTeam
|
||||
bind:showUser
|
||||
{groups}
|
||||
on:create={create}>
|
||||
<Button text noMargin on:click={() => (showDropdown = !showDropdown)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add role</span>
|
||||
</Button>
|
||||
</Actions>
|
||||
{:else}
|
||||
<article class="card u-grid u-cross-center u-width-full-line dashed">
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
<div class="common-section">
|
||||
<Actions
|
||||
bind:showCustom
|
||||
bind:showDropdown
|
||||
bind:showTeam
|
||||
bind:showUser
|
||||
{groups}
|
||||
on:create={create}>
|
||||
<Button secondary round on:click={() => (showDropdown = !showDropdown)}>
|
||||
<i class="icon-plus" />
|
||||
</Button>
|
||||
</Actions>
|
||||
</div>
|
||||
<div class="common-section">
|
||||
<span class="text"> Add a role to get started </span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{/if}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{:else if role === 'any'}
|
||||
<div>Any</div>
|
||||
{:else}
|
||||
<div>{role}</div>
|
||||
<div class="u-trim-1">{role}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Button, Form, InputSearch } from '$lib/elements/forms';
|
||||
import { Button, InputSearch } from '$lib/elements/forms';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { sdkForProject } from '$lib/stores/sdk';
|
||||
import { Query, type Models } from '@aw-labs/appwrite-console';
|
||||
import { AvatarInitials, Modal, Pagination } from '..';
|
||||
import { AvatarInitials, EmptySearch, Modal, PaginationInline } from '..';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { Permission } from './permissions.svelte';
|
||||
|
||||
@@ -58,54 +58,80 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<Form noStyle noMargin on:submit={create}>
|
||||
<Modal bind:show on:close={reset} size="big">
|
||||
<svelte:fragment slot="header">Select teams</svelte:fragment>
|
||||
<InputSearch bind:value={search} />
|
||||
{#if results?.teams}
|
||||
<div class="table-wrapper">
|
||||
<table class="table is-table-layout-auto is-remove-outer-styles">
|
||||
<tbody class="table-tbody">
|
||||
{#each results.teams as team (team.$id)}
|
||||
{@const role = `team:${team.$id}`}
|
||||
{@const exists = $groups.has(role)}
|
||||
<tr class="table-row">
|
||||
<td
|
||||
class="table-col"
|
||||
data-title="Enabled"
|
||||
style="--p-col-width:40">
|
||||
<input
|
||||
id={team.$id}
|
||||
type="checkbox"
|
||||
class="icon-check"
|
||||
aria-label="Create"
|
||||
checked={exists || selected.has(role)}
|
||||
disabled={exists}
|
||||
on:change={(event) => onSelection(event, role)} />
|
||||
</td>
|
||||
<td class="table-col" data-title="Team">
|
||||
<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="u-x-small">{team.$id}</div>
|
||||
</div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<Modal bind:show on:submit={create} on:close={reset} size="big">
|
||||
<svelte:fragment slot="header">Select teams</svelte:fragment>
|
||||
<InputSearch autofocus disabled={!results?.teams?.length && !search} bind:value={search} />
|
||||
{#if results?.teams?.length}
|
||||
<div class="table-wrapper">
|
||||
<table class="table is-table-layout-auto is-remove-outer-styles">
|
||||
<tbody class="table-tbody">
|
||||
{#each results.teams as team (team.$id)}
|
||||
{@const role = `team:${team.$id}`}
|
||||
{@const exists = $groups.has(role)}
|
||||
<tr class="table-row">
|
||||
<td class="table-col" data-title="Enabled" style="--p-col-width:40">
|
||||
<input
|
||||
id={team.$id}
|
||||
type="checkbox"
|
||||
class="icon-check"
|
||||
aria-label="Create"
|
||||
checked={exists || selected.has(role)}
|
||||
disabled={exists}
|
||||
on:change={(event) => onSelection(event, role)} />
|
||||
</td>
|
||||
<td class="table-col" data-title="Team">
|
||||
<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="u-x-small">{team.$id}</div>
|
||||
</div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {results?.total}</p>
|
||||
<PaginationInline limit={5} bind:offset sum={results?.total} hidePages />
|
||||
</div>
|
||||
{:else if search}
|
||||
<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>
|
||||
<p>There are no teams that match your search.</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 common-section u-main-center">
|
||||
<Button external href="https://appwrite.io/docs/client/teams" text
|
||||
>Documentation</Button>
|
||||
<Button secondary on:click={() => (search = '')}>Clear search</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {results?.total}</p>
|
||||
<Pagination limit={5} bind:offset sum={results?.total} hidePages />
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button submit disabled={!hasSelection}>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Form>
|
||||
{/if}
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<EmptySearch hidePages>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<p class="text u-line-height-1-5">
|
||||
You have no teams. Create a team to see them here.
|
||||
</p>
|
||||
<p class="text u-line-height-1-5">
|
||||
Need a hand? Check out our <a
|
||||
href="https://appwrite.io/docs/client/teams"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
documentation</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button submit disabled={!hasSelection}>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Button, Form, InputSearch } from '$lib/elements/forms';
|
||||
import { Button, InputSearch } from '$lib/elements/forms';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { AvatarInitials, Modal, Pagination } from '..';
|
||||
import { AvatarInitials, EmptySearch, Modal, PaginationInline } from '..';
|
||||
import { sdkForProject } from '$lib/stores/sdk';
|
||||
import { Query, type Models } from '@aw-labs/appwrite-console';
|
||||
import type { Writable } from 'svelte/store';
|
||||
@@ -58,49 +58,106 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form noStyle noMargin on:submit={create}>
|
||||
<Modal bind:show on:close={reset} size="big">
|
||||
<svelte:fragment slot="header">Select users</svelte:fragment>
|
||||
<InputSearch bind:value={search} />
|
||||
{#if results?.users}
|
||||
<div class="table-wrapper">
|
||||
<table class="table is-table-layout-auto is-remove-outer-styles">
|
||||
<tbody class="table-tbody">
|
||||
{#each results.users as user (user.$id)}
|
||||
{@const role = `user:${user.$id}`}
|
||||
{@const exists = $groups.has(role)}
|
||||
<tr class="table-row">
|
||||
<td class="table-col" data-title="Enabled" style="--p-col-width:40">
|
||||
<input
|
||||
id={user.$id}
|
||||
type="checkbox"
|
||||
class="icon-check"
|
||||
aria-label="Create"
|
||||
checked={exists || selected.has(role)}
|
||||
disabled={exists}
|
||||
on:change={(event) => onSelection(event, role)} />
|
||||
</td>
|
||||
<td class="table-col" data-title="User">
|
||||
<label class="u-flex u-cross-center u-gap-8" for={user.$id}>
|
||||
<AvatarInitials size={32} name={user.name} />
|
||||
<Modal bind:show on:submit={create} on:close={reset} size="big">
|
||||
<svelte:fragment slot="header">Select users</svelte:fragment>
|
||||
<InputSearch autofocus disabled={!results?.users?.length && !search} bind:value={search} />
|
||||
{#if results?.users?.length}
|
||||
<div class="table-wrapper">
|
||||
<table class="table is-table-layout-auto is-remove-outer-styles">
|
||||
<tbody class="table-tbody">
|
||||
{#each results.users as user (user.$id)}
|
||||
{@const role = `user:${user.$id}`}
|
||||
{@const exists = $groups.has(role)}
|
||||
<tr class="table-row">
|
||||
<td class="table-col" data-title="Enabled" style="--p-col-width:40">
|
||||
<input
|
||||
id={user.$id}
|
||||
type="checkbox"
|
||||
class="icon-check"
|
||||
aria-label="Create"
|
||||
checked={exists || selected.has(role)}
|
||||
disabled={exists}
|
||||
on:change={(event) => onSelection(event, role)} />
|
||||
</td>
|
||||
<td class="table-col" data-title="User">
|
||||
<label class="u-flex u-cross-center u-gap-8" for={user.$id}>
|
||||
{#if user.email || user.phone}
|
||||
{#if user.name}
|
||||
<AvatarInitials size={32} name={user.name} />
|
||||
<div class="u-line-height-1-5">
|
||||
<div class="body-text-2">
|
||||
{user.name}
|
||||
</div>
|
||||
<div class="u-x-small">{user.$id}</div>
|
||||
</div>
|
||||
{:else}
|
||||
<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">
|
||||
{user.email ? user.email : user.phone}
|
||||
</div>
|
||||
<div class="u-x-small">{user.$id}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<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">{user.name}</div>
|
||||
<div class="body-text-2">
|
||||
{user.name ? user.name : '-'}
|
||||
</div>
|
||||
<div class="u-x-small">{user.$id}</div>
|
||||
</div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {results?.total}</p>
|
||||
<Pagination limit={5} bind:offset sum={results?.total} hidePages />
|
||||
<PaginationInline limit={5} bind:offset sum={results?.total} hidePages />
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button submit disabled={!hasSelection}>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Form>
|
||||
{:else if search}
|
||||
<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>
|
||||
<p>There are no Users that match your search.</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 common-section u-main-center">
|
||||
<Button external href="https://appwrite.io/docs/server/users" text
|
||||
>Documentation</Button>
|
||||
<Button secondary on:click={() => (search = '')}>Clear search</Button>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<EmptySearch hidePages>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<p class="text u-line-height-1-5">
|
||||
You have no users. Create a user to see them here.
|
||||
</p>
|
||||
<p class="text u-line-height-1-5">
|
||||
Need a hand? Check out our <a
|
||||
href="https://appwrite.io/docs/server/users"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
documentation</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button submit disabled={!hasSelection}>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
export let search = '';
|
||||
export let placeholder = '';
|
||||
export let debounce = 250;
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
export let autofocus = false;
|
||||
export let isWithEndButton = true;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
search = '';
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
|
||||
$: valueChange(search);
|
||||
|
||||
const valueChange = (value: string) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(async () => {
|
||||
const url = new URL($page.url);
|
||||
if (url.search === value) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Reset to first page if search changes.
|
||||
*/
|
||||
if ($page.data.page > 1) {
|
||||
url.pathname = url.pathname.replace(new RegExp(`/${$page.data.page}*$`), '/');
|
||||
}
|
||||
|
||||
url.search = value;
|
||||
|
||||
goto(url, { keepfocus: true });
|
||||
}, debounce);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="u-flex u-gap-12 common-section u-main-space-between">
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<div class="input-text-wrapper" class:is-with-end-button={isWithEndButton}>
|
||||
<input
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{required}
|
||||
type="search"
|
||||
class="input-text"
|
||||
bind:value={search} />
|
||||
<span class="icon-search" aria-hidden="true" />
|
||||
{#if isWithEndButton && search}
|
||||
<button class="x-button" aria-label="Clear search" on:click={() => (search = '')}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
+44
-5
@@ -1,23 +1,62 @@
|
||||
export const scopes = [
|
||||
export const PAGE_LIMIT = 12; // default page limit
|
||||
export const CARD_LIMIT = 6; // default card limit
|
||||
export enum Dependencies {
|
||||
ORGANIZATION = 'dependency:organization',
|
||||
PROJECT = 'dependency:project',
|
||||
PROJECTS = 'dependency:projects',
|
||||
ACCOUNT = 'dependency:account',
|
||||
ACCOUNT_SESSIONS = 'dependency:account_sessions',
|
||||
USER = 'dependency:user',
|
||||
USERS = 'dependency:users',
|
||||
SESSIONS = 'dependency:sessions',
|
||||
TEAM = 'dependency:team',
|
||||
TEAMS = 'dependency:teams',
|
||||
MEMBERSHIPS = 'dependency:memberships',
|
||||
DATABASE = 'dependency:database',
|
||||
COLLECTION = 'dependency:collection',
|
||||
DOCUMENT = 'dependency:document',
|
||||
DOCUMENTS = 'dependency:documents',
|
||||
BUCKET = 'dependency:bucket',
|
||||
FILE = 'dependency:file',
|
||||
FILES = 'dependency:files',
|
||||
FUNCTION = 'dependency:function',
|
||||
FUNCTIONS = 'dependency:functions',
|
||||
VARIABLES = 'dependency:variables',
|
||||
DEPLOYMENTS = 'dependency:deployments',
|
||||
EXECUTIONS = 'dependency:executions',
|
||||
PLATFORM = 'dependency:platform',
|
||||
PLATFORMS = 'dependency:platforms',
|
||||
KEY = 'dependency:key',
|
||||
KEYS = 'dependency:keys',
|
||||
DOMAINS = 'dependency:domains',
|
||||
WEBHOOK = 'dependency:webhook',
|
||||
WEBHOOKS = 'dependency:webhooks'
|
||||
}
|
||||
|
||||
export const scopes: {
|
||||
scope: string;
|
||||
description: string;
|
||||
category: string;
|
||||
}[] = [
|
||||
{
|
||||
scope: 'users.read',
|
||||
description: "Access to read your project's users",
|
||||
category: 'Authentication'
|
||||
category: 'Auth'
|
||||
},
|
||||
{
|
||||
scope: 'users.write',
|
||||
description: "Access to create, update, and delete your project's users",
|
||||
category: 'Authentication'
|
||||
category: 'Auth'
|
||||
},
|
||||
{
|
||||
scope: 'teams.read',
|
||||
description: "Access to read your project's teams",
|
||||
category: 'Authentication'
|
||||
category: 'Auth'
|
||||
},
|
||||
{
|
||||
scope: 'teams.write',
|
||||
description: "Access to create, update, and delete your project's teams",
|
||||
category: 'Authentication'
|
||||
category: 'Auth'
|
||||
},
|
||||
{
|
||||
scope: 'databases.read',
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<a
|
||||
{disabled}
|
||||
{href}
|
||||
target={external ? '_blank' : '_self'}
|
||||
target={external ? '_blank' : ''}
|
||||
rel={external ? 'noopener noreferrer' : ''}
|
||||
class="button"
|
||||
class:is-only-icon={round}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
|
||||
const pattern = String.raw`^[^]+\s+[^]+\s+[^]+\s+[^]+\s+[^]`;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
element.focus();
|
||||
@@ -57,6 +59,7 @@
|
||||
{maxlength}
|
||||
{readonly}
|
||||
{step}
|
||||
{pattern}
|
||||
type="text"
|
||||
class="input-text"
|
||||
bind:value
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
const handleInvalid = (event: Event) => {
|
||||
function handleInvalid(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.valueMissing) {
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
error = element.validationMessage;
|
||||
};
|
||||
}
|
||||
|
||||
$: if (value) {
|
||||
error = null;
|
||||
@@ -45,6 +45,7 @@
|
||||
{disabled}
|
||||
{readonly}
|
||||
{required}
|
||||
step=".001"
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
type="datetime-local"
|
||||
class="input-text"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { FormItem, Helper } from '.';
|
||||
|
||||
export let id: string;
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
export let id: string;
|
||||
export let value: string | number | boolean;
|
||||
export let placeholder = '';
|
||||
export let required = false;
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
</div>
|
||||
<input
|
||||
{id}
|
||||
placeholder={!tags.length ? placeholder : ''}
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{readonly}
|
||||
type="text"
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
</script>
|
||||
|
||||
<div class="table-row" role="row" on:keyup={clickOnEnter} on:click|preventDefault>
|
||||
<!-- svelte-ignore a11y-unknown-role -->
|
||||
<tr
|
||||
class="table-row"
|
||||
role="row button"
|
||||
tabindex="0"
|
||||
on:keyup={clickOnEnter}
|
||||
on:click|preventDefault>
|
||||
<slot />
|
||||
</div>
|
||||
</tr>
|
||||
|
||||
@@ -8,3 +8,7 @@ export function difference(arr1: unknown[], arr2: unknown[]) {
|
||||
const intersection = new Set(arr1.filter((elem) => !set.has(elem)));
|
||||
return Array.from(intersection);
|
||||
}
|
||||
|
||||
export function symmetricDifference(arr1: unknown[], arr2: unknown[]) {
|
||||
return difference(arr1, arr2).concat(difference(arr2, arr1));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
export function pageToOffset(page: number, limit: number): number {
|
||||
return page ? page * limit - limit : 0;
|
||||
}
|
||||
|
||||
type TabElement = { href: string; title: string; hasChildren?: boolean };
|
||||
|
||||
export function isTabSelected(
|
||||
tab: TabElement,
|
||||
pathname: string,
|
||||
basePath: string,
|
||||
tabs: TabElement[]
|
||||
) {
|
||||
if (!tab?.hasChildren) {
|
||||
return tab.href === pathname;
|
||||
} else {
|
||||
if (tab.href === pathname) return true;
|
||||
|
||||
if (tab.href === basePath) {
|
||||
if (!tabs.some((t) => pathname.includes(t.href) && t.href !== tab.href)) {
|
||||
return pathname.includes(tab.href);
|
||||
}
|
||||
} else {
|
||||
return pathname.includes(tab.href);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
export function calculateSize(bytes: number, decimals = 1) {
|
||||
@@ -20,55 +22,16 @@ export function bytesToSize(value: number, unit: string) {
|
||||
return value / Math.pow(1024, index);
|
||||
}
|
||||
|
||||
export function humanFileSize(value: number): {
|
||||
export function humanFileSize(bytes: number): {
|
||||
value: string;
|
||||
unit: string;
|
||||
} {
|
||||
if (!value) return { value: 'No', unit: 'Bytes' };
|
||||
const length = value.toString().length;
|
||||
const unit =
|
||||
length < 7
|
||||
? 'kilobyte'
|
||||
: length < 11
|
||||
? 'megabyte'
|
||||
: length < 15
|
||||
? 'gigabyte'
|
||||
: length < 19
|
||||
? 'terabyte'
|
||||
: 'petabyte';
|
||||
|
||||
const formatter = Intl.NumberFormat('en', {
|
||||
style: 'unit',
|
||||
maximumSignificantDigits: 3,
|
||||
maximumFractionDigits: 2,
|
||||
unit
|
||||
});
|
||||
|
||||
while (value > 1000) {
|
||||
value /= 1000;
|
||||
}
|
||||
|
||||
const parts: {
|
||||
integer: string;
|
||||
unit: string;
|
||||
fraction?: string;
|
||||
decimal?: string;
|
||||
} = formatter.formatToParts(value).reduce(
|
||||
(prev, curr) => {
|
||||
prev[curr.type] = curr.value;
|
||||
|
||||
return prev;
|
||||
},
|
||||
{
|
||||
integer: '',
|
||||
unit: '',
|
||||
decimal: '',
|
||||
fraction: ''
|
||||
}
|
||||
);
|
||||
const value = prettyBytes(bytes, {
|
||||
locale: 'en'
|
||||
}).split(' ');
|
||||
|
||||
return {
|
||||
value: `${parts.integer}${parts.decimal}${parts.fraction}`,
|
||||
unit: parts.unit
|
||||
value: value[0],
|
||||
unit: value[1]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Empty, Pagination } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { EmptySearch, Pagination, Trim } from '$lib/components';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -13,19 +12,12 @@
|
||||
import { Container } from '$lib/layout';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { pageLimit } from '$lib/stores/layout';
|
||||
import { onMount } from 'svelte';
|
||||
import { accountActivity } from '../store';
|
||||
import { Query } from '@aw-labs/appwrite-console';
|
||||
import { createPersistentPagination } from '$lib/stores/pagination';
|
||||
import { PAGE_LIMIT } from '$lib/constants';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
|
||||
const offset = createPersistentPagination($pageLimit);
|
||||
|
||||
onMount(async () => {
|
||||
await accountActivity.load([Query.offset($offset), Query.limit($pageLimit)]);
|
||||
});
|
||||
|
||||
$: accountActivity.load([Query.offset($offset), Query.limit($pageLimit)]);
|
||||
export let logs: Models.LogList;
|
||||
export let path: string;
|
||||
export let offset = 0;
|
||||
|
||||
const getBrowser = (clientCode: string) => {
|
||||
return sdkForConsole.avatars.getBrowser(clientCode, 40, 40);
|
||||
@@ -33,21 +25,18 @@
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
{#if $accountActivity?.total}
|
||||
{#if logs.total}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableCellHead>Date</TableCellHead>
|
||||
<TableCellHead>Event</TableCellHead>
|
||||
<TableCellHead>Client</TableCellHead>
|
||||
<TableCellHead>Event</TableCellHead>
|
||||
<TableCellHead>Location</TableCellHead>
|
||||
<TableCellHead>IP</TableCellHead>
|
||||
<TableCellHead>Date</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each $accountActivity.logs as log}
|
||||
{#each logs.logs as log}
|
||||
<TableRow>
|
||||
<TableCellText title="Date">{toLocaleDateTime(log.time)}</TableCellText>
|
||||
<TableCellText title="Event">{log.event}</TableCellText>
|
||||
|
||||
<TableCell title="Client">
|
||||
{#if log.clientName}
|
||||
<div class="u-flex u-cross-center u-gap-12">
|
||||
@@ -58,14 +47,12 @@
|
||||
src={getBrowser(log.clientCode).toString()}
|
||||
alt={log.clientName} />
|
||||
</div>
|
||||
<p
|
||||
class="text u-trim"
|
||||
title={`${log.clientName} ${log.clientVersion} on ${log.osName} ${log.osVersion}`}>
|
||||
<Trim>
|
||||
{log.clientName}
|
||||
{log.clientVersion}
|
||||
on {log.osName}
|
||||
{log.osVersion}
|
||||
</p>
|
||||
</Trim>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="u-flex u-cross-center u-gap-12">
|
||||
@@ -74,6 +61,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</TableCell>
|
||||
<TableCellText title="Event">{log.event}</TableCellText>
|
||||
|
||||
<TableCellText title="Location">
|
||||
{#if log.countryCode !== '--'}
|
||||
@@ -83,28 +71,25 @@
|
||||
{/if}
|
||||
</TableCellText>
|
||||
<TableCellText title="IP">{log.ip}</TableCellText>
|
||||
<TableCellText title="Date">{toLocaleDateTime(log.time)}</TableCellText>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {logs.total}</p>
|
||||
<Pagination limit={PAGE_LIMIT} {path} {offset} sum={logs.total} />
|
||||
</div>
|
||||
{:else}
|
||||
<Empty>
|
||||
<EmptySearch>
|
||||
<div class="u-flex u-flex-vertical u-cross-center">
|
||||
<div class="common-section">
|
||||
<p>No logs available</p>
|
||||
</div>
|
||||
<div class="common-section">
|
||||
<Button
|
||||
external
|
||||
secondary
|
||||
href="https://appwrite.io/docs/server/authentication?sdk=nodejs-default#usersGetLogs"
|
||||
>Documentation</Button>
|
||||
<div class="u-text-center">
|
||||
<p class="text u-line-height-1-5">
|
||||
You have no activity. Once your users start interacting with your app you'll
|
||||
see their activity here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Empty>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {$accountActivity?.total}</p>
|
||||
<Pagination limit={$pageLimit} bind:offset={$offset} sum={$accountActivity?.total} />
|
||||
</div>
|
||||
</Container>
|
||||
@@ -1,53 +1,24 @@
|
||||
<script lang="ts" context="module">
|
||||
export type Breadcrumb = {
|
||||
title: string;
|
||||
href?: string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { breadcrumbs as breadcrumbsStore, level, type Breadcrumb } from '$lib/stores/layout';
|
||||
|
||||
$: breadcrumbs = [...$breadcrumbsStore]
|
||||
.map(([level, value]) => ({ level, value }))
|
||||
.sort((a, b) => (a.level > b.level ? 1 : -1))
|
||||
.filter((n) => n.level <= $level);
|
||||
|
||||
function truncate(
|
||||
breadcrumbs: {
|
||||
value: Breadcrumb;
|
||||
}[]
|
||||
) {
|
||||
/**
|
||||
* Maximum of 4 Breadcrumbs, truncated from the left.
|
||||
*/
|
||||
if ($level > 4) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
value: {
|
||||
href: null,
|
||||
title: '...'
|
||||
}
|
||||
},
|
||||
...breadcrumbs.slice(-3)
|
||||
];
|
||||
}
|
||||
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
function buildHref(index: number) {
|
||||
return breadcrumbs
|
||||
.slice(0, index + 1)
|
||||
.reduce((prev, next) => `${prev}/${next.value.href}`, '');
|
||||
}
|
||||
export let breadcrumbs: Breadcrumb[];
|
||||
</script>
|
||||
|
||||
<nav class="breadcrumbs is-only-desktop" aria-label="breadcrumb">
|
||||
<ol class="breadcrumbs-list">
|
||||
{#each truncate(breadcrumbs) as breadcrumb, i}
|
||||
{#each breadcrumbs as breadcrumb}
|
||||
<li class="breadcrumbs-item">
|
||||
{#if breadcrumb.value.href}
|
||||
<a
|
||||
href={buildHref(breadcrumbs.length > 4 ? i + breadcrumbs.length - 4 : i)}
|
||||
title={breadcrumb.value.title}>
|
||||
{breadcrumb.value.title}
|
||||
{#if breadcrumb.href}
|
||||
<a href={breadcrumb.href} title={breadcrumb.title}>
|
||||
{breadcrumb.title}
|
||||
</a>
|
||||
{:else}
|
||||
<span>{breadcrumb.value.title}</span>
|
||||
<span>{breadcrumb.title}</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<script lang="ts">
|
||||
export let overlapCover = false;
|
||||
export let size: 'small' | 'medium' | 'large' | 'xl' = null;
|
||||
|
||||
$: style = size
|
||||
? `--p-container-max-size: var(--container-max-size, var(--container-size-${size}))`
|
||||
: '';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="container"
|
||||
{style}
|
||||
class:u-margin-block-start-24={!overlapCover}
|
||||
class:u-margin-block-start-negative-56={overlapCover}>
|
||||
<slot />
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<script lang="ts">
|
||||
export let adjustContentToCover = false;
|
||||
|
||||
export let size: 'small' | 'medium' | 'large' | 'xl' = null;
|
||||
|
||||
$: style = size
|
||||
? `--p-container-max-size: var(--container-max-size, var(--container-size-${size}))`
|
||||
: '';
|
||||
</script>
|
||||
|
||||
<div class="top-cover" class:is-adjust-content-to-cover={adjustContentToCover}>
|
||||
<div class="container">
|
||||
<div class="container" {style}>
|
||||
<div class="u-flex u-cross-center u-gap-16 u-margin-block-start-20">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Heading } from '$lib/components';
|
||||
|
||||
export let href: string = null;
|
||||
</script>
|
||||
|
||||
<Heading size="4" tag="h1" trimmed={false}>
|
||||
{#if href}
|
||||
<a class="button is-text is-only-icon" {href} aria-label="page back">
|
||||
<span class="icon-cheveron-left" aria-hidden="true" />
|
||||
</a>
|
||||
{/if}
|
||||
<span class="text u-trim-1">
|
||||
<slot />
|
||||
</span>
|
||||
</Heading>
|
||||
@@ -1,27 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { onMount } from 'svelte';
|
||||
import { Breadcrumbs } from '.';
|
||||
import { AvatarInitials, DropListItem, DropListLink } from '$lib/components';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { organizationList, organization, newOrgModal } from '$lib/stores/organization';
|
||||
import AppwriteLogo from '$lib/images/appwrite-gray-light.svg';
|
||||
import LightMode from '$lib/images/mode/light-mode.svg';
|
||||
import DarkMode from '$lib/images/mode/dark-mode.svg';
|
||||
import SystemMode from '$lib/images/mode/system-mode.svg';
|
||||
import { organizationList, organization, newOrgModal } from '$lib/stores/organization';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
let showDropdown = false;
|
||||
let droplistElement: HTMLDivElement;
|
||||
|
||||
onMount(async () => {
|
||||
await organizationList.load();
|
||||
if (!$organization) {
|
||||
await organization.load($organizationList.teams[0].$id);
|
||||
}
|
||||
});
|
||||
|
||||
const onBlur = (event: MouseEvent) => {
|
||||
if (
|
||||
showDropdown &&
|
||||
@@ -40,7 +32,9 @@
|
||||
<img src={AppwriteLogo} width="132" height="34" alt="Appwrite" />
|
||||
</a>
|
||||
|
||||
<Breadcrumbs />
|
||||
{#if $page.data.breadcrumbs}
|
||||
<svelte:component this={$page.data.breadcrumbs} />
|
||||
{/if}
|
||||
|
||||
<div class="main-header-end">
|
||||
<nav class="u-flex is-only-desktop">
|
||||
@@ -53,7 +47,7 @@
|
||||
<span class="text">Support</span>
|
||||
</a>
|
||||
</nav>
|
||||
<nav class="user-profile">
|
||||
<nav class="u-flex u-height-100-percents u-sep-inline-start">
|
||||
{#if $user}
|
||||
<div class="drop-wrapper" class:is-open={showDropdown} bind:this={droplistElement}>
|
||||
<button class="user-profile-button" on:click={() => (showDropdown = !showDropdown)}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as Container } from './container.svelte';
|
||||
export { default as Cover } from './cover.svelte';
|
||||
export { default as CoverTitle } from './coverTitle.svelte';
|
||||
export { default as Footer } from './footer.svelte';
|
||||
export { default as Header } from './header.svelte';
|
||||
export { default as Navigation } from './navigation.svelte';
|
||||
@@ -12,3 +13,5 @@ export { default as WizardStep } from './wizardStep.svelte';
|
||||
export { default as Breadcrumbs } from './breadcrumbs.svelte';
|
||||
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';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { log } from '$lib/stores/logs';
|
||||
import { Status, Tab, Tabs } from '../components';
|
||||
import { Output, Status, Tab, Tabs } from '../components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { base } from '$app/paths';
|
||||
import { app } from '$lib/stores/app';
|
||||
@@ -35,8 +35,17 @@
|
||||
.getElementsByClassName('code-panel-content')[0]
|
||||
.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
$log.show = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
{#if $log.data}
|
||||
<section class="cover-frame">
|
||||
<header class="cover-frame-header u-flex u-gap-16 u-main-space-between u-cross-center">
|
||||
@@ -59,7 +68,12 @@
|
||||
alt="technology" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="body-text-2">Deployment ID: {$log.data.$id}</h2>
|
||||
<div class="u-flex u-gap-12 u-cross-center">
|
||||
<h2 class="body-text-2">Deployment ID:</h2>
|
||||
<Output value={$log.data.$id}>
|
||||
{$log.data.$id}
|
||||
</Output>
|
||||
</div>
|
||||
<time class="u-block"
|
||||
>Created at: {toLocaleDateTime($log.data.$createdAt)}</time>
|
||||
<div>Size: {size.value} {size.unit}</div>
|
||||
@@ -122,7 +136,12 @@
|
||||
alt="technology" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="body-text-2">Execution ID: {$log.data.$id}</h2>
|
||||
<div class="u-flex u-gap-12 u-cross-center">
|
||||
<h2 class="body-text-2">Execution ID:</h2>
|
||||
<Output value={$log.data.$id}>
|
||||
{$log.data.$id}
|
||||
</Output>
|
||||
</div>
|
||||
<time class="u-block">
|
||||
Created at: {toLocaleDateTime($log.data.$createdAt)}</time>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { event, type AnalyticsActionParam } from '$lib/actions/analytics';
|
||||
|
||||
$: project = $page.params.project;
|
||||
$: path = `${base}/console/project-${project}`;
|
||||
|
||||
const defaultEventOptions: Partial<AnalyticsActionParam> = {
|
||||
category: 'console/navigation',
|
||||
name: 'click_menu_link',
|
||||
action: 'click'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="side-nav">
|
||||
@@ -15,11 +22,24 @@
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(`${path}/overview`)}
|
||||
href={path}>
|
||||
href={path}
|
||||
use:event={{
|
||||
...defaultEventOptions,
|
||||
label: 'Home Link'
|
||||
}}>
|
||||
<span class="icon-chart-bar" aria-hidden="true" />
|
||||
<span class="text">Overview</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(`${path}/auth`)}
|
||||
href={`${path}/auth`}>
|
||||
<span class="icon-user-group" aria-hidden="true" />
|
||||
<span class="text">Auth</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
@@ -29,26 +49,6 @@
|
||||
<span class="text">Databases</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(`${path}/storage`)}
|
||||
href={`${path}/storage`}>
|
||||
<span class="icon-folder" aria-hidden="true" />
|
||||
<span class="text">Storage</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(
|
||||
`${path}/authentication`
|
||||
)}
|
||||
href={`${path}/authentication`}>
|
||||
<span class="icon-user-group" aria-hidden="true" />
|
||||
<span class="text">Authentication</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
@@ -58,6 +58,15 @@
|
||||
<span class="text">Functions</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(`${path}/storage`)}
|
||||
href={`${path}/storage`}>
|
||||
<span class="icon-folder" aria-hidden="true" />
|
||||
<span class="text">Storage</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { navigating } from '$app/stores';
|
||||
|
||||
let width: number;
|
||||
let updater: ReturnType<typeof setTimeout>;
|
||||
let completed = false;
|
||||
|
||||
const minimum = 0.08;
|
||||
const maximum = 0.994;
|
||||
const settleTime = 700;
|
||||
const intervalTime = 800;
|
||||
const stepSizes = [0, 0.005, 0.01, 0.02];
|
||||
|
||||
function animate(): void {
|
||||
if (updater) {
|
||||
clearInterval(updater);
|
||||
}
|
||||
updater = setInterval(() => {
|
||||
const randomStep = stepSizes[Math.floor(Math.random() * stepSizes.length)];
|
||||
const step = getIncrement(width) + randomStep;
|
||||
if (width < maximum) {
|
||||
width = width + step;
|
||||
}
|
||||
if (width > maximum) {
|
||||
width = maximum;
|
||||
stop();
|
||||
}
|
||||
}, intervalTime);
|
||||
}
|
||||
|
||||
function start(): void {
|
||||
width = minimum;
|
||||
animate();
|
||||
}
|
||||
|
||||
function complete(): void {
|
||||
clearInterval(updater);
|
||||
width = 1;
|
||||
setTimeout(() => {
|
||||
completed = true;
|
||||
setTimeout(() => {
|
||||
completed = false;
|
||||
width = 0;
|
||||
}, settleTime);
|
||||
}, settleTime);
|
||||
}
|
||||
|
||||
function getIncrement(number: number): number {
|
||||
if (number >= 0 && number < 0.2) return 0.1;
|
||||
else if (number >= 0.2 && number < 0.5) return 0.04;
|
||||
else if (number >= 0.5 && number < 0.8) return 0.02;
|
||||
else if (number >= 0.8 && number < 0.99) return 0.005;
|
||||
return 0;
|
||||
}
|
||||
|
||||
$: if ($navigating) {
|
||||
start();
|
||||
} else {
|
||||
complete();
|
||||
}
|
||||
|
||||
$: barStyle = (width && width * 100 && `width: ${width * 100}%;`) || '';
|
||||
</script>
|
||||
|
||||
{#if width}
|
||||
<div class="progress-bar" class:progress-bar-hiding={completed} style={barStyle} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.progress-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
transition: width 0.2s ease-in-out;
|
||||
z-index: 5;
|
||||
background: hsl(var(--color-primary-200));
|
||||
}
|
||||
|
||||
.progress-bar-hiding {
|
||||
transition: top 0.8s ease;
|
||||
top: -8px;
|
||||
}
|
||||
</style>
|
||||
+14
-121
@@ -1,57 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { navigating, page } from '$app/stores';
|
||||
import { tabs, title, backButton, copyData, titleDropdown } from '$lib/stores/layout';
|
||||
import { Cover } from '.';
|
||||
import {
|
||||
Copy,
|
||||
DropList,
|
||||
DropListItem,
|
||||
DropListLink,
|
||||
AvatarGroup,
|
||||
Tabs,
|
||||
Tab,
|
||||
Heading
|
||||
} from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
organization,
|
||||
memberList,
|
||||
newOrgModal,
|
||||
newMemberModal
|
||||
} from '$lib/stores/organization';
|
||||
import { base } from '$app/paths';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { log } from '$lib/stores/logs';
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
|
||||
export let isOpen = false;
|
||||
export let showSideNavigation = false;
|
||||
|
||||
let y: number;
|
||||
let showDropdown = false;
|
||||
|
||||
navigating.subscribe(() => {
|
||||
if (isOpen) isOpen = false;
|
||||
});
|
||||
|
||||
let avatars = [];
|
||||
let avatarsTotal = 0;
|
||||
|
||||
memberList.subscribe((value) => {
|
||||
if (value?.total > 0) {
|
||||
avatarsTotal = value.total;
|
||||
avatars = value.memberships.map((team) => team.userName);
|
||||
}
|
||||
});
|
||||
|
||||
const logout = async () => {
|
||||
await user.logout();
|
||||
await goto(`${base}/login`);
|
||||
};
|
||||
|
||||
const toggleMenu = () => {
|
||||
y = 0;
|
||||
isOpen = !isOpen;
|
||||
@@ -63,6 +25,16 @@
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeNavigate((n) => {
|
||||
/**
|
||||
* Hide wizard when navigation is triggered by popstate.
|
||||
*/
|
||||
if (n.type === 'popstate' && $wizard.show) {
|
||||
wizard.hide();
|
||||
n.cancel();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window bind:scrollY={y} />
|
||||
@@ -86,89 +58,10 @@
|
||||
<slot name="side" />
|
||||
</nav>
|
||||
<section class="main-content">
|
||||
<Cover>
|
||||
<svelte:fragment slot="header">
|
||||
{#if $backButton}
|
||||
<a class="back-button" href={$backButton} aria-label="page back">
|
||||
<span class="icon-cheveron-left" aria-hidden="true" />
|
||||
</a>
|
||||
{/if}
|
||||
{#if $titleDropdown?.length && $organization}
|
||||
<DropList bind:show={showDropdown} scrollable={true} noArrow>
|
||||
<button
|
||||
class="button is-text u-padding-inline-0"
|
||||
on:click={() => (showDropdown = !showDropdown)}>
|
||||
<Heading tag="h1" size="4">
|
||||
<span class="text"> {$organization.name}</span>
|
||||
{#if $page.data?.header}
|
||||
<svelte:component this={$page.data.header} />
|
||||
{/if}
|
||||
|
||||
<span
|
||||
class={`icon-cheveron-${showDropdown ? 'up' : 'down'}`}
|
||||
aria-hidden="true" />
|
||||
</Heading>
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
{#each $titleDropdown as org}
|
||||
<DropListLink
|
||||
href={`${base}/console/organization-${org.$id}`}
|
||||
on:click={() => {
|
||||
showDropdown = false;
|
||||
}}>
|
||||
{org.name}
|
||||
</DropListLink>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="other">
|
||||
<section class="drop-section">
|
||||
<ul class="drop-list">
|
||||
<DropListItem
|
||||
icon="plus"
|
||||
on:click={() => {
|
||||
showDropdown = false;
|
||||
newOrgModal.set(true);
|
||||
}}>New Organization</DropListItem>
|
||||
</ul>
|
||||
</section></svelte:fragment>
|
||||
</DropList>
|
||||
<div class="u-margin-inline-start-auto">
|
||||
<div class="u-flex u-gap-16">
|
||||
<AvatarGroup size={40} {avatars} total={avatarsTotal} />
|
||||
<Button secondary on:click={() => newMemberModal.set(true)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Invite</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Heading tag="h1" size="4">
|
||||
<span class="text">{$title ? $title : '-'}</span>
|
||||
</Heading>
|
||||
{/if}
|
||||
{#if $page.url.pathname.includes('/console/account')}
|
||||
<div class="u-margin-inline-start-auto">
|
||||
<Button secondary on:click={logout}>Logout</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $copyData?.value}
|
||||
<Copy value={$copyData.value}>
|
||||
<Pill button>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
{$copyData.text}
|
||||
</Pill>
|
||||
</Copy>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
{#if $tabs.length}
|
||||
<Tabs>
|
||||
{#each $tabs as tab}
|
||||
<Tab href={tab.href} selected={$page.url.pathname === tab.href}>
|
||||
{tab.title}
|
||||
</Tab>
|
||||
{/each}
|
||||
</Tabs>
|
||||
{/if}
|
||||
</Cover>
|
||||
<slot />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
|
||||
<div class="u-margin-block-start-auto" />
|
||||
<p class="u-max-width-500 u-width-full-line u-margin-block-start-24">
|
||||
version 0.15.2.402
|
||||
<!-- version 0.15.2.402 -->
|
||||
</p>
|
||||
<div class="u-margin-block-start-20" />
|
||||
</div>
|
||||
|
||||
+61
-48
@@ -13,14 +13,16 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import { BarChart, LineChart } from '$lib/charts';
|
||||
import { Card, DropTabs, DropTabsItem, Heading, Tiles } from '$lib/components';
|
||||
import { Card, DropTabs, DropTabsLink, Heading, Tiles } from '$lib/components';
|
||||
import { Colors } from '$lib/charts/config';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
type MetricMetadata = {
|
||||
title: string;
|
||||
legend: string;
|
||||
};
|
||||
|
||||
export let title: string;
|
||||
export let count: Models.Metric[];
|
||||
export let created: Models.Metric[];
|
||||
@@ -33,23 +35,24 @@
|
||||
export let readMetadata: MetricMetadata;
|
||||
export let updatedMetadata: MetricMetadata;
|
||||
export let deletedMetadata: MetricMetadata;
|
||||
|
||||
export let range: UsagePeriods;
|
||||
export let path: string = null;
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<div class="u-flex u-main-space-between common-section">
|
||||
<Heading tag="h2" size="5">{title}</Heading>
|
||||
<DropTabs>
|
||||
<DropTabsItem on:click={() => (range = '24h')} disabled={range === '24h'}>
|
||||
<DropTabsLink href={`${path}/24h`} disabled={$page.params.period === '24h'}>
|
||||
24h
|
||||
</DropTabsItem>
|
||||
<DropTabsItem on:click={() => (range = '30d')} disabled={range === '30d'}>
|
||||
</DropTabsLink>
|
||||
<DropTabsLink
|
||||
href={`${path}/30d`}
|
||||
disabled={!$page.params.period || $page.params.period === '30d'}>
|
||||
30d
|
||||
</DropTabsItem>
|
||||
<DropTabsItem on:click={() => (range = '90d')} disabled={range === '90d'}>
|
||||
</DropTabsLink>
|
||||
<DropTabsLink href={`${path}/90d`} disabled={$page.params.period === '90d'}>
|
||||
90d
|
||||
</DropTabsItem>
|
||||
</DropTabsLink>
|
||||
</DropTabs>
|
||||
</div>
|
||||
<Card>
|
||||
@@ -57,13 +60,15 @@
|
||||
<Heading tag="h6" size="6">{last(count).value}</Heading>
|
||||
<p>{countMetadata.title}</p>
|
||||
<div class="u-margin-block-start-16" />
|
||||
<BarChart
|
||||
series={[
|
||||
{
|
||||
name: countMetadata.legend,
|
||||
data: [...count.map((e) => [e.date, e.value])]
|
||||
}
|
||||
]} />
|
||||
<div style="height: 12rem;">
|
||||
<BarChart
|
||||
series={[
|
||||
{
|
||||
name: countMetadata.legend,
|
||||
data: [...count.map((e) => [e.date, e.value])]
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
<Tiles>
|
||||
@@ -72,14 +77,16 @@
|
||||
<Heading tag="h6" size="6">{total(created)}</Heading>
|
||||
<p>{createdMetadata.title}</p>
|
||||
<div class="u-margin-block-start-16" />
|
||||
<LineChart
|
||||
series={[
|
||||
{
|
||||
name: createdMetadata.legend,
|
||||
data: [...created.map((e) => [e.date, e.value])],
|
||||
color: Colors.Secondary
|
||||
}
|
||||
]} />
|
||||
<div style="height: 12rem;">
|
||||
<LineChart
|
||||
series={[
|
||||
{
|
||||
name: createdMetadata.legend,
|
||||
data: [...created.map((e) => [e.date, e.value])],
|
||||
color: Colors.Secondary
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
<Card isTile>
|
||||
@@ -87,14 +94,16 @@
|
||||
<Heading tag="h6" size="6">{total(read)}</Heading>
|
||||
<p>{readMetadata.title}</p>
|
||||
<div class="u-margin-block-start-16" />
|
||||
<LineChart
|
||||
series={[
|
||||
{
|
||||
name: readMetadata.legend,
|
||||
data: [...read.map((e) => [e.date, e.value])],
|
||||
color: Colors.Tertiary
|
||||
}
|
||||
]} />
|
||||
<div style="height: 12rem;">
|
||||
<LineChart
|
||||
series={[
|
||||
{
|
||||
name: readMetadata.legend,
|
||||
data: [...read.map((e) => [e.date, e.value])],
|
||||
color: Colors.Tertiary
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
<Card isTile>
|
||||
@@ -102,14 +111,16 @@
|
||||
<Heading tag="h6" size="6">{total(updated)}</Heading>
|
||||
<p>{updatedMetadata.title}</p>
|
||||
<div class="u-margin-block-start-16" />
|
||||
<LineChart
|
||||
series={[
|
||||
{
|
||||
name: updatedMetadata.legend,
|
||||
data: [...updated.map((e) => [e.date, e.value])],
|
||||
color: Colors.Quaternary
|
||||
}
|
||||
]} />
|
||||
<div style="height: 12rem;">
|
||||
<LineChart
|
||||
series={[
|
||||
{
|
||||
name: updatedMetadata.legend,
|
||||
data: [...updated.map((e) => [e.date, e.value])],
|
||||
color: Colors.Quaternary
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
<Card isTile>
|
||||
@@ -117,14 +128,16 @@
|
||||
<Heading tag="h6" size="6">{total(deleted)}</Heading>
|
||||
<p>{deletedMetadata.title}</p>
|
||||
<div class="u-margin-block-start-16" />
|
||||
<LineChart
|
||||
series={[
|
||||
{
|
||||
name: deletedMetadata.legend,
|
||||
data: [...deleted.map((e) => [e.date, e.value])],
|
||||
color: Colors.Quinary
|
||||
}
|
||||
]} />
|
||||
<div style="height: 12rem;">
|
||||
<LineChart
|
||||
series={[
|
||||
{
|
||||
name: deletedMetadata.legend,
|
||||
data: [...deleted.map((e) => [e.date, e.value])],
|
||||
color: Colors.Quinary
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
</Tiles>
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
let showExitModal = false;
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && !showExitModal) {
|
||||
const openDialog = document.querySelectorAll('dialog[open]');
|
||||
if (event.key === 'Escape' && !showExitModal && !openDialog?.length) {
|
||||
event.preventDefault();
|
||||
dispatch('exit');
|
||||
wizard.hide();
|
||||
@@ -102,7 +103,9 @@
|
||||
{currentStep} />
|
||||
</aside>
|
||||
<div class="wizard-media">
|
||||
<slot name="media" />
|
||||
{#if $wizard.media}
|
||||
<img src={$wizard.media} alt="wizard media" loading="lazy" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="wizard-main">
|
||||
<Form noStyle on:submit={submit}>
|
||||
@@ -114,11 +117,12 @@
|
||||
<div class="form-footer">
|
||||
<div class="u-flex u-main-end u-gap-12">
|
||||
{#if !isLastStep && sortedSteps[currentStep - 1][1].optional}
|
||||
<Button text on:click={() => dispatch('finish')}
|
||||
>Skip optional steps</Button>
|
||||
<Button text on:click={() => dispatch('finish')}>
|
||||
Skip optional steps
|
||||
</Button>
|
||||
{/if}
|
||||
{#if currentStep === 1}
|
||||
<Button secondary on:click={wizard.hide}>Cancel</Button>
|
||||
<Button secondary on:click={handleExit}>Cancel</Button>
|
||||
<Button submit>Next</Button>
|
||||
{:else if isLastStep}
|
||||
<Button secondary on:click={() => currentStep--}>Back</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let show = false;
|
||||
@@ -11,16 +11,14 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form noMargin on:submit={handleSubmit}>
|
||||
<Modal bind:show 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
|
||||
irreversible.
|
||||
</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button secondary submit>Exit</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Form>
|
||||
<Modal bind:show on:submit={handleSubmit} 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
|
||||
irreversible.
|
||||
</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button secondary submit>Exit</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { Copy } from '../components';
|
||||
|
||||
export let value = false;
|
||||
export let value: string;
|
||||
</script>
|
||||
|
||||
<Copy {value}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { Modal } from '../components';
|
||||
|
||||
export let show = false;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { project } from '../../routes/console/project-[project]/store';
|
||||
import { get, writable, readable } from 'svelte/store';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import type { Navigation } from '@sveltejs/kit';
|
||||
import { writable, readable } from 'svelte/store';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
|
||||
export type Tab = {
|
||||
href: string;
|
||||
@@ -11,68 +9,20 @@ export type Tab = {
|
||||
export type Breadcrumb = {
|
||||
href: string;
|
||||
title: string;
|
||||
level?: number;
|
||||
};
|
||||
|
||||
export type updateLayoutArguments = {
|
||||
title: string;
|
||||
titleDropdown?: Models.Team[];
|
||||
level?: number;
|
||||
tabs?: Tab[];
|
||||
back?: string;
|
||||
breadcrumbs?: Breadcrumb[] | Breadcrumb;
|
||||
copy?: {
|
||||
text: string;
|
||||
value: string;
|
||||
};
|
||||
customBase?: string;
|
||||
navigate?: Navigation;
|
||||
header?: typeof SvelteComponent;
|
||||
breadcrumb?: typeof SvelteComponent;
|
||||
};
|
||||
|
||||
export const level = writable<number>();
|
||||
export const title = writable<string>('');
|
||||
export const titleDropdown = writable<Models.Team[]>([]);
|
||||
export const backButton = writable<string>('');
|
||||
export const tabs = writable<Tab[]>([]);
|
||||
export const breadcrumbs = writable<Map<number, Breadcrumb>>(new Map());
|
||||
export const copyData = writable({
|
||||
text: '',
|
||||
value: ''
|
||||
});
|
||||
export const header = writable<typeof SvelteComponent>();
|
||||
export const breadcrumb = writable<typeof SvelteComponent>();
|
||||
|
||||
export function updateLayout(args: updateLayoutArguments) {
|
||||
const projectId = get(project)?.$id;
|
||||
const base = args?.customBase ?? projectId ? `/console/project-${projectId}` : ``;
|
||||
|
||||
if (args?.tabs) {
|
||||
args.tabs.map((tab) => {
|
||||
tab.href = `${base}/${tab.href}`;
|
||||
});
|
||||
}
|
||||
|
||||
if (args?.navigate?.to) {
|
||||
const previousTabs = get(tabs);
|
||||
if (previousTabs.some((t) => `${base}/${t.href}` === args.navigate.to.url.pathname)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
title.set(args.title);
|
||||
titleDropdown.set(args.titleDropdown ?? []);
|
||||
backButton.set(args.back ?? null);
|
||||
copyData.set(args.copy ?? null);
|
||||
level.set(args.level ?? null);
|
||||
tabs.set(args.tabs ?? []);
|
||||
|
||||
if (args.breadcrumbs) {
|
||||
if (!Array.isArray(args.breadcrumbs)) {
|
||||
args.breadcrumbs = [args.breadcrumbs];
|
||||
}
|
||||
for (const breadcrumb of args.breadcrumbs) {
|
||||
breadcrumbs.update((n) => n.set(breadcrumb.level ?? args.level, breadcrumb));
|
||||
}
|
||||
}
|
||||
header.set(args.header ?? null);
|
||||
breadcrumb.set(args.breadcrumb ?? null);
|
||||
}
|
||||
|
||||
export const pageLimit = readable(12); // default page limit
|
||||
export const cardLimit = readable(6); // default page limit
|
||||
export const cardLimit = readable(6); // default card limit
|
||||
|
||||
+121
-282
@@ -1,18 +1,14 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
import Apple from '../../routes/console/project-[project]/authentication/_appleOAuth.svelte';
|
||||
import Microsoft from '../../routes/console/project-[project]/authentication/_microsoftOAuth.svelte';
|
||||
import Okta from '../../routes/console/project-[project]/authentication/_oktaOAuth.svelte';
|
||||
import Auth0 from '../../routes/console/project-[project]/authentication/_auth0OAuth.svelte';
|
||||
import Main from '../../routes/console/project-[project]/authentication/_mainOAuth.svelte';
|
||||
import Apple from '../../routes/console/project-[project]/auth/_appleOAuth.svelte';
|
||||
import Microsoft from '../../routes/console/project-[project]/auth/_microsoftOAuth.svelte';
|
||||
import Okta from '../../routes/console/project-[project]/auth/_oktaOAuth.svelte';
|
||||
import Auth0 from '../../routes/console/project-[project]/auth/_auth0OAuth.svelte';
|
||||
import Main from '../../routes/console/project-[project]/auth/_mainOAuth.svelte';
|
||||
|
||||
export type Provider = {
|
||||
name: string;
|
||||
export type Provider = Models.Provider & {
|
||||
icon: string;
|
||||
active: boolean;
|
||||
id: string | null;
|
||||
secret: string | null;
|
||||
docs?: string;
|
||||
component?: typeof SvelteComponent;
|
||||
};
|
||||
@@ -22,279 +18,122 @@ export type Providers = {
|
||||
};
|
||||
|
||||
const setProviders = (project: Models.Project): Provider[] => {
|
||||
return [
|
||||
{
|
||||
name: 'Amazon',
|
||||
icon: 'amazon',
|
||||
active: false,
|
||||
id: project?.providerAmazonAppid,
|
||||
secret: project?.providerAmazonSecret,
|
||||
docs: 'https://developer.amazon.com/apps-and-games/services-and-apis',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Apple',
|
||||
icon: 'apple',
|
||||
active: false,
|
||||
id: project?.providerAppleAppid,
|
||||
secret: project?.providerAppleSecret,
|
||||
docs: 'https://developer.apple.com/',
|
||||
component: Apple
|
||||
},
|
||||
{
|
||||
name: 'Auth0',
|
||||
icon: 'auth0',
|
||||
active: false,
|
||||
id: project?.providerAuth0Appid,
|
||||
secret: project?.providerAuth0Secret,
|
||||
docs: 'https://auth0.com/developers',
|
||||
component: Auth0
|
||||
},
|
||||
{
|
||||
name: 'Bitbucket',
|
||||
icon: 'bitBucket',
|
||||
active: false,
|
||||
id: project?.providerBitbucketAppid,
|
||||
secret: project?.providerBitbucketSecret,
|
||||
docs: 'https://developer.atlassian.com/bitbucket',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Bitly',
|
||||
icon: 'bitly',
|
||||
active: false,
|
||||
id: project?.providerBitlyAppid,
|
||||
secret: project?.providerBitlySecret,
|
||||
docs: 'https://dev.bitly.com/v4_documentation.html',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Box',
|
||||
icon: 'box',
|
||||
active: false,
|
||||
id: project?.providerBoxAppid,
|
||||
secret: project?.providerBoxSecret,
|
||||
docs: 'https://developer.box.com/reference/',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Discord',
|
||||
icon: 'discord',
|
||||
active: false,
|
||||
id: project?.providerDiscordAppid,
|
||||
secret: project?.providerDiscordSecret,
|
||||
docs: 'https://discordapp.com/developers/docs/topics/oauth2',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Dropbox',
|
||||
icon: 'dropbox',
|
||||
active: false,
|
||||
id: project?.providerDropboxAppid,
|
||||
secret: project?.providerDropboxSecret,
|
||||
docs: 'https://www.dropbox.com/developers/documentation',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Facebook',
|
||||
icon: 'facebook',
|
||||
active: false,
|
||||
id: project?.providerFacebookAppid,
|
||||
secret: project?.providerFacebookSecret,
|
||||
docs: 'https://developers.facebook.com/',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Github',
|
||||
icon: 'github',
|
||||
active: false,
|
||||
id: project?.providerGithubAppid,
|
||||
secret: project?.providerGithubSecret,
|
||||
docs: 'https://developer.github.com/',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Gitlab',
|
||||
icon: 'gitlab',
|
||||
active: false,
|
||||
id: project?.providerGitlabAppid,
|
||||
secret: project?.providerGitlabSecret,
|
||||
docs: 'https://docs.gitlab.com/ee/api/',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Google',
|
||||
icon: 'google',
|
||||
active: false,
|
||||
id: project?.providerGoogleAppid,
|
||||
secret: project?.providerGoogleSecret,
|
||||
docs: 'https://support.google.com/googleapi/answer/6158849',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Linkedin',
|
||||
icon: 'linkedin',
|
||||
active: false,
|
||||
id: project?.providerLinkedinAppid,
|
||||
secret: project?.providerLinkedinSecret,
|
||||
docs: 'https://developer.linkedin.com/',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Microsoft',
|
||||
icon: 'microsoft',
|
||||
active: false,
|
||||
id: project?.providerMicrosoftAppid,
|
||||
secret: project?.providerMicrosoftSecret,
|
||||
docs: 'https://developer.microsoft.com/en-us/',
|
||||
component: Microsoft
|
||||
},
|
||||
{
|
||||
name: 'Notion',
|
||||
icon: 'notion',
|
||||
active: false,
|
||||
id: project?.providerNotionAppid,
|
||||
secret: project?.providerNotionSecret,
|
||||
docs: 'https://developers.notion.com/docs',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Okta',
|
||||
icon: 'okta',
|
||||
active: false,
|
||||
id: project?.providerOktaAppid,
|
||||
secret: project?.providerOktaSecret,
|
||||
docs: 'https://developer.okta.com/',
|
||||
component: Okta
|
||||
},
|
||||
{
|
||||
name: 'Paypal',
|
||||
icon: 'paypal',
|
||||
active: false,
|
||||
id: project?.providerPaypalAppid,
|
||||
secret: project?.providerPaypalSecret,
|
||||
docs: 'https://developer.paypal.com/docs/api/overview/',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Paypal (sandbox)',
|
||||
icon: 'paypal',
|
||||
active: false,
|
||||
id: project?.providerPaypalSandboxAppid,
|
||||
secret: project?.providerPaypalSandboxSecret,
|
||||
docs: 'https://developer.paypal.com/docs/api/overview/',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Salesforce',
|
||||
icon: 'salesforce',
|
||||
active: false,
|
||||
id: project?.providerSalesforceAppid,
|
||||
secret: project?.providerSalesforceSecret,
|
||||
docs: 'https://developer.salesforce.com/docs/',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Slack',
|
||||
icon: 'slack',
|
||||
active: false,
|
||||
id: project?.providerSlackAppid,
|
||||
secret: project?.providerSlackSecret,
|
||||
docs: 'https://api.slack.com/',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Spotify',
|
||||
icon: 'spotify',
|
||||
active: false,
|
||||
id: project?.providerSpotifyAppid,
|
||||
secret: project?.providerSpotifySecret,
|
||||
docs: 'https://developer.spotify.com/documentation/general/guides/authorization-guide/',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Stripe',
|
||||
icon: 'stripe',
|
||||
active: false,
|
||||
id: project?.providerStripeAppid,
|
||||
secret: project?.providerStripeSecret,
|
||||
docs: 'https://stripe.com/docs/api',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Tradeshift',
|
||||
icon: 'tradeshift',
|
||||
active: false,
|
||||
id: project?.providerTradeshiftAppid,
|
||||
secret: project?.providerTradeshiftSecret,
|
||||
docs: 'https://developers.tradeshift.com/docs/api',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Tradeshift(sandbox)',
|
||||
icon: 'tradeshift',
|
||||
active: false,
|
||||
id: project?.providerTradeshiftBoxAppid,
|
||||
secret: project?.providerTradeshiftBoxSecret,
|
||||
docs: 'https://developers.tradeshift.com/docs/api',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Twitch',
|
||||
icon: 'twitch',
|
||||
active: false,
|
||||
id: project?.providerTwitchAppid,
|
||||
secret: project?.providerTwitchSecret,
|
||||
docs: 'https://dev.twitch.tv/docs/authentication',
|
||||
component: Main
|
||||
},
|
||||
return (
|
||||
project?.providers.map((n) => {
|
||||
let docs: Provider['docs'];
|
||||
let icon: Provider['icon'] = n.name.toLowerCase();
|
||||
let component: Provider['component'] = Main;
|
||||
|
||||
{
|
||||
name: 'Wordpress',
|
||||
icon: 'wordpress',
|
||||
active: false,
|
||||
id: project?.providerWordpressAppid,
|
||||
secret: project?.providerWordpressSecret,
|
||||
docs: 'https://developer.wordpress.com/docs/oauth2/',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Yahoo',
|
||||
icon: 'yahoo',
|
||||
active: false,
|
||||
id: project?.providerYahooAppid,
|
||||
secret: project?.providerYahooSecret,
|
||||
docs: 'https://developer.yahoo.com/oauth2/guide/flows_authcode/',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Yammer',
|
||||
icon: 'ms_yammer',
|
||||
active: false,
|
||||
id: project?.providerYammerAppid,
|
||||
secret: project?.providerYammerSecret,
|
||||
docs: 'https://developer.yammer.com/docs/oauth-2',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Yandex',
|
||||
icon: 'yandex',
|
||||
active: false,
|
||||
id: project?.providerYandexAppid,
|
||||
secret: project?.providerYandexSecret,
|
||||
docs: 'https://tech.yandex.com/oauth/',
|
||||
component: Main
|
||||
},
|
||||
{
|
||||
name: 'Zoom',
|
||||
icon: 'zoom',
|
||||
active: false,
|
||||
id: project?.providerZoomAppid,
|
||||
secret: project?.providerZoomSecret,
|
||||
docs: 'https://marketplace.zoom.us/docs/guides/auth/oauth/',
|
||||
component: Main
|
||||
}
|
||||
];
|
||||
switch (n.name.toLowerCase()) {
|
||||
case 'amazon':
|
||||
docs = 'https://developer.amazon.com/apps-and-games/services-and-apis';
|
||||
break;
|
||||
case 'apple':
|
||||
docs = 'https://developer.apple.com/';
|
||||
component = Apple;
|
||||
break;
|
||||
case 'auth0':
|
||||
docs = 'https://auth0.com/developers';
|
||||
component = Auth0;
|
||||
break;
|
||||
case 'bitbucket':
|
||||
docs = 'https://developer.atlassian.com/bitbucket';
|
||||
break;
|
||||
case 'bitly':
|
||||
docs = 'https://dev.bitly.com/v4_documentation.html';
|
||||
break;
|
||||
case 'box':
|
||||
docs = 'https://developer.box.com/reference/';
|
||||
break;
|
||||
case 'discord':
|
||||
docs = 'https://discordapp.com/developers/docs/topics/oauth2';
|
||||
break;
|
||||
case 'dropbox':
|
||||
docs = 'https://www.dropbox.com/developers/documentation';
|
||||
break;
|
||||
case 'facebook':
|
||||
docs = 'https://developers.facebook.com/';
|
||||
break;
|
||||
case 'github':
|
||||
docs = 'https://developer.github.com';
|
||||
break;
|
||||
case 'gitlab':
|
||||
docs = 'https://docs.gitlab.com/ee/api/';
|
||||
break;
|
||||
case 'google':
|
||||
docs = 'https://support.google.com/googleapi/answer/6158849';
|
||||
break;
|
||||
case 'linkedin':
|
||||
docs = 'https://developer.linkedin.com/';
|
||||
break;
|
||||
case 'microsoft':
|
||||
docs = 'https://developer.microsoft.com/en-us/';
|
||||
component = Microsoft;
|
||||
break;
|
||||
case 'notion':
|
||||
docs = 'https://developers.notion.com/docs';
|
||||
break;
|
||||
case 'okta':
|
||||
docs = 'https://developer.okta.com';
|
||||
component = Okta;
|
||||
break;
|
||||
case 'paypal':
|
||||
docs = 'https://developer.paypal.com/docs/api/overview/';
|
||||
break;
|
||||
case 'paypalsandbox':
|
||||
icon = 'paypal';
|
||||
docs = 'https://developer.paypal.com/docs/api/overview/';
|
||||
break;
|
||||
case 'salesforce':
|
||||
docs = 'https://developer.salesforce.com/docs/';
|
||||
break;
|
||||
case 'slack':
|
||||
docs = 'https://api.slack.com/';
|
||||
break;
|
||||
case 'spotify':
|
||||
docs =
|
||||
'https://developer.spotify.com/documentation/general/guides/authorization-guide/';
|
||||
break;
|
||||
case 'stripe':
|
||||
docs = 'https://stripe.com/docs/api';
|
||||
break;
|
||||
case 'tradeshift':
|
||||
docs = 'https://developers.tradeshift.com/docs/api';
|
||||
break;
|
||||
case 'tradeshiftbox':
|
||||
icon = 'tradeshift';
|
||||
docs = 'https://developers.tradeshift.com/docs/api';
|
||||
break;
|
||||
case 'twitch':
|
||||
docs = 'https://dev.twitch.tv/docs/auth';
|
||||
break;
|
||||
case 'wordpress':
|
||||
docs = 'https://developer.wordpress.com/docs/oauth2/';
|
||||
break;
|
||||
case 'yahoo':
|
||||
docs = 'https://developer.yahoo.com/oauth2/guide/flows_authcode/';
|
||||
break;
|
||||
case 'yammer':
|
||||
docs = 'https://developer.yammer.com/docs/oauth-2';
|
||||
break;
|
||||
case 'yandex':
|
||||
docs = 'https://tech.yandex.com/oauth/';
|
||||
break;
|
||||
case 'zoom':
|
||||
docs = 'https://marketplace.zoom.us/docs/guides/auth/oauth/';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
...n,
|
||||
icon,
|
||||
docs,
|
||||
component
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
};
|
||||
|
||||
function createOAuthProviders() {
|
||||
|
||||
@@ -1,84 +1,12 @@
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { page } from '$app/stores';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import { writable } from 'svelte/store';
|
||||
import { get } from 'svelte/store';
|
||||
import { base } from '$app/paths';
|
||||
import { goto } from '$app/navigation';
|
||||
import { cachedStore } from '$lib/helpers/cache';
|
||||
|
||||
export const newOrgModal = writable<boolean>(false);
|
||||
export const newMemberModal = writable<boolean>(false);
|
||||
|
||||
export const organizationList = cachedStore<
|
||||
Models.TeamList,
|
||||
{
|
||||
load: () => Promise<void>;
|
||||
}
|
||||
>('organizationList', function ({ set }) {
|
||||
return {
|
||||
load: async () => {
|
||||
const response = await sdkForConsole.teams.list();
|
||||
set(response);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const organization = cachedStore<
|
||||
Models.Team,
|
||||
{
|
||||
load: (teamId: string) => Promise<void>;
|
||||
}
|
||||
>('organization', function ({ set }) {
|
||||
return {
|
||||
load: async (teamId) => {
|
||||
const response = await sdkForConsole.teams.get(teamId);
|
||||
set(response);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const projectList = cachedStore<
|
||||
Models.ProjectList,
|
||||
{
|
||||
load: (queries: string[], search?: string) => Promise<void>;
|
||||
}
|
||||
>('projectList', function ({ set }) {
|
||||
return {
|
||||
load: async (queries, search) => {
|
||||
const response = await sdkForConsole.projects.list(queries, search);
|
||||
set(response);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const memberList = cachedStore<
|
||||
Models.MembershipList,
|
||||
{
|
||||
load: (teamId: string, queries?: string[], search?: string) => Promise<void>;
|
||||
}
|
||||
>('memberList', function ({ set }) {
|
||||
return {
|
||||
load: async (teamId, queries, search) => {
|
||||
const response = await sdkForConsole.teams.listMemberships(teamId, queries, search);
|
||||
set(response);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const redirectTo = async () => {
|
||||
let org = get(organization);
|
||||
if (org) {
|
||||
await goto(`${base}/console/organization-${org.$id}`);
|
||||
} else {
|
||||
await organizationList.load();
|
||||
const orgList = get(organizationList);
|
||||
if (orgList?.total) {
|
||||
await organization.load(orgList.teams[0].$id);
|
||||
org = get(organization);
|
||||
await goto(`${base}/console/organization-${org.$id}`);
|
||||
} else {
|
||||
newOrgModal.set(true);
|
||||
await goto(`${base}/console`);
|
||||
}
|
||||
}
|
||||
};
|
||||
export const organizationList = derived(
|
||||
page,
|
||||
($page) => $page.data.organizations as Models.TeamList
|
||||
);
|
||||
export const organization = derived(page, ($page) => $page.data.organization as Models.Team);
|
||||
export const members = derived(page, ($page) => $page.data.members as Models.MembershipList);
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
Users
|
||||
} from '@aw-labs/appwrite-console';
|
||||
|
||||
const endpoint = import.meta.env.VITE_APPWRITE_ENDPOINT.toString();
|
||||
const endpoint =
|
||||
import.meta.env.VITE_APPWRITE_ENDPOINT?.toString() ?? `${globalThis?.location?.origin}/v1`;
|
||||
const clientConsole = new Client();
|
||||
clientConsole.setEndpoint(endpoint).setProject('console');
|
||||
|
||||
|
||||
+6
-22
@@ -1,24 +1,8 @@
|
||||
import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import { writable } from 'svelte/store';
|
||||
import { sdkForConsole } from './sdk';
|
||||
|
||||
function createUserStore() {
|
||||
const { subscribe, set } = writable<Models.Account<Record<string, unknown>>>();
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
fetchUser: async () => {
|
||||
set(await sdkForConsole.account.get());
|
||||
},
|
||||
logout: async () => {
|
||||
await sdkForConsole.account.deleteSession('current');
|
||||
/**
|
||||
* Clear cache.
|
||||
*/
|
||||
globalThis.sessionStorage.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const user = createUserStore();
|
||||
export const user = derived(
|
||||
page,
|
||||
($page) => $page.data.account as Models.Account<Record<string, string>>
|
||||
);
|
||||
|
||||
@@ -3,23 +3,27 @@ import { writable } from 'svelte/store';
|
||||
|
||||
export type WizardStore = {
|
||||
show: boolean;
|
||||
media?: string;
|
||||
component?: typeof SvelteComponent;
|
||||
interceptor?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function createWizardStore() {
|
||||
const { subscribe, update } = writable<WizardStore>({
|
||||
const { subscribe, update, set } = writable<WizardStore>({
|
||||
show: false,
|
||||
component: null,
|
||||
interceptor: null
|
||||
interceptor: null,
|
||||
media: null
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
start: (component: typeof SvelteComponent) =>
|
||||
set,
|
||||
start: (component: typeof SvelteComponent, media: string = null) =>
|
||||
update((n) => {
|
||||
n.show = true;
|
||||
n.component = component;
|
||||
n.media = media;
|
||||
|
||||
return n;
|
||||
}),
|
||||
@@ -34,6 +38,7 @@ function createWizardStore() {
|
||||
update((n) => {
|
||||
n.show = false;
|
||||
n.component = null;
|
||||
n.media = null;
|
||||
|
||||
return n;
|
||||
})
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
import '@aw-labs/ui/src/_index.scss';
|
||||
import 'tippy.js/dist/tippy.css';
|
||||
+21
-26
@@ -2,25 +2,21 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { redirectTo } from '$lib/stores/organization';
|
||||
import { onCLS, onFID, onLCP, onFCP, onINP, onTTFB } from 'web-vitals';
|
||||
import { reportWebVitals } from '$lib/helpers/vitals';
|
||||
import { onMount } from 'svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { browser, dev } from '$app/environment';
|
||||
import { app } from '$lib/stores/app';
|
||||
import Notifications from '$lib/layout/notifications.svelte';
|
||||
import Loading from './_loading.svelte';
|
||||
|
||||
let loaded = false;
|
||||
import { Progress, Notifications } from '$lib/layout';
|
||||
import { loading } from './store';
|
||||
import Loading from './loading.svelte';
|
||||
|
||||
if (browser) {
|
||||
window.VERCEL_ANALYTICS_ID = import.meta.env.VERCEL_ANALYTICS_ID?.toString() ?? false;
|
||||
window.GOOGLE_ANALYTICS = import.meta.env.VITE_GOOGLE_ANALYTICS?.toString() ?? false;
|
||||
}
|
||||
|
||||
const acceptedRoutes = ['/login', '/register', '/recover', '/invite'];
|
||||
|
||||
onMount(async () => {
|
||||
/**
|
||||
* Reporting Web Vitals.
|
||||
@@ -34,28 +30,25 @@
|
||||
onTTFB(reportWebVitals);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$user) {
|
||||
await user.fetchUser();
|
||||
} else {
|
||||
user.fetchUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle initial load.
|
||||
*/
|
||||
const acceptedRoutes = ['/login', '/register', '/recover', '/invite'];
|
||||
if ($user) {
|
||||
if (!$page.url.pathname.startsWith('/console')) {
|
||||
await redirectTo();
|
||||
await goto(`${base}/console`, {
|
||||
replaceState: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
} else {
|
||||
if (acceptedRoutes.includes($page.url.pathname)) {
|
||||
await goto(`${base}${$page.url.pathname}${$page.url.search}`);
|
||||
} else {
|
||||
/**
|
||||
* Clear cache.
|
||||
*/
|
||||
window.sessionStorage.clear();
|
||||
await goto(`${base}/login`);
|
||||
await goto(`${base}/login`, {
|
||||
replaceState: true
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
loaded = true;
|
||||
loading.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -96,12 +89,14 @@
|
||||
|
||||
<Notifications />
|
||||
|
||||
{#if loaded}
|
||||
<slot />
|
||||
{:else}
|
||||
<slot />
|
||||
|
||||
{#if $loading}
|
||||
<Loading />
|
||||
{/if}
|
||||
|
||||
<Progress />
|
||||
|
||||
<style lang="scss" global>
|
||||
.tippy-box {
|
||||
--p-tooltip-text-color: var(--color-neutral-10);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import '@aw-labs/ui/src/_index.scss';
|
||||
import 'tippy.js/dist/tippy.css';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const ssr = false;
|
||||
|
||||
export const load: LayoutLoad = async ({ depends, url }) => {
|
||||
depends(Dependencies.ACCOUNT);
|
||||
try {
|
||||
const account = await sdkForConsole.account.get();
|
||||
|
||||
return {
|
||||
account,
|
||||
organizations: sdkForConsole.teams.list()
|
||||
};
|
||||
} catch (error) {
|
||||
const acceptedRoutes = ['/login', '/register', '/recover', '/invite'];
|
||||
|
||||
if (!acceptedRoutes.includes(url.pathname)) {
|
||||
throw redirect(303, '/login');
|
||||
}
|
||||
}
|
||||
};
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { base } from '$app/paths';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = async () => {
|
||||
export const load: PageLoad = async () => {
|
||||
throw redirect(302, `${base}/login`);
|
||||
};
|
||||
|
||||
@@ -2,36 +2,18 @@
|
||||
import Shell from '$lib/layout/shell.svelte';
|
||||
import SideNavigation from '$lib/layout/navigation.svelte';
|
||||
import Header from '$lib/layout/header.svelte';
|
||||
import { updateLayout } from '$lib/stores/layout';
|
||||
import {
|
||||
organization,
|
||||
organizationList,
|
||||
newOrgModal,
|
||||
redirectTo
|
||||
} from '$lib/stores/organization';
|
||||
import Create from './_createOrganization.svelte';
|
||||
import { newOrgModal } from '$lib/stores/organization';
|
||||
import Create from './createOrganization.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { Logs } from '$lib/layout';
|
||||
import { log } from '$lib/stores/logs';
|
||||
import { onMount } from 'svelte';
|
||||
import { loading } from '../store';
|
||||
|
||||
updateLayout({
|
||||
title: $organization?.name ?? '',
|
||||
level: 0
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
if ($page.url.pathname === '/console' && !$newOrgModal) {
|
||||
await redirectTo();
|
||||
}
|
||||
});
|
||||
|
||||
afterNavigate(async () => {
|
||||
if ($page.url.pathname === '/console' && !$newOrgModal) {
|
||||
await redirectTo();
|
||||
}
|
||||
onMount(() => {
|
||||
loading.set(false);
|
||||
});
|
||||
|
||||
beforeNavigate(() => {
|
||||
@@ -44,14 +26,11 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Console - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<Shell
|
||||
showSideNavigation={$page.url.pathname !== '/console' &&
|
||||
!$page?.params.organization &&
|
||||
!$page.url.pathname.includes('/console/account')}>
|
||||
!$page.url.pathname.includes('/console/account') &&
|
||||
!$page.url.pathname.includes('/console/onboarding')}>
|
||||
<svelte:fragment slot="header">
|
||||
<Header />
|
||||
</svelte:fragment>
|
||||
@@ -66,9 +45,7 @@
|
||||
<svelte:component this={$wizard.component} />
|
||||
{/if}
|
||||
|
||||
{#if $newOrgModal}
|
||||
<Create bind:show={$newOrgModal} closable={!!$organizationList?.total} />
|
||||
{/if}
|
||||
<Create bind:show={$newOrgModal} />
|
||||
|
||||
{#if $log.show}
|
||||
<Logs />
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { base } from '$app/paths';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { organizations } = await parent();
|
||||
if (organizations.total) {
|
||||
const teamId = localStorage.getItem('organization') ?? organizations.teams[0].$id;
|
||||
throw redirect(303, `${base}/console/organization-${teamId}`);
|
||||
} else {
|
||||
throw redirect(303, `${base}/console/onboarding`);
|
||||
}
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Modal, CustomId } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { InputText, Button, Form, FormList } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { organization, organizationList } from '$lib/stores/organization';
|
||||
import { titleDropdown } from '$lib/stores/layout';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let show = false;
|
||||
export let closable = true;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let name: string;
|
||||
let id: string;
|
||||
let showCustomId = false;
|
||||
let error: string;
|
||||
|
||||
const create = async () => {
|
||||
try {
|
||||
const team = await sdkForConsole.teams.create(id ?? 'unique()', name);
|
||||
dispatch('created');
|
||||
await organizationList.load();
|
||||
titleDropdown.set($organizationList.teams);
|
||||
organization.set(team);
|
||||
await goto(`/console/organization-${$organization.$id}`);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${name} has been created`
|
||||
});
|
||||
name = null;
|
||||
id = null;
|
||||
show = false;
|
||||
} catch ({ message }) {
|
||||
error = message;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Form on:submit={create}>
|
||||
<Modal {error} size="big" bind:show {closable}>
|
||||
<svelte:fragment slot="header">Create New Organization</svelte:fragment>
|
||||
<FormList>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Enter name"
|
||||
bind:value={name}
|
||||
autofocus={true}
|
||||
required />
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}>
|
||||
<span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Organization ID
|
||||
</span>
|
||||
</Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId bind:show={showCustomId} name="organization" bind:id />
|
||||
{/if}
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button disabled={!closable} secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Form>
|
||||
@@ -1,46 +1,3 @@
|
||||
<script lang="ts">
|
||||
import { updateLayout } from '$lib/stores/layout';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const path = 'console/account';
|
||||
|
||||
onMount(handle);
|
||||
afterNavigate(handle);
|
||||
|
||||
async function handle(event = null) {
|
||||
updateLayout({
|
||||
navigate: event,
|
||||
title: $user?.name,
|
||||
level: 0,
|
||||
customBase: '',
|
||||
breadcrumbs: {
|
||||
title: `${$user.name}`,
|
||||
href: path
|
||||
},
|
||||
tabs: [
|
||||
{
|
||||
href: path,
|
||||
title: 'Overview'
|
||||
},
|
||||
{
|
||||
href: `${path}/sessions`,
|
||||
title: 'Sessions'
|
||||
},
|
||||
{
|
||||
href: `${path}/activity`,
|
||||
title: 'Activity'
|
||||
},
|
||||
{
|
||||
href: `${path}/organizations`,
|
||||
title: 'Organizations'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>User - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import Header from './header.svelte';
|
||||
import Breadcrumbs from './breadcrumbs.svelte';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
return {
|
||||
header: Header,
|
||||
breadcrumbs: Breadcrumbs
|
||||
};
|
||||
};
|
||||
@@ -6,9 +6,10 @@
|
||||
import { user } from '$lib/stores/user';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { title, breadcrumbs } from '$lib/stores/layout';
|
||||
import { base } from '$app/paths';
|
||||
import Delete from './_delete.svelte';
|
||||
import Delete from './delete.svelte';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
let name: string = null,
|
||||
email: string = null,
|
||||
@@ -25,11 +26,7 @@
|
||||
async function updateName() {
|
||||
try {
|
||||
await sdkForConsole.account.updateName(name);
|
||||
$user.name = name;
|
||||
title.set(name);
|
||||
const breadcrumb = $breadcrumbs.get(0);
|
||||
breadcrumb.title = name;
|
||||
$breadcrumbs = $breadcrumbs.set($breadcrumbs.size, breadcrumb);
|
||||
invalidate(Dependencies.ACCOUNT);
|
||||
addNotification({
|
||||
message: 'Name has been updated',
|
||||
type: 'success'
|
||||
@@ -44,7 +41,7 @@
|
||||
async function updateEmail() {
|
||||
try {
|
||||
await sdkForConsole.account.updateEmail(email, emailPassword);
|
||||
$user.email = email;
|
||||
invalidate(Dependencies.ACCOUNT);
|
||||
addNotification({
|
||||
message: 'Email has been updated',
|
||||
type: 'success'
|
||||
@@ -158,7 +155,7 @@
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
<CardGrid>
|
||||
<CardGrid danger>
|
||||
<div>
|
||||
<Heading tag="h6" size="7">Delete Account</Heading>
|
||||
</div>
|
||||
@@ -172,7 +169,7 @@
|
||||
<AvatarInitials size={48} name={$user.name} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">
|
||||
<h6 class="u-bold">{$user.name}</h6>
|
||||
<h6 class="u-bold u-trim-1">{$user.name}</h6>
|
||||
</svelte:fragment>
|
||||
</Box>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Activity } from '$lib/layout';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
const path = '/console/account/activity';
|
||||
</script>
|
||||
|
||||
<Activity {path} logs={data.logs} offset={data.offset} />
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Query } from '@aw-labs/appwrite-console';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { pageToOffset } from '$lib/helpers/load';
|
||||
import { PAGE_LIMIT } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
const page = Number(params.page);
|
||||
const offset = pageToOffset(page, PAGE_LIMIT);
|
||||
|
||||
return {
|
||||
offset,
|
||||
logs: await sdkForConsole.account.listLogs([Query.offset(offset), Query.limit(PAGE_LIMIT)])
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Breadcrumbs } from '$lib/layout';
|
||||
import { user } from '$lib/stores/user';
|
||||
|
||||
$: breadcrumbs = [
|
||||
{
|
||||
href: `/console/account`,
|
||||
title: $user?.name
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<Breadcrumbs {breadcrumbs} />
|
||||
+9
-11
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { user } from '$lib/stores/user';
|
||||
@@ -24,13 +24,11 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<Form on:submit={deleteAccount}>
|
||||
<Modal bind:show={showDelete} warning>
|
||||
<svelte:fragment slot="header">Delete Account</svelte:fragment>
|
||||
<p>Are you sure you want to delete your account?</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
|
||||
<Button secondary submit>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Form>
|
||||
<Modal bind:show={showDelete} on:submit={deleteAccount} warning>
|
||||
<svelte:fragment slot="header">Delete Account</svelte:fragment>
|
||||
<p>Are you sure you want to delete your account?</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
|
||||
<Button secondary submit>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Tab, Tabs } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { isTabSelected } from '$lib/helpers/load';
|
||||
import { Cover, CoverTitle } from '$lib/layout';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { user } from '$lib/stores/user';
|
||||
|
||||
const path = `/console/account`;
|
||||
|
||||
$: tabs = [
|
||||
{
|
||||
href: path,
|
||||
title: 'Overview'
|
||||
},
|
||||
{
|
||||
href: `${path}/sessions`,
|
||||
title: 'Sessions'
|
||||
},
|
||||
{
|
||||
href: `${path}/activity`,
|
||||
title: 'Activity',
|
||||
hasChildren: true
|
||||
},
|
||||
{
|
||||
href: `${path}/organizations`,
|
||||
title: 'Organizations',
|
||||
hasChildren: true
|
||||
}
|
||||
];
|
||||
|
||||
async function logout() {
|
||||
await sdkForConsole.account.deleteSession('current');
|
||||
await goto(`${base}/login`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Cover>
|
||||
<svelte:fragment slot="header">
|
||||
<CoverTitle>
|
||||
{$user.name}
|
||||
</CoverTitle>
|
||||
<div class="u-margin-inline-start-auto">
|
||||
<Button secondary on:click={logout}>Logout</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<Tabs>
|
||||
{#each tabs as tab}
|
||||
<Tab href={tab.href} selected={isTabSelected(tab, $page.url.pathname, path, tabs)}>
|
||||
{tab.title}
|
||||
</Tab>
|
||||
{/each}
|
||||
</Tabs>
|
||||
</Cover>
|
||||
+19
-19
@@ -10,16 +10,12 @@
|
||||
} from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Container } from '$lib/layout';
|
||||
import CreateOrganization from '../../_createOrganization.svelte';
|
||||
import { organizationList } from '$lib/stores/organization';
|
||||
import { onMount } from 'svelte';
|
||||
import CreateOrganization from '../../../createOrganization.svelte';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { cardLimit } from '$lib/stores/layout';
|
||||
import { createPersistentPagination } from '$lib/stores/pagination';
|
||||
import { CARD_LIMIT } from '$lib/constants';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
onMount(async () => {
|
||||
await organizationList.load();
|
||||
});
|
||||
export let data: PageData;
|
||||
|
||||
const getMemberships = async (teamId: string) => {
|
||||
const memberships = await sdkForConsole.teams.listMemberships(teamId);
|
||||
@@ -27,7 +23,6 @@
|
||||
};
|
||||
|
||||
let addOrganization = false;
|
||||
const offset = createPersistentPagination($cardLimit);
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -43,17 +38,18 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if $organizationList?.teams?.length}
|
||||
{#if data.organizations.teams.length}
|
||||
<CardContainer
|
||||
total={$organizationList.total}
|
||||
offset={$offset}
|
||||
total={data.organizations.total}
|
||||
offset={data.offset}
|
||||
on:click={() => (addOrganization = true)}>
|
||||
{#each $organizationList.teams as organization, index}
|
||||
{#each data.organizations.teams as organization, index}
|
||||
{@const avatarList = getMemberships(organization.$id)}
|
||||
{#if index >= $offset && index < $cardLimit + $offset}
|
||||
{#if index >= data.offset && index < CARD_LIMIT + data.offset}
|
||||
<GridItem1 href={`${base}/console/organization-${organization.$id}`}>
|
||||
<svelte:fragment slot="eyebrow"
|
||||
>{organization?.total ? organization?.total : 'No'} projects</svelte:fragment>
|
||||
<svelte:fragment slot="eyebrow">
|
||||
{organization?.total ? organization?.total : 'No'} projects
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">
|
||||
{organization.name}
|
||||
</svelte:fragment>
|
||||
@@ -70,13 +66,17 @@
|
||||
</svelte:fragment>
|
||||
</CardContainer>
|
||||
{:else}
|
||||
<Empty isButton single on:click={() => (addOrganization = true)}>
|
||||
<Empty single on:click={() => (addOrganization = true)}>
|
||||
<p>Create a new organization</p>
|
||||
</Empty>
|
||||
{/if}
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {$organizationList?.total}</p>
|
||||
<Pagination limit={$cardLimit} bind:offset={$offset} sum={$organizationList?.total} />
|
||||
<p class="text">Total results: {data.organizations.total}</p>
|
||||
<Pagination
|
||||
limit={CARD_LIMIT}
|
||||
path="console/account/organizations"
|
||||
offset={data.offset}
|
||||
sum={data.organizations.total} />
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Query } from '@aw-labs/appwrite-console';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { pageToOffset } from '$lib/helpers/load';
|
||||
import { PAGE_LIMIT } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
const page = Number(params.page);
|
||||
const offset = pageToOffset(page, PAGE_LIMIT);
|
||||
|
||||
return {
|
||||
offset,
|
||||
organizations: await sdkForConsole.teams.list([
|
||||
Query.offset(offset),
|
||||
Query.limit(PAGE_LIMIT)
|
||||
])
|
||||
};
|
||||
};
|
||||
+13
-18
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Empty, Heading, Pagination } from '$lib/components';
|
||||
import { Empty, Heading } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Pill } from '$lib/elements';
|
||||
import {
|
||||
@@ -13,17 +13,12 @@
|
||||
} from '$lib/elements/table';
|
||||
import { Container } from '$lib/layout';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { pageLimit } from '$lib/stores/layout';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import { accountSession } from '../store';
|
||||
import { onMount } from 'svelte';
|
||||
import { createPersistentPagination } from '$lib/stores/pagination';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
const offset = createPersistentPagination($pageLimit);
|
||||
|
||||
onMount(async () => {
|
||||
await accountSession.load();
|
||||
});
|
||||
export let data: PageData;
|
||||
|
||||
const getBrowser = (clientCode: string) => {
|
||||
return sdkForConsole.avatars.getBrowser(clientCode, 40, 40);
|
||||
@@ -31,11 +26,11 @@
|
||||
|
||||
const logout = async (session: Models.Session) => {
|
||||
await sdkForConsole.account.deleteSession(session.$id);
|
||||
await accountSession.load();
|
||||
invalidate(Dependencies.ACCOUNT_SESSIONS);
|
||||
};
|
||||
const logoutAll = async () => {
|
||||
await sdkForConsole.account.deleteSessions();
|
||||
await accountSession.load();
|
||||
invalidate(Dependencies.ACCOUNT_SESSIONS);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -48,7 +43,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if $accountSession?.total}
|
||||
{#if data.sessions.total}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableCellHead>Client</TableCellHead>
|
||||
@@ -57,10 +52,10 @@
|
||||
<TableCellHead width={100} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each $accountSession.sessions as session}
|
||||
{#each data.sessions.sessions as session}
|
||||
<TableRow>
|
||||
<TableCell title="Client">
|
||||
<div class="u-flex u-gap-12">
|
||||
<div class="u-flex u-gap-12 u-cross-center">
|
||||
<div class="u-flex u-cross-center u-gap-12">
|
||||
{#if session.clientName}
|
||||
<div class="avatar is-small">
|
||||
@@ -113,7 +108,7 @@
|
||||
<Button
|
||||
external
|
||||
secondary
|
||||
href="https://appwrite.io/docs/server/authentication?sdk=nodejs-default#usersGetsessions">
|
||||
href="https://appwrite.io/docs/server/auth?sdk=nodejs-default#usersGetsessions">
|
||||
Documentation
|
||||
</Button>
|
||||
</div>
|
||||
@@ -121,7 +116,7 @@
|
||||
</Empty>
|
||||
{/if}
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {$accountSession?.total}</p>
|
||||
<Pagination limit={$pageLimit} bind:offset={$offset} sum={$accountSession?.total} />
|
||||
<p class="text">Total results: {data.sessions.total}</p>
|
||||
<!-- <Pagination limit={PAGE_LIMIT} offset={data.offset} sum={data.sessions.total} /> -->
|
||||
</div>
|
||||
</Container>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ depends }) => {
|
||||
depends(Dependencies.ACCOUNT_SESSIONS);
|
||||
|
||||
return {
|
||||
sessions: await sdkForConsole.account.listSessions()
|
||||
};
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { cachedStore } from '$lib/helpers/cache';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
|
||||
export const accountSession = cachedStore<
|
||||
Models.SessionList,
|
||||
{
|
||||
load: () => Promise<void>;
|
||||
}
|
||||
>('accountSession', function ({ set }) {
|
||||
return {
|
||||
load: async () => {
|
||||
const response = await sdkForConsole.account.listSessions();
|
||||
set(response);
|
||||
}
|
||||
};
|
||||
});
|
||||
export const accountActivity = cachedStore<
|
||||
Models.LogList,
|
||||
{
|
||||
load: (queries: string[]) => Promise<void>;
|
||||
}
|
||||
>('accountActivity', function ({ set }) {
|
||||
return {
|
||||
load: async (queries) => {
|
||||
const response = await sdkForConsole.account.listLogs(queries);
|
||||
set(response);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import { Modal, CustomId } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { InputText, Button, FormList } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export let show = false;
|
||||
|
||||
let name: string;
|
||||
let id: string;
|
||||
let showCustomId = false;
|
||||
let error: string;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const create = async () => {
|
||||
try {
|
||||
const org = await sdkForConsole.teams.create(id ?? 'unique()', name);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
dispatch('created');
|
||||
await goto(`/console/organization-${org.$id}`);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${name} has been created`
|
||||
});
|
||||
name = null;
|
||||
id = null;
|
||||
show = false;
|
||||
} catch ({ message }) {
|
||||
error = message;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal {error} on:submit={create} size="big" bind:show>
|
||||
<svelte:fragment slot="header">Create New Organization</svelte:fragment>
|
||||
<FormList>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Enter name"
|
||||
bind:value={name}
|
||||
autofocus={true}
|
||||
required />
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}>
|
||||
<span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Organization ID
|
||||
</span>
|
||||
</Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId bind:show={showCustomId} name="Organization" bind:id />
|
||||
{/if}
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,5 @@
|
||||
<svelte:head>
|
||||
<title>Onboarding - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<slot />
|
||||
@@ -0,0 +1,8 @@
|
||||
import Header from './header.svelte';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
return {
|
||||
header: Header
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { Card } from '$lib/components';
|
||||
import CustomId from '$lib/components/customId.svelte';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button, Form, InputText } from '$lib/elements/forms';
|
||||
import FormList from '$lib/elements/forms/formList.svelte';
|
||||
import { Container } from '$lib/layout';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { ID } from '@aw-labs/appwrite-console';
|
||||
|
||||
let name: string;
|
||||
let id: string;
|
||||
let showCustomId = false;
|
||||
let loading = false;
|
||||
async function createProject() {
|
||||
try {
|
||||
loading = true;
|
||||
const org = await createOrganization();
|
||||
const project = await sdkForConsole.projects.create(
|
||||
id ?? ID.unique(),
|
||||
name,
|
||||
org.$id,
|
||||
'default'
|
||||
);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
goto(`/console/project-${project.$id}`);
|
||||
} catch ({ message }) {
|
||||
addNotification({
|
||||
message,
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createOrganization() {
|
||||
return await sdkForConsole.teams.create(ID.unique(), 'Personal Projects');
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container overlapCover size="large">
|
||||
<Card>
|
||||
<Form on:submit={createProject}>
|
||||
<FormList>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Project name"
|
||||
placeholder="First Appwrite Project"
|
||||
disabled={loading}
|
||||
bind:value={name} />
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}>
|
||||
<span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Project ID
|
||||
</span>
|
||||
</Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId bind:show={showCustomId} name="Project ID" bind:id />
|
||||
{/if}
|
||||
<Button fullWidth submit disabled={loading}>Create project</Button>
|
||||
</FormList>
|
||||
</Form>
|
||||
</Card>
|
||||
</Container>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Cover, CoverTitle } from '$lib/layout';
|
||||
</script>
|
||||
|
||||
<Cover size="large">
|
||||
<CoverTitle>Let's create your first project</CoverTitle>
|
||||
</Cover>
|
||||
@@ -1,70 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { pageLimit, updateLayout } from '$lib/stores/layout';
|
||||
import { onMount } from 'svelte';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import {
|
||||
organizationList,
|
||||
organization,
|
||||
memberList,
|
||||
newOrgModal,
|
||||
newMemberModal
|
||||
} from '$lib/stores/organization';
|
||||
import CreateMember from './_createMember.svelte';
|
||||
import Create from '../_createOrganization.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { Query } from '@aw-labs/appwrite-console';
|
||||
|
||||
$: organizationId = $page.params.organization;
|
||||
$: path = `console/organization-${organizationId}`;
|
||||
|
||||
onMount(handle);
|
||||
afterNavigate(handle);
|
||||
|
||||
async function handle(event = null) {
|
||||
const promises = Promise.all([
|
||||
organization.load(organizationId),
|
||||
memberList.load(organizationId, [Query.limit($pageLimit)])
|
||||
]);
|
||||
|
||||
if ($organization?.$id !== organizationId) {
|
||||
await promises;
|
||||
}
|
||||
|
||||
updateLayout({
|
||||
navigate: event,
|
||||
title: $organization?.name,
|
||||
titleDropdown: $organizationList.teams,
|
||||
level: 0,
|
||||
customBase: '',
|
||||
breadcrumbs: {
|
||||
title: `${$organization.name}`,
|
||||
href: path
|
||||
},
|
||||
tabs: [
|
||||
{
|
||||
href: path,
|
||||
title: 'Projects'
|
||||
},
|
||||
{
|
||||
href: `${path}/members`,
|
||||
title: 'Members'
|
||||
},
|
||||
{
|
||||
href: `${path}/settings`,
|
||||
title: 'Settings'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
import { newOrgModal, newMemberModal } from '$lib/stores/organization';
|
||||
import CreateMember from './createMember.svelte';
|
||||
import Create from '../createOrganization.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Organizations - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if $organization}
|
||||
<slot />
|
||||
{/if}
|
||||
<slot />
|
||||
|
||||
<Create bind:show={$newOrgModal} />
|
||||
<CreateMember bind:showCreate={$newMemberModal} />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user