react-iiif-vault 1.0.9 → 1.0.11

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/README.md CHANGED
@@ -1,213 +1,531 @@
1
- # React vault
2
- React bindings for Hyperion vault.
3
-
4
- ## Road map
5
-
6
- Technical proof of concepts
7
- - Retrieval hooks
8
- - Selectors that have dependencies on other selectors (typed)
9
- - memoization of selectors
10
- - possible move to react-redux-hooks
11
-
12
- Set of proof of concept Vault bindings/hooks/contexts for:
13
- - Collections
14
- - Manifest
15
- - Canvas
16
- - Annotation
17
- - Image service
18
- - VaultContext
19
-
20
- Complete the base hooks implementation for setting up contexts and selectors and build up hook collection
21
- - AnnotationContext
22
- - AnnotationPageContext
23
- - CanvasContext
24
- - CollectionContext
25
- - ManifestContext
26
- - useAnnotation
27
- - useAnnotationPage
28
- - useCanvas
29
- - useCollection
30
- - useDispatch
31
- - useExternalAnnotationList
32
- - useExternalCollection
33
- - useExternalManifest
34
- - useManifest
35
- - useVault
36
- - useVaultEffect
37
-
38
- Create viewer abstractions
39
- - SimpleViewerContext
40
- - SingleCanvasContext
41
-
42
- Canvas clock implementation
43
- - useAnnotationsAtTime
44
- - useCanvasClock
45
- - useCanvasTimeline
46
-
47
- Other helpers
48
- - RangeContext
49
- - usePaintingAnnotations
50
- - useRange
51
- - useSearchService
52
- - useVirtualCanvas
53
-
54
-
55
- ### New hooks
56
-
57
- #### useRenderingStrategy
58
- There are many ways that a canvas can be rendered, some are easier than others. A hook
59
- that breaks these down into easier to render chunks will make iteratively supporting
60
- features easier.
61
-
62
- This hook will return a type, some data and some actions. The types may be something like:
63
-
64
- - Single image - A single image or image service.
65
- - Composite image - A composition of more than one image or image service.
66
- - Choice - A choice that needs to be given to the user (with functions to make choice)
67
- - Audio - A single audio file, or sequential audio files
68
- - Video - A single video file, or sequential video files
69
- - Complex timeline - A composite of either multiple audio, video and composite images on a timeline.
70
-
71
- This gives implementations a goal, and can easily let users know that they don't support
72
- the given canvas without an error.
73
-
74
- The data models returned from this strategy should be similar to each other. A single image
75
- should have a way to load an image service, generate tiles or fixed sizes. Composite image
76
- should be the same as single image but with multiple. A choice should be available from the
77
- hook regardless of the type, to support a menu-like system for choices even after a choice
78
- is made. Audio and video should use a similar interface with controls. Complex timeline should
79
- be very similar to audio/video with the same controls but with additional structure similar
80
- to composite image.
81
-
82
- ```typescript
83
- type UseRenderingStrategy = {
84
- type: 'single-image' | 'composite-image' | 'audio' | 'video' | 'complex-timeline';
85
- image: { type: 'fixed-size' } | { type: 'image-service' } | null;
86
- images: [];
87
- media: { type: 'audio' } | { type: 'video' } | { type: 'sequence' };
88
- duration: number;
89
- choice: {} | null;
1
+ # React IIIF Vault
2
+ This library is a fully featured IIIF Library for reading and displaying IIIF Manifests, Collections and Annotations.
3
+
4
+ It is built on `@iiif/helpers` ([Repository](https://github.com/IIIF-Commons/iiif-helpers)) and uses the IIIF Vault to
5
+ request, parse, upgrade and store IIIF. It also contains the React implementation of [Canvas Panel](https://github.com/digirati-co-uk/iiif-canvas-panel) which is built on [Atlas Viewer](https://github.com/atlas-viewer/atlas).
6
+
7
+ ```
8
+ npm i react-iiif-vault
9
+ ```
10
+
11
+ It is recommended to install the helpers too, and the TypeScript types for IIIF, if you are using TypeScript.
12
+ ```
13
+ npm i @iiif/helpers @iiif/presentation-3
14
+ ```
15
+
16
+ ## Canvas Panel
17
+
18
+ The easiest way to get a simple headless and extensible viewer is to use the Canvas Panel component. You can
19
+ use it as a single component, or you can build your own Viewer from it's part, deciding which types of Content you want to support (images, video, audio, 3D, HTML etc.).
20
+
21
+ ```tsx
22
+ import { CanvasPanel } from 'react-iiif-vault';
23
+
24
+ function MyViewer() {
25
+ return <CanvasPanel manifest="https://digirati-co-uk.github.io/wunder.json" />
90
26
  }
