bananas-commerce-admin 0.20.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/esm/components/Card/CardFieldAutoComplete.js +2 -2
  2. package/dist/esm/components/Card/CardFieldAutoComplete.js.map +1 -1
  3. package/dist/esm/components/Card/CardFieldSelect.js +6 -3
  4. package/dist/esm/components/Card/CardFieldSelect.js.map +1 -1
  5. package/dist/esm/components/Card/index.js +17 -1
  6. package/dist/esm/components/Card/index.js.map +1 -1
  7. package/dist/esm/components/ContribInlines.js +3 -2
  8. package/dist/esm/components/ContribInlines.js.map +1 -1
  9. package/dist/esm/extensions/catalog/components/ArticleCard.js +9 -4
  10. package/dist/esm/extensions/catalog/components/ArticleCard.js.map +1 -1
  11. package/dist/esm/extensions/catalog/contrib/ProductArticles.js +48 -0
  12. package/dist/esm/extensions/catalog/contrib/ProductArticles.js.map +1 -0
  13. package/dist/esm/extensions/catalog/index.js +9 -0
  14. package/dist/esm/extensions/catalog/index.js.map +1 -1
  15. package/dist/esm/extensions/pim/components/ProductCard.js +123 -0
  16. package/dist/esm/extensions/pim/components/ProductCard.js.map +1 -0
  17. package/dist/esm/extensions/pim/components/ProductPropertiesCard.js +71 -0
  18. package/dist/esm/extensions/pim/components/ProductPropertiesCard.js.map +1 -0
  19. package/dist/esm/extensions/pim/components/ProductPropertyField.js +25 -0
  20. package/dist/esm/extensions/pim/components/ProductPropertyField.js.map +1 -0
  21. package/dist/esm/extensions/pim/components/ProductRow.js +11 -0
  22. package/dist/esm/extensions/pim/components/ProductRow.js.map +1 -0
  23. package/dist/esm/extensions/pim/contrib/ArticleProduct.js +110 -0
  24. package/dist/esm/extensions/pim/contrib/ArticleProduct.js.map +1 -0
  25. package/dist/esm/extensions/pim/index.js +43 -0
  26. package/dist/esm/extensions/pim/index.js.map +1 -0
  27. package/dist/esm/extensions/pim/pages/product/create.js +41 -0
  28. package/dist/esm/extensions/pim/pages/product/create.js.map +1 -0
  29. package/dist/esm/extensions/pim/pages/product/detail.js +46 -0
  30. package/dist/esm/extensions/pim/pages/product/detail.js.map +1 -0
  31. package/dist/esm/extensions/pim/pages/product/list.js +54 -0
  32. package/dist/esm/extensions/pim/pages/product/list.js.map +1 -0
  33. package/dist/esm/extensions/pim/types/contrib.js +2 -0
  34. package/dist/esm/extensions/pim/types/contrib.js.map +1 -0
  35. package/dist/esm/extensions/pim/types/product.js +2 -0
  36. package/dist/esm/extensions/pim/types/product.js.map +1 -0
  37. package/dist/esm/index.js +1 -0
  38. package/dist/esm/index.js.map +1 -1
  39. package/dist/types/components/Card/CardFieldAutoComplete.d.ts +3 -0
  40. package/dist/types/components/Card/CardFieldSelect.d.ts +1 -0
  41. package/dist/types/components/ContribInlines.d.ts +1 -0
  42. package/dist/types/extensions/catalog/components/ArticleCard.d.ts +12 -0
  43. package/dist/types/extensions/catalog/contrib/ProductArticles.d.ts +4 -0
  44. package/dist/types/extensions/catalog/types/contrib.d.ts +8 -0
  45. package/dist/types/extensions/pim/components/ProductCard.d.ts +23 -0
  46. package/dist/types/extensions/pim/components/ProductPropertiesCard.d.ts +8 -0
  47. package/dist/types/extensions/pim/components/ProductPropertyField.d.ts +8 -0
  48. package/dist/types/extensions/pim/components/ProductRow.d.ts +6 -0
  49. package/dist/types/extensions/pim/contrib/ArticleProduct.d.ts +4 -0
  50. package/dist/types/extensions/pim/index.d.ts +7 -0
  51. package/dist/types/extensions/pim/pages/product/create.d.ts +3 -0
  52. package/dist/types/extensions/pim/pages/product/detail.d.ts +4 -0
  53. package/dist/types/extensions/pim/pages/product/list.d.ts +4 -0
  54. package/dist/types/extensions/pim/types/contrib.d.ts +19 -0
  55. package/dist/types/extensions/pim/types/product.d.ts +35 -0
  56. package/dist/types/index.d.ts +1 -0
  57. package/dist/types/types/index.d.ts +1 -1
  58. package/package.json +6 -1
  59. package/src/components/Card/CardFieldAutoComplete.tsx +14 -1
  60. package/src/components/Card/CardFieldSelect.tsx +6 -2
  61. package/src/components/Card/index.tsx +21 -1
  62. package/src/components/ContribInlines.tsx +5 -2
  63. package/src/extensions/catalog/components/ArticleCard.tsx +22 -4
  64. package/src/extensions/catalog/contrib/ProductArticles.tsx +82 -0
  65. package/src/extensions/catalog/index.tsx +9 -0
  66. package/src/extensions/catalog/types/contrib.ts +9 -0
  67. package/src/extensions/pim/components/ProductCard.tsx +242 -0
  68. package/src/extensions/pim/components/ProductPropertiesCard.tsx +114 -0
  69. package/src/extensions/pim/components/ProductPropertyField.tsx +67 -0
  70. package/src/extensions/pim/components/ProductRow.tsx +23 -0
  71. package/src/extensions/pim/contrib/ArticleProduct.tsx +215 -0
  72. package/src/extensions/pim/index.tsx +67 -0
  73. package/src/extensions/pim/pages/product/create.tsx +63 -0
  74. package/src/extensions/pim/pages/product/detail.tsx +85 -0
  75. package/src/extensions/pim/pages/product/list.tsx +87 -0
  76. package/src/extensions/pim/types/contrib.ts +21 -0
  77. package/src/extensions/pim/types/product.ts +52 -0
  78. package/src/index.ts +1 -0
  79. package/src/types/index.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bananas-commerce-admin",
