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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 111 additions & 79 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@
"lit": "^3.3.3",
"pane-registry": "5.0.2-1",
"rdflib": "2.4.1",
"solid-logic": "6.0.1-4",
"solid-ui": "5.0.2-3",
"solid-logic": "6.0.1-5",
"solid-ui": "5.0.2-4",
"vite": "^8.0.16",
"vitest": "^4.0.18",
"vitest-axe": "^0.1.0"
Expand Down
45 changes: 34 additions & 11 deletions src/components/source-editor-card/SourceEditorCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,26 +230,49 @@ export default class SourceEditorCard extends WebComponent {
}
const options: { data: string; contentType: string | undefined; headers?: { 'if-match': string } } = { data, contentType }
if (eTag) options.headers = { 'if-match': eTag } // avoid overwriting changed files -> status 412

let response: Response
try {
const response = await fetcher.webOperation('PUT', saveSubject.uri, options)
response = await fetcher.webOperation('PUT', saveSubject.uri, options)
if (!happy(response, 'PUT')) return
} catch (err: any) {
const { showError } = getStatusSection()
showError('Error saving back: ' + err)
return
}

if (sourceContext.resourceUri === saveSubject.uri) {
this._originalContent = data
/// @@ show edited: make save button disabled until edited again.
try {
const response = await fetcher.webOperation('HEAD', saveSubject.uri) // , defaultFetchHeaders())
if (!happy(response, 'HEAD')) return
const metadata = await solidLogicSingleton.resource.fetchMetadata(saveSubject)
sourceContext.updateContent(data)
}

let refreshError: unknown
try {
if (typeof fetcher.load === 'function') {
await fetcher.load(saveSubject, { force: true, clearPreviousData: true })
Comment thread
SharonStrats marked this conversation as resolved.
}
const metadataResponse = await fetcher.webOperation('HEAD', saveSubject.uri) // , defaultFetchHeaders())
if (!metadataResponse.ok) {
throw new Error(`HTTP error on HEAD! Status: ${metadataResponse.status}`)
}
const metadata = await solidLogicSingleton.resource.fetchMetadata(saveSubject)
if (sourceContext.resourceUri === saveSubject.uri) {
sourceContext.updateMetadata({
contentType: metadata.contentType,
eTag: metadata.eTag
})
this._resetEditorState()
} catch (err) {
throw err
}
} catch (err: any) {
} catch (err) {
refreshError = err
}

if (refreshError) {
const { showError } = getStatusSection()
showError('Error saving back: ' + err)
showError('Saved, but failed to refresh metadata: ' + refreshError)
}

if (sourceContext.resourceUri === saveSubject.uri) {
this._resetEditorState()
}
}

