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.
- package/dist/esm/components/Card/CardFieldAutoComplete.js +2 -2
- package/dist/esm/components/Card/CardFieldAutoComplete.js.map +1 -1
- package/dist/esm/components/Card/CardFieldSelect.js +6 -3
- package/dist/esm/components/Card/CardFieldSelect.js.map +1 -1
- package/dist/esm/components/Card/index.js +17 -1
- package/dist/esm/components/Card/index.js.map +1 -1
- package/dist/esm/components/ContribInlines.js +3 -2
- package/dist/esm/components/ContribInlines.js.map +1 -1
- package/dist/esm/extensions/catalog/components/ArticleCard.js +9 -4
- package/dist/esm/extensions/catalog/components/ArticleCard.js.map +1 -1
- package/dist/esm/extensions/catalog/contrib/ProductArticles.js +48 -0
- package/dist/esm/extensions/catalog/contrib/ProductArticles.js.map +1 -0
- package/dist/esm/extensions/catalog/index.js +9 -0
- package/dist/esm/extensions/catalog/index.js.map +1 -1
- package/dist/esm/extensions/pim/components/ProductCard.js +123 -0
- package/dist/esm/extensions/pim/components/ProductCard.js.map +1 -0
- package/dist/esm/extensions/pim/components/ProductPropertiesCard.js +71 -0
- package/dist/esm/extensions/pim/components/ProductPropertiesCard.js.map +1 -0
- package/dist/esm/extensions/pim/components/ProductPropertyField.js +25 -0
- package/dist/esm/extensions/pim/components/ProductPropertyField.js.map +1 -0
- package/dist/esm/extensions/pim/components/ProductRow.js +11 -0
- package/dist/esm/extensions/pim/components/ProductRow.js.map +1 -0
- package/dist/esm/extensions/pim/contrib/ArticleProduct.js +110 -0
- package/dist/esm/extensions/pim/contrib/ArticleProduct.js.map +1 -0
- package/dist/esm/extensions/pim/index.js +43 -0
- package/dist/esm/extensions/pim/index.js.map +1 -0
- package/dist/esm/extensions/pim/pages/product/create.js +41 -0
- package/dist/esm/extensions/pim/pages/product/create.js.map +1 -0
- package/dist/esm/extensions/pim/pages/product/detail.js +46 -0
- package/dist/esm/extensions/pim/pages/product/detail.js.map +1 -0
- package/dist/esm/extensions/pim/pages/product/list.js +54 -0
- package/dist/esm/extensions/pim/pages/product/list.js.map +1 -0
- package/dist/esm/extensions/pim/types/contrib.js +2 -0
- package/dist/esm/extensions/pim/types/contrib.js.map +1 -0
- package/dist/esm/extensions/pim/types/product.js +2 -0
- package/dist/esm/extensions/pim/types/product.js.map +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/types/components/Card/CardFieldAutoComplete.d.ts +3 -0
- package/dist/types/components/Card/CardFieldSelect.d.ts +1 -0
- package/dist/types/components/ContribInlines.d.ts +1 -0
- package/dist/types/extensions/catalog/components/ArticleCard.d.ts +12 -0
- package/dist/types/extensions/catalog/contrib/ProductArticles.d.ts +4 -0
- package/dist/types/extensions/catalog/types/contrib.d.ts +8 -0
- package/dist/types/extensions/pim/components/ProductCard.d.ts +23 -0
- package/dist/types/extensions/pim/components/ProductPropertiesCard.d.ts +8 -0
- package/dist/types/extensions/pim/components/ProductPropertyField.d.ts +8 -0
- package/dist/types/extensions/pim/components/ProductRow.d.ts +6 -0
- package/dist/types/extensions/pim/contrib/ArticleProduct.d.ts +4 -0
- package/dist/types/extensions/pim/index.d.ts +7 -0
- package/dist/types/extensions/pim/pages/product/create.d.ts +3 -0
- package/dist/types/extensions/pim/pages/product/detail.d.ts +4 -0
- package/dist/types/extensions/pim/pages/product/list.d.ts +4 -0
- package/dist/types/extensions/pim/types/contrib.d.ts +19 -0
- package/dist/types/extensions/pim/types/product.d.ts +35 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/types/index.d.ts +1 -1
- package/package.json +6 -1
- package/src/components/Card/CardFieldAutoComplete.tsx +14 -1
- package/src/components/Card/CardFieldSelect.tsx +6 -2
- package/src/components/Card/index.tsx +21 -1
- package/src/components/ContribInlines.tsx +5 -2
- package/src/extensions/catalog/components/ArticleCard.tsx +22 -4
- package/src/extensions/catalog/contrib/ProductArticles.tsx +82 -0
- package/src/extensions/catalog/index.tsx +9 -0
- package/src/extensions/catalog/types/contrib.ts +9 -0
- package/src/extensions/pim/components/ProductCard.tsx +242 -0
- package/src/extensions/pim/components/ProductPropertiesCard.tsx +114 -0
- package/src/extensions/pim/components/ProductPropertyField.tsx +67 -0
- package/src/extensions/pim/components/ProductRow.tsx +23 -0
- package/src/extensions/pim/contrib/ArticleProduct.tsx +215 -0
- package/src/extensions/pim/index.tsx +67 -0
- package/src/extensions/pim/pages/product/create.tsx +63 -0
- package/src/extensions/pim/pages/product/detail.tsx +85 -0
- package/src/extensions/pim/pages/product/list.tsx +87 -0
- package/src/extensions/pim/types/contrib.ts +21 -0
- package/src/extensions/pim/types/product.ts +52 -0
- package/src/index.ts +1 -0
- 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
|
+
}
|