Skip to content

Commit 632f813

Browse files
Vinnlunknown
authored andcommitted
Show a Scratchpad's author
Added primarily to test navigating between different resources without a full page refresh.
1 parent 46478b9 commit 632f813

8 files changed

Lines changed: 166 additions & 11 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`View mode should properly render the pad's contents 1`] = `"<div>First line<br>Second line<br></div>"`;
3+
exports[`View mode should properly render the pad's contents 1`] = `"<div>First line<br>Second line<br><hr><small></small></div>"`;

scratchpad/data.test.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-env jest */
22
import $rdf from 'rdflib'
3-
import { getInitialisationStatements, getSetContentsStatements, getContents, isPad, getTitle } from './data'
3+
import { getInitialisationStatements, getSetContentsStatements, getContents, isPad, getTitle, getLatestAuthor } from './data'
44
import vocab from 'solid-namespace'
55

66
const ns = vocab($rdf)
@@ -119,6 +119,67 @@ Second line`
119119
})
120120
})
121121

122+
describe('getLatestAuthor()', () => {
123+
it('should be able to get the latest author', async () => {
124+
const mockStore = $rdf.graph()
125+
const mockPad = $rdf.sym('https://mock-pad')
126+
const mockEarlyAuthor = $rdf.sym('https:/mock-early-author')
127+
const mockLateAuthor = $rdf.sym('https:/mock-late-author')
128+
129+
const mockFirstLine = $rdf.sym('https://arbitrary-line-1')
130+
mockStore.add(mockPad, ns.pad('next'), mockFirstLine, mockPad.doc())
131+
mockStore.add(mockFirstLine, ns.sioc('content'), 'First line', mockPad.doc())
132+
mockStore.add(mockFirstLine, ns.dc('created'), new Date(0), mockPad.doc())
133+
mockStore.add(mockFirstLine, ns.dc('author'), mockEarlyAuthor, mockPad.doc())
134+
const mockSecondLine = $rdf.sym('https://arbitrary-line-2')
135+
mockStore.add(mockFirstLine, ns.pad('next'), mockSecondLine, mockPad.doc())
136+
mockStore.add(mockSecondLine, ns.sioc('content'), 'Second line', mockPad.doc())
137+
mockStore.add(mockSecondLine, ns.dc('created'), new Date(24 * 60 * 60 * 1000), mockPad.doc())
138+
mockStore.add(mockSecondLine, ns.dc('author'), mockLateAuthor, mockPad.doc())
139+
mockStore.add(mockSecondLine, ns.pad('next'), mockPad, mockPad.doc())
140+
141+
expect(getLatestAuthor(mockStore, mockPad)).toEqual(mockLateAuthor)
142+
})
143+
144+
it('should return an author even when all lines were authored at the same time', async () => {
145+
const mockStore = $rdf.graph()
146+
const mockPad = $rdf.sym('https://mock-pad')
147+
const mockEarlyAuthor = $rdf.sym('https:/mock-early-author')
148+
const mockLateAuthor = $rdf.sym('https:/mock-late-author')
149+
150+
const mockFirstLine = $rdf.sym('https://arbitrary-line-1')
151+
mockStore.add(mockPad, ns.pad('next'), mockFirstLine, mockPad.doc())
152+
mockStore.add(mockFirstLine, ns.sioc('content'), 'First line', mockPad.doc())
153+
mockStore.add(mockFirstLine, ns.dc('created'), new Date(0), mockPad.doc())
154+
mockStore.add(mockFirstLine, ns.dc('author'), mockEarlyAuthor, mockPad.doc())
155+
const mockSecondLine = $rdf.sym('https://arbitrary-line-2')
156+
mockStore.add(mockFirstLine, ns.pad('next'), mockSecondLine, mockPad.doc())
157+
mockStore.add(mockSecondLine, ns.sioc('content'), 'Second line', mockPad.doc())
158+
mockStore.add(mockSecondLine, ns.dc('created'), new Date(0), mockPad.doc())
159+
mockStore.add(mockSecondLine, ns.dc('author'), mockLateAuthor, mockPad.doc())
160+
mockStore.add(mockSecondLine, ns.pad('next'), mockPad, mockPad.doc())
161+
162+
expect(getLatestAuthor(mockStore, mockPad)).not.toBeNull()
163+
})
164+
165+
it('should return null if no author data is present', async () => {
166+
const mockStore = $rdf.graph()
167+
const mockPad = $rdf.sym('https://mock-pad')
168+
169+
const mockFirstLine = $rdf.sym('https://arbitrary-line-1')
170+
mockStore.add(mockPad, ns.pad('next'), mockFirstLine, mockPad.doc())
171+
mockStore.add(mockFirstLine, ns.sioc('content'), 'First line', mockPad.doc())
172+
mockStore.add(mockFirstLine, ns.dc('created'), new Date(0), mockPad.doc())
173+
const mockSecondLine = $rdf.sym('https://arbitrary-line-2')
174+
mockStore.add(mockFirstLine, ns.pad('next'), mockSecondLine, mockPad.doc())
175+
mockStore.add(mockSecondLine, ns.sioc('content'), 'Second line', mockPad.doc())
176+
mockStore.add(mockSecondLine, ns.dc('created'), new Date(0), mockPad.doc())
177+
mockStore.add(mockSecondLine, ns.pad('next'), mockPad, mockPad.doc())
178+
179+
expect(getLatestAuthor(mockStore, mockPad)).toBeNull()
180+
})
181+
})
182+
122183
describe('getTitle()', () => {
123184
it('should return a document\'s title', async () => {
124185
const mockStore = $rdf.graph()

scratchpad/data.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,50 @@ export function getContents (
134134

135135
return lines.join('\n')
136136
}
137+
138+
export function getLatestAuthor (
139+
store: IndexedFormula,
140+
pad: NamedNode
141+
): NamedNode | null {
142+
const [firstLineStatement] = store.statementsMatching(pad, ns.pad('next'), null, pad.doc(), true)
143+
let prevLine: Node = firstLineStatement.object
144+
const datesAndAuthors = []
145+
while (prevLine.value !== pad.value) {
146+
const [currentLineStatement] = store.statementsMatching(prevLine, ns.pad('next'), null, pad.doc(), true)
147+
const [lineDateStatement] = store.statementsMatching(
148+
currentLineStatement.subject,
149+
ns.dc('created'),
150+
null,
151+
pad.doc(),
152+
true
153+
)
154+
const [lineAuthorStatement] = store.statementsMatching(
155+
currentLineStatement.subject,
156+
ns.dc('author'),
157+
null,
158+
pad.doc(),
159+
true
160+
)
161+
if (lineDateStatement && lineAuthorStatement) {
162+
datesAndAuthors.push({
163+
created: new Date(lineDateStatement.object.value),
164+
author: lineAuthorStatement.object
165+
})
166+
}
167+
prevLine = currentLineStatement.object
168+
}
169+
170+
if (datesAndAuthors.length === 0) {
171+
return null
172+
}
173+
174+
const latestAuthor = datesAndAuthors.reduce(
175+
(latestAuthor, lineDateAndAuthor) => {
176+
return (latestAuthor.created.getTime() < lineDateAndAuthor.created.getTime())
177+
? lineDateAndAuthor
178+
: latestAuthor
179+
}
180+
)
181+
182+
return latestAuthor.author
183+
}

scratchpad/paneWrapper.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { pane } from './pane'
33
import { initialise } from './data'
44
import { Namespaces } from 'solid-namespace'
55
import UI from 'solid-ui'
6-
import { IndexedFormula } from 'rdflib'
6+
import { IndexedFormula, NamedNode } from 'rdflib'
77

88
const store: IndexedFormula = UI.store
99
const ns: Namespaces = UI.ns
@@ -41,10 +41,16 @@ const paneWrapper: PaneDefinition = {
4141
container: container,
4242
subject: subject,
4343
store: UI.store,
44+
visitNode: visitNode,
4445
user: UI.authn.currentUser()
4546
})
4647
return container
4748
}
4849
}
4950

51+
function visitNode (node: NamedNode) {
52+
const outliner = (window as any).panes.getOutliner(document)
53+
outliner.GotoSubject(node, true, undefined, true, undefined)
54+
}
55+
5056
export default paneWrapper

scratchpad/view.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ describe('View mode', () => {
3030
view({
3131
container: container,
3232
subject: mockPad,
33-
store: mockStore
33+
store: mockStore,
34+
visitNode: jest.fn()
3435
})
3536
const button = container.querySelector('button')
3637
expect(button).toBeNull()
@@ -47,7 +48,8 @@ describe('View mode', () => {
4748
container: container,
4849
subject: mockPad,
4950
store: mockStore,
50-
user: mockUser
51+
user: mockUser,
52+
visitNode: jest.fn()
5153
})
5254
const button = container.querySelector('button')
5355
expect(button).toBeDefined()
@@ -63,7 +65,8 @@ describe('View mode', () => {
6365
view({
6466
container: container,
6567
subject: mockPad,
66-
store: mockStore
68+
store: mockStore,
69+
visitNode: jest.fn()
6770
})
6871
expect(container.outerHTML).toMatchSnapshot()
6972
})
@@ -81,6 +84,7 @@ describe('Edit mode', () => {
8184
container: container,
8285
subject: mockPad,
8386
store: mockStore,
87+
visitNode: jest.fn(),
8488
user: mockUser
8589
})
8690
const button = container.querySelector('button')

scratchpad/view.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import { getContents, getSetContentsStatements } from './data'
1+
import vocab from 'solid-namespace'
2+
import $rdf from 'rdflib'
3+
import { getContents, getSetContentsStatements, getLatestAuthor } from './data'
24
import { ViewParams } from '../types'
35

4-
export function view ({ container, subject, store, user }: ViewParams) {
6+
const ns = vocab($rdf)
7+
8+
export function view ({ container, subject, store, visitNode, user }: ViewParams) {
59
toViewMode()
610

711
function toViewMode () {
@@ -14,6 +18,8 @@ export function view ({ container, subject, store, user }: ViewParams) {
1418
container.appendChild(document.createElement('br'))
1519
})
1620

21+
container.appendChild(document.createElement('hr'))
22+
1723
if (user) {
1824
const editButton = document.createElement('button')
1925
editButton.textContent = 'Edit'
@@ -22,6 +28,33 @@ export function view ({ container, subject, store, user }: ViewParams) {
2228
toEditMode()
2329
})
2430
container.appendChild(editButton)
31+
container.appendChild(document.createElement('br'))
32+
}
33+
34+
const authorContainer = document.createElement('small')
35+
container.appendChild(authorContainer)
36+
showLatestAuthor(authorContainer)
37+
}
38+
39+
/* istanbul ignore next [This function depends on a side effect (fetch), so skip it for testing for now:] */
40+
async function showLatestAuthor (authorContainer: HTMLElement) {
41+
const latestAuthor = getLatestAuthor(store, subject)
42+
if (latestAuthor) {
43+
const fetcher = $rdf.fetcher(store, {})
44+
await fetcher.load(latestAuthor.uri)
45+
const [nameStatement] = store.statementsMatching(latestAuthor, ns.vcard('fn'), null, null, true)
46+
47+
authorContainer.appendChild(document.createTextNode('Latest author: '))
48+
const authorLink = document.createElement('a')
49+
authorLink.href = latestAuthor.uri
50+
const name = (nameStatement) ? nameStatement.object.value : latestAuthor.uri
51+
authorLink.textContent = name
52+
authorLink.title = `View the profile of ${name}`
53+
authorLink.addEventListener('click', (event) => {
54+
event.preventDefault()
55+
visitNode(latestAuthor)
56+
})
57+
authorContainer.appendChild(authorLink)
2558
}
2659
}
2760

types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface ViewParams {
3030
container: HTMLElement;
3131
subject: NamedNode;
3232
store: IndexedFormula;
33+
visitNode: (node: NamedNode) => void;
3334
user?: NamedNode;
3435
};
3536

typings/rdflib/index.d.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,15 +347,15 @@ export class Statement {
347347
/**
348348
* The statement's subject
349349
*/
350-
subject: Node;
350+
subject: NamedNode;
351351
/**
352352
* The statement's predicate
353353
*/
354-
predicate: Node;
354+
predicate: NamedNode;
355355
/**
356356
* The statement's object
357357
*/
358-
object: Node;
358+
object: NamedNode;
359359
/**
360360
* The origin of this statement
361361
*/
@@ -1089,10 +1089,13 @@ export class Fetcher {
10891089
static CONTENT_TYPE_BY_EXT: {
10901090
[ext: string]: string;
10911091
};
1092+
<<<<<<< HEAD
10921093
/**
10931094
* Loads a web resource or resources into the store.
10941095
* @param uri Resource to load, provided either as a NamedNode object or a plain URL. If multiple resources are passed as an array, they will be fetched in parallel.
10951096
*/
1097+
=======
1098+
>>>>>>> d6d756b... Show a Scratchpad's author
10961099
load: (uri: NamedNode[] | string[] | NamedNode | string, options?: FetchOptions) => Promise<Response>;
10971100
}
10981101
/**

0 commit comments

Comments
 (0)