diff --git a/package-lock.json b/package-lock.json
index 5e600704..5aa0d8ff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8284,9 +8284,9 @@
"license": "MIT"
},
"node_modules/n3": {
- "version": "2.2.5",
- "resolved": "https://registry.npmjs.org/n3/-/n3-2.2.5.tgz",
- "integrity": "sha512-lR/0N7zVayC6N0uWqrMkPpJn8Up4+qBsjWiBnoOWWEi/ldTcygs2Dw1iAgozzDXLW/fcxXFfbMzpGHlc/1MBLQ==",
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/n3/-/n3-2.2.7.tgz",
+ "integrity": "sha512-+fyUtz1gQn7LS9GVOGwFjT2HURt/0nHYxuRvyaxQbOwFFPXsZJa41Accf9vo/sj3YhgYvb21wOOJT9fN82r9SQ==",
"license": "MIT",
"dependencies": {
"buffer": "^6.0.3",
diff --git a/src/components/file-explorer-header/FileExplorerHeader.styles.css b/src/components/file-explorer-header/FileExplorerHeader.styles.css
index d75d59f4..866bd8cb 100644
--- a/src/components/file-explorer-header/FileExplorerHeader.styles.css
+++ b/src/components/file-explorer-header/FileExplorerHeader.styles.css
@@ -7,6 +7,7 @@
flex-wrap: nowrap;
width: 100%;
text-align: right;
- padding: 15px;
+ 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
index 136cdc3f..b51c4d82 100644
--- a/src/components/file-explorer-header/FileExplorerHeader.ts
+++ b/src/components/file-explorer-header/FileExplorerHeader.ts
@@ -10,8 +10,8 @@ import styles from './FileExplorerHeader.styles.css'
import './FileExplorerHeaderSummary'
import './FileExplorerHeaderControls'
import { PaneIcon } from './types'
-import { fetchContentAndMetadata, type FileExplorerResourceMetadata } from './helper'
-
+import { fetchResourceMetadata } from '../../utils/podUtils'
+import { type FileExplorerResourceMetadata } from './types'
@customElement('file-explorer-header')
export default class FileExplorerHeader extends WebComponent {
static styles = styles
@@ -55,7 +55,7 @@ export default class FileExplorerHeader extends WebComponent {
if (!this.fileExplorerContext?.store || !this.fileExplorerContext.subjectUri) return
try {
- const { metadata } = await fetchContentAndMetadata(this.fileExplorerContext.store, sym(this.fileExplorerContext.subjectUri))
+ const metadata = await fetchResourceMetadata(this.fileExplorerContext.store, sym(this.fileExplorerContext.subjectUri))
this.responseMetadata = {
modified: metadata.modified,
isPublic: metadata.isPublic,
diff --git a/src/components/file-explorer-header/FileExplorerHeaderControls.styles.css b/src/components/file-explorer-header/FileExplorerHeaderControls.styles.css
index d962f83b..626163df 100644
--- a/src/components/file-explorer-header/FileExplorerHeaderControls.styles.css
+++ b/src/components/file-explorer-header/FileExplorerHeaderControls.styles.css
@@ -31,6 +31,12 @@
white-space: nowrap;
}
+ icon-lucide-share-2,
+ icon-lucide-pencil {
+ width: 16px;
+ height: 16px;
+ }
+
@media (max-width: 900px) {
div {
padding-right: 3px;
@@ -42,5 +48,17 @@
justify-content: flex-start;
gap: 5px;
}
+
+ .file-explorer-header-access-button,
+ .file-explorer-header-edit-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
index b6668ce4..5d5364dc 100644
--- a/src/components/file-explorer-header/FileExplorerHeaderControls.ts
+++ b/src/components/file-explorer-header/FileExplorerHeaderControls.ts
@@ -1,5 +1,5 @@
import { WebComponent } from 'solid-ui'
-import { customElement, property } from 'lit/decorators.js'
+import { customElement, property, state } from 'lit/decorators.js'
import { consume } from '@lit/context'
import { html, nothing } from 'lit'
import 'solid-ui/components/button'
@@ -8,20 +8,50 @@ 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 }> = []
+ 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
@@ -32,10 +62,6 @@ export default class FileExplorerHeaderControls extends WebComponent {
this.sourceContext?.setEditing?.()
} */
- private handleEditingClick () {
- this.fileExplorerContext.edit?.onEdit?.()
- }
-
private getEditTooltip () {
if (!this.fileExplorerContext.paneSupportsEditing) return 'Not Supported'
if (!this.canEdit) return 'No Access'
@@ -49,24 +75,36 @@ export default class FileExplorerHeaderControls extends WebComponent {
}
render () {
+ const isContainerResource = isContainerSubject(this.fileExplorerContext.store, this.fileExplorerContext.subjectUri)
+
return html`
${this.renderDirtyIndicator()}
-
-
-
-
-
-
+ ${!isContainerResource && !this.isMobile
+ ? html`
+
+
+ `
+ : nothing}
`
diff --git a/src/components/file-explorer-header/FileExplorerHeaderSummary.styles.css b/src/components/file-explorer-header/FileExplorerHeaderSummary.styles.css
index 0fce991a..db9952b7 100644
--- a/src/components/file-explorer-header/FileExplorerHeaderSummary.styles.css
+++ b/src/components/file-explorer-header/FileExplorerHeaderSummary.styles.css
@@ -12,8 +12,25 @@
h1 {
color: var(--solid-ui-color-gray-700, #364153);
- font-size: var(--solid-ui-font-size-2xl, 1.5rem);
+ 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 {
@@ -82,8 +99,50 @@
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
index a02b6947..2ca785d5 100644
--- a/src/components/file-explorer-header/FileExplorerHeaderSummary.ts
+++ b/src/components/file-explorer-header/FileExplorerHeaderSummary.ts
@@ -8,8 +8,10 @@ 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 { type FileExplorerResourceMetadata } from './helper'
+import { getContainerItemCount, isContainerSubject } from '../../utils/podUtils'
+import type { FileExplorerResourceMetadata } from './types'
@customElement('file-explorer-header-summary')
export default class FileExplorerHeaderSummary extends WebComponent {
@@ -93,11 +95,41 @@ export default class FileExplorerHeaderSummary extends WebComponent {
}
}
+ 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 modified = this.formatModifiedDate(this.responseMetadata.modified)
const isPublic = this.responseMetadata.isPublic
+ const isContainerResource = isContainerSubject(this.fileExplorerContext?.store, this.fileExplorerContext?.subjectUri)
return html`
`
}
diff --git a/src/components/file-explorer-header/FileExplorerProvider.ts b/src/components/file-explorer-header/FileExplorerProvider.ts
index 07ea4c24..3696b3e5 100644
--- a/src/components/file-explorer-header/FileExplorerProvider.ts
+++ b/src/components/file-explorer-header/FileExplorerProvider.ts
@@ -9,6 +9,7 @@ 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'
const PERSON_ICON = personIcon
const FRIENDS_ICON = friendsIcon
@@ -20,7 +21,7 @@ function createFileExplorerContextValue (value: {
soloPane?: boolean
onBack?: () => void
openPane?: (subject: NamedNode, paneName: string) => void
- handleSharingClick?: () => void
+ handleAccessClick?: () => void
paneSupportsEditing?: boolean
edit?: {
onEdit?: () => void
@@ -35,7 +36,7 @@ function createFileExplorerContextValue (value: {
soloPane: value.soloPane,
onBack: value.onBack,
openPane: value.openPane,
- handleSharingClick: value.handleSharingClick,
+ handleAccessClick: value.handleAccessClick,
paneSupportsEditing: value.paneSupportsEditing,
edit: value.edit
}
@@ -66,7 +67,7 @@ export default class FileExplorerProvider extends WebComponent {
accessor showHeader: boolean = true
@property({ attribute: false })
- accessor handleSharingClick: (() => void) | undefined = undefined
+ accessor handleAccessClick: (() => void) | undefined = undefined
// TODO: Need to research this more, brought it over from manager.
@property({ attribute: false })
@@ -125,7 +126,7 @@ export default class FileExplorerProvider extends WebComponent {
soloPane: this.soloPane,
onBack: this.onBack,
openPane: this.openPane,
- handleSharingClick: this.handleSharingClick,
+ handleAccessClick: this.handleAccessClick,
paneSupportsEditing: false,
edit: this.edit
})
@@ -181,7 +182,7 @@ export default class FileExplorerProvider extends WebComponent {
soloPane: this.soloPane,
onBack: this.onBack,
openPane: this.openPane,
- handleSharingClick: this.handleSharingClick,
+ handleAccessClick: this.handleAccessClick,
paneSupportsEditing: this.paneSupportsEditing,
edit: this.edit
})
@@ -228,7 +229,7 @@ export default class FileExplorerProvider extends WebComponent {
changedProperties.has('soloPane') ||
changedProperties.has('onBack') ||
changedProperties.has('openPane') ||
- changedProperties.has('handleSharingClick') ||
+ changedProperties.has('handleAccessClick') ||
changedProperties.has('pane') ||
changedProperties.has('isDirty')
) {
diff --git a/src/components/file-explorer-header/helper.ts b/src/components/file-explorer-header/helper.ts
deleted file mode 100644
index 129c30bb..00000000
--- a/src/components/file-explorer-header/helper.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-// TODO: this code will need to be moved later
-import { ACL_LINK } from 'solid-logic'
-import { ns } from 'solid-ui'
-import type { LiveStore, NamedNode } from 'rdflib'
-
-export 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>()
- if (!headerValue) return permissions
-
- for (const entry of headerValue.split(',')) {
- const match = entry.trim().match(/^([A-Za-z]+)\s*=\s*"([^"]*)"$/)
- if (match) {
- const [, permissionGroup, accessModes] = match
- const modes = accessModes.trim().split(/\s+/).filter(Boolean)
- permissions.set(permissionGroup.toLowerCase(), new Set(modes.map(mode => mode.toLowerCase())))
- }
- }
-
- return permissions
-}
-
-function deriveAccessFlags (wacAllow: string | null | undefined) {
- if (!wacAllow) {
- return { canEdit: false, isPublic: false }
- }
-
- const permissions = parseWacAllowHeader(wacAllow)
- const userModes = permissions.get('user') ?? new Set()
- const publicModes = permissions.get('public') ?? new Set()
-
- return {
- canEdit: userModes.has('write'),
- isPublic: publicModes.has('read') || publicModes.has('write')
- }
-}
-
-export function getResponseMetadata (store: LiveStore, subject: NamedNode, response: Response): FileExplorerResourceMetadata {
- let contentType: string | undefined
- let canEdit = false
- let isPublic = false
- let eTag: string | undefined
- let modified: string | undefined
-
- if (response.headers && response.headers.get('content-type')) {
- contentType = response.headers.get('content-type')?.split(';')[0] ?? undefined
- const accessFlags = deriveAccessFlags(response.headers.get('wac-allow'))
-
- canEdit = accessFlags.canEdit
- isPublic = accessFlags.isPublic
- eTag = response.headers.get('etag') ?? undefined
- modified = store.anyValue(subject as any, ns.dct('modified')) || store.anyValue(subject as any, ns.dc('modified')) || undefined
- } else {
- const reqs = store.each(
- null,
- store.sym('http://www.w3.org/2007/ont/link#requestedURI'),
- subject
- )
- reqs.forEach((req: any) => {
- const responseNode = store.any(
- req as any,
- store.sym('http://www.w3.org/2007/ont/link#response')
- )
- if (responseNode && responseNode.termType === 'NamedNode') {
- contentType = store.anyValue(responseNode as any, ns.httph('content-type')) || undefined
- const wacAllow = (store.anyValue(responseNode as any, ns.httph('wac-allow')) as string | undefined) ||
- (store.anyValue(responseNode as any, ns.httph('WAC-Allow')) as string | undefined)
- const accessFlags = deriveAccessFlags(wacAllow)
- canEdit = accessFlags.canEdit
- isPublic = accessFlags.isPublic
- eTag = store.anyValue(responseNode as any, ns.httph('etag')) || undefined
- modified = store.anyValue(subject as any, ns.dct('modified')) || store.anyValue(subject as any, ns.dc('modified')) || undefined
- }
- })
- }
-
- const aclUri = store.any(subject, ACL_LINK)?.value || undefined
- return { contentType, canEdit, isPublic, aclUri, eTag, modified }
-}
-
-export async function fetchContentAndMetadata (store: LiveStore, subject: NamedNode): Promise<{ content: string, metadata: FileExplorerResourceMetadata }> {
- const fetcher = store.fetcher
-
- const response = await fetcher.webOperation('GET', subject.uri)
- const content = (response as Response & { responseText?: string }).responseText
-
- if (content === undefined) {
- throw new Error('No text in response object!!')
- }
-
- const metadata = getResponseMetadata(store, subject, response)
- if (!metadata.contentType) {
- throw new Error('No content-type available!')
- }
-
- return { content, metadata }
-}
diff --git a/src/components/file-explorer-header/index.ts b/src/components/file-explorer-header/index.ts
new file mode 100644
index 00000000..2140b416
--- /dev/null
+++ b/src/components/file-explorer-header/index.ts
@@ -0,0 +1,2 @@
+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
index b3b3ecbd..826d13a8 100644
--- a/src/components/file-explorer-header/types.ts
+++ b/src/components/file-explorer-header/types.ts
@@ -1 +1,10 @@
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/ResourceActionsMenu.styles.css b/src/components/resource-actions-menu/ResourceActionsMenu.styles.css
new file mode 100644
index 00000000..33a5392f
--- /dev/null
+++ b/src/components/resource-actions-menu/ResourceActionsMenu.styles.css
@@ -0,0 +1,6 @@
+:host {
+ .ellipsisIcon {
+ width: 16px;
+ height: 16px;
+ }
+}
diff --git a/src/components/resource-actions-menu/ResourceActionsMenu.ts b/src/components/resource-actions-menu/ResourceActionsMenu.ts
index 884b3004..e441e70d 100644
--- a/src/components/resource-actions-menu/ResourceActionsMenu.ts
+++ b/src/components/resource-actions-menu/ResourceActionsMenu.ts
@@ -1,17 +1,19 @@
-import { showDialog, utils, WebComponent } from 'solid-ui'
+import { WebComponent } from 'solid-ui'
import { customElement, property } from 'lit/decorators.js'
-import { html } from 'lit'
+import { html, nothing } from 'lit'
import 'solid-ui/components/button'
import 'solid-ui/components/menu'
import 'solid-ui/components/menu-item'
-import DeleteResourceDialog from './DeleteResourceDialog'
import '~icons/lucide/ellipsis-vertical'
-import '~icons/lucide/trash-2'
+import '~icons/lucide/share-2'
+import '~icons/lucide/pencil'
import { LiveStore } from 'rdflib'
-// TODO: Add Error Status section
-
+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
@@ -19,74 +21,31 @@ export default class ResourceActionsMenu extends WebComponent {
accessor subjectUri: string | undefined
@property({ attribute: false })
- accessor menuItems: Array<{ label: string, icon?: HTMLElement, action: (event: Event) => void }> = []
-
- private confirmDelete (resourceName: string) {
- return new Promise(resolve => {
- showDialog(DeleteResourceDialog, {
- props: { resourceName },
- onClose: (result) => resolve(result === true)
- })
- })
- }
-
- /* private gotoParentFolder(resourceUri: string) {
- const sourceContext = this.sourceContext
- const outliner = sourceContext?.context?.getOutliner?.(sourceContext.context.dom)
-
- if (!outliner) return
-
- const parentFolderUri = this.getParentFolderUri(resourceUri)
- ;(outliner as any).GotoSubject(sourceContext.context.session.store.sym(parentFolderUri), true, undefined, true, undefined)
- } */
+ accessor handleAccessClick: (() => void) | undefined = undefined
- private async deleteResourceIfPresent (store: LiveStore, uri: string) {
- try {
- await store.fetcher.webOperation('DELETE', uri)
- } catch (err: any) {
- const status = err?.response?.status ?? err?.status
- if (status === 404) return
- throw err
- }
- }
-
- // TODO: Below is for a file only. I need to move this to a function
- // that function needs to check if it's a container and if so we can do the
- // recursive delete function.
- // dont' forget public and private type indexes.
- private async handleDelete (event: Event) {
- event.preventDefault()
-
- if (!this.store?.fetcher || !this.subjectUri) return
-
- const store = this.store
- const resourceNode = store.sym(this.subjectUri)
+ @property({ attribute: false })
+ accessor handleEditingClick: (() => void) | undefined = undefined
- const confirmation = await this.confirmDelete(utils.label(resourceNode))
- if (!confirmation) return
+ @property({ type: Boolean })
+ accessor paneSupportsEditing = false
- try {
- await this.deleteResourceIfPresent(store, resourceNode.value)
+ @property({ type: Boolean })
+ accessor canEdit = false
- const aclUri = this.subjectUri + '.acl'
- if (aclUri) {
- await this.deleteResourceIfPresent(store, aclUri)
- store.removeDocument(store.sym(aclUri))
- }
+ @property({ type: Boolean })
+ accessor isMobile = false
- store.removeDocument(resourceNode)
- // this.gotoParentFolder(resourceNode.value)
- } catch (err) {
- // error('Error deleting resource:', err)
- // getStatusSection()?.showError('Failed to delete resource. Check console for details.')
- }
- }
+ @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)}>
@@ -94,10 +53,30 @@ export default class ResourceActionsMenu extends WebComponent {
${item.label}
`)}
- this.handleDelete(event)}>
-
- Delete
-
+ ${canEdit
+ ? html`
+
+
+ Edit
+
+ `
+ : nothing}
+ ${!isContainerResource && this.isMobile && canManageAccess
+ ? html`
+
+
+ Manage Access
+
+ `
+ : nothing}
+ ${isContainerResource && canManageAccess
+ ? html`
+
+
+ Manage Access
+
+ `
+ : nothing}
`
}
diff --git a/src/outline/manager.css b/src/outline/manager.css
index 4915eed5..8e7901e2 100644
--- a/src/outline/manager.css
+++ b/src/outline/manager.css
@@ -80,3 +80,12 @@
width: 12px;
height: 12px;
}
+
+@media (max-width: 600px) {
+ .obj,
+ .iconTD {
+ font-size: 100%;
+ margin: 0 !important;
+ vertical-align: top;
+ }
+}
diff --git a/src/outline/manager.js b/src/outline/manager.js
index 1207ad30..afb06da2 100644
--- a/src/outline/manager.js
+++ b/src/outline/manager.js
@@ -13,7 +13,7 @@ import { UserInput } from './userInput.js'
import * as queryByExample from './queryByExample.js'
import { loadContainerRepresentation } from '../utils/podUtils'
import { isWebIdUri } from '../utils/webIdUtils'
-import '../components/file-explorer-header/FileExplorerProvider'
+import '../components/file-explorer-header'
export default function (context) {
const dom = context.dom
@@ -711,10 +711,7 @@ export default function (context) {
// Add the x more here
const moreTR = dom.createElement('tr')
const moreTD = moreTR.appendChild(dom.createElement('td'))
- moreTD.setAttribute(
- 'style',
- 'margin: 0.2em; border: none; padding: 0; vertical-align: top;'
- )
+ moreTD.classList.add('obj')
moreTD.setAttribute('notSelectable', 'false')
if (predDups > n) {
// what is this for??
@@ -790,23 +787,14 @@ export default function (context) {
termWidget.construct = function (dom) {
dom = dom || document
const td = dom.createElement('TD')
- td.setAttribute(
- 'style',
- 'margin: 0.2em; border: none; padding: 0; vertical-align: top;'
- )
td.setAttribute('class', 'iconTD')
td.setAttribute('notSelectable', 'true')
- td.style.width = '0px'
return td
}
termWidget.addIcon = function (td, icon, listener) {
const iconTD = td.childNodes[1]
if (!iconTD) return
- let width = iconTD.style.width
const img = UI.utils.AJARImage(icon.src, icon.alt, icon.tooltip, dom)
- width = parseInt(width)
- width = width + icon.width
- iconTD.style.width = width + 'px'
iconTD.appendChild(img)
if (listener) {
img.addEventListener('click', listener)
@@ -816,10 +804,6 @@ export default function (context) {
const iconTD = td.childNodes[1]
let baseURI
if (!iconTD) return
- let width = iconTD.style.width
- width = parseInt(width)
- width = width - icon.width
- iconTD.style.width = width + 'px'
for (let x = 0; x < iconTD.childNodes.length; x++) {
const elt = iconTD.childNodes[x]
const eltSrc = elt.src
diff --git a/src/utils/podUtils.ts b/src/utils/podUtils.ts
index aa7b4aaa..27abd7dd 100644
--- a/src/utils/podUtils.ts
+++ b/src/utils/podUtils.ts
@@ -1,7 +1,40 @@
import { store } from 'solid-logic'
import { ns } from 'solid-ui'
-import { NamedNode, parse } from 'rdflib'
+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'
+
+function parseWacAllowHeader (headerValue: string | null | undefined) {
+ const permissions = new Map>()
+ if (!headerValue) return permissions
+
+ for (const entry of headerValue.split(',')) {
+ const match = entry.trim().match(/^([A-Za-z]+)\s*=\s*"([^"]*)"$/)
+ if (match) {
+ const [, permissionGroup, accessModes] = match
+ const modes = accessModes.trim().split(/\s+/).filter(Boolean)
+ permissions.set(permissionGroup.toLowerCase(), new Set(modes.map(mode => mode.toLowerCase())))
+ }
+ }
+
+ return permissions
+}
+
+function deriveAccessFlags (wacAllow: string | null | undefined) {
+ if (!wacAllow) {
+ return { canEdit: false, isPublic: false }
+ }
+
+ const permissions = parseWacAllowHeader(wacAllow)
+ const userModes = permissions.get('user') ?? new Set()
+ const publicModes = permissions.get('public') ?? new Set()
+
+ return {
+ canEdit: userModes.has('write'),
+ isPublic: publicModes.has('read') || publicModes.has('write')
+ }
+}
export async function getPodStorages (url: NamedNode): Promise {
if (isWebIdUri(url)) {
@@ -56,3 +89,102 @@ export async function loadContainerRepresentation (subject) {
}
}
}
+
+export function isContainerSubject (store: LiveStore | undefined, subjectUri: string | undefined): boolean {
+ if (!store || !subjectUri) return false
+
+ const subject = store.sym(subjectUri)
+ const typeUris = store.findTypeURIs(subject)
+ return Boolean(
+ typeUris[ns.ldp('Container').uri] ||
+ typeUris[ns.ldp('BasicContainer').uri] ||
+ subject.uri.endsWith('/')
+ )
+}
+
+// The shared rdflib store can hold duplicate ldp:contains statements for the
+// same child resource when the container and companion metadata are both loaded.
+// Count unique visible children here so the header summary matches the list UI.
+export function getContainerItemCount (store: LiveStore | undefined, subjectUri: string | undefined): number {
+ if (!store || !subjectUri) return 0
+
+ const subject = store.sym(subjectUri)
+ const seen = new Set()
+
+ for (const item of store.each(subject, ns.ldp('contains'))) {
+ if (item.termType === 'NamedNode') {
+ const resource = item as NamedNode
+ const parent = resource.dir()
+
+ if (parent) {
+ const pathEnd = resource.uri.slice(parent.uri.length)
+ if (
+ !pathEnd.startsWith('.') &&
+ !pathEnd.endsWith('.acl') &&
+ !pathEnd.endsWith('~')
+ ) {
+ seen.add(resource.uri)
+ }
+ }
+ }
+ }
+ return seen.size
+}
+
+export function getResponseMetadata (store: LiveStore, subject: NamedNode, response: Response): FileExplorerResourceMetadata {
+ let contentType: string | undefined
+ let canEdit = false
+ let isPublic = false
+ let eTag: string | undefined
+ let modified: string | undefined
+
+ if (response.headers && response.headers.get('content-type')) {
+ contentType = response.headers.get('content-type')?.split(';')[0] ?? undefined
+ const accessFlags = deriveAccessFlags(response.headers.get('wac-allow'))
+
+ canEdit = accessFlags.canEdit
+ isPublic = accessFlags.isPublic
+ eTag = response.headers.get('etag') ?? undefined
+ modified = store.anyValue(subject as any, ns.dct('modified')) || store.anyValue(subject as any, ns.dc('modified')) || undefined
+ } else {
+ const reqs = store.each(
+ null,
+ store.sym('http://www.w3.org/2007/ont/link#requestedURI'),
+ subject
+ )
+ reqs.forEach((req: any) => {
+ const responseNode = store.any(
+ req as any,
+ store.sym('http://www.w3.org/2007/ont/link#response')
+ )
+ if (responseNode && responseNode.termType === 'NamedNode') {
+ contentType = store.anyValue(responseNode as any, ns.httph('content-type')) || undefined
+ const wacAllow = (store.anyValue(responseNode as any, ns.httph('wac-allow')) as string | undefined) ||
+ (store.anyValue(responseNode as any, ns.httph('WAC-Allow')) as string | undefined)
+ const accessFlags = deriveAccessFlags(wacAllow)
+ canEdit = accessFlags.canEdit
+ isPublic = accessFlags.isPublic
+ eTag = store.anyValue(responseNode as any, ns.httph('etag')) || undefined
+ modified = store.anyValue(subject as any, ns.dct('modified')) || store.anyValue(subject as any, ns.dc('modified')) || undefined
+ }
+ })
+ }
+
+ const aclUri = store.any(subject, ACL_LINK)?.value || undefined
+ return { contentType, canEdit, isPublic, aclUri, eTag, modified }
+}
+
+export async function fetchResourceMetadata (store: LiveStore, subject: NamedNode): Promise {
+ const response = await store.fetcher.webOperation('HEAD', subject.uri)
+
+ if (!response.ok) {
+ throw new Error(`HEAD request failed with status ${response.status}`)
+ }
+
+ const metadata = getResponseMetadata(store, subject, response)
+ if (!metadata.contentType) {
+ throw new Error('No content-type available!')
+ }
+
+ return metadata
+}