bananas-commerce-admin 0.21.2 → 0.22.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 (44) hide show
  1. package/dist/esm/Admin.js +35 -9
  2. package/dist/esm/Admin.js.map +1 -1
  3. package/dist/esm/extensions/pim/components/ProductRow.js +16 -0
  4. package/dist/esm/extensions/pim/components/ProductRow.js.map +1 -1
  5. package/dist/esm/extensions/pim/pages/product/list.js +1 -0
  6. package/dist/esm/extensions/pim/pages/product/list.js.map +1 -1
  7. package/dist/esm/extensions/user/components/InviteUserModal.js +106 -0
  8. package/dist/esm/extensions/user/components/InviteUserModal.js.map +1 -0
  9. package/dist/esm/extensions/user/index.js +30 -0
  10. package/dist/esm/extensions/user/index.js.map +1 -0
  11. package/dist/esm/extensions/user/pages/staff/detail.js +143 -0
  12. package/dist/esm/extensions/user/pages/staff/detail.js.map +1 -0
  13. package/dist/esm/extensions/user/pages/staff/list.js +61 -0
  14. package/dist/esm/extensions/user/pages/staff/list.js.map +1 -0
  15. package/dist/esm/extensions/user/types/staff.js +2 -0
  16. package/dist/esm/extensions/user/types/staff.js.map +1 -0
  17. package/dist/esm/index.js +1 -0
  18. package/dist/esm/index.js.map +1 -1
  19. package/dist/esm/pages/AcceptInvitePage.js +125 -0
  20. package/dist/esm/pages/AcceptInvitePage.js.map +1 -0
  21. package/dist/esm/pages/ResetPasswordPage.js +125 -0
  22. package/dist/esm/pages/ResetPasswordPage.js.map +1 -0
  23. package/dist/types/extensions/pim/types/product.d.ts +1 -0
  24. package/dist/types/extensions/user/components/InviteUserModal.d.ts +9 -0
  25. package/dist/types/extensions/user/index.d.ts +4 -0
  26. package/dist/types/extensions/user/pages/staff/detail.d.ts +4 -0
  27. package/dist/types/extensions/user/pages/staff/list.d.ts +4 -0
  28. package/dist/types/extensions/user/types/staff.d.ts +24 -0
  29. package/dist/types/index.d.ts +1 -0
  30. package/dist/types/pages/AcceptInvitePage.d.ts +9 -0
  31. package/dist/types/pages/ResetPasswordPage.d.ts +9 -0
  32. package/package.json +1 -1
  33. package/src/Admin.tsx +53 -26
  34. package/src/extensions/pim/components/ProductRow.tsx +32 -0
  35. package/src/extensions/pim/pages/product/list.tsx +1 -0
  36. package/src/extensions/pim/types/product.ts +1 -0
  37. package/src/extensions/user/components/InviteUserModal.tsx +179 -0
  38. package/src/extensions/user/index.tsx +51 -0
  39. package/src/extensions/user/pages/staff/detail.tsx +262 -0
  40. package/src/extensions/user/pages/staff/list.tsx +100 -0
  41. package/src/extensions/user/types/staff.ts +27 -0
  42. package/src/index.ts +1 -0
  43. package/src/pages/AcceptInvitePage.tsx +232 -0
  44. package/src/pages/ResetPasswordPage.tsx +236 -0
