Skip to content

Commit cac3f76

Browse files
committed
#171 First pass refactor to match new design
AI was used to for the Header which will be rewritten in another ticket Prompt: Make the lit html and the dom manipulation work together in Header.ts Co-authored-by: Model GPT5.4 <copilot@github.com>
1 parent 82c7531 commit cac3f76

15 files changed

Lines changed: 653 additions & 392 deletions

dev/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ finishLogin()
3333

3434
// const targetURIToShow = "https://timbl.com/timbl/Automation/mother/tracker.n3#mother"
3535
// const targetURIToShow = 'https://sstratsianis.solidcommunity.net/TestingTracker/index.ttl#this'
36-
const targetURIToShow = 'https://testingsolidos.solidcommunity.net/profile/card#me'
37-
36+
// const targetURIToShow = 'https://testingsolidos.solidcommunity.net/profile/card#me'
37+
const targetURIToShow = 'https://sharontest.solidcommunity.net/profile/card#me'
3838
const context = {
3939
dom: document,
4040
session: {

jest.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default {
66
customExportConditions: ['node']
77
},
88
setupFilesAfterEnv: ["./test/helpers/jest.setup.js"],
9-
transformIgnorePatterns: ["/node_modules/(?!lit-html).+\\.js"],
9+
transformIgnorePatterns: ["/node_modules/(?!(@lit|@lit-labs|lit|lit-html|lit-element)/).+\\.js$"],
1010
roots: ['<rootDir>/src', '<rootDir>/test'],
1111
moduleNameMapper: {
1212
'^SolidLogic$': 'solid-logic',

package-lock.json

Lines changed: 11 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"@babel/preset-env": "^7.28.6",
6060
"@babel/preset-typescript": "^7.29.7",
6161
"@testing-library/dom": "^10.4.1",
62+
"@types/mime-types": "^3.0.1",
6263
"babel-loader": "^10.0.0",
6364
"babel-plugin-inline-import": "^3.0.0",
6465
"copy-webpack-plugin": "^14.0.0",

src/Header.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { html } from 'lit'
2+
import { ref } from 'lit/directives/ref.js'
3+
import { parse, serialize, NamedNode, LiveStore } from 'rdflib'
4+
import { widgets, icons } from 'solid-ui'
5+
import { getStatusSection } from './StatusSection'
6+
import { applyResponseHeaders, checkSyntax, getResponseHeaders, happy, refresh, setControlVisible, setEdited, setUnedited } from './helpers'
7+
import { SourcePaneState } from './types'
8+
import { compactable } from './compactableFormats'
9+
10+
function mountButton (host: HTMLElement, button: HTMLElement) {
11+
host.replaceChildren(button)
12+
}
13+
14+
export function renderHeader (store: LiveStore, subject: NamedNode, sourcePaneState: SourcePaneState) {
15+
async function saveBack (store: LiveStore, subject: NamedNode, sourcePaneState: SourcePaneState) {
16+
const fetcher = store.fetcher
17+
const textArea = document.querySelector('.sourcePaneTextArea') as HTMLTextAreaElement
18+
const data = textArea.value
19+
const { contentType, eTag } = sourcePaneState
20+
if (!checkSyntax(store, subject, data, contentType, subject)) {
21+
setEdited(sourcePaneState)
22+
const { showError } = getStatusSection()
23+
showError('Syntax error: fix the document before saving.')
24+
return
25+
}
26+
const options: { data: string; contentType: string | undefined; headers?: { 'if-match': string } } = { data, contentType }
27+
if (eTag) options.headers = { 'if-match': eTag } // avoid overwriting changed files -> status 412
28+
try {
29+
const response = await fetcher.webOperation('PUT', subject.uri, options)
30+
if (!happy(response, 'PUT')) return
31+
/// @@ show edited: make save button disabled until edited again.
32+
try {
33+
const response = await fetcher.webOperation('HEAD', subject.uri) // , defaultFetchHeaders())
34+
if (!happy(response, 'HEAD')) return
35+
applyResponseHeaders(sourcePaneState, getResponseHeaders(store, subject, response))
36+
setUnedited(subject, sourcePaneState)
37+
} catch (err) {
38+
throw err
39+
}
40+
} catch (err: any) {
41+
const { showError } = getStatusSection()
42+
showError('Error saving back: ' + err)
43+
}
44+
}
45+
46+
function setEditable (sourcePaneState: SourcePaneState) {
47+
const textArea = document.querySelector('.sourcePaneTextArea') as HTMLTextAreaElement
48+
const cancelButton = document.querySelector('.sourcePaneCancelButton') as HTMLElement
49+
const saveButton = document.querySelector('.sourcePaneSaveButton') as HTMLElement
50+
const myEditButton = document.querySelector('.sourcePaneEditButton') as HTMLElement
51+
const myCompactButton = document.querySelector('.sourcePaneCompactButton') as HTMLElement
52+
if (sourcePaneState.broken) return
53+
sourcePaneState.editing = true
54+
setControlVisible(cancelButton, true) // not logically needed but may be comforting
55+
setControlVisible(saveButton, false)
56+
setControlVisible(myEditButton, false)
57+
setControlVisible(myCompactButton, false) // do not allow compact while editing
58+
if (textArea) textArea.removeAttribute('readonly')
59+
}
60+
61+
function compactHandler (store: LiveStore, subject: NamedNode, sourcePaneState: SourcePaneState) {
62+
const { contentType } = sourcePaneState
63+
const compactContentType = contentType?.split(';')[0]
64+
const textArea = document.querySelector('.sourcePaneTextArea') as HTMLTextAreaElement
65+
const cancelButton = document.querySelector('.sourcePaneCancelButton') as HTMLElement
66+
const { showError } = getStatusSection()
67+
68+
if (compactContentType && compactable[compactContentType]) {
69+
try {
70+
parse(textArea.value, store, subject.uri, compactContentType)
71+
// for jsonld serialize which is a Promise. New rdflib
72+
const serialized = Promise.resolve(serialize(store.sym(subject.uri), store, subject.uri, compactContentType))
73+
serialized.then(result => {
74+
if (typeof result === 'string') textArea.value = result /*return div*/
75+
})
76+
setControlVisible(cancelButton, true)
77+
} catch (e: any) {
78+
showError(String(e))
79+
}
80+
}
81+
}
82+
83+
return html`
84+
<header class="sourcePaneHeader sourcePaneControls">
85+
<span class="sourcePaneControlHost" ${ref((host: Element | undefined) => {
86+
if (host instanceof HTMLElement) {
87+
mountButton(host, createEditButton(host.ownerDocument, () => setEditable(sourcePaneState)))
88+
}
89+
})}></span>
90+
<span class="sourcePaneControlHost" ${ref((host: Element | undefined) => {
91+
if (host instanceof HTMLElement) {
92+
mountButton(host, createCompactButton(host.ownerDocument, () => compactHandler(store, subject, sourcePaneState)))
93+
94+
}
95+
})}></span>
96+
<span class="sourcePaneControlHost" ${ref((host: Element | undefined) => {
97+
if (host instanceof HTMLElement) {
98+
mountButton(host, createCancelButton(host.ownerDocument, () => refresh(store, subject, sourcePaneState)))
99+
}
100+
})}></span>
101+
<span class="sourcePaneControlHost" ${ref((host: Element | undefined) => {
102+
if (host instanceof HTMLElement) {
103+
mountButton(host, createSaveButton(host.ownerDocument, () => saveBack(store, subject, sourcePaneState)))
104+
}
105+
})}></span>
106+
</header>
107+
`
108+
}
109+
110+
export function createEditButton (dom: Document, onClick?: EventListener) {
111+
const myEditButton = widgets.button(
112+
dom,
113+
icons.iconBase + 'noun_253504.svg',
114+
'Edit'
115+
)
116+
myEditButton.classList.add('sourcePaneEditButton')
117+
if (onClick) {
118+
myEditButton.addEventListener('click', onClick)
119+
}
120+
return myEditButton
121+
}
122+
123+
export function createCompactButton (dom: Document, onClick?: EventListener) {
124+
const myCompactButton = widgets.button(
125+
dom,
126+
undefined,
127+
'Compact',
128+
{ needsBorder: true }
129+
)
130+
myCompactButton.classList.add('sourcePaneCompactButton')
131+
if (onClick) {
132+
myCompactButton.addEventListener('click', onClick)
133+
}
134+
return myCompactButton
135+
}
136+
137+
export function createCancelButton (dom: Document, onClick?: EventListener) {
138+
const myCancelButton = widgets.cancelButton(dom)
139+
myCancelButton.classList.add('sourcePaneCancelButton')
140+
if (onClick) {
141+
myCancelButton.addEventListener('click', onClick)
142+
}
143+
return myCancelButton
144+
}
145+
146+
export function createSaveButton (dom: Document, onClick?: EventListener) {
147+
const mySaveButton = widgets.continueButton(dom)
148+
mySaveButton.classList.add('sourcePaneSaveButton')
149+
if (onClick) {
150+
mySaveButton.addEventListener('click', onClick)
151+
}
152+
return mySaveButton
153+
}

src/SourceCard.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { html } from 'lit'
2+
import { LiveStore, NamedNode } from 'rdflib'
3+
import { setEdited } from './helpers'
4+
import { SourcePaneState } from './types'
5+
6+
export function renderSourceCard (store: LiveStore, subject: NamedNode, sourcePaneState: SourcePaneState) {
7+
return html`
8+
<main class="sourcePaneMain">
9+
<textarea class="sourcePaneTextArea" @keyup=${() => setEdited(sourcePaneState)}></textarea>
10+
</main>
11+
`
12+
}

src/StatusSection.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.sourcePaneStatus {
2+
display: none;
3+
box-sizing: border-box;
4+
transition: all 0.2s ease-in-out;
5+
}
6+
7+
.sourcePaneStatus[data-visible='true'] {
8+
display: block;
9+
}
10+
11+
.error-text {
12+
margin: 0;
13+
padding: 12px;
14+
background-color: #fde8e8;
15+
border: 1px solid #f8b4b4;
16+
border-radius: 4px;
17+
color: #9b1c1c;
18+
font-weight: 500;
19+
}

src/StatusSection.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { html } from 'lit'
2+
import './StatusSection.css'
3+
4+
export function getStatusSection(){
5+
function showError(message: string) {
6+
const statusSection = document.querySelector('.sourcePaneStatus') as HTMLElement | null
7+
const statusMessageArea = document.getElementById('statusMessageArea')
8+
if (statusSection) {
9+
statusSection.dataset.visible = 'true'
10+
}
11+
if (statusMessageArea) {
12+
statusMessageArea.hidden = false
13+
const errorText = statusMessageArea.querySelector('.error-text')
14+
if (errorText) {
15+
errorText.textContent = message
16+
}
17+
}
18+
}
19+
20+
function clearError() {
21+
const statusSection = document.querySelector('.sourcePaneStatus') as HTMLElement | null
22+
const statusMessageArea = document.getElementById('statusMessageArea')
23+
if (statusSection) {
24+
statusSection.dataset.visible = 'false'
25+
}
26+
if (statusMessageArea) {
27+
statusMessageArea.hidden = true
28+
const errorText = statusMessageArea.querySelector('.error-text')
29+
if (errorText) {
30+
errorText.textContent = ''
31+
}
32+
}
33+
}
34+
35+
function renderStatusSection() {
36+
const statusMessage = ''
37+
38+
return html`
39+
<section class="sourcePaneStatus" role="alert" data-visible="false">
40+
<div id="statusMessageArea" hidden>
41+
<p class="error-text">${statusMessage}</p>
42+
<button @click=${() => clearError()}>X</button>
43+
</div>
44+
</section>
45+
`
46+
}
47+
return {
48+
renderStatusSection,
49+
showError,
50+
clearError
51+
}
52+
}

src/compactableFormats.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const compactable: Record<string, boolean> = {
2+
'text/n3': true,
3+
'text/turtle': true,
4+
'application/ld+json': true
5+
}

0 commit comments

Comments
 (0)