@flamingo-stack/openframe-frontend-core 0.0.577 → 0.0.578-1950.5326.1
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-JQI726T3.js → chunk-3LMYQGAJ.js} +3 -2
- package/dist/chunk-3LMYQGAJ.js.map +1 -0
- package/dist/{chunk-RUJKOU5F.js → chunk-5IAGT5LB.js} +2 -2
- package/dist/{chunk-TU32TSAK.js → chunk-5NG2P2KI.js} +5 -5
- package/dist/{chunk-GDWBJFMG.cjs → chunk-ABWA3W3M.cjs} +32 -32
- package/dist/{chunk-GDWBJFMG.cjs.map → chunk-ABWA3W3M.cjs.map} +1 -1
- package/dist/{chunk-U3SDSA5K.js → chunk-AJ67T7LI.js} +4 -4
- package/dist/{chunk-THDGDYJ3.js → chunk-BFJBE4PQ.js} +5 -5
- package/dist/{chunk-KE2TCUOH.cjs → chunk-BLPGMX2L.cjs} +190 -190
- package/dist/{chunk-KE2TCUOH.cjs.map → chunk-BLPGMX2L.cjs.map} +1 -1
- package/dist/{chunk-HSOMYCUG.cjs → chunk-E5QAU63Y.cjs} +11 -11
- package/dist/{chunk-HSOMYCUG.cjs.map → chunk-E5QAU63Y.cjs.map} +1 -1
- package/dist/{chunk-ZBJ6XSDO.js → chunk-ESXUEMPA.js} +3 -3
- package/dist/{chunk-CFTZAZ7R.cjs → chunk-G3AJXLKH.cjs} +28 -28
- package/dist/{chunk-CFTZAZ7R.cjs.map → chunk-G3AJXLKH.cjs.map} +1 -1
- package/dist/{chunk-XG2WNF4L.cjs → chunk-GOEBT72V.cjs} +19 -19
- package/dist/{chunk-XG2WNF4L.cjs.map → chunk-GOEBT72V.cjs.map} +1 -1
- package/dist/{chunk-LYOGSVFO.js → chunk-IBSOYTGZ.js} +3 -3
- package/dist/{chunk-AQ5MZVYN.js → chunk-IWK2NFVR.js} +3 -3
- package/dist/{chunk-2YCUEUIZ.js → chunk-JJH4H4CJ.js} +6 -6
- package/dist/{chunk-RSMUAAPW.js → chunk-KLG3MGRP.js} +3 -3
- package/dist/{chunk-RNUH5IHZ.js → chunk-KPE277X2.js} +3 -3
- package/dist/{chunk-XBBCUC32.cjs → chunk-KQBYQBQN.cjs} +15 -15
- package/dist/{chunk-XBBCUC32.cjs.map → chunk-KQBYQBQN.cjs.map} +1 -1
- package/dist/{chunk-IQFXDS5O.js → chunk-LEUJNBWU.js} +7 -7
- package/dist/{chunk-4D2BR756.js → chunk-LPFA56WY.js} +52 -31
- package/dist/chunk-LPFA56WY.js.map +1 -0
- package/dist/{chunk-75X6BRID.cjs → chunk-M7FNT5AC.cjs} +453 -432
- package/dist/chunk-M7FNT5AC.cjs.map +1 -0
- package/dist/{chunk-JCH6HGQD.js → chunk-MCWAZ5XI.js} +2 -2
- package/dist/{chunk-ET3Q4FIZ.cjs → chunk-NJDL7F7K.cjs} +10 -10
- package/dist/{chunk-ET3Q4FIZ.cjs.map → chunk-NJDL7F7K.cjs.map} +1 -1
- package/dist/{chunk-MIY5FYX3.cjs → chunk-NNFQLTJ3.cjs} +4 -3
- package/dist/chunk-NNFQLTJ3.cjs.map +1 -0
- package/dist/{chunk-XBSW7IF6.cjs → chunk-NPNYQSCG.cjs} +21 -21
- package/dist/{chunk-XBSW7IF6.cjs.map → chunk-NPNYQSCG.cjs.map} +1 -1
- package/dist/{chunk-NQCGDZJ3.cjs → chunk-OIJWSYKP.cjs} +3 -3
- package/dist/{chunk-NQCGDZJ3.cjs.map → chunk-OIJWSYKP.cjs.map} +1 -1
- package/dist/{chunk-WOHHZZJK.cjs → chunk-POB6GNIR.cjs} +57 -57
- package/dist/{chunk-WOHHZZJK.cjs.map → chunk-POB6GNIR.cjs.map} +1 -1
- package/dist/{chunk-NEKT67O5.js → chunk-QHGARH6P.js} +6 -6
- package/dist/{chunk-5DPCIIJ7.cjs → chunk-R7OJZCE6.cjs} +80 -80
- package/dist/{chunk-5DPCIIJ7.cjs.map → chunk-R7OJZCE6.cjs.map} +1 -1
- package/dist/{chunk-6FJABKPX.cjs → chunk-SETRICAM.cjs} +6 -6
- package/dist/{chunk-6FJABKPX.cjs.map → chunk-SETRICAM.cjs.map} +1 -1
- package/dist/{chunk-4KR7JWKB.js → chunk-SHJXC7A4.js} +4 -4
- package/dist/{chunk-NP3XN6ZX.js → chunk-SNZZ6Q6G.js} +4 -4
- package/dist/chunk-SNZZ6Q6G.js.map +1 -0
- package/dist/{chunk-APFGR2KK.cjs → chunk-UQPA2XT7.cjs} +28 -28
- package/dist/{chunk-APFGR2KK.cjs.map → chunk-UQPA2XT7.cjs.map} +1 -1
- package/dist/{chunk-GPWU3PPR.cjs → chunk-VEHZNQ77.cjs} +30 -30
- package/dist/{chunk-GPWU3PPR.cjs.map → chunk-VEHZNQ77.cjs.map} +1 -1
- package/dist/{chunk-RCLJSENK.cjs → chunk-WUZQYZXI.cjs} +6 -6
- package/dist/{chunk-RCLJSENK.cjs.map → chunk-WUZQYZXI.cjs.map} +1 -1
- package/dist/{chunk-CIUYHBAY.cjs → chunk-YOYHJQX3.cjs} +5 -5
- package/dist/{chunk-CIUYHBAY.cjs.map → chunk-YOYHJQX3.cjs.map} +1 -1
- package/dist/{chunk-FCVAVOXS.js → chunk-ZBVMPVJF.js} +3 -3
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/embeddable-chat.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-unified-chat.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/chat/nav-link-anchor-via-runtime.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +9 -2
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/chat/utils/chat-nav-resolution.d.ts +9 -3
- package/dist/components/chat/utils/chat-nav-resolution.d.ts.map +1 -1
- package/dist/components/chat/utils/execute-navigation.d.ts +11 -0
- package/dist/components/chat/utils/execute-navigation.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +1 -1
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/nav-anchor-props.d.ts +4 -0
- package/dist/components/chat/utils/nav-anchor-props.d.ts.map +1 -1
- package/dist/components/chat/utils/resolve-fetched-card-href.d.ts +42 -3
- package/dist/components/chat/utils/resolve-fetched-card-href.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +8 -8
- package/dist/components/docs/index.js +7 -7
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +24 -24
- package/dist/components/help-center-pages/index.js +15 -15
- package/dist/components/index.cjs +136 -136
- package/dist/components/index.js +15 -15
- package/dist/components/meeting-scheduler/index.cjs +31 -31
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +7 -7
- package/dist/components/navigation/index.js +6 -6
- package/dist/components/onboarding-guides/index.cjs +7 -7
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +8 -8
- package/dist/components/tickets/index.js +7 -7
- package/dist/components/ui/index.cjs +4 -4
- package/dist/components/ui/index.js +3 -3
- package/dist/index.cjs +8 -8
- package/dist/index.js +7 -7
- package/dist/utils/content-href.d.ts +26 -5
- package/dist/utils/content-href.d.ts.map +1 -1
- package/dist/utils/index.cjs +1 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/embeddable-chat.tsx +9 -2
- package/src/components/chat/entity-cards/.dispatch.md +1 -1
- package/src/components/chat/entity-cards/dispatch.tsx +43 -23
- package/src/components/chat/hooks/.use-unified-chat.md +5 -1
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +4 -7
- package/src/components/chat/hooks/use-unified-chat.ts +8 -2
- package/src/components/chat/nav-link-anchor-via-runtime.tsx +8 -10
- package/src/components/chat/types/.unified-chat-state.types.md +2 -1
- package/src/components/chat/types/unified-chat-state.types.ts +9 -2
- package/src/components/chat/utils/__tests__/execute-navigation.test.ts +190 -0
- package/src/components/chat/utils/__tests__/resolve-fetched-card-href.test.ts +75 -2
- package/src/components/chat/utils/chat-nav-resolution.ts +9 -3
- package/src/components/chat/utils/execute-navigation.ts +32 -2
- package/src/components/chat/utils/index.ts +1 -1
- package/src/components/chat/utils/nav-anchor-props.ts +4 -2
- package/src/components/chat/utils/resolve-fetched-card-href.ts +50 -2
- package/src/utils/.content-href.md +1 -1
- package/src/utils/__tests__/content-href.test.ts +3 -0
- package/src/utils/content-href.ts +30 -3
- package/src/utils/index.ts +1 -0
- package/dist/chunk-4D2BR756.js.map +0 -1
- package/dist/chunk-75X6BRID.cjs.map +0 -1
- package/dist/chunk-JQI726T3.js.map +0 -1
- package/dist/chunk-MIY5FYX3.cjs.map +0 -1
- package/dist/chunk-NP3XN6ZX.js.map +0 -1
- package/dist/components/chat/utils/nav-click-handler.d.ts +0 -24
- package/dist/components/chat/utils/nav-click-handler.d.ts.map +0 -1
- package/src/components/chat/utils/.nav-click-handler.md +0 -43
- package/src/components/chat/utils/nav-click-handler.ts +0 -34
- /package/dist/{chunk-RUJKOU5F.js.map → chunk-5IAGT5LB.js.map} +0 -0
- /package/dist/{chunk-TU32TSAK.js.map → chunk-5NG2P2KI.js.map} +0 -0
- /package/dist/{chunk-U3SDSA5K.js.map → chunk-AJ67T7LI.js.map} +0 -0
- /package/dist/{chunk-THDGDYJ3.js.map → chunk-BFJBE4PQ.js.map} +0 -0
- /package/dist/{chunk-ZBJ6XSDO.js.map → chunk-ESXUEMPA.js.map} +0 -0
- /package/dist/{chunk-LYOGSVFO.js.map → chunk-IBSOYTGZ.js.map} +0 -0
- /package/dist/{chunk-AQ5MZVYN.js.map → chunk-IWK2NFVR.js.map} +0 -0
- /package/dist/{chunk-2YCUEUIZ.js.map → chunk-JJH4H4CJ.js.map} +0 -0
- /package/dist/{chunk-RSMUAAPW.js.map → chunk-KLG3MGRP.js.map} +0 -0
- /package/dist/{chunk-RNUH5IHZ.js.map → chunk-KPE277X2.js.map} +0 -0
- /package/dist/{chunk-IQFXDS5O.js.map → chunk-LEUJNBWU.js.map} +0 -0
- /package/dist/{chunk-JCH6HGQD.js.map → chunk-MCWAZ5XI.js.map} +0 -0
- /package/dist/{chunk-NEKT67O5.js.map → chunk-QHGARH6P.js.map} +0 -0
- /package/dist/{chunk-4KR7JWKB.js.map → chunk-SHJXC7A4.js.map} +0 -0
- /package/dist/{chunk-FCVAVOXS.js.map → chunk-ZBVMPVJF.js.map} +0 -0
package/package.json
CHANGED
|
@@ -98,9 +98,9 @@ import type { UnifiedChatState } from './types/unified-chat-state.types';
|
|
|
98
98
|
import { formatChatAttachmentMarkdownForBubble } from './utils/chat-attachment-markdown';
|
|
99
99
|
import { resolveHrefForRuntime } from './utils/chat-nav-resolution';
|
|
100
100
|
import { chatChipClass } from './utils/chip-styles';
|
|
101
|
+
import { executeNavigation } from './utils/execute-navigation';
|
|
101
102
|
import { resolveIcon } from './utils/icon-library';
|
|
102
103
|
import { computeIsNewTab, newTabAnchorAttrs } from './utils/nav-anchor-props';
|
|
103
|
-
import { handleChatNavClick } from './utils/nav-click-handler';
|
|
104
104
|
import { formatSingularLookupInvocation } from './utils/slash-dispatch-utils';
|
|
105
105
|
import { getSourceIconName } from './utils/source-icons';
|
|
106
106
|
import { resolveSourceRowCTA, sourceRowCtxFromRuntime } from './utils/source-row-cta';
|
|
@@ -732,7 +732,14 @@ function SourceChip({
|
|
|
732
732
|
const buildClickHandler =
|
|
733
733
|
(href: string, path: string | null | undefined, targetPlatform: string | null, isNewTab: boolean) =>
|
|
734
734
|
(e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
735
|
-
const handled =
|
|
735
|
+
const handled = executeNavigation({
|
|
736
|
+
event: e,
|
|
737
|
+
runtime,
|
|
738
|
+
href,
|
|
739
|
+
path,
|
|
740
|
+
targetPlatform,
|
|
741
|
+
fallbackNavigate: router.push,
|
|
742
|
+
});
|
|
736
743
|
if (handled && !isNewTab && onClose) onClose();
|
|
737
744
|
};
|
|
738
745
|
|
|
@@ -52,5 +52,5 @@ const card = renderChatInlineEntityCard(chatRef, {
|
|
|
52
52
|
## Related Files
|
|
53
53
|
|
|
54
54
|
- [`chat-ref.types.ts`](https://github.com/flamingo-stack/openframe-oss-lib/blob/main/src/types/chat-ref.types.ts) — `ChatRef` shape
|
|
55
|
-
- [`
|
|
55
|
+
- [`execute-navigation.ts`](https://github.com/flamingo-stack/openframe-oss-lib/blob/main/src/utils/execute-navigation.ts) — `executeNavigation`
|
|
56
56
|
- [`product-release-card-defaults.ts`](https://github.com/flamingo-stack/openframe-oss-lib/blob/main/src/components/cards/product-release-card-defaults.ts) — Fallback prop builder
|
|
@@ -83,10 +83,10 @@ import type { BaseProgramItem, ProgramConfig } from '../types/entities/program-t
|
|
|
83
83
|
import { getStatusColorScheme } from '../utils/agent-status-message';
|
|
84
84
|
import { resolveHrefForRuntime } from '../utils/chat-nav-resolution';
|
|
85
85
|
import { safeHref } from '../utils/compact-card-classes';
|
|
86
|
+
import { executeNavigation } from '../utils/execute-navigation';
|
|
86
87
|
import { clickupTaskUrl } from '../utils/external-app-urls';
|
|
87
88
|
import { computeIsNewTab, buildAnchorProps } from '../utils/nav-anchor-props';
|
|
88
|
-
import {
|
|
89
|
-
import { resolveFetchedCardHref, readFetchedCardTitle } from '../utils/resolve-fetched-card-href';
|
|
89
|
+
import { resolveFetchedCardHref, pickFetchedCardHref, readFetchedCardTitle } from '../utils/resolve-fetched-card-href';
|
|
90
90
|
import { getSourceLabel } from '../utils/source-icons';
|
|
91
91
|
import { resolveSourceRowCTA, resolveSourceIcon, sourceRowCtxFromRuntime } from '../utils/source-row-cta';
|
|
92
92
|
import { BlockCard } from './block-card';
|
|
@@ -1215,13 +1215,18 @@ interface ChatCardRegistryEntry {
|
|
|
1215
1215
|
* non-null `href`, the pre-computed `isNewTab` reflects the actual
|
|
1216
1216
|
* destination, and the click goes through `handleChatNavClick` (no
|
|
1217
1217
|
* silent bypass of embed-mode / close-on-nav). Takes precedence over
|
|
1218
|
-
* the `composeContentUrl` fallback below
|
|
1218
|
+
* the `composeContentUrl` fallback below — but NOT over an explicit
|
|
1219
|
+
* host `override` for the type (see `pickFetchedCardHref`). */
|
|
1219
1220
|
fallbackHref?: (item: ChatCardItem) => string | null;
|
|
1220
|
-
/** Opt OUT of the post-fetch `composeContentUrl`
|
|
1221
|
+
/** Opt OUT of the post-fetch `composeContentUrl` SYNTHESIS (see
|
|
1221
1222
|
* `resolveFetchedCardHref`). Set for types with NO public destination
|
|
1222
1223
|
* — the seam would otherwise synthesize `<contentOrigin>/<type>/<id>`
|
|
1223
|
-
* from its default branch and hand the user a 404.
|
|
1224
|
-
*
|
|
1224
|
+
* from its default branch and hand the user a 404.
|
|
1225
|
+
*
|
|
1226
|
+
* It does NOT opt out of the seam itself: an explicit host `override`
|
|
1227
|
+
* for the type still wins (`pickFetchedCardHref`). That split is the
|
|
1228
|
+
* point — a host that re-homes a type must be able to say so even when
|
|
1229
|
+
* the row already carries the content host's own url. */
|
|
1225
1230
|
noComposedHref?: boolean;
|
|
1226
1231
|
}
|
|
1227
1232
|
|
|
@@ -1234,6 +1239,12 @@ interface ChatCardRegistryEntry {
|
|
|
1234
1239
|
* (`fallbackHref`), no `<origin>/<type>/<id>` synthesis
|
|
1235
1240
|
* (`noComposedHref`). Loading shows the skeleton, exactly like every
|
|
1236
1241
|
* other fetch card.
|
|
1242
|
+
*
|
|
1243
|
+
* NOTE `item.url` is minted by the CONTENT host for its OWN surface, so an
|
|
1244
|
+
* embedder that serves the same rows elsewhere overrides the type in its
|
|
1245
|
+
* `composeContentUrl` config and that wins here — see `pickFetchedCardHref`.
|
|
1246
|
+
* (Observed: hub tickets are `/tickets?ticket=…`, which in the OpenFrame
|
|
1247
|
+
* console resolved against its own unrelated `/tickets` board.)
|
|
1237
1248
|
*/
|
|
1238
1249
|
function refHydratedEntry(
|
|
1239
1250
|
docType: string,
|
|
@@ -1667,7 +1678,7 @@ function ChatCardNavWrap({
|
|
|
1667
1678
|
if (targetEl?.closest?.('button')) return;
|
|
1668
1679
|
if (!targetEl?.closest?.('a')) return;
|
|
1669
1680
|
|
|
1670
|
-
const handled =
|
|
1681
|
+
const handled = executeNavigation({ event: e, runtime, href, path, targetPlatform, fallbackNavigate: router.push });
|
|
1671
1682
|
if (!handled) return;
|
|
1672
1683
|
// Modifier-clicks fall through (handled=false) without stopPropagation
|
|
1673
1684
|
// so ancestor telemetry handlers still see the bubble.
|
|
@@ -1763,16 +1774,22 @@ export function ChatCardLoader({
|
|
|
1763
1774
|
// visit. `safeHref` blocks `javascript:` / `data:` payloads even
|
|
1764
1775
|
// though the registry callers compose hub-internal strings today.
|
|
1765
1776
|
//
|
|
1766
|
-
// Two
|
|
1777
|
+
// Two producers, ranked by `pickFetchedCardHref` (which owns the
|
|
1778
|
+
// precedence and its rationale):
|
|
1767
1779
|
// 1. the registry's per-type `fallbackHref` — an explicit non-content
|
|
1768
|
-
// destination (marketing campaign → `/admin
|
|
1780
|
+
// destination (marketing campaign → `/admin/...`, or a ref-hydrated
|
|
1781
|
+
// row's own `item.url`);
|
|
1769
1782
|
// 2. the host's `composeContentUrl` seam via `resolveFetchedCardHref` —
|
|
1770
1783
|
// the SAME resolver page cards go through. This is what makes cards
|
|
1771
1784
|
// clickable on every transport: the wire ships bare
|
|
1772
1785
|
// `[card://type:id]` markers with no metadata, so the ref reaches
|
|
1773
1786
|
// us with `url: null` and `resolveSourceRowCTA` has nothing to route.
|
|
1787
|
+
// …except an EXPLICIT host override outranks both: the seam is asked
|
|
1788
|
+
// even for `fallbackHref` / `noComposedHref` types now, and its answer is
|
|
1789
|
+
// taken only when the host actually re-homed the type (never when the
|
|
1790
|
+
// composer merely synthesized one) — the case those two flags guard against.
|
|
1774
1791
|
const composedHref =
|
|
1775
|
-
fetchEntry?.contentRefType && !resolvedChatRef.url && item
|
|
1792
|
+
fetchEntry?.contentRefType && !resolvedChatRef.url && item
|
|
1776
1793
|
? resolveFetchedCardHref({
|
|
1777
1794
|
contentRefType: fetchEntry.contentRefType,
|
|
1778
1795
|
id: resolvedChatRef.id,
|
|
@@ -1780,19 +1797,22 @@ export function ChatCardLoader({
|
|
|
1780
1797
|
composeContentUrl: runtime.composeContentUrl,
|
|
1781
1798
|
})
|
|
1782
1799
|
: null;
|
|
1783
|
-
const
|
|
1784
|
-
fetchEntry && !resolvedChatRef.url && item
|
|
1785
|
-
? {
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1800
|
+
const hrefChoice =
|
|
1801
|
+
fetchEntry && !resolvedChatRef.url && item
|
|
1802
|
+
? pickFetchedCardHref({
|
|
1803
|
+
composed: composedHref,
|
|
1804
|
+
itemHref: fetchEntry.fallbackHref?.(item) ?? null,
|
|
1805
|
+
allowComposed: !fetchEntry.noComposedHref,
|
|
1806
|
+
})
|
|
1807
|
+
: null;
|
|
1808
|
+
const hrefResolvedChatRef: ChatRef = hrefChoice
|
|
1809
|
+
? {
|
|
1810
|
+
...resolvedChatRef,
|
|
1811
|
+
url: safeHref(hrefChoice.href),
|
|
1812
|
+
// The `item` branch carries no platform of its own — keep the ref's.
|
|
1813
|
+
targetPlatform: hrefChoice.targetPlatform ?? resolvedChatRef.targetPlatform ?? null,
|
|
1814
|
+
}
|
|
1815
|
+
: resolvedChatRef;
|
|
1796
1816
|
|
|
1797
1817
|
// Title enrichment, same synthetic-ref gap as the href above: a Mingo
|
|
1798
1818
|
// `[card://type:id]` marker produces `title: <id>` because the transport
|
|
@@ -26,7 +26,11 @@ Hook input shape.
|
|
|
26
26
|
### `useUnifiedChat(options)`
|
|
27
27
|
Main hook. Returns a stable `UnifiedChatState` composed of:
|
|
28
28
|
- **Active adapter state** — messages, streaming phase, token usage, connection
|
|
29
|
-
- **Stable action callbacks** — `sendMessage`, `stopMessage`, `clearMessages`, `discussRef`,
|
|
29
|
+
- **Stable action callbacks** — `sendMessage`, `stopMessage`, `clearMessages`, `discussRef`, and
|
|
30
|
+
`displayRef` **only when the active adapter has one**. `displayRef` is optional on the state and
|
|
31
|
+
the NATS/Mingo adapters omit it, because consumers gate the card menu's "Display" row on the
|
|
32
|
+
callback being there; forwarding an always-truthy wrapper around a missing method would advertise
|
|
33
|
+
a display path that isn't there and throw on click.
|
|
30
34
|
- **Dialog management** — `selectDialog`, `startNewDialog`, `deleteDialog`, `renameDialog`, `archiveDialog`, `loadMoreDialogs`, `setDialogScope`, `reloadDialogs`, `loadMoreMessages`
|
|
31
35
|
- **Approvals** — `approveRequest`, `rejectRequest`
|
|
32
36
|
|
|
@@ -1093,11 +1093,10 @@ export function useNatsChatAdapter(
|
|
|
1093
1093
|
},
|
|
1094
1094
|
[sendMessage],
|
|
1095
1095
|
);
|
|
1096
|
-
//
|
|
1097
|
-
//
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
}, []);
|
|
1096
|
+
// No `displayRef` here: it dispatches a `/… display "<x>"` slash command and
|
|
1097
|
+
// the agent transport has no slash-command registry. OMITTED rather than
|
|
1098
|
+
// stubbed — the card menu gates on the callback's presence, so a stub costs
|
|
1099
|
+
// the row "Ask Mingo", which does work in Mingo mode.
|
|
1101
1100
|
|
|
1102
1101
|
// ─── Return shape ─────────────────────────────────────────────────────────
|
|
1103
1102
|
|
|
@@ -1115,7 +1114,6 @@ export function useNatsChatAdapter(
|
|
|
1115
1114
|
stopMessage,
|
|
1116
1115
|
clearMessages,
|
|
1117
1116
|
discussRef,
|
|
1118
|
-
displayRef,
|
|
1119
1117
|
// Model badge + token usage for the composer's `<ModelDisplay>`.
|
|
1120
1118
|
// Model: live `metadata` frame → config baseline (host AI-config).
|
|
1121
1119
|
// Tokens: live `token_usage` frame / dialog snapshot (`dialogTokenUsage`).
|
|
@@ -1160,7 +1158,6 @@ export function useNatsChatAdapter(
|
|
|
1160
1158
|
stopMessage,
|
|
1161
1159
|
clearMessages,
|
|
1162
1160
|
discussRef,
|
|
1163
|
-
displayRef,
|
|
1164
1161
|
dialogs,
|
|
1165
1162
|
dialogId,
|
|
1166
1163
|
selectDialog,
|
|
@@ -153,10 +153,16 @@ export function useUnifiedChat(options: UseUnifiedChatOptions): UnifiedChatState
|
|
|
153
153
|
(ref: Parameters<UnifiedChatState['discussRef']>[0]) => activeStateRef.current.discussRef(ref),
|
|
154
154
|
[],
|
|
155
155
|
);
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
// `displayRef` is OPTIONAL on the state and consumers gate the "Display"
|
|
157
|
+
// affordance on its presence, so the forward has to be able to be absent
|
|
158
|
+
// too: an always-truthy wrapper around a missing method advertises a
|
|
159
|
+
// display path that isn't there and throws when the menu row is clicked.
|
|
160
|
+
// Only the flag flips identity — the wrapper itself stays stable.
|
|
161
|
+
const displayRefForward = useCallback(
|
|
162
|
+
(ref: Parameters<NonNullable<UnifiedChatState['displayRef']>>[0]) => activeStateRef.current.displayRef?.(ref),
|
|
158
163
|
[],
|
|
159
164
|
);
|
|
165
|
+
const displayRef = activeState.displayRef ? displayRefForward : undefined;
|
|
160
166
|
|
|
161
167
|
// Dialog-management forwards — one thin wrapper per action so the
|
|
162
168
|
// returned identity stays stable as long as the active adapter's
|
|
@@ -27,8 +27,8 @@ import { useRequiredChatRuntime } from '../../contexts/chat-runtime-context';
|
|
|
27
27
|
import { useRouter } from '../../embed-shims/next-navigation';
|
|
28
28
|
import { useChatPanel } from './chat-panel-context';
|
|
29
29
|
import { resolveHrefForRuntime } from './utils/chat-nav-resolution';
|
|
30
|
+
import { executeNavigation } from './utils/execute-navigation';
|
|
30
31
|
import { computeIsNewTab, newTabAnchorAttrs } from './utils/nav-anchor-props';
|
|
31
|
-
import { handleChatNavClick } from './utils/nav-click-handler';
|
|
32
32
|
|
|
33
33
|
export interface NavLinkAnchorViaRuntimeProps {
|
|
34
34
|
href: string;
|
|
@@ -54,16 +54,14 @@ export function NavLinkAnchorViaRuntime({
|
|
|
54
54
|
const isNewTab = computeIsNewTab(runtime, resolvedHref, targetPlatform ?? null);
|
|
55
55
|
|
|
56
56
|
const onClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
|
57
|
-
const handled =
|
|
58
|
-
e,
|
|
57
|
+
const handled = executeNavigation({
|
|
58
|
+
event: e,
|
|
59
59
|
runtime,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
router.push,
|
|
66
|
-
);
|
|
60
|
+
href: resolvedHref,
|
|
61
|
+
path,
|
|
62
|
+
targetPlatform,
|
|
63
|
+
fallbackNavigate: router.push,
|
|
64
|
+
});
|
|
67
65
|
if (handled && !isNewTab && panel?.closeChat) panel.closeChat();
|
|
68
66
|
};
|
|
69
67
|
return (
|
|
@@ -51,6 +51,7 @@ function MyChatShell({ chat }: { chat: UnifiedChatState }) {
|
|
|
51
51
|
| `currentProvider` / `currentModelLabel` | ✅ Populated | `null` |
|
|
52
52
|
| `currentUsageBreakdown` | ✅ After trailing frame | `null` |
|
|
53
53
|
| `DialogTokenUsage` (separate) | `null` | ✅ From GraphQL |
|
|
54
|
-
| `discussRef`
|
|
54
|
+
| `discussRef` | ✅ Active | ✅ Active (prompt carries the reference as text) |
|
|
55
|
+
| `displayRef` | ✅ Active | **omitted** (optional — consumers fall back to "Ask Mingo") |
|
|
55
56
|
|
|
56
57
|
> **Source:** [`unified-chat-state.types.ts`](https://github.com/flamingo-stack/openframe-oss-lib/blob/main/unified-chat-state.types.ts)
|
|
@@ -286,9 +286,16 @@ export interface UnifiedChatState {
|
|
|
286
286
|
|
|
287
287
|
/**
|
|
288
288
|
* Trigger the chat's "Display this row" affordance — same as `discussRef`
|
|
289
|
-
* but for read-only card display rather than a question. Guide-mode only
|
|
289
|
+
* but for read-only card display rather than a question. Guide-mode only:
|
|
290
|
+
* it dispatches a `/<cmd> display "<x>"` slash command, and only the SSE
|
|
291
|
+
* transport has a slash-command registry.
|
|
292
|
+
*
|
|
293
|
+
* OPTIONAL, and OMITTED — not stubbed — by adapters that have no display
|
|
294
|
+
* path. Consumers gate the affordance on its presence (`entity-cards`'
|
|
295
|
+
* dispatcher falls back to "Ask Mingo" when it is absent), so a no-op stub
|
|
296
|
+
* renders a dead "Display" row where the action that works belongs.
|
|
290
297
|
*/
|
|
291
|
-
displayRef
|
|
298
|
+
displayRef?: (ref: ChatRef) => void;
|
|
292
299
|
|
|
293
300
|
// ─── Per-turn LLM metadata (Guide/SSE only) ───────────────────────────────
|
|
294
301
|
/**
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { ChatRuntime } from '../../../../contexts/chat-runtime-context';
|
|
3
|
+
import { stripSameOriginToPath } from '../chat-nav-resolution';
|
|
4
|
+
import { decideNewTab } from '../decide-new-tab';
|
|
5
|
+
import { executeNavigation } from '../execute-navigation';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The click path had no tests, which is why every host bug it produced was
|
|
9
|
+
* answered with another defensive layer instead of a case here. These pin the
|
|
10
|
+
* two decisions a click makes — WHICH TAB, and WHO PERFORMS IT.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** jsdom's own origin — the strip is a comparison against it, so it must be real. */
|
|
14
|
+
const ORIGIN = new URL(window.location.href).origin;
|
|
15
|
+
|
|
16
|
+
function runtimeWith(navigation: Partial<ChatRuntime['navigation']> = {}): ChatRuntime {
|
|
17
|
+
return {
|
|
18
|
+
endpoints: { chatStreamUrl: `${ORIGIN}/chat` },
|
|
19
|
+
navigation: { mode: 'host', ...navigation },
|
|
20
|
+
source: 'openframe',
|
|
21
|
+
} as ChatRuntime;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function primaryClick() {
|
|
25
|
+
return { preventDefault: vi.fn(), button: 0 };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
window.history.replaceState(null, '', '/dashboard');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('stripSameOriginToPath', () => {
|
|
33
|
+
it('reduces a same-origin absolute URL to a router-pushable path', () => {
|
|
34
|
+
expect(stripSameOriginToPath(`${ORIGIN}/help-center/guides?slug=a#top`)).toBe('/help-center/guides?slug=a#top');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('strips a CROSS-origin URL too — the caller owns that decision', () => {
|
|
38
|
+
// Deliberate: the hub is one app across several origins and relies on this to
|
|
39
|
+
// keep an absolute in-app href on the host it is running (preview, localhost).
|
|
40
|
+
// The "is this foreign?" question belongs to `runNavigation`, tested below.
|
|
41
|
+
expect(stripSameOriginToPath('https://www.flamingo.run/webinars/x')).toBe('/webinars/x');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('passes relative hrefs through untouched', () => {
|
|
45
|
+
expect(stripSameOriginToPath('/devices/details?id=1')).toBe('/devices/details?id=1');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('decideNewTab', () => {
|
|
50
|
+
it('keeps a relative href in the same tab', () => {
|
|
51
|
+
expect(decideNewTab({ href: '/help-center/guides', currentSource: 'openframe' })).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('sends a cross-origin href to a new tab', () => {
|
|
55
|
+
expect(decideNewTab({ href: 'https://www.flamingo.run/blog/x', currentSource: 'openframe' })).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('lets a matching targetPlatform beat the origin compare — same tab, cross-origin', () => {
|
|
59
|
+
// The trap openframe hit: the RAG tags every row `openframe`, so a hub URL
|
|
60
|
+
// matched `source` and went same-tab. Pinned because it is surprising, and
|
|
61
|
+
// because hosts that do NOT host that content must pass `targetPlatform: null`.
|
|
62
|
+
expect(
|
|
63
|
+
decideNewTab({
|
|
64
|
+
href: 'https://www.flamingo.run/webinars/x',
|
|
65
|
+
targetPlatform: 'openframe',
|
|
66
|
+
currentSource: 'openframe',
|
|
67
|
+
}),
|
|
68
|
+
).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('forces a new tab in embed mode regardless of origin', () => {
|
|
72
|
+
expect(decideNewTab({ href: '/anything', currentSource: 'openframe', runtimeMode: 'embed' })).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('executeNavigation', () => {
|
|
77
|
+
it('hands a same-tab click to the host navigate callback', () => {
|
|
78
|
+
const navigate = vi.fn().mockReturnValue(true);
|
|
79
|
+
const fallbackNavigate = vi.fn();
|
|
80
|
+
const event = primaryClick();
|
|
81
|
+
|
|
82
|
+
const handled = executeNavigation({
|
|
83
|
+
event,
|
|
84
|
+
runtime: runtimeWith({ navigate }),
|
|
85
|
+
href: '/help-center/guides',
|
|
86
|
+
fallbackNavigate,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(handled).toBe(true);
|
|
90
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
91
|
+
expect(navigate).toHaveBeenCalledWith(expect.objectContaining({ href: '/help-center/guides' }));
|
|
92
|
+
expect(fallbackNavigate).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('falls back to the host router when navigate declines', () => {
|
|
96
|
+
const fallbackNavigate = vi.fn();
|
|
97
|
+
|
|
98
|
+
executeNavigation({
|
|
99
|
+
event: primaryClick(),
|
|
100
|
+
runtime: runtimeWith({ navigate: () => false }),
|
|
101
|
+
href: '/help-center/guides',
|
|
102
|
+
fallbackNavigate,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(fallbackNavigate).toHaveBeenCalledWith('/help-center/guides');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('opens an absolute SAME-origin href in a new tab', () => {
|
|
109
|
+
// Not a defect: `isCrossOriginUrl` is textual on purpose (SSR-deterministic,
|
|
110
|
+
// see its tests), so "in-app links stay relative" is a real host obligation.
|
|
111
|
+
// Pinned because hosts keep rediscovering it by absolutizing an in-app href
|
|
112
|
+
// and getting a redundant new tab.
|
|
113
|
+
const openExternal = vi.fn();
|
|
114
|
+
const fallbackNavigate = vi.fn();
|
|
115
|
+
|
|
116
|
+
executeNavigation({
|
|
117
|
+
event: primaryClick(),
|
|
118
|
+
runtime: runtimeWith({ openExternal }),
|
|
119
|
+
href: `${ORIGIN}/help-center/guides`,
|
|
120
|
+
fallbackNavigate,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(openExternal).toHaveBeenCalledWith(`${ORIGIN}/help-center/guides`);
|
|
124
|
+
expect(fallbackNavigate).not.toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('still strips an absolute SAME-origin href down to a path', () => {
|
|
128
|
+
// The hub's soft-RSC-nav case: one app, several origins, absolute in-app
|
|
129
|
+
// href routed same-tab. It must stay a soft nav, not a full page load.
|
|
130
|
+
const fallbackNavigate = vi.fn();
|
|
131
|
+
|
|
132
|
+
executeNavigation({
|
|
133
|
+
event: primaryClick(),
|
|
134
|
+
runtime: runtimeWith({ navigate: () => false, decideNewTab: () => false }),
|
|
135
|
+
href: `${ORIGIN}/releases/v1?x=1#top`,
|
|
136
|
+
fallbackNavigate,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(fallbackNavigate).toHaveBeenCalledWith('/releases/v1?x=1#top');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('passes a cross-origin href to the router WHOLE', () => {
|
|
143
|
+
// Regression: it used to arrive origin-stripped and 404 locally.
|
|
144
|
+
const fallbackNavigate = vi.fn();
|
|
145
|
+
|
|
146
|
+
executeNavigation({
|
|
147
|
+
event: primaryClick(),
|
|
148
|
+
runtime: runtimeWith({ navigate: () => false, decideNewTab: () => false }),
|
|
149
|
+
href: 'https://www.flamingo.run/webinars/x',
|
|
150
|
+
fallbackNavigate,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(fallbackNavigate).toHaveBeenCalledWith('https://www.flamingo.run/webinars/x');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('routes a new-tab decision through openExternal', () => {
|
|
157
|
+
const openExternal = vi.fn();
|
|
158
|
+
const fallbackNavigate = vi.fn();
|
|
159
|
+
|
|
160
|
+
executeNavigation({
|
|
161
|
+
event: primaryClick(),
|
|
162
|
+
runtime: runtimeWith({ openExternal, decideNewTab: () => true }),
|
|
163
|
+
href: 'https://www.flamingo.run/blog/x',
|
|
164
|
+
fallbackNavigate,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(openExternal).toHaveBeenCalledWith('https://www.flamingo.run/blog/x');
|
|
168
|
+
expect(fallbackNavigate).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('leaves modifier clicks to the browser', () => {
|
|
172
|
+
const navigate = vi.fn();
|
|
173
|
+
const event = { preventDefault: vi.fn(), button: 0, metaKey: true };
|
|
174
|
+
|
|
175
|
+
const handled = executeNavigation({ event, runtime: runtimeWith({ navigate }), href: '/x' });
|
|
176
|
+
|
|
177
|
+
expect(handled).toBe(false);
|
|
178
|
+
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
179
|
+
expect(navigate).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('reports an empty href as handled without navigating anywhere', () => {
|
|
183
|
+
const fallbackNavigate = vi.fn();
|
|
184
|
+
const event = primaryClick();
|
|
185
|
+
|
|
186
|
+
expect(executeNavigation({ event, runtime: runtimeWith(), href: '', fallbackNavigate })).toBe(true);
|
|
187
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
188
|
+
expect(fallbackNavigate).not.toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { makeComposeContentUrl } from '../../../../utils/content-href';
|
|
3
|
-
import { resolveFetchedCardHref } from '../resolve-fetched-card-href';
|
|
3
|
+
import { pickFetchedCardHref, resolveFetchedCardHref } from '../resolve-fetched-card-href';
|
|
4
4
|
|
|
5
5
|
const HUB = 'https://www.flamingo.run';
|
|
6
6
|
|
|
@@ -34,7 +34,7 @@ describe('resolveFetchedCardHref — Mingo cards with no ref metadata', () => {
|
|
|
34
34
|
item: { id: 'db-uuid-9', title: 'Deep Google Workspace tenant management' },
|
|
35
35
|
composeContentUrl: compose,
|
|
36
36
|
}),
|
|
37
|
-
).toEqual({ href: '/help-center/roadmap?search=86ad3qvv5', targetPlatform: null });
|
|
37
|
+
).toEqual({ href: '/help-center/roadmap?search=86ad3qvv5', targetPlatform: null, hostOverride: true });
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
it('hosted type uses the fetched row slug for its in-app detail route', () => {
|
|
@@ -152,3 +152,76 @@ describe('makeComposeContentUrl — the new `slug` hint', () => {
|
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
describe('pickFetchedCardHref — which producer owns a fetched card destination', () => {
|
|
157
|
+
const composedPlain = { href: `${HUB}/tickets/47979601418`, targetPlatform: null };
|
|
158
|
+
const composedOverride = {
|
|
159
|
+
href: '/help-center/tickets?ticket=479&search=479#ticket-479',
|
|
160
|
+
targetPlatform: null,
|
|
161
|
+
hostOverride: true,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
it('an explicit host override beats the fetched row url', () => {
|
|
165
|
+
// The regression: `hubspot_ticket_self` hydrates from the hub, whose row
|
|
166
|
+
// carries `/tickets?ticket=…` — a path the OpenFrame console also owns, for
|
|
167
|
+
// an unrelated ticket board. The host re-homes the type; that has to win.
|
|
168
|
+
expect(
|
|
169
|
+
pickFetchedCardHref({
|
|
170
|
+
composed: composedOverride,
|
|
171
|
+
itemHref: '/tickets?ticket=479&search=479#ticket-479',
|
|
172
|
+
allowComposed: false,
|
|
173
|
+
}),
|
|
174
|
+
).toEqual({
|
|
175
|
+
source: 'hostOverride',
|
|
176
|
+
href: '/help-center/tickets?ticket=479&search=479#ticket-479',
|
|
177
|
+
targetPlatform: null,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('the row url still beats a merely SYNTHESIZED href — what noComposedHref guards', () => {
|
|
182
|
+
expect(
|
|
183
|
+
pickFetchedCardHref({ composed: composedPlain, itemHref: '/tickets?ticket=479', allowComposed: false }),
|
|
184
|
+
).toEqual({ source: 'item', href: '/tickets?ticket=479' });
|
|
185
|
+
// …and even when synthesis is allowed, a row that knows its own url wins.
|
|
186
|
+
expect(
|
|
187
|
+
pickFetchedCardHref({ composed: composedPlain, itemHref: '/tickets?ticket=479', allowComposed: true }),
|
|
188
|
+
).toEqual({ source: 'item', href: '/tickets?ticket=479' });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('falls through to the composed href only when the registry allows synthesis', () => {
|
|
192
|
+
expect(pickFetchedCardHref({ composed: composedPlain, itemHref: null, allowComposed: true })).toEqual({
|
|
193
|
+
source: 'composed',
|
|
194
|
+
href: `${HUB}/tickets/47979601418`,
|
|
195
|
+
targetPlatform: null,
|
|
196
|
+
});
|
|
197
|
+
expect(pickFetchedCardHref({ composed: composedPlain, itemHref: null, allowComposed: false })).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('nothing to link to → null (card renders unlinked)', () => {
|
|
201
|
+
expect(pickFetchedCardHref({ composed: null, itemHref: null, allowComposed: true })).toBeNull();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('an override with no host seam wired cannot appear — null composed, row url stands', () => {
|
|
205
|
+
expect(pickFetchedCardHref({ composed: null, itemHref: '/tickets?ticket=479', allowComposed: true })).toEqual({
|
|
206
|
+
source: 'item',
|
|
207
|
+
href: '/tickets?ticket=479',
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('makeComposeContentUrl — hostOverride flag', () => {
|
|
213
|
+
it('marks ONLY the explicit overrides branch', () => {
|
|
214
|
+
expect(compose({ type: 'roadmap_item', identifier: 'task-1' })).toEqual({
|
|
215
|
+
href: '/help-center/roadmap?search=task-1',
|
|
216
|
+
targetPlatform: null,
|
|
217
|
+
hostOverride: true,
|
|
218
|
+
});
|
|
219
|
+
// hosted, verbatim-externalUrl and origin-synthesis branches stay unflagged,
|
|
220
|
+
// so a fetched row's own url keeps beating them.
|
|
221
|
+
expect(compose({ type: 'product_release', identifier: 'rel-1', slug: 'v1' }).hostOverride).toBeUndefined();
|
|
222
|
+
expect(
|
|
223
|
+
compose({ type: 'blog_post', identifier: 'b-1', externalUrl: `${HUB}/blog/x` }).hostOverride,
|
|
224
|
+
).toBeUndefined();
|
|
225
|
+
expect(compose({ type: 'blog_post', identifier: 'b-1' }).hostOverride).toBeUndefined();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -32,9 +32,15 @@ export function isModifierClick(e: {
|
|
|
32
32
|
return e.button !== 0 || !!e.metaKey || !!e.ctrlKey || !!e.shiftKey || !!e.altKey;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/** Strip the origin from
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
/** Strip the origin from an absolute URL, returning a relative path the host's
|
|
36
|
+
* router will treat as in-app navigation. Already-relative URLs pass through.
|
|
37
|
+
*
|
|
38
|
+
* Strips the origin off ANY absolute URL, cross-origin included — the caller
|
|
39
|
+
* decides. The hub relies on that: its platforms are one Next app on several
|
|
40
|
+
* origins, so an absolute `https://openmsp.ai/x` opened same-tab must stay on
|
|
41
|
+
* whatever host is running (prod, a preview deploy, or localhost). Callers that
|
|
42
|
+
* can be handed a genuinely FOREIGN href must check the origin first, or they
|
|
43
|
+
* push a path that 404s on their own — see `runNavigation`. */
|
|
38
44
|
export function stripSameOriginToPath(href: string): string {
|
|
39
45
|
if (!href.startsWith('http')) return href;
|
|
40
46
|
try {
|
|
@@ -31,7 +31,12 @@
|
|
|
31
31
|
import type { ChatRuntime } from '../../../contexts/chat-runtime-context';
|
|
32
32
|
import { navigateSamePageHash, STICKY_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav';
|
|
33
33
|
import { NEW_TAB_FEATURES, stripSameOriginToPath } from './chat-nav-resolution';
|
|
34
|
-
import { computeIsNewTab } from './nav-anchor-props';
|
|
34
|
+
import { computeIsNewTab, isSameOriginAbsoluteHref } from './nav-anchor-props';
|
|
35
|
+
|
|
36
|
+
/** Absolute `http(s)` URL pointing at an origin that is not ours. */
|
|
37
|
+
function isForeignOriginHref(href: string): boolean {
|
|
38
|
+
return /^https?:\/\//i.test(href) && !isSameOriginAbsoluteHref(href);
|
|
39
|
+
}
|
|
35
40
|
|
|
36
41
|
/** Minimal mouse-event surface — structural so chip buttons / tiles can call it
|
|
37
42
|
* without casting `React.MouseEvent`. */
|
|
@@ -78,7 +83,12 @@ function runNavigation(
|
|
|
78
83
|
else window.open(href, '_blank', NEW_TAB_FEATURES);
|
|
79
84
|
return;
|
|
80
85
|
}
|
|
81
|
-
|
|
86
|
+
// A FOREIGN href that the host's `decideNewTab` routed same-tab must reach the
|
|
87
|
+
// router whole: stripping its origin pushes `/webinars/x` onto OUR origin and
|
|
88
|
+
// 404s. Same-origin absolutes still get stripped, so an in-app soft nav stays
|
|
89
|
+
// one. (`stripSameOriginToPath` itself strips unconditionally on purpose — the
|
|
90
|
+
// hub is one app across several origins and depends on that.)
|
|
91
|
+
const target = isForeignOriginHref(href) ? href : stripSameOriginToPath(href);
|
|
82
92
|
// Same-page hash target (e.g. a chat card deep-linking to `/faqs#faq-item-49`):
|
|
83
93
|
// route through the UNIFIED same-page-hash primitive FIRST. A host router /
|
|
84
94
|
// `router.push` performs `pushState`, which the HTML spec says does NOT fire a
|
|
@@ -139,3 +149,23 @@ export function executeNavigationImperative({
|
|
|
139
149
|
}: ExecuteNavigationImperativeArgs): void {
|
|
140
150
|
runNavigation(runtime, href, undefined, targetPlatform, fallbackNavigate);
|
|
141
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @deprecated Call {@link executeNavigation} directly. Kept only so external
|
|
155
|
+
* embedders' imports keep resolving; no lib code path goes through it.
|
|
156
|
+
*/
|
|
157
|
+
export function handleChatNavClick(
|
|
158
|
+
event: NavClickEvent,
|
|
159
|
+
runtime: ChatRuntime | null | undefined,
|
|
160
|
+
{ href, path, targetPlatform }: ChatNavClickInput,
|
|
161
|
+
fallbackNavigate?: (path: string) => void,
|
|
162
|
+
): boolean {
|
|
163
|
+
return executeNavigation({ event, runtime, href, path, targetPlatform, fallbackNavigate });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** @deprecated Argument shape of the deprecated {@link handleChatNavClick}. */
|
|
167
|
+
export interface ChatNavClickInput {
|
|
168
|
+
href: string;
|
|
169
|
+
path?: string | null;
|
|
170
|
+
targetPlatform?: string | null;
|
|
171
|
+
}
|