91
27
  ```
92
28
 
93
- ## Querying and indexing
29
+ There are a lot of options you can pass to Canvas Panel to customise the way it renders IIIF and also
30
+ slots for inserting UI that will have access to the [Contexts](https://react.dev/learn/passing-data-deeply-with-context) provided by the library.
94
31
 
95
- ```ts
32
+ ![](./images/canvas-panel.jpg)
96
33
 
97
- // Builds a memoized query
98
- const filter = useFilter(() => {
99
- type: 'Canvas',
100
- id: id => id.startsWidth(`https://${myDomain}/`),
101
- }, [myDomain]);
102
-
103
- // The first time this is called it will build an index based on your query. (label + $contains)
104
- const search = useVaultSearch([filter, { label: { $contains: `my-query` } }]);
105
-
106
- //
107
- search.results // now contains the results.
108
-
109
- // Imperative example.
110
- // For this filter to work, it must match reference equality.
111
- const myFilterObject = vault.createFilter({
112
- type: 'Canvas',
113
- id: id => id.startsWidth('https://mydomain/'),
114
- });
115
-
116
- const myIndex = vault.createIndex({
117
- name: 'My index',
118
- refresh: true,
119
- filter: myFilterObject,
120
- indexes: ['viewingHint'],
121
- });
122
-
123
- // This is roughly what would be stored.
124
- const state = {
125
- indexes: [
126
- {
127
- filter: myFilterObject,
128
- index: {
129
- viewingHint: {
130
- individuals: ['http://manifest-1/canvas-1.json'],
131
- paged: ['http://manifest-1/canvas-2.json', 'http://manifest-1/canvas-3.json'],
132
- }
133
- }
134
- }
135
- ]
34
+ For example, you can use the `useSimpleViewer()` hook to gain access to controls for moving forward and back and
35
+ also the `useManifest` or other resource hooks to get access to the IIIF.
36
+
37
+ ```tsx
38
+ import { CanvasPanel, useSimpleViewer, useManifest, LocaleString } from 'react-iiif-vault';
39
+
40
+ function MyViewer() {
41
+ return (
42
+ <CanvasPanel
43
+ header={<Label />}
44
+ manifest="https://digirati-co-uk.github.io/wunder.json"
45
+ >
46
+ <MyControls />
47
+ </CanvasPanel>
48
+ );
49
+ }
50
+
51
+ function MyControls() {
52
+ const { previousCanvas, nextCanvas } = useSimpleViewer();
53
+ return (
54
+ <div>
55
+ <button onClick={previousCanvas}>Prev</button>
56
+ <button onClick={nextCanvas}>Next</button>
57
+ </div>
58
+ );
59
+ }
60
+
61
+ function Label() {
62
+ const manifest = useManifest();
63
+
64
+ if (!manifest) {
65
+ return <div>Loading..</div>;
66
+ }
67
+
68
+ return (
69
+ <LocaleString as="h2" className="text-2xl my-3">
70
+ {manifest.label}
71
+ </LocaleString>
72
+ );
73
+ }
74
+ ```
75
+
76
+ The `useSimpleViewer()` hook returns the following:
77
+ ```ts
78
+ type SimpleViewerContext = {
79
+ items: Reference<'Canvas'>[];
80
+ sequence: number[][];
81
+ hasNext: boolean;
82
+ hasPrevious: boolean;
83
+ setSequenceIndex: (newId: number) => void;
84
+ setCurrentCanvasId: (newId: string) => void;
85
+ setCurrentCanvasIndex: (newId: number) => void;
86
+ currentSequenceIndex: number;
87
+ nextCanvas: () => void;
88
+ previousCanvas: () => void;
136
89
  };
90
+ ```
137
91
 
138
- // And this is how it would be used.
139
- const results = vault.query([myFilterObject, {
140
- // This would be pre-filtered by the index.
141
- viewingHint: 'individuals',
142
- // Then this would be executed against the found objects.
143
- title: { $contains: 'Test' },
144
- }]);
92
+ For paged items, `sequence` will be a list of indices into `items`. For example:
93
+ ```ts
94
+ const sequence = [
95
+ [0],
96
+ [1, 2],
97
+ [3, 4]
98
+ ];
99
+
100
+ const items = [
101
+ {/* front page */},
102
+ {/* page 1v */},
103
+ {/* page 1r */},
104
+ {/* page 2v */},
105
+ {/* page 2r */},
106
+ ];
145
107
  ```
146
108
 
109
+ You can create a list of the sequence, grouped by "row" with a simple map:
110
+ ```ts
111
+ const itemSequence = sequence.map(row => row.map(idx => items[idx]));
112
+ // [
113
+ // [ {/* front page */} ],
114
+ // [ {/* page 1v */}, {/* page 1r */} ]
115
+ // [ {/* page 2v */}, {/* page 2r */} ]
116
+ // ]
117
+ ```
147
118
 
148
- ## Vault event manager
119
+ The sequence is pre-generated, so paging forward and back is as simple as going to the next index in the sequence.
120
+ The items returned will be all the IIIF Canvases that should be rendered.
149
121
 
150
- ```js
122
+ For continuous Manifests (e.g. a long Scroll), there will only be one item in the sequence:
123
+ ```ts
124
+ const sequence = [
125
+ [0, 1, 2, 3, 4 ...],
126
+ ]
127
+ ```
128
+ Indicating that all the canvases should be displayed in a single view.
151
129
 
152
- // desired API:
130
+ You can disable paging by passing `pagingEnabled={false}` to `<CanvasPanel />`.
153
131
 
154
- const annotation = getAnnotationFromSomwehere();
155
- const manager = useEventManager();
132
+ You can grab the [React ref](https://react.dev/learn/referencing-values-with-refs) from the `<CanvasPanel />` component to control it from outside of the component.
156
133
 
157
- useEffect(() => {
158
- const lastAnnotation = annotation;
159
- const clickHandler = (e, anno) => {
160
- // ...
161
- };
162
-
163
- manager.select(lastAnnotation, 'optional-label').addEventListener('click', clickHandler);
134
+ Example:
135
+ ```tsx
136
+ function MyViewer() {
137
+ const ref = useRef();
138
+
139
+
140
+ return <>
141
+ <CanvasPanel ref={ref} manifest={...} />
142
+ <div>
143
+ <button onClick={() => ref.current.previousCanvas()}>Prev</button>
144
+ <button onClick={() => ref.current.nextCanvas()}>Next</button>
145
+ </div>
146
+ </>;
147
+ }
148
+ ```
149
+
150
+ The ref is the same as what is returned from `useSimpleViewer()`.
151
+
152
+ ## Simple Viewer Provider
153
+
154
+ One of the main components of this Library is the `<SimpleViewerProvider />`. This is a component you can
155
+ wrap around other IIIF components to load a IIIF Manifest and enable all the other hooks and components.
156
+
157
+ It takes the following properties:
158
+ ```
159
+ manifest: string;
160
+ pagingEnabled?: boolean;
161
+ startCanvas?: string;
162
+ rangeId?: string;
163
+ ```
164
+
165
+ Example:
166
+ ```tsx
167
+ import { SimpleViewerProvider, useManifest, LocaleString } from 'react-iiif-vault';
168
+
169
+ function MyViewer() {
170
+ return (
171
+ <SimpleViewerProvider manifest="https://digirati-co-uk.github.io/wunder.json">
172
+ <ManifestTitle />
173
+ </SimpleViewerProvider>
174
+ );
175
+ }
176
+
177
+
178
+ function ManifestTitle() {
179
+ const manifest = useManifest();
180
+
181
+ return <LocaleString as="h1">{manifest.label}</LocaleString>
182
+ }
183
+ ```
184
+ Will display:
185
+
186
+ > # Wunder der Vererbung / von Fritz Bolle.
164
187
 
165
- return () => {
166
- manager.select(lastAnnotation).removeEventListener('click', clickHandler);
188
+ Components inside this context can also use the `useSimpleViewer()` hook, similar to Canvas Panel.
189
+
190
+ ## Vault provider
191
+
192
+ If you want to use the context manually, and not build a viewer specifically, you can wrap your application in a `VaultProvider`, passing a custom Vault instance if you want (This can be useful for server side rendering).
193
+
194
+ ```tsx
195
+ function MyApp() {
196
+ return (
197
+ <VaultProvider>
198
+ <App />
199
+ </VaultProvider>
200
+ );
201
+ }
202
+ ```
203
+
204
+ From anywhere in your app, you will be able to access the Vault using:
205
+ ```ts
206
+ const vault = useVault();
207
+ ```
208
+
209
+ #### Example NextJS hydration of IIIF Manifest
210
+ For server side rendering, you can pass IIIF resources into Vault. You will need a client component
211
+ that wraps other components. Only client components can use the hooks, since they depend on the provider.
212
+ ```tsx
213
+ // ManifestLoader.tsx
214
+ "use client";
215
+ import { SimpleViewerProvider, VaultProvider } from "react-iiif-vault";
216
+ import { Vault } from "@iiif/helpers/vault";
217
+ import type { Manifest } from '@iiif/presentation-3';
218
+
219
+ export const vault = new Vault();
220
+
221
+ export function ManifestLoader(props: { manifest: Manifest; children: React.ReactNode }) {
222
+ // On the client, use `vault.requestStatus()` to check if the Manifest already exists
223
+ // if not, use `vault.loadSync()` to load it and ensure its loaded immediately from the JSON.
224
+ if (props.manifest && props.manifest.id && !vault.requestStatus(props.manifest.id)) {
225
+ vault.loadSync(props.manifest.id, props.manifest)
167
226
  }
168
- }, [annotation]);
169
227
 
