astro-tractstack 2.3.2 → 2.3.4
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 +7 -4
- package/dist/index.js +51 -8
- package/package.json +1 -1
- package/templates/custom/shopify/Cart.tsx +279 -118
- package/templates/custom/shopify/CartIcon.tsx +8 -8
- package/templates/custom/shopify/CheckoutModal.tsx +328 -65
- package/templates/custom/shopify/ShopifyCartManager.tsx +117 -60
- 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/cart.astro +7 -1
- package/templates/src/components/Header.astro +4 -2
- 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 +249 -4
- package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
- package/templates/src/components/form/shopify/SchedulingSection.tsx +44 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +66 -21
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +266 -18
- package/templates/src/components/storykeep/controls/content/ManageContent.tsx +1 -0
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +240 -65
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +91 -10
- 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/constants.ts +2 -0
- package/templates/src/layouts/Layout.astro +26 -0
- package/templates/src/pages/api/auth/logout.ts +35 -2
- package/templates/src/pages/api/google/oauth/callback.ts +50 -0
- package/templates/src/pages/api/google/oauth/disconnect.ts +32 -0
- package/templates/src/pages/api/google/oauth/start.ts +32 -0
- package/templates/src/pages/api/google/oauth/status.ts +32 -0
- 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/privacy.astro +84 -0
- package/templates/src/pages/storykeep/advanced.astro +4 -1
- package/templates/src/pages/terms.astro +47 -0
- package/templates/src/stores/nodes.ts +8 -0
- package/templates/src/stores/shopify.ts +5 -0
- package/templates/src/types/tractstack.ts +87 -0
- package/templates/src/utils/api/advancedConfig.ts +2 -1
- package/templates/src/utils/api/advancedHelpers.ts +20 -0
- package/templates/src/utils/api/bookingHelpers.ts +3 -1
- package/templates/src/utils/api/brandConfig.ts +2 -0
- package/templates/src/utils/api/brandHelpers.ts +14 -1
- package/templates/src/utils/api/salesHelpers.ts +21 -0
- package/templates/src/utils/booking/appointmentMode.ts +135 -0
- package/templates/src/utils/customHelpers.ts +287 -2
- package/utils/inject-files.ts +47 -4
- package/templates/src/components/codehooks/SandboxAuthWrapper.tsx +0 -101
- package/templates/src/utils/actions/actionButton.ts +0 -103
- package/templates/src/utils/actions/preParse_Clicked.ts +0 -87
|
@@ -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,12 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
276
501
|
updatedKnownResources['service'] = {
|
|
277
502
|
gid: { type: 'string', optional: true },
|
|
278
503
|
group: { type: 'string', optional: true },
|
|
279
|
-
|
|
280
|
-
|
|
504
|
+
allowRemote: {
|
|
505
|
+
type: 'boolean',
|
|
506
|
+
optional: false,
|
|
507
|
+
defaultValue: false,
|
|
508
|
+
},
|
|
509
|
+
remoteOnly: { type: 'boolean', optional: false, defaultValue: false },
|
|
281
510
|
bookingLengthMinutes: {
|
|
282
511
|
type: 'number',
|
|
283
512
|
optional: false,
|
|
@@ -473,6 +702,10 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
473
702
|
<ShopifyDashboard_Bookings existingResources={resources} />
|
|
474
703
|
)}
|
|
475
704
|
|
|
705
|
+
{activeTab === 'sales' && (
|
|
706
|
+
<ShopifyDashboard_Sales existingResources={resources} />
|
|
707
|
+
)}
|
|
708
|
+
|
|
476
709
|
{activeTab === 'products' && (
|
|
477
710
|
<ShopifyDashboard_Products
|
|
478
711
|
resources={resources}
|
|
@@ -497,9 +730,10 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
497
730
|
|
|
498
731
|
{activeTab === 'search' && (
|
|
499
732
|
<ShopifyDashboard_Search
|
|
500
|
-
|
|
733
|
+
linkedStatusMap={linkedStatusMap}
|
|
501
734
|
onSelectProduct={setSelectedProduct}
|
|
502
735
|
onLink={handleLink}
|
|
736
|
+
onMarkShared={handleMarkShared}
|
|
503
737
|
onUnlink={handleUnlink}
|
|
504
738
|
onEdit={handleEditFromCatalog}
|
|
505
739
|
/>
|
|
@@ -568,7 +802,7 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
568
802
|
Import as...
|
|
569
803
|
</h3>
|
|
570
804
|
<p className="mt-2 text-sm text-gray-500">
|
|
571
|
-
|
|
805
|
+
Choose import type for "{targetProduct.title}".
|
|
572
806
|
</p>
|
|
573
807
|
<div className="mt-6 flex flex-col gap-3">
|
|
574
808
|
<button
|
|
@@ -579,6 +813,14 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
579
813
|
>
|
|
580
814
|
Product
|
|
581
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>
|
|
582
824
|
<button
|
|
583
825
|
onClick={() =>
|
|
584
826
|
executePreFlightCheck('service', targetProduct)
|
|
@@ -612,7 +854,7 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
612
854
|
<button
|
|
613
855
|
onClick={() => {
|
|
614
856
|
setShowSmartCartWarning(false);
|
|
615
|
-
startCreateFlow(
|
|
857
|
+
void startCreateFlow(
|
|
616
858
|
pendingImport.category,
|
|
617
859
|
pendingImport.product
|
|
618
860
|
);
|
|
@@ -651,7 +893,13 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
651
893
|
draftResource.categorySlug || ''
|
|
652
894
|
] || {}
|
|
653
895
|
}
|
|
896
|
+
tenantRemoteOnly={Boolean(
|
|
897
|
+
internalBrandConfig?.scheduling?.remoteOnly
|
|
898
|
+
)}
|
|
654
899
|
isCreate={isCreateMode}
|
|
900
|
+
onOpenLinkedProduct={(resourceId) => {
|
|
901
|
+
void handleEditResource(resourceId);
|
|
902
|
+
}}
|
|
655
903
|
onClose={(saved) => {
|
|
656
904
|
setShowResourceModal(false);
|
|
657
905
|
setIsCreateMode(true);
|
|
@@ -278,6 +278,7 @@ const ManageContent = ({
|
|
|
278
278
|
fullContentMap={currentContentMap}
|
|
279
279
|
categorySlug={activeResourceForm.category}
|
|
280
280
|
categorySchema={knownResources[activeResourceForm.category] || {}}
|
|
281
|
+
tenantRemoteOnly={Boolean(brandConfig?.SCHEDULING?.remoteOnly)}
|
|
281
282
|
onClose={async (saved: boolean) => {
|
|
282
283
|
setActiveResourceForm(null);
|
|
283
284
|
if (saved) await refreshData();
|
|
@@ -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"
|