@deenruv/admin-dashboard 1.0.16 → 1.0.17-dev.15

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 (83) hide show
  1. package/dist/DeenruvAdminPanel.js +35 -46
  2. package/dist/access/access-context.js +10 -0
  3. package/dist/access/built-in-routes.js +300 -0
  4. package/dist/access/index.js +6 -0
  5. package/dist/access/navigation.js +33 -0
  6. package/dist/access/permission-access.js +9 -0
  7. package/dist/access/permission-routes.js +3 -0
  8. package/dist/access/types.js +1 -0
  9. package/dist/components/GlobalSearch.js +37 -62
  10. package/dist/components/Menu/ActiveAdmins.js +1 -1
  11. package/dist/components/Menu/ChannelSwitcher.js +1 -1
  12. package/dist/components/Menu/LanguagesDropdown.js +1 -1
  13. package/dist/components/Menu/Navigation.js +68 -229
  14. package/dist/components/Menu/NavigationFooter.js +7 -4
  15. package/dist/components/Menu/Notifications.js +1 -1
  16. package/dist/components/Menu/index.js +50 -16
  17. package/dist/components/storefrontAction/StorefrontAction.js +9 -0
  18. package/dist/components/storefrontAction/StorefrontAction.logic.js +27 -0
  19. package/dist/components/storefrontAction/index.js +2 -0
  20. package/dist/index.css +54 -44
  21. package/dist/locales/en/admins.json +62 -0
  22. package/dist/locales/en/collections.json +21 -0
  23. package/dist/locales/en/common.json +2 -0
  24. package/dist/locales/en/orders.json +8 -0
  25. package/dist/locales/en/products.json +29 -2
  26. package/dist/locales/en/promotions.json +4 -0
  27. package/dist/locales/pl/admins.json +62 -0
  28. package/dist/locales/pl/collections.json +21 -0
  29. package/dist/locales/pl/common.json +2 -0
  30. package/dist/locales/pl/orders.json +8 -0
  31. package/dist/locales/pl/products.json +29 -2
  32. package/dist/locales/pl/promotions.json +4 -0
  33. package/dist/pages/Root.js +31 -15
  34. package/dist/pages/admins/Detail.js +1 -1
  35. package/dist/pages/admins/List.js +8 -3
  36. package/dist/pages/admins/Provision.js +173 -0
  37. package/dist/pages/admins/Provision.logic.js +53 -0
  38. package/dist/pages/admins/index.js +1 -0
  39. package/dist/pages/assets/List.js +3 -3
  40. package/dist/pages/channels/Detail.js +1 -1
  41. package/dist/pages/collections/Detail.js +24 -6
  42. package/dist/pages/collections/List.js +87 -20
  43. package/dist/pages/collections/_components/CollectionDetailView.js +15 -2
  44. package/dist/pages/collections/_components/FiltersCard.js +51 -11
  45. package/dist/pages/collections/_components/collectionStorefrontAction/CollectionStorefrontAction.js +41 -0
  46. package/dist/pages/collections/_components/collectionStorefrontAction/CollectionStorefrontAction.logic.js +44 -0
  47. package/dist/pages/collections/_components/collectionStorefrontAction/index.js +2 -0
  48. package/dist/pages/countries/Detail.js +1 -1
  49. package/dist/pages/customers/_components/PersonalDataCard.js +2 -1
  50. package/dist/pages/extensions/Extensions.js +3 -7
  51. package/dist/pages/facets/Detail.js +1 -1
  52. package/dist/pages/orders/Detail.js +38 -5
  53. package/dist/pages/orders/List.js +1 -16
  54. package/dist/pages/orders/_components/CancelAndRefundDialog.js +1 -2
  55. package/dist/pages/orders/_components/OrderLineCustomFields.js +1 -1
  56. package/dist/pages/orders/_components/ProductsTable.js +1 -2
  57. package/dist/pages/orders/_components/RefundPaymentCard.js +1 -2
  58. package/dist/pages/payment-methods/Detail.js +1 -1
  59. package/dist/pages/product-variants/List.js +3 -3
  60. package/dist/pages/products/Detail.js +62 -5
  61. package/dist/pages/products/_components/AssetsCard.js +15 -2
  62. package/dist/pages/products/_components/OptionsCard.js +7 -5
  63. package/dist/pages/products/_components/PriceCard.js +21 -8
  64. package/dist/pages/products/_components/ProductDetailView.js +7 -3
  65. package/dist/pages/products/_components/Variant.js +53 -26
  66. package/dist/pages/products/_components/VariantsTab.js +1 -1
  67. package/dist/pages/products/_components/productStorefrontAction/ProductStorefrontAction.js +42 -0
  68. package/dist/pages/products/_components/productStorefrontAction/ProductStorefrontAction.logic.js +39 -0
  69. package/dist/pages/products/_components/productStorefrontAction/index.js +2 -0
  70. package/dist/pages/promotions/Detail.js +34 -3
  71. package/dist/pages/promotions/_components/EnabledCard.js +2 -2
  72. package/dist/pages/promotions/_components/PromotionDetailSidebar.js +2 -1
  73. package/dist/pages/roles/Detail.js +1 -1
  74. package/dist/pages/roles/_components/PermissionsTable.js +0 -1
  75. package/dist/pages/sellers/Detail.js +1 -1
  76. package/dist/pages/shipping-methods/Detail.js +10 -6
  77. package/dist/pages/shipping-methods/_components/CalculatorCard.js +27 -11
  78. package/dist/pages/shipping-methods/_components/CheckerCard.js +28 -12
  79. package/dist/pages/stock-locations/Detail.js +1 -1
  80. package/dist/pages/tax-rates/Detail.js +1 -1
  81. package/dist/pages/zones/Detail.js +1 -1
  82. package/dist/version.js +1 -1
  83. package/package.json +68 -68
@@ -1,9 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { OrderLineCustomFields } from "./OrderLineCustomFields.js";
3
- import { Checkbox, ImageWithPreview, Input, Label, priceFormatter, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, useOrder, } from '@deenruv/react-ui-devkit';
3
+ import { Checkbox, ImageWithPreview, Input, Label, priceFormatter, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, useOrder, useTranslation, } from '@deenruv/react-ui-devkit';
4
4
  import { Tag } from 'lucide-react';
5
5
  import { useCallback } from 'react';
6
- import { useTranslation } from 'react-i18next';
7
6
  export const ProductsTable = ({ setRefundLines, refundLines }) => {
8
7
  const { t } = useTranslation('orders');
9
8
  const { mode, currentOrder } = useOrder();
@@ -1,6 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Checkbox, CustomCard, Input, Label, priceFormatter, Textarea, useOrder } from '@deenruv/react-ui-devkit';
3
- import { useTranslation } from 'react-i18next';
2
+ import { Checkbox, CustomCard, Input, Label, priceFormatter, Textarea, useOrder, useTranslation, } from '@deenruv/react-ui-devkit';
4
3
  export const RefundPaymentCard = ({ refundReason, setRefundReason, cancelShipping, setCancelShipping, refundAmount, setRefundAmount, }) => {
5
4
  const { t } = useTranslation('orders');
6
5
  const { modifiedOrder } = useOrder();
@@ -44,7 +44,7 @@ export const PaymentMethodsDetailPage = () => {
44
44
  if (!id) {
45
45
  throw new Error('Could not find the id.');
46
46
  }
47
- return remove({ input: { id } });
47
+ return remove({ id });
48
48
  }, [remove, id]);
