@woodsportal/hubspot-kit 5.0.2 → 5.0.3
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 +6 -0
- package/package.json +2 -2
- package/scripts/tailwind/prefix-plugin.js +131 -182
- package/scripts/tailwind/prefix-plugin.test.mjs +110 -0
- package/scripts/tailwind/tailwind-content.js +29 -28
- package/scripts/vite/create-dev-server-config.mjs +14 -3
- package/scripts/vite/portal-build-shared.js +5 -3
- package/src/dev-module-config/__tests__/build-api-routes-custom-menu.test.ts +32 -0
- package/src/dev-module-config/__tests__/custom-menu.test.ts +77 -0
- package/src/dev-module-config/__tests__/drill-down-panels.test.ts +65 -0
- package/src/dev-module-config/__tests__/fixtures/golden-module.fixture.ts +2 -0
- package/src/dev-module-config/__tests__/flatten-visible-fields.test.ts +34 -1
- package/src/dev-module-config/__tests__/module-config-transform.test.ts +27 -0
- package/src/dev-module-config/runtime/module-config-runtime.ts +3 -1
- package/src/dev-module-config/schema/custom-menu.ts +124 -0
- package/src/dev-module-config/schema/validate-module-config.ts +2 -0
- package/src/dev-module-config/transform/build-hubspot-data.ts +14 -0
- package/src/dev-module-config/ui/FieldRenderer.tsx +2 -0
- package/src/dev-module-config/ui/ModuleConfigAnimatedPresence.tsx +4 -2
- package/src/dev-module-config/ui/ModuleConfigShell.tsx +16 -4
- package/src/dev-module-config/ui/editors/MenuOptionsEditor.tsx +34 -0
- package/src/dev-module-config/ui/hubspot/HsOccurrenceGroupEditor.tsx +16 -11
- package/src/dev-module-config/ui/navigation/ModuleConfigNavContext.tsx +27 -23
- package/src/dev-module-config/ui/navigation/drill-down-panels.ts +48 -0
- package/src/routing/build-api-routes.ts +45 -15
- package/src/routing/custom-menu-path.ts +14 -0
- package/templates/module-hybrid-cdn/src/assets/css/main.prod.css +2 -2
|
@@ -14,7 +14,7 @@ function DrillDownSync({ id, panel }: { id: string; panel: ReactNode }) {
|
|
|
14
14
|
|
|
15
15
|
useLayoutEffect(() => {
|
|
16
16
|
registerDrillDownPanel(id, panel)
|
|
17
|
-
})
|
|
17
|
+
}, [id, panel, registerDrillDownPanel])
|
|
18
18
|
|
|
19
19
|
useLayoutEffect(() => {
|
|
20
20
|
return () => registerDrillDownPanel(id, null)
|
|
@@ -75,25 +75,28 @@ export function HsOccurrenceGroupEditor({
|
|
|
75
75
|
const navSectionId = `section-${path}-${instanceId}`
|
|
76
76
|
const navSectionLabel = sectionLabel ?? field.label
|
|
77
77
|
const scopeBase = editorScope ?? path
|
|
78
|
+
const visibilityRepeaterPath = visibilityContext?.repeaterPath
|
|
79
|
+
? `${visibilityContext.repeaterPath}.${field.name}`
|
|
80
|
+
: path
|
|
78
81
|
|
|
79
82
|
const itemVisibilityContext = useMemo(
|
|
80
83
|
(): VisibilityContext => ({
|
|
81
84
|
moduleValues: visibilityContext?.moduleValues ?? moduleValues,
|
|
82
|
-
repeaterPath:
|
|
85
|
+
repeaterPath: visibilityRepeaterPath,
|
|
83
86
|
itemIndex: editingIndex ?? undefined,
|
|
84
87
|
itemValues: editingIndex !== null ? items[editingIndex] : undefined,
|
|
85
88
|
}),
|
|
86
|
-
[visibilityContext?.moduleValues, moduleValues,
|
|
89
|
+
[visibilityContext?.moduleValues, moduleValues, visibilityRepeaterPath, editingIndex, items],
|
|
87
90
|
)
|
|
88
91
|
|
|
89
92
|
const visibleChildren = useMemo(() => {
|
|
90
93
|
if (editingIndex === null || !items[editingIndex]) return []
|
|
91
94
|
return flattenVisibleFields(children, moduleValues, undefined, {
|
|
92
|
-
repeaterPath:
|
|
95
|
+
repeaterPath: visibilityRepeaterPath,
|
|
93
96
|
itemIndex: editingIndex,
|
|
94
97
|
itemValues: items[editingIndex],
|
|
95
98
|
})
|
|
96
|
-
}, [children, moduleValues, editingIndex, items,
|
|
99
|
+
}, [children, moduleValues, editingIndex, items, visibilityRepeaterPath])
|
|
97
100
|
|
|
98
101
|
const closeEditor = () => {
|
|
99
102
|
setEditingIndex((current) => {
|
|
@@ -129,17 +132,19 @@ export function HsOccurrenceGroupEditor({
|
|
|
129
132
|
const openEditor = (index: number, item: ModuleFieldValues) => {
|
|
130
133
|
const itemLabel = String(item[labelField] || field.label || 'Item')
|
|
131
134
|
const itemNavId = `${path}-item-${index}-${instanceId}`
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
if (!editorScope) {
|
|
136
|
+
pushSegment({
|
|
137
|
+
id: navSectionId,
|
|
138
|
+
label: navSectionLabel,
|
|
139
|
+
onPop: closeEditor,
|
|
140
|
+
})
|
|
141
|
+
registerPopHandler(navSectionId, closeEditor)
|
|
142
|
+
}
|
|
137
143
|
pushSegment({
|
|
138
144
|
id: itemNavId,
|
|
139
145
|
label: itemLabel,
|
|
140
146
|
onPop: closeEditor,
|
|
141
147
|
})
|
|
142
|
-
registerPopHandler(navSectionId, closeEditor)
|
|
143
148
|
registerPopHandler(itemNavId, closeEditor)
|
|
144
149
|
setEditingIndex(index)
|
|
145
150
|
}
|
|
@@ -8,6 +8,12 @@ import {
|
|
|
8
8
|
} from 'react'
|
|
9
9
|
|
|
10
10
|
import { devModuleDisplayName } from '../../dev-module-display-name'
|
|
11
|
+
import {
|
|
12
|
+
applyDrillDownPanelRegistration,
|
|
13
|
+
listDrillDownLayers,
|
|
14
|
+
type DrillDownLayer,
|
|
15
|
+
type DrillDownPanelMap,
|
|
16
|
+
} from './drill-down-panels'
|
|
11
17
|
|
|
12
18
|
export type BreadcrumbSegment = {
|
|
13
19
|
id: string
|
|
@@ -18,6 +24,7 @@ export type BreadcrumbSegment = {
|
|
|
18
24
|
type ModuleConfigNavContextValue = {
|
|
19
25
|
segments: Array<BreadcrumbSegment>
|
|
20
26
|
drillDownPanel: ReactNode | null
|
|
27
|
+
drillDownLayers: Array<DrillDownLayer>
|
|
21
28
|
pushSegment: (segment: Omit<BreadcrumbSegment, 'onClick'> & { onPop?: () => void }) => void
|
|
22
29
|
popTo: (id: string) => void
|
|
23
30
|
resetNav: () => void
|
|
@@ -42,34 +49,29 @@ const ModuleConfigNavContext = createContext<ModuleConfigNavContextValue | null>
|
|
|
42
49
|
export function ModuleConfigNavProvider({ children }: { children: ReactNode }) {
|
|
43
50
|
const [stack, setStack] = useState<Array<BreadcrumbSegment>>([ROOT_SEGMENT])
|
|
44
51
|
const [popHandlers, setPopHandlers] = useState<Record<string, () => void>>({})
|
|
45
|
-
const [
|
|
46
|
-
|
|
52
|
+
const [drillDown, setDrillDown] = useState<{ panels: DrillDownPanelMap; order: string[] }>({
|
|
53
|
+
panels: {},
|
|
54
|
+
order: [],
|
|
55
|
+
})
|
|
47
56
|
|
|
48
57
|
const registerDrillDownPanel = useCallback((id: string, panel: ReactNode | null) => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return next
|
|
55
|
-
})
|
|
56
|
-
setDrillDownOrder((order) => order.filter((entry) => entry !== id))
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
setDrillDownPanels((prev) => ({ ...prev, [id]: panel }))
|
|
61
|
-
setDrillDownOrder((order) => (order.includes(id) ? order : [...order, id]))
|
|
58
|
+
setDrillDown((prev) => {
|
|
59
|
+
const next = applyDrillDownPanelRegistration(prev.panels, prev.order, id, panel)
|
|
60
|
+
if (next.panels === prev.panels && next.order === prev.order) return prev
|
|
61
|
+
return next
|
|
62
|
+
})
|
|
62
63
|
}, [])
|
|
63
64
|
|
|
64
65
|
const clearAllDrillDownPanels = useCallback(() => {
|
|
65
|
-
|
|
66
|
-
setDrillDownOrder([])
|
|
66
|
+
setDrillDown({ panels: {}, order: [] })
|
|
67
67
|
}, [])
|
|
68
68
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
const drillDownLayers = useMemo(
|
|
70
|
+
() => listDrillDownLayers(drillDown.panels, drillDown.order),
|
|
71
|
+
[drillDown],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const drillDownPanel = drillDownLayers[drillDownLayers.length - 1]?.panel ?? null
|
|
73
75
|
|
|
74
76
|
const setDrillDownPanel = useCallback(
|
|
75
77
|
(panel: ReactNode | null) => {
|
|
@@ -120,7 +122,7 @@ export function ModuleConfigNavProvider({ children }: { children: ReactNode }) {
|
|
|
120
122
|
setStack((prev) => {
|
|
121
123
|
const existing = prev.findIndex((item) => item.id === segment.id)
|
|
122
124
|
if (existing >= 0) {
|
|
123
|
-
return [...prev.slice(0, existing
|
|
125
|
+
return [...prev.slice(0, existing), { id: segment.id, label: segment.label }]
|
|
124
126
|
}
|
|
125
127
|
return [...prev, { id: segment.id, label: segment.label }]
|
|
126
128
|
})
|
|
@@ -147,8 +149,8 @@ export function ModuleConfigNavProvider({ children }: { children: ReactNode }) {
|
|
|
147
149
|
|
|
148
150
|
if (middle.length > 2) {
|
|
149
151
|
const collapsed: Array<BreadcrumbSegment> = [
|
|
150
|
-
middle[0]!,
|
|
151
152
|
{ id: 'ellipsis', label: '…' },
|
|
153
|
+
middle[middle.length - 2]!,
|
|
152
154
|
middle[middle.length - 1]!,
|
|
153
155
|
]
|
|
154
156
|
return [ROOT_SEGMENT, moduleTitle, ...collapsed]
|
|
@@ -161,6 +163,7 @@ export function ModuleConfigNavProvider({ children }: { children: ReactNode }) {
|
|
|
161
163
|
() => ({
|
|
162
164
|
segments,
|
|
163
165
|
drillDownPanel,
|
|
166
|
+
drillDownLayers,
|
|
164
167
|
pushSegment,
|
|
165
168
|
popTo,
|
|
166
169
|
resetNav,
|
|
@@ -171,6 +174,7 @@ export function ModuleConfigNavProvider({ children }: { children: ReactNode }) {
|
|
|
171
174
|
[
|
|
172
175
|
segments,
|
|
173
176
|
drillDownPanel,
|
|
177
|
+
drillDownLayers,
|
|
174
178
|
pushSegment,
|
|
175
179
|
popTo,
|
|
176
180
|
resetNav,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export type DrillDownPanelMap = Record<string, ReactNode>
|
|
4
|
+
|
|
5
|
+
export type DrillDownLayer = {
|
|
6
|
+
id: string
|
|
7
|
+
panel: ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function applyDrillDownPanelRegistration(
|
|
11
|
+
panels: DrillDownPanelMap,
|
|
12
|
+
order: string[],
|
|
13
|
+
id: string,
|
|
14
|
+
panel: ReactNode | null,
|
|
15
|
+
): { panels: DrillDownPanelMap; order: string[] } {
|
|
16
|
+
if (panel === null) {
|
|
17
|
+
if (!(id in panels) && !order.includes(id)) {
|
|
18
|
+
return { panels, order }
|
|
19
|
+
}
|
|
20
|
+
const nextPanels = { ...panels }
|
|
21
|
+
delete nextPanels[id]
|
|
22
|
+
return {
|
|
23
|
+
panels: nextPanels,
|
|
24
|
+
order: order.filter((entry) => entry !== id),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (Object.is(panels[id], panel) && order.includes(id)) {
|
|
29
|
+
return { panels, order }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
panels: { ...panels, [id]: panel },
|
|
34
|
+
order: order.includes(id) ? order : [...order, id],
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function listDrillDownLayers(
|
|
39
|
+
panels: DrillDownPanelMap,
|
|
40
|
+
order: string[],
|
|
41
|
+
): Array<DrillDownLayer> {
|
|
42
|
+
const layers: Array<DrillDownLayer> = []
|
|
43
|
+
for (const id of order) {
|
|
44
|
+
const panel = panels[id]
|
|
45
|
+
if (panel != null) layers.push({ id, panel })
|
|
46
|
+
}
|
|
47
|
+
return layers
|
|
48
|
+
}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { formatCustomObjectLabel, formatPath } from './route-format'
|
|
2
|
+
import { buildCustomMenuPath } from './custom-menu-path'
|
|
3
|
+
import type { OpenType } from '../dev-module-config/schema/custom-menu'
|
|
4
|
+
|
|
5
|
+
function isCustomMenuItem(menuItem: Record<string, unknown>): boolean {
|
|
6
|
+
return (
|
|
7
|
+
menuItem?.objectType === 'CUSTOM_MENU' ||
|
|
8
|
+
Boolean(menuItem?.isCustomMenu) ||
|
|
9
|
+
Boolean(String(menuItem?.menuUrl ?? '').trim())
|
|
10
|
+
)
|
|
11
|
+
}
|
|
2
12
|
|
|
3
13
|
export const makeLink = (menuItem: Record<string, unknown>) => {
|
|
14
|
+
if (isCustomMenuItem(menuItem)) {
|
|
15
|
+
const label = String(menuItem.label ?? menuItem.tabName ?? menuItem.menu_name ?? '').trim()
|
|
16
|
+
return buildCustomMenuPath(label)
|
|
17
|
+
}
|
|
18
|
+
|
|
4
19
|
let link = menuItem?.hubspotObjectTypeId as string | undefined
|
|
5
20
|
|
|
6
21
|
if (menuItem?.companyAsMediator) {
|
|
@@ -19,19 +34,34 @@ export const makeLink = (menuItem: Record<string, unknown>) => {
|
|
|
19
34
|
}
|
|
20
35
|
|
|
21
36
|
export function buildApiRoutes(sidebarMenuOptions: Array<Record<string, unknown>>) {
|
|
22
|
-
return sidebarMenuOptions.map((menuItem) =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
return sidebarMenuOptions.map((menuItem) => {
|
|
38
|
+
const isCustomMenu = isCustomMenuItem(menuItem)
|
|
39
|
+
const selectOption = (menuItem.selectOption as OpenType | undefined) ?? 'open_in_new_tab'
|
|
40
|
+
const menuUrl = isCustomMenu ? String(menuItem.menuUrl ?? '').trim() : undefined
|
|
41
|
+
const openInNewTab = isCustomMenu ? selectOption === 'open_in_new_tab' : undefined
|
|
42
|
+
const title = formatCustomObjectLabel((menuItem.tabName || menuItem.label) as string)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
hubspotObjectTypeId: `${menuItem.hubspotObjectTypeId ?? ''}`,
|
|
46
|
+
path: makeLink(menuItem),
|
|
47
|
+
title,
|
|
48
|
+
icon: menuItem.icon,
|
|
49
|
+
isRequiredAuth: !isCustomMenu,
|
|
50
|
+
isHeader: true,
|
|
51
|
+
isCustomMenu,
|
|
52
|
+
selectOption: isCustomMenu ? selectOption : undefined,
|
|
53
|
+
menuUrl,
|
|
54
|
+
openInNewTab,
|
|
55
|
+
target: isCustomMenu && openInNewTab ? '_blank' : undefined,
|
|
56
|
+
rel: isCustomMenu && openInNewTab ? 'noopener noreferrer' : undefined,
|
|
57
|
+
companyAsMediator: menuItem.companyAsMediator,
|
|
58
|
+
pipeLineId: menuItem.pipeLineId,
|
|
59
|
+
specPipeLine: menuItem.specPipeLine,
|
|
60
|
+
objectDescription: menuItem.objectDescription,
|
|
61
|
+
objectUserProperties: menuItem.objectUserProperties,
|
|
62
|
+
homeCardsView: menuItem.homeCardsView,
|
|
63
|
+
objectUserPropertiesView: menuItem.objectUserPropertiesView,
|
|
64
|
+
objectType: isCustomMenu ? 'CUSTOM_MENU' : menuItem.objectType,
|
|
65
|
+
}
|
|
66
|
+
})
|
|
37
67
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { formatPath } from './route-format'
|
|
2
|
+
|
|
3
|
+
export const CUSTOM_MENU_PATH_PREFIX = 'custom-menu:'
|
|
4
|
+
|
|
5
|
+
export function buildCustomMenuPath(menuName: string): string {
|
|
6
|
+
const slug = formatPath(menuName).trim()
|
|
7
|
+
return slug ? `${CUSTOM_MENU_PATH_PREFIX}${slug}` : '#'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseCustomMenuPath(path: string): string | null {
|
|
11
|
+
if (!path.startsWith(CUSTOM_MENU_PATH_PREFIX)) return null
|
|
12
|
+
const slug = path.slice(CUSTOM_MENU_PATH_PREFIX.length).trim()
|
|
13
|
+
return slug || null
|
|
14
|
+
}
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
@import 'tailwindcss' prefix(dw);
|
|
9
9
|
@import './tailwind.css.config.css';
|
|
10
10
|
|
|
11
|
-
/*
|
|
12
|
-
@source '../../../.temp
|
|
11
|
+
/* Exact file — Tailwind v4 skips gitignored directories for glob @source */
|
|
12
|
+
@source '../../../.temp/tailwind-classes.tsx';
|