@woodsportal/hubspot-kit 1.0.3 → 1.0.5
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/cli.js +24 -1
- package/dist/cli.js.map +1 -1
- package/examples/greenfield/module-hybrid-cdn/src/assets/css/main.dev.css +3 -0
- package/examples/greenfield/module-hybrid-cdn/src/defaultData.ts +7 -0
- package/examples/greenfield/module-hybrid-cdn/src/hubspot/types.ts +17 -0
- package/examples/greenfield/module-hybrid-cdn/src/main.tsx +4 -2
- package/examples/greenfield/module-monolith/src/defaultData.ts +7 -0
- package/package.json +5 -1
- package/src/dev/bootstrap-greenfield.tsx +51 -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/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 +354 -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 +483 -0
- package/src/dev-module-config/ui/module-config-toolbar-theme.ts +35 -0
- package/src/dev-module-config/ui/navigation/ModuleConfigNavContext.tsx +188 -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/index.html +12 -1
- package/templates/module-hybrid-cdn/package.json +1 -1
- package/templates/module-hybrid-cdn/src/assets/css/main.dev.css +3 -0
- package/templates/module-hybrid-cdn/src/defaultData.ts +7 -0
- package/templates/module-hybrid-cdn/src/hubspot/types.ts +17 -0
- package/templates/module-hybrid-cdn/src/main.tsx +4 -2
- package/templates/module-monolith/index.html +12 -0
- package/templates/module-monolith/package.json +1 -1
- package/templates/module-monolith/src/assets/css/main.dev.css +3 -0
- package/templates/module-monolith/src/defaultData.ts +7 -0
- package/templates/module-monolith/src/hubspot/types.ts +17 -0
- package/templates/module-monolith/src/main.tsx +4 -0
- package/templates/theme-starter/package.json +1 -1
- package/vite/vite.config.dev.mjs +7 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type ModuleConfigToolbarTheme = 'light' | 'dark'
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'woodsportal:module-config-toolbar-theme'
|
|
4
|
+
|
|
5
|
+
export function detectPortalTheme(): ModuleConfigToolbarTheme {
|
|
6
|
+
if (typeof document === 'undefined') return 'light'
|
|
7
|
+
return document.documentElement.classList.contains('dark') ? 'dark' : 'light'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function loadModuleConfigToolbarTheme(): ModuleConfigToolbarTheme {
|
|
11
|
+
if (typeof window === 'undefined') return 'light'
|
|
12
|
+
try {
|
|
13
|
+
const stored = window.localStorage.getItem(STORAGE_KEY)
|
|
14
|
+
if (stored === 'dark' || stored === 'light') return stored
|
|
15
|
+
} catch {
|
|
16
|
+
// ignore
|
|
17
|
+
}
|
|
18
|
+
return detectPortalTheme()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function saveModuleConfigToolbarTheme(theme: ModuleConfigToolbarTheme): void {
|
|
22
|
+
try {
|
|
23
|
+
window.localStorage.setItem(STORAGE_KEY, theme)
|
|
24
|
+
} catch {
|
|
25
|
+
// ignore
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function toggleModuleConfigToolbarTheme(
|
|
30
|
+
theme: ModuleConfigToolbarTheme,
|
|
31
|
+
): ModuleConfigToolbarTheme {
|
|
32
|
+
const next: ModuleConfigToolbarTheme = theme === 'dark' ? 'light' : 'dark'
|
|
33
|
+
saveModuleConfigToolbarTheme(next)
|
|
34
|
+
return next
|
|
35
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react'
|
|
9
|
+
|
|
10
|
+
export type BreadcrumbSegment = {
|
|
11
|
+
id: string
|
|
12
|
+
label: string
|
|
13
|
+
onClick?: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type ModuleConfigNavContextValue = {
|
|
17
|
+
segments: Array<BreadcrumbSegment>
|
|
18
|
+
drillDownPanel: ReactNode | null
|
|
19
|
+
pushSegment: (segment: Omit<BreadcrumbSegment, 'onClick'> & { onPop?: () => void }) => void
|
|
20
|
+
popTo: (id: string) => void
|
|
21
|
+
resetNav: () => void
|
|
22
|
+
registerPopHandler: (id: string, handler: () => void) => void
|
|
23
|
+
/** @deprecated use registerDrillDownPanel */
|
|
24
|
+
setDrillDownPanel: (panel: ReactNode | null) => void
|
|
25
|
+
registerDrillDownPanel: (id: string, panel: ReactNode | null) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ROOT_SEGMENT: BreadcrumbSegment = { id: 'contents', label: 'Contents' }
|
|
29
|
+
|
|
30
|
+
const ModuleConfigNavContext = createContext<ModuleConfigNavContextValue | null>(null)
|
|
31
|
+
|
|
32
|
+
export function ModuleConfigNavProvider({ children }: { children: ReactNode }) {
|
|
33
|
+
const [stack, setStack] = useState<Array<BreadcrumbSegment>>([ROOT_SEGMENT])
|
|
34
|
+
const [popHandlers, setPopHandlers] = useState<Record<string, () => void>>({})
|
|
35
|
+
const [drillDownPanels, setDrillDownPanels] = useState<Record<string, ReactNode>>({})
|
|
36
|
+
const [drillDownOrder, setDrillDownOrder] = useState<string[]>([])
|
|
37
|
+
|
|
38
|
+
const registerDrillDownPanel = useCallback((id: string, panel: ReactNode | null) => {
|
|
39
|
+
if (panel === null) {
|
|
40
|
+
setDrillDownPanels((prev) => {
|
|
41
|
+
if (!(id in prev)) return prev
|
|
42
|
+
const next = { ...prev }
|
|
43
|
+
delete next[id]
|
|
44
|
+
return next
|
|
45
|
+
})
|
|
46
|
+
setDrillDownOrder((order) => order.filter((entry) => entry !== id))
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setDrillDownPanels((prev) => ({ ...prev, [id]: panel }))
|
|
51
|
+
setDrillDownOrder((order) => (order.includes(id) ? order : [...order, id]))
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
const clearAllDrillDownPanels = useCallback(() => {
|
|
55
|
+
setDrillDownPanels({})
|
|
56
|
+
setDrillDownOrder([])
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
const drillDownPanel = useMemo(() => {
|
|
60
|
+
const topId = drillDownOrder[drillDownOrder.length - 1]
|
|
61
|
+
return topId ? (drillDownPanels[topId] ?? null) : null
|
|
62
|
+
}, [drillDownOrder, drillDownPanels])
|
|
63
|
+
|
|
64
|
+
const setDrillDownPanel = useCallback(
|
|
65
|
+
(panel: ReactNode | null) => {
|
|
66
|
+
if (panel === null) {
|
|
67
|
+
clearAllDrillDownPanels()
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
registerDrillDownPanel('__legacy__', panel)
|
|
71
|
+
},
|
|
72
|
+
[clearAllDrillDownPanels, registerDrillDownPanel],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const registerPopHandler = useCallback((id: string, handler: () => void) => {
|
|
76
|
+
setPopHandlers((prev) => ({ ...prev, [id]: handler }))
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
const popTo = useCallback(
|
|
80
|
+
(id: string) => {
|
|
81
|
+
if (id === 'module' || id === 'contents') {
|
|
82
|
+
for (const segment of stack.slice(1)) {
|
|
83
|
+
popHandlers[segment.id]?.()
|
|
84
|
+
}
|
|
85
|
+
setStack([ROOT_SEGMENT])
|
|
86
|
+
clearAllDrillDownPanels()
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setStack((prev) => {
|
|
91
|
+
const index = prev.findIndex((segment) => segment.id === id)
|
|
92
|
+
if (index < 0) return prev
|
|
93
|
+
|
|
94
|
+
const removed = prev.slice(index + 1)
|
|
95
|
+
for (const segment of removed) {
|
|
96
|
+
popHandlers[segment.id]?.()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return prev.slice(0, index + 1)
|
|
100
|
+
})
|
|
101
|
+
},
|
|
102
|
+
[clearAllDrillDownPanels, popHandlers, stack],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const pushSegment = useCallback(
|
|
106
|
+
(segment: Omit<BreadcrumbSegment, 'onClick'> & { onPop?: () => void }) => {
|
|
107
|
+
if (segment.onPop) {
|
|
108
|
+
setPopHandlers((prev) => ({ ...prev, [segment.id]: segment.onPop! }))
|
|
109
|
+
}
|
|
110
|
+
setStack((prev) => {
|
|
111
|
+
const existing = prev.findIndex((item) => item.id === segment.id)
|
|
112
|
+
if (existing >= 0) {
|
|
113
|
+
return [...prev.slice(0, existing + 1), { id: segment.id, label: segment.label }]
|
|
114
|
+
}
|
|
115
|
+
return [...prev, { id: segment.id, label: segment.label }]
|
|
116
|
+
})
|
|
117
|
+
},
|
|
118
|
+
[],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
const resetNav = useCallback(() => {
|
|
122
|
+
setStack([ROOT_SEGMENT])
|
|
123
|
+
clearAllDrillDownPanels()
|
|
124
|
+
}, [clearAllDrillDownPanels])
|
|
125
|
+
|
|
126
|
+
const segments = useMemo(() => {
|
|
127
|
+
const moduleTitle: BreadcrumbSegment = {
|
|
128
|
+
id: 'module',
|
|
129
|
+
label: 'woodsportal-module',
|
|
130
|
+
onClick: stack.length > 1 ? () => popTo('module') : undefined,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (stack.length <= 1) {
|
|
134
|
+
return [ROOT_SEGMENT, moduleTitle]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const middle = stack.slice(1).map((segment) => ({
|
|
138
|
+
...segment,
|
|
139
|
+
onClick: () => popTo(segment.id),
|
|
140
|
+
}))
|
|
141
|
+
|
|
142
|
+
if (middle.length > 2) {
|
|
143
|
+
const collapsed: Array<BreadcrumbSegment> = [
|
|
144
|
+
middle[0]!,
|
|
145
|
+
{ id: 'ellipsis', label: '…' },
|
|
146
|
+
middle[middle.length - 1]!,
|
|
147
|
+
]
|
|
148
|
+
return [ROOT_SEGMENT, moduleTitle, ...collapsed]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return [ROOT_SEGMENT, moduleTitle, ...middle]
|
|
152
|
+
}, [popTo, stack])
|
|
153
|
+
|
|
154
|
+
const value = useMemo(
|
|
155
|
+
() => ({
|
|
156
|
+
segments,
|
|
157
|
+
drillDownPanel,
|
|
158
|
+
pushSegment,
|
|
159
|
+
popTo,
|
|
160
|
+
resetNav,
|
|
161
|
+
registerPopHandler,
|
|
162
|
+
setDrillDownPanel,
|
|
163
|
+
registerDrillDownPanel,
|
|
164
|
+
}),
|
|
165
|
+
[
|
|
166
|
+
segments,
|
|
167
|
+
drillDownPanel,
|
|
168
|
+
pushSegment,
|
|
169
|
+
popTo,
|
|
170
|
+
resetNav,
|
|
171
|
+
registerPopHandler,
|
|
172
|
+
setDrillDownPanel,
|
|
173
|
+
registerDrillDownPanel,
|
|
174
|
+
],
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<ModuleConfigNavContext.Provider value={value}>{children}</ModuleConfigNavContext.Provider>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function useModuleConfigNav() {
|
|
183
|
+
const ctx = useContext(ModuleConfigNavContext)
|
|
184
|
+
if (!ctx) {
|
|
185
|
+
throw new Error('useModuleConfigNav must be used within ModuleConfigNavProvider')
|
|
186
|
+
}
|
|
187
|
+
return ctx
|
|
188
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { MODULE_CONFIG_CHANGED } from '../schema/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DEV-only: bump when module config is applied so the portal can re-read hubSpotData exports.
|
|
6
|
+
*/
|
|
7
|
+
export function useDevModuleConfigLiveRevision(): number {
|
|
8
|
+
const [revision, setRevision] = useState(0)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!import.meta.env.DEV) return
|
|
12
|
+
|
|
13
|
+
const onApplied = () => {
|
|
14
|
+
setRevision((value) => value + 1)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
window.addEventListener(MODULE_CONFIG_CHANGED, onApplied)
|
|
18
|
+
return () => window.removeEventListener(MODULE_CONFIG_CHANGED, onApplied)
|
|
19
|
+
}, [])
|
|
20
|
+
|
|
21
|
+
return revision
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { getDevHubSpotLiveExports, type DevHubSpotLiveExports } from '@/data/dev-hubspot-sync'
|
|
3
|
+
import { refreshHubSpotUserDetails } from '@/data/hubSpotData'
|
|
4
|
+
import { useDevModuleConfigLiveRevision } from './use-dev-module-config-live-revision'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DEV-only: re-read hubSpotData exports after module config Apply.
|
|
8
|
+
* Production callers receive revision 0 and should keep using module-level imports.
|
|
9
|
+
*/
|
|
10
|
+
export function useHubSpotLiveExports(): {
|
|
11
|
+
revision: number
|
|
12
|
+
live: DevHubSpotLiveExports | null
|
|
13
|
+
} {
|
|
14
|
+
const revision = useDevModuleConfigLiveRevision()
|
|
15
|
+
|
|
16
|
+
return useMemo(() => {
|
|
17
|
+
if (!import.meta.env.DEV) {
|
|
18
|
+
return { revision: 0, live: null }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
refreshHubSpotUserDetails()
|
|
22
|
+
return {
|
|
23
|
+
revision,
|
|
24
|
+
live: getDevHubSpotLiveExports(),
|
|
25
|
+
}
|
|
26
|
+
}, [revision])
|
|
27
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
getModuleConfigState,
|
|
4
|
+
getModuleFieldValues,
|
|
5
|
+
initModuleConfigRuntime,
|
|
6
|
+
updateModuleFieldValues,
|
|
7
|
+
} from '../runtime/module-config-runtime'
|
|
8
|
+
import { validateModuleConfig, type ValidationError } from '../schema/validate-module-config'
|
|
9
|
+
import { loadWorkspaceMeta } from '../storage/workspace-store'
|
|
10
|
+
import { MODULE_CONFIG_CHANGED } from '../schema/types'
|
|
11
|
+
import type { ModuleConfigState } from '../runtime/module-config-runtime'
|
|
12
|
+
import type { ModuleFieldValues } from '../schema/types'
|
|
13
|
+
|
|
14
|
+
export function useModuleConfig() {
|
|
15
|
+
const [state, setState] = useState<ModuleConfigState | null>(() => getModuleConfigState())
|
|
16
|
+
const [draftValues, setDraftValues] = useState<ModuleFieldValues>(() => getModuleFieldValues())
|
|
17
|
+
const [appliedValues, setAppliedValues] = useState<ModuleFieldValues>(() => getModuleFieldValues())
|
|
18
|
+
const [validationErrors, setValidationErrors] = useState<Array<ValidationError>>([])
|
|
19
|
+
const [persistenceSource, setPersistenceSource] = useState<'local' | 'example' | 'empty'>('empty')
|
|
20
|
+
const [isApplying, setIsApplying] = useState(false)
|
|
21
|
+
|
|
22
|
+
const isDirty =
|
|
23
|
+
JSON.stringify(draftValues) !== JSON.stringify(appliedValues)
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
void loadWorkspaceMeta().then((source) => setPersistenceSource(source ?? 'empty'))
|
|
27
|
+
}, [appliedValues])
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const onChange = (event: Event) => {
|
|
31
|
+
const detail = (event as CustomEvent<ModuleConfigState>).detail
|
|
32
|
+
setState(detail)
|
|
33
|
+
setDraftValues(detail.moduleValues)
|
|
34
|
+
setAppliedValues(detail.moduleValues)
|
|
35
|
+
setValidationErrors([])
|
|
36
|
+
}
|
|
37
|
+
window.addEventListener(MODULE_CONFIG_CHANGED, onChange)
|
|
38
|
+
return () => window.removeEventListener(MODULE_CONFIG_CHANGED, onChange)
|
|
39
|
+
}, [])
|
|
40
|
+
|
|
41
|
+
const setFieldValue = useCallback((path: string, value: unknown) => {
|
|
42
|
+
setDraftValues((current) => {
|
|
43
|
+
const next = structuredClone(current)
|
|
44
|
+
const parts = path.split('.')
|
|
45
|
+
let cursor: Record<string, unknown> = next
|
|
46
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
47
|
+
const key = parts[i]!
|
|
48
|
+
if (cursor[key] === undefined || cursor[key] === null) cursor[key] = {}
|
|
49
|
+
cursor = cursor[key] as Record<string, unknown>
|
|
50
|
+
}
|
|
51
|
+
cursor[parts[parts.length - 1]!] = value
|
|
52
|
+
return next
|
|
53
|
+
})
|
|
54
|
+
setValidationErrors((errors) => errors.filter((error) => !error.path.startsWith(path)))
|
|
55
|
+
}, [])
|
|
56
|
+
|
|
57
|
+
const applyChanges = useCallback(async (): Promise<boolean> => {
|
|
58
|
+
const currentState = getModuleConfigState()
|
|
59
|
+
if (!currentState) return false
|
|
60
|
+
|
|
61
|
+
const errors = validateModuleConfig(currentState.schema, draftValues)
|
|
62
|
+
if (errors.length > 0) {
|
|
63
|
+
setValidationErrors(errors)
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setIsApplying(true)
|
|
68
|
+
try {
|
|
69
|
+
const updated = await updateModuleFieldValues(draftValues)
|
|
70
|
+
setState(updated)
|
|
71
|
+
setDraftValues(updated.moduleValues)
|
|
72
|
+
setAppliedValues(updated.moduleValues)
|
|
73
|
+
setValidationErrors([])
|
|
74
|
+
const source = await loadWorkspaceMeta()
|
|
75
|
+
setPersistenceSource(source ?? 'local')
|
|
76
|
+
return true
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('[module-config] Apply failed', error)
|
|
79
|
+
setValidationErrors([
|
|
80
|
+
{
|
|
81
|
+
path: '_apply',
|
|
82
|
+
label: 'Apply',
|
|
83
|
+
message:
|
|
84
|
+
error instanceof Error
|
|
85
|
+
? error.message
|
|
86
|
+
: 'Failed to save config. Restart yarn local if the dev server was started before the module-config plugin was added.',
|
|
87
|
+
},
|
|
88
|
+
])
|
|
89
|
+
return false
|
|
90
|
+
} finally {
|
|
91
|
+
setIsApplying(false)
|
|
92
|
+
}
|
|
93
|
+
}, [draftValues])
|
|
94
|
+
|
|
95
|
+
const discardDraft = useCallback(() => {
|
|
96
|
+
setDraftValues(appliedValues)
|
|
97
|
+
setValidationErrors([])
|
|
98
|
+
}, [appliedValues])
|
|
99
|
+
|
|
100
|
+
const confirmDiscardIfDirty = useCallback((): boolean => {
|
|
101
|
+
if (!isDirty) return true
|
|
102
|
+
return window.confirm('You have unsaved changes. Discard them?')
|
|
103
|
+
}, [isDirty])
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
state,
|
|
107
|
+
moduleValues: draftValues,
|
|
108
|
+
appliedValues,
|
|
109
|
+
isDirty,
|
|
110
|
+
isApplying,
|
|
111
|
+
validationErrors,
|
|
112
|
+
persistenceSource,
|
|
113
|
+
setFieldValue,
|
|
114
|
+
applyChanges,
|
|
115
|
+
discardDraft,
|
|
116
|
+
confirmDiscardIfDirty,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function ensureModuleConfigReady() {
|
|
121
|
+
if (!getModuleConfigState()) {
|
|
122
|
+
await initModuleConfigRuntime()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { formatCustomObjectLabel, formatPath } from './route-format'
|
|
2
|
+
|
|
3
|
+
export const makeLink = (menuItem: Record<string, unknown>) => {
|
|
4
|
+
let link = menuItem?.hubspotObjectTypeId as string | undefined
|
|
5
|
+
|
|
6
|
+
if (menuItem?.companyAsMediator) {
|
|
7
|
+
link = `${link}-c`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (menuItem?.specPipeLine && menuItem?.pipeLineId) {
|
|
11
|
+
link = `${link}-${menuItem?.pipeLineId}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!link) {
|
|
15
|
+
return `/${formatPath((menuItem.tabName || menuItem.label) as string)}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return `/${link}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildApiRoutes(sidebarMenuOptions: Array<Record<string, unknown>>) {
|
|
22
|
+
return sidebarMenuOptions.map((menuItem) => ({
|
|
23
|
+
hubspotObjectTypeId: `${menuItem.hubspotObjectTypeId ?? ''}`,
|
|
24
|
+
path: makeLink(menuItem),
|
|
25
|
+
title: formatCustomObjectLabel((menuItem.tabName || menuItem.label) as string),
|
|
26
|
+
icon: menuItem.icon,
|
|
27
|
+
isRequiredAuth: true,
|
|
28
|
+
isHeader: true,
|
|
29
|
+
companyAsMediator: menuItem.companyAsMediator,
|
|
30
|
+
pipeLineId: menuItem.pipeLineId,
|
|
31
|
+
specPipeLine: menuItem.specPipeLine,
|
|
32
|
+
objectDescription: menuItem.objectDescription,
|
|
33
|
+
objectUserProperties: menuItem.objectUserProperties,
|
|
34
|
+
homeCardsView: menuItem.homeCardsView,
|
|
35
|
+
objectUserPropertiesView: menuItem.objectUserPropertiesView,
|
|
36
|
+
}))
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Route path / label helpers — runtime-safe, no consumer path aliases. */
|
|
2
|
+
|
|
3
|
+
export const formatPath = (key: unknown) => {
|
|
4
|
+
if (!key || typeof key !== 'string') return ''
|
|
5
|
+
|
|
6
|
+
return key
|
|
7
|
+
.replace(/\s+/g, '-')
|
|
8
|
+
.replace(/#/g, '%23')
|
|
9
|
+
.replace(/\b\w/g, (l: string) => l.toLowerCase())
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatCustomObjectLabel(key = '') {
|
|
13
|
+
if (!key || typeof key !== 'string') return ''
|
|
14
|
+
|
|
15
|
+
return key
|
|
16
|
+
.replace(/-/g, ' ')
|
|
17
|
+
.replace(/%23/g, '#')
|
|
18
|
+
.replace(/\b[a-z]/g, (l: string) => l.toUpperCase())
|
|
19
|
+
}
|
|
@@ -1 +1,12 @@
|
|
|
1
|
-
<!doctype html
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>WoodsCLI dev</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"deploy:prod": "wp deploy --tier prod"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@woodsportal/hubspot-kit": "^1.0.
|
|
12
|
+
"@woodsportal/hubspot-kit": "^1.0.5",
|
|
13
13
|
"@tailwindcss/vite": "^4.0.6",
|
|
14
14
|
"@tanstack/router-plugin": "^1.121.2",
|
|
15
15
|
"@vitejs/plugin-react": "^4.3.4",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Runtime contract for window.hubSpotData (greenfield stub). */
|
|
2
|
+
export type HubSpotPortalData = {
|
|
3
|
+
dataSourceSet?: unknown
|
|
4
|
+
hubId?: string | number
|
|
5
|
+
apiBaseUrlOption?: string
|
|
6
|
+
userData?: unknown
|
|
7
|
+
objectList?: unknown[]
|
|
8
|
+
isInEditor?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
interface Window {
|
|
13
|
+
hubSpotData?: HubSpotPortalData
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export {}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>WoodsCLI dev</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"deploy:prod": "wp deploy --tier prod"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@woodsportal/hubspot-kit": "^1.0.
|
|
12
|
+
"@woodsportal/hubspot-kit": "^1.0.5",
|
|
13
13
|
"@tailwindcss/vite": "^4.0.6",
|
|
14
14
|
"@tanstack/router-plugin": "^1.121.2",
|
|
15
15
|
"@vitejs/plugin-react": "^4.3.4",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Runtime contract for window.hubSpotData (greenfield stub). */
|
|
2
|
+
export type HubSpotPortalData = {
|
|
3
|
+
dataSourceSet?: unknown
|
|
4
|
+
hubId?: string | number
|
|
5
|
+
apiBaseUrlOption?: string
|
|
6
|
+
userData?: unknown
|
|
7
|
+
objectList?: unknown[]
|
|
8
|
+
isInEditor?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
interface Window {
|
|
13
|
+
hubSpotData?: HubSpotPortalData
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export {}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"deploy:prod": "wp deploy --tier prod"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@woodsportal/hubspot-kit": "^1.0.
|
|
12
|
+
"@woodsportal/hubspot-kit": "^1.0.5",
|
|
13
13
|
"@tailwindcss/vite": "^4.0.6",
|
|
14
14
|
"@tanstack/router-plugin": "^1.121.2",
|
|
15
15
|
"@vitejs/plugin-react": "^4.3.4",
|
package/vite/vite.config.dev.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineConfig } from 'vite'
|
|
2
2
|
import viteReact from '@vitejs/plugin-react'
|
|
3
3
|
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
|
|
4
|
+
import { existsSync } from 'node:fs'
|
|
4
5
|
import { dirname, resolve } from 'node:path'
|
|
5
6
|
import { fileURLToPath } from 'node:url'
|
|
6
7
|
import tailwindcss from '@tailwindcss/vite'
|
|
@@ -14,10 +15,14 @@ import { createDevReactPluginOptions } from '../scripts/vite/react-dev-plugin-op
|
|
|
14
15
|
import { ensureDevKitShims } from '../scripts/vite/ensure-dev-kit-shims.js'
|
|
15
16
|
import isPrefix from '../scripts/tailwind/prefix.env.js'
|
|
16
17
|
import { findProjectRoot, getKitRoot } from '../scripts/project-root.mjs'
|
|
18
|
+
import { readWoodsportalConfig } from '../scripts/read-woodsportal-config.mjs'
|
|
17
19
|
|
|
18
20
|
const kitRoot = getKitRoot()
|
|
19
21
|
const projectRoot = findProjectRoot()
|
|
20
22
|
const { moduleConfigRoot, bootstrapShim } = ensureDevKitShims(projectRoot, kitRoot)
|
|
23
|
+
const { config } = readWoodsportalConfig(projectRoot)
|
|
24
|
+
const routesDir = resolve(projectRoot, 'src/routes')
|
|
25
|
+
const hasTanStackRoutes = existsSync(routesDir)
|
|
21
26
|
|
|
22
27
|
export default defineConfig(({ mode, command }) => {
|
|
23
28
|
return {
|
|
@@ -27,7 +32,7 @@ export default defineConfig(({ mode, command }) => {
|
|
|
27
32
|
...(command === 'serve' && mode === 'dev'
|
|
28
33
|
? [sdkLogBridge(), moduleConfigStore(projectRoot)]
|
|
29
34
|
: []),
|
|
30
|
-
TanStackRouterVite({ autoCodeSplitting: false }),
|
|
35
|
+
...(hasTanStackRoutes ? [TanStackRouterVite({ autoCodeSplitting: false })] : []),
|
|
31
36
|
viteReact(createDevReactPluginOptions()),
|
|
32
37
|
tailwindcss(),
|
|
33
38
|
...(isPrefix(mode) ? [tailwindPrefixPlugin(), TailwindContentPlugin()] : []),
|
|
@@ -37,6 +42,7 @@ export default defineConfig(({ mode, command }) => {
|
|
|
37
42
|
alias: {
|
|
38
43
|
'@/dev/module-config': moduleConfigRoot,
|
|
39
44
|
'@woodsportal/hubspot-kit/dev/bootstrap': bootstrapShim,
|
|
45
|
+
'@wphs/adapter': resolve(projectRoot, config?.dev?.adapter ?? './wphs.adapter.ts'),
|
|
40
46
|
'@': resolve(projectRoot, './src'),
|
|
41
47
|
},
|
|
42
48
|
},
|