Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
25 changes: 25 additions & 0 deletions src/components/resource-actions-menu/ResourceActionsMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>
}
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()
})
})
1 change: 1 addition & 0 deletions src/lib/file-explorer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './context'
export * from './events'
export * from './relevant-panes'
export * from './compose-explorer'
export * from './pane-menu-items'
66 changes: 66 additions & 0 deletions src/lib/file-explorer/pane-menu-items.ts
Original file line number Diff line number Diff line change
@@ -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<PaneDefinition, 'icon'> & {
icon: string | PaneIconResolver
}

export type PaneIconResolver = (
subject: NamedNode,
context: DataBrowserContext
) => PaneMenuIcon | undefined | Promise<PaneMenuIcon | undefined>

export type PaneMenuPreparation = {
subject: NamedNode
context: DataBrowserContext
panes: PaneMenuPane[]
openPane: (subject: NamedNode, paneName: string, event: Event) => void
iconOverrides?: Record<string, PaneIconResolver>
}

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<PaneMenuItem[]> {
const items: Array<PaneMenuItem | undefined> = 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)
}
72 changes: 72 additions & 0 deletions test/unit/file-explorer/pane-menu-items.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
1 change: 1 addition & 0 deletions test/unit/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ describe('Index', () => {
'pad',
'participation',
'preferences',
'preparePaneMenuItems',
'showDialog',
'style',
'table',
Expand Down
Loading