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.
Files changed (49) hide show
  1. package/bin/create-tractstack.js +5 -2
  2. package/dist/index.js +32 -4
  3. package/package.json +1 -1
  4. package/templates/custom/customHelpers.ts +45 -0
  5. package/templates/custom/shopify/Cart.tsx +197 -105
  6. package/templates/custom/shopify/CartIcon.tsx +8 -8
  7. package/templates/custom/shopify/CheckoutModal.tsx +145 -68
  8. package/templates/custom/shopify/ShopifyCartManager.tsx +67 -22
  9. package/templates/custom/shopify/ShopifyCheckout.tsx +2 -26
  10. package/templates/custom/shopify/ShopifyProductGrid.tsx +8 -0
  11. package/templates/custom/shopify/ShopifyServiceList.tsx +25 -37
  12. package/templates/custom/shopify/shopifyCustomHelper.ts +10 -0
  13. package/templates/custom/shopify/shopifyHelpers.ts +298 -0
  14. package/templates/src/components/Header.astro +2 -2
  15. package/templates/src/components/codehooks/SearchWidget.tsx +1 -1
  16. package/templates/src/components/compositor/Node.tsx +39 -9
  17. package/templates/src/components/compositor/nodes/CreativePane.tsx +175 -88
  18. package/templates/src/components/edit/pane/AddPanePanel.tsx +2 -2
  19. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -5
  20. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -15
  21. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  22. package/templates/src/components/form/advanced/APIConfigSection.tsx +35 -8
  23. package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
  24. package/templates/src/components/search/SearchResults.tsx +1 -1
  25. package/templates/src/components/search/SearchWrapper.tsx +1 -1
  26. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +59 -23
  27. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +257 -18
  28. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
  29. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +162 -67
  30. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
  31. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +5 -2
  32. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +479 -0
  33. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +7 -3
  34. package/templates/src/layouts/Layout.astro +26 -0
  35. package/templates/src/pages/api/auth/logout.ts +35 -2
  36. package/templates/src/pages/api/sales/list.ts +66 -0
  37. package/templates/src/pages/api/sales/metrics.ts +60 -0
  38. package/templates/src/pages/context/[...contextSlug].astro +50 -31
  39. package/templates/src/pages/storykeep/advanced.astro +4 -1
  40. package/templates/src/stores/nodes.ts +8 -0
  41. package/templates/src/types/tractstack.ts +57 -0
  42. package/templates/src/utils/api/advancedConfig.ts +2 -1
  43. package/templates/src/utils/api/advancedHelpers.ts +4 -0
  44. package/templates/src/utils/api/brandConfig.ts +2 -0
  45. package/templates/src/utils/api/brandHelpers.ts +6 -0
  46. package/templates/src/utils/api/salesHelpers.ts +21 -0
  47. package/utils/inject-files.ts +32 -4
  48. package/templates/src/utils/customHelpers.ts +0 -89
  49. /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: string;
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 linkedResourceMap = useMemo(() => {
101
- const map = new Map<string, ResourceNode>();
102
- resources.forEach((r) => {
103
- if (r.optionsPayload?.gid) {
104
- map.set(r.optionsPayload.gid, r);
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 executePreFlightCheck = (category: string, product: ShopifyProduct) => {
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 = (category: string, product: ShopifyProduct) => {
186
- const schema = internalBrandConfig?.knownResources[category] || {};
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
- setResources((prev) => prev.filter((r) => r.id !== resourceId));
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
- linkedResourceMap={linkedResourceMap}
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
- Should "{targetProduct.title}" be a Product or Service?
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
- linkedResourceMap: Map<string, ResourceNode>;
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
- linkedResourceMap,
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 linkedResource = linkedResourceMap.get(product.id);
207
- const isLinked = !!linkedResource;
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
- {isLinked && (
219
- <span
220
- className={`mt-1 inline-flex items-center rounded-full px-2 py-0.5 text-xs font-bold ring-1 ring-inset ${
221
- linkedResource.categorySlug === 'service'
222
- ? 'bg-indigo-50 text-indigo-700 ring-indigo-600/20'
223
- : 'bg-cyan-50 text-cyan-700 ring-cyan-600/20'
224
- }`}
225
- >
226
- Synced:{' '}
227
- <span
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
- </span>
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
- {isLinked ? (
251
+ {canonicalProduct ? (
245
252
  <>
246
253
  <button
247
- onClick={() => onEdit(product, linkedResource)}
254
+ onClick={() => onEdit(product, canonicalProduct)}
248
255
  className="text-cyan-600 hover:text-cyan-900"
249
- title="Edit Resource"
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={() => onUnlink(linkedResource.id)}
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 Resource"
276
+ title="Unlink Canonical Product"
263
277
  >
264
278
  <TrashIcon
265
279
  className="h-5 w-5"