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
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+
3
+ import CardFieldAutoComplete from "../../../components/Card/CardFieldAutoComplete";
4
+ import CardFieldNumber from "../../../components/Card/CardFieldNumber";
5
+ import CardFieldText from "../../../components/Card/CardFieldText";
6
+ import { ProductProperty, ProductPropertyValue } from "../types/product";
7
+
8
+ export interface ProductPropertyFieldProps {
9
+ property: ProductProperty;
10
+ value: ProductPropertyValue;
11
+ formName?: string;
12
+ }
13
+
14
+ export const ProductPropertyField: React.FC<ProductPropertyFieldProps> = ({
15
+ property,
16
+ value,
17
+ formName,
18
+ }) => {
19
+ switch (property.type) {
20
+ case "string": {
21
+ const currentValue = value as string;
22
+ return (
23
+ <CardFieldText
24
+ formName={formName ?? property.name}
25
+ label={property.title}
26
+ required={property.min_length ? true : false}
27
+ value={currentValue}
28
+ />
29
+ );
30
+ }
31
+
32
+ case "integer": {
33
+ const currentValue = parseInt(value as string);
34
+ return (
35
+ <CardFieldNumber
36
+ formName={formName ?? property.name}
37
+ label={property.title}
38
+ max={property.max_value}
39
+ min={property.min_value}
40
+ required={property.min_value !== undefined ? true : false}
41
+ value={Number.isNaN(currentValue) ? undefined : currentValue}
42
+ />
43
+ );
44
+ }
45
+
46
+ case "array": {
47
+ const currentValue = value as string[];
48
+
49
+ return (
50
+ <CardFieldAutoComplete
51
+ autoSelect
52
+ clearOnBlur
53
+ freeSolo
54
+ fallback={"—"}
55
+ formName={formName ?? property.name}
56
+ label={property.title}
57
+ value={(currentValue ?? []).map((val: ProductPropertyValue) => ({
58
+ id: val as string,
59
+ label: val as string,
60
+ }))}
61
+ />
62
+ );
63
+ }
64
+ }
65
+
66
+ return <></>;
67
+ };
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+
3
+ import CancelIcon from "@mui/icons-material/Cancel";
4
+ import CheckCircleIcon from "@mui/icons-material/CheckCircle";
5
+
6
+ import { TableCell } from "../../../components/Table/TableCell";
7
+ import { NavigatingTableRow } from "../../../components/Table/TableRow";
8
+ import { ProductList } from "../types/product";
9
+
10
+ export interface ProductRowProps {
11
+ product: ProductList;
12
+ }
13
+
14
+ export const ProductRow: React.FC<ProductRowProps> = ({ product }) => (
15
+ <NavigatingTableRow route="pim.product:detail" routeParams={{ id: product.id }}>
16
+ <TableCell>{product.name}</TableCell>
17
+ <TableCell>{product.item_type}</TableCell>
18
+ <TableCell>{product.number}</TableCell>
19
+ <TableCell align="right">
20
+ {product.is_active ? <CheckCircleIcon color="success" /> : <CancelIcon color="error" />}
21
+ </TableCell>
22
+ </NavigatingTableRow>
23
+ );
@@ -0,0 +1,215 @@
1
+ import React, { useEffect, useState } from "react";
2
+
3
+ import { TableBody, TableHead, TableRow } from "@mui/material";
4
+ import Box from "@mui/material/Box";
5
+ import ImageList from "@mui/material/ImageList";
6
+ import ImageListItem from "@mui/material/ImageListItem";
7
+ import ImageListItemBar from "@mui/material/ImageListItemBar";
8
+ import Modal from "@mui/material/Modal";
9
+
10
+ import Card from "../../../components/Card";
11
+ import CardContent from "../../../components/Card/CardContent";
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 Table from "../../../components/Table";
17
+ import { TableCell } from "../../../components/Table/TableCell";
18
+ import TableHeading from "../../../components/Table/TableHeading";
19
+ import { useApi } from "../../../contexts/ApiContext";
20
+ import { useI18n } from "../../../contexts/I18nContext";
21
+ import { ContribComponent } from "../../../types";
22
+ import { ProductPropertyField } from "../components/ProductPropertyField";
23
+ import { ArticleAsset, ArticleProductResponse } from "../types/contrib";
24
+ import { ProductItemTypeOption, ProductPropertiesData } from "../types/product";
25
+
26
+ const ArticleProductCard: ContribComponent<ArticleProductResponse> = ({ data, params }) => {
27
+ const { t } = useI18n();
28
+ const api = useApi();
29
+ const [selectedAsset, setSelectedAsset] = useState<ArticleAsset | null>(null);
30
+ const [productItemTypes, setProductItemTypes] = useState<ProductItemTypeOption[]>([]);
31
+
32
+ const { code: articleCode } = params as { code: string };
33
+ const { product, assets } = data;
34
+
35
+ const articleAssets = assets.filter(
36
+ (asset) => asset.article_code == articleCode || asset.article_code == "",
37
+ );
38
+ const imageAssets = articleAssets.filter((asset) => asset.asset_type == "image");
39
+ const otherAssets = articleAssets.filter((asset) => asset.asset_type !== "image");
40
+
41
+ const productProperties = product.properties as ProductPropertiesData;
42
+ const productItemType = productItemTypes.find((itemType) => itemType.name == product.item_type);
43
+ const productPropertiesList = productItemType ? productItemType.product_properties : [];
44
+ const productPropertiesRows = productPropertiesList.reduce<(typeof productPropertiesList)[]>(
45
+ (rows, prop, index) => {
46
+ if (index % 2 === 0) rows.push([prop]);
47
+ else rows[rows.length - 1].push(prop);
48
+ return rows;
49
+ },
50
+ [],
51
+ );
52
+
53
+ useEffect(() => {
54
+ api.operations["pim.item_types:options"].call({}).then(async (response) => {
55
+ if (response.ok) {
56
+ setProductItemTypes(await response.json());
57
+ }
58
+ });
59
+ }, []);
60
+
61
+ return (
62
+ <Card columns={2} isEditable={false}>
63
+ <CardHeader title="Product" />
64
+
65
+ <CardContent>
66
+ <CardRow>
67
+ <CardFieldText formName="name" label={t("Name")} required={true} value={product.name} />
68
+ <CardFieldSelect
69
+ formName="item_type"
70
+ label={t("Type")}
71
+ options={productItemTypes.map((itemType) => ({
72
+ id: itemType.name,
73
+ label: itemType.name,
74
+ }))}
75
+ required={true}
76
+ size={1}
77
+ value={
78
+ product.item_type ? { id: product.item_type, label: product.item_type } : undefined
79
+ }
80
+ />
81
+ </CardRow>
82
+ <CardRow>
83
+ <CardFieldText
84
+ formName="description"
85
+ label={t("Description")}
86
+ required={true}
87
+ value={product.description}
88
+ />
89
+ </CardRow>
90
+ </CardContent>
91
+
92
+ {productPropertiesRows.length > 0 && (
93
+ <>
94
+ <CardHeader title={t("Properties")} />
95
+
96
+ <CardContent>
97
+ {productPropertiesRows.map((row, index) => (
98
+ <CardRow key={index}>
99
+ {row.map((prop) => (
100
+ <ProductPropertyField
101
+ key={prop.name}
102
+ property={prop}
103
+ value={productProperties[prop.name]}
104
+ />
105
+ ))}
106
+ </CardRow>
107
+ ))}
108
+ </CardContent>
109
+ </>
110
+ )}
111
+
112
+ {articleAssets.length > 0 && <CardHeader title={t("Assets")} />}
113
+
114
+ {imageAssets.length > 0 && (
115
+ <>
116
+ <Modal
117
+ aria-describedby="modal-modal-description"
118
+ aria-labelledby="modal-modal-title"
119
+ open={selectedAsset !== null}
120
+ onClose={() => setSelectedAsset(null)}
121
+ >
122
+ <Box
123
+ sx={{
124
+ position: "absolute",
125
+ top: "50%",
126
+ left: "50%",
127
+ transform: "translate(-50%, -50%)",
128
+ width: "100vw", // Set explicit width
129
+ height: "100vh", // Set explicit height
130
+ bgcolor: "rgba(0, 0, 0, 0.7)", // Dark overlay background
131
+ boxShadow: 24,
132
+ p: 2, // Reduced padding
133
+ display: "flex",
134
+ justifyContent: "center",
135
+ alignItems: "center",
136
+ overflow: "hidden", // Hide any overflowing content
137
+ outline: "none",
138
+ }}
139
+ onClick={() => setSelectedAsset(null)}
140
+ >
141
+ {selectedAsset !== null && (
142
+ <img
143
+ alt={selectedAsset.caption}
144
+ loading="lazy"
145
+ src={selectedAsset.url}
146
+ style={{
147
+ maxWidth: "100%",
148
+ maxHeight: "100%",
149
+ objectFit: "contain",
150
+ borderRadius: "8px", // Subtle rounded corners for the image
151
+ }}
152
+ />
153
+ )}
154
+ </Box>
155
+ </Modal>
156
+
157
+ <CardContent
158
+ sx={{
159
+ width: "100%",
160
+ height: 400,
161
+ overflowY: "scroll",
162
+ paddingTop: 0,
163
+ paddingBottom: 0,
164
+ paddingLeft: 3,
165
+ paddingRight: 3,
166
+ }}
167
+ >
168
+ <ImageList cols={3} gap={8} variant="masonry">
169
+ {imageAssets.map((asset) => (
170
+ <ImageListItem key={asset.id} onClick={() => setSelectedAsset(asset)}>
171
+ <img alt={asset.caption} loading="lazy" src={asset.url} />
172
+ <ImageListItemBar subtitle={asset.caption} />
173
+ </ImageListItem>
174
+ ))}
175
+ </ImageList>
176
+ </CardContent>
177
+ </>
178
+ )}
179
+
180
+ {otherAssets.length > 0 && (
181
+ <Table count={otherAssets.length}>
182
+ <TableHead>
183
+ <TableRow>
184
+ <TableHeading>{t("Asset ID")}</TableHeading>
185
+ <TableHeading align="right">{t("Type")}</TableHeading>
186
+ <TableHeading align="right">{t("Caption")}</TableHeading>
187
+ </TableRow>
188
+ </TableHead>
189
+
190
+ <TableBody
191
+ sx={{ ".MuiTableRow-root:last-child > .MuiTableCell-root": { borderBottom: "none" } }}
192
+ >
193
+ {otherAssets.map((asset) => (
194
+ <TableRow key={asset.id} sx={{ height: 56 }}>
195
+ <TableCell sx={{ py: 0 }}>
196
+ <a href={asset.url} rel="noreferrer" target="_blank">
197
+ {asset.id}
198
+ </a>
199
+ </TableCell>
200
+ <TableCell align="right" sx={{ py: 0 }}>
201
+ {asset.asset_type}
202
+ </TableCell>
203
+ <TableCell align="right" sx={{ py: 0 }}>
204
+ {asset.caption}
205
+ </TableCell>
206
+ </TableRow>
207
+ ))}
208
+ </TableBody>
209
+ </Table>
210
+ )}
211
+ </Card>
212
+ );
213
+ };
214
+
215
+ export default ArticleProductCard;
@@ -0,0 +1,67 @@
1
+ import InventoryOutlinedIcon from "@mui/icons-material/InventoryOutlined";
2
+
3
+ import { OpenAPI } from "openapi-types";
4
+
5
+ import WorkspacesIcon from "../../assets/symbols/Workspaces";
6
+ import { ArticleDetail } from "../../extensions/catalog";
7
+ import { RouterExtension } from "../../router/Router";
8
+ import { ContribComponentMap, NavigationOverrides, PageComponent } from "../../types";
9
+
10
+ export * from "./types/contrib";
11
+ export * from "./types/product";
12
+
13
+ const routes: Record<
14
+ string,
15
+ Record<
16
+ string,
17
+ {
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ page: () => PageComponent<any> | Promise<PageComponent<any>>;
20
+ request?: OpenAPI.Request;
21
+ defaultRequest?: OpenAPI.Request;
22
+ offline?: boolean;
23
+ }
24
+ >
25
+ > = {
26
+ product: {
27
+ create: { page: async () => (await import("./pages/product/create")).default, offline: true },
28
+ detail: { page: async () => (await import("./pages/product/detail")).default },
29
+ list: { page: async () => (await import("./pages/product/list")).default },
30
+ },
31
+ };
32
+
33
+ export const router: RouterExtension = {
34
+ app: "pim",
35
+ pages: (route) => {
36
+ const { page, ...hit } = routes[route.view]?.[route.action] ?? {};
37
+
38
+ if (page != null) {
39
+ return {
40
+ page: page(),
41
+ ...hit,
42
+ };
43
+ }
44
+
45
+ return undefined;
46
+ },
47
+ };
48
+
49
+ export const navigation: NavigationOverrides = {
50
+ "pim.product:list": {
51
+ icon: WorkspacesIcon,
52
+ permission: "pim.view_product",
53
+ },
54
+ } as const;
55
+
56
+ export const contrib: Record<string, ContribComponentMap> = {
57
+ catalog: {
58
+ "catalog:article:detail:product": {
59
+ title: "Product",
60
+ icon: InventoryOutlinedIcon,
61
+ component: async () => (await import("./contrib/ArticleProduct")).default,
62
+ predicate: (article: ArticleDetail) => !!article.product_number,
63
+ variant: "inline",
64
+ permission: "pim.view_product",
65
+ },
66
+ },
67
+ } as const;
@@ -0,0 +1,63 @@
1
+ import React, { useEffect, useState } from "react";
2
+
3
+ import CategoryOutlinedIcon from "@mui/icons-material/CategoryOutlined";
4
+
5
+ import { Header } from "../../../../components/Header";
6
+ import { Page } from "../../../../components/Page";
7
+ import { Tab } from "../../../../components/Tab";
8
+ import { TabPanel } from "../../../../components/TabPanel";
9
+ import { TabPanels } from "../../../../components/TabPanels";
10
+ import { Tabs } from "../../../../components/Tabs";
11
+ import { TitleBar } from "../../../../components/TitleBar";
12
+ import Content, { LeftColumn } from "../../../../containers/Content";
13
+ import { useApi } from "../../../../contexts/ApiContext";
14
+ import { useRouter } from "../../../../contexts/RouterContext";
15
+ import { PageComponent } from "../../../../types";
16
+ import { ProductCard } from "../../components/ProductCard";
17
+ import { ProductItemTypeOption } from "../../types/product";
18
+
19
+ const ProductCreatePage: PageComponent = () => {
20
+ const { navigate } = useRouter();
21
+ const api = useApi();
22
+
23
+ const [productItemTypes, setProductItemTypes] = useState<ProductItemTypeOption[]>([]);
24
+
25
+ useEffect(() => {
26
+ api.operations["pim.item_types:options"].call({}).then(async (response) => {
27
+ if (response.ok) {
28
+ setProductItemTypes(await response.json());
29
+ }
30
+ });
31
+ }, []);
32
+
33
+ return (
34
+ <Page>
35
+ <Header variant="opaque">
36
+ <TitleBar title="Create Product" />
37
+ <Tabs>
38
+ <Tab key="default" icon={<CategoryOutlinedIcon />} label="Product" value="default" />
39
+ </Tabs>
40
+ </Header>
41
+
42
+ <TabPanels>
43
+ <TabPanel key="default" value="default">
44
+ <Content layout="fixedWidth">
45
+ <LeftColumn>
46
+ <ProductCard
47
+ create
48
+ itemTypes={productItemTypes}
49
+ onCreated={(product) => {
50
+ navigate(`pim.product:detail`, {
51
+ params: { id: product.id },
52
+ });
53
+ }}
54
+ />
55
+ </LeftColumn>
56
+ </Content>
57
+ </TabPanel>
58
+ </TabPanels>
59
+ </Page>
60
+ );
61
+ };
62
+
63
+ export default ProductCreatePage;
@@ -0,0 +1,85 @@
1
+ import React, { useEffect, useState } from "react";
2
+
3
+ import CategoryOutlinedIcon from "@mui/icons-material/CategoryOutlined";
4
+ import Typography from "@mui/material/Typography";
5
+
6
+ import { ContribInlines } from "../../../../components/ContribInlines";
7
+ import { Header } from "../../../../components/Header";
8
+ import { Page } from "../../../../components/Page";
9
+ import { Tab } from "../../../../components/Tab";
10
+ import { TabPanel } from "../../../../components/TabPanel";
11
+ import { TabPanels } from "../../../../components/TabPanels";
12
+ import { Tabs } from "../../../../components/Tabs";
13
+ import { TitleBar } from "../../../../components/TitleBar";
14
+ import Content, { LeftColumn, RightColumn } from "../../../../containers/Content";
15
+ import { useApi } from "../../../../contexts/ApiContext";
16
+ import { PageComponent } from "../../../../types";
17
+ import { ProductCard } from "../../components/ProductCard";
18
+ import { ProductPropertiesCard } from "../../components/ProductPropertiesCard";
19
+ import { ProductDetail, ProductItemTypeOption } from "../../types/product";
20
+
21
+ const ProductDetailPage: PageComponent<ProductDetail> = ({ data }) => {
22
+ const [product, setProduct] = useState(data);
23
+ const [productItemTypes, setProductItemTypes] = useState<ProductItemTypeOption[]>([]);
24
+ const api = useApi();
25
+
26
+ useEffect(() => {
27
+ api.operations["pim.item_types:options"].call({}).then(async (response) => {
28
+ if (response.ok) {
29
+ setProductItemTypes(await response.json());
30
+ }
31
+ });
32
+ }, []);
33
+
34
+ const productItemType = productItemTypes.find((itemType) => itemType.name == product.item_type);
35
+
36
+ return (
37
+ <Page>
38
+ <Header variant="opaque">
39
+ <TitleBar
40
+ title={
41
+ <>
42
+ {data.name}
43
+ <Typography sx={{ pl: 1 }} variant="button">
44
+ {data.number}
45
+ </Typography>
46
+ </>
47
+ }
48
+ />
49
+ <Tabs data={product}>
50
+ <Tab key="default" icon={<CategoryOutlinedIcon />} label="Product" value="default" />
51
+ </Tabs>
52
+ </Header>
53
+
54
+ <TabPanels contribParams={{ number: data.number }}>
55
+ <TabPanel key="default" value="default">
56
+ <Content layout="fixedWidth">
57
+ <LeftColumn>
58
+ <ProductCard itemTypes={productItemTypes} product={product} onUpdated={setProduct} />
59
+
60
+ {productItemType && (
61
+ <ProductPropertiesCard
62
+ itemType={productItemType}
63
+ productProperties={product.properties}
64
+ setProductProperties={(properties) =>
65
+ setProduct((previous) => ({ ...previous, properties }))
66
+ }
67
+ />
68
+ )}
69
+ <ContribInlines contribParams={{ number: data.number }} data={data} />
70
+ </LeftColumn>
71
+ <RightColumn>
72
+ <ContribInlines
73
+ contribParams={{ number: data.number }}
74
+ data={data}
75
+ variant="sidebar"
76
+ />
77
+ </RightColumn>
78
+ </Content>
79
+ </TabPanel>
80
+ </TabPanels>
81
+ </Page>
82
+ );
83
+ };
84
+
85
+ export default ProductDetailPage;
@@ -0,0 +1,87 @@
1
+ import React from "react";
2
+
3
+ import { Button, TableBody } from "@mui/material";
4
+
5
+ import ActionBar from "../../../../components/ActionBar";
6
+ import { Header } from "../../../../components/Header";
7
+ import { Page } from "../../../../components/Page";
8
+ import SearchBar from "../../../../components/SearchBar";
9
+ import Table from "../../../../components/Table";
10
+ import TableHead from "../../../../components/Table/TableHead";
11
+ import TableHeading from "../../../../components/Table/TableHeading";
12
+ import TableCard from "../../../../components/TableCard";
13
+ import { TitleBar } from "../../../../components/TitleBar";
14
+ import Content, { ContentWrapperWithActionBar } from "../../../../containers/Content";
15
+ import { useI18n } from "../../../../contexts/I18nContext";
16
+ import { useRouter } from "../../../../contexts/RouterContext";
17
+ import { useUser } from "../../../../contexts/UserContext";
18
+ import { LimitOffset, PageComponent } from "../../../../types";
19
+ import { hasPermission } from "../../../../util/has_permission";
20
+ import { ProductRow } from "../../components/ProductRow";
21
+ import { ProductList } from "../../types/product";
22
+
23
+ const ProductListPage: PageComponent<LimitOffset<ProductList>> = ({ data }) => {
24
+ const { navigate } = useRouter();
25
+ const { user } = useUser();
26
+ const { t } = useI18n();
27
+
28
+ return (
29
+ <Page>
30
+ <Header>
31
+ <TitleBar title={t("Products")}>
32
+ <SearchBar
33
+ defaultValue={new URLSearchParams(window.location.search).get("search") ?? ""}
34
+ placeholder={t("Search for product number or name")}
35
+ onSubmit={(input) => {
36
+ if (input === "") {
37
+ navigate("pim.product:list", {
38
+ replace: true,
39
+ });
40
+ } else {
41
+ navigate("pim.product:list", {
42
+ query: {
43
+ search: input,
44
+ },
45
+ replace: true,
46
+ });
47
+ }
48
+ }}
49
+ />
50
+ </TitleBar>
51
+ </Header>
52
+
53
+ <ContentWrapperWithActionBar>
54
+ <Content layout="fullWidth">
55
+ <TableCard>
56
+ <Table pagination count={data?.count}>
57
+ <TableHead>
58
+ <TableHeading>{t("Name")}</TableHeading>
59
+ <TableHeading>{t("Type")}</TableHeading>
60
+ <TableHeading>{t("Product Number")}</TableHeading>
61
+ <TableHeading align="right">{t("Active")}</TableHeading>
62
+ </TableHead>
63
+
64
+ <TableBody>
65
+ {data?.results.map((product) => <ProductRow key={product.id} product={product} />)}
66
+ </TableBody>
67
+ </Table>
68
+ </TableCard>
69
+ </Content>
70
+
71
+ {hasPermission(user, "pim.add_product") && (
72
+ <ActionBar>
73
+ <Button
74
+ color="primary"
75
+ variant="contained"
76
+ onClick={() => navigate("pim.product:create")}
77
+ >
78
+ {t("Create product")}
79
+ </Button>
80
+ </ActionBar>
81
+ )}
82
+ </ContentWrapperWithActionBar>
83
+ </Page>
84
+ );
85
+ };
86
+
87
+ export default ProductListPage;
@@ -0,0 +1,21 @@
1
+ export interface ArticleProduct {
2
+ number: string;
3
+ item_type: string;
4
+ name: string;
5
+ description: string;
6
+ properties: object;
7
+ is_active: boolean;
8
+ }
9
+
10
+ export interface ArticleAsset {
11
+ id: string;
12
+ asset_type: string;
13
+ caption: string;
14
+ article_code: string;
15
+ url: string;
16
+ }
17
+
18
+ export interface ArticleProductResponse {
19
+ product: ArticleProduct;
20
+ assets: ArticleAsset[];
21
+ }
@@ -0,0 +1,52 @@
1
+ export interface ProductList {
2
+ id: number;
3
+ number: string;
4
+ item_type: string;
5
+ name: string;
6
+ description: string;
7
+ is_active: boolean;
8
+ }
9
+
10
+ export type ProductPropertyValue =
11
+ | string
12
+ | number
13
+ | boolean
14
+ | string[]
15
+ | number[]
16
+ | boolean[]
17
+ | null
18
+ | undefined;
19
+ export type ProductPropertiesData = Record<
20
+ string,
21
+ string | number | boolean | string[] | number[] | boolean[]
22
+ >;
23
+
24
+ export interface ProductDetail {
25
+ id: number;
26
+ number: string;
27
+ item_type: string;
28
+ name: string;
29
+ description: string;
30
+ is_active: boolean;
31
+ properties: ProductPropertiesData;
32
+ }
33
+
34
+ export interface ProductCreated {
35
+ id: number;
36
+ }
37
+
38
+ export interface ProductProperty {
39
+ name: string;
40
+ title: string;
41
+ type: string;
42
+
43
+ min_length?: number;
44
+ min_value?: number;
45
+ max_value?: number;
46
+ array_type?: string;
47
+ }
48
+
49
+ export interface ProductItemTypeOption {
50
+ name: string;
51
+ product_properties: ProductProperty[];
52
+ }