@flamingo-stack/openframe-frontend-core 0.0.423 → 0.0.424-snapshot.20260714200421
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-MSZ4WBCD.cjs → chunk-5DAVSIXQ.cjs} +21 -21
- package/dist/{chunk-MSZ4WBCD.cjs.map → chunk-5DAVSIXQ.cjs.map} +1 -1
- package/dist/{chunk-WWS3R3UD.js → chunk-5RFLFMCS.js} +4 -4
- package/dist/{chunk-4XLJWX2N.js → chunk-7EUIW46X.js} +2 -2
- package/dist/{chunk-JPZE2P4Y.js → chunk-7OMNZR2A.js} +6 -6
- package/dist/{chunk-7W4474XS.js → chunk-7TA6WMMV.js} +5 -5
- package/dist/{chunk-KNO5CB3W.cjs → chunk-BESGMTIN.cjs} +7 -7
- package/dist/{chunk-KNO5CB3W.cjs.map → chunk-BESGMTIN.cjs.map} +1 -1
- package/dist/{chunk-YOEQWHUD.js → chunk-BY3NKTNZ.js} +2 -2
- package/dist/{chunk-VFKQMAUF.cjs → chunk-C5RS3QLV.cjs} +3 -3
- package/dist/{chunk-VFKQMAUF.cjs.map → chunk-C5RS3QLV.cjs.map} +1 -1
- package/dist/{chunk-WOHMJJKD.cjs → chunk-C7AXZBFC.cjs} +29 -29
- package/dist/{chunk-WOHMJJKD.cjs.map → chunk-C7AXZBFC.cjs.map} +1 -1
- package/dist/{chunk-LK3KXZIN.js → chunk-CRPQEP7D.js} +3 -3
- package/dist/{chunk-ZKIYCRVQ.cjs → chunk-EE5NH5SH.cjs} +3 -3
- package/dist/{chunk-ZKIYCRVQ.cjs.map → chunk-EE5NH5SH.cjs.map} +1 -1
- package/dist/{chunk-SVUDKNS7.js → chunk-FBCN76VE.js} +3 -3
- package/dist/{chunk-IRIPRXO5.cjs → chunk-GA6MWC56.cjs} +14 -14
- package/dist/{chunk-IRIPRXO5.cjs.map → chunk-GA6MWC56.cjs.map} +1 -1
- package/dist/{chunk-FVI2ESVQ.cjs → chunk-HNRGQCTE.cjs} +112 -87
- package/dist/chunk-HNRGQCTE.cjs.map +1 -0
- package/dist/{chunk-B2255TIZ.js → chunk-IQW3LEUV.js} +29 -4
- package/dist/chunk-IQW3LEUV.js.map +1 -0
- package/dist/{chunk-GR2CMONR.js → chunk-LHWLXA4X.js} +7 -7
- package/dist/{chunk-2Q2SKYCZ.js → chunk-LKMUH4IO.js} +6 -6
- package/dist/{chunk-WO2NOBF3.cjs → chunk-LRDVU7NB.cjs} +43 -43
- package/dist/{chunk-WO2NOBF3.cjs.map → chunk-LRDVU7NB.cjs.map} +1 -1
- package/dist/{chunk-SR6LEWCF.cjs → chunk-N2TJ3HAM.cjs} +10 -10
- package/dist/{chunk-SR6LEWCF.cjs.map → chunk-N2TJ3HAM.cjs.map} +1 -1
- package/dist/{chunk-FFCGB73K.js → chunk-QXYOGJQJ.js} +2 -2
- package/dist/{chunk-NNF5XOMH.js → chunk-RAGHFNE7.js} +2 -2
- package/dist/{chunk-OTNL36LC.cjs → chunk-RUPCSQLF.cjs} +40 -40
- package/dist/{chunk-OTNL36LC.cjs.map → chunk-RUPCSQLF.cjs.map} +1 -1
- package/dist/{chunk-OMUCATVA.js → chunk-RVWC4P3H.js} +2 -2
- package/dist/{chunk-TIFJIHV2.cjs → chunk-SHDV5QCI.cjs} +77 -77
- package/dist/{chunk-TIFJIHV2.cjs.map → chunk-SHDV5QCI.cjs.map} +1 -1
- package/dist/{chunk-CBY3VQKJ.cjs → chunk-SPR4MJX5.cjs} +86 -86
- package/dist/{chunk-CBY3VQKJ.cjs.map → chunk-SPR4MJX5.cjs.map} +1 -1
- package/dist/{chunk-G7UE6RKV.cjs → chunk-SSJNN5BS.cjs} +20 -3
- package/dist/chunk-SSJNN5BS.cjs.map +1 -0
- package/dist/{chunk-HGBEKYXL.cjs → chunk-SZWVVAZ6.cjs} +5 -5
- package/dist/{chunk-HGBEKYXL.cjs.map → chunk-SZWVVAZ6.cjs.map} +1 -1
- package/dist/{chunk-TMSP5FYV.cjs → chunk-THVQWRV6.cjs} +36 -36
- package/dist/{chunk-TMSP5FYV.cjs.map → chunk-THVQWRV6.cjs.map} +1 -1
- package/dist/{chunk-SMUKHJJM.cjs → chunk-UDRJ6KOT.cjs} +27 -27
- package/dist/{chunk-SMUKHJJM.cjs.map → chunk-UDRJ6KOT.cjs.map} +1 -1
- package/dist/{chunk-I4YIWY3O.js → chunk-VR3VNXUT.js} +7 -7
- package/dist/{chunk-MOBZMJAP.js → chunk-ZGE4KMI6.js} +3 -3
- package/dist/{chunk-PLJLE4A4.js → chunk-ZJJERZUA.js} +20 -3
- package/dist/chunk-ZJJERZUA.js.map +1 -0
- package/dist/components/case-studies/index.cjs +11 -11
- package/dist/components/case-studies/index.js +5 -5
- package/dist/components/chat/index.cjs +5 -5
- package/dist/components/chat/index.js +4 -4
- package/dist/components/contact/index.cjs +6 -6
- package/dist/components/contact/index.js +5 -5
- package/dist/components/docs/index.cjs +8 -8
- package/dist/components/docs/index.js +7 -7
- package/dist/components/embeds/index.cjs +6 -6
- package/dist/components/embeds/index.js +5 -5
- package/dist/components/faq/index.cjs +7 -7
- package/dist/components/faq/index.js +6 -6
- package/dist/components/features/index.cjs +5 -5
- package/dist/components/features/index.js +4 -4
- package/dist/components/features/seo-editor-preview.d.ts.map +1 -1
- 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 +141 -141
- package/dist/components/index.js +13 -13
- package/dist/components/navigation/index.cjs +5 -5
- package/dist/components/navigation/index.js +4 -4
- package/dist/components/onboarding-guides/index.cjs +9 -9
- package/dist/components/onboarding-guides/index.js +8 -8
- package/dist/components/related-content/index.cjs +7 -7
- package/dist/components/related-content/index.js +6 -6
- package/dist/components/tickets/index.cjs +8 -8
- package/dist/components/tickets/index.js +7 -7
- package/dist/components/ui/index.cjs +5 -5
- package/dist/components/ui/index.js +4 -4
- package/dist/embed-shims/index.cjs +3 -3
- package/dist/embed-shims/index.js +2 -2
- package/dist/embed-shims/next-navigation.cjs +3 -3
- package/dist/embed-shims/next-navigation.d.ts.map +1 -1
- package/dist/embed-shims/next-navigation.js +1 -1
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +7 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -4
- package/dist/utils/index.cjs +4 -0
- 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 +4 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/seo-description.d.ts +14 -0
- package/dist/utils/seo-description.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/.tool-icon.md +3 -3
- package/src/components/features/seo-editor-preview.tsx +24 -1
- package/src/components/icons-v2-generated/brand-logos/.index.md +1 -2
- package/src/components/platform/.ShellTypeBadge.md +1 -1
- package/src/components/platform/.ToolBadge.md +2 -2
- package/src/embed-shims/__tests__/next-navigation.test.ts +165 -0
- package/src/embed-shims/next-navigation.tsx +46 -9
- package/src/stories/.OSTypeBadge.stories.md +1 -1
- package/src/stories/.ShellTypeBadge.stories.md +1 -1
- package/src/stories/.Toaster.stories.md +1 -1
- package/src/stories/.ToolBadge.stories.md +2 -2
- package/src/stories/.ToolExecutionDisplay.stories.md +4 -4
- package/src/types/.os.types.md +1 -1
- package/src/types/.shell.types.md +1 -1
- package/src/types/.tool.types.md +3 -3
- package/src/utils/.tool-utils.md +1 -3
- package/src/utils/index.ts +1 -0
- package/src/utils/seo-description.ts +13 -0
- package/dist/chunk-B2255TIZ.js.map +0 -1
- package/dist/chunk-FVI2ESVQ.cjs.map +0 -1
- package/dist/chunk-G7UE6RKV.cjs.map +0 -1
- package/dist/chunk-PLJLE4A4.js.map +0 -1
- /package/dist/{chunk-WWS3R3UD.js.map → chunk-5RFLFMCS.js.map} +0 -0
- /package/dist/{chunk-4XLJWX2N.js.map → chunk-7EUIW46X.js.map} +0 -0
- /package/dist/{chunk-JPZE2P4Y.js.map → chunk-7OMNZR2A.js.map} +0 -0
- /package/dist/{chunk-7W4474XS.js.map → chunk-7TA6WMMV.js.map} +0 -0
- /package/dist/{chunk-YOEQWHUD.js.map → chunk-BY3NKTNZ.js.map} +0 -0
- /package/dist/{chunk-LK3KXZIN.js.map → chunk-CRPQEP7D.js.map} +0 -0
- /package/dist/{chunk-SVUDKNS7.js.map → chunk-FBCN76VE.js.map} +0 -0
- /package/dist/{chunk-GR2CMONR.js.map → chunk-LHWLXA4X.js.map} +0 -0
- /package/dist/{chunk-2Q2SKYCZ.js.map → chunk-LKMUH4IO.js.map} +0 -0
- /package/dist/{chunk-FFCGB73K.js.map → chunk-QXYOGJQJ.js.map} +0 -0
- /package/dist/{chunk-NNF5XOMH.js.map → chunk-RAGHFNE7.js.map} +0 -0
- /package/dist/{chunk-OMUCATVA.js.map → chunk-RVWC4P3H.js.map} +0 -0
- /package/dist/{chunk-I4YIWY3O.js.map → chunk-VR3VNXUT.js.map} +0 -0
- /package/dist/{chunk-MOBZMJAP.js.map → chunk-ZGE4KMI6.js.map} +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the `seo_description` length budget.
|
|
3
|
+
*
|
|
4
|
+
* Unlike `seo_title` (soft search-engine guidance), this is a HARD cap: the
|
|
5
|
+
* `seo_description` DB columns are `varchar(160)` and Postgres rejects longer
|
|
6
|
+
* values outright ("value too long for type character varying(160)").
|
|
7
|
+
*
|
|
8
|
+
* This number lives in exactly ONE place. Consumers:
|
|
9
|
+
* - `SEOEditorPreview` — input `maxLength` + the too-long alert.
|
|
10
|
+
* - the hub — imports it (via `@flamingo-stack/openframe-frontend-core/utils`)
|
|
11
|
+
* for the AI generation prompt guidance and the rewrite-to-fit helper.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SEO_DESCRIPTION_MAX_LENGTH = 160;
|
|
14
|
+
//# sourceMappingURL=seo-description.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-description.d.ts","sourceRoot":"","sources":["../../src/utils/seo-description.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,MAAM,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- source-hash: 2db0b6673e5d8f02f2a3713fb6528d5d -->
|
|
2
|
-
Renders the appropriate logo icon for a given MSP tool type (Fleet MDM, MeshCentral,
|
|
2
|
+
Renders the appropriate logo icon for a given MSP tool type (Fleet MDM, MeshCentral, Authentik, Osquery, or OpenFrame variants).
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
|
@@ -25,9 +25,9 @@ import { ToolTypeValues } from "../types/tool.types";
|
|
|
25
25
|
// Render a Fleet MDM icon at default size (16px)
|
|
26
26
|
<ToolIcon toolType={ToolTypeValues.FLEET_MDM} />
|
|
27
27
|
|
|
28
|
-
// Render a
|
|
28
|
+
// Render a MeshCentral icon at 24px with a custom class
|
|
29
29
|
<ToolIcon
|
|
30
|
-
toolType={ToolTypeValues.
|
|
30
|
+
toolType={ToolTypeValues.MESHCENTRAL}
|
|
31
31
|
size={24}
|
|
32
32
|
className="h-6 w-6 text-accent"
|
|
33
33
|
/>
|
|
@@ -10,6 +10,7 @@ import Image from '../../embed-shims/next-image';
|
|
|
10
10
|
// SSOT for the field cap (server-safe constant). The seo_title renders as the
|
|
11
11
|
// page <title> verbatim (no brand suffix), so this is the full ~60-char budget.
|
|
12
12
|
import { SEO_TITLE_MAX_LENGTH } from '../../utils/seo-title';
|
|
13
|
+
import { SEO_DESCRIPTION_MAX_LENGTH } from '../../utils/seo-description';
|
|
13
14
|
|
|
14
15
|
export interface SEOEditorPreviewProps {
|
|
15
16
|
// SEO fields - must be strings (not undefined)
|
|
@@ -76,6 +77,11 @@ export function SEOEditorPreview({
|
|
|
76
77
|
const seoTitleLength = (seoTitle || '').length;
|
|
77
78
|
const seoTitleTooLong = seoTitleLength > SEO_TITLE_MAX_LENGTH;
|
|
78
79
|
|
|
80
|
+
// SEO description length state — the column is varchar(160); flag over-length so
|
|
81
|
+
// the editor (or a programmatically-set value) can be brought within budget.
|
|
82
|
+
const seoDescriptionLength = (seoDescription || '').length;
|
|
83
|
+
const seoDescriptionTooLong = seoDescriptionLength > SEO_DESCRIPTION_MAX_LENGTH;
|
|
84
|
+
|
|
79
85
|
// Use fallback values if OG fields are empty
|
|
80
86
|
const displayTitle = seoTitle.trim() || title || 'Untitled';
|
|
81
87
|
const displayDescription = seoDescription.trim() || summary || 'No description';
|
|
@@ -137,7 +143,7 @@ export function SEOEditorPreview({
|
|
|
137
143
|
<div className="flex items-center justify-between gap-2">
|
|
138
144
|
<span className="text-[11px] text-ods-error font-['DM_Sans']">
|
|
139
145
|
{seoTitleTooLong
|
|
140
|
-
? `Too long
|
|
146
|
+
? `Too long: search engines may truncate this title (keep it under ${SEO_TITLE_MAX_LENGTH})`
|
|
141
147
|
: ''}
|
|
142
148
|
</span>
|
|
143
149
|
<span
|
|
@@ -206,10 +212,27 @@ export function SEOEditorPreview({
|
|
|
206
212
|
value={seoDescription || ''}
|
|
207
213
|
onChange={(e) => onSeoDescriptionChange(e.target.value)}
|
|
208
214
|
disabled={disabled}
|
|
215
|
+
maxLength={SEO_DESCRIPTION_MAX_LENGTH}
|
|
216
|
+
invalid={seoDescriptionTooLong}
|
|
209
217
|
placeholder="Enter SEO meta description..."
|
|
210
218
|
className="bg-ods-bg border-ods-border text-ods-text-primary flex-1 resize-none"
|
|
211
219
|
rows={6}
|
|
212
220
|
/>
|
|
221
|
+
<div className="flex items-center justify-between gap-2">
|
|
222
|
+
<span className="text-[11px] text-ods-error font-['DM_Sans']">
|
|
223
|
+
{seoDescriptionTooLong
|
|
224
|
+
? `Too long: search engines may truncate this description (keep it under ${SEO_DESCRIPTION_MAX_LENGTH})`
|
|
225
|
+
: ''}
|
|
226
|
+
</span>
|
|
227
|
+
<span
|
|
228
|
+
className={cn(
|
|
229
|
+
"text-[11px] font-['DM_Sans'] tabular-nums shrink-0",
|
|
230
|
+
seoDescriptionTooLong ? 'text-ods-error font-semibold' : 'text-ods-text-secondary'
|
|
231
|
+
)}
|
|
232
|
+
>
|
|
233
|
+
{seoDescriptionLength}/{SEO_DESCRIPTION_MAX_LENGTH}
|
|
234
|
+
</span>
|
|
235
|
+
</div>
|
|
213
236
|
{!seoDescription && summary && (
|
|
214
237
|
<p className="text-[11px] text-ods-accent font-['DM_Sans']">
|
|
215
238
|
Auto-populated from summary
|
|
@@ -13,7 +13,7 @@ A flat collection of named icon exports organized by brand/platform category:
|
|
|
13
13
|
| **Microsoft** | `MicrosoftLogoIcon`, `MicrosoftLogoGreyIcon`, `Office365LogoIcon`, `WindowsIcon`, `WindowsLogoIcon` |
|
|
14
14
|
| **AI Platforms** | `AnthropicLogoIcon`, `GeminiLogoIcon`, `OpenaiLogoIcon` (+ grey variants) |
|
|
15
15
|
| **Social** | `FacebookIcon`, `InstagramIcon`, `LinkedinIcon`, `TwitterIcon`, `TwitterXIcon`, `TiktokIcon`, `YoutubeIcon` |
|
|
16
|
-
| **MSP/IT Tools** | `
|
|
16
|
+
| **MSP/IT Tools** | `MeshcentralLogoIcon`, `FleetMdmLogoIcon`, `OsqueryLogoIcon`, `PowershellLogoIcon` |
|
|
17
17
|
| **Dev Tools** | `GithubIcon`, `FigmaIcon`, `FramerIcon`, `TrelloIcon`, `ClickupLogoIcon` |
|
|
18
18
|
|
|
19
19
|
> Many brands include both a standard and a `Grey` variant (e.g., `SlackLogoIcon` / `SlackLogoGreyIcon`), useful for active/inactive states.
|
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
GithubIcon,
|
|
27
27
|
SlackLogoIcon,
|
|
28
28
|
SlackLogoGreyIcon,
|
|
29
|
-
TacticalRmmLogoIcon,
|
|
30
29
|
OpenaiLogoIcon,
|
|
31
30
|
} from '@/components/icons/brands';
|
|
32
31
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- source-hash: f614dd9faeadf368597d094822bcd3c7 -->
|
|
2
|
-
Displays a badge combining an icon and label for a given shell/script type, supporting all
|
|
2
|
+
Displays a badge combining an icon and label for a given shell/script type, supporting all OpenFrame RMM shell types (PowerShell, CMD, Bash, Python, Nushell, Deno, Shell).
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- source-hash: 5668aa064d8c050beff751a96d6469be -->
|
|
2
|
-
A React component that renders a visual badge combining a tool icon and label for OpenFrame-integrated MSP tools (e.g.,
|
|
2
|
+
A React component that renders a visual badge combining a tool icon and label for OpenFrame-integrated MSP tools (e.g., Fleet MDM, MeshCentral).
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
|
@@ -15,7 +15,7 @@ A React component that renders a visual badge combining a tool icon and label fo
|
|
|
15
15
|
import { ToolBadge } from './components/ToolBadge'
|
|
16
16
|
|
|
17
17
|
// Basic usage in a table cell
|
|
18
|
-
<ToolBadge toolType="
|
|
18
|
+
<ToolBadge toolType="OPENFRAME_RMM" />
|
|
19
19
|
|
|
20
20
|
// With custom styling
|
|
21
21
|
<ToolBadge
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Regression tests for the `next/navigation` shim fallback (unregistered host).
|
|
5
|
+
*
|
|
6
|
+
* The guarantee under test: an unregistered `useRouter().push/replace` must
|
|
7
|
+
* perform a same-origin SPA navigation via the History API and NEVER hard-load
|
|
8
|
+
* the document — a `?search=` write from `useApiParams` used to hard-reload via
|
|
9
|
+
* `window.location.replace`. The registered path must delegate to the host
|
|
10
|
+
* router untouched.
|
|
11
|
+
*
|
|
12
|
+
* `impl` is module-level singleton state, so each test re-imports the module
|
|
13
|
+
* fresh via `vi.resetModules()` to isolate registration state. We also stub
|
|
14
|
+
* `window.location` with a spy object so any hard navigation (assign/replace/
|
|
15
|
+
* reload) is observable — jsdom leaves those methods unimplemented anyway.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const ORIGIN = 'http://localhost:3000'
|
|
19
|
+
|
|
20
|
+
async function freshModule() {
|
|
21
|
+
vi.resetModules()
|
|
22
|
+
return import('../next-navigation')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let assignSpy: ReturnType<typeof vi.fn>
|
|
26
|
+
let replaceSpy: ReturnType<typeof vi.fn>
|
|
27
|
+
let reloadSpy: ReturnType<typeof vi.fn>
|
|
28
|
+
let pushStateSpy: ReturnType<typeof vi.spyOn>
|
|
29
|
+
let replaceStateSpy: ReturnType<typeof vi.spyOn>
|
|
30
|
+
let originalLocation: Location
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
originalLocation = window.location
|
|
34
|
+
assignSpy = vi.fn()
|
|
35
|
+
replaceSpy = vi.fn()
|
|
36
|
+
reloadSpy = vi.fn()
|
|
37
|
+
Object.defineProperty(window, 'location', {
|
|
38
|
+
configurable: true,
|
|
39
|
+
value: {
|
|
40
|
+
href: `${ORIGIN}/`,
|
|
41
|
+
origin: ORIGIN,
|
|
42
|
+
pathname: '/',
|
|
43
|
+
search: '',
|
|
44
|
+
assign: assignSpy,
|
|
45
|
+
replace: replaceSpy,
|
|
46
|
+
reload: reloadSpy,
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
// Real jsdom history — spy without changing behavior so we can assert which
|
|
50
|
+
// History API the fallback used (push vs replace encodes the nav semantics).
|
|
51
|
+
pushStateSpy = vi.spyOn(window.history, 'pushState')
|
|
52
|
+
replaceStateSpy = vi.spyOn(window.history, 'replaceState')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
Object.defineProperty(window, 'location', { configurable: true, value: originalLocation })
|
|
57
|
+
vi.restoreAllMocks()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('fallback useRouter (unregistered host)', () => {
|
|
61
|
+
it('replace() writes the URL via history.replaceState — never a hard navigation', async () => {
|
|
62
|
+
const { useRouter } = await freshModule()
|
|
63
|
+
const popstate = vi.fn()
|
|
64
|
+
window.addEventListener('popstate', popstate)
|
|
65
|
+
|
|
66
|
+
useRouter().replace('?search=laptop')
|
|
67
|
+
|
|
68
|
+
expect(replaceStateSpy).toHaveBeenCalledWith(null, '', `${ORIGIN}/?search=laptop`)
|
|
69
|
+
expect(pushStateSpy).not.toHaveBeenCalled()
|
|
70
|
+
// No full-document navigation of any kind — this is the reported bug.
|
|
71
|
+
expect(assignSpy).not.toHaveBeenCalled()
|
|
72
|
+
expect(replaceSpy).not.toHaveBeenCalled()
|
|
73
|
+
expect(reloadSpy).not.toHaveBeenCalled()
|
|
74
|
+
// Subscribers (usePathname/useSearchParams) get a re-render signal.
|
|
75
|
+
expect(popstate).toHaveBeenCalledTimes(1)
|
|
76
|
+
|
|
77
|
+
window.removeEventListener('popstate', popstate)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('push() uses history.pushState (new entry) — never a hard navigation', async () => {
|
|
81
|
+
const { useRouter } = await freshModule()
|
|
82
|
+
useRouter().push('/scripts?tab=list')
|
|
83
|
+
|
|
84
|
+
expect(pushStateSpy).toHaveBeenCalledWith(null, '', `${ORIGIN}/scripts?tab=list`)
|
|
85
|
+
expect(replaceStateSpy).not.toHaveBeenCalled()
|
|
86
|
+
expect(assignSpy).not.toHaveBeenCalled()
|
|
87
|
+
expect(reloadSpy).not.toHaveBeenCalled()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('falls back to a real navigation for cross-origin targets, honoring replace semantics', async () => {
|
|
91
|
+
const { useRouter } = await freshModule()
|
|
92
|
+
useRouter().replace('https://example.com/evil')
|
|
93
|
+
|
|
94
|
+
// replace() must use location.replace (no back-button entry), not assign.
|
|
95
|
+
expect(replaceSpy).toHaveBeenCalledWith('https://example.com/evil')
|
|
96
|
+
expect(assignSpy).not.toHaveBeenCalled()
|
|
97
|
+
expect(replaceStateSpy).not.toHaveBeenCalled()
|
|
98
|
+
expect(pushStateSpy).not.toHaveBeenCalled()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('uses location.assign for a cross-origin push (new history entry)', async () => {
|
|
102
|
+
const { useRouter } = await freshModule()
|
|
103
|
+
useRouter().push('https://example.com/evil')
|
|
104
|
+
|
|
105
|
+
expect(assignSpy).toHaveBeenCalledWith('https://example.com/evil')
|
|
106
|
+
expect(replaceSpy).not.toHaveBeenCalled()
|
|
107
|
+
expect(pushStateSpy).not.toHaveBeenCalled()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('falls back to a real navigation for a malformed href (push → assign)', async () => {
|
|
111
|
+
const { useRouter } = await freshModule()
|
|
112
|
+
useRouter().push('http://[invalid')
|
|
113
|
+
|
|
114
|
+
expect(assignSpy).toHaveBeenCalledWith('http://[invalid')
|
|
115
|
+
expect(replaceSpy).not.toHaveBeenCalled()
|
|
116
|
+
expect(pushStateSpy).not.toHaveBeenCalled()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('falls back to a real navigation for a malformed href (replace → replace)', async () => {
|
|
120
|
+
const { useRouter } = await freshModule()
|
|
121
|
+
useRouter().replace('http://[invalid')
|
|
122
|
+
|
|
123
|
+
expect(replaceSpy).toHaveBeenCalledWith('http://[invalid')
|
|
124
|
+
expect(assignSpy).not.toHaveBeenCalled()
|
|
125
|
+
expect(replaceStateSpy).not.toHaveBeenCalled()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('back/forward remain history-based (unchanged)', async () => {
|
|
129
|
+
const { useRouter } = await freshModule()
|
|
130
|
+
const backSpy = vi.spyOn(window.history, 'back').mockImplementation(() => {})
|
|
131
|
+
const forwardSpy = vi.spyOn(window.history, 'forward').mockImplementation(() => {})
|
|
132
|
+
useRouter().back()
|
|
133
|
+
useRouter().forward()
|
|
134
|
+
expect(backSpy).toHaveBeenCalledTimes(1)
|
|
135
|
+
expect(forwardSpy).toHaveBeenCalledTimes(1)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('registered useRouter (Next host)', () => {
|
|
140
|
+
it('delegates to the host router and never touches window.location or history', async () => {
|
|
141
|
+
const { useRouter, registerNavigation } = await freshModule()
|
|
142
|
+
const hostRouter = {
|
|
143
|
+
push: vi.fn(),
|
|
144
|
+
replace: vi.fn(),
|
|
145
|
+
back: vi.fn(),
|
|
146
|
+
forward: vi.fn(),
|
|
147
|
+
refresh: vi.fn(),
|
|
148
|
+
prefetch: vi.fn(),
|
|
149
|
+
}
|
|
150
|
+
registerNavigation({ useRouter: () => hostRouter })
|
|
151
|
+
|
|
152
|
+
const r = useRouter()
|
|
153
|
+
r.replace('?search=laptop', { scroll: false })
|
|
154
|
+
r.push('/scripts')
|
|
155
|
+
|
|
156
|
+
expect(hostRouter.replace).toHaveBeenCalledWith('?search=laptop', { scroll: false })
|
|
157
|
+
expect(hostRouter.push).toHaveBeenCalledWith('/scripts')
|
|
158
|
+
// The fallback path is completely bypassed — no navigation side effects.
|
|
159
|
+
expect(assignSpy).not.toHaveBeenCalled()
|
|
160
|
+
expect(replaceSpy).not.toHaveBeenCalled()
|
|
161
|
+
expect(reloadSpy).not.toHaveBeenCalled()
|
|
162
|
+
expect(pushStateSpy).not.toHaveBeenCalled()
|
|
163
|
+
expect(replaceStateSpy).not.toHaveBeenCalled()
|
|
164
|
+
})
|
|
165
|
+
})
|
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
* After registration, every exported hook in this file delegates to
|
|
23
23
|
* the real implementation. Without registration, the stubs run:
|
|
24
24
|
*
|
|
25
|
-
* - `useRouter()` → push/replace
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* - `useRouter()` → push/replace do a same-origin SPA navigation via the
|
|
26
|
+
* History API (`pushState`/`replaceState` + a synthetic `popstate`), so a
|
|
27
|
+
* query-param write never reloads the document; cross-origin/malformed
|
|
28
|
+
* hrefs fall back to `window.location`. back/forward use
|
|
29
|
+
* `history.back/forward`; refresh reloads; prefetch is a no-op.
|
|
28
30
|
* - `usePathname()` → returns `window.location.pathname` (popstate-
|
|
29
31
|
* subscribed so back/forward re-renders).
|
|
30
32
|
* - `useSearchParams()` → returns `URLSearchParams` view of
|
|
@@ -34,10 +36,12 @@
|
|
|
34
36
|
* - `notFound()` / `redirect()` / `permanentRedirect()` —
|
|
35
37
|
* best-effort equivalents using `window.location`.
|
|
36
38
|
*
|
|
37
|
-
* The fallback path subscribes to `popstate
|
|
38
|
-
* `
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* The fallback path subscribes to `popstate`. Native `pushState`/
|
|
40
|
+
* `replaceState` do NOT fire popstate, but the fallback `useRouter`'s
|
|
41
|
+
* push/replace dispatch a synthetic `popstate` so this file's own
|
|
42
|
+
* `usePathname`/`useSearchParams` subscribers re-render. A URL mutated
|
|
43
|
+
* through some OTHER API still won't auto-re-render here — embedders
|
|
44
|
+
* that need that should register a real router or supply their own.
|
|
41
45
|
*/
|
|
42
46
|
'use client'
|
|
43
47
|
import { useEffect, useState } from 'react'
|
|
@@ -95,11 +99,44 @@ const noopRouter: RouterStub = {
|
|
|
95
99
|
prefetch: () => {},
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
/**
|
|
103
|
+
* SPA navigation for the unregistered fallback. Uses the History API instead of
|
|
104
|
+
* `window.location.assign/replace` so a same-origin URL change (e.g. a table's
|
|
105
|
+
* `?search=` write via `useApiParams`) never triggers a full document reload —
|
|
106
|
+
* even if a real router was never registered, or the registration landed on a
|
|
107
|
+
* DIFFERENT module instance than this one (duplicate lib copy / ESM-CJS dual
|
|
108
|
+
* package). A synthetic `popstate` is dispatched so this file's own
|
|
109
|
+
* `usePathname`/`useSearchParams` subscribers re-render in response.
|
|
110
|
+
* Cross-origin or malformed hrefs still fall back to a real `window.location`
|
|
111
|
+
* navigation, which is the only correct behavior there.
|
|
112
|
+
*/
|
|
113
|
+
function softNavigate(href: string, mode: 'push' | 'replace'): void {
|
|
114
|
+
if (typeof window === 'undefined') return
|
|
115
|
+
try {
|
|
116
|
+
const url = new URL(href, window.location.href)
|
|
117
|
+
if (url.origin !== window.location.origin) {
|
|
118
|
+
// Preserve the caller's history semantics: replace() must not leave a
|
|
119
|
+
// back-button entry, so it uses `location.replace`, not `location.assign`.
|
|
120
|
+
if (mode === 'push') window.location.assign(url.href)
|
|
121
|
+
else window.location.replace(url.href)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
if (mode === 'push') window.history.pushState(null, '', url.href)
|
|
125
|
+
else window.history.replaceState(null, '', url.href)
|
|
126
|
+
window.dispatchEvent(new PopStateEvent('popstate'))
|
|
127
|
+
} catch {
|
|
128
|
+
// Malformed href — a real navigation is safer than swallowing it, but still
|
|
129
|
+
// honor push vs replace history semantics.
|
|
130
|
+
if (mode === 'push') window.location.assign(href)
|
|
131
|
+
else window.location.replace(href)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
98
135
|
function fallbackUseRouter(): RouterStub {
|
|
99
136
|
if (typeof window === 'undefined') return noopRouter
|
|
100
137
|
return {
|
|
101
|
-
push: (href: string) =>
|
|
102
|
-
replace: (href: string) =>
|
|
138
|
+
push: (href: string) => softNavigate(href, 'push'),
|
|
139
|
+
replace: (href: string) => softNavigate(href, 'replace'),
|
|
103
140
|
back: () => window.history.back(),
|
|
104
141
|
forward: () => window.history.forward(),
|
|
105
142
|
refresh: () => window.location.reload(),
|
|
@@ -31,4 +31,4 @@ export const AllOSTypes: Story = {
|
|
|
31
31
|
};
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
The stories demonstrate OS type normalization from various MDM sources (Fleet
|
|
34
|
+
The stories demonstrate OS type normalization from various MDM sources (Fleet) with support for aliases like "Darwin" → "macOS" and "win32" → "Windows". Includes examples for icon-only, label-only, and combined display modes with customizable sizing options.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- source-hash: fe5ee5e74229fbc813833a5d6c81b725 -->
|
|
2
|
-
Storybook configuration for the ShellTypeBadge component, defining visual documentation and interactive examples for all supported
|
|
2
|
+
Storybook configuration for the ShellTypeBadge component, defining visual documentation and interactive examples for all supported OpenFrame RMM shell types including PowerShell, CMD, Bash, Python, Nushell, Deno, and generic Shell.
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
|
@@ -9,7 +9,7 @@ Storybook stories for the `Toaster` / `ToastCard` component system, covering sta
|
|
|
9
9
|
| `Playground` | Story | Arg-driven interactive card — use Storybook Controls to tweak `variant`, `title`, `description`, `duration`, and `dismissible` |
|
|
10
10
|
| `CommandApprovalCollapsed` | Story | `CommandApprovalToast` in its default collapsed state |
|
|
11
11
|
| `CommandApprovalExpanded` | Story | `CommandApprovalToast` with `defaultExpanded` showing the command preview and Approve/Reject actions |
|
|
12
|
-
| `CommandApprovalPerTool` | Story | One expanded approval toast per supported tool type (`FLEET_MDM`, `
|
|
12
|
+
| `CommandApprovalPerTool` | Story | One expanded approval toast per supported tool type (`FLEET_MDM`, `MESHCENTRAL`, `AUTHENTIK`, `OSQUERY`, `OPENFRAME`) to verify icon mapping |
|
|
13
13
|
| `CommandApprovalLive` | Story | Fires real Sonner toasts via `showCommandApprovalToast()` with `onApprove`/`onReject` callbacks |
|
|
14
14
|
| `LiveTriggers` | Story | Button panel that fires every variant as a live Sonner toast using both `toast()` and `showToast()` |
|
|
15
15
|
|
|
@@ -4,7 +4,7 @@ Storybook configuration file for the ToolBadge component, providing interactive
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
|
6
6
|
- **meta**: Main Storybook configuration with component metadata, controls, and documentation
|
|
7
|
-
- **Story exports**: Individual stories showcasing different tool types (
|
|
7
|
+
- **Story exports**: Individual stories showcasing different tool types (Fleet MDM, MeshCentral, etc.)
|
|
8
8
|
- **AllTools story**: Comparison view showing multiple tool badges together
|
|
9
9
|
- **argTypes**: Interactive controls for `toolType` selection and `className` customization
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ Storybook configuration file for the ToolBadge component, providing interactive
|
|
|
14
14
|
// Individual tool badge stories
|
|
15
15
|
export const Default: Story = {
|
|
16
16
|
args: {
|
|
17
|
-
toolType: '
|
|
17
|
+
toolType: 'OPENFRAME_RMM',
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
|
|
@@ -5,7 +5,7 @@ Storybook stories for the `ToolExecutionDisplay` chat component, covering execut
|
|
|
5
5
|
|
|
6
6
|
| Export | Description |
|
|
7
7
|
|--------|-------------|
|
|
8
|
-
| `CollapsedExecuting` | Spinner state while a tool (e.g. `run_script` via
|
|
8
|
+
| `CollapsedExecuting` | Spinner state while a tool (e.g. `run_script` via OpenFrame RMM) is in progress |
|
|
9
9
|
| `CollapsedSuccess` | Green check state after successful tool execution, with result output |
|
|
10
10
|
| `CollapsedFailure` | Red error icon state after a failed tool execution (e.g. Fleet MDM profile deploy) |
|
|
11
11
|
| `constrainedDecorator` | Helper decorator that wraps stories in a `max-width` container (default 400px) |
|
|
@@ -18,7 +18,7 @@ import type { ToolExecutionData } from "../components/chat/types";
|
|
|
18
18
|
|
|
19
19
|
const message: ToolExecutionData = {
|
|
20
20
|
type: "EXECUTED_TOOL",
|
|
21
|
-
integratedToolType: "
|
|
21
|
+
integratedToolType: "OPENFRAME_RMM",
|
|
22
22
|
toolFunction: "run_script",
|
|
23
23
|
parameters: { RunAsUser: "False", timeoutSeconds: 60 },
|
|
24
24
|
result: "Script output here...",
|
|
@@ -34,8 +34,8 @@ Three fixture payloads cover the core display states:
|
|
|
34
34
|
|
|
35
35
|
| Fixture | `type` | `integratedToolType` | `success` |
|
|
36
36
|
|---------|--------|----------------------|-----------|
|
|
37
|
-
| `executingMessage` | `EXECUTING_TOOL` | `
|
|
38
|
-
| `executedSuccessMessage` | `EXECUTED_TOOL` | `
|
|
37
|
+
| `executingMessage` | `EXECUTING_TOOL` | `OPENFRAME_RMM` | — |
|
|
38
|
+
| `executedSuccessMessage` | `EXECUTED_TOOL` | `OPENFRAME_RMM` | `true` |
|
|
39
39
|
| `executedFailureMessage` | `EXECUTED_TOOL` | `FLEET_MDM` | `false` |
|
|
40
40
|
|
|
41
41
|
The component renders a collapsed card by default; clicking toggles full details including parameters and result output.
|
package/src/types/.os.types.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- source-hash: 6581d93b3edee981ded786a177e99959 -->
|
|
2
|
-
Defines the centralized source of truth for operating system type metadata across the Flamingo platform, normalizing OS values from Fleet MDM
|
|
2
|
+
Defines the centralized source of truth for operating system type metadata across the Flamingo platform, normalizing OS values from Fleet MDM and GraphQL into a unified structure.
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- source-hash: b8477e619839e162b7edcfaaf835462d -->
|
|
2
|
-
Centralized type definitions for shell/script types supported across the Flamingo platform. Provides a single source of truth for shell metadata, icons, and labels based on
|
|
2
|
+
Centralized type definitions for shell/script types supported across the Flamingo platform. Provides a single source of truth for shell metadata, icons, and labels based on OpenFrame RMM supported types.
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
package/src/types/.tool.types.md
CHANGED
|
@@ -3,7 +3,7 @@ Centralized type definitions for MSP tool types used throughout the Flamingo/Ope
|
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
|
6
|
-
- **`ToolTypeValues`** - Const assertion object containing all available tool type constants (
|
|
6
|
+
- **`ToolTypeValues`** - Const assertion object containing all available tool type constants (FLEET_MDM, MESHCENTRAL, etc.)
|
|
7
7
|
- **`ToolType`** - Union type derived from ToolTypeValues for type-safe tool type references
|
|
8
8
|
- **`toolLabels`** - Record mapping tool types to human-readable display names
|
|
9
9
|
|
|
@@ -13,10 +13,10 @@ Centralized type definitions for MSP tool types used throughout the Flamingo/Ope
|
|
|
13
13
|
import { ToolType, ToolTypeValues, toolLabels } from './tool.types'
|
|
14
14
|
|
|
15
15
|
// Type-safe tool type usage
|
|
16
|
-
const currentTool: ToolType = ToolTypeValues.
|
|
16
|
+
const currentTool: ToolType = ToolTypeValues.MESHCENTRAL
|
|
17
17
|
|
|
18
18
|
// Get display label for UI
|
|
19
|
-
const displayName = toolLabels[currentTool] // "
|
|
19
|
+
const displayName = toolLabels[currentTool] // "MeshCentral"
|
|
20
20
|
|
|
21
21
|
// Use in component props
|
|
22
22
|
interface ToolBadgeProps {
|
package/src/utils/.tool-utils.md
CHANGED
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
} from './tool-utils'
|
|
23
23
|
|
|
24
24
|
// Normalize various input formats
|
|
25
|
-
normalizeToolType('tactical') // => 'TACTICAL_RMM'
|
|
26
25
|
normalizeToolType('FLEET-MDM') // => 'FLEET_MDM'
|
|
27
26
|
normalizeToolType('unknown') // => undefined
|
|
28
27
|
|
|
@@ -31,7 +30,6 @@ normalizeToolTypeWithFallback('mesh') // => 'MESHCENTRAL'
|
|
|
31
30
|
normalizeToolTypeWithFallback('invalid') // => 'SYSTEM'
|
|
32
31
|
|
|
33
32
|
// Convert to display labels
|
|
34
|
-
toToolLabel('tactical') // => 'Tactical'
|
|
35
33
|
toToolLabel('fleet_mdm') // => 'Fleet'
|
|
36
34
|
|
|
37
35
|
// Validation
|
|
@@ -39,4 +37,4 @@ isValidToolType('authentik') // => true
|
|
|
39
37
|
isValidToolType('invalid') // => false
|
|
40
38
|
```
|
|
41
39
|
|
|
42
|
-
The utility supports all major MSP tools including
|
|
40
|
+
The utility supports all major MSP tools including Fleet MDM, MeshCentral, Authentik, and OpenFrame components with comprehensive alias mapping for different naming conventions.
|
package/src/utils/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ export { getBaseUrl } from '../utils/cn'
|
|
|
29
29
|
// SEO title length budget — server-safe constant (SSOT). Consumed by the hub
|
|
30
30
|
// (prompt guidance + DB check value) and by SEOEditorPreview (input maxLength).
|
|
31
31
|
export { SEO_TITLE_MAX_LENGTH } from './seo-title'
|
|
32
|
+
export { SEO_DESCRIPTION_MAX_LENGTH } from './seo-description'
|
|
32
33
|
|
|
33
34
|
// Brand silhouette SVG registry (CSS mask-image tinting) — centralized so
|
|
34
35
|
// consumers (e.g. company-hub deck) don't ship their own copies of the marks.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the `seo_description` length budget.
|
|
3
|
+
*
|
|
4
|
+
* Unlike `seo_title` (soft search-engine guidance), this is a HARD cap: the
|
|
5
|
+
* `seo_description` DB columns are `varchar(160)` and Postgres rejects longer
|
|
6
|
+
* values outright ("value too long for type character varying(160)").
|
|
7
|
+
*
|
|
8
|
+
* This number lives in exactly ONE place. Consumers:
|
|
9
|
+
* - `SEOEditorPreview` — input `maxLength` + the too-long alert.
|
|
10
|
+
* - the hub — imports it (via `@flamingo-stack/openframe-frontend-core/utils`)
|
|
11
|
+
* for the AI generation prompt guidance and the rewrite-to-fit helper.
|
|
12
|
+
*/
|
|
13
|
+
export const SEO_DESCRIPTION_MAX_LENGTH = 160;
|