@@ -0,0 +1,232 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { useSearchParams } from "react-router-dom";
3
+
4
+ import LoadingButton from "@mui/lab/LoadingButton";
5
+ import {
6
+ Alert,
7
+ Backdrop,
8
+ Box,
9
+ Dialog,
10
+ DialogActions,
11
+ DialogContent,
12
+ DialogTitle,
13
+ Stack,
14
+ TextField,
15
+ Typography,
16
+ } from "@mui/material";
17
+ import { styled } from "@mui/material/styles";
18
+
19
+ import { useSnackbar } from "notistack";
20
+
21
+ import Brand from "../components/Brand";
22
+ import Content from "../containers/Content";
23
+ import { useApi } from "../contexts/ApiContext";
24
+ import { useI18n } from "../contexts/I18nContext";
25
+ import { LogoType } from "../types";
26
+
27
+ export interface AcceptInvitePageProps {
28
+ title?: string;
29
+ logo?: LogoType;
30
+ logoHeight?: number | string;
31
+ }
32
+
33
+ const StyledDialog = styled(Dialog, {
34
+ name: "BananasAcceptInviteDialog",
35
+ slot: "Root",
36
+ })(() => ({}));
37
+
38
+ const StyledDialogTitle = styled(DialogTitle, {
39
+ name: "BananasAcceptInviteDialog",
40
+ slot: "Title",
41
+ })(({ theme }) =>
42
+ theme.unstable_sx({
43
+ bgcolor: "primary.main",
44
+ color: "primary.contrastText",
45
+ }),
46
+ );
47
+
48
+ const StyledBackdrop = styled(Backdrop, {
49
+ name: "BananasAcceptInviteDialog",
50
+ slot: "Backdrop",
51
+ })(({ theme }) =>
52
+ theme.unstable_sx({
53
+ bgcolor: "primary.dark",
54
+ m: 0,
55
+ p: 2,
56
+ textAlign: "center",
57
+ alignItems: "middle",
58
+ justifyContent: "center",
59
+ display: "flex",
60
+ }),
61
+ );
62
+
63
+ export const AcceptInvitePage: React.FC<AcceptInvitePageProps> = ({ title, logo, logoHeight }) => {
64
+ const { enqueueSnackbar } = useSnackbar();
65
+ const { t } = useI18n();
66
+ const api = useApi();
67
+ const [searchParams] = useSearchParams();
68
+
69
+ const [password, setPassword] = useState("");
70
+ const [confirmPassword, setConfirmPassword] = useState("");
71
+ const [loading, setLoading] = useState(false);
72
+ const [error, setError] = useState("");
73
+ const [tokenError, setTokenError] = useState("");
74
+
75
+ const token = searchParams.get("token");
76
+
77
+ useEffect(() => {
78
+ if (!token) {
79
+ setTokenError(t("No invite token provided"));
80
+ }
81
+ }, [token, t]);
82
+
83
+ const handleSubmit = async (event: React.FormEvent) => {
84
+ event.preventDefault();
85
+ setError("");
86
+
87
+ if (!token) {
88
+ setTokenError(t("No invite token provided"));
89
+ return;
90
+ }
91
+
92
+ if (password.length < 8) {
93
+ setError(t("Password must be at least 8 characters long"));
94
+ return;
95
+ }
96
+
97
+ if (password !== confirmPassword) {
98
+ setError(t("Passwords do not match"));
99
+ return;
100
+ }
101
+
102
+ setLoading(true);
103
+
104
+ try {
105
+ const response = await api?.operations["user.staff:invite-accept"].call({
106
+ body: {
107
+ token,
108
+ password,
109
+ },
110
+ });
111
+
112
+ if (response?.ok) {
113
+ const data = await response.json();
114
+ enqueueSnackbar(data.message || t("User created successfully!"), {
115
+ variant: "success",
116
+ });
117
+
118
+ // User is already logged in on the backend, redirect to dashboard
119
+ // Use full page navigation to reinitialize the UserContext
120
+ window.location.href = "/";
121
+ } else {
122
+ const errorData = await response?.json();
123
+ if (errorData?.detail && Array.isArray(errorData.detail)) {
124
+ const tokenErrors = errorData.detail.filter((err: { loc?: string[]; msg?: string }) =>
125
+ err.loc?.includes("token"),
126
+ );
127
+ if (tokenErrors.length > 0) {
128
+ setTokenError(tokenErrors[0].msg);
129
+ } else {
130
+ setError(errorData.detail[0]?.msg || t("Failed to accept invite"));
131
+ }
132
+ } else {
133
+ setError(t("Failed to accept invite. Please try again."));
134
+ }
135
+ }
136
+ } catch (error) {
137
+ console.error("[ACCEPT_INVITE]", error);
138
+ setError(t("An unexpected error occurred. Please try again."));
139
+ } finally {
140
+ setLoading(false);
141
+ }
142
+ };
143
+
144
+ return (
145
+ <Content layout="fullWidth">
146
+ <StyledDialog
147
+ open
148
+ BackdropComponent={StyledBackdrop}
149
+ PaperProps={{ elevation: 1 }}
150
+ sx={{ "> * > *": { width: "100%" } }}
151
+ >
152
+ <StyledDialogTitle>
153
+ {logo ? (
154
+ <Brand
155
+ LogoProps={{ style: { width: "auto", height: logoHeight ?? "24px !important" } }}
156
+ src={logo}
157
+ />
158
+ ) : (
159
+ <Typography sx={{ fontWeight: "bold", color: "inherit" }}>{title}</Typography>
160
+ )}
161
+ </StyledDialogTitle>
162
+
163
+ <Box component="form" onSubmit={handleSubmit}>
164
+ <DialogContent>
165
+ <Stack spacing={2}>
166
+ <Typography variant="h6">{t("Accept Staff Invite")}</Typography>
167
+ <Typography color="text.secondary" variant="body2">
168
+ {t("Set your password to complete your account setup.")}
169
+ </Typography>
170
+
171
+ {tokenError && <Alert severity="error">{tokenError}</Alert>}
172
+
173
+ {error && !tokenError && <Alert severity="error">{error}</Alert>}
174
+
175
+ {!tokenError && (
176
+ <>
177
+ <TextField
178
+ fullWidth
179
+ required
180
+ color="secondary"
181
+ disabled={loading}
182
+ inputProps={{ "aria-label": "Password", minLength: 8 }}
183
+ label={t("Password")}
184
+ name="password"
185
+ type="password"
186
+ value={password}
187
+ onChange={(e) => setPassword(e.target.value)}
188
+ />
189
+ <TextField
190
+ fullWidth
191
+ required
192
+ color="secondary"
193
+ disabled={loading}
194
+ inputProps={{ "aria-label": "Confirm Password", minLength: 8 }}
195
+ label={t("Confirm Password")}
196
+ name="confirmPassword"
197
+ type="password"
198
+ value={confirmPassword}
199
+ onChange={(e) => setConfirmPassword(e.target.value)}
200
+ />
201
+ </>
202
+ )}
203
+ </Stack>
204
+ </DialogContent>
205
+
206
+ <DialogActions
207
+ sx={{
208
+ pt: 0,
209
+ pb: 2,
210
+ }}
211
+ >
212
+ {!tokenError && (
213
+ <LoadingButton
214
+ aria-label="accept-invite"
215
+ color="primary"
216
+ disabled={!token}
217
+ loading={loading}
218
+ sx={{ margin: "auto" }}
219
+ type="submit"
220
+ variant="contained"
221
+ >
222
+ {t("Create Account")}
223
+ </LoadingButton>
224
+ )}
225
+ </DialogActions>
226
+ </Box>
227
+ </StyledDialog>
228
+ </Content>
229
+ );
230
+ };
231
+
232
+ export default AcceptInvitePage;
@@ -0,0 +1,236 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { useSearchParams } from "react-router-dom";
3
+
4
+ import LoadingButton from "@mui/lab/LoadingButton";
5
+ import {
6
+ Alert,
7
+ Backdrop,
8
+ Box,
9
+ Dialog,
10
+ DialogActions,
11
+ DialogContent,
12
+ DialogTitle,
13
+ Stack,
14
+ TextField,
15
+ Typography,
16
+ } from "@mui/material";
17
+ import { styled } from "@mui/material/styles";
18
+
19
+ import { useSnackbar } from "notistack";
20
+
21
+ import Brand from "../components/Brand";
22
+ import Content from "../containers/Content";
23
+ import { useApi } from "../contexts/ApiContext";
24
+ import { useI18n } from "../contexts/I18nContext";
25
+ import { LogoType } from "../types";
26
+
27
+ export interface ResetPasswordPageProps {
28
+ title?: string;
29
+ logo?: LogoType;
30
+ logoHeight?: number | string;
31
+ }
32
+
33
+ const StyledDialog = styled(Dialog, {
34
+ name: "BananasResetPasswordDialog",
35
+ slot: "Root",
36
+ })(() => ({}));
37
+
38
+ const StyledDialogTitle = styled(DialogTitle, {
39
+ name: "BananasResetPasswordDialog",
40
+ slot: "Title",
41
+ })(({ theme }) =>
42
+ theme.unstable_sx({
43
+ bgcolor: "primary.main",
44
+ color: "primary.contrastText",
45
+ }),
46
+ );
47
+
48
+ const StyledBackdrop = styled(Backdrop, {
49
+ name: "BananasResetPasswordDialog",
50
+ slot: "Backdrop",
51
+ })(({ theme }) =>
52
+ theme.unstable_sx({
53
+ bgcolor: "primary.dark",
54
+ m: 0,
55
+ p: 2,
56
+ textAlign: "center",
57
+ alignItems: "middle",
58
+ justifyContent: "center",
59
+ display: "flex",
60
+ }),
61
+ );
62
+
63
+ export const ResetPasswordPage: React.FC<ResetPasswordPageProps> = ({
64
+ title,
65
+ logo,
66
+ logoHeight,
67
+ }) => {
68
+ const { enqueueSnackbar } = useSnackbar();
69
+ const { t } = useI18n();
70
+ const api = useApi();
71
+ const [searchParams] = useSearchParams();
72
+
73
+ const [password, setPassword] = useState("");
74
+ const [confirmPassword, setConfirmPassword] = useState("");
75
+ const [loading, setLoading] = useState(false);
76
+ const [error, setError] = useState("");
77
+ const [tokenError, setTokenError] = useState("");
78
+
79
+ const token = searchParams.get("token");
80
+
81
+ useEffect(() => {
82
+ if (!token) {
83
+ setTokenError(t("No password reset token provided"));
84
+ }
85
+ }, [token, t]);
86
+
87
+ const handleSubmit = async (event: React.FormEvent) => {
88
+ event.preventDefault();
89
+ setError("");
90
+
91
+ if (!token) {
92
+ setTokenError(t("No password reset token provided"));
93
+ return;
94
+ }
95
+
96
+ if (password.length < 8) {
97
+ setError(t("Password must be at least 8 characters long"));
98
+ return;
99
+ }
100
+
101
+ if (password !== confirmPassword) {
102
+ setError(t("Passwords do not match"));
103
+ return;
104
+ }
105
+
106
+ setLoading(true);
107
+
108
+ try {
109
+ const response = await api?.operations["user.staff:password-reset-confirm"].call({
110
+ body: {
111
+ token,
112
+ new_password: password,
113
+ },
114
+ });
115
+
116
+ if (response?.ok) {
117
+ const data = await response.json();
118
+ enqueueSnackbar(data.message || t("Password reset successfully!"), {
119
+ variant: "success",
120
+ });
121
+
122
+ // User is logged in on the backend after password reset, redirect to dashboard
123
+ // Use full page navigation to reinitialize the UserContext
124
+ window.location.href = "/";
125
+ } else {
126
+ const errorData = await response?.json();
127
+ if (errorData?.detail && Array.isArray(errorData.detail)) {
128
+ const tokenErrors = errorData.detail.filter((err: { loc?: string[]; msg?: string }) =>
129
+ err.loc?.includes("token"),
130
+ );
131
+ if (tokenErrors.length > 0) {
132
+ setTokenError(tokenErrors[0].msg);
133
+ } else {
134
+ setError(errorData.detail[0]?.msg || t("Failed to reset password"));
135
+ }
136
+ } else {
137
+ setError(t("Failed to reset password. Please try again."));
138
+ }
139
+ }
140
+ } catch (error) {
141
+ console.error("[RESET_PASSWORD]", error);
142
+ setError(t("An unexpected error occurred. Please try again."));
143
+ } finally {
144
+ setLoading(false);
145
+ }
146
+ };
147
+
148
+ return (
149
+ <Content layout="fullWidth">
150
+ <StyledDialog
151
+ open
152
+ BackdropComponent={StyledBackdrop}
153
+ PaperProps={{ elevation: 1 }}
154
+ sx={{ "> * > *": { width: "100%" } }}
155
+ >
156
+ <StyledDialogTitle>
157
+ {logo ? (
158
+ <Brand
159
+ LogoProps={{ style: { width: "auto", height: logoHeight ?? "24px !important" } }}
160
+ src={logo}
161
+ />
162
+ ) : (
163
+ <Typography sx={{ fontWeight: "bold", color: "inherit" }}>{title}</Typography>
164
+ )}
165
+ </StyledDialogTitle>
166
+
167
+ <Box component="form" onSubmit={handleSubmit}>
168
+ <DialogContent>
169
+ <Stack spacing={2}>
170
+ <Typography variant="h6">{t("Reset Your Password")}</Typography>
171
+ <Typography color="text.secondary" variant="body2">
172
+ {t("Enter your new password below.")}
173
+ </Typography>
174
+
175
+ {tokenError && <Alert severity="error">{tokenError}</Alert>}
176
+
177
+ {error && !tokenError && <Alert severity="error">{error}</Alert>}
178
+
179
+ {!tokenError && (
180
+ <>
181
+ <TextField
182
+ fullWidth
183
+ required
184
+ color="secondary"
185
+ disabled={loading}
186
+ inputProps={{ "aria-label": "New Password", minLength: 8 }}
187
+ label={t("New Password")}
188
+ name="password"
189
+ type="password"
190
+ value={password}
191
+ onChange={(e) => setPassword(e.target.value)}
192
+ />
193
+ <TextField
194
+ fullWidth
195
+ required
196
+ color="secondary"
197
+ disabled={loading}
198
+ inputProps={{ "aria-label": "Confirm New Password", minLength: 8 }}
199
+ label={t("Confirm New Password")}
200
+ name="confirmPassword"
201
+ type="password"
202
+ value={confirmPassword}
203
+ onChange={(e) => setConfirmPassword(e.target.value)}
204
+ />
205
+ </>
206
+ )}
207
+ </Stack>
208
+ </DialogContent>
209
+
210
+ <DialogActions
211
+ sx={{
212
+ pt: 0,
213
+ pb: 2,
214
+ }}
215
+ >
216
+ {!tokenError && (
217
+ <LoadingButton
218
+ aria-label="reset-password"
219
+ color="primary"
220
+ disabled={!token}
221
+ loading={loading}
222
+ sx={{ margin: "auto" }}
223
+ type="submit"
224
+ variant="contained"
225
+ >
226
+ {t("Reset Password")}
227
+ </LoadingButton>
228
+ )}
229
+ </DialogActions>
230
+ </Box>
231
+ </StyledDialog>
232
+ </Content>
233
+ );
234
+ };
235
+
236
+ export default ResetPasswordPage;