Merge pull request #496 from appwrite/1.4.x

1.4.x
This commit is contained in:
Torsten Dittmann
2023-08-28 18:56:34 +02:00
committed by GitHub
463 changed files with 25295 additions and 4351 deletions
+3
View File
@@ -24,5 +24,8 @@ module.exports = {
browser: true,
es2017: true,
node: true
},
globals: {
globalThis: false // false means it is not writeable
}
};
+2
View File
@@ -13,3 +13,5 @@ node_modules
node_modules/
dist/
.vercel
*.swp
+8192 -1715
View File
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -19,16 +19,21 @@
},
"dependencies": {
"@analytics/google-analytics": "^1.0.5",
"@appwrite.io/pink": "^0.0.6-rc.14",
"@appwrite.io/console": "0.2.0",
"@analytics/google-tag-manager": "^0.5.3",
"@appwrite.io/console": "npm:matej-appwrite-console@7.1.130",
"@appwrite.io/pink": "0.1.0-next.8",
"@appwrite.io/pink-icons": "^0.1.0-next.8",
"@popperjs/core": "^2.11.6",
"@sentry/svelte": "^7.44.2",
"@sentry/tracing": "^7.44.2",
"ai": "^2.1.15",
"analytics": "^0.8.1",
"dayjs": "^1.11.9",
"deep-equal": "^2.2.2",
"dotenv": "^16.0.3",
"echarts": "^5.4.1",
"logrocket": "^3.0.1",
"nanoid": "^4.0.2",
"pretty-bytes": "^6.1.0",
"prismjs": "^1.29.0",
"svelte-confetti": "^1.2.2",
@@ -44,6 +49,7 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/svelte": "^3.2.2",
"@testing-library/user-event": "^14.4.3",
"@types/deep-equal": "^1.0.1",
"@types/prismjs": "^1.26.0",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
+1 -1
View File
@@ -47,7 +47,7 @@
%sveltekit.head%
</head>
<body>
<body data-sveltekit-preload-data="hover">
<div id="svelte">%sveltekit.body%</div>
</body>
</html>
+22 -1
View File
@@ -142,6 +142,7 @@ export enum Submit {
ProjectUpdateName = 'submit_project_update_name',
ProjectUpdateTeam = 'submit_project_update_team',
ProjectService = 'submit_project_service',
ProjectUpdateSMTP = 'submit_project_update_smtp',
MemberCreate = 'submit_member_create',
MemberDelete = 'submit_member_delete',
MembershipUpdateStatus = 'submit_membership_update_status',
@@ -153,6 +154,7 @@ export enum Submit {
AuthStatusUpdate = 'submit_auth_status_update',
AuthPasswordHistoryUpdate = 'submit_auth_password_history_limit_update',
AuthPasswordDictionaryUpdate = 'submit_auth_password_dictionary_update',
AuthPersonalDataCheckUpdate = 'submit_auth_personal_data_check_update',
SessionsLengthUpdate = 'submit_sessions_length_update',
SessionsLimitUpdate = 'submit_sessions_limit_update',
SessionDelete = 'submit_session_delete',
@@ -181,8 +183,13 @@ export enum Submit {
FunctionUpdateName = 'submit_function_update_name',
FunctionUpdatePermissions = 'submit_function_update_permissions',
FunctionUpdateSchedule = 'submit_function_update_schedule',
FunctionUpdateConfiguration = 'submit_function_update_configuration',
FunctionUpdateLogging = 'submit_function_update_logging',
FunctionUpdateTimeout = 'submit_function_update_timeout',
FunctionUpdateEvents = 'submit_function_update_events',
FunctionConnectRepo = 'submit_function_disconnect_repo',
FunctionDisconnectRepo = 'submit_function_disconnect_repo',
FunctionRedeploy = 'submit_function_redeploy',
DeploymentCreate = 'submit_deployment_create',
DeploymentDelete = 'submit_deployment_delete',
DeploymentUpdate = 'submit_deployment_update',
@@ -190,6 +197,7 @@ export enum Submit {
VariableCreate = 'submit_variable_create',
VariableDelete = 'submit_variable_delete',
VariableUpdate = 'submit_variable_update',
VariableEditor = 'submit_variable_editor',
KeyCreate = 'submit_key_create',
KeyDelete = 'submit_key_delete',
KeyUpdateName = 'submit_key_update_name',
@@ -220,5 +228,18 @@ export enum Submit {
BucketUpdateExtensions = 'submit_bucket_update_extensions',
FileCreate = 'submit_file_create',
FileDelete = 'submit_file_delete',
FileUpdatePermissions = 'submit_file_update_permissions'
FileUpdatePermissions = 'submit_file_update_permissions',
InstallationCreate = 'submit_installation_create',
InstallationDelete = 'submit_installation_delete',
EmailChangeLocale = 'submit_email_change_locale',
EmailResetTemplate = 'submit_email_reset_template',
EmailUpdateInviteTemplate = 'submit_email_update_invite_template',
EmailUpdateMagicUrlTemplate = 'submit_email_update_magic_url_template',
EmailUpdateRecoveryTemplate = 'submit_email_update_recovery_template',
EmailUpdateVerificationTemplate = 'submit_email_update_verification_template',
SmsChangeLocale = 'submit_sms_change_locale',
SmsResetTemplate = 'submit_sms_reset_template',
SmsUpdateInviteTemplate = 'submit_sms_update_invite_template',
SmsUpdateLoginTemplate = 'submit_sms_update_login_template',
SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template'
}
+20
View File
@@ -0,0 +1,20 @@
import type { Action } from 'svelte/action';
export type MultiActionArray = Array<(node: HTMLElement) => ReturnType<Action>>;
export function multiAction(node: HTMLElement, arr: MultiActionArray) {
const destroyFns = arr.map((fn) => {
const actionReturn = fn(node);
return actionReturn
? actionReturn.destroy
: () => {
/* noop */
};
});
return {
destroy() {
destroyFns.forEach((fn) => fn());
}
};
}
+42
View File
@@ -0,0 +1,42 @@
import { tick } from 'svelte';
import type { Action } from 'svelte/action';
export type PortalConfig = string | HTMLElement | undefined;
export const portal: Action<HTMLElement, PortalConfig> = (el, target = 'body') => {
let targetEl;
async function update(newTarget: PortalConfig) {
target = newTarget;
if (typeof target === 'string') {
targetEl = document.querySelector(target);
if (targetEl === null) {
await tick();
targetEl = document.querySelector(target);
}
if (targetEl === null) {
throw new Error(`No element found matching css selector: "${target}"`);
}
} else if (target instanceof HTMLElement) {
targetEl = target;
} else {
throw new TypeError(
`Unknown portal target type: ${
target === null ? 'null' : typeof target
}. Allowed types: string (CSS selector) or HTMLElement.`
);
}
targetEl.appendChild(el);
el.hidden = false;
}
function destroy() {
el.remove();
}
update(target);
return {
update,
destroy
};
};
+158
View File
@@ -0,0 +1,158 @@
<script lang="ts" context="module">
type Context = Readable<{
isInitialPanel: boolean;
open: boolean;
}>;
type ReadableValue<T> = T extends Readable<infer U> ? U : never;
const contextKey = 'command-center';
export const getCommandCenterCtx = () => getContext<Context>(contextKey);
const setCommandCenterCtx = (value: ReadableValue<Context>) => {
const store = writable(value);
setContext(contextKey, store);
return store;
};
export const toggleCommandCenter = () => {
if (get(subPanels).length > 0) {
clearSubPanels();
} else {
addSubPanel(RootPanel);
}
};
</script>
<script lang="ts">
import { dev } from '$app/environment';
import { portal } from '$lib/actions/portal';
import { last } from '$lib/helpers/array';
import { debounce } from '$lib/helpers/debounce';
import { getContext, setContext } from 'svelte';
import { get, writable, type Readable } from 'svelte/store';
import { fade } from 'svelte/transition';
import { commandCenterKeyDownHandler, disableCommands, registerCommands } from './commands';
import { RootPanel } from './panels';
import { addSubPanel, clearSubPanels, subPanels } from './subPanels';
import { addNotification } from '$lib/stores/notifications';
let debugOverlayEnabled = false;
$: $registerCommands([
{
callback: toggleCommandCenter,
keys: ['k'],
ctrl: true,
forceEnable: true
},
{
label: 'Toggle debug overlay',
callback: () => {
debugOverlayEnabled = !debugOverlayEnabled;
addNotification({
title: 'Debug overlay',
message: debugOverlayEnabled ? 'Enabled' : 'Disabled',
type: 'info'
});
},
keys: ['d', 'o'],
group: 'misc',
disabled: !dev
}
]);
$: openSubPanel = last($subPanels) ?? null;
$: $disableCommands(!!openSubPanel);
$: if (openSubPanel) {
document.documentElement.classList.add('u-overflow-hidden');
} else {
document.documentElement.classList.remove('u-overflow-hidden');
}
let dialog: HTMLDivElement;
function handleBlur(event: MouseEvent) {
if (event.target === dialog) {
clearSubPanels();
}
}
const ctx = setCommandCenterCtx({
isInitialPanel: true,
open: false
});
$: if (!openSubPanel) {
$ctx.isInitialPanel = true;
}
$: $subPanels.length > 1 && ($ctx.isInitialPanel = false);
$: $ctx.open = !!openSubPanel;
let keys: string[] = [];
const resetKeys = debounce(() => {
keys = [];
}, 1000);
function isInputEvent(event: KeyboardEvent) {
return ['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName);
}
const handleKeydown = (e) => {
if (!$subPanels.length) {
if (isInputEvent(e)) return;
keys = [...keys, e.key].slice(-10);
resetKeys();
}
$commandCenterKeyDownHandler(e);
};
</script>
<svelte:window on:mousedown={handleBlur} on:keydown={handleKeydown} />
{#if openSubPanel}
<div class="dialog" bind:this={dialog} transition:fade={{ duration: 100 }}>
<svelte:component this={openSubPanel.component} />
</div>
{/if}
{#if dev && debugOverlayEnabled}
<div class="debug-keys" use:portal>
{#each keys as key, i (i)}
<kbd class="kbd" transition:fade|local={{ duration: 150 }}>
{key.length === 1 ? key.toUpperCase() : key}
</kbd>
{/each}
</div>
{/if}
<style lang="scss">
.dialog {
padding: 0.5rem;
position: fixed;
inset: 0;
background-color: hsl(var(--color-neutral-500) / 0.5);
z-index: 9999;
}
.debug-keys {
position: fixed;
bottom: 10%;
left: 50%;
transform: translateX(-50%);
padding: 0.5rem;
z-index: 9999;
display: flex;
gap: 1rem;
font-size: 2rem;
.kbd {
padding-inline: 0.5rem;
padding-block: 1.5rem;
}
}
</style>
+354
View File
@@ -0,0 +1,354 @@
import { debounce } from '$lib/helpers/debounce';
import { isMac } from '$lib/helpers/platform';
import { wizard } from '$lib/stores/wizard';
import { onMount } from 'svelte';
import { derived, writable } from 'svelte/store';
import { nanoid } from 'nanoid/non-secure';
import { trackEvent } from '$lib/actions/analytics';
const groups = [
'ungrouped',
'navigation',
'projects',
'organizations',
'auth',
'help',
'account',
'platforms',
'databases',
'functions',
'storage',
'domains',
'webhooks',
'integrations',
'migrations',
'users',
'collections',
'attributes',
'indexes',
'documents',
'teams',
'security',
'buckets',
'files',
'misc',
'settings'
] as const;
export type CommandGroup = (typeof groups)[number];
type BaseCommand = {
callback: () => void;
label?: string;
disabled?: boolean;
forceEnable?: boolean;
group?: CommandGroup;
icon?: string;
rank?: number;
nested?: boolean;
keepOpen?: boolean;
};
type KeyedCommand = BaseCommand & {
keys: string[];
/* Ctrl on Windows/Linux, Meta on Mac */
ctrl?: boolean;
shift?: boolean;
/* Alt on Windows/Linux, Option on Mac */
alt?: boolean;
};
export function isKeyedCommand(command: Command): command is KeyedCommand {
return 'keys' in command && Array.isArray((command as KeyedCommand).keys);
}
export type Command = KeyedCommand | BaseCommand;
export const commandMap = writable<Map<string, Command[]>>(new Map());
export const disabledMap = writable<Map<string, boolean>>(new Map());
// Derived stores
export const commands = derived(commandMap, ($commandMap) => {
return Array.from($commandMap.values()).flat();
});
const commandsEnabled = derived(disabledMap, ($disabledMap) => {
// If there's an item on the disabledMap that's true, then disable the command center
return Array.from($disabledMap.values()).every((disabled) => !disabled);
});
function isInputEvent(event: KeyboardEvent) {
return ['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName);
}
function getCommandRank(command: KeyedCommand) {
const { keys, ctrl: meta, shift, alt } = command;
const modifiers = [meta, shift, alt].filter(Boolean).length;
return (keys?.length || 0) + modifiers * 10;
}
function hasDisputing(command: KeyedCommand, allCommands: Command[]) {
return allCommands.some((otherCommand) => {
if (command === otherCommand) {
return false;
}
if (!isKeyedCommand(otherCommand)) {
return false;
}
const keysString = command.keys.join('+');
const otherKeysString = otherCommand?.keys?.join('+');
const cmdRank = getCommandRank(command);
const otherCmdRank = getCommandRank(otherCommand);
return (
(keysString.includes(otherKeysString) || otherKeysString.includes(keysString)) &&
cmdRank <= otherCmdRank
);
});
}
export const commandCenterKeyDownHandler = derived(
[commandMap, commandsEnabled, wizard],
([$commandMap, enabled, $wizard]) => {
const commandsArr = Array.from($commandMap.values()).flat();
let recentKeyCodes: number[] = [];
let validCommands: KeyedCommand[] = [];
const reset = debounce(() => {
recentKeyCodes = [];
validCommands = [];
}, 1000);
const getHighestPriorityCommand = () => {
if (!validCommands.length) return;
if (validCommands.length === 1) {
return validCommands[0];
}
// Rank commands by how many keys and modifiers they have.
// Each key is worth 1 point, each modifier is worth 10 points.
// The command with the highest score wins.
const rankedCommands = validCommands.map((command) => {
return { command, score: getCommandRank(command) };
});
const highestScore = Math.max(...rankedCommands.map(({ score }) => score));
const highestScoreCommands = rankedCommands.filter(
({ score }) => score === highestScore
);
if (highestScoreCommands.length === 1) {
return highestScoreCommands[0].command;
}
// If there's still a tie, the command with the most modifiers wins.
// And if even that's a tie, the first command wins.
const mostModifiers = Math.max(
...highestScoreCommands.map(({ command }) => {
const { ctrl: meta, shift, alt } = command;
return [meta, shift, alt].filter(Boolean).length;
})
);
const mostModifiersCommands = highestScoreCommands.filter(({ command }) => {
const { ctrl: meta, shift, alt } = command;
return [meta, shift, alt].filter(Boolean).length === mostModifiers;
});
return mostModifiersCommands[0]?.command;
};
const rankAndExecute = debounce(() => {
const command = getHighestPriorityCommand();
command?.callback();
reset.immediate();
}, 200);
const execute = (command: KeyedCommand) => {
if (hasDisputing(command, commandsArr)) {
validCommands.push(command);
rankAndExecute();
} else {
command.callback();
reset.immediate();
}
};
return (event: KeyboardEvent) => {
recentKeyCodes.push(event.keyCode);
reset();
for (const command of commandsArr) {
if (!isKeyedCommand(command)) continue;
if (!command.forceEnable) {
if (command.disabled || !enabled || isInputEvent(event) || $wizard.show) {
continue;
}
}
const { keys, ctrl: meta, shift, alt } = command;
const isMetaPressed = meta
? isMac()
? event.metaKey
: event.ctrlKey
: !(isMac() ? event.metaKey : event.ctrlKey);
const isShiftPressed = shift ? event.shiftKey : !event.shiftKey;
const isAltPressed = alt ? event.altKey : !event.altKey;
const commandKeyCodes = keys?.map((key) => key.toUpperCase().charCodeAt(0));
const allKeysPressed = commandKeyCodes
? recentKeyCodes.join('').includes(commandKeyCodes.join(''))
: false;
if (allKeysPressed && isMetaPressed && isShiftPressed && isAltPressed) {
event.preventDefault();
execute(command);
}
}
};
}
);
// Methods
export const registerCommands = {
subscribe(runner: (cb: (newCommands: Command[]) => void) => void) {
const uuid = nanoid();
runner((newCommands: Command[]) => {
commandMap.update((curr) => {
const commandsWithTracking = newCommands.map((command) => {
const trackingCallback = () => {
if (command.label) {
trackEvent('command', { label: command.label, group: command.group });
}
command.callback();
};
return { ...command, callback: trackingCallback };
});
curr.set(uuid, commandsWithTracking);
return curr;
});
});
return () => {
commandMap.update((curr) => {
curr.delete(uuid);
return curr;
});
};
}
};
export const disableCommands = {
subscribe(runner: (cb: (disabled: boolean) => void) => void) {
const uuid = nanoid();
runner((disabled: boolean) => {
disabledMap.update((curr) => {
curr.set(uuid, disabled);
return curr;
});
});
return () => {
disabledMap.update((curr) => {
curr.delete(uuid);
return curr;
});
};
}
};
type CommandGroupRanks = Partial<Record<CommandGroup, number>>;
type GroupRanksMap = Map<string, CommandGroupRanks>;
const groupRanksMap = writable<GroupRanksMap>(new Map());
export const updateCommandGroupRanks = {
subscribe(runner: (cb: (updater: CommandGroupRanks) => void) => void) {
const uuid = nanoid();
runner((groupRank: CommandGroupRanks) => {
groupRanksMap.update((curr) => {
curr.set(uuid, groupRank);
return curr;
});
});
return () => {
groupRanksMap.update((curr) => {
curr.delete(uuid);
return curr;
});
};
}
};
export const commandGroupRanks = derived(groupRanksMap, ($groupRankTransformations) => {
const initialRanks = {
...Object.fromEntries(groups.map((group) => [group, 0])),
ungrouped: 9999,
databases: 50,
users: 40,
teams: 30,
projects: 20,
organizations: 10,
navigation: 0,
help: -20,
misc: -30
} as CommandGroupRanks;
const transformations = Array.from($groupRankTransformations.values());
return transformations.reduce((prev, curr) => ({ ...prev, ...curr }), initialRanks);
});
export type Searcher = (query: string) => Promise<Command[]>;
const searchersMap = writable<Map<string, Searcher[]>>(new Map());
export const registerSearchers = {
subscribe(runner: (cb: (...searchers: Searcher[]) => void) => void) {
const uuid = nanoid();
runner((...searchers: Searcher[]) => {
searchersMap.update((curr) => {
curr.set(uuid, [...searchers]);
return curr;
});
});
return () => {
searchersMap.update((curr) => {
curr.delete(uuid);
return curr;
});
};
}
};
export const searchers = derived(searchersMap, ($searchersMap) => {
return Array.from($searchersMap.values()).flat();
});
export const initSearcher = (searcher: Searcher) => {
const search = writable('');
const results = writable<Command[]>([]);
const searcherDebounced = debounce(async (query: string) => {
results.set(await searcher(query));
}, 500);
onMount(() => {
searcherDebounced.immediate('');
return search.subscribe((query) => {
searcherDebounced(query);
});
});
return {
search,
results
};
};
+4
View File
@@ -0,0 +1,4 @@
export * from './commands';
export * from './subPanels';
import CommandCenter from './commandCenter.svelte';
export { CommandCenter };
+311
View File
@@ -0,0 +1,311 @@
<script lang="ts">
import Template from './template.svelte';
import { Alert, AvatarInitials, Code, LoadingDots, SvgIcon } from '$lib/components';
import { user } from '$lib/stores/user';
import { useCompletion } from 'ai/svelte';
import { subPanels } from '../subPanels';
import { isLanguage, type Language } from '$lib/components/code.svelte';
import { preferences } from '$lib/stores/preferences';
import { VARS } from '$lib/system';
const endpoint = VARS.APPWRITE_ENDPOINT ?? `${globalThis?.location?.origin}/v1`;
const { input, handleSubmit, completion, isLoading, complete, error } = useCompletion({
api: endpoint + '/console/assistant',
headers: {
'content-type': 'application/json'
}
});
const examples = [
'How to add platform in the console?',
'How can I manage users, permissions, and access control in Appwrite?',
'How can I set up database collections and documents in Appwrite?',
'How do I configure and manage server-side functions in Appwrite?',
'How to add custom domain in the console?'
];
type Answer = Array<
| {
type: 'code';
language: Language;
value: string;
}
| {
type: 'text';
value: string;
}
>;
function parseCompletion(c: string): Answer | null {
if (!c) return null;
const answer: Answer = [];
// get all code matches
const codeMatches = Array.from(c.matchAll(/```([a-z]*)(.*?)(```|$)/gs));
let i = 0;
let codeIdx = 0;
while (i < c.length) {
const nextCodeMatch = codeMatches[codeIdx] ?? null;
if (!nextCodeMatch) {
answer.push({
type: 'text',
value: c.slice(i)
});
break;
} else if (i >= nextCodeMatch.index) {
let language = nextCodeMatch[1];
if (language === 'javascript') language = 'js';
answer.push({
type: 'code',
value: nextCodeMatch[2].startsWith('\n')
? nextCodeMatch[2].slice(1)
: nextCodeMatch[2],
language: isLanguage(language) ? language : 'js'
});
i = nextCodeMatch.index + nextCodeMatch[0].length;
codeIdx++;
} else {
answer.push({
type: 'text',
value: c.slice(i, nextCodeMatch.index)
});
i = nextCodeMatch.index;
}
}
return answer;
}
$: answer = parseCompletion($completion);
function getInitials(name: string) {
const [first, last] = name.split(' ');
return `${first?.[0] ?? ''}${last?.[0] ?? ''}`;
}
</script>
<Template
options={$isLoading || answer
? undefined
: examples.map((e) => {
return {
label: e,
callback: () => {
$input = e;
complete($input);
},
group: 'Examples'
};
})}
clearOnCallback={false}
on:keydown={(e) => {
if (e.detail.key !== 'Escape') {
e.detail.cancel();
}
}}
--min-height="40rem"
--max-height="52.5rem">
<div slot="search">
<span class="experimental border-gradient">EXPERIMENTAL</span>
</div>
<div slot="option" let:option class="u-flex u-cross-center u-gap-8">
<i class="icon-question-mark-circle" />
<span>{option.label}</span>
</div>
{#if !$preferences.hideAiDisclaimer}
<div style="padding: 1rem; padding-block-end: 0;">
<Alert
type="default"
dismissible
on:dismiss={() => {
$preferences.hideAiDisclaimer = true;
}}>
<span slot="title">
We collect user responses to refine our experimental AI feature.
</span>
</Alert>
</div>
{/if}
{#if $isLoading || answer}
<div class="content">
<div class="u-flex u-gap-8 u-cross-center">
<div class="avatar is-size-x-small">{getInitials($user.name)}</div>
<p class="u-opacity-75">{$input}</p>
</div>
<div class="u-flex u-gap-8 u-margin-block-start-24">
<div class="logo">
<SvgIcon name="sparkles" type="color" />
</div>
<div class="answer">
{#if $isLoading && !$completion}
<LoadingDots />
{:else}
{#each answer as part}
{#if part.type === 'text'}
<p>{part.value.trimStart()}</p>
{:else if part.type === 'code'}
{#key part.value}
<div
class="u-margin-block-start-8"
style="margin-block-end: 1rem;">
<Code
label={part.language}
language={part.language}
code={part.value}
noMargin
noBoxPadding
withCopy />
</div>
{/key}
{/if}
{/each}
{/if}
</div>
</div>
</div>
{/if}
{#if $error}
<div style="padding: 1rem; padding-block-end: 0;">
<Alert type="error">
<span slot="title">Something went wrong</span>
<p>
An unexpected error occurred while handling your request. Please try again
later.
</p>
</Alert>
</div>
{/if}
<div class="footer" slot="footer">
<div class="u-flex u-cross-center u-gap-4">
<AvatarInitials size={32} name={$user.name} />
<form
class="input-text-wrapper u-width-full-line"
style="--amount-of-buttons: 1;"
on:submit|preventDefault={handleSubmit}>
<!-- svelte-ignore a11y-autofocus -->
<input
type="text"
class="input-text"
placeholder="Ask a question..."
autofocus
bind:value={$input}
disabled={$isLoading} />
<div class="options-list">
<button
class="options-list-button"
aria-label="ask AI"
type="submit"
disabled={!$input.trim() || $isLoading}>
<span class="icon-arrow-sm-right" aria-hidden="true" />
</button>
</div>
</form>
</div>
<div class="u-flex u-main-end u-cross-center u-gap-16 u-margin-block-start-16">
<div class="u-flex u-cross-center u-gap-4">
<kbd class="kbd">Enter</kbd>
<span>to search</span>
</div>
<div class="sep" />
<div class="u-flex u-cross-center u-gap-4">
<kbd class="kbd">Esc</kbd>
<span>to {$subPanels.length === 1 ? 'close' : 'go back'}</span>
</div>
</div>
</div>
</Template>
<style lang="scss">
:global(.theme-dark) .content {
--logo-bg: #282a3b;
}
:global(.theme-light) .content {
--logo-bg: #f2f2f8;
}
:global(.theme-dark) .footer {
--sep-clr: hsl(var(--color-neutral-150));
}
:global(.theme-light) .footer {
--sep-clr: hsl(var(--color-neutral-30));
}
.content {
overflow: auto;
padding: 1rem;
.logo {
display: flex;
width: 1.5rem;
height: 1.5rem;
justify-content: center;
align-items: center;
flex-shrink: 0;
border-radius: 0.25rem;
background: var(--logo-bg);
}
.answer {
overflow: hidden;
p {
white-space: pre-wrap;
}
}
}
.footer {
.sep {
width: 1px;
height: 1.5rem;
background-color: var(--sep-clr);
}
}
.experimental {
display: flex;
padding: 0.09375rem 0.25rem;
align-items: center;
color: var(--light-neutrals-30, #e8e9f0);
text-align: center;
font-family: Inter;
font-size: 0.625rem;
font-style: normal;
font-weight: 500;
line-height: 150%; /* 0.9375rem */
letter-spacing: 0.075rem;
text-transform: uppercase;
background: rgba(240, 46, 101, 0.24);
--border-gradient: linear-gradient(
to bottom,
rgba(240, 46, 101, 0.48) 0%,
rgba(240, 46, 101, 0) 150%
)
border-box;
--border-size: 0.03rem;
--border-radius: 0.25rem;
border-radius: var(--border-radius);
}
:global(.theme-light) .experimental {
color: rgba(240, 46, 101, 1);
}
</style>
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { bucketSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(bucketSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { collectionsSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(collectionsSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,28 @@
<script lang="ts">
import { initCreateAttribute } from '$routes/console/project-[project]/databases/database-[database]/collection-[collection]/+layout.svelte';
import { attributeOptions } from '$routes/console/project-[project]/databases/database-[database]/collection-[collection]/attributes/store';
import Template from './template.svelte';
let search = '';
let options = attributeOptions.map((option) => {
return {
label: option.name,
icon: option.icon,
callback() {
initCreateAttribute(option.name);
}
};
});
$: filteredOptions = options.filter((option) => {
return option.label.toLowerCase().includes(search.toLowerCase());
});
</script>
<Template options={filteredOptions} bind:search>
<div class="u-flex u-cross-center u-gap-8" slot="option" let:option>
<i class="icon-{option.icon}" />
<span>{option.label}</span>
</div>
</Template>
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { dbSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(dbSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { fileSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(fileSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { functionsSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(functionsSearcher);
</script>
<Template options={$results} bind:search={$search} />
+80
View File
@@ -0,0 +1,80 @@
import type { SubPanel } from '../subPanels';
export { default as Template } from './template.svelte';
import Root from './root.svelte';
export const RootPanel: SubPanel = {
name: 'root',
component: Root
};
import AI from './ai.svelte';
export const AIPanel: SubPanel = {
name: 'Ask the AI',
component: AI
};
import Projects from './projects.svelte';
export const ProjectsPanel: SubPanel = {
name: 'Projects',
component: Projects
};
import Organizations from './organizations.svelte';
export const OrganizationsPanel: SubPanel = {
name: 'Organizations',
component: Organizations
};
import Platforms from './platforms.svelte';
export const PlatformsPanel: SubPanel = {
name: 'Platforms',
component: Platforms
};
import Databases from './databases.svelte';
export const DatabasesPanel: SubPanel = {
name: 'Databases',
component: Databases
};
import Collections from './collections.svelte';
export const CollectionsPanel: SubPanel = {
name: 'Collections',
component: Collections
};
import CreateAttribute from './createAttribute.svelte';
export const CreateAttributePanel: SubPanel = {
name: 'Create Attribute',
component: CreateAttribute
};
import Users from './users.svelte';
export const UsersPanel: SubPanel = {
name: 'Users',
component: Users
};
import Teams from './teams.svelte';
export const TeamsPanel: SubPanel = {
name: 'Teams',
component: Teams
};
import Functions from './functions.svelte';
export const FunctionsPanel: SubPanel = {
name: 'Functions',
component: Functions
};
import Buckets from './buckets.svelte';
export const BucketsPanel: SubPanel = {
name: 'Buckets',
component: Buckets
};
import Files from './files.svelte';
export const FilesPanel: SubPanel = {
name: 'Files',
component: Files
};
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { orgSearcher } from '../searchers';
import Template from './template.svelte';
const { results, search } = initSearcher(orgSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,55 @@
<script lang="ts">
import {
Platform,
addPlatform
} from '$routes/console/project-[project]/overview/platforms/+page.svelte';
import Template from './template.svelte';
let search = '';
let platforms = [
{
label: 'Web',
icon: 'code',
group: 'platforms',
callback: () => {
addPlatform(Platform.Web);
}
},
{
label: 'Flutter',
icon: 'flutter',
group: 'platforms',
callback: () => {
addPlatform(Platform.Flutter);
}
},
{
label: 'Android',
icon: 'android',
group: 'platforms',
callback: () => {
addPlatform(Platform.Android);
}
},
{
label: 'Apple',
icon: 'apple',
group: 'platforms',
callback: () => {
addPlatform(Platform.Apple);
}
}
] as const;
$: filteredPlatforms = platforms.filter((platform) => {
return platform.label.toLowerCase().includes(search.toLowerCase());
});
</script>
<Template options={filteredPlatforms} bind:search>
<div class="u-flex u-cross-center u-gap-8" slot="option" let:option>
<i class="icon-{option.icon}" />
<span>{option.label}</span>
</div>
</Template>
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { projectsSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(projectsSearcher);
</script>
<Template options={$results} bind:search={$search} />
+87
View File
@@ -0,0 +1,87 @@
<!--
This is the root command panel. It precedes all other command panels.
-->
<script lang="ts">
import { debounce } from '$lib/helpers/debounce';
import { isMac } from '$lib/helpers/platform';
import { commands, searchers, type Command, isKeyedCommand } from '../commands';
import Template from './template.svelte';
let search = '';
let searchResults: Omit<Command, 'keys'>[] = [];
const executeSearch = debounce(async (s: string) => {
$searchers.forEach(async (searcher) => {
const results = await searcher(s);
searchResults = [...searchResults, ...results];
});
}, 500);
$: {
if (search) {
searchResults = [];
executeSearch(search);
} else {
searchResults = [];
executeSearch.cancel();
}
}
$: results = [
...$commands.filter((command) => {
return (
!command.disabled &&
command.label &&
command.label.toLowerCase().includes(search.toLowerCase())
);
}),
...searchResults
] as Array<Command | Omit<Command, 'keys'>>;
const hasCtrl = (command: Command) => {
return 'ctrl' in command && command.ctrl;
};
const hasShift = (command: Command) => {
return 'shift' in command && command.shift;
};
const hasAlt = (command: Command) => {
return 'alt' in command && command.alt;
};
</script>
<Template options={results} bind:search searchPlaceholder="Search for commands or content...">
<div slot="option" class="u-flex u-main-space-between content" let:option={command}>
<div class="u-flex u-gap-8 u-cross-center">
<i class="icon-{command.icon ?? 'arrow-sm-right'}" />
<span>
{command.label}
</span>
</div>
<div class="u-flex u-gap-4 u-cross-center">
{#if hasCtrl(command)}
<kbd class="kbd"> {isMac() ? '⌘' : 'Ctrl'} </kbd>
{/if}
{#if hasShift(command)}
<kbd class="kbd"> {isMac() ? '⇧' : 'Shift'} </kbd>
{/if}
{#if hasAlt(command)}
<kbd class="kbd"> {isMac() ? '⌥' : 'Alt'} </kbd>
{/if}
{#if isKeyedCommand(command)}
{#each command.keys as key, i}
{@const hasNext = command.keys.length - 1 !== i}
<kbd class="kbd">
{key.toUpperCase()}
</kbd>
{#if hasNext}
<span class="u-margin-inline-4" style:opacity={0.5}>then</span>
{/if}
{/each}
{/if}
</div>
</div>
<svelte:fragment slot="no-options">No commands found</svelte:fragment>
</Template>
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { teamSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(teamSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,569 @@
<script lang="ts">
import { commandGroupRanks, type Command, type CommandGroup } from '../commands';
// This is the template for all panels used in the command center.
// Use this component when you want to create a new panel.
import { createEventDispatcher, tick } from 'svelte';
import { getCommandCenterCtx } from '../commandCenter.svelte';
import { clearSubPanels, popSubPanel, subPanels } from '../subPanels';
type Option = $$Generic<Omit<Command, 'group'> & { group?: string }>;
export let options: Option[] | null = null;
export let search = '';
export let searchPlaceholder = 'Search...';
export let fullheight = false;
export let clearOnCallback = true;
let selected = 0;
let usingKeyboard = false;
let contentEl: HTMLElement;
async function triggerOption(option: Option) {
const prevPanels = $subPanels.length;
option.callback();
if (prevPanels === $subPanels.length && clearOnCallback && !option.keepOpen) {
clearSubPanels();
}
}
const dispatch = createEventDispatcher<{
keydown: {
originalEvent: KeyboardEvent;
cancel: () => void;
key: string;
};
}>();
function handleKeyDown(event: KeyboardEvent) {
if (!open) return;
usingKeyboard = true;
let canceled = false;
dispatch('keydown', {
originalEvent: event,
cancel: () => (canceled = true),
key: event.key
});
if (canceled) return;
if (options) {
if (event.key === 'ArrowDown') {
event.preventDefault();
if (event.metaKey) {
selected = options.length - 1;
} else {
selected = selected === options.length - 1 ? options.length - 1 : selected + 1;
}
} else if (event.key === 'ArrowUp') {
event.preventDefault();
if (event.metaKey) {
selected = 0;
} else {
selected = selected === 0 ? 0 : selected - 1;
}
} else if (event.key === 'Enter') {
const option = groupsAndOptions.find(
(item) => 'index' in item && item.index === selected
) as IndexedOption;
if (!option) return;
event.preventDefault();
triggerOption(option);
} else if (event.key === 'Home') {
event.preventDefault();
selected = 0;
} else if (event.key === 'End') {
event.preventDefault();
selected = options.length - 1;
}
tick().then(() => {
if (!cardEl) return;
const selectedEl = contentEl.querySelector('[data-selected]');
if (selectedEl) {
selectedEl.scrollIntoView({
block: 'nearest'
});
}
});
}
if (event.key === 'Escape') {
event.preventDefault();
popSubPanel();
}
}
$: if (selected > options?.length - 1) {
selected = options?.length - 1;
} else if (usingKeyboard && selected < 0 && options?.length) {
selected = 0;
}
const commandCenterCtx = getCommandCenterCtx();
let cardEl: HTMLElement;
type Group = { type: 'group'; name: string };
type IndexedOption = Option & { index: number };
function isGroup(item: Option | Group): item is Group {
return !!item && 'type' in item && item.type === 'group';
}
const getGroupsAndOptions = (options: Option[]) => {
if (!options) return null;
const groupedOptions = new Map<string, Option[]>();
groupedOptions.set('ungrouped', []);
for (const option of options) {
if (!option.group) {
groupedOptions.set('ungrouped', [...groupedOptions.get('ungrouped'), option]);
} else {
groupedOptions.set(option.group, [
...(groupedOptions.get(option.group) || []),
option
]);
}
}
// Organize groups
const sortedOptions = [...groupedOptions.entries()]
.sort(([a], [b]) => {
const aRank = $commandGroupRanks[a] || 0;
const bRank = $commandGroupRanks[b] || 0;
if (aRank < bRank) {
return 1;
} else if (aRank > bRank) {
return -1;
} else {
return 0;
}
})
.map(([groupName, options]) => {
return [
groupName,
options.sort((a, b) => {
const aRank = a.rank || 0;
const bRank = b.rank || 0;
if (aRank < bRank) {
return 1;
} else if (aRank > bRank) {
return -1;
} else {
return 0;
}
})
] as [CommandGroup, Option[]];
});
// return a flat array of groups and indexed options
let optionIndex = 0;
const groupsAndOptions: (Group | IndexedOption)[] = [];
for (const [groupName, options] of sortedOptions) {
if (groupName !== 'ungrouped') {
groupsAndOptions.push({ type: 'group', name: groupName });
}
for (const option of options) {
groupsAndOptions.push({ ...option, index: optionIndex++ });
}
}
return groupsAndOptions;
};
$: groupsAndOptions = getGroupsAndOptions(options);
const getOptionClickHandler = (option: IndexedOption) => () => {
triggerOption(option);
};
const getOptionFocusHandler =
(option: IndexedOption, hover = false) =>
() => {
selected = option.index;
usingKeyboard = hover ? false : usingKeyboard;
};
const getOptionBlurHandler = () => () => {
selected = -1;
usingKeyboard = false;
};
const castOption = (option: IndexedOption) => option as Option;
function getBreadcrumbs(subPanels: typeof $subPanels) {
return subPanels.filter((panel) => panel.name !== 'root').map((panel) => panel.name);
}
let breadcrumbs = getBreadcrumbs($subPanels);
// Avoid clearing subpanels before the closing transition is finished
$: if ($subPanels.length) {
breadcrumbs = getBreadcrumbs($subPanels);
}
const handleCrumbClick = (index: number) => {
if (index === breadcrumbs.length - 1) {
popSubPanel();
} else {
const numCrumbsToPop = breadcrumbs.length - index - 1;
for (let i = 0; i < numCrumbsToPop; i++) {
popSubPanel();
}
}
};
const isFirstNested = (index: number) => {
const prevItem = groupsAndOptions[index - 1];
const item = groupsAndOptions[index];
if (isGroup(item)) return false;
if (!item.nested) return false;
return !isGroup(prevItem) && !prevItem?.nested;
};
const isLastNested = (index: number) => {
const nextItem = groupsAndOptions[index + 1];
const item = groupsAndOptions[index];
if (isGroup(item)) return false;
if (!item.nested) return false;
return isGroup(nextItem) || !nextItem?.nested;
};
</script>
<svelte:window on:keydown={handleKeyDown} />
<div
class="card"
class:fullheight
bind:this={cardEl}
class:press={!$commandCenterCtx.isInitialPanel}
class:scale-up={$commandCenterCtx.isInitialPanel && $commandCenterCtx.open}>
<div class="search-wrapper">
{#each breadcrumbs as crumb, i}
{@const isLast = i === breadcrumbs.length - 1}
<button class="crumb" on:click={() => handleCrumbClick(i)}>
<span>{crumb}</span>
<i class="icon-x" />
</button>
{#if !isLast}
<span style="opacity: 50%">/</span>
{/if}
{/each}
<slot name="search">
<div class="u-flex default-search u-width-full-line">
<!-- svelte-ignore a11y-autofocus -->
<input type="text" placeholder={searchPlaceholder} autofocus bind:value={search} />
</div>
</slot>
</div>
<div class="content" bind:this={contentEl}>
<slot />
{#if groupsAndOptions}
<ul class="options">
{#each groupsAndOptions as item, i}
{@const isSelected = !isGroup(item) && item.index === selected}
{#if isGroup(item)}
<li class="group eyebrow-heading-3">
{item.name}
</li>
{:else}
<li
class="result"
data-selected={isSelected ? true : undefined}
class:nested={item.nested}
class:first-nested={isFirstNested(i)}
class:last-nested={isLastNested(i)}>
{#if isSelected}
<div class="bg" />
{/if}
<button
class="option"
on:click={getOptionClickHandler(item)}
on:mouseover={getOptionFocusHandler(item, true)}
on:mouseleave={getOptionBlurHandler()}
on:focus={getOptionFocusHandler(item)}>
<slot name="option" option={castOption(item)}>
<div class="u-flex u-gap-8 u-cross-center">
<i class="icon-{item.icon ?? 'arrow-sm-right'}" />
<span>
{item.label}
</span>
</div>
</slot>
</button>
</li>
{/if}
{:else}
<li class="result">
<slot name="no-options">
<span class="text">No options found</span>
</slot>
</li>
{/each}
</ul>
{/if}
</div>
<div class="footer">
<slot name="footer">
<div class=" u-flex u-flex u-cross-center u-main-space-between">
<div class="u-flex u-cross-center u-gap-4">
<kbd class="kbd">Enter</kbd> <span>to select</span>
</div>
<div class="u-flex u-cross-center u-gap-4">
<kbd class="kbd">Esc</kbd>
<span>to {$subPanels.length > 1 ? 'go back' : 'close'}</span>
</div>
</div>
</slot>
</div>
</div>
<style lang="scss">
// Animations
@keyframes press {
0% {
scale: 1;
}
30% {
scale: 0.985;
}
100% {
scale: 1;
}
}
.press {
animation: press 250ms ease;
}
@keyframes scale-up {
0% {
scale: 0.95;
}
100% {
scale: 1;
}
}
.scale-up {
animation: scale-up 150ms cubic-bezier(0.5, 1, 0.89, 1);
}
// Theme
:global(.theme-light) .card {
--cmd-center-bg: hsl(var(--color-neutral-0));
--cmd-center-border: hsl(var(--color-neutral-10));
--cmd-center-shadow: 0px 16px 32px 0px rgba(55, 59, 77, 0.04);
--kbd-bg: hsl(var(--color-neutral-30));
--crumb-bg: hsl(var(--color-neutral-10));
--crumb-color: hsl(var(--color-neutral-100));
--result-bg: hsl(var(--color-neutral-10));
--footer-bg: linear-gradient(180deg, #fff 49.38%, #e8e9f0 100%);
--icon-color: hsl(var(--color-neutral-50));
--label-color: hsl(var(--color-neutral-100));
}
:global(.theme-dark) .card {
--cmd-center-bg: rgba(27, 27, 40, 0.8);
--cmd-center-border: hsl(var(--color-neutral-150));
--cmd-center-shadow: 0px 16px 32px 0px #14141f;
--kbd-bg: hsl(var(--color-neutral-150));
--crumb-bg: hsl(var(--color-neutral-150));
--crumb-color: hsl(var(--color-neutral-30));
--result-bg: hsl(var(--color-neutral-200));
--footer-bg: linear-gradient(180deg, #1b1b28 0%, #282a3b 100%);
--icon-color: hsl(var(--color-neutral-70));
--label-color: hsl(var(--color-neutral-30));
}
// Elements
.card {
position: absolute;
--top: clamp(64px, 10vh, 400px);
top: var(--top);
left: 50%;
translate: -50%;
display: flex;
flex-direction: column;
width: var(--width, 42.5rem);
max-width: 100%;
min-height: var(--min-height);
max-height: min(calc(100vh - var(--top) - 4rem), var(--max-height, 32rem));
overflow: hidden;
padding: 0;
border-radius: 0.5rem;
border: 1px solid var(--cmd-center-border);
background: var(--cmd-center-bg);
box-shadow: var(--cmd-center-shadow);
backdrop-filter: blur(6px);
&.fullheight {
height: var(--max-height, 32rem);
}
:global(.kbd) {
background-color: var(--kbd-bg);
padding-inline: 0.25rem;
}
}
.search-wrapper {
display: flex;
gap: 0.25rem;
align-items: center;
width: 100%;
border-bottom: 1px solid hsl(var(--color-border));
font-size: 16px;
padding: 1rem;
.default-search {
input {
margin: -1rem;
padding: 1rem;
border: none;
background-color: transparent;
}
}
}
.crumb {
display: flex;
padding: 0.09375rem 0.25rem;
align-items: center;
gap: 0.25rem;
border-radius: 0.25rem;
background: var(--crumb-bg);
color: var(--crumb-color);
text-align: center;
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: normal;
white-space: nowrap;
&:hover {
opacity: 0.75;
}
i {
font-size: 10px;
}
}
.content {
overflow-y: auto;
flex-grow: 1;
.options {
padding: 1rem;
.group {
color: hsl(var(--color-neutral-70));
margin-inline-start: 0.25rem;
margin-block-end: 0.25rem;
position: relative;
z-index: 10;
font-size: 10px !important;
&:not(:first-child) {
margin-block-start: 1rem;
}
}
.result {
position: relative;
scroll-margin-block: 0.5rem;
.bg {
position: absolute;
inset: 0;
background-color: var(--result-bg);
border-radius: 0.5rem;
translate: 0 -1px;
}
.option {
padding: 0.5rem 9.5px;
font-size: 14px;
position: relative;
z-index: 10;
width: 100%;
box-shadow: none !important;
color: var(--label-color);
:global(i[class^='icon-']) {
font-size: 1rem !important;
width: 1rem !important;
height: 1rem !important;
color: var(--icon-color);
position: relative;
}
:global(i[class^='icon-']::before) {
position: absolute;
top: 50%;
left: 50%;
translate: -50% -50%;
}
}
&.nested {
margin-left: 30px;
&.first-nested::before {
top: 8px;
}
&.last-nested::before {
height: calc(100% - 8px);
}
&::before {
content: '';
position: absolute;
left: -8px;
border-left: 1px solid hsl(var(--color-border));
height: 100%;
}
}
}
}
}
.footer {
background: var(--footer-bg);
border-top: 1px solid hsl(var(--color-border));
padding: 0.5rem 1rem;
}
</style>
@@ -0,0 +1,9 @@
<script lang="ts">
import { initSearcher } from '../commands';
import { userSearcher } from '../searchers';
import Template from './template.svelte';
const { search, results } = initSearcher(userSearcher);
</script>
<Template options={$results} bind:search={$search} />
@@ -0,0 +1,81 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import { project } from '$routes/console/project-[project]/store';
import { Query, type Models } from '@appwrite.io/console';
import { get } from 'svelte/store';
import type { Command, Searcher } from '../commands';
import { addSubPanel } from '../subPanels';
import { FilesPanel } from '../panels';
const getBucketCommand = (bucket: Models.Bucket, projectId: string) => {
return {
label: `${bucket.name}`,
callback() {
goto(`/console/project-${projectId}/storage/bucket-${bucket.$id}`);
},
group: 'buckets',
icon: 'folder'
} satisfies Command;
};
export const bucketSearcher = (async (query: string) => {
const { buckets } = await sdk.forProject.storage.listBuckets([Query.orderDesc('$createdAt')]);
const $project = get(project);
const filtered = buckets.filter((bucket) => bucket.name.includes(query));
if (filtered.length === 1) {
const bucket = filtered[0];
return [
getBucketCommand(bucket, $project.$id),
{
label: 'Find files',
async callback() {
await goto(`/console/project-${$project.$id}/storage/bucket-${bucket.$id}`);
addSubPanel(FilesPanel);
},
group: 'buckets',
nested: true,
icon: 'search',
keepOpen: true
},
{
label: 'Permissions',
async callback() {
await goto(
`/console/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#permissions`
);
scrollBy({ top: -100 });
},
group: 'buckets',
nested: true,
icon: 'key'
},
{
label: 'Extensions',
async callback() {
await goto(
`/console/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#extensions`
);
},
group: 'buckets',
nested: true,
icon: 'puzzle'
},
{
label: 'File Security',
async callback() {
await goto(
`/console/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#file-security`
);
scrollBy({ top: -100 });
},
group: 'buckets',
nested: true,
icon: 'lock-closed'
}
];
}
return filtered.map((bucket) => getBucketCommand(bucket, $project.$id));
}) satisfies Searcher;
@@ -0,0 +1,27 @@
import { goto } from '$app/navigation';
import { database } from '$routes/console/project-[project]/databases/database-[database]/store';
import { project } from '$routes/console/project-[project]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
export const collectionsSearcher = (async (query: string) => {
const databaseId = get(database).$id;
const { collections } = await sdk.forProject.databases.listCollections(databaseId);
const projectId = get(project).$id;
return collections
.filter((col) => col.name.toLowerCase().includes(query.toLowerCase()))
.map(
(col) =>
({
group: 'collections',
label: col.name,
callback: () => {
goto(
`/console/project-${projectId}/databases/database-${databaseId}/collection-${col.$id}`
);
}
} as const)
);
}) satisfies Searcher;
@@ -0,0 +1,23 @@
import { goto } from '$app/navigation';
import { project } from '$routes/console/project-[project]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { sdk } from '$lib/stores/sdk';
export const dbSearcher = (async (query: string) => {
const { databases } = await sdk.forProject.databases.list();
return databases
.filter((db) => db.name.toLowerCase().includes(query.toLowerCase()))
.map(
(db) =>
({
group: 'databases',
label: db.name,
callback: () => {
goto(`/console/project-${get(project).$id}/databases/database-${db.$id}`);
},
icon: 'database'
} as const)
);
}) satisfies Searcher;
+27
View File
@@ -0,0 +1,27 @@
import { sdk } from '$lib/stores/sdk';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import { bucket } from '$routes/console/project-[project]/storage/bucket-[bucket]/store';
import { Query } from '@appwrite.io/console';
import { goto } from '$app/navigation';
import { project } from '$routes/console/project-[project]/store';
export const fileSearcher = (async (query: string) => {
const $bucket = get(bucket);
const $project = get(project);
const { files } = await sdk.forProject.storage.listFiles(
$bucket.$id,
[Query.orderDesc('')],
query || undefined
);
return files.map((file) => ({
label: file.name,
callback: () => {
goto(`/console/project-${$project.$id}/storage/bucket-${$bucket.$id}/file-${file.$id}`);
},
icon: 'document',
group: 'files'
}));
}) satisfies Searcher;
@@ -0,0 +1,81 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import { project } from '$routes/console/project-[project]/store';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/stores';
import { showCreateDeployment } from '$routes/console/project-[project]/functions/function-[function]/store';
const getFunctionCommand = (fn: Models.Function, projectId: string) => {
return {
label: fn.name,
callback: () => {
goto(`/console/project-${projectId}/functions/function-${fn.$id}`);
},
group: 'functions',
icon: 'lightning-bolt'
} as const;
};
export const functionsSearcher = (async (query: string) => {
const { functions } = await sdk.forProject.functions.list();
const projectId = get(project).$id;
const filtered = functions.filter((fn) => fn.name.toLowerCase().includes(query.toLowerCase()));
if (filtered.length === 1) {
const func = filtered[0];
return [
getFunctionCommand(func, projectId),
{
label: 'Create deployment',
nested: true,
async callback() {
const $page = get(page);
if (!$page.url.pathname.endsWith(func.$id)) {
await goto(`/console/project-${projectId}/functions/function-${func.$id}`);
}
showCreateDeployment.set(true);
},
group: 'functions',
icon: 'plus'
},
{
label: 'Go to deployments',
nested: true,
callback() {
goto(`/console/project-${projectId}/functions/function-${func.$id}`);
},
group: 'functions'
},
{
label: 'Go to usage',
nested: true,
callback() {
goto(`/console/project-${projectId}/functions/function-${func.$id}/usage`);
},
group: 'functions'
},
{
label: 'Go to executions',
nested: true,
callback() {
goto(`/console/project-${projectId}/functions/function-${func.$id}/executions`);
},
group: 'functions'
},
{
label: 'Go to settings',
nested: true,
callback() {
goto(`/console/project-${projectId}/functions/function-${func.$id}/settings`);
},
group: 'functions'
}
];
}
return filtered.map((fn) => getFunctionCommand(fn, projectId));
}) satisfies Searcher;
+9
View File
@@ -0,0 +1,9 @@
export * from './databases';
export * from './users';
export * from './organizations';
export * from './collections';
export * from './projects';
export * from './teams';
export * from './functions';
export * from './buckets';
export * from './files';
@@ -0,0 +1,18 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import type { Searcher } from '../commands';
export const orgSearcher = (async (query: string) => {
const { teams } = await sdk.forConsole.teams.list();
return teams
.filter((organization) => organization.name.toLowerCase().includes(query.toLowerCase()))
.map((organization) => {
return {
label: organization.name,
callback: () => {
goto(`/console/organization-${organization.$id}`);
},
group: 'organizations'
} as const;
});
}) satisfies Searcher;
@@ -0,0 +1,25 @@
import { goto } from '$app/navigation';
import { organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import { get } from 'svelte/store';
import type { Searcher } from '../commands';
export const projectsSearcher = (async (query: string) => {
const { projects } = await sdk.forConsole.projects.list([
Query.equal('teamId', get(organization).$id),
Query.orderDesc('')
]);
return projects
.filter((project) => project.name.toLowerCase().includes(query.toLowerCase()))
.map((project) => {
return {
label: project.name,
callback: () => {
goto(`/console/project-${project.$id}`);
},
group: 'projects'
} as const;
});
}) satisfies Searcher;
+45
View File
@@ -0,0 +1,45 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import { project } from '$routes/console/project-[project]/store';
import { get } from 'svelte/store';
import type { Command, Searcher } from '../commands';
import type { Models } from '@appwrite.io/console';
const getTeamCommand = (team: Models.Team<Models.Preferences>, projectId: string) =>
({
label: team.name,
callback: () => {
goto(`/console/project-${projectId}/auth/teams/team-${team.$id}`);
},
group: 'teams',
icon: 'user-circle'
} satisfies Command);
export const teamSearcher = (async (query: string) => {
const { teams } = await sdk.forProject.teams.list([], query);
const projectId = get(project).$id;
if (teams.length === 1) {
return [
getTeamCommand(teams[0], projectId),
{
label: 'Go to members',
callback: () => {
goto(`/console/project-${projectId}/auth/teams/team-${teams[0].$id}/members`);
},
group: 'teams',
nested: true
},
{
label: 'Go to activity',
callback: () => {
goto(`/console/project-${projectId}/auth/teams/team-${teams[0].$id}/activity`);
},
group: 'teams',
nested: true
}
];
}
return teams.map((team) => getTeamCommand(team, projectId));
}) satisfies Searcher;
+62
View File
@@ -0,0 +1,62 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import { project } from '$routes/console/project-[project]/store';
import { get } from 'svelte/store';
import type { Command, Searcher } from '../commands';
import type { Models } from '@appwrite.io/console';
import { promptDeleteUser } from '$routes/console/project-[project]/auth/user-[user]/dangerZone.svelte';
const getUserCommand = (user: Models.User<Models.Preferences>, projectId: string) =>
({
label: user.name,
callback: () => {
goto(`/console/project-${projectId}/auth/user-${user.$id}`);
},
group: 'users',
icon: 'user-circle'
} satisfies Command);
export const userSearcher = (async (query: string) => {
const { users } = await sdk.forProject.users.list([], query || undefined);
const projectId = get(project).$id;
if (users.length === 1) {
return [
getUserCommand(users[0], projectId),
{
label: 'Delete user',
callback: () => {
promptDeleteUser(users[0].$id);
},
group: 'users',
nested: true,
icon: 'trash'
},
{
label: 'Go to activity',
callback: () => {
goto(`/console/project-${projectId}/auth/user-${users[0].$id}/activity`);
},
group: 'users',
nested: true
},
{
label: 'Go to sessions',
callback: () => {
goto(`/console/project-${projectId}/auth/user-${users[0].$id}/sessions`);
},
group: 'users',
nested: true
},
{
label: 'Go to memberships',
callback: () => {
goto(`/console/project-${projectId}/auth/user-${users[0].$id}/memberships`);
},
group: 'users',
nested: true
}
];
}
return users.map((user) => getUserCommand(user, projectId));
}) satisfies Searcher;
+31
View File
@@ -0,0 +1,31 @@
import type { SvelteComponentDev } from 'svelte/internal';
import { writable } from 'svelte/store';
export type SubPanel = {
name: string;
component: typeof SvelteComponentDev;
};
type CastSubPanel = Omit<SubPanel, 'component'> & {
component: unknown;
};
export const subPanels = writable<Array<SubPanel>>([]);
export function addSubPanel(subPanel: CastSubPanel) {
subPanels.update((curr) => {
curr.push(subPanel as SubPanel);
return curr;
});
}
export function popSubPanel() {
subPanels.update((curr) => {
curr = curr.slice(0, -1);
return curr;
});
}
export function clearSubPanels() {
subPanels.set([]);
}
+31 -10
View File
@@ -3,14 +3,20 @@
import type { Buttons } from '../stores/notifications';
export let dismissible = false;
export let type: 'info' | 'success' | 'warning' | 'error' = 'info';
export let type: 'info' | 'success' | 'warning' | 'error' | 'default' = 'info';
export let buttons: Buttons[] = [];
export let isAction = false;
export let isStandalone = false;
let classes = '';
export { classes as class };
const dispatch = createEventDispatcher();
</script>
<section
class="alert"
class="alert {classes}"
class:is-action={isAction}
class:is-standalone={isStandalone}
class:is-success={type === 'success'}
class:is-warning={type === 'warning'}
class:is-danger={type === 'error'}
@@ -27,7 +33,7 @@
</button>
{/if}
<span
class:icon-info={type === 'info'}
class:icon-info={type === 'info' || type === 'default'}
class:icon-check-circle={type === 'success'}
class:icon-exclamation={type === 'warning'}
class:icon-exclamation-circle={type === 'error'}
@@ -38,16 +44,31 @@
<slot name="title" />
</h6>
{/if}
<p class="alert-message"><slot /></p>
{#if buttons?.length}
{#if $$slots.default}
<p class="alert-message"><slot /></p>
{/if}
{#if ($$slots.buttons || buttons?.length) && !isAction}
<div class="alert-buttons u-flex">
{#each buttons as button}
<button class="button is-text" on:click={button.method}>
<span class="text">{button.name}</span>
</button>
{/each}
<slot name="buttons">
{#each buttons as button}
<button type="button" class="button is-text" on:click={button.method}>
<span class="text">{button.name}</span>
</button>
{/each}
</slot>
</div>
{/if}
</div>
{#if ($$slots.buttons || buttons?.length) && isAction}
<div class="alert-buttons u-flex u-gap-16 u-cross-child-center">
<slot name="buttons">
{#each buttons as button}
<button type="button" class="button is-text" on:click={button.method}>
<span class="text">{button.name}</span>
</button>
{/each}
</slot>
</div>
{/if}
</div>
</section>
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
export let direction: 'up' | 'down' | 'left' | 'right' = 'right';
$: angle = {
down: 0,
left: 90,
up: 180,
right: 270
}[direction];
</script>
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style:transform="rotate({angle}deg)">
<path
d="M7.64645 15.8622C7.84171 16.0574 8.15829 16.0574 8.35355 15.8622L11.5355 12.6802C11.7308 12.4849 11.7308 12.1683 11.5355 11.9731C11.3403 11.7778 11.0237 11.7778 10.8284 11.9731L8 14.8015L5.17157 11.9731C4.97631 11.7778 4.65973 11.7778 4.46447 11.9731C4.2692 12.1683 4.2692 12.4849 4.46447 12.6802L7.64645 15.8622ZM7.5 1V15.5086H8.5V1H7.5Z"
fill="#C4C6D7" />
</svg>
+20 -2
View File
@@ -2,11 +2,22 @@
import AvatarInitials from './avatarInitials.svelte';
export let avatars: string[] = [];
export let icons: string[] = [];
export let total = avatars.length;
export let size = 40;
export let avatarSize: keyof typeof Sizes = 'medium';
export let bordered = false;
enum Sizes {
xsmall = 'is-size-x-small',
small = 'is-size-small',
medium = '',
large = 'is-size-large',
xlarge = 'is-size-x-large'
}
</script>
<ul class="avatars-group">
<ul class="avatars-group" class:is-with-border={bordered}>
{#each avatars as name, index}
{#if index < 2}
<li class="avatars-group-item">
@@ -14,9 +25,16 @@
</li>
{/if}
{/each}
{#each icons as icon}
<li class="avatars-group-item">
<span class="avatar {Sizes[avatarSize]}"><span class={`icon-${icon}`} /></span>
</li>
{/each}
{#if total > 2}
<li class="avatars-group-item">
<div class="avatar">+{total - 2}</div>
<div class="avatar {Sizes[avatarSize]}">+{total - 2}</div>
</li>
{/if}
</ul>
+2 -1
View File
@@ -4,8 +4,9 @@
export let name: string;
export let size: number;
export let background: string | undefined = undefined;
$: src = sdk.forConsole.avatars.getInitials(name, size * 2, size * 2).toString();
$: src = sdk.forConsole.avatars.getInitials(name, size * 2, size * 2, background).toString();
</script>
<Avatar {name} {size} {src} />
+19 -8
View File
@@ -1,9 +1,20 @@
<div class="box">
<div class="u-flex u-gap-16">
<slot name="image" />
<div class="u-cross-child-center u-line-height-1-5">
<slot name="title" />
<slot />
</div>
</div>
<script lang="ts">
export let radius: keyof typeof radiuses = 'small';
export let padding = 24;
let classes = '';
export { classes as class };
enum radiuses {
xsmall = '--border-radius-extra-large',
small = '--border-radius-small',
medium = '--border-radius-medium',
large = '--border-radius-large'
}
</script>
<div
class="box {classes}"
style:--box-border-radius={`var(${radiuses[radius]})`}
style:--box-padding={`${padding / 16}rem`}>
<slot />
</div>
+13
View File
@@ -0,0 +1,13 @@
<script lang="ts">
import { Box } from '.';
</script>
<Box>
<div class="u-flex u-gap-16">
<slot name="image" />
<div class="u-cross-child-center u-line-height-1-5">
<slot name="title" />
<slot />
</div>
</div>
</Box>
+4 -3
View File
@@ -1,11 +1,12 @@
<script>
<script lang="ts">
import { Card } from './';
export let danger = false;
export let hideOverflow = false;
</script>
<Card {danger}>
<div class="common-section grid-1-2">
<div class="common-section grid-1-2" class:hideOverflow>
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
<slot />
</div>
@@ -21,7 +22,7 @@
</Card>
<style lang="scss">
.grid-1-2 > * {
.hideOverflow > * {
width: 100%;
overflow: hidden;
}
+30 -5
View File
@@ -1,3 +1,12 @@
<script lang="ts" context="module">
const langArr = ['js', 'html', 'dart', 'kotlin', 'json', 'sh', 'yml', 'swift'] as const;
export type Language = (typeof langArr)[number];
export function isLanguage(str: string): str is Language {
return langArr.includes(str as Language);
}
</script>
<script lang="ts">
import { Pill } from '$lib/elements';
import Prism from 'prismjs';
@@ -20,6 +29,8 @@
export let withLineNumbers = false;
export let withCopy = false;
export let noMargin = false;
export let noBoxPadding = false;
export let allowScroll = false;
Prism.plugins.customClass.prefix('prism-');
@@ -28,7 +39,7 @@
});
</script>
<section class="box u-overflow-hidden" class:common-section={!noMargin}>
<section class="box u-overflow-hidden" class:common-section={!noMargin} class:noBoxPadding>
<div
class="controls u-position-absolute u-inset-inline-end-8 u-inset-block-start-8 u-flex u-gap-8">
{#if label}
@@ -47,9 +58,10 @@
</Copy>
{/if}
</div>
<pre class={`language-${language}`} class:line-numbers={withLineNumbers}><code
>{code}</code></pre>
<pre
class:with-scroll={allowScroll}
class={`language-${language}`}
class:line-numbers={withLineNumbers}><code>{code}</code></pre>
</section>
<style lang="scss" global>
@@ -68,6 +80,19 @@
}
}
.noBoxPadding {
padding: 0 !important;
}
.with-scroll {
height: 100%;
overflow: auto;
}
pre {
padding-inline-end: 7rem !important; // Add space for label and copy btn
}
code,
pre {
&[class*='language-'] {
@@ -100,7 +125,7 @@
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background: hsl(var(--p-box-background-color));
padding-block-start: 4%;
margin: 0;
}
.prism-token {
+6 -2
View File
@@ -1,15 +1,19 @@
<script lang="ts">
import { clickOnEnter } from '$lib/helpers/a11y';
export let withIndentation = false;
export let open = false;
</script>
<li class="collapsible-item">
<details class="collapsible-wrapper" {open}>
<summary class="collapsible-button">
<summary class="collapsible-button" on:keyup={clickOnEnter} on:click>
<slot name="beforetitle" />
<div>
<span class="text"><slot name="title" /></span>
<span class="collapsible-button-optional"><slot name="subtitle" /></span>
{#if $$slots.subtitle}
<span class="collapsible-button-optional"><slot name="subtitle" /></span>
{/if}
</div>
<div class="icon">
<span class="icon-cheveron-down" aria-hidden="true" />
+2 -1
View File
@@ -7,6 +7,7 @@
export let name: string;
export let id: string;
export let autofocus = true;
export let fullWidth = false;
$: if (!show) {
id = null;
@@ -21,7 +22,7 @@
}
</script>
<InnerModal bind:show>
<InnerModal bind:show {fullWidth}>
<svelte:fragment slot="title">{name} ID</svelte:fragment>
<svelte:fragment slot="subtitle">
Enter a custom {name} ID. Leave blank for a randomly generated one.
+12 -2
View File
@@ -79,16 +79,26 @@
event.target === element ||
element.contains(event.target as Node) ||
event.target === tooltip ||
tooltip.contains(event.target as Node)
tooltip.contains(event.target as Node) ||
// Avoid deleted elements triggering blur
!document.body.contains(event.target as Node)
)
) {
show = false;
dispatch('blur');
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && show) {
event.preventDefault();
show = false;
dispatch('blur');
}
};
</script>
<svelte:window on:click={onBlur} />
<svelte:window on:click={onBlur} on:keydown={onKeyDown} />
<div class:drop-wrapper={!noStyle} class:u-cross-child-start={childStart} bind:this={element}>
<slot />
+7 -1
View File
@@ -4,10 +4,16 @@
export let href: string;
export let icon: string = null;
export let disabled = false;
export let external = false;
</script>
<li class="drop-list-item" on:click on:keyup={clickOnEnter}>
<a {href} class="drop-button" class:is-disabled={disabled}>
<a
{href}
class="drop-button"
class:is-disabled={disabled}
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}>
<span class="text"><slot /></span>
{#if icon}
<span class={`icon-${icon}`} aria-hidden="true" />
+8 -5
View File
@@ -4,11 +4,12 @@
import Dark from '$lib/images/search-dark.svg';
import PaginationInline from './paginationInline.svelte';
export let hidePagination = false;
export let hidePages = false;
</script>
<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">
<div class="u-flex u-flex-vertical u-cross-center u-gap-24 u-overflow-hidden">
{#if $app.themeInUse === 'dark'}
<img src={Dark} alt="create" aria-hidden="true" />
{:else}
@@ -18,7 +19,9 @@
</div>
</article>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: 0</p>
<PaginationInline limit={1} offset={0} sum={0} {hidePages} />
</div>
{#if !hidePagination}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: 0</p>
<PaginationInline limit={1} offset={0} sum={0} {hidePages} />
</div>
{/if}
+1 -2
View File
@@ -245,8 +245,7 @@
}
</script>
<Modal bind:show onSubmit={create} size="big">
<svelte:fragment slot="header">Create Event</svelte:fragment>
<Modal title="Create event" bind:show onSubmit={create} size="big">
<slot />
<div>
<p class="u-text">Choose a service</p>
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
type NumSize = 1 | 2 | 3;
type Size = NumSize | `${NumSize}`;
export let tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export let size: Size;
export let trimmed = true;
export let id: string = null;
export let style: string = undefined;
let classes: string = undefined;
export { classes as class };
</script>
<svelte:element
this={tag}
class="eyebrow-heading-{size} {classes}"
class:u-trim-1={trimmed}
{id}
{style}>
<slot />
</svelte:element>
@@ -0,0 +1,39 @@
<script lang="ts">
import { portal } from '$lib/actions/portal';
import { fly } from 'svelte/transition';
export let show = false;
</script>
{#if show}
<div class="floating-action-bar" transition:fly|local={{ y: '6rem' }} use:portal>
<slot />
</div>
{/if}
<style lang="scss">
.floating-action-bar {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
z-index: 100;
border-radius: 0.5rem;
padding: 0.75rem 1rem;
width: clamp(0px, calc(100vw - 4rem), 32.5rem);
}
:global(.theme-dark) .floating-action-bar {
border: 1px solid hsl(var(--color-neutral-200));
background: hsl(var(--color-neutral-300));
box-shadow: 0px 6px 16px 8px #14141f;
}
:global(.theme-light) .floating-action-bar {
border: 1px solid hsl(var(--color-neutral-30));
background: hsl(var(--color-neutral-0));
box-shadow: 0px 6px 16px 0px rgba(55, 59, 77, 0.14);
}
</style>
+4 -1
View File
@@ -1,6 +1,9 @@
<script lang="ts">
type NumSize = 1 | 2 | 3 | 4 | 5 | 6 | 7;
type Size = NumSize | `${NumSize}`;
export let tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export let size: '1' | '2' | '3' | '4' | '5' | '6' | '7';
export let size: Size;
export let trimmed = true;
export let id: string = null;
</script>
+2
View File
@@ -3,6 +3,7 @@
export let value: string;
export let event: string = null;
export let centered = true;
function truncateText(node: HTMLElement) {
const MAX_TRIES = 100;
@@ -49,6 +50,7 @@
<Copy {value} {event}>
<div
class="interactive-text-output is-buttons-on-top"
class:u-text-center={centered}
style:min-inline-size="0"
style:display="inline-flex">
<span
+9
View File
@@ -1,3 +1,4 @@
export { default as Arrow } from './arrow.svelte';
export { default as Modal } from './modal.svelte';
export { default as Pagination } from './pagination.svelte';
export { default as PaginationInline } from './paginationInline.svelte';
@@ -28,6 +29,7 @@ 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 BoxAvatar } from './boxAvatar.svelte';
export { default as Search } from './search.svelte';
export { default as SearchQuery } from './searchQuery.svelte';
export { default as GridItem1 } from './gridItem1.svelte';
@@ -51,3 +53,10 @@ export { default as PaginationWithLimit } from './paginationWithLimit.svelte';
export { default as ClickableList } from './clickableList.svelte';
export { default as ClickableListItem } from './clickableListItem.svelte';
export { default as Id } from './id.svelte';
export { default as NumericList } from './numericList.svelte';
export { default as NumericListItem } from './numericListItem.svelte';
export { default as EyebrowHeading } from './eyebrowHeading.svelte';
export { default as SvgIcon } from './svgIcon.svelte';
export { default as MigrationBox } from './migrationBox.svelte';
export { default as FloatingActionBar } from './floatingActionBar.svelte';
export { default as LoadingDots } from './loadingDots.svelte';
+2 -1
View File
@@ -3,11 +3,12 @@
export let show = false;
export let closable = true;
export let fullWidth = false;
</script>
{#if show}
<FormItem>
<section class="modal is-inner-modal">
<section class="modal is-inner-modal" class:u-width-full-line={fullWidth}>
<div class="modal-form">
<header class="modal-header">
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
+14 -6
View File
@@ -7,6 +7,8 @@
export let icon: string = null;
export let fullHeight = true;
export let borderRadius: 'xsmall' | 'small' | 'medium' | 'large' = 'small';
export let backgroundColor: string = null;
export let backgroundColorHover: string = null;
enum Radius {
xsmall = '--border-radius-xsmall',
@@ -20,8 +22,10 @@
class="card is-allow-focus u-cursor-pointer"
class:u-height-100-percent={fullHeight}
style:--card-padding={`${padding}rem`}
style:--card-border-radius={`var(${Radius[borderRadius]})`}>
<div class="u-flex u-gap-16">
style:--card-border-radius={`var(${Radius[borderRadius]})`}
style:--p-card-bg-color-default={backgroundColor}
style:--p-card-bg-color-hover={backgroundColorHover}>
<div class="u-flex u-gap-8">
<input
class="is-small u-margin-block-start-2"
type="radio"
@@ -31,10 +35,14 @@
bind:group
on:click />
<div class="u-flex u-flex-vertical u-gap-4">
<h4 class="body-text-2 u-bold"><slot name="title" /></h4>
<p class="u-color-text-gray u-small">
<slot />
</p>
{#if $$slots.title}
<h4 class="body-text-2 u-bold"><slot name="title" /></h4>
{/if}
{#if $$slots.default}
<p class="u-color-text-gray u-small">
<slot />
</p>
{/if}
</div>
{#if icon}
<span class={`icon-${icon} u-margin-inline-start-auto`} aria-hidden="true" />
+56
View File
@@ -0,0 +1,56 @@
<!-- Loading dots -->
<script>
let className = '';
export { className as class };
</script>
<div class="loading-dots {className}">
<div class="dot" />
<div class="dot" />
<div class="dot" />
</div>
<style>
.loading-dots {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
gap: 0.25rem;
padding-inline: 0.5rem;
}
.dot {
width: 0.25rem;
height: 0.25rem;
border-radius: 50%;
background-color: hsl(var(--p-body-text-color));
animation: loading-dots 1s infinite ease-in-out;
}
.dot:nth-child(1) {
animation-delay: 0.1s;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.3s;
}
@keyframes loading-dots {
0% {
transform: translateY(0.25rem);
}
25% {
transform: translateY(0rem);
}
50%,
100% {
transform: translateY(0.25rem);
}
}
</style>
+101
View File
@@ -0,0 +1,101 @@
<script lang="ts" context="module">
import { last } from '$lib/helpers/array';
import { debounce } from '$lib/helpers/debounce';
import { parseIfString } from '$lib/helpers/object';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
export const showMigrationBox = writable(false);
</script>
<script lang="ts">
let migration: Models.Migration;
type Counter = {
pending: number;
error: number;
success: number;
processing: number;
skip: number;
warning: number;
};
type TotalCounter = {
done: number;
processing: number;
};
$: percentage = (function getPercentage() {
if (!migration) return 0;
if (migration.status === 'failed') return 100;
if (migration.status === 'completed') return 100;
const statusCounters = parseIfString(migration.statusCounters) as Record<string, Counter>;
const totalCounter: TotalCounter = Object.values(statusCounters).reduce(
(curr, acc) => {
return {
done: curr.done + acc.success + acc.error + acc.skip + acc.warning,
processing: curr.processing + acc.processing + acc.pending
};
},
{ done: 0, processing: 0 } as TotalCounter
);
const res = Math.round(
(totalCounter.done / (totalCounter.done + totalCounter.processing)) * 100
);
return Number.isNaN(res) ? 0 : res;
})();
const fetchMigrations = debounce(async () => {
const { migrations } = await sdk.forProject.migrations.list();
migration = last(migrations);
}, 1000);
fetchMigrations();
onMount(async () => {
return sdk.forConsole.client.subscribe(['project', 'console'], async (response) => {
if (response.events.includes('migrations.*')) {
fetchMigrations();
}
});
});
</script>
{#if $showMigrationBox && migration}
<section class="upload-box is-float">
<header class="upload-box-header">
<h4 class="upload-box-title">
<span class="text">Importing Data</span>
</h4>
<button
class="upload-box-button"
aria-label="close migration box"
on:click={() => ($showMigrationBox = false)}>
<span class="icon-x" aria-hidden="true" />
</button>
</header>
<div class="upload-box-content is-open">
<section class="progress-bar">
<div class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
<span>{percentage}%</span>
</div>
<div
class="progress-bar-container"
class:is-danger={migration.status === 'failed'}
style="--graph-size:{percentage}%" />
</section>
</div>
</section>
{/if}
<style>
.upload-box-content {
padding: 1.5rem;
min-width: 400px;
max-width: 100vw;
}
</style>
+15 -2
View File
@@ -3,6 +3,7 @@
import { Alert } from '$lib/components';
import { trackEvent } from '$lib/actions/analytics';
import { Form } from '$lib/elements/forms';
import { disableCommands } from '$lib/commandCenter';
export let show = false;
export let size: 'small' | 'big' = null;
@@ -11,9 +12,11 @@
export let error: string = null;
export let closable = true;
export let headerDivider = true;
export let onSubmit: () => Promise<void> | void = function () {
export let onSubmit: (e: SubmitEvent) => Promise<void> | void = function () {
return;
};
export let title = '';
export let description = '';
let dialog: HTMLDialogElement;
let alert: HTMLElement;
@@ -70,6 +73,8 @@
closeModal();
}
$: $disableCommands(show);
$: if (error) {
alert?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
}
@@ -99,8 +104,11 @@
<span class={`icon-${icon}`} aria-hidden="true" />
</div>
{/if}
<h4 class="modal-title heading-level-5">
<slot name="header" />
<slot name="title">
{title}
</slot>
</h4>
</div>
{#if closable}
@@ -119,6 +127,11 @@
</button>
{/if}
</div>
<p>
<slot name="description">
{description}
</slot>
</p>
</header>
<div class="modal-content">
{#if error}
+3
View File
@@ -0,0 +1,3 @@
<ol class="numeric-list">
<slot />
</ol>
@@ -0,0 +1,7 @@
<script lang="ts">
export let fullWidth = false;
</script>
<li class="numeric-list-item">
<div class="u-margin-block-start-8" class:u-width-full-line={fullWidth}><slot /></div>
</li>
+1 -2
View File
@@ -25,8 +25,7 @@
$: disabled = !value || $groups.has(value);
</script>
<Modal bind:show on:close={reset} onSubmit={create}>
<svelte:fragment slot="header">Custom permission</svelte:fragment>
<Modal title="Custom permission" bind:show on:close={reset} onSubmit={create}>
<p class="text">
Custom permissions allow you to grant access to specific users or teams using their ID and
role.
@@ -19,8 +19,8 @@
TableRow
} from '$lib/elements/table';
import { symmetricDifference } from '$lib/helpers/array';
import { onDestroy, onMount } from 'svelte';
import { writable, type Unsubscriber } from 'svelte/store';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import Actions from './actions.svelte';
import Row from './row.svelte';
import Table from '$lib/elements/table/table.svelte';
@@ -32,13 +32,12 @@
let showTeam = false;
let showCustom = false;
let showDropdown = false;
let unsubscribe: Unsubscriber;
const groups = writable<Map<string, Permission>>(new Map());
onMount(() => {
permissions.forEach(fromPermissionString);
unsubscribe = groups.subscribe(() => {
return groups.subscribe(() => {
const current = exportRoles();
if (symmetricDifference(current, permissions).length) {
permissions = current;
@@ -46,12 +45,6 @@
});
});
onDestroy(() => {
if (unsubscribe) {
unsubscribe();
}
});
function create(event: CustomEvent<string[]>) {
for (const role of event.detail) {
addRole(role);
+3 -10
View File
@@ -10,8 +10,8 @@
TableRow
} from '$lib/elements/table';
import { symmetricDifference } from '$lib/helpers/array';
import { onDestroy, onMount } from 'svelte';
import { writable, type Unsubscriber } from 'svelte/store';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import Actions from './actions.svelte';
import type { Permission } from './permissions.svelte';
import Row from './row.svelte';
@@ -22,13 +22,12 @@
let showTeam = false;
let showCustom = false;
let showDropdown = false;
let unsubscribe: Unsubscriber;
const groups = writable<Map<string, Permission>>(new Map());
onMount(() => {
roles.forEach(addRole);
unsubscribe = groups.subscribe((n) => {
return groups.subscribe((n) => {
const current = Array.from(n.keys());
if (symmetricDifference(current, roles).length) {
roles = current;
@@ -36,12 +35,6 @@
});
});
onDestroy(() => {
if (unsubscribe) {
unsubscribe();
}
});
function create(event: CustomEvent<string[]>) {
for (const role of event.detail) {
addRole(role);
+1 -2
View File
@@ -61,8 +61,7 @@
}
</script>
<Modal bind:show onSubmit={create} on:close={reset} size="big">
<svelte:fragment slot="header">Select teams</svelte:fragment>
<Modal title="Select teams" bind:show onSubmit={create} on:close={reset} size="big">
<p class="text">
Grant access to any member of a specific team. To grant access to team members with specific
roles, you will need to set a <button
+1 -2
View File
@@ -61,8 +61,7 @@
}
</script>
<Modal bind:show onSubmit={create} on:close={reset} size="big">
<svelte:fragment slot="header">Select users</svelte:fragment>
<Modal title="Select users" bind:show onSubmit={create} on:close={reset} size="big">
<p class="text">Grant access to any authenticated or anonymous user.</p>
<InputSearch
autofocus
+7 -1
View File
@@ -1,3 +1,9 @@
<ul class="secondary-tabs">
<script lang="ts">
export let large = false;
let classes: string = undefined;
export { classes as class };
</script>
<ul class="secondary-tabs {classes}" class:is-large={large}>
<slot />
</ul>
+23 -5
View File
@@ -1,21 +1,39 @@
<script lang="ts">
export let href: string;
export let href: string = null;
export let disabled = false;
export let stretch = false;
export let fullWidth = false;
export let center = false;
</script>
<li class="secondary-tabs-item">
<li class="secondary-tabs-item" class:u-stretch={stretch}>
{#if href}
{#if disabled}
<button class="secondary-tabs-button" disabled>
<button
class="secondary-tabs-button"
class:u-width-full-line={fullWidth}
class:u-text-center={center}
disabled
type="button">
<span class="text"><slot /></span>
</button>
{:else}
<a class="secondary-tabs-button" {href}>
<a
class="secondary-tabs-button"
class:u-width-full-line={fullWidth}
class:u-text-center={center}
{href}>
<span class="text"><slot /></span>
</a>
{/if}
{:else}
<button class="secondary-tabs-button" {disabled} on:click>
<button
class="secondary-tabs-button"
class:u-width-full-line={fullWidth}
class:u-text-center={center}
type="button"
{disabled}
on:click>
<span class="text"><slot /></span>
</button>
{/if}
+25
View File
@@ -0,0 +1,25 @@
<script lang="ts">
import { iconPath } from '$lib/stores/app';
export let name: string;
export let type: 'color' | 'grayscale' = 'color';
export let size = 40;
export let iconSize: keyof typeof iconSizes = 'medium';
let className = '';
export { className as class };
enum iconSizes {
small = '--icon-size-small',
medium = '--icon-size-medium',
large = '--icon-size-large',
xlarge = '--icon-size-extra-large'
}
</script>
<img
class={className}
width={size}
height={size}
style:inline-size={`var(${iconSizes[iconSize]})`}
src={$iconPath(name, type)}
alt={name} />
-1
View File
@@ -22,7 +22,6 @@
await goto(href);
await waitUntil(() => {
console.log('tickUntil', el);
return el.classList.contains('is-selected');
}, 1000);
el.focus();
+4 -2
View File
@@ -3,6 +3,7 @@
import { throttle } from '$lib/helpers/functions';
import { onMount } from 'svelte';
export let alternativeTrim = false;
let showTooltip = false;
let container: HTMLSpanElement | null;
@@ -15,11 +16,12 @@
<svelte:window on:resize={throttle(onResize, 250)} />
<span class="text u-trim" bind:this={container}>
<span class={`text ${alternativeTrim ? 'u-trim-1' : 'u-trim'}`} bind:this={container}>
{#if showTooltip}
<span
use:tooltip={{
content: container.innerText
content: container.innerText,
maxWidth: '30rem'
}}>
<slot />
</span>
+2 -1
View File
@@ -23,6 +23,7 @@
export let hideView = false;
export let hideColumns = false;
export let allowNoColumns = false;
export let showColsTextMobile = false;
let showSelectColumns = false;
@@ -84,7 +85,7 @@
class="icon-view-boards u-opacity-50"
aria-hidden="true"
aria-label="columns" />
<span class="text">Columns</span>
<span class="text {showColsTextMobile ? '' : 'is-only-desktop'}">Columns</span>
<span class="inline-tag">{selectedColumnsNumber}</span>
</Button>
<svelte:fragment slot="list">
+18 -1
View File
@@ -5,6 +5,8 @@ export const INTERVAL = 5 * 60000; // default interval to check for feedback
export enum Dependencies {
ORGANIZATION = 'dependency:organization',
PROJECT = 'dependency:project',
PROJECT_VARIABLES = 'dependency:project_variables',
PROJECT_INSTALLATIONS = 'dependency:project_installations',
PROJECTS = 'dependency:projects',
ACCOUNT = 'dependency:account',
ACCOUNT_SESSIONS = 'dependency:account_sessions',
@@ -23,8 +25,11 @@ export enum Dependencies {
FILE = 'dependency:file',
FILES = 'dependency:files',
FUNCTION = 'dependency:function',
FUNCTION_DOMAINS = 'dependency:function_domains',
FUNCTION_INSTALLATIONS = 'dependency:function_installations',
FUNCTIONS = 'dependency:functions',
VARIABLES = 'dependency:variables',
DEPLOYMENT = 'dependency:deployment',
DEPLOYMENTS = 'dependency:deployments',
EXECUTIONS = 'dependency:executions',
PLATFORM = 'dependency:platform',
@@ -33,7 +38,9 @@ export enum Dependencies {
KEYS = 'dependency:keys',
DOMAINS = 'dependency:domains',
WEBHOOK = 'dependency:webhook',
WEBHOOKS = 'dependency:webhooks'
WEBHOOKS = 'dependency:webhooks',
MIGRATIONS = 'dependency:migrations',
COLLECTIONS = 'dependency:collections'
}
export const scopes: {
@@ -168,6 +175,16 @@ export const scopes: {
scope: 'health.read',
description: "Access to read your project's health status",
category: 'Other'
},
{
scope: 'migrations.read',
description: "Access to read your project's migration status",
category: 'Other'
},
{
scope: 'migrations.write',
description: 'Access to create migrations',
category: 'Other'
}
];
+25 -19
View File
@@ -3,6 +3,7 @@
import { getContext, hasContext } from 'svelte';
import { readable } from 'svelte/store';
import type { FormContext } from './form.svelte';
import { multiAction, type MultiActionArray } from '$lib/actions/multi-actions';
export let submit = false;
export let secondary = false;
@@ -17,6 +18,9 @@
export let ariaLabel: string = null;
export let noMargin = false;
export let event: string = null;
let classes: string = undefined;
export { classes as class };
export let actions: MultiActionArray = [];
const isSubmitting = hasContext('form')
? getContext<FormContext>('form').isSubmitting
@@ -33,6 +37,21 @@
from: 'button'
});
}
$: resolvedClasses = [
'button',
disabled && 'is-disabled',
round && 'is-only-icon',
secondary && 'is-secondary',
github && 'is-github',
text && 'is-text',
danger && 'is-danger',
fullWidth && 'is-full-width',
noMargin && 'u-padding-inline-0',
classes
]
.filter(Boolean)
.join(' ');
</script>
{#if href}
@@ -41,16 +60,9 @@
{href}
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}
class="button"
class:is-disabled={disabled}
class:is-only-icon={round}
class:is-secondary={secondary}
class:is-github={github}
class:is-text={text}
class:is-danger={danger}
class:is-full-width={fullWidth}
class:u-padding-inline-0={noMargin}
aria-label={ariaLabel}>
class={resolvedClasses}
aria-label={ariaLabel}
use:multiAction={actions}>
<slot />
</a>
{:else}
@@ -58,16 +70,10 @@
on:click
on:click={track}
disabled={internalDisabled}
class="button"
class:is-only-icon={round}
class:is-secondary={secondary}
class:is-github={github}
class:is-danger={danger}
class:is-text={text}
class:is-full-width={fullWidth}
class:u-padding-inline-0={noMargin}
class={resolvedClasses}
aria-label={ariaLabel}
type={submit ? 'submit' : 'button'}
aria-label={ariaLabel}>
use:multiAction={actions}>
<slot />
</button>
{/if}
+3 -3
View File
@@ -13,15 +13,15 @@
export let noMargin = false;
export let noStyle = false;
export let isModal = false;
export let onSubmit: () => Promise<void> | void;
export let onSubmit: (e: SubmitEvent) => Promise<void> | void;
const { isSubmitting } = setContext<FormContext>('form', {
isSubmitting: writable(false)
});
async function submit() {
async function submit(e: SubmitEvent) {
isSubmitting.set(true);
await onSubmit();
await onSubmit(e);
isSubmitting.set(false);
}
</script>
+2 -1
View File
@@ -1,7 +1,8 @@
<script lang="ts">
export let fullWidth = false;
export let isMultiple = false;
</script>
<li class="form-item" class:u-width-full-line={fullWidth}>
<li class="form-item" class:is-multiple={isMultiple} class:u-width-full-line={fullWidth}>
<slot />
</li>
@@ -0,0 +1,8 @@
<script lang="ts">
export let fullWidth = false;
export let alignEnd = false;
</script>
<div class="form-item-part" class:u-cross-child-end={alignEnd} class:u-stretch={fullWidth}>
<slot />
</div>
+3 -1
View File
@@ -1,10 +1,12 @@
<script lang="ts">
export let isCommonSection = false;
export let gap = 24;
let classes = '';
export { classes as class };
</script>
<ul
class="form-list"
class="form-list {classes}"
class:common-section={isCommonSection}
style={`--form-list-gap: ${gap / 16}rem;`}>
<slot />
+1
View File
@@ -1,5 +1,6 @@
export { default as Form } from './form.svelte';
export { default as FormItem } from './formItem.svelte';
export { default as FormItemPart } from './formItemPart.svelte';
export { default as FormList } from './formList.svelte';
export { default as Button } from './button.svelte';
export { default as InputDomain } from './inputDomain.svelte';
+13 -6
View File
@@ -1,15 +1,17 @@
<script lang="ts">
import { FormItem, Helper, Label } from '.';
export let label: string;
export let label: string | undefined = undefined;
export let optionalText: string | undefined = undefined;
export let tooltip: string = null;
export let showLabel = true;
export let id: string;
export let value = false;
export let indeterminate = false;
export let required = false;
export let disabled = false;
let element: HTMLInputElement;
export let element: HTMLInputElement | undefined = undefined;
let error: string;
const handleInvalid = (event: Event) => {
@@ -27,19 +29,24 @@
</script>
<FormItem>
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{#if label}
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{/if}
<div class="input-text-wrapper">
<input
{id}
{disabled}
{required}
{indeterminate}
type="checkbox"
bind:this={element}
bind:checked={value}
on:invalid={handleInvalid} />
on:invalid={handleInvalid}
on:click
on:change />
</div>
{#if error}
<Helper type="warning">{error}</Helper>
+24 -2
View File
@@ -8,6 +8,7 @@
export let value = false;
export let required = false;
export let disabled = false;
export let tooltip: string = null;
let element: HTMLInputElement;
let error: string;
@@ -39,8 +40,29 @@
on:invalid={handleInvalid} />
<div class="choice-item-content">
<div class:u-hide={!showLabel} class="choice-item-title">{label}</div>
{#if $$slots}
{#if (label && showLabel) || tooltip}
<div class="u-flex u-gap-4">
{#if label}
<h6 class:u-hide={!showLabel} class="choice-item-title">
{label}
</h6>
{/if}
{#if tooltip}
<button type="button" class="tooltip" aria-label="variables info">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
<span class="tooltip-popup" role="tooltip">
<p class="text">
{tooltip}
</p>
</span>
</button>
{/if}
</div>
{/if}
{#if $$slots.default}
<p class="choice-item-paragraph"><slot /></p>
{/if}
</div>
+2 -1
View File
@@ -15,6 +15,7 @@
export let readonly = false;
export let autofocus = false;
export let autocomplete = false;
export let tooltip: string = null;
let element: HTMLInputElement;
let error: string;
@@ -55,7 +56,7 @@
</script>
<FormItem>
<Label {required} {optionalText} hide={!showLabel} for={id}>
<Label {required} {optionalText} {tooltip} hide={!showLabel} for={id}>
{label}
</Label>
+10 -3
View File
@@ -2,7 +2,7 @@
import { Trim } from '$lib/components';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { onMount } from 'svelte';
import { Helper } from '.';
import { Helper, Label } from '.';
export let label: string = null;
export let files: FileList;
@@ -10,6 +10,8 @@
export let allowedFileExtensions: string[] = [];
export let maxSize: number = null;
export let required = false;
export let optionalText: string = null;
export let tooltip: string = null;
export let error: string = null;
let input: HTMLInputElement;
@@ -20,7 +22,9 @@
const hasInvalidExt = Array.from(value).some((file) => {
const fileExtension = file.name.split('.').pop();
return !allowedFileExtensions.includes(fileExtension);
return allowedFileExtensions?.length
? !allowedFileExtensions.includes(fileExtension)
: false;
});
if (hasInvalidExt) {
error = 'Invalid file extension';
@@ -89,11 +93,14 @@
<div>
{#if label}
<p class="text">{label}</p>
<Label {required} {optionalText} {tooltip} hide={!label}>
{label}
</Label>
{/if}
<div
class="box is-no-shadow u-padding-24"
style="--box-border-radius:var(--border-radius-xsmall); z-index: 1"
class:u-margin-block-start-8={!!label}
class:is-border-dashed={!hovering}
class:is-hover-with-file={hovering}
on:drop|preventDefault={dropHandler}
+2 -2
View File
@@ -47,12 +47,12 @@
</div>
</FormItem>
<div
class="u-flex u-gap-4 u-margin-block-start-8 u-small u-cross-center"
class="u-flex u-gap-4 u-margin-block-start-8 u-small"
class:u-color-text-warning={icon === 'exclamation'}>
<span
class:icon-info={icon === 'info'}
class:icon-exclamation={icon === 'exclamation'}
class="u-cross-center u-line-height-1 u-icon-small u-color-text-gray"
class="u-cross-center u-line-height-1 u-color-text-gray"
aria-hidden="true" />
<span class="text u-line-height-1-5">
Allowed characters: alphanumeric, non-leading hyphen, underscore, period
+6 -4
View File
@@ -3,7 +3,7 @@
import { FormItem, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
export let label: string;
export let label: string | undefined = undefined;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let id: string;
@@ -65,9 +65,11 @@
</script>
<FormItem>
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{#if label}
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{/if}
<div class="input-text-wrapper">
<input
+1 -1
View File
@@ -51,7 +51,7 @@
{label}
</Label>
<div class="input-text-wrapper">
<div class="input-text-wrapper" style={showPasswordButton ? '--amount-of-buttons: 1' : ''}>
{#if showInPlainText}
<input
{id}
+7 -4
View File
@@ -2,12 +2,13 @@
import { FormItem, Helper, Label } from '.';
export let id: string;
export let label: string;
export let label: string | undefined = undefined;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let value: string | number | boolean;
export let placeholder = '';
export let required = false;
export let hideRequired = false;
export let disabled = false;
export let options: {
value: string | boolean | number;
@@ -45,9 +46,11 @@
</script>
<FormItem>
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{#if label}
<Label {required} {hideRequired} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{/if}
<div class="select">
<select
@@ -16,13 +16,17 @@
export let label: string;
export let name = 'elements';
export let optionalText: string | undefined = undefined;
export let tooltip: string | undefined = undefined;
export let showLabel = true;
export let placeholder = '';
export let required = false;
export let hideRequired = false;
export let disabled = false;
export let fullWidth = false;
export let autofocus = false;
export let interactiveOutput = false;
// Input value
// stretch is used inside of a flex container to give the element flex:1
export let stretch = true;
export let search = '';
// The actual selected value
export let value: Option['value'];
@@ -82,9 +86,13 @@
value = option.value;
search = option.label;
// It's not working without this line.
!$$slots.output && (element.value = search);
if (!$$slots.output) {
element.value = search;
} else {
search = '';
}
hasFocus = false;
search = '';
dispatch('select', option);
}
@@ -99,7 +107,10 @@
$: showClearBtn = (hasFocus && search) || value;
</script>
<li class="u-position-relative form-item u-stretch">
<li
class="u-position-relative form-item"
class:u-width-full-line={fullWidth}
class:u-stretch={stretch}>
<DropList
bind:show={hasFocus}
noStyle
@@ -109,7 +120,7 @@
position="static"
fullWidth={true}
fixed>
<Label {required} {optionalText} hide={!showLabel} for={id}>
<Label {required} {hideRequired} {optionalText} hide={!showLabel} for={id} {tooltip}>
{label}
</Label>
@@ -43,6 +43,7 @@
</div>
<div class="choice-item-content">
<div class="choice-item-title">{label}</div>
<slot name="description" />
</div>
</label>
{#if error}
+26 -10
View File
@@ -1,16 +1,18 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import { FormItem, FormItemPart, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
import TextCounter from './textCounter.svelte';
export let label: string;
export let label: string = undefined;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let id: string;
export let name: string = id;
export let value = '';
export let placeholder = '';
export let required = false;
export let hideRequired = false;
export let nullable = false;
export let disabled = false;
export let readonly = false;
@@ -19,6 +21,7 @@
export let fullWidth = false;
export let maxlength: number = null;
export let tooltip: string = null;
export let isMultiple = false;
let element: HTMLInputElement;
let error: string;
@@ -40,8 +43,11 @@
error = element.validationMessage;
};
$: if (value) {
error = null;
$: {
value;
if (element?.validity?.valid) {
error = null;
}
}
let prevValue = '';
@@ -57,16 +63,25 @@
$: showTextCounter = !!maxlength;
$: showNullCheckbox = nullable && !required;
type $$Events = {
input: Event & { target: HTMLInputElement };
};
$: wrapper = isMultiple ? FormItemPart : FormItem;
</script>
<FormItem {fullWidth}>
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
<svelte:component this={wrapper} {fullWidth}>
{#if label}
<Label {required} {hideRequired} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{/if}
<div class="input-text-wrapper">
<input
{id}
{name}
{placeholder}
{disabled}
{readonly}
@@ -78,7 +93,8 @@
bind:value
class:u-padding-inline-end-56={typeof maxlength === 'number'}
bind:this={element}
on:invalid={handleInvalid} />
on:invalid={handleInvalid}
on:input />
{#if showTextCounter || showNullCheckbox}
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
@@ -98,4 +114,4 @@
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
</FormItem>
</svelte:component>
+4 -1
View File
@@ -3,10 +3,12 @@
import { FormItem, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
import TextCounter from './textCounter.svelte';
import { clickOnEnter } from '$lib/helpers/a11y';
export let label: string;
export let showLabel = true;
export let id: string;
export let name: string = id;
export let value = '';
export let placeholder = '';
export let required = false;
@@ -60,9 +62,10 @@
{label}
</Label>
<div class="input-text-wrapper">
<div class="input-text-wrapper" on:click on:keyup={clickOnEnter}>
<textarea
{id}
{name}
{placeholder}
{disabled}
{readonly}
+9 -3
View File
@@ -1,6 +1,7 @@
<script lang="ts">
interface $$Props extends Partial<HTMLLabelElement> {
required?: boolean;
hideRequired?: boolean;
optionalText?: string | undefined;
hide?: boolean;
tooltip?: string;
@@ -8,12 +9,17 @@
}
export let required: $$Props['required'] = false;
export let hideRequired: $$Props['hideRequired'] = false;
export let optionalText: $$Props['optionalText'] = undefined;
export let hide: $$Props['hide'] = false;
export let tooltip: $$Props['tooltip'] = null;
</script>
<label class:is-required={required} class:u-hide={hide} class="label" {...$$restProps}>
<label
class:is-required={required && !hideRequired}
class:u-hide={hide}
class="label"
{...$$restProps}>
<slot />
</label>
@@ -22,8 +28,8 @@
{/if}
{#if tooltip}
<button class="tooltip" aria-label="variables info">
<span class="icon-info" aria-hidden="true" />
<button type="button" on:click|preventDefault class="tooltip" aria-label="input tooltip">
<span class="icon-info" aria-hidden="true" style="font-size: var(--icon-size-small)" />
<span class="tooltip-popup" role="tooltip">
<p class="text">
{tooltip}
+3 -1
View File
@@ -3,11 +3,13 @@
export let onlyDesktop = false;
export let width: number = null;
export let showOverflow = false;
let className = '';
export { className as class };
</script>
<div
style={width ? `--p-col-width:${width?.toString()}` : ''}
class="table-col"
class="table-col {className}"
class:u-overflow-visible={showOverflow}
class:is-only-desktop={onlyDesktop}
data-title={title}
+40
View File
@@ -0,0 +1,40 @@
<script lang="ts">
import { toggle } from '$lib/helpers/array';
import { isHTMLInputElement } from '$lib/helpers/types';
import { TableCell } from '.';
import { InputCheckbox } from '../forms';
export let id: string;
export let selectedIds: string[] = [];
let el: HTMLInputElement;
const handleClick = (e: Event) => {
// Prevent the link from being followed
e.preventDefault();
if (!isHTMLInputElement(el)) return;
selectedIds = toggle(selectedIds, id);
// Hack to make sure the checkbox is checked, independent of the
// preventDefault() call above
window.setTimeout(() => {
el.checked = selectedIds.includes(id);
});
};
</script>
<TableCell class="u-position-relative">
<div class="touch-area" on:click={handleClick} on:keypress={handleClick} />
<InputCheckbox
bind:element={el}
id="select-{id}"
value={selectedIds.includes(id)}
on:click={handleClick} />
</TableCell>
<style lang="scss">
.touch-area {
position: absolute;
inset: 0;
}
</style>
@@ -0,0 +1,33 @@
<script lang="ts">
import { isHTMLInputElement } from '$lib/helpers/types';
import { TableCellHead } from '.';
import { InputCheckbox } from '../forms';
export let selected: string[] = [];
export let pageItemsIds: string[] = [];
function handleClick(e: MouseEvent) {
if (!isHTMLInputElement(e.target)) return;
if (e.target.checked) {
const set = new Set(selected);
pageItemsIds.forEach((id) => set.add(id));
selected = Array.from(set);
} else {
selected = selected.filter((id) => {
return !pageItemsIds.includes(id);
});
}
}
$: someSelected = pageItemsIds.some((id) => selected.includes(id));
$: allSelected = pageItemsIds.every((id) => selected.includes(id));
</script>
<TableCellHead width={10}>
<InputCheckbox
id="select-all"
indeterminate={someSelected && !allSelected}
value={allSelected}
on:click={handleClick} />
</TableCellHead>
+10 -1
View File
@@ -1,8 +1,17 @@
<script lang="ts">
export let href: string;
export let title: string;
export let external = false;
export let noStyle = false;
</script>
<div class="table-col" data-title={title} role="cell" data-private>
<a role="button" tabindex="0" class="link" {href}><slot /></a>
<a
role="button"
tabindex="0"
class:link={!noStyle}
{href}
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}><slot /></a>
</div>
+2
View File
@@ -9,6 +9,8 @@ export { default as TableRowLink } from './rowLink.svelte';
export { default as TableRowButton } from './rowButton.svelte';
export { default as TableCell } from './cell.svelte';
export { default as TableCellHead } from './cellHead.svelte';
export { default as TableCellHeadCheck } from './cellHeadCheck.svelte';
export { default as TableCellLink } from './cellLink.svelte';
export { default as TableCellAvatar } from './cellAvatar.svelte';
export { default as TableCellText } from './cellText.svelte';
export { default as TableCellCheck } from './cellCheck.svelte';
+2
View File
@@ -1,12 +1,14 @@
<script lang="ts">
export let noMargin = false;
export let noStyles = false;
export let style = '';
</script>
<div
class="table is-selected-columns-mobile"
class:u-margin-block-start-32={!noMargin}
class:is-remove-outer-styles={noStyles}
{style}
role="table"
data-private>
<slot />

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