sanity-plugin-taxonomy-manager 4.7.2 → 5.0.0
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.
- package/LICENSE +1 -1
- package/README.md +13 -108
- package/{lib → dist}/index.d.ts +66 -48
- package/dist/index.js +106 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -46
- package/src/components/Children.tsx +4 -3
- package/src/components/Concepts.tsx +2 -2
- package/src/components/Hierarchy.test.tsx +92 -0
- package/src/components/Hierarchy.tsx +65 -30
- package/src/components/TopConcepts.tsx +2 -2
- package/src/components/TreeStructure.tsx +3 -3
- package/src/components/inputs/ArrayHierarchyInput.test.tsx +102 -0
- package/src/components/inputs/ArrayHierarchyInput.tsx +23 -24
- package/src/components/inputs/Identifier.tsx +1 -1
- package/src/components/inputs/InputHierarchy.test.tsx +38 -0
- package/src/components/inputs/InputHierarchy.tsx +20 -68
- package/src/components/inputs/ReferenceHierarchyInput.test.tsx +109 -0
- package/src/components/inputs/ReferenceHierarchyInput.tsx +24 -25
- package/src/components/interactions/ConceptDetailLink.tsx +0 -1
- package/src/components/interactions/ConceptEditAction.tsx +1 -1
- package/src/components/interactions/ConceptSelectLink.test.tsx +49 -0
- package/src/components/interactions/ConceptSelectLink.tsx +10 -6
- package/src/context.ts +34 -14
- package/src/core/__snapshots__/queries.test.ts.snap +386 -0
- package/src/core/__snapshots__/semanticRecommendations.test.ts.snap +10 -0
- package/src/core/createId.test.ts +35 -0
- package/src/core/filters.test.ts +64 -0
- package/src/core/filters.ts +68 -0
- package/src/core/ids.test.ts +68 -0
- package/src/core/ids.ts +61 -0
- package/src/core/mutations.test.ts +114 -0
- package/src/core/mutations.ts +105 -0
- package/src/core/ports.ts +59 -0
- package/src/core/queries.test.ts +50 -0
- package/src/{queries.ts → core/queries.ts} +7 -6
- package/src/core/semanticRecommendations.test.ts +160 -0
- package/src/core/semanticRecommendations.ts +127 -0
- package/src/core/tree/annotateRecommendations.test.ts +43 -0
- package/src/core/tree/annotateRecommendations.ts +30 -0
- package/src/core/tree/pruneConcepts.test.ts +77 -0
- package/src/core/tree/pruneConcepts.ts +72 -0
- package/src/core/validation.test.ts +42 -0
- package/src/core/validation.ts +42 -0
- package/src/helpers/baseIriField.ts +22 -0
- package/src/helpers/branchFilter.test.ts +48 -0
- package/src/helpers/branchFilter.ts +17 -26
- package/src/helpers/schemeFilter.test.ts +51 -0
- package/src/helpers/schemeFilter.ts +11 -23
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useCreateConcept.tsx +33 -110
- package/src/hooks/useRemoveConcept.tsx +24 -36
- package/src/hooks/useSemanticRecommendations.tsx +75 -0
- package/src/index.test.ts +31 -0
- package/src/index.ts +2 -6
- package/src/seams/StudioDataAdapter.ts +81 -0
- package/src/seams/TaxonomyPortContext.tsx +33 -0
- package/src/skosConcept.tsx +23 -234
- package/src/skosConceptScheme.tsx +2 -4
- package/src/structure.tsx +60 -0
- package/src/test/FakeDataPort.test.ts +62 -0
- package/src/test/FakeDataPort.ts +69 -0
- package/src/test/inputHarness.tsx +71 -0
- package/src/test/renderWithUi.test.tsx +14 -0
- package/src/test/renderWithUi.tsx +24 -0
- package/src/test-setup.ts +35 -0
- package/src/types.tsx +30 -11
- package/src/views/ConceptUseView.tsx +4 -10
- package/lib/index.esm.d.mts +0 -492
- package/lib/index.esm.esm.js +0 -182
- package/lib/index.esm.esm.js.map +0 -1
- package/lib/index.esm.mjs +0 -182
- package/lib/index.esm.mjs.map +0 -1
- package/lib/index.js +0 -182
- package/lib/index.js.map +0 -1
- package/sanity.json +0 -8
- package/src/config.ts +0 -16
- package/src/helpers/baseIriField.tsx +0 -42
- package/src/hooks/useEmbeddingsRecs.tsx +0 -75
- package/src/skosConcept.module.css +0 -10
- package/src/structure.ts +0 -40
- package/v2-incompatible.js +0 -11
- /package/src/{helpers → core}/createId.ts +0 -0
package/src/skosConcept.tsx
CHANGED
|
@@ -1,38 +1,19 @@
|
|
|
1
|
-
import {WarningOutlineIcon} from '@sanity/icons'
|
|
2
|
-
import {DocumentId, getPublishedId} from '@sanity/id-utils'
|
|
3
1
|
import {AiOutlineTag, AiOutlineTags} from 'react-icons/ai'
|
|
4
2
|
import {defineType, defineField} from 'sanity'
|
|
5
|
-
import type {SanityDocument} from 'sanity'
|
|
6
3
|
|
|
7
4
|
import {Identifier} from './components/inputs'
|
|
5
|
+
import {createId} from './core/createId'
|
|
6
|
+
import {conceptFilter, prefLabelUniquenessResult} from './core/validation'
|
|
8
7
|
import baseIriField from './helpers/baseIriField'
|
|
9
|
-
import {createId} from './helpers/createId'
|
|
10
|
-
import styles from './skosConcept.module.css'
|
|
11
8
|
import type {Options} from './types'
|
|
12
9
|
|
|
13
|
-
const conceptFilter = ({document}: {document: SanityDocument}) => {
|
|
14
|
-
const publishedId = getPublishedId(DocumentId(document._id))
|
|
15
|
-
return {
|
|
16
|
-
filter: '!(_id in $broader || _id in $related || _id == $self)',
|
|
17
|
-
params: {
|
|
18
|
-
self: publishedId,
|
|
19
|
-
broader: Array.isArray(document?.broader)
|
|
20
|
-
? document.broader.map(({_ref}: {_ref: string}) => _ref)
|
|
21
|
-
: [],
|
|
22
|
-
related: Array.isArray(document?.related)
|
|
23
|
-
? document.related.map(({_ref}: {_ref: string}) => _ref)
|
|
24
|
-
: [],
|
|
25
|
-
},
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
10
|
/**
|
|
30
11
|
* Sanity document scheme for SKOS Taxonomy Concepts
|
|
31
12
|
*/
|
|
32
13
|
export default function skosConcept(
|
|
33
14
|
baseUri?: Options['baseUri'],
|
|
34
15
|
customConceptFields: Options['customConceptFields'] = [],
|
|
35
|
-
ident?: Options['ident']
|
|
16
|
+
ident?: Options['ident'],
|
|
36
17
|
) {
|
|
37
18
|
return defineType({
|
|
38
19
|
name: 'skosConcept',
|
|
@@ -73,125 +54,40 @@ export default function skosConcept(
|
|
|
73
54
|
.fetch(
|
|
74
55
|
`*[_type == "skosConcept" && prefLabel == "${
|
|
75
56
|
prefLabel as string
|
|
76
|
-
}" && !(_id in path("drafts.**") || _id in path("versions.**"))][0]._id
|
|
57
|
+
}" && !(_id in path("drafts.**") || _id in path("versions.**"))][0]._id`,
|
|
58
|
+
)
|
|
59
|
+
.then((conceptId) =>
|
|
60
|
+
prefLabelUniquenessResult(conceptId as string | null, context.document?._id),
|
|
77
61
|
)
|
|
78
|
-
.then((conceptId) => {
|
|
79
|
-
if (conceptId && !context.document?._id.includes(conceptId as string)) {
|
|
80
|
-
return 'Preferred Label must be unique.'
|
|
81
|
-
}
|
|
82
|
-
return true
|
|
83
|
-
})
|
|
84
62
|
}),
|
|
85
63
|
}),
|
|
86
64
|
defineField({
|
|
87
65
|
name: 'definition',
|
|
88
66
|
title: 'Definition',
|
|
89
67
|
type: 'text',
|
|
90
|
-
description:
|
|
91
|
-
<details className={styles.description}>
|
|
92
|
-
<summary>A complete explanation of the intended meaning of the concept.</summary>
|
|
93
|
-
<div>
|
|
94
|
-
<kbd>
|
|
95
|
-
Example: documentation
|
|
96
|
-
<br />
|
|
97
|
-
Definition: "The process of storing and retrieving information in all fields of
|
|
98
|
-
knowledge."
|
|
99
|
-
</kbd>
|
|
100
|
-
<p>
|
|
101
|
-
For more information on the recommended usage of the SKOS documentation properties,
|
|
102
|
-
see
|
|
103
|
-
<a
|
|
104
|
-
href="https://www.w3.org/TR/2009/NOTE-skos-primer-20090818/#secdocumentation"
|
|
105
|
-
target="_blank"
|
|
106
|
-
rel="noreferrer"
|
|
107
|
-
>
|
|
108
|
-
{' '}
|
|
109
|
-
W3C SKOS Primer: 2.4 Documentary Notes
|
|
110
|
-
</a>
|
|
111
|
-
</p>
|
|
112
|
-
</div>
|
|
113
|
-
</details>
|
|
114
|
-
),
|
|
68
|
+
description: 'A complete explanation of the intended meaning of the concept.',
|
|
115
69
|
rows: 3,
|
|
116
70
|
}),
|
|
117
71
|
defineField({
|
|
118
72
|
name: 'example',
|
|
119
73
|
title: 'Examples',
|
|
120
74
|
type: 'text',
|
|
121
|
-
description:
|
|
122
|
-
<details className={styles.description}>
|
|
123
|
-
<summary>An example of the use of the concept.</summary>
|
|
124
|
-
<div>
|
|
125
|
-
<kbd>
|
|
126
|
-
Example: organizations of science and culture
|
|
127
|
-
<br />
|
|
128
|
-
Example: "academies of science, general museums, world fairs"
|
|
129
|
-
</kbd>
|
|
130
|
-
<p>
|
|
131
|
-
For more information on the recommended usage of the SKOS documentation properties,
|
|
132
|
-
see
|
|
133
|
-
<a
|
|
134
|
-
href="https://www.w3.org/TR/2009/NOTE-skos-primer-20090818/#secdocumentation"
|
|
135
|
-
target="_blank"
|
|
136
|
-
rel="noreferrer"
|
|
137
|
-
>
|
|
138
|
-
{' '}
|
|
139
|
-
W3C SKOS Primer: 2.4 Documentary Notes
|
|
140
|
-
</a>
|
|
141
|
-
</p>
|
|
142
|
-
</div>
|
|
143
|
-
</details>
|
|
144
|
-
),
|
|
75
|
+
description: 'An example of the use of the concept.',
|
|
145
76
|
rows: 3,
|
|
146
77
|
}),
|
|
147
78
|
defineField({
|
|
148
79
|
name: 'scopeNote',
|
|
149
80
|
title: 'Scope Note',
|
|
150
81
|
type: 'text',
|
|
151
|
-
description:
|
|
152
|
-
|
|
153
|
-
<summary>
|
|
154
|
-
A brief statement on the intended meaning of this concept, especially as an indication
|
|
155
|
-
of how the use of the concept is limited in indexing practice.
|
|
156
|
-
</summary>
|
|
157
|
-
<div>
|
|
158
|
-
<kbd>
|
|
159
|
-
Example: microwave frequencies
|
|
160
|
-
<br />
|
|
161
|
-
Scope Note: "Used for frequencies between 1Ghz and 300Ghz"
|
|
162
|
-
</kbd>
|
|
163
|
-
<p>
|
|
164
|
-
For more information on the recommended usage of the SKOS documentation properties,
|
|
165
|
-
see
|
|
166
|
-
<a
|
|
167
|
-
href="https://www.w3.org/TR/2009/NOTE-skos-primer-20090818/#secdocumentation"
|
|
168
|
-
target="_blank"
|
|
169
|
-
rel="noreferrer"
|
|
170
|
-
>
|
|
171
|
-
{' '}
|
|
172
|
-
W3C SKOS Primer: 2.4 Documentary Notes
|
|
173
|
-
</a>
|
|
174
|
-
</p>
|
|
175
|
-
</div>
|
|
176
|
-
</details>
|
|
177
|
-
),
|
|
82
|
+
description:
|
|
83
|
+
'A brief statement on the intended meaning of this concept, especially as an indication of how the use of the concept is limited in indexing practice.',
|
|
178
84
|
rows: 3,
|
|
179
85
|
}),
|
|
180
86
|
defineField({
|
|
181
87
|
name: 'altLabel',
|
|
182
88
|
title: 'Alternate Label(s)',
|
|
183
89
|
type: 'array',
|
|
184
|
-
description:
|
|
185
|
-
<details className={styles.description}>
|
|
186
|
-
<summary>Synonyms, near-synonyms, abbreviations, and acronyms to a concept.</summary>
|
|
187
|
-
<div>
|
|
188
|
-
<p>
|
|
189
|
-
<WarningOutlineIcon /> Preferred, alternative, and hidden label sets must not
|
|
190
|
-
overlap.
|
|
191
|
-
</p>
|
|
192
|
-
</div>
|
|
193
|
-
</details>
|
|
194
|
-
),
|
|
90
|
+
description: 'Synonyms, near-synonyms, abbreviations, and acronyms to a concept.',
|
|
195
91
|
of: [{type: 'string'}],
|
|
196
92
|
validation: (Rule) => Rule.unique(),
|
|
197
93
|
}),
|
|
@@ -199,24 +95,8 @@ export default function skosConcept(
|
|
|
199
95
|
name: 'hiddenLabel',
|
|
200
96
|
title: 'Hidden Label(s)',
|
|
201
97
|
type: 'array',
|
|
202
|
-
description:
|
|
203
|
-
|
|
204
|
-
<summary>
|
|
205
|
-
Character strings that need to be accessible to applications performing text-based
|
|
206
|
-
indexing and search operations, but which should not be displayed to end users.
|
|
207
|
-
</summary>
|
|
208
|
-
<div>
|
|
209
|
-
<p>
|
|
210
|
-
Hidden labels may for instance be used to include misspelled variants of other
|
|
211
|
-
lexical labels.
|
|
212
|
-
</p>
|
|
213
|
-
<p>
|
|
214
|
-
<WarningOutlineIcon /> Preferred, alternative, and hidden label sets must not
|
|
215
|
-
overlap.
|
|
216
|
-
</p>
|
|
217
|
-
</div>
|
|
218
|
-
</details>
|
|
219
|
-
),
|
|
98
|
+
description:
|
|
99
|
+
'Character strings that need to be accessible to applications performing text-based indexing and search operations, but which should not be displayed to end users.',
|
|
220
100
|
of: [{type: 'string'}],
|
|
221
101
|
validation: (Rule) => Rule.unique(),
|
|
222
102
|
}),
|
|
@@ -237,17 +117,8 @@ export default function skosConcept(
|
|
|
237
117
|
defineField({
|
|
238
118
|
name: 'broader',
|
|
239
119
|
title: 'Broader Concept(s)',
|
|
240
|
-
description:
|
|
241
|
-
|
|
242
|
-
<summary>
|
|
243
|
-
Create hierarchy between concepts, for example to create category/subcategory,
|
|
244
|
-
part/whole, or class/instance relationships.
|
|
245
|
-
</summary>
|
|
246
|
-
<div>
|
|
247
|
-
<p>Broader and Associated relationships are mutually exclusive.</p>
|
|
248
|
-
</div>
|
|
249
|
-
</details>
|
|
250
|
-
),
|
|
120
|
+
description:
|
|
121
|
+
'Create hierarchy between concepts, for example to create category/subcategory, part/whole, or class/instance relationships.',
|
|
251
122
|
type: 'array',
|
|
252
123
|
of: [
|
|
253
124
|
{
|
|
@@ -262,17 +133,8 @@ export default function skosConcept(
|
|
|
262
133
|
defineField({
|
|
263
134
|
name: 'related',
|
|
264
135
|
title: 'Related Concept(s)',
|
|
265
|
-
description:
|
|
266
|
-
|
|
267
|
-
<summary>
|
|
268
|
-
Indicate that two concepts are inherently "related", but that one is not in any way
|
|
269
|
-
more general than the other.
|
|
270
|
-
</summary>
|
|
271
|
-
<div>
|
|
272
|
-
<p>Broader and Associated relationships are mutually exclusive.</p>
|
|
273
|
-
</div>
|
|
274
|
-
</details>
|
|
275
|
-
),
|
|
136
|
+
description:
|
|
137
|
+
'Indicate that two concepts are inherently "related", but that one is not in any way more general than the other.',
|
|
276
138
|
type: 'array',
|
|
277
139
|
of: [
|
|
278
140
|
{
|
|
@@ -289,96 +151,23 @@ export default function skosConcept(
|
|
|
289
151
|
name: 'historyNote',
|
|
290
152
|
title: 'History Notes',
|
|
291
153
|
type: 'text',
|
|
292
|
-
description:
|
|
293
|
-
<details className={styles.description}>
|
|
294
|
-
<summary>Significant changes to the meaning or the form of this concept.</summary>
|
|
295
|
-
<div>
|
|
296
|
-
<kbd>
|
|
297
|
-
Example: person with disabilities
|
|
298
|
-
<br />
|
|
299
|
-
History Note: "Estab. 1992; heading was: handicapped [1884 - 1992]."
|
|
300
|
-
</kbd>
|
|
301
|
-
<p>
|
|
302
|
-
For more information on the recommended usage of the SKOS documentation properties,
|
|
303
|
-
see
|
|
304
|
-
<a
|
|
305
|
-
href="https://www.w3.org/TR/2009/NOTE-skos-primer-20090818/#secdocumentation"
|
|
306
|
-
target="_blank"
|
|
307
|
-
rel="noreferrer"
|
|
308
|
-
>
|
|
309
|
-
{' '}
|
|
310
|
-
W3C SKOS Primer: 2.4 Documentary Notes
|
|
311
|
-
</a>
|
|
312
|
-
</p>
|
|
313
|
-
</div>
|
|
314
|
-
</details>
|
|
315
|
-
),
|
|
154
|
+
description: 'Significant changes to the meaning or the form of this concept.',
|
|
316
155
|
rows: 3,
|
|
317
156
|
}),
|
|
318
157
|
defineField({
|
|
319
158
|
name: 'editorialNote',
|
|
320
159
|
title: 'Editorial Notes',
|
|
321
160
|
type: 'text',
|
|
322
|
-
description:
|
|
323
|
-
|
|
324
|
-
<summary>
|
|
325
|
-
Information to aid in administrative housekeeping, such as reminders of editorial work
|
|
326
|
-
still to be done, or warnings in the event that future editorial changes might be
|
|
327
|
-
made.
|
|
328
|
-
</summary>
|
|
329
|
-
<div>
|
|
330
|
-
<kbd>
|
|
331
|
-
Example: doubleclick
|
|
332
|
-
<br />
|
|
333
|
-
Editorial Note: "Review this term after the company merger is complete."
|
|
334
|
-
</kbd>
|
|
335
|
-
<p>
|
|
336
|
-
For more information on the recommended usage of the SKOS documentation properties,
|
|
337
|
-
see
|
|
338
|
-
<a
|
|
339
|
-
href="https://www.w3.org/TR/2009/NOTE-skos-primer-20090818/#secdocumentation"
|
|
340
|
-
target="_blank"
|
|
341
|
-
rel="noreferrer"
|
|
342
|
-
>
|
|
343
|
-
{' '}
|
|
344
|
-
W3C SKOS Primer: 2.4 Documentary Notes
|
|
345
|
-
</a>
|
|
346
|
-
</p>
|
|
347
|
-
</div>
|
|
348
|
-
</details>
|
|
349
|
-
),
|
|
161
|
+
description:
|
|
162
|
+
'Information to aid in administrative housekeeping, such as reminders of editorial work still to be done, or warnings in the event that future editorial changes might be made.',
|
|
350
163
|
rows: 3,
|
|
351
164
|
}),
|
|
352
165
|
defineField({
|
|
353
166
|
name: 'changeNote',
|
|
354
167
|
title: 'Change Notes',
|
|
355
168
|
type: 'text',
|
|
356
|
-
description:
|
|
357
|
-
|
|
358
|
-
<summary>
|
|
359
|
-
Fine-grained changes to a concept, for the purposes of administration and maintenance.
|
|
360
|
-
</summary>
|
|
361
|
-
<div>
|
|
362
|
-
<kbd>
|
|
363
|
-
Example: tomato
|
|
364
|
-
<br />
|
|
365
|
-
Change Note: "Moved from under 'fruits' to under 'vegetables' by Horace Gray"
|
|
366
|
-
</kbd>
|
|
367
|
-
<p>
|
|
368
|
-
For more information on the recommended usage of the SKOS documentation properties,
|
|
369
|
-
see
|
|
370
|
-
<a
|
|
371
|
-
href="https://www.w3.org/TR/2009/NOTE-skos-primer-20090818/#secdocumentation"
|
|
372
|
-
target="_blank"
|
|
373
|
-
rel="noreferrer"
|
|
374
|
-
>
|
|
375
|
-
{' '}
|
|
376
|
-
W3C SKOS Primer: 2.4 Documentary Notes
|
|
377
|
-
</a>
|
|
378
|
-
</p>
|
|
379
|
-
</div>
|
|
380
|
-
</details>
|
|
381
|
-
),
|
|
169
|
+
description:
|
|
170
|
+
'Fine-grained changes to a concept, for the purposes of administration and maintenance.',
|
|
382
171
|
rows: 3,
|
|
383
172
|
}),
|
|
384
173
|
],
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
// import {nanoid} from 'nanoid'
|
|
2
|
-
// import type {FieldDefinition} from 'sanity'
|
|
3
1
|
import {defineArrayMember, defineField, defineType} from 'sanity'
|
|
4
2
|
|
|
5
3
|
import {Identifier, ManagementControls} from './components/inputs'
|
|
6
4
|
import baseIriField from './helpers/baseIriField'
|
|
7
|
-
import {createId} from './
|
|
5
|
+
import {createId} from './core/createId'
|
|
8
6
|
import NodeTree from './static/NodeTree'
|
|
9
7
|
import type {Options} from './types'
|
|
10
8
|
|
|
@@ -14,7 +12,7 @@ import type {Options} from './types'
|
|
|
14
12
|
export default function skosConceptScheme(
|
|
15
13
|
baseUri?: Options['baseUri'],
|
|
16
14
|
customSchemeFields: Options['customSchemeFields'] = [],
|
|
17
|
-
ident?: Options['ident']
|
|
15
|
+
ident?: Options['ident'],
|
|
18
16
|
) {
|
|
19
17
|
return defineType({
|
|
20
18
|
name: 'skosConceptScheme',
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {EditIcon, DocumentsIcon} from '@sanity/icons'
|
|
2
|
+
import type {StructureBuilder, ListBuilder, DefaultDocumentNodeResolver} from 'sanity/structure'
|
|
3
|
+
|
|
4
|
+
import {TreeView} from './components/TreeView'
|
|
5
|
+
import {TaxonomyConfigContext} from './context'
|
|
6
|
+
import NodeTree from './static/NodeTree'
|
|
7
|
+
import type {Options, TreeViewProps} from './types'
|
|
8
|
+
import {ConceptUseView} from './views/ConceptUseView'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* #### Default Desk Structure for Concept and Concept Scheme
|
|
12
|
+
* Sets defaultDocumentNode. Consider exporting in the future,
|
|
13
|
+
* if there is a use case for mixing taxonomy views in the main
|
|
14
|
+
* desk structure.
|
|
15
|
+
*/
|
|
16
|
+
export const structure = (S: StructureBuilder): ListBuilder =>
|
|
17
|
+
S.list()
|
|
18
|
+
.title('Taxonomy Manager')
|
|
19
|
+
.items([
|
|
20
|
+
S.documentTypeListItem('skosConceptScheme').title('Concept Schemes'),
|
|
21
|
+
S.documentTypeListItem('skosConcept').title('Concepts'),
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* #### Default Document Node (factory)
|
|
26
|
+
* Builds the `defaultDocumentNode` resolver, closing over plugin config so the
|
|
27
|
+
* structure Tree View can read `ident` from React context — replacing the old
|
|
28
|
+
* `config.ts` `getPluginConfig` singleton. Called once at plugin init, so the
|
|
29
|
+
* wrapper component identity stays stable across resolver invocations.
|
|
30
|
+
*/
|
|
31
|
+
export const createDefaultDocumentNode = (
|
|
32
|
+
config: {ident?: Options['ident']} = {}
|
|
33
|
+
): DefaultDocumentNodeResolver => {
|
|
34
|
+
// Provide plugin config to the structure Tree View subtree (the only place
|
|
35
|
+
// concept creation reads `ident`). Input-component trees render without this
|
|
36
|
+
// provider and fall back to the context default — they never create concepts.
|
|
37
|
+
const TreeViewWithConfig = (props: TreeViewProps) => (
|
|
38
|
+
<TaxonomyConfigContext.Provider value={{ident: config.ident}}>
|
|
39
|
+
<TreeView {...props} />
|
|
40
|
+
</TaxonomyConfigContext.Provider>
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return (S, {schemaType}) => {
|
|
44
|
+
// Conditionally return a different configuration based on the schema type
|
|
45
|
+
switch (schemaType) {
|
|
46
|
+
case 'skosConceptScheme':
|
|
47
|
+
return S.document().views([
|
|
48
|
+
S.view.component(TreeViewWithConfig).title('Tree View').icon(NodeTree),
|
|
49
|
+
S.view.form().icon(EditIcon),
|
|
50
|
+
])
|
|
51
|
+
case 'skosConcept':
|
|
52
|
+
return S.document().views([
|
|
53
|
+
S.view.form().icon(EditIcon),
|
|
54
|
+
S.view.component(ConceptUseView).title('Tagged Resources').icon(DocumentsIcon),
|
|
55
|
+
])
|
|
56
|
+
default:
|
|
57
|
+
return S.document().views([S.view.form().icon(EditIcon)])
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {renderHook} from '@testing-library/react'
|
|
2
|
+
import {describe, expect, it} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {planCreateConcept} from '../core/mutations'
|
|
5
|
+
import type {ConceptSchemeDocument, DocumentConcepts} from '../types'
|
|
6
|
+
|
|
7
|
+
import {createFakeDataPort} from './FakeDataPort'
|
|
8
|
+
|
|
9
|
+
const scheme = {_id: 'scheme-1', _type: 'skosConceptScheme'} as ConceptSchemeDocument['displayed']
|
|
10
|
+
const TREE: DocumentConcepts = {topConcepts: [], concepts: []}
|
|
11
|
+
|
|
12
|
+
describe('createFakeDataPort', () => {
|
|
13
|
+
it('useWatchTree returns the configured fixture synchronously and records params', () => {
|
|
14
|
+
const port = createFakeDataPort({tree: TREE})
|
|
15
|
+
const params = {mode: 'trunk', documentId: 'scheme-1', perspective: undefined} as const
|
|
16
|
+
|
|
17
|
+
const {result} = renderHook(() => port.useWatchTree(params))
|
|
18
|
+
|
|
19
|
+
expect(result.current).toEqual({data: TREE, loading: false, error: null})
|
|
20
|
+
expect(port.lastWatchParams).toEqual(params)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('defaults to a null, non-loading, error-free result', () => {
|
|
24
|
+
const port = createFakeDataPort()
|
|
25
|
+
const {result} = renderHook(() =>
|
|
26
|
+
port.useWatchTree({mode: 'trunk', documentId: 's', perspective: undefined})
|
|
27
|
+
)
|
|
28
|
+
expect(result.current).toEqual({data: null, loading: false, error: null})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('useApplyConceptPlan records each plan it is given, in order', async () => {
|
|
32
|
+
const port = createFakeDataPort()
|
|
33
|
+
const plan = planCreateConcept({
|
|
34
|
+
scheme,
|
|
35
|
+
conceptType: 'topConcept',
|
|
36
|
+
newConceptUuid: 'u',
|
|
37
|
+
conceptId: 'abc123',
|
|
38
|
+
schemeBaseIri: undefined,
|
|
39
|
+
newConceptKey: 'k',
|
|
40
|
+
broaderKey: 'b',
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const {result} = renderHook(() => port.useApplyConceptPlan())
|
|
44
|
+
await result.current(plan)
|
|
45
|
+
|
|
46
|
+
expect(port.appliedPlans).toEqual([plan])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('useSemanticRecommendations returns configured recs and counts search triggers', () => {
|
|
50
|
+
const port = createFakeDataPort({
|
|
51
|
+
recommendations: [{conceptId: 'd1', score: 0.9}],
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const {result} = renderHook(() => port.useSemanticRecommendations())
|
|
55
|
+
expect(result.current.conceptRecs).toHaveLength(1)
|
|
56
|
+
expect(result.current.recsError).toBeNull()
|
|
57
|
+
|
|
58
|
+
result.current.triggerSearch('scheme-7')
|
|
59
|
+
expect(port.triggeredSearches).toBe(1)
|
|
60
|
+
expect(port.lastSearchSchemeId).toBe('scheme-7')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConceptPlan,
|
|
3
|
+
ConceptTreeParams,
|
|
4
|
+
SemanticRecommendationsResult,
|
|
5
|
+
TaxonomyDataPort,
|
|
6
|
+
WatchResult,
|
|
7
|
+
} from '../core/ports'
|
|
8
|
+
import type {ConceptRecommendation, DocumentConcepts} from '../types'
|
|
9
|
+
|
|
10
|
+
/** Fixtures + canned responses for a {@link createFakeDataPort}. */
|
|
11
|
+
export interface FakeDataPortConfig {
|
|
12
|
+
tree?: DocumentConcepts | null
|
|
13
|
+
loading?: boolean
|
|
14
|
+
error?: Error | null
|
|
15
|
+
recommendations?: ConceptRecommendation[]
|
|
16
|
+
recsError?: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A {@link TaxonomyDataPort} that also exposes recorded interactions for assertions. */
|
|
20
|
+
export interface FakeDataPort extends TaxonomyDataPort {
|
|
21
|
+
/** Plans passed to the applyConceptPlan function, in call order. */
|
|
22
|
+
readonly appliedPlans: ConceptPlan[]
|
|
23
|
+
/** Number of `triggerSearch()` calls. */
|
|
24
|
+
triggeredSearches: number
|
|
25
|
+
/** The `schemeId` passed to the most recent `triggerSearch()` call. */
|
|
26
|
+
lastSearchSchemeId: string | undefined
|
|
27
|
+
/** Params from the most recent `useWatchTree` call. */
|
|
28
|
+
lastWatchParams: ConceptTreeParams | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* In-memory `TaxonomyDataPort` for component/tree tests. Returns the configured
|
|
33
|
+
* fixtures synchronously (no Studio, no network) and records mutations and
|
|
34
|
+
* search triggers for assertions. The hook members call no React hooks, so they
|
|
35
|
+
* return stable references and never suspend. Provide via:
|
|
36
|
+
* `<TaxonomyPortProvider port={createFakeDataPort({tree})}>`.
|
|
37
|
+
*/
|
|
38
|
+
export function createFakeDataPort(config: FakeDataPortConfig = {}): FakeDataPort {
|
|
39
|
+
const watchResult: WatchResult<DocumentConcepts> = {
|
|
40
|
+
data: config.tree ?? null,
|
|
41
|
+
loading: config.loading ?? false,
|
|
42
|
+
error: config.error ?? null,
|
|
43
|
+
}
|
|
44
|
+
const recsResult: SemanticRecommendationsResult = {
|
|
45
|
+
conceptRecs: config.recommendations ?? [],
|
|
46
|
+
recsError: config.recsError ?? null,
|
|
47
|
+
triggerSearch: (schemeId?: string) => {
|
|
48
|
+
fake.triggeredSearches += 1
|
|
49
|
+
fake.lastSearchSchemeId = schemeId
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
const apply = (plan: ConceptPlan): Promise<void> => {
|
|
53
|
+
fake.appliedPlans.push(plan)
|
|
54
|
+
return Promise.resolve()
|
|
55
|
+
}
|
|
56
|
+
const fake: FakeDataPort = {
|
|
57
|
+
appliedPlans: [],
|
|
58
|
+
triggeredSearches: 0,
|
|
59
|
+
lastSearchSchemeId: undefined,
|
|
60
|
+
lastWatchParams: null,
|
|
61
|
+
useWatchTree: (params: ConceptTreeParams) => {
|
|
62
|
+
fake.lastWatchParams = params
|
|
63
|
+
return watchResult
|
|
64
|
+
},
|
|
65
|
+
useApplyConceptPlan: () => apply,
|
|
66
|
+
useSemanticRecommendations: () => recsResult,
|
|
67
|
+
}
|
|
68
|
+
return fake
|
|
69
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type {ArrayHierarchyInput} from '../components/inputs/ArrayHierarchyInput'
|
|
4
|
+
import type {ReferenceHierarchyInput} from '../components/inputs/ReferenceHierarchyInput'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shared fixtures for the input-component jsdom tests. The components read Studio
|
|
8
|
+
* primitives (`useClient`/`useFormValue`/`usePerspective`, `FormField`) that each
|
|
9
|
+
* test file stubs via `vi.mock('sanity', …)`; this module supplies the bulky,
|
|
10
|
+
* mock-independent pieces: a chainable client stub and field-props factories.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** A Studio client stub that resolves the scheme lookup and no-ops mutations. */
|
|
14
|
+
export function fakeSchemeClient(
|
|
15
|
+
scheme: Record<string, unknown> = {_id: 'scheme-x', _type: 'skosConceptScheme', title: 'Test Scheme'}
|
|
16
|
+
) {
|
|
17
|
+
const client: Record<string, unknown> = {
|
|
18
|
+
withConfig: () => client,
|
|
19
|
+
fetch: vi.fn().mockResolvedValue({displayed: scheme}),
|
|
20
|
+
patch: vi.fn(() => client),
|
|
21
|
+
set: vi.fn(() => client),
|
|
22
|
+
setIfMissing: vi.fn(() => client),
|
|
23
|
+
append: vi.fn(() => client),
|
|
24
|
+
commit: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
create: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
transaction: vi.fn(() => client),
|
|
27
|
+
}
|
|
28
|
+
return client as unknown as ReturnType<typeof import('sanity').useClient>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A resolved field filter. Real `schemeFilter()`/`branchFilter()` resolve to a
|
|
33
|
+
* single-argument function; the components treat a zero-arity filter as
|
|
34
|
+
* misconfigured (`filter.length === 0`), so this keeps arity 1.
|
|
35
|
+
*/
|
|
36
|
+
export const filterFor =
|
|
37
|
+
(params: Record<string, unknown>, extra: Record<string, unknown> = {}) =>
|
|
38
|
+
(_opts: unknown) =>
|
|
39
|
+
Promise.resolve({filter: 'scheme-filter', params, ...extra})
|
|
40
|
+
|
|
41
|
+
const sharedProps = {
|
|
42
|
+
level: 0,
|
|
43
|
+
value: undefined,
|
|
44
|
+
validation: [],
|
|
45
|
+
presence: [],
|
|
46
|
+
renderDefault: () => <div data-testid="default-input">default input</div>,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Minimal `ReferenceHierarchyInput` props; `renderDefault` renders a recognizable marker. */
|
|
50
|
+
export function referenceFieldProps(overrides: Record<string, unknown> = {}) {
|
|
51
|
+
return {
|
|
52
|
+
...sharedProps,
|
|
53
|
+
name: 'subject',
|
|
54
|
+
title: 'Subject',
|
|
55
|
+
path: ['subject'],
|
|
56
|
+
schemaType: {options: {filter: filterFor({schemeId: 'scheme-x'})}},
|
|
57
|
+
...overrides,
|
|
58
|
+
} as unknown as Parameters<typeof ReferenceHierarchyInput>[0]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Minimal `ArrayHierarchyInput` props; the filter lives on `of[0].options`. */
|
|
62
|
+
export function arrayFieldProps(overrides: Record<string, unknown> = {}) {
|
|
63
|
+
return {
|
|
64
|
+
...sharedProps,
|
|
65
|
+
name: 'topics',
|
|
66
|
+
title: 'Topics',
|
|
67
|
+
path: ['topics'],
|
|
68
|
+
schemaType: {of: [{options: {filter: filterFor({schemeId: 'scheme-x'})}}]},
|
|
69
|
+
...overrides,
|
|
70
|
+
} as unknown as Parameters<typeof ArrayHierarchyInput>[0]
|
|
71
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {Text} from '@sanity/ui'
|
|
2
|
+
import {screen} from '@testing-library/react'
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {renderWithUi} from './renderWithUi'
|
|
6
|
+
|
|
7
|
+
// Smoke test for the whole harness: vitest + jsdom + esbuild JSX +
|
|
8
|
+
// test-setup (matchMedia/jest-dom) + the @sanity/ui provider stack.
|
|
9
|
+
describe('test harness', () => {
|
|
10
|
+
it('renders a @sanity/ui component inside the provider stack', () => {
|
|
11
|
+
renderWithUi(<Text>taxonomy harness ok</Text>)
|
|
12
|
+
expect(screen.getByText('taxonomy harness ok')).toBeInTheDocument()
|
|
13
|
+
})
|
|
14
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {LayerProvider, ThemeProvider, ToastProvider} from '@sanity/ui'
|
|
2
|
+
import {buildTheme} from '@sanity/ui/theme'
|
|
3
|
+
import {render, type RenderResult} from '@testing-library/react'
|
|
4
|
+
import type {ReactElement, ReactNode} from 'react'
|
|
5
|
+
|
|
6
|
+
const theme = buildTheme()
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Render a component inside the @sanity/ui provider stack (theme, layers,
|
|
10
|
+
* toasts) so Studio UI primitives — popovers, portals, toasts — behave as they
|
|
11
|
+
* do in the Studio, without booting a full Studio. Used by component
|
|
12
|
+
* interaction tests.
|
|
13
|
+
*/
|
|
14
|
+
export function renderWithUi(ui: ReactElement): RenderResult {
|
|
15
|
+
return render(ui, {
|
|
16
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
17
|
+
<ThemeProvider theme={theme}>
|
|
18
|
+
<LayerProvider>
|
|
19
|
+
<ToastProvider>{children}</ToastProvider>
|
|
20
|
+
</LayerProvider>
|
|
21
|
+
</ThemeProvider>
|
|
22
|
+
),
|
|
23
|
+
})
|
|
24
|
+
}
|