@asd20/ui-next 2.36.0 → 2.38.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 +20 -0
- package/package.json +1 -1
- package/src/components/atoms/Asd20RichBodyContent/index.vue +2 -1
- package/src/components/atoms/Asd20RichBodyContent/messageDocumentRenderer.js +4 -5
- package/src/components/templates/Asd20HandbookTemplate/index.vue +3 -2
- package/src/helpers/liveMessageData.js +123 -42
- package/src/helpers/liveMessageDataRuntime.js +12 -4
- package/src/mixins/pageTemplateMixin.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# [2.38.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.37.0...ui-next-v2.38.0) (2026-11-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add organization email and omit missing public data ([8b1b8a7](https://github.com/academydistrict20/asd20-ui-next/commit/8b1b8a79a6b6ea04b8925cce517d022c839a1955))
|
|
9
|
+
|
|
10
|
+
# [2.37.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.36.0...ui-next-v2.37.0) (2026-11-10)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* use generic organization labels for message data ([c4c7c5a](https://github.com/academydistrict20/asd20-ui-next/commit/c4c7c5ac0d5857966cfaccabb9d5f8c0fb7ed99a))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* resolve shared message data in the current school context ([808af89](https://github.com/academydistrict20/asd20-ui-next/commit/808af89ac9e8d01395aafc770c02c9e9fbeda5e0))
|
|
21
|
+
* support configurable live Team rosters ([bc32c32](https://github.com/academydistrict20/asd20-ui-next/commit/bc32c325f4dcfd5ea13856ce90da081e093de95e))
|
|
22
|
+
|
|
3
23
|
# [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
24
|
|
|
5
25
|
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
@click="onContentClick"
|
|
7
7
|
>
|
|
8
8
|
<p
|
|
9
|
-
v-if="liveDataError && !liveDataProvider"
|
|
9
|
+
v-if="showLiveDataDiagnostics && liveDataError && !liveDataProvider"
|
|
10
10
|
role="status"
|
|
11
11
|
>
|
|
12
12
|
{{ liveDataError }} <button
|
|
@@ -76,6 +76,7 @@ export default {
|
|
|
76
76
|
props: {
|
|
77
77
|
bodyContent: { type: String, default: '' },
|
|
78
78
|
bodyJson: { type: [String, Object], default: '' },
|
|
79
|
+
organizationId: { type: String, default: '' },
|
|
79
80
|
headingLevelOffset: { type: Number, default: 0 },
|
|
80
81
|
headingIdPrefix: { type: String, default: '' },
|
|
81
82
|
},
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
contextualAccordionItemKey,
|
|
5
5
|
normalizeHeadingContext,
|
|
6
6
|
} from './headingContext.js'
|
|
7
|
-
import { validateLiveDataReference
|
|
7
|
+
import { validateLiveDataReference } from '../../../helpers/liveMessageData.js'
|
|
8
8
|
|
|
9
9
|
const MESSAGE_DOCUMENT_SCHEMA_VERSION = 1
|
|
10
10
|
|
|
@@ -96,8 +96,8 @@ const MARK_RENDER_ORDER = Object.freeze({
|
|
|
96
96
|
superscript: 6,
|
|
97
97
|
})
|
|
98
98
|
const NODE_ATTRIBUTE_NAMES = Object.freeze({
|
|
99
|
-
liveDataField: ['source', 'sourceId', 'field'],
|
|
100
|
-
liveDataBlock: ['source', 'sourceId', 'field'],
|
|
99
|
+
liveDataField: ['source', 'sourceId', 'field', 'binding', 'teamRole'],
|
|
100
|
+
liveDataBlock: ['source', 'sourceId', 'field', 'binding', 'teamRole', 'memberFields', 'memberOrder', 'memberSort'],
|
|
101
101
|
doc: ['schemaVersion'],
|
|
102
102
|
text: [],
|
|
103
103
|
hardBreak: [],
|
|
@@ -807,8 +807,7 @@ function renderImage(attrs, className = '') {
|
|
|
807
807
|
|
|
808
808
|
function renderStandardNode(node, context, path, accordionItemId) {
|
|
809
809
|
if (node.type === 'liveDataField' || node.type === 'liveDataBlock') {
|
|
810
|
-
|
|
811
|
-
return `<${tag} class="asd20-live-data-unavailable">${escapeHtml(liveDataLabel(node.attrs))} is currently unavailable.</${tag}>`
|
|
810
|
+
return ''
|
|
812
811
|
}
|
|
813
812
|
if (node.type === 'text') return renderMarkedText(node, context, path)
|
|
814
813
|
if (node.type === 'hardBreak') return '<br>'
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
>
|
|
148
148
|
<slot name="sidebar-status" />
|
|
149
149
|
<p
|
|
150
|
-
v-if="liveDataError && !liveDataManaged"
|
|
150
|
+
v-if="showLiveDataDiagnostics && liveDataError && !liveDataManaged"
|
|
151
151
|
role="status"
|
|
152
152
|
>
|
|
153
153
|
{{ liveDataError }} <button
|
|
@@ -298,7 +298,7 @@ import { installDocumentPrintLifecycle } from '../../../helpers/documentPrint'
|
|
|
298
298
|
import { shouldHandleClientRoutingEvent } from '../../../helpers/linkPolicy'
|
|
299
299
|
import pageTemplateMixin from '../../../mixins/pageTemplateMixin'
|
|
300
300
|
import { publicationHeadingPlanKey } from '../../atoms/Asd20RichBodyContent/headingContext'
|
|
301
|
-
import { liveMessageDataMixin, liveMessageDataProviderKey } from '../../../helpers/liveMessageDataRuntime'
|
|
301
|
+
import { liveMessageDataMixin, liveMessageDataProviderKey, liveMessageDataOrganizationKey } from '../../../helpers/liveMessageDataRuntime'
|
|
302
302
|
|
|
303
303
|
export default {
|
|
304
304
|
name: 'Asd20HandbookTemplate',
|
|
@@ -320,6 +320,7 @@ export default {
|
|
|
320
320
|
return {
|
|
321
321
|
[publicationHeadingPlanKey]: () => this.publicationHeadings.idPlan,
|
|
322
322
|
[liveMessageDataProviderKey]: () => ({ snapshot: this.liveDataSnapshot }),
|
|
323
|
+
[liveMessageDataOrganizationKey]: () => this.liveDataContextOrganizationId,
|
|
323
324
|
}
|
|
324
325
|
},
|
|
325
326
|
props: {
|
|
@@ -3,19 +3,22 @@
|
|
|
3
3
|
|
|
4
4
|
// Canonical public live-data contract. The shared UI's copy is generated by
|
|
5
5
|
// scripts/sync-live-message-data.mjs; never add arbitrary paths or queries here.
|
|
6
|
-
const LIVE_DATA_VERSION =
|
|
6
|
+
const LIVE_DATA_VERSION = 4;
|
|
7
|
+
const ADMINISTRATION_TEAM_TAG = "asd20:handbook:administration";
|
|
8
|
+
const LIVE_DATA_MEMBER_FIELDS = Object.freeze({ name: "Name", title: "Title", phone: "Phone" });
|
|
7
9
|
const LIVE_DATA_FIELDS = Object.freeze({
|
|
8
10
|
organization: Object.freeze({
|
|
9
|
-
title: "
|
|
10
|
-
address: "
|
|
11
|
-
phone: "
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
title: "Organization Name",
|
|
12
|
+
address: "Organization Address",
|
|
13
|
+
phone: "Organization Main Phone",
|
|
14
|
+
email: "Organization Email",
|
|
15
|
+
hours: "Organization Office Hours",
|
|
16
|
+
administrators: "Organization Administrators",
|
|
14
17
|
}),
|
|
15
18
|
team: Object.freeze({
|
|
16
|
-
contactPhone: "Team
|
|
17
|
-
officeHours: "Team
|
|
18
|
-
staff: "Team
|
|
19
|
+
contactPhone: "Team Contact Phone",
|
|
20
|
+
officeHours: "Team Office Hours",
|
|
21
|
+
staff: "Team Staff List",
|
|
19
22
|
}),
|
|
20
23
|
});
|
|
21
24
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -24,23 +27,41 @@ const LIVE_DATA_NODE_TYPES = new Set(["liveDataField", "liveDataBlock"]);
|
|
|
24
27
|
function validateLiveDataReference(node) {
|
|
25
28
|
const attrs = node?.attrs;
|
|
26
29
|
if (!LIVE_DATA_NODE_TYPES.has(node?.type) || !attrs ||
|
|
27
|
-
Object.keys(attrs).some(key => !["source", "sourceId", "field"].includes(key)) ||
|
|
30
|
+
Object.keys(attrs).some(key => !["source", "sourceId", "field", "binding", "teamRole", "memberFields", "memberOrder", "memberSort"].includes(key)) ||
|
|
28
31
|
typeof attrs.source !== "string" || typeof attrs.field !== "string" ||
|
|
29
32
|
!Object.hasOwn(LIVE_DATA_FIELDS, attrs.source) ||
|
|
30
33
|
!Object.hasOwn(LIVE_DATA_FIELDS[attrs.source], attrs.field) ||
|
|
31
|
-
|
|
34
|
+
node.content?.length || node.marks?.length) {
|
|
32
35
|
return false;
|
|
33
36
|
}
|
|
37
|
+
const binding = attrs.binding ?? "fixed";
|
|
38
|
+
if (!["fixed", "currentSchool"].includes(binding)) return false;
|
|
39
|
+
if (binding === "fixed") {
|
|
40
|
+
if (typeof attrs.sourceId !== "string" || !UUID.test(attrs.sourceId) || attrs.teamRole != null) return false;
|
|
41
|
+
} else if ((attrs.sourceId != null && attrs.sourceId !== "") ||
|
|
42
|
+
(attrs.source === "team" ? attrs.teamRole !== "administration" : attrs.teamRole != null)) return false;
|
|
43
|
+
if (attrs.memberFields != null && (!Array.isArray(attrs.memberFields) || !attrs.memberFields.length ||
|
|
44
|
+
attrs.memberFields.length > 3 || new Set(attrs.memberFields).size !== attrs.memberFields.length ||
|
|
45
|
+
attrs.memberFields.some(field => !Object.hasOwn(LIVE_DATA_MEMBER_FIELDS, field)))) return false;
|
|
46
|
+
if (attrs.memberOrder != null && (!Array.isArray(attrs.memberOrder) || attrs.memberOrder.length > 100 ||
|
|
47
|
+
attrs.memberOrder.some(id => typeof id !== "string" || !UUID.test(id)) ||
|
|
48
|
+
new Set(attrs.memberOrder.map(id => id.toLowerCase())).size !== attrs.memberOrder.length)) return false;
|
|
49
|
+
if (attrs.memberSort != null && !["alphabetical", "custom", "administration"].includes(attrs.memberSort)) return false;
|
|
50
|
+
if (attrs.memberOrder?.length && (binding === "currentSchool" || (attrs.memberSort != null && attrs.memberSort !== "custom"))) return false;
|
|
51
|
+
if (binding === "currentSchool" && attrs.memberSort === "custom") return false;
|
|
52
|
+
if ((attrs.memberFields != null || attrs.memberOrder != null || attrs.memberSort != null) && !(attrs.source === "team" && attrs.field === "staff")) return false;
|
|
34
53
|
const block = ["staff", "administrators"].includes(attrs.field);
|
|
35
54
|
return node.type === (block ? "liveDataBlock" : "liveDataField");
|
|
36
55
|
}
|
|
37
56
|
|
|
38
57
|
function liveDataLabel(attrs) {
|
|
39
|
-
|
|
58
|
+
const label = LIVE_DATA_FIELDS[attrs?.source]?.[attrs?.field] || "Live information";
|
|
59
|
+
return attrs?.binding === "currentSchool" ? `Current Organization · ${label}` : label;
|
|
40
60
|
}
|
|
41
61
|
|
|
42
62
|
function liveDataKey(attrs) {
|
|
43
|
-
|
|
63
|
+
const base = `${attrs.source}:${attrs.binding === "currentSchool" ? `currentSchool:${attrs.teamRole || ""}` : attrs.sourceId.toLowerCase()}:${attrs.field}`;
|
|
64
|
+
return attrs.field === "staff" ? `${base}:${JSON.stringify([attrs.memberFields ?? Object.keys(LIVE_DATA_MEMBER_FIELDS), (attrs.memberOrder || []).map(id => id.toLowerCase()), attrs.memberSort || (attrs.memberOrder?.length ? "custom" : "alphabetical")])}` : base;
|
|
44
65
|
}
|
|
45
66
|
|
|
46
67
|
function collectLiveDataReferences(bodyJson) {
|
|
@@ -70,11 +91,67 @@ const dataText = value => typeof value === "string" ? value.trim().slice(0, 1200
|
|
|
70
91
|
const textNode = text => ({ type: "text", text });
|
|
71
92
|
const paragraph = text => ({ type: "paragraph", content: [textNode(text)] });
|
|
72
93
|
|
|
73
|
-
function
|
|
94
|
+
function liveDataSource(attrs, snapshot, organizationId = snapshot?.contextOrganizationId) {
|
|
74
95
|
const collection = attrs.source === "organization" ? snapshot?.organizations : snapshot?.teams;
|
|
75
|
-
|
|
96
|
+
let source;
|
|
97
|
+
if (attrs.binding === "currentSchool") {
|
|
98
|
+
if (typeof organizationId !== "string" || !UUID.test(organizationId)) return null;
|
|
99
|
+
const id = organizationId.toLowerCase();
|
|
100
|
+
if (attrs.source === "organization") source = collection?.find(item => item.id?.toLowerCase() === id);
|
|
101
|
+
else {
|
|
102
|
+
const candidates = collection?.filter(item => item.ownerOrganizationId?.toLowerCase() === id && item.isPublic === true &&
|
|
103
|
+
item.ownerOrganization?.status === "Published" && Array.isArray(item.tags) && item.tags.includes(ADMINISTRATION_TEAM_TAG));
|
|
104
|
+
if (candidates?.length !== 1) return null;
|
|
105
|
+
source = candidates[0];
|
|
106
|
+
}
|
|
107
|
+
} else source = collection?.find(item => item.id?.toLowerCase() === attrs.sourceId.toLowerCase());
|
|
76
108
|
if (!source || (attrs.source === "organization" && source.status !== "Published") ||
|
|
77
109
|
(attrs.source === "team" && (source.isPublic !== true || source.ownerOrganization?.status !== "Published"))) return null;
|
|
110
|
+
return source;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Public roster values are presentation data, never authored attributes. Order
|
|
114
|
+
// uses stable person IDs; departed members disappear and new members sort last.
|
|
115
|
+
function administrationRoleRank(title) {
|
|
116
|
+
const parts = dataText(title).normalize("NFKC").toLowerCase().split(/[,;|/()–—]|\s+-\s+/)
|
|
117
|
+
.map(part => part.replace(/[^\p{L}\p{N}]+/gu, " ").trim());
|
|
118
|
+
// Match whole role segments: an assistant/secretary to the Principal is not
|
|
119
|
+
// the Principal, and Assistant Principal must never match Principal first.
|
|
120
|
+
const ranks = parts.map(part => {
|
|
121
|
+
part = part.replace(/^(?:(?:alternative|online|home|elementary|middle|high|junior|secondary|primary|school|k 8)\s+)+/, "");
|
|
122
|
+
if (/^(?:(?:elementary|middle|high|junior high) school )?(?:assistant|asst|associate|vice) principals?(?: (?:elementary|middle|high|junior high) school)?$/.test(part)) return 1;
|
|
123
|
+
if (/^(?:(?:elementary|middle|high|junior high) school )?principals?(?: (?:elementary|middle|high|junior high) school)?$/.test(part) || part === "head of school") return 0;
|
|
124
|
+
if (/^(?:school |student )?dean(?: of students| of student affairs)?$/.test(part)) return 2;
|
|
125
|
+
return 3;
|
|
126
|
+
});
|
|
127
|
+
return Math.min(...ranks, 3);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function liveDataMembers(attrs, snapshot, { organizationId } = {}) {
|
|
131
|
+
const source = liveDataSource(attrs, snapshot, organizationId);
|
|
132
|
+
if (!source) return [];
|
|
133
|
+
const entries = attrs.field === "staff" ? source.people : source.administrators;
|
|
134
|
+
if (!Array.isArray(entries) || entries.length > 100) return [];
|
|
135
|
+
const unique = new Map();
|
|
136
|
+
entries.forEach(entry => {
|
|
137
|
+
const person = attrs.field === "staff" ? entry?.person : entry;
|
|
138
|
+
if (!person || (attrs.field === "staff" && person.isPublic !== true)) return;
|
|
139
|
+
const name = dataText(person.displayName) || [dataText(person.firstName), dataText(person.lastName)].filter(Boolean).join(" ");
|
|
140
|
+
if (!name) return;
|
|
141
|
+
const member = { id: dataText(person.id).toLowerCase(), name, title: dataText(person.title), phone: dataText(person.phone) };
|
|
142
|
+
const key = member.id || JSON.stringify(member);
|
|
143
|
+
if (!unique.has(key)) unique.set(key, member);
|
|
144
|
+
});
|
|
145
|
+
const order = new Map((attrs.memberOrder || []).map((id, index) => [id.toLowerCase(), index]));
|
|
146
|
+
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
147
|
+
const rank = member => attrs.memberSort === "administration" ? administrationRoleRank(member.title) :
|
|
148
|
+
attrs.memberSort === "alphabetical" ? 0 : order.get(member.id) ?? Infinity;
|
|
149
|
+
return [...unique.values()].sort((a, b) => rank(a) - rank(b) || compare(a.name, b.name) || compare(a.id, b.id));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function liveDataContent(attrs, snapshot, organizationId) {
|
|
153
|
+
const source = liveDataSource(attrs, snapshot, organizationId);
|
|
154
|
+
if (!source) return null;
|
|
78
155
|
if (attrs.field === "address") {
|
|
79
156
|
const address = source.address || {};
|
|
80
157
|
const value = [dataText(address.street1),
|
|
@@ -83,18 +160,9 @@ function liveDataContent(attrs, snapshot) {
|
|
|
83
160
|
return value ? [textNode(value)] : null;
|
|
84
161
|
}
|
|
85
162
|
if (["staff", "administrators"].includes(attrs.field)) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
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);
|
|
163
|
+
const fields = attrs.memberFields ?? Object.keys(LIVE_DATA_MEMBER_FIELDS);
|
|
164
|
+
const lines = liveDataMembers(attrs, snapshot, { organizationId }).map(member => Object.keys(LIVE_DATA_MEMBER_FIELDS)
|
|
165
|
+
.filter(field => fields.includes(field)).map(field => member[field]).filter(Boolean).join(" — ")).filter(Boolean);
|
|
98
166
|
return lines.length ? [{ type: "bulletList", content: lines.map(line => ({ type: "listItem", content: [paragraph(line)] })) }] : null;
|
|
99
167
|
}
|
|
100
168
|
const value = dataText(source[attrs.field]);
|
|
@@ -103,34 +171,47 @@ function liveDataContent(attrs, snapshot) {
|
|
|
103
171
|
|
|
104
172
|
// Materialization returns a copy containing only ordinary document nodes.
|
|
105
173
|
// Persist the authored references, never this presentation projection.
|
|
106
|
-
function resolveLiveDataDocument(bodyJson, snapshot, { strict = true } = {}) {
|
|
174
|
+
function resolveLiveDataDocument(bodyJson, snapshot, { strict = true, organizationId, unavailable = "message" } = {}) {
|
|
107
175
|
collectLiveDataReferences(bodyJson);
|
|
108
176
|
const value = typeof bodyJson === "string" ? JSON.parse(bodyJson || "null") : bodyJson;
|
|
109
177
|
const diagnostics = [];
|
|
110
178
|
function resolve(node) {
|
|
111
179
|
if (LIVE_DATA_NODE_TYPES.has(node?.type)) {
|
|
112
|
-
const content = liveDataContent(node.attrs, snapshot);
|
|
180
|
+
const content = liveDataContent(node.attrs, snapshot, organizationId);
|
|
113
181
|
if (content) return content;
|
|
114
182
|
diagnostics.push({ code: "live-data-unavailable", reference: liveDataKey(node.attrs) });
|
|
183
|
+
if (unavailable === "omit") return [];
|
|
115
184
|
const message = `${liveDataLabel(node.attrs)} is currently unavailable.`;
|
|
116
185
|
return node.type === "liveDataBlock" ? [paragraph(message)] : [textNode(message)];
|
|
117
186
|
}
|
|
118
187
|
if (!node || typeof node !== "object") return [node];
|
|
188
|
+
const content = Array.isArray(node.content) ? node.content.flatMap(resolve) : undefined;
|
|
189
|
+
if (unavailable === "omit" && node.type === "heading" && content?.length === 0 &&
|
|
190
|
+
node.content.some(child => LIVE_DATA_NODE_TYPES.has(child.type))) return [];
|
|
119
191
|
return [{ ...node, ...(node.attrs ? { attrs: { ...node.attrs } } : {}),
|
|
120
|
-
...(
|
|
192
|
+
...(content ? { content } : {}) }];
|
|
121
193
|
}
|
|
122
194
|
const document = resolve(value)[0];
|
|
195
|
+
// Keep an all-missing projection renderable without falling back to authored
|
|
196
|
+
// HTML. This placeholder is presentation-only and never saved to the message.
|
|
197
|
+
if (unavailable === "omit" && diagnostics.length && document?.type === "doc" && document.content?.length === 0)
|
|
198
|
+
document.content = [{ type: "paragraph" }];
|
|
123
199
|
if (strict && diagnostics.length) throw new Error("Required live information is unavailable. Review the message data sources.");
|
|
124
200
|
return { document, diagnostics };
|
|
125
201
|
}
|
|
126
202
|
|
|
127
203
|
// One anonymous query captures every dependency for a document batch. No admin
|
|
128
204
|
// secret, user session, arbitrary field names, or private employee data is used.
|
|
129
|
-
async function fetchLiveDataSnapshot(documents, { endpoint, fetchImpl = globalThis.fetch, signal } = {}) {
|
|
205
|
+
async function fetchLiveDataSnapshot(documents, { endpoint, fetchImpl = globalThis.fetch, signal, organizationId } = {}) {
|
|
130
206
|
const references = new Map();
|
|
131
207
|
documents.forEach(document => collectLiveDataReferences(document).forEach(attrs => references.set(liveDataKey(attrs), attrs)));
|
|
132
|
-
const
|
|
133
|
-
|
|
208
|
+
const current = [...references.values()].filter(ref => ref.binding === "currentSchool");
|
|
209
|
+
if (current.length && (typeof organizationId !== "string" || !UUID.test(organizationId))) throw new Error("Choose an organization to preview current-organization information.");
|
|
210
|
+
const organizationIds = [...new Set([...references.values()].filter(ref => ref.source === "organization" && ref.binding !== "currentSchool").map(ref => ref.sourceId.toLowerCase()))];
|
|
211
|
+
if (current.length) organizationIds.push(organizationId.toLowerCase());
|
|
212
|
+
const uniqueOrganizationIds = [...new Set(organizationIds)].sort();
|
|
213
|
+
const teamIds = [...new Set([...references.values()].filter(ref => ref.source === "team" && ref.binding !== "currentSchool").map(ref => ref.sourceId.toLowerCase()))].sort();
|
|
214
|
+
const currentTeam = current.some(ref => ref.source === "team");
|
|
134
215
|
if (organizationIds.length + teamIds.length > 50 || references.size > 100) throw new Error("Too many live data sources.");
|
|
135
216
|
if (!references.size) return { version: LIVE_DATA_VERSION, organizations: [], teams: [] };
|
|
136
217
|
if (!endpoint) throw new Error("Public message data is not configured.");
|
|
@@ -138,27 +219,27 @@ async function fetchLiveDataSnapshot(documents, { endpoint, fetchImpl = globalTh
|
|
|
138
219
|
method: "POST", credentials: "omit", redirect: "error", cache: "no-store", signal,
|
|
139
220
|
headers: { "Content-Type": "application/json", "Cache-Control": "no-cache" },
|
|
140
221
|
body: JSON.stringify({
|
|
141
|
-
query: `query liveMessageData($organizationIds: [uuid!]!, $teamIds: [uuid!]!) {
|
|
222
|
+
query: `query liveMessageData($organizationIds: [uuid!]!, $teamIds: [uuid!]!${currentTeam ? ", $contextOrganizationIds: [uuid!]!" : ""}) {
|
|
142
223
|
organizations(where: {id: {_in: $organizationIds}, status: {_eq: "Published"}}) {
|
|
143
|
-
id status title address phone hours administrators
|
|
224
|
+
id status title address phone email hours administrators
|
|
144
225
|
}
|
|
145
|
-
teams: groups(where: {id: {_in: $teamIds}, isPublic: {_eq: true}, ownerOrganization: {status: {_eq: "Published"}}}) {
|
|
146
|
-
id title isPublic contactPhone officeHours ownerOrganization { status }
|
|
226
|
+
teams: groups(where: {${currentTeam ? `_or: [{id: {_in: $teamIds}}, {ownerOrganizationId: {_in: $contextOrganizationIds}, tags: {_contains: ["${ADMINISTRATION_TEAM_TAG}"]}}]` : "id: {_in: $teamIds}"}, isPublic: {_eq: true}, ownerOrganization: {status: {_eq: "Published"}}}, limit: 101) {
|
|
227
|
+
id title isPublic contactPhone officeHours ownerOrganizationId tags ownerOrganization { status }
|
|
147
228
|
people(where: {person: {isPublic: {_eq: true}}}, order_by: {person: {id: asc}}, limit: 101) {
|
|
148
229
|
person { id displayName firstName lastName title phone isPublic }
|
|
149
230
|
}
|
|
150
231
|
}
|
|
151
|
-
}`, variables: { organizationIds, teamIds },
|
|
232
|
+
}`, variables: { organizationIds: uniqueOrganizationIds, teamIds, ...(currentTeam ? { contextOrganizationIds: [organizationId.toLowerCase()] } : {}) },
|
|
152
233
|
}),
|
|
153
234
|
});
|
|
154
235
|
if (!response.ok) throw new Error("Public message data is unavailable.");
|
|
155
236
|
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 };
|
|
237
|
+
if (result.errors || !Array.isArray(result.data?.organizations) || !Array.isArray(result.data?.teams) || result.data.teams.length > 100) throw new Error("Public message data is unavailable.");
|
|
238
|
+
return { version: LIVE_DATA_VERSION, ...(current.length ? { contextOrganizationId: organizationId.toLowerCase() } : {}), organizations: result.data.organizations, teams: result.data.teams };
|
|
158
239
|
}
|
|
159
240
|
|
|
160
241
|
export {
|
|
161
|
-
LIVE_DATA_VERSION, LIVE_DATA_FIELDS, LIVE_DATA_NODE_TYPES,
|
|
242
|
+
LIVE_DATA_VERSION, LIVE_DATA_FIELDS, LIVE_DATA_NODE_TYPES, LIVE_DATA_MEMBER_FIELDS, ADMINISTRATION_TEAM_TAG,
|
|
162
243
|
validateLiveDataReference, liveDataLabel, liveDataKey,
|
|
163
|
-
collectLiveDataReferences, resolveLiveDataDocument, fetchLiveDataSnapshot,
|
|
244
|
+
collectLiveDataReferences, resolveLiveDataDocument, fetchLiveDataSnapshot, liveDataMembers, administrationRoleRank,
|
|
164
245
|
};
|
|
@@ -3,18 +3,25 @@ import { resolveRuntimeConfigValue } from './runtimeConfig'
|
|
|
3
3
|
|
|
4
4
|
export const liveMessageDataEndpointKey = 'liveMessageDataEndpoint'
|
|
5
5
|
export const liveMessageDataProviderKey = 'liveMessageDataProvider'
|
|
6
|
+
export const liveMessageDataOrganizationKey = 'liveMessageDataOrganization'
|
|
6
7
|
|
|
7
8
|
// The host supplies liveDataDocuments and may provide a batch snapshot to its
|
|
8
9
|
// descendants. Only references trigger a request; ordinary messages are inert.
|
|
9
10
|
export const liveMessageDataMixin = {
|
|
10
11
|
inject: {
|
|
12
|
+
showLiveDataDiagnostics: { from: 'liveMessageDataDiagnostics', default: false },
|
|
11
13
|
liveDataEndpoint: { from: liveMessageDataEndpointKey, default: '' },
|
|
12
14
|
liveDataProvider: { from: liveMessageDataProviderKey, default: null },
|
|
15
|
+
liveDataOrganizationProvider: { from: liveMessageDataOrganizationKey, default: null },
|
|
13
16
|
},
|
|
14
17
|
data: () => ({ liveDataSnapshot: null, liveDataLoading: false, liveDataError: '', liveDataEpoch: 0, liveDataMounted: false, liveDataController: null }),
|
|
15
18
|
computed: {
|
|
19
|
+
liveDataContextOrganizationId() { return this.organizationId || (this.liveDataOrganizationProvider ? this.liveDataOrganizationProvider() : this.organization?.id) || '' },
|
|
16
20
|
liveDataReferencesKey() {
|
|
17
|
-
try {
|
|
21
|
+
try {
|
|
22
|
+
const references = this.liveDataDocuments.flatMap(doc => collectLiveDataReferences(doc))
|
|
23
|
+
return references.length ? JSON.stringify([references, references.some(ref => ref.binding === 'currentSchool') ? this.liveDataContextOrganizationId : '']) : '[]'
|
|
24
|
+
}
|
|
18
25
|
catch { return '' }
|
|
19
26
|
},
|
|
20
27
|
effectiveLiveDataSnapshot() { return this.liveDataProvider ? this.liveDataProvider().snapshot : this.liveDataSnapshot },
|
|
@@ -38,10 +45,11 @@ export const liveMessageDataMixin = {
|
|
|
38
45
|
const snapshot = await fetchLiveDataSnapshot(this.liveDataDocuments, {
|
|
39
46
|
endpoint: this.liveDataEndpoint || resolveRuntimeConfigValue({ runtimeConfig: this.$config, runtimeConfigKeys: ['graphqlEndpoint'] }),
|
|
40
47
|
signal: controller.signal,
|
|
48
|
+
organizationId: this.liveDataContextOrganizationId,
|
|
41
49
|
})
|
|
42
|
-
// Validate
|
|
50
|
+
// Validate references while retaining available fields in a partial capture.
|
|
43
51
|
this.liveDataDocuments.forEach(doc => {
|
|
44
|
-
if (collectLiveDataReferences(doc).length) resolveLiveDataDocument(doc, snapshot)
|
|
52
|
+
if (collectLiveDataReferences(doc).length) resolveLiveDataDocument(doc, snapshot, { strict: false, organizationId: this.liveDataContextOrganizationId })
|
|
45
53
|
})
|
|
46
54
|
if (epoch === this.liveDataEpoch) this.liveDataSnapshot = snapshot
|
|
47
55
|
} catch { if (epoch === this.liveDataEpoch) this.liveDataError = 'Live information is currently unavailable.' }
|
|
@@ -50,7 +58,7 @@ export const liveMessageDataMixin = {
|
|
|
50
58
|
projectLiveMessageData(bodyJson) {
|
|
51
59
|
try {
|
|
52
60
|
if (!collectLiveDataReferences(bodyJson).length) return bodyJson
|
|
53
|
-
return resolveLiveDataDocument(bodyJson, this.effectiveLiveDataSnapshot, { strict: false }).document
|
|
61
|
+
return resolveLiveDataDocument(bodyJson, this.effectiveLiveDataSnapshot, { strict: false, organizationId: this.liveDataContextOrganizationId, unavailable: this.showLiveDataDiagnostics ? 'message' : 'omit' }).document
|
|
54
62
|
} catch { return bodyJson }
|
|
55
63
|
},
|
|
56
64
|
},
|
|
@@ -2,6 +2,8 @@ import responsiveBreakpointMixin from './responsiveBreakpointMixin'
|
|
|
2
2
|
import { buildBreadcrumbLinks } from '../helpers/breadcrumbLinks'
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
|
+
inject: { pageLiveDataOrganizationProvider: { from: 'liveMessageDataOrganization', default: null } },
|
|
6
|
+
provide() { return { liveMessageDataOrganization: () => this.pageLiveDataOrganizationProvider ? this.pageLiveDataOrganizationProvider() : this.organization?.id || '' } },
|
|
5
7
|
mixins: [responsiveBreakpointMixin],
|
|
6
8
|
|
|
7
9
|
data: () => ({
|