From 5c7207fb65a1e134172589d8fe79d92a7a98c1e0 Mon Sep 17 00:00:00 2001 From: bymyself Date: Fri, 30 Jan 2026 00:17:25 -0800 Subject: [PATCH] refactor: address review feedback - Consolidate layout config into single computed with Record type - Use reactive props destructuring per AGENTS.md - Change h-[640px] back to max-h-[640px] for proper sizing - Replace hardcoded zinc color with semantic text-muted-foreground - Add defineSlots to VirtualGrid for type safety - Add unit tests for VirtualGrid and FormDropdownMenu Amp-Thread-ID: https://ampcode.com/threads/T-019c0ca8-be8d-770e-ab31-349937cd2acf Co-authored-by: Amp --- src/components/common/VirtualGrid.test.ts | 121 +++++++++++++ src/components/common/VirtualGrid.vue | 4 + .../form/dropdown/FormDropdownMenu.test.ts | 159 ++++++++++++++++++ .../form/dropdown/FormDropdownMenu.vue | 78 ++++----- 4 files changed, 313 insertions(+), 49 deletions(-) create mode 100644 src/components/common/VirtualGrid.test.ts create mode 100644 src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.test.ts diff --git a/src/components/common/VirtualGrid.test.ts b/src/components/common/VirtualGrid.test.ts new file mode 100644 index 0000000000..0795709501 --- /dev/null +++ b/src/components/common/VirtualGrid.test.ts @@ -0,0 +1,121 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import { nextTick } from 'vue' + +import VirtualGrid from './VirtualGrid.vue' + +type TestItem = { key: string; name: string } + +function createItems(count: number): TestItem[] { + return Array.from({ length: count }, (_, i) => ({ + key: `item-${i}`, + name: `Item ${i}` + })) +} + +describe('VirtualGrid', () => { + const defaultGridStyle = { + display: 'grid', + gridTemplateColumns: 'repeat(4, 1fr)', + gap: '1rem' + } + + it('renders items within the visible range', async () => { + const items = createItems(100) + const wrapper = mount(VirtualGrid, { + props: { + items, + gridStyle: defaultGridStyle, + defaultItemHeight: 100, + defaultItemWidth: 100, + maxColumns: 4, + bufferRows: 1 + }, + slots: { + item: `` + }, + attachTo: document.body + }) + + await nextTick() + + const renderedItems = wrapper.findAll('.test-item') + expect(renderedItems.length).toBeLessThan(items.length) + + wrapper.unmount() + }) + + it('provides correct index in slot props', async () => { + const items = createItems(20) + const receivedIndices: number[] = [] + + const wrapper = mount(VirtualGrid, { + props: { + items, + gridStyle: defaultGridStyle, + defaultItemHeight: 50, + defaultItemWidth: 100, + maxColumns: 1, + bufferRows: 0 + }, + slots: { + item: ({ index }: { index: number }) => { + receivedIndices.push(index) + return null + } + }, + attachTo: document.body + }) + + await nextTick() + + if (receivedIndices.length > 0) { + expect(receivedIndices[0]).toBe(0) + for (let i = 1; i < receivedIndices.length; i++) { + expect(receivedIndices[i]).toBe(receivedIndices[i - 1] + 1) + } + } + + wrapper.unmount() + }) + + it('respects maxColumns prop', async () => { + const items = createItems(10) + const wrapper = mount(VirtualGrid, { + props: { + items, + gridStyle: defaultGridStyle, + maxColumns: 2 + }, + attachTo: document.body + }) + + await nextTick() + + const gridElement = wrapper.find('[style*="grid"]') + expect(gridElement.exists()).toBe(true) + + wrapper.unmount() + }) + + it('renders empty when no items provided', async () => { + const wrapper = mount(VirtualGrid, { + props: { + items: [], + gridStyle: defaultGridStyle + }, + slots: { + item: `` + } + }) + + await nextTick() + + const renderedItems = wrapper.findAll('.test-item') + expect(renderedItems.length).toBe(0) + }) +}) diff --git a/src/components/common/VirtualGrid.vue b/src/components/common/VirtualGrid.vue index 373610b88a..2a87c29b50 100644 --- a/src/components/common/VirtualGrid.vue +++ b/src/components/common/VirtualGrid.vue @@ -57,6 +57,10 @@ const emit = defineEmits<{ 'approach-end': [] }>() +defineSlots<{ + item: (props: { item: T & { key: string }; index: number }) => unknown +}>() + const itemHeight = ref(defaultItemHeight) const itemWidth = ref(defaultItemWidth) const container = ref(null) diff --git a/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.test.ts b/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.test.ts new file mode 100644 index 0000000000..5948dd7664 --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.test.ts @@ -0,0 +1,159 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import { nextTick } from 'vue' + +import FormDropdownMenu from './FormDropdownMenu.vue' +import type { DropdownItem, LayoutMode } from './types' + +function createItem(id: string, name: string): DropdownItem { + return { + id, + mediaSrc: '', + name, + label: name, + metadata: '' + } +} + +describe('FormDropdownMenu', () => { + const defaultProps = { + items: [createItem('1', 'Item 1'), createItem('2', 'Item 2')], + isSelected: () => false, + filterOptions: [], + sortOptions: [] + } + + it('renders empty state when no items', async () => { + const wrapper = mount(FormDropdownMenu, { + props: { + ...defaultProps, + items: [] + }, + global: { + stubs: { + FormDropdownMenuFilter: true, + FormDropdownMenuActions: true, + VirtualGrid: true + }, + mocks: { + $t: (key: string) => key + } + } + }) + + await nextTick() + + const emptyIcon = wrapper.find('.icon-\\[lucide--circle-off\\]') + expect(emptyIcon.exists()).toBe(true) + }) + + it('renders VirtualGrid when items exist', async () => { + const wrapper = mount(FormDropdownMenu, { + props: defaultProps, + global: { + stubs: { + FormDropdownMenuFilter: true, + FormDropdownMenuActions: true, + VirtualGrid: true + } + } + }) + + await nextTick() + + const virtualGrid = wrapper.findComponent({ name: 'VirtualGrid' }) + expect(virtualGrid.exists()).toBe(true) + }) + + it('transforms items to include key property for VirtualGrid', async () => { + const items = [createItem('1', 'Item 1'), createItem('2', 'Item 2')] + const wrapper = mount(FormDropdownMenu, { + props: { + ...defaultProps, + items + }, + global: { + stubs: { + FormDropdownMenuFilter: true, + FormDropdownMenuActions: true, + VirtualGrid: true + } + } + }) + + await nextTick() + + const virtualGrid = wrapper.findComponent({ name: 'VirtualGrid' }) + const virtualItems = virtualGrid.props('items') + + expect(virtualItems).toHaveLength(2) + expect(virtualItems[0]).toHaveProperty('key', '1') + expect(virtualItems[1]).toHaveProperty('key', '2') + }) + + it('passes correct layout config for grid mode', async () => { + const wrapper = mount(FormDropdownMenu, { + props: { + ...defaultProps, + layoutMode: 'grid' as LayoutMode + }, + global: { + stubs: { + FormDropdownMenuFilter: true, + FormDropdownMenuActions: true, + VirtualGrid: true + } + } + }) + + await nextTick() + + const virtualGrid = wrapper.findComponent({ name: 'VirtualGrid' }) + expect(virtualGrid.props('maxColumns')).toBe(4) + expect(virtualGrid.props('defaultItemHeight')).toBe(120) + }) + + it('passes correct layout config for list mode', async () => { + const wrapper = mount(FormDropdownMenu, { + props: { + ...defaultProps, + layoutMode: 'list' as LayoutMode + }, + global: { + stubs: { + FormDropdownMenuFilter: true, + FormDropdownMenuActions: true, + VirtualGrid: true + } + } + }) + + await nextTick() + + const virtualGrid = wrapper.findComponent({ name: 'VirtualGrid' }) + expect(virtualGrid.props('maxColumns')).toBe(1) + expect(virtualGrid.props('defaultItemHeight')).toBe(64) + }) + + it('passes correct layout config for list-small mode', async () => { + const wrapper = mount(FormDropdownMenu, { + props: { + ...defaultProps, + layoutMode: 'list-small' as LayoutMode + }, + global: { + stubs: { + FormDropdownMenuFilter: true, + FormDropdownMenuActions: true, + VirtualGrid: true + } + } + }) + + await nextTick() + + const virtualGrid = wrapper.findComponent({ name: 'VirtualGrid' }) + expect(virtualGrid.props('maxColumns')).toBe(1) + expect(virtualGrid.props('defaultItemHeight')).toBe(40) + }) +}) diff --git a/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue b/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue index cc3f18d12c..70657fbc32 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue @@ -27,7 +27,8 @@ interface Props { updateKey?: MaybeRefOrGetter } -const props = defineProps() +const { items, isSelected, filterOptions, sortOptions, searcher, updateKey } = + defineProps() const emit = defineEmits<{ (e: 'item-click', item: DropdownItem, index: number): void }>() @@ -38,61 +39,40 @@ const layoutMode = defineModel('layoutMode') const sortSelected = defineModel('sortSelected') const searchQuery = defineModel('searchQuery') -// VirtualGrid configuration based on layout mode -const maxColumns = computed(() => { - switch (layoutMode.value) { - case 'grid': - return 4 - case 'list': - case 'list-small': - return 1 - default: - return 4 - } -}) +// VirtualGrid layout configuration +type LayoutConfig = { + maxColumns: number + itemHeight: number + itemWidth: number + gap: string +} -const defaultItemHeight = computed(() => { - switch (layoutMode.value) { - case 'grid': - return 120 - case 'list': - return 64 - case 'list-small': - return 40 - default: - return 120 +const LAYOUT_CONFIGS: Record = { + grid: { maxColumns: 4, itemHeight: 120, itemWidth: 89, gap: '1rem 0.5rem' }, + list: { maxColumns: 1, itemHeight: 64, itemWidth: 380, gap: '0.5rem' }, + 'list-small': { + maxColumns: 1, + itemHeight: 40, + itemWidth: 380, + gap: '0.25rem' } -}) +} -const defaultItemWidth = computed(() => { - switch (layoutMode.value) { - case 'grid': - return 89 - case 'list': - case 'list-small': - return 380 - default: - return 89 - } -}) +const layoutConfig = computed( + () => LAYOUT_CONFIGS[layoutMode.value ?? 'grid'] +) const gridStyle = computed(() => ({ display: 'grid', - gridTemplateColumns: - layoutMode.value === 'grid' ? 'repeat(4, 1fr)' : 'repeat(1, 1fr)', - gap: - layoutMode.value === 'grid' - ? '1rem 0.5rem' - : layoutMode.value === 'list' - ? '0.5rem' - : '0.25rem', + gridTemplateColumns: `repeat(${layoutConfig.value.maxColumns}, 1fr)`, + gap: layoutConfig.value.gap, padding: '1rem', width: '100%' })) type VirtualDropdownItem = DropdownItem & { key: string } const virtualItems = computed(() => - props.items.map((item) => ({ + items.map((item) => ({ ...item, key: String(item.id) })) @@ -101,7 +81,7 @@ const virtualItems = computed(() =>