@asd20/ui-next 2.32.1 → 2.32.2
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/package.json +1 -1
- package/src/components/atoms/Asd20RichBodyContent/messageDocumentRenderer.js +4 -2
- package/src/components/templates/Asd20HandbookTemplate/index.vue +31 -5
- package/src/helpers/handbookDocument.js +29 -2
- package/src/helpers/handbookLinks.js +12 -3
- package/src/helpers/mapPageQueryResultToPageTemplateProps.js +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.32.2](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.32.1...ui-next-v2.32.2) (2026-10-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* refine handbook template; fix interior handbook links; move ctas; move translation indicator ([89c8b63](https://github.com/academydistrict20/asd20-ui-next/commit/89c8b6308e81ac6a0005f0d0fc8dac0ec92ecf34))
|
|
9
|
+
|
|
3
10
|
## [2.32.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.32.0...ui-next-v2.32.1) (2026-10-12)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -1116,7 +1116,7 @@ export function buildRichBodyRenderModel(input = {}) {
|
|
|
1116
1116
|
}
|
|
1117
1117
|
|
|
1118
1118
|
// This prepass is deliberately independent of Vue instances and mounted topics.
|
|
1119
|
-
export function buildPublicationHeadingModels({ bodies = [], reservedIds = [], documentUrl, linkResolverFactory } = {}) {
|
|
1119
|
+
export function buildPublicationHeadingModels({ bodies = [], reservedIds = [], reservedIdsFactory, documentUrl, linkResolverFactory } = {}) {
|
|
1120
1120
|
const prefixes = new Set()
|
|
1121
1121
|
const prepared = bodies.map(body => {
|
|
1122
1122
|
const prefix = body.headingIdPrefix
|
|
@@ -1134,7 +1134,9 @@ export function buildPublicationHeadingModels({ bodies = [], reservedIds = [], d
|
|
|
1134
1134
|
render(session)
|
|
1135
1135
|
return { context, render, session }
|
|
1136
1136
|
})
|
|
1137
|
-
const
|
|
1137
|
+
const sourceIds = prepared.flatMap(row => row.session.entries.map(entry => entry.sourceId).filter(Boolean))
|
|
1138
|
+
const headingPlan = buildHeadingIdPlan(prepared.map(row => row.session),
|
|
1139
|
+
[...reservedIds, ...(reservedIdsFactory?.(sourceIds) || [])])
|
|
1138
1140
|
const idPlan = linkResolverFactory
|
|
1139
1141
|
? Object.freeze({ ...headingPlan, linkResolver: linkResolverFactory(headingPlan) })
|
|
1140
1142
|
: headingPlan
|
|
@@ -137,6 +137,7 @@
|
|
|
137
137
|
class="asd20-template-handbook__index"
|
|
138
138
|
aria-label="Handbook topics"
|
|
139
139
|
>
|
|
140
|
+
<slot name="sidebar-status" />
|
|
140
141
|
<button
|
|
141
142
|
class="asd20-template-handbook__index-toggle"
|
|
142
143
|
type="button"
|
|
@@ -234,15 +235,30 @@
|
|
|
234
235
|
class="asd20-template-handbook__summary"
|
|
235
236
|
v-html="topicSummary(entry.message)"
|
|
236
237
|
/>
|
|
238
|
+
<div
|
|
239
|
+
v-if="topicCallsToAction(entry.message).length"
|
|
240
|
+
class="asd20-template-handbook__calls-to-action"
|
|
241
|
+
>
|
|
242
|
+
<asd20-button
|
|
243
|
+
v-for="cta in topicCallsToAction(entry.message)"
|
|
244
|
+
:key="cta.label"
|
|
245
|
+
:label="cta.label"
|
|
246
|
+
:link="cta.url"
|
|
247
|
+
:data-asd20-handbook-link="cta.handbookTargetId || undefined"
|
|
248
|
+
bordered
|
|
249
|
+
transparent
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
237
252
|
<hr
|
|
238
253
|
v-if="topicSummary(entry.message)"
|
|
239
254
|
class="asd20-template-handbook__summary-divider"
|
|
240
255
|
/>
|
|
241
256
|
<asd20-messaging
|
|
242
|
-
v-bind="
|
|
257
|
+
v-bind="entry.message"
|
|
243
258
|
heading=""
|
|
244
259
|
short-description=""
|
|
245
260
|
detail-link=""
|
|
261
|
+
:calls-to-action="[]"
|
|
246
262
|
:heading-level-offset="1"
|
|
247
263
|
:heading-id-prefix="entry.key"
|
|
248
264
|
/>
|
|
@@ -373,11 +389,11 @@ export default {
|
|
|
373
389
|
topicSummary(message) {
|
|
374
390
|
return this.handbookDocument.linkResolver.html(message.shortDescription || message.summary || '')
|
|
375
391
|
},
|
|
376
|
-
|
|
377
|
-
return
|
|
392
|
+
topicCallsToAction(message) {
|
|
393
|
+
return this.handbookDocument.linkResolver.callsToAction(message.callsToAction)
|
|
378
394
|
},
|
|
379
395
|
entryForFragment(id) {
|
|
380
|
-
return this.topicEntries.find(entry => entry.titleId === id ||
|
|
396
|
+
return this.topicEntries.find(entry => entry.titleId === id || entry.legacyTitleId === id ||
|
|
381
397
|
Object.entries(this.publicationHeadings.idPlan.idsBySourceKey).some(([key, target]) => key.startsWith(entry.key + ':') && target === id))
|
|
382
398
|
},
|
|
383
399
|
onFragmentChange() {
|
|
@@ -388,6 +404,10 @@ export default {
|
|
|
388
404
|
try { id = decodeURIComponent(String(fragment || '').replace(/^#/, '')) } catch { return false }
|
|
389
405
|
const entry = this.entryForFragment(id)
|
|
390
406
|
if (!entry) return false
|
|
407
|
+
if (id === entry.legacyTitleId) {
|
|
408
|
+
id = entry.titleId
|
|
409
|
+
window.history.replaceState(window.history.state, '', `#${encodeURIComponent(id)}`)
|
|
410
|
+
}
|
|
391
411
|
const epoch = ++this.fragmentEpoch
|
|
392
412
|
this.selectedMessageId = entry.key
|
|
393
413
|
this.expandedTopicId = entry.key
|
|
@@ -529,7 +549,13 @@ export default {
|
|
|
529
549
|
margin: 0 0 space(1);
|
|
530
550
|
border: 0;
|
|
531
551
|
border-top: 1px solid currentColor;
|
|
532
|
-
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
&__calls-to-action {
|
|
555
|
+
display: flex;
|
|
556
|
+
flex-wrap: wrap;
|
|
557
|
+
gap: space(0.25);
|
|
558
|
+
margin-bottom: space(1);
|
|
533
559
|
}
|
|
534
560
|
|
|
535
561
|
&__index ol {
|
|
@@ -4,6 +4,15 @@ import { createPublicationHeadingPrefix } from '../components/atoms/Asd20RichBod
|
|
|
4
4
|
import { parseFragment } from 'parse5'
|
|
5
5
|
import { createHandbookLinkResolver } from './handbookLinks'
|
|
6
6
|
|
|
7
|
+
function topicSlug(message) {
|
|
8
|
+
// CMS slugs survive translation; remove their publishing-date suffix.
|
|
9
|
+
const label = message.slug
|
|
10
|
+
? String(message.slug).replace(/-\d{4}-\d{2}-\d{2}$/, '')
|
|
11
|
+
: message.heading || message.title || 'topic'
|
|
12
|
+
return String(label).normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
|
|
13
|
+
.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'topic'
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
export function documentImageUrls(html, baseUrl) {
|
|
8
17
|
const urls = new Set()
|
|
9
18
|
const walk = node => {
|
|
@@ -26,15 +35,33 @@ export function buildHandbookDocument({ messages = [], metaDescription = '', doc
|
|
|
26
35
|
const ordinal = counts.get(identity) || 0
|
|
27
36
|
counts.set(identity, ordinal + 1)
|
|
28
37
|
const key = createPublicationHeadingPrefix(documentId, JSON.stringify([identity, ordinal]))
|
|
29
|
-
return { message, key,
|
|
38
|
+
return { message, key, legacyTitleId: `${key}-title`, subtopicsId: `${key}-subtopics` }
|
|
30
39
|
})
|
|
40
|
+
const fixedIds = ['main', 'handbook-content', 'handbook-topics', ...reservedIds,
|
|
41
|
+
...entries.flatMap(entry => [entry.legacyTitleId, entry.subtopicsId])]
|
|
42
|
+
const slugs = entries.map(entry => topicSlug(entry.message))
|
|
43
|
+
const candidates = new Set(slugs)
|
|
31
44
|
const coverKey = createPublicationHeadingPrefix(documentId, 'cover')
|
|
32
45
|
const bodies = entries.map(entry => ({ bodyContent: entry.message.bodyContent, bodyJson: entry.message.bodyJson, headingLevelOffset: 1, headingIdPrefix: entry.key }))
|
|
33
46
|
if (includeIntroductionBody) bodies.push({ bodyContent: messages[0]?.bodyContent, bodyJson: messages[0]?.bodyJson, headingIdPrefix: coverKey })
|
|
34
47
|
const headingModels = buildPublicationHeadingModels({
|
|
35
48
|
documentUrl,
|
|
36
49
|
bodies,
|
|
37
|
-
reservedIds:
|
|
50
|
+
reservedIds: fixedIds,
|
|
51
|
+
reservedIdsFactory: sourceIds => {
|
|
52
|
+
// Keep authored section bookmarks intact when a title has the same slug.
|
|
53
|
+
const used = new Set([...fixedIds, ...sourceIds])
|
|
54
|
+
entries.forEach((entry, index) => {
|
|
55
|
+
const slug = slugs[index]
|
|
56
|
+
let id = slug, suffix = 2
|
|
57
|
+
while (used.has(id)) {
|
|
58
|
+
do { id = `${slug}-${suffix++}` } while (candidates.has(id))
|
|
59
|
+
}
|
|
60
|
+
used.add(id)
|
|
61
|
+
entry.titleId = id
|
|
62
|
+
})
|
|
63
|
+
return entries.map(entry => entry.titleId)
|
|
64
|
+
},
|
|
38
65
|
linkResolverFactory: idPlan => createHandbookLinkResolver(entries, documentUrl, idPlan),
|
|
39
66
|
})
|
|
40
67
|
const introduction = buildPublicationIntroduction(messages[0], metaDescription)
|
|
@@ -20,11 +20,11 @@ function canonicalLinks(message) {
|
|
|
20
20
|
const detail = links.find(link => link?.type === 'Detail Link')
|
|
21
21
|
// Match the website mapper's single selected Detail Link. The same field is
|
|
22
22
|
// retained in the PDF source, keeping online/PDF behavior identical.
|
|
23
|
-
return [message.detailLink || detail?.url].filter(Boolean)
|
|
23
|
+
return [message.detailLink || detail?.url, ...(Array.isArray(message.contentPageUrls) ? message.contentPageUrls : [])].filter(Boolean)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
26
|
+
// Included topics are identified by declared detail links and CMS page/message
|
|
27
|
+
// relationships. Duplicate/unknown destinations remain ordinary links.
|
|
28
28
|
export function createHandbookLinkResolver(entries, documentUrl, idPlan) {
|
|
29
29
|
const destinations = new Map()
|
|
30
30
|
const handbookPage = destination(documentUrl, documentUrl)?.key
|
|
@@ -40,7 +40,16 @@ export function createHandbookLinkResolver(entries, documentUrl, idPlan) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
function resolve(value) {
|
|
43
|
+
// Existing bookmarked topic fragments remain valid after adopting slugs.
|
|
44
|
+
let legacyFragment = ''
|
|
45
|
+
if (typeof value === 'string' && value.trim().startsWith('#')) legacyFragment = value.trim().slice(1)
|
|
43
46
|
const url = destination(value, documentUrl)
|
|
47
|
+
if (url?.key === handbookPage) legacyFragment = url.hash.slice(1)
|
|
48
|
+
if (legacyFragment) {
|
|
49
|
+
try { legacyFragment = decodeURIComponent(legacyFragment) } catch { return null }
|
|
50
|
+
const entry = entries.find(entry => entry.legacyTitleId === legacyFragment)
|
|
51
|
+
if (entry) return { href: `#${encodeURIComponent(entry.titleId)}`, targetId: entry.titleId }
|
|
52
|
+
}
|
|
44
53
|
const topics = url && destinations.get(url.key)
|
|
45
54
|
if (topics?.size !== 1) return null
|
|
46
55
|
const entry = [...topics][0]
|
|
@@ -272,6 +272,14 @@ export default function mapPageQueryResultToPageTemplateProps(
|
|
|
272
272
|
callsToAction,
|
|
273
273
|
detailLink: detailLink ? detailLink.link.url : '',
|
|
274
274
|
detailLinkLabel: detailLink ? detailLink.link.title : '',
|
|
275
|
+
// Page paths are relative to the current website. A message can be
|
|
276
|
+
// shared across organizations, so only retain this site's pages.
|
|
277
|
+
contentPageUrls: (m.message.pages || [])
|
|
278
|
+
.map(row => row.page)
|
|
279
|
+
.filter(page => page?.status === 'Published' &&
|
|
280
|
+
page.ownerOrganizationId === pageData.ownerOrganizationId)
|
|
281
|
+
.map(page => page.path?.path)
|
|
282
|
+
.filter(path => typeof path === 'string' && path.startsWith('/') && !path.startsWith('//')),
|
|
275
283
|
}
|
|
276
284
|
}),
|
|
277
285
|
people: pageData.people,
|