contacts-pane 2.4.8 → 2.4.12-beta4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.eslintrc +3 -1
  2. package/.github/workflows/ci.yml +74 -0
  3. package/.nvmrc +1 -1
  4. package/LICENSE.md +0 -0
  5. package/Makefile +0 -0
  6. package/README.md +0 -0
  7. package/__tests__/unit/data-reformat-test.js +166 -0
  8. package/__tests__/unit/data-reformat-test.ts +185 -0
  9. package/__tests__/unit/setup.js +74 -0
  10. package/__tests__/unit/setup.ts +77 -0
  11. package/babel.config.js +13 -0
  12. package/card.ai +0 -0
  13. package/card.png +0 -0
  14. package/contactLogic.js +84 -7
  15. package/contactsPane.js +64 -20
  16. package/diff.txt +0 -0
  17. package/exampleOfOpenData/mit-wikidata-details.ttl +0 -0
  18. package/exampleOfOpenData/mit-wikidata-query.sparql +0 -0
  19. package/exampleOfOpenData/wikidata-get.json +0 -0
  20. package/groupMembershipControl.js +56 -13
  21. package/individual.js +3 -2
  22. package/individualForm.js +0 -0
  23. package/jest.config.js +11 -0
  24. package/jest.setup.ts +10 -0
  25. package/lib/autocompleteBar.js +99 -0
  26. package/lib/autocompleteBar.js.map +1 -0
  27. package/lib/autocompleteField.js +157 -0
  28. package/lib/autocompleteField.js.map +1 -0
  29. package/lib/autocompletePicker.js +240 -0
  30. package/lib/autocompletePicker.js.map +1 -0
  31. package/lib/forms.js +315 -0
  32. package/lib/instituteDetailsQuery.js +38 -0
  33. package/lib/publicData.js +387 -0
  34. package/lib/publicData.js.map +1 -0
  35. package/lib/vcard.js +916 -0
  36. package/mintNewAddressBook.js +5 -4
  37. package/mugshotGallery.js +16 -4
  38. package/organizationForm.js +0 -0
  39. package/organizationForm.ttl +0 -0
  40. package/package.json +26 -13
  41. package/shapes/contacts-shapes.ttl +0 -0
  42. package/src/autocompleteBar.ts +92 -0
  43. package/src/autocompleteField.ts +180 -0
  44. package/src/autocompletePicker.ts +253 -0
  45. package/src/forms.ttl +1 -1
  46. package/src/instituteDetailsQuery.sparql +0 -0
  47. package/src/publicData.ts +385 -0
  48. package/src/vcard.ttl +0 -0
  49. package/toolsPane.js +70 -19
  50. package/tsconfig.json +16 -0
  51. package/webidControl.js +49 -4
