Merge pull request #1625 from appwrite/update-command-center

Command center to pink2
This commit is contained in:
Ernst Mulders
2025-02-06 13:49:22 +01:00
committed by GitHub
14 changed files with 187 additions and 151 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
import { debounce } from '$lib/helpers/debounce';
import { isMac } from '$lib/helpers/platform';
import { wizard } from '$lib/stores/wizard';
import { onMount } from 'svelte';
import { type ComponentType, onMount } from 'svelte';
import { derived, writable } from 'svelte/store';
import { nanoid } from 'nanoid/non-secure';
import { trackEvent } from '$lib/actions/analytics';
@@ -49,7 +49,7 @@ type BaseCommand = {
disabled?: boolean;
forceEnable?: boolean;
group?: CommandGroup;
icon?: string;
icon?: ComponentType;
image?: string;
rank?: number;
nested?: boolean;
+43 -40
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { Remarkable } from 'remarkable';
import Template from './template.svelte';
import { Keyboard, Layout } from '@appwrite.io/pink-svelte';
const markdownInstance = new Remarkable();
@@ -227,47 +228,49 @@
</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={(e) => {
handleSubmit(e);
}}>
<!-- 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>
<Layout.Stack slot="footer">
<Layout.Stack gap="l">
<Layout.Stack direction="row" gap="s">
<AvatarInitials size={32} name={$user.name} />
<form
class="input-text-wrapper u-width-full-line"
style="--amount-of-buttons: 1;"
on:submit|preventDefault={(e) => {
handleSubmit(e);
}}>
<!-- 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>
</Layout.Stack>
<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>
<Layout.Stack direction="row" justifyContent="space-between" gap="xxl"
><Layout.Stack direction="row" alignItems="center" gap="xxs">
<Keyboard key="Enter" autoWidth={true} /> <span>to search</span></Layout.Stack>
<Layout.Stack
direction="row"
justifyContent="flex-end"
alignItems="center"
gap="xxs">
<Keyboard key="Esc" autoWidth={true} />
<span>to {$subPanels.length === 1 ? 'close' : 'go back'}</span></Layout.Stack>
</Layout.Stack>
</Layout.Stack>
</Layout.Stack>
</Template>
<style lang="scss">
+30 -15
View File
@@ -8,6 +8,8 @@
import { isMac } from '$lib/helpers/platform';
import { commands, searchers, type Command, isKeyedCommand } from '../commands';
import Template from './template.svelte';
import { Icon, Keyboard, Layout } from '@appwrite.io/pink-svelte';
import { IconArrowSmRight } from '@appwrite.io/pink-icons-svelte';
let search = '';
@@ -53,43 +55,56 @@
};
</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">
<Template options={results} bind:search searchPlaceholder="Search...">
<Layout.Stack
slot="option"
direction="row"
justifyContent="space-between"
alignItems="center"
let:option={command}>
<Layout.Stack direction="row" alignItems="center" gap="s">
{#if command.image}
<img
src={`${base}/icons/${$app.themeInUse}/color/${command.image}.svg`}
alt={command.label} />
{:else if command.icon}
<Icon icon={command.icon} size="s" color="--color-fgcolor-neutral-tertiary" />
{:else}
<i class="icon-{command.icon ?? 'arrow-sm-right'}" />
<Icon icon={IconArrowSmRight} size="s" color="--color-fgcolor-neutral-tertiary" />
{/if}
<span>
{command.label}
</span>
</div>
<div class="u-flex u-gap-4 u-cross-center">
</Layout.Stack>
<Layout.Stack direction="row" justifyContent="flex-end" alignItems="center" gap="s">
{#if hasCtrl(command)}
<kbd class="kbd"> {isMac() ? '⌘' : 'Ctrl'} </kbd>
<Keyboard autoWidth={!isMac()} key={isMac() ? '⌘' : 'Ctrl'} />
{/if}
{#if hasShift(command)}
<kbd class="kbd"> {isMac() ? '⇧' : 'Shift'} </kbd>
<Keyboard autoWidth={!isMac()} key={isMac() ? '⇧' : 'Shift'} />
{/if}
{#if hasAlt(command)}
<kbd class="kbd"> {isMac() ? '⌥' : 'Alt'} </kbd>
<Keyboard autoWidth={!isMac()} key={isMac() ? '⌥' : 'Alt'} />
{/if}
{#if isKeyedCommand(command)}
{#each command.keys as key, i}
{@const hasNext = command.keys.length - 1 !== i}
<kbd class="kbd">
{key.toUpperCase()}
</kbd>
<Keyboard key={key.toUpperCase()} />
{#if hasNext}
<span class="u-margin-inline-4" style:opacity={0.5}>then</span>
<span class="then">then</span>
{/if}
{/each}
{/if}
</div>
</div>
</Layout.Stack>
</Layout.Stack>
<svelte:fragment slot="no-options">No commands found</svelte:fragment>
</Template>
<style>
.then {
color: var(--color-fgcolor-neutral-tertiary, #97979b);
font-size: var(--font-size-s, 14px);
font-weight: 400;
}
</style>
+48 -60
View File
@@ -8,6 +8,8 @@
import { getCommandCenterCtx } from '../commandCenter.svelte';
import { clearSubPanels, popSubPanel, subPanels } from '../subPanels';
import { IconArrowSmRight } from '@appwrite.io/pink-icons-svelte';
import { Icon, Keyboard, Layout } from '@appwrite.io/pink-svelte';
/* eslint no-undef: "off" */
type Option = $$Generic<Omit<Command, 'group'> & { group?: string }>;
@@ -293,12 +295,22 @@
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'}" />
<Layout.Stack direction="column" gap="s">
{#if item.icon}
<Icon
icon={item.icon}
size="s"
color="--color-fgcolor-neutral-tertiary" />
{:else}
<Icon
icon={IconArrowSmRight}
size="s"
color="--color-fgcolor-neutral-tertiary" />
{/if}
<span>
{item.label}
</span>
</div>
</Layout.Stack>
</slot>
</button>
</li>
@@ -316,15 +328,17 @@
<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>
<Layout.Stack direction="row" justifyContent="space-between"
><Layout.Stack direction="row" alignItems="center" gap="xxs">
<Keyboard key="Enter" autoWidth={true} /> <span>to select</span></Layout.Stack>
<Layout.Stack
direction="row"
justifyContent="flex-end"
alignItems="center"
gap="xxs">
<Keyboard key="Esc" autoWidth={true} />
<span>to {$subPanels.length > 1 ? 'go back' : 'close'}</span></Layout.Stack>
</Layout.Stack>
</slot>
</div>
</div>
@@ -363,45 +377,16 @@
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-15));
--kbd-color: hsl(var(--color-neutral-60));
--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 0%, #e8e9f0 100%);
--icon-color: hsl(var(--color-neutral-50));
--label-color: hsl(var(--color-neutral-60));
}
:global(.theme-dark) .card {
--cmd-center-bg: hsl(var(--color-neutral-90));
--cmd-center-border: hsl(var(--color-neutral-80));
--cmd-center-shadow: 0px 16px 32px 0px #14141f;
--kbd-bg: hsl(var(--color-neutral-80));
--kbd-color: hsl(var(--color-neutral-15));
--crumb-bg: hsl(var(--color-neutral-150));
--crumb-color: hsl(var(--color-neutral-30));
--result-bg: hsl(var(--color-neutral-85));
--footer-bg: linear-gradient(
180deg,
hsl(var(--color-neutral-100)) 0%,
hsl(var(--color-neutral-85)) 100%
);
--icon-color: hsl(var(--color-neutral-70));
--label-color: hsl(var(--color-neutral-15));
.card {
--cmd-center-bg: var(--color-bgcolor-neutral-primary, #fff);
--footer-bg: var(--color-bgcolor-neutral-primary, #fff);
--cmd-center-border: var(--color-border-neutral, #ededf0);
--result-bg: var(--color-overlay-neutral-hover, rgba(25, 25, 28, 0.03));
--kbd-bg: var(--color-overlay-on-neutral, rgba(0, 0, 0, 0.06));
--kbd-color: var(--color-fgcolor-neutral-secondary, #56565c);
--icon-color: var(--color-fgcolor-neutral-tertiary, #97979b);
--label-color: var(--color-fgColor-neutral-secondary, #56565c);
--crumb-color: var(--color-fgColor-neutral-secondary, #56565c);
}
// Elements
@@ -420,11 +405,14 @@
max-height: min(calc(100vh - var(--top) - 4rem), var(--max-height, 32rem));
overflow: hidden;
padding: 0;
box-shadow:
0 56px 32px 0 rgba(0, 0, 0, 0.02),
0 6px 14px 0 rgba(0, 0, 0, 0.04),
0 24px 25px 0 rgba(0, 0, 0, 0.03);
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 {
@@ -434,7 +422,7 @@
:global(.kbd) {
color: var(--kbd-color);
background-color: var(--kbd-bg);
padding-inline: 0.25rem;
padding-inline: var(--space-2, 4px);
}
}
@@ -444,7 +432,7 @@
align-items: center;
width: 100%;
border-bottom: 1px solid hsl(var(--color-border));
border-bottom: 1px solid var(--color-border-neutral, #ededf0);
font-size: 16px;
padding: 1rem;
@@ -465,7 +453,6 @@
gap: 0.25rem;
border-radius: 0.25rem;
background: var(--crumb-bg);
color: var(--crumb-color);
text-align: center;
font-family: Inter;
@@ -493,13 +480,14 @@
padding: 1rem;
.group {
color: hsl(var(--color-neutral-70));
color: var(--color-fgcolor-neutral-secondary, #56565c);
margin-inline-start: 0.25rem;
margin-block-end: 0.25rem;
position: relative;
z-index: 10;
font-size: 10px !important;
font-size: var(--font-size-xs, 12px);
font-weight: 500;
&:not(:first-child) {
margin-block-start: 1rem;
@@ -560,7 +548,7 @@
content: '';
position: absolute;
left: -8px;
border-left: 1px solid hsl(var(--color-border));
border-left: 1px solid var(--color-border-neutral, #ededf0);
height: 100%;
}
}
@@ -570,8 +558,8 @@
.footer {
background: var(--footer-bg);
border-top: 1px solid hsl(var(--color-border));
border-top: 1px solid var(--color-border-neutral, #ededf0);
padding: 0.5rem 1rem;
padding: 0.75rem 1rem;
}
</style>
+3 -2
View File
@@ -7,6 +7,7 @@ import type { Models } from '@appwrite.io/console';
import { page } from '$app/stores';
import { showCreateDeployment } from '$routes/(console)/project-[project]/functions/function-[function]/store';
import { base } from '$app/paths';
import { IconLightningBolt, IconPlus } from '@appwrite.io/pink-icons-svelte';
const getFunctionCommand = (fn: Models.Function, projectId: string) => {
return {
@@ -15,7 +16,7 @@ const getFunctionCommand = (fn: Models.Function, projectId: string) => {
goto(`${base}/project-${projectId}/functions/function-${fn.$id}`);
},
group: 'functions',
icon: 'lightning-bolt'
icon: IconLightningBolt
} as const;
};
@@ -41,7 +42,7 @@ export const functionsSearcher = (async (query: string) => {
showCreateDeployment.set(true);
},
group: 'functions',
icon: 'plus'
icon: IconPlus
},
{
label: 'Go to deployments',
+19 -8
View File
@@ -44,6 +44,16 @@
import { base } from '$app/paths';
import { canSeeProjects } from '$lib/stores/roles';
import { BottomModalAlert } from '$lib/components';
import {
IconAnnotation,
IconBookOpen,
IconDiscord,
IconPencil,
IconPlus,
IconQuestionMarkCircle,
IconSparkles,
IconSwitchHorizontal
} from '@appwrite.io/pink-icons-svelte';
function kebabToSentenceCase(str: string) {
return str
@@ -88,7 +98,7 @@
addSubPanel(AIPanel);
},
keys: ['a', 'i'],
icon: 'sparkles',
icon: IconSparkles,
disabled: !isAssistantEnabled
},
{
@@ -97,7 +107,8 @@
isCloud ? goto(`${base}/create-organization`) : newOrgModal.set(true);
},
keys: ['c', 'o'],
group: 'organizations'
group: 'organizations',
icon: IconPlus
},
{
label: 'Open documentation',
@@ -105,7 +116,7 @@
window.open('https://appwrite.io/docs', '_blank');
},
group: 'help',
icon: 'book-open'
icon: IconBookOpen
},
{
label: 'Contact support',
@@ -113,7 +124,7 @@
window.open('https://appwrite.io/discord', '_blank');
},
group: 'help',
icon: 'question-mark-circle'
icon: IconQuestionMarkCircle
},
{
label: 'Send feedback',
@@ -121,7 +132,7 @@
feedback.toggleFeedback();
},
group: 'help',
icon: 'annotation'
icon: IconAnnotation
},
{
label: 'Join Discord community',
@@ -129,7 +140,7 @@
window.open('https://appwrite.io/discord', '_blank');
},
group: 'help',
icon: 'discord'
icon: IconDiscord
},
...(['auto', 'dark', 'light'] as const).map((theme) => {
return {
@@ -143,7 +154,7 @@
});
},
group: 'misc',
icon: 'switch-horizontal',
icon: IconSwitchHorizontal,
keys: ['t', theme[0]]
} as const;
}),
@@ -165,7 +176,7 @@
},
disabled: !$project?.$id,
group: 'security',
icon: 'pencil'
icon: IconPencil
}) as const
),
// Settings
@@ -33,6 +33,7 @@
import { organization } from '$lib/stores/organization';
import { canWriteProjects } from '$lib/stores/roles';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
export let data;
@@ -94,7 +95,7 @@
keys: ['c'],
disabled: ($readOnly && !GRACE_PERIOD_OVERRIDE) || !$canWriteProjects,
group: 'projects',
icon: 'plus'
icon: IconPlus
}
]);
@@ -10,6 +10,7 @@
import { project } from '../store';
import { showCreateUser } from './+page.svelte';
import { showCreateTeam } from './teams/+page.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
$: $registerCommands([
{
@@ -22,7 +23,7 @@
},
keys: $page.url.pathname.endsWith('auth') ? ['c'] : ['c', 'u'],
group: 'users',
icon: 'plus',
icon: IconPlus,
rank: $page.url.pathname.endsWith('auth') ? 10 : 0,
disabled: ($readOnly && !GRACE_PERIOD_OVERRIDE) || !$canWriteUsers
},
@@ -37,7 +38,7 @@
keys: $page.url.pathname.endsWith('teams') ? ['c'] : ['c', 't'],
group: 'teams',
icon: 'plus',
icon: IconPlus,
rank: $page.url.pathname.endsWith('teams') ? 10 : 0,
disabled: ($readOnly && !GRACE_PERIOD_OVERRIDE) || !$canWriteTeams
},
@@ -1,4 +1,4 @@
import type { SvelteComponent } from 'svelte';
import type { ComponentType, SvelteComponent } from 'svelte';
import { writable } from 'svelte/store';
import Boolean, { submitBoolean, updateBoolean } from './boolean.svelte';
import Email, { submitEmail, updateEmail } from './email.svelte';
@@ -11,6 +11,17 @@ import Url, { submitUrl, updateUrl } from './url.svelte';
import Datetime, { submitDatetime, updateDatetime } from './datetime.svelte';
import type { Attributes } from '../store';
import Relationship, { submitRelationship, updateRelationship } from './relationship.svelte';
import {
IconCalendar,
IconHashtag,
IconLink,
IconLocationMarker,
IconMail,
IconRelationship,
IconText,
IconToggle,
IconViewList
} from '@appwrite.io/pink-icons-svelte';
export type Option = {
name:
@@ -39,7 +50,7 @@ export type Option = {
originalKey: string
) => Promise<void>;
format?: 'email' | 'ip' | 'url' | 'enum';
icon: string;
icon: ComponentType;
};
export const attributeOptions: Option[] = [
@@ -49,7 +60,7 @@ export const attributeOptions: Option[] = [
type: 'string',
create: submitString,
update: updateString,
icon: 'text'
icon: IconText
},
{
name: 'Integer',
@@ -57,7 +68,7 @@ export const attributeOptions: Option[] = [
type: 'integer',
create: submitInteger,
update: updateInteger,
icon: 'hashtag'
icon: IconHashtag
},
{
name: 'Float',
@@ -65,7 +76,7 @@ export const attributeOptions: Option[] = [
type: 'double',
create: submitFloat,
update: updateFloat,
icon: 'hashtag'
icon: IconHashtag
},
{
name: 'Boolean',
@@ -73,7 +84,7 @@ export const attributeOptions: Option[] = [
type: 'boolean',
create: submitBoolean,
update: updateBoolean,
icon: 'toggle'
icon: IconToggle
},
{
name: 'Datetime',
@@ -81,7 +92,7 @@ export const attributeOptions: Option[] = [
type: 'datetime',
create: submitDatetime,
update: updateDatetime,
icon: 'calendar'
icon: IconCalendar
},
{
name: 'Email',
@@ -90,7 +101,7 @@ export const attributeOptions: Option[] = [
format: 'email',
create: submitEmail,
update: updateEmail,
icon: 'mail'
icon: IconMail
},
{
name: 'IP',
@@ -99,7 +110,7 @@ export const attributeOptions: Option[] = [
format: 'ip',
create: submitIp,
update: updateIp,
icon: 'location-marker'
icon: IconLocationMarker
},
{
name: 'URL',
@@ -108,7 +119,7 @@ export const attributeOptions: Option[] = [
format: 'url',
create: submitUrl,
update: updateUrl,
icon: 'link'
icon: IconLink
},
{
name: 'Enum',
@@ -117,7 +128,7 @@ export const attributeOptions: Option[] = [
format: 'enum',
create: submitEnum,
update: updateEnum,
icon: 'view-list'
icon: IconViewList
},
{
name: 'Relationship',
@@ -125,7 +136,7 @@ export const attributeOptions: Option[] = [
type: 'relationship',
create: submitRelationship,
update: updateRelationship,
icon: 'relationship'
icon: IconRelationship
}
];
@@ -14,6 +14,7 @@
import { topicsSearcher } from '$lib/commandCenter/searchers/topics';
import { canWriteMessages } from '$lib/stores/roles';
import { project } from '../store';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
// TODO: finalize the commands
$: $registerCommands([
@@ -23,7 +24,7 @@
callback() {
addSubPanel(CreateMessagePanel);
},
icon: 'plus',
icon: IconPlus,
group: 'messaging',
disabled: !$canWriteMessages
},
@@ -30,6 +30,7 @@
import { hasOnboardingDismissed } from '$lib/helpers/onboarding';
import { Layout } from '@appwrite.io/pink-svelte';
import { writable, type Writable } from 'svelte/store';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
$: projectId = $page.params.project;
$: path = `${base}/project-${projectId}/overview`;
@@ -61,13 +62,13 @@
callback() {
addSubPanel(PlatformsPanel);
},
icon: 'plus',
icon: IconPlus,
group: 'integrations',
disabled: !$canWriteProjects
},
{
label: 'Create API Key',
icon: 'plus',
icon: IconPlus,
callback() {
createApiKey();
},
@@ -3,11 +3,12 @@
import { registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
import { canWriteProjects } from '$lib/stores/roles';
import { openWebhooksWizard } from './webhooks/+page.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
$: $registerCommands([
{
label: 'Create webhook',
icon: 'plus',
icon: IconPlus,
keys: $page.url.pathname.includes('webhooks') ? ['c'] : ['c', 'w'],
callback: () => {
openWebhooksWizard();
@@ -7,6 +7,7 @@
import { canWriteBuckets } from '$lib/stores/roles';
import { project } from '../store';
import { showCreateBucket } from './+page.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
$: $registerCommands([
{
@@ -18,7 +19,7 @@
$showCreateBucket = true;
},
keys: $page.url.pathname.endsWith('storage') ? ['c'] : ['c', 'b'],
icon: 'plus',
icon: IconPlus,
group: 'buckets',
disabled: !$canWriteBuckets
},
@@ -12,6 +12,7 @@
import { project } from '../../store';
import { showCreateFile } from './+page.svelte';
import { bucket } from './store';
import { IconKey, IconLockClosed, IconPlus, IconPuzzle } from '@appwrite.io/pink-icons-svelte';
$: $registerCommands([
{
@@ -24,7 +25,7 @@
},
keys: $page.url.pathname.endsWith($bucket.$id) ? ['c'] : ['c', 'f'],
group: 'files',
icon: 'plus'
icon: IconPlus
},
{
label: 'Permissions',
@@ -35,7 +36,7 @@
scrollBy({ top: -100 });
},
group: 'buckets',
icon: 'key',
icon: IconKey,
disabled: !$canWriteBuckets
},
{
@@ -46,7 +47,7 @@
);
},
group: 'buckets',
icon: 'puzzle',
icon: IconPuzzle,
disabled: !$canWriteBuckets
},
{
@@ -58,7 +59,7 @@
scrollBy({ top: -100 });
},
group: 'buckets',
icon: 'lock-closed',
icon: IconLockClosed,
disabled: !$canWriteBuckets
},
{