nuxt / ui

A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.
https://ui.nuxt.com
MIT License
4.04k stars 509 forks source link

CommandPalette: support submenus #2003

Open chris-si opened 3 months ago

chris-si commented 3 months ago

Description

Hi,

I think it would be great if the command palette would 'natively' support submenus to enable an easy way to build some more feature-rich command palettes. I implemented submenus by manually replacing the assigned command groups and saving the 'history' of groups assigned to the command palette so I can go back via a manually injected back button.

The following code is a snippet of my code that doesn't work as I put it there because of some missing code, but it outlines what I did well enough to understand my submenu implementation.

Additional context

<template>
  <UModal v-model="model">
    <UCommandPalette
      ref="commandPaletteRef"
      :loading="isDataLoading"
      :groups="selectedCommandGroups"
      :autoselect="true"
      :nullable="false"
      @update:model-value="onCommandSelect"
      @close="() => (model = false)"
      :close-button="{
        icon: 'i-heroicons-x-mark-20-solid',
        color: 'gray',
        variant: 'link',
        padded: false,
      }"
      :empty-state="{
        icon: 'i-heroicons-magnifying-glass-20-solid',
        label: `We couldn't find any items.`,
        queryLabel: `We couldn't find any items with that term. Please try again.`,
      }">
    </UCommandPalette>
  </UModal>
</template>

<script setup lang="ts">
import type { Group, Command } from '#ui/types';
import type { UCommandPalette } from '#build/components';
import { nanoid } from 'nanoid';

const toast = useToast();
const router = useRouter();

const model = defineModel<boolean>();

const commandPaletteStore = useCommandPaletteStore();
const { commandPaletteGroups, isDataLoading, realms } = storeToRefs(commandPaletteStore);
const commandPaletteRef = ref<InstanceType<typeof UCommandPalette>>();

const newEntitySubmenuGroup = computed<Group[]>(() => {
  return [
    {
      key: 'action-new-entity-sub-menu',
      label: 'Create new entity: Select a realm',
      commands: realms.value.map((realm) => ({
        id: `action-new-entity-realm-${realm.workspaceId}`,
        label: realm.ownershipType === 'PERSONAL' ? 'My Library' : realm.name,
        icon: realm.ownershipType === 'PERSONAL' ? 'i-heroicons-user' : 'i-heroicons-user-group',
        click: () => navigateTo(`${commandPaletteStore.getWorkspacePrefix(realm)}/entity/new`),
      })),
    },
  ];
});

const actionCommands = computed<Command[]>(() => {
  return [
    {
      id: 'action-new-entity',
      label: 'Create new entity',
      icon: 'i-heroicons-document',
      submenu: newEntitySubmenuGroup.value,
    },
  ];
});

const navigationCommands: Command[] = [
  {
    id: 'navigation-home',
    label: 'Home',
    icon: 'i-heroicons-home',
    click: () => navigateTo('/'),
  },
  {
    id: 'navigation-settings',
    label: 'Settings',
    icon: 'i-heroicons-cog',
    click: () => navigateTo('/settings'),
  },
  {
    id: 'navigation-back',
    label: 'Go back',
    icon: 'i-heroicons-arrow-uturn-left',
    click: () => router.back(),
  },
];

const commandGroups = computed(() => {
  return [
    ...commandPaletteGroups.value,
    {
      key: 'actions',
      label: 'Actions',
      commands: actionCommands.value.filter((command) => !command.hidden),
    },
    {
      key: 'navigation',
      label: 'Navigation',
      commands: navigationCommands,
    },
  ] satisfies Group[];
});

const selectedCommandGroups = ref<Group[]>(commandGroups.value);

const commandPaletteMenuHistory = ref<Group[][]>([commandGroups.value]);

function onCommandSelect(option: Command) {
  if (option && option.isSubmenuBackButton) {
    commandPaletteMenuHistory.value.pop();
    selectedCommandGroups.value = commandPaletteMenuHistory.value[commandPaletteMenuHistory.value.length - 1];
    return;
  }

  if (option && option.submenu) {
    const id = nanoid();
    const submenu: Group[] = [
      {
        key: `submenu-navigation-${id}`,
        static: true,
        commands: [
          {
            id: `command-back-${id}`,
            label: 'Back',
            icon: 'i-heroicons-arrow-left',
            isSubmenuBackButton: true,
          },
        ],
      },
      ...option.submenu,
    ];
    commandPaletteMenuHistory.value.push(submenu);
    selectedCommandGroups.value = submenu;

    return;
  }

  model.value = false;
  if (option && option.click) {
    option.click();
  }
}

onBeforeMount(async () => {
  await commandPaletteStore.loadData();
  selectedCommandGroups.value = commandGroups.value;
});
</script>
benjamincanac commented 3 months ago

This is something we might consider later on in v3 😊