170
- // ...
228
+ return (
229
+ <SimpleViewerProvider manifest={props.manifest} vault={vault}>
230
+ {props.children}
231
+ </SimpleViewerProvider>
232
+ );
233
+ }
234
+ ```
171
235
 
172
- <MyAnnotationComponent {...mananger.eventsFor(annotation)} />
236
+ You can then use this in a server component, passing down the Manifest JSON.
237
+ ```ts
238
+ // app/page.tsx
239
+ import { readFile } from 'node:fs/promises';
173
240
 
174
- // .. or
241
+ export default async function Page() {
242
+ const manifestJson = await readFile('./manifests/my-manifest.json').then(s => JSON.parse(s));
175
243
 
176
- const annotationEvents = useEvents(annotation, 'optional-label');
244
+ return (
245
+ <ManifestLoader manifest={manifestJson}>
246
+ {/* ... Other server or client components ... */}
247
+ </ManifestLoader>
248
+ );
249
+ }
250
+ ```
177
251
 
178
- <MyAnnotationComponent {...annotationEvents} />
252
+ This will prevent the IIIF Resource being requested remotely, speeding up the initial rendering of pages.
253
+
254
+ ## Providers + Hooks
255
+
256
+ Some hooks, like `use{RESOURCE}` require a context to be set. Some will be available from the `SimpleViewerProvider` and others may be required before using the hooks. The available providers are:
257
+ - `<AnnotationProvider annotation="..." />` - Single annotation context, enables:
258
+ - `useAnnotation()`
259
+ - `usePaintingAnnotation()`
260
+ - `<AnnotationPageProvider annotationPage="..." />` - Single annotation page context, enables `useAnnotationPage()`
261
+ - `<CanvasContext canvas="..." />` - Single canvas context, enables:
262
+ - `useThumbnail()`
263
+ - `useCanvas()`
264
+ - `usePaintables()`
265
+ - `useRenderingStrategy()`
266
+ - `useLoadImageService()`
267
+ - `useImageTile()`
268
+ - `useImageService()`
269
+ - `<CollectionContext collection="..." />` - Single collection context, enables `useCollection()`
270
+ - `<ManifestContext manifest="..." />` - Single manifest context, enables:
271
+ - `useManifest()`
272
+ - `useThumbnail()`
273
+ - `useSearchService()`
274
+ - `<RangeContext range="..." />` - Single range context, enables `useRange()`
275
+
276
+ ## Components
277
+
278
+ Included are a few components that can be used within a Canvas Panel, Simple Viewer or Manifest provider.
279
+
280
+ ### Image
281
+ This is a component that can render an Image from an image service or image service ID.
282
+ ```tsx
283
+ <Image
284
+ size={{ width: 256 }}
285
+ src="https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen"
286
+ />
287
+ ```
288
+ It supports:
289
+ - `rotation`
290
+ - `region`
291
+ - `quality`
292
+ - `size`
293
+ - `format`
179
294
 
180
- // .. or
295
+ You can also pass `fetchImageService={true}` to enable it to fetch the image service. This can be useful if
296
+ you want some validation on the generated URLs (e.g. level0 services). In the future more validation will be
297
+ provided. You can also pass in an image service object in the `src={{ id: ..., profile: ... }}`.
181
298
 
182
- const div = useRef();
299
+ If you only pass in the image service ID, then you have to ensure that you provide valid options supported
300
+ by the image service.
183
301
 
184
- useBindEvents(div, annotation, 'optional-label');
302
+ ### Single Canvas Thumbnail
185
303
 
186
- <MyAnnotationComponent ref={div} />
304
+ This will display a thumbnail using either a `canvasId` or the current canvas in the context.
187
305
 
306
+ ```tsx
307
+ <SimpleViewerContext manifest="https://digirati-co-uk.github.io/wunder.json">
308
+ <SingleCanvasThumbnail size={{ width: 128 }} />
309
+ </SimpleViewerContext>
188
310
  ```
189
311
 
312
+ It supports the following props:
313
+ ```ts
314
+ interface SingleCanvasThumbnailProps {
315
+ canvasId?: string;
316
+ size?: Partial<SizeParameter>;
317
+ visible?: boolean;
318
+ alt?: string;
319
+ dereference?: boolean;
320
+
321
+ // Style
322
+ figure?: boolean;
323
+ showLabel?: boolean;
324
+ classes?: {
325
+ figure?: string;
326
+ img?: string;
327
+ label?: string;
328
+ imageWrapper?: string;
329
+ };
190
330
 
331
+ // Slots.
332
+ placeholder?: React.ReactNode;
333
+ fallback?: React.ReactNode;
334
+ }
335
+ ```
191
336
 
192
- ### Storing annotation pages
337
+ ### Sequence thumbnails
338
+
339
+ This wraps the `SingleCanvasThumbnail` but provides a list that is lazy-loaded based on the current sequence from the Simple Viewer Context.
340
+
341
+ ![](./images/sequence.jpg)
342
+
343
+ Example:
344
+ ```tsx
345
+ <SimpleViewerContext manifest="https://digirati-co-uk.github.io/wunder.json">
346
+ <SequenceThumbnails
347
+ classes={{
348
+ container: 'flex gap-1 overflow-x-auto',
349
+ row: 'flex gap-2 border border-gray-200 flex-none p-2 m-2',
350
+ img: 'max-h-[128px] max-w-[128px] object-contain h-full w-full',
351
+ selected: {
352
+ row: 'flex gap-2 border border-blue-400 flex-none p-2 m-2 bg-blue-100',
353
+ },
354
+ }}
355
+ fallback={
356
+ <div className="flex items-center justify-center w-32 h-32 bg-gray-200 text-gray-400 select-none">
357
+ No thumb
358
+ </div>
359
+ }
360
+ />
361
+ </SimpleViewerContext>
362
+ ```
363
+
364
+ The available props:
365
+ ```ts
366
+ interface SequenceThumbnailsProps {
367
+ flat?: boolean;
368
+ size?: { width: number; height?: number };
369
+ classes?: {
370
+ container?: string;
371
+ row?: string;
372
+ item?: string;
373
+
374
+ // SingleCanvasThumbnail
375
+ figure?: string;
376
+ imageWrapper?: string;
377
+ img?: string;
378
+ label?: string;
379
+
380
+ selected?: {
381
+ row?: string;
382
+ item?: string;
383
+ figure?: string;
384
+ img?: string;
385
+ label?: string;
386
+ imageWrapper?: string;
387
+ };
388
+ };
389
+
390
+ figure?: boolean;
391
+ showLabel?: boolean;
392
+ // Slots
393
+ fallback?: React.ReactNode;
394
+ }
395
+ ```
193
396
 
194
- - Annotation pages on canvases
195
- - Annotation pages on manifests
196
- - Annotation pages with manifestId in meta
197
- - Annotation pages with canvasId in meta
397
+ ### Metadata components
198
398
 
199
- #### Meta
399
+ These components will display metadata for different resources:
400
+ - `ManifestMetadata` - Displays only the metadata for the current Manifest
401
+ - `CombinedMetadata` - Displays the metadata for the current Manifest, Canvas and Range - combined
402
+ - `Metadata` - Has an extra `metadata={}` property, where you can pass down your own metadata.
200
403
 
201
- Example state:
202
- ```js
203
- const state = {
204
- meta: {
205
- 'https://example.org/annotation-list/1': {
206
- annotationListManager: {
207
- isActive: true,
208
- resources: ['https://example.org/manifest-1'],
404
+ ![](./images/metadata.jpg)
405
+
406
+ Example:
407
+
408
+ ```tsx
409
+ <CombinedMetadata
410
+ allowHtml={true}
411
+ classes={{
412
+ container: 'm-4',
413
+ row: 'border-b border-gray-200',
414
+ label: 'font-bold p-2 text-slate-600',
415
+ value: 'text-sm p-2 text-slate-800',
416
+ empty: 'text-gray-400',
417
+ }}
418
+ />
419
+ ```
420
+
421
+ These are provided without styles, and a `classes={}` prop for adding class names. The full list of options are available here:
422
+ ```ts
423
+ export interface MetadataProps {
424
+ config?: FacetConfig[];
425
+ metadata?: Array<{ label: InternationalString; value: InternationalString } | null>;
426
+ labelWidth?: number;
427
+ allowHtml?: boolean;
428
+ showEmptyMessage?: boolean;
429
+ separator?: string;
430
+
431
+ classes?: {
432
+ container?: string;
433
+ row?: string;
434
+ label?: string;
435
+ value?: string;
436
+ empty?: string;
437
+ };
438
+
439
+ emptyMessage?: string;
440
+ emptyValueFallback?: string;
441
+ emptyLabelFallback?: string;
442
+
443
+ // Slots.
444
+ tableHeader?: React.ReactNode;
445
+ tableFooter?: React.ReactNode;
446
+ emptyFallback?: React.ReactNode;
447
+ }
448
+ ```
449
+
450
+ The `facetConfig` options allows you to change the way the metadata is displayed. The types are:
451
+ ```ts
452
+ type FacetConfig = {
453
+ id: string;
454
+ label: InternationalString;
455
+ keys: string[];
456
+ values?: FacetConfigValue[];
457
+ };
458
+
459
+ type FacetConfigValue = {
460
+ id: string;
461
+ label: InternationalString;
462
+ values: string[];
463
+ key: string;
464
+ };
465
+ ```
466
+
467
+ Example:
468
+ ```ts
469
+ const facetConfig = [
470
+ {
471
+ id: 'topics',
472
+ label: { en: ['Topics'] },
473
+ keys: ['Topic', 'Subject'],
474
+ },
475
+ {
476
+ id: 'collections',
477
+ label: { en: ['Collection'] },
478
+ keys: ['Collection'],
479
+ values: [
480
+ {
481
+ id: 'featured',
482
+ label: { en: ['Featured collection'] },
483
+ values: ['col_00001', 'col_00002'],
484
+ },
485
+ {
486
+ id: 'paintings',
487
+ label: { en: ['Paintings'] },
488
+ values: ['col_00003'],
209
489
  }
210
- },
490
+ ]
211
491
  }
212
- };
492
+ ];
213
493
  ```
494
+
495
+ It's unlikely that this type of configuration would be created by hand, instead a tool would be used to clean up the Metadata or curated from multiple sources. In the example above, given the following metadata input:
496
+ ```json
497
+ [
498
+ {
499
+ "label": {"none": ["Topic"]},
500
+ "value": {"none": ["Some topic"]}
501
+ },
502
+ {
503
+ "label": {"none": ["Subject"]},
504
+ "value": {"none": ["Some subject", "Another subject"]}
505
+ },
506
+ {
507
+ "label": {"none": ["Collection"]},
508
+ "value": {"none": ["col_0003"]}
509
+ },
510
+ {
511
+ "label": {"none": ["Object identifier"]},
512
+ "value": {"none": ["123456"]}
513
+ }
514
+ ]
515
+ ```
516
+
517
+ Would be transformed to:
518
+ ```json
519
+ [
520
+ {
521
+ "label": {"en": ["Topics"]},
522
+ "value": {"none": ["Some topic", "Some subject", "Another subject"]}
523
+ },
524
+ {
525
+ "label": {"en": ["Collection"]},
526
+ "value": {"en": ["Paintings"]}
527
+ }
528
+ ]
529
+ ```
530
+
531
+ So the metadata that wasn't configured is skipped, values mapped and combined.