@woodsportal/hubspot-kit 1.0.4 → 1.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +4 -2
- package/dist/{chunk-JDGIAYHX.js → chunk-GTTIJVAR.js} +43 -3
- package/dist/chunk-GTTIJVAR.js.map +1 -0
- package/dist/cli.js +1400 -338
- package/dist/cli.js.map +1 -1
- package/dist/exec-U2OJATR3.js +12 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/examples/greenfield/module-hybrid-cdn/README.md +40 -3
- package/examples/greenfield/module-hybrid-cdn/public/fields.json +44 -1
- package/examples/greenfield/module-hybrid-cdn/src/app.tsx +8 -0
- package/examples/greenfield/module-hybrid-cdn/src/assets/css/main.dev.css +6 -1
- package/examples/greenfield/module-hybrid-cdn/src/cdn/app-entry.ts +32 -0
- package/examples/greenfield/module-hybrid-cdn/src/cdn/vendor-entry.ts +28 -0
- package/examples/greenfield/module-hybrid-cdn/src/components/layout/section-card.tsx +20 -0
- package/examples/greenfield/module-hybrid-cdn/src/components/layout/starter-nav.tsx +28 -0
- package/examples/greenfield/module-hybrid-cdn/src/components/layout/starter-shell.tsx +18 -0
- package/examples/greenfield/module-hybrid-cdn/src/config/router.ts +4 -0
- package/examples/greenfield/module-hybrid-cdn/src/config/routes.ts +5 -0
- package/examples/greenfield/module-hybrid-cdn/src/defaultData.ts +2 -7
- package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-guide-page.tsx +93 -0
- package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-home-page.tsx +21 -0
- package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-home.tsx +52 -0
- package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-single-page.tsx +17 -0
- package/examples/greenfield/module-hybrid-cdn/src/features/starter/content/woodscli-guide-content.ts +86 -0
- package/examples/greenfield/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.ts +47 -0
- package/examples/greenfield/module-hybrid-cdn/src/features/starter/index.ts +6 -0
- package/examples/greenfield/module-hybrid-cdn/src/hubspot/bootstrap.ts +10 -0
- package/examples/greenfield/module-hybrid-cdn/src/hubspot/default-data.ts +7 -0
- package/examples/greenfield/module-hybrid-cdn/src/integrations/tanstack-router/router.tsx +25 -0
- package/examples/greenfield/module-hybrid-cdn/src/main.tsx +4 -3
- package/examples/greenfield/module-hybrid-cdn/src/portal/mount-dev-preview.tsx +8 -0
- package/examples/greenfield/module-hybrid-cdn/src/portal/mount-portal.tsx +12 -0
- package/examples/greenfield/module-hybrid-cdn/src/routeTree.gen.ts +77 -0
- package/examples/greenfield/module-hybrid-cdn/src/routes/__root.tsx +15 -0
- package/examples/greenfield/module-hybrid-cdn/src/routes/guide.tsx +7 -0
- package/examples/greenfield/module-hybrid-cdn/src/routes/index.tsx +7 -0
- package/examples/greenfield/module-hybrid-cdn/tsconfig.json +20 -0
- package/examples/greenfield/module-monolith/public/fields.json +44 -1
- package/examples/greenfield/module-monolith/src/app.tsx +8 -0
- package/examples/greenfield/module-monolith/src/assets/css/main.dev.css +8 -0
- package/examples/greenfield/module-monolith/src/components/layout/section-card.tsx +20 -0
- package/examples/greenfield/module-monolith/src/components/layout/starter-nav.tsx +28 -0
- package/examples/greenfield/module-monolith/src/components/layout/starter-shell.tsx +18 -0
- package/examples/greenfield/module-monolith/src/config/router.ts +4 -0
- package/examples/greenfield/module-monolith/src/config/routes.ts +5 -0
- package/examples/greenfield/module-monolith/src/defaultData.ts +2 -7
- package/examples/greenfield/module-monolith/src/features/starter/components/starter-guide-page.tsx +93 -0
- package/examples/greenfield/module-monolith/src/features/starter/components/starter-home-page.tsx +21 -0
- package/examples/greenfield/module-monolith/src/features/starter/components/starter-home.tsx +52 -0
- package/examples/greenfield/module-monolith/src/features/starter/components/starter-single-page.tsx +17 -0
- package/examples/greenfield/module-monolith/src/features/starter/content/woodscli-guide-content.ts +86 -0
- package/examples/greenfield/module-monolith/src/features/starter/hooks/use-starter-preview.ts +47 -0
- package/examples/greenfield/module-monolith/src/features/starter/index.ts +6 -0
- package/examples/greenfield/module-monolith/src/hubspot/default-data.ts +7 -0
- package/examples/greenfield/module-monolith/src/hubspot/types.ts +17 -0
- package/examples/greenfield/module-monolith/src/integrations/tanstack-router/router.tsx +25 -0
- package/examples/greenfield/module-monolith/src/main.tsx +5 -0
- package/examples/greenfield/module-monolith/src/portal/mount-dev-preview.tsx +8 -0
- package/examples/greenfield/module-monolith/src/portal/mount-portal.tsx +12 -0
- package/examples/greenfield/module-monolith/src/routeTree.gen.ts +77 -0
- package/examples/greenfield/module-monolith/src/routes/__root.tsx +15 -0
- package/examples/greenfield/module-monolith/src/routes/guide.tsx +7 -0
- package/examples/greenfield/module-monolith/src/routes/index.tsx +7 -0
- package/examples/greenfield/module-monolith/tsconfig.json +20 -0
- package/package.json +4 -1
- package/scripts/check-hubspot-bundle-size.mjs +5 -2
- package/scripts/ensure-wphs-workspace.mjs +2 -1
- package/scripts/hubspot-module-postbuild.mjs +1 -1
- package/scripts/read-woodsportal-config.mjs +4 -1
- package/scripts/run-hubspot-module-postbuild.mjs +9 -4
- package/scripts/vite/bootstrap-shim-resolver.js +47 -0
- package/scripts/vite/ensure-dev-kit-shims.js +7 -1
- package/scripts/vite/react-dev-plugin-options.js +2 -1
- package/scripts/watch-hubspot.mjs +107 -17
- package/scripts/wphs-paths.mjs +1 -0
- package/scripts/write-audit-artifacts.mjs +2 -1
- package/src/dev/bootstrap-greenfield.tsx +59 -0
- package/src/dev/bootstrap.tsx +46 -0
- package/src/dev-module-config/__tests__/fixtures/fields.json +3014 -0
- package/src/dev-module-config/__tests__/fixtures/golden-module.fixture.ts +102 -0
- package/src/dev-module-config/__tests__/module-config-storage.test.ts +40 -0
- package/src/dev-module-config/__tests__/module-config-transform.test.ts +55 -0
- package/src/dev-module-config/__tests__/normalize-repeater-items.test.ts +46 -0
- package/src/dev-module-config/dev-module-display-name.ts +5 -0
- package/src/dev-module-config/runtime/module-config-bridge.ts +27 -0
- package/src/dev-module-config/runtime/module-config-runtime.ts +185 -0
- package/src/dev-module-config/schema/build-default-item.ts +7 -0
- package/src/dev-module-config/schema/deep-merge.ts +27 -0
- package/src/dev-module-config/schema/extract-defaults.ts +62 -0
- package/src/dev-module-config/schema/normalize-choices.ts +18 -0
- package/src/dev-module-config/schema/normalize-repeater-items.ts +31 -0
- package/src/dev-module-config/schema/parse-fields-json.ts +2 -0
- package/src/dev-module-config/schema/types.ts +75 -0
- package/src/dev-module-config/schema/validate-module-config.ts +81 -0
- package/src/dev-module-config/schema/visibility.ts +130 -0
- package/src/dev-module-config/storage/config-json.ts +46 -0
- package/src/dev-module-config/storage/workspace-store.ts +139 -0
- package/src/dev-module-config/transform/build-hubspot-data.ts +409 -0
- package/src/dev-module-config/ui/FieldRenderer.tsx +353 -0
- package/src/dev-module-config/ui/ModuleConfigAnimatedPresence.tsx +72 -0
- package/src/dev-module-config/ui/ModuleConfigCollapsible.tsx +43 -0
- package/src/dev-module-config/ui/ModuleConfigDockSideIcon.tsx +27 -0
- package/src/dev-module-config/ui/ModuleConfigOverlay.tsx +134 -0
- package/src/dev-module-config/ui/ModuleConfigPersistenceStatus.tsx +89 -0
- package/src/dev-module-config/ui/ModuleConfigShell.tsx +355 -0
- package/src/dev-module-config/ui/ModuleConfigThemeToggle.tsx +32 -0
- package/src/dev-module-config/ui/ModuleConfigUiContext.tsx +65 -0
- package/src/dev-module-config/ui/editors/DeveloperOptionsEditor.tsx +61 -0
- package/src/dev-module-config/ui/editors/MenuOptionsEditor.tsx +103 -0
- package/src/dev-module-config/ui/hubspot/HsColorField.tsx +106 -0
- package/src/dev-module-config/ui/hubspot/HsColorPickerPopover.tsx +210 -0
- package/src/dev-module-config/ui/hubspot/HsFieldLabel.tsx +24 -0
- package/src/dev-module-config/ui/hubspot/HsFieldWidgets.tsx +248 -0
- package/src/dev-module-config/ui/hubspot/HsImageField.tsx +112 -0
- package/src/dev-module-config/ui/hubspot/HsOccurrenceGroupEditor.tsx +243 -0
- package/src/dev-module-config/ui/hubspot/HsRepeaterList.tsx +178 -0
- package/src/dev-module-config/ui/hubspot/HsSectionHeader.tsx +20 -0
- package/src/dev-module-config/ui/hubspot/HsToggle.tsx +39 -0
- package/src/dev-module-config/ui/hubspot/HsToggleRow.tsx +30 -0
- package/src/dev-module-config/ui/hubspot/hs-color-palette.ts +11 -0
- package/src/dev-module-config/ui/hubspot/module-config-typography.ts +26 -0
- package/src/dev-module-config/ui/module-config-motion.css +134 -0
- package/src/dev-module-config/ui/module-config-motion.ts +28 -0
- package/src/dev-module-config/ui/module-config-overlay-side.ts +29 -0
- package/src/dev-module-config/ui/module-config-surfaces.ts +27 -0
- package/src/dev-module-config/ui/module-config-theme.css +489 -0
- package/src/dev-module-config/ui/module-config-toolbar-theme.ts +35 -0
- package/src/dev-module-config/ui/navigation/ModuleConfigNavContext.tsx +194 -0
- package/src/dev-module-config/ui/use-dev-module-config-live-revision.ts +22 -0
- package/src/dev-module-config/ui/use-hubspot-live-exports.ts +27 -0
- package/src/dev-module-config/ui/use-module-config.ts +124 -0
- package/src/routing/build-api-routes.ts +37 -0
- package/src/routing/route-format.ts +19 -0
- package/templates/module-hybrid-cdn/README.md +53 -3
- package/templates/module-hybrid-cdn/index.html +12 -1
- package/templates/module-hybrid-cdn/package.json +6 -2
- package/templates/module-hybrid-cdn/public/fields.json +44 -1
- package/templates/module-hybrid-cdn/src/app.tsx +8 -0
- package/templates/module-hybrid-cdn/src/assets/css/main.dev.css +6 -1
- package/templates/module-hybrid-cdn/src/cdn/app-entry.ts +32 -0
- package/templates/module-hybrid-cdn/src/cdn/vendor-entry.ts +28 -0
- package/templates/module-hybrid-cdn/src/components/layout/section-card.tsx +20 -0
- package/templates/module-hybrid-cdn/src/components/layout/starter-nav.tsx +28 -0
- package/templates/module-hybrid-cdn/src/components/layout/starter-shell.tsx +18 -0
- package/templates/module-hybrid-cdn/src/config/router.ts +4 -0
- package/templates/module-hybrid-cdn/src/config/routes.ts +5 -0
- package/templates/module-hybrid-cdn/src/defaultData.ts +2 -7
- package/templates/module-hybrid-cdn/src/features/starter/components/starter-guide-page.tsx +93 -0
- package/templates/module-hybrid-cdn/src/features/starter/components/starter-home-page.tsx +21 -0
- package/templates/module-hybrid-cdn/src/features/starter/components/starter-home.tsx +52 -0
- package/templates/module-hybrid-cdn/src/features/starter/components/starter-single-page.tsx +17 -0
- package/templates/module-hybrid-cdn/src/features/starter/content/woodscli-guide-content.ts +86 -0
- package/templates/module-hybrid-cdn/src/features/starter/hooks/starter-preview-values.ts +42 -0
- package/templates/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.dev.ts +17 -0
- package/templates/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.ts +30 -0
- package/templates/module-hybrid-cdn/src/features/starter/index.ts +6 -0
- package/templates/module-hybrid-cdn/src/hubspot/bootstrap.ts +10 -0
- package/templates/module-hybrid-cdn/src/hubspot/default-data.ts +7 -0
- package/templates/module-hybrid-cdn/src/hubspot/module-entry.tsx +9 -0
- package/templates/module-hybrid-cdn/src/integrations/tanstack-router/router.tsx +25 -0
- package/templates/module-hybrid-cdn/src/main.tsx +4 -3
- package/templates/module-hybrid-cdn/src/portal/mount-dev-preview.tsx +8 -0
- package/templates/module-hybrid-cdn/src/portal/mount-portal.tsx +12 -0
- package/templates/module-hybrid-cdn/src/routeTree.gen.ts +77 -0
- package/templates/module-hybrid-cdn/src/routes/__root.tsx +15 -0
- package/templates/module-hybrid-cdn/src/routes/guide.tsx +7 -0
- package/templates/module-hybrid-cdn/src/routes/index.tsx +7 -0
- package/templates/module-hybrid-cdn/tsconfig.json +20 -0
- package/templates/module-hybrid-cdn/vite.config.monolith.mjs +8 -0
- package/templates/module-monolith/README.md +59 -0
- package/templates/module-monolith/index.html +12 -0
- package/templates/module-monolith/package.json +10 -3
- package/templates/module-monolith/public/fields.json +44 -1
- package/templates/module-monolith/public/module.html +1 -0
- package/templates/module-monolith/src/app.tsx +8 -0
- package/templates/module-monolith/src/assets/css/main.dev.css +6 -1
- package/templates/module-monolith/src/components/layout/section-card.tsx +20 -0
- package/templates/module-monolith/src/components/layout/starter-nav.tsx +28 -0
- package/templates/module-monolith/src/components/layout/starter-shell.tsx +18 -0
- package/templates/module-monolith/src/config/router.ts +4 -0
- package/templates/module-monolith/src/config/routes.ts +5 -0
- package/templates/module-monolith/src/defaultData.ts +2 -7
- package/templates/module-monolith/src/features/starter/components/starter-guide-page.tsx +93 -0
- package/templates/module-monolith/src/features/starter/components/starter-home-page.tsx +21 -0
- package/templates/module-monolith/src/features/starter/components/starter-home.tsx +52 -0
- package/templates/module-monolith/src/features/starter/components/starter-single-page.tsx +17 -0
- package/templates/module-monolith/src/features/starter/content/woodscli-guide-content.ts +86 -0
- package/templates/module-monolith/src/features/starter/hooks/starter-preview-values.ts +42 -0
- package/templates/module-monolith/src/features/starter/hooks/use-starter-preview.dev.ts +17 -0
- package/templates/module-monolith/src/features/starter/hooks/use-starter-preview.ts +30 -0
- package/templates/module-monolith/src/features/starter/index.ts +6 -0
- package/templates/module-monolith/src/hubspot/default-data.ts +7 -0
- package/templates/module-monolith/src/hubspot/module-entry.tsx +9 -0
- package/templates/module-monolith/src/integrations/tanstack-router/router.tsx +25 -0
- package/templates/module-monolith/src/main.tsx +4 -3
- package/templates/module-monolith/src/portal/mount-dev-preview.tsx +8 -0
- package/templates/module-monolith/src/portal/mount-portal.tsx +12 -0
- package/templates/module-monolith/src/routeTree.gen.ts +77 -0
- package/templates/module-monolith/src/routes/__root.tsx +15 -0
- package/templates/module-monolith/src/routes/guide.tsx +7 -0
- package/templates/module-monolith/src/routes/index.tsx +7 -0
- package/templates/module-monolith/tsconfig.json +20 -0
- package/templates/module-monolith/vite.config.monolith.mjs +8 -0
- package/templates/module-monolith/woodscli.json +1 -0
- package/templates/shared/.env.dev.example +5 -0
- package/templates/shared/.env.prod.example +5 -0
- package/templates/shared/.env.stg.example +3 -0
- package/templates/shared/portal.cdn.json.example +8 -0
- package/templates/theme-starter/package.json +5 -2
- package/vite/vite.config.dev.mjs +9 -1
- package/vite/vite.config.monolith.mjs +22 -17
- package/dist/chunk-JDGIAYHX.js.map +0 -1
- package/dist/exec-K5C6LZP4.js +0 -10
- /package/dist/{exec-K5C6LZP4.js.map → exec-U2OJATR3.js.map} +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type HubSpotFieldTab = 'CONTENT' | 'STYLE'
|
|
2
|
+
|
|
3
|
+
export type HubSpotFieldVisibility = {
|
|
4
|
+
controlling_field?: string | null
|
|
5
|
+
controlling_field_path?: string | null
|
|
6
|
+
controlling_value_regex?: string | null
|
|
7
|
+
property?: string | null
|
|
8
|
+
operator?: 'EQUAL' | 'NOT_EQUAL' | string | null
|
|
9
|
+
access?: string | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type HubSpotFieldDefinition = {
|
|
13
|
+
id?: string
|
|
14
|
+
name: string
|
|
15
|
+
label: string
|
|
16
|
+
help_text?: string
|
|
17
|
+
inline_help_text?: string
|
|
18
|
+
required?: boolean
|
|
19
|
+
locked?: boolean
|
|
20
|
+
type: string
|
|
21
|
+
tab?: HubSpotFieldTab
|
|
22
|
+
display?: string
|
|
23
|
+
display_width?: string | null
|
|
24
|
+
expanded?: boolean
|
|
25
|
+
children?: Array<HubSpotFieldDefinition>
|
|
26
|
+
choices?: Array<{ label: string; value: string } | [string, string]>
|
|
27
|
+
default?: unknown
|
|
28
|
+
visibility?: HubSpotFieldVisibility
|
|
29
|
+
occurrence?: {
|
|
30
|
+
min?: number | null
|
|
31
|
+
max?: number | null
|
|
32
|
+
default?: number | null
|
|
33
|
+
}
|
|
34
|
+
group_occurrence_meta?: unknown
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Nested module field values keyed like HubSpot `module.*` paths */
|
|
38
|
+
export type ModuleFieldValues = Record<string, unknown>
|
|
39
|
+
|
|
40
|
+
export const MODULE_CONFIG_CHANGED = 'woodsportal:module-config-changed'
|
|
41
|
+
|
|
42
|
+
export const HUBSPOT_ONLY_FIELD_NAMES = new Set([
|
|
43
|
+
'portal_cdn_base_url',
|
|
44
|
+
'portal_build_id',
|
|
45
|
+
'portal_vendor_script',
|
|
46
|
+
'portal_app_script',
|
|
47
|
+
'portal_app_css',
|
|
48
|
+
'hubspot_data_source',
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
/** HubSpot stores these as booleans but renders them as group expand/collapse in the native editor. */
|
|
52
|
+
export const MODULE_CONFIG_UI_HIDDEN_FIELD_NAMES = new Set([
|
|
53
|
+
'developer_option',
|
|
54
|
+
'backend_server_url',
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
/** Top-level HubSpot sections that should start collapsed in the dev module config UI. */
|
|
58
|
+
export const MODULE_CONFIG_DEFAULT_COLLAPSED_SECTION_NAMES = new Set([
|
|
59
|
+
'home',
|
|
60
|
+
'menu_options',
|
|
61
|
+
'other_options',
|
|
62
|
+
'sso_buttons_customize_options',
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
export function isModuleConfigSectionDefaultCollapsed(fieldName: string): boolean {
|
|
66
|
+
return MODULE_CONFIG_DEFAULT_COLLAPSED_SECTION_NAMES.has(fieldName)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Shown on Content tab only — fields without `tab` in fields.json would otherwise appear on Styles too. */
|
|
70
|
+
export const MODULE_CONFIG_CONTENT_TAB_FIELD_NAMES = new Set([
|
|
71
|
+
'add_home_tab',
|
|
72
|
+
'developerMode',
|
|
73
|
+
'developer_options',
|
|
74
|
+
'home',
|
|
75
|
+
])
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { isFieldVisible, type VisibilityContext } from '../schema/visibility'
|
|
2
|
+
import { getNestedValue } from '../runtime/module-config-runtime'
|
|
3
|
+
import type { HubSpotFieldDefinition, ModuleFieldValues } from '../schema/types'
|
|
4
|
+
|
|
5
|
+
export type ValidationError = {
|
|
6
|
+
path: string
|
|
7
|
+
label: string
|
|
8
|
+
message: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isEmptyValue(value: unknown): boolean {
|
|
12
|
+
if (value === null || value === undefined) return true
|
|
13
|
+
if (typeof value === 'string') return value.trim().length === 0
|
|
14
|
+
if (typeof value === 'boolean' || typeof value === 'number') return false
|
|
15
|
+
if (Array.isArray(value)) return value.length === 0
|
|
16
|
+
if (typeof value === 'object') {
|
|
17
|
+
const record = value as Record<string, unknown>
|
|
18
|
+
if ('src' in record) return !String(record.src ?? '').trim()
|
|
19
|
+
if ('url' in record) {
|
|
20
|
+
const url = record.url as { href?: string } | undefined
|
|
21
|
+
return !String(url?.href ?? '').trim()
|
|
22
|
+
}
|
|
23
|
+
if ('color' in record) return !String(record.color ?? '').trim()
|
|
24
|
+
return Object.keys(record).length === 0
|
|
25
|
+
}
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function validateFieldsAtPath(
|
|
30
|
+
fields: Array<HubSpotFieldDefinition>,
|
|
31
|
+
moduleValues: ModuleFieldValues,
|
|
32
|
+
pathPrefix: string,
|
|
33
|
+
context: VisibilityContext,
|
|
34
|
+
errors: Array<ValidationError>,
|
|
35
|
+
): void {
|
|
36
|
+
for (const field of fields) {
|
|
37
|
+
if (!isFieldVisible(field, context)) continue
|
|
38
|
+
|
|
39
|
+
const path = pathPrefix ? `${pathPrefix}.${field.name}` : field.name
|
|
40
|
+
const value = getNestedValue(moduleValues, path)
|
|
41
|
+
|
|
42
|
+
if (field.required && isEmptyValue(value)) {
|
|
43
|
+
errors.push({
|
|
44
|
+
path,
|
|
45
|
+
label: field.label,
|
|
46
|
+
message: `${field.label} is required`,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (field.type === 'group' && field.children?.length) {
|
|
51
|
+
if (field.occurrence && Array.isArray(value)) {
|
|
52
|
+
value.forEach((item, index) => {
|
|
53
|
+
const itemPath = `${path}.${index}`
|
|
54
|
+
validateFieldsAtPath(field.children ?? [], moduleValues, itemPath, {
|
|
55
|
+
...context,
|
|
56
|
+
repeaterPath: path,
|
|
57
|
+
itemIndex: index,
|
|
58
|
+
itemValues: item as ModuleFieldValues,
|
|
59
|
+
}, errors)
|
|
60
|
+
})
|
|
61
|
+
} else if (!field.occurrence) {
|
|
62
|
+
validateFieldsAtPath(field.children, moduleValues, path, context, errors)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function validateModuleConfig(
|
|
69
|
+
schema: Array<HubSpotFieldDefinition>,
|
|
70
|
+
moduleValues: ModuleFieldValues,
|
|
71
|
+
): Array<ValidationError> {
|
|
72
|
+
const errors: Array<ValidationError> = []
|
|
73
|
+
validateFieldsAtPath(schema, moduleValues, '', { moduleValues }, errors)
|
|
74
|
+
|
|
75
|
+
const seen = new Set<string>()
|
|
76
|
+
return errors.filter((error) => {
|
|
77
|
+
if (seen.has(error.path)) return false
|
|
78
|
+
seen.add(error.path)
|
|
79
|
+
return true
|
|
80
|
+
})
|
|
81
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { HubSpotFieldDefinition, ModuleFieldValues } from './types'
|
|
2
|
+
|
|
3
|
+
export type VisibilityContext = {
|
|
4
|
+
/** Full module values */
|
|
5
|
+
moduleValues: ModuleFieldValues
|
|
6
|
+
/** e.g. menu_options.objects */
|
|
7
|
+
repeaterPath?: string
|
|
8
|
+
/** Index within repeater array */
|
|
9
|
+
itemIndex?: number
|
|
10
|
+
/** Current item values when editing a repeater row */
|
|
11
|
+
itemValues?: ModuleFieldValues
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getByPath(obj: ModuleFieldValues, path: string): unknown {
|
|
15
|
+
const parts = path.split('.')
|
|
16
|
+
let current: unknown = obj
|
|
17
|
+
for (const part of parts) {
|
|
18
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
if (/^\d+$/.test(part)) {
|
|
22
|
+
current = (current as Array<unknown>)[Number(part)]
|
|
23
|
+
} else {
|
|
24
|
+
current = (current as Record<string, unknown>)[part]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return current
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveControlValue(
|
|
31
|
+
controllingPath: string,
|
|
32
|
+
context: VisibilityContext,
|
|
33
|
+
): unknown {
|
|
34
|
+
const { moduleValues, repeaterPath, itemIndex, itemValues } = context
|
|
35
|
+
|
|
36
|
+
if (repeaterPath && itemValues && controllingPath.startsWith(`${repeaterPath}.`)) {
|
|
37
|
+
const relative = controllingPath.slice(repeaterPath.length + 1)
|
|
38
|
+
const topKey = relative.split('.')[0]
|
|
39
|
+
if (topKey in itemValues) {
|
|
40
|
+
return getByPath(itemValues, relative)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (repeaterPath && itemIndex !== undefined && controllingPath.startsWith(`${repeaterPath}.`)) {
|
|
45
|
+
const relative = controllingPath.slice(repeaterPath.length + 1)
|
|
46
|
+
const list = getByPath(moduleValues, repeaterPath)
|
|
47
|
+
if (Array.isArray(list) && list[itemIndex]) {
|
|
48
|
+
return getByPath(list[itemIndex] as ModuleFieldValues, relative)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return getByPath(moduleValues, controllingPath)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function regexMatches(value: unknown, regex: string): boolean {
|
|
56
|
+
if (regex === 'true') return value === true || value === 'true'
|
|
57
|
+
if (regex === 'false') return value === false || value === 'false'
|
|
58
|
+
try {
|
|
59
|
+
return new RegExp(regex).test(String(value ?? ''))
|
|
60
|
+
} catch {
|
|
61
|
+
return String(value ?? '') === regex
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isVisibilityContext(
|
|
66
|
+
value: ModuleFieldValues | VisibilityContext,
|
|
67
|
+
): value is VisibilityContext {
|
|
68
|
+
return (
|
|
69
|
+
typeof value === 'object' &&
|
|
70
|
+
value !== null &&
|
|
71
|
+
'moduleValues' in value &&
|
|
72
|
+
typeof (value as VisibilityContext).moduleValues === 'object'
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isFieldVisible(
|
|
77
|
+
field: HubSpotFieldDefinition,
|
|
78
|
+
moduleValuesOrContext: ModuleFieldValues | VisibilityContext,
|
|
79
|
+
): boolean {
|
|
80
|
+
const context: VisibilityContext = isVisibilityContext(moduleValuesOrContext)
|
|
81
|
+
? moduleValuesOrContext
|
|
82
|
+
: { moduleValues: moduleValuesOrContext }
|
|
83
|
+
|
|
84
|
+
const visibility = field.visibility
|
|
85
|
+
if (!visibility?.controlling_field_path) return true
|
|
86
|
+
|
|
87
|
+
const controlValue = resolveControlValue(visibility.controlling_field_path, context)
|
|
88
|
+
const regex = visibility.controlling_value_regex ?? 'true'
|
|
89
|
+
|
|
90
|
+
// HubSpot-only field hidden locally — treat as false so CRM-specific fields stay hidden.
|
|
91
|
+
if (
|
|
92
|
+
visibility.controlling_field_path === 'hubspot_data_source' &&
|
|
93
|
+
(controlValue === undefined || controlValue === null || controlValue === '')
|
|
94
|
+
) {
|
|
95
|
+
const matchesFalse = regexMatches(false, regex)
|
|
96
|
+
if (visibility.operator === 'NOT_EQUAL') return !matchesFalse
|
|
97
|
+
return matchesFalse
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const matches = regexMatches(controlValue, regex)
|
|
101
|
+
|
|
102
|
+
if (visibility.operator === 'NOT_EQUAL') return !matches
|
|
103
|
+
return matches
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function flattenVisibleFields(
|
|
107
|
+
fields: Array<HubSpotFieldDefinition>,
|
|
108
|
+
moduleValues: ModuleFieldValues,
|
|
109
|
+
tab?: 'CONTENT' | 'STYLE',
|
|
110
|
+
context?: Omit<VisibilityContext, 'moduleValues'>,
|
|
111
|
+
): Array<HubSpotFieldDefinition> {
|
|
112
|
+
const result: Array<HubSpotFieldDefinition> = []
|
|
113
|
+
const visibilityContext: VisibilityContext = { moduleValues, ...context }
|
|
114
|
+
|
|
115
|
+
for (const field of fields) {
|
|
116
|
+
if (tab && field.tab && field.tab !== tab) continue
|
|
117
|
+
if (!isFieldVisible(field, visibilityContext)) continue
|
|
118
|
+
result.push(field)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function chunkFields<T>(items: Array<T>, size: number): Array<Array<T>> {
|
|
125
|
+
const pages: Array<Array<T>> = []
|
|
126
|
+
for (let i = 0; i < items.length; i += size) {
|
|
127
|
+
pages.push(items.slice(i, i + size))
|
|
128
|
+
}
|
|
129
|
+
return pages.length ? pages : [[]]
|
|
130
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ModuleFieldValues } from '../schema/types'
|
|
2
|
+
|
|
3
|
+
export type WorkspaceConfigRecord = {
|
|
4
|
+
version: number
|
|
5
|
+
module: ModuleFieldValues
|
|
6
|
+
updatedAt: string | null
|
|
7
|
+
source?: 'local' | 'example' | 'empty'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function exportConfigJson(moduleValues: ModuleFieldValues): string {
|
|
11
|
+
return JSON.stringify({ version: 1, module: moduleValues }, null, 2)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function importConfigJson(raw: string): ModuleFieldValues {
|
|
15
|
+
const parsed = JSON.parse(raw) as { module?: ModuleFieldValues } | ModuleFieldValues
|
|
16
|
+
if ('module' in parsed && parsed.module && typeof parsed.module === 'object') {
|
|
17
|
+
return parsed.module as ModuleFieldValues
|
|
18
|
+
}
|
|
19
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
20
|
+
return parsed as ModuleFieldValues
|
|
21
|
+
}
|
|
22
|
+
throw new Error('Invalid module config JSON')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function uploadBrandingAsset(file: File): Promise<string> {
|
|
26
|
+
const buffer = await file.arrayBuffer()
|
|
27
|
+
const bytes = new Uint8Array(buffer)
|
|
28
|
+
let binary = ''
|
|
29
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
30
|
+
binary += String.fromCharCode(bytes[i]!)
|
|
31
|
+
}
|
|
32
|
+
const data = btoa(binary)
|
|
33
|
+
|
|
34
|
+
const response = await fetch('/__dev/branding-asset', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/json' },
|
|
37
|
+
body: JSON.stringify({ filename: file.name, data }),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(`Upload failed: ${response.status}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const payload = (await response.json()) as { src: string }
|
|
45
|
+
return payload.src
|
|
46
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { deepMerge } from '../schema/deep-merge'
|
|
2
|
+
import type { ModuleFieldValues } from '../schema/types'
|
|
3
|
+
import type { WorkspaceConfigRecord } from './config-json'
|
|
4
|
+
|
|
5
|
+
const DB_NAME = 'woodsportal-module-config'
|
|
6
|
+
const DB_VERSION = 1
|
|
7
|
+
const OVERRIDES_KEY = 'active'
|
|
8
|
+
|
|
9
|
+
type OverridesRecord = {
|
|
10
|
+
version: number
|
|
11
|
+
patches: ModuleFieldValues
|
|
12
|
+
updatedAt: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { exportConfigJson, importConfigJson, uploadBrandingAsset } from './config-json'
|
|
16
|
+
|
|
17
|
+
export async function loadWorkspaceMeta(): Promise<WorkspaceConfigRecord['source']> {
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch('/__dev/module-config')
|
|
20
|
+
if (!response.ok) return 'empty'
|
|
21
|
+
const record = (await response.json()) as WorkspaceConfigRecord
|
|
22
|
+
return record.source ?? 'empty'
|
|
23
|
+
} catch {
|
|
24
|
+
return 'empty'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function loadOverrides(): Promise<ModuleFieldValues> {
|
|
29
|
+
try {
|
|
30
|
+
const migrated = await migrateFromIndexedDbIfNeeded()
|
|
31
|
+
if (migrated) return migrated
|
|
32
|
+
|
|
33
|
+
const response = await fetch('/__dev/module-config')
|
|
34
|
+
if (!response.ok) return {}
|
|
35
|
+
const record = (await response.json()) as WorkspaceConfigRecord
|
|
36
|
+
return record.module ?? {}
|
|
37
|
+
} catch {
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function saveOverrides(patches: ModuleFieldValues): Promise<void> {
|
|
43
|
+
const record: WorkspaceConfigRecord = {
|
|
44
|
+
version: 1,
|
|
45
|
+
module: patches,
|
|
46
|
+
updatedAt: new Date().toISOString(),
|
|
47
|
+
source: 'local',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const response = await fetch('/__dev/module-config', {
|
|
51
|
+
method: 'PUT',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify(record),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`Failed to save module config: ${response.status}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function clearOverrides(): Promise<void> {
|
|
62
|
+
const response = await fetch('/__dev/module-config', { method: 'DELETE' })
|
|
63
|
+
if (!response.ok && response.status !== 204) {
|
|
64
|
+
throw new Error(`Failed to clear module config: ${response.status}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function mergeWithDefaults(
|
|
69
|
+
defaults: ModuleFieldValues,
|
|
70
|
+
patches: ModuleFieldValues,
|
|
71
|
+
): Promise<ModuleFieldValues> {
|
|
72
|
+
return deepMerge(defaults, patches)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function migrateFromIndexedDbIfNeeded(): Promise<ModuleFieldValues | null> {
|
|
76
|
+
if (typeof indexedDB === 'undefined') return null
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch('/__dev/module-config')
|
|
80
|
+
if (!response.ok) return null
|
|
81
|
+
const record = (await response.json()) as WorkspaceConfigRecord
|
|
82
|
+
if (record.source === 'local') return null
|
|
83
|
+
|
|
84
|
+
const idbPatches = await readIndexedDbOverrides()
|
|
85
|
+
if (!idbPatches || Object.keys(idbPatches).length === 0) return null
|
|
86
|
+
|
|
87
|
+
await saveOverrides(idbPatches)
|
|
88
|
+
await clearIndexedDbOverrides()
|
|
89
|
+
console.info('[module-config] migrated IndexedDB overrides → .dev/module-config.local.json')
|
|
90
|
+
return idbPatches
|
|
91
|
+
} catch {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function openDb(): Promise<IDBDatabase> {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
|
99
|
+
request.onerror = () => reject(request.error ?? new Error('IndexedDB open failed'))
|
|
100
|
+
request.onsuccess = () => resolve(request.result)
|
|
101
|
+
request.onupgradeneeded = () => {
|
|
102
|
+
const db = request.result
|
|
103
|
+
if (!db.objectStoreNames.contains('overrides')) {
|
|
104
|
+
db.createObjectStore('overrides')
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readIndexedDbOverrides(): Promise<ModuleFieldValues | null> {
|
|
111
|
+
try {
|
|
112
|
+
const db = await openDb()
|
|
113
|
+
return await new Promise((resolve, reject) => {
|
|
114
|
+
const tx = db.transaction('overrides', 'readonly')
|
|
115
|
+
const request = tx.objectStore('overrides').get(OVERRIDES_KEY)
|
|
116
|
+
request.onerror = () => reject(request.error)
|
|
117
|
+
request.onsuccess = () => {
|
|
118
|
+
const record = request.result as OverridesRecord | undefined
|
|
119
|
+
resolve(record?.patches ?? null)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
} catch {
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function clearIndexedDbOverrides(): Promise<void> {
|
|
128
|
+
try {
|
|
129
|
+
const db = await openDb()
|
|
130
|
+
await new Promise<void>((resolve, reject) => {
|
|
131
|
+
const tx = db.transaction('overrides', 'readwrite')
|
|
132
|
+
const request = tx.objectStore('overrides').delete(OVERRIDES_KEY)
|
|
133
|
+
request.onerror = () => reject(request.error)
|
|
134
|
+
request.onsuccess = () => resolve()
|
|
135
|
+
})
|
|
136
|
+
} catch {
|
|
137
|
+
// ignore migration cleanup errors
|
|
138
|
+
}
|
|
139
|
+
}
|