From 0af5ccb4fd4b9c07c12888417e1cee9f6ca40e29 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sun, 27 Sep 2026 09:27:32 +1000 Subject: [PATCH 01/11] use the new manage access dialog --- src/outline/manager.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/outline/manager.js b/src/outline/manager.js index fe5029bd..612e0b0f 100644 --- a/src/outline/manager.js +++ b/src/outline/manager.js @@ -6,7 +6,7 @@ import * as paneRegistry from 'pane-registry' import './manager.css' import * as $rdf from 'rdflib' import * as UI from 'solid-ui' -import { store } from 'solid-logic' +import { solidLogicSingleton, store } from 'solid-logic' import { propertyViews } from './propertyViews' import { outlineIcons } from './outlineIcons.js' // @@ chec import { UserInput } from './userInput.js' @@ -14,6 +14,7 @@ import * as queryByExample from './queryByExample.js' import { loadContainerRepresentation } from '../utils/podUtils' import { isWebIdUri } from '../utils/webIdUtils' import '../components/file-explorer-header' +import AccessControlModal from 'solid-ui/components/access-control-modal' export default function (context) { const dom = context.dom @@ -474,8 +475,16 @@ export default function (context) { provider.paneRenderOptions = options provider.soloPane = options.solo provider.openPane = (paneSubject, paneName) => openPaneInPlace(paneSubject, paneRegistry.byName(paneName)) - // TODO: for now we do this until we create sharing dialog in solid-panes - provider.handleAccessClick = () => openPaneInPlace(subject, paneRegistry.byName('sharing')) + provider.handleAccessClick = async () => { + const accessGrants = await solidLogicSingleton.acl.findAccessGrants(subject) + + UI.showDialog(AccessControlModal, { + props: { + subjectUri: subject.uri, + accessGrants + } + }) + } if (provider.pane) { tr.classList.add('outlinePaneRow') From cc118567840babaeb2c9e0a3d8549b412bb39695 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sun, 27 Sep 2026 21:40:13 +1000 Subject: [PATCH 02/11] remove table structure --- src/outline/manager.js | 394 +++++++++---------------------- src/outline/outlineDomHelpers.ts | 225 ++++++++++++++++++ src/outline/userInput.js | 80 ++++--- 3 files changed, 381 insertions(+), 318 deletions(-) create mode 100644 src/outline/outlineDomHelpers.ts diff --git a/src/outline/manager.js b/src/outline/manager.js index fe5029bd..f220dbbb 100644 --- a/src/outline/manager.js +++ b/src/outline/manager.js @@ -13,6 +13,7 @@ import { UserInput } from './userInput.js' import * as queryByExample from './queryByExample.js' import { loadContainerRepresentation } from '../utils/podUtils' import { isWebIdUri } from '../utils/webIdUtils' +import { createOutlineDomHelpers } from './outlineDomHelpers' import '../components/file-explorer-header' export default function (context) { @@ -40,11 +41,34 @@ export default function (context) { this.UserInput = new UserInput(this) this.clipboardAddress = 'tabulator:clipboard' // Weird this.UserInput.clipboardInit(this.clipboardAddress) - const outlineElement = this.outlineElement + + const { + outlineObjectDiv, + outlinePredicateDiv, + termWidget + } = createOutlineDomHelpers({ + dom, + UI, + kb: store, + outlineIcons, + expandMouseDownListener, + selectableTDClickListener, + setSelected, + viewAsBoringDefault, + removeNodeIconMouseDownListener + }) + + this.outlineObjectDiv = outlineObjectDiv + this.outlinePredicateDiv = outlinePredicateDiv + // old name: outlineObjectTD + this.outlineObjectTD = outlineObjectDiv + // old name: outlinePredicateTD + this.outlinePredicateTD = outlinePredicateDiv + this.termWidget = termWidget this.init = function () { - const table = getOutlineContainer() - table.outline = this + const outlineHost = getOutlineContainer() + outlineHost.outline = this } this.getLayoutMode = function () { @@ -86,7 +110,7 @@ export default function (context) { // / ////////////////////// Representing data - // Represent an object in summary form as a table cell + // Represent an object in summary form as a content block function appendRemoveIcon (node, subject, removeNode) { const image = UI.utils.AJARImage( @@ -173,129 +197,6 @@ export default function (context) { return img } // appendAccessIcon - /** make the td for an object (grammatical object) - * @param obj - an RDF term - * @param view - a VIEW function (rather than a bool asImage) - **/ - - this.outlineObjectTD = function outlineObjectTD ( - obj, - view, - deleteNode, - statement - ) { - const td = dom.createElement('td') - td.classList.add('obj') - td.setAttribute('notSelectable', 'false') - if (!obj) { - td.textContent = 'No object available.' - return td - } - const theClass = 'obj' - - // set about and put 'expand' icon - if ( - obj.termType === 'NamedNode' || - obj.termType === 'BlankNode' || - (obj.termType === 'Literal' && - obj.value.slice && - (obj.value.slice(0, 6) === 'ftp://' || - obj.value.slice(0, 8) === 'https://' || - obj.value.slice(0, 7) === 'http://')) - ) { - td.setAttribute('about', obj.toNT()) - td.appendChild( - UI.utils.AJARImage( - UI.icons.originalIconBase + 'tbl-expand-trans.png', - 'expand', - undefined, - dom - ) - ).addEventListener('click', expandMouseDownListener) - } - td.setAttribute('class', theClass) // this is how you find an object - if (kb.whether(obj, UI.ns.rdf('type'), UI.ns.link('Request'))) { - td.className = 'undetermined' - } // @@? why-timbl - - if (!view) { - // view should be a function pointer - view = viewAsBoringDefault - } - td.appendChild(view(obj)) - if (deleteNode) { - appendRemoveIcon(td, obj, deleteNode) - } - - // set DOM methods - td.tabulatorSelect = function () { - setSelected(this, true) - } - td.tabulatorDeselect = function () { - setSelected(this, false) - } - - td.addEventListener('click', selectableTDClickListener) - return td - } // outlineObjectTD - - this.outlinePredicateTD = function outlinePredicateTD ( - predicate, - newTr, - inverse, - internal - ) { - const predicateTD = dom.createElement('TD') - predicateTD.setAttribute('about', predicate.toNT()) - predicateTD.setAttribute('class', internal ? 'pred internal' : 'pred') - - let lab - switch (predicate.termType) { - case 'BlankNode': // TBD - predicateTD.className = 'undetermined' - break - case 'NamedNode': - lab = UI.utils.predicateLabelForXML(predicate, inverse) - break - case 'Collection': // some choices of predicate - lab = UI.utils.predicateLabelForXML(predicate.elements[0], inverse) - } - lab = lab ? lab.slice(0, 1).toUpperCase() + lab.slice(1) : '...' - // if (kb.statementsMatching(predicate,rdf('type'), UI.ns.link('Request')).length) predicateTD.className='undetermined'; - - const labelTD = dom.createElement('TD') - labelTD.classList.add('labelTD') - labelTD.setAttribute('notSelectable', 'true') - labelTD.appendChild(dom.createTextNode(lab)) - predicateTD.appendChild(labelTD) - labelTD.style.width = '100%' - predicateTD.appendChild(termWidget.construct(dom)) // termWidget is global??? - for (const w in outlineIcons.termWidgets) { - if (!newTr || !newTr.AJAR_statement) break // case for TBD as predicate - // alert(Icon.termWidgets[w]+' '+Icon.termWidgets[w].filter) - if ( - outlineIcons.termWidgets[w].filter && - outlineIcons.termWidgets[w].filter( - newTr.AJAR_statement, - 'pred', - inverse - ) - ) { - termWidget.addIcon(predicateTD, outlineIcons.termWidgets[w]) - } - } - - // set DOM methods - predicateTD.tabulatorSelect = function () { - setSelected(this, true) - } - predicateTD.tabulatorDeselect = function () { - setSelected(this, false) - } - predicateTD.addEventListener('click', selectableTDClickListener) - return predicateTD - } // outlinePredicateTD - function getOutlineContainer () { return getOrCreateContainer('OutlineView', 'Resource browser') } @@ -319,11 +220,8 @@ export default function (context) { * @returns {HTMLElement} */ function getOrCreateContainer (id) { - const containerHost = - document.getElementById('MainContent') || - document.body - // OutlineView is a table + // OutlineView is a simple block container if (id === 'OutlineView') { const existingOutline = document.getElementById('OutlineView') if (existingOutline) { @@ -335,7 +233,7 @@ export default function (context) { document.body if (containerHost) { - const OutlineView = document.createElement('table') + const OutlineView = document.createElement('div') OutlineView.id = 'OutlineView' OutlineView.classList.add('outline-view') OutlineView.setAttribute('aria-label', 'Resource browser') @@ -387,17 +285,6 @@ export default function (context) { } } - function findContainingTable (td) { - let containingTable - for (containingTable = td; containingTable.parentNode; containingTable = containingTable.parentNode) { - if (containingTable.nodeName === 'TABLE') break - } - if (containingTable.nodeName !== 'TABLE') { - throw new Error('outline: internal error.') - } - return containingTable - } - function renderPaneIntoProvider (provider, subject, pane, options) { if (!provider || !pane) return @@ -427,10 +314,12 @@ export default function (context) { if (!outlineView) return const subjectId = subject.toNT() - const subjectTd = outlineView.querySelector('td.paneView[about="' + subjectId + '"]') - if (!subjectTd) return + const subjectNode = outlineView.querySelector('[about="' + subjectId + '"]') + if (!subjectNode) return - const provider = subjectTd.querySelector('file-explorer-provider') + const provider = subjectNode.matches?.('file-explorer-provider') + ? subjectNode + : subjectNode.querySelector('file-explorer-provider') if (!provider) return if (!pane) { UI.log.warn('outline: Unable to open pane in place: pane is undefined.') @@ -442,23 +331,18 @@ export default function (context) { renderPaneIntoProvider(provider, subject, pane, provider.paneRenderOptions) } - async function expandedProviderTR (subject, requiredPane, options) { + async function expandedProviderTR (subject, requiredPane, options, provider) { options = options || {} - // TODO: hide the main storage header - // const showHeader = !!requiredPane - const tr = dom.createElement('tr') + provider = provider || dom.createElement('file-explorer-provider') + provider.classList.add('paneView', 'tdFlex') + provider.setAttribute('notSelectable', 'true') + provider.setAttribute('about', subject.toNT()) if (options.hover) { // By default no hide till hover as community deems it confusing - tr.classList.add('hoverControl') + provider.classList.add('hoverControl') } - const td = tr.appendChild(dom.createElement('td')) - td.classList.add('paneView', 'tdFlex') - td.setAttribute('notSelectable', 'true') - td.setAttribute('about', subject.toNT()) - td.setAttribute('colspan', '2') - const provider = td.appendChild(dom.createElement('file-explorer-provider')) provider.context = context provider.subjectUri = subject.uri provider.onBack = () => collapseMouseDownListener({ target: provider }) @@ -478,10 +362,9 @@ export default function (context) { provider.handleAccessClick = () => openPaneInPlace(subject, paneRegistry.byName('sharing')) if (provider.pane) { - tr.classList.add('outlinePaneRow') renderPaneIntoProvider(provider, subject, provider.pane, options) } - return tr + return provider } // / ////////////////////////////////////////////////////////////////////////// @@ -510,13 +393,12 @@ export default function (context) { // Remove a node from the DOM so that Firefox refreshes the screen OK // Just deleting it cause whitespace to accumulate. function removeAndRefresh (d) { - const table = d.parentNode - const par = table.parentNode - const placeholder = dom.createElement('table') - placeholder.classList.add('placeholderTable') - par.replaceChild(placeholder, table) - table.removeChild(d) - par.replaceChild(table, placeholder) // Attempt to + const parent = d.parentNode + const grandParent = parent.parentNode + const placeholder = dom.createElement('div') + grandParent.replaceChild(placeholder, parent) + parent.removeChild(d) + grandParent.replaceChild(parent, placeholder) // Attempt to } const propertyTable = (this.propertyTable = function propertyTable ( @@ -525,21 +407,19 @@ export default function (context) { requiredPane, options ) { - UI.log.debug('Property table for: ' + subject) + UI.log.debug('Property block for: ' + subject) subject = kb.canon(subject) // if (!requiredPane) requiredPane = panes.defaultPane; if (!table) { - // Create a new property table - table = dom.createElement('table') - table.classList.add('tableFullWidth') - expandedProviderTR(subject, requiredPane, options).then(providerTR => { - table.appendChild(providerTR) - }) - - return table + const provider = dom.createElement('file-explorer-provider') + expandedProviderTR(subject, requiredPane, options, provider) + return provider } else { - // New display of existing table, keeping expanded bits + const existingProvider = table.matches?.('file-explorer-provider') + ? table + : table.firstElementChild || table + expandedProviderTR(subject, requiredPane, options, existingProvider) UI.log.info('Re-expand: ' + table) // do some other stuff here return table @@ -547,12 +427,13 @@ export default function (context) { }) /* propertyTable */ function propertyTR (doc, st, inverse) { - const tr = doc.createElement('TR') + const tr = doc.createElement('div') tr.AJAR_statement = st tr.AJAR_inverse = inverse // tr.AJAR_variable = null; // @@ ?? was just 'tr.AJAR_variable' tr.setAttribute('predTR', 'true') - const predicateTD = thisOutline.outlinePredicateTD(st.predicate, tr, inverse) + tr.setAttribute('role', 'row') + const predicateTD = thisOutline.outlinePredicateDiv(st.predicate, tr, inverse) tr.appendChild(predicateTD) // @@ add 'internal' to predicateTD's class for style? mno return tr } @@ -560,10 +441,6 @@ export default function (context) { // / ////////// Property list function appendPropertyTRs (parent, plist, inverse, predicateFilter) { - // UI.log.info('@appendPropertyTRs, 'this' is %s, dom is %s, '+ // Gives 'can't access dead object' - // 'thisOutline.document is %s', this, dom.location, thisOutline.document.location); - // UI.log.info('@appendPropertyTRs, dom is now ' + this.document.location); - // UI.log.info('@appendPropertyTRs, dom is now ' + thisOutline.document.location); UI.log.debug('Property list length = ' + plist.length) if (plist.length === 0) return '' let sel, j, k @@ -629,7 +506,7 @@ export default function (context) { sel(plist[k]).lang.indexOf(outline.labeller.LanguagePreference) >= 0 ) { tr.appendChild( - thisOutline.outlineObjectTD( + thisOutline.outlineObjectDiv( sel(plist[k]), defaultpropview, undefined, @@ -644,7 +521,7 @@ export default function (context) { } tr.appendChild( - thisOutline.outlineObjectTD(sel(s), defaultpropview, undefined, s) + thisOutline.outlineObjectDiv(sel(s), defaultpropview, undefined, s) ) /* Note: showNobj shows between n to 2n objects. @@ -679,10 +556,10 @@ export default function (context) { displayed++ s = plist[j + l] defaultpropview = views.defaults[s.predicate.uri] - const trObj = dom.createElement('tr') + const trObj = dom.createElement('div') trObj.style.colspan = '1' trObj.appendChild( - thisOutline.outlineObjectTD( + thisOutline.outlineObjectDiv( sel(plist[j + l]), defaultpropview, undefined, @@ -709,8 +586,12 @@ export default function (context) { if (show < predDups) { // Add the x more here - const moreTR = dom.createElement('tr') - const moreTD = moreTR.appendChild(dom.createElement('td')) + const moreTR = dom.createElement('div') + const moreTD = moreTR.appendChild(dom.createElement('div')) + moreTD.setAttribute( + 'style', + 'margin: 0.2em; border: none; padding: 0; vertical-align: top;' + ) moreTD.classList.add('obj') moreTD.setAttribute('notSelectable', 'false') if (predDups > n) { @@ -782,7 +663,6 @@ export default function (context) { /* termWidget ** */ - const termWidget = {} // @@@@@@ global globalThis.termWidget = termWidget termWidget.construct = function (dom) { dom = dom || document @@ -972,10 +852,10 @@ export default function (context) { // UI.log.info('selection has ' +selection.map(function(item){return item.textContent;}).join(', ')); // UI.log.debug('@outline setSelected, intended to '+(newValue?'select ':'deselect ')+node+node.textContent); // if (newValue === selected(node)) return; //we might not need this anymore... - if (node.nodeName !== 'TD') { + if (node.nodeName !== 'TD' && node.nodeName !== 'DIV') { UI.log.debug('down' + node.nodeName) throw new Error( - 'Expected TD in setSelected: ' + + 'Expected TD or DIV in setSelected: ' + node.nodeName + ' : ' + node.textContent @@ -1041,6 +921,7 @@ export default function (context) { selection = [] } + /* srcElement is the old IE-era version of event.target. */ /** Get the target of an event **/ this.targetOf = function (e) { let target @@ -1091,13 +972,8 @@ export default function (context) { ) { setSelected(selectedTd.nextSibling, true) } else { - const newSelected = dom.evaluate( - 'table/div/tr/td[2]', - selectedTd, - null, - XPathResult.FIRST_ORDERED_NODE_TYPE, - null - ).singleNodeValue + const nextRow = selectedTd.parentNode && selectedTd.parentNode.nextSibling + const newSelected = nextRow ? nextRow.lastChild : null setSelected(newSelected, true) } break @@ -1110,7 +986,7 @@ export default function (context) { setSelected(selectedTd.previousSibling, true) return true // do not shrink signal } else { - setSelected(UI.utils.ancestor(selectedTd.parentNode, 'TD'), true) + setSelected(selectedTd.parentNode && selectedTd.parentNode.firstChild, true) } // supplied by thieOutline.focusTd break case 'moveTo': @@ -1247,7 +1123,7 @@ export default function (context) { this.walk('right') return } - if (selectedTd.firstChild.tagName !== 'TABLE') { + if (!selectedTd.firstElementChild?.matches('file-explorer-provider')) { // not expanded sf.addCallback('done', setSelectedAfterward) sf.addCallback('fail', setSelectedAfterward) @@ -1412,9 +1288,9 @@ export default function (context) { // var p = target.parentNode let node for ( - node = UI.utils.ancestor(target, 'TD'); + node = UI.utils.ancestor(target, 'td') || UI.utils.ancestor(target, 'div'); node && !(node.getAttribute('notSelectable') === 'false'); // Default now is not selectable - node = UI.utils.ancestor(node.parentNode, 'TD') + node = UI.utils.ancestor(node.parentNode, 'td') || UI.utils.ancestor(node.parentNode, 'div') ) { // ... } @@ -1444,7 +1320,7 @@ export default function (context) { // go to UserInput let st if (node.parentNode) st = node.parentNode.AJAR_statement - if (!st) return // For example in the title TD of an expanded pane + if (!st) return // For example in the title block of an expanded pane const target = st.why const editable = store.updater.editable(target.uri, kb) if (sel && editable) thisOutline.UserInput.Click(e, selection[0]) // was next 2 lines @@ -1550,27 +1426,13 @@ export default function (context) { // first expand newTable = propertyTable(subject, undefined, pane, options) } else { - UI.log.info(' ... p is ' + p) - for ( - newTable = p.firstChild; - newTable.nextSibling; - newTable = newTable.nextSibling - ) { - UI.log.info(' ... checking node ' + newTable) - if (newTable.nodeName === 'table') break - } - newTable = propertyTable(subject, newTable, pane, options) + newTable = propertyTable(subject, p, pane, options) } already = true - if ( - UI.utils.ancestor(p, 'TABLE') && - UI.utils.ancestor(p, 'TABLE').style.backgroundColor === 'white' - ) { - newTable.style.backgroundColor = '#eee' - } else { - newTable.style.backgroundColor = 'white' + if (newTable !== p) { + UI.utils.emptyNode(p).appendChild(newTable) } - UI.utils.emptyNode(p).appendChild(newTable) + thisOutline.focusTd = p // I don't know why I couldn't use 'this'...because not defined in callbacks UI.log.debug('expand: Node for ' + subject + ' expanded') // fetch seeAlso when render() @@ -1654,23 +1516,12 @@ export default function (context) { sf.addCallback('done', expand) // @@@@@@@ This can really mess up existing work sf.addCallback('fail', expand) // Need to do if there s one a gentle resync of page with store - const returnConditions = [] // this is quite a general way to do cut and paste programming - // I might make a class for this if (subject.uri && subject.uri.split(':')[0] === 'rdf') { // what is this? -tim render() return } - for (let i = 0; i < returnConditions.length; i++) { - let returnCode - if (returnCode === returnConditions[i](subject)) { - render() - UI.log.debug('outline 1815') - if (returnCode[1]) outlineElement.removeChild(outlineElement.lastChild) - return - } - } if ( subject.uri && !immediate && @@ -1746,21 +1597,7 @@ export default function (context) { } // outlineExpand function outlineCollapse (p, subject) { - let row = UI.utils.ancestor(p, 'TR') - row = UI.utils.ancestor(row.parentNode, 'TR') // two levels up - let statement - if (row) statement = row.AJAR_statement - let level // find level (the enclosing TD) - for ( - level = p.parentNode; - level.tagName !== 'TD'; - level = level.parentNode - ) { - if (typeof level === 'undefined') { - alert('Not enclosed in TD!') - return - } - } + const statement = p.AJAR_statement || (p.parentNode && p.parentNode.AJAR_statement) UI.log.debug('Collapsing subject ' + subject) let myview @@ -1770,12 +1607,12 @@ export default function (context) { } UI.log.debug('view= ' + myview) let deleteNode - if (level.parentNode.parentNode.id === 'outline') { - deleteNode = level.parentNode + if (p.parentNode && p.parentNode.id === 'outline') { + deleteNode = p } thisOutline.replaceTD( - thisOutline.outlineObjectTD(subject, myview, deleteNode, statement), - level + thisOutline.outlineObjectDiv(subject, myview, deleteNode, statement), + p ) } // outlineCollapse @@ -1799,15 +1636,10 @@ export default function (context) { function outlineRefocus (p, subject) { // Shift-expand or shift-collapse: Maximize - let outer = null - for (let level = p.parentNode; level; level = level.parentNode) { - UI.log.debug('level ' + level.tagName) - if (level.tagName === 'TD') outer = level - } // find outermost td - UI.utils.emptyNode(outer).appendChild(propertyTable(subject)) + UI.utils.emptyNode(p).appendChild(propertyTable(subject)) setUrlBarAndTitle(subject) // dom.title = UI.utils.label(subject) - outer.setAttribute('about', subject.toNT()) + p.setAttribute('about', subject.toNT()) } // outlineRefocus outline.outlineRefocus = outlineRefocus @@ -1844,40 +1676,43 @@ export default function (context) { @param pane -- optional -- pane to be used for expanded display @param solo -- optional -- the window will be cleared out and only the subject displayed @param referer -- optional -- where did we hear about this from anyway? - @param table -- option -- default is an HTML table element in which to put the outline. + @param host -- option -- default is the outline host element used for the view. @param showNavbar -- optional -- when false, suppress automatic navbar reveal */ - this.GotoSubject = function (subject, expand, pane, solo, referrer, table, showNavbar = true) { - const outlineContainer = getOutlineContainer() - if (showNavbar && (!table || table === outlineContainer)) { + this.GotoSubject = function (subject, expand, pane, solo, referrer, host, showNavbar = true) { + const outlineHost = getOutlineContainer() + if (showNavbar && (!host || host === outlineHost)) { showSolidPanesNavbar() } - table = table || outlineContainer // if does not exist create a compatible host in the current shell + host = host || outlineHost // if does not exist create a compatible host in the current shell if (solo) { - UI.utils.emptyNode(table) - table.style.width = '100%' + host.style.width = '100%' + UI.utils.emptyNode(host) } - function GotoSubjectDefault () { - const tr = dom.createElement('tr') - tr.classList.add('outlineRow1') - tr.style.verticalAlign = 'top' - table.appendChild(tr) - const td = thisOutline.outlineObjectTD(subject, undefined, tr) - tr.appendChild(td) - return td + function createSubjectBlock () { + const subjectBlock = dom.createElement('div') + subjectBlock.classList.add('outline-subject-block') + const block = thisOutline.outlineObjectDiv(subject, undefined, subjectBlock) + subjectBlock.appendChild(block) + host.appendChild(subjectBlock) + return subjectBlock } - const td = GotoSubjectDefault() + const subjectBlock = solo ? createSubjectBlock() : host if (solo) setUrlBarAndTitle(subject) // dom.title = UI.utils.label(subject) // 'Tabulator: '+ No need to advertize if (expand) { - outlineExpand(td, subject, { + outlineExpand(subjectBlock, subject, { pane, solo }) + } else { + if (!solo) { + createSubjectBlock() + } } if ( @@ -1976,8 +1811,7 @@ export default function (context) { } } else if (obj.termType === 'Collection') { // obj.elements is an array of the elements in the collection - rep = dom.createElement('table') - rep.classList.add('tableFullWidth') + rep = dom.createElement('div') rep.setAttribute('about', obj.toNT()) /* Not sure which looks best -- with or without. I think without @@ -1987,13 +1821,13 @@ export default function (context) { */ for (let i = 0; i < obj.elements.length; i++) { const elt = obj.elements[i] - const row = rep.appendChild(dom.createElement('tr')) - const numcell = row.appendChild(dom.createElement('td')) + const row = rep.appendChild(dom.createElement('div')) + const numcell = row.appendChild(dom.createElement('div')) numcell.classList.add('obj') numcell.setAttribute('notSelectable', 'false') numcell.setAttribute('about', obj.toNT()) numcell.innerHTML = i + 1 + ')' - row.appendChild(thisOutline.outlineObjectTD(elt)) + row.appendChild(thisOutline.outlineObjectDiv(elt)) } } else if (obj.termType === 'Graph') { rep = paneRegistry diff --git a/src/outline/outlineDomHelpers.ts b/src/outline/outlineDomHelpers.ts new file mode 100644 index 00000000..d91910c3 --- /dev/null +++ b/src/outline/outlineDomHelpers.ts @@ -0,0 +1,225 @@ +import { uri } from 'rdflib' +import { utils } from 'solid-ui' + +type OutlineIcon = { + src: string + alt: string + tooltip?: string + width: number +} + +type TermWidget = { + construct: (domArg?: Document) => HTMLElement + addIcon: (iconHost: ParentNode, icon: OutlineIcon, listener?: EventListener) => void + removeIcon: (iconHost: ParentNode, icon: OutlineIcon) => void + replaceIcon: (iconHost: ParentNode, oldIcon: OutlineIcon, newIcon: OutlineIcon, listener?: EventListener) => void +} + +export function createOutlineDomHelpers ({ + dom, + UI, + kb, + outlineIcons, + expandMouseDownListener, + selectableTDClickListener, + setSelected, + viewAsBoringDefault, + removeNodeIconMouseDownListener +}) { + function appendRemoveIcon (node, subject, removeNode) { + const image = utils.AJARImage( + outlineIcons.src.icon_remove_node, + 'remove', + undefined, + dom + ) + image.addEventListener('click', removeNodeIconMouseDownListener) + image.node = removeNode + image.setAttribute('about', subject.toNT()) + image.style.marginLeft = '5px' + image.style.marginRight = '10px' + node.appendChild(image) + return image + } + + const termWidget = {} as TermWidget + globalThis.termWidget = termWidget + + termWidget.construct = function (domArg) { + domArg = domArg || document + const iconContainer = domArg.createElement('div') + iconContainer.setAttribute( + 'style', + 'margin: 0.2em; border: none; padding: 0; vertical-align: top;' + ) + iconContainer.setAttribute('class', 'iconTD') + iconContainer.setAttribute('notSelectable', 'true') + iconContainer.style.width = '0px' + return iconContainer + } + + termWidget.addIcon = function (iconHost, icon, listener) { + const iconTD = iconHost.childNodes[1] as HTMLElement | undefined + if (!iconTD) return + const img = utils.AJARImage(icon.src, icon.alt, icon.tooltip, dom) + const width = Number.parseInt(iconTD.style.width || '0', 10) + icon.width + iconTD.style.width = width + 'px' + iconTD.appendChild(img) + if (listener) { + img.addEventListener('click', listener) + } + } + + termWidget.removeIcon = function (iconHost, icon) { + const iconTD = iconHost.childNodes[1] as HTMLElement | undefined + let baseURI + if (!iconTD) return + const width = Number.parseInt(iconTD.style.width || '0', 10) - icon.width + iconTD.style.width = width + 'px' + for (let x = 0; x < iconTD.childNodes.length; x++) { + const elt = iconTD.childNodes[x] as HTMLImageElement + const eltSrc = elt.src + + try { + baseURI = dom.location.href.split('?')[0] + } catch (e) { + baseURI = '' + } + const relativeIconSrc = uri.join(icon.src, baseURI) + if (eltSrc === relativeIconSrc) { + iconTD.removeChild(elt) + } + } + } + + termWidget.replaceIcon = function (iconHost, oldIcon, newIcon, listener) { + termWidget.removeIcon(iconHost, oldIcon) + termWidget.addIcon(iconHost, newIcon, listener) + } + + // old name: outlineObjectTD + function outlineObjectDiv ( + obj, + view, + deleteNode, + statement + ) { + const objectDiv = dom.createElement('div') + objectDiv.classList.add('obj') + objectDiv.setAttribute('notSelectable', 'false') + objectDiv.setAttribute('role', 'option') + objectDiv.setAttribute('tabindex', '0') + objectDiv.setAttribute('data-outline-node', 'object') + if (!obj) { + objectDiv.textContent = 'No object available.' + return objectDiv + } + const theClass = 'obj' + + if ( + obj.termType === 'NamedNode' || + obj.termType === 'BlankNode' || + (obj.termType === 'Literal' && + obj.value.slice && + (obj.value.slice(0, 6) === 'ftp://' || + obj.value.slice(0, 8) === 'https://' || + obj.value.slice(0, 7) === 'http://')) + ) { + objectDiv.setAttribute('about', obj.toNT()) + objectDiv.appendChild( + UI.utils.AJARImage( + UI.icons.originalIconBase + 'tbl-expand-trans.png', + 'expand', + undefined, + dom + ) + ).addEventListener('click', expandMouseDownListener) + } + objectDiv.setAttribute('class', theClass) + if (kb.whether(obj, UI.ns.rdf('type'), UI.ns.link('Request'))) { + objectDiv.className = 'undetermined' + } + + if (!view) { + view = viewAsBoringDefault + } + objectDiv.appendChild(view(obj)) + if (deleteNode) { + appendRemoveIcon(objectDiv, obj, deleteNode) + } + + objectDiv.tabulatorSelect = function () { + setSelected(this, true) + } + objectDiv.tabulatorDeselect = function () { + setSelected(this, false) + } + + objectDiv.addEventListener('click', selectableTDClickListener) + return objectDiv + } + + // old name: outlinePredicateTD + function outlinePredicateDiv ( + predicate, + newTr, + inverse, + internal + ) { + const predicateTD = dom.createElement('div') + predicateTD.setAttribute('about', predicate.toNT()) + predicateTD.setAttribute('class', internal ? 'pred internal' : 'pred') + predicateTD.setAttribute('role', 'row') + predicateTD.setAttribute('data-outline-node', 'predicate') + + let lab + switch (predicate.termType) { + case 'BlankNode': + predicateTD.className = 'undetermined' + break + case 'NamedNode': + lab = UI.utils.predicateLabelForXML(predicate, inverse) + break + case 'Collection': + lab = UI.utils.predicateLabelForXML(predicate.elements[0], inverse) + } + lab = lab ? lab.slice(0, 1).toUpperCase() + lab.slice(1) : '...' + + const labelTD = dom.createElement('div') + labelTD.classList.add('labelTD') + labelTD.setAttribute('notSelectable', 'true') + labelTD.appendChild(dom.createTextNode(lab)) + predicateTD.appendChild(labelTD) + labelTD.style.width = '100%' + predicateTD.appendChild(termWidget.construct(dom)) + for (const w in outlineIcons.termWidgets) { + if (!newTr || !newTr.AJAR_statement) break + if ( + outlineIcons.termWidgets[w].filter && + outlineIcons.termWidgets[w].filter( + newTr.AJAR_statement, + 'pred', + inverse + ) + ) { + termWidget.addIcon(predicateTD, outlineIcons.termWidgets[w]) + } + } + + predicateTD.tabulatorSelect = function () { + setSelected(this, true) + } + predicateTD.tabulatorDeselect = function () { + setSelected(this, false) + } + predicateTD.addEventListener('click', selectableTDClickListener) + return predicateTD + } + + return { + appendRemoveIcon, + outlineObjectDiv, + outlinePredicateDiv, + termWidget + } +} diff --git a/src/outline/userInput.js b/src/outline/userInput.js index e6a7af47..d276e483 100644 --- a/src/outline/userInput.js +++ b/src/outline/userInput.js @@ -54,6 +54,23 @@ export function UserInput (outline) { let movedArrow = false // hq + function getStatementRow (node) { + let current = node + while (current && current !== myDocument) { + if (current.AJAR_statement) { + return current + } + current = current.parentNode + } + + return UI.utils.ancestor(node, 'DIV') || UI.utils.ancestor(node, 'TR') + } + + function getStatementContainer (node) { + const row = getStatementRow(node) + return row?.parentNode || UI.utils.ancestor(node, 'TABLE') + } + // var updateService=new updateCenter(kb); if (!UserInputFormula) { @@ -87,11 +104,11 @@ export function UserInput (outline) { const target = UI.utils.getTarget(e) // UI.log.warn(ancestor(target,'TABLE').textContent); - const insertTr = myDocument.createElement('tr') - UI.utils - .ancestor(target, 'DIV') - .insertBefore(insertTr, UI.utils.ancestor(target, 'TR')) - const tempTr = myDocument.createElement('tr') + const insertTr = myDocument.createElement('div') + const targetRow = getStatementRow(target) + const targetContainer = getStatementContainer(target) + targetContainer.insertBefore(insertTr, targetRow) + const tempTr = myDocument.createElement('div') const reqTerm1 = This.generateRequest('(TBD)', tempTr, true) insertTr.appendChild(tempTr.firstChild) const reqTerm2 = This.generateRequest( @@ -104,11 +121,10 @@ export function UserInput (outline) { // Take the why of the last TR and write to it. if ( - UI.utils.ancestor(target, 'TR').previousSibling && // there is a previous predicate/object line - UI.utils.ancestor(target, 'TR').previousSibling.AJAR_statement + targetRow.previousSibling && // there is a previous predicate/object line + targetRow.previousSibling.AJAR_statement ) { - const preStat = UI.utils.ancestor(target, 'TR').previousSibling - .AJAR_statement + const preStat = targetRow.previousSibling.AJAR_statement // This should always(?) input a non-inverse statement This.formUndetStat( insertTr, @@ -122,7 +138,7 @@ export function UserInput (outline) { // no previous row: write to the document defining the subject const subject = UI.utils.getAbout( store, - UI.utils.ancestor(target.parentNode.parentNode, 'TD') + targetContainer.parentNode ) const doc = store.sym($rdf.Util.uri.docpart(subject.uri)) This.formUndetStat(insertTr, subject, reqTerm1, reqTerm2, doc, false) @@ -321,7 +337,7 @@ export function UserInput (outline) { let trNode try { obj = UI.utils.getTerm(target) - trNode = UI.utils.ancestor(target, 'TR') + trNode = getStatementRow(target) } catch (e) { UI.log.warn('userinput.js: ' + e + UI.utils.getAbout(store, selectedTd)) UI.log.error(target + ' getStatement Error:' + e) @@ -449,7 +465,7 @@ export function UserInput (outline) { ) // TODO: DEFINE ERROR CALLBACK defaultpropview = this.views.defaults[s.predicate.uri] - trCache = UI.utils.ancestor(this.lastModified, 'TR') + trCache = getStatementRow(this.lastModified) try { store.updater.update([], [s], function ( uri, @@ -493,7 +509,7 @@ export function UserInput (outline) { // TODO: DEFINE ERROR CALLBACK const valueCache = this.lastModified.value - trCache = UI.utils.ancestor(this.lastModified, 'TR') + trCache = getStatementRow(this.lastModified) const oldValue = this.lastModified.defaultValue s2 = $rdf.st( s.subject, @@ -548,10 +564,7 @@ export function UserInput (outline) { bnodeTerm: s.subject }) } else { - s1 = UI.utils.ancestor( - UI.utils.ancestor(this.lastModified, 'TR').parentNode, - 'TR' - ).AJAR_statement + s1 = getStatementRow(this.lastModified).AJAR_statement s2 = $rdf.st(s.subject, selectedPredicate, textTerm, s.why) const type = store.the(s.subject, rdf('type')) s3 = store.anyStatementMatching( @@ -562,10 +575,7 @@ export function UserInput (outline) { ) // TODO: DEFINE ERROR CALLBACK // because the table is repainted, so... - trCache = UI.utils.ancestor( - UI.utils.ancestor(this.lastModified, 'TR'), - 'TD' - ).parentNode + trCache = getStatementRow(this.lastModified).parentNode try { store.updater.update([], [s1, s2, s3], function ( uri, @@ -593,10 +603,7 @@ export function UserInput (outline) { // a subtle bug occurs here, if foaf:nick hasn't been dereferneced, // this add will cause a repainting } - const enclosingTd = UI.utils.ancestor( - this.lastModified.parentNode.parentNode, - 'TD' - ) + const enclosingTd = getStatementContainer(this.lastModified).parentNode const defaultPane = panes.byName('default') // @@ check outline.outlineExpand(enclosingTd, s.subject, { pane: defaultPane, @@ -618,7 +625,7 @@ export function UserInput (outline) { } } else if (this.lastModified.isNew) { // generate 'Request', there is no way you can input ' (Please Input) ' - trNode = UI.utils.ancestor(this.lastModified, 'TR') + trNode = getStatementRow(this.lastModified) reqTerm = this.generateRequest( '(To be determined. Re-type of drag an object onto this field)' ) @@ -638,10 +645,7 @@ export function UserInput (outline) { return } else if (s.predicate.termType === 'Collection') { store.removeMany(s.subject) - const upperTr = UI.utils.ancestor( - UI.utils.ancestor(this.lastModified, 'TR').parentNode, - 'TR' - ) + const upperTr = getStatementRow(this.lastModified) preStat = upperTr.AJAR_statement reqTerm = this.generateRequest( '(To be determined. Re-type of drag an object onto this field)' @@ -678,7 +682,7 @@ export function UserInput (outline) { } } // case modified - literal modification only(for now). - trNode = UI.utils.ancestor(this.lastModified, 'TR') + trNode = getStatementRow(this.lastModified) // var defaultpropview = this.views.defaults[s.predicate.uri] if (!this.statIsInverse) { @@ -913,9 +917,9 @@ export function UserInput (outline) { break } case 'selected': { // header , undetermined generated - const paneDiv = UI.utils.ancestor(selectedTd, 'TABLE').lastChild + const paneDiv = getStatementContainer(selectedTd) const newTr = paneDiv.insertBefore( - myDocument.createElement('tr'), + myDocument.createElement('div'), paneDiv.lastChild ) // var titleTerm=UI.utils.getAbout(kb,UI.utils.ancestor(newTr,'TD')); @@ -1000,7 +1004,7 @@ export function UserInput (outline) { */ subject = UI.utils.getAbout( store, - UI.utils.ancestor(selectedTd, 'TABLE').parentNode + getStatementContainer(selectedTd).parentNode ) subjectClass = store.any(subject, rdf('type')) sparqlText = [] @@ -1057,7 +1061,7 @@ export function UserInput (outline) { */ subject = UI.utils.getAbout( store, - UI.utils.ancestor(selectedTd, 'TABLE').parentNode + getStatementContainer(selectedTd).parentNode ) subjectClass = store.any(subject, rdf('type')) const object = selectedTd.parentNode.AJAR_statement.object @@ -1498,7 +1502,7 @@ export function UserInput (outline) { getStatementAbout: function getStatementAbout (something) { // var trNode=something.parentNode; - const trNode = UI.utils.ancestor(something, 'TR') + const trNode = getStatementRow(something) if (!trNode) { throw new Error('No ancestor TR for the TD we clicked on:' + something) } @@ -1611,7 +1615,7 @@ export function UserInput (outline) { } // if(!isEnd && HCIoptions["bottom insert highlights"].enabled) trIterator=trIterator.previousSibling; - const insertTr = myDocument.createElement('tr') + const insertTr = myDocument.createElement('div') // style stuff, I'll have to investigate appendPropertyTRs() somehow insertTr.style.colspan = '1' insertTr.style.display = 'block' @@ -1657,7 +1661,7 @@ export function UserInput (outline) { if (trNew) { trNode = trNew } else { - trNode = UI.utils.ancestor(this.lastModified, 'TR') + trNode = getStatementRow(this.lastModified) } UI.utils.emptyNode(trNode) } From 315b270b6ac382b12f5861e371a6f944dc542cae Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sun, 27 Sep 2026 22:31:30 +1000 Subject: [PATCH 03/11] Addressed PR feedback Prompt: several prompts to help resolve the PR feedback because this is very detailed. Not sure if this code will stay the way it is as more will be rewritten. Co-authored-by: GPT-5.4 Mini --- src/defaultPane.css | 32 ++++++------ src/outline/manager.js | 82 +++++++++---------------------- src/outline/outlineDomHelpers.ts | 4 +- src/outline/queryByExample.js | 13 +++-- src/outline/userInput.js | 2 +- test/unit/outline/manager.test.ts | 37 +++++++++----- 6 files changed, 76 insertions(+), 94 deletions(-) diff --git a/src/defaultPane.css b/src/defaultPane.css index bbb216a5..1fa74a19 100644 --- a/src/defaultPane.css +++ b/src/defaultPane.css @@ -17,13 +17,13 @@ margin-bottom: var(--spacing-base, 0.5rem); } -.defaultPane > tr { +.defaultPane > div { display: grid; grid-template-columns: minmax(0, var(--default-pane-predicate-column)) minmax(0, 1fr); align-items: flex-start; } -.defaultPane > tr > td.pred { +.defaultPane > div > div.pred { grid-column: 1; display: flex; align-items: flex-start; @@ -32,7 +32,7 @@ min-width: 0; } -.defaultPane > tr > td.pred > .labelTD { +.defaultPane > div > div.pred > .labelTD { flex: 1 1 auto; min-width: 0; margin: 0; @@ -40,61 +40,61 @@ padding-bottom: 0; } -.defaultPane > tr > td.pred > .iconTD { +.defaultPane > div > div.pred > .iconTD { flex: 0 0 auto; width: auto; margin: 0; } -.defaultPane > tr > td.obj { +.defaultPane > div > div.obj { grid-column: 2; min-width: 0; box-sizing: border-box; align-self: start; } -.defaultPane > tr > td[colspan='2'] { +.defaultPane > div > div[colspan='2'] { grid-column: 1 / -1; } -.defaultPane[data-layout='mobile'] > tr { +.defaultPane[data-layout='mobile'] > div { grid-template-columns: minmax(0, 1fr); } -.defaultPane[data-layout='mobile'] > tr > td.pred, -.defaultPane[data-layout='mobile'] > tr > td.obj { +.defaultPane[data-layout='mobile'] > div > div.pred, +.defaultPane[data-layout='mobile'] > div > div.obj { display: block; grid-column: 1; width: 100%; box-sizing: border-box; } -.defaultPane[data-layout='mobile'] > tr > td.pred { +.defaultPane[data-layout='mobile'] > div > div.pred { padding-bottom: var(--spacing-small, 0.1rem); } -.defaultPane[data-layout='mobile'] > tr > td.obj { +.defaultPane[data-layout='mobile'] > div > div.obj { padding-left: var(--default-pane-object-indent) !important; } @media (max-width: 700px) { - .defaultPane > tr { + .defaultPane > div { grid-template-columns: minmax(0, 1fr); } - .defaultPane > tr > td.pred, - .defaultPane > tr > td.obj { + .defaultPane > div > div.pred, + .defaultPane > div > div.obj { display: block; grid-column: 1; width: 100%; box-sizing: border-box; } - .defaultPane > tr > td.pred { + .defaultPane > div > div.pred { padding-bottom: var(--spacing-small, 0.1rem); } - .defaultPane > tr > td.obj { + .defaultPane > div > div.obj { padding-left: var(--default-pane-object-indent) !important; } } diff --git a/src/outline/manager.js b/src/outline/manager.js index f220dbbb..b118fb26 100644 --- a/src/outline/manager.js +++ b/src/outline/manager.js @@ -41,8 +41,8 @@ export default function (context) { this.UserInput = new UserInput(this) this.clipboardAddress = 'tabulator:clipboard' // Weird this.UserInput.clipboardInit(this.clipboardAddress) - - const { + + const { outlineObjectDiv, outlinePredicateDiv, termWidget @@ -220,7 +220,6 @@ export default function (context) { * @returns {HTMLElement} */ function getOrCreateContainer (id) { - // OutlineView is a simple block container if (id === 'OutlineView') { const existingOutline = document.getElementById('OutlineView') @@ -337,6 +336,7 @@ export default function (context) { provider = provider || dom.createElement('file-explorer-provider') provider.classList.add('paneView', 'tdFlex') provider.setAttribute('notSelectable', 'true') + provider.setAttribute('role', 'grid') provider.setAttribute('about', subject.toNT()) if (options.hover) { // By default no hide till hover as community deems it confusing @@ -660,51 +660,6 @@ export default function (context) { this.appendPropertyTRs = appendPropertyTRs - /* termWidget - ** - */ - globalThis.termWidget = termWidget - termWidget.construct = function (dom) { - dom = dom || document - const td = dom.createElement('TD') - td.setAttribute('class', 'iconTD') - td.setAttribute('notSelectable', 'true') - return td - } - termWidget.addIcon = function (td, icon, listener) { - const iconTD = td.childNodes[1] - if (!iconTD) return - const img = UI.utils.AJARImage(icon.src, icon.alt, icon.tooltip, dom) - iconTD.appendChild(img) - if (listener) { - img.addEventListener('click', listener) - } - } - termWidget.removeIcon = function (td, icon) { - const iconTD = td.childNodes[1] - let baseURI - if (!iconTD) return - for (let x = 0; x < iconTD.childNodes.length; x++) { - const elt = iconTD.childNodes[x] - const eltSrc = elt.src - - // ignore first '?' and everything after it //Kenny doesn't know what this is for - try { - baseURI = dom.location.href.split('?')[0] - } catch (e) { - baseURI = '' - } - const relativeIconSrc = $rdf.uri.join(icon.src, baseURI) - if (eltSrc === relativeIconSrc) { - iconTD.removeChild(elt) - } - } - } - termWidget.replaceIcon = function (td, oldIcon, newIcon, listener) { - termWidget.removeIcon(td, oldIcon) - termWidget.addIcon(td, newIcon, listener) - } - // / /////////////////////////////////////////////////// VALUE BROWSER VIEW // / /////////////////////////////////////////////////////// TABLE VIEW @@ -773,9 +728,16 @@ export default function (context) { function setSelectedParent (node, inc) { const onIcon = outlineIcons.termWidgets.optOn const offIcon = outlineIcons.termWidgets.optOff + const isPredicateRow = (element) => { + return Boolean( + element && + typeof element.getAttribute === 'function' && + (element.getAttribute('predTR') === 'true' || element.getAttribute('data-outline-node') === 'predicate') + ) + } for (let n = node; n.parentNode; n = n.parentNode) { while (true) { - if (n.getAttribute('predTR')) { + if (isPredicateRow(n)) { let num = n.getAttribute('parentOfSelected') if (!num) num = 0 else num = parseInt(num) @@ -798,8 +760,8 @@ export default function (context) { ) } break - } else if (n.previousSibling && n.previousSibling.nodeName === 'TR') { - n = n.previousSibling + } else if (n.previousElementSibling) { + n = n.previousElementSibling } else break } } @@ -852,7 +814,7 @@ export default function (context) { // UI.log.info('selection has ' +selection.map(function(item){return item.textContent;}).join(', ')); // UI.log.debug('@outline setSelected, intended to '+(newValue?'select ':'deselect ')+node+node.textContent); // if (newValue === selected(node)) return; //we might not need this anymore... - if (node.nodeName !== 'TD' && node.nodeName !== 'DIV') { + if (node.nodeName !== 'TD' && node.nodeName !== 'DIV') { UI.log.debug('down' + node.nodeName) throw new Error( 'Expected TD or DIV in setSelected: ' + @@ -921,7 +883,7 @@ export default function (context) { selection = [] } - /* srcElement is the old IE-era version of event.target. */ + /* srcElement is the old IE-era version of event.target. */ /** Get the target of an event **/ this.targetOf = function (e) { let target @@ -1108,8 +1070,10 @@ export default function (context) { break case 37: { // left if (this.walk('left')) return - const titleTd = UI.utils.ancestor(selectedTd.parentNode, 'TD') - outlineCollapse(selectedTd, UI.utils.getAbout(kb, titleTd)) + const subjectNode = selectedTd.firstElementChild?.matches('file-explorer-provider') + ? selectedTd.firstElementChild + : selectedTd.querySelector('file-explorer-provider') + outlineCollapse(selectedTd, UI.utils.getAbout(kb, subjectNode)) break } case 39: // right @@ -1233,7 +1197,7 @@ export default function (context) { const target = thisOutline.targetOf(e) const subject = UI.utils.getAbout(kb, target) const pane = e.altKey ? paneRegistry.byName('internal') : undefined - const p = target.parentNode.parentNode + const p = target.closest?.('[data-outline-node="object"]') || target.parentNode?.parentNode || target outlineCollapse(p, subject, pane) } @@ -1288,9 +1252,9 @@ export default function (context) { // var p = target.parentNode let node for ( - node = UI.utils.ancestor(target, 'td') || UI.utils.ancestor(target, 'div'); + node = UI.utils.ancestor(target, 'td') || UI.utils.ancestor(target, 'tr') || UI.utils.ancestor(target, 'div'); node && !(node.getAttribute('notSelectable') === 'false'); // Default now is not selectable - node = UI.utils.ancestor(node.parentNode, 'td') || UI.utils.ancestor(node.parentNode, 'div') + node = UI.utils.ancestor(node.parentNode, 'td') || UI.utils.ancestor(node.parentNode, 'tr') || UI.utils.ancestor(node.parentNode, 'div') ) { // ... } @@ -1700,7 +1664,7 @@ export default function (context) { return subjectBlock } - const subjectBlock = solo ? createSubjectBlock() : host + const subjectBlock = createSubjectBlock() if (solo) setUrlBarAndTitle(subject) // dom.title = UI.utils.label(subject) // 'Tabulator: '+ No need to advertize diff --git a/src/outline/outlineDomHelpers.ts b/src/outline/outlineDomHelpers.ts index d91910c3..40d3cc61 100644 --- a/src/outline/outlineDomHelpers.ts +++ b/src/outline/outlineDomHelpers.ts @@ -107,7 +107,7 @@ export function createOutlineDomHelpers ({ const objectDiv = dom.createElement('div') objectDiv.classList.add('obj') objectDiv.setAttribute('notSelectable', 'false') - objectDiv.setAttribute('role', 'option') + objectDiv.setAttribute('role', 'gridcell') objectDiv.setAttribute('tabindex', '0') objectDiv.setAttribute('data-outline-node', 'object') if (!obj) { @@ -169,7 +169,7 @@ export function createOutlineDomHelpers ({ const predicateTD = dom.createElement('div') predicateTD.setAttribute('about', predicate.toNT()) predicateTD.setAttribute('class', internal ? 'pred internal' : 'pred') - predicateTD.setAttribute('role', 'row') + predicateTD.setAttribute('role', 'gridcell') predicateTD.setAttribute('data-outline-node', 'predicate') let lab diff --git a/src/outline/queryByExample.js b/src/outline/queryByExample.js index 9f7ddd76..9835f4ee 100644 --- a/src/outline/queryByExample.js +++ b/src/outline/queryByExample.js @@ -14,11 +14,18 @@ const optionalSubqueriesIndex = [] function predParentOf (node) { let n = node + const isPredicateRow = (element) => { + return Boolean( + element && + typeof element.getAttribute === 'function' && + (element.getAttribute('predTR') === 'true' || element.getAttribute('data-outline-node') === 'predicate') + ) + } while (true) { - if (n.getAttribute('predTR')) { + if (isPredicateRow(n)) { return n - } else if (n.previousSibling && n.previousSibling.nodeName === 'TR') { - n = n.previousSibling + } else if (n.previousElementSibling) { + n = n.previousElementSibling } else { console.log('Could not find predParent') return node diff --git a/src/outline/userInput.js b/src/outline/userInput.js index d276e483..01d5e66a 100644 --- a/src/outline/userInput.js +++ b/src/outline/userInput.js @@ -337,7 +337,7 @@ export function UserInput (outline) { let trNode try { obj = UI.utils.getTerm(target) - trNode = getStatementRow(target) + trNode = getStatementRow(target) } catch (e) { UI.log.warn('userinput.js: ' + e + UI.utils.getAbout(store, selectedTd)) UI.log.error(target + ' getStatement Error:' + e) diff --git a/test/unit/outline/manager.test.ts b/test/unit/outline/manager.test.ts index 693663a4..66b322f1 100644 --- a/test/unit/outline/manager.test.ts +++ b/test/unit/outline/manager.test.ts @@ -32,7 +32,7 @@ const mockPaneRegistry = { } describe('manager', () => { - describe('outline object td', () => { + describe('outline object div', () => { describe('for a named node', () => { let result beforeAll(() => { @@ -40,11 +40,11 @@ describe('manager', () => { const row = document.createElement('tr') table.appendChild(row) const manager = new OutlineManager({ dom: document, session: { paneRegistry: mockPaneRegistry } }) - result = manager.outlineObjectTD(sym('https://namednode.example/'), null, null, null) + result = manager.outlineObjectDiv(sym('https://namednode.example/'), null, null, null) row.appendChild(result) }) - it('is a html td element', () => { - expect(result.nodeName).toBe('TD') + it('is a html div element', () => { + expect(result.nodeName).toBe('DIV') }) it('about attribute refers to node', () => { expect(result).toHaveAttribute('about', '') @@ -95,10 +95,10 @@ describe('manager', () => { let result beforeAll(() => { const manager = new OutlineManager({ dom: document }) - result = manager.outlineObjectTD(sym('tel:+1-201-555-0123'), null, null, null) + result = manager.outlineObjectDiv(sym('tel:+1-201-555-0123'), null, null, null) }) - it('is a html td element', () => { - expect(result.nodeName).toBe('TD') + it('is a html div element', () => { + expect(result.nodeName).toBe('DIV') }) it('about attribute refers to tel uri', () => { expect(result).toHaveAttribute('about', '') @@ -142,10 +142,10 @@ describe('manager', () => { let result beforeAll(() => { const manager = new OutlineManager({ dom: document }) - result = manager.outlineObjectTD(lit('some text'), null, null, null) + result = manager.outlineObjectDiv(lit('some text'), null, null, null) }) - it('is a html td element', () => { - expect(result.nodeName).toBe('TD') + it('is a html div element', () => { + expect(result.nodeName).toBe('DIV') }) it('has no about attribute', () => { expect(result).not.toHaveAttribute('about') @@ -172,10 +172,10 @@ describe('manager', () => { let result beforeAll(() => { const manager = new OutlineManager({ dom: document }) - result = manager.outlineObjectTD(blankNode('blank-node'), null, null, null) + result = manager.outlineObjectDiv(blankNode('blank-node'), null, null, null) }) - it('is a html td element', () => { - expect(result.nodeName).toBe('TD') + it('is a html div element', () => { + expect(result.nodeName).toBe('DIV') }) it('has about attribute', () => { expect(result).toHaveAttribute('about', '_:blank-node') @@ -193,5 +193,16 @@ describe('manager', () => { expect(result).toHaveTextContent('...') }) }) + + describe('outlineObjectTD compatibility alias', () => { + it('returns the same div-based object block', () => { + const manager = new OutlineManager({ dom: document }) + const result = manager.outlineObjectTD(sym('https://alias.example/'), null, null, null) + + expect(result.nodeName).toBe('DIV') + expect(result).toHaveAttribute('about', '') + expect(result).toHaveClass('obj') + }) + }) }) }) From 7fa81034eede1fa2047cff76fcefc0d7c5a48fd8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 3 Oct 2026 21:32:05 +0000 Subject: [PATCH 04/11] chore: update solidos dependencies (latest: rdflib@2.4.1 solid-logic@6.0.0 solid-ui@5.0.1 pane-registry@5.0.1 activitystreams-pane@2.0.0 chat-pane@4.0.0 contacts-pane@4.0.0 folder-pane@4.0.0 issue-pane@4.0.0 meeting-pane@4.0.0 profile-pane@4.0.0 source-pane@4.0.0) --- package-lock.json | 266 +++++++++++++++++++++++----------------------- 1 file changed, 133 insertions(+), 133 deletions(-) diff --git a/package-lock.json b/package-lock.json index f706ff2b..c814cff7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2119,9 +2119,9 @@ } }, "node_modules/@csstools/cascade-layer-name-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-3.0.1.tgz", - "integrity": "sha512-EqmSJE8CcNoGXcgpiOD12PPMeuJZwiGMX69HPoam+GzltcjZjr6THQ0S+8a0NtQuefJtILzoDyQj+fp7+Qiorg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-3.0.2.tgz", + "integrity": "sha512-B731RJ9pfJj3D5GJPp8ccsaaHMdwBmlOb4QWAYQUFpmMxonxJvNeeIoo0E89CjMJaKDvQXy9MKpSXY/i7XgeFA==", "dev": true, "funding": [ { @@ -2138,7 +2138,7 @@ "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2" } }, @@ -2163,9 +2163,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.1.tgz", - "integrity": "sha512-EtC7SoN1j6J4E4DCwg5QgbO5TGxgxIA1RXqe+W+qUM+BUcezx9wT+/tiQ/WO2yCX4i5X+Cuf9ciJ22aP4UEwWw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz", + "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==", "devOptional": true, "funding": [ { @@ -2182,14 +2182,14 @@ "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-color-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.4.tgz", - "integrity": "sha512-DyefytAZ735mX4Dq/WcDAXFtXhaEFvme0ZS9tVEBAc2whxUthXr0R0L2rmEPm59SrMBkGrFzQviBXMs/UtnABQ==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.6.tgz", + "integrity": "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==", "devOptional": true, "funding": [ { @@ -2204,20 +2204,20 @@ "license": "MIT", "dependencies": { "@csstools/color-helpers": "^6.1.2", - "@csstools/css-calc": "^3.4.1" + "@csstools/css-calc": "^3.4.3" }, "engines": { "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.1.tgz", - "integrity": "sha512-ShL8BqPfbKJrJiKFH0xBbN0i7Nrh9HXYRuF+pzyj93R/BL2YAsUxJeqANErzqM+0JGl7vjHp+3OIgd/DL69YIA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz", + "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==", "devOptional": true, "funding": [ { @@ -2238,9 +2238,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz", - "integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz", + "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==", "devOptional": true, "funding": [ { @@ -2283,9 +2283,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.1.tgz", - "integrity": "sha512-DR5NpEOOxDOhmMWPJlhYAV9gTmV4XpfYjtE+aXSCurkxS65KzNogI4wqzRIoujieQcmFkeRtK1nRIF8ImUZTDg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.2.tgz", + "integrity": "sha512-qx19O8AbbLLbDSAcI6YOby/Vq8rV51psgIvlF5OZT/L8fQJDCyQifL6k+kYU4a8ySpzJ4RBzZkIs530bAn8y4Q==", "dev": true, "funding": [ { @@ -2302,7 +2302,7 @@ "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2" } }, @@ -2815,9 +2815,9 @@ } }, "node_modules/@lezer/javascript": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.5.tgz", - "integrity": "sha512-sWg4yX1J6XW67AaAynVt0iwF0M5c+np36TEu+P2ifAJ8haRYvHnWDV28r1jdwnJehWCwXECutAUy56K4RBZIyg==", + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.6.tgz", + "integrity": "sha512-sEvOqpl4vJQca0NugSQK02vQPbc5KrvoojnYGoqqFgFO0MtcYhwGiHKVEB+LuXYJnfyXo4QPA8ztbrEa4Fw5NA==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -3024,9 +3024,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.151.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", - "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/oxc-project" @@ -3075,9 +3075,9 @@ } }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", - "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", "cpu": [ "arm" ], @@ -3091,9 +3091,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", - "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", "cpu": [ "arm64" ], @@ -3107,9 +3107,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", - "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", "cpu": [ "arm64" ], @@ -3123,9 +3123,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", - "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", "cpu": [ "x64" ], @@ -3139,9 +3139,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", - "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", "cpu": [ "x64" ], @@ -3155,9 +3155,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", - "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", "cpu": [ "arm" ], @@ -3171,9 +3171,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", - "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", "cpu": [ "arm64" ], @@ -3190,9 +3190,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", - "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", "cpu": [ "arm64" ], @@ -3209,9 +3209,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", - "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", "cpu": [ "ppc64" ], @@ -3228,9 +3228,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", - "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", "cpu": [ "s390x" ], @@ -3247,9 +3247,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", - "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", "cpu": [ "x64" ], @@ -3266,9 +3266,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", - "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", "cpu": [ "x64" ], @@ -3285,9 +3285,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", - "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", "cpu": [ "arm64" ], @@ -3301,9 +3301,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", - "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", "cpu": [ "arm64" ], @@ -3317,9 +3317,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", - "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", "cpu": [ "x64" ], @@ -3566,9 +3566,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.20.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz", - "integrity": "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==", + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3620,9 +3620,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz", - "integrity": "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", "dev": true, "license": "MIT", "engines": { @@ -4434,9 +4434,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.26", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz", - "integrity": "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==", + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4634,9 +4634,9 @@ } }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "license": "MIT", "engines": { "node": ">=18" @@ -5095,9 +5095,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.442", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.442.tgz", - "integrity": "sha512-najZYZ3+ZpjN1z3VOsrBiv5ej18vQgfV2lvEmxWzfmKrk4bdm3Owseyud4yGU6DfFn9pyunoAHDDyM4KmP78Ew==", + "version": "1.5.444", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.444.tgz", + "integrity": "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==", "dev": true, "license": "ISC" }, @@ -5770,9 +5770,9 @@ } }, "node_modules/exsolve": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", - "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.3.tgz", + "integrity": "sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==", "dev": true, "license": "MIT" }, @@ -6132,9 +6132,9 @@ "license": "BSD-2-Clause" }, "node_modules/globals": { - "version": "17.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", - "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", "dev": true, "license": "MIT", "engines": { @@ -7975,9 +7975,9 @@ "license": "MIT" }, "node_modules/n3": { - "version": "2.7.12", - "resolved": "https://registry.npmjs.org/n3/-/n3-2.7.12.tgz", - "integrity": "sha512-Hy6dfGg9yniLQpSBirvH2E2yO3EG7dSmA3aefRbtO411oqtzG8roD3h1kTc/N065bivCOPfTFIjf0KlwW7KlnQ==", + "version": "2.10.6", + "resolved": "https://registry.npmjs.org/n3/-/n3-2.10.6.tgz", + "integrity": "sha512-LtWkgNd9dtqOTLBIScGei02zo3U0JTh9JtGKpOiEWJ15vEfxXZbIX5XURyjstAoc6YYTElQvJ+8vTvJ8G4pEZw==", "license": "MIT", "dependencies": { "buffer": "^6.0.3", @@ -8625,9 +8625,9 @@ } }, "node_modules/postcss-custom-media": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-12.0.2.tgz", - "integrity": "sha512-qn+su6Ux/TVzBSPYlRxC3Kq5DZNWBZt7y1wj38mnHT+whYnM21IxuYVKJ2E9qahBo0KGvcv/1rAB/F5iY1UiIA==", + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-12.0.3.tgz", + "integrity": "sha512-pgOICT/unQexoA/Vh4ZvyAUrcwwA/nHh0SJcOwOombpwDEj1x5N6zK+kz97wy9NslhU6UxhE5c5Q/M2gpYfkJA==", "dev": true, "funding": [ { @@ -8641,10 +8641,10 @@ ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^3.0.1", - "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/cascade-layer-name-parser": "^3.0.2", + "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2", - "@csstools/media-query-list-parser": "^5.0.1" + "@csstools/media-query-list-parser": "^5.0.2" }, "engines": { "node": ">=20.19.0" @@ -9082,12 +9082,12 @@ } }, "node_modules/rolldown": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", - "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.151.0", + "@oxc-project/types": "=0.152.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -9097,21 +9097,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.11", - "@rolldown/binding-android-arm64": "1.2.11", - "@rolldown/binding-darwin-arm64": "1.2.11", - "@rolldown/binding-darwin-x64": "1.2.11", - "@rolldown/binding-freebsd-x64": "1.2.11", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", - "@rolldown/binding-linux-arm64-gnu": "1.2.11", - "@rolldown/binding-linux-arm64-musl": "1.2.11", - "@rolldown/binding-linux-ppc64-gnu": "1.2.11", - "@rolldown/binding-linux-s390x-gnu": "1.2.11", - "@rolldown/binding-linux-x64-gnu": "1.2.11", - "@rolldown/binding-linux-x64-musl": "1.2.11", - "@rolldown/binding-openharmony-arm64": "1.2.11", - "@rolldown/binding-win32-arm64-msvc": "1.2.11", - "@rolldown/binding-win32-x64-msvc": "1.2.11" + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" } }, "node_modules/safe-array-concat": { @@ -9516,9 +9516,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -10209,9 +10209,9 @@ } }, "node_modules/unplugin-dts": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unplugin-dts/-/unplugin-dts-1.1.1.tgz", - "integrity": "sha512-Z2VcabC0hjYuzF62oWmZXVoxagkM8bUWgqUy+dotoJTPnooR5mNaxbpv63JCmcaxdGCW2Bb75omK6FOryOYCRg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unplugin-dts/-/unplugin-dts-1.1.2.tgz", + "integrity": "sha512-RDqy90XUmFj6rz4U5YoTMdYdaCJe2ZnQZaWIjkWS4952Gv0zBTQyqWtlGgVMPYHa982dURvyoc+sUOFIcDFv+w==", "dev": true, "license": "MIT", "dependencies": { @@ -10360,15 +10360,15 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", - "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", + "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", - "rolldown": "~1.2.9", + "rolldown": "~1.2.11", "tinyglobby": "^0.2.17" }, "bin": { From 766fe0245671a83ef1de11e2970a3a3262a37c45 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Fri, 16 Oct 2026 10:17:32 +1100 Subject: [PATCH 05/11] fix a few merge issues --- package-lock.json | 15 ++++++++------- package.json | 1 + src/outline/manager.js | 25 +++++++++++++------------ 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index e8cc22ac..71ca8ef3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "5.1.0-0", "license": "MIT", "dependencies": { + "@lit/context": "^1.1.6", "@solid/better-simple-slideshow": "^0.1.0", "activitystreams-pane": "2.0.0", "chat-pane": "4.0.0", @@ -9003,9 +9004,9 @@ } }, "node_modules/react": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", - "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9018,15 +9019,15 @@ "license": "MIT" }, "node_modules/react-dom": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", - "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", "license": "MIT", "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { - "react": "^19.3.0" + "react": "^19.3.1" } }, "node_modules/react-is": { diff --git a/package.json b/package.json index 1da91f28..0d0b48ae 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ }, "homepage": "https://github.com/SolidOS/solid-panes", "dependencies": { + "@lit/context": "^1.1.6", "@solid/better-simple-slideshow": "^0.1.0", "activitystreams-pane": "2.0.0", "chat-pane": "4.0.0", diff --git a/src/outline/manager.js b/src/outline/manager.js index 7836ade4..f65d6042 100644 --- a/src/outline/manager.js +++ b/src/outline/manager.js @@ -19,6 +19,7 @@ import { nextHistoryState } from '../navigation/state' import { announceNavigation } from '../navigation/events' import { resolveSource } from '../navigation/options' import { refreshNavbarVisibility } from '../navigation/navbarVisibility' +import { createOutlineDomHelpers } from './outlineDomHelpers' import '../components/file-explorer-header' import AccessControlModal from 'solid-ui/components/access-control-modal' @@ -1724,7 +1725,7 @@ export default function (context) { showSolidPanesNavbar(subject) } - host = host || outlineHost // if does not exist create a compatible host in the current shell + const host = table || outlineContainer if (solo) { host.style.width = '100%' UI.utils.emptyNode(host) @@ -1737,27 +1738,27 @@ export default function (context) { ? pane.subject : subject - function GotoSubjectDefault () { - const tr = dom.createElement('tr') - tr.classList.add('outlineRow1') - tr.style.verticalAlign = 'top' - table.appendChild(tr) - const td = thisOutline.outlineObjectTD(displayedSubject, undefined, tr) - tr.appendChild(td) - return td + function createSubjectBlock () { + const subjectBlock = dom.createElement('div') + subjectBlock.classList.add('outline-subject-block') + const block = thisOutline.outlineObjectDiv(displayedSubject, undefined, subjectBlock) + subjectBlock.appendChild(block) + host.appendChild(subjectBlock) + return subjectBlock } - const subjectBlock = createSubjectBlock() + const subjectBlock = solo ? createSubjectBlock() : host if (solo) setUrlBarAndTitle(displayedSubject) // dom.title = UI.utils.label(subject) // 'Tabulator: '+ No need to advertize if (expand) { - outlineExpand(td, displayedSubject, { + outlineExpand(subjectBlock, displayedSubject, { pane, solo, headerOnlyInViewerPod: !showNavbar || !!(pane && pane.subject && pane.subject.uri) }) - if (solo && displayedSubject !== subject) openPathWithin(table, displayedSubject, subject) + } else if (!solo) { + createSubjectBlock() } if ( From 9af3309cd71b9846de41482a0a19d74fae809c40 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sat, 17 Oct 2026 05:32:35 +1100 Subject: [PATCH 06/11] Move the file explorer provider and header to solid-ui Prompt: Fix stale async header updates with a generation counter Co-authored-by: GPT-5.6 Luna --- .../FileExplorerHeader.styles.css | 13 - .../FileExplorerHeader.ts | 86 ------ .../FileExplorerHeaderControls.styles.css | 69 ----- .../FileExplorerHeaderControls.ts | 116 -------- .../FileExplorerHeaderSummary.styles.css | 148 ---------- .../FileExplorerHeaderSummary.ts | 151 ---------- .../FileExplorerProvider.styles.css | 20 -- .../FileExplorerProvider.ts | 261 ------------------ src/components/file-explorer-header/index.ts | 2 - src/components/file-explorer-header/types.ts | 10 - .../DeleteResourceDialog.ts | 36 --- .../ResourceActionsMenu.styles.css | 12 - .../ResourceActionsMenu.ts | 83 ------ src/outline/fileExplorerHelpers.ts | 55 ++++ src/outline/manager.css | 29 +- src/outline/manager.js | 90 ++++-- src/utils/podUtils.ts | 10 +- .../components/fileExplorerProvider.test.ts | 64 ----- test/unit/mainPage/reload.test.ts | 8 +- test/unit/outline/manager.test.ts | 87 +++++- 20 files changed, 239 insertions(+), 1111 deletions(-) delete mode 100644 src/components/file-explorer-header/FileExplorerHeader.styles.css delete mode 100644 src/components/file-explorer-header/FileExplorerHeader.ts delete mode 100644 src/components/file-explorer-header/FileExplorerHeaderControls.styles.css delete mode 100644 src/components/file-explorer-header/FileExplorerHeaderControls.ts delete mode 100644 src/components/file-explorer-header/FileExplorerHeaderSummary.styles.css delete mode 100644 src/components/file-explorer-header/FileExplorerHeaderSummary.ts delete mode 100644 src/components/file-explorer-header/FileExplorerProvider.styles.css delete mode 100644 src/components/file-explorer-header/FileExplorerProvider.ts delete mode 100644 src/components/file-explorer-header/index.ts delete mode 100644 src/components/file-explorer-header/types.ts delete mode 100644 src/components/resource-actions-menu/DeleteResourceDialog.ts delete mode 100644 src/components/resource-actions-menu/ResourceActionsMenu.styles.css delete mode 100644 src/components/resource-actions-menu/ResourceActionsMenu.ts create mode 100644 src/outline/fileExplorerHelpers.ts delete mode 100644 test/unit/components/fileExplorerProvider.test.ts diff --git a/src/components/file-explorer-header/FileExplorerHeader.styles.css b/src/components/file-explorer-header/FileExplorerHeader.styles.css deleted file mode 100644 index 866bd8cb..00000000 --- a/src/components/file-explorer-header/FileExplorerHeader.styles.css +++ /dev/null @@ -1,13 +0,0 @@ -:host { - header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 6px; - flex-wrap: nowrap; - width: 100%; - text-align: right; - padding: 10px; - border-bottom: 1px solid var(--solid-ui-color-gray-200, #cbd5e1); - } -} diff --git a/src/components/file-explorer-header/FileExplorerHeader.ts b/src/components/file-explorer-header/FileExplorerHeader.ts deleted file mode 100644 index b51c4d82..00000000 --- a/src/components/file-explorer-header/FileExplorerHeader.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { sym } from 'rdflib' -import { WebComponent, type FileExplorerContext, fileExplorerContext } from 'solid-ui' -import { customElement, property, state } from 'lit/decorators.js' -import { consume } from '@lit/context' -import { html } from 'lit' -import '~icons/lucide/share-2' -import '~icons/lucide/pencil' -import '~icons/lucide/ellipsis-vertical' -import styles from './FileExplorerHeader.styles.css' -import './FileExplorerHeaderSummary' -import './FileExplorerHeaderControls' -import { PaneIcon } from './types' -import { fetchResourceMetadata } from '../../utils/podUtils' -import { type FileExplorerResourceMetadata } from './types' -@customElement('file-explorer-header') -export default class FileExplorerHeader extends WebComponent { - static styles = styles - - private _loadedMetadataForUri: string | undefined - - @consume({ context: fileExplorerContext, subscribe: true }) - accessor fileExplorerContext: FileExplorerContext = undefined as unknown as FileExplorerContext - - @property({ attribute: false }) - accessor menuItems: Array<{ label: string, action: (event: Event) => void }> = [] - - @property({ attribute: false }) - accessor paneIcon: PaneIcon = undefined as unknown as PaneIcon - - @state() - accessor responseMetadata: Pick = { - modified: undefined, - isPublic: false, - canEdit: false, - aclUri: undefined - } - - private getDefaultResponseMetadata (): Pick { - return { - modified: undefined, - isPublic: false, - canEdit: false, - aclUri: undefined - } - } - - protected updated () { - if (this.fileExplorerContext?.store && this.fileExplorerContext.subjectUri && this._loadedMetadataForUri !== this.fileExplorerContext.subjectUri) { - this._loadedMetadataForUri = this.fileExplorerContext.subjectUri - this.loadResponseMetadata() - } - } - - private async loadResponseMetadata () { - if (!this.fileExplorerContext?.store || !this.fileExplorerContext.subjectUri) return - - try { - const metadata = await fetchResourceMetadata(this.fileExplorerContext.store, sym(this.fileExplorerContext.subjectUri)) - this.responseMetadata = { - modified: metadata.modified, - isPublic: metadata.isPublic, - canEdit: metadata.canEdit, - aclUri: metadata.aclUri - } - } catch (error) { - this.responseMetadata = this.getDefaultResponseMetadata() - console.warn('Failed to load response metadata', error) - } - } - - render () { - return html` -
- - -
- ` - } -} diff --git a/src/components/file-explorer-header/FileExplorerHeaderControls.styles.css b/src/components/file-explorer-header/FileExplorerHeaderControls.styles.css deleted file mode 100644 index dc411a3c..00000000 --- a/src/components/file-explorer-header/FileExplorerHeaderControls.styles.css +++ /dev/null @@ -1,69 +0,0 @@ -:host { - flex: 0 1 auto; - min-width: 0; - - div { - display: flex; - justify-content: flex-end; - align-items: center; - gap: 6px; - flex: 0 0 auto; - text-align: right; - } - - /* generative ai */ - div > * { - flex: 0 1 auto; - } - - .file-explorer-header-action-button { - width: var(--solid-ui-clickable-area, 42px); - height: var(--solid-ui-clickable-area, 42px); - padding: 0; - } - - /* TODO: look into what colors to use this is just to show */ - .dirtyIndicator { - display: inline-flex; - align-items: center; - border: 1px solid #F59E0B; - color: #92400E; - background: #FFFBEB; - border-radius: 999px; - font-size: 0.72rem; - font-weight: 600; - line-height: 1; - padding: 4px 8px; - white-space: nowrap; - } - - icon-lucide-user-round-cog, - icon-lucide-pencil { - width: 16px; - height: 16px; - } - - @media (max-width: 900px) { - div { - padding-right: 3px; - } - } - - @media (max-width: 600px) { - div { - justify-content: flex-start; - gap: 5px; - } - - .file-explorer-header-action-button { - display: none; - } - - icon-lucide-ellipsis-vertical { - width: 15.36px; - height: 15.36px; - flex-shrink: 0; - aspect-ratio: 1 / 1; - } - } -} diff --git a/src/components/file-explorer-header/FileExplorerHeaderControls.ts b/src/components/file-explorer-header/FileExplorerHeaderControls.ts deleted file mode 100644 index 5e03da39..00000000 --- a/src/components/file-explorer-header/FileExplorerHeaderControls.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { WebComponent } from 'solid-ui' -import { customElement, property, state } from 'lit/decorators.js' -import { consume } from '@lit/context' -import { html, nothing } from 'lit' -import 'solid-ui/components/button' -import '~icons/lucide/user-round-cog' -import '~icons/lucide/pencil' -import styles from './FileExplorerHeaderControls.styles.css' -import '../resource-actions-menu/ResourceActionsMenu' -import { fileExplorerContext, type FileExplorerContext } from 'solid-ui' -import { isContainerSubject } from '../../utils/podUtils' - -@customElement('file-explorer-header-controls') -export default class FileExplorerHeaderControls extends WebComponent { - static styles = styles - - private mobileMediaQuery: MediaQueryList | undefined - private readonly mobileQuery = '(max-width: 600px)' - private readonly handleMobileMediaChange = (event: MediaQueryListEvent) => { - this.isMobile = event.matches - } - - @consume({ context: fileExplorerContext, subscribe: true }) - accessor fileExplorerContext: FileExplorerContext = undefined as unknown as FileExplorerContext - - @property({ attribute: false }) - accessor menuItems: Array<{ label: string, action: (event: Event) => void, icon?: HTMLElement }> = [] - - @property({ type: Boolean }) - accessor canEdit: boolean = false - - @state() - accessor isMobile = typeof window !== 'undefined' && typeof window.matchMedia === 'function' - ? window.matchMedia('(max-width: 600px)').matches - : false - - connectedCallback () { - super.connectedCallback() - - if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { - return - } - - this.mobileMediaQuery = window.matchMedia(this.mobileQuery) - this.isMobile = this.mobileMediaQuery.matches - this.mobileMediaQuery.addEventListener('change', this.handleMobileMediaChange) - } - - disconnectedCallback () { - this.mobileMediaQuery?.removeEventListener('change', this.handleMobileMediaChange) - this.mobileMediaQuery = undefined - super.disconnectedCallback() - } - - // TODO: Add broken then use this function to set tooltip and disable edit button - /* private setEditable() { - const sourcePaneState = this.sourceContext?.sourcePaneState - const { canEdit } = this.sourceContext?.headerMetadata ?? { canEdit: false } - const subject = this.sourceContext?.subject - if (!sourcePaneState || !canEdit || sourcePaneState.broken || (subject && subject.endsWith('/'))) return - - this.sourceContext?.setEditing?.() - } */ - - private getEditTooltip () { - if (!this.canEdit) return 'No Access' - return 'Edit' - } - - private renderDirtyIndicator () { - if (!this.fileExplorerContext.edit?.isDirty) return nothing - - return html`Unsaved` - } - - render () { - const isContainerResource = isContainerSubject(this.fileExplorerContext.store, this.fileExplorerContext.subjectUri) - const supportsEditing = this.fileExplorerContext.paneSupportsEditing - - return html` -
- ${this.renderDirtyIndicator()} - ${!isContainerResource && !this.isMobile - ? html` - - - - ${supportsEditing - ? html` - - - - ` - : nothing} - ` - : nothing} - -
- ` - } -} diff --git a/src/components/file-explorer-header/FileExplorerHeaderSummary.styles.css b/src/components/file-explorer-header/FileExplorerHeaderSummary.styles.css deleted file mode 100644 index db9952b7..00000000 --- a/src/components/file-explorer-header/FileExplorerHeaderSummary.styles.css +++ /dev/null @@ -1,148 +0,0 @@ - :host { - .file-explorer-header-summary { - display: flex; - justify-content: flex-start; - align-items: center; - gap: 6px; - flex-wrap: wrap; - flex: 1 1 auto; - min-width: 0; - text-align: left; - } - - h1 { - color: var(--solid-ui-color-gray-700, #364153); - font-size: var(--solid-ui-font-size-xl, 1.25rem); /* while in expand; will go to 2xl when new design is complete */ - font-weight: 500; - display: flex; - align-items: center; - gap: 8px; - margin: 0; - } - - .resource-info { - display: flex; - flex-direction: column; - min-width: 0; - } - - .container-info { - display: flex; - flex-direction: row; - gap: 10px; - min-width: 0; - } - - .pane-icon { - display: flex; - width: 40px; - height: 40px; - padding: 7.5px 7px 6.5px 7px; - justify-content: center; - align-items: center; - aspect-ratio: 1/1; - /* border-radius: 100px; */ - /* background: var(--slate-600, #45556C); */ - } - - .pane-icon img { - width: 100%; - height: 100%; - object-fit: contain; - display: block; - } - - p { - display: flex; - align-items: center; - gap: 5px; - color: var(--solid-ui-gray-500, #6A7282); - font-size: var(--solid-ui-font-size-xs, .75rem); - font-weight: 500; - } - - .public { - display: flex; - padding: 2px 5px; - justify-content: center; - align-items: center; - gap: 3px; - color: var(--blue-400, #51A2FF); - border-radius: 5px; - background: var(--blue-50, #EFF6FF); - } - - icon-lucide-globe { - stroke-width: 1.021px; - stroke: var(--blue-400, #51A2FF); - } - - .private { - display: flex; - padding: 2px 5px; - justify-content: center; - align-items: center; - gap: 3px; - color: var(--yellow-600, #D08700); - border-radius: 5px; - background: var(--yellow-100, #FEF9C2); - } - - icon-lucide-lock-keyhole { - stroke-width: 1.021px; - stroke: var(--yellow-600, #D08700); - } - - icon-lucide-globe, - icon-lucide-lock-keyhole { - width: 0.875rem; - height: 0.875rem; - } - - .resource-date { - font-size: inherit; - } - - icon-lucide-arrow-left { - width: 1.125rem; - height: 1.125rem; - } - - @media (max-width: 600px) { - .file-explorer-header-summary { - gap: 7px; - } - - h1 { - font-size: 14px; - } - - .pane-icon { - width: 18.2px; - height: 18.2px; - flex-shrink: 0; - aspect-ratio: 1 / 1; - padding: 0; - } - - p { - font-size: 14px; - } - - .resource-date { - font-size: 10px; - } - - .public, - .private { - font-size: 9px; - } - - icon-lucide-arrow-left { - width: 15px; - height: 18px; - flex-shrink: 0; - aspect-ratio: 5 / 6; - } - } -} diff --git a/src/components/file-explorer-header/FileExplorerHeaderSummary.ts b/src/components/file-explorer-header/FileExplorerHeaderSummary.ts deleted file mode 100644 index 2ca785d5..00000000 --- a/src/components/file-explorer-header/FileExplorerHeaderSummary.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { sym } from 'rdflib' -import { widgets, utils, WebComponent, fileExplorerContext, type FileExplorerContext } from 'solid-ui' -import 'solid-ui/components/button' -import { customElement, property, query, state } from 'lit/decorators.js' -import { html } from 'lit' -import { consume } from '@lit/context' -import { PaneIcon } from './types' -import '~icons/lucide/globe' -import '~icons/lucide/lock-keyhole' -import '~icons/lucide/arrow-left' -import '~icons/lucide/folder' -import styles from './FileExplorerHeaderSummary.styles.css' -import { getContainerItemCount, isContainerSubject } from '../../utils/podUtils' -import type { FileExplorerResourceMetadata } from './types' - -@customElement('file-explorer-header-summary') -export default class FileExplorerHeaderSummary extends WebComponent { - static styles = styles - - private _draggableSubjectUri: string | undefined - private _resolvedPaneIconFor: PaneIcon | undefined - - @consume({ context: fileExplorerContext, subscribe: true }) - accessor fileExplorerContext: FileExplorerContext = undefined as unknown as FileExplorerContext - - @property({ attribute: false }) - accessor paneIcon: PaneIcon | undefined - - @property({ attribute: false }) - accessor onBackClick: (() => void) | undefined - - @property({ attribute: false }) - accessor responseMetadata: Pick = { - modified: undefined, - isPublic: false - } - - @state() - accessor resolvedPaneIcon: string | undefined = undefined - - @query('h1') - private accessor titleHeading: HTMLHeadingElement | null = null - - private formatModifiedDate (modified: string | undefined) { - if (!modified) return '' - - const date = new Date(modified) - if (Number.isNaN(date.getTime())) return modified - - const parts = new Intl.DateTimeFormat('en-GB', { - day: '2-digit', - month: 'short', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: true - }).formatToParts(date) - - const getPart = (type: Intl.DateTimeFormatPartTypes) => parts.find(part => part.type === type)?.value ?? '' - const day = getPart('day') - const month = getPart('month') - const year = getPart('year') - const hour = getPart('hour') - const minute = getPart('minute') - const dayPeriod = getPart('dayPeriod').toUpperCase() - - return `${day} ${month}, ${year} at ${hour}:${minute} ${dayPeriod}` - } - - protected updated () { - if (this.paneIcon !== undefined && this._resolvedPaneIconFor !== this.paneIcon) { - this._resolvedPaneIconFor = this.paneIcon - this.resolvePaneIcon() - } - - if (!this.titleHeading || !this.fileExplorerContext?.subjectUri || this._draggableSubjectUri === this.fileExplorerContext.subjectUri) return - - widgets.makeDraggable(this.titleHeading, sym(this.fileExplorerContext.subjectUri)) // doing it this way for now, just to keep the same functionality - this._draggableSubjectUri = this.fileExplorerContext.subjectUri - } - - // Needed because humanReadable pane can return a promise for an icon. - private async resolvePaneIcon () { - try { - if (this.paneIcon == null) { - this.resolvedPaneIcon = undefined - return - } - - const icon = await this.paneIcon - this.resolvedPaneIcon = icon ?? undefined - } catch (error) { - this.resolvedPaneIcon = undefined - console.warn('file-explorer-header-summary: failed to resolve pane icon', error) - } - } - - private renderContainerResourceHeader (label: string, isPublic: boolean) { - const itemCount = getContainerItemCount(this.fileExplorerContext?.store, this.fileExplorerContext?.subjectUri) ?? 0 - return html` -
-

