@museumwnf/viewer-core 1.14.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 -0
- package/README.md +635 -0
- package/package.json +50 -0
- package/src/AppRoot.vue +58 -0
- package/src/catalogue/dates.js +209 -0
- package/src/catalogue/facets.js +61 -0
- package/src/catalogue/keywordIndex.js +289 -0
- package/src/catalogue/listQuery.js +100 -0
- package/src/catalogue/pagination.js +57 -0
- package/src/catalogue/searchLanguage.js +32 -0
- package/src/catalogue/timeline.js +220 -0
- package/src/composables/useCatalogueData.js +141 -0
- package/src/composables/useDataPackage.js +98 -0
- package/src/composables/useEntities.js +100 -0
- package/src/composables/useRecordLanguage.js +71 -0
- package/src/conventions.js +148 -0
- package/src/createViewer.js +29 -0
- package/src/i18n/I18nText.vue +17 -0
- package/src/i18n/I18nTextInline.vue +17 -0
- package/src/i18n/index.js +88 -0
- package/src/i18n/language.js +127 -0
- package/src/i18n/markdown.js +265 -0
- package/src/index.js +50 -0
- package/src/injectionKeys.js +1 -0
- package/src/languages.js +49 -0
- package/src/legacy/index.js +58 -0
- package/src/record/collectionTree.js +307 -0
- package/src/record/derivations.js +154 -0
- package/src/record/partners.js +87 -0
- package/src/record/recordSheet.js +231 -0
- package/src/rights.js +53 -0
- package/src/router/index.js +138 -0
- package/src/siteConfig.js +37 -0
- package/src/styles/base.css +49 -0
- package/src/testing/checkOfferedLanguages.js +67 -0
- package/src/testing/index.js +3 -0
- package/src/testing/smoke.js +103 -0
- package/src/testing/viteConfig.js +90 -0
- package/src/views/DetailView.vue +53 -0
- package/src/views/HomeView.vue +20 -0
- package/src/views/ListView.vue +61 -0
- package/src/views/NotFoundView.vue +8 -0
- package/src/vite.js +8 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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>
|
|
@@ -0,0 +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>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watchEffect } from 'vue'
|
|
3
|
+
import { useRoute } from 'vue-router'
|
|
4
|
+
import { useDataPackage } from '../composables/useDataPackage.js'
|
|
5
|
+
import { paginate } from '../catalogue/pagination.js'
|
|
6
|
+
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
entity: { type: String, required: true },
|
|
9
|
+
pageSize: { type: Number, default: 20 },
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const route = useRoute()
|
|
13
|
+
const { loadEntity } = useDataPackage()
|
|
14
|
+
|
|
15
|
+
const records = ref(null)
|
|
16
|
+
watchEffect(async () => {
|
|
17
|
+
records.value = await loadEntity(props.entity)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// The same page arithmetic every website's lists use, so this generic list
|
|
21
|
+
// and a site's results page cannot disagree on where page 3 starts.
|
|
22
|
+
const paged = computed(() => paginate(records.value ?? [], route.query.page, props.pageSize))
|
|
23
|
+
const page = computed(() => paged.value.currentPage)
|
|
24
|
+
const totalPages = computed(() => paged.value.lastPage)
|
|
25
|
+
const pageRecords = computed(() => paged.value.rows)
|
|
26
|
+
|
|
27
|
+
function labelOf(record) {
|
|
28
|
+
return record.title ?? record.name ?? record.label ?? record.id
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<section class="vc-list">
|
|
34
|
+
<h1>{{ entity }}</h1>
|
|
35
|
+
<p v-if="records === null">{{ $t('core.status.loading') }}</p>
|
|
36
|
+
<p v-else-if="records.length === 0">{{ $t('core.list.empty') }}</p>
|
|
37
|
+
<template v-else>
|
|
38
|
+
<ul>
|
|
39
|
+
<li v-for="record in pageRecords" :key="record.id">
|
|
40
|
+
<router-link :to="`/${entity}/${record.id}`">{{ labelOf(record) }}</router-link>
|
|
41
|
+
</li>
|
|
42
|
+
</ul>
|
|
43
|
+
<nav v-if="totalPages > 1" class="vc-pagination">
|
|
44
|
+
<router-link v-if="page > 1" :to="{ query: { page: page - 1 } }">
|
|
45
|
+
{{ $t('core.pagination.previous') }}
|
|
46
|
+
</router-link>
|
|
47
|
+
<!-- The position is rendered here, next to the texts, rather than
|
|
48
|
+
inserted into one: a text that has to carry a number cannot be
|
|
49
|
+
translated without carrying the number's place in the sentence
|
|
50
|
+
too, and that is the one thing this system does not do. -->
|
|
51
|
+
<span class="vc-pagination__position">{{ page }} / {{ totalPages }}</span>
|
|
52
|
+
<router-link v-if="page < totalPages" :to="{ query: { page: page + 1 } }">
|
|
53
|
+
{{ $t('core.pagination.next') }}
|
|
54
|
+
</router-link>
|
|
55
|
+
</nav>
|
|
56
|
+
</template>
|
|
57
|
+
<p>
|
|
58
|
+
<router-link to="/">{{ $t('core.nav.home') }}</router-link>
|
|
59
|
+
</p>
|
|
60
|
+
</section>
|
|
61
|
+
</template>
|
package/src/vite.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vite configuration helper for viewer websites.
|
|
3
|
+
* This is a separate entry point because Vite's config loader runs under Node.js,
|
|
4
|
+
* which cannot parse .vue files. The ./testing barrel re-exports mountSite and
|
|
5
|
+
* other test utilities that import createViewer → AppRoot.vue, making it unsafe
|
|
6
|
+
* to import from a vite.config.js. This entry avoids that chain.
|
|
7
|
+
*/
|
|
8
|
+
export { defineViewerConfig } from './testing/viteConfig.js'
|