mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-23 22:25:05 +00:00
## Summary Temporarily patch FE-569 by keeping the affected portaled Reka dropdowns and menus above their containing PrimeVue dialogs when PrimeVue auto z-index state has been elevated. ## Changes - **What**: Added a small compatibility helper, `usePrimeVueOverlayChildStyle`, that returns an anchor ref plus a computed inline style for child popover content. The helper finds the nearest PrimeVue dialog mask (`.p-dialog-mask` / `.p-overlay-mask`) from the parent surface and, only when found, applies `parent z-index + 1` to the affected Reka overlay content. - **What**: Applied that helper at the exact PrimeVue parent surfaces where the issue was found. This PR does not add a global overlay policy and does not change every Reka select/dropdown in the app. - **What**: Added optional `contentStyle`/`selectContentStyle` plumbing only where needed so the style reaches the actual portaled Reka overlay root. - **What**: Added focused unit coverage for the helper contract: no PrimeVue parent preserves existing stacking, PrimeVue dialog/overlay masks render child content above the parent, low parent z-index values respect the Reka floor, and invalid z-index values do not inject an inline override. - **Approach**: This is intentionally a minimal, parent-scoped band-aid. It avoids a global PrimeVue overlay scanner because global sampling can be polluted by unrelated persistent PrimeVue roots such as Toast and would turn this fix into a broader layering policy. - **Approach**: The patch targets the confirmed failure mode: a Reka child overlay rendering below its owning PrimeVue dialog after PrimeVue autoZIndex has been elevated. It does not attempt to solve PrimeVue z-index globally. - **Lifecycle**: This is temporary migration compatibility. PrimeVue dialogs and controls are being incrementally migrated to Reka UI, so `usePrimeVueOverlayChildStyle` and the optional style props added for FE-569 should be removed once the affected parent surfaces move to Reka. - **Breaking**: None. New props are optional and no public API contract is changed. - **Dependencies**: None. ## Patched Entry Points This PR pinpoints the six affected user-facing surfaces below. Each patch is applied from the PrimeVue dialog parent and passed only to the Reka child overlay content that can render underneath that parent. https://github.com/user-attachments/assets/d0d1522a-ffc7-4934-9e7a-06b83e20f809 1. **Workflow Template Library filters** - **How to enter**: click the Templates button in the left sidebar, or open the Comfy menu and choose **Browse Templates**. - **Affected elements**: the template filter popovers in `WorkflowTemplateSelectorDialog`: **Model**, **Use case**, **Runs on**, and **Sort by**. - **Patch point**: `WorkflowTemplateSelectorDialog.vue` anchors to the template dialog content filter area and passes `selectContentStyle` to the affected `MultiSelect` / `SingleSelect` controls. https://github.com/user-attachments/assets/3641fa24-da51-4392-a904-9085f8a5a2f4 2. **Manager dialog header controls** - **How to enter**: open Manager from the top/menu Manager entry when the new Manager UI is available. - **Affected elements**: the Manager header controls in `ManagerDialog`: search mode `SingleSelect`, search autocomplete suggestions, and **Sort** `SingleSelect`. - **Patch point**: `ManagerDialog.vue` anchors to the dialog header and passes `selectContentStyle` to those three Reka overlays. https://github.com/user-attachments/assets/cf25cc06-f851-48ef-9d9c-9ec2da8afc06 3. **Asset Browser filter bar** - **How to enter**: open the Asset Browser from an eligible model widget browse action, the Model Library flow, or another `useAssetBrowserDialog` caller. - **Affected elements**: `AssetFilterBar` controls: **File formats**, **Base models**, **Ownership**, and **Sort by**. - **Patch point**: `AssetBrowserModal.vue` anchors to the PrimeVue dialog header and passes the style through `AssetFilterBar` to its `MultiSelect` / `SingleSelect` controls. https://github.com/user-attachments/assets/e27bd805-10c0-4b3b-97f3-9e11faa47021 4. **Asset Browser model info panel** - **How to enter**: open Asset Browser, select an asset, then use the right-side model info panel. - **Affected element**: the **Model type** select in `ModelInfoPanel`. - **Patch point**: `AssetBrowserModal.vue` reuses the same parent-scoped style and passes it to `ModelInfoPanel` as `selectContentStyle`. https://github.com/user-attachments/assets/5e9f7ef0-ebd7-4987-ba1b-2137c034086f 5. **Upload Model confirmation step** - **How to enter**: open Asset Browser, click **Upload**, enter/fetch model metadata, then proceed to the confirmation step. - **Affected element**: the **Model type** `SingleSelect` in `UploadModelConfirmation`. - **Patch point**: `UploadModelConfirmation.vue` anchors within the upload dialog content and passes `selectContentStyle` to the model type selector. https://github.com/user-attachments/assets/ec145f26-8621-455b-915e-bedee47e1cbd 6. **Settings > Keybinding panel controls** - **How to enter**: open Settings from the sidebar/menu, then select the **Keybinding** panel. - **Affected elements**: the keybinding preset select, the preset overflow dropdown menu, and the row context menu inside `KeybindingPanel`. - **Patch point**: `KeybindingPanel.vue` anchors to the settings dialog panel and passes `keybindingOverlayContentStyle` only to those Reka overlay roots. ## Review Focus - Confirm the patch stays narrowly scoped to the six known PrimeVue parent + Reka child overlay surfaces above. - Confirm `contentStyle` reaches the actual portaled Reka overlay content in each patched path. - Confirm the fallback behavior preserves existing stacking when no PrimeVue parent overlay is found; in that case the helper returns an empty style object and leaves existing Tailwind z-index classes alone. - Please avoid expanding this into a larger overlay refactor. The goal is a clean, backport-friendly compatibility patch while the Reka migration continues. Validation performed: - `pnpm exec vitest run src/composables/usePopoverSizing.test.ts` - `pnpm typecheck` - `pnpm lint` (passes with existing unrelated warnings only) - `pnpm format:check` - commit hook lint-staged checks (`oxfmt`, `stylelint`, `oxlint`, `eslint --fix`, `pnpm typecheck`) - pre-push `pnpm knip` Linear: FE-569 ## Bug Screenshots https://github.com/user-attachments/assets/e73761af-9867-4c50-ab0d-4e32e59011e1 https://github.com/user-attachments/assets/145daf4d-3268-428b-9987-1e1afd0b866f ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12038-fix-keep-Reka-overlays-above-PrimeVue-dialogs-3596d73d365081e7af49dbc4d3905962) by [Unito](https://www.unito.io)
644 lines
21 KiB
Vue
644 lines
21 KiB
Vue
<template>
|
|
<div
|
|
:ref="primeVueOverlay.overlayScopeRef"
|
|
class="keybinding-panel flex flex-col gap-2"
|
|
>
|
|
<Teleport defer to="#keybinding-panel-header">
|
|
<SearchInput
|
|
v-model="filters['global'].value"
|
|
class="max-w-96"
|
|
size="lg"
|
|
:placeholder="
|
|
$t('g.searchPlaceholder', { subject: $t('g.keybindings') })
|
|
"
|
|
/>
|
|
</Teleport>
|
|
|
|
<Teleport defer to="#keybinding-panel-actions">
|
|
<div class="flex items-center gap-2">
|
|
<KeybindingPresetToolbar
|
|
:preset-names="presetNames"
|
|
:content-style="keybindingOverlayContentStyle"
|
|
@presets-changed="refreshPresetList"
|
|
/>
|
|
<DropdownMenu
|
|
:entries="menuEntries"
|
|
:style="keybindingOverlayContentStyle"
|
|
icon="icon-[lucide--ellipsis]"
|
|
item-class="text-sm gap-2"
|
|
button-size="unset"
|
|
button-class="size-10"
|
|
to="#keybinding-panel-actions"
|
|
align="end"
|
|
>
|
|
<template #button>
|
|
<Button
|
|
size="unset"
|
|
class="size-10"
|
|
data-testid="keybinding-preset-menu"
|
|
>
|
|
<i class="icon-[lucide--ellipsis]" />
|
|
</Button>
|
|
</template>
|
|
</DropdownMenu>
|
|
</div>
|
|
</Teleport>
|
|
|
|
<ContextMenuRoot>
|
|
<ContextMenuTrigger as-child>
|
|
<div @contextmenu.capture="clearContextMenuTarget">
|
|
<DataTable
|
|
v-model:selection="selectedCommandData"
|
|
v-model:expanded-rows="expandedRows"
|
|
:value="commandsData"
|
|
data-key="id"
|
|
:global-filter-fields="['id', 'label']"
|
|
:filters="filters"
|
|
:paginator="true"
|
|
:rows="50"
|
|
:rows-per-page-options="[25, 50, 100]"
|
|
selection-mode="single"
|
|
context-menu
|
|
striped-rows
|
|
:pt="{
|
|
header: 'px-0'
|
|
}"
|
|
@row-click="handleRowClick($event)"
|
|
@row-dblclick="handleRowDblClick($event.data)"
|
|
@row-contextmenu="handleRowContextMenu($event)"
|
|
>
|
|
<Column
|
|
field="id"
|
|
:header="$t('g.command')"
|
|
sortable
|
|
class="max-w-64 2xl:max-w-full"
|
|
:pt="{ bodyCell: 'p-1 min-h-8' }"
|
|
>
|
|
<template #body="slotProps">
|
|
<div
|
|
class="flex items-center gap-1 truncate"
|
|
:class="slotProps.data.keybindings.length < 2 && 'pl-5'"
|
|
:title="slotProps.data.id"
|
|
>
|
|
<i
|
|
v-if="slotProps.data.keybindings.length >= 2"
|
|
class="icon-[lucide--chevron-right] size-4 shrink-0 text-muted-foreground transition-transform"
|
|
:class="
|
|
expandedCommandIds.has(slotProps.data.id) && 'rotate-90'
|
|
"
|
|
/>
|
|
<i
|
|
v-if="
|
|
slotProps.data.keybindings.some(
|
|
(b: KeybindingImpl) => b.combo.isBrowserReserved
|
|
)
|
|
"
|
|
v-tooltip="$t('g.browserReservedKeybindingTooltip')"
|
|
class="icon-[lucide--triangle-alert] shrink-0 text-warning-background"
|
|
/>
|
|
{{ slotProps.data.label }}
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
<Column
|
|
field="keybindings"
|
|
:header="$t('g.keybinding')"
|
|
:pt="{ bodyCell: 'p-1 min-h-8' }"
|
|
>
|
|
<template #body="slotProps">
|
|
<div
|
|
v-if="slotProps.data.keybindings.length > 0"
|
|
class="flex items-center gap-1"
|
|
>
|
|
<template
|
|
v-for="(binding, idx) in (
|
|
slotProps.data as ICommandData
|
|
).keybindings.slice(0, 2)"
|
|
:key="binding.combo.serialize()"
|
|
>
|
|
<span v-if="idx > 0" class="text-muted-foreground">,</span>
|
|
<KeyComboDisplay
|
|
:key-combo="binding.combo"
|
|
:is-modified="slotProps.data.isModified"
|
|
/>
|
|
</template>
|
|
<span
|
|
v-if="slotProps.data.keybindings.length > 2"
|
|
class="rounded-sm px-1.5 py-0.5 text-xs text-muted-foreground"
|
|
>
|
|
{{
|
|
$t('g.nMoreKeybindings', {
|
|
count: slotProps.data.keybindings.length - 2
|
|
})
|
|
}}
|
|
</span>
|
|
</div>
|
|
<span v-else>-</span>
|
|
</template>
|
|
</Column>
|
|
<Column
|
|
field="source"
|
|
:header="$t('g.source')"
|
|
:pt="{ bodyCell: 'p-1 min-h-8' }"
|
|
>
|
|
<template #body="slotProps">
|
|
<span class="overflow-hidden text-ellipsis">{{
|
|
slotProps.data.source || '-'
|
|
}}</span>
|
|
</template>
|
|
</Column>
|
|
<Column field="actions" header="" :pt="{ bodyCell: 'p-1 min-h-8' }">
|
|
<template #body="slotProps">
|
|
<div class="actions flex flex-row justify-end">
|
|
<Button
|
|
v-if="slotProps.data.keybindings.length === 1"
|
|
v-tooltip="$t('g.edit')"
|
|
variant="textonly"
|
|
size="icon"
|
|
:aria-label="$t('g.edit')"
|
|
@click="
|
|
editKeybinding(
|
|
slotProps.data,
|
|
slotProps.data.keybindings[0]
|
|
)
|
|
"
|
|
>
|
|
<i class="icon-[lucide--pencil]" />
|
|
</Button>
|
|
<Button
|
|
v-tooltip="$t('g.addNewKeybinding')"
|
|
variant="textonly"
|
|
size="icon"
|
|
:aria-label="$t('g.addNewKeybinding')"
|
|
@click="addKeybinding(slotProps.data)"
|
|
>
|
|
<i class="icon-[lucide--plus]" />
|
|
</Button>
|
|
<Button
|
|
v-tooltip="$t('g.reset')"
|
|
variant="textonly"
|
|
size="icon"
|
|
:aria-label="$t('g.reset')"
|
|
:disabled="!slotProps.data.isModified"
|
|
@click="resetKeybinding(slotProps.data)"
|
|
>
|
|
<i class="icon-[lucide--rotate-ccw]" />
|
|
</Button>
|
|
<Button
|
|
v-tooltip="$t('g.delete')"
|
|
variant="textonly"
|
|
size="icon"
|
|
:aria-label="$t('g.delete')"
|
|
:disabled="slotProps.data.keybindings.length === 0"
|
|
@click="handleRemoveKeybindingFromMenu(slotProps.data)"
|
|
>
|
|
<i class="icon-[lucide--trash-2]" />
|
|
</Button>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
<template #expansion="slotProps">
|
|
<div class="pl-4" data-testid="keybinding-expansion-content">
|
|
<div
|
|
v-for="(binding, idx) in (slotProps.data as ICommandData)
|
|
.keybindings"
|
|
:key="binding.combo.serialize()"
|
|
data-testid="keybinding-expansion-binding"
|
|
class="flex items-center justify-between border-b border-border-subtle py-1.5 last:border-b-0"
|
|
>
|
|
<div class="flex items-center gap-4">
|
|
<span class="text-muted-foreground">{{
|
|
slotProps.data.label
|
|
}}</span>
|
|
<KeyComboDisplay
|
|
:key-combo="binding.combo"
|
|
:is-modified="slotProps.data.isModified"
|
|
/>
|
|
</div>
|
|
<div class="flex flex-row">
|
|
<Button
|
|
v-tooltip="$t('g.edit')"
|
|
variant="textonly"
|
|
size="icon"
|
|
:aria-label="$t('g.edit')"
|
|
@click="editKeybinding(slotProps.data, binding)"
|
|
>
|
|
<i class="icon-[lucide--pencil]" />
|
|
</Button>
|
|
<Button
|
|
v-tooltip="$t('g.removeKeybinding')"
|
|
variant="textonly"
|
|
size="icon"
|
|
:aria-label="$t('g.removeKeybinding')"
|
|
@click="removeSingleKeybinding(slotProps.data, idx)"
|
|
>
|
|
<i class="icon-[lucide--trash-2]" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</DataTable>
|
|
</div>
|
|
</ContextMenuTrigger>
|
|
<ContextMenuPortal>
|
|
<ContextMenuContent
|
|
:style="keybindingOverlayContentStyle"
|
|
class="z-1200 min-w-56 rounded-lg border border-border-subtle bg-base-background px-2 py-3 shadow-interface"
|
|
>
|
|
<ContextMenuItem
|
|
class="flex cursor-pointer items-center gap-2 rounded-sm px-3 py-2 text-sm text-text-primary outline-none select-none hover:bg-node-component-surface-hovered focus:bg-node-component-surface-hovered data-disabled:cursor-default data-disabled:opacity-50"
|
|
:disabled="
|
|
!contextMenuTarget || contextMenuTarget.keybindings.length === 0
|
|
"
|
|
@select="ctxChangeKeybinding"
|
|
>
|
|
<i class="icon-[lucide--pencil] size-4" />
|
|
{{ $t('g.changeKeybinding') }}
|
|
</ContextMenuItem>
|
|
<ContextMenuItem
|
|
class="flex cursor-pointer items-center gap-2 rounded-sm px-3 py-2 text-sm text-text-primary outline-none select-none hover:bg-node-component-surface-hovered focus:bg-node-component-surface-hovered"
|
|
@select="ctxAddKeybinding"
|
|
>
|
|
<i class="icon-[lucide--plus] size-4" />
|
|
{{ $t('g.addNewKeybinding') }}
|
|
</ContextMenuItem>
|
|
<ContextMenuSeparator class="my-1 h-px bg-border-subtle" />
|
|
<ContextMenuItem
|
|
class="flex cursor-pointer items-center gap-2 rounded-sm px-3 py-2 text-sm text-text-primary outline-none select-none hover:bg-node-component-surface-hovered focus:bg-node-component-surface-hovered data-disabled:cursor-default data-disabled:opacity-50"
|
|
:disabled="!contextMenuTarget?.isModified"
|
|
@select="ctxResetToDefault"
|
|
>
|
|
<i class="icon-[lucide--rotate-ccw] size-4" />
|
|
{{ $t('g.resetToDefault') }}
|
|
</ContextMenuItem>
|
|
<ContextMenuItem
|
|
class="flex cursor-pointer items-center gap-2 rounded-sm px-3 py-2 text-sm text-text-primary outline-none select-none hover:bg-node-component-surface-hovered focus:bg-node-component-surface-hovered data-disabled:cursor-default data-disabled:opacity-50"
|
|
:disabled="
|
|
!contextMenuTarget || contextMenuTarget.keybindings.length === 0
|
|
"
|
|
@select="ctxRemoveKeybinding"
|
|
>
|
|
<i class="icon-[lucide--trash-2] size-4" />
|
|
{{ $t('g.removeKeybinding') }}
|
|
</ContextMenuItem>
|
|
</ContextMenuContent>
|
|
</ContextMenuPortal>
|
|
</ContextMenuRoot>
|
|
|
|
<Button
|
|
v-tooltip="$t('g.resetAllKeybindingsTooltip')"
|
|
class="mt-4 w-full"
|
|
variant="destructive-textonly"
|
|
@click="resetAllKeybindings"
|
|
>
|
|
<i class="icon-[lucide--rotate-ccw]" />
|
|
{{ $t('g.resetAll') }}
|
|
</Button>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { MenuItem } from 'primevue/menuitem'
|
|
import { FilterMatchMode } from '@primevue/core/api'
|
|
import Column from 'primevue/column'
|
|
import DataTable from 'primevue/datatable'
|
|
import { useToast } from 'primevue/usetoast'
|
|
import {
|
|
ContextMenuContent,
|
|
ContextMenuItem,
|
|
ContextMenuPortal,
|
|
ContextMenuRoot,
|
|
ContextMenuSeparator,
|
|
ContextMenuTrigger
|
|
} from 'reka-ui'
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import DropdownMenu from '@/components/common/DropdownMenu.vue'
|
|
import { showConfirmDialog } from '@/components/dialog/confirm/confirmDialog'
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import SearchInput from '@/components/ui/search-input/SearchInput.vue'
|
|
import { useEditKeybindingDialog } from '@/composables/useEditKeybindingDialog'
|
|
import { usePrimeVueOverlayChildStyle } from '@/composables/usePopoverSizing'
|
|
import type { KeybindingImpl } from '@/platform/keybindings/keybinding'
|
|
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
|
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
|
import { useKeybindingPresetService } from '@/platform/keybindings/presetService'
|
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
import { useCommandStore } from '@/stores/commandStore'
|
|
import { useDialogStore } from '@/stores/dialogStore'
|
|
import { normalizeI18nKey } from '@/utils/formatUtil'
|
|
|
|
import KeybindingPresetToolbar from './keybinding/KeybindingPresetToolbar.vue'
|
|
import KeyComboDisplay from './keybinding/KeyComboDisplay.vue'
|
|
|
|
const filters = ref({
|
|
global: { value: '', matchMode: FilterMatchMode.CONTAINS }
|
|
})
|
|
|
|
const keybindingStore = useKeybindingStore()
|
|
const keybindingService = useKeybindingService()
|
|
const presetService = useKeybindingPresetService()
|
|
const settingStore = useSettingStore()
|
|
const commandStore = useCommandStore()
|
|
const dialogStore = useDialogStore()
|
|
const { t } = useI18n()
|
|
const primeVueOverlay = usePrimeVueOverlayChildStyle()
|
|
const keybindingOverlayContentStyle = primeVueOverlay.contentStyle
|
|
|
|
const presetNames = ref<string[]>([])
|
|
|
|
async function refreshPresetList() {
|
|
presetNames.value = (await presetService.listPresets()) ?? []
|
|
}
|
|
|
|
async function initPresets() {
|
|
await refreshPresetList()
|
|
const currentName = settingStore.get('Comfy.Keybinding.CurrentPreset')
|
|
if (currentName !== 'default') {
|
|
const preset = await presetService.loadPreset(currentName)
|
|
if (preset) {
|
|
keybindingStore.savedPresetData = preset
|
|
keybindingStore.currentPresetName = currentName
|
|
} else {
|
|
await presetService.switchToDefaultPreset()
|
|
}
|
|
}
|
|
}
|
|
|
|
onMounted(() => initPresets())
|
|
|
|
// "..." menu entries (teleported to header)
|
|
async function saveAsNewPreset() {
|
|
await presetService.promptAndSaveNewPreset()
|
|
refreshPresetList()
|
|
}
|
|
|
|
async function handleDeletePreset() {
|
|
await presetService.deletePreset(keybindingStore.currentPresetName)
|
|
refreshPresetList()
|
|
}
|
|
|
|
async function handleImportPreset() {
|
|
await presetService.importPreset()
|
|
refreshPresetList()
|
|
}
|
|
|
|
const showSaveAsNew = computed(
|
|
() =>
|
|
keybindingStore.currentPresetName !== 'default' ||
|
|
keybindingStore.isCurrentPresetModified
|
|
)
|
|
|
|
const menuEntries = computed<MenuItem[]>(() => [
|
|
...(showSaveAsNew.value
|
|
? [
|
|
{
|
|
label: t('g.keybindingPresets.saveAsNewPreset'),
|
|
icon: 'icon-[lucide--save]',
|
|
command: saveAsNewPreset
|
|
}
|
|
]
|
|
: []),
|
|
{
|
|
label: t('g.keybindingPresets.resetToDefault'),
|
|
icon: 'icon-[lucide--rotate-cw]',
|
|
command: () =>
|
|
presetService.switchPreset('default').then(() => refreshPresetList())
|
|
},
|
|
{
|
|
label: t('g.keybindingPresets.deletePreset'),
|
|
icon: 'icon-[lucide--trash-2]',
|
|
disabled: keybindingStore.currentPresetName === 'default',
|
|
command: handleDeletePreset
|
|
},
|
|
{
|
|
label: t('g.keybindingPresets.importPreset'),
|
|
icon: 'icon-[lucide--file-input]',
|
|
command: handleImportPreset
|
|
},
|
|
{
|
|
label: t('g.keybindingPresets.exportPreset'),
|
|
icon: 'icon-[lucide--file-output]',
|
|
command: () => presetService.exportPreset()
|
|
}
|
|
])
|
|
|
|
// Keybinding table logic
|
|
interface ICommandData {
|
|
id: string
|
|
keybindings: KeybindingImpl[]
|
|
label: string
|
|
source?: string
|
|
isModified: boolean
|
|
}
|
|
|
|
const commandsData = computed<ICommandData[]>(() => {
|
|
return Object.values(commandStore.commands).map((command) => ({
|
|
id: command.id,
|
|
label: t(
|
|
`commands.${normalizeI18nKey(command.id)}.label`,
|
|
command.label ?? ''
|
|
),
|
|
keybindings: keybindingStore.getKeybindingsByCommandId(command.id),
|
|
source: command.source,
|
|
isModified: keybindingStore.isCommandKeybindingModified(command.id)
|
|
}))
|
|
})
|
|
|
|
const expandedCommandIds = ref<Set<string>>(new Set())
|
|
|
|
const expandedRows = computed({
|
|
get() {
|
|
const result: Record<string, boolean> = {}
|
|
for (const id of expandedCommandIds.value) {
|
|
result[id] = true
|
|
}
|
|
return result
|
|
},
|
|
set(value: Record<string, boolean>) {
|
|
expandedCommandIds.value = new Set(Object.keys(value))
|
|
}
|
|
})
|
|
|
|
function toggleExpanded(commandId: string) {
|
|
if (expandedCommandIds.value.has(commandId)) {
|
|
expandedCommandIds.value.delete(commandId)
|
|
} else {
|
|
expandedCommandIds.value.add(commandId)
|
|
}
|
|
}
|
|
|
|
watch(filters, () => expandedCommandIds.value.clear(), { deep: true })
|
|
|
|
const selectedCommandData = ref<ICommandData | null>(null)
|
|
const editKeybindingDialog = useEditKeybindingDialog()
|
|
|
|
const contextMenuTarget = ref<ICommandData | null>(null)
|
|
|
|
function editKeybinding(commandData: ICommandData, binding: KeybindingImpl) {
|
|
editKeybindingDialog.show({
|
|
commandId: commandData.id,
|
|
commandLabel: commandData.label,
|
|
currentCombo: binding.combo,
|
|
mode: 'edit',
|
|
existingBinding: binding
|
|
})
|
|
}
|
|
|
|
function addKeybinding(commandData: ICommandData) {
|
|
editKeybindingDialog.show({
|
|
commandId: commandData.id,
|
|
commandLabel: commandData.label,
|
|
currentCombo: null,
|
|
mode: 'add'
|
|
})
|
|
}
|
|
|
|
function handleRowClick(event: { originalEvent: Event; data: ICommandData }) {
|
|
const target = event.originalEvent.target as HTMLElement
|
|
if (target.closest('.actions')) return
|
|
const commandData = event.data
|
|
if (
|
|
commandData.keybindings.length >= 2 ||
|
|
expandedCommandIds.value.has(commandData.id)
|
|
) {
|
|
toggleExpanded(commandData.id)
|
|
}
|
|
}
|
|
|
|
function handleRowDblClick(commandData: ICommandData) {
|
|
if (commandData.keybindings.length === 0) {
|
|
addKeybinding(commandData)
|
|
} else if (commandData.keybindings.length === 1) {
|
|
editKeybinding(commandData, commandData.keybindings[0])
|
|
}
|
|
}
|
|
|
|
function handleRowContextMenu(event: {
|
|
originalEvent: Event
|
|
data: ICommandData
|
|
}) {
|
|
contextMenuTarget.value = event.data
|
|
}
|
|
|
|
function clearContextMenuTarget() {
|
|
contextMenuTarget.value = null
|
|
}
|
|
|
|
async function removeSingleKeybinding(
|
|
commandData: ICommandData,
|
|
index: number
|
|
) {
|
|
const binding = commandData.keybindings[index]
|
|
if (binding) {
|
|
keybindingStore.unsetKeybinding(binding)
|
|
if (commandData.keybindings.length <= 2) {
|
|
expandedCommandIds.value.delete(commandData.id)
|
|
}
|
|
await keybindingService.persistUserKeybindings()
|
|
}
|
|
}
|
|
|
|
function handleRemoveAllKeybindings(commandData: ICommandData) {
|
|
const dialog = showConfirmDialog({
|
|
headerProps: { title: t('g.removeAllKeybindingsTitle') },
|
|
props: { promptText: t('g.removeAllKeybindingsMessage') },
|
|
footerProps: {
|
|
confirmText: t('g.removeAll'),
|
|
confirmVariant: 'destructive',
|
|
onCancel: () => dialogStore.closeDialog(dialog),
|
|
onConfirm: async () => {
|
|
keybindingStore.removeAllKeybindingsForCommand(commandData.id)
|
|
await keybindingService.persistUserKeybindings()
|
|
dialogStore.closeDialog(dialog)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function handleRemoveKeybindingFromMenu(commandData: ICommandData) {
|
|
if (commandData.keybindings.length >= 2) {
|
|
handleRemoveAllKeybindings(commandData)
|
|
} else {
|
|
removeSingleKeybinding(commandData, 0)
|
|
}
|
|
}
|
|
|
|
function ctxChangeKeybinding() {
|
|
if (!contextMenuTarget.value) return
|
|
const target = contextMenuTarget.value
|
|
if (target.keybindings.length === 1) {
|
|
editKeybinding(target, target.keybindings[0])
|
|
} else if (target.keybindings.length >= 2) {
|
|
if (!expandedCommandIds.value.has(target.id)) {
|
|
toggleExpanded(target.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
function ctxAddKeybinding() {
|
|
if (contextMenuTarget.value) {
|
|
addKeybinding(contextMenuTarget.value)
|
|
}
|
|
}
|
|
|
|
function ctxResetToDefault() {
|
|
if (contextMenuTarget.value) {
|
|
resetKeybinding(contextMenuTarget.value)
|
|
}
|
|
}
|
|
|
|
function ctxRemoveKeybinding() {
|
|
if (
|
|
contextMenuTarget.value &&
|
|
contextMenuTarget.value.keybindings.length > 0
|
|
) {
|
|
handleRemoveKeybindingFromMenu(contextMenuTarget.value)
|
|
}
|
|
}
|
|
|
|
async function resetKeybinding(commandData: ICommandData) {
|
|
if (keybindingStore.resetKeybindingForCommand(commandData.id)) {
|
|
expandedCommandIds.value.delete(commandData.id)
|
|
await keybindingService.persistUserKeybindings()
|
|
} else {
|
|
console.warn(
|
|
`No changes made when resetting keybinding for command: ${commandData.id}`
|
|
)
|
|
}
|
|
}
|
|
|
|
const toast = useToast()
|
|
|
|
function resetAllKeybindings() {
|
|
const dialog = showConfirmDialog({
|
|
headerProps: {
|
|
title: t('g.resetAllKeybindingsTitle')
|
|
},
|
|
props: {
|
|
promptText: t('g.resetAllKeybindingsMessage')
|
|
},
|
|
footerProps: {
|
|
confirmText: t('g.resetAll'),
|
|
confirmVariant: 'destructive',
|
|
onCancel: () => {
|
|
dialogStore.closeDialog(dialog)
|
|
},
|
|
onConfirm: async () => {
|
|
keybindingStore.resetAllKeybindings()
|
|
await keybindingService.persistUserKeybindings()
|
|
dialogStore.closeDialog(dialog)
|
|
toast.add({
|
|
severity: 'info',
|
|
summary: t('g.info'),
|
|
detail: t('g.allKeybindingsReset'),
|
|
life: 3000
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
</script>
|