@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.
Files changed (27) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +2 -2
  3. package/scripts/tailwind/prefix-plugin.js +131 -182
  4. package/scripts/tailwind/prefix-plugin.test.mjs +110 -0
  5. package/scripts/tailwind/tailwind-content.js +29 -28
  6. package/scripts/vite/create-dev-server-config.mjs +14 -3
  7. package/scripts/vite/portal-build-shared.js +5 -3
  8. package/src/dev-module-config/__tests__/build-api-routes-custom-menu.test.ts +32 -0
  9. package/src/dev-module-config/__tests__/custom-menu.test.ts +77 -0
  10. package/src/dev-module-config/__tests__/drill-down-panels.test.ts +65 -0
  11. package/src/dev-module-config/__tests__/fixtures/golden-module.fixture.ts +2 -0
  12. package/src/dev-module-config/__tests__/flatten-visible-fields.test.ts +34 -1
  13. package/src/dev-module-config/__tests__/module-config-transform.test.ts +27 -0
  14. package/src/dev-module-config/runtime/module-config-runtime.ts +3 -1
  15. package/src/dev-module-config/schema/custom-menu.ts +124 -0
  16. package/src/dev-module-config/schema/validate-module-config.ts +2 -0
  17. package/src/dev-module-config/transform/build-hubspot-data.ts +14 -0
  18. package/src/dev-module-config/ui/FieldRenderer.tsx +2 -0
  19. package/src/dev-module-config/ui/ModuleConfigAnimatedPresence.tsx +4 -2
  20. package/src/dev-module-config/ui/ModuleConfigShell.tsx +16 -4
  21. package/src/dev-module-config/ui/editors/MenuOptionsEditor.tsx +34 -0
  22. package/src/dev-module-config/ui/hubspot/HsOccurrenceGroupEditor.tsx +16 -11
  23. package/src/dev-module-config/ui/navigation/ModuleConfigNavContext.tsx +27 -23
  24. package/src/dev-module-config/ui/navigation/drill-down-panels.ts +48 -0
  25. package/src/routing/build-api-routes.ts +45 -15
  26. package/src/routing/custom-menu-path.ts +14 -0
  27. 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: path,
85
+ repeaterPath: visibilityRepeaterPath,
83
86
  itemIndex: editingIndex ?? undefined,
84
87
  itemValues: editingIndex !== null ? items[editingIndex] : undefined,
85
88
  }),
86
- [visibilityContext?.moduleValues, moduleValues, path, editingIndex, items],
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: path,
95
+ repeaterPath: visibilityRepeaterPath,
93
96
  itemIndex: editingIndex,
94
97
  itemValues: items[editingIndex],
95
98
  })
96
- }, [children, moduleValues, editingIndex, items, path])
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
- pushSegment({
133
- id: navSectionId,
134
- label: navSectionLabel,
135
- onPop: closeEditor,
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 [drillDownPanels, setDrillDownPanels] = useState<Record<string, ReactNode>>({})
46
- const [drillDownOrder, setDrillDownOrder] = useState<string[]>([])
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
- if (panel === null) {
50
- setDrillDownPanels((prev) => {
51
- if (!(id in prev)) return prev
52
- const next = { ...prev }
53
- delete next[id]
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
- setDrillDownPanels({})
66
- setDrillDownOrder([])
66
+ setDrillDown({ panels: {}, order: [] })
67
67
  }, [])
68
68
 
69
- const drillDownPanel = useMemo(() => {
70
- const topId = drillDownOrder[drillDownOrder.length - 1]
71
- return topId ? (drillDownPanels[topId] ?? null) : null
72
- }, [drillDownOrder, drillDownPanels])
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 + 1), { id: segment.id, label: segment.label }]
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
- 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
+ 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
- /* Prefixed utilities from tailwind-content.js (.temp) must match prefix plugin on TSX */
12
- @source '../../../.temp/**/*.{js,jsx,ts,tsx}';
11
+ /* Exact file Tailwind v4 skips gitignored directories for glob @source */
12
+ @source '../../../.temp/tailwind-classes.tsx';