astro-tractstack 2.3.3 → 2.3.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/bin/create-tractstack.js +5 -2
- package/dist/index.js +32 -4
- package/package.json +1 -1
- package/templates/custom/customHelpers.ts +45 -0
- package/templates/custom/shopify/Cart.tsx +197 -105
- package/templates/custom/shopify/CartIcon.tsx +8 -8
- package/templates/custom/shopify/CheckoutModal.tsx +145 -68
- package/templates/custom/shopify/ShopifyCartManager.tsx +67 -22
- package/templates/custom/shopify/ShopifyCheckout.tsx +2 -26
- package/templates/custom/shopify/ShopifyProductGrid.tsx +8 -0
- package/templates/custom/shopify/ShopifyServiceList.tsx +25 -37
- package/templates/custom/shopify/shopifyCustomHelper.ts +10 -0
- package/templates/custom/shopify/shopifyHelpers.ts +298 -0
- package/templates/src/components/Header.astro +2 -2
- package/templates/src/components/codehooks/SearchWidget.tsx +1 -1
- package/templates/src/components/compositor/Node.tsx +39 -9
- package/templates/src/components/compositor/nodes/CreativePane.tsx +175 -88
- package/templates/src/components/edit/pane/AddPanePanel.tsx +2 -2
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -5
- package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -15
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
- package/templates/src/components/form/advanced/APIConfigSection.tsx +35 -8
- package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
- package/templates/src/components/search/SearchResults.tsx +1 -1
- package/templates/src/components/search/SearchWrapper.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +59 -23
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +257 -18
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +162 -67
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +5 -2
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +479 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +7 -3
- package/templates/src/layouts/Layout.astro +26 -0
- package/templates/src/pages/api/auth/logout.ts +35 -2
- package/templates/src/pages/api/sales/list.ts +66 -0
- package/templates/src/pages/api/sales/metrics.ts +60 -0
- package/templates/src/pages/context/[...contextSlug].astro +50 -31
- package/templates/src/pages/storykeep/advanced.astro +4 -1
- package/templates/src/stores/nodes.ts +8 -0
- package/templates/src/types/tractstack.ts +57 -0
- package/templates/src/utils/api/advancedConfig.ts +2 -1
- package/templates/src/utils/api/advancedHelpers.ts +4 -0
- package/templates/src/utils/api/brandConfig.ts +2 -0
- package/templates/src/utils/api/brandHelpers.ts +6 -0
- package/templates/src/utils/api/salesHelpers.ts +21 -0
- package/utils/inject-files.ts +32 -4
- package/templates/src/utils/customHelpers.ts +0 -89
- /package/templates/{src/utils/booking → custom/shopify}/appointmentMode.ts +0 -0
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import ResourceForm from './controls/content/ResourceForm';
|
|
12
12
|
import { saveBrandConfigWithStateUpdate } from '@/utils/api/brandConfig';
|
|
13
13
|
import {
|
|
14
|
+
createResource,
|
|
14
15
|
deleteResource,
|
|
15
16
|
getResource,
|
|
16
17
|
getResourcesByCategory,
|
|
@@ -27,6 +28,7 @@ import ShopifyDashboard_Schedule from './shopify/ShopifyDashboard_Schedule';
|
|
|
27
28
|
import ShopifyDashboard_Search from './shopify/ShopifyDashboard_Search';
|
|
28
29
|
import ShopifyDashboard_Bookings from './shopify/ShopifyDashboard_Bookings';
|
|
29
30
|
import ShopifyDashboard_Emails from './shopify/ShopifyDashboard_Emails';
|
|
31
|
+
import ShopifyDashboard_Sales from './shopify/ShopifyDashboard_Sales';
|
|
30
32
|
|
|
31
33
|
interface DashboardShopifyProps {
|
|
32
34
|
brandConfig: BrandConfig;
|
|
@@ -34,6 +36,12 @@ interface DashboardShopifyProps {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
type MachineState = 'INIT' | 'CONFIG' | 'UPDATE' | 'READY';
|
|
39
|
+
type ImportCategory = 'product' | 'service' | 'shared';
|
|
40
|
+
|
|
41
|
+
export interface ShopifyLinkedStatus {
|
|
42
|
+
canonicalProduct: ResourceNode | null;
|
|
43
|
+
linkedServices: ResourceNode[];
|
|
44
|
+
}
|
|
37
45
|
|
|
38
46
|
export default function StoryKeepDashboard_Shopify({
|
|
39
47
|
brandConfig,
|
|
@@ -60,7 +68,7 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
60
68
|
|
|
61
69
|
const [showSmartCartWarning, setShowSmartCartWarning] = useState(false);
|
|
62
70
|
const [pendingImport, setPendingImport] = useState<{
|
|
63
|
-
category:
|
|
71
|
+
category: ImportCategory;
|
|
64
72
|
product: ShopifyProduct;
|
|
65
73
|
} | null>(null);
|
|
66
74
|
|
|
@@ -72,10 +80,56 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
72
80
|
const [wantService, setWantService] = useState(true);
|
|
73
81
|
const [isSaving, setIsSaving] = useState(false);
|
|
74
82
|
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const hasOpenModal =
|
|
85
|
+
Boolean(selectedProduct) ||
|
|
86
|
+
(showTypeSelector && Boolean(targetProduct)) ||
|
|
87
|
+
(showSmartCartWarning && Boolean(pendingImport)) ||
|
|
88
|
+
(showResourceModal && Boolean(draftResource));
|
|
89
|
+
if (!hasOpenModal) return;
|
|
90
|
+
|
|
91
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
92
|
+
if (event.key !== 'Escape') return;
|
|
93
|
+
|
|
94
|
+
if (showResourceModal && draftResource) {
|
|
95
|
+
setShowResourceModal(false);
|
|
96
|
+
setIsCreateMode(true);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (showSmartCartWarning && pendingImport) {
|
|
100
|
+
setShowSmartCartWarning(false);
|
|
101
|
+
setPendingImport(null);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (showTypeSelector && targetProduct) {
|
|
105
|
+
setShowTypeSelector(false);
|
|
106
|
+
setTargetProduct(null);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (selectedProduct) {
|
|
110
|
+
setSelectedProduct(null);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
115
|
+
return () => {
|
|
116
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
117
|
+
};
|
|
118
|
+
}, [
|
|
119
|
+
selectedProduct,
|
|
120
|
+
showTypeSelector,
|
|
121
|
+
targetProduct,
|
|
122
|
+
showSmartCartWarning,
|
|
123
|
+
pendingImport,
|
|
124
|
+
showResourceModal,
|
|
125
|
+
draftResource,
|
|
126
|
+
]);
|
|
127
|
+
|
|
75
128
|
// Tab definitions
|
|
76
129
|
const tabs = [
|
|
77
130
|
{ id: 'dashboards', name: 'Dashboard' },
|
|
78
131
|
{ id: 'bookings', name: 'Bookings' },
|
|
132
|
+
{ id: 'sales', name: 'Sales' },
|
|
79
133
|
{ id: 'products', name: 'Products' },
|
|
80
134
|
{ id: 'services', name: 'Services' },
|
|
81
135
|
{ id: 'schedule', name: 'Schedule' },
|
|
@@ -97,12 +151,27 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
97
151
|
}
|
|
98
152
|
}, [brandConfig]);
|
|
99
153
|
|
|
100
|
-
const
|
|
101
|
-
const map = new Map<string,
|
|
102
|
-
resources.forEach((
|
|
103
|
-
|
|
104
|
-
|
|
154
|
+
const linkedStatusMap = useMemo(() => {
|
|
155
|
+
const map = new Map<string, ShopifyLinkedStatus>();
|
|
156
|
+
resources.forEach((resource) => {
|
|
157
|
+
const gid =
|
|
158
|
+
typeof resource.optionsPayload?.gid === 'string'
|
|
159
|
+
? resource.optionsPayload.gid
|
|
160
|
+
: '';
|
|
161
|
+
if (!gid) return;
|
|
162
|
+
|
|
163
|
+
const current = map.get(gid) || {
|
|
164
|
+
canonicalProduct: null,
|
|
165
|
+
linkedServices: [],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (resource.categorySlug === 'product' && !current.canonicalProduct) {
|
|
169
|
+
current.canonicalProduct = resource;
|
|
170
|
+
}
|
|
171
|
+
if (resource.categorySlug === 'service') {
|
|
172
|
+
current.linkedServices.push(resource);
|
|
105
173
|
}
|
|
174
|
+
map.set(gid, current);
|
|
106
175
|
});
|
|
107
176
|
return map;
|
|
108
177
|
}, [resources]);
|
|
@@ -139,6 +208,8 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
139
208
|
if (hasProductSchema && hasServiceSchema) {
|
|
140
209
|
setTargetProduct(product);
|
|
141
210
|
setShowTypeSelector(true);
|
|
211
|
+
} else if (hasProductSchema) {
|
|
212
|
+
executePreFlightCheck('product', product);
|
|
142
213
|
} else if (hasServiceSchema) {
|
|
143
214
|
executePreFlightCheck('service', product);
|
|
144
215
|
} else {
|
|
@@ -171,10 +242,17 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
171
242
|
}
|
|
172
243
|
};
|
|
173
244
|
|
|
174
|
-
const
|
|
245
|
+
const handleMarkShared = (product: ShopifyProduct) => {
|
|
246
|
+
void startCreateFlow('shared', product);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const executePreFlightCheck = (
|
|
250
|
+
category: ImportCategory,
|
|
251
|
+
product: ShopifyProduct
|
|
252
|
+
) => {
|
|
175
253
|
const hasMode = product.options.some((opt) => opt.name === 'Mode');
|
|
176
|
-
if (category === 'service' || hasMode) {
|
|
177
|
-
startCreateFlow(category, product);
|
|
254
|
+
if (category === 'service' || category === 'shared' || hasMode) {
|
|
255
|
+
void startCreateFlow(category, product);
|
|
178
256
|
} else {
|
|
179
257
|
setPendingImport({ category, product });
|
|
180
258
|
setShowSmartCartWarning(true);
|
|
@@ -182,12 +260,112 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
182
260
|
}
|
|
183
261
|
};
|
|
184
262
|
|
|
185
|
-
const startCreateFlow = (
|
|
186
|
-
|
|
263
|
+
const startCreateFlow = async (
|
|
264
|
+
category: ImportCategory,
|
|
265
|
+
product: ShopifyProduct
|
|
266
|
+
) => {
|
|
267
|
+
const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
|
|
268
|
+
const productSchema = internalBrandConfig?.knownResources['product'] || {};
|
|
269
|
+
const existingCanonical = resources.find(
|
|
270
|
+
(r) =>
|
|
271
|
+
r.categorySlug === 'product' && r.optionsPayload?.gid === product.id
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (category === 'shared') {
|
|
275
|
+
if (existingCanonical) {
|
|
276
|
+
setDraftResource({
|
|
277
|
+
...existingCanonical,
|
|
278
|
+
optionsPayload: {
|
|
279
|
+
...(existingCanonical.optionsPayload || {}),
|
|
280
|
+
gid: product.id,
|
|
281
|
+
shopifyData:
|
|
282
|
+
existingCanonical.optionsPayload?.shopifyData ||
|
|
283
|
+
JSON.stringify(product),
|
|
284
|
+
sharedServiceFee: true,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
setIsCreateMode(false);
|
|
288
|
+
} else {
|
|
289
|
+
const mergedOptions: Record<string, any> = {
|
|
290
|
+
gid: product.id,
|
|
291
|
+
shopifyData: JSON.stringify(product),
|
|
292
|
+
sharedServiceFee: true,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
Object.entries(productSchema).forEach(([key, def]) => {
|
|
296
|
+
if (mergedOptions[key] !== undefined) return;
|
|
297
|
+
if (def.type === 'number') {
|
|
298
|
+
mergedOptions[key] = def.defaultValue ?? def.minNumber ?? 0;
|
|
299
|
+
} else if (def.type === 'boolean') {
|
|
300
|
+
mergedOptions[key] = def.defaultValue ?? false;
|
|
301
|
+
} else if (def.type === 'string') {
|
|
302
|
+
mergedOptions[key] = def.defaultValue ?? '';
|
|
303
|
+
} else if (def.type === 'multi') {
|
|
304
|
+
mergedOptions[key] = def.defaultValue ?? [];
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
setDraftResource({
|
|
309
|
+
title: product.title,
|
|
310
|
+
oneliner: product.description || '',
|
|
311
|
+
slug: `product-${product.handle}`.toLowerCase(),
|
|
312
|
+
categorySlug: 'product',
|
|
313
|
+
optionsPayload: mergedOptions,
|
|
314
|
+
});
|
|
315
|
+
setIsCreateMode(true);
|
|
316
|
+
}
|
|
317
|
+
setShowTypeSelector(false);
|
|
318
|
+
setShowResourceModal(true);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (category === 'service') {
|
|
323
|
+
if (!existingCanonical) {
|
|
324
|
+
const productOptions: Record<string, any> = {
|
|
325
|
+
gid: product.id,
|
|
326
|
+
shopifyData: JSON.stringify(product),
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
Object.entries(productSchema).forEach(([key, def]) => {
|
|
330
|
+
if (productOptions[key] !== undefined) return;
|
|
331
|
+
if (def.type === 'number') {
|
|
332
|
+
productOptions[key] = def.defaultValue ?? def.minNumber ?? 0;
|
|
333
|
+
} else if (def.type === 'boolean') {
|
|
334
|
+
productOptions[key] = def.defaultValue ?? false;
|
|
335
|
+
} else if (def.type === 'string') {
|
|
336
|
+
productOptions[key] = def.defaultValue ?? '';
|
|
337
|
+
} else if (def.type === 'multi') {
|
|
338
|
+
productOptions[key] = def.defaultValue ?? [];
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
await createResource(tenantId, {
|
|
344
|
+
title: product.title,
|
|
345
|
+
oneliner: product.description || '',
|
|
346
|
+
slug: `product-${product.handle}`.toLowerCase(),
|
|
347
|
+
categorySlug: 'product',
|
|
348
|
+
optionsPayload: productOptions,
|
|
349
|
+
} as any);
|
|
350
|
+
await refreshResources();
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error('Failed to ensure canonical product', error);
|
|
353
|
+
alert('Failed to create canonical product for this service.');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const schema =
|
|
360
|
+
category === 'product'
|
|
361
|
+
? internalBrandConfig?.knownResources['product'] || {}
|
|
362
|
+
: internalBrandConfig?.knownResources['service'] || {};
|
|
187
363
|
const mergedOptions: Record<string, any> = {
|
|
188
364
|
gid: product.id,
|
|
189
|
-
shopifyData: JSON.stringify(product),
|
|
190
365
|
};
|
|
366
|
+
if (category === 'product') {
|
|
367
|
+
mergedOptions.shopifyData = JSON.stringify(product);
|
|
368
|
+
}
|
|
191
369
|
|
|
192
370
|
Object.entries(schema).forEach(([key, def]) => {
|
|
193
371
|
if (mergedOptions[key] === undefined) {
|
|
@@ -226,11 +404,53 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
226
404
|
}
|
|
227
405
|
|
|
228
406
|
try {
|
|
407
|
+
const target = resources.find((r) => r.id === resourceId);
|
|
408
|
+
const targetGid =
|
|
409
|
+
typeof target?.optionsPayload?.gid === 'string'
|
|
410
|
+
? target.optionsPayload.gid
|
|
411
|
+
: '';
|
|
412
|
+
|
|
413
|
+
if (target?.categorySlug === 'product' && targetGid) {
|
|
414
|
+
const linkedServices = resources.filter(
|
|
415
|
+
(r) =>
|
|
416
|
+
r.categorySlug === 'service' && r.optionsPayload?.gid === targetGid
|
|
417
|
+
);
|
|
418
|
+
if (linkedServices.length > 0) {
|
|
419
|
+
alert(
|
|
420
|
+
'Cannot delete a product linked to services. Delete linked services first.'
|
|
421
|
+
);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
229
426
|
await deleteResource(
|
|
230
427
|
window.TRACTSTACK_CONFIG?.tenantId || 'default',
|
|
231
428
|
resourceId
|
|
232
429
|
);
|
|
233
|
-
|
|
430
|
+
|
|
431
|
+
if (target?.categorySlug === 'service' && targetGid) {
|
|
432
|
+
const remainingLinked = resources.filter(
|
|
433
|
+
(r) =>
|
|
434
|
+
r.id !== resourceId &&
|
|
435
|
+
r.categorySlug === 'service' &&
|
|
436
|
+
r.optionsPayload?.gid === targetGid
|
|
437
|
+
);
|
|
438
|
+
if (remainingLinked.length === 0) {
|
|
439
|
+
const canonicalProduct = resources.find(
|
|
440
|
+
(r) =>
|
|
441
|
+
r.categorySlug === 'product' &&
|
|
442
|
+
r.optionsPayload?.gid === targetGid
|
|
443
|
+
);
|
|
444
|
+
if (canonicalProduct) {
|
|
445
|
+
await deleteResource(
|
|
446
|
+
window.TRACTSTACK_CONFIG?.tenantId || 'default',
|
|
447
|
+
canonicalProduct.id
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
await refreshResources();
|
|
234
454
|
} catch (error) {
|
|
235
455
|
console.error('Unlink failed', error);
|
|
236
456
|
alert('Failed to delete resource');
|
|
@@ -258,6 +478,11 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
258
478
|
gid: { type: 'string', optional: false },
|
|
259
479
|
allowMultiple: { type: 'boolean', optional: false },
|
|
260
480
|
group: { type: 'string', optional: true },
|
|
481
|
+
sharedServiceFee: {
|
|
482
|
+
type: 'boolean',
|
|
483
|
+
optional: false,
|
|
484
|
+
defaultValue: false,
|
|
485
|
+
},
|
|
261
486
|
shopifyData: { type: 'string', optional: false },
|
|
262
487
|
shopifyImage: { type: 'string', optional: true, defaultValue: '{}' },
|
|
263
488
|
...(wantService
|
|
@@ -276,8 +501,6 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
276
501
|
updatedKnownResources['service'] = {
|
|
277
502
|
gid: { type: 'string', optional: true },
|
|
278
503
|
group: { type: 'string', optional: true },
|
|
279
|
-
shopifyData: { type: 'string', optional: true },
|
|
280
|
-
shopifyImage: { type: 'string', optional: true, defaultValue: '{}' },
|
|
281
504
|
allowRemote: {
|
|
282
505
|
type: 'boolean',
|
|
283
506
|
optional: false,
|
|
@@ -479,6 +702,10 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
479
702
|
<ShopifyDashboard_Bookings existingResources={resources} />
|
|
480
703
|
)}
|
|
481
704
|
|
|
705
|
+
{activeTab === 'sales' && (
|
|
706
|
+
<ShopifyDashboard_Sales existingResources={resources} />
|
|
707
|
+
)}
|
|
708
|
+
|
|
482
709
|
{activeTab === 'products' && (
|
|
483
710
|
<ShopifyDashboard_Products
|
|
484
711
|
resources={resources}
|
|
@@ -503,9 +730,10 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
503
730
|
|
|
504
731
|
{activeTab === 'search' && (
|
|
505
732
|
<ShopifyDashboard_Search
|
|
506
|
-
|
|
733
|
+
linkedStatusMap={linkedStatusMap}
|
|
507
734
|
onSelectProduct={setSelectedProduct}
|
|
508
735
|
onLink={handleLink}
|
|
736
|
+
onMarkShared={handleMarkShared}
|
|
509
737
|
onUnlink={handleUnlink}
|
|
510
738
|
onEdit={handleEditFromCatalog}
|
|
511
739
|
/>
|
|
@@ -574,7 +802,7 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
574
802
|
Import as...
|
|
575
803
|
</h3>
|
|
576
804
|
<p className="mt-2 text-sm text-gray-500">
|
|
577
|
-
|
|
805
|
+
Choose import type for "{targetProduct.title}".
|
|
578
806
|
</p>
|
|
579
807
|
<div className="mt-6 flex flex-col gap-3">
|
|
580
808
|
<button
|
|
@@ -585,6 +813,14 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
585
813
|
>
|
|
586
814
|
Product
|
|
587
815
|
</button>
|
|
816
|
+
<button
|
|
817
|
+
onClick={() =>
|
|
818
|
+
executePreFlightCheck('shared', targetProduct)
|
|
819
|
+
}
|
|
820
|
+
className="flex w-full items-center justify-center rounded-md border border-cyan-300 bg-cyan-50 px-4 py-2 text-sm font-bold text-cyan-700 shadow-sm hover:bg-cyan-100"
|
|
821
|
+
>
|
|
822
|
+
Shared Canonical Product
|
|
823
|
+
</button>
|
|
588
824
|
<button
|
|
589
825
|
onClick={() =>
|
|
590
826
|
executePreFlightCheck('service', targetProduct)
|
|
@@ -618,7 +854,7 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
618
854
|
<button
|
|
619
855
|
onClick={() => {
|
|
620
856
|
setShowSmartCartWarning(false);
|
|
621
|
-
startCreateFlow(
|
|
857
|
+
void startCreateFlow(
|
|
622
858
|
pendingImport.category,
|
|
623
859
|
pendingImport.product
|
|
624
860
|
);
|
|
@@ -661,6 +897,9 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
661
897
|
internalBrandConfig?.scheduling?.remoteOnly
|
|
662
898
|
)}
|
|
663
899
|
isCreate={isCreateMode}
|
|
900
|
+
onOpenLinkedProduct={(resourceId) => {
|
|
901
|
+
void handleEditResource(resourceId);
|
|
902
|
+
}}
|
|
664
903
|
onClose={(saved) => {
|
|
665
904
|
setShowResourceModal(false);
|
|
666
905
|
setIsCreateMode(true);
|
|
@@ -15,23 +15,26 @@ import {
|
|
|
15
15
|
type ShopifyProduct,
|
|
16
16
|
} from '@/stores/shopify';
|
|
17
17
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
18
|
+
import type { ShopifyLinkedStatus } from '@/components/storykeep/Dashboard_Shopify';
|
|
18
19
|
|
|
19
20
|
interface ProductTableProps {
|
|
20
21
|
products: ShopifyProduct[];
|
|
21
|
-
|
|
22
|
+
linkedStatusMap: Map<string, ShopifyLinkedStatus>;
|
|
22
23
|
onRefresh: () => void;
|
|
23
24
|
isRefreshing: boolean;
|
|
24
25
|
onSelectProduct: (product: ShopifyProduct) => void;
|
|
25
26
|
onLink: (product: ShopifyProduct) => void;
|
|
27
|
+
onMarkShared: (product: ShopifyProduct) => void;
|
|
26
28
|
onUnlink: (resourceId: string) => void;
|
|
27
29
|
onEdit: (product: ShopifyProduct, resource: ResourceNode) => void;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export default function ProductTable({
|
|
31
33
|
products,
|
|
32
|
-
|
|
34
|
+
linkedStatusMap,
|
|
33
35
|
onSelectProduct,
|
|
34
36
|
onLink,
|
|
37
|
+
onMarkShared,
|
|
35
38
|
onUnlink,
|
|
36
39
|
onEdit,
|
|
37
40
|
}: ProductTableProps) {
|
|
@@ -203,8 +206,10 @@ export default function ProductTable({
|
|
|
203
206
|
</tr>
|
|
204
207
|
) : (
|
|
205
208
|
products.map((product) => {
|
|
206
|
-
const
|
|
207
|
-
const
|
|
209
|
+
const linkedStatus = linkedStatusMap.get(product.id);
|
|
210
|
+
const canonicalProduct = linkedStatus?.canonicalProduct || null;
|
|
211
|
+
const linkedServiceCount =
|
|
212
|
+
linkedStatus?.linkedServices.length || 0;
|
|
208
213
|
|
|
209
214
|
return (
|
|
210
215
|
<tr key={product.id} className="hover:bg-gray-50">
|
|
@@ -215,22 +220,24 @@ export default function ProductTable({
|
|
|
215
220
|
>
|
|
216
221
|
{product.title}
|
|
217
222
|
</div>
|
|
218
|
-
{
|
|
219
|
-
<
|
|
220
|
-
className=
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
className="max-w-xs truncate"
|
|
229
|
-
title={linkedResource.title}
|
|
230
|
-
>
|
|
231
|
-
{linkedResource.title}
|
|
223
|
+
{canonicalProduct && (
|
|
224
|
+
<div className="mt-1 flex flex-wrap items-center gap-1">
|
|
225
|
+
<span className="inline-flex items-center rounded-full bg-cyan-50 px-2 py-0.5 text-xs font-bold text-cyan-700 ring-1 ring-inset ring-cyan-600/20">
|
|
226
|
+
Synced canonical:{' '}
|
|
227
|
+
<span
|
|
228
|
+
className="ml-1 max-w-xs truncate"
|
|
229
|
+
title={canonicalProduct.title}
|
|
230
|
+
>
|
|
231
|
+
{canonicalProduct.title}
|
|
232
|
+
</span>
|
|
232
233
|
</span>
|
|
233
|
-
|
|
234
|
+
{linkedServiceCount > 0 && (
|
|
235
|
+
<span className="inline-flex items-center rounded-full bg-indigo-50 px-2 py-0.5 text-xs font-bold text-indigo-700 ring-1 ring-inset ring-indigo-600/20">
|
|
236
|
+
{linkedServiceCount} service
|
|
237
|
+
{linkedServiceCount === 1 ? '' : 's'}
|
|
238
|
+
</span>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
234
241
|
)}
|
|
235
242
|
</td>
|
|
236
243
|
<td
|
|
@@ -241,12 +248,12 @@ export default function ProductTable({
|
|
|
241
248
|
</td>
|
|
242
249
|
<td className="whitespace-nowrap px-6 py-4 text-right text-sm font-bold">
|
|
243
250
|
<div className="flex items-center justify-end space-x-2">
|
|
244
|
-
{
|
|
251
|
+
{canonicalProduct ? (
|
|
245
252
|
<>
|
|
246
253
|
<button
|
|
247
|
-
onClick={() => onEdit(product,
|
|
254
|
+
onClick={() => onEdit(product, canonicalProduct)}
|
|
248
255
|
className="text-cyan-600 hover:text-cyan-900"
|
|
249
|
-
title="Edit
|
|
256
|
+
title="Edit Canonical Product"
|
|
250
257
|
>
|
|
251
258
|
<PencilIcon
|
|
252
259
|
className="h-5 w-5"
|
|
@@ -257,9 +264,16 @@ export default function ProductTable({
|
|
|
257
264
|
</span>
|
|
258
265
|
</button>
|
|
259
266
|
<button
|
|
260
|
-
onClick={() =>
|
|
267
|
+
onClick={() => onMarkShared(product)}
|
|
268
|
+
className="rounded border border-cyan-200 bg-cyan-50 px-2 py-0.5 text-xs font-bold text-cyan-700 hover:bg-cyan-100"
|
|
269
|
+
title="Tag Canonical Product as Shared"
|
|
270
|
+
>
|
|
271
|
+
Shared
|
|
272
|
+
</button>
|
|
273
|
+
<button
|
|
274
|
+
onClick={() => onUnlink(canonicalProduct.id)}
|
|
261
275
|
className="text-red-600 hover:text-red-900"
|
|
262
|
-
title="Unlink
|
|
276
|
+
title="Unlink Canonical Product"
|
|
263
277
|
>
|
|
264
278
|
<TrashIcon
|
|
265
279
|
className="h-5 w-5"
|