From 054a731b120d6fa6491e5e18ed914c782492d677 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Mon, 2 Nov 2026 19:26:24 +1100 Subject: [PATCH 1/3] move pane menu item prepration to solid-ui --- .../ResourceActionsMenu.test.ts | 25 +++++++ src/lib/file-explorer/index.ts | 1 + src/lib/file-explorer/pane-menu-items.ts | 66 +++++++++++++++++ .../file-explorer/pane-menu-items.test.ts | 72 +++++++++++++++++++ test/unit/index.test.ts | 1 + 5 files changed, 165 insertions(+) create mode 100644 src/lib/file-explorer/pane-menu-items.ts create mode 100644 test/unit/file-explorer/pane-menu-items.test.ts diff --git a/src/components/resource-actions-menu/ResourceActionsMenu.test.ts b/src/components/resource-actions-menu/ResourceActionsMenu.test.ts index 07e9e01f8..8523fb7d8 100644 --- a/src/components/resource-actions-menu/ResourceActionsMenu.test.ts +++ b/src/components/resource-actions-menu/ResourceActionsMenu.test.ts @@ -92,4 +92,29 @@ describe('solid-ui-resource-actions-menu', () => { open.mockRestore() menu.remove() }) + + it('renders supplied pane actions and dispatches their event', async () => { + const menu = document.createElement('solid-ui-resource-actions-menu') as HTMLElement & { + subjectUri?: string + menuItems?: Array<{ label: string, action: (event: Event) => void }> + updateComplete: Promise + } + const action = vi.fn() + menu.subjectUri = 'https://example.test/resource.ttl' + menu.menuItems = [{ label: 'View as Source', action }] + + document.body.appendChild(menu) + await menu.updateComplete + + const item = Array.from(menu.shadowRoot?.querySelectorAll('solid-ui-menu-item') || []) + .find(candidate => candidate.textContent?.includes('View as Source')) + expect(item).toBeTruthy() + + const event = new CustomEvent('solid-ui-select', { bubbles: true, composed: true }) + item?.dispatchEvent(event) + expect(action).toHaveBeenCalledOnce() + expect(action).toHaveBeenCalledWith(event) + + menu.remove() + }) }) diff --git a/src/lib/file-explorer/index.ts b/src/lib/file-explorer/index.ts index b495aab66..da7c8c7e3 100644 --- a/src/lib/file-explorer/index.ts +++ b/src/lib/file-explorer/index.ts @@ -2,3 +2,4 @@ export * from './context' export * from './events' export * from './relevant-panes' export * from './compose-explorer' +export * from './pane-menu-items' diff --git a/src/lib/file-explorer/pane-menu-items.ts b/src/lib/file-explorer/pane-menu-items.ts new file mode 100644 index 000000000..f95185464 --- /dev/null +++ b/src/lib/file-explorer/pane-menu-items.ts @@ -0,0 +1,66 @@ +import type { DataBrowserContext, PaneDefinition } from 'pane-registry' +import type { NamedNode } from 'rdflib' + +export type PaneMenuIcon = string + +export type PaneMenuItem = { + key?: string + label: string + icon?: PaneMenuIcon + action: (event: Event) => void +} + +export type PaneMenuPane = Omit & { + icon: string | PaneIconResolver +} + +export type PaneIconResolver = ( + subject: NamedNode, + context: DataBrowserContext +) => PaneMenuIcon | undefined | Promise + +export type PaneMenuPreparation = { + subject: NamedNode + context: DataBrowserContext + panes: PaneMenuPane[] + openPane: (subject: NamedNode, paneName: string, event: Event) => void + iconOverrides?: Record +} + +async function resolvePaneIcon ( + pane: PaneMenuPane, + subject: NamedNode, + context: DataBrowserContext, + override: PaneIconResolver | undefined +) { + if (override) return override(subject, context) + return typeof pane.icon === 'function' + ? pane.icon(subject, context) + : pane.icon +} + +export async function preparePaneMenuItems ( + input: PaneMenuPreparation +): Promise { + const items: Array = await Promise.all(input.panes.map(async pane => { + const label = pane.label(input.subject, input.context) + if (!label) return undefined + + const icon = await resolvePaneIcon( + pane, + input.subject, + input.context, + input.iconOverrides?.[pane.name] + ) + + const item: PaneMenuItem = { + key: pane.name, + label, + ...(icon ? { icon } : {}), + action: (event: Event) => input.openPane(input.subject, pane.name, event) + } + return item + })) + + return items.filter((item): item is PaneMenuItem => item !== undefined) +} diff --git a/test/unit/file-explorer/pane-menu-items.test.ts b/test/unit/file-explorer/pane-menu-items.test.ts new file mode 100644 index 000000000..6bab6b6d6 --- /dev/null +++ b/test/unit/file-explorer/pane-menu-items.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it, vi } from 'vitest' +import type { DataBrowserContext } from 'pane-registry' +import { sym } from 'rdflib' +import { + preparePaneMenuItems, + type PaneIconResolver, + type PaneMenuPane +} from '../../../src/lib/file-explorer/pane-menu-items' + +const subject = sym('https://example.test/resource') +const context = {} as DataBrowserContext + +function createPane (name: string, icon: string | PaneIconResolver): PaneMenuPane { + return { + name, + icon, + label: () => name, + render: () => document.createElement('div') + } +} + +describe('preparePaneMenuItems', () => { + it('prepares labeled pane actions in caller-provided order', async () => { + const openPane = vi.fn() + const panes = [createPane('first', 'first.svg'), createPane('second', 'second.svg')] + + const items = await preparePaneMenuItems({ subject, context, panes, openPane }) + + expect(items.map(item => item.label)).toEqual(['first', 'second']) + expect(items.map(item => item.icon)).toEqual(['first.svg', 'second.svg']) + + const event = new Event('select') + items[1].action(event) + expect(openPane).toHaveBeenCalledWith(subject, 'second', event) + }) + + it('skips panes without labels and resolves function-valued icons', async () => { + const iconResolver = vi.fn(() => 'resolved.svg') + const panes = [ + { ...createPane('hidden', 'hidden.svg'), label: () => null }, + { ...createPane('visible', iconResolver), label: () => 'Visible' } + ] + + const items = await preparePaneMenuItems({ + subject, + context, + panes, + openPane: vi.fn() + }) + + expect(items).toHaveLength(1) + expect(items[0].label).toBe('Visible') + expect(items[0].icon).toBe('resolved.svg') + expect(iconResolver).toHaveBeenCalledWith(subject, context) + }) + + it('uses a host icon override without changing pane ordering', async () => { + const override = vi.fn(() => 'profile.svg') + const pane = createPane('profile', 'default.svg') + + const items = await preparePaneMenuItems({ + subject, + context, + panes: [pane], + openPane: vi.fn(), + iconOverrides: { profile: override } + }) + + expect(items[0].icon).toBe('profile.svg') + expect(override).toHaveBeenCalledWith(subject, context) + }) +}) diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index b030f2526..eb4fc5657 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -54,6 +54,7 @@ describe('Index', () => { 'pad', 'participation', 'preferences', + 'preparePaneMenuItems', 'showDialog', 'style', 'table', From ff2d1a8b6fcc935bb7e9a2d63e5264e4125b1a7c Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Mon, 2 Nov 2026 22:19:17 +1100 Subject: [PATCH 2/3] update version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e45c6267c..85053847d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "solid-ui", - "version": "5.0.2-5", + "version": "5.0.2-6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "solid-ui", - "version": "5.0.2-5", + "version": "5.0.2-6", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1fa5d05cb..7b0df550f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solid-ui", - "version": "5.0.2-5", + "version": "5.0.2-6", "description": "UI library for Solid applications", "main": "dist/index.cjs.js", "types": "dist/index.d.ts", From e2778c74f41ac042f96ff493e493fc0c579ac06e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 2 Nov 2026 11:21:00 +0000 Subject: [PATCH 3/3] chore: update solidos dependencies (dev: solid-logic@6.0.1-5 pane-registry@5.0.2-2) (latest: rdflib@2.4.1) --- package-lock.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85053847d..d8d130cbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5312,9 +5312,9 @@ } }, "node_modules/@types/jsdom/node_modules/entities": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", - "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-9.0.0.tgz", + "integrity": "sha512-oJFNDJg3kTPwAD1cNn2+WmUq4wFoo09npc3Ydf1D7ED4smRiWrsKnaLxykrTWgyGEcusc87P5dxlAleol9kpkg==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5325,13 +5325,13 @@ } }, "node_modules/@types/jsdom/node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.1.0.tgz", + "integrity": "sha512-sq3qBFpee5jeocbFbO48VHlGr8sT7mbXS0vxlKN6sottBtJzrOG3998sr0llSZcQAGgfpdmEYAIZE0P9z0dE8w==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^8.0.0" + "entities": "^9.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -9093,9 +9093,9 @@ } }, "node_modules/globals": { - "version": "17.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", - "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", + "version": "17.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.14.0.tgz", + "integrity": "sha512-UtdkTVyAJePQv0J2M9Xe4L0ruNSDV314XiIJsADI2Zf40bcdAonE/D/OhAU8AT99oij9mhLByb+NvA3QWZYgCg==", "dev": true, "license": "MIT", "engines": { @@ -10186,9 +10186,9 @@ } }, "node_modules/jsdom/node_modules/entities": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", - "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-9.0.0.tgz", + "integrity": "sha512-oJFNDJg3kTPwAD1cNn2+WmUq4wFoo09npc3Ydf1D7ED4smRiWrsKnaLxykrTWgyGEcusc87P5dxlAleol9kpkg==", "devOptional": true, "license": "BSD-2-Clause", "engines": { @@ -10199,13 +10199,13 @@ } }, "node_modules/jsdom/node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.1.0.tgz", + "integrity": "sha512-sq3qBFpee5jeocbFbO48VHlGr8sT7mbXS0vxlKN6sottBtJzrOG3998sr0llSZcQAGgfpdmEYAIZE0P9z0dE8w==", "devOptional": true, "license": "MIT", "dependencies": { - "entities": "^8.0.0" + "entities": "^9.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1"