diff --git a/package-lock.json b/package-lock.json index 473e4d1..d06bc19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "lit": "^3.3.3", "rdflib": "2.4.1", - "solid-logic": "6.0.1-4", + "solid-logic": "6.0.1-5", "solid-ui": "5.0.2-4", "vite": "^8.0.16", "vitest": "^4.0.18", @@ -3255,16 +3255,16 @@ "license": "MIT" }, "node_modules/@typescript-eslint/parser": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.0.tgz", - "integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz", + "integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.72.0", - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/typescript-estree": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", "debug": "^4.4.3" }, "engines": { @@ -3280,14 +3280,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz", - "integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz", + "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.72.0", - "@typescript-eslint/types": "^8.72.0", + "@typescript-eslint/tsconfig-utils": "^8.72.1", + "@typescript-eslint/types": "^8.72.1", "debug": "^4.4.3" }, "engines": { @@ -3302,14 +3302,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz", - "integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz", + "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0" + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3320,9 +3320,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz", - "integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz", + "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==", "dev": true, "license": "MIT", "engines": { @@ -3337,9 +3337,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz", - "integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz", + "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==", "dev": true, "license": "MIT", "engines": { @@ -3351,16 +3351,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz", - "integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz", + "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.72.0", - "@typescript-eslint/tsconfig-utils": "8.72.0", - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0", + "@typescript-eslint/project-service": "8.72.1", + "@typescript-eslint/tsconfig-utils": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -3418,13 +3418,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz", - "integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz", + "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/types": "8.72.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3884,9 +3884,9 @@ } }, "node_modules/browserslist": { - "version": "4.29.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", - "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", "dev": true, "funding": [ { @@ -3904,10 +3904,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.29", - "caniuse-lite": "^1.0.30001816", - "electron-to-chromium": "^1.5.459", - "node-releases": "^2.0.59", + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", "update-browserslist-db": "^1.3.4" }, "bin": { @@ -4156,9 +4156,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.464", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz", - "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==", + "version": "1.5.465", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.465.tgz", + "integrity": "sha512-zg4iUpbeGw7YNZsswc2xKr2uiBex2d8dELAXo92Icw3but1ULbrvK8USe6wVLaWjgJnKBARv0pchx9Owktx6wQ==", "dev": true, "license": "ISC" }, @@ -5695,6 +5695,22 @@ "solid-logic": "6.0.1-4" } }, + "node_modules/pane-registry/node_modules/solid-logic": { + "version": "6.0.1-4", + "resolved": "https://registry.npmjs.org/solid-logic/-/solid-logic-6.0.1-4.tgz", + "integrity": "sha512-EVvS+5g91aUoONqGtM1vPnMimtwH6c/72td5b4qBRcudDjeRU/hycPg8p5abmx1YWZseLA1EFK76+E/t5bcPFw==", + "license": "MIT", + "dependencies": { + "@dokieli/web-access-control": "^1.1.0", + "@uvdsl/solid-oidc-client-browser": "^0.2.3", + "solid-namespace": "^0.5.4", + "vite": "^8.0.16", + "vitest": "^4.1.9" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5791,9 +5807,9 @@ } }, "node_modules/postcss": { - "version": "8.5.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", - "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", "funding": [ { "type": "opencollective", @@ -6180,9 +6196,9 @@ "license": "ISC" }, "node_modules/solid-logic": { - "version": "6.0.1-4", - "resolved": "https://registry.npmjs.org/solid-logic/-/solid-logic-6.0.1-4.tgz", - "integrity": "sha512-EVvS+5g91aUoONqGtM1vPnMimtwH6c/72td5b4qBRcudDjeRU/hycPg8p5abmx1YWZseLA1EFK76+E/t5bcPFw==", + "version": "6.0.1-5", + "resolved": "https://registry.npmjs.org/solid-logic/-/solid-logic-6.0.1-5.tgz", + "integrity": "sha512-U6IBElhpCpNkNrJq4TujBT7VMfl8xyY0U2PiuyltI5SiORhMAtLMHOqO6liSH5PO50y7UjAOMP1G8VbLjnYIOw==", "license": "MIT", "dependencies": { "@dokieli/web-access-control": "^1.1.0", @@ -6237,6 +6253,22 @@ "fsevents": "*" } }, + "node_modules/solid-ui/node_modules/solid-logic": { + "version": "6.0.1-4", + "resolved": "https://registry.npmjs.org/solid-logic/-/solid-logic-6.0.1-4.tgz", + "integrity": "sha512-EVvS+5g91aUoONqGtM1vPnMimtwH6c/72td5b4qBRcudDjeRU/hycPg8p5abmx1YWZseLA1EFK76+E/t5bcPFw==", + "license": "MIT", + "dependencies": { + "@dokieli/web-access-control": "^1.1.0", + "@uvdsl/solid-oidc-client-browser": "^0.2.3", + "solid-namespace": "^0.5.4", + "vite": "^8.0.16", + "vitest": "^4.1.9" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/solidos-toolkit": { "version": "1.0.0-dev.2fb6be508170f3b28ca79d738aae71cd48f6d87e", "resolved": "https://registry.npmjs.org/solidos-toolkit/-/solidos-toolkit-1.0.0-dev.2fb6be508170f3b28ca79d738aae71cd48f6d87e.tgz", diff --git a/package.json b/package.json index b65b6f5..185071e 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "dependencies": { "lit": "^3.3.3", "rdflib": "2.4.1", - "solid-logic": "6.0.1-4", + "solid-logic": "6.0.1-5", "solid-ui": "5.0.2-4", "vite": "^8.0.16", "vitest": "^4.0.18", diff --git a/src/components/folder-pane-container/FolderPaneContainer.ts b/src/components/folder-pane-container/FolderPaneContainer.ts index 1831a52..4d9200e 100644 --- a/src/components/folder-pane-container/FolderPaneContainer.ts +++ b/src/components/folder-pane-container/FolderPaneContainer.ts @@ -1,27 +1,40 @@ import { html, nothing } from 'lit' import { property } from 'lit/decorators.js' import { state } from 'lit/decorators.js' -import type { DataBrowserContext } from 'pane-registry' -import type { ResourceMetadata } from 'solid-logic' +import type { DataBrowserContext, PaneDefinition } from 'pane-registry' import type { NamedNode } from 'rdflib' -import { customElement, FILE_EXPLORER_MANAGE_ACCESS_EVENT, getRelevantPanes, utils, WebComponent } from 'solid-ui' +import type { ResourceMetadata } from 'solid-logic' +import { customElement, FILE_EXPLORER_MANAGE_ACCESS_EVENT, getPreferredPane, getRelevantPanes, utils, WebComponent } from 'solid-ui' import type { Resource } from '../../types' -import { getContainerMemberCountLabel, getResourceAccessState, getResourcesForContainer, type FolderPaneResourceLogic } from '../../helpers' +import { + getContainerMemberCountLabel, + getIndexedApplicationSubject, + getResourceAccessState, + getResourcesForContainer, + type FolderPaneResourceLogic +} from '../../helpers' import { createRequestQueue } from '../../requestQueue' +import * as debug from '../../debug' import 'solid-ui/components/resource-actions-menu' import '~icons/lucide/file' import '~icons/lucide/file-box' import '~icons/lucide/folder' -import styles from './FolderPaneContainer.styles.css' import '~icons/lucide/globe' import '~icons/lucide/lock-keyhole' +import styles from './FolderPaneContainer.styles.css' type ResourceActionMenuItem = { + key?: string label: string icon?: string action: (event: Event) => void } +type ResourceAccess = { + state: 'public' | 'private' | 'unknown' | 'error' + canManageAccess: boolean +} + @customElement('folder-pane-container') export default class FolderPaneContainer extends WebComponent { static styles = styles @@ -53,10 +66,8 @@ export default class FolderPaneContainer extends WebComponent { private accessor optimisticResources: Resource[] = [] @state() - private accessor resourceAccess = new Map() + private accessor resourceAccess = new Map() + @state() private accessor memberLoad = new Map() @@ -65,11 +76,9 @@ export default class FolderPaneContainer extends WebComponent { @state() private accessor loadError: string | undefined = undefined - private actionMenuGeneration = 0 + private resourceActionMenuLoads = new Map() private resourceGeneration = 0 private updateWorkScheduled = false - // Member counts and access indicators cost one request per child, so they - // are fetched only for visible items, a few at a time. private detailsQueue = createRequestQueue() private detailsObserver: IntersectionObserver | undefined @@ -138,14 +147,14 @@ export default class FolderPaneContainer extends WebComponent { private async loadResources () { const generation = ++this.resourceGeneration this.resetResourceDetails() + this.resourceActionMenuItems = new Map() + this.resourceActionMenuLoads = new Map() const resources = this.getResourcesFromStore() this.resources = resources this.loading = false this.loadError = undefined if (generation !== this.resourceGeneration) return - // Member counts and access are advisory: they load as items scroll into view. - void this.loadResourceActionMenuItems(generation) } private loadResourceDetails (resource: Resource) { @@ -163,7 +172,7 @@ export default class FolderPaneContainer extends WebComponent { this.memberLoad = new Map(this.memberLoad).set(resource.id, 'loaded') } catch (error) { if (generation !== this.resourceGeneration) return - console.warn('Unable to load folder-pane container member count', error) + debug.warn('Unable to load folder-pane container member count', error) this.memberLoad = new Map(this.memberLoad).set(resource.id, 'error') } } @@ -187,20 +196,6 @@ export default class FolderPaneContainer extends WebComponent { } } - private renderAccessIndicator (resource: Resource) { - const access = this.resourceAccess.get(resource.id) - if (!access) return nothing - // The icon element has no ARIA role of its own, so `title` alone is not - // exposed to assistive technology (WCAG 1.1.1 Non-text Content). Giving it - // an explicit img role and label ensures the public/private state is - // announced, not just shown as a mouse-hover tooltip. - return access.state === 'public' - ? html`` - : access.state === 'private' - ? html`` - : nothing - } - private resetResourceDetails () { this.detailsQueue.clear() this.detailsObserver?.disconnect() @@ -236,13 +231,13 @@ export default class FolderPaneContainer extends WebComponent { connectedCallback () { super.connectedCallback() - // Re-observe items after a reconnect; details were dropped on disconnect. if (this.hasUpdated) this.requestUpdate() } disconnectedCallback () { super.disconnectedCallback() this.resourceGeneration++ + this.resourceActionMenuLoads = new Map() this.resetResourceDetails() } @@ -250,6 +245,8 @@ export default class FolderPaneContainer extends WebComponent { this.resourceGeneration++ this.resetResourceDetails() this.resourceActionMenuItems = new Map() + this.resourceActionMenuLoads = new Map() + this.resourceAccess = new Map() this.optimisticResources = [] if (reloadFromServer && this.subject && this.browserContext) { try { @@ -260,54 +257,13 @@ export default class FolderPaneContainer extends WebComponent { } catch (error) { await this.loadResources() this.loadError = 'The folder could not be refreshed. Showing the last known listing.' - console.warn('Unable to refresh folder-pane container', error) + debug.warn('Unable to refresh folder-pane container', error) return } } await this.loadResources() } - private async loadResourceActionMenuItems (resourceGeneration = this.resourceGeneration) { - const actionGeneration = ++this.actionMenuGeneration - if (!this.browserContext) { - this.resourceActionMenuItems = new Map() - return - } - - const menuItems = await Promise.all(this.resources.map(async resource => { - const panes = await getRelevantPanes(resource.subject, this.browserContext!, { - excludedPaneNames: ['sharing'], - fallbackPaneName: 'internal' - }) - const items = await Promise.all(panes.map(async pane => { - const label = pane.label(resource.subject, this.browserContext!) || pane.name - const paneWithIcon = pane as Omit & { - icon?: string | ((subject: NamedNode, context: DataBrowserContext) => string | undefined | Promise) - } - const iconValue = paneWithIcon.icon - const iconSource = typeof iconValue === 'function' - ? await iconValue(resource.subject, this.browserContext!) - : iconValue - return { - label, - icon: iconSource, - action: () => { - this.dispatchEvent(new CustomEvent('resource-open-pane', { - detail: { resource: resource.subject, paneName: pane.name }, - bubbles: true, - composed: true - })) - } - } - })) - return [resource.id, items] as const - })) - - if (resourceGeneration === this.resourceGeneration && actionGeneration === this.actionMenuGeneration) { - this.resourceActionMenuItems = new Map(menuItems) - } - } - protected updated (changedProperties: Map) { if ( changedProperties.has('subject') || @@ -326,8 +282,8 @@ export default class FolderPaneContainer extends WebComponent { void this.loadResources() this.observeResourceItems() }) + return } - return } this.observeResourceItems() } @@ -340,6 +296,137 @@ export default class FolderPaneContainer extends WebComponent { })) } + private setResourceActionMenuItems (resource: Resource, items: ResourceActionMenuItem[]) { + this.resourceActionMenuItems = new Map(this.resourceActionMenuItems).set(resource.id, items) + } + + private renderAccessIndicator (resource: Resource) { + const access = this.resourceAccess.get(resource.id) + return access?.state === 'public' + ? html`` + : access?.state === 'private' + ? html`` + : nothing + } + + private openResourcePane (resource: Resource, paneName: string) { + this.dispatchEvent(new CustomEvent('resource-open-pane', { + detail: { resource: resource.subject, paneName }, + bubbles: true, + composed: true + })) + } + + private async getPaneMenuItem ( + pane: PaneDefinition, + subject: NamedNode, + resource: Resource, + label?: string + ): Promise { + const paneWithIcon = pane as Omit & { + icon?: string | ((subject: NamedNode, context: DataBrowserContext) => string | undefined | Promise) + } + const iconValue = paneWithIcon.icon + return { + key: pane.name, + label: label || pane.label(subject, this.browserContext!) || pane.name, + icon: typeof iconValue === 'function' + ? await iconValue(subject, this.browserContext!) + : iconValue, + action: (event) => { + event.stopPropagation() + this.openResourcePane(resource, pane.name) + } + } + } + + private async getContentsMenuItem (resource: Resource): Promise { + if (!resource.isContainer || !this.browserContext) return undefined + const folderPane = this.browserContext.session.paneRegistry.byName('folder') + return folderPane + ? this.getPaneMenuItem(folderPane, resource.subject, resource, 'Contents') + : undefined + } + + private async loadResourceActionMenuItems (resource: Resource) { + if (!this.browserContext) return + const previousLoad = this.resourceActionMenuLoads.get(resource.id) + if (previousLoad === 'loading' || previousLoad === 'loaded') return + + const resourceGeneration = this.resourceGeneration + this.resourceActionMenuLoads.set(resource.id, 'loading') + const accessMetadata = this.browserContext.session.logic.resource.fetchMetadata(resource.subject) + .then((metadata: ResourceMetadata) => metadata.access) + .catch(error => { + debug.warn('Unable to load folder-pane access metadata for resource actions', error) + return undefined + }) + const contents = await this.getContentsMenuItem(resource) + if (resourceGeneration !== this.resourceGeneration) return + if (contents) this.setResourceActionMenuItems(resource, [contents]) + + try { + try { + await this.browserContext.session.store.fetcher.load(resource.subject) + } catch (error) { + debug.warn('Unable to preload resource for folder-pane action menu', error) + } + if (resourceGeneration !== this.resourceGeneration) return + + let paneSubject = resource.subject + if (resource.isContainer && this.resourceLogic?.containerHasIndexDocument(resource.subject)) { + paneSubject = this.browserContext.session.store.sym(getIndexedApplicationSubject(resource.subject)) + try { + await this.browserContext.session.store.fetcher.load(paneSubject.doc()) + } catch (error) { + debug.warn('Unable to preload index document for folder-pane action menu', error) + } + if (resourceGeneration !== this.resourceGeneration) return + } + + const panes = await getRelevantPanes(paneSubject, this.browserContext, { + excludedPaneNames: ['sharing'], + fallbackPaneName: 'internal' + }) + if (resourceGeneration !== this.resourceGeneration) return + + const preferredPane = getPreferredPane(paneSubject, this.browserContext, panes) + const nonFolderPanes = panes.filter(pane => pane.name !== 'folder') + const orderedPanes = preferredPane && preferredPane.name !== 'folder' + ? [preferredPane, ...nonFolderPanes.filter(pane => pane !== preferredPane)] + : nonFolderPanes + const items = await Promise.all(orderedPanes.map(pane => + this.getPaneMenuItem(pane, paneSubject, resource) + )) + if (resourceGeneration !== this.resourceGeneration) return + + if (contents) items.splice(1, 0, contents) + this.setResourceActionMenuItems(resource, items) + this.resourceActionMenuLoads.set(resource.id, 'loaded') + } catch (error) { + if (resourceGeneration !== this.resourceGeneration) return + this.resourceActionMenuLoads.set(resource.id, 'error') + debug.warn('Unable to load resource panes for folder-pane action menu', error) + } finally { + const access = await accessMetadata + if (resourceGeneration === this.resourceGeneration && access) { + this.resourceAccess = new Map(this.resourceAccess).set(resource.id, { + state: getResourceAccessState(access), + canManageAccess: access.canManageAccess + }) + } else if (resourceGeneration === this.resourceGeneration) { + this.resourceAccess = new Map(this.resourceAccess).set(resource.id, { + state: 'error', + canManageAccess: false + }) + } + } + } + + private handleResourceActionsMenuClick (resource: Resource) { + void this.loadResourceActionMenuItems(resource) + } + private renderResourceActionsMenu (resource: Resource, placement: 'grid' | 'list') { return html`
this.handleResourceActionsMenuClick(resource)} >
` @@ -436,11 +524,11 @@ export default class FolderPaneContainer extends WebComponent { ? html`${label}` : nothing} ${this.renderResourceActionsMenu(resource, 'grid')} diff --git a/src/components/folder-pane-sidebar/FolderPaneSidebar.ts b/src/components/folder-pane-sidebar/FolderPaneSidebar.ts index 7f02491..d39f8e2 100644 --- a/src/components/folder-pane-sidebar/FolderPaneSidebar.ts +++ b/src/components/folder-pane-sidebar/FolderPaneSidebar.ts @@ -12,6 +12,7 @@ import '~icons/lucide/folder' import '~icons/lucide/globe' import '~icons/lucide/trash-2' import { getResourcesForContainer, type FolderPaneResourceLogic } from '../../helpers' +import * as debug from '../../debug' type SpecialIcon = 'folder' | 'globe' | 'trash' @@ -83,7 +84,7 @@ export default class FolderPaneSidebar extends WebComponent { try { await this.store?.fetcher?.load(resource.subject) } catch (error) { - console.error('Unable to expand storage container', error) + debug.error('Unable to expand storage container', error) this.expansionError = `Unable to load ${utils.label(resource.subject)}. Try expanding it again.` return } diff --git a/src/components/folder-pane-storage-header/FolderPaneStorageHeader.ts b/src/components/folder-pane-storage-header/FolderPaneStorageHeader.ts index 40b1ce9..59e8639 100644 --- a/src/components/folder-pane-storage-header/FolderPaneStorageHeader.ts +++ b/src/components/folder-pane-storage-header/FolderPaneStorageHeader.ts @@ -15,6 +15,7 @@ import '~icons/lucide/plus' import '~icons/lucide/upload' import '~icons/lucide/search' import { getCreationPanes, getPaneLabel } from '../../helpers' +import * as debug from '../../debug' import styles from './FolderPaneStorageHeader.styles.css' @customElement('folder-pane-storage-header') @@ -56,7 +57,7 @@ export default class FolderPaneStorageHeader extends WebComponent { this.availablePanes = getCreationPanes(panes) } }) - .catch(error => console.error('Unable to load folder creation panes', error)) + .catch(error => debug.error('Unable to load folder creation panes', error)) } private get breadcrumbResources () { diff --git a/src/components/folder-pane-view/FolderPaneView.ts b/src/components/folder-pane-view/FolderPaneView.ts index f6330ed..168f395 100644 --- a/src/components/folder-pane-view/FolderPaneView.ts +++ b/src/components/folder-pane-view/FolderPaneView.ts @@ -20,6 +20,7 @@ import FolderPaneCreateDialog from '../folder-pane-storage-header/FolderPaneCrea import { getPaneLabel } from '../../helpers' import { isSafeCreationName, invalidCreationNameMessage } from '../../creationSafety' import { mintPaneInstance, PaneMintError, setMintStatus } from '../../mintPaneInstance' +import * as debug from '../../debug' type CreatedResource = { container: NamedNode @@ -173,7 +174,7 @@ export default class FolderPaneView extends WebComponent { newInstance: stillOwnsNavigation ? created.newInstance : undefined }) } catch (error) { - console.error('Unable to create folder-pane resource', error) + debug.error('Unable to create folder-pane resource', error) if (!(error instanceof PaneMintError) && this.statusArea) { setMintStatus(this.statusArea, 'error', `Unable to create ${paneName}: ${error instanceof Error ? error.message : String(error)}`) } @@ -511,6 +512,9 @@ export default class FolderPaneView extends WebComponent { } this.renderExplorerInto(target, subject, nextPane, { ...options, + paneOptions: nextPane.name === 'folder' + ? { folderView: 'container', fullView: true, solo: true } + : options.paneOptions, isCurrent: () => isCurrentRenderRequest( request, this.renderGeneration, @@ -569,7 +573,7 @@ export default class FolderPaneView extends WebComponent { if (!isCurrentSelection(selectedResource)) return - if (this.subject?.sameTerm(selectedResource) && !indexRequested && !isJavaScriptFile(selectedResource)) { + if (this.subject?.sameTerm(selectedResource) && pane?.name !== 'folder' && !indexRequested && !isJavaScriptFile(selectedResource)) { this.renderContainerView(selectedResource) return } @@ -594,6 +598,11 @@ export default class FolderPaneView extends WebComponent { if (!isCurrentSelection(selectedResource)) return this.sidebar?.refreshResources?.() + if (pane?.name === 'folder') { + this.renderContainerView(selectedResource) + return + } + if (indexRequested || this.resourceLogic.containerHasIndexDocument(selectedResource)) { const applicationSubject = this.store.sym( getIndexedApplicationSubject(selectedResource, pane?.name) @@ -700,6 +709,10 @@ export default class FolderPaneView extends WebComponent { if (!resource || !pane) return event.stopPropagation() this.searchQuery = '' + if (pane.name === 'folder') { + this.showSubject(resource, pane) + return + } // Some minting panes (e.g. Dokieli) delegate viewing to another pane and // have no render(); let the relevant-pane lookup choose in that case. this.showSubject(resource, typeof pane?.render === 'function' ? pane : undefined) diff --git a/src/debug.ts b/src/debug.ts new file mode 100644 index 0000000..5dcc73e --- /dev/null +++ b/src/debug.ts @@ -0,0 +1,15 @@ +export function log (...args: any[]) { + console.log(...args) +} + +export function warn (...args: any[]) { + console.warn(...args) +} + +export function error (...args: any[]) { + console.error(...args) +} + +export function trace (...args: any[]) { + console.trace(...args) +}