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

Filter by extension

Filter by extension


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

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"rdflib": "2.4.1",
"solid-logic": "6.0.1-5",
"solid-namespace": "^0.5.4",
"solid-ui": "5.0.2-4",
"solid-ui": "5.0.2-5",
"source-pane": "4.0.1-0"
},
"overrides": {
Expand Down
25 changes: 24 additions & 1 deletion src/mainPage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ import { HUMAN_READABLE_PREVIEW_FRAME_NAME } from '../humanReadablePane'
// Symbol used to stash the last render-relevant env snapshot on the outliner
// so refreshUI can skip a full GotoSubject re-render when nothing changed.
const LAST_RENDER_ENV_KEY = '__lastRenderEnvSignature'
let activeExplorerRefreshPending = false

function renderEnvSignature (env?: RenderEnvironment): string {
if (!env) return ''
return [env.layout, env.theme, env.inputMode].join('|')
return [
env.layout,
env.theme,
env.inputMode,
env.viewport?.width,
env.viewport?.height
].join('|')
}

function ensureMainContent () {
Expand Down Expand Up @@ -91,6 +98,22 @@ export async function refreshUI (outliner: OutlineManager) {
const previousSignature = (outliner as any)?.[LAST_RENDER_ENV_KEY] ?? ''
const envChanged = currentSignature !== previousSignature

// Folder explorers adapt through CSS. Recreating one from the page's
// history entry discards its internal resource/pane selection on resize.
const themeUnchanged = currentSignature.split('|')[1] === previousSignature.split('|')[1]
if (envChanged && themeUnchanged && document.querySelector('#MainContent folder-pane-view')) {
if (activeExplorerRefreshPending) return
activeExplorerRefreshPending = true
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()))
try {
;(outliner as OutlineManager & { refreshActiveExplorerPanes?: () => void }).refreshActiveExplorerPanes?.()
;(outliner as any)[LAST_RENDER_ENV_KEY] = currentSignature
} finally {
activeExplorerRefreshPending = false
}
return
Comment thread
SharonStrats marked this conversation as resolved.
Resolved
}

if (envChanged && store && typeof outliner?.GotoSubject === 'function') {
// Re-render what the current history entry displays without changing the
// URL or losing that it came from the account menu.
Expand Down
32 changes: 32 additions & 0 deletions src/outline/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@

// Represent an object in summary form as a content block

function appendRemoveIcon (node, subject, removeNode) {

Check warning on line 130 in src/outline/manager.js

View workflow job for this annotation

GitHub Actions / build (24)

'appendRemoveIcon' is defined but never used

Check warning on line 130 in src/outline/manager.js

View workflow job for this annotation

GitHub Actions / build (22)

'appendRemoveIcon' is defined but never used
const image = UI.utils.AJARImage(
outlineIcons.src.icon_remove_node,
'remove',
Expand Down Expand Up @@ -300,6 +300,38 @@
renderPaneIntoProvider(provider, subject, pane, explorerStates.get(provider)?.options || {})
}

this.refreshActiveExplorerPanes = function () {
const providers = Array.from(dom.querySelectorAll(EXPLORER_PROVIDER_TAG))
for (const provider of providers) {
const subjectUri = provider.subjectUri
const pane = provider.pane
if (!subjectUri || !pane) continue

const currentPane = provider.querySelector(':scope > .paneDiv')
if (!currentPane) continue
// Re-rendering the folder shell recreates its view and loses the resource
// selected inside it. Nested providers can refresh their active resource
// pane without replacing the shell.
if (currentPane.querySelector(':scope > folder-pane-view')) continue

let paneElement
try {
paneElement = pane.render(
$rdf.sym(subjectUri),
context,
provider.paneRenderOptions || explorerStates.get(provider)?.options || {}
)
} catch (error) {
UI.log.error('outline: Failed to refresh pane layout: ' + UI.utils.stackString(error))
continue
}

paneElement.classList.add('paneDiv')
currentPane.replaceWith(paneElement)
renderedPanes.set(provider, { pane, subjectUri })
}
}

function composeExplorer (provider, subject, pane, paneElement) {
const state = explorerStates.get(provider)
UI.composeExplorer(provider, {
Expand Down
13 changes: 13 additions & 0 deletions test/unit/mainPage/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,19 @@ describe('browser navigation without a reload', () => {
expect(GotoSubject).toHaveBeenCalledOnce()
})

it('refreshes when the viewport changes within the same layout', async () => {
const GotoSubject = await setupLoaded()
const outliner = getOutliner()
outliner.context = {
session: { store },
environment: { ...environment, viewport: { width: 1279, height: 800 } }
}

await refreshUI(outliner)

expect(GotoSubject).toHaveBeenCalledOnce()
})

it('falls back to popstate when the Navigation API is unavailable', async () => {
Reflect.deleteProperty(window, 'navigation')
vi.resetModules()
Expand Down
57 changes: 57 additions & 0 deletions test/unit/outline/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,63 @@ describe('manager', () => {
expect(provider.querySelector('.paneDiv')).not.toBe(paneElement)
})

it('re-renders the selected pane when the layout changes without replacing its explorer', async () => {
const subject = sym(`${window.location.origin}/layout-dependent.ttl`)
const pane = {
...MockPane,
render: (_subject: NamedNode, context: { environment?: { layout?: string } }) => {
const element = document.createElement('div')
element.className = 'layout-dependent-pane'
element.dataset.layout = context.environment?.layout ?? 'desktop'
return element
}
}
const context = {
dom: document,
session: { paneRegistry: { list: [pane], byName: findPane }, store },
environment: { layout: 'desktop' }
}
const manager = new OutlineManager(context)
const outlineView = document.createElement('div')
outlineView.id = 'OutlineView'
document.body.appendChild(outlineView)
const provider = manager.propertyTable(subject, undefined, pane, {})
outlineView.appendChild(provider)

await vi.waitFor(() => expect(provider.querySelector('.layout-dependent-pane')).not.toBeNull())
expect(provider.querySelector<HTMLElement>('.layout-dependent-pane')?.dataset.layout).toBe('desktop')

context.environment.layout = 'mobile'
manager.refreshActiveExplorerPanes()

await vi.waitFor(() => expect(provider.querySelector<HTMLElement>('.layout-dependent-pane')?.dataset.layout).toBe('mobile'))
expect(provider.subjectUri).toBe(subject.uri)
expect(provider.pane).toBe(pane)
})

it('does not re-render the folder shell while refreshing a nested resource pane', async () => {
const folderPane = { ...MockPane, name: 'folder', render: vi.fn(MockPane.render) }
const subject = sym(`${window.location.origin}/storage/`)
const manager = new OutlineManager({
dom: document,
session: { paneRegistry: { list: [folderPane], byName: findPane }, store },
environment: { layout: 'desktop' }
})
const provider = manager.propertyTable(subject, undefined, folderPane, {})
document.body.appendChild(provider)
await vi.waitFor(() => expect(provider.querySelector('.paneDiv')).not.toBeNull())

const folderShell = document.createElement('folder-pane-view')
provider.querySelector('.paneDiv')?.appendChild(folderShell)
folderPane.render.mockClear()

manager.context.environment.layout = 'mobile'
manager.refreshActiveExplorerPanes()

expect(folderPane.render).not.toHaveBeenCalled()
expect(provider.querySelector('folder-pane-view')).toBe(folderShell)
})

it('does not add a resource header for index.ttl#this', async () => {
const provider = createExplorer(MockPane, { solo: true }, sym(`${window.location.origin}/addressbook/index.ttl#this`))
await vi.waitFor(() => expect(provider.querySelector('.paneDiv')).not.toBeNull())
Expand Down
Loading