@@ -0,0 +1,385 @@
1
+ /* Logic to access public data stores
2
+ *
3
+ * including filtering resut by natural language etc
4
+ */
5
+ import { Literal, NamedNode, parse } from 'rdflib'
6
+ import { store } from 'solid-logic'
7
+ import { ns } from 'solid-ui'
8
+ import * as instituteDetailsQuery from '../lib/instituteDetailsQuery.js'
9
+
10
+ export const AUTOCOMPLETE_LIMIT = 3000 // How many to get from server
11
+
12
+ const subjectRegexp = /\$\(subject\)/g
13
+
14
+ interface Term {
15
+ type: string;
16
+ value: string
17
+ }
18
+
19
+ interface Binding {
20
+ subject: Term;
21
+ name?: Term
22
+ location?: Term
23
+ coordinates?: Term
24
+ }
25
+
26
+ type Bindings = Binding[]
27
+
28
+ export type QueryParameters =
29
+ { label: string;
30
+ logo: string;
31
+ searchByNameQuery?: string;
32
+ searchByNameURI?: string;
33
+ insitituteDetailsQuery?: string;
34
+ endpoint?: string;
35
+ class: object
36
+ }
37
+
38
+ // Schema.org seems to suggest NGOs are non-profit and Corporaions are for-profit
39
+ // but doesn't have explicit classes
40
+ export const wikidataClasses = {
41
+ Corporation: 'http://www.wikidata.org/entity/Q6881511', // Enterprise is for-profit
42
+ EducationalOrganization: 'http://www.wikidata.org/entity/Q178706', // insitution
43
+ GovernmentOrganization: 'http://www.wikidata.org/entity/Q327333', // government agency
44
+ MedicalOrganization: 'http://www.wikidata.org/entity/Q4287745',
45
+ MusicGroup: 'http://www.wikidata.org/entity/Q32178211', // music organization
46
+ NGO: 'http://www.wikidata.org/entity/Q163740', // nonprofit organization @@
47
+ Occupation: 'http://www.wikidata.org/entity/Q28640', // Profession
48
+ // Organization: 'http://www.wikidata.org/entity/Q43229',
49
+ Project: 'http://www.wikidata.org/entity/Q170584',
50
+ SportsOrganization: 'http://www.wikidata.org/entity/Q4438121',
51
+ }
52
+
53
+ export async function getPreferredLanguages () {
54
+ return [ 'fr', 'en', 'de', 'it'] // @@ testing only -- code me later
55
+ }
56
+ export const escoParameters:QueryParameters = {
57
+ label: 'ESCO',
58
+ logo: 'https://ec.europa.eu/esco/portal/static_resource2/images/logo/logo_en.gif',
59
+ searchByNameQuery: null, // No sparql endpoint
60
+ searchByNameURI: 'https://ec.europa.eu/esco/api/search?language=$(language)&type=occupation&text=$(name)',
61
+ endpoint: null,
62
+ class: {}
63
+ }
64
+
65
+ export const dbpediaParameters:QueryParameters = {
66
+ label: 'DBPedia',
67
+ logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/DBpediaLogo.svg/263px-DBpediaLogo.svg.png',
68
+ searchByNameQuery: `select distinct ?subject, ?name where {
69
+ ?subject a $(class); rdfs:label ?name
70
+ FILTER regex(?name, "$(name)", "i")
71
+ } LIMIT $(limit)`,
72
+ endpoint: 'https://dbpedia.org/sparql/',
73
+ class: { AcademicInsitution: 'http://umbel.org/umbel/rc/EducationalOrganization'}
74
+ }
75
+
76
+ export const wikidataParameters = {
77
+ label: 'WikiData',
78
+ logo: 'https://www.wikimedia.org/static/images/project-logos/wikidatawiki.png',
79
+ endpoint: 'https://query.wikidata.org/sparql',
80
+ class: { AcademicInsitution: 'http://www.wikidata.org/entity/Q4671277',
81
+ Enterprise: 'http://www.wikidata.org/entity/Q6881511',
82
+ Business: 'http://www.wikidata.org/entity/Q4830453',
83
+ NGO: 'http://www.wikidata.org/entity/Q79913',
84
+ CharitableOrganization: 'http://www.wikidata.org/entity/Q708676',
85
+ Insitute: 'http://www.wikidata.org/entity/Q1664720',
86
+ },
87
+ searchByNameQuery: `SELECT ?subject ?name
88
+ WHERE {
89
+ ?klass wdt:P279* $(class) .
90
+ ?subject wdt:P31 ?klass .
91
+ ?subject rdfs:label ?name.
92
+ FILTER regex(?name, "$(name)", "i")
93
+ } LIMIT $(limit) `, // was SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en" }
94
+
95
+ insitituteDetailsQuery: `CONSTRUCT
96
+ { wd:Q49108 schema:name ?itemLabel;
97
+ schema:logo ?logo;
98
+ schema:logo ?sealImage;
99
+ schema:subOrganization ?subsidiary .
100
+ ?subsidiary schema:name ?subsidiaryLabel .
101
+ }
102
+ WHERE
103
+ {
104
+ wd:Q49108 # rdfs:label ?itemLabel ;
105
+ wdt:P154 ?logo;
106
+ wdt:P158 ?sealImage ;
107
+ wdt:P355 ?subsidiary .
108
+ # ?subsidiary rdfs:label ?subsidiaryLabel .
109
+
110
+ SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE], fr". }
111
+ }`
112
+ }
113
+
114
+ /* From an array of bindings with a names for each row,
115
+ * remove dupliacte names for the same thing, leaving the user's
116
+ * preferred language version
117
+ */
118
+ export function filterByLanguage (bindings, languagePrefs) {
119
+ let uris = {}
120
+ bindings.forEach(binding => { // Organize names by their subject
121
+ const uri = binding.subject.value
122
+ uris[uri] = uris[uri] || []
123
+ uris[uri].push(binding)
124
+ })
125
+
126
+ var languagePrefs2 = languagePrefs
127
+ languagePrefs2.reverse() // prefered last
128
+
129
+ var slimmed = []
130
+ for (const u in uris) { // needs hasOwnProperty ?
131
+ const bindings = uris[u]
132
+ const sortMe = bindings.map(binding => {
133
+ return [ languagePrefs2.indexOf(binding.name['xml:lang']), binding]
134
+ })
135
+ sortMe.sort() // best at th ebottom
136
+ sortMe.reverse() // best at the top
137
+ slimmed.push(sortMe[0][1])
138
+ } // map u
139
+ console.log(` Filter by language: ${bindings.length} -> ${slimmed.length}`)
140
+ return slimmed
141
+ }
142
+
143
+ export var wikidataClassMap = {
144
+ 'http://www.wikidata.org/entity/Q15936437': ns.schema('CollegeOrUniversity'), // research university
145
+ 'http://www.wikidata.org/entity/Q1664720': ns.schema('EducationalOrganization'), // insitute @@
146
+ 'http://www.wikidata.org/entity/Q43229': ns.schema('Organization'), // research university
147
+ 'http://www.wikidata.org/entity/Q3918': ns.schema('CollegeOrUniversity'), // university
148
+ 'http://www.wikidata.org/entity/Q170584': ns.schema('Project'), // university
149
+ 'http://www.wikidata.org/entity/Q327333': ns.schema('GovernmentOrganization'), // gobvt agency
150
+ 'http://www.wikidata.org/entity/Q2221906': ns.schema('Place'), // geographic location
151
+
152
+ }
153
+ export var predMap = { // allow other mappings top added in theory
154
+ class: ns.rdf('type'),
155
+ // logo: ns.schema('logo'),
156
+ sealImage: ns.schema('logo'),
157
+ //image: ns.schema('image'), defaults to shema
158
+ shortName:ns.foaf('nick'),
159
+ subsidiary: ns.schema('subOrganization')
160
+ }
161
+
162
+ export function loadFromBindings (kb, solidSubject:NamedNode, bindings, doc) {
163
+ var results = {}
164
+ console.log(`loadFromBindings: subject: ${solidSubject}`)
165
+ console.log(` doc: ${doc}`)
166
+ bindings.forEach(binding => {
167
+ for (const key in binding) {
168
+ const result = binding[key]
169
+ const combined = JSON.stringify(result) // ( result.type, result.value )
170
+ results[key] = results[key] || new Set()
171
+ results[key].add(combined) // remove duplicates
172
+ }
173
+ })
174
+ for (const key in results) {
175
+ const values = results[key]
176
+ console.log(` results ${key} -> ${values}`)
177
+ values.forEach(combined => {
178
+ const result = JSON.parse(combined)
179
+ const { type, value } = result
180
+ var obj
181
+ if (type === 'uri') {
182
+ obj = kb.sym(value)
183
+ } else if (type === 'literal') {
184
+ obj = new Literal(value, result.language, result.datatype)
185
+ } else {
186
+ throw new Error(`loadFromBindings: unexpected type: ${type}`)
187
+ }
188
+ if (key == 'type') {
189
+ if (wikidataClassMap[value]) {
190
+ obj = wikidataClassMap[value]
191
+ } else {
192
+ console.warn('Unmapped Wikidata Class: ' + value)
193
+ }
194
+ } else if (key === 'coordinates') {
195
+ // const latlong = value // Like 'Point(-71.106111111 42.375)'
196
+ console.log(' @@@ hey a point: ' + value)
197
+ const regexp =/.*\(([-0-9\.-]*) ([-0-9\.-]*)\)/
198
+ const match = regexp.exec(value)
199
+ const float = ns.xsd('float')
200
+ const latitude = new Literal(match[1], null, float)
201
+ const longitude = new Literal(match[2], null, float)
202
+ kb.add(solidSubject, ns.schema('longitude'), longitude, doc)
203
+ kb.add(solidSubject, ns.schema('latitude'), latitude, doc)
204
+ } else if (predMap[key]) {
205
+ const pred = predMap[key] || ns.schema(key) // fallback to just using schema.org
206
+ kb.add(solidSubject, pred, obj, doc) // @@ deal with non-string and objects
207
+ console.log(` public data ${pred} ${obj}.`)
208
+ }
209
+ })
210
+ }
211
+ }
212
+
213
+ /* ESCO sopecific
214
+ */
215
+
216
+ /* Query all entities of given class and partially matching name
217
+ */
218
+ export async function queryESCODataByName (filter: string, theClass:NamedNode, queryTarget: QueryParameters): Promise<Bindings> {
219
+ const queryURI = queryTarget.searchByNameURI
220
+ .replace('$(name)', filter)
221
+ .replace('$(limit)', '' + AUTOCOMPLETE_LIMIT)
222
+ .replace('$(class)', theClass)
223
+ console.log('Querying ESCO data - uri: ' + queryURI)
224
+
225
+ const options = { credentials: 'omit',
226
+ headers: { 'Accept': 'application/json'}
227
+ } // CORS
228
+ var response
229
+ response = await store.fetcher.webOperation('GET', queryURI, options)
230
+ //complain('Error querying db of organizations: ' + err)
231
+ const text = response.responseText
232
+ console.log(' Query result text' + text.slice(0,500) + '...')
233
+ if (text.length === 0) throw new Error('Wot no text back from ESCO query ' + queryURI)
234
+ const json = JSON.parse(text)
235
+ console.log(' Query result JSON' + JSON.stringify(json, null, 4).slice(0,500) + '...')
236
+
237
+ const results = json._embedded.results // Array
238
+ const bindings = results.map(result => {
239
+ const name = result.title
240
+ const uri = result.uri // like http://data.europa.eu/esco/occupation/57af9090-55b4-4911-b2d0-86db01c00b02
241
+ return { name: { value: name, type: 'literal'}, uri: {type: 'IRI', value: uri}} // simulate SPARQL bindings
242
+ })
243
+ return bindings
244
+ // return queryPublicDataSelect(sparql, queryTarget)
245
+ }
246
+
247
+
248
+ /* Query all entities of given class and partially matching name
249
+ */
250
+ export async function queryPublicDataByName (filter: string, theClass:NamedNode, queryTarget: QueryParameters): Promise<Bindings> {
251
+ const sparql = queryTarget.searchByNameQuery
252
+ .replace('$(name)', filter)
253
+ .replace('$(limit)', '' + AUTOCOMPLETE_LIMIT)
254
+ .replace('$(class)', theClass)
255
+ console.log('Querying public data - sparql: ' + sparql)
256
+ return queryPublicDataSelect(sparql, queryTarget)
257
+ }
258
+
259
+ export async function queryPublicDataSelect (sparql: string, queryTarget: QueryParameters): Promise<Bindings> {
260
+ const myUrlWithParams = new URL(queryTarget.endpoint);
261
+ myUrlWithParams.searchParams.append("query", sparql);
262
+ const queryURI = myUrlWithParams.href
263
+ console.log(' queryPublicDataSelect uri: ' + queryURI);
264
+
265
+ const options = { credentials: 'omit',
266
+ headers: { 'Accept': 'application/json'}
267
+ } // CORS
268
+ var response
269
+ response = await store.fetcher.webOperation('GET', queryURI, options)
270
+ //complain('Error querying db of organizations: ' + err)
271
+ const text = response.responseText
272
+ // console.log(' Query result text' + text.slice(0,100) + '...')
273
+ if (text.length === 0) throw new Error('Wot no text back from query ' + queryURI)
274
+ const json = JSON.parse(text)
275
+ console.log(' Query result JSON' + JSON.stringify(json, null, 4).slice(0,100) + '...')
276
+ const bindings = json.results.bindings
277
+ return bindings
278
+ }
279
+
280
+ export async function queryPublicDataConstruct (sparql: string, pubicId: NamedNode, queryTarget: QueryParameters): Promise<Bindings> {
281
+ console.log('queryPublicDataConstruct: sparql:', sparql)
282
+ const myUrlWithParams = new URL(queryTarget.endpoint);
283
+ myUrlWithParams.searchParams.append("query", sparql);
284
+ const queryURI = myUrlWithParams.href
285
+ console.log(' queryPublicDataConstruct uri: ' + queryURI);
286
+ const options = { credentials: 'omit', // CORS
287
+ headers: { 'Accept': 'text/turtle'}
288
+ }
289
+ const response = await store.fetcher.webOperation('GET', queryURI, options)
290
+ const text = response.responseText
291
+ const report = text.lenth > 500 ? text.slice(0,200) + ' ... ' + text.slice(-200) : text
292
+ console.log(' queryPublicDataConstruct result text:' + report)
293
+ if (text.length === 0) throw new Error('queryPublicDataConstruct: No text back from construct query:' + queryURI)
294
+ parse(text, store, pubicId.uri, 'text/turtle')
295
+ return
296
+ }
297
+
298
+ export async function loadPublicDataThing (kb, subject: NamedNode, publicDataID: NamedNode) {
299
+
300
+ if (publicDataID.uri.startsWith('https://dbpedia.org/resource/')) {
301
+ return getDbpediaDetails(kb, subject, publicDataID)
302
+ } else if (publicDataID.uri.match(/^https?:\/\/www\.wikidata\.org\/entity\/.*/)) {
303
+ const QId = publicDataID.uri.split('/')[4]
304
+ const dataURI = `http://www.wikidata.org/wiki/Special:EntityData/${QId}.ttl`
305
+ // In fact loading the data URI gives much to much irrelevant data, from wikidata.
306
+ await getWikidataDetails(kb, subject, publicDataID)
307
+ // await getWikidataLocation(kb, subject, publicDataID) -- should get that in the details query now
308
+ } else {
309
+ const iDToFetch = publicDataID.uri.startsWith('http:') ? kb.sym('https:' + publicDataID.uri.slice(5))
310
+ : publicDataID
311
+ return kb.fetcher.load(iDToFetch, { credentials: 'omit',
312
+ headers: { 'Accept': 'text/turtle'}
313
+ })
314
+ }
315
+ }
316
+
317
+ export async function getWikidataDetails (kb, solidSubject:NamedNode, publicDataID:NamedNode) {
318
+ const subjRegexp = /wd:Q49108/g
319
+ const sparql = instituteDetailsQuery.replace(subjRegexp, publicDataID)
320
+ await queryPublicDataConstruct(sparql, publicDataID, wikidataParameters)
321
+ console.log('getWikidataDetails: loaded.', publicDataID)
322
+ }
323
+
324
+ export async function getWikidataDetailsOld (kb, solidSubject:NamedNode, publicDataID:NamedNode) {
325
+ const sparql = `select distinct * where {
326
+ optional { $(subject) wdt:P31 ?class } # instance of
327
+ optional { $(subject) wdt:P154 ?logo }
328
+ optional { $(subject) wdt:P158 ?sealImage }
329
+ # optional { $(subject) wdt:P159 ?headquartersLocation }
330
+
331
+ optional { $(subject) wdt:P17 ?country }
332
+ optional { $(subject) wdt:P18 ?image }
333
+ optional { $(subject) wdt:P1813 ?shortName }
334
+
335
+ optional { $(subject) wdt:P355 ?subsidiary }
336
+ # SERVICE wikibase:label { bd:serviceParam wikibase:language "fr,en,de,it" }
337
+ }`
338
+ .replace(subjectRegexp, publicDataID)
339
+ const bindings = await queryPublicDataSelect(sparql, wikidataParameters)
340
+ loadFromBindings (kb, publicDataID, bindings, publicDataID.doc()) //arg2 was solidSubject
341
+ }
342
+
343
+ export async function getWikidataLocation (kb, solidSubject:NamedNode, publicDataID:NamedNode) {
344
+ const sparql = `select distinct * where {
345
+
346
+ $(subject) wdt:P276 ?location .
347
+
348
+ optional { ?location wdt:P2044 ?elevation }
349
+ optional { ?location wdt:P131 ?region }
350
+ optional { ?location wdt:P625 ?coordinates }
351
+ optional { ?location wdt:P17 ?country }
352
+
353
+ # SERVICE wikibase:label { bd:serviceParam wikibase:language "fr,en,de,it" }
354
+ }`.replace(subjectRegexp, publicDataID)
355
+ console.log( ' location query sparql:' + sparql)
356
+ const bindings = await queryPublicDataSelect(sparql, wikidataParameters)
357
+ console.log(' location query bindings:', bindings)
358
+ loadFromBindings (kb, publicDataID, bindings, publicDataID.doc()) // was solidSubject
359
+ }
360
+
361
+
362
+ export async function getDbpediaDetails (kb, solidSubject:NamedNode, publicDataID:NamedNode) {
363
+ // Note below the string form of the named node with <> works in SPARQL
364
+ const sparql = `select distinct ?city, ?state, ?country, ?homepage, ?logo, ?lat, ?long, WHERE {
365
+ OPTIONAL { <${publicDataID}> <http://dbpedia.org/ontology/city> ?city }
366
+ OPTIONAL { ${publicDataID} <http://dbpedia.org/ontology/state> ?state }
367
+ OPTIONAL { ${publicDataID} <http://dbpedia.org/ontology/country> ?country }
368
+ OPTIONAL { ${publicDataID} foaf:homepage ?homepage }
369
+ OPTIONAL { ${publicDataID} foaf:lat ?lat; foaf:long ?long }
370
+ OPTIONAL { ${publicDataID} <http://dbpedia.org/ontology/country> ?country }
371
+ }`
372
+ const predMap = {
373
+ city: ns.vcard('locality'),
374
+ state: ns.vcard('region'),
375
+ country: ns.vcard('country-name'),
376
+ homepage: ns.foaf('homepage'),
377
+ lat: ns.geo('latitude'),
378
+ long: ns.geo('longitude'),
379
+ }
380
+ const bindings = await queryPublicDataSelect(sparql, dbpediaParameters)
381
+ bindings.forEach(binding => {
382
+ const uri = binding.subject.value // @@ To be written
383
+ const name = binding.name.value
384
+ })
385
+ }
package/src/vcard.ttl CHANGED
File without changes
package/toolsPane.js CHANGED
@@ -3,7 +3,8 @@
3
3
  /* global confirm, $rdf */
