From fc2932bef8361e1ee3a326ee1c8cd226e72df9c3 Mon Sep 17 00:00:00 2001 From: tglide <26071571+TGlide@users.noreply.github.com> Date: Mon, 15 May 2023 19:36:54 +0100 Subject: [PATCH] feat: command center POC --- src/lib/components/dialog.svelte | 89 ++++++++++++++ src/lib/components/index.ts | 1 + src/lib/helpers/commandCenter.ts | 98 ++++++++++++++++ src/routes/console/+layout.svelte | 29 +++++ src/routes/console/commandCenter.svelte | 109 ++++++++++++++++++ .../project-[project]/auth/+page.svelte | 40 +++++-- .../project-[project]/databases/+page.svelte | 27 +++-- .../project-[project]/functions/+page.svelte | 14 ++- .../overview/platforms/+page.svelte | 6 + .../project-[project]/storage/+page.svelte | 13 +++ src/routes/console/project-[project]/store.ts | 47 ++++++++ 11 files changed, 455 insertions(+), 18 deletions(-) create mode 100644 src/lib/components/dialog.svelte create mode 100644 src/lib/helpers/commandCenter.ts create mode 100644 src/routes/console/commandCenter.svelte diff --git a/src/lib/components/dialog.svelte b/src/lib/components/dialog.svelte new file mode 100644 index 000000000..aa055883e --- /dev/null +++ b/src/lib/components/dialog.svelte @@ -0,0 +1,89 @@ + + + + + + {#if show} + + {/if} + + + diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts index f891b8255..b775626ff 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/index.ts @@ -54,3 +54,4 @@ 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 Dialog } from './dialog.svelte'; diff --git a/src/lib/helpers/commandCenter.ts b/src/lib/helpers/commandCenter.ts new file mode 100644 index 000000000..ced51ded4 --- /dev/null +++ b/src/lib/helpers/commandCenter.ts @@ -0,0 +1,98 @@ +import { onDestroy } from 'svelte'; +import { derived, writable } from 'svelte/store'; + +type Command = { + keys: string[]; + /* Ctrl on Windows/Linux, Meta on Mac */ + meta?: boolean; + shift?: boolean; + alt?: boolean; + callback: () => void; + label?: string; + disabled?: boolean; + forceEnable?: boolean; +}; + +type CommandCenterState = { + commandMap: Map; + enabled: boolean; +}; + +export const commandCenter = (function init() { + const store = writable({ + commandMap: new Map(), + enabled: true + }); + + return { + ...store + }; +})(); + +export const commands = derived(commandCenter, ($commandCenter) => { + return Array.from($commandCenter.commandMap.values()).flat(); +}); + +export function CommandRegistrant() { + const uuid = crypto.randomUUID(); + + onDestroy(() => { + commandCenter.update((curr) => { + curr.commandMap.delete(uuid); + return curr; + }); + }); + + return { + register(newCommands: Command[]) { + commandCenter.update((curr) => { + curr.commandMap.set(uuid, newCommands); + return curr; + }); + } + }; +} + +export function ExtendCommandRegistrant(baseCommands: Command[]) { + return () => { + const registrant = CommandRegistrant(); + + return { + register: (newCommands?: Command[]) => { + registrant.register([...baseCommands, ...(newCommands ?? [])]); + } + }; + }; +} + +export const commandCenterKeyDownHandler = derived(commandCenter, ({ commandMap, enabled }) => { + const commandsArr = Array.from(commandMap.values()).flat(); + + return (event: KeyboardEvent) => { + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + + for (const command of commandsArr) { + if (command.disabled || (!enabled && !command.forceEnable)) continue; + + const { keys, meta, shift, alt, callback } = command; + + const keyCode = event.keyCode; + const isMetaPressed = meta ? (isMac ? event.metaKey : event.ctrlKey) : true; + const isShiftPressed = shift ? event.shiftKey : true; + const isAltPressed = alt ? event.altKey : true; + + const commandKeyCodes = keys.map((key) => key.toUpperCase().charCodeAt(0)); + + if ( + commandKeyCodes.includes(keyCode) && + isMetaPressed && + isShiftPressed && + isAltPressed + ) { + event.preventDefault(); + callback(); + return; + } + } + }; +}); diff --git a/src/routes/console/+layout.svelte b/src/routes/console/+layout.svelte index 870794b40..df65ef6e0 100644 --- a/src/routes/console/+layout.svelte +++ b/src/routes/console/+layout.svelte @@ -15,6 +15,31 @@ import { loading } from '../store'; import Create from './createOrganization.svelte'; + import { goto } from '$app/navigation'; + import { commandCenterKeyDownHandler, CommandRegistrant } from '$lib/helpers/commandCenter'; + import CommandCenter from './commandCenter.svelte'; + + const { register } = CommandRegistrant(); + $: register([ + { + label: 'Go to Account', + callback: () => { + goto('/console/account'); + }, + keys: ['a'], + meta: true, + shift: true + }, + { + label: 'Go to Home', + callback: () => { + goto('/console'); + }, + keys: ['h'], + alt: true + } + ]); + onMount(() => { loading.set(false); @@ -46,6 +71,10 @@ } + + + + + import Dialog from '$lib/components/dialog.svelte'; + import { CommandRegistrant, commands, commandCenter } from '$lib/helpers/commandCenter'; + import { isMac } from '$lib/helpers/platform'; + + let open = false; + let search = ''; + let selected = 0; + + const { register } = CommandRegistrant(); + $: register([ + { + callback: () => { + open = !open; + }, + keys: ['k'], + meta: true, + forceEnable: true + } + ]); + + $: results = $commands.filter((command) => { + return ( + !command.disabled && + command.label && + command.label.toLowerCase().includes(search.toLowerCase()) + ); + }); + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'ArrowDown') { + event.preventDefault(); + selected = selected === results.length - 1 ? 0 : selected + 1; + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + selected = selected === 0 ? results.length - 1 : selected - 1; + } else if (event.key === 'Enter') { + event.preventDefault(); + if (results[selected]) { + results[selected].callback(); + open = false; + search = ''; + } + } + } + + $: { + results; + selected = 0; + } + + $: $commandCenter.enabled = !open; + + + + + +
+ + +
    + {#each results as command, i} +
  • + + {command.label} + +
    + {#if command.meta} + {isMac ? '⌘' : 'ctrl'} + {/if} + {#if command.shift} + + {/if} + {#if command.alt} + {isMac ? '⌥' : 'alt'} + {/if} + {#each command.keys as key} + + {key.toUpperCase()} + + {/each} +
    +
  • + {:else} +
  • + No commands found +
  • + {/each} +
+
+
+ + diff --git a/src/routes/console/project-[project]/auth/+page.svelte b/src/routes/console/project-[project]/auth/+page.svelte index 0063f7ea1..4c1038511 100644 --- a/src/routes/console/project-[project]/auth/+page.svelte +++ b/src/routes/console/project-[project]/auth/+page.svelte @@ -1,31 +1,32 @@ diff --git a/src/routes/console/project-[project]/databases/+page.svelte b/src/routes/console/project-[project]/databases/+page.svelte index 0d0e1e553..62f2e9089 100644 --- a/src/routes/console/project-[project]/databases/+page.svelte +++ b/src/routes/console/project-[project]/databases/+page.svelte @@ -1,16 +1,17 @@ diff --git a/src/routes/console/project-[project]/functions/+page.svelte b/src/routes/console/project-[project]/functions/+page.svelte index 66b23203d..7881cd7b0 100644 --- a/src/routes/console/project-[project]/functions/+page.svelte +++ b/src/routes/console/project-[project]/functions/+page.svelte @@ -14,10 +14,12 @@ import { tooltip } from '$lib/actions/tooltip'; import { app } from '$lib/stores/app'; import { wizard } from '$lib/stores/wizard'; - import { beforeNavigate } from '$app/navigation'; + import { beforeNavigate, goto } from '$app/navigation'; import { toLocaleDateTime } from '$lib/helpers/date'; import Create from './createFunction.svelte'; import type { PageData } from './$types'; + import { CommandRegistrant } from '$lib/helpers/commandCenter'; + import { projectRegistrant } from '../store'; export let data: PageData; @@ -32,6 +34,16 @@ beforeNavigate(() => { wizard.hide(); }); + + const { register } = $projectRegistrant(); + $: register([ + { + label: 'Create function', + callback: openWizard, + keys: ['c'], + disabled: $wizard.show + } + ]); diff --git a/src/routes/console/project-[project]/overview/platforms/+page.svelte b/src/routes/console/project-[project]/overview/platforms/+page.svelte index 5c2332b05..852bb7a27 100644 --- a/src/routes/console/project-[project]/overview/platforms/+page.svelte +++ b/src/routes/console/project-[project]/overview/platforms/+page.svelte @@ -36,6 +36,9 @@ import { base } from '$app/paths'; import { app } from '$lib/stores/app'; import type { PageData } from './$types'; + import { CommandRegistrant } from '$lib/helpers/commandCenter'; + import { goto } from '$app/navigation'; + import { project, projectRegistrant } from '../../store'; export let data: PageData; @@ -70,6 +73,9 @@ return 'unknown'; } }; + + const { register } = $projectRegistrant(); + $: register();
diff --git a/src/routes/console/project-[project]/storage/+page.svelte b/src/routes/console/project-[project]/storage/+page.svelte index 826598d66..ed3d7c4ac 100644 --- a/src/routes/console/project-[project]/storage/+page.svelte +++ b/src/routes/console/project-[project]/storage/+page.svelte @@ -17,6 +17,7 @@ import { tooltip } from '$lib/actions/tooltip'; import type { Models } from '@appwrite.io/console'; import type { PageData } from './$types'; + import { projectRegistrant } from '../store'; export let data: PageData; @@ -28,6 +29,18 @@ showCreate = false; await goto(`${base}/console/project-${project}/storage/bucket-${event.detail.$id}`); } + + const { register } = $projectRegistrant(); + $: register([ + { + label: 'Create bucket', + callback: () => { + showCreate = true; + }, + keys: ['c'], + disabled: showCreate + } + ]); diff --git a/src/routes/console/project-[project]/store.ts b/src/routes/console/project-[project]/store.ts index 2c96c8070..7dc6da82c 100644 --- a/src/routes/console/project-[project]/store.ts +++ b/src/routes/console/project-[project]/store.ts @@ -2,8 +2,55 @@ import { derived, writable } from 'svelte/store'; import { page } from '$app/stores'; import type { Models } from '@appwrite.io/console'; import type { BarSeriesOption } from 'echarts/charts'; +import { ExtendCommandRegistrant } from '$lib/helpers/commandCenter'; +import { goto } from '$app/navigation'; export const project = derived(page, ($page) => $page.data.project as Models.Project); + +export const projectRegistrant = derived< + typeof project, + ReturnType +>(project, ($project) => { + return ExtendCommandRegistrant([ + { + label: 'Go to Overview', + keys: ['o'], + callback: () => { + goto(`/console/project-${$project.$id}`); + } + }, + + { + label: 'Go to Auth', + callback: () => { + goto(`/console/project-${$project.$id}/auth`); + }, + keys: ['a'] + }, + { + label: 'Go to Databases', + callback: () => { + goto(`/console/project-${$project.$id}/databases`); + }, + keys: ['d'] + }, + { + label: 'Go to Functions', + callback: () => { + goto(`/console/project-${$project.$id}/functions`); + }, + keys: ['f'] + }, + { + label: 'Go to Storage', + callback: () => { + goto(`/console/project-${$project.$id}/storage`); + }, + keys: ['s'] + } + ]); +}); + export const onboarding = derived( project, ($project) => $project.platforms.length === 0 && $project.keys.length === 0