49
49
  return (_jsx("div", { className: "relative flex flex-col gap-y-4", children: _jsx(DetailView, { id: id, locationId: "paymentMethods-detail-view", main: {
50
50
  name: 'paymentMethod',
@@ -24,10 +24,10 @@ const fetch = async ({ page, perPage, filter, filterOperator, sort }, additional
24
24
  const onRemove = async (items) => {
25
25
  try {
26
26
  const ids = items.map((item) => item.id);
27
- const { deleteProducts } = await apiClient('mutation')({
28
- deleteProducts: [{ ids }, { message: true, result: true }],
27
+ const { deleteProductVariants } = await apiClient('mutation')({
28
+ deleteProductVariants: [{ ids }, { message: true, result: true }],
29
29
  });
30
- return !!deleteProducts.length;
30
+ return !!deleteProductVariants.length;
31
31
  }
32
32
  catch (error) {
33
33
  return error;
@@ -4,7 +4,8 @@ import { VariantsTab } from "./_components/VariantsTab";
4
4
  import { OptionsTab } from "./_components/OptionsTab";
5
5
  import { ProductDetailView } from './_components/ProductDetailView';
6
6
  import { ProductDetailSidebar } from './_components/ProductDetailSidebar';
7
- import { useTranslation, createDeenruvForm, DetailView, useMutation } from '@deenruv/react-ui-devkit';
7
+ import { ProductStorefrontAction } from './_components/productStorefrontAction';
8
+ import { DEFAULT_CHANNEL_CODE, useTranslation, createDeenruvForm, DetailView, useMutation, useSettings, } from '@deenruv/react-ui-devkit';
8
9
  import { $, Permission, scalars, typedGql } from '@deenruv/admin-types';
9
10
  import { useMemo } from 'react';
10
11
  const EditProductMutation = typedGql('mutation', { scalars })({
@@ -13,6 +14,12 @@ const EditProductMutation = typedGql('mutation', { scalars })({
13
14
  const CreateProductMutation = typedGql('mutation', { scalars })({
14
15
  createProduct: [{ input: $('input', 'CreateProductInput!') }, { id: true }],
15
16
  });
17
+ const CreateProductVariantsMutation = typedGql('mutation', { scalars })({
18
+ createProductVariants: [{ input: $('input', '[CreateProductVariantInput!]!') }, { id: true }],
19
+ });
20
+ const AssignProductVariantsToChannelMutation = typedGql('mutation', { scalars })({
21
+ assignProductVariantsToChannel: [{ input: $('input', 'AssignProductVariantsToChannelInput!') }, { id: true }],
22
+ });
16
23
  const DeleteProductMutation = typedGql('mutation', { scalars })({
17
24
  deleteProduct: [{ id: $('id', 'ID!') }, { result: true }],
18
25
  });
@@ -21,7 +28,10 @@ export const ProductsDetailPage = () => {
21
28
  const { t } = useTranslation('products');
22
29
  const [update] = useMutation(EditProductMutation);
23
30
  const [create] = useMutation(CreateProductMutation);
31
+ const [createVariants] = useMutation(CreateProductVariantsMutation);
32
+ const [assignProductVariantsToChannel] = useMutation(AssignProductVariantsToChannelMutation);
24
33
  const [remove] = useMutation(DeleteProductMutation);
34
+ const selectedChannel = useSettings((p) => p.selectedChannel);
25
35
  const defaultTabs = useMemo(() => {
26
36
  const tabs = [];
27
37
  if (id) {
@@ -48,19 +58,64 @@ export const ProductsDetailPage = () => {
48
58
  return [t('validation.nameSlugRequired')];
49
59
  },
50
60
  },
61
+ ...{
62
+ initialVariantSku: {
63
+ validate: (v) => {
64
+ if (!id && (!v || typeof v !== 'string' || !v.trim())) {
65
+ return [t('validation.initialVariantSkuRequired')];
66
+ }
67
+ },
68
+ },
69
+ initialVariantPrice: {
70
+ initialValue: 0,
71
+ },
72
+ initialVariantName: {},
73
+ },
51
74
  },
52
- onSubmitted: (data) => {
75
+ onSubmitted: async (data) => {
53
76
  if (!data.translations)
54
77
  throw new Error('Name is required.');
78
+ const translations = data.translations;
55
79
  const input = {
56
- translations: data.translations,
80
+ translations,
57
81
  assetIds: data.assetIds,
58
82
  featuredAssetId: data.featuredAssetId,
59
83
  facetValueIds: data.facetValueIds,
60
84
  enabled: data.enabled,
61
85
  ...(data.customFields ? { customFields: data.customFields } : {}),
62
86
  };
63
- return id ? update({ input: { id, ...input } }) : create({ input: input });
87
+ if (id)
88
+ return update({ input: { id, ...input } });
89
+ const response = await create({ input });
90
+ const initialVariantName = typeof data.initialVariantName === 'string' ? data.initialVariantName.trim() : '';
91
+ const initialVariantSku = typeof data.initialVariantSku === 'string' ? data.initialVariantSku.trim() : '';
92
+ const initialVariantPrice = Number(data.initialVariantPrice ?? 0);
93
+ const variantsResponse = await createVariants({
94
+ input: [
95
+ {
96
+ productId: response.createProduct.id,
97
+ translations: translations.map((translation) => ({
98
+ languageCode: translation.languageCode,
99
+ name: initialVariantName || translation.name || initialVariantSku,
100
+ })),
101
+ sku: initialVariantSku,
102
+ price: Number.isFinite(initialVariantPrice) ? initialVariantPrice : 0,
103
+ },
104
+ ],
105
+ });
106
+ const createdVariantId = variantsResponse.createProductVariants?.[0]?.id;
107
+ if (createdVariantId &&
108
+ selectedChannel?.id &&
109
+ selectedChannel.code &&
110
+ selectedChannel.code !== DEFAULT_CHANNEL_CODE) {
111
+ await assignProductVariantsToChannel({
112
+ input: {
113
+ productVariantIds: [createdVariantId],
114
+ channelId: selectedChannel.id,
115
+ },
116
+ });
117
+ }
118
+ return response;
64
119
  },
65
120
  onDeleted: () => {
66
121
  if (id)
@@ -69,7 +124,9 @@ export const ProductsDetailPage = () => {
69
124
  throw new Error('No id');
70
125
  },
71
126
  }),
72
- }, defaultTabs: defaultTabs, permissions: {
127
+ }, defaultTabs: defaultTabs, topActions: {
128
+ inline: [_jsx(ProductStorefrontAction, {}, "product-storefront-action")],
129
+ }, permissions: {
73
130
  create: Permission.CreateProduct,
74
131
  edit: Permission.UpdateProduct,
75
132
  delete: Permission.DeleteProduct,
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { DropdownMenuItem, Label, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuTrigger, apiClient, cn, CustomCard, CardIcons, AssetsModalInput, useTranslation, createDialogFromComponent, } from '@deenruv/react-ui-devkit';
2
+ import { DropdownMenuItem, Label, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuTrigger, apiClient, cn, CustomCard, CardIcons, AssetsModalInput, AssetUploadButton, useTranslation, createDialogFromComponent, } from '@deenruv/react-ui-devkit';
3
3
  import { assetsSelector } from "../../../graphql/base";
4
4
  import { ImageOff } from 'lucide-react';
5
5
  import { useCallback, useEffect, useState } from 'react';
@@ -31,5 +31,18 @@ export const AssetsCard = ({ featuredAssetId, assetsIds, onAddAsset, onFeaturedA
31
31
  const newAssets = assetsIds?.filter((aId) => aId !== id);
32
32
  onAssetsChange(newAssets || []);
33
33
  }, [assetsIds, onAssetsChange]);
34
- return (_jsx(CustomCard, { title: t('assets'), color: "green", icon: _jsx(CardIcons.asset, {}), children: _jsx("div", { className: "flex flex-col gap-6", children: _jsxs("div", { className: "flex gap-6", children: [_jsxs("div", { children: [_jsx(Label, { children: t('details.featuredAsset') }), _jsxs("div", { className: "flex flex-col pt-3", children: [_jsx("div", { className: "mb-4 flex h-36 min-w-36 items-center justify-center border border-solid border-gray-300 p-2 shadow", children: featureAsset?.preview ? (_jsx("img", { src: featureAsset.preview, className: "h-32", alt: "Main image preview" })) : (_jsxs("div", { className: "flex size-full flex-col items-center justify-center gap-2 bg-gray-200 p-3", children: [_jsx(ImageOff, { size: 32 }), t('details.noFeaturedAsset')] })) }), _jsx(AssetsModalInput, { setValue: (a) => onAddAsset(a?.id) })] })] }), _jsxs("div", { children: [_jsx(Label, { children: t('details.otherAssets') }), _jsx("div", { className: "flex gap-3 pt-3", children: assets?.length ? (assets.map((asset) => (_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("div", { className: cn('flex h-20 min-w-20 cursor-pointer items-center justify-center border border-solid border-gray-300 p-2 shadow', asset.id === featuredAssetId && 'border-2 border-blue-500'), children: asset?.preview && _jsx("img", { src: asset.preview, className: "h-16" }) }) }), _jsx(DropdownMenuContent, { className: "w-56", side: "bottom", align: "end", children: _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuItem, { onClick: () => createDialogFromComponent(EditAssetDialog, asset, { className: 'max-w-4xl' }), children: t('details.editAsset') }), _jsx(DropdownMenuItem, { onClick: () => onFeaturedAssetChange(asset.id), children: t('details.setAsFeatured') }), _jsx(DropdownMenuItem, { onClick: () => handleRemoveAsset(asset.id), children: t('details.removeAsset') })] }) })] }, asset.id)))) : (_jsx("p", { children: t('details.noAssets') })) })] })] }) }) }));
34
+ const handleAssetAdded = useCallback((id) => {
35
+ if (!id)
36
+ return;
37
+ if (!assetsIds?.includes(id)) {
38
+ onAddAsset(id);
39
+ }
40
+ if (!featuredAssetId) {
41
+ onFeaturedAssetChange(id);
42
+ }
43
+ }, [assetsIds, featuredAssetId, onAddAsset, onFeaturedAssetChange]);
44
+ return (_jsx(CustomCard, { title: t('assets'), color: "green", icon: _jsx(CardIcons.asset, {}), children: _jsx("div", { className: "flex flex-col gap-6", children: _jsxs("div", { className: "flex gap-6", children: [_jsxs("div", { children: [_jsx(Label, { children: t('details.featuredAsset') }), _jsxs("div", { className: "flex flex-col pt-3", children: [_jsx("div", { className: "mb-4 flex h-36 min-w-36 items-center justify-center border border-solid border-gray-300 p-2 shadow", children: featureAsset?.preview ? (_jsx("img", { src: featureAsset.preview, className: "h-32", alt: "Main image preview" })) : (_jsxs("div", { className: "flex size-full flex-col items-center justify-center gap-2 bg-gray-200 p-3", children: [_jsx(ImageOff, { size: 32 }), t('details.noFeaturedAsset')] })) }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(AssetsModalInput, { setValue: (a) => handleAssetAdded(a?.id) }), _jsx(AssetUploadButton, { buttonProps: { size: 'sm' }, cb: (asset) => {
45
+ handleAssetAdded(asset.id);
46
+ return {};
47
+ }, children: t('details.uploadAsset') })] })] })] }), _jsxs("div", { children: [_jsx(Label, { children: t('details.otherAssets') }), _jsx("div", { className: "flex gap-3 pt-3", children: assets?.length ? (assets.map((asset) => (_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("div", { className: cn('flex h-20 min-w-20 cursor-pointer items-center justify-center border border-solid border-gray-300 p-2 shadow', asset.id === featuredAssetId && 'border-2 border-blue-500'), children: asset?.preview && _jsx("img", { src: asset.preview, className: "h-16" }) }) }), _jsx(DropdownMenuContent, { className: "w-56", side: "bottom", align: "end", children: _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuItem, { onClick: () => createDialogFromComponent(EditAssetDialog, asset, { className: 'max-w-4xl' }), children: t('details.editAsset') }), _jsx(DropdownMenuItem, { onClick: () => onFeaturedAssetChange(asset.id), children: t('details.setAsFeatured') }), _jsx(DropdownMenuItem, { onClick: () => handleRemoveAsset(asset.id), children: t('details.removeAsset') })] }) })] }, asset.id)))) : (_jsx("p", { children: t('details.noAssets') })) })] })] }) }) }));
35
48
  };
