@museumwnf/viewer-core 2.3.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 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 this package, so a site 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. |
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,9 +560,9 @@ 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`, `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`) and their helpers (`countryLabel`/`countryIdForCode`, `timelineGalleryItems`; the exhibition shape adds `usesLocalTimeline`, `hasTimeline`, `timelineCountries`, `findEvents` for its own local-vs-country chronology switch) |
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`) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@museumwnf/viewer-core",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
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",
@@ -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.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.
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
- countries, tags, countryById, tagById, itemById, labelOf, itemRoute, tr, defaultLang, mdInline, projectName,
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`, 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
- }
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,
@@ -25,14 +25,15 @@ import { useExhibitionSheet } from './useExhibitionSheet.js'
25
25
  * Reads `data.defaultLang`, `data.dynastyById`, `data.isExploreRecord`,
26
26
  * `data.isHiddenPartner`, `data.labelOf`, `data.partnerById`,
27
27
  * `data.partnerRoute`, `data.translations`, what `useExhibitionSheet` reads,
28
- * and `timeline.countryIdForCode`, `timeline.findEvents`,
29
- * `timeline.hasTimeline`, `timeline.timelineCountries`, `timeline.timelineSpec`.
28
+ * and `timeline.countryIdForCode`, `timeline.hasTimeline`,
29
+ * `timeline.timelineEvents` (`countries`, `findEvents`), `timeline.timelineSpec`.
30
30
  */
31
31
  export function useExhibitionItemDetail(data, timeline) {
32
32
  const {
33
33
  defaultLang, dynastyById, isExploreRecord, isHiddenPartner, labelOf, partnerById, partnerRoute, translations,
34
34
  } = data
35
- const { countryIdForCode, findEvents, hasTimeline, timelineCountries, timelineSpec } = timeline
35
+ const { countryIdForCode, hasTimeline, timelineEvents, timelineSpec } = timeline
36
+ const { countries: timelineCountries, findEvents } = timelineEvents
36
37
  const { itemSheet: platformSheet } = useExhibitionSheet(data)
37
38
 
38
39
  function chipClass(record) {
@@ -50,11 +51,11 @@ export function useExhibitionItemDetail(data, timeline) {
50
51
  .filter((d) => d && dynastyTr(d, language).history)
51
52
  }
52
53
 
53
- // The country the "Timeline for this item" popout opens on: the legacy
54
- // two-letter code whose id is the record's own country.
55
- function countryCodeOf(countryId) {
56
- for (const [code] of timelineCountries.value) {
57
- if (countryIdForCode(code) === countryId) return code
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
58
59
  }
59
60
  return null
60
61
  }
@@ -63,9 +64,9 @@ export function useExhibitionItemDetail(data, timeline) {
63
64
  // the country and the period, under the keys the results page's controls
64
65
  // read — never a key it has no control for (a local timeline has no
65
66
  // country), which it would ignore (inventory-app#2022).
66
- function periodSearch(countryCode, range) {
67
+ function periodSearch(country, range) {
67
68
  const keys = new Set((timelineSpec.value?.controls ?? []).map((control) => control.key))
68
- const query = { country: countryCode, begin: range[0], end: range[1] }
69
+ const query = { country, begin: range[0], end: range[1] }
69
70
  return { name: 'timeline-results', query: Object.fromEntries(Object.entries(query).filter(([key]) => keys.has(key))) }
70
71
  }
71
72
 
@@ -115,9 +116,9 @@ export function useExhibitionItemDetail(data, timeline) {
115
116
  if (range[0] == null) return null
116
117
  return {
117
118
  heading: 'exhibition.section.timeline',
118
- countries: timelineCountries.value.map(([value, label]) => ({ value, label })),
119
- defaultCountry: () => countryCodeOf(record.country_id) ?? 'all',
120
- events: (countryCode) => findEvents({ countryCode, start: range[0], end: range[1] }),
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] }),
121
122
  range,
122
123
  era: (year) => eraLabel(year, ctx.t),
123
124
  searchTo: periodSearch,
@@ -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.timelineEvents`,
13
- * `data.countries`, `data.countryById`, `data.items`, `data.labelOf`,
14
- * `data.tr`, `data.defaultLang`. `config` carries nothing today.
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, timelineEvents, countries, countryById, items, labelOf, tr, defaultLang } = data
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
- * Legacy's `/events?ic[]=&ya=&yo=` — events for a country within a year
201
- * range, ordered chronologically. Kept for the item sheet's own "Timeline
202
- * for this item" popout, which reads a single item's country and dates
203
- * rather than a URL's filters and has no results page of its own to hand
204
- * to `TimelineResultsView`.
205
- */
206
- function findEvents({ countryCode, start, end }) {
207
- const countryId = countryIdForCode(countryCode)
208
- const from = start === '' || start == null ? null : Number(start)
209
- const to = end === '' || end == null ? null : Number(end)
210
-
211
- return eventPool.value
212
- .filter((e) => {
213
- if (countryId && e.country_id !== countryId) return false
214
- const year = e.year_from
215
- if (!Number.isFinite(year)) return false
216
- if (from != null && year < from) return false
217
- if (to != null && year > to) return false
218
- return true
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 (viewer-core's
231
- // `useTimelineEvents`) owns the country/local axis, the year buckets and
232
- // the pagination; an exhibition supplies only what is its own — the
233
- // legacy country-code table, the local/country scope switch and the
234
- // controls it drives (no country picker when the section is the
235
- // exhibition's own chronology), and the "See Gallery" join against
236
- // member items, which stays here because an item's own shape (what
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: timelineCountryName,
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 (the site's own `dataset.config.js`).
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
- ? timelineCountryName(countryIdForCode(filters.country))
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, timelineCountries, timelineCountryName, countryIdForCode,
308
- findEvents, timelineSpec, timelineGallerySpec,
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.countries`, `data.tags`, `data.countryById`, `data.tagById`,
27
- * `data.labelOf`, `data.itemRoute`, `data.tr`, `data.defaultLang`,
28
- * `data.mdInline` and `data.manifest`. `config` carries nothing today.
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 { countries, tags, countryById, tagById, labelOf, itemRoute, tr, defaultLang, mdInline, manifest } = data
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`, the legacy tag id for a
37
- // facet: the values legacy's URLs carried, so a link shared then still
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
  }
@@ -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.timelines`, `data.countries`, `data.countryById`,
14
- * `data.labelOf`, `data.items`, `data.tr`, `data.loadTranslations`,
15
- * `data.defaultLang`. `config` carries nothing today.
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 { timelines, countries, countryById, labelOf, items, tr, loadTranslations, defaultLang } = data
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`. What
32
- // stays here is what is genuinely a gallery's own: the DXA legacy
33
- // 2-letter country code (a gallery's Timeline URLs are keyed on it, not
34
- // on the inventory id) and the two specs the composed views render from.
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 —