@museumwnf/viewer-core 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +44 -44
- package/README.md +658 -635
- package/package.json +50 -50
- package/src/AppRoot.vue +58 -58
- package/src/catalogue/dates.js +209 -209
- package/src/catalogue/facets.js +61 -61
- package/src/catalogue/keywordIndex.js +289 -289
- package/src/catalogue/listQuery.js +100 -100
- package/src/catalogue/pagination.js +57 -57
- package/src/catalogue/searchLanguage.js +32 -32
- package/src/catalogue/timeline.js +220 -220
- package/src/composables/useCatalogueData.js +141 -141
- package/src/composables/useDataPackage.js +98 -98
- package/src/composables/useEntities.js +100 -100
- package/src/composables/useRecordLanguage.js +71 -71
- package/src/conventions.js +252 -148
- package/src/createViewer.js +29 -29
- package/src/i18n/I18nText.vue +17 -17
- package/src/i18n/I18nTextInline.vue +17 -17
- package/src/i18n/index.js +88 -88
- package/src/i18n/language.js +127 -127
- package/src/i18n/markdown.js +265 -265
- package/src/index.js +51 -50
- package/src/injectionKeys.js +1 -1
- package/src/languages.js +49 -49
- package/src/legacy/index.js +58 -58
- package/src/record/collectionTree.js +307 -307
- package/src/record/derivations.js +154 -154
- package/src/record/partners.js +87 -87
- package/src/record/recordSheet.js +231 -231
- package/src/rights.js +53 -53
- package/src/router/index.js +138 -138
- package/src/siteConfig.js +37 -37
- package/src/styles/base.css +49 -49
- package/src/testing/checkOfferedLanguages.js +67 -67
- package/src/testing/index.js +3 -3
- package/src/testing/smoke.js +103 -103
- package/src/testing/viteConfig.js +90 -90
- package/src/views/DetailView.vue +53 -53
- package/src/views/HomeView.vue +20 -20
- package/src/views/ListView.vue +61 -61
- package/src/views/NotFoundView.vue +8 -8
- package/src/vite.js +8 -8
package/src/testing/smoke.js
CHANGED
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
import { createViewer } from '../createViewer.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Mount a website to a detached DOM node on a given hash, returning the app,
|
|
5
|
-
* host, and router for test assertions. The router is ready and the app is
|
|
6
|
-
* mounted before the promise resolves.
|
|
7
|
-
*
|
|
8
|
-
* `config` and `messages` are the same objects a website's main.js assembles.
|
|
9
|
-
* `hash` defaults to the home page.
|
|
10
|
-
*/
|
|
11
|
-
export async function mountSite(config, messages, hash = '#/') {
|
|
12
|
-
window.location.hash = hash
|
|
13
|
-
const app = createViewer({ ...config, messages })
|
|
14
|
-
const host = document.createElement('div')
|
|
15
|
-
document.body.appendChild(host)
|
|
16
|
-
app.mount(host)
|
|
17
|
-
const router = app.config.globalProperties.$router
|
|
18
|
-
await router.isReady()
|
|
19
|
-
return { app, host, router }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Check that a configuration's routes are properly declared: every route has
|
|
24
|
-
* a name, no route is a catch-all, and every legacy route is listed in
|
|
25
|
-
* `legacyPaths`.
|
|
26
|
-
*
|
|
27
|
-
* Returns the list of problems found (empty if all is well). Routes without
|
|
28
|
-
* names appear by index in the message.
|
|
29
|
-
*/
|
|
30
|
-
export function checkRoutes(config, { names = [], legacyPaths = [] } = {}) {
|
|
31
|
-
const problems = []
|
|
32
|
-
const declaredNames = (config.extraViews ?? []).map((r) => r.name)
|
|
33
|
-
const declaredLegacy = (config.legacyRoutes ?? []).map((r) => r.path)
|
|
34
|
-
|
|
35
|
-
// Check every route has a name.
|
|
36
|
-
for (let i = 0; i < (config.extraViews ?? []).length; i++) {
|
|
37
|
-
if (!config.extraViews[i].name) {
|
|
38
|
-
problems.push(`Route ${i} has no name.`)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Check no route is a catch-all.
|
|
43
|
-
if ((config.extraViews ?? []).some((r) => r.path?.includes('pathMatch'))) {
|
|
44
|
-
problems.push('A route is a catch-all (contains pathMatch); the catch-all belongs to the router.')
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Check expected names are present.
|
|
48
|
-
for (const name of names) {
|
|
49
|
-
if (!declaredNames.includes(name)) {
|
|
50
|
-
problems.push(`Expected route "${name}" not found.`)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Check expected legacy paths are present.
|
|
55
|
-
for (const path of legacyPaths) {
|
|
56
|
-
if (!declaredLegacy.includes(path)) {
|
|
57
|
-
problems.push(`Expected legacy path "${path}" not found.`)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return problems
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Check that every route has a `meta.section` string. A route without one
|
|
66
|
-
* leaves the menu unaware of what section the visitor is in.
|
|
67
|
-
*
|
|
68
|
-
* Returns the list of problems found (empty if all is well).
|
|
69
|
-
*/
|
|
70
|
-
export function checkSectionMeta(config) {
|
|
71
|
-
const problems = []
|
|
72
|
-
|
|
73
|
-
for (const route of config.extraViews ?? []) {
|
|
74
|
-
if (typeof route.meta?.section !== 'string' || !route.meta.section.trim()) {
|
|
75
|
-
problems.push(`Route "${route.name}" has no section metadata.`)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return problems
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Check that the texts in a mounted host include entries from the given
|
|
84
|
-
* namespaces. Matches each namespace against the rendered text with a
|
|
85
|
-
* case-insensitive regex like `/\b(ns1|ns2)\.[a-z]/i`.
|
|
86
|
-
*
|
|
87
|
-
* Returns the list of text keys matched (empty if none found). A missing
|
|
88
|
-
* key renders as its bare name, e.g. `"catalogue.era.ad"`, which the regex
|
|
89
|
-
* would match.
|
|
90
|
-
*/
|
|
91
|
-
export function checkTextsRendered(host, { namespaces = [] } = {}) {
|
|
92
|
-
if (!namespaces.length) return []
|
|
93
|
-
|
|
94
|
-
const text = host.textContent
|
|
95
|
-
const pattern = new RegExp(`\\b(${namespaces.join('|')})\\.[a-z]`, 'gi')
|
|
96
|
-
const found = new Set()
|
|
97
|
-
|
|
98
|
-
for (const match of text.matchAll(pattern)) {
|
|
99
|
-
found.add(match[1])
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return Array.from(found)
|
|
103
|
-
}
|
|
1
|
+
import { createViewer } from '../createViewer.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mount a website to a detached DOM node on a given hash, returning the app,
|
|
5
|
+
* host, and router for test assertions. The router is ready and the app is
|
|
6
|
+
* mounted before the promise resolves.
|
|
7
|
+
*
|
|
8
|
+
* `config` and `messages` are the same objects a website's main.js assembles.
|
|
9
|
+
* `hash` defaults to the home page.
|
|
10
|
+
*/
|
|
11
|
+
export async function mountSite(config, messages, hash = '#/') {
|
|
12
|
+
window.location.hash = hash
|
|
13
|
+
const app = createViewer({ ...config, messages })
|
|
14
|
+
const host = document.createElement('div')
|
|
15
|
+
document.body.appendChild(host)
|
|
16
|
+
app.mount(host)
|
|
17
|
+
const router = app.config.globalProperties.$router
|
|
18
|
+
await router.isReady()
|
|
19
|
+
return { app, host, router }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check that a configuration's routes are properly declared: every route has
|
|
24
|
+
* a name, no route is a catch-all, and every legacy route is listed in
|
|
25
|
+
* `legacyPaths`.
|
|
26
|
+
*
|
|
27
|
+
* Returns the list of problems found (empty if all is well). Routes without
|
|
28
|
+
* names appear by index in the message.
|
|
29
|
+
*/
|
|
30
|
+
export function checkRoutes(config, { names = [], legacyPaths = [] } = {}) {
|
|
31
|
+
const problems = []
|
|
32
|
+
const declaredNames = (config.extraViews ?? []).map((r) => r.name)
|
|
33
|
+
const declaredLegacy = (config.legacyRoutes ?? []).map((r) => r.path)
|
|
34
|
+
|
|
35
|
+
// Check every route has a name.
|
|
36
|
+
for (let i = 0; i < (config.extraViews ?? []).length; i++) {
|
|
37
|
+
if (!config.extraViews[i].name) {
|
|
38
|
+
problems.push(`Route ${i} has no name.`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check no route is a catch-all.
|
|
43
|
+
if ((config.extraViews ?? []).some((r) => r.path?.includes('pathMatch'))) {
|
|
44
|
+
problems.push('A route is a catch-all (contains pathMatch); the catch-all belongs to the router.')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check expected names are present.
|
|
48
|
+
for (const name of names) {
|
|
49
|
+
if (!declaredNames.includes(name)) {
|
|
50
|
+
problems.push(`Expected route "${name}" not found.`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check expected legacy paths are present.
|
|
55
|
+
for (const path of legacyPaths) {
|
|
56
|
+
if (!declaredLegacy.includes(path)) {
|
|
57
|
+
problems.push(`Expected legacy path "${path}" not found.`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return problems
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check that every route has a `meta.section` string. A route without one
|
|
66
|
+
* leaves the menu unaware of what section the visitor is in.
|
|
67
|
+
*
|
|
68
|
+
* Returns the list of problems found (empty if all is well).
|
|
69
|
+
*/
|
|
70
|
+
export function checkSectionMeta(config) {
|
|
71
|
+
const problems = []
|
|
72
|
+
|
|
73
|
+
for (const route of config.extraViews ?? []) {
|
|
74
|
+
if (typeof route.meta?.section !== 'string' || !route.meta.section.trim()) {
|
|
75
|
+
problems.push(`Route "${route.name}" has no section metadata.`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return problems
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check that the texts in a mounted host include entries from the given
|
|
84
|
+
* namespaces. Matches each namespace against the rendered text with a
|
|
85
|
+
* case-insensitive regex like `/\b(ns1|ns2)\.[a-z]/i`.
|
|
86
|
+
*
|
|
87
|
+
* Returns the list of text keys matched (empty if none found). A missing
|
|
88
|
+
* key renders as its bare name, e.g. `"catalogue.era.ad"`, which the regex
|
|
89
|
+
* would match.
|
|
90
|
+
*/
|
|
91
|
+
export function checkTextsRendered(host, { namespaces = [] } = {}) {
|
|
92
|
+
if (!namespaces.length) return []
|
|
93
|
+
|
|
94
|
+
const text = host.textContent
|
|
95
|
+
const pattern = new RegExp(`\\b(${namespaces.join('|')})\\.[a-z]`, 'gi')
|
|
96
|
+
const found = new Set()
|
|
97
|
+
|
|
98
|
+
for (const match of text.matchAll(pattern)) {
|
|
99
|
+
found.add(match[1])
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return Array.from(found)
|
|
103
|
+
}
|
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The shared Vite configuration for viewer websites, returning a config object
|
|
5
|
-
* ready to pass to `defineConfig()`. The seven websites duplicate this shape:
|
|
6
|
-
* the `@inventory-data` alias to the data package, `optimizeDeps` to inline
|
|
7
|
-
* the viewer packages, `testTimeout`, and the test environment.
|
|
8
|
-
*
|
|
9
|
-
* `dataPackage` is the npm package name, e.g. `'@museumwnf/islamicart-data'`.
|
|
10
|
-
* `inline` is an optional array of extra packages to inline in tests, beyond
|
|
11
|
-
* the required `'@museumwnf/viewer-core'` and `'@museumwnf/viewer-layout'`
|
|
12
|
-
* (and, transitionally, their pre-#1722 `'@metanull/*'` spellings — see the
|
|
13
|
-
* comment on `test.server.deps.inline` below).
|
|
14
|
-
* `plugins` is an optional array of Vite plugins (e.g. `@vitejs/plugin-vue`),
|
|
15
|
-
* which the site must supply because this package does not depend on them.
|
|
16
|
-
* `root` is an optional project root path (default `process.cwd()`); use it when
|
|
17
|
-
* loading the config from a context other than the project directory.
|
|
18
|
-
*/
|
|
19
|
-
export function defineViewerConfig({
|
|
20
|
-
dataPackage,
|
|
21
|
-
inline = [],
|
|
22
|
-
plugins = [],
|
|
23
|
-
root = process.cwd(),
|
|
24
|
-
} = {}) {
|
|
25
|
-
return {
|
|
26
|
-
plugins,
|
|
27
|
-
resolve: {
|
|
28
|
-
alias: {
|
|
29
|
-
// viewer-core reads every JSON of the data package through this alias.
|
|
30
|
-
// Resolve against the caller's project root, not this file's installed
|
|
31
|
-
// location: import.meta.url is the package's own path inside node_modules,
|
|
32
|
-
// not the site's project root where its node_modules actually exists.
|
|
33
|
-
'@inventory-data': path.join(root, 'node_modules', dataPackage),
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
optimizeDeps: {
|
|
37
|
-
// viewer-core ships .vue source that esbuild pre-bundling cannot parse;
|
|
38
|
-
// viewer-layout must not be pre-bundled either or its chunk gets a second
|
|
39
|
-
// copy of the Vue runtime in dev (both packages share the app's vue).
|
|
40
|
-
// The /i18n subpath is listed as well as the package: Vite pre-bundles a
|
|
41
|
-
// subpath as its own entry, and a second copy of the text module would be
|
|
42
|
-
// a second, empty set of texts for whatever imported it.
|
|
43
|
-
//
|
|
44
|
-
// Both the @museumwnf and @metanull spellings are listed: this list is
|
|
45
|
-
// matched against the *site's own* import specifier, which still says
|
|
46
|
-
// @metanull/* until every site completes its own move in
|
|
47
|
-
// metanull/inventory-app#1722. Matching only the new name silently stops
|
|
48
|
-
// excluding the package for every current consumer.
|
|
49
|
-
exclude: [
|
|
50
|
-
'@museumwnf/viewer-core',
|
|
51
|
-
'@museumwnf/viewer-core/i18n',
|
|
52
|
-
'@museumwnf/viewer-layout',
|
|
53
|
-
'@metanull/viewer-core',
|
|
54
|
-
'@metanull/viewer-core/i18n',
|
|
55
|
-
'@metanull/viewer-layout',
|
|
56
|
-
],
|
|
57
|
-
// The runtime deps reach the browser through those excluded packages, so
|
|
58
|
-
// the dev-server dependency scan cannot discover them until the website's
|
|
59
|
-
// own views import them directly. Without this list a late discovery
|
|
60
|
-
// pre-bundles a second copy of Vue next to the raw one already loaded,
|
|
61
|
-
// and the dev server crashes on boot. Listing them pre-bundles each
|
|
62
|
-
// exactly once, and the excluded packages get the same copy.
|
|
63
|
-
include: ['vue', 'vue-router'],
|
|
64
|
-
},
|
|
65
|
-
test: {
|
|
66
|
-
environment: 'jsdom',
|
|
67
|
-
testTimeout: 60000,
|
|
68
|
-
server: {
|
|
69
|
-
deps: {
|
|
70
|
-
// viewer-core ships .vue source; Node cannot load it unless Vitest
|
|
71
|
-
// processes the package instead of externalizing it, matched by the
|
|
72
|
-
// specifier the *importing site* actually wrote. viewer-layout's
|
|
73
|
-
// composed views import viewer-core, so the layout is processed too.
|
|
74
|
-
// Both scopes are listed for the same transitional reason as
|
|
75
|
-
// optimizeDeps.exclude above: sites still import @metanull/* until
|
|
76
|
-
// metanull/inventory-app#1722, so matching only @museumwnf/* would
|
|
77
|
-
// externalize this package for every current consumer, sending its
|
|
78
|
-
// raw .vue files to Node's loader instead of Vitest's transform.
|
|
79
|
-
inline: [
|
|
80
|
-
'@museumwnf/viewer-core',
|
|
81
|
-
'@museumwnf/viewer-layout',
|
|
82
|
-
'@metanull/viewer-core',
|
|
83
|
-
'@metanull/viewer-layout',
|
|
84
|
-
...inline,
|
|
85
|
-
],
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
}
|
|
90
|
-
}
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The shared Vite configuration for viewer websites, returning a config object
|
|
5
|
+
* ready to pass to `defineConfig()`. The seven websites duplicate this shape:
|
|
6
|
+
* the `@inventory-data` alias to the data package, `optimizeDeps` to inline
|
|
7
|
+
* the viewer packages, `testTimeout`, and the test environment.
|
|
8
|
+
*
|
|
9
|
+
* `dataPackage` is the npm package name, e.g. `'@museumwnf/islamicart-data'`.
|
|
10
|
+
* `inline` is an optional array of extra packages to inline in tests, beyond
|
|
11
|
+
* the required `'@museumwnf/viewer-core'` and `'@museumwnf/viewer-layout'`
|
|
12
|
+
* (and, transitionally, their pre-#1722 `'@metanull/*'` spellings — see the
|
|
13
|
+
* comment on `test.server.deps.inline` below).
|
|
14
|
+
* `plugins` is an optional array of Vite plugins (e.g. `@vitejs/plugin-vue`),
|
|
15
|
+
* which the site must supply because this package does not depend on them.
|
|
16
|
+
* `root` is an optional project root path (default `process.cwd()`); use it when
|
|
17
|
+
* loading the config from a context other than the project directory.
|
|
18
|
+
*/
|
|
19
|
+
export function defineViewerConfig({
|
|
20
|
+
dataPackage,
|
|
21
|
+
inline = [],
|
|
22
|
+
plugins = [],
|
|
23
|
+
root = process.cwd(),
|
|
24
|
+
} = {}) {
|
|
25
|
+
return {
|
|
26
|
+
plugins,
|
|
27
|
+
resolve: {
|
|
28
|
+
alias: {
|
|
29
|
+
// viewer-core reads every JSON of the data package through this alias.
|
|
30
|
+
// Resolve against the caller's project root, not this file's installed
|
|
31
|
+
// location: import.meta.url is the package's own path inside node_modules,
|
|
32
|
+
// not the site's project root where its node_modules actually exists.
|
|
33
|
+
'@inventory-data': path.join(root, 'node_modules', dataPackage),
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
optimizeDeps: {
|
|
37
|
+
// viewer-core ships .vue source that esbuild pre-bundling cannot parse;
|
|
38
|
+
// viewer-layout must not be pre-bundled either or its chunk gets a second
|
|
39
|
+
// copy of the Vue runtime in dev (both packages share the app's vue).
|
|
40
|
+
// The /i18n subpath is listed as well as the package: Vite pre-bundles a
|
|
41
|
+
// subpath as its own entry, and a second copy of the text module would be
|
|
42
|
+
// a second, empty set of texts for whatever imported it.
|
|
43
|
+
//
|
|
44
|
+
// Both the @museumwnf and @metanull spellings are listed: this list is
|
|
45
|
+
// matched against the *site's own* import specifier, which still says
|
|
46
|
+
// @metanull/* until every site completes its own move in
|
|
47
|
+
// metanull/inventory-app#1722. Matching only the new name silently stops
|
|
48
|
+
// excluding the package for every current consumer.
|
|
49
|
+
exclude: [
|
|
50
|
+
'@museumwnf/viewer-core',
|
|
51
|
+
'@museumwnf/viewer-core/i18n',
|
|
52
|
+
'@museumwnf/viewer-layout',
|
|
53
|
+
'@metanull/viewer-core',
|
|
54
|
+
'@metanull/viewer-core/i18n',
|
|
55
|
+
'@metanull/viewer-layout',
|
|
56
|
+
],
|
|
57
|
+
// The runtime deps reach the browser through those excluded packages, so
|
|
58
|
+
// the dev-server dependency scan cannot discover them until the website's
|
|
59
|
+
// own views import them directly. Without this list a late discovery
|
|
60
|
+
// pre-bundles a second copy of Vue next to the raw one already loaded,
|
|
61
|
+
// and the dev server crashes on boot. Listing them pre-bundles each
|
|
62
|
+
// exactly once, and the excluded packages get the same copy.
|
|
63
|
+
include: ['vue', 'vue-router'],
|
|
64
|
+
},
|
|
65
|
+
test: {
|
|
66
|
+
environment: 'jsdom',
|
|
67
|
+
testTimeout: 60000,
|
|
68
|
+
server: {
|
|
69
|
+
deps: {
|
|
70
|
+
// viewer-core ships .vue source; Node cannot load it unless Vitest
|
|
71
|
+
// processes the package instead of externalizing it, matched by the
|
|
72
|
+
// specifier the *importing site* actually wrote. viewer-layout's
|
|
73
|
+
// composed views import viewer-core, so the layout is processed too.
|
|
74
|
+
// Both scopes are listed for the same transitional reason as
|
|
75
|
+
// optimizeDeps.exclude above: sites still import @metanull/* until
|
|
76
|
+
// metanull/inventory-app#1722, so matching only @museumwnf/* would
|
|
77
|
+
// externalize this package for every current consumer, sending its
|
|
78
|
+
// raw .vue files to Node's loader instead of Vitest's transform.
|
|
79
|
+
inline: [
|
|
80
|
+
'@museumwnf/viewer-core',
|
|
81
|
+
'@museumwnf/viewer-layout',
|
|
82
|
+
'@metanull/viewer-core',
|
|
83
|
+
'@metanull/viewer-layout',
|
|
84
|
+
...inline,
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/views/DetailView.vue
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { computed, ref, watchEffect } from 'vue'
|
|
3
|
-
import { useDataPackage } from '../composables/useDataPackage.js'
|
|
4
|
-
import { renderBlock } from '../i18n/markdown.js'
|
|
5
|
-
|
|
6
|
-
const props = defineProps({
|
|
7
|
-
entity: { type: String, required: true },
|
|
8
|
-
id: { type: String, required: true },
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
const { loadEntity } = useDataPackage()
|
|
12
|
-
|
|
13
|
-
const record = ref(undefined)
|
|
14
|
-
watchEffect(async () => {
|
|
15
|
-
// Read both props before the first await so the effect tracks them.
|
|
16
|
-
const { entity, id } = props
|
|
17
|
-
const all = await loadEntity(entity)
|
|
18
|
-
record.value = all.find((r) => String(r.id) === String(id)) ?? null
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
const fields = computed(() =>
|
|
22
|
-
record.value ? Object.entries(record.value).filter(([key]) => key !== 'id') : []
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
// Through the same pipeline as every text, which escapes raw HTML rather than
|
|
26
|
-
// rendering it. A data package holds Markdown: the importer converts the legacy
|
|
27
|
-
// HTML on the way in, so HTML reaching a field here is a fault upstream and the
|
|
28
|
-
// fix belongs in the importer. Rendering it would hide that, and would make a
|
|
29
|
-
// museum record — written by hand, years ago, in another system — the one input
|
|
30
|
-
// this application trusts with markup.
|
|
31
|
-
function renderValue(value) {
|
|
32
|
-
return typeof value === 'string' ? renderBlock(value, { breaks: true }) : JSON.stringify(value)
|
|
33
|
-
}
|
|
34
|
-
</script>
|
|
35
|
-
|
|
36
|
-
<template>
|
|
37
|
-
<article class="vc-detail">
|
|
38
|
-
<p v-if="record === undefined">{{ $t('core.status.loading') }}</p>
|
|
39
|
-
<p v-else-if="record === null">{{ $t('core.detail.notFound') }}</p>
|
|
40
|
-
<template v-else>
|
|
41
|
-
<h1>{{ record.title ?? record.name ?? record.id }}</h1>
|
|
42
|
-
<dl>
|
|
43
|
-
<template v-for="[key, value] in fields" :key="key">
|
|
44
|
-
<dt>{{ key }}</dt>
|
|
45
|
-
<dd v-html="renderValue(value)"></dd>
|
|
46
|
-
</template>
|
|
47
|
-
</dl>
|
|
48
|
-
</template>
|
|
49
|
-
<p>
|
|
50
|
-
<router-link :to="`/${entity}`">{{ $t('core.nav.backToList') }}</router-link>
|
|
51
|
-
</p>
|
|
52
|
-
</article>
|
|
53
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watchEffect } from 'vue'
|
|
3
|
+
import { useDataPackage } from '../composables/useDataPackage.js'
|
|
4
|
+
import { renderBlock } from '../i18n/markdown.js'
|
|
5
|
+
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
entity: { type: String, required: true },
|
|
8
|
+
id: { type: String, required: true },
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const { loadEntity } = useDataPackage()
|
|
12
|
+
|
|
13
|
+
const record = ref(undefined)
|
|
14
|
+
watchEffect(async () => {
|
|
15
|
+
// Read both props before the first await so the effect tracks them.
|
|
16
|
+
const { entity, id } = props
|
|
17
|
+
const all = await loadEntity(entity)
|
|
18
|
+
record.value = all.find((r) => String(r.id) === String(id)) ?? null
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const fields = computed(() =>
|
|
22
|
+
record.value ? Object.entries(record.value).filter(([key]) => key !== 'id') : []
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// Through the same pipeline as every text, which escapes raw HTML rather than
|
|
26
|
+
// rendering it. A data package holds Markdown: the importer converts the legacy
|
|
27
|
+
// HTML on the way in, so HTML reaching a field here is a fault upstream and the
|
|
28
|
+
// fix belongs in the importer. Rendering it would hide that, and would make a
|
|
29
|
+
// museum record — written by hand, years ago, in another system — the one input
|
|
30
|
+
// this application trusts with markup.
|
|
31
|
+
function renderValue(value) {
|
|
32
|
+
return typeof value === 'string' ? renderBlock(value, { breaks: true }) : JSON.stringify(value)
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<article class="vc-detail">
|
|
38
|
+
<p v-if="record === undefined">{{ $t('core.status.loading') }}</p>
|
|
39
|
+
<p v-else-if="record === null">{{ $t('core.detail.notFound') }}</p>
|
|
40
|
+
<template v-else>
|
|
41
|
+
<h1>{{ record.title ?? record.name ?? record.id }}</h1>
|
|
42
|
+
<dl>
|
|
43
|
+
<template v-for="[key, value] in fields" :key="key">
|
|
44
|
+
<dt>{{ key }}</dt>
|
|
45
|
+
<dd v-html="renderValue(value)"></dd>
|
|
46
|
+
</template>
|
|
47
|
+
</dl>
|
|
48
|
+
</template>
|
|
49
|
+
<p>
|
|
50
|
+
<router-link :to="`/${entity}`">{{ $t('core.nav.backToList') }}</router-link>
|
|
51
|
+
</p>
|
|
52
|
+
</article>
|
|
53
|
+
</template>
|
package/src/views/HomeView.vue
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { inject } from 'vue'
|
|
3
|
-
import { VIEWER_CONFIG } from '../injectionKeys.js'
|
|
4
|
-
|
|
5
|
-
const config = inject(VIEWER_CONFIG, {})
|
|
6
|
-
const entities = config.features?.entities ?? []
|
|
7
|
-
</script>
|
|
8
|
-
|
|
9
|
-
<template>
|
|
10
|
-
<section class="vc-home">
|
|
11
|
-
<h1>{{ config.siteName }}</h1>
|
|
12
|
-
<nav v-if="entities.length">
|
|
13
|
-
<ul>
|
|
14
|
-
<li v-for="entity in entities" :key="entity">
|
|
15
|
-
<router-link :to="`/${entity}`">{{ entity }}</router-link>
|
|
16
|
-
</li>
|
|
17
|
-
</ul>
|
|
18
|
-
</nav>
|
|
19
|
-
</section>
|
|
20
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { inject } from 'vue'
|
|
3
|
+
import { VIEWER_CONFIG } from '../injectionKeys.js'
|
|
4
|
+
|
|
5
|
+
const config = inject(VIEWER_CONFIG, {})
|
|
6
|
+
const entities = config.features?.entities ?? []
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<section class="vc-home">
|
|
11
|
+
<h1>{{ config.siteName }}</h1>
|
|
12
|
+
<nav v-if="entities.length">
|
|
13
|
+
<ul>
|
|
14
|
+
<li v-for="entity in entities" :key="entity">
|
|
15
|
+
<router-link :to="`/${entity}`">{{ entity }}</router-link>
|
|
16
|
+
</li>
|
|
17
|
+
</ul>
|
|
18
|
+
</nav>
|
|
19
|
+
</section>
|
|
20
|
+
</template>
|