astro-tractstack 2.3.5 → 2.4.0

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 (49) hide show
  1. package/bin/create-tractstack.js +38 -59
  2. package/dist/index.js +60 -36
  3. package/package.json +46 -9
  4. package/templates/custom/minimal/codehooks.ts +13 -0
  5. package/templates/custom/shopify/ShopifyProductGrid.tsx +4 -4
  6. package/templates/custom/shopify/ShopifyServiceList.tsx +4 -4
  7. package/templates/custom/with-examples/codehooks.ts +15 -0
  8. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +38 -23
  9. package/templates/src/components/codehooks/EpinetTableView.tsx +5 -2
  10. package/templates/src/components/codehooks/EpinetWrapper.tsx +10 -5
  11. package/templates/src/components/codehooks/FeaturedArticle.astro +3 -3
  12. package/templates/src/components/codehooks/ListContent.astro +3 -3
  13. package/templates/src/components/compositor/Node.tsx +13 -2
  14. package/templates/src/components/compositor/nodes/Pane.tsx +2 -14
  15. package/templates/src/components/edit/pane/AddPanePanel.tsx +3 -2
  16. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +35 -14
  17. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  18. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +2 -2
  19. package/templates/src/components/storykeep/Dashboard_Analytics.tsx +8 -4
  20. package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +5 -2
  21. package/templates/src/components/storykeep/state/FetchAnalytics.tsx +8 -4
  22. package/templates/src/components/storykeep/widgets/Wizard.tsx +4 -2
  23. package/templates/src/lib/codeHookHelper.ts +156 -0
  24. package/templates/src/lib/resources.ts +41 -0
  25. package/templates/src/lib/storyData.ts +1 -2
  26. package/templates/src/pages/[...slug]/edit.astro +3 -3
  27. package/templates/src/pages/[...slug].astro +76 -70
  28. package/templates/src/pages/codehooks/[...hookId].astro +18 -0
  29. package/templates/src/pages/codehooks/bunny-video.astro +9 -0
  30. package/templates/src/pages/codehooks/custom-hero.astro +6 -0
  31. package/templates/src/pages/codehooks/epinet.astro +15 -0
  32. package/templates/src/pages/codehooks/featured-article.astro +13 -0
  33. package/templates/src/pages/codehooks/get-crafting.astro +8 -0
  34. package/templates/src/pages/codehooks/list-content.astro +13 -0
  35. package/templates/src/pages/codehooks/search-widget.astro +13 -0
  36. package/templates/src/pages/codehooks/shopify-product-grid.astro +25 -0
  37. package/templates/src/pages/codehooks/shopify-service-list.astro +25 -0
  38. package/templates/src/pages/context/[...contextSlug]/edit.astro +3 -3
  39. package/templates/src/pages/context/[...contextSlug].astro +47 -10
  40. package/templates/src/pages/sandbox.astro +3 -14
  41. package/templates/src/stores/analytics.ts +77 -107
  42. package/utils/inject-files.ts +62 -37
  43. package/templates/custom/minimal/CodeHook.astro +0 -72
  44. package/templates/custom/with-examples/CodeHook.astro +0 -81
  45. package/templates/custom/with-examples/ProductCard.astro +0 -29
  46. package/templates/custom/with-examples/ProductCardWrapper.astro +0 -43
  47. package/templates/custom/with-examples/ProductGrid.astro +0 -64
  48. package/templates/src/components/codehooks/ProductCardSetup.tsx +0 -157
  49. package/templates/src/components/codehooks/ProductGridSetup.tsx +0 -279