4
4
 
5
5
  import * as UI from 'solid-ui'
6
- import { saveNewGroup, addPersonToGroup } from './contactLogic'
6
+ import { store } from 'solid-logic'
7
+ import { saveNewGroup, addPersonToGroup, groupMembers } from './contactLogic'
7
8
  export function toolsPane (
8
9
  selectAllGroups,
9
10
  selectedGroups,
@@ -13,7 +14,7 @@ export function toolsPane (
13
14
  me
14
15
  ) {
15
16
  const dom = dataBrowserContext.dom
16
- const kb = UI.store
17
+ const kb = store
17
18
  const ns = UI.ns
18
19
  const VCARD = ns.vcard
19
20
 
@@ -68,7 +69,7 @@ export function toolsPane (
68
69
 
69
70
  //
70
71
  try {
71
- await UI.authn.registrationControl(context, book, ns.vcard('AddressBook'))
72
+ await UI.login.registrationControl(context, book, ns.vcard('AddressBook'))
72
73
  } catch (e) {
73
74
  UI.widgets.complain(context, 'registrationControl: ' + e)
74
75
  }
@@ -84,7 +85,9 @@ export function toolsPane (
84
85
  function stats () {
85
86
  const totalCards = kb.each(undefined, VCARD('inAddressBook'), book).length
86
87
  log('' + totalCards + ' cards loaded. ')
87
- const groups = kb.each(book, VCARD('includesGroup'))
88
+ let groups = kb.each(book, VCARD('includesGroup'))
89
+ const strings = new Set(groups.map(group => group.uri)) // remove dups
90
+ groups = [...strings].map(uri => kb.sym(uri))
88
91
  log('' + groups.length + ' total groups. ')
89
92
  const gg = []
90
93
  for (const g in selectedGroups) {
@@ -136,7 +139,7 @@ export function toolsPane (
136
139
 
137
140
  for (let i = 0; i < gg.length; i++) {
138
141
  const g = kb.sym(gg[i])
139
- const a = kb.each(g, ns.vcard('hasMember'))
142
+ const a = groupMembers(kb, g)
140
143
  log(UI.utils.label(g) + ': ' + a.length + ' members')
141
144
  for (let j = 0; j < a.length; j++) {
142
145
  const card = a[j]
@@ -160,7 +163,7 @@ export function toolsPane (
160
163
  stats.nameEmailIndex = kb.any(book, ns.vcard('nameEmailIndex'))
161
164
  log('Loading name index...')
162
165
 
163
- UI.store.fetcher.nowOrWhenFetched(
166
+ store.fetcher.nowOrWhenFetched(
164
167
  stats.nameEmailIndex,
165
168
  undefined,
166
169
  function (_ok, _message) {
@@ -356,6 +359,7 @@ export function toolsPane (
356
359
  const other = stats.nameLessIndex[cardText]
357
360
  if (other) {
358
361
  log(' Matches with ' + other)
362
+ // alain not sure it works we may need to concat with 'sameAs' group.doc (.map(st => st.why))
359
363
  const cardGroups = kb.each(null, ns.vcard('hasMember'), card)
360
364
  const otherGroups = kb.each(null, ns.vcard('hasMember'), other)
361
365
  for (let j = 0; j < cardGroups.length; j++) {
@@ -431,9 +435,9 @@ export function toolsPane (
431
435
  for (let i = 0; i < stats.uniques.length; i++) {
432
436
  stats.uniquesSet[stats.uniques[i].uri] = true
433
437
  }
434
- stats.groupMembers = kb
435
- .statementsMatching(null, ns.vcard('hasMember'))
436
- .map(st => st.object)
438
+ stats.groupMembers = []
439
+ kb.each(null, ns.vcard('hasMember'))
440
+ .map(group => { stats.groupMembers = stats.groupMembers.concat(groupMembers(kb, group)) })
437
441
  log(' Naive group members ' + stats.groupMembers.length)
438
442
  stats.groupMemberSet = []
439
443
  for (let j = 0; j < stats.groupMembers.length; j++) {
@@ -574,7 +578,10 @@ export function toolsPane (
574
578
  log(' Regenerating group of uniques...' + cleanGroup)
575
579
  const data = sz.statementsToN3(sts)
576
580
 
577
- return kb.fetcher.webOperation('PUT', cleanGroup, { data })
581
+ return kb.fetcher.webOperation('PUT', cleanGroup, {
582
+ data: data,
583
+ contentType: 'text/turtle'
584
+ })
578
585
  })
579
586
  .then(() => {
580
587
  log(' Done uniques group ' + cleanGroup)
@@ -614,12 +621,14 @@ export function toolsPane (
614
621
  .then(scanForDuplicates)
615
622
  .then(checkGroupMembers)
616
623
  .then(checkAllNameless)
617
- .then((resolve, reject) => {
618
- if (confirm('Write new clean versions?')) {
619
- resolve(true)
620
- } else {
621
- reject()
622
- }
624
+ .then(() => {
625
+ return new Promise(function (resolve, reject) {
626
+ if (confirm('Write new clean versions?')) {
627
+ resolve(true)
628
+ } else {
629
+ reject()
630
+ }
631
+ })
623
632
  })
624
633
  .then(saveCleanPeople)
625
634
  .then(saveAllGroups)
@@ -659,13 +668,15 @@ export function toolsPane (
659
668
 
660
669
  const reverseIndex = {}
661
670
  const groupless = []
662
- const groups = kb.each(book, VCARD('includesGroup'))
663
-
671
+ let groups = kb.each(book, VCARD('includesGroup'))
672
+ const strings = new Set(groups.map(group => group.uri)) // remove dups
673
+ groups = [...strings].map(uri => kb.sym(uri))
664
674
  log('' + groups.length + ' total groups. ')
665
675
 
666
676
  for (let i = 0; i < groups.length; i++) {
667
677
  const g = groups[i]
668
- const a = kb.each(g, ns.vcard('hasMember'))
678
+ const a = groupMembers(kb, g)
679
+
669
680
  log(UI.utils.label(g) + ': ' + a.length + ' members')
670
681
  for (let j = 0; j < a.length; j++) {
671
682
  kb.allAliases(a[j]).forEach(function (y) {
@@ -713,6 +724,46 @@ export function toolsPane (
713
724
  fixGrouplessButton.style.cssText = buttonStyle
714
725
  fixGrouplessButton.textContent = 'Put all individuals with no group in a new group'
715
726
  fixGrouplessButton.addEventListener('click', _event => fixGroupless(book))
727
+
728
+ async function fixToOldDataModel (book) {
729
+ async function updateToOldDataModel(groups) {
730
+ let ds = []
731
+ let ins = []
732
+ groups.forEach(group => {
733
+ let vcardOrWebids = kb.statementsMatching(null, ns.owl('sameAs'), null, group.doc()).map(st => st.subject)
734
+ const strings = new Set(vcardOrWebids.map(contact => contact.uri)) // remove dups
735
+ vcardOrWebids = [...strings].map(uri => kb.sym(uri))
736
+ vcardOrWebids.forEach(item => {
737
+ if (!kb.each(item, ns.vcard('fn'), null, group.doc()).length) {
738
+ // delete item this is a new data model, item is a webid not a card.
739
+ ds = ds.concat(kb
740
+ .statementsMatching(item, ns.owl('sameAs'), null, group.doc())
741
+ .concat(kb.statementsMatching(undefined, undefined, item, group.doc())))
742
+ // add webid card to group
743
+ const cards = kb.each(item, ns.owl('sameAs'), null, group.doc())
744
+ cards.forEach(card => {
745
+ ins = ins.concat($rdf.st(card, ns.owl('sameAs'), item, group.doc()))
746
+ .concat($rdf.st(group, ns.vcard('hasMember'), card, group.doc()))
747
+ })
748
+ }
749
+ })
750
+ })
751
+ if (ds.length && confirm('Groups can be updated to old data model ?')) {
752
+ await kb.updater.updateMany(ds, ins)
753
+ alert('Update done')
754
+ } else { if (!ds.length) alert('Nothing to update.\nAll Groups already use the old data model.')}
755
+ }
756
+ let groups = kb.each(book, VCARD('includesGroup'))
757
+ const strings = new Set(groups.map(group => group.uri)) // remove dups
758
+ groups = [...strings].map(uri => kb.sym(uri))
759
+ updateToOldDataModel(groups)
760
+ }
761
+
762
+ const fixToOldDataModelButton = pane.appendChild(dom.createElement('button'))
763
+ fixToOldDataModelButton.style.cssText = buttonStyle
764
+ fixToOldDataModelButton.textContent = 'Revert groups to old data model'
765
+ fixToOldDataModelButton.addEventListener('click', _event => fixToOldDataModel(book))
766
+
716
767
  } // main
717
768
  main()
718
769
  return pane
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2015",
4
+ "module": "commonjs",
5
+ "strict": false,
6
+ "outDir": "lib",
7
+ "sourceMap": true,
8
+ "allowSyntheticDefaultImports" : true // https://github.com/inrupt/solid-client-authn-js/issues/3219
9
+ // "esModuleInterop": true // ,
10
+ // "skipLibCheck": true // otherwise it takes *.d.ts from node_modules see: https://stackoverflow.com/questions/51634361/how-to-force-tsc-to-ignore-node-modules-folder
11
+ },
12
+ "include": [
13
+ "./src/**/*"
14
+ ],
15
+ "exclude": ["**/*.spec.ts", "**/*.test.ts"]
16
+ }