src/pad/padPane.ts
import { authn, icons, ns, pad, widgets } from 'solid-ui'
// @@ TODO: serialize is not part rdflib type definitions
// Might be fixed in https://github.com/linkeddata/rdflib.js/issues/341
// @ts-ignore
import { graph, log, NamedNode, Namespace, sym, serialize, UpdateManager, Fetcher } from 'rdflib'
import { PaneDefinition } from 'pane-registry'
/* pad Pane
**
*/
const paneDef: PaneDefinition = {
// icon: (module.__dirname || __dirname) + 'images/ColourOn.png',
icon: icons.iconBase + 'noun_79217.svg',
name: 'pad',
audience: [ns.solid('PowerUser')],
// Does the subject deserve an pad pane?
label: function (subject, context) {
var t = context.session.store.findTypeURIs(subject)
if (t['http://www.w3.org/ns/pim/pad#Notepad']) {
return 'pad'
}
return null // No under other circumstances
},
mintClass: ns.pad('Notepad'),
mintNew: function (context, newPaneOptions: any) {
const store = context.session.store
const updater = store.updater as UpdateManager
if (newPaneOptions.me && !newPaneOptions.me.uri) {
throw new Error('notepad mintNew: Invalid userid')
}
var newInstance = (newPaneOptions.newInstance =
newPaneOptions.newInstance ||
store.sym(newPaneOptions.newBase + 'index.ttl#this'))
// var newInstance = kb.sym(newBase + 'pad.ttl#thisPad');
var newPadDoc = newInstance.doc()
store.add(newInstance, ns.rdf('type'), ns.pad('Notepad'), newPadDoc)
// @@ TODO Remove casting
;(store.add as any)(newInstance, ns.dc('title'), 'Shared Notes', newPadDoc)
;(store.add as any)(newInstance, ns.dc('created'), new Date(), newPadDoc)
if (newPaneOptions.me) {
store.add(newInstance, ns.dc('author'), newPaneOptions.me, newPadDoc)
}
// kb.add(newInstance, ns.pad('next'), newInstance, newPadDoc);
// linked list empty @@
var chunk = store.sym(newInstance.uri + '_line0')
store.add(newInstance, ns.pad('next'), chunk, newPadDoc) // Linked list has one entry
store.add(chunk, ns.pad('next'), newInstance, newPadDoc)
store.add(chunk, ns.dc('author'), newPaneOptions.me, newPadDoc)
// @@ TODO Remove casting
;(store.add as any)(chunk, ns.sioc('content'), '', newPadDoc)
return new Promise(function (resolve, reject) {
updater.put(
newPadDoc,
store.statementsMatching(undefined, undefined, undefined, newPadDoc),
'text/turtle',
function (uri2: string, ok: boolean, message: string) {
if (ok) {
resolve(newPaneOptions)
} else {
reject(
new Error('FAILED to save new tool at: ' + uri2 + ' : ' + message)
)
}
}
)
})
},
// and follow instructions there
// @@ TODO Set better type for paneOptions
render: function (subject, context, paneOptions: any) {
const dom = context.dom
const store = context.session.store
// Utility functions
var complainIfBad = function (ok: boolean, message: string) {
if (!ok) {
div.appendChild(widgets.errorMessageBlock(dom, message, 'pink'))
}
}
var clearElement = function (ele: HTMLElement) {
while (ele.firstChild) {
ele.removeChild(ele.firstChild)
}
return ele
}
// Access control
// Two variations of ACL for this app, public read and public read/write
// In all cases owner has read write control
var genACLtext = function (
docURI: string,
aclURI: string,
allWrite: boolean
) {
var g = graph()
var auth = Namespace('http://www.w3.org/ns/auth/acl#')
var a = g.sym(aclURI + '#a1')
var acl = g.sym(aclURI)
var doc = g.sym(docURI)
g.add(a, ns.rdf('type'), auth('Authorization'), acl)
g.add(a, auth('accessTo'), doc, acl)
g.add(a, auth('agent'), me, acl)
g.add(a, auth('mode'), auth('Read'), acl)
g.add(a, auth('mode'), auth('Write'), acl)
g.add(a, auth('mode'), auth('Control'), acl)
a = g.sym(aclURI + '#a2')
g.add(a, ns.rdf('type'), auth('Authorization'), acl)
g.add(a, auth('accessTo'), doc, acl)
g.add(a, auth('agentClass'), ns.foaf('Agent'), acl)
g.add(a, auth('mode'), auth('Read'), acl)
if (allWrite) {
g.add(a, auth('mode'), auth('Write'), acl)
}
// TODO: Figure out why `serialize` isn't on the type definition according to TypeScript:
return serialize(acl, g, aclURI, 'text/turtle')
}
/**
* @param docURI
* @param allWrite
* @param callbackFunction
*
* @returns {Promise<Response>}
*/
var setACL = function setACL (
docURI: string,
allWrite: boolean,
callbackFunction: Function
) {
// @@ TODO Remove casting of aclDoc
var aclDoc = store.any(
sym(docURI),
sym('http://www.iana.org/assignments/link-relations/acl')
) as NamedNode // @@ check that this get set by web.js
if (aclDoc) {
// Great we already know where it is
var aclText = genACLtext(docURI, (aclDoc as NamedNode).uri, allWrite)
// @@ TODO Remove casting of fetcher
return (fetcher as any)
.webOperation('PUT', (aclDoc as NamedNode).uri, {
data: aclText,
contentType: 'text/turtle'
})
.then((_result: any) => callbackFunction(true))
.catch((err: Error) => {
callbackFunction(false, err.message)
})
} else {
return fetcher
.load(docURI)
.catch((err: Error) => {
callbackFunction(false, 'Getting headers for ACL: ' + err)
})
.then(() => {
// @@ TODO Remove casting
var aclDoc = store.any(
sym(docURI),
sym('http://www.iana.org/assignments/link-relations/acl')
) as NamedNode
if (!aclDoc) {
// complainIfBad(false, "No Link rel=ACL header for " + docURI);
throw new Error('No Link rel=ACL header for ' + docURI)
}
var aclText = genACLtext(docURI, aclDoc.uri, allWrite)
// @@ TODO Remove casting of fetcher
return (fetcher as any).webOperation('PUT', aclDoc.uri, {
data: aclText,
contentType: 'text/turtle'
})
})
.then((_result: any) => callbackFunction(true))
.catch((err: Error) => {
callbackFunction(false, err.message)
})
}
}
// Reproduction: spawn a new instance
//
// Viral growth path: user of app decides to make another instance
var newInstanceButton = function () {
var button = div.appendChild(dom.createElement('button'))
button.textContent = 'Start another pad'
button.addEventListener('click', function () {
return showBootstrap(subject, spawnArea, 'pad')
})
return button
}
// Option of either using the workspace system or just typing in a URI
var showBootstrap = function showBootstrap (
thisInstance: any,
container: HTMLElement,
noun: string
) {
var div = clearElement(container)
var appDetails = { noun: 'notepad' }
div.appendChild(
authn.newAppInstance(dom, appDetails, initializeNewInstanceInWorkspace)
)
div.appendChild(dom.createElement('hr')) // @@
var p = div.appendChild(dom.createElement('p'))
p.textContent =
'Where would you like to store the data for the ' +
noun +
'? ' +
'Give the URL of the directory where you would like the data stored.'
var baseField = div.appendChild(dom.createElement('input'))
baseField.setAttribute('type', 'text')
baseField.size = 80 // really a string
;(baseField as any).label = 'base URL'
baseField.autocomplete = 'on'
div.appendChild(dom.createElement('br')) // @@
var button = div.appendChild(dom.createElement('button'))
button.textContent = 'Start new ' + noun + ' at this URI'
button.addEventListener('click', function (_e) {
var newBase = baseField.value
if (newBase.slice(-1) !== '/') {
newBase += '/'
}
initializeNewInstanceAtBase(thisInstance, newBase)
})
}
// Create new document files for new instance of app
var initializeNewInstanceInWorkspace = function (ws: NamedNode) {
// @@ TODO Clean up type for newBase
var newBase: any = store.any(ws, ns.space('uriPrefix'))
if (!newBase) {
newBase = ws.uri.split('#')[0]
} else {
newBase = newBase.value
}
if (newBase.slice(-1) !== '/') {
log.error(appPathSegment + ': No / at end of uriPrefix ' + newBase) // @@ paramater?
newBase = newBase + '/'
}
var now = new Date()
newBase += appPathSegment + '/id' + now.getTime() + '/' // unique id
initializeNewInstanceAtBase(thisInstance, newBase)
}
var initializeNewInstanceAtBase = function (
thisInstance: any,
newBase: string
) {
var here = sym(thisInstance.uri.split('#')[0])
var base = here // @@ ???
var newPadDoc = store.sym(newBase + 'pad.ttl')
var newIndexDoc = store.sym(newBase + 'index.html')
var toBeCopied = [{ local: 'index.html', contentType: 'text/html' }]
const newInstance = store.sym(newPadDoc.uri + '#thisPad')
// log.debug("\n Ready to put " + kb.statementsMatching(undefined, undefined, undefined, there)); //@@
var agenda: Function[] = []
var f // @@ This needs some form of visible progress bar
for (f = 0; f < toBeCopied.length; f++) {
var item = toBeCopied[f]
var fun = function copyItem (item: any) {
agenda.push(function () {
var newURI = newBase + item.local
console.log('Copying ' + base + item.local + ' to ' + newURI)
var setThatACL = function () {
setACL(newURI, false, function (ok: boolean, message: string) {
if (!ok) {
complainIfBad(
ok,
'FAILED to set ACL ' + newURI + ' : ' + message
)
console.log('FAILED to set ACL ' + newURI + ' : ' + message)
} else {
agenda.shift()!() // beware too much nesting
}
})
}
;(store as any).fetcher // @@ TODO Remove casting
.webCopy(
base + item.local,
newBase + item.local,
item.contentType
)
.then(() => authn.checkUser())
.then((webId: string) => {
me = webId
setThatACL()
})
.catch((err: Error) => {
console.log(
'FAILED to copy ' + base + item.local + ' : ' + err.message
)
complainIfBad(
false,
'FAILED to copy ' + base + item.local + ' : ' + err.message
)
})
})
}
fun(item)
}
agenda.push(function createNewPadDataFile () {
store.add(newInstance, ns.rdf('type'), PAD('Notepad'), newPadDoc)
// TODO @@ Remove casting of add
;(store.add as any)(
newInstance,
ns.dc('created'),
new Date(),
newPadDoc
)
if (me) {
store.add(newInstance, ns.dc('author'), me, newPadDoc)
}
store.add(newInstance, PAD('next'), newInstance, newPadDoc) // linked list empty
// Keep a paper trail @@ Revisit when we have non-public ones @@ Privacy
store.add(newInstance, ns.space('inspiration'), thisInstance, padDoc)
store.add(newInstance, ns.space('inspiration'), thisInstance, newPadDoc)
updater.put(
newPadDoc,
store.statementsMatching(undefined, undefined, undefined, newPadDoc),
'text/turtle',
function (_uri2: string, ok: boolean, message: string) {
if (ok) {
agenda.shift()!()
} else {
complainIfBad(
ok,
'FAILED to save new notepad at: ' +
newPadDoc.uri +
' : ' +
message
)
console.log(
'FAILED to save new notepad at: ' +
newPadDoc.uri +
' : ' +
message
)
}
}
)
})
agenda.push(function () {
setACL(newPadDoc.uri, true, function (ok: boolean, body: string) {
complainIfBad(
ok,
'Failed to set Read-Write ACL on pad data file: ' + body
)
if (ok) agenda.shift()!()
})
})
agenda.push(function () {
// give the user links to the new app
var p = div.appendChild(dom.createElement('p'))
p.setAttribute('style', 'font-size: 140%;')
p.innerHTML =
"Your <a href='" +
newIndexDoc.uri +
"'><b>new notepad</b></a> is ready. " +
"<br/><br/><a href='" +
newIndexDoc.uri +
"'>Go to new pad</a>"
})
agenda.shift()!()
// Created new data files.
}
// Update on incoming changes
var showResults = function (exists: boolean) {
console.log('showResults()')
me = authn.currentUser()
authn.checkUser().then((webId: string) => {
me = webId
})
var title =
store.any(subject, ns.dc('title')) || store.any(subject, ns.vcard('fn'))
if (paneOptions.solo && typeof window !== 'undefined' && title) {
window.document.title = title.value
}
options.exists = exists
padEle = pad.notepad(dom, padDoc, subject, me, options)
naviMain.appendChild(padEle)
var partipationTarget =
store.any(subject, ns.meeting('parentMeeting')) || subject
pad.manageParticipation(
dom,
naviMiddle2,
padDoc,
partipationTarget,
me,
options
)
// @@ TODO Remove casting of updater
;(store.updater as any).setRefreshHandler(padDoc, padEle.reloadAndSync) // initiated =
}
// Read or create empty data file
var loadPadData = function () {
// @@ TODO Remove casting of fetcher
;(fetcher as any).nowOrWhenFetched(padDoc.uri, undefined, function (
ok: boolean,
body: string,
response: any
) {
if (!ok) {
if (response.status === 404) {
// / Check explicitly for 404 error
console.log('Initializing results file ' + padDoc)
updater.put(padDoc, [], 'text/turtle', function (
_uri2: string,
ok: boolean,
message: string
) {
if (ok) {
clearElement(naviMain)
showResults(false)
} else {
complainIfBad(
ok,
'FAILED to create results file at: ' +
padDoc.uri +
' : ' +
message
)
console.log(
'FAILED to craete results file at: ' +
padDoc.uri +
' : ' +
message
)
}
})
} else {
// Other error, not 404 -- do not try to overwite the file
complainIfBad(ok, 'FAILED to read results file: ' + body)
}
} else {
// Happy read
clearElement(naviMain)
if (store.holds(subject, ns.rdf('type'), ns.wf('TemplateInstance'))) {
showBootstrap(subject, naviMain, 'pad')
}
showResults(true)
naviMiddle3.appendChild(newInstanceButton())
}
})
}
// Body of Pane
var appPathSegment = 'app-pad.timbl.com' // how to allocate this string and connect to
// @@ TODO Remove castings
const fetcher = (store as any).fetcher as Fetcher
const updater = (store as any).updater as UpdateManager
var me: any
var PAD = Namespace('http://www.w3.org/ns/pim/pad#')
var thisInstance = subject
var padDoc = subject.doc()
var padEle
var div = dom.createElement('div')
// Build the DOM
var structure = div.appendChild(dom.createElement('table')) // @@ make responsive style
structure.setAttribute(
'style',
'background-color: white; min-width: 94%; margin-right:3% margin-left: 3%; min-height: 13em;'
)
var naviLoginoutTR = structure.appendChild(dom.createElement('tr'))
naviLoginoutTR.appendChild(dom.createElement('td')) // naviLoginout1
naviLoginoutTR.appendChild(dom.createElement('td'))
naviLoginoutTR.appendChild(dom.createElement('td'))
var naviTop = structure.appendChild(dom.createElement('tr')) // stuff
var naviMain = naviTop.appendChild(dom.createElement('td'))
naviMain.setAttribute('colspan', '3')
var naviMiddle = structure.appendChild(dom.createElement('tr')) // controls
var naviMiddle1 = naviMiddle.appendChild(dom.createElement('td'))
var naviMiddle2 = naviMiddle.appendChild(dom.createElement('td'))
var naviMiddle3 = naviMiddle.appendChild(dom.createElement('td'))
var naviStatus = structure.appendChild(dom.createElement('tr')) // status etc
var statusArea = naviStatus.appendChild(dom.createElement('div'))
var naviSpawn = structure.appendChild(dom.createElement('tr')) // create new
var spawnArea = naviSpawn.appendChild(dom.createElement('div'))
var naviMenu = structure.appendChild(dom.createElement('tr'))
naviMenu.setAttribute('class', 'naviMenu')
// naviMenu.setAttribute('style', 'margin-top: 3em;');
naviMenu.appendChild(dom.createElement('td')) // naviLeft
naviMenu.appendChild(dom.createElement('td'))
naviMenu.appendChild(dom.createElement('td'))
var options: any = { statusArea: statusArea, timingArea: naviMiddle1 }
loadPadData()
return div
}
}
// ends
export default paneDef