3
- "version": "0.20.2",
3
+ "version": "0.21.0",
4
4
  "description": "What's this, an admin for apes?",
5
5
  "keywords": [
6
6
  "admin",
@@ -12,6 +12,11 @@
12
12
  "bananas-commerce-admin"
13
13
  ],
14
14
  "author": "Elias Sjögreen",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/5m/bananas-commerce.git",
18
+ "directory": "admin/packages/bananas-commerce-admin"
19
+ },
15
20
  "module": "./dist/esm/index.js",
16
21
  "main": "./dist/cjs/index.js",
17
22
  "types": "./dist/types/index.d.ts",
@@ -16,6 +16,9 @@ export interface CardFieldAutoCompleteProps extends CardFieldBaseProps, React.Pr
16
16
  isEditable?: boolean;
17
17
  type: "autocomplete";
18
18
  value: FormOption[];
19
+ autoSelect?: boolean;
20
+ clearOnBlur?: boolean;
21
+ freeSolo?: boolean;
19
22
  }
20
23
 
21
24
  export const CardFieldAutoComplete: React.FC<Omit<CardFieldAutoCompleteProps, "type">> = ({
@@ -29,6 +32,9 @@ export const CardFieldAutoComplete: React.FC<Omit<CardFieldAutoCompleteProps, "t
29
32
  options,
30
33
  size = "grow",
31
34
  value: defaultValue,
35
+ autoSelect = false,
36
+ clearOnBlur = false,
37
+ freeSolo = false,
32
38
  ...props
33
39
  }) => {
34
40
  const { isCompact, isEditing } = useCardContext();
@@ -41,13 +47,20 @@ export const CardFieldAutoComplete: React.FC<Omit<CardFieldAutoCompleteProps, "t
41
47
  {isEditing && isEditable ? (
42
48
  <Stack alignItems="center" justifyContent="space-between">
43
49
  <Autocomplete
50
+ autoSelect={autoSelect}
51
+ clearOnBlur={clearOnBlur}
52
+ freeSolo={freeSolo}
44
53
  isOptionEqualToValue={(option: FormOption, value: FormOption) => option.id === value.id}
45
54
  multiple={true}
46
55
  options={options ?? []}
47
56
  renderInput={(props) => <TextField {...props} label={label} />}
48
57
  sx={{ width: "100%" }}
49
58
  value={selectedOptions}
50
- onChange={(_, value) => setSelectedOptions(value as FormOption[])}
59
+ onChange={(_, value) =>
60
+ setSelectedOptions(
61
+ value.map((v) => (typeof v == "string" ? { id: v, label: v } : v)) as FormOption[],
62
+ )
63
+ }
51
64
  {...props}
52
65
  />
53
66
 
@@ -21,6 +21,7 @@ export interface CardFieldSelectProps
21
21
  emptyValue?: FormOption;
22
22
  value: FormOption | undefined;
23
23
  isEditable?: boolean;
24
+ onUpdated?: (val: string) => void;
24
25
  }
25
26
 
26
27
  export const CardFieldSelect: React.FC<Omit<CardFieldSelectProps, "type">> = ({
@@ -37,6 +38,7 @@ export const CardFieldSelect: React.FC<Omit<CardFieldSelectProps, "type">> = ({
37
38
  required = false,
38
39
  size = "grow",
39
40
  value: defaultValue = undefined,
41
+ onUpdated,
40
42
  ...props
41
43
  }) => {
42
44
  const { isCompact, isEditing } = useCardContext();
@@ -44,9 +46,11 @@ export const CardFieldSelect: React.FC<Omit<CardFieldSelectProps, "type">> = ({
44
46
 
45
47
  const handleChange = useCallback(
46
48
  (event: SelectChangeEvent<unknown>) => {
47
- setValue(event.target.value as string);
49
+ const currentValue = event.target.value as string;
50
+ setValue(currentValue);
51
+ if (onUpdated) onUpdated(currentValue);
48
52
  },
49
- [setValue],
53
+ [setValue, onUpdated],
50
54
  );
51
55
 
52
56
  return (
@@ -104,6 +104,26 @@ export interface CardProps<T = unknown> extends Omit<MuiCardProps, "onSubmit"> {
104
104
  gridProps?: Omit<Grid2Props, "size">;
105
105
  }
106
106
 
107
+ function formDataToObject(
108
+ formData: FormData,
109
+ ): Record<string, FormDataEntryValue | FormDataEntryValue[]> {
110
+ const values: Record<string, FormDataEntryValue | FormDataEntryValue[]> = {};
111
+
112
+ for (const [key, value] of formData.entries()) {
113
+ const existing = values[key];
114
+
115
+ if (existing == null) {
116
+ values[key] = value;
117
+ } else if (Array.isArray(existing)) {
118
+ existing.push(value);
119
+ } else {
120
+ values[key] = [existing, value];
121
+ }
122
+ }
123
+
124
+ return values;
125
+ }
126
+
107
127
  /**
108
128
  * A card component that wraps a form with `onSubmit` and provides `cardContext`.
109
129
  * This should be your building block for all admin forms not better represented by a Table.
@@ -168,7 +188,7 @@ export function Card<T>({
168
188
 
169
189
  const form = event.currentTarget;
170
190
  const formData = new FormData(form);
171
- const values = Object.fromEntries(formData);
191
+ const values = formDataToObject(formData);
172
192
 
173
193
  setIsDisabled(true);
174
194
 
@@ -10,12 +10,14 @@ import { usePage } from "./Page";
10
10
  export interface ContribInlinesProps {
11
11
  contribParams?: Record<string | number | symbol, unknown>;
12
12
  data?: unknown;
13
+ variant?: string;
13
14
  }
14
15
 
15
- export const ContribInlines: React.FC<ContribInlinesProps> = ({ contribParams, data }) => {
16
+ export const ContribInlines: React.FC<ContribInlinesProps> = ({ contribParams, data, variant }) => {
16
17
  const params = useParams();
17
18
  const page = usePage();
18
19
  const { user } = useUser();
20
+ const componentVariant = variant ?? "inline";
19
21
 
20
22
  contribParams = { ...params, ...contribParams };
21
23
 
@@ -23,7 +25,8 @@ export const ContribInlines: React.FC<ContribInlinesProps> = ({ contribParams, d
23
25
  <>
24
26
  {page.contrib
25
27
  .filter(
26
- (operation) => operation.method === "GET" && operation.component?.variant === "inline",
28
+ (operation) =>
29
+ operation.method === "GET" && operation.component?.variant === componentVariant,
27
30
  )
28
31
  .map((operation) => {
29
32
  if (operation.component?.predicate && data && !operation.component?.predicate(data))
@@ -34,6 +34,19 @@ export type ArticleCardProps =
34
34
  onUpdated: (article: ArticleDetail) => void;
35
35
  };
36
36
 
37
+ export interface ArticleCardFormValues {
38
+ code: string;
39
+ item_type: string;
40
+ is_active: string;
41
+ name: string;
42
+ description: string;
43
+ product_number: string;
44
+ model_number: string;
45
+ tax_code: string;
46
+ gtin: string;
47
+ variant: string;
48
+ }
49
+
37
50
  export const ArticleCard: React.FC<ArticleCardProps> = ({
38
51
  article,
39
52
  create,
@@ -62,7 +75,7 @@ export const ArticleCard: React.FC<ArticleCardProps> = ({
62
75
  };
63
76
 
64
77
  const handleSave = useCallback(
65
- async (values: unknown) => {
78
+ async ({ is_active, ...values }: ArticleCardFormValues) => {
66
79
  if (create != null) {
67
80
  const action = api.operations["catalog.article:create"];
68
81
  if (!action) {
@@ -71,7 +84,10 @@ export const ArticleCard: React.FC<ArticleCardProps> = ({
71
84
 
72
85
  const response = await action.call({
73
86
  params,
74
- body: values,
87
+ body: {
88
+ is_active: is_active == "on",
89
+ ...values,
90
+ },
75
91
  });
76
92
 
77
93
  if (response.ok) {
@@ -90,7 +106,10 @@ export const ArticleCard: React.FC<ArticleCardProps> = ({
90
106
 
91
107
  const response = await action.call({
92
108
  params,
93
- body: values,
109
+ body: {
110
+ is_active: is_active == "on",
111
+ ...values,
112
+ },
94
113
  });
95
114
 
96
115
  if (response.ok) {
@@ -173,7 +192,6 @@ export const ArticleCard: React.FC<ArticleCardProps> = ({
173
192
  size={1}
174
193
  value={article.gtin}
175
194
  />
176
- <input name="is_active" type="hidden" value="false" />
177
195
  <CardFieldCheckbox
178
196
  formName="is_active"
179
197
  label={t("Active")}
@@ -0,0 +1,82 @@
1
+ import React from "react";
2
+
3
+ import NavigateNextIcon from "@mui/icons-material/NavigateNext";
4
+ import { TableBody, TableRow } from "@mui/material";
5
+
6
+ import Card from "../../../components/Card";
7
+ import CardContent from "../../../components/Card/CardContent";
8
+ import CardHeader from "../../../components/Card/CardHeader";
9
+ import Table from "../../../components/Table";
10
+ import { TableCell } from "../../../components/Table/TableCell";
11
+ import { useI18n } from "../../../contexts/I18nContext";
12
+ import { useRouter } from "../../../contexts/RouterContext";
13
+ import { ContribComponent } from "../../../types";
14
+ import { ProductArticle } from "../types/contrib";
15
+
16
+ const ProductArticlesCard: ContribComponent<ProductArticle[]> = ({ data }) => {
17
+ const { t } = useI18n();
18
+ const { navigate } = useRouter();
19
+
20
+ const articles = data;
21
+
22
+ return (
23
+ <Card>
24
+ <CardHeader title={t("Articles")} />
25
+ <CardContent
26
+ sx={{
27
+ p: 0,
28
+ "&:last-child": { pb: 0 },
29
+ }}
30
+ >
31
+ <Table
32
+ count={articles.length}
33
+ tableContainerProps={{
34
+ sx: (theme) => ({
35
+ borderTop: `1px solid ${theme.palette.divider}`,
36
+ px: 0,
37
+ }),
38
+ }}
39
+ >
40
+ <TableBody
41
+ sx={{
42
+ "& .MuiTableRow-root:first-of-type .MuiTableCell-root": (theme) => ({
43
+ borderTop: `1px solid ${theme.palette.divider}`,
44
+ }),
45
+ "& .MuiTableRow-root:last-of-type .MuiTableCell-root": {
46
+ borderBottom: "none",
47
+ },
48
+ }}
49
+ >
50
+ {articles.map((article) => {
51
+ return (
52
+ <TableRow
53
+ key={article.id}
54
+ hover
55
+ sx={() => ({
56
+ cursor: "pointer",
57
+ })}
58
+ onClick={() => navigate("catalog.article:detail", { params: { id: article.id } })}
59
+ >
60
+ <TableCell>{article.variant}</TableCell>
61
+ <TableCell
62
+ typographyProps={{
63
+ variant: "caption",
64
+ color: "text.secondary",
65
+ }}
66
+ >
67
+ {article.code}
68
+ </TableCell>
69
+ <TableCell align="right" padding="checkbox">
70
+ <NavigateNextIcon color="action" fontSize="small" />
71
+ </TableCell>
72
+ </TableRow>
73
+ );
74
+ })}
75
+ </TableBody>
76
+ </Table>
77
+ </CardContent>
78
+ </Card>
79
+ );
80
+ };
81
+
82
+ export default ProductArticlesCard;
@@ -62,4 +62,13 @@ export const contrib: Record<string, ContribComponentMap> = {
62
62
  permission: "catalog.view_article",
63
63
  },
64
64
  },
65
+ pim: {
66
+ "pim:product:detail:articles": {
67
+ title: "Product",
68
+ icon: StorefrontIcon,
69
+ component: async () => (await import("./contrib/ProductArticles")).default,
70
+ variant: "sidebar",
71
+ permission: "catalog.view_article",
72
+ },
73
+ },
65
74
  } as const;
@@ -14,3 +14,12 @@ export interface SiteItem {
14
14
  export interface SiteItemsResponse {
15
15
  items: SiteItem[];
16
16
  }
17
+
18
+ export interface ProductArticle {
19
+ id: number;
20
+ item_type: string;
21
+ name: string;
22
+ variant: string;
23
+ code: string;
24
+ is_active: boolean;
25
+ }
@@ -0,0 +1,242 @@
1
+ import React, { useCallback, useMemo, useState } from "react";
2
+ import { useParams } from "react-router-dom";
3
+
4
+ import CancelIcon from "@mui/icons-material/Cancel";
5
+ import CheckCircleIcon from "@mui/icons-material/CheckCircle";
6
+
7
+ import Card from "../../../components/Card";
8
+ import CardActions from "../../../components/Card/CardActions";
9
+ import CardCancelButton from "../../../components/Card/CardCancelButton";
10
+ import CardContent from "../../../components/Card/CardContent";
11
+ import CardFieldCheckbox from "../../../components/Card/CardFieldCheckbox";
12
+ import CardFieldSelect from "../../../components/Card/CardFieldSelect";
13
+ import CardFieldText from "../../../components/Card/CardFieldText";
14
+ import CardHeader from "../../../components/Card/CardHeader";
15
+ import CardRow from "../../../components/Card/CardRow";
16
+ import CardSaveButton from "../../../components/Card/CardSaveButton";
17
+ import { useApi } from "../../../contexts/ApiContext";
18
+ import { useI18n } from "../../../contexts/I18nContext";
19
+ import { useUser } from "../../../contexts/UserContext";
20
+ import { hasPermission } from "../../../util/has_permission";
21
+ import { ProductCreated, ProductDetail, ProductItemTypeOption } from "../types/product";
22
+
23
+ import { ProductPropertyField } from "./ProductPropertyField";
24
+
25
+ export type ProductCardProps =
26
+ | {
27
+ create: true;
28
+ product?: ProductDetail;
29
+ onCreated: (product: ProductCreated) => void;
30
+ onUpdated?: never;
31
+ itemTypes: ProductItemTypeOption[];
32
+ }
33
+ | {
34
+ create?: false;
35
+ product: ProductDetail;
36
+ onCreated?: never;
37
+ onUpdated: (product: ProductDetail) => void;
38
+ itemTypes: ProductItemTypeOption[];
39
+ };
40
+
41
+ export interface ProductCardFormValues {
42
+ number: string;
43
+ item_type: string;
44
+ is_active: string;
45
+ name: string;
46
+ description: string;
47
+ }
48
+
49
+ export const ProductCard: React.FC<ProductCardProps> = ({
50
+ product,
51
+ create,
52
+ onCreated,
53
+ onUpdated,
54
+ itemTypes,
55
+ }) => {
56
+ const params = useParams();
57
+ const api = useApi();
58
+ const { t } = useI18n();
59
+ const { user } = useUser();
60
+ const canCreate = useMemo(() => hasPermission(user, "pim.add_product"), [user]);
61
+ const canChange = useMemo(() => hasPermission(user, "pim.change_product"), [user]);
62
+
63
+ product ??= {
64
+ id: 0,
65
+ name: "",
66
+ description: "",
67
+ item_type: "",
68
+ number: "",
69
+ is_active: true,
70
+ } as ProductDetail;
71
+
72
+ const [productItemType, setProductItemType] = useState<ProductItemTypeOption | undefined>(
73
+ undefined,
74
+ );
75
+ const productPropertiesList = productItemType ? productItemType.product_properties : [];
76
+
77
+ const productPropertiesRows = productPropertiesList.reduce<(typeof productPropertiesList)[]>(
78
+ (rows, prop, index) => {
79
+ if (index % 2 === 0) rows.push([prop]);
80
+ else rows[rows.length - 1].push(prop);
81
+ return rows;
82
+ },
83
+ [],
84
+ );
85
+
86
+ const handleSave = useCallback(
87
+ async ({ is_active, ...values }: ProductCardFormValues) => {
88
+ if (create != null) {
89
+ const action = api.operations["pim.product:create"];
90
+ if (!action) {
91
+ throw new Error('Invalid action "pim.product:create".');
92
+ }
93
+
94
+ const productData: Record<string, unknown> = {};
95
+ const productProperties: Record<string, unknown> = {};
96
+
97
+ for (const [key, value] of Object.entries(values)) {
98
+ if (key.startsWith("properties.")) {
99
+ const propertyName = key.replace("properties.", "");
100
+ productProperties[propertyName] = value;
101
+ } else {
102
+ productData[key] = value;
103
+ }
104
+ }
105
+
106
+ const response = await action.call({
107
+ params,
108
+ body: {
109
+ is_active: is_active == "on",
110
+ ...productData,
111
+ properties: productProperties,
112
+ },
113
+ });
114
+
115
+ if (response.ok) {
116
+ const createdProduct = await response.json();
117
+ onCreated?.(createdProduct);
118
+ return t("Product created successfully.");
119
+ } else {
120
+ console.error("[PRODUCT_CARD]", response);
121
+ throw new Error("creating product.");
122
+ }
123
+ } else {
124
+ const action = api.operations["pim.product:update"];
125
+ if (!action) {
126
+ throw new Error('Invalid action "pim.product:update".');
127
+ }
128
+
129
+ const response = await action.call({
130
+ params,
131
+ body: {
132
+ is_active: is_active == "on",
133
+ ...values,
134
+ },
135
+ });
136
+
137
+ if (response.ok) {
138
+ const updatedProduct = await response.json();
139
+ onUpdated?.(updatedProduct);
140
+ return t("Product updated successfully.");
141
+ } else {
142
+ console.error("[PRODUCT_CARD]", response);
143
+ throw new Error("updating product fields.");
144
+ }
145
+ }
146
+ },
147
+ [api, params, create, onCreated, onUpdated, t],
148
+ );
149
+
150
+ return (
151
+ <Card
152
+ alwaysEditable={create && canCreate}
153
+ columns={3}
154
+ defaultEditing={create && canCreate}
155
+ isEditable={(create && canCreate) || (!create && canChange)}
156
+ onSubmit={handleSave}
157
+ >
158
+ <CardHeader title={t("Product")} />
159
+
160
+ <CardContent>
161
+ <CardRow>
162
+ <CardFieldText
163
+ formName="number"
164
+ label={t("Product number")}
165
+ required={true}
166
+ value={product.number}
167
+ />
168
+
169
+ <CardFieldSelect
170
+ formName="item_type"
171
+ label={t("Type")}
172
+ options={itemTypes.map((itemType) => ({ id: itemType.name, label: itemType.name }))}
173
+ required={true}
174
+ size={1}
175
+ value={
176
+ product.item_type ? { id: product.item_type, label: product.item_type } : undefined
177
+ }
178
+ onUpdated={(value) => {
179
+ const selectedItemType = itemTypes.find((itemType) => itemType.name == value);
180
+ setProductItemType(selectedItemType);
181
+ }}
182
+ />
183
+
184
+ <CardFieldCheckbox
185
+ formName="is_active"
186
+ label={t("Active")}
187
+ noValue={<CancelIcon color="error" fontSize="small" />}
188
+ size={1}
189
+ value={product.is_active}
190
+ yesValue={<CheckCircleIcon color="success" fontSize="small" />}
191
+ />
192
+ </CardRow>
193
+
194
+ <CardRow>
195
+ <CardFieldText
196
+ formName="name"
197
+ label={t("Name")}
198
+ required={true}
199
+ size={3}
200
+ value={product.name}
201
+ />
202
+ </CardRow>
203
+ <CardRow>
204
+ <CardFieldText
205
+ formName="description"
206
+ label={t("Description")}
207
+ size={3}
208
+ value={product.description}
209
+ />
210
+ </CardRow>
211
+ </CardContent>
212
+
213
+ {create && productPropertiesRows.length > 0 && (
214
+ <>
215
+ <CardHeader title={t("Properties")} />
216
+
217
+ <CardContent>
218
+ {productPropertiesRows.map((row, index) => (
219
+ <CardRow key={index}>
220
+ {row.map((prop) => (
221
+ <ProductPropertyField
222
+ key={`properties.${prop.name}`}
223
+ formName={`properties.${prop.name}`}
224
+ property={prop}
225
+ value={undefined}
226
+ />
227
+ ))}
228
+ </CardRow>
229
+ ))}
230
+ </CardContent>
231
+ </>
232
+ )}
233
+
234
+ {(canChange || canCreate) && (
235
+ <CardActions>
236
+ {!create && <CardCancelButton />}
237
+ <CardSaveButton label={create ? "Create" : "Save"} />
238
+ </CardActions>
239
+ )}
240
+ </Card>
241
+ );
242
+ };
@@ -0,0 +1,114 @@
1
+ import React from "react";
2
+ import { useParams } from "react-router-dom";
3
+
4
+ import Card from "../../../components/Card";
5
+ import CardActions from "../../../components/Card/CardActions";
6
+ import CardCancelButton from "../../../components/Card/CardCancelButton";
7
+ import CardContent from "../../../components/Card/CardContent";
8
+ import CardHeader from "../../../components/Card/CardHeader";
9
+ import CardRow from "../../../components/Card/CardRow";
10
+ import CardSaveButton from "../../../components/Card/CardSaveButton";
11
+ import { useApi } from "../../../contexts/ApiContext";
12
+ import { useI18n } from "../../../contexts/I18nContext";
13
+ import { useUser } from "../../../contexts/UserContext";
14
+ import { hasPermission } from "../../../util/has_permission";
15
+ import { ProductItemTypeOption, ProductPropertiesData } from "../types/product";
16
+
17
+ import { ProductPropertyField } from "./ProductPropertyField";
18
+
19
+ export interface ProductPropertiesCardProps {
20
+ itemType: ProductItemTypeOption;
21
+ productProperties: ProductPropertiesData;
22
+ setProductProperties: (properties: ProductPropertiesData) => void;
23
+ }
24
+
25
+ export const ProductPropertiesCard: React.FC<ProductPropertiesCardProps> = ({
26
+ itemType,
27
+ productProperties,
28
+ setProductProperties,
29
+ }) => {
30
+ const params = useParams();
31
+ const api = useApi();
32
+ const { t } = useI18n();
33
+ const { user } = useUser();
34
+
35
+ const productPropertiesList = itemType ? itemType.product_properties : [];
36
+
37
+ const productPropertiesRows = productPropertiesList.reduce<(typeof productPropertiesList)[]>(
38
+ (rows, prop, index) => {
39
+ if (index % 2 === 0) rows.push([prop]);
40
+ else rows[rows.length - 1].push(prop);
41
+ return rows;
42
+ },
43
+ [],
44
+ );
45
+
46
+ const handleSubmit = async (values: ProductPropertiesData) => {
47
+ const action = api.operations["pim.product:update"];
48
+ if (!action) {
49
+ throw new Error('Invalid action "pim.product:update".');
50
+ }
51
+
52
+ productPropertiesList.map((prop) => {
53
+ switch (prop.type) {
54
+ case "array": {
55
+ values[prop.name] =
56
+ typeof values[prop.name] === "string"
57
+ ? [values[prop.name] as string]
58
+ : ((values[prop.name] as string[]) ?? []);
59
+ break;
60
+ }
61
+ }
62
+ });
63
+
64
+ const response = await action.call({
65
+ params,
66
+ body: {
67
+ item_type: itemType.name,
68
+ properties: {
69
+ ...values,
70
+ },
71
+ },
72
+ });
73
+
74
+ if (response.ok) {
75
+ const updatedProduct = await response.json();
76
+ setProductProperties(updatedProduct.properties);
77
+ return t("Product properties updated successfully.");
78
+ } else {
79
+ console.error("[PRODUCT_PROPERTIES_CARD]", response);
80
+ throw new Error("updating product properties fields.");
81
+ }
82
+ };
83
+
84
+ return (
85
+ <>
86
+ <Card<ProductPropertiesData>
87
+ columns={2}
88
+ isEditable={hasPermission(user, "pim.change_product")}
89
+ onSubmit={handleSubmit}
90
+ >
91
+ <CardHeader title={t("Properties")} />
92
+
93
+ <CardContent>
94
+ {productPropertiesRows.map((row, index) => (
95
+ <CardRow key={index}>
96
+ {row.map((prop) => (
97
+ <ProductPropertyField
98
+ key={prop.name}
99
+ property={prop}
100
+ value={productProperties[prop.name]}
101
+ />
102
+ ))}
103
+ </CardRow>
104
+ ))}
105
+ </CardContent>
106
+
107
+ <CardActions>
108
+ <CardCancelButton />
109
+ <CardSaveButton />
110
+ </CardActions>
111
+ </Card>
112
+ </>
113
+ );
114
+ };