@@ -1,10 +1,12 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useState } from 'react';
3
- import { Table, TableBody, TableCell, TableRow, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, apiClient, CustomCard, CardIcons, useTranslation, } from '@deenruv/react-ui-devkit';
3
+ import { Table, TableBody, TableCell, TableRow, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, apiClient, CustomCard, CardIcons, useTranslation, useSettings, } from '@deenruv/react-ui-devkit';
4
4
  import { OptionGroupSelector } from "../../../graphql/products";
5
5
  import { toast } from 'sonner';
6
+ import { AddOptionGroupDialog } from "./AddOptionGroupDialog";
6
7
  export const OptionsCard = ({ optionGroups: options, productId, onChange, optionIds, createMode, }) => {
7
8
  const { t } = useTranslation('products');
9
+ const contentLng = useSettings((p) => p.translationsLanguage);
8
10
  const [optionGroups, setOptionGroups] = useState();
9
11
  const fetchOptionGroups = useCallback(async () => {
10
12
  if (productId) {
@@ -23,7 +25,7 @@ export const OptionsCard = ({ optionGroups: options, productId, onChange, option
23
25
  toast.error(t('toasts.fetchProductErrorToast'));
24
26
  }
25
27
  }
26
- }, [productId]);
28
+ }, [productId, t]);
27
29
  useEffect(() => {
28
30
  fetchOptionGroups();
29
31
  }, [fetchOptionGroups]);
