@asd20/ui-next 2.30.0 → 2.31.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.
@@ -107,6 +107,7 @@ function normalizeField(field) {
107
107
  ),
108
108
  options: undefined,
109
109
  visibleWhen: normalizeCondition(field.visibleWhen),
110
+ requiredUnlessUploaded: boundedText(field.requiredUnlessUploaded, 'Upload field name', 64),
110
111
  requiredWhen:
111
112
  field.requiredWhen == null
112
113
  ? undefined
@@ -191,13 +192,13 @@ export function isPublicFormFieldVisible(field, values) {
191
192
  }
192
193
 
193
194
  export function isPublicFormFieldRequired(field, values) {
194
- return (
195
- field.required ||
196
- field.requiredWhen?.some(condition =>
197
- condition.values.includes(String(values[condition.fieldName] || '').trim())
198
- ) ||
199
- false
195
+ const required = field.required || field.requiredWhen?.some(condition =>
196
+ condition.values.includes(String(values[condition.fieldName] || '').trim())
200
197
  )
198
+ const uploaded = field.requiredUnlessUploaded &&
199
+ Array.isArray(values[field.requiredUnlessUploaded]) &&
200
+ values[field.requiredUnlessUploaded].length > 0
201
+ return Boolean(required && !uploaded)
201
202
  }
202
203
 
203
204
  export async function serializePublicFormValues(definition, values) {
@@ -252,6 +253,12 @@ export function normalizePublicFormDefinition(definition) {
252
253
  throw new Error('The public form contains duplicate field names.')
253
254
 
254
255
  for (const field of fields) {
256
+ if (field.requiredUnlessUploaded) {
257
+ const upload = fields.find(candidate => candidate.name === field.requiredUnlessUploaded)
258
+ if (!upload || upload.type !== 'file' || upload === field) {
259
+ throw new Error('The public form has an invalid upload requirement reference.')
260
+ }
261
+ }
255
262
  for (const condition of [
256
263
  field.visibleWhen,
257
264
  ...(field.requiredWhen || []),
@@ -0,0 +1,53 @@
1
+ import { parseFragment } from 'parse5'
2
+
3
+ // Only empty paragraphs/whitespace are editorially empty. Media, placeholders,
4
+ // links, tables, and otherwise unknown elements must remain visible to review.
5
+ function nonemptyHtml(html) {
6
+ const visit = node => {
7
+ if (node.nodeName === '#text') return Boolean(node.value.trim())
8
+ if (node.tagName && node.tagName !== 'p') return true
9
+ return (node.childNodes || []).some(visit)
10
+ }
11
+ return (parseFragment(String(html || '')).childNodes || []).some(visit)
12
+ }
13
+
14
+ function nonemptyJson(value) {
15
+ if (!value) return false
16
+ let doc = value
17
+ if (typeof value === 'string') {
18
+ try { doc = JSON.parse(value) } catch { return true }
19
+ }
20
+ if (doc?.version === 1 && Array.isArray(doc.blocks)) return doc.blocks.length > 0
21
+ const visit = node => {
22
+ if (node?.type === 'text') return Boolean(node.text?.trim())
23
+ if (!['doc', 'paragraph'].includes(node?.type)) return true
24
+ return (node.content || []).some(visit)
25
+ }
26
+ return visit(doc)
27
+ }
28
+
29
+ export function buildPublicationIntroduction(firstMessage, metaDescription = '') {
30
+ const message = firstMessage || {}
31
+ const lead = message.shortDescription || message.summary || metaDescription || ''
32
+ const excludedFields = ['heading', 'title', 'bodyContent', 'bodyJson', 'longDescription', 'images', 'callsToAction', 'detailLink', 'detailLinkLabel']
33
+ const unmappedFields = []
34
+ for (const field of ['bodyContent', 'longDescription']) {
35
+ if (nonemptyHtml(message[field])) unmappedFields.push(field)
36
+ }
37
+ if (nonemptyJson(message.bodyJson)) unmappedFields.push('bodyJson')
38
+ for (const field of ['images', 'callsToAction']) {
39
+ if (message[field]?.length) unmappedFields.push(field)
40
+ }
41
+ // Detail links direct readers from wayfinding messages to detailed pages.
42
+ // The handbook already is that destination; their omission is intentional.
43
+ // Only explicitly typed detail links qualify. Other links still need review.
44
+ if (message.links?.some(item => (item?.link ?? item)?.type !== 'Detail Link')) unmappedFields.push('links')
45
+ if (message.shortDescription && nonemptyHtml(message.summary) && message.summary !== lead) unmappedFields.push('summary')
46
+ return {
47
+ messageId: message.id ?? null, mode: 'first-summary-lead', lead,
48
+ detailLinkPolicy: 'omit-wayfinding',
49
+ leadSource: message.shortDescription || message.summary ? 'summary' : metaDescription ? 'metaDescription' : 'none',
50
+ excludedFields: [...excludedFields, 'links', ...(message.shortDescription ? ['summary'] : [])],
51
+ diagnostics: unmappedFields.length ? [{ code: 'introduction-content-unmapped', fields: unmappedFields }] : [],
52
+ }
53
+ }
package/templates.js CHANGED
@@ -27,3 +27,4 @@ export { default as Asd20SearchAppTemplate } from './src/components/templates/As
27
27
  export { default as Asd20WayfindingAlternateTemplate } from './src/components/templates/Asd20WayfindingAlternateTemplate/index.vue'
28
28
  export { default as Asd20WayfindingImageTemplate } from './src/components/templates/Asd20WayfindingImageTemplate/index.vue'
29
29
  export { default as Asd20WayfindingTemplate } from './src/components/templates/Asd20WayfindingTemplate/index.vue'
30
+ export { default as Asd20HandbookDocument } from './src/components/templates/Asd20HandbookDocument/index.vue'