- ${label} -

-

- ${itemCount} items - ${isPublic - ? html`` - : html``} -

-
- ` - } - - private renderResourceHeader (label: string, isPublic: boolean) { - const modified = this.formatModifiedDate(this.responseMetadata.modified) - - return html` -
-

- ${label} -

-

${modified} ${isPublic ? html` Public` : html` Private`}

-
- ` - } - - render () { - const subject = this.fileExplorerContext?.subjectUri ? sym(this.fileExplorerContext.subjectUri) : undefined - const label = subject ? utils.label(subject) : '' - const isPublic = this.responseMetadata.isPublic - const isContainerResource = isContainerSubject(this.fileExplorerContext?.store, this.fileExplorerContext?.subjectUri) - - return html` -
- - - - - ${this.resolvedPaneIcon ? html`` : ''} - - ${isContainerResource ? this.renderContainerResourceHeader(label, isPublic) : this.renderResourceHeader(label, isPublic)} -
- - ` - } -} diff --git a/src/components/file-explorer-header/FileExplorerProvider.styles.css b/src/components/file-explorer-header/FileExplorerProvider.styles.css deleted file mode 100644 index 298eedaa..00000000 --- a/src/components/file-explorer-header/FileExplorerProvider.styles.css +++ /dev/null @@ -1,20 +0,0 @@ -:host { - display: block; - width: 100%; - min-width: 0; - - .file-explorer-provider { - display: flex; - flex-direction: column; - width: 100%; - min-width: 0; - } - - slot.pane { - display: block; - flex: 1 1 auto; - width: 100%; - min-width: 0; - overflow: auto; - } -} diff --git a/src/components/file-explorer-header/FileExplorerProvider.ts b/src/components/file-explorer-header/FileExplorerProvider.ts deleted file mode 100644 index 2b46dc12..00000000 --- a/src/components/file-explorer-header/FileExplorerProvider.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { html, nothing, type PropertyValues } from 'lit' -import type { NamedNode, LiveStore } from 'rdflib' -import { DataBrowserContext, type PaneDefinition } from 'pane-registry' -import { utils, WebComponent } from 'solid-ui' -import { provide } from '@lit/context' -import { fileExplorerContext, type FileExplorerContext, type FileExplorerEdit } from 'solid-ui' -import { customElement, property, state } from 'lit/decorators.js' -import './FileExplorerHeader' -import styles from './FileExplorerProvider.styles.css' -import personIcon from '../../icons/person.svg' -import friendsIcon from '../../icons/friends.svg' -import '~icons/lucide/share-2' -import { explorerHeaderForPane } from '../../navigation/chrome' - -const PERSON_ICON = personIcon -const FRIENDS_ICON = friendsIcon - -function createFileExplorerContextValue (value: { - store: LiveStore | undefined - subjectUri: string | undefined - pane?: PaneDefinition - soloPane?: boolean - onBack?: () => void - openPane?: (subject: NamedNode, paneName: string) => void - handleAccessClick?: () => void - paneSupportsEditing?: boolean - edit?: { - onEdit?: () => void - isDirty?: boolean - updateDirtyState?: (dirty: boolean) => void - } -}): FileExplorerContext { - return { - store: value.store as LiveStore, - subjectUri: value.subjectUri, - pane: value.pane, - soloPane: value.soloPane, - onBack: value.onBack, - openPane: value.openPane, - handleAccessClick: value.handleAccessClick, - paneSupportsEditing: value.paneSupportsEditing, - edit: value.edit - } -} -@customElement('file-explorer-provider') -export default class FileExplorerProvider extends WebComponent { - static styles = styles - - @property({ attribute: false }) - accessor context: DataBrowserContext | undefined = undefined - - @property({ attribute: false }) - accessor subjectUri: string | undefined = undefined - - @property({ attribute: false }) - accessor onBack: (() => void) | undefined = undefined - - @property({ attribute: false }) - accessor relevantPanes: PaneDefinition[] = [] - - @property({ attribute: false }) - accessor pane: PaneDefinition | undefined = undefined - - @property({ attribute: false }) - accessor paneRenderOptions: Record = {} - - @property({ attribute: false }) - accessor showHeader: boolean = true - - @property({ attribute: false }) - accessor handleAccessClick: (() => void) | undefined = undefined - - // TODO: Need to research this more, brought it over from manager. - @property({ attribute: false }) - accessor soloPane: boolean | undefined = undefined - - @property({ attribute: false }) - accessor openPane: ((subject: NamedNode, paneName: string) => void) | undefined = undefined - - @state() - accessor menuItems: Array<{ label: string, icon?: HTMLElement, action: (event: Event) => void }> = [] - - @state() - accessor isEditing: boolean = false - - @state() - accessor isDirty: boolean = false - - @state() - accessor paneSupportsEditing: boolean = false - - // TODO: For now this works, but check if there is a better way. - // because this means file explorer will know about the pane. - // what if other panes want to use this. If so maybe we should - // add more to the context to capture the provider for the pane that - // will be editing... anyway this shouldn't be just for source pane. - private beginEditingInSourcePane = () => { - const sourceProvider = this.querySelector('source-pane-source-provider') as { - beginEditing?: () => void - } | null - - sourceProvider?.beginEditing?.() - } - - // TODO: same as below. on Edit should be something more generic - @state() - accessor edit: FileExplorerEdit = { - onEdit: this.beginEditingInSourcePane, - isDirty: false, - updateDirtyState: (dirty: boolean) => { - if (this.isDirty === dirty) return - - this.isDirty = dirty - this.edit = { - ...this.edit, - isDirty: dirty - } - this.refreshFileExplorerContextValue() - } - } - - @provide({ context: fileExplorerContext }) - accessor fileExplorerContextValue: FileExplorerContext = createFileExplorerContextValue({ - store: this.context?.session.store as LiveStore, - subjectUri: this.subjectUri, - pane: this.pane, - soloPane: this.soloPane, - onBack: this.onBack, - openPane: this.openPane, - handleAccessClick: this.handleAccessClick, - paneSupportsEditing: false, - edit: this.edit - }) - - // Change the state for the pane so it can show the new icon. - openSelectedPane (pane: PaneDefinition) { - const store = this.context?.session.store as LiveStore - if (!store || !this.subjectUri) return - this.paneSupportsEditing = pane.name === 'source' - this.pane = pane - } - - private getPaneIcon (pane, subject, context) { - if (!pane) return undefined - - const icon = typeof pane.icon === 'function' - ? pane.icon(subject, context) - : pane.icon - return icon - } - - private async getPaneItems (subject, context, relevantPanes) { - const dom = context.dom - const menuItems = await Promise.all(relevantPanes.map(async pane => { - const label = pane.label(subject, context) - - let iconSrc = '' - if (pane.name === 'profile') { - iconSrc = PERSON_ICON - } else if (pane.name === 'social') { - iconSrc = FRIENDS_ICON - } else { - iconSrc = await this.getPaneIcon(pane, subject, context) - } - - const icon = utils.AJARImage(iconSrc, label, label, dom) - icon.setAttribute('slot', 'left-icon') - - return { - label, - icon, - action: () => this.openPane?.(subject, pane.name) - } - })) - return menuItems - } - - private refreshFileExplorerContextValue () { - this.fileExplorerContextValue = createFileExplorerContextValue({ - store: this.context?.session.store as LiveStore, - subjectUri: this.subjectUri, - pane: this.pane, - soloPane: this.soloPane, - onBack: this.onBack, - openPane: this.openPane, - handleAccessClick: this.handleAccessClick, - paneSupportsEditing: this.paneSupportsEditing, - edit: this.edit - }) - } - - private async refreshMenuItems () { - const store = this.context?.session.store as LiveStore - if (!store || !this.subjectUri) return - - const subject = store.sym(this.subjectUri) - const menuItems = await this.getPaneItems(subject, this.context as DataBrowserContext, this.relevantPanes) - - this.menuItems = menuItems - } - - protected willUpdate (changedProperties: PropertyValues) { - super.willUpdate(changedProperties) - const store = this.context?.session.store as LiveStore - if (!store) { - throw new Error('The element is missing the required `context.session.store` value. Ensure the `context` property is set.') - } - if (!this.subjectUri) { - throw new Error('The element is missing the required `subjectUri` property.') - } - - if ( - changedProperties.has('subjectUri') || - changedProperties.has('relevantPanes') || - changedProperties.has('context') - ) { - this.refreshMenuItems().catch(error => { - console.warn('Failed to refresh menu items', error) - }) - } - - if (changedProperties.has('pane') || changedProperties.has('subjectUri') || changedProperties.has('context')) { - this.paneSupportsEditing = this.pane?.name === 'source' - } - - if ( - changedProperties.has('context') || - changedProperties.has('subjectUri') || - changedProperties.has('pane') || - changedProperties.has('soloPane') || - changedProperties.has('onBack') || - changedProperties.has('openPane') || - changedProperties.has('handleAccessClick') || - changedProperties.has('pane') || - changedProperties.has('isDirty') - ) { - this.refreshFileExplorerContextValue() - } - } - - render () { - const store = this.context?.session.store as LiveStore - const subject = store.sym(this.subjectUri as string) - // Profile and friends panes present a person, not the resource they are opened on. - const showHeader = explorerHeaderForPane(this.showHeader, this.pane?.name) - return html` -
- ${showHeader - ? html` - - ` - : nothing} - -
- ` - } -} diff --git a/src/components/file-explorer-header/index.ts b/src/components/file-explorer-header/index.ts deleted file mode 100644 index 2140b416..00000000 --- a/src/components/file-explorer-header/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as FileExplorerHeader } from './FileExplorerHeader' -export { default as FileExplorerProvider } from './FileExplorerProvider' diff --git a/src/components/file-explorer-header/types.ts b/src/components/file-explorer-header/types.ts deleted file mode 100644 index 826d13a8..00000000 --- a/src/components/file-explorer-header/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type PaneIcon = string | Promise | null | undefined - -export type FileExplorerResourceMetadata = { - contentType: string | undefined - canEdit: boolean - isPublic: boolean - aclUri: string | undefined - eTag: string | undefined - modified: string | undefined -} diff --git a/src/components/resource-actions-menu/DeleteResourceDialog.ts b/src/components/resource-actions-menu/DeleteResourceDialog.ts deleted file mode 100644 index 42b1908f..00000000 --- a/src/components/resource-actions-menu/DeleteResourceDialog.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { customElement, DialogComponent } from 'solid-ui' -import { html } from 'lit' -import { property } from 'lit/decorators.js' - -import 'solid-ui/components/button' -import 'solid-ui/components/dialog' -import 'solid-ui/components/dialog-content' -import 'solid-ui/components/dialog-footer' - -@customElement('resource-delete-dialog') -export default class DeleteResourceDialog extends DialogComponent { - @property({ type: String }) - accessor resourceName = 'this resource' - - private cancel = () => this.close(false) - - private confirm = () => this.close(true) - - protected render () { - return html` - - -

