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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bananas-commerce-admin",
|
|
3
|
-
"version": "0.
|
|
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) =>
|
|
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
|
-
|
|
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 =
|
|
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) =>
|
|
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:
|
|
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:
|
|
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:
|
|
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;
|
|
@@ -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
|
+
};
|