@asd20/ui-next 2.37.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 CHANGED
@@ -1,5 +1,12 @@
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
+
3
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)
4
11
 
5
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.37.0",
3
+ "version": "2.38.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -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
@@ -4,7 +4,7 @@ import {
4
4
  contextualAccordionItemKey,
5
5
  normalizeHeadingContext,
6
6
  } from './headingContext.js'
7
- import { validateLiveDataReference, liveDataLabel } from '../../../helpers/liveMessageData.js'
7
+ import { validateLiveDataReference } from '../../../helpers/liveMessageData.js'
8
8
 
9
9
  const MESSAGE_DOCUMENT_SCHEMA_VERSION = 1
10
10
 
@@ -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
- const tag = node.type === 'liveDataField' ? 'span' : 'div'
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
@@ -3,21 +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 = 3;
6
+ const LIVE_DATA_VERSION = 4;
7
7
  const ADMINISTRATION_TEAM_TAG = "asd20:handbook:administration";
8
8
  const LIVE_DATA_MEMBER_FIELDS = Object.freeze({ name: "Name", title: "Title", phone: "Phone" });
9
9
  const LIVE_DATA_FIELDS = Object.freeze({
10
10
  organization: Object.freeze({
11
- title: "Organization name",
12
- address: "Organization address",
13
- phone: "Organization main phone",
14
- hours: "Organization office hours",
15
- administrators: "Organization administrators",
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",
16
17
  }),
17
18
  team: Object.freeze({
18
- contactPhone: "Team contact phone",
19
- officeHours: "Team office hours",
20
- staff: "Team staff list",
19
+ contactPhone: "Team Contact Phone",
20
+ officeHours: "Team Office Hours",
21
+ staff: "Team Staff List",
21
22
  }),
22
23
  });
23
24
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -55,7 +56,7 @@ function validateLiveDataReference(node) {
55
56
 
56
57
  function liveDataLabel(attrs) {
57
58
  const label = LIVE_DATA_FIELDS[attrs?.source]?.[attrs?.field] || "Live information";
58
- return attrs?.binding === "currentSchool" ? `Current organization · ${label}` : label;
59
+ return attrs?.binding === "currentSchool" ? `Current Organization · ${label}` : label;
59
60
  }
60
61
 
61
62
  function liveDataKey(attrs) {
@@ -170,7 +171,7 @@ function liveDataContent(attrs, snapshot, organizationId) {
170
171
 
171
172
  // Materialization returns a copy containing only ordinary document nodes.
172
173
  // Persist the authored references, never this presentation projection.
173
- function resolveLiveDataDocument(bodyJson, snapshot, { strict = true, organizationId } = {}) {
174
+ function resolveLiveDataDocument(bodyJson, snapshot, { strict = true, organizationId, unavailable = "message" } = {}) {
174
175
  collectLiveDataReferences(bodyJson);
175
176
  const value = typeof bodyJson === "string" ? JSON.parse(bodyJson || "null") : bodyJson;
176
177
  const diagnostics = [];
@@ -179,14 +180,22 @@ function resolveLiveDataDocument(bodyJson, snapshot, { strict = true, organizati
179
180
  const content = liveDataContent(node.attrs, snapshot, organizationId);
180
181
  if (content) return content;
181
182
  diagnostics.push({ code: "live-data-unavailable", reference: liveDataKey(node.attrs) });
183
+ if (unavailable === "omit") return [];
182
184
  const message = `${liveDataLabel(node.attrs)} is currently unavailable.`;
183
185
  return node.type === "liveDataBlock" ? [paragraph(message)] : [textNode(message)];
184
186
  }
185
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 [];
186
191
  return [{ ...node, ...(node.attrs ? { attrs: { ...node.attrs } } : {}),
187
- ...(Array.isArray(node.content) ? { content: node.content.flatMap(resolve) } : {}) }];
192
+ ...(content ? { content } : {}) }];
188
193
  }
189
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" }];
190
199
  if (strict && diagnostics.length) throw new Error("Required live information is unavailable. Review the message data sources.");
191
200
  return { document, diagnostics };
192
201
  }
@@ -212,7 +221,7 @@ async function fetchLiveDataSnapshot(documents, { endpoint, fetchImpl = globalTh
212
221
  body: JSON.stringify({
213
222
  query: `query liveMessageData($organizationIds: [uuid!]!, $teamIds: [uuid!]!${currentTeam ? ", $contextOrganizationIds: [uuid!]!" : ""}) {
214
223
  organizations(where: {id: {_in: $organizationIds}, status: {_eq: "Published"}}) {
215
- id status title address phone hours administrators
224
+ id status title address phone email hours administrators
216
225
  }
217
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) {
218
227
  id title isPublic contactPhone officeHours ownerOrganizationId tags ownerOrganization { status }
@@ -9,6 +9,7 @@ export const liveMessageDataOrganizationKey = 'liveMessageDataOrganization'
9
9
  // descendants. Only references trigger a request; ordinary messages are inert.
10
10
  export const liveMessageDataMixin = {
11
11
  inject: {
12
+ showLiveDataDiagnostics: { from: 'liveMessageDataDiagnostics', default: false },
12
13
  liveDataEndpoint: { from: liveMessageDataEndpointKey, default: '' },
13
14
  liveDataProvider: { from: liveMessageDataProviderKey, default: null },
14
15
  liveDataOrganizationProvider: { from: liveMessageDataOrganizationKey, default: null },
@@ -46,9 +47,9 @@ export const liveMessageDataMixin = {
46
47
  signal: controller.signal,
47
48
  organizationId: this.liveDataContextOrganizationId,
48
49
  })
49
- // Validate availability before accepting a partial capture.
50
+ // Validate references while retaining available fields in a partial capture.
50
51
  this.liveDataDocuments.forEach(doc => {
51
- if (collectLiveDataReferences(doc).length) resolveLiveDataDocument(doc, snapshot, { organizationId: this.liveDataContextOrganizationId })
52
+ if (collectLiveDataReferences(doc).length) resolveLiveDataDocument(doc, snapshot, { strict: false, organizationId: this.liveDataContextOrganizationId })
52
53
  })
53
54
  if (epoch === this.liveDataEpoch) this.liveDataSnapshot = snapshot
54
55
  } catch { if (epoch === this.liveDataEpoch) this.liveDataError = 'Live information is currently unavailable.' }
@@ -57,7 +58,7 @@ export const liveMessageDataMixin = {
57
58
  projectLiveMessageData(bodyJson) {
58
59
  try {
59
60
  if (!collectLiveDataReferences(bodyJson).length) return bodyJson
60
- return resolveLiveDataDocument(bodyJson, this.effectiveLiveDataSnapshot, { strict: false, organizationId: this.liveDataContextOrganizationId }).document
61
+ return resolveLiveDataDocument(bodyJson, this.effectiveLiveDataSnapshot, { strict: false, organizationId: this.liveDataContextOrganizationId, unavailable: this.showLiveDataDiagnostics ? 'message' : 'omit' }).document
61
62
  } catch { return bodyJson }
62
63
  },
63
64
  },