@@ -149,6 +149,7 @@ export const Node = memo((props: NodeProps) => {
149
149
  first={true}
150
150
  ctx={ctx}
151
151
  isContextPane={true}
152
+ isSandboxMode={props.isSandboxMode || false}
152
153
  />
153
154
  </>
154
155
  );
@@ -201,7 +202,12 @@ export const Node = memo((props: NodeProps) => {
201
202
  element = (
202
203
  <>
203
204
  {first && (
204
- <AddPanePanel nodeId={props.nodeId} first={true} ctx={ctx} />
205
+ <AddPanePanel
206
+ nodeId={props.nodeId}
207
+ first={true}
208
+ ctx={ctx}
209
+ isSandboxMode={props.isSandboxMode || false}
210
+ />
205
211
  )}
206
212
  <div className="py-0.5">
207
213
  <ConfigPanePanel
@@ -217,7 +223,12 @@ export const Node = memo((props: NodeProps) => {
217
223
  {content}
218
224
  </PanelVisibilityWrapper>
219
225
  </div>
220
- <AddPanePanel nodeId={props.nodeId} first={false} ctx={ctx} />
226
+ <AddPanePanel
227
+ nodeId={props.nodeId}
228
+ first={false}
229
+ ctx={ctx}
230
+ isSandboxMode={props.isSandboxMode || false}
231
+ />
221
232
  </>
222
233
  );
223
234
  }
@@ -5,8 +5,6 @@ import { RenderChildren } from './RenderChildren';
5
5
  import FeaturedArticleSetup from '@/components/codehooks/FeaturedArticleSetup';
6
6
  import ListContentSetup from '@/components/codehooks/ListContentSetup';
7
7
  import BunnyVideoSetup from '@/components/codehooks/BunnyVideoSetup';
8
- import { ProductCardSetup } from '@/components/codehooks/ProductCardSetup';
9
- import { ProductGridSetup } from '@/components/codehooks/ProductGridSetup';
10
8
  import { PaneOverlay } from '@/components/compositor/tools/PaneOverlay';
11
9
  import type {
12
10
  PaneNode,
@@ -15,13 +13,7 @@ import type {
15
13
  } from '@/types/compositorTypes';
16
14
  import type { NodeProps } from '@/types/nodeProps';
17
15
 
18
- const TARGETS = [
19
- 'list-content',
20
- 'featured-article',
21
- 'bunny-video',
22
- 'product-card',
23
- 'product-grid',
24
- ];
16
+ const TARGETS = ['list-content', 'featured-article', 'bunny-video'];
25
17
 
26
18
  const CodeHookContainer = ({
27
19
  payload,
@@ -171,11 +163,7 @@ const Pane = memo(
171
163
  id={getCtx(props).getNodeSlug(props.nodeId)}
172
164
  className={useFlexLayout ? '' : wrapperClasses}
173
165
  >
174
- {codeHookPayload && codeHookTarget === 'product-card' ? (
175
- <ProductCardSetup nodeId={props.nodeId} params={codeHookParams} />
176
- ) : codeHookPayload && codeHookTarget === 'product-grid' ? (
177
- <ProductGridSetup nodeId={props.nodeId} params={codeHookParams} />
178
- ) : codeHookPayload && codeHookTarget === 'featured-article' ? (
166
+ {codeHookPayload && codeHookTarget === 'featured-article' ? (
179
167
  <FeaturedArticleSetup
180
168
  nodeId={props.nodeId}
181
169
  params={codeHookParams}
@@ -94,13 +94,14 @@ const AddPanePanel = ({
94
94
  />
95
95
  ) : mode === PaneAddMode.REUSE && !isContextPane ? (
96
96
  <AddPaneReUsePanel nodeId={nodeId} first={first} setMode={setMode} />
97
- ) : mode === PaneAddMode.CODEHOOK && !isContextPane ? (
97
+ ) : mode === PaneAddMode.CODEHOOK ? (
98
98
  <AddPaneCodeHookPanel
99
99
  nodeId={nodeId}
100
100
  first={first}
101
101
  setMode={setMode}
102
102
  isStoryFragment={isStoryFragment}
103
103
  isContextPane={isContextPane}
104
+ isSandboxMode={isSandboxMode}
104
105
  />
105
106
  ) : mode === PaneAddMode.PASTE ? (
106
107
  <AddPanePanel_paste
@@ -139,7 +140,7 @@ const AddPanePanel = ({
139
140
  )}
140
141
  </>
141
142
  )}
142
- {!isTemplate && !isContextPane && (
143
+ {!isTemplate && (
143
144
  <button
144
145
  onClick={() => setMode(PaneAddMode.CODEHOOK)}
145
146
  className="rounded bg-white px-2 py-1 text-sm text-cyan-700 shadow-sm transition-colors hover:bg-cyan-700 hover:text-white"
@@ -5,7 +5,11 @@ import { Combobox } from '@ark-ui/react';
5
5
  import { createListCollection } from '@ark-ui/react/collection';
6
6
  import ChevronUpDownIcon from '@heroicons/react/20/solid/ChevronUpDownIcon';
7
7
  import CheckIcon from '@heroicons/react/20/solid/CheckIcon';
8
- import { codehookMapStore, fullContentMapStore } from '@/stores/storykeep';
8
+ import {
9
+ brandConfigStore,
10
+ codehookMapStore,
11
+ fullContentMapStore,
12
+ } from '@/stores/storykeep';
9
13
  import { getCtx } from '@/stores/nodes';
10
14
  import { findUniqueSlug } from '@/utils/helpers';
11
15
  import { PaneAddMode, type TemplatePane } from '@/types/compositorTypes';
@@ -16,6 +20,7 @@ interface AddPaneCodeHookPanelProps {
16
20
  setMode: (mode: PaneAddMode) => void;
17
21
  isStoryFragment?: boolean;
18
22
  isContextPane?: boolean;
23
+ isSandboxMode?: boolean;
19
24
  }
20
25
 
21
26
  const AddPaneCodeHookPanel = ({
@@ -24,10 +29,13 @@ const AddPaneCodeHookPanel = ({
24
29
  setMode,
25
30
  isStoryFragment = false,
26
31
  isContextPane = false,
32
+ isSandboxMode = false,
27
33
  }: AddPaneCodeHookPanelProps) => {
28
34
  const [selected, setSelected] = useState<string | null>(null);
29
35
  const [query, setQuery] = useState('');
30
36
  const $contentMap = useStore(fullContentMapStore);
37
+ const brandConfig = useStore(brandConfigStore);
38
+ const hasShopify = brandConfig?.HAS_SHOPIFY === true;
31
39
 
32
40
  const existingSlugs = $contentMap
33
41
  .filter((item) => ['Pane', 'StoryFragment'].includes(item.type))
@@ -39,9 +47,22 @@ const AddPaneCodeHookPanel = ({
39
47
 
40
48
  const availableCodeHooks = codehookMapStore.get();
41
49
 
42
- // Filter hooks based on search query
50
+ const isHookVisibleInPicker = (hookName: string) => {
51
+ if (
52
+ (hookName === 'shopify-product-grid' ||
53
+ hookName === 'shopify-service-list') &&
54
+ !hasShopify
55
+ ) {
56
+ return false;
57
+ }
58
+ if (hookName === 'get-crafting' && !isSandboxMode) {
59
+ return false;
60
+ }
61
+ return true;
62
+ };
63
+
64
+ // Filter hooks based on search query and tenant/sandbox gates
43
65
  const filteredHooks = useMemo(() => {
44
- // Start with available hooks
45
66
  const hooks =
46
67
  query === ''
47
68
  ? [...availableCodeHooks]
@@ -49,9 +70,8 @@ const AddPaneCodeHookPanel = ({
49
70
  hook.toLowerCase().includes(query.toLowerCase())
50
71
  );
51
72
 
52
- // Create a new array with unavailable hooks removed (don't just filter - we want to show them as disabled)
53
- return hooks;
54
- }, [availableCodeHooks, query]);
73
+ return hooks.filter(isHookVisibleInPicker);
74
+ }, [availableCodeHooks, query, hasShopify, isSandboxMode]);
55
75
 
56
76
  // Create collection for Ark UI Combobox
57
77
  const collection = useMemo(() => {
@@ -64,21 +84,22 @@ const AddPaneCodeHookPanel = ({
64
84
 
65
85
  const isHookAvailable = (hookName: string) => {
66
86
  if (
67
- (hookName === 'featured-content' ||
68
- hookName === 'list-content' ||
69
- hookName === 'featured-article') &&
87
+ (hookName === 'list-content' || hookName === 'featured-article') &&
70
88
  !hasStoryFragments
71
89
  ) {
72
- return hasStoryFragments;
90
+ return false;
91
+ }
92
+ if (
93
+ hookName === 'bunny-video' &&
94
+ import.meta.env.PUBLIC_ENABLE_BUNNY !== 'true'
95
+ ) {
96
+ return false;
73
97
  }
74
98
  return true;
75
99
  };
76
100
 
77
101
  const getDisplayName = (hookName: string) => {
78
- if (
79
- (hookName === 'featured-content' || hookName === 'list-content') &&
80
- !hasStoryFragments
81
- ) {
102
+ if (hookName === 'list-content' && !hasStoryFragments) {
82
103
  return `${hookName} (not yet available; no pages found)`;
83
104
  }
84
105
  return hookName;
@@ -260,7 +260,7 @@ const ConfigPanePanel = ({
260
260
  )}
261
261
  </>
262
262
  )}
263
- {isCodeHook && !isContextPane && (
263
+ {isCodeHook && (
264
264
  <button
265
265
  onClick={handleCodeHookConfig}
266
266
  className={buttonClass}
@@ -13,7 +13,7 @@ import {
13
13
  type MenuNode,
14
14
  } from '@/types/compositorTypes';
15
15
  import MenuForm from '@/components/storykeep/controls/content/MenuForm';
16
- import { fullContentMapStore, getFullContentMap } from '@/stores/analytics';
16
+ import { getFullContentMap, setTenantFullContentMap } from '@/stores/analytics';
17
17
  import type { FullContentMapItem } from '@/types/tractstack';
18
18
 
19
19
  interface StoryFragmentMenuPanelProps {
@@ -53,7 +53,7 @@ const StoryFragmentMenuPanel = ({
53
53
  if (!contentMap) {
54
54
  const currentContentMap = await api.getContentMapWithTimestamp();
55
55
  if (currentContentMap.success && currentContentMap.data) {
56
- fullContentMapStore.set(tenantId, currentContentMap.data);
56
+ setTenantFullContentMap(tenantId, currentContentMap.data);
57
57
  setContentMap(currentContentMap.data.data);
58
58
  }
59
59
  }
@@ -1,7 +1,11 @@
1
1
  import { useState, useCallback, useMemo, Component } from 'react';
2
2
  import type { ReactNode } from 'react';
3
3
  import { useStore } from '@nanostores/react';
4
- import { epinetCustomFilters } from '@/stores/analytics';
4
+ import {
5
+ epinetCustomFilters,
6
+ getEpinetCustomFilters,
7
+ setEpinetCustomFilters,
8
+ } from '@/stores/analytics';
5
9
  import { classNames } from '@/utils/helpers';
6
10
  import ArrowDownTrayIcon from '@heroicons/react/24/outline/ArrowDownTrayIcon';
7
11
  import DashboardActivity from './Dashboard_Activity';
@@ -167,7 +171,7 @@ export default function StoryKeepDashboard_Analytics({
167
171
 
168
172
  const handleBeliefFilterChange = (beliefSlug: string, value: string) => {
169
173
  const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
170
- const currentFilters = epinetCustomFilters.get();
174
+ const currentFilters = getEpinetCustomFilters();
171
175
  let newFilters = [...(currentFilters.appliedFilters || [])];
172
176
 
173
177
  if (value === 'All') {
@@ -183,7 +187,7 @@ export default function StoryKeepDashboard_Analytics({
183
187
  }
184
188
  }
185
189
 
186
- epinetCustomFilters.set(tenantId, {
190
+ setEpinetCustomFilters(tenantId, {
187
191
  ...currentFilters,
188
192
  appliedFilters: newFilters,
189
193
  });
@@ -220,7 +224,7 @@ export default function StoryKeepDashboard_Analytics({
220
224
  nowUTC.getTime() - hoursBack * 60 * 60 * 1000
221
225
  );
222
226
 
223
- epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
227
+ setEpinetCustomFilters(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
224
228
  ...$epinetCustomFilters,
225
229
  enabled: true,
226
230
  startTimeUTC: startTimeUTC.toISOString(),
@@ -1,7 +1,10 @@
1
1
  import { useState, useEffect, useRef } from 'react';
2
2
  import { Switch } from '@ark-ui/react';
3
3
  import { useStore } from '@nanostores/react';
4
- import { epinetCustomFilters } from '@/stores/analytics';
4
+ import {
5
+ epinetCustomFilters,
6
+ setEpinetCustomFilters,
7
+ } from '@/stores/analytics';
5
8
  import { classNames } from '@/utils/helpers';
6
9
  import type { FullContentMapItem } from '@/types/tractstack';
7
10
 
@@ -104,7 +107,7 @@ const ContentBrowser = ({
104
107
  const setStandardDuration = (hours: number) => {
105
108
  const nowUTC = new Date();
106
109
  const startTimeUTC = new Date(nowUTC.getTime() - hours * 60 * 60 * 1000);
107
- epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
110
+ setEpinetCustomFilters(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
108
111
  ...$epinetCustomFilters,
109
112
  enabled: true,
110
113
  startTimeUTC: startTimeUTC.toISOString(),
@@ -1,6 +1,10 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { useStore } from '@nanostores/react';
3
- import { epinetCustomFilters } from '@/stores/analytics';
3
+ import {
4
+ epinetCustomFilters,
5
+ getEpinetCustomFilters,
6
+ setEpinetCustomFilters,
7
+ } from '@/stores/analytics';
4
8
  import { TractStackAPI } from '@/utils/api';
5
9
 
6
10
  const VERBOSE = false;
@@ -252,7 +256,7 @@ class AnalyticsService {
252
256
  this.setCachedResponse(cacheKey, analyticsData);
253
257
  onUpdate(analyticsData);
254
258
 
255
- epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
259
+ setEpinetCustomFilters(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
256
260
  ...filters,
257
261
  availableFilters: data.availableFilters || [],
258
262
  });
@@ -298,9 +302,9 @@ class AnalyticsService {
298
302
  if (VERBOSE) console.log('🏁 Initializing analytics filters');
299
303
  const nowUTC = new Date();
300
304
  const oneWeekAgoUTC = new Date(nowUTC.getTime() - 7 * 24 * 60 * 60 * 1000);
301
- const current = epinetCustomFilters.get();
305
+ const current = getEpinetCustomFilters();
302
306
  if (!current.enabled) {
303
- epinetCustomFilters.set(tenantId, {
307
+ setEpinetCustomFilters(tenantId, {
304
308
  enabled: true,
305
309
  visitorType: 'all',
306
310
  selectedUserId: null,
@@ -139,7 +139,7 @@ export default function Wizard({
139
139
  const buildWizardData = async () => {
140
140
  try {
141
141
  const homePage = activeContentMap.find(
142
- (item) => item.slug === homeSlug
142
+ (item: FullContentMapItem) => item.slug === homeSlug
143
143
  );
144
144
 
145
145
  let homeData = null;
@@ -173,7 +173,9 @@ export default function Wizard({
173
173
  hasPanes: !!homePage?.panes?.length,
174
174
  hasSeo: !!homePage?.description,
175
175
  hasMenu: !!homeData?.menuId,
176
- hasAnyMenu: activeContentMap.some((item) => item.type === 'Menu'),
176
+ hasAnyMenu: activeContentMap.some(
177
+ (item: FullContentMapItem) => item.type === 'Menu'
178
+ ),
177
179
  };
178
180
 
179
181
  setWizardData(data);
@@ -0,0 +1,156 @@
1
+ import { getCachedFullContentMap, getFullContentMap } from '@/stores/analytics';
2
+ import { getCodeHookResources } from '@/lib/resources';
3
+ import type { ResourceNode } from '@/types/compositorTypes';
4
+
5
+ export interface CodeHookBladeContext {
6
+ tenantId: string;
7
+ paneId: string;
8
+ optionsStr: string;
9
+ options?: { params: { options: string } };
10
+ }
11
+
12
+ export interface CodeHookResourceFilters {
13
+ categories: string[];
14
+ slugs: string[];
15
+ }
16
+
17
+ export function parseCodeHookBladeContext(
18
+ searchParams: URLSearchParams
19
+ ): CodeHookBladeContext {
20
+ const tenantId = searchParams.get('tenantId') || 'default';
21
+ const paneId = searchParams.get('paneId') || '';
22
+ const optionsStr = searchParams.get('options') || '';
23
+ const options = optionsStr ? { params: { options: optionsStr } } : undefined;
24
+ return { tenantId, paneId, optionsStr, options };
25
+ }
26
+
27
+ export async function resolveCodeHookFullContentMap(
28
+ tenantId: string
29
+ ): Promise<any[]> {
30
+ let fullContentMap = getCachedFullContentMap(tenantId);
31
+ if (!fullContentMap.length) {
32
+ fullContentMap = await getFullContentMap(tenantId);
33
+ }
34
+ return fullContentMap;
35
+ }
36
+
37
+ export function parseCodeHookResourceFilters(
38
+ optionsStr: string,
39
+ logLabel?: string
40
+ ): CodeHookResourceFilters {
41
+ const categories: string[] = [];
42
+ const slugs: string[] = [];
43
+ if (!optionsStr) {
44
+ return { categories, slugs };
45
+ }
46
+ try {
47
+ const parsed = JSON.parse(optionsStr);
48
+ if (typeof parsed.category === 'string' && parsed.category) {
49
+ categories.push(...parsed.category.split('|'));
50
+ }
51
+ if (typeof parsed.slugs === 'string' && parsed.slugs) {
52
+ slugs.push(...parsed.slugs.split(','));
53
+ }
54
+ if (typeof parsed.slug === 'string' && parsed.slug) {
55
+ slugs.push(parsed.slug);
56
+ }
57
+ } catch (e) {
58
+ console.error(`Invalid options for ${logLabel ?? 'codehook resources'}`, e);
59
+ }
60
+ return { categories, slugs };
61
+ }
62
+
63
+ export async function resolveCodeHookResources(
64
+ tenantId: string,
65
+ optionsStr: string,
66
+ logLabel?: string
67
+ ): Promise<ResourceNode[]> {
68
+ const { categories, slugs } = parseCodeHookResourceFilters(
69
+ optionsStr,
70
+ logLabel
71
+ );
72
+ return getCodeHookResources(tenantId, categories, slugs);
73
+ }
74
+
75
+ export function buildCodeHookBladePath(
76
+ hookId: string,
77
+ ctx: Pick<CodeHookBladeContext, 'paneId' | 'tenantId' | 'optionsStr'>
78
+ ): string {
79
+ const params = new URLSearchParams({
80
+ paneId: ctx.paneId,
81
+ tenantId: ctx.tenantId,
82
+ });
83
+ if (ctx.optionsStr) {
84
+ params.set('options', ctx.optionsStr);
85
+ }
86
+ return `/codehooks/${hookId}?${params.toString()}`;
87
+ }
88
+
89
+ export function resolveSSRFetchOrigin(pageUrl: URL, siteUrl?: string): string {
90
+ if (siteUrl) {
91
+ try {
92
+ return new URL(siteUrl).origin;
93
+ } catch {
94
+ // fall through to page origin
95
+ }
96
+ }
97
+ return pageUrl.origin;
98
+ }
99
+
100
+ export async function fetchCodeHookBladeHtml(
101
+ bladePath: string,
102
+ request: Request,
103
+ origin: string
104
+ ): Promise<string> {
105
+ try {
106
+ const url = new URL(bladePath, origin);
107
+ const res = await fetch(url, {
108
+ headers: {
109
+ cookie: request.headers.get('cookie') ?? '',
110
+ accept: 'text/html',
111
+ },
112
+ });
113
+ if (!res.ok) {
114
+ console.error(
115
+ `Failed to SSR-fetch codehook blade ${bladePath}. Status: ${res.status}`
116
+ );
117
+ return '';
118
+ }
119
+ return res.text();
120
+ } catch (error) {
121
+ console.error(`Error SSR-fetching codehook blade ${bladePath}:`, error);
122
+ return '';
123
+ }
124
+ }
125
+
126
+ export async function fetchCodeHookBladesForPanes(options: {
127
+ paneIds: string[];
128
+ codeHookTargets: Record<string, string>;
129
+ tenantId: string;
130
+ request: Request;
131
+ origin: string;
132
+ }): Promise<Record<string, string>> {
133
+ const { paneIds, codeHookTargets, tenantId, request, origin } = options;
134
+ const result: Record<string, string> = {};
135
+
136
+ await Promise.all(
137
+ paneIds
138
+ .filter((paneId) => codeHookTargets[paneId])
139
+ .map(async (paneId) => {
140
+ const hookId = codeHookTargets[paneId];
141
+ const optionsStr = codeHookTargets[`${paneId}-${hookId}`] || '';
142
+ const bladePath = buildCodeHookBladePath(hookId, {
143
+ paneId,
144
+ tenantId,
145
+ optionsStr,
146
+ });
147
+ result[paneId] = await fetchCodeHookBladeHtml(
148
+ bladePath,
149
+ request,
150
+ origin
151
+ );
152
+ })
153
+ );
154
+
155
+ return result;
156
+ }
@@ -1,6 +1,47 @@
1
1
  import { headerResourcesStore, HEADER_RESOURCES_TTL } from '@/stores/resources';
2
2
  import type { ResourceNode } from '@/types/compositorTypes';
3
3
 
4
+ // Stateless resource fetch for codehook blades. POSTs categories/slugs to the
5
+ // nodes/resources endpoint, unwraps the { resources, count } envelope, and
6
+ // returns the FLAT ResourceNode[] (the API shape) unchanged - no grouping.
7
+ // Components that need a keyed view reshape internally. No cache.
8
+ export async function getCodeHookResources(
9
+ tenantId: string,
10
+ categories: string[] = [],
11
+ slugs: string[] = []
12
+ ): Promise<ResourceNode[]> {
13
+ if (categories.length === 0 && slugs.length === 0) {
14
+ return [];
15
+ }
16
+
17
+ const goBackend =
18
+ import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
19
+
20
+ try {
21
+ const response = await fetch(`${goBackend}/api/v1/nodes/resources`, {
22
+ method: 'POST',
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ 'X-Tenant-ID': tenantId,
26
+ },
27
+ body: JSON.stringify({ categories, slugs }),
28
+ });
29
+
30
+ if (!response.ok) {
31
+ console.error(
32
+ `Failed to fetch codehook resources. Status: ${response.status}`
33
+ );
34
+ return [];
35
+ }
36
+
37
+ const payload = await response.json();
38
+ return (payload.resources as ResourceNode[]) || [];
39
+ } catch (error) {
40
+ console.error('Error fetching codehook resources:', error);
41
+ return [];
42
+ }
43
+ }
44
+
4
45
  export async function getHeaderResources(
5
46
  tenantId: string,
6
47
  categories: string[],
@@ -1,5 +1,5 @@
1
1
  import { handleFailedResponse } from '@/utils/backend';
2
- import type { ImpressionNode, ResourceNode } from '@/types/compositorTypes';
2
+ import type { ImpressionNode } from '@/types/compositorTypes';
3
3
 
4
4
  export interface StoryData {
5
5
  id: string;
@@ -8,7 +8,6 @@ export interface StoryData {
8
8
  paneIds: string[];
9
9
  codeHookTargets: Record<string, string>;
10
10
  codeHookVisibility: Record<string, boolean | string[]>;
11
- resourcesPayload: Record<string, ResourceNode[]>;
12
11
  impressions: ImpressionNode[];
13
12
  fragments: Record<string, string>;
14
13
  menu: any;
@@ -6,7 +6,7 @@ import { getFullContentMap } from '@/stores/analytics';
6
6
  import { getBrandConfig } from '@/utils/api/brandConfig';
7
7
  import { joinUrlPaths } from '@/utils/helpers';
8
8
  import { handleFailedResponse } from '@/utils/backend';
9
- import { components as codeHookComponents } from '@/custom/CodeHook.astro';
9
+ import { availableCodeHookIds } from '@/custom/codehooks';
10
10
  import StoryKeepHeader from '@/components/edit/Header';
11
11
  import StoryKeepToolBar from '@/components/edit/ToolBar';
12
12
  import StoryKeepToolMode from '@/components/edit/ToolMode';
@@ -188,7 +188,7 @@ for (const [key, value] of Astro.url.searchParams) {
188
188
  fullContentMap={fullContentMap}
189
189
  fullCanonicalURL={fullCanonicalURL}
190
190
  urlParams={urlParams}
191
- availableCodeHooks={Object.keys(codeHookComponents)}
191
+ availableCodeHooks={availableCodeHookIds}
192
192
  client:only="react"
193
193
  />
194
194
  </div>
@@ -213,7 +213,7 @@ for (const [key, value] of Astro.url.searchParams) {
213
213
  }
214
214
  <div class="pointer-events-auto max-h-full">
215
215
  <SettingsPanel
216
- availableCodeHooks={Object.keys(codeHookComponents)}
216
+ availableCodeHooks={availableCodeHookIds}
217
217
  client:only="react"
218
218
  />
219
219
  </div>