@glossarist/concept-browser 0.7.92 → 0.7.93
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/package.json +2 -2
- package/src/__tests__/components/GenericRelationList.test.ts +4 -4
- package/src/__tests__/components/GenericRelationList.test.ts.bak +147 -0
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +6 -6
- package/src/__tests__/composables/use-concept-edges-v2.test.ts.bak +5 -5
- package/src/__tests__/wire-keys.test.ts +1 -1
- package/src/__tests__/wire-keys.test.ts.bak +65 -0
- package/src/adapters/model-bridge/hyperedge.ts +9 -0
- package/src/adapters/non-verbal/glossarist-augment.d.ts +22 -7
- package/src/adapters/types.ts +4 -1
- package/src/adapters/wire-keys.ts +1 -0
- package/src/components/GenericRelationList.vue +11 -3
- package/src/composables/use-concept-edges.ts +11 -5
- package/src/views/ConceptView.vue +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.93",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"astro": "^7.0.7",
|
|
38
38
|
"d3": "^7.9.0",
|
|
39
39
|
"favicons": "^7.2.0",
|
|
40
|
-
"glossarist": "^0.4.
|
|
40
|
+
"glossarist": "^0.4.34",
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"jszip": "^3.10.1",
|
|
43
43
|
"pinia": "^2.3.1",
|
|
@@ -58,8 +58,8 @@ function makeRelation(overrides: Partial<GenericRelationWire> = {}): GenericRela
|
|
|
58
58
|
source: 'https://example.org/test/concept/5-1',
|
|
59
59
|
comprehensive: 'https://example.org/test/concept/5-1',
|
|
60
60
|
members: [
|
|
61
|
-
{ uri: 'https://example.org/test/concept/5-4', presence: 'required', count: 'exactly_one',
|
|
62
|
-
{ uri: 'https://example.org/test/concept/5-5', presence: 'required', count: 'exactly_one',
|
|
61
|
+
{ uri: 'https://example.org/test/concept/5-4', presence: 'required', count: 'exactly_one', delimitingCharacteristic: { eng: "test characteristic" } },
|
|
62
|
+
{ uri: 'https://example.org/test/concept/5-5', presence: 'required', count: 'exactly_one', delimitingCharacteristic: { eng: "test characteristic" } },
|
|
63
63
|
],
|
|
64
64
|
completeness: 'complete',
|
|
65
65
|
register: 'test',
|
|
@@ -120,8 +120,8 @@ describe('GenericRelationList — wire-type contract mount test', () => {
|
|
|
120
120
|
props: {
|
|
121
121
|
relations: [makeRelation({
|
|
122
122
|
members: [
|
|
123
|
-
{ uri: 'https://example.org/test/concept/5-4', presence: 'optional', count: 'multiple',
|
|
124
|
-
{ uri: 'https://example.org/test/concept/5-5', presence: 'required', count: 'exactly_one',
|
|
123
|
+
{ uri: 'https://example.org/test/concept/5-4', presence: 'optional', count: 'multiple', delimitingCharacteristic: { eng: "delimiting by light sensors" } },
|
|
124
|
+
{ uri: 'https://example.org/test/concept/5-5', presence: 'required', count: 'exactly_one', delimitingCharacteristic: { eng: "test characteristic" } },
|
|
125
125
|
],
|
|
126
126
|
})],
|
|
127
127
|
manifest,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenericRelationList mount test — pins the wire-type contract so
|
|
3
|
+
* the 5 type bugs found in audit (entry.titles, member.ref,
|
|
4
|
+
* is_delimiting casing, comprehensive.source, missing props)
|
|
5
|
+
* can't regress.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
8
|
+
import { mount } from '@vue/test-utils';
|
|
9
|
+
import GenericRelationList from '../../components/GenericRelationList.vue';
|
|
10
|
+
import type { GenericRelationWire } from '../../adapters/types';
|
|
11
|
+
|
|
12
|
+
// Stub the composables the component depends on
|
|
13
|
+
vi.mock('../../stores/vocabulary', () => ({
|
|
14
|
+
useVocabularyStore: () => ({
|
|
15
|
+
graph: { getNode: () => null },
|
|
16
|
+
datasets: new Map(),
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('../../adapters/factory', () => ({
|
|
21
|
+
getFactory: () => ({
|
|
22
|
+
resolve: (uri: string) => ({ type: 'unresolved' as const, uri }),
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock('../../i18n', () => ({
|
|
27
|
+
useI18n: () => ({ t: (key: string) => key }),
|
|
28
|
+
locale: { value: 'eng' },
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock('vue-router', () => ({
|
|
32
|
+
useRouter: () => ({ push: vi.fn() }),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const manifest = {
|
|
36
|
+
id: 'test',
|
|
37
|
+
datasetUri: 'https://example.org/test',
|
|
38
|
+
title: 'Test',
|
|
39
|
+
description: '',
|
|
40
|
+
owner: '',
|
|
41
|
+
baseUrl: '',
|
|
42
|
+
languages: ['eng'],
|
|
43
|
+
conceptCount: 0,
|
|
44
|
+
conceptUrlTemplate: '',
|
|
45
|
+
indexUrl: '',
|
|
46
|
+
contextUrl: '',
|
|
47
|
+
uriBase: 'https://example.org',
|
|
48
|
+
status: 'valid',
|
|
49
|
+
schemaVersion: '3',
|
|
50
|
+
tags: [],
|
|
51
|
+
lastUpdated: '',
|
|
52
|
+
sourceRepo: '',
|
|
53
|
+
chunkSize: 100,
|
|
54
|
+
} as any;
|
|
55
|
+
|
|
56
|
+
function makeRelation(overrides: Partial<GenericRelationWire> = {}): GenericRelationWire {
|
|
57
|
+
return {
|
|
58
|
+
source: 'https://example.org/test/concept/5-1',
|
|
59
|
+
comprehensive: 'https://example.org/test/concept/5-1',
|
|
60
|
+
members: [
|
|
61
|
+
{ uri: 'https://example.org/test/concept/5-4', presence: 'required', count: 'exactly_one', isDelimiting: false },
|
|
62
|
+
{ uri: 'https://example.org/test/concept/5-5', presence: 'required', count: 'exactly_one', isDelimiting: false },
|
|
63
|
+
],
|
|
64
|
+
completeness: 'complete',
|
|
65
|
+
register: 'test',
|
|
66
|
+
...overrides,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('GenericRelationList — wire-type contract mount test', () => {
|
|
71
|
+
it('mounts without errors for a well-formed relation', () => {
|
|
72
|
+
const w = mount(GenericRelationList, {
|
|
73
|
+
props: {
|
|
74
|
+
relations: [makeRelation()],
|
|
75
|
+
manifest,
|
|
76
|
+
registerId: 'test',
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
expect(w.html()).toContain('generic-relation-card');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('renders completeness badge', () => {
|
|
83
|
+
const w = mount(GenericRelationList, {
|
|
84
|
+
props: {
|
|
85
|
+
relations: [makeRelation({ completeness: 'partial' })],
|
|
86
|
+
manifest,
|
|
87
|
+
registerId: 'test',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
expect(w.html()).toContain('completeness-partial');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('renders multiple relations', () => {
|
|
94
|
+
const w = mount(GenericRelationList, {
|
|
95
|
+
props: {
|
|
96
|
+
relations: [
|
|
97
|
+
makeRelation({ comprehensive: 'https://example.org/test/concept/5-1' }),
|
|
98
|
+
makeRelation({ comprehensive: 'https://example.org/test/concept/5-2' }),
|
|
99
|
+
],
|
|
100
|
+
manifest,
|
|
101
|
+
registerId: 'test',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
expect(w.findAll('.generic-relation-card')).toHaveLength(2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('renders criterion when present', () => {
|
|
108
|
+
const w = mount(GenericRelationList, {
|
|
109
|
+
props: {
|
|
110
|
+
relations: [makeRelation({ criterion: { eng: 'by realization medium' } })],
|
|
111
|
+
manifest,
|
|
112
|
+
registerId: 'test',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
expect(w.html()).toContain('by realization medium');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('renders member presence/count badges for non-default values', () => {
|
|
119
|
+
const w = mount(GenericRelationList, {
|
|
120
|
+
props: {
|
|
121
|
+
relations: [makeRelation({
|
|
122
|
+
members: [
|
|
123
|
+
{ uri: 'https://example.org/test/concept/5-4', presence: 'optional', count: 'multiple', isDelimiting: true },
|
|
124
|
+
{ uri: 'https://example.org/test/concept/5-5', presence: 'required', count: 'exactly_one', isDelimiting: false },
|
|
125
|
+
],
|
|
126
|
+
})],
|
|
127
|
+
manifest,
|
|
128
|
+
registerId: 'test',
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const html = w.html();
|
|
132
|
+
expect(html).toContain('Optional');
|
|
133
|
+
expect(html).toContain('Multiple');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('renders empty section when relations array is empty', () => {
|
|
137
|
+
const w = mount(GenericRelationList, {
|
|
138
|
+
props: {
|
|
139
|
+
relations: [],
|
|
140
|
+
manifest,
|
|
141
|
+
registerId: 'test',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
expect(w.find('section.generic-relations').exists()).toBe(true);
|
|
145
|
+
expect(w.findAll('.generic-relation-card')).toHaveLength(0);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -39,7 +39,7 @@ function makePartitive(
|
|
|
39
39
|
if (fields.presence) data.presence = fields.presence;
|
|
40
40
|
if (fields.count) data.count = fields.count;
|
|
41
41
|
if (fields.isDelimiting) data.is_delimiting = fields.isDelimiting;
|
|
42
|
-
return PartitiveMemberModel
|
|
42
|
+
return new PartitiveMemberModel(data as any) as PartitiveMember;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function makeRelation(opts: {
|
|
@@ -48,7 +48,7 @@ function makeRelation(opts: {
|
|
|
48
48
|
completeness?: 'complete' | 'partial';
|
|
49
49
|
criterion?: Record<string, string>;
|
|
50
50
|
}): PartitiveHyperedge {
|
|
51
|
-
return PartitiveHyperedgeModel
|
|
51
|
+
return new PartitiveHyperedgeModel(opts as any) as PartitiveHyperedge;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () => {
|
|
@@ -84,14 +84,14 @@ describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () =>
|
|
|
84
84
|
|
|
85
85
|
it('is_delimiting defaults to false', () => {
|
|
86
86
|
const m = makePartitive('test', '1.1');
|
|
87
|
-
expect(m.is_delimiting).toBe(false);
|
|
88
|
-
expect(m.is_delimiting).toBe(false);
|
|
87
|
+
expect((m as any).is_delimiting).toBe(false);
|
|
88
|
+
expect((m as any).is_delimiting).toBe(false);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it('is_delimiting is preserved when set to true', () => {
|
|
92
92
|
const m = makePartitive('test', '1.1', { isDelimiting: true });
|
|
93
|
-
expect(m.is_delimiting).toBe(true);
|
|
94
|
-
expect(m.is_delimiting).toBe(true);
|
|
93
|
+
expect((m as any).is_delimiting).toBe(true);
|
|
94
|
+
expect((m as any).is_delimiting).toBe(true);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
it('rejects the vacuous (presence=optional, count=at_least_one)', () => {
|
|
@@ -84,14 +84,14 @@ describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () =>
|
|
|
84
84
|
|
|
85
85
|
it('is_delimiting defaults to false', () => {
|
|
86
86
|
const m = makePartitive('test', '1.1');
|
|
87
|
-
expect(m.is_delimiting).toBe(false);
|
|
88
|
-
expect(m.is_delimiting).toBe(false);
|
|
87
|
+
expect((m as any).is_delimiting).toBe(false);
|
|
88
|
+
expect((m as any).is_delimiting).toBe(false);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it('is_delimiting is preserved when set to true', () => {
|
|
92
92
|
const m = makePartitive('test', '1.1', { isDelimiting: true });
|
|
93
|
-
expect(m.is_delimiting).toBe(true);
|
|
94
|
-
expect(m.is_delimiting).toBe(true);
|
|
93
|
+
expect((m as any).is_delimiting).toBe(true);
|
|
94
|
+
expect((m as any).is_delimiting).toBe(true);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
it('rejects the vacuous (presence=optional, count=at_least_one)', () => {
|
|
@@ -107,7 +107,7 @@ describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () =>
|
|
|
107
107
|
comprehensive: makeRef('test', '1.3'),
|
|
108
108
|
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
109
109
|
});
|
|
110
|
-
expect(r.members).toHaveLength(2);
|
|
110
|
+
expect((r as any).members).toHaveLength(2);
|
|
111
111
|
expect((r as any).completeness).toBe('complete');
|
|
112
112
|
expect((r as any).isComplete).toBe(true);
|
|
113
113
|
expect((r as any).isPartial).toBe(false);
|
|
@@ -60,6 +60,6 @@ describe('GL wire-key SSOT', () => {
|
|
|
60
60
|
// The point of this test: when adding a key, you remember to update
|
|
61
61
|
// the count; when removing, you notice the test fails. Catches
|
|
62
62
|
// accidental drift in either direction.
|
|
63
|
-
expect(Object.keys(GL).length).toBe(
|
|
63
|
+
expect(Object.keys(GL).length).toBe(91);
|
|
64
64
|
});
|
|
65
65
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-key SSOT contract.
|
|
3
|
+
*
|
|
4
|
+
* Audit-driven (AUDIT-2026-07-29 P1-3).
|
|
5
|
+
*
|
|
6
|
+
* The GL record is the single source of truth for the JSON-LD wire-format
|
|
7
|
+
* keys used across src/adapters/. This spec pins:
|
|
8
|
+
*
|
|
9
|
+
* - Every entry is a string starting with 'gl:' (or '@' for JSON-LD core)
|
|
10
|
+
* - No two entries share the same value (injectivity — one key, one name)
|
|
11
|
+
* - The set of values matches what's declared in jsonld-types.ts
|
|
12
|
+
* (catches drift if someone adds a key to one but not the other)
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { GL } from '../adapters/wire-keys';
|
|
16
|
+
|
|
17
|
+
describe('GL wire-key SSOT', () => {
|
|
18
|
+
it('every value is a non-empty string', () => {
|
|
19
|
+
for (const value of Object.values(GL)) {
|
|
20
|
+
expect(typeof value).toBe('string');
|
|
21
|
+
expect(value.length).toBeGreaterThan(0);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('every value starts with gl: or @ (JSON-LD namespace)', () => {
|
|
26
|
+
for (const value of Object.values(GL)) {
|
|
27
|
+
expect(value.startsWith('gl:') || value.startsWith('@')).toBe(true);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('no two entries share the same value (injectivity)', () => {
|
|
32
|
+
const values = Object.values(GL);
|
|
33
|
+
expect(new Set(values).size).toBe(values.length);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('MECE keys are present (presence, count, isDelimiting, multiplicity, certainty)', () => {
|
|
37
|
+
expect(GL.PRESENCE).toBe('gl:presence');
|
|
38
|
+
expect(GL.COUNT).toBe('gl:count');
|
|
39
|
+
expect(GL.IS_DELIMITING).toBe('gl:isDelimiting');
|
|
40
|
+
expect(GL.MULTIPLICITY).toBe('gl:multiplicity');
|
|
41
|
+
expect(GL.CERTAINTY).toBe('gl:certainty');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('JSON-LD core keys are present', () => {
|
|
45
|
+
expect(GL.ID).toBe('@id');
|
|
46
|
+
expect(GL.TYPE).toBe('@type');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('LOCAL_ID is distinct from ID (one is gl:id, the other @id)', () => {
|
|
50
|
+
// Both represent "id" but in different namespaces:
|
|
51
|
+
// GL.ID (@id) — JSON-LD IRI for the concept
|
|
52
|
+
// GL.LOCAL_ID (gl:id) — source-local id within ConceptSource/Origin/Ref
|
|
53
|
+
expect(GL.ID).toBe('@id');
|
|
54
|
+
expect(GL.LOCAL_ID).toBe('gl:id');
|
|
55
|
+
expect(GL.ID).not.toBe(GL.LOCAL_ID);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('count matches documented set (no drift)', () => {
|
|
59
|
+
// Update this number when adding a new key.
|
|
60
|
+
// The point of this test: when adding a key, you remember to update
|
|
61
|
+
// the count; when removing, you notice the test fails. Catches
|
|
62
|
+
// accidental drift in either direction.
|
|
63
|
+
expect(Object.keys(GL).length).toBe(90);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -53,6 +53,15 @@ export function mapHyperedgeFromJsonLd(
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
if (m[GL.IS_DELIMITING] === true) out.is_delimiting = true;
|
|
56
|
+
// ISO 704:2022 §5.5.4.2.1 — GenericMember carries a required
|
|
57
|
+
// delimitingCharacteristic (the intension difference between
|
|
58
|
+
// this specific concept and its coordinate concepts).
|
|
59
|
+
const dc = m[GL.DELIMITING_CHARACTERISTIC];
|
|
60
|
+
if (dc) {
|
|
61
|
+
out.delimitingCharacteristic = typeof dc === 'string'
|
|
62
|
+
? { default: dc }
|
|
63
|
+
: dc;
|
|
64
|
+
}
|
|
56
65
|
return out;
|
|
57
66
|
})
|
|
58
67
|
.filter((m): m is Record<string, unknown> => m !== null);
|
|
@@ -182,22 +182,37 @@ declare module 'glossarist' {
|
|
|
182
182
|
ref?: ConceptRef;
|
|
183
183
|
presence?: PartitivePresence;
|
|
184
184
|
count?: PartitiveCount;
|
|
185
|
-
is_delimiting?: boolean;
|
|
186
185
|
});
|
|
187
186
|
readonly ref: ConceptRef;
|
|
188
187
|
readonly presence: PartitivePresence;
|
|
189
188
|
readonly count: PartitiveCount;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
readonly isOptional: boolean;
|
|
193
|
-
get isDelimiting(): boolean;
|
|
189
|
+
required(): boolean;
|
|
190
|
+
optional(): boolean;
|
|
194
191
|
toJSON(): Record<string, unknown>;
|
|
195
192
|
static fromJSON(data: Record<string, unknown>): HyperedgeMember;
|
|
196
193
|
static identityOf(value: unknown): string;
|
|
197
194
|
}
|
|
198
195
|
|
|
199
|
-
export class PartitiveMember extends HyperedgeMember {
|
|
200
|
-
|
|
196
|
+
export class PartitiveMember extends HyperedgeMember {
|
|
197
|
+
constructor(data?: {
|
|
198
|
+
ref?: ConceptRef;
|
|
199
|
+
presence?: PartitivePresence;
|
|
200
|
+
count?: PartitiveCount;
|
|
201
|
+
is_delimiting?: boolean;
|
|
202
|
+
});
|
|
203
|
+
readonly is_delimiting: boolean;
|
|
204
|
+
get isDelimiting(): boolean;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export class GenericMember extends HyperedgeMember {
|
|
208
|
+
constructor(data?: {
|
|
209
|
+
ref?: ConceptRef;
|
|
210
|
+
presence?: PartitivePresence;
|
|
211
|
+
count?: PartitiveCount;
|
|
212
|
+
delimitingCharacteristic?: Record<string, string> | string;
|
|
213
|
+
});
|
|
214
|
+
readonly delimitingCharacteristic: Record<string, string>;
|
|
215
|
+
}
|
|
201
216
|
|
|
202
217
|
export class AbstractHyperedge extends GlossaristModel {
|
|
203
218
|
readonly comprehensive: ConceptRef;
|
package/src/adapters/types.ts
CHANGED
|
@@ -200,7 +200,10 @@ export interface GenericMemberWire {
|
|
|
200
200
|
uri: string;
|
|
201
201
|
presence: 'required' | 'optional';
|
|
202
202
|
count: 'exactly_one' | 'at_least_one' | 'multiple';
|
|
203
|
-
|
|
203
|
+
/** ISO 704:2022 §5.5.4.2.1 — the delimiting characteristic that
|
|
204
|
+
* distinguishes this specific concept from its coordinate concepts.
|
|
205
|
+
* Required on every GenericMember. LocalizedString shape. */
|
|
206
|
+
delimitingCharacteristic: Record<string, string>;
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
export interface GraphNode {
|
|
@@ -42,12 +42,17 @@ function designationFor(uri: string): string {
|
|
|
42
42
|
return resolveDesignation(uri, store, factory, locale.value);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** Resolve a LocalizedString delimitingCharacteristic to the current locale. */
|
|
46
|
+
function characteristicText(dc: Record<string, string>): string {
|
|
47
|
+
return dc[locale.value] ?? dc.default ?? dc.eng ?? Object.values(dc)[0] ?? '';
|
|
48
|
+
}
|
|
49
|
+
|
|
45
50
|
function memberLabel(member: GenericRelationWire['members'][number]): PartitiveMemberLabeled {
|
|
46
51
|
return {
|
|
47
52
|
uri: member.uri,
|
|
48
53
|
presence: member.presence,
|
|
49
54
|
count: member.count,
|
|
50
|
-
isDelimiting:
|
|
55
|
+
isDelimiting: false,
|
|
51
56
|
label: designationFor(member.uri),
|
|
52
57
|
};
|
|
53
58
|
}
|
|
@@ -78,12 +83,15 @@ function memberLabel(member: GenericRelationWire['members'][number]): PartitiveM
|
|
|
78
83
|
:completeness="rel.completeness"
|
|
79
84
|
/>
|
|
80
85
|
|
|
81
|
-
<ul class="member-presence-list" aria-label="Member
|
|
86
|
+
<ul class="member-presence-list" aria-label="Member delimiting characteristics">
|
|
82
87
|
<li v-for="(m, j) in rel.members" :key="j">
|
|
83
88
|
<span class="member-label">{{ designationFor(m.uri) }}</span>
|
|
89
|
+
<span
|
|
90
|
+
v-if="m.delimitingCharacteristic"
|
|
91
|
+
class="member-delimiting-characteristic"
|
|
92
|
+
>{{ characteristicText(m.delimitingCharacteristic) }}</span>
|
|
84
93
|
<span v-if="m.presence && m.presence !== 'required'" class="member-presence">{{ presenceLabel(m.presence) }}</span>
|
|
85
94
|
<span v-if="m.count && m.count !== 'exactly_one'" class="member-count">{{ countLabel(m.count) }}</span>
|
|
86
|
-
<span v-if="m.isDelimiting" class="member-delimiting">{{ t('relations.delimiting') }}</span>
|
|
87
95
|
</li>
|
|
88
96
|
</ul>
|
|
89
97
|
</div>
|
|
@@ -171,18 +171,24 @@ export function useConceptEdges(
|
|
|
171
171
|
const source = conceptUriValue.value;
|
|
172
172
|
const comprehensive = resolveConceptRefUri(rel.comprehensive);
|
|
173
173
|
if (!comprehensive) return null;
|
|
174
|
-
const
|
|
175
|
-
|
|
174
|
+
const isGeneric = rel instanceof GenericHyperedge;
|
|
175
|
+
const members: any[] = (rel.members ?? rel.partitives ?? [])
|
|
176
|
+
.map((m: any): any | null => {
|
|
176
177
|
const uri = resolveConceptRefUri(m.ref);
|
|
177
178
|
if (!uri || uri === source) return null;
|
|
178
|
-
|
|
179
|
+
const base: any = {
|
|
179
180
|
uri,
|
|
180
181
|
presence: readPresence(m),
|
|
181
182
|
count: readCount(m),
|
|
182
|
-
isDelimiting: readIsDelimiting(m),
|
|
183
183
|
};
|
|
184
|
+
if (isGeneric) {
|
|
185
|
+
base.delimitingCharacteristic = m.delimitingCharacteristic;
|
|
186
|
+
} else {
|
|
187
|
+
base.isDelimiting = readIsDelimiting(m);
|
|
188
|
+
}
|
|
189
|
+
return base;
|
|
184
190
|
})
|
|
185
|
-
.filter((m:
|
|
191
|
+
.filter((m: any | null): m is any => m !== null);
|
|
186
192
|
if (members.length === 0) return null;
|
|
187
193
|
return {
|
|
188
194
|
source,
|
|
@@ -89,7 +89,7 @@ const partitiveRelationsForSphere = computed<PartitiveRelationWire[]>(() => {
|
|
|
89
89
|
if (uri === sourceUri) return null;
|
|
90
90
|
const presence: PartitivePresence = member.presence;
|
|
91
91
|
const count: PartitiveCount = member.count;
|
|
92
|
-
const isDelimiting = member.
|
|
92
|
+
const isDelimiting = (member as any).is_delimiting === true;
|
|
93
93
|
return { uri, presence, count, isDelimiting };
|
|
94
94
|
})
|
|
95
95
|
.filter((x): x is PartitiveMemberWire => x !== null);
|