@museumwnf/viewer-core 2.2.0 → 2.4.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/README.md +7 -4
- package/package.json +1 -1
- package/src/dxa/countryCodes.js +92 -0
- package/src/dxa/exhibition/useExhibitionCollection.js +8 -12
- package/src/dxa/exhibition/useExhibitionData.js +6 -1
- package/src/dxa/exhibition/useExhibitionItemDetail.js +131 -0
- package/src/dxa/exhibition/useExhibitionThemes.js +279 -0
- package/src/dxa/exhibition/useExhibitionTimeline.js +38 -183
- package/src/dxa/gallery/useGalleryCollection.js +8 -11
- package/src/dxa/gallery/useGalleryData.js +6 -0
- package/src/dxa/gallery/useGalleryItemDetail.js +110 -0
- package/src/dxa/gallery/useGalleryTimeline.js +9 -95
- package/src/dxa/index.js +3 -0
package/README.md
CHANGED
|
@@ -402,7 +402,7 @@ Timeline pages are all one of these three axes over the same event shape.
|
|
|
402
402
|
|
|
403
403
|
| Export | Meaning |
|
|
404
404
|
| --- | --- |
|
|
405
|
-
| `useTimelineEvents({ scope, countryLabel, countryIdForCode, tr, timelinesEntity, eventsEntity })` | `{ countries, yearRange, yearBuckets(t), hasTimeline, usesLocalTimeline, findEvents({ country, collection, begin, end }) }`. `scope` is `'country'` (the worldwide merge), `'local'` (an exhibition's own `thg_local` chronology in place of the merge — `countries` is empty, there is nothing to filter by) or `'collection'` (the merge again, plus a `collection` filter on the exhibition a timeline is bound to, `null` meaning the Permanent Collection). `countries` is one entry per country with a chronology, `{ value, label }`, alphabetized, with an `{ value: 'all', label: null }` marker first (never, for `'local'`) — the marker carries no label of its own so a picker names it through its own catalogue entry. `countryLabel(id)` labels a country; the DXA legacy 2-letter code table stays out of
|
|
405
|
+
| `useTimelineEvents({ scope, countryLabel, countryIdForCode, tr, timelinesEntity, eventsEntity })` | `{ countries, yearRange, yearBuckets(t), hasTimeline, usesLocalTimeline, findEvents({ country, collection, begin, end }) }`. `scope` is `'country'` (the worldwide merge), `'local'` (an exhibition's own `thg_local` chronology in place of the merge — `countries` is empty, there is nothing to filter by) or `'collection'` (the merge again, plus a `collection` filter on the exhibition a timeline is bound to, `null` meaning the Permanent Collection). `countries` is one entry per country with a chronology, `{ value, label }`, alphabetized, with an `{ value: 'all', label: null }` marker first (never, for `'local'`) — the marker carries no label of its own so a picker names it through its own catalogue entry. `countryLabel(id)` labels a country; the DXA legacy 2-letter code table stays out of the engine (the DXA families' data layer carries it, `/dxa`), so a caller passing one instead resolves it through `countryIdForCode(code)`. `tr(id)` returns an event's translated fields, already resolved for entity and language, the same way a site's own data composable already binds it. `findEvents` returns the scope's events in `[begin, end]`, chronological, `display_order` breaking a tie on `year_from`, an event with no year at all last. |
|
|
406
406
|
| `overlapsRange(event, begin, end)` | legacy `class.hcr.inc.php`'s overlap rule, standalone: a dated event overlaps once its known end is past `begin` and its start is before `end`; an open-ended event (no known end) overlaps on its start alone; an undated event matches nothing but an unbounded search. |
|
|
407
407
|
| `effectiveYearTo(event)` | `event.year_to`, or `null` when it is `0` or absent — legacy's convention for an open-ended period. |
|
|
408
408
|
| `eventDateLabel(event, text, t)` | the label under an event, trying in order: a narrative chronology's own period name (`text.name` — a sort key there, not a label), a curated description pair (`text.date_from_description` / `text.date_to_description`), and the derived year range through `eraLabel`, for the worldwide merge which has neither. |
|
|
@@ -560,12 +560,15 @@ below for whichever future site needs its own).
|
|
|
560
560
|
|
|
561
561
|
| Export (gallery / exhibition) | Reads | Returns |
|
|
562
562
|
| --- | --- | --- |
|
|
563
|
-
| `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`) |
|
|
564
|
-
| `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
|
|
565
|
-
| `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
|
|
563
|
+
| `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), the DXA legacy country code both ways (`countryIdForCode(code)`, idempotent on an id, and `countryLabel(countryId)`, one table the collection and the timeline both read), `chromeImage`, `manifest`, `defaultLang`, plus family-specific fields (gallery: `siblingGalleries`/`pickSiblings`; exhibition: `isHiddenPartner`, `isInstitution`, `projectName`, `exhibitionTitle`/`exhibitionSubtitle`/`exhibitionHeadline`/`bannerCaption`, `siblingSites`) |
|
|
564
|
+
| `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` — the data layer's —, `tagLabelForLegacy`, gallery's own `tagIdForLegacy`, `hasEveryTag`) |
|
|
565
|
+
| `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`), `timelineEvents` (viewer-core's `useTimelineEvents` engine — for an exhibition, the one its flags put in force, country or local), and the data layer's `countryLabel`/`countryIdForCode`; the gallery adds `timelineGalleryItems`, the exhibition `usesLocalTimeline` and `hasTimeline` for its local-vs-country chronology switch |
|
|
566
566
|
| `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 |
|
|
567
567
|
| `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 |
|
|
568
568
|
| `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`) |
|
|
569
|
+
| `useGalleryItemDetail(data, timeline)` / `useExhibitionItemDetail(data, timeline)` | `data`, the family's `timeline` object; the site's `projectColors` and `noticeProjects` through `useSiteConfig()`, read lazily | `{ itemDetail }`, the item page's `RecordSheetView` spec: the family sheet plus the source-database chip, the Explore-partner notice, the holding-museum link (none for an exhibition's hidden partner), and the related block's labels, outside-reference chips, dynasty popouts and timeline popout. The exhibition gives a record with no project the `mwnf-chip--Explore` chip, and its "search this period" link carries only the timeline controls' keys |
|
|
570
|
+
| `useExhibitionThemes(data)` | `data` | the exhibition's themes: `themeTree` (a `useCollectionTree` over themes.json, `entity: 'themes'`), `themes`, `aboutTheme`, `listedThemes`, `themeByRouteId`/`themeRouteId` (legacy's `display_order - 1`), `romanFor`, `themePictures`, `pictureById`, `themeText`/`pictureText`, `owningTheme`, `subIndexOf`, `pictureParent`, `itemDetailString`, and `useThemePictures(node)`, the live `pictures` `PictureGallery`/`PictureNarrative` read |
|
|
571
|
+
| `buildThemePictures(nodePictures, deps)`, `romanFor(displayOrder)` | — | the pure halves of the above: a node's pictures with their `related`/`backRelated` links resolved across the whole tree, and legacy's Roman theme numeral (display order 2 is "I") |
|
|
569
572
|
|
|
570
573
|
### The declaration outside a component
|
|
571
574
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
|
|
3
|
+
// The DXA legacy country code, the one a gallery's or an exhibition's URLs
|
|
4
|
+
// carry (`?country=eg`, the facet values, the timeline's own links), and the
|
|
5
|
+
// inventory id the records are keyed by (`egy`). Both families read it for
|
|
6
|
+
// their collection and their timeline through this one table
|
|
7
|
+
// (inventory-app#2056); each of the four once defined its own.
|
|
8
|
+
//
|
|
9
|
+
// Names and legacy codes both come from countries.json. The exporter scopes
|
|
10
|
+
// that file to "member item countries ∪ their holders' countries ∪ the
|
|
11
|
+
// global timeline's countries", so it covers every timeline country — the
|
|
12
|
+
// `Intl.DisplayNames` fallback below is therefore dead code for them, and is
|
|
13
|
+
// kept only so a regressed package degrades to a rendered ISO code rather
|
|
14
|
+
// than a raw id. The `code` countries.json ships is the country's
|
|
15
|
+
// `backward_compatibility`, which is exactly the code legacy's own URLs
|
|
16
|
+
// used, including the ones that are not ISO 3166-1 alpha-2 (`uk`, `pa`,
|
|
17
|
+
// `qt`, `rm`, `sb`, `ua`, …). Those are the reason the fallback must stay
|
|
18
|
+
// unreachable rather than merely rare: read as ISO, `ua` is Ukraine and `sb`
|
|
19
|
+
// is the Solomon Islands, where legacy means the UAE and Serbia. Only
|
|
20
|
+
// countries.json can name them correctly.
|
|
21
|
+
|
|
22
|
+
// Two legacy codes are not ISO 3166-1 alpha-2.
|
|
23
|
+
const LEGACY_TO_ISO = { uk: 'GB', pa: 'PS' }
|
|
24
|
+
|
|
25
|
+
const regionNames = (() => {
|
|
26
|
+
try {
|
|
27
|
+
return new Intl.DisplayNames(['en'], { type: 'region' })
|
|
28
|
+
} catch {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
})()
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The legacy country code both ways, over a family data layer's
|
|
35
|
+
* `countries`, `countryById`, `timelines` and `labelOf`.
|
|
36
|
+
*
|
|
37
|
+
* - `countryIdForCode(code)`: the inventory id for a legacy code; idempotent
|
|
38
|
+
* on an id (the composed views' own country controls write ids), `null`
|
|
39
|
+
* for none, `'all'` or an unknown code.
|
|
40
|
+
* - `countryLabel(countryId)`: a country's name, for one countries.json
|
|
41
|
+
* lacks too (read off its timeline row).
|
|
42
|
+
*/
|
|
43
|
+
export function useLegacyCountryCodes({ countries, countryById, timelines, labelOf }) {
|
|
44
|
+
const countryIdSet = computed(() => new Set((countries.value ?? []).map((c) => c.id)))
|
|
45
|
+
|
|
46
|
+
// A lookup, not a parse. The fallback exists only for the regressed-package
|
|
47
|
+
// case described above, and it must agree with GLOBAL_TIMELINE_LIKE_PATTERNS
|
|
48
|
+
// in the exporter's timeline exporter: the country sits after the literal
|
|
49
|
+
// `country` segment in BOTH keyspaces (`mwnf3:hcr:country:<cc>` and
|
|
50
|
+
// `mwnf3_sharing_history:sh_hcr:country:<cc>:exhibition:2`), and it is the
|
|
51
|
+
// last segment in only the first — taking the last one yields `2` on the
|
|
52
|
+
// second.
|
|
53
|
+
function legacyCodeOf(timeline) {
|
|
54
|
+
const fromPackage = countryById.value.get(timeline.country_id)?.code
|
|
55
|
+
if (fromPackage) return fromPackage
|
|
56
|
+
const parts = (timeline.backward_compatibility ?? '').split(':')
|
|
57
|
+
const at = parts.indexOf('country')
|
|
58
|
+
return (at >= 0 ? parts[at + 1] : null) || timeline.country_id
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function nameFor(timeline) {
|
|
62
|
+
if (countryIdSet.value.has(timeline.country_id)) return labelOf('countries', timeline.country_id)
|
|
63
|
+
const legacy = legacyCodeOf(timeline)
|
|
64
|
+
const iso = LEGACY_TO_ISO[legacy] ?? String(legacy).toUpperCase()
|
|
65
|
+
try {
|
|
66
|
+
return regionNames?.of(iso) ?? iso
|
|
67
|
+
} catch {
|
|
68
|
+
return iso
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function countryLabel(countryId) {
|
|
73
|
+
if (countryIdSet.value.has(countryId)) return labelOf('countries', countryId)
|
|
74
|
+
const timeline = (timelines.value ?? []).find((t) => t.country_id === countryId)
|
|
75
|
+
return timeline ? nameFor(timeline) : countryId
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A country served by both chronologies has two timeline rows carrying the
|
|
79
|
+
// same code and the same `country_id`, so either row answers.
|
|
80
|
+
function countryIdForCode(code) {
|
|
81
|
+
if (!code || code === 'all') return null
|
|
82
|
+
// Already an id: pass it through unchanged, before the legacy-code
|
|
83
|
+
// lookups below get a chance to find nothing and match every record.
|
|
84
|
+
if (countryIdSet.value.has(code)) return code
|
|
85
|
+
const timeline = (timelines.value ?? []).find((t) => legacyCodeOf(t) === code)
|
|
86
|
+
if (timeline) return timeline.country_id
|
|
87
|
+
// A country with no chronology: countries.json's own code.
|
|
88
|
+
return (countries.value ?? []).find((c) => c.code === code)?.id ?? null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { countryIdForCode, countryLabel }
|
|
92
|
+
}
|
|
@@ -22,26 +22,22 @@ export { DATE_MODE, FACET_CATEGORIES, FACET_LABEL_KEYS, PAGE_SIZE, useFacetLabel
|
|
|
22
22
|
* The exhibition's collection spec, over its own data layer's `data` (the
|
|
23
23
|
* return value of {@link import('./useExhibitionData.js').useExhibitionData}).
|
|
24
24
|
*
|
|
25
|
-
* Reads `data.
|
|
26
|
-
* `data.
|
|
27
|
-
* `data.
|
|
28
|
-
* nothing today.
|
|
25
|
+
* Reads `data.tags`, `data.countryById`, `data.tagById`, `data.itemById`,
|
|
26
|
+
* `data.labelOf`, `data.itemRoute`, `data.tr`, `data.defaultLang`,
|
|
27
|
+
* `data.mdInline`, `data.projectName`, `data.countryIdForCode`. `config`
|
|
28
|
+
* carries nothing today.
|
|
29
29
|
*/
|
|
30
30
|
export function useExhibitionCollection(data, config = {}) {
|
|
31
31
|
void config
|
|
32
32
|
const {
|
|
33
|
-
|
|
33
|
+
tags, countryById, tagById, itemById, labelOf, itemRoute, tr, defaultLang, mdInline, projectName, countryIdForCode,
|
|
34
34
|
} = data
|
|
35
35
|
|
|
36
36
|
// ── What the URL carries ─────────────────────────────────────────────
|
|
37
37
|
//
|
|
38
|
-
// The legacy 2-letter country code for `country
|
|
39
|
-
//
|
|
40
|
-
// resolves now.
|
|
41
|
-
|
|
42
|
-
function countryIdForCode(code) {
|
|
43
|
-
return code ? (countries.value ?? []).find((c) => c.code === code)?.id ?? null : null
|
|
44
|
-
}
|
|
38
|
+
// The legacy 2-letter country code for `country` (the data layer's
|
|
39
|
+
// `countryIdForCode`), the legacy tag id for a facet: the values legacy's
|
|
40
|
+
// URLs carried, so a link shared then still resolves now.
|
|
45
41
|
|
|
46
42
|
function tagLabelForLegacy(legacyId) {
|
|
47
43
|
return (tags.value ?? []).find((t) => t.legacy_tag_id === legacyId)?.label ?? legacyId
|
|
@@ -4,6 +4,7 @@ import { useCatalogueData } from '../../composables/useCatalogueData.js'
|
|
|
4
4
|
import { useDataPackage } from '../../composables/useDataPackage.js'
|
|
5
5
|
import { byId, entityRef } from '../../composables/useEntities.js'
|
|
6
6
|
import { projectLabel } from '../../conventions.js'
|
|
7
|
+
import { useLegacyCountryCodes } from '../countryCodes.js'
|
|
7
8
|
|
|
8
9
|
// The data layer every DXA exhibition site (the-use-of-colours-in-art,
|
|
9
10
|
// water-in-islam, …) wrote for itself: the entities an exhibition reads,
|
|
@@ -134,6 +135,10 @@ export function useExhibitionData(config = {}) {
|
|
|
134
135
|
return country ? labelOf('countries', country.id) : code
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
// The legacy code both ways, the one table the collection and the
|
|
139
|
+
// timeline read.
|
|
140
|
+
const { countryIdForCode, countryLabel } = useLegacyCountryCodes({ countries, countryById, timelines, labelOf })
|
|
141
|
+
|
|
137
142
|
/** The canonical item route: the package id, and no language in the path. */
|
|
138
143
|
function itemRoute(item) {
|
|
139
144
|
return { name: 'item', params: { id: item.id } }
|
|
@@ -236,7 +241,7 @@ export function useExhibitionData(config = {}) {
|
|
|
236
241
|
tr, md, mdInline, mdStrip, labelOf, loadEnglish, availableLanguages, loadTranslations, translations,
|
|
237
242
|
chromeImage,
|
|
238
243
|
partnerById, countryById, tagById, dynastyById, glossaryById, languageByCode,
|
|
239
|
-
countryByCode, countryLabelFromCode,
|
|
244
|
+
countryByCode, countryLabelFromCode, countryIdForCode, countryLabel,
|
|
240
245
|
itemRoute, isInstitution, partnerRoute, partnerObjectsRoute,
|
|
241
246
|
isExploreRecord, projectName,
|
|
242
247
|
exhibitionTitle, exhibitionSubtitle, exhibitionHeadline, bannerCaption,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { eraLabel, roundOutward } from '../../catalogue/dates.js'
|
|
2
|
+
import { useSiteConfig } from '../../siteConfig.js'
|
|
3
|
+
import { useExhibitionSheet } from './useExhibitionSheet.js'
|
|
4
|
+
|
|
5
|
+
// An exhibition's item page, as viewer-layout's `RecordSheetView` spec
|
|
6
|
+
// (inventory-app#2053): `useExhibitionSheet`'s field sheet, plus the blocks
|
|
7
|
+
// the view reads through `spec.sourceDatabase`, `spec.notice`, `spec.museum`
|
|
8
|
+
// and `spec.related.*`. The six exhibitions each carried this layer as their
|
|
9
|
+
// own `composables/sheet.js`, the same code in all six.
|
|
10
|
+
//
|
|
11
|
+
// What stays an exhibition's own is read from its `dataset.config.js`
|
|
12
|
+
// through `useSiteConfig()`, lazily, as the galleries do:
|
|
13
|
+
//
|
|
14
|
+
// * `projectColors`, project id → one of viewer-layout's `mwnf-chip--*`
|
|
15
|
+
// classes, for the source-database chip and an outside reference's chip;
|
|
16
|
+
// a record with no project at all is an Explore record, whose chip is
|
|
17
|
+
// `mwnf-chip--Explore`;
|
|
18
|
+
// * `noticeProjects`, the projects whose sheets still carry legacy's
|
|
19
|
+
// Explore-partner notice.
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The exhibition's item-page spec, over its data layer's `data` and its
|
|
23
|
+
* timeline layer's `timeline` (`useExhibitionTimeline`).
|
|
24
|
+
*
|
|
25
|
+
* Reads `data.defaultLang`, `data.dynastyById`, `data.isExploreRecord`,
|
|
26
|
+
* `data.isHiddenPartner`, `data.labelOf`, `data.partnerById`,
|
|
27
|
+
* `data.partnerRoute`, `data.translations`, what `useExhibitionSheet` reads,
|
|
28
|
+
* and `timeline.countryIdForCode`, `timeline.hasTimeline`,
|
|
29
|
+
* `timeline.timelineEvents` (`countries`, `findEvents`), `timeline.timelineSpec`.
|
|
30
|
+
*/
|
|
31
|
+
export function useExhibitionItemDetail(data, timeline) {
|
|
32
|
+
const {
|
|
33
|
+
defaultLang, dynastyById, isExploreRecord, isHiddenPartner, labelOf, partnerById, partnerRoute, translations,
|
|
34
|
+
} = data
|
|
35
|
+
const { countryIdForCode, hasTimeline, timelineEvents, timelineSpec } = timeline
|
|
36
|
+
const { countries: timelineCountries, findEvents } = timelineEvents
|
|
37
|
+
const { itemSheet: platformSheet } = useExhibitionSheet(data)
|
|
38
|
+
|
|
39
|
+
function chipClass(record) {
|
|
40
|
+
return useSiteConfig().projectColors?.[record.project_id] ?? (isExploreRecord(record) ? 'mwnf-chip--Explore' : null)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function dynastyTr(dynasty, language) {
|
|
44
|
+
return translations('dynasties', language)[dynasty.id] ?? translations('dynasties', defaultLang)[dynasty.id] ?? {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Legacy popped out a dynasty only when it had a history to show.
|
|
48
|
+
function dynastiesWithHistory(record, language) {
|
|
49
|
+
return (record.dynasty_ids ?? [])
|
|
50
|
+
.map((id) => dynastyById.value.get(id))
|
|
51
|
+
.filter((d) => d && dynastyTr(d, language).history)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The country the "Timeline for this item" popout opens on: the picker's
|
|
55
|
+
// entry for the record's own country.
|
|
56
|
+
function countryOf(countryId) {
|
|
57
|
+
for (const { value } of timelineCountries.value) {
|
|
58
|
+
if (countryIdForCode(value) === countryId) return value
|
|
59
|
+
}
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The popout's "search this period" link: the timeline results filtered to
|
|
64
|
+
// the country and the period, under the keys the results page's controls
|
|
65
|
+
// read — never a key it has no control for (a local timeline has no
|
|
66
|
+
// country), which it would ignore (inventory-app#2022).
|
|
67
|
+
function periodSearch(country, range) {
|
|
68
|
+
const keys = new Set((timelineSpec.value?.controls ?? []).map((control) => control.key))
|
|
69
|
+
const query = { country, begin: range[0], end: range[1] }
|
|
70
|
+
return { name: 'timeline-results', query: Object.fromEntries(Object.entries(query).filter(([key]) => keys.has(key))) }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const itemDetail = {
|
|
74
|
+
...platformSheet,
|
|
75
|
+
|
|
76
|
+
sourceDatabase: {
|
|
77
|
+
chipClass: (record) => chipClass(record),
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
notice: {
|
|
81
|
+
show: (record) => (useSiteConfig().noticeProjects ?? []).includes(record.project_id),
|
|
82
|
+
label: 'exhibition.item.explorePartnerNote',
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// A hidden museum keeps its name on the sheet and loses the link: it has
|
|
86
|
+
// no page to link to.
|
|
87
|
+
museum: {
|
|
88
|
+
route: (partnerId) => {
|
|
89
|
+
const partner = partnerById.value.get(partnerId)
|
|
90
|
+
if (!partner || isHiddenPartner(partner)) return null
|
|
91
|
+
return partnerRoute(partner)
|
|
92
|
+
},
|
|
93
|
+
label: (partnerId) => labelOf('partners', partnerId),
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
related: {
|
|
97
|
+
...platformSheet.related,
|
|
98
|
+
title: 'exhibition.related.title',
|
|
99
|
+
description: 'exhibition.related.description',
|
|
100
|
+
outsideChip: (ref) => chipClass(ref),
|
|
101
|
+
notInPackageLabel: 'exhibition.results.notInThisExhibition',
|
|
102
|
+
artisticIntroductionLabel: 'exhibition.nav.artisticIntroduction',
|
|
103
|
+
databaseLabel: 'exhibition.search.relatedDatabase',
|
|
104
|
+
overallDatabase: { label: 'exhibition.search.overallDatabase', linkLabel: 'exhibition.nav.overallDatabase' },
|
|
105
|
+
onDisplayIn: { linkPendingLabel: 'exhibition.item.linkPending' },
|
|
106
|
+
dynasties: (record, language) => ({
|
|
107
|
+
records: dynastiesWithHistory(record, language),
|
|
108
|
+
tr: (d) => dynastyTr(d, language),
|
|
109
|
+
}),
|
|
110
|
+
// Withheld when the exhibition reports no chronology, or the item has
|
|
111
|
+
// no date to anchor a range on: legacy then printed no "timeline"
|
|
112
|
+
// anywhere on the sheet.
|
|
113
|
+
timeline: (record, ctx) => {
|
|
114
|
+
if (!hasTimeline.value) return null
|
|
115
|
+
const range = roundOutward(record.start_date, record.end_date)
|
|
116
|
+
if (range[0] == null) return null
|
|
117
|
+
return {
|
|
118
|
+
heading: 'exhibition.section.timeline',
|
|
119
|
+
countries: timelineCountries.value.map((c) => ({ value: c.value, label: c.label ?? ctx.t('timeline.form.allCountries') })),
|
|
120
|
+
defaultCountry: () => countryOf(record.country_id) ?? 'all',
|
|
121
|
+
events: (country) => findEvents({ country, begin: range[0], end: range[1] }),
|
|
122
|
+
range,
|
|
123
|
+
era: (year) => eraLabel(year, ctx.t),
|
|
124
|
+
searchTo: periodSearch,
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { itemDetail }
|
|
131
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import { useI18n } from '../../i18n/index.js'
|
|
3
|
+
import { entityRef } from '../../composables/useEntities.js'
|
|
4
|
+
import { useCollectionTree } from '../../record/collectionTree.js'
|
|
5
|
+
|
|
6
|
+
// An exhibition's themes: the tour its theme pages, its themes list and its
|
|
7
|
+
// theme galleries read (inventory-app#2053). The six exhibitions each carried
|
|
8
|
+
// this as `composables/themes.js`, `useThemePresentation.js` and
|
|
9
|
+
// `useThemePictures.js`, the same code in all six.
|
|
10
|
+
//
|
|
11
|
+
// themes.json is the ordered tree: top-level themes, each with its sub-themes
|
|
12
|
+
// and its curated picture selections. `useCollectionTree({ source: 'themes' })`
|
|
13
|
+
// walks it, and its `previous`/`next` (depth-first) is legacy's tour order:
|
|
14
|
+
// About → theme 1 overview → its sub-themes → theme 2 overview → ….
|
|
15
|
+
//
|
|
16
|
+
// Two rules the data fixes rather than taste:
|
|
17
|
+
//
|
|
18
|
+
// * Theme 0 ("About the Exhibition") is an ordinary top-level theme legacy
|
|
19
|
+
// renders at /about and skips on /themes. Its display order is 1, so the
|
|
20
|
+
// themes list starts at display order 2 and numbers those "Theme I"
|
|
21
|
+
// upwards — which is why `romanFor` subtracts one.
|
|
22
|
+
// * The theme id in the keyspace is not the display order. The route
|
|
23
|
+
// carries `display_order - 1`, as legacy's `theme.display - 1` did, so a
|
|
24
|
+
// legacy address pasted after the `#` lands on the same theme.
|
|
25
|
+
//
|
|
26
|
+
// A theme's selections point at `picture` items, which are not members of the
|
|
27
|
+
// exhibition and so not in items.json — only their parents are. Every label a
|
|
28
|
+
// theme page shows (name, holding museum, location, country) is read off the
|
|
29
|
+
// parent record, which is also what "see the full record" links to.
|
|
30
|
+
// `parent_in_package` says whether the parent is there at all: a curated
|
|
31
|
+
// picture whose parent was not exported still renders its own image.
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Legacy numbered its themes in Roman numerals, counting from the About
|
|
35
|
+
* theme. EssayView's `numbering: 'roman'` counts a node's position among its
|
|
36
|
+
* siblings, which it derives from `tree.root` — always null for a themes.json
|
|
37
|
+
* package — so it would number every top-level theme "I". The numeral an
|
|
38
|
+
* exhibition shows is always the owning theme's, so one function serves
|
|
39
|
+
* themes and sub-themes.
|
|
40
|
+
*/
|
|
41
|
+
export function romanFor(displayOrder) {
|
|
42
|
+
const lookup = [
|
|
43
|
+
['M', 1000], ['CM', 900], ['D', 500], ['CD', 400], ['C', 100], ['XC', 90],
|
|
44
|
+
['L', 50], ['XL', 40], ['X', 10], ['IX', 9], ['V', 5], ['IV', 4], ['I', 1],
|
|
45
|
+
]
|
|
46
|
+
let n = displayOrder - 1
|
|
47
|
+
let out = ''
|
|
48
|
+
for (const [sym, value] of lookup) {
|
|
49
|
+
while (n >= value) { out += sym; n -= value }
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The `pictures` PictureGallery/PictureNarrative read (viewer-layout's
|
|
56
|
+
* docs/theme-components.md) for one node, as a pure function of its data so
|
|
57
|
+
* the graph rules are tested without Vue or a package.
|
|
58
|
+
*
|
|
59
|
+
* A picture's `related`/`backRelated` targets resolve against `pictureById`,
|
|
60
|
+
* the WHOLE tree's picture index, not the node's own selections:
|
|
61
|
+
* `theme_item_related` rows can name a picture curated under another theme,
|
|
62
|
+
* and scoping the lookup to the node is what used to drop such a link.
|
|
63
|
+
*
|
|
64
|
+
* @param nodePictures - the node's own selections, in curator order
|
|
65
|
+
* @param deps.pictureById - Map<pictureId, rawPicture> for every picture in the tree
|
|
66
|
+
* @param deps.resolvePicture - (raw) => `{ id, image, imageAlt, name, detail, to }`,
|
|
67
|
+
* the node-independent shape, used for the node's pictures and every target
|
|
68
|
+
* @param deps.imageCaptionFor - (raw) => this node's own caption for the picture
|
|
69
|
+
* @param deps.fieldsFor - (raw) => this node's panel `fields` for the picture
|
|
70
|
+
* @param deps.relationText - (link) => the forward relation's text
|
|
71
|
+
* @param deps.reciprocalText - (link) => the backward relation's text
|
|
72
|
+
*/
|
|
73
|
+
export function buildThemePictures(nodePictures, deps) {
|
|
74
|
+
const {
|
|
75
|
+
pictureById: byId, resolvePicture, imageCaptionFor, fieldsFor, relationText, reciprocalText,
|
|
76
|
+
} = deps
|
|
77
|
+
|
|
78
|
+
return (nodePictures ?? []).map((raw) => {
|
|
79
|
+
const related = (raw.related ?? [])
|
|
80
|
+
.map((link) => {
|
|
81
|
+
const target = byId.get(link.picture_item_id)
|
|
82
|
+
return target ? { picture: resolvePicture(target), text: relationText(link) } : null
|
|
83
|
+
})
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
|
|
86
|
+
// Every other picture anywhere in the tree whose own `related` names this
|
|
87
|
+
// one, so a target on another theme's page still shows "related to" back.
|
|
88
|
+
const backRelated = []
|
|
89
|
+
for (const other of byId.values()) {
|
|
90
|
+
for (const link of other.related ?? []) {
|
|
91
|
+
if (link.picture_item_id === raw.picture_item_id) {
|
|
92
|
+
backRelated.push({ picture: resolvePicture(other), reciprocalText: reciprocalText(link) })
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
...resolvePicture(raw),
|
|
99
|
+
imageCaption: imageCaptionFor(raw),
|
|
100
|
+
fields: fieldsFor(raw),
|
|
101
|
+
// Not part of the components' contract: the theme page turns a
|
|
102
|
+
// selection back into legacy's `?image=<display_order>` with it.
|
|
103
|
+
displayOrder: raw.display_order,
|
|
104
|
+
related,
|
|
105
|
+
backRelated,
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The exhibition's themes, over its data layer's `data`.
|
|
112
|
+
*
|
|
113
|
+
* Reads `data.defaultLang`, `data.itemById`, `data.itemRoute`, `data.labelOf`,
|
|
114
|
+
* `data.mdInline`, `data.tr`.
|
|
115
|
+
*/
|
|
116
|
+
export function useExhibitionThemes(data) {
|
|
117
|
+
const { defaultLang, itemById, itemRoute, labelOf, mdInline, tr } = data
|
|
118
|
+
|
|
119
|
+
// EssayView reads `spec.tree.entity` to know where a node's own text lives,
|
|
120
|
+
// even for a tree built here rather than from its `{ themes: true }`
|
|
121
|
+
// shorthand; without it, every title/quote/body lookup would read
|
|
122
|
+
// 'collections', not a themes.json package's entity.
|
|
123
|
+
const themeTree = useCollectionTree({ source: 'themes', entity: 'themes' })
|
|
124
|
+
themeTree.entity = 'themes'
|
|
125
|
+
|
|
126
|
+
// The raw top-level array, in the order the themes list and the tour read
|
|
127
|
+
// them: the tree's own `root` is always null for this shape, so it cannot
|
|
128
|
+
// hand back every top-level theme itself.
|
|
129
|
+
const rawThemes = entityRef('themes')
|
|
130
|
+
const themes = computed(() => rawThemes.value ?? [])
|
|
131
|
+
|
|
132
|
+
const aboutTheme = computed(() => themes.value.find((t) => t.display_order === 1) ?? null)
|
|
133
|
+
|
|
134
|
+
/** The themes the themes page lists: everything after the About theme. */
|
|
135
|
+
const listedThemes = computed(() => themes.value.filter((t) => t.display_order > 1))
|
|
136
|
+
|
|
137
|
+
/** Route id → theme: legacy's `/theme/:id` carries `display_order - 1`. */
|
|
138
|
+
function themeByRouteId(id) {
|
|
139
|
+
const n = Number(id)
|
|
140
|
+
return themes.value.find((t) => t.display_order - 1 === n) ?? null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function themeRouteId(theme) {
|
|
144
|
+
return (theme?.display_order ?? 1) - 1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A node's picture selections, in curator order. */
|
|
148
|
+
function themePictures(theme) {
|
|
149
|
+
return [...(theme?.pictures ?? [])].sort((a, b) => (a.display_order ?? 0) - (b.display_order ?? 0))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Every picture selection in the tree, by its own id (not a catalogue id). */
|
|
153
|
+
const pictureById = computed(() => {
|
|
154
|
+
const map = new Map()
|
|
155
|
+
for (const node of themeTree.byId.value.values()) {
|
|
156
|
+
for (const picture of node.pictures ?? []) map.set(picture.picture_item_id, picture)
|
|
157
|
+
}
|
|
158
|
+
return map
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// translations/themes.<lang>.json is keyed two ways: by theme id for the
|
|
162
|
+
// theme's own title/quote/presentation, and by `<theme id>/<picture item
|
|
163
|
+
// id>` for one picture's curated text in that theme — the same picture in
|
|
164
|
+
// two themes carries two descriptions.
|
|
165
|
+
function themeText(theme, lang = defaultLang) {
|
|
166
|
+
return tr('themes', theme?.id, lang)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function pictureText(theme, picture, lang = defaultLang) {
|
|
170
|
+
if (!theme?.id || !picture?.picture_item_id) return {}
|
|
171
|
+
return tr('themes', `${theme.id}/${picture.picture_item_id}`, lang)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The top-level theme that owns a node: the node itself, or its parent. */
|
|
175
|
+
function owningTheme(node) {
|
|
176
|
+
if (!node) return null
|
|
177
|
+
const parents = themeTree.parents(node.id)
|
|
178
|
+
return parents.length ? parents[0] : node
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A node's 1-based position among its owning theme's sub-themes; null for the theme itself. */
|
|
182
|
+
function subIndexOf(node) {
|
|
183
|
+
if (!node) return null
|
|
184
|
+
const owner = owningTheme(node)
|
|
185
|
+
if (!owner || owner.id === node.id) return null
|
|
186
|
+
const index = themeTree.children(owner.id).findIndex((child) => child.id === node.id)
|
|
187
|
+
return index === -1 ? null : index + 1
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** A picture selection's parent record, or null when it is not a member. */
|
|
191
|
+
function pictureParent(picture) {
|
|
192
|
+
if (!picture?.parent_in_package) return null
|
|
193
|
+
return itemById.value.get(picture.parent_item_id) ?? null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Legacy's `itemDetailString`: museum, location, country, blanks dropped. */
|
|
197
|
+
function itemDetailString(item) {
|
|
198
|
+
if (!item) return ''
|
|
199
|
+
const sheet = tr('items', item.id, defaultLang)
|
|
200
|
+
return [
|
|
201
|
+
sheet.holder || labelOf('partners', item.partner_id),
|
|
202
|
+
sheet.location,
|
|
203
|
+
labelOf('countries', item.country_id),
|
|
204
|
+
].filter(Boolean).join(', ')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A node's `pictures`, live: its selections resolved against the parent
|
|
209
|
+
* records, the translations and the current locale. Call it in a
|
|
210
|
+
* component's setup; `node` is a ref.
|
|
211
|
+
*/
|
|
212
|
+
function useThemePictures(node) {
|
|
213
|
+
const { t, locale } = useI18n()
|
|
214
|
+
|
|
215
|
+
function resolvePicture(raw) {
|
|
216
|
+
const parent = pictureParent(raw)
|
|
217
|
+
const name = parent ? labelOf('items', parent.id) : ''
|
|
218
|
+
return {
|
|
219
|
+
id: raw.picture_item_id,
|
|
220
|
+
image: raw.image_url,
|
|
221
|
+
imageAlt: name,
|
|
222
|
+
name: mdInline(name),
|
|
223
|
+
detail: itemDetailString(parent),
|
|
224
|
+
to: parent ? itemRoute(parent) : null,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function imageCaptionFor(raw) {
|
|
229
|
+
if (!node.value?.id) return ''
|
|
230
|
+
return tr('themes', `${node.value.id}/${raw.picture_item_id}`, locale.value).image_caption ?? ''
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Legacy labelled only "also known as"; artist names and dates stand
|
|
234
|
+
// alone, as `fields` rows with an empty label.
|
|
235
|
+
function fieldsFor(raw) {
|
|
236
|
+
const parent = pictureParent(raw)
|
|
237
|
+
if (!parent) return []
|
|
238
|
+
const sheet = tr('items', parent.id, defaultLang)
|
|
239
|
+
const fields = []
|
|
240
|
+
if (sheet.alternate_name) {
|
|
241
|
+
fields.push({ label: `${t('sheet.field.alsoKnownAs')}:`, value: mdInline(sheet.alternate_name) })
|
|
242
|
+
}
|
|
243
|
+
if (parent.artist_names?.length) fields.push({ label: '', value: parent.artist_names.join(', ') })
|
|
244
|
+
if (sheet.dates) fields.push({ label: '', value: sheet.dates })
|
|
245
|
+
return fields
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const relationText = (link) => link?.descriptions?.[locale.value] ?? link?.descriptions?.en ?? ''
|
|
249
|
+
const reciprocalText = (link) => link?.reciprocal_descriptions?.[locale.value] ?? link?.reciprocal_descriptions?.en ?? ''
|
|
250
|
+
|
|
251
|
+
return computed(() => buildThemePictures(themePictures(node.value), {
|
|
252
|
+
pictureById: pictureById.value,
|
|
253
|
+
resolvePicture,
|
|
254
|
+
imageCaptionFor,
|
|
255
|
+
fieldsFor,
|
|
256
|
+
relationText,
|
|
257
|
+
reciprocalText,
|
|
258
|
+
}))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
themeTree,
|
|
263
|
+
themes,
|
|
264
|
+
aboutTheme,
|
|
265
|
+
listedThemes,
|
|
266
|
+
themeByRouteId,
|
|
267
|
+
themeRouteId,
|
|
268
|
+
romanFor,
|
|
269
|
+
themePictures,
|
|
270
|
+
pictureById,
|
|
271
|
+
themeText,
|
|
272
|
+
pictureText,
|
|
273
|
+
owningTheme,
|
|
274
|
+
subIndexOf,
|
|
275
|
+
pictureParent,
|
|
276
|
+
itemDetailString,
|
|
277
|
+
useThemePictures,
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { computed } from 'vue'
|
|
2
2
|
import { eraLabel, roundOutward, yearBucketsFromRange } from '../../catalogue/dates.js'
|
|
3
|
+
import { useTimelineEvents } from '../../catalogue/timeline.js'
|
|
3
4
|
import { PAGE_SIZE } from '../shared.js'
|
|
4
5
|
|
|
5
6
|
export { eraLabel, roundOutward }
|
|
@@ -9,13 +10,13 @@ export { eraLabel, roundOutward }
|
|
|
9
10
|
* `PAGE_SIZE`/`tile` its {@link import('./useExhibitionCollection.js').useExhibitionCollection}
|
|
10
11
|
* already declared.
|
|
11
12
|
*
|
|
12
|
-
* Reads `data.exhibition`, `data.timelines`, `data.
|
|
13
|
-
* `data.
|
|
14
|
-
* `
|
|
13
|
+
* Reads `data.exhibition`, `data.timelines`, `data.items`, `data.tr`,
|
|
14
|
+
* `data.defaultLang`, `data.countryIdForCode`, `data.countryLabel`.
|
|
15
|
+
* `config` carries nothing today.
|
|
15
16
|
*/
|
|
16
17
|
export function useExhibitionTimeline(data, collection, config = {}) {
|
|
17
18
|
void config
|
|
18
|
-
const { exhibition, timelines,
|
|
19
|
+
const { exhibition, timelines, items, tr, defaultLang, countryIdForCode, countryLabel } = data
|
|
19
20
|
const { tile } = collection
|
|
20
21
|
|
|
21
22
|
// ── Which chronology this site's Timeline section shows ─────────────────
|
|
@@ -33,13 +34,8 @@ export function useExhibitionTimeline(data, collection, config = {}) {
|
|
|
33
34
|
// full — every DXA site gets the worldwide merge whatever its flags say —
|
|
34
35
|
// possibly with no `thg_local` row at all. The flags gate NAVIGATION, not
|
|
35
36
|
// data.
|
|
36
|
-
//
|
|
37
|
-
// The `thg_local` row has no `country_id`, which is exactly why it must
|
|
38
|
-
// be separated by `source` rather than left to the country filter: on
|
|
39
|
-
// "All Countries" it would otherwise be interleaved into the worldwide
|
|
40
|
-
// list.
|
|
41
37
|
const localTimeline = computed(
|
|
42
|
-
() => timelines.value.find((t) => t.source === 'thg_local') ?? null,
|
|
38
|
+
() => (timelines.value ?? []).find((t) => t.source === 'thg_local') ?? null,
|
|
43
39
|
)
|
|
44
40
|
|
|
45
41
|
/** True when the Timeline section is the exhibition's own chronology. */
|
|
@@ -61,180 +57,39 @@ export function useExhibitionTimeline(data, collection, config = {}) {
|
|
|
61
57
|
() => Boolean(exhibition.value?.has_timeline || exhibition.value?.has_country_timeline),
|
|
62
58
|
)
|
|
63
59
|
|
|
64
|
-
/** The events this site's manual Timeline reads (ItemSheet's own popout). */
|
|
65
|
-
const eventPool = computed(() => {
|
|
66
|
-
const local = localTimeline.value
|
|
67
|
-
if (usesLocalTimeline.value) {
|
|
68
|
-
return timelineEvents.value.filter((e) => e.timeline_id === local.id)
|
|
69
|
-
}
|
|
70
|
-
if (!local) return timelineEvents.value
|
|
71
|
-
return timelineEvents.value.filter((e) => e.timeline_id !== local.id)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
// The global country timeline, served by legacy `/v2/events`. It is
|
|
75
|
-
// country-scoped and project-independent, and also a MERGE of two
|
|
76
|
-
// chronologies rather than one table: legacy's `App\MWNF\DAO\v2\Events`
|
|
77
|
-
// unions `mwnf3.hcr` (the Discover Islamic Art country chronologies) with
|
|
78
|
-
// `mwnf3_sharing_history.sh_hcr` restricted to exhibition 2, "Political
|
|
79
|
-
// Context", and sorts the result by year. The package mirrors that: each
|
|
80
|
-
// row of `timelines.json` is tagged `source: 'mwnf3' | 'sharing_history'`,
|
|
81
|
-
// and `timeline_events.json` keys every event by `country_id`.
|
|
82
|
-
//
|
|
83
|
-
// A country can be served by BOTH sources, so anything user-facing must
|
|
84
|
-
// key on the country and never on the timeline row: `findEvents` filters
|
|
85
|
-
// on `country_id`, which is what merges the two chronologies into one
|
|
86
|
-
// year-ordered list the way legacy did.
|
|
87
|
-
//
|
|
88
|
-
// Names and legacy codes both come from countries.json. The exporter
|
|
89
|
-
// scopes that file to "member item countries ∪ their holders' countries ∪
|
|
90
|
-
// the global timeline's countries", so it covers every timeline country —
|
|
91
|
-
// the `Intl.DisplayNames` fallback below is therefore dead code for them,
|
|
92
|
-
// and is kept only so a regressed package degrades to a rendered ISO code
|
|
93
|
-
// rather than a raw id. The `code` countries.json ships is the country's
|
|
94
|
-
// `backward_compatibility`, which is exactly the code legacy's own
|
|
95
|
-
// timeline URLs used, including the ones that are not ISO 3166-1
|
|
96
|
-
// alpha-2 (`uk`, `pa`, `qt`, `rm`, `sb`, `ua`, …). Those are the reason
|
|
97
|
-
// the fallback must stay unreachable rather than merely rare: read as
|
|
98
|
-
// ISO, `ua` is Ukraine and `sb` is the Solomon Islands, where legacy
|
|
99
|
-
// means the UAE and Serbia. Only countries.json can name them correctly.
|
|
100
|
-
const regionNames = (() => {
|
|
101
|
-
try {
|
|
102
|
-
return new Intl.DisplayNames(['en'], { type: 'region' })
|
|
103
|
-
} catch {
|
|
104
|
-
return null
|
|
105
|
-
}
|
|
106
|
-
})()
|
|
107
|
-
|
|
108
|
-
// Two legacy codes are not ISO 3166-1 alpha-2.
|
|
109
|
-
const LEGACY_TO_ISO = { uk: 'GB', pa: 'PS' }
|
|
110
|
-
|
|
111
|
-
// The set of inventory country ids present in countries.json. Ids and
|
|
112
|
-
// legacy codes are distinct — legacy codes are backward_compatibility
|
|
113
|
-
// values, while ids are the canonical keys in the inventory system.
|
|
114
|
-
const countryIdSet = computed(
|
|
115
|
-
() => new Set(countries.value.map((c) => c.id)),
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
// A lookup, not a parse. The fallback exists only for the
|
|
119
|
-
// regressed-package case described above, and it must agree with
|
|
120
|
-
// GLOBAL_TIMELINE_LIKE_PATTERNS in the exporter's timeline exporter: the
|
|
121
|
-
// country sits after the literal `country` segment in BOTH keyspaces
|
|
122
|
-
// (`mwnf3:hcr:country:<cc>` and
|
|
123
|
-
// `mwnf3_sharing_history:sh_hcr:country:<cc>:exhibition:2`), and it is
|
|
124
|
-
// the last segment in only the first — taking the last one yields `2` on
|
|
125
|
-
// the second.
|
|
126
|
-
function legacyCodeOf(timeline) {
|
|
127
|
-
const fromPackage = countryById.value.get(timeline.country_id)?.code
|
|
128
|
-
if (fromPackage) return fromPackage
|
|
129
|
-
const parts = (timeline.backward_compatibility ?? '').split(':')
|
|
130
|
-
const at = parts.indexOf('country')
|
|
131
|
-
return (at >= 0 ? parts[at + 1] : null) || timeline.country_id
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function nameFor(timeline) {
|
|
135
|
-
const fromPackage = countries.value.some((c) => c.id === timeline.country_id)
|
|
136
|
-
? labelOf('countries', timeline.country_id)
|
|
137
|
-
: null
|
|
138
|
-
if (fromPackage) return fromPackage
|
|
139
|
-
const legacy = legacyCodeOf(timeline)
|
|
140
|
-
const iso = LEGACY_TO_ISO[legacy] ?? String(legacy).toUpperCase()
|
|
141
|
-
try {
|
|
142
|
-
return regionNames?.of(iso) ?? iso
|
|
143
|
-
} catch {
|
|
144
|
-
return iso
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Countries that actually have a chronology, alphabetized, "All" first.
|
|
150
|
-
*
|
|
151
|
-
* One entry per COUNTRY, not per timeline row: a country served by BOTH
|
|
152
|
-
* `mwnf3` and `sharing_history` has two rows, and would otherwise appear
|
|
153
|
-
* twice in every country picker on the site. Only the item sheet's own
|
|
154
|
-
* manual "Timeline for this item" picker reads this now — `timelineSpec`
|
|
155
|
-
* below leaves the picker to viewer-core's `useTimelineEvents`.
|
|
156
|
-
*/
|
|
157
|
-
const timelineCountries = computed(() => {
|
|
158
|
-
// No picker at all when the section is the exhibition's own chronology
|
|
159
|
-
// — legacy hides the select on `countriesAvailable === false`.
|
|
160
|
-
if (usesLocalTimeline.value) return []
|
|
161
|
-
const byCountry = new Map()
|
|
162
|
-
for (const timeline of timelines.value) {
|
|
163
|
-
if (!timeline.country_id || byCountry.has(timeline.country_id)) continue
|
|
164
|
-
byCountry.set(timeline.country_id, [legacyCodeOf(timeline), nameFor(timeline)])
|
|
165
|
-
}
|
|
166
|
-
const rows = [...byCountry.values()].sort((a, b) => a[1].localeCompare(b[1]))
|
|
167
|
-
return [['all', 'All Countries'], ...rows]
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
/** Display name for an event's country — also `timelineSpec`'s `countryLabel`. */
|
|
171
|
-
function timelineCountryName(countryId) {
|
|
172
|
-
if (countries.value.some((c) => c.id === countryId)) return labelOf('countries', countryId)
|
|
173
|
-
const timeline = timelines.value.find((t) => t.country_id === countryId)
|
|
174
|
-
return timeline ? nameFor(timeline) : countryId
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Legacy 2-letter code → the inventory country id the events are keyed
|
|
179
|
-
* by. A country served by both chronologies has two rows carrying the
|
|
180
|
-
* same code and the same `country_id`, so either row answers. Idempotent
|
|
181
|
-
* on an id already: `TimelineResultsView`'s own country picker offers
|
|
182
|
-
* ids, so this must resolve BOTH the legacy code a bookmarked or
|
|
183
|
-
* cross-page link carries and the id the picker itself round-trips.
|
|
184
|
-
*/
|
|
185
|
-
function countryIdForCode(code) {
|
|
186
|
-
if (!code || code === 'all') return null
|
|
187
|
-
if (countryIdSet.value.has(code)) return code
|
|
188
|
-
if (countryById.value.has(code)) return code
|
|
189
|
-
const timeline = timelines.value.find((t) => legacyCodeOf(t) === code)
|
|
190
|
-
if (timeline) return timeline.country_id
|
|
191
|
-
// Countries with no chronology still reach here from the collection
|
|
192
|
-
// page's "Timeline for this Search" link, which uses countries.json's
|
|
193
|
-
// own codes.
|
|
194
|
-
return countries.value.find((c) => c.code === code)?.id ?? null
|
|
195
|
-
}
|
|
196
|
-
|
|
197
60
|
const eventsTr = (id) => tr('timeline_events', id, defaultLang)
|
|
198
61
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
})
|
|
220
|
-
.map((e) => ({
|
|
221
|
-
...e,
|
|
222
|
-
countryName: timelineCountryName(e.country_id),
|
|
223
|
-
text: eventsTr(e.id),
|
|
224
|
-
}))
|
|
225
|
-
.sort((a, b) => (a.year_from - b.year_from) || (a.display_order ?? 0) - (b.display_order ?? 0))
|
|
62
|
+
// ── The events ───────────────────────────────────────────────────────
|
|
63
|
+
//
|
|
64
|
+
// viewer-core's `useTimelineEvents` is the one engine over both
|
|
65
|
+
// chronologies: the worldwide country merge (`scope: 'country'`, which
|
|
66
|
+
// keeps the `thg_local` row out of it by `source`, since that row has no
|
|
67
|
+
// country to filter on) and the exhibition's own narrative one
|
|
68
|
+
// (`scope: 'local'`). The exhibition's flags pick between them once the
|
|
69
|
+
// exhibition record has loaded, so both are built and `timelineEvents`
|
|
70
|
+
// reads the one in force. The item page's "Timeline for this item" popout
|
|
71
|
+
// reads it; the Timeline pages build their own from `timelineSpec` below.
|
|
72
|
+
const engine = { countryLabel, countryIdForCode, tr: eventsTr }
|
|
73
|
+
const countryEvents = useTimelineEvents({ scope: 'country', ...engine })
|
|
74
|
+
const localEvents = useTimelineEvents({ scope: 'local', ...engine })
|
|
75
|
+
const inForce = () => (usesLocalTimeline.value ? localEvents : countryEvents)
|
|
76
|
+
|
|
77
|
+
const timelineEvents = {
|
|
78
|
+
countries: computed(() => inForce().countries.value),
|
|
79
|
+
yearRange: computed(() => inForce().yearRange.value),
|
|
80
|
+
yearBuckets: (t) => inForce().yearBuckets(t),
|
|
81
|
+
findEvents: (filters) => inForce().findEvents(filters),
|
|
226
82
|
}
|
|
227
83
|
|
|
228
84
|
// ── The Timeline entrance/results spec ───────────────────────────────
|
|
229
85
|
//
|
|
230
|
-
// Feeds `TimelineResultsView`: the engine
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
// counts as "in this period") is the site's, not the package's.
|
|
86
|
+
// Feeds `TimelineResultsView`: the engine owns the country/local axis,
|
|
87
|
+
// the year buckets and the pagination; an exhibition supplies only what
|
|
88
|
+
// is its own — the local/country scope switch and the controls it drives
|
|
89
|
+
// (no country picker when the section is the exhibition's own
|
|
90
|
+
// chronology), and the "See Gallery" join against member items, which
|
|
91
|
+
// stays here because an item's own shape (what counts as "in this
|
|
92
|
+
// period") is the site's, not the package's.
|
|
238
93
|
|
|
239
94
|
function yearOptions({ years, t }) {
|
|
240
95
|
return yearBucketsFromRange(years.min, years.max, t)
|
|
@@ -258,7 +113,7 @@ export function useExhibitionTimeline(data, collection, config = {}) {
|
|
|
258
113
|
|
|
259
114
|
const timelineSpec = computed(() => ({
|
|
260
115
|
scope: usesLocalTimeline.value ? 'local' : 'country',
|
|
261
|
-
countryLabel
|
|
116
|
+
countryLabel,
|
|
262
117
|
countryIdForCode,
|
|
263
118
|
tr: eventsTr,
|
|
264
119
|
controls: usesLocalTimeline.value
|
|
@@ -275,7 +130,7 @@ export function useExhibitionTimeline(data, collection, config = {}) {
|
|
|
275
130
|
// results spec's own `begin`/`end` (viewer-layout's `TimelineResultsView`
|
|
276
131
|
// renders those controls by that name, not `start`/`end`), so the legacy
|
|
277
132
|
// `/timeline-gallery/:country/:start/:end/:page` redirect is renamed to
|
|
278
|
-
// match on the way in
|
|
133
|
+
// match on the way in.
|
|
279
134
|
const timelineGallerySpec = {
|
|
280
135
|
entity: 'items',
|
|
281
136
|
keys: ['country', 'begin', 'end'],
|
|
@@ -296,7 +151,7 @@ export function useExhibitionTimeline(data, collection, config = {}) {
|
|
|
296
151
|
{
|
|
297
152
|
label: t('timeline.results.galleryHeading'),
|
|
298
153
|
value: filters.country
|
|
299
|
-
?
|
|
154
|
+
? countryLabel(countryIdForCode(filters.country))
|
|
300
155
|
: t('timeline.form.allCountries'),
|
|
301
156
|
},
|
|
302
157
|
{ count: pageInfo.total, value: t('catalogue.results.objects') },
|
|
@@ -304,7 +159,7 @@ export function useExhibitionTimeline(data, collection, config = {}) {
|
|
|
304
159
|
}
|
|
305
160
|
|
|
306
161
|
return {
|
|
307
|
-
usesLocalTimeline, hasTimeline,
|
|
308
|
-
|
|
162
|
+
usesLocalTimeline, hasTimeline, countryLabel, countryIdForCode,
|
|
163
|
+
timelineEvents, timelineSpec, timelineGallerySpec,
|
|
309
164
|
}
|
|
310
165
|
}
|
|
@@ -23,23 +23,20 @@ export { DATE_MODE, FACET_CATEGORIES, FACET_LABEL_KEYS, PAGE_SIZE, useFacetLabel
|
|
|
23
23
|
* The gallery's collection spec, over its own data layer's `data` (the
|
|
24
24
|
* return value of {@link import('./useGalleryData.js').useGalleryData}).
|
|
25
25
|
*
|
|
26
|
-
* Reads `data.
|
|
27
|
-
* `data.
|
|
28
|
-
* `data.
|
|
26
|
+
* Reads `data.tags`, `data.countryById`, `data.tagById`, `data.labelOf`,
|
|
27
|
+
* `data.itemRoute`, `data.tr`, `data.defaultLang`, `data.mdInline`,
|
|
28
|
+
* `data.manifest` and `data.countryIdForCode`. `config` carries nothing
|
|
29
|
+
* today.
|
|
29
30
|
*/
|
|
30
31
|
export function useGalleryCollection(data, config = {}) {
|
|
31
32
|
void config
|
|
32
|
-
const {
|
|
33
|
+
const { tags, countryById, tagById, labelOf, itemRoute, tr, defaultLang, mdInline, manifest, countryIdForCode } = data
|
|
33
34
|
|
|
34
35
|
// ── What the URL carries ─────────────────────────────────────────────
|
|
35
36
|
//
|
|
36
|
-
// The legacy 2-letter country code for `country
|
|
37
|
-
//
|
|
38
|
-
// resolves now.
|
|
39
|
-
|
|
40
|
-
function countryIdForCode(code) {
|
|
41
|
-
return code ? (countries.value ?? []).find((c) => c.code === code)?.id ?? null : null
|
|
42
|
-
}
|
|
37
|
+
// The legacy 2-letter country code for `country` (the data layer's
|
|
38
|
+
// `countryIdForCode`), the legacy tag id for a facet: the values legacy's
|
|
39
|
+
// URLs carried, so a link shared then still resolves now.
|
|
43
40
|
|
|
44
41
|
function tagIdForLegacy(legacyId) {
|
|
45
42
|
return legacyId ? (tags.value ?? []).find((t) => t.legacy_tag_id === legacyId)?.id ?? null : null
|
|
@@ -3,6 +3,7 @@ import { mediaUrl } from '../../siteConfig.js'
|
|
|
3
3
|
import { useCatalogueData } from '../../composables/useCatalogueData.js'
|
|
4
4
|
import { useDataPackage } from '../../composables/useDataPackage.js'
|
|
5
5
|
import { itemFromUidPath as legacyItemFromUidPath, partnerFromKey as legacyPartnerFromKey } from '../../legacy/index.js'
|
|
6
|
+
import { useLegacyCountryCodes } from '../countryCodes.js'
|
|
6
7
|
|
|
7
8
|
// The data layer every DXA gallery site (carpets, amulets, …) wrote for
|
|
8
9
|
// itself: the entities a gallery reads, the lookup maps over them, its
|
|
@@ -62,6 +63,10 @@ export function useGalleryData(config = {}) {
|
|
|
62
63
|
const timelineById = catalogue.index('timelines')
|
|
63
64
|
const languageByCode = catalogue.index('languages', 'code')
|
|
64
65
|
|
|
66
|
+
// The legacy country code both ways, the one table the collection and the
|
|
67
|
+
// timeline read.
|
|
68
|
+
const { countryIdForCode, countryLabel } = useLegacyCountryCodes({ countries, countryById, timelines, labelOf })
|
|
69
|
+
|
|
65
70
|
// ── Routes ─────────────────────────────────────────────────────────────
|
|
66
71
|
//
|
|
67
72
|
// The canonical routes carry the package id; the language never travels
|
|
@@ -137,6 +142,7 @@ export function useGalleryData(config = {}) {
|
|
|
137
142
|
availableLanguages, loadTranslations, translations,
|
|
138
143
|
gallery, items, tags, partners, countries, languages, dynasties, glossary, timelines, timelineEvents,
|
|
139
144
|
itemById, partnerById, countryById, tagById, dynastyById, timelineById, languageByCode,
|
|
145
|
+
countryIdForCode, countryLabel,
|
|
140
146
|
itemRoute, partnerRoute, partnerObjectsRoute, itemFromUidPath, partnerFromKey,
|
|
141
147
|
chromeImage, siblingGalleries, siblingUrl, pickSiblings,
|
|
142
148
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { eraLabel, roundOutward } from '../../catalogue/dates.js'
|
|
2
|
+
import { useSiteConfig } from '../../siteConfig.js'
|
|
3
|
+
import { useGallerySheet } from './useGallerySheet.js'
|
|
4
|
+
|
|
5
|
+
// A gallery's item page, as viewer-layout's `RecordSheetView` spec
|
|
6
|
+
// (inventory-app#2054): `useGallerySheet`'s field sheet, plus the blocks the
|
|
7
|
+
// view reads through `spec.sourceDatabase`, `spec.notice`, `spec.museum` and
|
|
8
|
+
// `spec.related.*` — the project chips, the Explore-partner notice, the
|
|
9
|
+
// holding-museum link, and the related block's outside references,
|
|
10
|
+
// cross-database links, dynasty popouts and timeline popout. The 37
|
|
11
|
+
// galleries each carried this layer in their own `composables/gallery.js`,
|
|
12
|
+
// the same code in all 37.
|
|
13
|
+
//
|
|
14
|
+
// What stays a gallery's own is read from its `dataset.config.js` through
|
|
15
|
+
// `useSiteConfig()`, lazily, inside the functions the view calls at render
|
|
16
|
+
// time — `dataset.config.js` is imported before `createViewer()` sets the
|
|
17
|
+
// config, so a read at module level would only ever see the empty default:
|
|
18
|
+
//
|
|
19
|
+
// * `projectColors`, project id → one of viewer-layout's `mwnf-chip--*`
|
|
20
|
+
// classes, for the source-database chip and an outside reference's chip;
|
|
21
|
+
// * `noticeProjects`, the projects whose sheets still carry legacy's
|
|
22
|
+
// Explore-partner notice.
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The gallery's item-page spec, over its data layer's `data` and its
|
|
26
|
+
* timeline layer's `timeline` (`useGalleryTimeline`).
|
|
27
|
+
*
|
|
28
|
+
* Reads `data.defaultLang`, `data.dynastyById`, `data.labelOf`,
|
|
29
|
+
* `data.partnerById`, `data.partnerRoute`, `data.translations`, what
|
|
30
|
+
* `useGallerySheet` reads, and `timeline.countryIdForCode`,
|
|
31
|
+
* `timeline.timelineEvents` (`countries`, `findEvents`).
|
|
32
|
+
*/
|
|
33
|
+
export function useGalleryItemDetail(data, timeline) {
|
|
34
|
+
const { defaultLang, dynastyById, labelOf, partnerById, partnerRoute, translations } = data
|
|
35
|
+
const { countryIdForCode, timelineEvents } = timeline
|
|
36
|
+
const { countries: timelineCountries, findEvents } = timelineEvents
|
|
37
|
+
const { itemSheet: platformSheet } = useGallerySheet(data)
|
|
38
|
+
|
|
39
|
+
const chipClass = (record) => useSiteConfig().projectColors?.[record.project_id] ?? null
|
|
40
|
+
|
|
41
|
+
function museumRoute(partnerId) {
|
|
42
|
+
const partner = partnerById.value.get(partnerId)
|
|
43
|
+
return partner ? partnerRoute(partner) : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function dynastyTranslation(dynasty, language) {
|
|
47
|
+
return translations('dynasties', language)[dynasty.id] ?? translations('dynasties', defaultLang)[dynasty.id] ?? {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Legacy popped out a dynasty only when it had a history to show.
|
|
51
|
+
function itemDynasties(record, language) {
|
|
52
|
+
const records = (record.dynasty_ids ?? [])
|
|
53
|
+
.map((id) => dynastyById.value.get(id))
|
|
54
|
+
.filter((d) => d && dynastyTranslation(d, language).history)
|
|
55
|
+
return records.length ? { records, tr: (d) => dynastyTranslation(d, language) } : null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The timeline popout, for the record's own date range: the gallery's own
|
|
59
|
+
// countries, events and search route.
|
|
60
|
+
function itemTimeline(record, ctx) {
|
|
61
|
+
const [from, to] = roundOutward(record.start_date, record.end_date)
|
|
62
|
+
if (from == null) return null
|
|
63
|
+
return {
|
|
64
|
+
heading: 'gallery.section.timeline',
|
|
65
|
+
countries: timelineCountries.value.map((c) => ({ value: c.value, label: c.label ?? ctx.t('timeline.form.allCountries') })),
|
|
66
|
+
defaultCountry: () => {
|
|
67
|
+
for (const { value: code } of timelineCountries.value) {
|
|
68
|
+
if (countryIdForCode(code) === record.country_id) return code
|
|
69
|
+
}
|
|
70
|
+
return 'all'
|
|
71
|
+
},
|
|
72
|
+
events: (country) => findEvents({ country, begin: from, end: to }),
|
|
73
|
+
range: [from, to],
|
|
74
|
+
era: (year) => eraLabel(year, ctx.t),
|
|
75
|
+
searchTo: (country, [begin, end]) => ({ name: 'timeline-results', query: { country, begin, end } }),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const itemDetail = {
|
|
80
|
+
...platformSheet,
|
|
81
|
+
sourceDatabase: {
|
|
82
|
+
chipClass,
|
|
83
|
+
},
|
|
84
|
+
notice: {
|
|
85
|
+
show: (record) => (useSiteConfig().noticeProjects ?? []).includes(record.project_id),
|
|
86
|
+
label: 'gallery.item.explorePartnerNote',
|
|
87
|
+
},
|
|
88
|
+
museum: {
|
|
89
|
+
route: (partnerId) => museumRoute(partnerId),
|
|
90
|
+
label: (partnerId) => labelOf('partners', partnerId),
|
|
91
|
+
},
|
|
92
|
+
related: {
|
|
93
|
+
...platformSheet.related,
|
|
94
|
+
title: 'gallery.related.title',
|
|
95
|
+
description: 'gallery.related.description',
|
|
96
|
+
notInPackageLabel: 'gallery.results.notInThisGallery',
|
|
97
|
+
// An outside `related_items` reference carries its `project_id`
|
|
98
|
+
// (inventory-app#1807), so it reads the same `projectColors` map.
|
|
99
|
+
outsideChip: chipClass,
|
|
100
|
+
artisticIntroductionLabel: 'gallery.nav.artisticIntroduction',
|
|
101
|
+
databaseLabel: 'gallery.search.relatedDatabase',
|
|
102
|
+
overallDatabase: { label: 'gallery.search.overallDatabase', linkLabel: 'gallery.nav.overallDatabase' },
|
|
103
|
+
onDisplayIn: { linkPendingLabel: 'gallery.item.linkPending' },
|
|
104
|
+
dynasties: (record, language) => itemDynasties(record, language),
|
|
105
|
+
timeline: itemTimeline,
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { itemDetail }
|
|
110
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { computed } from 'vue'
|
|
2
1
|
import { dateRange, eraLabel, roundOutward, yearBucketsFromRange } from '../../catalogue/dates.js'
|
|
3
2
|
import { useTimelineEvents } from '../../catalogue/timeline.js'
|
|
4
3
|
import { PAGE_SIZE } from '../shared.js'
|
|
@@ -10,13 +9,13 @@ export { eraLabel, roundOutward }
|
|
|
10
9
|
* `PAGE_SIZE`/`tile` its {@link import('./useGalleryCollection.js').useGalleryCollection}
|
|
11
10
|
* already declared.
|
|
12
11
|
*
|
|
13
|
-
* Reads `data.
|
|
14
|
-
* `data.
|
|
15
|
-
* `
|
|
12
|
+
* Reads `data.labelOf`, `data.items`, `data.tr`, `data.loadTranslations`,
|
|
13
|
+
* `data.defaultLang`, `data.countryIdForCode`, `data.countryLabel`.
|
|
14
|
+
* `config` carries nothing today.
|
|
16
15
|
*/
|
|
17
16
|
export function useGalleryTimeline(data, collection, config = {}) {
|
|
18
17
|
void config
|
|
19
|
-
const {
|
|
18
|
+
const { labelOf, items, tr, loadTranslations, defaultLang, countryIdForCode, countryLabel } = data
|
|
20
19
|
const { tile } = collection
|
|
21
20
|
|
|
22
21
|
// `TimelineResultsView` takes `tr` as a plain callback rather than an
|
|
@@ -28,96 +27,11 @@ export function useGalleryTimeline(data, collection, config = {}) {
|
|
|
28
27
|
|
|
29
28
|
// The merge itself — the worldwide country chronology, `mwnf3.hcr`
|
|
30
29
|
// combined with Sharing History's exhibition-2 rows the way legacy's
|
|
31
|
-
// `/v2/events` served it — is viewer-core's `useTimelineEvents
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// Names and legacy codes both come from countries.json. The exporter
|
|
37
|
-
// scopes that file to "member item countries ∪ their holders' countries ∪
|
|
38
|
-
// the global timeline's countries", so it covers every timeline country —
|
|
39
|
-
// the `Intl.DisplayNames` fallback below is therefore dead code for them,
|
|
40
|
-
// and is kept only so a regressed package degrades to a rendered ISO code
|
|
41
|
-
// rather than a raw id. The `code` countries.json ships is the country's
|
|
42
|
-
// `backward_compatibility`, which is exactly the code legacy's own
|
|
43
|
-
// timeline URLs used, including the ones that are not ISO 3166-1
|
|
44
|
-
// alpha-2 (`uk`, `pa`, `qt`, `rm`, `sb`, `ua`, …). Those are the reason
|
|
45
|
-
// the fallback must stay unreachable rather than merely rare: read as
|
|
46
|
-
// ISO, `ua` is Ukraine and `sb` is the Solomon Islands, where legacy
|
|
47
|
-
// means the UAE and Serbia. Only countries.json can name them correctly.
|
|
48
|
-
const regionNames = (() => {
|
|
49
|
-
try {
|
|
50
|
-
return new Intl.DisplayNames(['en'], { type: 'region' })
|
|
51
|
-
} catch {
|
|
52
|
-
return null
|
|
53
|
-
}
|
|
54
|
-
})()
|
|
55
|
-
|
|
56
|
-
// Two legacy codes are not ISO 3166-1 alpha-2.
|
|
57
|
-
const LEGACY_TO_ISO = { uk: 'GB', pa: 'PS' }
|
|
58
|
-
|
|
59
|
-
// `TimelineResultsView`'s own country control (`countries` from
|
|
60
|
-
// viewer-core) writes the inventory id, not the legacy code
|
|
61
|
-
// (`?country=dza`, not `?country=dz`); `countryIdForCode` used to answer
|
|
62
|
-
// nothing for one, silently matching every event. Built once, next to
|
|
63
|
-
// `LEGACY_TO_ISO`, rather than inside the function it is read from.
|
|
64
|
-
const countryIdSet = computed(() => new Set(countries.value.map((c) => c.id)))
|
|
65
|
-
|
|
66
|
-
// A lookup, not a parse. The fallback exists only for the
|
|
67
|
-
// regressed-package case described above, and it must agree with
|
|
68
|
-
// GLOBAL_TIMELINE_LIKE_PATTERNS in the exporter's timeline exporter: the
|
|
69
|
-
// country sits after the literal `country` segment in BOTH keyspaces
|
|
70
|
-
// (`mwnf3:hcr:country:<cc>` and
|
|
71
|
-
// `mwnf3_sharing_history:sh_hcr:country:<cc>:exhibition:2`), and it is
|
|
72
|
-
// the last segment in only the first — taking the last one yields `2` on
|
|
73
|
-
// the second.
|
|
74
|
-
function legacyCodeOf(timeline) {
|
|
75
|
-
const fromPackage = countryById.value.get(timeline.country_id)?.code
|
|
76
|
-
if (fromPackage) return fromPackage
|
|
77
|
-
const parts = (timeline.backward_compatibility ?? '').split(':')
|
|
78
|
-
const at = parts.indexOf('country')
|
|
79
|
-
return (at >= 0 ? parts[at + 1] : null) || timeline.country_id
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function nameFor(timeline) {
|
|
83
|
-
const fromPackage = countries.value.some((c) => c.id === timeline.country_id)
|
|
84
|
-
? labelOf('countries', timeline.country_id)
|
|
85
|
-
: null
|
|
86
|
-
if (fromPackage) return fromPackage
|
|
87
|
-
const legacy = legacyCodeOf(timeline)
|
|
88
|
-
const iso = LEGACY_TO_ISO[legacy] ?? String(legacy).toUpperCase()
|
|
89
|
-
try {
|
|
90
|
-
return regionNames?.of(iso) ?? iso
|
|
91
|
-
} catch {
|
|
92
|
-
return iso
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Display name for a country the chronology carries, keyed by inventory id. */
|
|
97
|
-
function countryLabel(countryId) {
|
|
98
|
-
if (countries.value.some((c) => c.id === countryId)) return labelOf('countries', countryId)
|
|
99
|
-
const timeline = timelines.value.find((t) => t.country_id === countryId)
|
|
100
|
-
return timeline ? nameFor(timeline) : countryId
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Legacy 2-letter code → the inventory country id the events are keyed
|
|
105
|
-
* by. A country served by both chronologies has two rows carrying the
|
|
106
|
-
* same code and the same `country_id`, so either row answers.
|
|
107
|
-
*/
|
|
108
|
-
function countryIdForCode(code) {
|
|
109
|
-
if (!code || code === 'all') return null
|
|
110
|
-
// Already an id (the composed view's own control writes one): pass it
|
|
111
|
-
// through unchanged, before the legacy-code lookups below get a chance
|
|
112
|
-
// to find nothing and silently match every event.
|
|
113
|
-
if (countryIdSet.value.has(code)) return code
|
|
114
|
-
const timeline = timelines.value.find((t) => legacyCodeOf(t) === code)
|
|
115
|
-
if (timeline) return timeline.country_id
|
|
116
|
-
// Countries with no chronology still reach here from the collection
|
|
117
|
-
// page's "Timeline for this Search" link, which uses countries.json's
|
|
118
|
-
// own codes.
|
|
119
|
-
return countries.value.find((c) => c.code === code)?.id ?? null
|
|
120
|
-
}
|
|
30
|
+
// `/v2/events` served it — is viewer-core's `useTimelineEvents`, and the
|
|
31
|
+
// DXA legacy 2-letter country code a gallery's Timeline URLs are keyed on
|
|
32
|
+
// is the data layer's (`countryIdForCode`, `countryLabel`). What stays
|
|
33
|
+
// here is what is genuinely a gallery's own: the two specs the composed
|
|
34
|
+
// views render from.
|
|
121
35
|
|
|
122
36
|
/**
|
|
123
37
|
* The one merge engine every Timeline page on a gallery shares —
|
package/src/dxa/index.js
CHANGED
|
@@ -26,9 +26,12 @@ export { useGalleryCollection } from './gallery/useGalleryCollection.js'
|
|
|
26
26
|
export { useGalleryTimeline } from './gallery/useGalleryTimeline.js'
|
|
27
27
|
export { useGalleryPartner } from './gallery/useGalleryPartner.js'
|
|
28
28
|
export { useGallerySheet } from './gallery/useGallerySheet.js'
|
|
29
|
+
export { useGalleryItemDetail } from './gallery/useGalleryItemDetail.js'
|
|
29
30
|
|
|
30
31
|
export { useExhibitionData } from './exhibition/useExhibitionData.js'
|
|
31
32
|
export { useExhibitionCollection } from './exhibition/useExhibitionCollection.js'
|
|
32
33
|
export { useExhibitionTimeline } from './exhibition/useExhibitionTimeline.js'
|
|
33
34
|
export { useExhibitionPartner } from './exhibition/useExhibitionPartner.js'
|
|
34
35
|
export { useExhibitionSheet } from './exhibition/useExhibitionSheet.js'
|
|
36
|
+
export { useExhibitionItemDetail } from './exhibition/useExhibitionItemDetail.js'
|
|
37
|
+
export { buildThemePictures, romanFor, useExhibitionThemes } from './exhibition/useExhibitionThemes.js'
|