Expand Down
22 changes: 20 additions & 2 deletions src/components/source-provider/SourceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function createSourceContextValue(input: {
sourcePaneState: SourcePaneState
editorMetadata: EditorMetadata
updateSourcePaneState: SourceProvider['updateSourcePaneState']
updateContent: SourceProvider['updateOriginalContent']
updateMetadata: SourceProvider['updateEditorMetadata']
}): SourceContext {
return {
Expand All @@ -41,6 +42,7 @@ function createSourceContextValue(input: {
sourcePaneState: input.sourcePaneState,
editorMetadata: input.editorMetadata,
updateSourcePaneState: input.updateSourcePaneState,
updateContent: input.updateContent,
updateMetadata: input.updateMetadata,
}
}
Expand Down Expand Up @@ -76,17 +78,21 @@ export default class SourceProvider extends WebComponent {
sourcePaneState: createDefaultSourcePaneState(),
editorMetadata: createDefaultEditorMetadata(),
updateSourcePaneState: () => {},
updateContent: () => {},
updateMetadata: () => {},
})

private loadGeneration = 0

private loadContentAndMetadata = async () => {
if (!this.context || !this.subject) return

const subjectUri = this.subject.uri
const loadGeneration = ++this.loadGeneration

try {
const { content, metadata } = await solidLogicSingleton.resource.fetchContentAndMetadata(this.subject)
if (this.subject?.uri !== subjectUri) return
if (this.subject?.uri !== subjectUri || loadGeneration !== this.loadGeneration) return

this.originalContent = content
this.updateEditorMetadata(metadata)
Expand All @@ -99,13 +105,20 @@ export default class SourceProvider extends WebComponent {
}

private refreshSourceContextValue() {
const resourceUri = this.subject?.uri
const updateContent = (content: string) => {
if (this.subject?.uri !== resourceUri) return
this.updateOriginalContent(content)
}

this.sourceContextValue = createSourceContextValue({
dataLoaded: this.dataLoaded,
resourceUri: this.subject?.uri,
resourceUri,
originalContent: this.originalContent,
sourcePaneState: this.sourcePaneState,
editorMetadata: this.editorMetadata,
updateSourcePaneState: this.updateSourcePaneState,
updateContent,
updateMetadata: this.updateEditorMetadata,
})
}
Expand All @@ -122,6 +135,10 @@ export default class SourceProvider extends WebComponent {
}
}

updateOriginalContent = (content: string) => {
this.originalContent = content
}

updateEditorMetadata = (metadata: EditorMetadata) => {
this.editorMetadata = {
contentType: metadata.contentType,
Expand Down Expand Up @@ -187,6 +204,7 @@ export default class SourceProvider extends WebComponent {
}

if (changedProperties.has('context') || changedProperties.has('subject')) {
this.loadGeneration++
this.dataLoaded = false
}

Expand Down
1 change: 1 addition & 0 deletions src/primitives/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface SourceContext {
sourcePaneState: SourcePaneState,
editorMetadata: EditorMetadata,
updateSourcePaneState: <K extends keyof SourcePaneState>(key: K, value: SourcePaneState[K]) => void,
updateContent: (content: string) => void,
updateMetadata: (metadata: EditorMetadata) => void,
}

Expand Down
47 changes: 47 additions & 0 deletions test/sourceEditorCard.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { ContextProvider } from '@lit/context'
import { DEFAULT_FILE_EXPLORER_CONTEXT, fileExplorerContext } from 'solid-ui'
import { solidLogicSingleton } from 'solid-logic'
import { sourceContext } from '../src/primitives/context'

const { initializeEditor } = vi.hoisted(() => ({ initializeEditor: vi.fn(async () => {}) }))
Expand Down Expand Up @@ -45,6 +46,7 @@ describe('source-pane-source-editor-card', () => {
eTag: '"123"'
},
updateSourcePaneState: vi.fn(),
updateContent: vi.fn(),
updateMetadata: vi.fn()
}

Expand Down Expand Up @@ -275,4 +277,49 @@ describe('source-pane-source-editor-card', () => {
expect(editor.replaceContent).toHaveBeenCalledTimes(1)
expect(editor.replaceContent).toHaveBeenCalledWith('content three')
})

it('finalizes a successful save when metadata refresh fails', async () => {
const webOperation = vi.fn()
.mockResolvedValueOnce({ ok: true, status: 200 })
.mockResolvedValueOnce({ ok: true, status: 200 })
const fetchMetadata = vi.spyOn(solidLogicSingleton.resource, 'fetchMetadata')
.mockRejectedValueOnce(new Error('metadata unavailable'))
const card = createCard({ contentType: 'text/plain' })
const editor = {
focusEditor: vi.fn(),
setReadOnly: vi.fn(),
replaceContent: vi.fn(),
resetDirtyState: vi.fn(),
setLanguage: vi.fn(async () => {}),
getValue: vi.fn(() => 'updated'),
destroy: vi.fn()
}
card._editor = editor
card._editorReady = true
card.fileExplorerContext = {
...card.fileExplorerContext,
store: {
fetcher: {
webOperation,
load: vi.fn(async () => {})
}
}
}
document.body.appendChild(card)
card.beginEditing()
await card.updateComplete

await card.saveBack()

expect(webOperation).toHaveBeenNthCalledWith(
1,
'PUT',
card.fileExplorerContext.subjectUri,
expect.objectContaining({ data: 'updated' })
)
expect(fetchMetadata).toHaveBeenCalledOnce()
expect(card.sourceContext.updateContent).toHaveBeenCalledWith('updated')
expect(card.shadowRoot.querySelector('.sourcePaneSaveButton')).toBeNull()
fetchMetadata.mockRestore()
})
})
15 changes: 15 additions & 0 deletions test/sourceProvider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,19 @@ describe('source-pane-source-provider', () => {
expect(provider.originalContent).toBe('fresh content')
expect(provider.editorMetadata).toEqual({ contentType: 'text/plain', eTag: '"fresh"' })
})

it('ignores content updates from a previous resource after the subject changes', async () => {
const provider = createProvider()
document.body.appendChild(provider)
await flushUpdates(provider)

const updateContentForFirstResource = provider.sourceContextValue.updateContent
provider.subject = { uri: 'https://example.test/two.txt' }
await provider.updateComplete

updateContentForFirstResource('stale content')

expect(provider.originalContent).toBe('content one')
expect(provider.sourceContextValue.resourceUri).toBe('https://example.test/two.txt')
})
})
Loading