@asd20/ui-next 2.35.4 → 2.36.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/CHANGELOG.md +7 -0
- package/helpers.js +1 -0
- package/package.json +1 -1
- package/src/components/atoms/Asd20RichBodyContent/index.vue +15 -1
- package/src/components/atoms/Asd20RichBodyContent/messageDocumentRenderer.js +12 -1
- package/src/components/templates/Asd20HandbookTemplate/index.vue +21 -3
- package/src/helpers/liveMessageData.js +164 -0
- package/src/helpers/liveMessageDataRuntime.js +57 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# [2.36.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.35.4...ui-next-v2.36.0) (2026-11-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* resolve public live message fields and team rosters ([09beb75](https://github.com/academydistrict20/asd20-ui-next/commit/09beb75f45ef323a65ecf718a3e21b71c3807869))
|
|
9
|
+
|
|
3
10
|
## [2.35.4](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.35.3...ui-next-v2.35.4) (2026-11-09)
|
|
4
11
|
|
|
5
12
|
|
package/helpers.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as arraySearch } from './src/helpers/arraySearch.js'
|
|
2
|
+
export { LIVE_DATA_VERSION, LIVE_DATA_FIELDS, collectLiveDataReferences, resolveLiveDataDocument, fetchLiveDataSnapshot } from './src/helpers/liveMessageData.js'
|
|
2
3
|
export { buildPublicationIntroduction } from './src/helpers/publicationIntroduction.js'
|
|
3
4
|
export { prepareDocumentPrint, printDocument, installDocumentPrintLifecycle } from './src/helpers/documentPrint.js'
|
|
4
5
|
export { default as buildFileSearchUrl } from './src/helpers/buildFileSearchUrl.js'
|
package/package.json
CHANGED
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
:data-message-document-status="renderModel.status"
|
|
6
6
|
@click="onContentClick"
|
|
7
7
|
>
|
|
8
|
+
<p
|
|
9
|
+
v-if="liveDataError && !liveDataProvider"
|
|
10
|
+
role="status"
|
|
11
|
+
>
|
|
12
|
+
{{ liveDataError }} <button
|
|
13
|
+
type="button"
|
|
14
|
+
@click="refreshLiveMessageData"
|
|
15
|
+
>
|
|
16
|
+
Retry
|
|
17
|
+
</button>
|
|
18
|
+
</p>
|
|
8
19
|
<template v-for="(node, index) in renderNodes">
|
|
9
20
|
<div
|
|
10
21
|
v-if="node.type === 'html' && node.html"
|
|
@@ -50,11 +61,13 @@ import {
|
|
|
50
61
|
containsBodyBlockPlaceholder,
|
|
51
62
|
} from './messageDocumentRenderer'
|
|
52
63
|
import { publicationHeadingPlanKey } from './headingContext'
|
|
64
|
+
import { liveMessageDataMixin } from '../../../helpers/liveMessageDataRuntime'
|
|
53
65
|
|
|
54
66
|
export default {
|
|
55
67
|
name: 'Asd20RichBodyContent',
|
|
56
68
|
|
|
57
69
|
components: { Asd20BodyAccordion, Asd20TableauEmbed },
|
|
70
|
+
mixins: [liveMessageDataMixin],
|
|
58
71
|
|
|
59
72
|
inject: {
|
|
60
73
|
publicationHeadingPlan: { from: publicationHeadingPlanKey, default: null },
|
|
@@ -74,10 +87,11 @@ export default {
|
|
|
74
87
|
}),
|
|
75
88
|
|
|
76
89
|
computed: {
|
|
90
|
+
liveDataDocuments() { return [this.bodyJson] },
|
|
77
91
|
renderModel() {
|
|
78
92
|
return buildRichBodyRenderModel({
|
|
79
93
|
bodyContent: this.bodyContent,
|
|
80
|
-
bodyJson: this.bodyJson,
|
|
94
|
+
bodyJson: this.projectLiveMessageData(this.bodyJson),
|
|
81
95
|
headingContext: {
|
|
82
96
|
levelOffset: this.headingLevelOffset,
|
|
83
97
|
idPrefix: this.headingIdPrefix,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
contextualAccordionItemKey,
|
|
5
5
|
normalizeHeadingContext,
|
|
6
6
|
} from './headingContext.js'
|
|
7
|
+
import { validateLiveDataReference, liveDataLabel } from '../../../helpers/liveMessageData.js'
|
|
7
8
|
|
|
8
9
|
const MESSAGE_DOCUMENT_SCHEMA_VERSION = 1
|
|
9
10
|
|
|
@@ -24,8 +25,9 @@ export const MESSAGE_DOCUMENT_STATUS = Object.freeze({
|
|
|
24
25
|
const LEGACY_PLACEHOLDER_PATTERN =
|
|
25
26
|
/<div\b(?=[^>]*\bdata-cc-block=(['"])([^'"]+)\1)(?=[^>]*\bdata-block-id=(['"])([^'"]+)\3)[^>]*><\/div>/gi
|
|
26
27
|
|
|
27
|
-
const INLINE_NODE_TYPES = new Set(['text', 'hardBreak'])
|
|
28
|
+
const INLINE_NODE_TYPES = new Set(['text', 'hardBreak', 'liveDataField'])
|
|
28
29
|
const STANDARD_BLOCK_NODE_TYPES = new Set([
|
|
30
|
+
'liveDataBlock',
|
|
29
31
|
'paragraph',
|
|
30
32
|
'heading',
|
|
31
33
|
'blockquote',
|
|
@@ -94,6 +96,8 @@ const MARK_RENDER_ORDER = Object.freeze({
|
|
|
94
96
|
superscript: 6,
|
|
95
97
|
})
|
|
96
98
|
const NODE_ATTRIBUTE_NAMES = Object.freeze({
|
|
99
|
+
liveDataField: ['source', 'sourceId', 'field'],
|
|
100
|
+
liveDataBlock: ['source', 'sourceId', 'field'],
|
|
97
101
|
doc: ['schemaVersion'],
|
|
98
102
|
text: [],
|
|
99
103
|
hardBreak: [],
|
|
@@ -425,6 +429,9 @@ function validateNode(node, path) {
|
|
|
425
429
|
}
|
|
426
430
|
const attributeIssue = validateAttributeNames(node, path)
|
|
427
431
|
if (attributeIssue) return attributeIssue
|
|
432
|
+
if (node.type === 'liveDataField' || node.type === 'liveDataBlock') {
|
|
433
|
+
return validateLiveDataReference(node) ? null : issue('invalid-live-data', path)
|
|
434
|
+
}
|
|
428
435
|
|
|
429
436
|
if (node.type === 'doc') {
|
|
430
437
|
if (node.attrs?.schemaVersion !== MESSAGE_DOCUMENT_SCHEMA_VERSION) {
|
|
@@ -799,6 +806,10 @@ function renderImage(attrs, className = '') {
|
|
|
799
806
|
}
|
|
800
807
|
|
|
801
808
|
function renderStandardNode(node, context, path, accordionItemId) {
|
|
809
|
+
if (node.type === 'liveDataField' || node.type === 'liveDataBlock') {
|
|
810
|
+
const tag = node.type === 'liveDataField' ? 'span' : 'div'
|
|
811
|
+
return `<${tag} class="asd20-live-data-unavailable">${escapeHtml(liveDataLabel(node.attrs))} is currently unavailable.</${tag}>`
|
|
812
|
+
}
|
|
802
813
|
if (node.type === 'text') return renderMarkedText(node, context, path)
|
|
803
814
|
if (node.type === 'hardBreak') return '<br>'
|
|
804
815
|
if (node.type === 'paragraph') return `<p>${renderChildren(node, context, path, accordionItemId)}</p>`
|
|
@@ -146,6 +146,17 @@
|
|
|
146
146
|
aria-label="Handbook topics"
|
|
147
147
|
>
|
|
148
148
|
<slot name="sidebar-status" />
|
|
149
|
+
<p
|
|
150
|
+
v-if="liveDataError && !liveDataManaged"
|
|
151
|
+
role="status"
|
|
152
|
+
>
|
|
153
|
+
{{ liveDataError }} <button
|
|
154
|
+
type="button"
|
|
155
|
+
@click="refreshLiveMessageData"
|
|
156
|
+
>
|
|
157
|
+
Retry
|
|
158
|
+
</button>
|
|
159
|
+
</p>
|
|
149
160
|
<button
|
|
150
161
|
class="asd20-template-handbook__index-toggle"
|
|
151
162
|
type="button"
|
|
@@ -287,6 +298,7 @@ import { installDocumentPrintLifecycle } from '../../../helpers/documentPrint'
|
|
|
287
298
|
import { shouldHandleClientRoutingEvent } from '../../../helpers/linkPolicy'
|
|
288
299
|
import pageTemplateMixin from '../../../mixins/pageTemplateMixin'
|
|
289
300
|
import { publicationHeadingPlanKey } from '../../atoms/Asd20RichBodyContent/headingContext'
|
|
301
|
+
import { liveMessageDataMixin, liveMessageDataProviderKey } from '../../../helpers/liveMessageDataRuntime'
|
|
290
302
|
|
|
291
303
|
export default {
|
|
292
304
|
name: 'Asd20HandbookTemplate',
|
|
@@ -303,11 +315,15 @@ export default {
|
|
|
303
315
|
Asd20PrintHeader,
|
|
304
316
|
Asd20PrintContents,
|
|
305
317
|
},
|
|
306
|
-
mixins: [pageTemplateMixin],
|
|
318
|
+
mixins: [pageTemplateMixin, liveMessageDataMixin],
|
|
307
319
|
provide() {
|
|
308
|
-
return {
|
|
320
|
+
return {
|
|
321
|
+
[publicationHeadingPlanKey]: () => this.publicationHeadings.idPlan,
|
|
322
|
+
[liveMessageDataProviderKey]: () => ({ snapshot: this.liveDataSnapshot }),
|
|
323
|
+
}
|
|
309
324
|
},
|
|
310
325
|
props: {
|
|
326
|
+
liveDataManaged: { type: Boolean, default: false },
|
|
311
327
|
documentPrint: { type: Boolean, default: false },
|
|
312
328
|
contentLanguageCode: { type: String, default: '' },
|
|
313
329
|
contentDirection: { type: String, default: 'ltr' },
|
|
@@ -325,9 +341,11 @@ export default {
|
|
|
325
341
|
fragmentEpoch: 0,
|
|
326
342
|
}),
|
|
327
343
|
computed: {
|
|
344
|
+
liveDataDocuments() { return this.messages.map(message => message.bodyJson || '') },
|
|
345
|
+
liveDataMessages() { return this.messages.map(message => ({ ...message, bodyJson: this.projectLiveMessageData(message.bodyJson) })) },
|
|
328
346
|
handbookDocument() {
|
|
329
347
|
return buildHandbookDocument({
|
|
330
|
-
messages: this.
|
|
348
|
+
messages: this.liveDataMessages, metaDescription: this.metaDescription,
|
|
331
349
|
documentId: this.publicationDocumentId, documentUrl: this.handbookDocumentUrl,
|
|
332
350
|
reservedIds: this.publicationReservedIds,
|
|
333
351
|
})
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Generated from CommCenter packages/message-document/liveData.js.
|
|
2
|
+
/* global globalThis */
|
|
3
|
+
|
|
4
|
+
// Canonical public live-data contract. The shared UI's copy is generated by
|
|
5
|
+
// scripts/sync-live-message-data.mjs; never add arbitrary paths or queries here.
|
|
6
|
+
const LIVE_DATA_VERSION = 1;
|
|
7
|
+
const LIVE_DATA_FIELDS = Object.freeze({
|
|
8
|
+
organization: Object.freeze({
|
|
9
|
+
title: "School name",
|
|
10
|
+
address: "School address",
|
|
11
|
+
phone: "School main phone",
|
|
12
|
+
hours: "School office hours",
|
|
13
|
+
administrators: "School administrators",
|
|
14
|
+
}),
|
|
15
|
+
team: Object.freeze({
|
|
16
|
+
contactPhone: "Team contact phone",
|
|
17
|
+
officeHours: "Team office hours",
|
|
18
|
+
staff: "Team staff list",
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22
|
+
const LIVE_DATA_NODE_TYPES = new Set(["liveDataField", "liveDataBlock"]);
|
|
23
|
+
|
|
24
|
+
function validateLiveDataReference(node) {
|
|
25
|
+
const attrs = node?.attrs;
|
|
26
|
+
if (!LIVE_DATA_NODE_TYPES.has(node?.type) || !attrs ||
|
|
27
|
+
Object.keys(attrs).some(key => !["source", "sourceId", "field"].includes(key)) ||
|
|
28
|
+
typeof attrs.source !== "string" || typeof attrs.field !== "string" ||
|
|
29
|
+
!Object.hasOwn(LIVE_DATA_FIELDS, attrs.source) ||
|
|
30
|
+
!Object.hasOwn(LIVE_DATA_FIELDS[attrs.source], attrs.field) ||
|
|
31
|
+
typeof attrs.sourceId !== "string" || !UUID.test(attrs.sourceId) || node.content?.length || node.marks?.length) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const block = ["staff", "administrators"].includes(attrs.field);
|
|
35
|
+
return node.type === (block ? "liveDataBlock" : "liveDataField");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function liveDataLabel(attrs) {
|
|
39
|
+
return LIVE_DATA_FIELDS[attrs?.source]?.[attrs?.field] || "Live information";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function liveDataKey(attrs) {
|
|
43
|
+
return `${attrs.source}:${attrs.sourceId.toLowerCase()}:${attrs.field}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function collectLiveDataReferences(bodyJson) {
|
|
47
|
+
let value = bodyJson;
|
|
48
|
+
if (typeof bodyJson === "string") {
|
|
49
|
+
try { value = JSON.parse(bodyJson || "null"); }
|
|
50
|
+
catch { return []; } // Leave legacy/invalid JSON to the existing dual reader.
|
|
51
|
+
}
|
|
52
|
+
if (value?.type !== "doc") return [];
|
|
53
|
+
const references = new Map();
|
|
54
|
+
let count = 0;
|
|
55
|
+
function visit(node, depth = 0) {
|
|
56
|
+
if (++count > 50000 || depth > 100) throw new Error("Message data exceeds document limits.");
|
|
57
|
+
if (!node || typeof node !== "object") return;
|
|
58
|
+
if (LIVE_DATA_NODE_TYPES.has(node.type)) {
|
|
59
|
+
if (!validateLiveDataReference(node)) throw new Error("Invalid live data reference.");
|
|
60
|
+
references.set(liveDataKey(node.attrs), { ...node.attrs });
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(node.content)) node.content.forEach(child => visit(child, depth + 1));
|
|
63
|
+
}
|
|
64
|
+
visit(value);
|
|
65
|
+
if (references.size > 100) throw new Error("Too many live data references.");
|
|
66
|
+
return [...references.values()];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const dataText = value => typeof value === "string" ? value.trim().slice(0, 12000) : "";
|
|
70
|
+
const textNode = text => ({ type: "text", text });
|
|
71
|
+
const paragraph = text => ({ type: "paragraph", content: [textNode(text)] });
|
|
72
|
+
|
|
73
|
+
function liveDataContent(attrs, snapshot) {
|
|
74
|
+
const collection = attrs.source === "organization" ? snapshot?.organizations : snapshot?.teams;
|
|
75
|
+
const source = collection?.find(item => item.id?.toLowerCase() === attrs.sourceId.toLowerCase());
|
|
76
|
+
if (!source || (attrs.source === "organization" && source.status !== "Published") ||
|
|
77
|
+
(attrs.source === "team" && (source.isPublic !== true || source.ownerOrganization?.status !== "Published"))) return null;
|
|
78
|
+
if (attrs.field === "address") {
|
|
79
|
+
const address = source.address || {};
|
|
80
|
+
const value = [dataText(address.street1),
|
|
81
|
+
[dataText(address.city), dataText(address.state)].filter(Boolean).join(", "),
|
|
82
|
+
dataText(address.zip)].filter(Boolean).join(" · ");
|
|
83
|
+
return value ? [textNode(value)] : null;
|
|
84
|
+
}
|
|
85
|
+
if (["staff", "administrators"].includes(attrs.field)) {
|
|
86
|
+
const people = attrs.field === "staff"
|
|
87
|
+
? (source.people || []).map(item => item.person).filter(person => person?.isPublic === true)
|
|
88
|
+
: source.administrators;
|
|
89
|
+
if (!Array.isArray(people) || people.length > 100) return null;
|
|
90
|
+
const unique = new Map();
|
|
91
|
+
people.forEach(person => {
|
|
92
|
+
const name = dataText(person?.displayName) || [dataText(person?.firstName), dataText(person?.lastName)].filter(Boolean).join(" ");
|
|
93
|
+
if (!name) return;
|
|
94
|
+
const line = [name, dataText(person.title), dataText(person.phone)].filter(Boolean).join(" — ");
|
|
95
|
+
unique.set(person.id || line, line);
|
|
96
|
+
});
|
|
97
|
+
const lines = [...unique.values()].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
98
|
+
return lines.length ? [{ type: "bulletList", content: lines.map(line => ({ type: "listItem", content: [paragraph(line)] })) }] : null;
|
|
99
|
+
}
|
|
100
|
+
const value = dataText(source[attrs.field]);
|
|
101
|
+
return value ? [textNode(value)] : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Materialization returns a copy containing only ordinary document nodes.
|
|
105
|
+
// Persist the authored references, never this presentation projection.
|
|
106
|
+
function resolveLiveDataDocument(bodyJson, snapshot, { strict = true } = {}) {
|
|
107
|
+
collectLiveDataReferences(bodyJson);
|
|
108
|
+
const value = typeof bodyJson === "string" ? JSON.parse(bodyJson || "null") : bodyJson;
|
|
109
|
+
const diagnostics = [];
|
|
110
|
+
function resolve(node) {
|
|
111
|
+
if (LIVE_DATA_NODE_TYPES.has(node?.type)) {
|
|
112
|
+
const content = liveDataContent(node.attrs, snapshot);
|
|
113
|
+
if (content) return content;
|
|
114
|
+
diagnostics.push({ code: "live-data-unavailable", reference: liveDataKey(node.attrs) });
|
|
115
|
+
const message = `${liveDataLabel(node.attrs)} is currently unavailable.`;
|
|
116
|
+
return node.type === "liveDataBlock" ? [paragraph(message)] : [textNode(message)];
|
|
117
|
+
}
|
|
118
|
+
if (!node || typeof node !== "object") return [node];
|
|
119
|
+
return [{ ...node, ...(node.attrs ? { attrs: { ...node.attrs } } : {}),
|
|
120
|
+
...(Array.isArray(node.content) ? { content: node.content.flatMap(resolve) } : {}) }];
|
|
121
|
+
}
|
|
122
|
+
const document = resolve(value)[0];
|
|
123
|
+
if (strict && diagnostics.length) throw new Error("Required live information is unavailable. Review the message data sources.");
|
|
124
|
+
return { document, diagnostics };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// One anonymous query captures every dependency for a document batch. No admin
|
|
128
|
+
// secret, user session, arbitrary field names, or private employee data is used.
|
|
129
|
+
async function fetchLiveDataSnapshot(documents, { endpoint, fetchImpl = globalThis.fetch, signal } = {}) {
|
|
130
|
+
const references = new Map();
|
|
131
|
+
documents.forEach(document => collectLiveDataReferences(document).forEach(attrs => references.set(liveDataKey(attrs), attrs)));
|
|
132
|
+
const organizationIds = [...new Set([...references.values()].filter(ref => ref.source === "organization").map(ref => ref.sourceId.toLowerCase()))].sort();
|
|
133
|
+
const teamIds = [...new Set([...references.values()].filter(ref => ref.source === "team").map(ref => ref.sourceId.toLowerCase()))].sort();
|
|
134
|
+
if (organizationIds.length + teamIds.length > 50 || references.size > 100) throw new Error("Too many live data sources.");
|
|
135
|
+
if (!references.size) return { version: LIVE_DATA_VERSION, organizations: [], teams: [] };
|
|
136
|
+
if (!endpoint) throw new Error("Public message data is not configured.");
|
|
137
|
+
const response = await fetchImpl(endpoint, {
|
|
138
|
+
method: "POST", credentials: "omit", redirect: "error", cache: "no-store", signal,
|
|
139
|
+
headers: { "Content-Type": "application/json", "Cache-Control": "no-cache" },
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
query: `query liveMessageData($organizationIds: [uuid!]!, $teamIds: [uuid!]!) {
|
|
142
|
+
organizations(where: {id: {_in: $organizationIds}, status: {_eq: "Published"}}) {
|
|
143
|
+
id status title address phone hours administrators
|
|
144
|
+
}
|
|
145
|
+
teams: groups(where: {id: {_in: $teamIds}, isPublic: {_eq: true}, ownerOrganization: {status: {_eq: "Published"}}}) {
|
|
146
|
+
id title isPublic contactPhone officeHours ownerOrganization { status }
|
|
147
|
+
people(where: {person: {isPublic: {_eq: true}}}, order_by: {person: {id: asc}}, limit: 101) {
|
|
148
|
+
person { id displayName firstName lastName title phone isPublic }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}`, variables: { organizationIds, teamIds },
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
if (!response.ok) throw new Error("Public message data is unavailable.");
|
|
155
|
+
const result = await response.json();
|
|
156
|
+
if (result.errors || !Array.isArray(result.data?.organizations) || !Array.isArray(result.data?.teams)) throw new Error("Public message data is unavailable.");
|
|
157
|
+
return { version: LIVE_DATA_VERSION, organizations: result.data.organizations, teams: result.data.teams };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export {
|
|
161
|
+
LIVE_DATA_VERSION, LIVE_DATA_FIELDS, LIVE_DATA_NODE_TYPES,
|
|
162
|
+
validateLiveDataReference, liveDataLabel, liveDataKey,
|
|
163
|
+
collectLiveDataReferences, resolveLiveDataDocument, fetchLiveDataSnapshot,
|
|
164
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { collectLiveDataReferences, fetchLiveDataSnapshot, resolveLiveDataDocument } from './liveMessageData'
|
|
2
|
+
import { resolveRuntimeConfigValue } from './runtimeConfig'
|
|
3
|
+
|
|
4
|
+
export const liveMessageDataEndpointKey = 'liveMessageDataEndpoint'
|
|
5
|
+
export const liveMessageDataProviderKey = 'liveMessageDataProvider'
|
|
6
|
+
|
|
7
|
+
// The host supplies liveDataDocuments and may provide a batch snapshot to its
|
|
8
|
+
// descendants. Only references trigger a request; ordinary messages are inert.
|
|
9
|
+
export const liveMessageDataMixin = {
|
|
10
|
+
inject: {
|
|
11
|
+
liveDataEndpoint: { from: liveMessageDataEndpointKey, default: '' },
|
|
12
|
+
liveDataProvider: { from: liveMessageDataProviderKey, default: null },
|
|
13
|
+
},
|
|
14
|
+
data: () => ({ liveDataSnapshot: null, liveDataLoading: false, liveDataError: '', liveDataEpoch: 0, liveDataMounted: false, liveDataController: null }),
|
|
15
|
+
computed: {
|
|
16
|
+
liveDataReferencesKey() {
|
|
17
|
+
try { return JSON.stringify(this.liveDataDocuments.flatMap(doc => collectLiveDataReferences(doc))) }
|
|
18
|
+
catch { return '' }
|
|
19
|
+
},
|
|
20
|
+
effectiveLiveDataSnapshot() { return this.liveDataProvider ? this.liveDataProvider().snapshot : this.liveDataSnapshot },
|
|
21
|
+
},
|
|
22
|
+
watch: { liveDataReferencesKey() { if (this.liveDataMounted) this.refreshLiveMessageData() } },
|
|
23
|
+
mounted() { this.liveDataMounted = true; this.refreshLiveMessageData() },
|
|
24
|
+
beforeUnmount() { this.liveDataMounted = false; this.liveDataEpoch++; this.liveDataController?.abort() },
|
|
25
|
+
methods: {
|
|
26
|
+
async refreshLiveMessageData() {
|
|
27
|
+
const epoch = ++this.liveDataEpoch
|
|
28
|
+
this.liveDataController?.abort()
|
|
29
|
+
this.liveDataSnapshot = null
|
|
30
|
+
this.liveDataError = ''
|
|
31
|
+
this.liveDataLoading = false
|
|
32
|
+
if (this.liveDataProvider || this.liveDataManaged || !this.liveDataReferencesKey || this.liveDataReferencesKey === '[]') return
|
|
33
|
+
const controller = new AbortController()
|
|
34
|
+
this.liveDataController = controller
|
|
35
|
+
const timeout = setTimeout(() => controller.abort(), 15000)
|
|
36
|
+
this.liveDataLoading = true
|
|
37
|
+
try {
|
|
38
|
+
const snapshot = await fetchLiveDataSnapshot(this.liveDataDocuments, {
|
|
39
|
+
endpoint: this.liveDataEndpoint || resolveRuntimeConfigValue({ runtimeConfig: this.$config, runtimeConfigKeys: ['graphqlEndpoint'] }),
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
})
|
|
42
|
+
// Validate availability before accepting a partial capture.
|
|
43
|
+
this.liveDataDocuments.forEach(doc => {
|
|
44
|
+
if (collectLiveDataReferences(doc).length) resolveLiveDataDocument(doc, snapshot)
|
|
45
|
+
})
|
|
46
|
+
if (epoch === this.liveDataEpoch) this.liveDataSnapshot = snapshot
|
|
47
|
+
} catch { if (epoch === this.liveDataEpoch) this.liveDataError = 'Live information is currently unavailable.' }
|
|
48
|
+
finally { clearTimeout(timeout); if (epoch === this.liveDataEpoch) this.liveDataLoading = false }
|
|
49
|
+
},
|
|
50
|
+
projectLiveMessageData(bodyJson) {
|
|
51
|
+
try {
|
|
52
|
+
if (!collectLiveDataReferences(bodyJson).length) return bodyJson
|
|
53
|
+
return resolveLiveDataDocument(bodyJson, this.effectiveLiveDataSnapshot, { strict: false }).document
|
|
54
|
+
} catch { return bodyJson }
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}
|