Are you sure you want to permanently delete ${this.resourceName}?

-
- - - Go back - - - Yes - - -
- ` - } -} diff --git a/src/components/resource-actions-menu/ResourceActionsMenu.styles.css b/src/components/resource-actions-menu/ResourceActionsMenu.styles.css deleted file mode 100644 index 9b9e7b4d..00000000 --- a/src/components/resource-actions-menu/ResourceActionsMenu.styles.css +++ /dev/null @@ -1,12 +0,0 @@ -:host { - .file-explorer-header-more-button { - width: var(--solid-ui-clickable-area, 42px); - height: var(--solid-ui-clickable-area, 42px); - padding: 0; - } - - .ellipsisIcon { - width: 16px; - height: 16px; - } -} diff --git a/src/components/resource-actions-menu/ResourceActionsMenu.ts b/src/components/resource-actions-menu/ResourceActionsMenu.ts deleted file mode 100644 index 6a37b83c..00000000 --- a/src/components/resource-actions-menu/ResourceActionsMenu.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { WebComponent } from 'solid-ui' -import { customElement, property } from 'lit/decorators.js' -import { html, nothing } from 'lit' -import 'solid-ui/components/button' -import 'solid-ui/components/menu' -import 'solid-ui/components/menu-item' -import '~icons/lucide/ellipsis-vertical' -import '~icons/lucide/user-round-cog' -import '~icons/lucide/pencil' -import { LiveStore } from 'rdflib' -import styles from './ResourceActionsMenu.styles.css' -import { isContainerSubject } from '../../utils/podUtils' -@customElement('resource-actions-menu') -export default class ResourceActionsMenu extends WebComponent { - static styles = styles - - @property({ attribute: false }) - accessor store: LiveStore | undefined - - @property({ type: String }) - accessor subjectUri: string | undefined - - @property({ attribute: false }) - accessor handleAccessClick: (() => void) | undefined = undefined - - @property({ attribute: false }) - accessor handleEditingClick: (() => void) | undefined = undefined - - @property({ type: Boolean }) - accessor paneSupportsEditing = false - - @property({ type: Boolean }) - accessor canEdit = false - - @property({ type: Boolean }) - accessor isMobile = false - - @property({ attribute: false }) - accessor menuItems: Array<{ label: string, icon?: unknown, action: (event: Event) => void }> = [] - - render () { - const isContainerResource = isContainerSubject(this.store, this.subjectUri) - const canEdit = !isContainerResource && this.isMobile && this.paneSupportsEditing && this.canEdit && !!this.handleEditingClick - const canManageAccess = !!this.handleAccessClick - return html` - - - - - ${this.menuItems.map(item => html` - item.action(event)}> - ${item.icon} - ${item.label} - - `)} - ${canEdit - ? html` - - - Edit - - ` - : nothing} - ${!isContainerResource && this.isMobile && canManageAccess - ? html` - - - Manage Access - - ` - : nothing} - ${isContainerResource && canManageAccess - ? html` - - - Manage Access - - ` - : nothing} - - ` - } -} diff --git a/src/outline/fileExplorerHelpers.ts b/src/outline/fileExplorerHelpers.ts new file mode 100644 index 00000000..bb8f5e84 --- /dev/null +++ b/src/outline/fileExplorerHelpers.ts @@ -0,0 +1,55 @@ +import * as UI from 'solid-ui' +import type { DataBrowserContext, PaneDefinition } from 'pane-registry' +import type { NamedNode } from 'rdflib' +import personIcon from '../icons/person.svg' +import friendsIcon from '../icons/friends.svg' + +type ExplorerPane = Omit & { + icon: string | ((subject: NamedNode, context: DataBrowserContext) => string | undefined) +} + +export function getPaneIcon ( + pane: ExplorerPane | undefined, + subject: NamedNode, + context: DataBrowserContext +) { + if (!pane) return undefined + + return typeof pane.icon === 'function' + ? pane.icon(subject, context) + : pane.icon +} + +export async function getPaneItems ( + subject: NamedNode, + context: DataBrowserContext, + relevantPanes: ExplorerPane[], + openPane: ((subject: NamedNode, paneName: string) => void) | undefined +) { + if (typeof openPane !== 'function') return [] + + const menuItems = await Promise.all(relevantPanes.map(async pane => { + const label = pane.label(subject, context) + if (!label) return undefined + + let iconSrc: string | undefined + if (pane.name === 'profile') { + iconSrc = personIcon + } else if (pane.name === 'social') { + iconSrc = friendsIcon + } else { + iconSrc = await getPaneIcon(pane, subject, context) || undefined + } + + const icon = UI.utils.AJARImage(iconSrc || '', label, label, context.dom) + icon.setAttribute('slot', 'left-icon') + + return { + label, + icon, + action: () => openPane(subject, pane.name) + } + })) + + return menuItems.filter((item): item is NonNullable => item !== undefined) +} diff --git a/src/outline/manager.css b/src/outline/manager.css index ceadb86f..50b2ff09 100644 --- a/src/outline/manager.css +++ b/src/outline/manager.css @@ -19,13 +19,6 @@ width: 100%; } -.file-explorer-header { - display: flex; - justify-content: flex-start; - align-items: center; - flex-wrap: wrap; -} - .strongHeader { font-size: 150%; margin: 0 0.6rem 0 0; @@ -50,6 +43,28 @@ overflow: hidden; } +solid-ui-file-explorer-provider { + display: block; + width: 100%; + min-width: 0; +} + +solid-ui-file-explorer-provider.tdFlex { + flex-direction: column; + align-items: stretch; +} + +solid-ui-file-explorer-provider > solid-ui-file-explorer-header { + flex: 0 0 auto; + width: 100%; +} + +solid-ui-file-explorer-provider > .paneDiv, +solid-ui-file-explorer-provider > .exceptionPane { + flex: 1 1 auto; + width: 100%; +} + .paneDiv, .exceptionPane { display: block; diff --git a/src/outline/manager.js b/src/outline/manager.js index f65d6042..d643f4cb 100644 --- a/src/outline/manager.js +++ b/src/outline/manager.js @@ -13,18 +13,22 @@ import { UserInput } from './userInput.js' import * as queryByExample from './queryByExample.js' import { loadContainerRepresentation } from '../utils/podUtils' import { isWebIdUri } from '../utils/webIdUtils' -import { deriveExplorerHeader } from '../navigation/chrome' +import { deriveExplorerHeader, explorerHeaderForPane } from '../navigation/chrome' import { isPageInViewerPod } from '../navigation/podContext' import { nextHistoryState } from '../navigation/state' import { announceNavigation } from '../navigation/events' import { resolveSource } from '../navigation/options' import { refreshNavbarVisibility } from '../navigation/navbarVisibility' import { createOutlineDomHelpers } from './outlineDomHelpers' -import '../components/file-explorer-header' +import { getPaneIcon, getPaneItems } from './fileExplorerHelpers' +import 'solid-ui/components/file-explorer-provider' +import 'solid-ui/components/file-explorer-header' import AccessControlModal from 'solid-ui/components/access-control-modal' export default function (context) { const dom = context.dom + const explorerHeaders = new WeakMap() + const explorerHeaderGenerations = new WeakMap() this.document = context.dom this.context = context @@ -317,9 +321,9 @@ export default function (context) { const subjectNode = outlineView.querySelector('[about="' + subjectId + '"]') if (!subjectNode) return - const provider = subjectNode.matches?.('file-explorer-provider') + const provider = subjectNode.matches?.('solid-ui-file-explorer-provider') ? subjectNode - : subjectNode.querySelector('file-explorer-provider') + : subjectNode.querySelector('solid-ui-file-explorer-provider') if (!provider) return if (!pane) { UI.log.warn('outline: Unable to open pane in place: pane is undefined.') @@ -327,14 +331,40 @@ export default function (context) { } setQueryButtonVisibility(!!pane.requireQueryButton) - provider.openSelectedPane?.(pane) + provider.pane = pane + renderExplorerHeader(provider, subject, pane) renderPaneIntoProvider(provider, subject, pane, provider.paneRenderOptions) } + function renderExplorerHeader (provider, subject, pane) { + const headerState = explorerHeaders.get(provider) + const currentHeader = provider.querySelector(':scope > solid-ui-file-explorer-header') + + if (!headerState || !explorerHeaderForPane(headerState.showHeader, pane?.name)) { + currentHeader?.remove() + return + } + + const header = currentHeader || dom.createElement('solid-ui-file-explorer-header') + header.paneIcon = getPaneIcon(pane, subject, context) + header.menuItems = headerState.menuItems + if (!currentHeader) { + header.addEventListener(UI.FILE_EXPLORER_BACK_EVENT, () => provider.onBack?.()) + header.addEventListener(UI.FILE_EXPLORER_MANAGE_ACCESS_EVENT, () => { + provider.handleAccessClick().catch(error => { + UI.log.error('outline: Failed to open access controls: ' + UI.utils.stackString(error)) + }) + }) + provider.prepend(header) + } + } + async function expandedProviderTR (subject, requiredPane, options, provider) { options = options || {} - provider = provider || dom.createElement('file-explorer-provider') + provider = provider || dom.createElement('solid-ui-file-explorer-provider') + const generation = (explorerHeaderGenerations.get(provider) || 0) + 1 + explorerHeaderGenerations.set(provider, generation) provider.classList.add('paneView', 'tdFlex') provider.setAttribute('notSelectable', 'true') provider.setAttribute('role', 'grid') @@ -351,21 +381,25 @@ export default function (context) { const relevantPanes = options.hideList ? [] : await getRelevantPanes(subject, context) + if (explorerHeaderGenerations.get(provider) !== generation) return provider provider.relevantPanes = relevantPanes provider.pane = requiredPane || getPane(relevantPanes, subject) const isRootResource = !!(subject && subject.uri && subject.site && subject.site().uri === subject.uri) const viewerPodOnly = !!options.headerOnlyInViewerPod - provider.showHeader = deriveExplorerHeader({ + const showHeader = deriveExplorerHeader({ isRootResource, isWebId: isWebIdUri(subject), viewerPodOnly, - // Only worth looking up when the view does not show the page URL's own resource - inViewerPod: viewerPodOnly ? await isPageInViewerPod(document.location.href) : true + // Only worth looking up when the view does not show the page URL's own resource. + inViewerPod: viewerPodOnly ? await isPageInViewerPod(dom.location.href) : true }) + if (explorerHeaderGenerations.get(provider) !== generation) return provider + const headerState = { showHeader, menuItems: [] } + explorerHeaders.set(provider, headerState) provider.paneRenderOptions = options provider.soloPane = options.solo - provider.openPane = (paneSubject, paneName) => openPaneInPlace(paneSubject, paneRegistry.byName(paneName)) + provider.openPane = (paneSubject, paneName) => openPaneInPlace(paneSubject, context.session.paneRegistry.byName(paneName)) provider.handleAccessClick = async () => { const accessGrants = await solidLogicSingleton.acl.findAccessGrants(subject) @@ -380,6 +414,16 @@ export default function (context) { if (provider.pane) { renderPaneIntoProvider(provider, subject, provider.pane, options) } + renderExplorerHeader(provider, subject, provider.pane) + + if (showHeader) { + const menuItems = await getPaneItems(subject, context, relevantPanes, provider.openPane) + if (explorerHeaderGenerations.get(provider) === generation && explorerHeaders.get(provider) === headerState) { + headerState.menuItems = menuItems + const header = provider.querySelector(':scope > solid-ui-file-explorer-header') + if (header) header.menuItems = menuItems + } + } return provider } @@ -427,19 +471,17 @@ export default function (context) { subject = kb.canon(subject) // if (!requiredPane) requiredPane = panes.defaultPane; - if (!table) { - const provider = dom.createElement('file-explorer-provider') - expandedProviderTR(subject, requiredPane, options, provider) - return provider - } else { - const existingProvider = table.matches?.('file-explorer-provider') + const provider = table + ? table.matches?.('solid-ui-file-explorer-provider') ? table : table.firstElementChild || table - expandedProviderTR(subject, requiredPane, options, existingProvider) - UI.log.info('Re-expand: ' + table) - // do some other stuff here - return table - } + : dom.createElement('solid-ui-file-explorer-provider') + + expandedProviderTR(subject, requiredPane, options, provider).catch(error => { + UI.log.error('outline: Failed to set up file explorer: ' + UI.utils.stackString(error)) + }) + if (table) UI.log.info('Re-expand: ' + table) + return table || provider }) /* propertyTable */ function propertyTR (doc, st, inverse) { @@ -1086,9 +1128,9 @@ export default function (context) { break case 37: { // left if (this.walk('left')) return - const subjectNode = selectedTd.firstElementChild?.matches('file-explorer-provider') + const subjectNode = selectedTd.firstElementChild?.matches('solid-ui-file-explorer-provider') ? selectedTd.firstElementChild - : selectedTd.querySelector('file-explorer-provider') + : selectedTd.querySelector('solid-ui-file-explorer-provider') outlineCollapse(selectedTd, UI.utils.getAbout(kb, subjectNode)) break } @@ -1103,7 +1145,7 @@ export default function (context) { this.walk('right') return } - if (!selectedTd.firstElementChild?.matches('file-explorer-provider')) { + if (!selectedTd.firstElementChild?.matches('solid-ui-file-explorer-provider')) { // not expanded sf.addCallback('done', setSelectedAfterward) sf.addCallback('fail', setSelectedAfterward) diff --git a/src/utils/podUtils.ts b/src/utils/podUtils.ts index 27abd7dd..0072f5ca 100644 --- a/src/utils/podUtils.ts +++ b/src/utils/podUtils.ts @@ -3,7 +3,15 @@ import { ns } from 'solid-ui' import { LiveStore, NamedNode, parse } from 'rdflib' import { isWebIdUri } from './webIdUtils' import { ACL_LINK } from 'solid-logic' -import { FileExplorerResourceMetadata } from 'src/components/file-explorer-header/types' + +type FileExplorerResourceMetadata = { + contentType: string | undefined + canEdit: boolean + isPublic: boolean + aclUri: string | undefined + eTag: string | undefined + modified: string | undefined +} function parseWacAllowHeader (headerValue: string | null | undefined) { const permissions = new Map>() diff --git a/test/unit/components/fileExplorerProvider.test.ts b/test/unit/components/fileExplorerProvider.test.ts deleted file mode 100644 index b8141103..00000000 --- a/test/unit/components/fileExplorerProvider.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { store } from 'solid-logic' -import type { PaneDefinition } from 'pane-registry' - -vi.mock('../../../src/utils/podUtils', async (importOriginal) => ({ - ...await importOriginal(), - fetchResourceMetadata: async () => ({ modified: undefined, isPublic: false, canEdit: false, aclUri: undefined }) -})) - -import '../../../src/components/file-explorer-header' - -afterEach(() => { - document.body.replaceChildren() -}) - -type Provider = HTMLElement & { - context: unknown - subjectUri: string - pane: Pick - showHeader: boolean - updateComplete: Promise -} - -async function renderProvider (paneName: string, showHeader = true) { - const provider = document.createElement('file-explorer-provider') as Provider - provider.context = { dom: document, session: { store } } - provider.subjectUri = `${window.location.origin}/container/` - provider.pane = { name: paneName } - provider.showHeader = showHeader - document.body.appendChild(provider) - await provider.updateComplete - return provider -} - -describe('file explorer provider', () => { - it.each(['profile', 'social'])('does not render the file explorer header for the %s pane', async (paneName) => { - const provider = await renderProvider(paneName) - - expect(provider.shadowRoot!.querySelector('file-explorer-header')).toBeNull() - expect(provider.shadowRoot!.querySelector('slot.pane')).not.toBeNull() - }) - - it.each(['folder', 'source'])('renders the file explorer header for the %s pane', async (paneName) => { - const provider = await renderProvider(paneName) - - expect(provider.shadowRoot!.querySelector('file-explorer-header')).not.toBeNull() - }) - - it('still honours showHeader being off', async () => { - const provider = await renderProvider('folder', false) - - expect(provider.shadowRoot!.querySelector('file-explorer-header')).toBeNull() - }) - - it('removes the header when the displayed pane changes to the profile pane', async () => { - const provider = await renderProvider('folder') - expect(provider.shadowRoot!.querySelector('file-explorer-header')).not.toBeNull() - - provider.pane = { name: 'profile' } - await provider.updateComplete - - expect(provider.shadowRoot!.querySelector('file-explorer-header')).toBeNull() - }) -}) diff --git a/test/unit/mainPage/reload.test.ts b/test/unit/mainPage/reload.test.ts index b616efb0..512b799e 100644 --- a/test/unit/mainPage/reload.test.ts +++ b/test/unit/mainPage/reload.test.ts @@ -115,7 +115,7 @@ describe('page reload of the document that holds a WebID', () => { await initMainPage(store) const provider = await vi.waitFor(() => { - const element = document.querySelector('file-explorer-provider') + const element = document.querySelector('solid-ui-file-explorer-provider') expect(element).not.toBeNull() return element! }) @@ -129,7 +129,7 @@ describe('page reload of the document that holds a WebID', () => { expect(navbar).not.toHaveClass('navbar--hidden') expect(window.location.href).toBe(webId.doc().uri) expect(provider.subjectUri).toBe(webId.doc().uri) - expect(provider.showHeader).toBe(true) + expect(provider.querySelector('solid-ui-file-explorer-header')).not.toBeNull() expect(provider.pane?.name).not.toBe('profile') expect(window.history.state?.paneName).toBeUndefined() expect(navbar.navbarItems.filter(item => item.selected)).toEqual([]) @@ -165,13 +165,13 @@ describe('page reload of the document that holds a WebID', () => { await initMainPage(store) const provider = await vi.waitFor(() => { - const element = document.querySelector('file-explorer-provider') + const element = document.querySelector('solid-ui-file-explorer-provider') expect(element).not.toBeNull() return element! }) expect(provider.subjectUri).toBe(`${window.location.origin}${url}`) - expect(provider.showHeader).toBe(true) + expect(provider.querySelector('solid-ui-file-explorer-header')).not.toBeNull() expect(window.history.state?.paneName).toBeUndefined() expect(window.location.pathname).toBe(url) }) diff --git a/test/unit/outline/manager.test.ts b/test/unit/outline/manager.test.ts index 4dbc3a53..84dfe876 100644 --- a/test/unit/outline/manager.test.ts +++ b/test/unit/outline/manager.test.ts @@ -1,5 +1,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { authn, store } from 'solid-logic' +import type { PaneDefinition } from 'pane-registry' +import type FileExplorerHeader from 'solid-ui/components/file-explorer-header' const { isInViewerPodMock } = vi.hoisted(() => ({ isInViewerPodMock: vi.fn() })) @@ -301,13 +303,13 @@ describe('manager', () => { manager.GotoSubject(container, true, pane, true, undefined, table, false) const provider = await vi.waitFor(() => { - const element = table.querySelector('file-explorer-provider') + const element = table.querySelector('solid-ui-file-explorer-provider') expect(element).not.toBeNull() return element! }) expect(provider.subjectUri).toBe(profile.uri) - expect(provider.showHeader).toBe(false) + expect(provider.querySelector('solid-ui-file-explorer-header')).toBeNull() expect(table).toHaveTextContent(`Mock Pane for ${profile.uri}`) expect(window.location.href).toBe(container.uri) }) @@ -315,9 +317,86 @@ describe('manager', () => { describe('file explorer header', () => { const container = sym(`${window.location.origin}/other/folder/file.ttl`) + const findPane = vi.fn() + + function createExplorer ( + pane: Omit & { icon: string | (() => string | Promise) } = MockPane, + options = {}, + subject = container + ) { + const manager = new OutlineManager({ dom: document, session: { paneRegistry: { list: [pane], byName: findPane }, store } }) + const outlineView = document.createElement('div') + outlineView.id = 'OutlineView' + document.body.appendChild(outlineView) + const provider = manager.propertyTable(subject, undefined, pane, options) + outlineView.appendChild(provider) + return provider + } + beforeEach(() => { isInViewerPodMock.mockReset() + findPane.mockReset() + }) + + it.each([ + `${window.location.origin}/`, + `${window.location.origin}/profile/card#me` + ])('does not add a resource header for %s', async uri => { + const provider = createExplorer(MockPane, {}, sym(uri)) + await vi.waitFor(() => expect(provider.querySelector('.paneDiv')).not.toBeNull()) + expect(provider.querySelector('solid-ui-file-explorer-header')).toBeNull() + expect(isInViewerPodMock).not.toHaveBeenCalled() + }) + + it('renders pane and header before menu icons resolve', async () => { + let resolveIcon!: (icon: string) => void + const icon = new Promise(resolve => { resolveIcon = resolve }) + const provider = createExplorer({ ...MockPane, icon: () => icon }) + await vi.waitFor(() => expect(provider.querySelector('.paneDiv')).not.toBeNull()) + const header = provider.querySelector('solid-ui-file-explorer-header')! + expect(header).not.toBeNull() + expect(header.menuItems).toEqual([]) + + resolveIcon(MockPane.icon) + await vi.waitFor(() => expect(header.menuItems).toHaveLength(1)) + }) + + it('reports menu failures without removing the rendered pane or header', async () => { + const error = new Error('Unable to resolve pane icon') + const status = document.createElement('div') + status.id = 'status' + document.body.appendChild(status) + const icon = vi.fn() + .mockReturnValueOnce(MockPane.icon) + .mockImplementationOnce(() => { throw error }) + const provider = createExplorer({ + ...MockPane, + icon + }) + await vi.waitFor(() => expect(status).toHaveTextContent('Unable to resolve pane icon')) + expect(provider.querySelector('.paneDiv')).not.toBeNull() + expect(provider.querySelector('solid-ui-file-explorer-header')).not.toBeNull() }) + + it('reuses resource visibility and menu items when switching panes', async () => { + isInViewerPodMock.mockResolvedValue(true) + const provider = createExplorer(MockPane, { headerOnlyInViewerPod: true }) + await vi.waitFor(() => { + expect(provider.querySelector('solid-ui-file-explorer-header')?.menuItems).toHaveLength(1) + }) + const header = provider.querySelector('solid-ui-file-explorer-header')! + const menuItems = header.menuItems + findPane.mockReturnValue({ ...MockPane, name: 'profile' }) + menuItems[0].action(new Event('click')) + expect(provider.querySelector('solid-ui-file-explorer-header')).toBeNull() + + findPane.mockReturnValue(MockPane) + menuItems[0].action(new Event('click')) + const restored = provider.querySelector('solid-ui-file-explorer-header')! + expect(restored.menuItems).toBe(menuItems) + expect(isInViewerPodMock).toHaveBeenCalledTimes(1) + }) + async function headerShown (showNavbar: boolean, pane: object = MockPane) { const manager = new OutlineManager({ dom: document, session: { paneRegistry: mockPaneRegistry, store } }) window.history.replaceState({}, '', container.uri) @@ -325,11 +404,11 @@ describe('manager', () => { document.body.appendChild(table) manager.GotoSubject(container, true, pane, true, undefined, table, showNavbar) const provider = await vi.waitFor(() => { - const element = table.querySelector('file-explorer-provider') + const element = table.querySelector('solid-ui-file-explorer-provider') expect(element).not.toBeNull() return element! }) - return provider.showHeader + return provider.querySelector('solid-ui-file-explorer-header') !== null } it('is hidden for account-menu views when the URL is not in the viewer\'s pod', async () => { From 799377dbb80834b57d14360bb315314ab85d202c Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sat, 17 Oct 2026 05:48:41 +1100 Subject: [PATCH 07/11] use listeners --- src/outline/fileExplorerHelpers.ts | 6 ++-- src/outline/manager.js | 50 ++++++++++++++++++------------ 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/outline/fileExplorerHelpers.ts b/src/outline/fileExplorerHelpers.ts index bb8f5e84..238f9cfa 100644 --- a/src/outline/fileExplorerHelpers.ts +++ b/src/outline/fileExplorerHelpers.ts @@ -24,9 +24,9 @@ export async function getPaneItems ( subject: NamedNode, context: DataBrowserContext, relevantPanes: ExplorerPane[], - openPane: ((subject: NamedNode, paneName: string) => void) | undefined + dispatchOpenPane: ((subject: NamedNode, paneName: string) => void) | undefined ) { - if (typeof openPane !== 'function') return [] + if (typeof dispatchOpenPane !== 'function') return [] const menuItems = await Promise.all(relevantPanes.map(async pane => { const label = pane.label(subject, context) @@ -47,7 +47,7 @@ export async function getPaneItems ( return { label, icon, - action: () => openPane(subject, pane.name) + action: () => dispatchOpenPane(subject, pane.name) } })) diff --git a/src/outline/manager.js b/src/outline/manager.js index d643f4cb..f87bb628 100644 --- a/src/outline/manager.js +++ b/src/outline/manager.js @@ -29,6 +29,7 @@ export default function (context) { const dom = context.dom const explorerHeaders = new WeakMap() const explorerHeaderGenerations = new WeakMap() + const explorerProviderListeners = new WeakSet() this.document = context.dom this.context = context @@ -349,12 +350,6 @@ export default function (context) { header.paneIcon = getPaneIcon(pane, subject, context) header.menuItems = headerState.menuItems if (!currentHeader) { - header.addEventListener(UI.FILE_EXPLORER_BACK_EVENT, () => provider.onBack?.()) - header.addEventListener(UI.FILE_EXPLORER_MANAGE_ACCESS_EVENT, () => { - provider.handleAccessClick().catch(error => { - UI.log.error('outline: Failed to open access controls: ' + UI.utils.stackString(error)) - }) - }) provider.prepend(header) } } @@ -376,7 +371,26 @@ export default function (context) { provider.context = context provider.subjectUri = subject.uri - provider.onBack = () => collapseMouseDownListener({ target: provider }) + if (!explorerProviderListeners.has(provider)) { + provider.addEventListener(UI.FILE_EXPLORER_BACK_EVENT, () => collapseMouseDownListener({ target: provider })) + provider.addEventListener(UI.FILE_EXPLORER_MANAGE_ACCESS_EVENT, event => { + solidLogicSingleton.acl.findAccessGrants($rdf.sym(event.detail.subjectUri)).then(accessGrants => { + UI.showDialog(AccessControlModal, { + props: { + subjectUri: event.detail.subjectUri, + accessGrants + } + }) + }).catch(error => { + UI.log.error('outline: Failed to open access controls: ' + UI.utils.stackString(error)) + }) + }) + provider.addEventListener(UI.FILE_EXPLORER_OPEN_PANE_EVENT, event => { + const pane = context.session.paneRegistry.byName(event.detail.paneName) + openPaneInPlace($rdf.sym(event.detail.subjectUri), pane) + }) + explorerProviderListeners.add(provider) + } const relevantPanes = options.hideList ? [] @@ -399,17 +413,6 @@ export default function (context) { explorerHeaders.set(provider, headerState) provider.paneRenderOptions = options provider.soloPane = options.solo - provider.openPane = (paneSubject, paneName) => openPaneInPlace(paneSubject, context.session.paneRegistry.byName(paneName)) - provider.handleAccessClick = async () => { - const accessGrants = await solidLogicSingleton.acl.findAccessGrants(subject) - - UI.showDialog(AccessControlModal, { - props: { - subjectUri: subject.uri, - accessGrants - } - }) - } if (provider.pane) { renderPaneIntoProvider(provider, subject, provider.pane, options) @@ -417,7 +420,16 @@ export default function (context) { renderExplorerHeader(provider, subject, provider.pane) if (showHeader) { - const menuItems = await getPaneItems(subject, context, relevantPanes, provider.openPane) + const menuItems = await getPaneItems(subject, context, relevantPanes, (paneSubject, paneName) => { + provider.dispatchEvent(new CustomEvent(UI.FILE_EXPLORER_OPEN_PANE_EVENT, { + bubbles: true, + composed: true, + detail: { + subjectUri: paneSubject.uri, + paneName + } + })) + }) if (explorerHeaderGenerations.get(provider) === generation && explorerHeaders.get(provider) === headerState) { headerState.menuItems = menuItems const header = provider.querySelector(':scope > solid-ui-file-explorer-header') From 1f0103a77c39fee02eac1970d5098ca710ef02b2 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sat, 17 Oct 2026 06:04:11 +1100 Subject: [PATCH 08/11] Simplify the file explorer work in manager.js Prompt: Use a const for the provider and simplify by using explorerStates for generation, menuitems and options. --- src/outline/manager.js | 51 ++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/outline/manager.js b/src/outline/manager.js index f87bb628..aa6c25a9 100644 --- a/src/outline/manager.js +++ b/src/outline/manager.js @@ -25,10 +25,11 @@ import 'solid-ui/components/file-explorer-provider' import 'solid-ui/components/file-explorer-header' import AccessControlModal from 'solid-ui/components/access-control-modal' +const EXPLORER_PROVIDER_TAG = 'solid-ui-file-explorer-provider' + export default function (context) { const dom = context.dom - const explorerHeaders = new WeakMap() - const explorerHeaderGenerations = new WeakMap() + const explorerStates = new WeakMap() const explorerProviderListeners = new WeakSet() this.document = context.dom @@ -322,9 +323,9 @@ export default function (context) { const subjectNode = outlineView.querySelector('[about="' + subjectId + '"]') if (!subjectNode) return - const provider = subjectNode.matches?.('solid-ui-file-explorer-provider') + const provider = subjectNode.matches?.(EXPLORER_PROVIDER_TAG) ? subjectNode - : subjectNode.querySelector('solid-ui-file-explorer-provider') + : subjectNode.querySelector(EXPLORER_PROVIDER_TAG) if (!provider) return if (!pane) { UI.log.warn('outline: Unable to open pane in place: pane is undefined.') @@ -334,21 +335,21 @@ export default function (context) { setQueryButtonVisibility(!!pane.requireQueryButton) provider.pane = pane renderExplorerHeader(provider, subject, pane) - renderPaneIntoProvider(provider, subject, pane, provider.paneRenderOptions) + renderPaneIntoProvider(provider, subject, pane, explorerStates.get(provider)?.options || {}) } function renderExplorerHeader (provider, subject, pane) { - const headerState = explorerHeaders.get(provider) + const state = explorerStates.get(provider) const currentHeader = provider.querySelector(':scope > solid-ui-file-explorer-header') - if (!headerState || !explorerHeaderForPane(headerState.showHeader, pane?.name)) { + if (!state || !explorerHeaderForPane(state.showHeader, pane?.name)) { currentHeader?.remove() return } const header = currentHeader || dom.createElement('solid-ui-file-explorer-header') header.paneIcon = getPaneIcon(pane, subject, context) - header.menuItems = headerState.menuItems + header.menuItems = state.menuItems if (!currentHeader) { provider.prepend(header) } @@ -357,9 +358,14 @@ export default function (context) { async function expandedProviderTR (subject, requiredPane, options, provider) { options = options || {} - provider = provider || dom.createElement('solid-ui-file-explorer-provider') - const generation = (explorerHeaderGenerations.get(provider) || 0) + 1 - explorerHeaderGenerations.set(provider, generation) + provider = provider || dom.createElement(EXPLORER_PROVIDER_TAG) + const previousState = explorerStates.get(provider) + const state = { + generation: (previousState?.generation || 0) + 1, + menuItems: [], + options + } + explorerStates.set(provider, state) provider.classList.add('paneView', 'tdFlex') provider.setAttribute('notSelectable', 'true') provider.setAttribute('role', 'grid') @@ -395,7 +401,7 @@ export default function (context) { const relevantPanes = options.hideList ? [] : await getRelevantPanes(subject, context) - if (explorerHeaderGenerations.get(provider) !== generation) return provider + if (explorerStates.get(provider) !== state) return provider provider.relevantPanes = relevantPanes provider.pane = requiredPane || getPane(relevantPanes, subject) @@ -408,11 +414,8 @@ export default function (context) { // Only worth looking up when the view does not show the page URL's own resource. inViewerPod: viewerPodOnly ? await isPageInViewerPod(dom.location.href) : true }) - if (explorerHeaderGenerations.get(provider) !== generation) return provider - const headerState = { showHeader, menuItems: [] } - explorerHeaders.set(provider, headerState) - provider.paneRenderOptions = options - provider.soloPane = options.solo + if (explorerStates.get(provider) !== state) return provider + state.showHeader = showHeader if (provider.pane) { renderPaneIntoProvider(provider, subject, provider.pane, options) @@ -430,8 +433,8 @@ export default function (context) { } })) }) - if (explorerHeaderGenerations.get(provider) === generation && explorerHeaders.get(provider) === headerState) { - headerState.menuItems = menuItems + if (explorerStates.get(provider) === state) { + state.menuItems = menuItems const header = provider.querySelector(':scope > solid-ui-file-explorer-header') if (header) header.menuItems = menuItems } @@ -484,10 +487,10 @@ export default function (context) { // if (!requiredPane) requiredPane = panes.defaultPane; const provider = table - ? table.matches?.('solid-ui-file-explorer-provider') + ? table.matches?.(EXPLORER_PROVIDER_TAG) ? table : table.firstElementChild || table - : dom.createElement('solid-ui-file-explorer-provider') + : dom.createElement(EXPLORER_PROVIDER_TAG) expandedProviderTR(subject, requiredPane, options, provider).catch(error => { UI.log.error('outline: Failed to set up file explorer: ' + UI.utils.stackString(error)) @@ -1140,9 +1143,9 @@ export default function (context) { break case 37: { // left if (this.walk('left')) return - const subjectNode = selectedTd.firstElementChild?.matches('solid-ui-file-explorer-provider') + const subjectNode = selectedTd.firstElementChild?.matches(EXPLORER_PROVIDER_TAG) ? selectedTd.firstElementChild - : selectedTd.querySelector('solid-ui-file-explorer-provider') + : selectedTd.querySelector(EXPLORER_PROVIDER_TAG) outlineCollapse(selectedTd, UI.utils.getAbout(kb, subjectNode)) break } @@ -1157,7 +1160,7 @@ export default function (context) { this.walk('right') return } - if (!selectedTd.firstElementChild?.matches('solid-ui-file-explorer-provider')) { + if (!selectedTd.firstElementChild?.matches(EXPLORER_PROVIDER_TAG)) { // not expanded sf.addCallback('done', setSelectedAfterward) sf.addCallback('fail', setSelectedAfterward) From 4c063ccc9a3354b35e93cca94829be84255b8dff Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sat, 17 Oct 2026 09:28:21 +1100 Subject: [PATCH 09/11] Use getRelevant panes from solid-ui Prompt: use the getrelevant panes function from solid-ui Co-authored-by: GPT-5.6 Luna --- src/outline/manager.js | 27 ++++++--------------------- test/unit/outline/manager.test.ts | 1 + 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/outline/manager.js b/src/outline/manager.js index aa6c25a9..906127f2 100644 --- a/src/outline/manager.js +++ b/src/outline/manager.js @@ -249,28 +249,13 @@ export default function (context) { } } - async function getRelevantPanes (subject, context) { + async function loadRelevantPanes (subject, context) { // make sure container representation is loaded (when server returns index.html) if (subject.uri.endsWith('/')) { await loadContainerRepresentation(subject) } - const panes = context.session.paneRegistry - const relevantPanes = panes.list.filter( - pane => pane.label(subject, context) && !pane.global && pane.name !== 'sharing' - ) - if (relevantPanes.length === 0) { - // there are no relevant panes, simply return default pane (which ironically is internalPane) - return [panes.byName('internal')] - } - const filteredPanes = await UI.login.filterAvailablePanes(relevantPanes) - if (filteredPanes.length === 0) { - // if no relevant panes are available panes because of user role, we still allow for the most relevant pane to be viewed - return [relevantPanes[0]] - } - const firstRelevantPaneIndex = panes.list.indexOf(relevantPanes[0]) - const firstFilteredPaneIndex = panes.list.indexOf(filteredPanes[0]) - // if the first relevant pane is loaded before the panes available wrt role, we still want to offer the most relevant pane - return firstRelevantPaneIndex < firstFilteredPaneIndex - ? [relevantPanes[0]].concat(filteredPanes) - : filteredPanes + return UI.getRelevantPanes(subject, context, { + excludedPaneNames: ['sharing'], + fallbackPaneName: 'internal' + }) } function getPane (relevantPanes, subject) { @@ -400,7 +385,7 @@ export default function (context) { const relevantPanes = options.hideList ? [] - : await getRelevantPanes(subject, context) + : await loadRelevantPanes(subject, context) if (explorerStates.get(provider) !== state) return provider provider.relevantPanes = relevantPanes diff --git a/test/unit/outline/manager.test.ts b/test/unit/outline/manager.test.ts index 84dfe876..7161f460 100644 --- a/test/unit/outline/manager.test.ts +++ b/test/unit/outline/manager.test.ts @@ -14,6 +14,7 @@ vi.mock('solid-ui', async () => { const actual = await vi.importActual('solid-ui') return { ...actual, + getRelevantPanes: async (_subject, context) => context.session.paneRegistry.list, login: { ...actual.login, filterAvailablePanes: async panes => panes From ee06ffee0b017e67a3198eb2b0f3faadd33ef5c2 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Mon, 19 Oct 2026 08:27:44 +1100 Subject: [PATCH 10/11] refactor: compose file explorers through shared solid-ui helper - replace manual explorer DOM composition with UI.composeExplorer - scope explorer events to their owning provider - preserve solo-pane state and async header menu updates - simplify in-place pane navigation and clarify storage-pane ownership Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/outline/manager.js | 73 ++++++++--------- test/unit/outline/manager.test.ts | 125 +++++++++++++++++------------- 2 files changed, 103 insertions(+), 95 deletions(-) diff --git a/src/outline/manager.js b/src/outline/manager.js index 906127f2..a433b4cc 100644 --- a/src/outline/manager.js +++ b/src/outline/manager.js @@ -291,27 +291,10 @@ export default function (context) { pre.appendChild(dom.createTextNode(UI.utils.stackString(e))) } - paneDiv.classList.add('paneDiv') - const existingPaneDiv = provider.querySelector('.paneDiv') - if (existingPaneDiv) { - existingPaneDiv.replaceWith(paneDiv) - } else { - provider.appendChild(paneDiv) - } + composeExplorer(provider, subject, pane, paneDiv) } - function openPaneInPlace (subject, pane) { - const outlineView = dom.getElementById('OutlineView') - if (!outlineView) return - - const subjectId = subject.toNT() - const subjectNode = outlineView.querySelector('[about="' + subjectId + '"]') - if (!subjectNode) return - - const provider = subjectNode.matches?.(EXPLORER_PROVIDER_TAG) - ? subjectNode - : subjectNode.querySelector(EXPLORER_PROVIDER_TAG) - if (!provider) return + function openPaneInPlace (provider, subject, pane) { if (!pane) { UI.log.warn('outline: Unable to open pane in place: pane is undefined.') return @@ -319,25 +302,21 @@ export default function (context) { setQueryButtonVisibility(!!pane.requireQueryButton) provider.pane = pane - renderExplorerHeader(provider, subject, pane) renderPaneIntoProvider(provider, subject, pane, explorerStates.get(provider)?.options || {}) } - function renderExplorerHeader (provider, subject, pane) { + function composeExplorer (provider, subject, pane, paneElement) { const state = explorerStates.get(provider) - const currentHeader = provider.querySelector(':scope > solid-ui-file-explorer-header') - - if (!state || !explorerHeaderForPane(state.showHeader, pane?.name)) { - currentHeader?.remove() - return - } - - const header = currentHeader || dom.createElement('solid-ui-file-explorer-header') - header.paneIcon = getPaneIcon(pane, subject, context) - header.menuItems = state.menuItems - if (!currentHeader) { - provider.prepend(header) - } + UI.composeExplorer(provider, { + context, + subjectUri: subject.uri, + pane, + relevantPanes: provider.relevantPanes || [], + header: state && explorerHeaderForPane(state.showHeader, pane?.name) + ? { paneIcon: getPaneIcon(pane, subject, context), menuItems: state.menuItems } + : undefined, + paneElement + }) } async function expandedProviderTR (subject, requiredPane, options, provider) { @@ -362,9 +341,19 @@ export default function (context) { provider.context = context provider.subjectUri = subject.uri + // Preserve legacy solo-pane signal (set via options.solo, see GotoSubject) on the + // file-explorer provider element. No current consumer reads FileExplorerProvider.soloPane + // (solid-ui/src/components/file-explorer-provider/FileExplorerProvider.ts), but the property + // is part of its public API, so keep populating it instead of silently dropping the value. + provider.soloPane = options.solo if (!explorerProviderListeners.has(provider)) { - provider.addEventListener(UI.FILE_EXPLORER_BACK_EVENT, () => collapseMouseDownListener({ target: provider })) + provider.addEventListener(UI.FILE_EXPLORER_BACK_EVENT, event => { + if (!UI.isExplorerEvent(event, provider)) return + event.stopPropagation() + collapseMouseDownListener({ target: provider }) + }) provider.addEventListener(UI.FILE_EXPLORER_MANAGE_ACCESS_EVENT, event => { + event.stopPropagation() solidLogicSingleton.acl.findAccessGrants($rdf.sym(event.detail.subjectUri)).then(accessGrants => { UI.showDialog(AccessControlModal, { props: { @@ -377,8 +366,10 @@ export default function (context) { }) }) provider.addEventListener(UI.FILE_EXPLORER_OPEN_PANE_EVENT, event => { + if (!UI.isExplorerEvent(event, provider)) return + event.stopPropagation() const pane = context.session.paneRegistry.byName(event.detail.paneName) - openPaneInPlace($rdf.sym(event.detail.subjectUri), pane) + openPaneInPlace(provider, $rdf.sym(event.detail.subjectUri), pane) }) explorerProviderListeners.add(provider) } @@ -404,8 +395,9 @@ export default function (context) { if (provider.pane) { renderPaneIntoProvider(provider, subject, provider.pane, options) + } else { + composeExplorer(provider, subject, provider.pane) } - renderExplorerHeader(provider, subject, provider.pane) if (showHeader) { const menuItems = await getPaneItems(subject, context, relevantPanes, (paneSubject, paneName) => { @@ -420,8 +412,7 @@ export default function (context) { }) if (explorerStates.get(provider) === state) { state.menuItems = menuItems - const header = provider.querySelector(':scope > solid-ui-file-explorer-header') - if (header) header.menuItems = menuItems + composeExplorer(provider, subject, provider.pane) } } return provider @@ -1749,8 +1740,8 @@ export default function (context) { @param expand -- flag -- open the subject rather than keep folded closed @param pane -- optional -- pane to be used for expanded display. When the pane has its own subject (the profile, friends or storage of a person), that subject is displayed - instead of `subject`, which only stays the page URL; for a storage the container - or resource `subject` points at is opened inside it + instead of `subject`, which only stays the page URL. A storage pane owns + resource selection inside its own content target. @param solo -- optional -- the window will be cleared out and only the subject displayed @param referer -- optional -- where did we hear about this from anyway? @param table -- option -- default is an HTML table element in which to put the outline. diff --git a/test/unit/outline/manager.test.ts b/test/unit/outline/manager.test.ts index 7161f460..0bf303aa 100644 --- a/test/unit/outline/manager.test.ts +++ b/test/unit/outline/manager.test.ts @@ -192,7 +192,7 @@ describe('manager', () => { manager.GotoSubject(container, false, pane, true, undefined, table, false) - expect(table.querySelector('td.obj')).toHaveAttribute('about', `<${profile.uri}>`) + expect(table.querySelector('.obj')).toHaveAttribute('about', `<${profile.uri}>`) expect(window.location.href).toBe(container.uri) expect(window.history.state).toMatchObject({ paneName: 'mock', paneUri: profile.uri, viaAccountMenu: true }) }) @@ -205,33 +205,26 @@ describe('manager', () => { const navbarTable = document.createElement('table') manager.GotoSubject(page, false, { ...MockPane, subject: storage }, true, undefined, navbarTable) - expect(navbarTable.querySelector('td.obj')).toHaveAttribute('about', `<${storage.uri}>`) + expect(navbarTable.querySelector('.obj')).toHaveAttribute('about', `<${storage.uri}>`) expect(window.location.href).toBe(page.uri) expect(window.history.state).toMatchObject({ paneName: 'mock', paneUri: storage.uri }) const homeTable = document.createElement('table') manager.GotoSubject(page, false, MockPane, true, undefined, homeTable, false) - expect(homeTable.querySelector('td.obj')).toHaveAttribute('about', `<${page.uri}>`) + expect(homeTable.querySelector('.obj')).toHaveAttribute('about', `<${page.uri}>`) }) - describe('opening the URL inside a storage', () => { + describe('hosting storage independently of the page URL', () => { const root = `${window.location.origin}/other/` - // A folder pane that lists known children as expandable rows, like the real one - function setup (children: Record) { + function setup () { const folderPane = { name: 'folder', icon: '', label: () => 'Contents', - render: (subject: { uri: string }, context: { getOutliner: (dom: Document) => OutlineManager, dom: Document }) => { + render: (subject: { uri: string }) => { const div = document.createElement('div') - const rows = document.createElement('table') - for (const child of children[subject.uri] ?? []) { - const tr = document.createElement('tr') - tr.appendChild(context.getOutliner(context.dom).outlineObjectTD(sym(child), undefined, undefined, undefined)) - rows.appendChild(tr) - } - div.appendChild(rows) + div.textContent = `Storage shell for ${subject.uri}` return div } } @@ -243,53 +236,28 @@ describe('manager', () => { return { manager, folderPane } } - async function opened (target: string, children: Record) { - const { manager, folderPane } = setup(children) + async function opened (target: string) { + const { manager, folderPane } = setup() window.history.replaceState({}, '', target) const table = document.createElement('table') document.body.appendChild(table) manager.GotoSubject(sym(target), true, { ...folderPane, subject: sym(root) }, true, undefined, table) + await vi.waitFor(() => expect(table).toHaveTextContent(`Storage shell for ${root}`)) return table } - const isExpanded = (table: HTMLElement, uri: string) => - table.querySelector(`td.obj[about="<${uri}>"]`)?.firstElementChild?.nodeName === 'TABLE' - - const tree = { - [root]: [`${root}a/`, `${root}z/`], - [`${root}a/`]: [`${root}a/b/`, `${root}a/other.ttl`], - [`${root}a/b/`]: [`${root}a/b/file.ttl`] - } - - it('expands each level down to a container in the URL, leaving siblings closed', async () => { - const table = await opened(`${root}a/b/`, tree) - - await vi.waitFor(() => { - expect(isExpanded(table, `${root}a/`)).toBe(true) - expect(isExpanded(table, `${root}a/b/`)).toBe(true) - }) - expect(isExpanded(table, `${root}z/`)).toBe(false) - expect(table.querySelector(`td.obj[about="<${root}a/other.ttl>"]`)).not.toBeNull() - expect(window.location.href).toBe(`${root}a/b/`) - }) - - it('opens the containers of a resource in the URL', async () => { - const table = await opened(`${root}a/b/file.ttl`, tree) - - await vi.waitFor(() => { - expect(isExpanded(table, `${root}a/`)).toBe(true) - expect(isExpanded(table, `${root}a/b/`)).toBe(true) - }) - }) - - it('opens nothing when the URL is the storage itself or outside it', async () => { - const atRoot = await opened(root, tree) - await new Promise(resolve => setTimeout(resolve, 50)) - expect(isExpanded(atRoot, `${root}a/`)).toBe(false) - - const outside = await opened(`${window.location.origin}/elsewhere/a/`, tree) - await new Promise(resolve => setTimeout(resolve, 50)) - expect(isExpanded(outside, `${root}a/`)).toBe(false) + it.each([ + `${root}a/b/`, + `${root}a/b/file.ttl`, + root, + `${window.location.origin}/elsewhere/a/` + ])('renders the storage subject while preserving page URL %s', async target => { + const table = await opened(target) + const provider = table.querySelector('solid-ui-file-explorer-provider') + expect(provider?.subjectUri).toBe(root) + expect(provider?.querySelector(':scope > .paneDiv')).toHaveTextContent(`Storage shell for ${root}`) + expect(window.location.href).toBe(target) + expect(window.history.state).toMatchObject({ paneName: 'folder', paneUri: root }) }) }) @@ -398,6 +366,55 @@ describe('manager', () => { expect(isInViewerPodMock).toHaveBeenCalledTimes(1) }) + it('does not switch the outer pane for an unhandled nested pane-switch event', async () => { + const provider = createExplorer() + await vi.waitFor(() => expect(provider.querySelector('.paneDiv')).not.toBeNull()) + const paneElement = provider.querySelector('.paneDiv')! + const nested = document.createElement('solid-ui-file-explorer-provider') + paneElement.appendChild(nested) + nested.dispatchEvent(new CustomEvent('solid-ui-file-explorer-open-pane', { + bubbles: true, + composed: true, + detail: { subjectUri: container.uri, paneName: 'profile' } + })) + + expect(findPane).not.toHaveBeenCalled() + expect(provider.querySelector(':scope > .paneDiv')).toBe(paneElement) + }) + + it('switches the originating provider when the same subject appears twice', async () => { + const first = createExplorer() + const second = createExplorer() + await vi.waitFor(() => { + expect(first.querySelector('.paneDiv')).not.toBeNull() + expect(second.querySelector('solid-ui-file-explorer-header')?.menuItems).toHaveLength(1) + }) + const firstContent = first.querySelector('.paneDiv') + const secondContent = second.querySelector('.paneDiv') + findPane.mockReturnValue({ ...MockPane, name: 'other' }) + second.querySelector('solid-ui-file-explorer-header')!.menuItems[0].action(new Event('click')) + + expect(first.querySelector('.paneDiv')).toBe(firstContent) + expect(second.querySelector('.paneDiv')).not.toBe(secondContent) + expect(second.pane.name).toBe('other') + }) + + it('does not collapse the outer explorer for an unhandled nested Back event', async () => { + const provider = createExplorer() + await vi.waitFor(() => expect(provider.querySelector('.paneDiv')).not.toBeNull()) + const nested = document.createElement('solid-ui-file-explorer-provider') + provider.querySelector('.paneDiv')!.appendChild(nested) + const parent = provider.parentNode + nested.dispatchEvent(new CustomEvent('solid-ui-file-explorer-back', { + bubbles: true, + composed: true, + detail: { subjectUri: container.uri } + })) + + expect(provider.parentNode).toBe(parent) + expect(provider.contains(nested)).toBe(true) + }) + async function headerShown (showNavbar: boolean, pane: object = MockPane) { const manager = new OutlineManager({ dom: document, session: { paneRegistry: mockPaneRegistry, store } }) window.history.replaceState({}, '', container.uri) From c0807c53e5e3ba8cb57a806a979bfcc602fb9d28 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Oct 2026 21:33:13 +0000 Subject: [PATCH 11/11] chore: update solidos dependencies (dev: solid-logic@6.0.1-1 pane-registry@5.0.2-0 profile-pane@4.1.0-0) (latest: rdflib@2.4.1 solid-ui@5.0.1 activitystreams-pane@2.0.0 chat-pane@4.0.0 contacts-pane@4.0.0 folder-pane@4.0.0 issue-pane@4.0.0 meeting-pane@4.0.0 source-pane@4.0.0) --- package-lock.json | 146 +++++++++++++++++++++++----------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71ca8ef3..174ff415 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2338,9 +2338,9 @@ } }, "node_modules/@digitalbazaar/http-client/node_modules/undici": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", - "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", + "version": "6.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz", + "integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==", "license": "MIT", "engines": { "node": ">=18.17" @@ -3885,14 +3885,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", - "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.12.tgz", + "integrity": "sha512-6S7/iVknKglLAxMFxQGNppBI0iqJS2NgM6FBxavitj7KaDq/vywOWGf6dGu7F2L1Qy0G6S3uTp7aGshHrzK8JA==", "devOptional": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.11", + "@vitest/utils": "4.1.12", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -3906,8 +3906,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.11", - "vitest": "4.1.11" + "@vitest/browser": "4.1.12", + "vitest": "4.1.12" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -3916,15 +3916,15 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", - "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.12.tgz", + "integrity": "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", + "@vitest/spy": "4.1.12", + "@vitest/utils": "4.1.12", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -3933,12 +3933,12 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", - "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.12.tgz", + "integrity": "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==", "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.11", + "@vitest/spy": "4.1.12", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3959,9 +3959,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", - "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.12.tgz", + "integrity": "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==", "license": "MIT", "dependencies": { "tinyrainbow": "^3.1.0" @@ -3971,12 +3971,12 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", - "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.12.tgz", + "integrity": "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==", "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.11", + "@vitest/utils": "4.1.12", "pathe": "^2.0.3" }, "funding": { @@ -3984,13 +3984,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", - "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.12.tgz", + "integrity": "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==", "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.11", - "@vitest/utils": "4.1.11", + "@vitest/pretty-format": "4.1.12", + "@vitest/utils": "4.1.12", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3999,21 +3999,21 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", - "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz", + "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==", "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", - "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.12.tgz", + "integrity": "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==", "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.11", + "@vitest/pretty-format": "4.1.12", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -4488,9 +4488,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.28", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.28.tgz", - "integrity": "sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==", + "version": "2.11.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.29.tgz", + "integrity": "sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5199,9 +5199,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.456", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.456.tgz", - "integrity": "sha512-8hboqI6xesozteFb8aa7OGknvxUtTeRJLz0FoePysUEh/oWbddykIGdgpEJRbPRg/nIMfBf2DFos+H2MtvJl2A==", + "version": "1.5.459", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.459.tgz", + "integrity": "sha512-4SC9HHSFjO/liAhifpdOL5hG9b4BP7/5G3y3UPZVO8trlAXr1CaUEEJL0gWK3kOcYcZrOA5rkwG45Gra/lTIgg==", "dev": true, "license": "ISC" }, @@ -7901,9 +7901,9 @@ } }, "node_modules/lucide": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/lucide/-/lucide-1.57.0.tgz", - "integrity": "sha512-M2BHuzBxnD+5AiJzEXwWxswwz0MXoQPQcehoIJAgi0Lqe0i4/kksKaHx2R+UWRAbQhjTvucCDG6sBg/TqIwa8Q==", + "version": "1.59.0", + "resolved": "https://registry.npmjs.org/lucide/-/lucide-1.59.0.tgz", + "integrity": "sha512-0KTOAFYlTuLHdJOqnRCuPhscU29xtx/QnlHyrOfK0FynDhuPm50O3BH/e4wRnOS67ph0z7uovveCVcXUPLTPBw==", "license": "ISC" }, "node_modules/lz-string": { @@ -8363,9 +8363,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.58", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz", - "integrity": "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==", + "version": "2.0.59", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.59.tgz", + "integrity": "sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==", "dev": true, "license": "MIT", "engines": { @@ -8773,9 +8773,9 @@ } }, "node_modules/postcss": { - "version": "8.5.30", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", - "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", "funding": [ { "type": "opencollective", @@ -10339,9 +10339,9 @@ } }, "node_modules/undici": { - "version": "7.30.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", - "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "version": "7.30.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz", + "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==", "devOptional": true, "license": "MIT", "engines": { @@ -10688,18 +10688,18 @@ } }, "node_modules/vitest": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", - "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.11", - "@vitest/mocker": "4.1.11", - "@vitest/pretty-format": "4.1.11", - "@vitest/runner": "4.1.11", - "@vitest/snapshot": "4.1.11", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.12.tgz", + "integrity": "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==", + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.12", + "@vitest/mocker": "4.1.12", + "@vitest/pretty-format": "4.1.12", + "@vitest/runner": "4.1.12", + "@vitest/snapshot": "4.1.12", + "@vitest/spy": "4.1.12", + "@vitest/utils": "4.1.12", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -10727,12 +10727,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.11", - "@vitest/browser-preview": "4.1.11", - "@vitest/browser-webdriverio": "4.1.11", - "@vitest/coverage-istanbul": "4.1.11", - "@vitest/coverage-v8": "4.1.11", - "@vitest/ui": "4.1.11", + "@vitest/browser-playwright": "4.1.12", + "@vitest/browser-preview": "4.1.12", + "@vitest/browser-webdriverio": "^4.1.11", + "@vitest/coverage-istanbul": "4.1.12", + "@vitest/coverage-v8": "4.1.12", + "@vitest/ui": "4.1.12", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"