@museumwnf/viewer-core 1.15.1 → 1.16.1
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 +111 -2
- package/package.json +3 -2
- package/src/createStandardViewer.js +61 -0
- package/src/dxa/exhibition/useExhibitionCollection.js +176 -0
- package/src/dxa/exhibition/useExhibitionData.js +246 -0
- package/src/dxa/exhibition/useExhibitionPartner.js +62 -0
- package/src/dxa/exhibition/useExhibitionSheet.js +126 -0
- package/src/dxa/exhibition/useExhibitionTimeline.js +310 -0
- package/src/dxa/gallery/useGalleryCollection.js +175 -0
- package/src/dxa/gallery/useGalleryData.js +143 -0
- package/src/dxa/gallery/useGalleryPartner.js +77 -0
- package/src/dxa/gallery/useGallerySheet.js +118 -0
- package/src/dxa/gallery/useGalleryTimeline.js +226 -0
- package/src/dxa/index.js +34 -0
- package/src/dxa/shared.js +53 -0
- package/src/index.js +1 -0
package/README.md
CHANGED
|
@@ -44,7 +44,25 @@ The `defineViewerConfig` helper returns a Vite config object with the shared set
|
|
|
44
44
|
|
|
45
45
|
## Usage
|
|
46
46
|
|
|
47
|
-
`main.js
|
|
47
|
+
`main.js`, once its own CSS imports and locale glob are done (see
|
|
48
|
+
`createStandardViewer` below for exactly what a website still writes):
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import { createStandardViewer } from '@museumwnf/viewer-core'
|
|
52
|
+
import { catalogues } from '@museumwnf/viewer-i18n/gallery'
|
|
53
|
+
import config from './dataset.config.js'
|
|
54
|
+
import SiteShell from './SiteShell.vue'
|
|
55
|
+
|
|
56
|
+
const ownMessages = {}
|
|
57
|
+
for (const [path, module] of Object.entries(import.meta.glob('../locales/*.json', { eager: true }))) {
|
|
58
|
+
ownMessages[path.split('/').pop().replace(/\.json$/, '')] = module.default
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
createStandardViewer(config, SiteShell, { dictionary: catalogues, ownMessages })
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A website that wants the lower-level building block on its own — no shell
|
|
65
|
+
wiring, no message merge, caller mounts it — calls `createViewer()` directly:
|
|
48
66
|
|
|
49
67
|
```js
|
|
50
68
|
import { createViewer } from '@museumwnf/viewer-core'
|
|
@@ -54,6 +72,46 @@ createViewer(config).mount('#app')
|
|
|
54
72
|
|
|
55
73
|
## API
|
|
56
74
|
|
|
75
|
+
### `createStandardViewer(config, siteClass, options)` → the mounted Vue app
|
|
76
|
+
|
|
77
|
+
The one-call form of the `main.js` pattern above: merges `options.dictionary`
|
|
78
|
+
(a `@museumwnf/viewer-i18n/{standalone|gallery|exhibition}` catalogue) with
|
|
79
|
+
`options.ownMessages` (a website's own texts, local wins — same rule as
|
|
80
|
+
[`mergeMessages`](#texts)), calls `createViewer()` with `siteClass` as the
|
|
81
|
+
shell (only when `config.shell` isn't already set — an existing
|
|
82
|
+
`dataset.config.js` that sets `shell` itself keeps working unchanged),
|
|
83
|
+
mounts the result at `options.el` (`'#app'` by default) and returns the
|
|
84
|
+
mounted app.
|
|
85
|
+
|
|
86
|
+
| Argument | Type | Required | Meaning |
|
|
87
|
+
| --- | --- | --- | --- |
|
|
88
|
+
| `config` | object | yes | the website's `dataset.config.js` object, exactly as passed to `createViewer` |
|
|
89
|
+
| `siteClass` | Vue component | no | the website's `SiteShell`; used as `config.shell` when `config.shell` is unset |
|
|
90
|
+
| `options.dictionary` | object | no | the family catalogue from `@museumwnf/viewer-i18n` |
|
|
91
|
+
| `options.ownMessages` | object | no | the website's own catalogue (e.g. from the `import.meta.glob` loop); overloads `dictionary`, local wins |
|
|
92
|
+
| `options.el` | string \| Element | no | mount target, defaults to `'#app'` |
|
|
93
|
+
|
|
94
|
+
Without `options.dictionary`, the merge step is skipped and `config.messages`
|
|
95
|
+
(or `options.ownMessages` on its own) is used as-is — a website that has
|
|
96
|
+
already merged its texts, or has none to merge, needs neither.
|
|
97
|
+
|
|
98
|
+
`viewer-core` does not import `@museumwnf/viewer-i18n` itself: its three
|
|
99
|
+
family dictionaries are separate subpath entry points precisely so a
|
|
100
|
+
website's bundle carries only the one it uses, and choosing between them at
|
|
101
|
+
runtime from a string would cost that tree-shaking (or force every consumer
|
|
102
|
+
to depend on the whole package). The website still imports its own family's
|
|
103
|
+
`catalogues` and passes it in.
|
|
104
|
+
|
|
105
|
+
What a website's `main.js` still writes, and why it cannot move here:
|
|
106
|
+
|
|
107
|
+
- its own CSS imports (`@museumwnf/viewer-layout/style.css`, theme and site
|
|
108
|
+
stylesheets) — each is a static, site-relative specifier only the site's
|
|
109
|
+
own module can write;
|
|
110
|
+
- the `import.meta.glob('../locales/*.json', { eager: true })` loop — a
|
|
111
|
+
glob is resolved relative to the file that calls it, so from inside this
|
|
112
|
+
package it would look for `locales/` next to viewer-core, not next to the
|
|
113
|
+
website.
|
|
114
|
+
|
|
57
115
|
### `createViewer(config)` → Vue app (caller mounts it)
|
|
58
116
|
|
|
59
117
|
`config` is the website's `dataset.config.js` object:
|
|
@@ -119,7 +177,7 @@ interpolation, no pluralisation, no HTML. A value produced at run time — a
|
|
|
119
177
|
number, a date, a count — is rendered by the component *next to* the text,
|
|
120
178
|
never inside it, which is what lets a text be translated freely.
|
|
121
179
|
|
|
122
|
-
Shared texts come from [`@museumwnf/viewer-i18n`](https://github.com/
|
|
180
|
+
Shared texts come from [`@museumwnf/viewer-i18n`](https://github.com/museumwithnofrontiers/viewer-i18n);
|
|
123
181
|
a website's own texts come from its `locales/<lang>.json`. The website merges
|
|
124
182
|
them and passes the result as `config.messages` — local wins, and that is the
|
|
125
183
|
only merge rule:
|
|
@@ -137,6 +195,11 @@ for (const [path, module] of Object.entries(import.meta.glob('../locales/*.json'
|
|
|
137
195
|
createViewer({ ...config, messages: mergeMessages(catalogues, local) }).mount('#app')
|
|
138
196
|
```
|
|
139
197
|
|
|
198
|
+
`createStandardViewer(config, siteClass, { dictionary, ownMessages })` (see
|
|
199
|
+
[above](#createstandardviewerconfig-siteclass-options--the-mounted-vue-app))
|
|
200
|
+
does exactly this merge, plus the shell wiring and the mount — a website
|
|
201
|
+
normally reaches for that instead of calling `mergeMessages` itself.
|
|
202
|
+
|
|
140
203
|
| Export | Use |
|
|
141
204
|
| --- | --- |
|
|
142
205
|
| `$t(key)` | in a template |
|
|
@@ -433,6 +496,52 @@ needs it; the standalone sites never import it.
|
|
|
433
496
|
| `partnerFromKey(partners, countries, countryCode, legacyId)` | the partner a legacy `/partner/:country/:id` route names, or null — `partnerKey` in reverse |
|
|
434
497
|
| `itemFromUidPath(items, path)` | the item a legacy dbUid *path* names, or null — `backward_compatibility` with `:` swapped for `/`, matched case-insensitively (Sharing History stores its keys lowercase) |
|
|
435
498
|
|
|
499
|
+
### `@museumwnf/viewer-core/dxa`
|
|
500
|
+
|
|
501
|
+
The gallery/exhibition-pair composables the four live DXA sites each wrote
|
|
502
|
+
for themselves — `useCollection`/`useGalleryData`, the timeline, the
|
|
503
|
+
partner specs and the item sheet — byte-identical within each pair
|
|
504
|
+
(carpets/amulets — a gallery; the-use-of-colours-in-art/water-in-islam — an
|
|
505
|
+
exhibition). Promoted here per family, never as one composable that papers
|
|
506
|
+
over the two: the cross-family diff is real behaviour (the exhibition
|
|
507
|
+
shape's per-language-build 404 gate, its own local-vs-country timeline
|
|
508
|
+
switch, its manifest-driven citation line), confirmed file by file against
|
|
509
|
+
both pairs' `origin/main` (epic metanull/inventory-app#1730). A separate
|
|
510
|
+
entry point because only a gallery/exhibition site needs it.
|
|
511
|
+
|
|
512
|
+
A site composes its own data/catalogue/timeline/partner/sheet module from
|
|
513
|
+
these, threading the `data` object one family's
|
|
514
|
+
`useGalleryData()`/`useExhibitionData()` call returns into the rest — the
|
|
515
|
+
same explicit parameter every function below takes, rather than each
|
|
516
|
+
re-reading a module-level singleton:
|
|
517
|
+
|
|
518
|
+
```js
|
|
519
|
+
import {
|
|
520
|
+
useGalleryData, useGalleryCollection, useGalleryTimeline, useGalleryPartner, useGallerySheet,
|
|
521
|
+
} from '@museumwnf/viewer-core/dxa'
|
|
522
|
+
|
|
523
|
+
const data = useGalleryData()
|
|
524
|
+
const collection = useGalleryCollection(data)
|
|
525
|
+
const timeline = useGalleryTimeline(data, collection)
|
|
526
|
+
const partner = useGalleryPartner(data, collection)
|
|
527
|
+
const sheet = useGallerySheet(data)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
No export below reads a site name, a legacy project key, or a UUID: what a
|
|
531
|
+
family's composables need beyond the data package itself is a `config`
|
|
532
|
+
parameter (currently unused by either family — every DXA site so far
|
|
533
|
+
shares the same entity list and catalogue shape; documented per function
|
|
534
|
+
below for whichever future site needs its own).
|
|
535
|
+
|
|
536
|
+
| Export (gallery / exhibition) | Reads | Returns |
|
|
537
|
+
| --- | --- | --- |
|
|
538
|
+
| `useGalleryData(config)` / `useExhibitionData(config)` | `config.eager` (entity names `loadEnglish` loads eagerly; default the family's own list, the exhibition's also carrying `themes`) | the family's whole data layer: `tr`/`md`/`mdInline`/`mdStrip`/`labelOf`/`loadEnglish`/`translations`, every entity and lookup map (`items`, `partners`, `countries`, `tags`, `dynasties`, `timelines`, `timelineEvents`, …), the routes (`itemRoute`, `partnerRoute`, `partnerObjectsRoute`), the legacy URL wrappers (`itemFromUidPath`, `partnerFromKey` — gallery only), `chromeImage`, `manifest`, `defaultLang`, plus family-specific fields (gallery: `siblingGalleries`/`pickSiblings`; exhibition: `isHiddenPartner`, `isInstitution`, `projectName`, `exhibitionTitle`/`exhibitionSubtitle`/`exhibitionHeadline`/`bannerCaption`, `siblingSites`) |
|
|
539
|
+
| `useGalleryCollection(data, config)` / `useExhibitionCollection(data, config)` | the `data` object above; `config` unused today | `FACETS`, `tile(item, t)`, `collectionResults` (the `CatalogueResultsView` spec), plus the facet-value helpers (`countryIdForCode`, `tagLabelForLegacy`, gallery's own `tagIdForLegacy`, `hasEveryTag`) |
|
|
540
|
+
| `useGalleryTimeline(data, collection, config)` / `useExhibitionTimeline(data, collection, config)` | `data`, the `collection` object above (for its `tile`); `config` unused today | the timeline specs (`timelineResults`/`timelineSpec`, `timelineGallery`/`timelineGallerySpec`) and their helpers (`countryLabel`/`countryIdForCode`, `timelineGalleryItems`; the exhibition shape adds `usesLocalTimeline`, `hasTimeline`, `timelineCountries`, `findEvents` for its own local-vs-country chronology switch) |
|
|
541
|
+
| `useGalleryPartner(data, collection, config)` / `useExhibitionPartner(data, config)` | `data`, and (gallery only) `collection` for its `tile` | `partnerList`/`partnerListSpec`, `partnerSheet`/`partnerSheetSpec`, and (gallery only) `partnerObjects` — the exhibition shape's `partnerSpecs.js` never carried an objects-page spec |
|
|
542
|
+
| `useGallerySheet(data, config)` / `useExhibitionSheet(data, config)` | `data`; `config` unused today | `{ itemSheet }`, the `RecordView` spec — the exhibition shape's `citation.project` reads `data.projectName`, absent from the gallery shape |
|
|
543
|
+
| `PAGE_SIZE`, `DATE_MODE`, `FACET_CATEGORIES`, `FACET_LABEL_KEYS`, `useFacetLabels()` | — | the five THG catalogue facets, shared verbatim by both families (nine tiles a page, containment dates, `type`/`dynasty`/`subject`/`material`/`artist`) |
|
|
544
|
+
|
|
436
545
|
### The declaration outside a component
|
|
437
546
|
|
|
438
547
|
| Export | Meaning |
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@museumwnf/viewer-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.1",
|
|
4
4
|
"description": "Application engine for MWNF websites: turns a dataset.config.js into a mounted Vue application.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/
|
|
9
|
+
"url": "git+https://github.com/museumwithnofrontiers/viewer-core.git"
|
|
10
10
|
},
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"module": "./src/index.js",
|
|
20
20
|
"exports": {
|
|
21
21
|
".": "./src/index.js",
|
|
22
|
+
"./dxa": "./src/dxa/index.js",
|
|
22
23
|
"./i18n": "./src/i18n/index.js",
|
|
23
24
|
"./legacy": "./src/legacy/index.js",
|
|
24
25
|
"./testing": "./src/testing/index.js",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createViewer } from './createViewer.js'
|
|
2
|
+
import { mergeMessages } from './i18n/index.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build, wire up and mount a standard MWNF website in one call.
|
|
6
|
+
*
|
|
7
|
+
* Every website's `main.js` does the same three things after its own CSS
|
|
8
|
+
* and locale imports: merge the shared dictionary its family pulls in
|
|
9
|
+
* (`@museumwnf/viewer-i18n/{standalone|gallery|exhibition}`) under its own
|
|
10
|
+
* `locales/*.json` overloads (local wins), hand the result and the site's
|
|
11
|
+
* shell component to `createViewer()`, and mount the app. This helper does
|
|
12
|
+
* those three steps, so `main.js` is left with only what genuinely cannot
|
|
13
|
+
* move here:
|
|
14
|
+
*
|
|
15
|
+
* - its own CSS imports (`@museumwnf/viewer-layout/style.css`, the theme
|
|
16
|
+
* and site stylesheets) — Vite needs a static, site-relative specifier
|
|
17
|
+
* for each one, which only the site's own module can write;
|
|
18
|
+
* - the `import.meta.glob('../locales/*.json', { eager: true })` loop that
|
|
19
|
+
* loads the site's own texts — a glob is resolved relative to the file
|
|
20
|
+
* that calls it, so run from inside this package it would look for
|
|
21
|
+
* `locales/` next to viewer-core, not next to the website.
|
|
22
|
+
*
|
|
23
|
+
* `viewer-core` does not depend on `@museumwnf/viewer-i18n`: that package's
|
|
24
|
+
* three family dictionaries are independent subpath entry points precisely
|
|
25
|
+
* so a website's bundle carries only the one it uses. Importing one of them
|
|
26
|
+
* from here — or choosing between them at runtime from a string in
|
|
27
|
+
* `config` — would either force every consumer to carry all three or
|
|
28
|
+
* replace a bundler-time choice with one a bundler cannot tree-shake. The
|
|
29
|
+
* website still imports its own family's `catalogues` and passes it as
|
|
30
|
+
* `dictionary`.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} config - the site's `dataset.config.js` object, exactly
|
|
33
|
+
* as passed to {@link createViewer} today
|
|
34
|
+
* @param {object} [siteClass] - the site's `SiteShell` component, used as
|
|
35
|
+
* `config.shell` when `config` does not already set one (a config that
|
|
36
|
+
* still declares its own `shell` keeps taking precedence, so existing
|
|
37
|
+
* `dataset.config.js` files need no change)
|
|
38
|
+
* @param {object} [options]
|
|
39
|
+
* @param {object} [options.dictionary] - the family's shared catalogue,
|
|
40
|
+
* i.e. `catalogues` imported from
|
|
41
|
+
* `@museumwnf/viewer-i18n/{standalone|gallery|exhibition}`
|
|
42
|
+
* @param {object} [options.ownMessages] - the website's own catalogue,
|
|
43
|
+
* already loaded (e.g. via the `import.meta.glob` loop above); merged
|
|
44
|
+
* over `dictionary` with {@link mergeMessages} — local wins. Used on its
|
|
45
|
+
* own, without a `dictionary`, when a website has no shared family text
|
|
46
|
+
* @param {string|Element} [options.el] - the mount target, defaults to
|
|
47
|
+
* `'#app'`, exactly like every website's current `main.js`
|
|
48
|
+
* @returns {import('vue').App} the mounted Vue application (call
|
|
49
|
+
* `.unmount()` on it, e.g. in a test)
|
|
50
|
+
*/
|
|
51
|
+
export function createStandardViewer(config, siteClass, options = {}) {
|
|
52
|
+
const { dictionary, ownMessages, el = '#app' } = options
|
|
53
|
+
|
|
54
|
+
const messages = dictionary
|
|
55
|
+
? mergeMessages(dictionary, ownMessages ?? {})
|
|
56
|
+
: (config.messages ?? ownMessages ?? {})
|
|
57
|
+
|
|
58
|
+
const app = createViewer({ ...config, shell: config.shell ?? siteClass, messages })
|
|
59
|
+
app.mount(el)
|
|
60
|
+
return app
|
|
61
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { DATE_MODE, FACET_CATEGORIES, PAGE_SIZE } from '../shared.js'
|
|
2
|
+
|
|
3
|
+
export { DATE_MODE, FACET_CATEGORIES, FACET_LABEL_KEYS, PAGE_SIZE, useFacetLabels } from '../shared.js'
|
|
4
|
+
|
|
5
|
+
// The catalogue spec: what a DXA exhibition's lists filter and search on.
|
|
6
|
+
// The engine — query state, options, dates, pages, the boolean grammar — is
|
|
7
|
+
// viewer-core's own; what is declared here is only what is an exhibition's:
|
|
8
|
+
// the facet *predicates* over its own data, the haystack the search bar
|
|
9
|
+
// reads, and the tile a record becomes.
|
|
10
|
+
// `PAGE_SIZE`/`DATE_MODE`/`FACET_CATEGORIES`/`FACET_LABEL_KEYS`/
|
|
11
|
+
// `useFacetLabels` are shared verbatim with the gallery family — see
|
|
12
|
+
// `../shared.js`.
|
|
13
|
+
//
|
|
14
|
+
// Legacy asked the API for everything: `/items?ic[]=…&id[]=…&na=…&nz=…` for
|
|
15
|
+
// the results, and `/items/countries`, `/items/tags`, `/items/years` *with
|
|
16
|
+
// the same filters applied* for the dropdowns — which is what made the
|
|
17
|
+
// facets dependent (pick a country, and the type list shrinks to the types
|
|
18
|
+
// still reachable). The results page hands the engine the matching subset
|
|
19
|
+
// for that reason; the entrance hands it everything.
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The exhibition's collection spec, over its own data layer's `data` (the
|
|
23
|
+
* return value of {@link import('./useExhibitionData.js').useExhibitionData}).
|
|
24
|
+
*
|
|
25
|
+
* Reads `data.countries`, `data.tags`, `data.countryById`, `data.tagById`,
|
|
26
|
+
* `data.itemById`, `data.labelOf`, `data.itemRoute`, `data.tr`,
|
|
27
|
+
* `data.defaultLang`, `data.mdInline`, `data.projectName`. `config` carries
|
|
28
|
+
* nothing today.
|
|
29
|
+
*/
|
|
30
|
+
export function useExhibitionCollection(data, config = {}) {
|
|
31
|
+
void config
|
|
32
|
+
const {
|
|
33
|
+
countries, tags, countryById, tagById, itemById, labelOf, itemRoute, tr, defaultLang, mdInline, projectName,
|
|
34
|
+
} = data
|
|
35
|
+
|
|
36
|
+
// ── What the URL carries ─────────────────────────────────────────────
|
|
37
|
+
//
|
|
38
|
+
// The legacy 2-letter country code for `country`, the legacy tag id for a
|
|
39
|
+
// facet: the values legacy's URLs carried, so a link shared then still
|
|
40
|
+
// resolves now.
|
|
41
|
+
|
|
42
|
+
function countryIdForCode(code) {
|
|
43
|
+
return code ? (countries.value ?? []).find((c) => c.code === code)?.id ?? null : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function tagLabelForLegacy(legacyId) {
|
|
47
|
+
return (tags.value ?? []).find((t) => t.legacy_tag_id === legacyId)?.label ?? legacyId
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The facet spec for viewer-core's `useFacets`: the country by code, each
|
|
52
|
+
* tag category by legacy id, labels upper-cased on the first letter as
|
|
53
|
+
* legacy printed them.
|
|
54
|
+
*/
|
|
55
|
+
const FACETS = {
|
|
56
|
+
country: {
|
|
57
|
+
values: (item) => countryById.value.get(item.country_id)?.code ?? null,
|
|
58
|
+
label: (code) => labelOf('countries', countryIdForCode(code)),
|
|
59
|
+
},
|
|
60
|
+
...Object.fromEntries(
|
|
61
|
+
FACET_CATEGORIES.map((category) => [
|
|
62
|
+
category,
|
|
63
|
+
{
|
|
64
|
+
values: (item) =>
|
|
65
|
+
(item.tag_ids ?? [])
|
|
66
|
+
.map((id) => tagById.value.get(id))
|
|
67
|
+
.filter((tag) => tag && tag.category === category)
|
|
68
|
+
.map((tag) => tag.legacy_tag_id),
|
|
69
|
+
label: tagLabelForLegacy,
|
|
70
|
+
capitalize: true,
|
|
71
|
+
},
|
|
72
|
+
]),
|
|
73
|
+
),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── The search bar ───────────────────────────────────────────────────
|
|
77
|
+
//
|
|
78
|
+
// Legacy ran MySQL boolean full-text search over the English sheet; the
|
|
79
|
+
// haystack is the same set of fields, and the grammar is viewer-core's.
|
|
80
|
+
|
|
81
|
+
function haystack(item, text) {
|
|
82
|
+
return [
|
|
83
|
+
text.name, text.description, text.short_description, text.type, text.holder, text.dates,
|
|
84
|
+
text.location, text.provenance, text.alternate_name, text.place_of_production,
|
|
85
|
+
...(text.keywords ?? []), ...(text.materials ?? []),
|
|
86
|
+
item.internal_name, item.owner_reference, item.mwnf_reference,
|
|
87
|
+
labelOf('partners', item.partner_id), labelOf('countries', item.country_id),
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── The tile ──────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A record as viewer-layout's grid contract: the thumbnail, the name, the
|
|
95
|
+
* lines legacy's hover card carried (date, holder, place, source
|
|
96
|
+
* project). The project name is the manifest's own, read in the same
|
|
97
|
+
* `defaultLang` every other line of this tile is — nothing for a record
|
|
98
|
+
* legacy left nameless (the Explore case), so the line is dropped, not
|
|
99
|
+
* printed with a hole in it.
|
|
100
|
+
*/
|
|
101
|
+
function tile(item, t) {
|
|
102
|
+
const text = tr('items', item.id, defaultLang)
|
|
103
|
+
const project = projectName(item, defaultLang)
|
|
104
|
+
return {
|
|
105
|
+
id: item.id,
|
|
106
|
+
image: item.images?.[0]?.url ?? '',
|
|
107
|
+
imageAlt: labelOf('items', item.id),
|
|
108
|
+
name: mdInline(text.name ?? item.internal_name ?? ''),
|
|
109
|
+
meta: [
|
|
110
|
+
text.dates ?? '',
|
|
111
|
+
labelOf('partners', item.partner_id),
|
|
112
|
+
[text.location, labelOf('countries', item.country_id)].filter(Boolean).join(', '),
|
|
113
|
+
project ? `${t('catalogue.results.forProject')} ${project}` : '',
|
|
114
|
+
].filter(Boolean),
|
|
115
|
+
to: itemRoute(item),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── The results page, as a spec ──────────────────────────────────────
|
|
120
|
+
//
|
|
121
|
+
// What viewer-layout's `CatalogueResultsView` renders on
|
|
122
|
+
// `/collection-results`: the facets over the *matching* records (the
|
|
123
|
+
// dependent dropdowns above), the containment date rule, undated first,
|
|
124
|
+
// nine tiles a page, legacy's "Collection | <selections>" summary line.
|
|
125
|
+
// The panel itself is composed by the view's wrapper, in the aside where
|
|
126
|
+
// legacy put it, so no controls are declared here. Every text is an
|
|
127
|
+
// entry name.
|
|
128
|
+
|
|
129
|
+
// `from`/`to`, not `start`/`end`: the entrance's `SearchFormView` spec
|
|
130
|
+
// (`mode: 'facets'`, `dates: 'buckets'`) writes the date bounds under
|
|
131
|
+
// those two fixed keys — the view's own, not something a spec can rename
|
|
132
|
+
// — so the results side has to read the same ones to stay linked to it.
|
|
133
|
+
const KEYS = ['country', ...FACET_CATEGORIES, 'from', 'to']
|
|
134
|
+
|
|
135
|
+
/** The filter summary line legacy printed as "Collection | <selections>". */
|
|
136
|
+
function filterSummary(filters, t) {
|
|
137
|
+
const parts = []
|
|
138
|
+
if (filters.country) parts.push(labelOf('countries', countryIdForCode(filters.country)))
|
|
139
|
+
for (const key of FACET_CATEGORIES) if (filters[key]) parts.push(tagLabelForLegacy(filters[key]))
|
|
140
|
+
if (filters.from) parts.push(`${t('catalogue.filter.from')} ${filters.from}`)
|
|
141
|
+
if (filters.to) parts.push(`${t('catalogue.filter.to')} ${filters.to}`)
|
|
142
|
+
return parts.filter(Boolean).join(' | ')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const collectionResults = {
|
|
146
|
+
entity: 'items',
|
|
147
|
+
keys: KEYS,
|
|
148
|
+
facets: FACETS,
|
|
149
|
+
facetScope: 'matching',
|
|
150
|
+
filterMode: 'immediate',
|
|
151
|
+
// The engine reads the raw `items` entity; a per-language build ships
|
|
152
|
+
// every member's `languages` array but not every member's text in this
|
|
153
|
+
// language, and legacy 404s such a record (`itemById`'s own rule, in
|
|
154
|
+
// useExhibitionData.js). The results page must not offer what the sheet
|
|
155
|
+
// would refuse to open, so it is filtered to the same renderable
|
|
156
|
+
// subset.
|
|
157
|
+
scope: (item) => itemById.value.has(item.id),
|
|
158
|
+
dates: { mode: DATE_MODE, begin: 'from', end: 'to' },
|
|
159
|
+
sort: { undated: 'first' },
|
|
160
|
+
pageSize: PAGE_SIZE,
|
|
161
|
+
variant: 'grid',
|
|
162
|
+
recordRoute: 'item',
|
|
163
|
+
actionLabel: 'exhibition.action.seeDatabaseEntry',
|
|
164
|
+
empty: 'catalogue.results.noResults',
|
|
165
|
+
pagination: { jump: true },
|
|
166
|
+
|
|
167
|
+
record: (item, { t }) => tile(item, t),
|
|
168
|
+
|
|
169
|
+
summary: ({ filters, pageInfo, t }) => [
|
|
170
|
+
{ label: t('exhibition.section.collection'), value: filterSummary(filters, t) },
|
|
171
|
+
{ count: pageInfo.total, value: `${t('catalogue.results.outOf')} ${itemById.value.size} ${t('catalogue.results.objects')}` },
|
|
172
|
+
],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { FACETS, haystack, tile, collectionResults, countryIdForCode, tagLabelForLegacy }
|
|
176
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import { mediaUrl } from '../../siteConfig.js'
|
|
3
|
+
import { useCatalogueData } from '../../composables/useCatalogueData.js'
|
|
4
|
+
import { useDataPackage } from '../../composables/useDataPackage.js'
|
|
5
|
+
import { byId, entityRef } from '../../composables/useEntities.js'
|
|
6
|
+
import { projectLabel } from '../../conventions.js'
|
|
7
|
+
|
|
8
|
+
// The data layer every DXA exhibition site (the-use-of-colours-in-art,
|
|
9
|
+
// water-in-islam, …) wrote for itself: the entities an exhibition reads,
|
|
10
|
+
// the lookup maps over them, its routes, its legacy key mappings and its
|
|
11
|
+
// chrome. Promoted from the byte-identical copies the two live exhibitions
|
|
12
|
+
// carried (epic #1730) — what a future exhibition needs that is genuinely
|
|
13
|
+
// its own belongs in `config`, not in a fork of this file. The themes tree
|
|
14
|
+
// stays in a site's own `composables/themes.js`, on top of viewer-core's
|
|
15
|
+
// `useCollectionTree`.
|
|
16
|
+
|
|
17
|
+
/** Every entity an exhibition page reads English labels or fallbacks for. */
|
|
18
|
+
const DEFAULT_EAGER = ['items', 'partners', 'countries', 'glossary', 'dynasties', 'timeline_events', 'themes']
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The exhibition's data layer, bound to the installed data package. Each
|
|
22
|
+
* entity is a shared ref that stays `null` until a route declaring it in
|
|
23
|
+
* `meta.entities` brings its chunk in, so calling this loads nothing, and a
|
|
24
|
+
* page pays only for what it reads.
|
|
25
|
+
*
|
|
26
|
+
* `config.eager` overrides the entities `loadEnglish` loads eagerly
|
|
27
|
+
* (default: {@link DEFAULT_EAGER}).
|
|
28
|
+
*/
|
|
29
|
+
export function useExhibitionData(config = {}) {
|
|
30
|
+
const dataPackage = useDataPackage()
|
|
31
|
+
const manifest = dataPackage.manifest
|
|
32
|
+
|
|
33
|
+
// English is the base language of every catalogue in the platform: every
|
|
34
|
+
// list, label and fallback reads it. A record the visitor reads in
|
|
35
|
+
// another language is resolved on the sheet itself, by
|
|
36
|
+
// `useRecordLanguage`.
|
|
37
|
+
const defaultLang = 'en'
|
|
38
|
+
|
|
39
|
+
const exhibition = entityRef('exhibition')
|
|
40
|
+
const relatedContent = entityRef('related_content')
|
|
41
|
+
const tags = entityRef('tags')
|
|
42
|
+
const countries = entityRef('countries')
|
|
43
|
+
const languages = entityRef('languages')
|
|
44
|
+
const dynasties = entityRef('dynasties')
|
|
45
|
+
const glossary = entityRef('glossary')
|
|
46
|
+
const timelines = entityRef('timelines')
|
|
47
|
+
const timelineEvents = entityRef('timeline_events')
|
|
48
|
+
|
|
49
|
+
// E6: a hidden museum is exported but must not appear on any list or
|
|
50
|
+
// profile page. Its items still render — legacy hides the museum, not the
|
|
51
|
+
// object. Declared here as a `visible` predicate rather than coded into
|
|
52
|
+
// every page that lists partners; `isHiddenPartner` stays a named export
|
|
53
|
+
// because the item sheet's holder line is the one surface that needs the
|
|
54
|
+
// opposite of `visible` — the museum keeps its name and loses only the
|
|
55
|
+
// link, because it has no page to link to.
|
|
56
|
+
const hiddenPartnerIds = computed(() => new Set(exhibition.value?.hidden_partner_ids ?? []))
|
|
57
|
+
function isHiddenPartner(partner) {
|
|
58
|
+
return hiddenPartnerIds.value.has(partner?.id)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const catalogue = useCatalogueData({
|
|
62
|
+
eager: config.eager ?? DEFAULT_EAGER,
|
|
63
|
+
defaultLanguage: defaultLang,
|
|
64
|
+
visible: {
|
|
65
|
+
// An item's `languages` is what it has TRANSLATIONS in, so a
|
|
66
|
+
// non-empty array without this build's language means the text
|
|
67
|
+
// exists in some other language and not in this one — legacy's own
|
|
68
|
+
// instance 404s such a record, and this build drops it to match.
|
|
69
|
+
//
|
|
70
|
+
// An EMPTY array is a different case and must not be swept in with
|
|
71
|
+
// it: it means the package has no text in ANY language, which is a
|
|
72
|
+
// gap in the export rather than a fact about the record, and legacy
|
|
73
|
+
// serves those records regardless. They keep their legacy names
|
|
74
|
+
// through `labelOf`'s `internal_name` fallback and lose only their
|
|
75
|
+
// descriptions. Hence the `!i.languages?.length ||` guard, which
|
|
76
|
+
// reads like a redundant null-check and is not.
|
|
77
|
+
items: (i) => !i.languages?.length || i.languages.includes(defaultLang),
|
|
78
|
+
partners: (p) => !hiddenPartnerIds.value.has(p.id),
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const items = catalogue.entity('items')
|
|
83
|
+
const itemById = catalogue.index('items')
|
|
84
|
+
const visiblePartners = catalogue.entity('partners')
|
|
85
|
+
const visiblePartnerIndex = catalogue.index('partners')
|
|
86
|
+
function visiblePartnerById(id) {
|
|
87
|
+
return visiblePartnerIndex.value.get(id) ?? null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const {
|
|
91
|
+
tr, md, mdInline, mdStrip, labelOf, loadEnglish, availableLanguages, loadTranslations, translations,
|
|
92
|
+
} = catalogue
|
|
93
|
+
loadEnglish()
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Exhibition chrome images and the related-content PDFs.
|
|
97
|
+
* `banner_image_path`, `homepage_image_path` and `document_path` were
|
|
98
|
+
* never imported into inventory storage, so the package ships the legacy
|
|
99
|
+
* path and the address is built from the host the site's
|
|
100
|
+
* `dataset.config.js` declares under `media`. `size` ∈ zoom | hi_res |
|
|
101
|
+
* lo_res | small | full.
|
|
102
|
+
*/
|
|
103
|
+
function chromeImage(path, size = 'hi_res') {
|
|
104
|
+
return mediaUrl(path, size)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Lookup maps ────────────────────────────────────────────────────────
|
|
108
|
+
//
|
|
109
|
+
// Unfiltered, unlike `itemById`/`visiblePartnerById`: a holder line still
|
|
110
|
+
// needs to resolve a hidden museum's name (`isHiddenPartner` above is
|
|
111
|
+
// what suppresses the link), and every other lookup here has no
|
|
112
|
+
// visibility rule to begin with.
|
|
113
|
+
const partnerById = byId('partners')
|
|
114
|
+
const countryById = byId('countries')
|
|
115
|
+
const tagById = byId('tags')
|
|
116
|
+
const dynastyById = byId('dynasties')
|
|
117
|
+
const glossaryById = byId('glossary')
|
|
118
|
+
const languageByCode = byId('languages', 'code')
|
|
119
|
+
|
|
120
|
+
// countries.json is keyed by the inventory id (ISO 3166-1 alpha-3), but
|
|
121
|
+
// the legacy two-letter code is what related_content and the timeline
|
|
122
|
+
// keyspaces carry. `code` is the country's own backward_compatibility, so
|
|
123
|
+
// this is the bridge between the two — and the reason it is a lookup
|
|
124
|
+
// rather than a parse is that several legacy codes are not ISO (`uk`,
|
|
125
|
+
// `pa`, `qt`, `ua`, `sb`).
|
|
126
|
+
const countryByCode = computed(
|
|
127
|
+
() => new Map((countries.value ?? []).filter((c) => c.code).map((c) => [c.code, c])),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
/** The same label from a legacy two-letter code (`uk` → United Kingdom). */
|
|
131
|
+
function countryLabelFromCode(code) {
|
|
132
|
+
if (!code) return ''
|
|
133
|
+
const country = countryByCode.value.get(code)
|
|
134
|
+
return country ? labelOf('countries', country.id) : code
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The canonical item route: the package id, and no language in the path. */
|
|
138
|
+
function itemRoute(item) {
|
|
139
|
+
return { name: 'item', params: { id: item.id } }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Institutions (monument owners) and museums both live in partners.json —
|
|
143
|
+
// the package ships the union of legacy's /partners and /institutions
|
|
144
|
+
// because a static package has no endpoints to split them across. The
|
|
145
|
+
// viewer routes by `type`, which is what legacy's two page templates
|
|
146
|
+
// keyed off.
|
|
147
|
+
function isInstitution(partner) {
|
|
148
|
+
return partner?.type === 'institution'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function partnerRoute(partner) {
|
|
152
|
+
return {
|
|
153
|
+
name: isInstitution(partner) ? 'institution' : 'partner',
|
|
154
|
+
params: { id: partner.id },
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function partnerObjectsRoute(partner, page = 1) {
|
|
159
|
+
return {
|
|
160
|
+
name: isInstitution(partner) ? 'institution-monuments' : 'partner-objects',
|
|
161
|
+
params: { id: partner.id },
|
|
162
|
+
query: page > 1 ? { page } : {},
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Source projects ────────────────────────────────────────────────────
|
|
167
|
+
//
|
|
168
|
+
// A member is borrowed from the MWNF project that originally published
|
|
169
|
+
// it. The name is the data package's own `manifest.projects` entry
|
|
170
|
+
// (populated by the importer/exporter), keyed by this record's
|
|
171
|
+
// `project_id` (a UUID) — not viewer-core's deprecated, legacy-key-keyed
|
|
172
|
+
// `projectName()`/`projectFamily()`, and not an exhibition's own
|
|
173
|
+
// project-key comparisons either. The manifest already names an
|
|
174
|
+
// exhibition's own native project under its own uuid (the sibling
|
|
175
|
+
// `Collection` the importer creates alongside every `Project` carries the
|
|
176
|
+
// same title), so there is no special case here for a native member any
|
|
177
|
+
// more; the colour swatch is this exhibition's own editorial choice
|
|
178
|
+
// (`dataset.config.js`'s `projectColors`, read by the site's `ItemSheet`).
|
|
179
|
+
|
|
180
|
+
// Some members have no `project_id` at all: they come from the Explore
|
|
181
|
+
// monuments database rather than from a project, which is why provenance
|
|
182
|
+
// has to be read from the keyspace here instead of from a field. Legacy
|
|
183
|
+
// still coloured them and still printed an empty project name, so its
|
|
184
|
+
// citation read `"…" in , Museum With No Frontiers, …` with a hole in it.
|
|
185
|
+
// The colour is reproduced (the site's own `.mwnf-chip--Explore` rule);
|
|
186
|
+
// the empty name is not, because a label reading "for" with nothing after
|
|
187
|
+
// it is a rendering fault rather than a faithful copy. The line is
|
|
188
|
+
// dropped instead.
|
|
189
|
+
function isExploreRecord(item) {
|
|
190
|
+
return (item?.backward_compatibility ?? '').startsWith('mwnf3_explore:')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** `null` for a record with no project (the Explore case) or whose
|
|
194
|
+
* project the manifest does not name in `lang`. */
|
|
195
|
+
function projectName(item, lang = defaultLang) {
|
|
196
|
+
return projectLabel(manifest, item?.project_id, lang)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The exhibition's own per-language chrome text. */
|
|
200
|
+
function exhibitionTitle(lang = defaultLang) {
|
|
201
|
+
return exhibition.value?.titles?.[lang] ?? exhibition.value?.titles?.en ?? ''
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function exhibitionSubtitle(lang = defaultLang) {
|
|
205
|
+
return exhibition.value?.subtitles?.[lang] ?? exhibition.value?.subtitles?.en ?? ''
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function exhibitionHeadline(lang = defaultLang) {
|
|
209
|
+
return exhibition.value?.headlines?.[lang] ?? exhibition.value?.headlines?.en ?? ''
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function bannerCaption(lang = defaultLang) {
|
|
213
|
+
return exhibition.value?.banner_captions?.[lang] ?? exhibition.value?.banner_captions?.en ?? ''
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Sibling sites ──────────────────────────────────────────────────────
|
|
217
|
+
//
|
|
218
|
+
// Decision Q3: these are reference objects, not resolved links. The
|
|
219
|
+
// exporter records identity plus whatever the import carried; where a
|
|
220
|
+
// `legacy_host` came across we can link to it, and where it did not the
|
|
221
|
+
// entry still renders — it just does not become an anchor.
|
|
222
|
+
|
|
223
|
+
const siblingSites = computed(
|
|
224
|
+
() => (exhibition.value?.sibling_sites ?? []).filter((s) => !s.hidden),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
function siblingUrl(sibling) {
|
|
228
|
+
return sibling?.legacy_host || null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
manifest,
|
|
233
|
+
defaultLang,
|
|
234
|
+
exhibition, relatedContent, tags, countries, languages, dynasties, glossary, timelines, timelineEvents,
|
|
235
|
+
isHiddenPartner,
|
|
236
|
+
items, itemById, visiblePartners, visiblePartnerById,
|
|
237
|
+
tr, md, mdInline, mdStrip, labelOf, loadEnglish, availableLanguages, loadTranslations, translations,
|
|
238
|
+
chromeImage,
|
|
239
|
+
partnerById, countryById, tagById, dynastyById, glossaryById, languageByCode,
|
|
240
|
+
countryByCode, countryLabelFromCode,
|
|
241
|
+
itemRoute, isInstitution, partnerRoute, partnerObjectsRoute,
|
|
242
|
+
isExploreRecord, projectName,
|
|
243
|
+
exhibitionTitle, exhibitionSubtitle, exhibitionHeadline, bannerCaption,
|
|
244
|
+
siblingSites, siblingUrl,
|
|
245
|
+
}
|
|
246
|
+
}
|