@@ -32,7 +34,7 @@ export const OptionsCard = ({ optionGroups: options, productId, onChange, option
32
34
  newState[groupIdx] = optionId;
33
35
  onChange(newState);
34
36
  }, [optionIds, onChange]);
35
- return (_jsx(CustomCard, { title: t('options'), icon: _jsx(CardIcons.options, {}), color: "orange", children: !createMode ? (_jsx(Table, { children: _jsx(TableBody, { children: options?.map((o) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { className: "font-semibold capitalize", children: [o.group.name, ":"] }), _jsx(TableCell, { className: "capitalize", children: o.name })] }, o.name))) }) })) : (optionGroups?.map((group, i) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "w-1/3 font-semibold", children: [group.name, ":"] }), _jsx("div", { className: "w-2/3", children: _jsxs(Select, { value: optionIds?.[i] || '', onValueChange: (e) => {
36
- handleOptionChange(e, i);
37
- }, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: t('addVariantDialog.selectOption') }) }), _jsx(SelectContent, { children: group.options.map((o) => (_jsx(SelectItem, { value: o.id, className: "capitalize", children: o.name }, o.id))) })] }) })] }, group.name)))) }));
37
+ return (_jsx(CustomCard, { title: t('options'), icon: _jsx(CardIcons.options, {}), color: "orange", upperRight: createMode ? (_jsx(AddOptionGroupDialog, { currentTranslationLng: contentLng, onSuccess: fetchOptionGroups, productId: productId })) : undefined, children: !createMode ? (_jsx(Table, { children: _jsx(TableBody, { children: options?.map((o) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { className: "font-semibold capitalize", children: [o.group.name, ":"] }), _jsx(TableCell, { className: "capitalize", children: o.name })] }, o.name))) }) })) : optionGroups === undefined ? null : optionGroups.length ? (_jsx("div", { className: "flex flex-col gap-3", children: optionGroups.map((group, i) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "w-1/3 font-semibold", children: [group.name, ":"] }), _jsx("div", { className: "w-2/3", children: _jsxs(Select, { value: optionIds?.[i] || '', onValueChange: (e) => {
38
+ handleOptionChange(e, i);
39
+ }, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: t('addVariantDialog.selectOption') }) }), _jsx(SelectContent, { children: group.options.map((o) => (_jsx(SelectItem, { value: o.id, className: "capitalize", children: o.name }, o.id))) })] }) })] }, group.name))) })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: t('addVariantDialog.createOptionsHint') })) }));
38
40
  };
