-
Notifications
You must be signed in to change notification settings - Fork 6
/
webidControl.js
327 lines (296 loc) · 13 KB
/
webidControl.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
// Render a control to record the webids we have for this agent
/* eslint-disable multiline-ternary */
import * as UI from 'solid-ui'
import { store } from 'solid-logic'
import { updateMany } from './contactLogic'
// import { renderAutoComplete } from './lib/autocompletePicker' // dbpediaParameters
import { renderAutocompleteControl } from './lib/autocompleteBar'
// import { wikidataParameters, loadPublicDataThing, wikidataClasses } from './lib/publicData' // dbpediaParameters
const $rdf = UI.rdf
const ns = UI.ns
const widgets = UI.widgets
const utils = UI.utils
const kb = store
const style = UI.style
const wikidataClasses = widgets.publicData.wikidataClasses // @@ move to solid-logic
const wikidataParameters = widgets.publicData.wikidataParameters // @@ move to solid-logic
const WEBID_NOUN = 'Solid ID'
const PUBLICID_NOUN = 'In public data'
const DOWN_ARROW = UI.icons.iconBase + 'noun_1369241.svg'
const UP_ARROW = UI.icons.iconBase + 'noun_1369237.svg'
const webidPanelBackgroundColor = '#ffe6ff'
/// ///////////////////////// Logic
export async function addWebIDToContacts (person, webid, urlType, kb) {
/*
if (!webid.startsWith('https:')) { /// @@ well we will have other protcols like DID
if (webid.startsWith('http://') {
webid = 'https:' + webid.slice(5) // @@ No, data won't match in store. Add the 's' on fetch()
} else {
throw new Error('Does not look like a webid, must start with https: ' + webid)
}
}
*/
// check this is a url
try {
const _url = new URL(webid)
} catch (error) {
throw new Error(`${WEBID_NOUN}: ${webid} is not a valid url.`)
}
// create a person's webID
console.log(`Adding to ${person} a ${WEBID_NOUN}: ${webid}.`)
const vcardURLThing = kb.bnode()
const insertables = [
$rdf.st(person, ns.vcard('url'), vcardURLThing, person.doc()),
$rdf.st(vcardURLThing, ns.rdf('type'), urlType, person.doc()),
$rdf.st(vcardURLThing, ns.vcard('value'), webid, person.doc())
]
// insert WebID in groups
// replace person with WebID in vcard:hasMember (WebID may already exist)
// insert owl:sameAs
const groups = kb.each(null, ns.vcard('hasMember'), person)
let deletables = []
groups.forEach(group => {
deletables = deletables.concat(kb.statementsMatching(group, ns.vcard('hasMember'), person, group.doc()))
insertables.push($rdf.st(group, ns.vcard('hasMember'), kb.sym(webid), group.doc())) // May exist; do we need to check?
insertables.push($rdf.st(kb.sym(webid), ns.owl('sameAs'), person, group.doc()))
})
try {
await updateMany(deletables, insertables)
} catch (err) { throw new Error(`Could not create webId ${WEBID_NOUN}: ${webid}.`) }
}
export async function removeWebIDFromContacts (person, webid, urlType, kb) {
console.log(`Removing from ${person} their ${WEBID_NOUN}: ${webid}.`)
// remove webID from card
const existing = kb.each(person, ns.vcard('url'), null, person.doc())
.filter(urlObject => kb.holds(urlObject, ns.rdf('type'), urlType, person.doc()))
.filter(urlObject => kb.holds(urlObject, ns.vcard('value'), webid, person.doc()))
if (!existing.length) {
throw new Error(`Person ${person} does not have ${WEBID_NOUN} ${webid}.`)
}
const vcardURLThing = existing[0]
const deletables = [
$rdf.st(person, ns.vcard('url'), vcardURLThing, person.doc()),
$rdf.st(vcardURLThing, ns.rdf('type'), urlType, person.doc()),
$rdf.st(vcardURLThing, ns.vcard('value'), webid, person.doc())
]
await kb.updater.update(deletables, [])
// remove webIDs from groups
const groups = kb.each(null, ns.vcard('hasMember'), kb.sym(webid))
let removeFromGroups = []
const insertInGroups = []
groups.forEach(async group => {
removeFromGroups = removeFromGroups.concat(kb.statementsMatching(kb.sym(webid), ns.owl('sameAs'), person, group.doc()))
insertInGroups.push($rdf.st(group, ns.vcard('hasMember'), person, group.doc()))
if (kb.statementsMatching(kb.sym(webid), ns.owl('sameAs'), null, group.doc()).length < 2) {
removeFromGroups = removeFromGroups.concat(kb.statementsMatching(group, ns.vcard('hasMember'), kb.sym(webid), group.doc()))
}
})
await updateMany(removeFromGroups, insertInGroups)
}
// Trace things the same as this - other IDs for same thing
// returns as array of node
export function getSameAs (kb, thing, doc) { // Should this recurse?
const found = new Set()
const agenda = new Set([thing.uri])
while (agenda.size) {
const uri = Array.from(agenda)[0] // clumsy
agenda.delete(uri)
if (found.has(uri)) continue
found.add(uri)
const node = kb.sym(uri)
kb.each(node, ns.owl('sameAs'), null, doc)
.concat(kb.each(null, ns.owl('sameAs'), node, doc))
.forEach(next => {
console.log(' OWL sameAs found ' + next)
agenda.add(next.uri)
})
kb.each(node, ns.schema('sameAs'), null, doc)
.concat(kb.each(null, ns.schema('sameAs'), node, doc))
.forEach(next => {
console.log(' Schema sameAs found ' + next)
agenda.add(next.uri)
})
}
found.delete(thing.uri) // don't want the one we knew about
return Array.from(found).map(uri => kb.sym(uri)) // return as array of nodes
}
// find person webIDs
export function getPersonas (kb, person) {
const lits = vcardWebIDs(kb, person).concat(getSameAs(kb, person, person.doc()))
const strings = new Set(lits.map(lit => lit.value)) // remove dups
const personas = [...strings].map(uri => kb.sym(uri)) // The UI tables do better with Named Nodes than Literals
personas.sort() // for repeatability
personas.filter(x => !x.sameTerm(person))
return personas
}
export function vcardWebIDs (kb, person, urlType) {
return kb.each(person, ns.vcard('url'), null, person.doc())
.filter(urlObject => kb.holds(urlObject, ns.rdf('type'), urlType, person.doc()))
.map(urlObject => kb.any(urlObject, ns.vcard('value'), null, person.doc()))
.filter(x => !!x) // remove nulls
}
export function isOrganization (agent) {
const doc = agent.doc()
return kb.holds(agent, ns.rdf('type'), ns.vcard('Organization'), doc) ||
kb.holds(agent, ns.rdf('type'), ns.schema('Organization'), doc)
}
/// ////////////////////////////////////////////////////////////// UI
// Utility function to render another different pane
export function renderNamedPane (dom, subject, paneName, dataBrowserContext) {
const p = dataBrowserContext.session.paneRegistry.byName(paneName)
const d = p.render(subject, dataBrowserContext) // @@@ change some bits of context!
d.setAttribute(
'style',
'border: 0.1em solid #444; border-radius: 0.5em'
)
return d
}
export async function renderWebIdControl (person, dataBrowserContext) {
const options = {
longPrompt: `If you know someone's ${WEBID_NOUN}, you can do more stuff with them.
To record their ${WEBID_NOUN}, drag it onto the plus, or click the plus
to enter it by hand.`,
idNoun: WEBID_NOUN,
urlType: ns.vcard('WebID')
}
return renderIdControl(person, dataBrowserContext, options)
}
export async function renderPublicIdControl (person, dataBrowserContext) {
let orgClass = kb.sym('http://www.wikidata.org/wiki/Q43229')
let orgClassId = 'Organization'
for (const classId in wikidataClasses) {
if (kb.holds(person, ns.rdf('type'), ns.schema(classId), person.doc())) {
orgClass = kb.sym(wikidataClasses[classId])
orgClassId = classId
console.log(` renderPublicIdControl bingo: ${classId} -> ${orgClass}`)
}
}
const options = {
longPrompt: `If you know the ${PUBLICID_NOUN} of this ${orgClassId}, you can do more stuff with it.
To record its ${PUBLICID_NOUN}, drag it onto the plus, or click the magnifyinng glass
to search for it in WikiData.`,
idNoun: PUBLICID_NOUN,
urlType: ns.vcard('PublicId'),
dbLookup: true,
class: orgClass, // Organization
queryParams: wikidataParameters
}
return renderIdControl(person, dataBrowserContext, options)
}
// The main control rendered by this module
export async function renderIdControl (person, dataBrowserContext, options) {
// IDs which are as WebId in VCARD data
// like :me vcard:hasURL [ a vcard:WebId; vcard:value <https://...foo> ]
//
// Display the data about x specifically stored at x.doc()
// in a fold-away thing
//
function renderPersona (dom, persona, kb) {
function profileOpenHandler (_event) {
profileIsVisible = !profileIsVisible
main.style.visibility = profileIsVisible ? 'visible' : 'collapse'
openButton.children[0].src = profileIsVisible ? UP_ARROW : DOWN_ARROW // @@ fragile
}
function renderNewRow (webidObject) {
const webid = new $rdf.Literal(webidObject.uri)
async function deleteFunction () {
try {
await removeWebIDFromContacts(person, webid, options.urlType, kb)
} catch (err) {
div.appendChild(widgets.errorMessageBlock(dom, `Error removing Id ${webid} from ${person}: ${err}`))
}
await refreshWebIDTable()
}
const isWebId = options.urlType.sameTerm(ns.vcard('WebID'))
const delFunParam = options.editable ? deleteFunction : null
const opts = { deleteFunction: delFunParam, draggable: true }
if (isWebId) {
opts.title = webidObject.uri.split('/')[2]
opts.image = widgets.faviconOrDefault(dom, webidObject.site()) // just for domain
}
const row = widgets.personTR(dom, UI.ns.foaf('knows'), webidObject, opts)
if (isWebId) {
row.children[1].textConent = opts.title // @@ will be overwritten
row.style.backgroundColor = webidPanelBackgroundColor
}
row.style.padding = '0.2em'
return row
}
const div = dom.createElement('div')
div.style.width = '100%'
const personaTable = div.appendChild(dom.createElement('table'))
personaTable.style.width = '100%'
const nav = personaTable.appendChild(renderNewRow(persona))
nav.style.width = '100%'
const mainRow = personaTable.appendChild(dom.createElement('tr'))
const mainCell = mainRow.appendChild(dom.createElement('td'))
mainCell.setAttribute('colspan', 3)
let main
let profileIsVisible = true
const rhs = nav.children[2]
const openButton = rhs.appendChild(widgets.button(dom, DOWN_ARROW, 'View', profileOpenHandler))
openButton.style.float = 'right'
delete openButton.style.backgroundColor
delete openButton.style.border
const paneName = isOrganization(person) || isOrganization(persona) ? 'profile' : 'profile' // was default for org
widgets.publicData.loadPublicDataThing(kb, person, persona).then(_resp => {
// loadPublicDataThing(kb, person, persona).then(_resp => {
try {
main = renderNamedPane(dom, persona, paneName, dataBrowserContext)
console.log('main: ', main)
main.style.width = '100%'
console.log('renderIdControl: main element: ', main)
// main.style.visibility = 'collapse'
mainCell.appendChild(main)
} catch (err) {
main = widgets.errorMessageBlock(dom, `Problem displaying persona ${persona}: ${err}`)
mainCell.appendChild(main)
}
}, err => {
main = widgets.errorMessageBlock(dom, `Error loading persona ${persona}: ${err}`)
mainCell.appendChild(main)
})
return div
} // renderPersona
async function refreshWebIDTable () {
const personas = getPersonas(kb, person)
console.log('WebId personas: ' + person + ' -> ' + personas.map(p => p.uri).join(',\n '))
prompt.style.display = personas.length ? 'none' : ''
utils.syncTableToArrayReOrdered(profileArea, personas, persona => renderPersona(dom, persona, kb))
}
async function addOneIdAndRefresh (person, webid) {
try {
await addWebIDToContacts(person, webid, options.urlType, kb)
} catch (err) {
div.appendChild(widgets.errorMessageBlock(dom, `Error adding Id ${webid} to ${person}: ${err}`))
}
await refreshWebIDTable()
}
const { dom } = dataBrowserContext
options = options || {}
options.editable = kb.updater.editable(person.doc().uri, kb)
const div = dom.createElement('div')
div.style = 'border-radius:0.3em; border: 0.1em solid #888;' // padding: 0.8em;
if (getPersonas(kb, person).length === 0 && !options.editable) {
div.style.display = 'none'
return div // No point listing an empty list you can't change
}
const h4 = div.appendChild(dom.createElement('h4'))
h4.textContent = options.idNoun
h4.style = style.formHeadingStyle
h4.style.color = style.highlightColor
const prompt = div.appendChild(dom.createElement('p'))
prompt.style = style.commentStyle
prompt.textContent = options.longPrompt
const table = div.appendChild(dom.createElement('table'))
table.style.width = '100%'
if (options.editable) { // test
options.manualURIEntry = true // introduced in solid-ui 2.4.2
options.queryParams = options.queryParams || wikidataParameters
div.appendChild(await renderAutocompleteControl(dom, person, options, addOneIdAndRefresh))
// div.appendChild(await widgets.renderAutocompleteControl(dom, person, options, addOneIdAndRefresh))
}
const profileArea = div.appendChild(dom.createElement('div'))
await refreshWebIDTable()
return div
}