mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
feat: grouped options
This commit is contained in:
1 parent
92032f537a
commit
09df2013e3
6 files changed
+152
-51
No files matched your search
@@ -3,6 +3,18 @@ import { isMac } from '$lib/helpers/platform';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
// Store
|
||||
export type CommandGroup =
|
||||
| 'ungrouped'
|
||||
| 'navigation'
|
||||
| 'project'
|
||||
| 'auth'
|
||||
| 'help'
|
||||
| 'account'
|
||||
| 'platforms'
|
||||
| 'databases'
|
||||
| 'functions'
|
||||
| 'storage';
|
||||
|
||||
export type Command = {
|
||||
keys: string[];
|
||||
/* Ctrl on Windows/Linux, Meta on Mac */
|
||||
@@ -14,6 +26,7 @@ export type Command = {
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
forceEnable?: boolean;
|
||||
group?: CommandGroup;
|
||||
};
|
||||
|
||||
export const commandMap = writable<Map<string, Command[]>>(new Map());
|
||||
@@ -24,6 +37,20 @@ export const commands = derived(commandMap, ($commandMap) => {
|
||||
return Array.from($commandMap.values()).flat();
|
||||
});
|
||||
|
||||
export const groupedCommands = derived(commands, ($commands) => {
|
||||
const res = new Map<string, Command[]>();
|
||||
|
||||
for (const command of $commands) {
|
||||
if (!command.group) {
|
||||
res.set('ungrouped', [...(res.get('ungrouped') || []), command]);
|
||||
} else {
|
||||
res.set(command.group, [...(res.get(command.group) || []), command]);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import 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.
|
||||
|
||||
@@ -11,7 +13,7 @@
|
||||
import { quadOut } from 'svelte/easing';
|
||||
import { crossfade } from 'svelte/transition';
|
||||
|
||||
type BaseOption = { callback: () => void };
|
||||
type BaseOption = { callback: () => void; group?: CommandGroup };
|
||||
type Option = $$Generic<BaseOption>;
|
||||
export let options: Option[] | null = null;
|
||||
export let search = '';
|
||||
@@ -54,20 +56,20 @@
|
||||
event.preventDefault();
|
||||
selected = options.length - 1;
|
||||
}
|
||||
|
||||
tick().then(() => {
|
||||
if (!cardEl) return;
|
||||
const selectedEl = cardEl.querySelector('[data-selected]');
|
||||
|
||||
if (selectedEl) {
|
||||
selectedEl.scrollIntoView({
|
||||
block: 'nearest',
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tick().then(() => {
|
||||
if (!cardEl) return;
|
||||
const selectedEl = cardEl.querySelector('[data-selected]');
|
||||
|
||||
if (selectedEl) {
|
||||
selectedEl.scrollIntoView({
|
||||
block: 'nearest',
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
popSubPanel();
|
||||
@@ -87,6 +89,56 @@
|
||||
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 '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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// return a flat array of groups and indexed options
|
||||
let optionIndex = 0;
|
||||
const groupsAndOptions: (Group | IndexedOption)[] = [];
|
||||
for (const [groupName, options] of groupedOptions) {
|
||||
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) => () => {
|
||||
selected = option.index;
|
||||
};
|
||||
|
||||
const castOption = (option: IndexedOption) => option as Option;
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
@@ -97,36 +149,41 @@
|
||||
class:press={!$commandCenterCtx.isInitialPanel}
|
||||
class:scale-up={$commandCenterCtx.isInitialPanel && $commandCenterCtx.open}>
|
||||
<div class="u-flex u-flex-vertical u-width-full-line u-overflow-hidden">
|
||||
<div class="u-flex search-wrapper">
|
||||
<slot name="search">
|
||||
<slot name="search">
|
||||
<div class="u-flex search-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search for commands..."
|
||||
autofocus
|
||||
bind:value={search} />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
{#if options}
|
||||
{#if groupsAndOptions}
|
||||
<ul class="options u-margin-block-start-16 u-flex u-flex-vertical u-gap-8">
|
||||
{#each options as option, i}
|
||||
<li class="result" data-selected={selected === i ? true : undefined}>
|
||||
{#if selected === i}
|
||||
<div
|
||||
class="bg"
|
||||
in:send|local={{ key: 'bg' }}
|
||||
out:receive|local={{ key: 'bg' }} />
|
||||
{/if}
|
||||
<button
|
||||
class="option"
|
||||
on:click={() => {
|
||||
triggerOption(option);
|
||||
}}
|
||||
on:mouseover={() => (selected = i)}
|
||||
on:focus={() => (selected = i)}>
|
||||
<slot name="option" {option} />
|
||||
</button>
|
||||
</li>
|
||||
{#each groupsAndOptions as item}
|
||||
{@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}>
|
||||
{#if isSelected}
|
||||
<div
|
||||
class="bg"
|
||||
in:send|local={{ key: 'bg' }}
|
||||
out:receive|local={{ key: 'bg' }} />
|
||||
{/if}
|
||||
<button
|
||||
class="option"
|
||||
on:click={getOptionClickHandler(item)}
|
||||
on:mouseover={getOptionFocusHandler(item)}
|
||||
on:focus={getOptionFocusHandler(item)}>
|
||||
<slot name="option" option={castOption(item)} />
|
||||
</button>
|
||||
</li>
|
||||
{/if}
|
||||
{:else}
|
||||
<li class="result">
|
||||
<slot name="no-options">
|
||||
@@ -209,6 +266,13 @@
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.group {
|
||||
color: hsl(var(--color-neutral-70));
|
||||
margin-inline-start: 0.25rem;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.result {
|
||||
transition: 150ms;
|
||||
position: relative;
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
},
|
||||
keys: ['a'],
|
||||
ctrl: true,
|
||||
shift: true
|
||||
shift: true,
|
||||
group: 'navigation'
|
||||
},
|
||||
{
|
||||
label: 'Go to Home',
|
||||
@@ -48,7 +49,8 @@
|
||||
},
|
||||
keys: ['g', 'h'],
|
||||
// Check if pathname not like /console/organization-6374c26ba90067a6315d
|
||||
disabled: $page.url.pathname.includes('/console/organization-')
|
||||
disabled: $page.url.pathname.includes('/console/organization-'),
|
||||
group: 'navigation'
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { registerCommands } from '$lib/commandCenter';
|
||||
import { Command, registerCommands } from '$lib/commandCenter';
|
||||
import { CardContainer, Empty, GridItem1, Heading, PaginationWithLimit } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
@@ -48,12 +48,13 @@
|
||||
}
|
||||
|
||||
$: $registerCommands([
|
||||
...data.projects.projects.map((project, i) => ({
|
||||
...data.projects.projects.map<Command>((project, i) => ({
|
||||
label: `Project ${project.name}`,
|
||||
callback: () => {
|
||||
goto(`/console/project-${project.$id}`);
|
||||
},
|
||||
keys: ['p', `${i + 1}`]
|
||||
keys: ['p', `${i + 1}`],
|
||||
group: 'project'
|
||||
})),
|
||||
{
|
||||
label: 'Create project',
|
||||
@@ -61,7 +62,8 @@
|
||||
showCreate = true;
|
||||
},
|
||||
keys: ['c'],
|
||||
disabled: showCreate
|
||||
disabled: showCreate,
|
||||
group: 'project'
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
$: $registerCommands([
|
||||
{
|
||||
label: 'Go to Overview',
|
||||
keys: ['g', 'o'],
|
||||
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}`);
|
||||
}
|
||||
},
|
||||
keys: ['g', 'o'],
|
||||
group: 'navigation'
|
||||
},
|
||||
|
||||
{
|
||||
@@ -32,28 +32,32 @@
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}/auth`);
|
||||
},
|
||||
keys: ['g', 'a']
|
||||
keys: ['g', 'a'],
|
||||
group: 'navigation'
|
||||
},
|
||||
{
|
||||
label: 'Go to Databases',
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}/databases`);
|
||||
},
|
||||
keys: ['g', 'd']
|
||||
keys: ['g', 'd'],
|
||||
group: 'navigation'
|
||||
},
|
||||
{
|
||||
label: 'Go to Functions',
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}/functions`);
|
||||
},
|
||||
keys: ['g', 'f']
|
||||
keys: ['g', 'f'],
|
||||
group: 'navigation'
|
||||
},
|
||||
{
|
||||
label: 'Go to Storage',
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}/storage`);
|
||||
},
|
||||
keys: ['g', 's']
|
||||
keys: ['g', 's'],
|
||||
group: 'navigation'
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -44,14 +44,16 @@
|
||||
showCreate = true;
|
||||
},
|
||||
keys: ['c'],
|
||||
disabled: showCreate
|
||||
disabled: showCreate,
|
||||
group: 'auth'
|
||||
},
|
||||
{
|
||||
label: 'Go to Overview',
|
||||
callback: () => {
|
||||
goto(`${base}/console/project-${$project.$id}/overview`);
|
||||
},
|
||||
keys: ['o']
|
||||
keys: ['o'],
|
||||
group: 'navigation'
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
Reference in new issue
Block a user