@@ -1,13 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, apiClient, CustomCard, CardIcons, useTranslation, } from '@deenruv/react-ui-devkit';
3
- import { useCallback, useEffect, useState } from 'react';
4
- export const PriceCard = ({ priceValue, onPriceChange, currencyCode, taxRateValue, onTaxRateChange, }) => {
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ export const PriceCard = ({ priceValue, onPriceChange, currencyCode, taxRateValue, onTaxRateChange, showDefaultPriceWhenEmpty = false, }) => {
5
5
  const { t } = useTranslation('products');
6
6
  const [taxCategories, setTaxCategories] = useState([]);
7
7
  const [currentTaxCategory, setCurrentTaxCategory] = useState();
8
+ const defaultPrice = useMemo(() => ({
9
+ currencyCode,
10
+ price: 0,
11
+ }), [currencyCode]);
12
+ const pricesToRender = useMemo(() => (priceValue?.length ? priceValue : showDefaultPriceWhenEmpty ? [defaultPrice] : []), [defaultPrice, priceValue, showDefaultPriceWhenEmpty]);
8
13
  useEffect(() => {
9
- fetchTaxRates();
10
- }, []);
14
+ if (showDefaultPriceWhenEmpty && !priceValue?.length) {
15
+ onPriceChange([defaultPrice]);
16
+ }
17
+ }, [defaultPrice, onPriceChange, priceValue?.length, showDefaultPriceWhenEmpty]);
11
18
  const fetchTaxRates = useCallback(async () => {
12
19
  const response = await apiClient('query')({
13
20
  taxCategories: [{}, { items: { id: true, name: true } }],
@@ -19,9 +26,15 @@ export const PriceCard = ({ priceValue, onPriceChange, currencyCode, taxRateValu
19
26
  }));
20
27
  setTaxCategories(categoriesWithRates);
21
28
  }, []);
29
+ useEffect(() => {
30
+ fetchTaxRates();
31
+ }, [fetchTaxRates]);
32
+ useEffect(() => {
33
+ setCurrentTaxCategory(taxCategories.find((category) => category.id === taxRateValue));
34
+ }, [taxCategories, taxRateValue]);
22
35
  const handlePriceChange = useCallback((currencyCode, value) => {
23
- const newPrices = priceValue?.map((p) => (p.currencyCode === currencyCode ? { ...p, price: value } : p));
24
- onPriceChange(newPrices || []);
25
- }, [priceValue, onPriceChange]);
26
- return (_jsx(CustomCard, { title: t('details.price'), color: "rose", icon: _jsx(CardIcons.calc, {}), children: _jsxs("div", { className: "flex flex-col gap-y-4", children: [priceValue?.map((price) => (_jsx("div", { className: "flex items-center gap-x-2", children: _jsx(Input, { type: "currency", placeholder: t('price'), value: price.price, onChange: (e) => handlePriceChange(price.currencyCode, +e.target.value), step: 0.01, startAdornment: price.currencyCode }) }, price.currencyCode))), _jsxs(Select, { value: taxRateValue, onValueChange: onTaxRateChange, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Tax rate" }) }), _jsx(SelectContent, { children: taxCategories.map((tR) => (_jsx(SelectItem, { value: tR.id.toString(), children: tR.name }, tR.id))) })] }), currentTaxCategory?.value !== undefined && `${t('details.taxRateDescription')} ${currentTaxCategory?.value}%`] }) }));
36
+ const newPrices = pricesToRender.map((p) => (p.currencyCode === currencyCode ? { ...p, price: value } : p));
37
+ onPriceChange(newPrices);
38
+ }, [onPriceChange, pricesToRender]);
39
+ return (_jsx(CustomCard, { title: t('details.price'), color: "rose", icon: _jsx(CardIcons.calc, {}), children: _jsxs("div", { className: "flex flex-col gap-y-4", children: [pricesToRender.map((price) => (_jsx("div", { className: "flex items-center gap-x-2", children: _jsx(Input, { type: "currency", placeholder: t('price'), value: price.price, onChange: (e) => handlePriceChange(price.currencyCode, +e.target.value), step: 0.01, startAdornment: price.currencyCode }) }, price.currencyCode))), _jsxs(Select, { value: taxRateValue, onValueChange: onTaxRateChange, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Tax rate" }) }), _jsx(SelectContent, { children: taxCategories.map((tR) => (_jsx(SelectItem, { value: tR.id.toString(), children: tR.name }, tR.id))) })] }), currentTaxCategory?.value !== undefined && `${t('details.taxRateDescription')} ${currentTaxCategory?.value}%`] }) }));
27
40
  };
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useDetailView, DetailViewMarker, useSettings, setInArrayBy, EntityCustomFields, normalizeString, } from '@deenruv/react-ui-devkit';
2
+ import { useDetailView, DetailViewMarker, useSettings, setInArrayBy, EntityCustomFields, normalizeString, CustomCard, CardIcons, Input, useTranslation, } from '@deenruv/react-ui-devkit';
3
3
  import { useCallback, useEffect, useMemo, useRef } from 'react';
4
4
  import { BasicFieldsCard } from './BasicFieldsCard';
5
5
  import { AssetsCard } from './AssetsCard';
@@ -13,9 +13,10 @@ export const PRODUCT_FORM_KEYS = [
13
13
  'customFields',
14
14
  ];
15
15
  export const ProductDetailView = () => {
16
+ const { t } = useTranslation('products');
16
17
  const contentLng = useSettings((p) => p.translationsLanguage);
17
18
  const selectedChannel = useSettings((p) => p.selectedChannel);
18
- const { entity, id, form, loading, fetchEntity } = useDetailView('products-detail-view', ...PRODUCT_FORM_KEYS);
19
+ const { entity, id, form, fetchEntity } = useDetailView('products-detail-view', ...PRODUCT_FORM_KEYS);
19
20
  const { base } = form;
20
21
  // --- Slug auto-generation ---
21
22
  // Track whether the user has manually edited the slug for the current language.
@@ -42,6 +43,7 @@ export const ProductDetailView = () => {
42
43
  })();
43
44
  }, [selectedChannel?.id, contentLng]);
44
45
  const translations = base.watch('translations') || [];
46
+ const initialVariantPrice = base.watch('initialVariantPrice') ?? 0;
45
47
  const currentTranslationValue = useMemo(() => {
46
48
  return translations.find((v) => v.languageCode === contentLng);
47
49
  }, [translations, contentLng]);
@@ -78,7 +80,9 @@ export const ProductDetailView = () => {
78
80
  }, [base, base.setField]);
79
81
  return (_jsx("div", { children: _jsxs("div", { className: "flex w-full flex-col gap-4", children: [_jsx(BasicFieldsCard, { currentTranslationValue: currentTranslationValue, onChange: setTranslationField, onSlugManualEdit: handleSlugManualEdit, errors: base.formState.errors?.translations?.message
80
82
  ? [base.formState.errors.translations.message]
81
- : undefined }), _jsx(DetailViewMarker, { position: 'products-detail-view' }), _jsx(EntityCustomFields, { id: id, entityName: "product", hideButton: true, onChange: (customFields, translations) => {
83
+ : undefined }), !id && (_jsx(CustomCard, { title: t('initialVariant.title'), icon: _jsx(CardIcons.tag, {}), color: "rose", children: _jsxs("div", { className: "flex flex-col gap-4 pt-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t('initialVariant.description') }), _jsxs("div", { className: "grid gap-4 md:grid-cols-3", children: [_jsx(Input, { label: t('initialVariant.sku'), value: base.watch('initialVariantSku') ?? '', onChange: (e) => base.setField('initialVariantSku', e.target.value), errors: base.formState.errors?.initialVariantSku?.message
84
+ ? [base.formState.errors.initialVariantSku.message]
85
+ : undefined, required: true }), _jsx(Input, { type: "currency", label: t('initialVariant.price'), value: initialVariantPrice, onChange: (e) => base.setField('initialVariantPrice', +e.target.value), startAdornment: selectedChannel?.currencyCode, step: 0.01 }), _jsx(Input, { label: t('initialVariant.name'), placeholder: t('initialVariant.namePlaceholder'), value: base.watch('initialVariantName') ?? '', onChange: (e) => base.setField('initialVariantName', e.target.value) })] })] }) })), _jsx(DetailViewMarker, { position: 'products-detail-view' }), _jsx(EntityCustomFields, { id: id, entityName: "product", hideButton: true, onChange: (customFields, translations) => {
82
86
  base.setField('customFields', customFields);
83
87
  if (translations)
84
88
  base.setField('translations', translations);
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Button, CardIcons, ConfirmationDialog, CustomCard, Input, apiClient, setInArrayBy, useDeenruvForm, z, EntityCustomFields, useTranslation, EntityChannelManager, } from '@deenruv/react-ui-devkit';
3
- import { CurrencyCode } from '@deenruv/admin-types';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Button, CardIcons, ConfirmationDialog, CustomCard, Input, apiClient, setInArrayBy, useDeenruvForm, z, EntityCustomFields, useTranslation, useSettings, EntityChannelManager, DEFAULT_CHANNEL_CODE, } from '@deenruv/react-ui-devkit';
3
+ import { CurrencyCode, DeletionResult } from '@deenruv/admin-types';
4
4
  import { useCallback, useEffect } from 'react';
5
5
  import { toast } from 'sonner';
6
6
  import { AssetsCard } from "./AssetsCard";
@@ -10,6 +10,7 @@ import { OptionsCard } from "./OptionsCard";
10
10
  import { FacetValuesCard } from "./FacetValuesCard";
11
11
  export const Variant = ({ variant, currentTranslationLng, onActionCompleted, productId }) => {
12
12
  const { t } = useTranslation('products');
13
+ const selectedChannel = useSettings((p) => p.selectedChannel);
13
14
  const variantSchema = z.object({
14
15
  translations: z.array(z.any()).default([]),
15
16
  price: z.any().optional(),
@@ -50,6 +51,8 @@ export const Variant = ({ variant, currentTranslationLng, onActionCompleted, pro
50
51
  const formValues = form.watch();
51
52
  const translations = formValues.translations || [];
52
53
  const currentTranslationValue = translations.find((v) => v.languageCode === currentTranslationLng);
54
+ const variantDisplayName = currentTranslationValue?.name || variant?.name || t('addVariantDialog.new');
55
+ const variantDisplaySku = formValues.sku || variant?.sku;
53
56
  useEffect(() => {
54
57
  if (!variant)
55
58
  return;
@@ -67,18 +70,20 @@ export const Variant = ({ variant, currentTranslationLng, onActionCompleted, pro
67
70
  form.setField('trackInventory', variant.trackInventory);
68
71
  form.setField('facetValueIds', variant.facetValues.map((f) => f.id));
69
72
  }, [variant]);
70
- const createVariant = useCallback(() => {
73
+ const createVariant = useCallback(async () => {
71
74
  const values = form.getValues();
72
- if (productId && values.sku && values.translations)
73
- return apiClient('mutation')({
75
+ const firstPrice = Array.isArray(values.prices) ? values.prices[0]?.price : undefined;
76
+ if (!productId || !values.sku || !values.translations)
77
+ return;
78
+ try {
79
+ const response = await apiClient('mutation')({
74
80
  createProductVariants: [
75
81
  {
76
82
  input: [
77
83
  {
78
84
  productId,
79
85
  translations: values.translations,
80
- // price: +values.price,
81
- prices: values.prices,
86
+ price: firstPrice,
82
87
  sku: values.sku,
83
88
  assetIds: values.assetIds,
84
89
  featuredAssetId: values.featuredAssetId,
@@ -100,15 +105,31 @@ export const Variant = ({ variant, currentTranslationLng, onActionCompleted, pro
100
105
  id: true,
101
106
  },
102
107
  ],
103
- })
104
- .then(() => {
105
- toast(t('toasts.createProductVariantSuccessToast'));
106
- onActionCompleted();
107
- })
108
- .catch(() => {
109
- toast(t('toasts.createProductVariantErrorToast'));
110
108
  });
111
- }, [form, productId, onActionCompleted, t]);
109
+ const createdVariantId = response.createProductVariants?.[0]?.id;
110
+ if (createdVariantId &&
111
+ selectedChannel?.id &&
112
+ selectedChannel.code &&
113
+ selectedChannel.code !== DEFAULT_CHANNEL_CODE) {
114
+ await apiClient('mutation')({
115
+ assignProductVariantsToChannel: [
116
+ {
117
+ input: {
118
+ productVariantIds: [createdVariantId],
119
+ channelId: selectedChannel.id,
120
+ },
121
+ },
122
+ { id: true },
123
+ ],
124
+ });
125
+ }
126
+ toast(t('toasts.createProductVariantSuccessToast'));
127
+ onActionCompleted();
128
+ }
129
+ catch {
130
+ toast(t('toasts.createProductVariantErrorToast'));
131
+ }
132
+ }, [form, productId, selectedChannel?.id, selectedChannel?.code, onActionCompleted, t]);
112
133
  const updateVariant = useCallback(() => {
113
134
  if (!variant)
114
135
  return;
@@ -153,16 +174,22 @@ export const Variant = ({ variant, currentTranslationLng, onActionCompleted, pro
153
174
  if (!variant)
154
175
  return;
155
176
  apiClient('mutation')({
156
- deleteProductVariant: [{ id: variant.id }, { message: true }],
177
+ deleteProductVariant: [{ id: variant.id }, { message: true, result: true }],
157
178
  })
158
- .then(() => {
159
- onActionCompleted();
160
- toast(t('toasts.deleteProductVariantSuccessToast'), {
161
- description: new Date().toLocaleString(),
179
+ .then(({ deleteProductVariant }) => {
180
+ if (deleteProductVariant.result === DeletionResult.DELETED) {
181
+ onActionCompleted();
182
+ toast(t('toasts.deleteProductVariantSuccessToast'), {
183
+ description: new Date().toLocaleString(),
184
+ });
185
+ return;
186
+ }
187
+ toast.error(t('toasts.deleteProductVariantErrorToast'), {
188
+ description: deleteProductVariant.message,
162
189
  });
163
190
  })
164
191
  .catch(() => toast.error(t('toasts.deleteProductVariantErrorToast')));
165
- }, [variant]);
192
+ }, [variant, onActionCompleted, t]);
166
193
  const setTranslationField = useCallback((field, e) => {
167
194
  const currentValue = translations.find((t) => t.languageCode === currentTranslationLng);
168
195
  const baseTranslation = currentValue ?? {
@@ -183,15 +210,15 @@ export const Variant = ({ variant, currentTranslationLng, onActionCompleted, pro
183
210
  return;
184
211
  form.setField('assetIds', [...newIds, id]);
185
212
  };
186
- return (_jsxs("div", { className: "mt-4 flex flex-col gap-4", children: [_jsx("div", { className: "flex gap-3 self-end", children: variant ? (_jsxs(_Fragment, { children: [_jsx(ConfirmationDialog, { onConfirm: deleteVariant, children: _jsx(Button, { variant: 'destructive', children: t('forms.removeVariant') }) }), _jsx(Button, { onClick: updateVariant, children: t('forms.updateVariant') })] })) : (_jsx(Button, { onClick: createVariant, children: t('addVariantDialog.add') })) }), _jsxs("div", { className: "flex gap-4", children: [_jsxs("div", { className: "flex w-2/3 flex-col gap-4", children: [!!variant && (_jsx(StockCard, { priceValue: formValues.price, taxRateValue: formValues.taxCategoryId, outOfStockThresholdValue: formValues.outOfStockThreshold, stockLevelsValue: formValues.stockLevels, stockOnHandValue: formValues.stockOnHand, useGlobalOutOfStockThresholdValue: formValues.useGlobalOutOfStockThreshold, onThresholdChange: (e) => form.setField('outOfStockThreshold', +e.target.value), onUseGlobalChange: (e) => form.setField('useGlobalOutOfStockThreshold', e), onTrackInventoryChange: (e) => form.setField('trackInventory', e), onStockOnHandChange: (e) => form.setField('stockOnHand', +e.target.value), onStockLocationsChange: (e) => form.setField('stockLevels', e), allStockLocations: variant?.stockLevels, stockAllocated: variant?.stockAllocated, trackInventoryValue: formValues.trackInventory })), _jsx(PriceCard, { currencyCode: variant?.currencyCode || CurrencyCode.PLN, priceValue: formValues.prices, onPriceChange: (e) => form.setField('prices', e), taxRateValue: formValues.taxCategoryId ?? undefined, onTaxRateChange: (id) => form.setField('taxCategoryId', id) }), _jsx(AssetsCard, { onAddAsset: handleAddAsset, featuredAssetId: formValues.featuredAssetId, assetsIds: formValues.assetIds, onFeaturedAssetChange: (id) => form.setField('featuredAssetId', id), onAssetsChange: (ids) => form.setField('assetIds', ids) }), _jsx(EntityCustomFields, { entityName: "productVariant", id: variant?.id, hideButton: true, onChange: (customFields, translations) => {
213
+ return (_jsxs("div", { className: "mt-4 flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2 rounded-lg border bg-muted/30 p-4 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: t('variants') }), _jsx("h3", { className: "truncate text-lg font-semibold", children: variantDisplayName })] }), _jsxs("div", { className: "text-sm text-muted-foreground sm:text-right", children: [_jsxs("span", { className: "font-medium text-foreground", children: [t('sku'), ":"] }), " ", variantDisplaySku || '—'] })] }), _jsx("div", { className: "flex gap-3 self-end", children: variant ? (_jsxs(_Fragment, { children: [_jsx(ConfirmationDialog, { onConfirm: deleteVariant, children: _jsx(Button, { variant: 'destructive', children: t('forms.removeVariant') }) }), _jsx(Button, { onClick: updateVariant, children: t('forms.updateVariant') })] })) : (_jsx(Button, { onClick: createVariant, children: t('addVariantDialog.add') })) }), _jsxs("div", { className: "flex gap-4", children: [_jsxs("div", { className: "flex w-2/3 flex-col gap-4", children: [!!variant && (_jsx(StockCard, { priceValue: formValues.price, taxRateValue: formValues.taxCategoryId, outOfStockThresholdValue: formValues.outOfStockThreshold, stockLevelsValue: formValues.stockLevels, stockOnHandValue: formValues.stockOnHand, useGlobalOutOfStockThresholdValue: formValues.useGlobalOutOfStockThreshold, onThresholdChange: (e) => form.setField('outOfStockThreshold', +e.target.value), onUseGlobalChange: (e) => form.setField('useGlobalOutOfStockThreshold', e), onTrackInventoryChange: (e) => form.setField('trackInventory', e), onStockOnHandChange: (e) => form.setField('stockOnHand', +e.target.value), onStockLocationsChange: (e) => form.setField('stockLevels', e), allStockLocations: variant?.stockLevels, stockAllocated: variant?.stockAllocated, trackInventoryValue: formValues.trackInventory })), _jsx(PriceCard, { currencyCode: variant?.currencyCode || CurrencyCode.PLN, priceValue: formValues.prices, onPriceChange: (e) => form.setField('prices', e), taxRateValue: formValues.taxCategoryId ?? undefined, onTaxRateChange: (id) => form.setField('taxCategoryId', id), showDefaultPriceWhenEmpty: !variant }), _jsx(AssetsCard, { onAddAsset: handleAddAsset, featuredAssetId: formValues.featuredAssetId, assetsIds: formValues.assetIds, onFeaturedAssetChange: (id) => form.setField('featuredAssetId', id), onAssetsChange: (ids) => form.setField('assetIds', ids) }), _jsx(EntityCustomFields, { entityName: "productVariant", id: variant?.id, hideButton: true, onChange: (customFields, translations) => {
187
214
  form.setField('customFields', customFields);
188
215
  if (translations)
189
216
  form.setField('translations', translations);
190
217
  }, initialValues: variant && 'customFields' in variant
191
218
  ? { customFields: variant.customFields, translations: variant.translations }
192
- : { customFields: {} } })] }), _jsxs("div", { className: "flex w-1/3 flex-col gap-4", children: [_jsx(CustomCard, { title: t('name'), icon: _jsx(CardIcons.basic, {}), color: "purple", children: _jsxs("div", { className: "flex flex-col gap-y-4", children: [_jsx(Input, { label: t('sku'), placeholder: t('sku'), value: formValues.sku ?? undefined, onChange: (e) => form.setField('sku', e.target.value) }), _jsx(Input, { label: t('name'), placeholder: t('name'), value: currentTranslationValue?.name ?? undefined, onChange: (e) => setTranslationField('name', e) })] }) }), _jsx(EntityChannelManager, { entity: "productVariant", entityId: variant?.id, entityChannels: variant?.channels ?? [], onRemoveSuccess: onActionCompleted, entityName: variant?.name, entityVariantList: {
219
+ : { customFields: {} } })] }), _jsxs("div", { className: "flex w-1/3 flex-col gap-4", children: [_jsx(CustomCard, { title: t('name'), icon: _jsx(CardIcons.basic, {}), color: "purple", children: _jsxs("div", { className: "flex flex-col gap-y-4", children: [_jsx(Input, { label: t('sku'), placeholder: t('sku'), value: formValues.sku ?? undefined, onChange: (e) => form.setField('sku', e.target.value) }), _jsx(Input, { label: t('name'), placeholder: t('name'), value: currentTranslationValue?.name ?? undefined, onChange: (e) => setTranslationField('name', e) })] }) }), variant && (_jsx(EntityChannelManager, { entity: "productVariant", entityId: variant.id, entityChannels: variant.channels ?? [], onRemoveSuccess: onActionCompleted, entityName: variant.name, entityVariantList: {
193
220
  items: [
194
- { price: variant?.price, priceWithTax: variant?.priceWithTax, currencyCode: variant?.currencyCode },
221
+ { price: variant.price, priceWithTax: variant.priceWithTax, currencyCode: variant.currencyCode },
195
222
  ],
196
- } }), _jsx(OptionsCard, { optionGroups: variant?.options || [], productId: productId, optionIds: formValues.optionIds ?? undefined, onChange: (e) => form.setField('optionIds', e), createMode: !variant }), !!variant && (_jsx(FacetValuesCard, { facetValuesIds: formValues.facetValueIds ?? undefined, onChange: (e) => form.setField('facetValueIds', e) }))] })] })] }));
223
+ } })), _jsx(OptionsCard, { optionGroups: variant?.options || [], productId: productId, optionIds: formValues.optionIds ?? undefined, onChange: (e) => form.setField('optionIds', e), createMode: !variant }), !!variant && (_jsx(FacetValuesCard, { facetValuesIds: formValues.facetValueIds ?? undefined, onChange: (e) => form.setField('facetValueIds', e) }))] })] })] }));
197
224
  };
@@ -42,5 +42,5 @@ export const VariantsTab = () => {
42
42
  useEffect(() => {
43
43
  fetchData();
44
44
  }, [fetchData]);
45
- return (_jsxs("div", { className: "flex flex-col", children: [getMarker(), _jsx("div", { className: "flex flex-col items-end gap-4", children: _jsxs(Tabs, { defaultValue: variants?.[0]?.id, className: "w-full", value: activeTab, onValueChange: handleTabChange, children: [_jsxs(TabsList, { className: "h-auto flex-wrap justify-start", children: [_jsxs(TabsTrigger, { value: NEW_VARIANT_TAB_VALUE, className: "text-blue-600", children: [_jsx(PlusCircle, { size: 16, className: "mr-2 translate-y-px" }), t('addVariantDialog.new')] }, 'new-variant'), variants?.map((v) => (_jsx(TabsTrigger, { value: v.id, children: v.name }, v.id + '-trigger')))] }), _jsx(TabsContent, { value: NEW_VARIANT_TAB_VALUE, children: id && _jsx(Variant, { currentTranslationLng: contentLanguage, onActionCompleted: fetchData, productId: id }) }, 'new-variant-content'), id && variants?.length ? (variants?.map((v) => (_jsx(TabsContent, { value: v.id, children: _jsx(Variant, { currentTranslationLng: contentLanguage, variant: v, onActionCompleted: fetchData, productId: id }) }, v.id + '-content')))) : (_jsx("div", { className: "flex w-full items-center justify-center", children: activeTab !== NEW_VARIANT_TAB_VALUE && (_jsx(EmptyState, { columnsLength: 1, title: t('variantsTab.emptyState.title'), description: t('variantsTab.emptyState.description') })) }))] }) })] }));
45
+ return (_jsxs("div", { className: "flex flex-col", children: [getMarker(), _jsx("div", { className: "flex flex-col items-end gap-4", children: _jsxs(Tabs, { defaultValue: variants?.[0]?.id, className: "w-full", value: activeTab, onValueChange: handleTabChange, children: [_jsxs(TabsList, { className: "h-auto flex-wrap justify-start", children: [_jsxs(TabsTrigger, { value: NEW_VARIANT_TAB_VALUE, className: "text-blue-600", children: [_jsx(PlusCircle, { size: 16, className: "mr-2 translate-y-px" }), t('addVariantDialog.new')] }, 'new-variant'), variants?.map((v) => (_jsx(TabsTrigger, { value: v.id, className: "max-w-xs", children: _jsxs("span", { className: "flex min-w-0 flex-col items-start text-left leading-tight", children: [_jsx("span", { className: "max-w-full truncate", children: v.name?.trim() || t('variantsTab.unnamedVariant') }), v.sku && (_jsx("span", { className: "max-w-full truncate text-xs font-normal text-muted-foreground", children: v.sku }))] }) }, v.id + '-trigger')))] }), _jsx(TabsContent, { value: NEW_VARIANT_TAB_VALUE, children: id && _jsx(Variant, { currentTranslationLng: contentLanguage, onActionCompleted: fetchData, productId: id }) }, 'new-variant-content'), id && variants?.length ? (variants?.map((v) => (_jsx(TabsContent, { value: v.id, children: _jsx(Variant, { currentTranslationLng: contentLanguage, variant: v, onActionCompleted: fetchData, productId: id }) }, v.id + '-content')))) : (_jsx("div", { className: "flex w-full items-center justify-center", children: activeTab !== NEW_VARIANT_TAB_VALUE && (_jsx(EmptyState, { columnsLength: 1, title: t('variantsTab.emptyState.title'), description: t('variantsTab.emptyState.description') })) }))] }) })] }));
46
46
  };
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useDetailView, useGlobalStore, useSettings, useTranslation } from '@deenruv/react-ui-devkit';
3
+ import { StorefrontAction } from "../../../../components/storefrontAction";
4
+ import { getProductStorefrontActionState, } from './ProductStorefrontAction.logic';
5
+ const PRODUCT_STOREFRONT_FORM_KEYS = ['CreateProductInput', 'translations', 'enabled'];
6
+ export const ProductStorefrontAction = () => {
7
+ const { t } = useTranslation('products');
8
+ const selectedChannelCode = useSettings((state) => state.selectedChannel?.code);
9
+ const languageCode = useSettings((state) => state.translationsLanguage);
10
+ const resolveStorefrontEntityUrl = useGlobalStore((state) => state.ui?.resolveStorefrontEntityUrl);
11
+ const { entity, form, id } = useDetailView('products-detail-view', ...PRODUCT_STOREFRONT_FORM_KEYS);
12
+ const formTranslations = form.base.watch('translations');
13
+ const formEnabled = form.base.watch('enabled');
14
+ if (!id || !entity || !resolveStorefrontEntityUrl)
15
+ return null;
16
+ const state = getProductStorefrontActionState({
17
+ product: entity,
18
+ selectedChannelCode,
19
+ languageCode,
20
+ formTranslations,
21
+ formEnabled,
22
+ resolveStorefrontEntityUrl,
23
+ });
24
+ const label = t('storefrontAction.label');
25
+ if (state.kind === 'hidden')
26
+ return null;
27
+ const disabledTooltips = {
28
+ productDisabled: t('storefrontAction.disabled.productDisabled'),
29
+ missingChannel: t('storefrontAction.disabled.missingChannel'),
30
+ channelNotAssigned: t('storefrontAction.disabled.channelNotAssigned'),
31
+ missingSlug: t('storefrontAction.disabled.missingSlug'),
32
+ unsavedSlug: t('storefrontAction.disabled.unsavedSlug'),
33
+ unsavedEnabled: t('storefrontAction.disabled.unsavedEnabled'),
34
+ resolverFailed: t('storefrontAction.disabled.resolverFailed'),
35
+ resolverEmpty: t('storefrontAction.disabled.resolverEmpty'),
36
+ urlNotAbsolute: t('storefrontAction.disabled.urlNotAbsolute'),
37
+ unsupportedProtocol: t('storefrontAction.disabled.unsupportedProtocol'),
38
+ };
39
+ return (_jsx(StorefrontAction, { label: label, state: state.kind === 'ready'
40
+ ? { kind: 'ready', url: state.url }
41
+ : { kind: 'disabled', tooltip: disabledTooltips[state.reason] } }));
42
+ };
@@ -0,0 +1,39 @@
1
+ import { getStorefrontEntityUrlState, } from '../../../../components/storefrontAction/StorefrontAction.logic';
2
+ const getFormSlug = (formTranslations, languageCode) => {
3
+ if (!Array.isArray(formTranslations))
4
+ return undefined;
5
+ const translation = formTranslations.find((value) => typeof value === 'object' && value !== null && 'languageCode' in value && value.languageCode === languageCode);
6
+ return typeof translation?.slug === 'string' ? translation.slug : undefined;
7
+ };
8
+ export const getProductStorefrontActionState = ({ product, selectedChannelCode, languageCode, formTranslations, formEnabled, resolveStorefrontEntityUrl, }) => {
9
+ if (!selectedChannelCode)
10
+ return { kind: 'disabled', reason: 'missingChannel' };
11
+ const persistedTranslation = languageCode
12
+ ? product.translations.find((translation) => translation.languageCode === languageCode)
13
+ : undefined;
14
+ if (!languageCode || typeof persistedTranslation?.slug !== 'string' || !persistedTranslation.slug.trim()) {
15
+ return { kind: 'disabled', reason: 'missingSlug' };
16
+ }
17
+ const context = {
18
+ entityType: 'product',
19
+ entityId: product.id,
20
+ slug: persistedTranslation.slug,
21
+ channelCode: selectedChannelCode,
22
+ languageCode,
23
+ };
24
+ const urlState = getStorefrontEntityUrlState(context, resolveStorefrontEntityUrl);
25
+ if (urlState.kind === 'hidden')
26
+ return urlState;
27
+ if (!product.enabled)
28
+ return { kind: 'disabled', reason: 'productDisabled' };
29
+ if (!product.channels.some(({ code }) => code === selectedChannelCode)) {
30
+ return { kind: 'disabled', reason: 'channelNotAssigned' };
31
+ }
32
+ if (formEnabled !== product.enabled) {
33
+ return { kind: 'disabled', reason: 'unsavedEnabled' };
34
+ }
35
+ if (getFormSlug(formTranslations, languageCode) !== persistedTranslation.slug) {
36
+ return { kind: 'disabled', reason: 'unsavedSlug' };
37
+ }
38
+ return urlState.kind === 'ready' ? { ...urlState, context } : urlState;
39
+ };
@@ -0,0 +1,2 @@
1
+ export * from './ProductStorefrontAction';
2
+ export * from './ProductStorefrontAction.logic';