diff --git a/declarations.d.ts b/declarations.d.ts
index c6593da..14f6120 100644
--- a/declarations.d.ts
+++ b/declarations.d.ts
@@ -1 +1,2 @@
///
+///
diff --git a/package-lock.json b/package-lock.json
index 75793a2..7b5e172 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "source-pane",
- "version": "3.1.1-4",
+ "version": "3.1.1-5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "source-pane",
- "version": "3.1.1-4",
+ "version": "3.1.1-5",
"license": "MIT",
"dependencies": {
"@codemirror/commands": "^6.10.0",
@@ -33,6 +33,7 @@
"vitest-axe": "^0.1.0"
},
"devDependencies": {
+ "@iconify-json/lucide": "^1.2.116",
"@types/mime-types": "^3.0.1",
"@types/node": "^22.20.0",
"@vitest/coverage-v8": "^4.0.18",
@@ -2311,9 +2312,9 @@
}
},
"node_modules/@digitalbazaar/http-client/node_modules/undici": {
- "version": "6.27.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
- "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
+ "version": "6.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
+ "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
@@ -2655,6 +2656,16 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@iconify-json/lucide": {
+ "version": "1.2.119",
+ "resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.119.tgz",
+ "integrity": "sha512-KbrC7fT5wPshV1KLRM0k91poP0OH4eKEVywEOH0FCczKb+qth8Pcq1Hy9kS2tXeF52s23qzLnG8AJsExPSupeA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@iconify/types": "*"
+ }
+ },
"node_modules/@iconify/types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
@@ -4189,9 +4200,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.11.1",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz",
- "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==",
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.3.tgz",
+ "integrity": "sha512-sbT0Ui/CZwyAyy7icT1Gw5P1LKRlFaHwaF6tDCW5YHq2X5SeeZFphBuIagopSfwSSZq3sQcbmEL072yphxm7ew==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -4212,15 +4223,15 @@
}
},
"node_modules/brace-expansion": {
- "version": "5.0.7",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
- "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
+ "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": "20 || >=22"
}
},
"node_modules/browserslist": {
@@ -4823,9 +4834,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.395",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz",
- "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==",
+ "version": "1.5.396",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz",
+ "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==",
"dev": true,
"license": "ISC"
},
@@ -8049,9 +8060,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.22",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
- "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
+ "version": "8.5.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
+ "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
"funding": [
{
"type": "opencollective",
@@ -9420,9 +9431,9 @@
}
},
"node_modules/undici": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
- "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
+ "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
"devOptional": true,
"license": "MIT",
"engines": {
diff --git a/package.json b/package.json
index 9bcfb70..0f32f6d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "source-pane",
- "version": "3.1.1-4",
+ "version": "3.1.1-5",
"description": "Solid-compatible Panes: Source editor",
"main": "dist/index.cjs.js",
"exports": {
@@ -55,6 +55,7 @@
},
"homepage": "https://github.com/SolidOS/source-pane",
"devDependencies": {
+ "@iconify-json/lucide": "^1.2.116",
"@types/mime-types": "^3.0.1",
"@types/node": "^22.20.0",
"@vitest/coverage-v8": "^4.0.18",
diff --git a/src/Header.ts b/src/Header.ts
deleted file mode 100644
index 7c6cb3d..0000000
--- a/src/Header.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { html } from 'lit'
-import { ref } from 'lit/directives/ref.js'
-import { NamedNode, LiveStore } from 'rdflib'
-import { widgets, icons } from 'solid-ui'
-import { SourcePaneState } from './types'
-import type SourceEditorCard from './components/source-editor-card/SourceEditorCard'
-
-/* This we will use in the header ticket, didn't want to lose the code */
-export function canEditSource (subject: NamedNode, sourcePaneState: SourcePaneState) {
- const { allowed } = sourcePaneState
- return !subject.uri.endsWith('/') && (!allowed || allowed.includes('PUT'))
-}
-
-function mountButton (host: HTMLElement, button: HTMLElement) {
- host.replaceChildren(button)
-}
-
-export function renderHeader (store: LiveStore, subject: NamedNode, sourcePaneState: SourcePaneState) {
-
- function setEditable (sourcePaneState: SourcePaneState) {
- if (sourcePaneState.broken) return
- const editorCard = document.querySelector('solid-panes-source-editor-card') as SourceEditorCard | null
- editorCard?.updateEditingState(true)
- editorCard?.setReadOnly(false)
- editorCard?.focusEditor()
- }
-
- return html`
-
- `
-}
-
-export function createEditButton (dom: Document, onClick?: EventListener) {
- const myEditButton = widgets.button(
- dom,
- icons.iconBase + 'noun_253504.svg',
- 'Edit'
- )
- myEditButton.classList.add('sourcePaneEditButton')
- if (onClick) {
- myEditButton.addEventListener('click', onClick)
- }
- return myEditButton
-}
diff --git a/src/StatusSection.css b/src/StatusSection.css
index f71b3a6..36adee4 100644
--- a/src/StatusSection.css
+++ b/src/StatusSection.css
@@ -6,14 +6,46 @@
.sourcePaneStatus[data-visible='true'] {
display: block;
+ margin: 0;
+ padding: 12px;
+ background: var(--dialog-error-background, var(--pink-500, #ffdde6));
+ border-radius: 5px;
+ border: 1px solid var(--dialog-error-border, var(--pink-700, #ff98b3));
+}
+
+#statusMessageArea {
+ display: flex;
+ align-items: center;
+ gap: 6px;
}
.error-text {
margin: 0;
- padding: 12px;
- background-color: #fde8e8;
- border: 1px solid #f8b4b4;
- border-radius: 4px;
- color: #9b1c1c;
+ padding: 10px 12px;
+ background-color: transparent;
+ border: 0;
+ border-radius: 0;
+ color: var(--solid-ui-color-error, #b00020);
font-weight: 500;
+ flex: 1;
+}
+
+.statusCloseButton {
+ flex: 0 0 auto;
+ margin-top: 0;
+ align-self: center;
+ width: 28px;
+ height: 28px;
+ border: 0;
+ border-radius: 5px;
+ background: transparent;
+ color: var(--solid-ui-color-error, #b00020);
+ font-weight: 600;
+ line-height: 1;
+ padding: 0;
+ cursor: pointer;
+}
+
+.statusCloseButton:hover {
+ background: color-mix(in srgb, var(--solid-ui-color-error, #b00020) 10%, transparent);
}
diff --git a/src/StatusSection.ts b/src/StatusSection.ts
index 1a96e81..1fede1b 100644
--- a/src/StatusSection.ts
+++ b/src/StatusSection.ts
@@ -39,7 +39,7 @@ export function getStatusSection(){
${statusMessage}
-
+
`
diff --git a/src/components/header/SourceHeader.styles.css b/src/components/header/SourceHeader.styles.css
new file mode 100644
index 0000000..d75d59f
--- /dev/null
+++ b/src/components/header/SourceHeader.styles.css
@@ -0,0 +1,12 @@
+:host {
+ header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: nowrap;
+ width: 100%;
+ text-align: right;
+ padding: 15px;
+ }
+}
diff --git a/src/components/header/SourceHeader.ts b/src/components/header/SourceHeader.ts
new file mode 100644
index 0000000..f3aee2a
--- /dev/null
+++ b/src/components/header/SourceHeader.ts
@@ -0,0 +1,29 @@
+import { WebComponent } from 'solid-ui'
+import { customElement } from 'lit/decorators.js'
+import { html } from 'lit'
+import 'solid-ui/components/button'
+import { consume } from '@lit/context'
+import { sourceContext, SourceContext } from '../../primitives/context'
+import '~icons/lucide/share-2'
+import '~icons/lucide/pencil'
+import '~icons/lucide/ellipsis-vertical'
+import styles from './SourceHeader.styles.css'
+import './SourceHeaderSummary'
+import './SourceHeaderControls'
+
+@customElement('source-pane-source-header')
+export default class SourceHeader extends WebComponent {
+ static styles = styles
+
+ @consume({ context: sourceContext, subscribe: true })
+ accessor sourceContext: SourceContext = undefined as unknown as SourceContext
+
+ render () {
+ return html`
+
+ `
+ }
+}
diff --git a/src/components/header/SourceHeaderControls.styles.css b/src/components/header/SourceHeaderControls.styles.css
new file mode 100644
index 0000000..c2316f9
--- /dev/null
+++ b/src/components/header/SourceHeaderControls.styles.css
@@ -0,0 +1,28 @@
+:host {
+ 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;
+ }
+
+ @media (max-width: 900px) {
+ div {
+ padding-right: 3px;
+ }
+ }
+
+ @media (max-width: 600px) {
+ div {
+ justify-content: flex-start;
+ gap: 5px;
+ }
+ }
+}
diff --git a/src/components/header/SourceHeaderControls.ts b/src/components/header/SourceHeaderControls.ts
new file mode 100644
index 0000000..b82fc07
--- /dev/null
+++ b/src/components/header/SourceHeaderControls.ts
@@ -0,0 +1,49 @@
+import { WebComponent } from 'solid-ui'
+import { customElement } from 'lit/decorators.js'
+import { html } from 'lit'
+import 'solid-ui/components/button'
+import { consume } from '@lit/context'
+import { sourceContext, SourceContext } from '../../primitives/context'
+import '~icons/lucide/share-2'
+import '~icons/lucide/pencil'
+import '~icons/lucide/ellipsis-vertical'
+import styles from './SourceHeaderControls.styles.css'
+import './SourceHeaderSummary'
+
+@customElement('source-pane-source-header-controls')
+export default class SourceHeaderControls extends WebComponent {
+ static styles = styles
+
+ @consume({ context: sourceContext, subscribe: true })
+ accessor sourceContext: SourceContext = undefined as unknown as SourceContext
+
+ /* private canEditSource (subject: NamedNode, sourcePaneState: SourcePaneState) {
+ const { allowed } = sourcePaneState
+ return !subject.uri.endsWith('/') && (!allowed || allowed.includes('PUT'))
+ } */
+
+ 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?.()
+ }
+
+ render () {
+ return html`
+
+
+
+
+ this.setEditable()}>
+
+
+
+
+
+
+ `
+ }
+}
diff --git a/src/components/header/SourceHeaderSummary.styles.css b/src/components/header/SourceHeaderSummary.styles.css
new file mode 100644
index 0000000..98ea911
--- /dev/null
+++ b/src/components/header/SourceHeaderSummary.styles.css
@@ -0,0 +1,82 @@
+ :host {
+ .source-pane-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-2xl, 1.5rem);
+ font-weight: 500;
+ }
+
+ .source-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);
+ }
+
+ 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;
+ }
+
+ .source-pane-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);
+ }
+
+ .source-pane-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;
+ }
+
+ icon-lucide-arrow-left {
+ width: 1.125rem;
+ height: 1.125rem;
+ }
+}
\ No newline at end of file
diff --git a/src/components/header/SourceHeaderSummary.ts b/src/components/header/SourceHeaderSummary.ts
new file mode 100644
index 0000000..54d80aa
--- /dev/null
+++ b/src/components/header/SourceHeaderSummary.ts
@@ -0,0 +1,67 @@
+import { utils, WebComponent } from 'solid-ui'
+import 'solid-ui/components/button'
+import { customElement } from 'lit/decorators.js'
+import { html } from 'lit'
+import { consume } from '@lit/context'
+import { sym } from 'rdflib'
+import { sourceContext, SourceContext } from '../../primitives/context'
+import '~icons/lucide/globe'
+import '~icons/lucide/lock-keyhole'
+import '~icons/lucide/arrow-left'
+import styles from './SourceHeaderSummary.styles.css'
+import { sourcePaneIcon } from '../../icons/sourcePaneIcon'
+
+@customElement('source-pane-source-header-summary')
+export default class SourceHeaderSummary extends WebComponent {
+ static styles = styles
+
+ @consume({ context: sourceContext, subscribe: true })
+ accessor sourceContext: SourceContext = undefined as unknown as SourceContext
+
+ 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}`
+ }
+
+ render () {
+ const subject = this.sourceContext?.subject ? sym(this.sourceContext.subject) : undefined
+ const label = subject ? utils.label(subject) : ''
+ const modified = this.formatModifiedDate(this.sourceContext?.headerMetadata?.modified)
+ const isPublic = this.sourceContext?.headerMetadata?.isPublic ?? false
+
+ return html`
+
+ `
+ }
+}
diff --git a/src/components/source-editor-card/SourceEditorCard.styles.css b/src/components/source-editor-card/SourceEditorCard.styles.css
index 2d48d8c..59b48b8 100644
--- a/src/components/source-editor-card/SourceEditorCard.styles.css
+++ b/src/components/source-editor-card/SourceEditorCard.styles.css
@@ -8,6 +8,7 @@
flex-direction: column;
width: 100%;
align-items: stretch;
+ padding: 0 40px;
}
.sourcePaneEditor {
diff --git a/src/components/source-editor-card/SourceEditorCard.ts b/src/components/source-editor-card/SourceEditorCard.ts
index b833f33..10d7a10 100644
--- a/src/components/source-editor-card/SourceEditorCard.ts
+++ b/src/components/source-editor-card/SourceEditorCard.ts
@@ -2,26 +2,28 @@ import { html, nothing } from 'lit'
import { consume } from '@lit/context'
import { customElement, query, state } from 'lit/decorators.js'
import { NamedNode, parse, serialize } from 'rdflib'
+import { WebComponent } from 'solid-ui'
+import 'solid-ui/components/button'
import type { SourceEditor } from './SourceEditor'
-import { applyResponseHeaders, checkSyntax, fetchContentAndMetadata, getResponseHeaders, happy } from '../../helpers'
+import { checkSyntax, happy } from '../../helpers'
import styles from './SourceEditorCard.styles.css'
-import { WebComponent } from 'solid-ui'
import { getStatusSection } from '../../StatusSection'
-import 'solid-ui/components/button'
import { compactable } from '../../compactableFormats'
import { sourceContext, SourceContext } from '../../primitives/context'
+import { getResponseMetadata } from '../../resourceLoader'
-@customElement('solid-panes-source-editor-card')
+@customElement('source-pane-source-editor-card')
export default class SourceEditorCard extends WebComponent {
static styles = styles
private _editor?: SourceEditor
private _originalContent?: string
private _dirtyState = false
- private _editingState = false
+ private _initializing = false
@state()
accessor _editorReady = false
+
@query('.sourcePaneEditor')
accessor _editorMount: HTMLDivElement | null = null
@@ -63,8 +65,7 @@ export default class SourceEditorCard extends WebComponent {
}
updateEditingState(editing: boolean) {
- if (this._editingState === editing) return
- this._editingState = editing
+ if (this.sourceContext?.sourcePaneState.editing === editing) return
this.sourceContext?.updateSourcePaneState('editing', editing)
}
@@ -76,18 +77,19 @@ export default class SourceEditorCard extends WebComponent {
}
private async _initializeEditor () {
- if (this._editor) return
+ if (this._editor || this._initializing) return
+ this._initializing = true
const sourcePaneEditor = this._editorMount
const sourceContext = this.sourceContext
if (!sourcePaneEditor || !sourceContext) {
+ this._initializing = false
return
}
try {
const { SourceEditor } = await import('./SourceEditor')
- const { content, metadata } = await fetchContentAndMetadata(sourceContext.context.session.store as any, new NamedNode(sourceContext.subject), sourceContext.sourcePaneState as any)
- this._originalContent = content
+ this._originalContent = sourceContext.originalContent
this._editor = new SourceEditor()
- await this._editor.initialize(sourcePaneEditor, content, metadata.contentType, 'dark', dirty => {
+ await this._editor.initialize(sourcePaneEditor, sourceContext.originalContent ?? '', sourceContext.editorMetadata.contentType, 'dark', dirty => {
this.updateDirtyState(dirty)
})
this._editorReady = true
@@ -95,6 +97,8 @@ export default class SourceEditorCard extends WebComponent {
} catch (err) {
const { showError } = getStatusSection()
showError('Error fetching content: ' + err)
+ } finally {
+ this._initializing = false
}
}
@@ -110,7 +114,7 @@ export default class SourceEditorCard extends WebComponent {
}
this._editorReady = false
this._dirtyState = false
- this._editingState = false
+ this._initializing = false
}
private cancelHandler () {
@@ -128,10 +132,9 @@ export default class SourceEditorCard extends WebComponent {
const store = sourceContext.context.session.store as any
const subject = new NamedNode(sourceContext.subject)
- const sourcePaneState = sourceContext.sourcePaneState
const fetcher = store.fetcher
const data = this.getEditor()?.getValue() ?? ''
- const { contentType, eTag } = sourceContext.sourcePaneState
+ const { contentType, eTag } = sourceContext.editorMetadata
if (!checkSyntax(store, subject as any, data, contentType, subject as any)) {
const { showError } = getStatusSection()
showError('Syntax error: fix the document before saving.')
@@ -147,7 +150,8 @@ export default class SourceEditorCard extends WebComponent {
try {
const response = await fetcher.webOperation('HEAD', subject.uri) // , defaultFetchHeaders())
if (!happy(response, 'HEAD')) return
- applyResponseHeaders(sourcePaneState as any, getResponseHeaders(store, subject as any, response))
+ const metadata = getResponseMetadata(store, subject as any, response)
+ sourceContext.updateMetadata(metadata)
this._resetEditorState()
} catch (err) {
throw err
@@ -161,7 +165,7 @@ export default class SourceEditorCard extends WebComponent {
private prettyHandler () {
const sourceContext = this._requireSourceContext()
- const { contentType } = sourceContext.sourcePaneState
+ const { contentType } = sourceContext.editorMetadata
const compactContentType = contentType?.split(';')[0]
const { showError } = getStatusSection()
const store = sourceContext.context.session.store as any
@@ -181,29 +185,39 @@ export default class SourceEditorCard extends WebComponent {
}
}
}
+
+ private renderFooter() {
+ const sourceContext = this._requireSourceContext()
+ const compactContentType = sourceContext.editorMetadata.contentType?.split(';')[0]
+ const showPrettyButton = !sourceContext.sourcePaneState.editing && !!compactContentType && compactable[compactContentType]
+
+ if (sourceContext.sourcePaneState.editing) {
+ return html`
+
+ `
+ }
+
+ if (showPrettyButton) {
+ return html`
+
+ `
+ }
+
+ return nothing
+ }
render() {
- const sourceContext = this._requireSourceContext()
const sectionClass = this._editorReady ? 'sourcePaneCard' : 'sourcePaneCard sourcePaneCardLoading'
- const compactContentType = sourceContext.sourcePaneState.contentType?.split(';')[0]
- const showPrettyButton = !this._editingState && !!compactContentType && compactable[compactContentType]
- const prettyButton = showPrettyButton
- ? html`
- Prettify
- `
- : nothing
return html`
-
+ ${this.renderFooter()}
`
}
diff --git a/src/components/source-provider/SourceProvider.styles.css b/src/components/source-provider/SourceProvider.styles.css
new file mode 100644
index 0000000..67637b0
--- /dev/null
+++ b/src/components/source-provider/SourceProvider.styles.css
@@ -0,0 +1,12 @@
+:host {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ align-self: stretch;
+ box-sizing: border-box;
+ padding-bottom: 39px;
+}
+
+source-pane-source-header {
+ margin-bottom: 23px;
+}
diff --git a/src/components/source-provider/SourceProvider.ts b/src/components/source-provider/SourceProvider.ts
index 6d1fc37..975cc71 100644
--- a/src/components/source-provider/SourceProvider.ts
+++ b/src/components/source-provider/SourceProvider.ts
@@ -1,27 +1,67 @@
import { html, type PropertyValues } from 'lit'
import { provide } from '@lit/context'
-import { customElement, property } from 'lit/decorators.js'
+import { customElement, property, query, state } from 'lit/decorators.js'
import { NamedNode } from 'rdflib'
import { DataBrowserContext } from 'pane-registry'
-import { renderHeader } from '../../Header'
import { getStatusSection } from '../../StatusSection'
import { WebComponent } from 'solid-ui'
import { sourceContext, SourceContext } from '../../primitives/context'
-import { SourcePaneState } from '../../types'
-
+import { SourcePaneState, HeaderMetadata, EditorMetadata, ResourceMetadata } from '../../types'
+import type SourceEditorCard from '../source-editor-card/SourceEditorCard'
+import '../header/SourceHeader'
+import { fetchContentAndMetadata } from '../../resourceLoader'
+import styles from './SourceProvider.styles.css'
void import('../source-editor-card/SourceEditorCard').then(() => undefined)
-const defaultSourcePaneState: SourcePaneState = {
- broken: false,
- dirty: false,
- editing: false,
- allowed: undefined,
- contentType: undefined,
- eTag: undefined
+function createDefaultSourcePaneState(): SourcePaneState {
+ return {
+ broken: false,
+ dirty: false,
+ editing: false
+ }
+}
+
+function createDefaultHeaderMetadata(): HeaderMetadata {
+ return {
+ canEdit: false,
+ isPublic: false,
+ modified: undefined
+ }
+}
+
+function createDefaultEditorMetadata(): EditorMetadata {
+ return {
+ contentType: undefined,
+ eTag: undefined
+ }
}
+function createSourceContextValue(input: {
+ context: DataBrowserContext | undefined
+ subject: NamedNode | undefined
+ originalContent: string | undefined
+ sourcePaneState: SourcePaneState
+ headerMetadata: HeaderMetadata
+ editorMetadata: EditorMetadata
+ updateSourcePaneState: SourceProvider['updateSourcePaneState']
+ updateMetadata: SourceProvider['updateEditorMetadata']
+ setEditing: SourceProvider['setEditing']
+}): SourceContext {
+ return {
+ context: input.context as DataBrowserContext,
+ subject: input.subject?.uri ?? '',
+ originalContent: input.originalContent,
+ sourcePaneState: input.sourcePaneState,
+ headerMetadata: input.headerMetadata,
+ editorMetadata: input.editorMetadata,
+ updateSourcePaneState: input.updateSourcePaneState,
+ updateMetadata: input.updateMetadata,
+ setEditing: input.setEditing,
+ }
+}
@customElement('source-pane-source-provider')
export default class SourceProvider extends WebComponent {
+ static styles = styles
@property({ attribute: false })
accessor context: DataBrowserContext | undefined = undefined
@@ -29,38 +69,69 @@ export default class SourceProvider extends WebComponent {
@property({ attribute: false })
accessor subject: NamedNode | undefined = undefined
- @property({ attribute: false })
- accessor sourcePaneState: SourcePaneState = defaultSourcePaneState
+ @state()
+ accessor originalContent: string | undefined = undefined
- @provide({ context: sourceContext })
- accessor sourceContextValue: SourceContext = {
- context: undefined as unknown as DataBrowserContext,
- subject: '',
- sourcePaneState: defaultSourcePaneState,
- updateSourcePaneState: () => {},
- }
+ @state()
+ accessor dataLoaded = false
- // need this while we are using document.querySelector
- // and rendering plain HTML children. Can remove later when all
- // code is refactored to use context and components.
- createRenderRoot () {
- return this
- }
+ @state()
+ accessor sourcePaneState: SourcePaneState = createDefaultSourcePaneState()
- private _requireContext () {
- if (!this.context) {
- throw new Error('The element is missing the required `context` property.')
- }
+ @state()
+ accessor headerMetadata: HeaderMetadata = createDefaultHeaderMetadata()
+
+ @state()
+ accessor editorMetadata: EditorMetadata = createDefaultEditorMetadata()
- return this.context
- }
- private _requireSubject () {
- if (!this.subject) {
- throw new Error('The element is missing the required `subject` property.')
+ @provide({ context: sourceContext })
+ accessor sourceContextValue: SourceContext = createSourceContextValue({
+ context: undefined,
+ subject: undefined,
+ originalContent: undefined,
+ sourcePaneState: createDefaultSourcePaneState(),
+ headerMetadata: createDefaultHeaderMetadata(),
+ editorMetadata: createDefaultEditorMetadata(),
+ updateSourcePaneState: () => {},
+ updateMetadata: () => {},
+ setEditing: () => {},
+ })
+
+ @query('source-pane-source-editor-card')
+ private accessor editorCard: SourceEditorCard | null = null
+
+ private loadContentAndMetadata = async () => {
+ try {
+ if (!this.context) {
+ throw new Error('The element is missing the required `context` property.')
+ }
+ if (!this.subject) {
+ throw new Error('The element is missing the required `subject` property.')
+ }
+
+ const { content, metadata } = await fetchContentAndMetadata(this.context.session.store as any, this.subject)
+ this.originalContent = content
+ this.updateEditorMetadata(metadata)
+ this.dataLoaded = true
+ } catch (error: any) {
+ const { showError } = getStatusSection()
+ showError(error.message)
}
+ }
- return this.subject
+ private refreshSourceContextValue() {
+ this.sourceContextValue = createSourceContextValue({
+ context: this.context,
+ subject: this.subject,
+ originalContent: this.originalContent,
+ sourcePaneState: this.sourcePaneState,
+ headerMetadata: this.headerMetadata,
+ editorMetadata: this.editorMetadata,
+ updateSourcePaneState: this.updateSourcePaneState,
+ updateMetadata: this.updateEditorMetadata,
+ setEditing: this.setEditing,
+ })
}
updateSourcePaneState = (key: K, value: SourcePaneState[K]) => {
@@ -70,30 +141,46 @@ export default class SourceProvider extends WebComponent {
}
}
- protected override willUpdate (changedProperties: PropertyValues) {
- super.willUpdate(changedProperties)
+ updateEditorMetadata = (metadata: ResourceMetadata) => {
+ this.editorMetadata = {
+ contentType: metadata.contentType,
+ eTag: metadata.eTag
+ }
+ this.headerMetadata = {
+ canEdit: metadata.canEdit,
+ isPublic: metadata.isPublic,
+ modified: metadata.modified
+ }
+ }
- const context = this._requireContext()
- const subject = this._requireSubject()
+ setEditing = () => {
+ this.updateSourcePaneState('editing', true)
+ this.editorCard?.updateEditingState(true)
+ this.editorCard?.setReadOnly(false)
+ this.editorCard?.focusEditor()
+ }
- this.sourceContextValue = {
- context,
- subject: subject.uri,
- sourcePaneState: this.sourcePaneState,
- updateSourcePaneState: this.updateSourcePaneState,
+ protected async firstUpdated() {
+ await this.loadContentAndMetadata()
+ }
+
+ protected willUpdate (changedProperties: PropertyValues) {
+ super.willUpdate(changedProperties)
+ if (!this.context) {
+ throw new Error('The element is missing the required `context` property.')
}
+
+ this.refreshSourceContextValue()
}
render() {
- const context = this._requireContext()
- const subject = this._requireSubject()
- const store = context.session.store
- const { renderStatusSection } = getStatusSection()
-
return html`
- ${renderHeader(store as any, subject as any, this.sourcePaneState)}
-
- ${renderStatusSection()}
+ ${this.dataLoaded
+ ? html`
+
+
+ `
+ : html``}
`
}
}
diff --git a/src/helpers.ts b/src/helpers.ts
index a4c5385..aa78938 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -1,8 +1,6 @@
import { LiveStore, NamedNode, parse, serialize } from 'rdflib'
import { getStatusSection } from './StatusSection'
-import { error, log } from './debug'
-import { ns } from 'solid-ui'
-import { HttpResourceMetadata, SourcePaneState } from './types'
+import { error } from './debug'
const parseable: Record = {
'text/n3': true,
@@ -131,69 +129,3 @@ export function setControlVisible (button: HTMLElement | null, visible: boolean)
button.classList.toggle('sourcePaneControlVisible', visible)
button.classList.toggle('sourcePaneControlHidden', !visible)
}
-
-export function applyResponseHeaders (sourcePaneState: SourcePaneState, metadata: HttpResourceMetadata) {
- sourcePaneState.contentType = metadata.contentType
- sourcePaneState.allowed = metadata.allowed
- sourcePaneState.eTag = metadata.eTag
-}
-
-export function getResponseHeaders (store: LiveStore, subject: NamedNode, response: Response): HttpResourceMetadata {
- let contentType: string | undefined
- let allowed: string | undefined
- let eTag: string | undefined
- if (response.headers && response.headers.get('content-type')) {
- contentType = response.headers.get('content-type')?.split(';')[0] ?? undefined // Should work but headers may be empty
- allowed = response.headers.get('allow') ?? undefined // const cts = store.fetcher.getHeader(subject.doc(), 'content-type')
- eTag = response.headers.get('etag') ?? undefined
- } else {
- const reqs = store.each(
- null,
- store.sym('http://www.w3.org/2007/ont/link#requestedURI'),
- subject
- )
- reqs.forEach((req: any) => {
- const rrr = store.any(
- req as any,
- store.sym('http://www.w3.org/2007/ont/link#response')
- )
- if (rrr && rrr.termType === 'NamedNode') {
- contentType = store.anyValue(rrr as any, ns.httph('content-type')) || undefined
- allowed = store.anyValue(rrr as any, ns.httph('allow')) || undefined
- eTag = store.anyValue(rrr as any, ns.httph('etag')) || undefined
- if (!eTag) log('sourcePane: No eTag on GET')
- }
- })
- }
- return { contentType, allowed, eTag }
-}
-
-export async function fetchContentAndMetadata(store: LiveStore, subject: NamedNode, sourcePaneState: SourcePaneState): Promise<{ content: string, metadata: HttpResourceMetadata }> {
- const fetcher = store.fetcher
- const { showError } = getStatusSection()
-
- try {
- const response = await fetcher.webOperation('GET', subject.uri)
- if (!happy(response, 'GET')) {
- throw new Error('GET request failed')
- }
-
- const content = (response as Response & { responseText?: string }).responseText
- if (content === undefined) { // Defensive https://github.com/linkeddata/rdflib.js/issues/506
- throw new Error('source pane: No text in response object!!')
- }
-
- const metadata = getResponseHeaders(store, subject, response)
- if (!metadata.contentType) {
- throw new Error('Error: No content-type available!')
- }
- applyResponseHeaders(sourcePaneState, metadata)
- if (!metadata.allowed) {
- error('@@@@@@@@@@ No Allow: header from this server')
- }
- return { content, metadata }
- } catch (err: any) {
- showError('Error reading file: ' + err)
- throw err
- }
-}
diff --git a/src/icons/sourcePaneIcon.ts b/src/icons/sourcePaneIcon.ts
new file mode 100644
index 0000000..103c714
--- /dev/null
+++ b/src/icons/sourcePaneIcon.ts
@@ -0,0 +1,8 @@
+import { html } from 'lit'
+
+export const sourcePaneIcon = html`
+
+`
diff --git a/src/primitives/context.ts b/src/primitives/context.ts
index 3ce007d..ef8a543 100644
--- a/src/primitives/context.ts
+++ b/src/primitives/context.ts
@@ -1,12 +1,17 @@
import { DataBrowserContext } from 'pane-registry'
import { createContext } from '@lit/context'
-import { SourcePaneState } from '../types'
+import { EditorMetadata, HeaderMetadata, SourcePaneState, ResourceMetadata } from '../types'
export interface SourceContext {
context: DataBrowserContext,
readonly subject: string,
+ originalContent: string | undefined,
sourcePaneState: SourcePaneState,
- updateSourcePaneState: (key: K, value: SourcePaneState[K]) => void
+ headerMetadata: HeaderMetadata,
+ editorMetadata: EditorMetadata,
+ updateSourcePaneState: (key: K, value: SourcePaneState[K]) => void,
+ updateMetadata: (metadata: ResourceMetadata) => void,
+ setEditing: () => void
}
export const sourceContext = createContext(Symbol('source'))
diff --git a/src/resourceLoader.ts b/src/resourceLoader.ts
new file mode 100644
index 0000000..7be6792
--- /dev/null
+++ b/src/resourceLoader.ts
@@ -0,0 +1,104 @@
+import { ns } from 'solid-ui'
+import { log } from './debug'
+import { ResourceMetadata } from './types'
+import { LiveStore, NamedNode } from 'rdflib'
+import { happy } from './helpers'
+
+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) {
+ log('deriveAccessFlags: No WAC-Allow header found.')
+ 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): ResourceMetadata {
+ 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 // Should work but headers may be empty
+ 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
+ if (!eTag) log('sourcePane: No eTag on GET')
+ }
+ })
+ }
+ return { contentType, canEdit, isPublic, eTag, modified } as ResourceMetadata
+}
+
+export async function fetchContentAndMetadata(store: LiveStore, subject: NamedNode): Promise<{ content: string, metadata: ResourceMetadata }> {
+ const fetcher = store.fetcher
+
+ try {
+ const response = await fetcher.webOperation('GET', subject.uri)
+ if (!happy(response, 'GET')) {
+ throw new Error('GET request failed')
+ }
+
+ const content = (response as Response & { responseText?: string }).responseText
+ if (content === undefined) { // Defensive https://github.com/linkeddata/rdflib.js/issues/506
+ throw new Error('source pane: No text in response object!!')
+ }
+
+ const metadata = getResponseMetadata(store, subject, response)
+ if (!metadata.contentType) {
+ throw new Error('Error: No content-type available!')
+ }
+ return { content, metadata }
+ } catch (error: any) {
+ throw new Error(`Error reading file: ${error.message}`)
+ }
+
+}
diff --git a/src/sourcePane.css b/src/sourcePane.css
index 9dc6a3d..9aacd53 100644
--- a/src/sourcePane.css
+++ b/src/sourcePane.css
@@ -5,40 +5,11 @@
border: 1px solid var(--color-border, var(--gray-200, #E5E7EB));
}
-.sourcePaneControls {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- gap: 6px;
- flex-wrap: wrap;
- width: 100%;
- text-align: right;
- padding: 0 3px 16px 0;
-}
-/* generative ai */
-.sourcePaneControls > * {
- flex: 0 1 auto;
-}
-
-.sourcePaneHeader,
.sourcePaneCard,
.sourcePaneStatus {
width: 100%;
}
-/* This code is here because currently the shadow DOM isn't
-working just yet because I need to implement the header as a web component first
-once I do that I can move this to the host css of the source-provider */
-source-pane-source-provider {
- display: flex;
- flex-direction: column;
- width: 100%;
- align-self: stretch;
- box-sizing: border-box;
- padding: 20px 20px 29px;
- gap: 23px;
-}
-
.sourcePaneCompactButton {
box-sizing: border-box;
display: inline-flex;
@@ -51,10 +22,6 @@ source-pane-source-provider {
}
@media (max-width: 900px) {
- .sourcePaneControls {
- padding-right: 3px;
- }
-
.sourcePaneTextArea {
margin: 12px 2px;
padding: 12px;
@@ -62,11 +29,6 @@ source-pane-source-provider {
}
@media (max-width: 600px) {
- .sourcePaneControls {
- justify-content: flex-start;
- gap: 5px;
- }
-
.sourcePaneCompactButton {
min-width: 0;
}
@@ -75,11 +37,3 @@ source-pane-source-provider {
.sourcePaneTextAreaError {
color: var(--solid-ui-color-error, #b00020);
}
-
-.sourcePaneControlVisible {
- display: block;
-}
-
-.sourcePaneControlHidden {
- display: none;
-}
diff --git a/src/sourcePane.ts b/src/sourcePane.ts
index 8d717b2..da1697e 100644
--- a/src/sourcePane.ts
+++ b/src/sourcePane.ts
@@ -9,9 +9,9 @@ import { DataBrowserContext, NewPaneOptions } from 'pane-registry'
import * as mime from 'mime-types'
import { html, render as litRender } from 'lit'
import { log } from './debug'
+import { getStatusSection } from './StatusSection'
import './sourcePane.css'
import './components/source-provider/SourceProvider'
-import { SourcePaneState } from './types'
const pane = {
icon: icons.iconBase + 'noun_109873.svg', // noun_109873_51A7F9.svg
@@ -84,24 +84,16 @@ const pane = {
},
render: function (subject: NamedNode, context: DataBrowserContext) {
- const sourcePaneState: SourcePaneState = {
- broken: false,
- dirty: false,
- editing: false,
- allowed: undefined,
- contentType: undefined,
- eTag: undefined
- }
-
+ const { renderStatusSection } = getStatusSection()
const sourcePane = context.dom.createElement('div')
sourcePane.setAttribute('class', 'sourcePane')
litRender(html`
+ ${renderStatusSection()}
`, sourcePane)
return sourcePane
diff --git a/src/types.ts b/src/types.ts
index e4ea537..136eaff 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,11 +1,16 @@
-export type HttpResourceMetadata = {
+export type ResourceMetadata = {
contentType: string | undefined
- allowed: string | undefined
+ canEdit: boolean
+ isPublic: boolean
eTag: string | undefined
+ modified: string | undefined
}
+export type HeaderMetadata = Pick
+export type EditorMetadata = Pick
+
export type SourcePaneState = {
broken: boolean
dirty: boolean
editing: boolean
-} & HttpResourceMetadata
+}
diff --git a/test/helpers.test.js b/test/helpers.test.js
index 617cbdc..49d9689 100644
--- a/test/helpers.test.js
+++ b/test/helpers.test.js
@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { fetchContentAndMetadata, getResponseHeaders } from '../src/helpers.ts'
+import { fetchContentAndMetadata, getResponseMetadata } from '../src/resourceLoader.ts'
-describe('helpers', () => {
+describe('resourceLoader', () => {
beforeEach(() => {
vi.clearAllMocks()
})
@@ -12,19 +12,27 @@ describe('helpers', () => {
headers: {
get: vi.fn((name) => {
if (name === 'content-type') return 'text/turtle; charset=utf-8'
- if (name === 'allow') return 'GET,PUT'
+ if (name === 'wac-allow') return 'user="write", public="read"'
if (name === 'etag') return '"abc"'
return null
})
}
}
- const store = { each: vi.fn(), any: vi.fn(), anyValue: vi.fn(), sym: vi.fn() }
+ const store = {
+ each: vi.fn(),
+ any: vi.fn(),
+ anyValue: vi.fn(() => undefined),
+ sym: vi.fn()
+ }
const subject = { uri: 'https://example.org/profile/card' }
- expect(getResponseHeaders(store, subject, response)).toEqual({
+ expect(getResponseMetadata(store, subject, response)).toEqual({
contentType: 'text/turtle',
- allowed: 'GET,PUT',
+ canEdit: true,
+ isPublic: true,
eTag: '"abc"'
+ ,
+ modified: undefined
})
})
@@ -34,7 +42,7 @@ describe('helpers', () => {
headers: {
get: vi.fn((name) => {
if (name === 'content-type') return 'text/turtle'
- if (name === 'allow') return 'GET,PUT'
+ if (name === 'wac-allow') return 'user="write", public="read"'
if (name === 'etag') return '"abc"'
return null
})
@@ -47,27 +55,22 @@ describe('helpers', () => {
},
each: vi.fn(),
any: vi.fn(),
- anyValue: vi.fn(),
+ anyValue: vi.fn(() => undefined),
sym: vi.fn()
}
const subject = { uri: 'https://example.org/profile/card' }
- const sourcePaneState = { broken: false, contentType: undefined, allowed: undefined, eTag: undefined }
- const result = await fetchContentAndMetadata(store, subject, sourcePaneState)
+ const result = await fetchContentAndMetadata(store, subject)
expect(result).toEqual({
content: '<> a <#Thing>.',
metadata: {
contentType: 'text/turtle',
- allowed: 'GET,PUT',
- eTag: '"abc"'
+ canEdit: true,
+ isPublic: true,
+ eTag: '"abc"',
+ modified: undefined
}
})
- expect(sourcePaneState).toEqual({
- broken: false,
- contentType: 'text/turtle',
- allowed: 'GET,PUT',
- eTag: '"abc"'
- })
})
})
diff --git a/test/sourceEditorCard.test.js b/test/sourceEditorCard.test.js
index c50268c..f0e4155 100644
--- a/test/sourceEditorCard.test.js
+++ b/test/sourceEditorCard.test.js
@@ -2,7 +2,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
const fetchContentAndMetadata = vi.fn()
-vi.mock('../src/helpers.ts', () => ({
+vi.mock('../src/resourceMetadata.ts', () => ({
fetchContentAndMetadata,
}))
diff --git a/test/sourcePane.test.js b/test/sourcePane.test.js
index f0dcb80..747dc67 100644
--- a/test/sourcePane.test.js
+++ b/test/sourcePane.test.js
@@ -1,35 +1,27 @@
-import { render } from 'lit'
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
-let renderHeader
+let sourcePane
beforeAll(async () => {
- class MockSourceEditorCard extends HTMLElement {
- constructor () {
- super()
- this.updateEditingState = vi.fn()
- this.setReadOnly = vi.fn()
- this.focusEditor = vi.fn()
- }
- }
-
- if (!globalThis.customElements.get('solid-panes-source-editor-card')) {
- globalThis.customElements.define('solid-panes-source-editor-card', MockSourceEditorCard)
- }
+ await import('../src/components/source-provider/SourceProvider.ts')
+ await import('../src/components/header/SourceHeader.ts')
+ sourcePane = (await import('../src/sourcePane.ts')).default
- const headerModule = await import('../src/Header.ts')
- renderHeader = headerModule.renderHeader
})
-function renderHeaderIntoDocument (sourcePaneState) {
+function renderPaneIntoDocument () {
const container = document.createElement('div')
document.body.appendChild(container)
const store = { findTypeURIs: vi.fn(() => ({})) }
const subject = { uri: 'https://janedoe.example/test.ttl' }
- render(renderHeader(store, subject, sourcePaneState), container)
-
- return { container, subject, store }
+ const context = {
+ dom: document,
+ session: { store }
+ }
+ const rendered = sourcePane.render(subject, context)
+ container.appendChild(rendered)
+ return { container, subject, store, rendered }
}
describe('source-pane', () => {
@@ -38,42 +30,12 @@ describe('source-pane', () => {
vi.clearAllMocks()
})
- it('renders the header edit control', async () => {
- const sourcePaneState = {
- broken: false,
- editing: false,
- readonly: false,
- allowed: undefined,
- contentType: undefined,
- eTag: undefined
- }
-
- const { container } = renderHeaderIntoDocument(sourcePaneState)
- await Promise.resolve()
-
- expect(container.querySelector('header.sourcePaneHeader')).not.toBeNull()
- expect(container.querySelector('.sourcePaneEditButton')).not.toBeNull()
- })
-
- it('activates the editor when edit is clicked', async () => {
- const sourcePaneState = {
- broken: false,
- editing: false,
- readonly: false,
- allowed: undefined,
- contentType: undefined,
- eTag: undefined
- }
-
- const { container } = renderHeaderIntoDocument(sourcePaneState)
- const editorCard = document.createElement('solid-panes-source-editor-card')
- document.body.appendChild(editorCard)
-
+ it('renders the source provider wrapper', async () => {
+ const { rendered } = renderPaneIntoDocument()
await Promise.resolve()
- container.querySelector('.sourcePaneEditButton').click()
- expect(editorCard.updateEditingState).toHaveBeenCalledWith(true)
- expect(editorCard.setReadOnly).toHaveBeenCalledWith(false)
- expect(editorCard.focusEditor).toHaveBeenCalled()
+ expect(rendered).not.toBeNull()
+ expect(rendered.className).toBe('sourcePane')
+ expect(rendered.getAttribute('class')).toBe('sourcePane')
})
})