@flamingo-stack/openframe-frontend-core 0.0.509 → 0.0.510-snapshot.20260803134056
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/dist/{chunk-GE3JUTW2.js → chunk-27OKD7ZV.js} +44 -14
- package/dist/chunk-27OKD7ZV.js.map +1 -0
- package/dist/{chunk-UQDMJ6DH.cjs → chunk-2CE725NN.cjs} +16 -16
- package/dist/{chunk-UQDMJ6DH.cjs.map → chunk-2CE725NN.cjs.map} +1 -1
- package/dist/{chunk-Q4K3TGNP.cjs → chunk-3NECEXDF.cjs} +74 -44
- package/dist/chunk-3NECEXDF.cjs.map +1 -0
- package/dist/{chunk-CIOLOQKQ.cjs → chunk-4SG55XVA.cjs} +9 -9
- package/dist/{chunk-CIOLOQKQ.cjs.map → chunk-4SG55XVA.cjs.map} +1 -1
- package/dist/{chunk-SYZFFRVD.cjs → chunk-5BAGE7AI.cjs} +13 -13
- package/dist/{chunk-SYZFFRVD.cjs.map → chunk-5BAGE7AI.cjs.map} +1 -1
- package/dist/{chunk-6FIDTXYC.js → chunk-7AQSXQEL.js} +3 -3
- package/dist/{chunk-DYPYR7FR.cjs → chunk-7YV67K3I.cjs} +119 -119
- package/dist/{chunk-DYPYR7FR.cjs.map → chunk-7YV67K3I.cjs.map} +1 -1
- package/dist/{chunk-C7OY3PQP.cjs → chunk-A5YZUV6B.cjs} +23 -23
- package/dist/{chunk-C7OY3PQP.cjs.map → chunk-A5YZUV6B.cjs.map} +1 -1
- package/dist/{chunk-3CGZPAGR.cjs → chunk-C4EUXZSA.cjs} +9 -9
- package/dist/{chunk-3CGZPAGR.cjs.map → chunk-C4EUXZSA.cjs.map} +1 -1
- package/dist/{chunk-K5MSCMDK.cjs → chunk-DPLXHG7H.cjs} +40 -40
- package/dist/{chunk-K5MSCMDK.cjs.map → chunk-DPLXHG7H.cjs.map} +1 -1
- package/dist/{chunk-BTUHMCYW.js → chunk-FS3YSNSU.js} +193 -6
- package/dist/chunk-FS3YSNSU.js.map +1 -0
- package/dist/{chunk-4G5TLHJD.cjs → chunk-GHXTF35X.cjs} +3353 -3280
- package/dist/chunk-GHXTF35X.cjs.map +1 -0
- package/dist/{chunk-7VUKIWF6.cjs → chunk-HIV2GIFP.cjs} +3 -3
- package/dist/{chunk-7VUKIWF6.cjs.map → chunk-HIV2GIFP.cjs.map} +1 -1
- package/dist/{chunk-R4HKB3YD.cjs → chunk-IIPBTWQR.cjs} +22 -22
- package/dist/{chunk-R4HKB3YD.cjs.map → chunk-IIPBTWQR.cjs.map} +1 -1
- package/dist/{chunk-LBAKLT6T.cjs → chunk-IUZMWMVX.cjs} +83 -83
- package/dist/{chunk-LBAKLT6T.cjs.map → chunk-IUZMWMVX.cjs.map} +1 -1
- package/dist/{chunk-2T4GTV25.cjs → chunk-IXCBVJRU.cjs} +56 -33
- package/dist/chunk-IXCBVJRU.cjs.map +1 -0
- package/dist/{chunk-72XAID7Y.js → chunk-J4VB4EHW.js} +6 -6
- package/dist/{chunk-LFUPI7VX.js → chunk-JXMNLH7F.js} +3 -3
- package/dist/{chunk-A62C4C4V.cjs → chunk-KI4EGKNV.cjs} +3 -2
- package/dist/chunk-KI4EGKNV.cjs.map +1 -0
- package/dist/{chunk-M37PBG4N.cjs → chunk-LEWQRAZ4.cjs} +73 -73
- package/dist/{chunk-M37PBG4N.cjs.map → chunk-LEWQRAZ4.cjs.map} +1 -1
- package/dist/{chunk-XGIFOHKE.js → chunk-LVAJK2U2.js} +46 -23
- package/dist/chunk-LVAJK2U2.js.map +1 -0
- package/dist/{chunk-OB7YDOTK.cjs → chunk-NJADHL2G.cjs} +8 -8
- package/dist/{chunk-OB7YDOTK.cjs.map → chunk-NJADHL2G.cjs.map} +1 -1
- package/dist/{chunk-I2B6X77L.js → chunk-NKA3O4L6.js} +3 -3
- package/dist/{chunk-K6QTZ7CZ.js → chunk-NNWSGOUP.js} +3 -3
- package/dist/{chunk-EYTXD62B.cjs → chunk-OGVIGAMZ.cjs} +232 -45
- package/dist/chunk-OGVIGAMZ.cjs.map +1 -0
- package/dist/{chunk-QJJISLPA.cjs → chunk-OMN7A6RP.cjs} +32 -32
- package/dist/{chunk-QJJISLPA.cjs.map → chunk-OMN7A6RP.cjs.map} +1 -1
- package/dist/{chunk-TWSKTJHW.js → chunk-PRMR6WKC.js} +3 -3
- package/dist/{chunk-BT2WWDFP.cjs → chunk-PXQQFRUD.cjs} +11 -11
- package/dist/{chunk-BT2WWDFP.cjs.map → chunk-PXQQFRUD.cjs.map} +1 -1
- package/dist/{chunk-KBN7PMFT.js → chunk-RU3SCECX.js} +3 -3
- package/dist/{chunk-YQEPRYT5.cjs → chunk-RXFBW2EJ.cjs} +12 -12
- package/dist/{chunk-YQEPRYT5.cjs.map → chunk-RXFBW2EJ.cjs.map} +1 -1
- package/dist/{chunk-PO5O654H.js → chunk-TNZOUHHN.js} +6 -6
- package/dist/{chunk-3QSR22IC.js → chunk-TR7AMHGB.js} +10 -10
- package/dist/{chunk-V3ZAAFE3.js → chunk-TYVHM67X.js} +8 -8
- package/dist/{chunk-T7MCSA55.cjs → chunk-U4WPFX5K.cjs} +34 -34
- package/dist/{chunk-T7MCSA55.cjs.map → chunk-U4WPFX5K.cjs.map} +1 -1
- package/dist/{chunk-M4MKQKFF.js → chunk-UBTOOE55.js} +2 -2
- package/dist/{chunk-AEELJC4N.js → chunk-X7AJ3J42.js} +5292 -5219
- package/dist/chunk-X7AJ3J42.js.map +1 -0
- package/dist/{chunk-SZMA65OW.js → chunk-XURRAFIB.js} +2 -2
- package/dist/{chunk-7WZHBQ4J.js → chunk-YGS7BLSN.js} +5 -5
- package/dist/{chunk-RG2WRBPT.js → chunk-Z3ADCYC5.js} +2 -2
- package/dist/{chunk-KXZO2WZX.js → chunk-ZBQV7WH7.js} +3 -3
- package/dist/{chunk-4GPWLGKT.js → chunk-ZRILYK5U.js} +2 -1
- package/dist/{chunk-DC2TKS7C.js → chunk-ZSIEM7CL.js} +5 -5
- package/dist/components/calendar.cjs +4 -4
- package/dist/components/calendar.js +3 -3
- package/dist/components/case-studies/index.cjs +17 -17
- package/dist/components/case-studies/index.js +7 -7
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +13 -7
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +12 -6
- package/dist/components/chat/remark-mention-chips.d.ts +5 -2
- package/dist/components/chat/remark-mention-chips.d.ts.map +1 -1
- package/dist/components/chat/utils/discuss-ref-prompt.d.ts +48 -0
- package/dist/components/chat/utils/discuss-ref-prompt.d.ts.map +1 -0
- package/dist/components/chat/utils/index.d.ts +2 -0
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/resolve-fetched-card-href.d.ts +63 -0
- package/dist/components/chat/utils/resolve-fetched-card-href.d.ts.map +1 -0
- package/dist/components/chat/utils/source-row-cta.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +8 -8
- package/dist/components/contact/index.js +7 -7
- package/dist/components/docs/index.cjs +10 -10
- package/dist/components/docs/index.js +9 -9
- package/dist/components/embeds/index.cjs +8 -8
- package/dist/components/embeds/index.js +7 -7
- package/dist/components/faq/index.cjs +9 -9
- package/dist/components/faq/index.js +8 -8
- package/dist/components/features/index.cjs +7 -7
- package/dist/components/features/index.js +6 -6
- package/dist/components/help-center-pages/index.cjs +28 -28
- package/dist/components/help-center-pages/index.js +19 -19
- package/dist/components/icons/index.cjs +3 -3
- package/dist/components/icons/index.js +2 -2
- package/dist/components/index.cjs +253 -247
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +23 -17
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +92 -92
- package/dist/components/meeting-scheduler/index.js +9 -9
- package/dist/components/navigation/header.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +7 -7
- package/dist/components/navigation/index.js +6 -6
- package/dist/components/onboarding-guides/index.cjs +11 -11
- package/dist/components/onboarding-guides/index.js +10 -10
- package/dist/components/related-content/index.cjs +9 -9
- package/dist/components/related-content/index.js +8 -8
- package/dist/components/shared/doc-search/resolve-search-result-action.d.ts +11 -3
- package/dist/components/shared/doc-search/resolve-search-result-action.d.ts.map +1 -1
- package/dist/components/shared/doc-search/use-doc-search.d.ts +7 -0
- package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +11 -11
- package/dist/components/tickets/index.js +10 -10
- package/dist/components/ui/actions-menu.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +70 -70
- package/dist/components/ui/file-manager/index.js +3 -3
- package/dist/components/ui/index.cjs +13 -7
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +12 -6
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -1
- package/dist/components/ui/overlay-open-registry.d.ts +50 -0
- package/dist/components/ui/overlay-open-registry.d.ts.map +1 -0
- package/dist/components/ui/portal-container.d.ts +24 -0
- package/dist/components/ui/portal-container.d.ts.map +1 -1
- package/dist/hooks/index.cjs +4 -4
- package/dist/hooks/index.js +3 -3
- package/dist/index.cjs +20 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +24 -14
- package/dist/utils/anchor-id.d.ts +32 -0
- package/dist/utils/anchor-id.d.ts.map +1 -0
- package/dist/utils/content-href.d.ts +27 -6
- package/dist/utils/content-href.d.ts.map +1 -1
- package/dist/utils/index.cjs +213 -152
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +212 -153
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/list-url.d.ts.map +1 -1
- package/dist/utils/same-page-hash-nav.d.ts +17 -6
- package/dist/utils/same-page-hash-nav.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/__tests__/remark-mention-chips.test.ts +67 -0
- package/src/components/chat/chat-message-enhanced.tsx +3 -2
- package/src/components/chat/chat-message-list.tsx +49 -5
- package/src/components/chat/embeddable-chat.tsx +19 -2
- package/src/components/chat/entity-cards/dispatch.tsx +65 -4
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +28 -4
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +5 -3
- package/src/components/chat/remark-mention-chips.ts +6 -3
- package/src/components/chat/utils/__tests__/discuss-ref-prompt.test.ts +67 -0
- package/src/components/chat/utils/__tests__/resolve-fetched-card-href.test.ts +156 -0
- package/src/components/chat/utils/discuss-ref-prompt.ts +63 -0
- package/src/components/chat/utils/index.ts +16 -0
- package/src/components/chat/utils/resolve-fetched-card-href.ts +104 -0
- package/src/components/chat/utils/source-row-cta.ts +5 -1
- package/src/components/navigation/header.tsx +21 -16
- package/src/components/shared/doc-search/__tests__/resolve-search-result-action.test.ts +145 -0
- package/src/components/shared/doc-search/resolve-search-result-action.ts +46 -10
- package/src/components/shared/doc-search/use-doc-search.ts +50 -14
- package/src/components/ui/__tests__/overlay-open-registry.test.tsx +87 -0
- package/src/components/ui/actions-menu.tsx +65 -2
- package/src/components/ui/dropdown-menu.tsx +62 -12
- package/src/components/ui/markdown/base-components.tsx +26 -0
- package/src/components/ui/overlay-open-registry.tsx +99 -0
- package/src/components/ui/portal-container.tsx +31 -0
- package/src/stories/TopNavigation.stories.tsx +1 -1
- package/src/utils/__tests__/anchor-id.test.ts +139 -0
- package/src/utils/__tests__/content-href.test.ts +74 -0
- package/src/utils/__tests__/list-url.test.ts +11 -1
- package/src/utils/anchor-id.ts +106 -0
- package/src/utils/content-href.ts +113 -13
- package/src/utils/index.ts +8 -0
- package/src/utils/list-url.ts +6 -1
- package/src/utils/same-page-hash-nav.ts +28 -9
- package/dist/chunk-2T4GTV25.cjs.map +0 -1
- package/dist/chunk-4G5TLHJD.cjs.map +0 -1
- package/dist/chunk-A62C4C4V.cjs.map +0 -1
- package/dist/chunk-AEELJC4N.js.map +0 -1
- package/dist/chunk-BTUHMCYW.js.map +0 -1
- package/dist/chunk-EYTXD62B.cjs.map +0 -1
- package/dist/chunk-GE3JUTW2.js.map +0 -1
- package/dist/chunk-Q4K3TGNP.cjs.map +0 -1
- package/dist/chunk-XGIFOHKE.js.map +0 -1
- /package/dist/{chunk-6FIDTXYC.js.map → chunk-7AQSXQEL.js.map} +0 -0
- /package/dist/{chunk-72XAID7Y.js.map → chunk-J4VB4EHW.js.map} +0 -0
- /package/dist/{chunk-LFUPI7VX.js.map → chunk-JXMNLH7F.js.map} +0 -0
- /package/dist/{chunk-I2B6X77L.js.map → chunk-NKA3O4L6.js.map} +0 -0
- /package/dist/{chunk-K6QTZ7CZ.js.map → chunk-NNWSGOUP.js.map} +0 -0
- /package/dist/{chunk-TWSKTJHW.js.map → chunk-PRMR6WKC.js.map} +0 -0
- /package/dist/{chunk-KBN7PMFT.js.map → chunk-RU3SCECX.js.map} +0 -0
- /package/dist/{chunk-PO5O654H.js.map → chunk-TNZOUHHN.js.map} +0 -0
- /package/dist/{chunk-3QSR22IC.js.map → chunk-TR7AMHGB.js.map} +0 -0
- /package/dist/{chunk-V3ZAAFE3.js.map → chunk-TYVHM67X.js.map} +0 -0
- /package/dist/{chunk-M4MKQKFF.js.map → chunk-UBTOOE55.js.map} +0 -0
- /package/dist/{chunk-SZMA65OW.js.map → chunk-XURRAFIB.js.map} +0 -0
- /package/dist/{chunk-7WZHBQ4J.js.map → chunk-YGS7BLSN.js.map} +0 -0
- /package/dist/{chunk-RG2WRBPT.js.map → chunk-Z3ADCYC5.js.map} +0 -0
- /package/dist/{chunk-KXZO2WZX.js.map → chunk-ZBQV7WH7.js.map} +0 -0
- /package/dist/{chunk-4GPWLGKT.js.map → chunk-ZRILYK5U.js.map} +0 -0
- /package/dist/{chunk-DC2TKS7C.js.map → chunk-ZSIEM7CL.js.map} +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { makeComposeContentUrl } from '../../../../utils/content-href'
|
|
3
|
+
import { resolveFetchedCardHref } from '../resolve-fetched-card-href'
|
|
4
|
+
|
|
5
|
+
const HUB = 'https://www.flamingo.run'
|
|
6
|
+
|
|
7
|
+
/** The OpenFrame Help Center seam, verbatim in shape from
|
|
8
|
+
* `openframe-oss-frontend/src/app/(app)/help-center/help-center-content-href.ts`. */
|
|
9
|
+
const compose = makeComposeContentUrl({
|
|
10
|
+
hostedTypes: new Set(['onboarding_guide', 'product_release']),
|
|
11
|
+
contentOrigin: HUB,
|
|
12
|
+
suffixes: {
|
|
13
|
+
onboarding_guide: 'help-center/onboarding-guides',
|
|
14
|
+
product_release: 'help-center/releases',
|
|
15
|
+
blog_post: 'blog',
|
|
16
|
+
case_study: 'case-studies',
|
|
17
|
+
},
|
|
18
|
+
overrides: {
|
|
19
|
+
roadmap_item: (id) => ({
|
|
20
|
+
href: `/help-center/roadmap?search=${encodeURIComponent(id)}`,
|
|
21
|
+
targetPlatform: null,
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('resolveFetchedCardHref — Mingo cards with no ref metadata', () => {
|
|
27
|
+
it('override type deep-links on the MARKER id, not the fetched row id', () => {
|
|
28
|
+
// The `[card://roadmap_item:86ad3qvv5]` id is the ClickUp task id the
|
|
29
|
+
// roadmap list filters by; the fetched row carries its own db id.
|
|
30
|
+
expect(
|
|
31
|
+
resolveFetchedCardHref({
|
|
32
|
+
contentRefType: 'roadmap_item',
|
|
33
|
+
id: '86ad3qvv5',
|
|
34
|
+
item: { id: 'db-uuid-9', title: 'Deep Google Workspace tenant management' },
|
|
35
|
+
composeContentUrl: compose,
|
|
36
|
+
}),
|
|
37
|
+
).toEqual({ href: '/help-center/roadmap?search=86ad3qvv5', targetPlatform: null })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('hosted type uses the fetched row slug for its in-app detail route', () => {
|
|
41
|
+
expect(
|
|
42
|
+
resolveFetchedCardHref({
|
|
43
|
+
contentRefType: 'product_release',
|
|
44
|
+
id: 'row-uuid-1',
|
|
45
|
+
item: { id: 'row-uuid-1', slug: 'v1-2-0' },
|
|
46
|
+
composeContentUrl: compose,
|
|
47
|
+
}),
|
|
48
|
+
).toEqual({ href: '/help-center/releases/v1-2-0', targetPlatform: null })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('hosted type with no slug falls back to the marker id', () => {
|
|
52
|
+
expect(
|
|
53
|
+
resolveFetchedCardHref({
|
|
54
|
+
contentRefType: 'onboarding_guide',
|
|
55
|
+
id: 'guide-7',
|
|
56
|
+
item: { id: 'guide-7' },
|
|
57
|
+
composeContentUrl: compose,
|
|
58
|
+
}),
|
|
59
|
+
).toEqual({ href: '/help-center/onboarding-guides/guide-7', targetPlatform: null })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('non-hosted type resolves to the content hub', () => {
|
|
63
|
+
expect(
|
|
64
|
+
resolveFetchedCardHref({
|
|
65
|
+
contentRefType: 'case_study',
|
|
66
|
+
id: 'cs-1',
|
|
67
|
+
item: { id: 'cs-1', slug: 'acme-migration' },
|
|
68
|
+
composeContentUrl: compose,
|
|
69
|
+
}),
|
|
70
|
+
).toEqual({ href: `${HUB}/case-studies/acme-migration`, targetPlatform: null })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('legacy rail-vocab alias resolves to the SAME url as the canonical type', () => {
|
|
74
|
+
// The card registry keys blog entries by `blog_post_existing` (what
|
|
75
|
+
// `buildListUrl` wants). Both spellings must produce one url — the reported
|
|
76
|
+
// split was `/blog/what-is-mdr` vs `/blog_post_existing/bitwarden-…`.
|
|
77
|
+
const canonical = resolveFetchedCardHref({
|
|
78
|
+
contentRefType: 'blog_post',
|
|
79
|
+
id: 'p1',
|
|
80
|
+
item: { id: 'p1', slug: 'bitwarden-review-for-msps' },
|
|
81
|
+
composeContentUrl: compose,
|
|
82
|
+
})
|
|
83
|
+
const alias = resolveFetchedCardHref({
|
|
84
|
+
contentRefType: 'blog_post_existing',
|
|
85
|
+
id: 'p1',
|
|
86
|
+
item: { id: 'p1', slug: 'bitwarden-review-for-msps' },
|
|
87
|
+
composeContentUrl: compose,
|
|
88
|
+
})
|
|
89
|
+
expect(alias).toEqual(canonical)
|
|
90
|
+
expect(alias).toEqual({
|
|
91
|
+
href: `${HUB}/blog/bitwarden-review-for-msps`,
|
|
92
|
+
targetPlatform: null,
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('no seam wired → null (card stays unlinked, pre-existing behavior)', () => {
|
|
97
|
+
expect(
|
|
98
|
+
resolveFetchedCardHref({
|
|
99
|
+
contentRefType: 'roadmap_item',
|
|
100
|
+
id: '86ad3qvv5',
|
|
101
|
+
item: { id: '86ad3qvv5' },
|
|
102
|
+
}),
|
|
103
|
+
).toBeNull()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('guards a missing type / id / malformed item', () => {
|
|
107
|
+
expect(
|
|
108
|
+
resolveFetchedCardHref({ contentRefType: '', id: 'x', item: {}, composeContentUrl: compose }),
|
|
109
|
+
).toBeNull()
|
|
110
|
+
expect(
|
|
111
|
+
resolveFetchedCardHref({
|
|
112
|
+
contentRefType: 'case_study',
|
|
113
|
+
id: '',
|
|
114
|
+
item: {},
|
|
115
|
+
composeContentUrl: compose,
|
|
116
|
+
}),
|
|
117
|
+
).toBeNull()
|
|
118
|
+
// A non-object item (or a non-string slug) must not throw — it just loses
|
|
119
|
+
// the slug hint and falls back to the marker id.
|
|
120
|
+
expect(
|
|
121
|
+
resolveFetchedCardHref({
|
|
122
|
+
contentRefType: 'case_study',
|
|
123
|
+
id: 'cs-2',
|
|
124
|
+
item: { slug: 42 },
|
|
125
|
+
composeContentUrl: compose,
|
|
126
|
+
}),
|
|
127
|
+
).toEqual({ href: `${HUB}/case-studies/cs-2`, targetPlatform: null })
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('makeComposeContentUrl — the new `slug` hint', () => {
|
|
132
|
+
it('externalUrl still wins over the hint (SSE path unchanged)', () => {
|
|
133
|
+
expect(
|
|
134
|
+
compose({
|
|
135
|
+
type: 'product_release',
|
|
136
|
+
identifier: 'row-uuid-1',
|
|
137
|
+
slug: 'from-the-row',
|
|
138
|
+
externalUrl: `${HUB}/releases/from-the-url`,
|
|
139
|
+
}),
|
|
140
|
+
).toEqual({ href: '/help-center/releases/from-the-url', targetPlatform: null })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('applies to non-hosted types too — hub detail routes are slug-based', () => {
|
|
144
|
+
expect(compose({ type: 'blog_post', identifier: 'id-1', slug: 'hello-world' })).toEqual({
|
|
145
|
+
href: `${HUB}/blog/hello-world`,
|
|
146
|
+
targetPlatform: null,
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('no hint → identifier, unchanged for page-view and SSE callers', () => {
|
|
151
|
+
expect(compose({ type: 'blog_post', identifier: 'hello-world' })).toEqual({
|
|
152
|
+
href: `${HUB}/blog/hello-world`,
|
|
153
|
+
targetPlatform: null,
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE place the "Ask Mingo" (discuss) prompt for an inline entity card is
|
|
3
|
+
* composed — shared by every transport so the affordance reads identically
|
|
4
|
+
* wherever it is clicked.
|
|
5
|
+
*
|
|
6
|
+
* ## Why the two transports still differ underneath
|
|
7
|
+
*
|
|
8
|
+
* The prose is UX only. What narrows retrieval differs:
|
|
9
|
+
*
|
|
10
|
+
* - **SSE / guide** — `sendMessage(prompt, { commandOverride: { entityIdFilter:
|
|
11
|
+
* { tableId, id } } })`. Retrieval is strictly primary-key-driven; the
|
|
12
|
+
* sentence is decoration.
|
|
13
|
+
* - **Mingo / NATS** — the agent backend has no entity-id-filtered retrieval
|
|
14
|
+
* (`ContextItemType` covers DEVICE / SCRIPT / TICKET / ORGANIZATION / USER /
|
|
15
|
+
* KB_ARTICLE / POLICY / QUERY / SCHEDULED_SCRIPT — no content types), so the
|
|
16
|
+
* prompt is ALL the agent gets. Hence `includeReference`, which appends the
|
|
17
|
+
* type + id so the agent can still identify the exact row by text.
|
|
18
|
+
*
|
|
19
|
+
* When the backend grows an id-filter for the agent transport, the Mingo callers
|
|
20
|
+
* switch to it and drop `includeReference` — the prose stays put.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { sanitizeTitleForChat } from './slash-dispatch-utils'
|
|
24
|
+
|
|
25
|
+
export interface DiscussPromptRef {
|
|
26
|
+
/** documentType, e.g. `'roadmap_item'`. */
|
|
27
|
+
type: string
|
|
28
|
+
/** Primary key — the id from the `[card://type:id]` marker. */
|
|
29
|
+
id: string
|
|
30
|
+
/** Display title. May be the bare id on a synthetic ref (Mingo). */
|
|
31
|
+
title?: string | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BuildDiscussPromptOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Append `(<type> <id>)` so a transport WITHOUT structured entity filtering
|
|
37
|
+
* still points the agent at the exact row. Default `false` — the SSE path
|
|
38
|
+
* sends `entityIdFilter` and must keep the sentence clean.
|
|
39
|
+
*/
|
|
40
|
+
includeReference?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `Tell me more about <title>` — plus `(<type> <id>)` when
|
|
45
|
+
* `includeReference` is set and the title alone is ambiguous.
|
|
46
|
+
*
|
|
47
|
+
* Falls back to `this item` when the ref has no usable title, matching the
|
|
48
|
+
* long-standing SSE behavior. A title that is just the id (the synthetic ref a
|
|
49
|
+
* Mingo `[card://…]` marker produces before its row loads) is treated as
|
|
50
|
+
* missing, so the prompt never degrades to `Tell me more about 86ad3qvv5`.
|
|
51
|
+
*/
|
|
52
|
+
export function buildDiscussPrompt(
|
|
53
|
+
reference: DiscussPromptRef,
|
|
54
|
+
options: BuildDiscussPromptOptions = {},
|
|
55
|
+
): string {
|
|
56
|
+
const id = (reference.id ?? '').trim()
|
|
57
|
+
const rawTitle = (reference.title ?? '').trim()
|
|
58
|
+
const usableTitle = rawTitle && rawTitle !== id ? sanitizeTitleForChat(rawTitle) : ''
|
|
59
|
+
const prompt = `Tell me more about ${usableTitle || 'this item'}`
|
|
60
|
+
if (!options.includeReference || !id) return prompt
|
|
61
|
+
// Only worth the noise when the reference adds something the title doesn't.
|
|
62
|
+
return `${prompt} (${reference.type} ${id})`
|
|
63
|
+
}
|
|
@@ -196,4 +196,20 @@ export {
|
|
|
196
196
|
resolveSourceIcon,
|
|
197
197
|
} from './source-row-cta'
|
|
198
198
|
|
|
199
|
+
// Post-fetch enrichment for the synthetic ChatRef a `[card://type:id]` marker
|
|
200
|
+
// produces when the transport ships no refs metadata (Mingo/NATS).
|
|
201
|
+
export {
|
|
202
|
+
resolveFetchedCardHref,
|
|
203
|
+
readFetchedCardTitle,
|
|
204
|
+
type FetchedCardHrefInput,
|
|
205
|
+
} from './resolve-fetched-card-href'
|
|
206
|
+
|
|
207
|
+
// The ONE "Ask Mingo" prompt builder — shared by the SSE adapter, the NATS
|
|
208
|
+
// adapter, and hosts that inject their own Mingo state (openframe-frontend).
|
|
209
|
+
export {
|
|
210
|
+
buildDiscussPrompt,
|
|
211
|
+
type DiscussPromptRef,
|
|
212
|
+
type BuildDiscussPromptOptions,
|
|
213
|
+
} from './discuss-ref-prompt'
|
|
214
|
+
|
|
199
215
|
export * from './scripted-stream'
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-fetch href fallback for an inline chat card whose `ChatRef` carried no
|
|
3
|
+
* `externalUrl`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a fetch-mode card can arrive without a URL
|
|
6
|
+
*
|
|
7
|
+
* The two chat transports deliver entity references differently:
|
|
8
|
+
*
|
|
9
|
+
* - **SSE / guide transport** — the server ships a `refs` metadata frame, so
|
|
10
|
+
* each `[card://type:id]` marker expands into a FULL `ChatRef`
|
|
11
|
+
* (`title` / `url` / `targetPlatform` / `sourceRepo`). `resolveSourceRowCTA`
|
|
12
|
+
* then routes that `externalUrl` through the host's `composeContentUrl`
|
|
13
|
+
* seam and the card is clickable.
|
|
14
|
+
* - **Mingo / NATS transport** (`messageData: [{ type: 'GUIDE' | 'TEXT', text }]`)
|
|
15
|
+
* — the body carries bare `[card://type:id]` markers and NOTHING else.
|
|
16
|
+
* `chat-message-enhanced.tsx` synthesizes a minimal `{ type, id, title: id,
|
|
17
|
+
* url: null }` ref so fetch-mode cards can still self-fetch their row, but
|
|
18
|
+
* `resolveSourceRowCTA` has no `externalUrl`, no `path` and no
|
|
19
|
+
* `targetPlatform` to work with → `href: null` → the card renders with its
|
|
20
|
+
* real title and description but goes nowhere when clicked.
|
|
21
|
+
*
|
|
22
|
+
* This resolver closes that gap WITHOUT a second URL vocabulary: it calls the
|
|
23
|
+
* exact same `composeContentUrl` seam the page cards (`resolveContentHref`) and
|
|
24
|
+
* the SSE cards (`resolveSourceRowCTA`) use, so one host config —
|
|
25
|
+
* `hostedTypes` / `suffixes` / `overrides` — governs all three.
|
|
26
|
+
*
|
|
27
|
+
* `identifier` stays the marker's primary key (that is what `overrides` such as
|
|
28
|
+
* roadmap's `?search=<task id>` deep-link on); the row's `slug` rides the
|
|
29
|
+
* separate `slug` hint for hosted types whose detail route is slug-based.
|
|
30
|
+
*
|
|
31
|
+
* Pure — no React, no DOM. The caller owns `safeHref` validation.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { ComposeContentUrl } from '../../../utils/content-href'
|
|
35
|
+
import { canonicalContentRefType } from '../../../utils/list-url'
|
|
36
|
+
|
|
37
|
+
export interface FetchedCardHrefInput {
|
|
38
|
+
/** Canonical `documentType` for the seam (registry `contentRefType`). */
|
|
39
|
+
contentRefType: string
|
|
40
|
+
/** The marker's primary key — `ChatRef.id`, NOT the fetched row's id. */
|
|
41
|
+
id: string
|
|
42
|
+
/** The row the card fetched. Shape is per-type and opaque here; only
|
|
43
|
+
* `slug` and `platforms` are read, both defensively. */
|
|
44
|
+
item: unknown
|
|
45
|
+
/** Host seam. Absent (unwired runtime) → no fallback, same as today. */
|
|
46
|
+
composeContentUrl?: ComposeContentUrl
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Read `item.slug` when it is a non-empty string. */
|
|
50
|
+
function readSlug(item: unknown): string | null {
|
|
51
|
+
if (!item || typeof item !== 'object') return null
|
|
52
|
+
const slug = (item as { slug?: unknown }).slug
|
|
53
|
+
return typeof slug === 'string' && slug.trim() ? slug.trim() : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Human title of a fetched row — `title`, else `name`, else null.
|
|
58
|
+
*
|
|
59
|
+
* A Mingo synthetic ref carries `title: <the marker id>` because that is all
|
|
60
|
+
* the transport shipped. Everything downstream that prints or sends the title
|
|
61
|
+
* (the "Ask Mingo" prompt, host renderers falling back to `ref.title`) would
|
|
62
|
+
* otherwise say `86ad3qvv5`. Once the row loads, this is the real title.
|
|
63
|
+
*/
|
|
64
|
+
export function readFetchedCardTitle(item: unknown): string | null {
|
|
65
|
+
if (!item || typeof item !== 'object') return null
|
|
66
|
+
for (const key of ['title', 'name'] as const) {
|
|
67
|
+
const value = (item as Record<string, unknown>)[key]
|
|
68
|
+
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Read `item.platforms` when it is an array — the hub composer keys its
|
|
74
|
+
* cross-platform topology off it; the embedder default ignores it. */
|
|
75
|
+
function readPlatforms(item: unknown): Array<{ name?: string }> | undefined {
|
|
76
|
+
if (!item || typeof item !== 'object') return undefined
|
|
77
|
+
const platforms = (item as { platforms?: unknown }).platforms
|
|
78
|
+
return Array.isArray(platforms) ? (platforms as Array<{ name?: string }>) : undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve `{href, targetPlatform}` for a freshly fetched card row, or `null`
|
|
83
|
+
* when the host wired no seam (the caller then keeps the card unlinked, which
|
|
84
|
+
* is the pre-existing behavior).
|
|
85
|
+
*/
|
|
86
|
+
export function resolveFetchedCardHref({
|
|
87
|
+
contentRefType,
|
|
88
|
+
id,
|
|
89
|
+
item,
|
|
90
|
+
composeContentUrl,
|
|
91
|
+
}: FetchedCardHrefInput): { href: string; targetPlatform: string | null } | null {
|
|
92
|
+
if (!composeContentUrl || !contentRefType || !id) return null
|
|
93
|
+
return composeContentUrl({
|
|
94
|
+
// The card registry keys some entries by the legacy rail vocabulary
|
|
95
|
+
// (`blog_post_existing`) because that is what `buildListUrl` expects.
|
|
96
|
+
// The seam speaks the CANONICAL vocabulary — a host's `hostedTypes` /
|
|
97
|
+
// `overrides` are written against `blog_post`, so an un-aliased type
|
|
98
|
+
// misses them all and lands on `<origin>/blog_post_existing/<slug>`.
|
|
99
|
+
type: canonicalContentRefType(contentRefType),
|
|
100
|
+
identifier: id,
|
|
101
|
+
slug: readSlug(item),
|
|
102
|
+
platforms: readPlatforms(item),
|
|
103
|
+
})
|
|
104
|
+
}
|
|
@@ -29,6 +29,7 @@ import { getBaseUrl } from '../../../utils/cn'
|
|
|
29
29
|
import { safeHref } from './compact-card-classes'
|
|
30
30
|
import type { ChatRef } from '../chat-ref.types'
|
|
31
31
|
import type { ComposeContentUrl } from '../../../utils/content-href'
|
|
32
|
+
import { canonicalContentRefType } from '../../../utils/list-url'
|
|
32
33
|
|
|
33
34
|
/** Path sanitization — keep alphanumerics, slash, dash, dot, underscore;
|
|
34
35
|
* strip everything else. Defends against a hostile mapper returning a
|
|
@@ -233,7 +234,10 @@ export function resolveSourceRowCTA(
|
|
|
233
234
|
// from the externalUrl) and everything else to the externalUrl verbatim —
|
|
234
235
|
// the SAME resolver the page-view cards use, so chat + page links match.
|
|
235
236
|
const composed = ctx.composeContentUrl({
|
|
236
|
-
|
|
237
|
+
// Canonical vocabulary only — a rail-vocab alias would miss the host's
|
|
238
|
+
// `hostedTypes` / `overrides` and fall through to the seam's default
|
|
239
|
+
// `<origin>/<type>/<id>` branch. Same canonicalizer `buildListUrl` uses.
|
|
240
|
+
type: canonicalContentRefType(row.documentType),
|
|
237
241
|
identifier: idValue,
|
|
238
242
|
externalUrl: row.externalUrl,
|
|
239
243
|
targetPlatform: row.targetPlatform ?? null,
|
|
@@ -8,8 +8,8 @@ import { Menu01Icon, XmarkIcon } from '../icons-v2-generated';
|
|
|
8
8
|
import { Button } from '../ui/button';
|
|
9
9
|
import { HeaderButton } from './header-button';
|
|
10
10
|
import { MingoAiButton } from './mingo-ai-button';
|
|
11
|
-
import { TicketAlertsButton } from './ticket-alerts-button';
|
|
12
11
|
import { MOBILE_NAV_PANEL_ID } from './mobile-nav-panel';
|
|
12
|
+
import { TicketAlertsButton } from './ticket-alerts-button';
|
|
13
13
|
import { TopNavigation } from './top-navigation';
|
|
14
14
|
|
|
15
15
|
export interface HeaderProps {
|
|
@@ -20,6 +20,11 @@ export interface HeaderProps {
|
|
|
20
20
|
// Re-export from types for convenience
|
|
21
21
|
export type { HeaderConfig } from '../../types/navigation';
|
|
22
22
|
|
|
23
|
+
// Top-level nav-link typography (Figma 2936-6815): the compact h6 step
|
|
24
|
+
// (DM Sans 500, 14/20) with 24px icons. Overrides the `font="regular"` h4
|
|
25
|
+
// label and the Button base's 20px svg cap via cn()'s tailwind-merge.
|
|
26
|
+
const NAV_ITEM_CLASSES = 'text-h6 [&_svg]:h-6 [&_svg]:w-6';
|
|
27
|
+
|
|
23
28
|
export function Header({ config, platform }: HeaderProps) {
|
|
24
29
|
const [show, setShow] = useState(true);
|
|
25
30
|
const [lastScrollY, setLastScrollY] = useState(0);
|
|
@@ -139,9 +144,10 @@ export function Header({ config, platform }: HeaderProps) {
|
|
|
139
144
|
size="default"
|
|
140
145
|
font="regular"
|
|
141
146
|
className={cn(
|
|
142
|
-
// Top-level nav links
|
|
143
|
-
// (DM Sans 500)
|
|
144
|
-
//
|
|
147
|
+
// Top-level nav links (Figma 2936-6815): compact h6 label
|
|
148
|
+
// (DM Sans 500, 14/20) with 24px icons — overrides the
|
|
149
|
+
// font="regular" h4 step and the button's 20px svg cap.
|
|
150
|
+
NAV_ITEM_CLASSES,
|
|
145
151
|
item.isActive && 'bg-ods-bg-hover', // Active items get subtle gray background
|
|
146
152
|
isOpen && 'bg-ods-bg-hover', // Open dropdowns get gray background
|
|
147
153
|
item.className,
|
|
@@ -197,7 +203,11 @@ export function Header({ config, platform }: HeaderProps) {
|
|
|
197
203
|
}}
|
|
198
204
|
className={cn(
|
|
199
205
|
'flex justify-start w-full',
|
|
200
|
-
|
|
206
|
+
// Same caption step as the top-level nav links (designer:
|
|
207
|
+
// dropdowns follow the h6 caption size/weight too). The
|
|
208
|
+
// explicit font-medium beats small-legacy's font-bold via
|
|
209
|
+
// tailwind-merge instead of stylesheet order.
|
|
210
|
+
'text-h6 font-medium',
|
|
201
211
|
index < (item.children?.length ?? 0) - 1 && 'mb-1',
|
|
202
212
|
'text-ods-text-primary', // All dropdown items use primary text color
|
|
203
213
|
child.isActive && 'bg-ods-bg-hover', // Active dropdown items get gray background
|
|
@@ -232,6 +242,7 @@ export function Header({ config, platform }: HeaderProps) {
|
|
|
232
242
|
size="default"
|
|
233
243
|
font="regular"
|
|
234
244
|
className={cn(
|
|
245
|
+
NAV_ITEM_CLASSES,
|
|
235
246
|
'hover:bg-ods-bg-hover focus:bg-ods-bg-hover',
|
|
236
247
|
'whitespace-nowrap',
|
|
237
248
|
'text-ods-text-primary', // All items use primary text color
|
|
@@ -256,6 +267,7 @@ export function Header({ config, platform }: HeaderProps) {
|
|
|
256
267
|
size="default"
|
|
257
268
|
font="regular"
|
|
258
269
|
className={cn(
|
|
270
|
+
NAV_ITEM_CLASSES,
|
|
259
271
|
'hover:bg-ods-bg-hover focus:bg-ods-bg-hover',
|
|
260
272
|
'whitespace-nowrap',
|
|
261
273
|
'text-ods-text-primary', // All items use primary text color
|
|
@@ -269,12 +281,6 @@ export function Header({ config, platform }: HeaderProps) {
|
|
|
269
281
|
};
|
|
270
282
|
|
|
271
283
|
const hasNav = !!config.navigation && config.navigation.items.length > 0;
|
|
272
|
-
// Always-visible leading cells (admin-sidebar toggle). The logo zone's
|
|
273
|
-
// desktop `pl-xxl` (80px) exists to compensate for the ABSENCE of a leading
|
|
274
|
-
// cell — with one present it reads as a stray gap, so collapse it to the
|
|
275
|
-
// regular `pl-l` inset. (The marketing burger doesn't count: it is
|
|
276
|
-
// CSS-hidden on lg, exactly where the 80px inset applies.)
|
|
277
|
-
const hasLeftCells = !!config.actions?.left?.length;
|
|
278
284
|
const hasCta =
|
|
279
285
|
!!config.actions?.right?.length || !!(config.actions?.persistent && config.actions.persistent.length > 0);
|
|
280
286
|
|
|
@@ -343,11 +349,10 @@ export function Header({ config, platform }: HeaderProps) {
|
|
|
343
349
|
{config.logo.element}
|
|
344
350
|
</Link>
|
|
345
351
|
}
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
logoClassName={cn('md:pl-[var(--spacing-system-mf)]', hasLeftCells && 'lg:pl-[var(--spacing-system-l)]')}
|
|
352
|
+
// Big-bar rule (Figma 2936-6812): 24px fixed left inset on the logo
|
|
353
|
+
// zone at every breakpoint — the same 24px also reads as the gap
|
|
354
|
+
// between a leading cell (burger / admin toggle) and the logo.
|
|
355
|
+
logoClassName="pl-[var(--spacing-system-lf)] md:pl-[var(--spacing-system-lf)] lg:pl-[var(--spacing-system-lf)]"
|
|
351
356
|
center={
|
|
352
357
|
hasNav ? (
|
|
353
358
|
<nav
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { makeComposeContentUrl } from '../../../../utils/content-href'
|
|
3
|
+
import type { SearchResult } from '../../../ui/search-input'
|
|
4
|
+
import { resolveSearchResultAction } from '../resolve-search-result-action'
|
|
5
|
+
|
|
6
|
+
const HUB = 'https://www.flamingo.run'
|
|
7
|
+
|
|
8
|
+
/** A RAG row for a guide the hub owns, shaped like `mapDocSearchResults` emits. */
|
|
9
|
+
const guideRow: SearchResult = {
|
|
10
|
+
id: 'onboarding-guides/deploy-first-device',
|
|
11
|
+
title: 'Deploy your first device',
|
|
12
|
+
path: 'onboarding-guides/deploy-first-device',
|
|
13
|
+
metadata: {
|
|
14
|
+
documentType: 'onboarding_guide',
|
|
15
|
+
externalUrl: `${HUB}/onboarding-guides/deploy-first-device`,
|
|
16
|
+
sourceRepo: 'onboarding-guides',
|
|
17
|
+
id: 'guide-123',
|
|
18
|
+
targetPlatform: 'openframe',
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The OpenFrame Help Center seam: the two hosted types live under
|
|
23
|
+
* `/help-center/...`, list-filter types deep-link with `?search=<id>`. */
|
|
24
|
+
const helpCenterCompose = makeComposeContentUrl({
|
|
25
|
+
hostedTypes: new Set(['onboarding_guide', 'product_release']),
|
|
26
|
+
contentOrigin: HUB,
|
|
27
|
+
suffixes: {
|
|
28
|
+
onboarding_guide: 'help-center/onboarding-guides',
|
|
29
|
+
product_release: 'help-center/releases',
|
|
30
|
+
blog_post: 'blog',
|
|
31
|
+
},
|
|
32
|
+
overrides: {
|
|
33
|
+
delivery_item: (id) => ({ href: `/help-center/bug-fixes?search=${id}`, targetPlatform: null }),
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('resolveSearchResultAction — composeContentUrl seam', () => {
|
|
38
|
+
it('no seam wired → the RAG externalUrl verbatim (legacy behavior)', () => {
|
|
39
|
+
expect(resolveSearchResultAction(guideRow, 'openframe')).toEqual({
|
|
40
|
+
kind: 'navigate-same-tab',
|
|
41
|
+
href: `${HUB}/onboarding-guides/deploy-first-device`,
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('hosted type → the host in-app route, slug recovered from externalUrl', () => {
|
|
46
|
+
expect(
|
|
47
|
+
resolveSearchResultAction(guideRow, 'openframe', 'host', helpCenterCompose),
|
|
48
|
+
).toEqual({
|
|
49
|
+
kind: 'navigate-same-tab',
|
|
50
|
+
href: '/help-center/onboarding-guides/deploy-first-device',
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('relative composed href stays same-tab even in embed mode', () => {
|
|
55
|
+
// `decideNewTab` forces new-tab under embed; an in-app path must not be
|
|
56
|
+
// handed to window.open — the short-circuit runs before that decision.
|
|
57
|
+
expect(
|
|
58
|
+
resolveSearchResultAction(guideRow, 'openframe', 'embed', helpCenterCompose),
|
|
59
|
+
).toEqual({
|
|
60
|
+
kind: 'navigate-same-tab',
|
|
61
|
+
href: '/help-center/onboarding-guides/deploy-first-device',
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('override type → the host deep-link, primary key as identifier', () => {
|
|
66
|
+
const row: SearchResult = {
|
|
67
|
+
id: 'delivery-1',
|
|
68
|
+
title: 'Fix the thing',
|
|
69
|
+
metadata: {
|
|
70
|
+
documentType: 'delivery_item',
|
|
71
|
+
externalUrl: `${HUB}/bug-fixes-and-enhancements?search=abc123`,
|
|
72
|
+
sourceRepo: 'clickup-delivery',
|
|
73
|
+
id: 'abc123',
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
expect(resolveSearchResultAction(row, 'openframe', 'host', helpCenterCompose)).toEqual({
|
|
77
|
+
kind: 'navigate-same-tab',
|
|
78
|
+
href: '/help-center/bug-fixes?search=abc123',
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('non-hosted type → the seam returns externalUrl verbatim, opens out', () => {
|
|
83
|
+
const row: SearchResult = {
|
|
84
|
+
id: 'blog-1',
|
|
85
|
+
title: 'Hello',
|
|
86
|
+
metadata: {
|
|
87
|
+
documentType: 'blog_post',
|
|
88
|
+
externalUrl: `${HUB}/blog/hello`,
|
|
89
|
+
sourceRepo: 'blog-posts',
|
|
90
|
+
id: 'blog-1',
|
|
91
|
+
targetPlatform: 'flamingo',
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
expect(resolveSearchResultAction(row, 'openframe', 'host', helpCenterCompose)).toEqual({
|
|
95
|
+
kind: 'navigate-new-tab',
|
|
96
|
+
href: `${HUB}/blog/hello`,
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('row without documentType → seam skipped, externalUrl verbatim', () => {
|
|
101
|
+
const row: SearchResult = {
|
|
102
|
+
id: 'x',
|
|
103
|
+
title: 'Untyped',
|
|
104
|
+
metadata: { externalUrl: `${HUB}/somewhere`, targetPlatform: 'openframe' },
|
|
105
|
+
}
|
|
106
|
+
expect(resolveSearchResultAction(row, 'openframe', 'host', helpCenterCompose)).toEqual({
|
|
107
|
+
kind: 'navigate-same-tab',
|
|
108
|
+
href: `${HUB}/somewhere`,
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('no externalUrl → unchanged ask-ai / route / noop fallbacks', () => {
|
|
113
|
+
expect(
|
|
114
|
+
resolveSearchResultAction(
|
|
115
|
+
{
|
|
116
|
+
id: 'cap-1',
|
|
117
|
+
title: 'Cap Table',
|
|
118
|
+
metadata: { documentType: 'cap_table', sourceRepo: 'financial-cap-table', id: 'row-9' },
|
|
119
|
+
},
|
|
120
|
+
'company-hub',
|
|
121
|
+
'host',
|
|
122
|
+
helpCenterCompose,
|
|
123
|
+
),
|
|
124
|
+
).toEqual({
|
|
125
|
+
kind: 'ask-ai',
|
|
126
|
+
detail: {
|
|
127
|
+
source: 'company-hub',
|
|
128
|
+
ref: { type: 'cap_table', id: 'row-9', title: 'Cap Table', url: null },
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
expect(
|
|
133
|
+
resolveSearchResultAction(
|
|
134
|
+
{ id: 'doc', title: 'Doc', path: 'repo/architecture/api.md' },
|
|
135
|
+
'openframe',
|
|
136
|
+
'host',
|
|
137
|
+
helpCenterCompose,
|
|
138
|
+
),
|
|
139
|
+
).toEqual({ kind: 'route', path: 'repo/architecture/api.md' })
|
|
140
|
+
|
|
141
|
+
expect(
|
|
142
|
+
resolveSearchResultAction({ id: 'empty', title: 'Empty' }, 'openframe', 'host', helpCenterCompose),
|
|
143
|
+
).toEqual({ kind: 'noop' })
|
|
144
|
+
})
|
|
145
|
+
})
|