@vtex/faststore-plugin-buyer-portal 1.0.48 → 1.0.49

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/features/buying-policies/components/BasicBuyingPolicyDrawer/BasicBuyingPolicyDrawer.tsx +3 -3
  3. package/src/features/org-units/layouts/OrgUnitDetailsLayout/OrgUnitDetailsLayout.tsx +5 -5
  4. package/src/features/shared/components/Table/Table.tsx +2 -0
  5. package/src/features/shared/components/Table/TableCell/TableCell.tsx +7 -0
  6. package/src/features/shared/components/Table/TableHead/TableHead.tsx +1 -1
  7. package/src/features/shared/components/Table/TableRow/TableRow.tsx +1 -1
  8. package/src/features/shared/utils/api.ts +5 -4
  9. package/src/features/users/clients/UsersClient.ts +30 -0
  10. package/src/features/users/components/CreateUserDrawer/CreateUserDrawer.tsx +50 -38
  11. package/src/features/users/components/CreateUserDrawer/create-user-drawer.scss +27 -0
  12. package/src/features/users/components/UpdateUserDrawer/UpdateUserDrawer.tsx +214 -0
  13. package/src/features/users/components/UpdateUserDrawer/update-user-drawer.scss +40 -0
  14. package/src/features/users/components/UserDropdownMenu/UserDropdownMenu.tsx +18 -3
  15. package/src/features/users/components/UserDropdownMenu/user-dropdown-menu.scss +1 -0
  16. package/src/features/users/components/index.ts +4 -0
  17. package/src/features/users/hooks/index.ts +2 -0
  18. package/src/features/users/hooks/useGetUserById.ts +20 -0
  19. package/src/features/users/hooks/useUpdateUser.ts +23 -0
  20. package/src/features/users/layouts/UserDetailsLayout/UserDetailsLayout.tsx +25 -4
  21. package/src/features/users/layouts/UserDetailsLayout/user-details-layout.scss +1 -0
  22. package/src/features/users/layouts/UsersLayout/UsersLayout.tsx +46 -30
  23. package/src/features/users/layouts/UsersLayout/users-layout.scss +4 -1
  24. package/src/features/users/mocks/users-data.ts +2 -2
  25. package/src/features/users/services/add-user-to-org-unit.service.ts +6 -2
  26. package/src/features/users/services/get-user-by-id.service.ts +2 -2
  27. package/src/features/users/services/get-users-by-org-unit-id.service.ts +7 -3
  28. package/src/features/users/services/index.ts +4 -1
  29. package/src/features/users/services/update-user.service.ts +27 -0
  30. package/src/features/users/types/UserData.ts +1 -2
  31. package/src/features/users/utils/index.ts +1 -0
  32. package/src/features/users/utils/roles.ts +26 -0
  33. package/src/pages/users.tsx +2 -3
  34. package/src/features/users/services/get-user-details.service.ts +0 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.0.48",
3
+ "version": "1.0.49",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -82,7 +82,7 @@ export const BasicBuyingPolicyDrawer = ({
82
82
  }));
83
83
  };
84
84
 
85
- const isAllFeldsFilled = Object.keys(form).every((key) => {
85
+ const isAllFieldsFilled = Object.keys(form).every((key) => {
86
86
  const value = form[key as keyof typeof form];
87
87
 
88
88
  if (key === "action") {
@@ -110,7 +110,7 @@ export const BasicBuyingPolicyDrawer = ({
110
110
  const handleConfirmClick = () => {
111
111
  setIsTouched(true);
112
112
 
113
- if (!isAllFeldsFilled) {
113
+ if (!isAllFieldsFilled) {
114
114
  return;
115
115
  }
116
116
 
@@ -160,7 +160,7 @@ export const BasicBuyingPolicyDrawer = ({
160
160
  });
161
161
  };
162
162
 
163
- const isConfirmButtonEnabled = isAllFeldsFilled && !isLoading;
163
+ const isConfirmButtonEnabled = isAllFieldsFilled && !isLoading;
164
164
 
165
165
  return (
166
166
  <BasicDrawer data-fs-bp-basic-buying-policy-drawer close={close} {...props}>
@@ -40,7 +40,7 @@ export const OrgUnitsDetailsLayout = ({
40
40
  {/* TODO: Add person here */}
41
41
  <OrgUnitDetailsNavbar
42
42
  orgName={orgUnit.name}
43
- person={{ name: user?.name ?? "", role: user?.role }}
43
+ person={{ name: user?.name ?? "", role: user?.roles?.[0] }}
44
44
  />
45
45
  <section data-fs-org-units-details-section>
46
46
  <HeaderInside title={orgUnit.name}>
@@ -67,7 +67,7 @@ export const OrgUnitsDetailsLayout = ({
67
67
  footerMessage="Manage contract settings"
68
68
  footerLink={buyerPortalRoutes.profileDetails({
69
69
  orgUnitId: orgUnit.id,
70
- contractId: contracts[0].id,
70
+ contractId: contracts[0]?.id ?? "",
71
71
  })}
72
72
  enableFooter
73
73
  >
@@ -75,7 +75,7 @@ export const OrgUnitsDetailsLayout = ({
75
75
  <VerticalNav.Menu title={orgUnit.name}>
76
76
  {getContractSettingsLinks({
77
77
  orgUnitId: orgUnit.id,
78
- contractId: contracts[0].id,
78
+ contractId: contracts[0]?.id ?? "",
79
79
  }).map(({ name, link }) => (
80
80
  <VerticalNav.Link key={name} link={link}>
81
81
  {name}
@@ -139,14 +139,14 @@ export const OrgUnitsDetailsLayout = ({
139
139
  footerMessage="Manage finance and compliance settings"
140
140
  footerLink={buyerPortalRoutes.buyingPolicies({
141
141
  orgUnitId: orgUnit.id,
142
- contractId: contracts[0].id,
142
+ contractId: contracts[0]?.id,
143
143
  })}
144
144
  enableFooter
145
145
  >
146
146
  <VerticalNav.Menu title="Finance and Compliance">
147
147
  {getFinanceSettingsLinks({
148
148
  orgUnitId: orgUnit.id,
149
- contractId: contracts[0].id,
149
+ contractId: contracts[0]?.id ?? "",
150
150
  }).map((option) => (
151
151
  <VerticalNav.Link key={option.name} link={option.link}>
152
152
  {option.name}
@@ -2,6 +2,7 @@ import { TableRow } from "./TableRow/TableRow";
2
2
  import { TableBody } from "./TableBody/TableBody";
3
3
  import TableHead from "./TableHead/TableHead";
4
4
  import { TableLoading } from "./TableLoading/TableLoading";
5
+ import { TableCell } from "./TableCell/TableCell";
5
6
 
6
7
  export type TableProps = {
7
8
  children: React.ReactNode;
@@ -15,3 +16,4 @@ Table.Head = TableHead;
15
16
  Table.Body = TableBody;
16
17
  Table.Row = TableRow;
17
18
  Table.Loading = TableLoading;
19
+ Table.Cell = TableCell;
@@ -0,0 +1,7 @@
1
+ import type { ComponentProps } from "react";
2
+
3
+ export type TableCellProps = ComponentProps<"td">;
4
+
5
+ export const TableCell = ({ ...otherProps }) => (
6
+ <td data-fs-bp-table-cell {...otherProps} />
7
+ );
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import type React from "react";
2
2
 
3
3
  export type TableColumn<T extends string = string> = {
4
4
  key: T;
@@ -47,7 +47,7 @@ export const TableRow = ({
47
47
  <SearchHighlight text={title} highlight={searchTerm} />
48
48
  </span>
49
49
  </td>
50
- <td>{children}</td>
50
+ {children}
51
51
  <td data-fs-bp-table-row-dropdown>
52
52
  {dropdownMenu && (
53
53
  <Dropdown>
@@ -1,10 +1,11 @@
1
1
  import { API_URL } from "./constants";
2
2
  import storeConfig from "discovery.config";
3
3
 
4
- export function getApiUrl(operation: string = "", customerId: string = "") {
5
- return `${API_URL(`${storeConfig?.secureSubdomain ?? ""}`, operation)}${
6
- customerId ? `/${customerId}` : ""
7
- }`;
4
+ export function getApiUrl(operation = "", customerId = "") {
5
+ return `${API_URL(
6
+ `https://${storeConfig?.api.storeId}.myvtex.com`,
7
+ operation
8
+ )}${customerId ? `/${customerId}` : ""}`;
8
9
  }
9
10
 
10
11
  export function getPostalCodeApiUrl() {
@@ -121,6 +121,36 @@ class UsersClient extends Client {
121
121
  },
122
122
  });
123
123
  }
124
+
125
+ updateUser(
126
+ props: {
127
+ orgUnitId: string;
128
+ userId: string;
129
+ name?: string;
130
+ role?: string;
131
+ },
132
+ cookie: string
133
+ ) {
134
+ const { orgUnitId, userId, ...data } = props;
135
+
136
+ return this.patch<
137
+ unknown,
138
+ {
139
+ name?: string;
140
+ role?: string;
141
+ }
142
+ >(
143
+ `units/${orgUnitId}/users/${userId}`,
144
+ {
145
+ ...data,
146
+ },
147
+ {
148
+ headers: {
149
+ Cookie: cookie,
150
+ },
151
+ }
152
+ );
153
+ }
124
154
  }
125
155
 
126
156
  const usersClient = new UsersClient();
@@ -4,14 +4,14 @@ import { useUI } from "@faststore/ui";
4
4
  import { useRouter } from "next/router";
5
5
  import {
6
6
  type BasicDrawerProps,
7
- AutocompleteDropdown,
8
7
  BasicDrawer,
9
8
  ErrorMessage,
10
9
  Icon,
11
10
  InputText,
12
11
  } from "../../../shared/components";
13
- import { roles, type Role } from "../../../shared/utils/roles";
14
12
  import { useAddUserToOrgUnit } from "../../hooks";
13
+ import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
14
+ import { rolesOptions } from "../../utils";
15
15
 
16
16
  export type CreateUserDrawerProps = Omit<BasicDrawerProps, "children"> & {
17
17
  onCreate?: () => void;
@@ -45,25 +45,25 @@ export const CreateUserDrawer = ({
45
45
  const [form, setForm] = useState<{
46
46
  name: string;
47
47
  email: string;
48
- role: Role;
48
+ roles: string[];
49
49
  }>({
50
50
  name: "",
51
51
  email: "",
52
- role: "Buyer",
52
+ roles: [],
53
53
  });
54
54
 
55
55
  const [isTouched, setIsTouched] = useState(false);
56
56
 
57
- const { name, email, role } = form;
57
+ const { name, email, roles } = form;
58
58
 
59
- const updateField = (field: keyof typeof form, value: string) => {
59
+ const updateField = (field: keyof typeof form, value: string | string[]) => {
60
60
  setForm((prev) => ({
61
61
  ...prev,
62
62
  [field]: value,
63
63
  }));
64
64
  };
65
65
 
66
- const handleAddUserSuccess = (data: unknown) => {
66
+ const handleAddUserSuccess = ({ user }: { user: { id: string } }) => {
67
67
  pushToast({
68
68
  message: "User successfully added",
69
69
  status: "INFO",
@@ -72,8 +72,12 @@ export const CreateUserDrawer = ({
72
72
  data-fs-bp-toast-view-button
73
73
  type="button"
74
74
  onClick={() => {
75
- //TODO: Redirect to the user page
76
- router.push(`/user/${"user.id"}`);
75
+ router.push(
76
+ buyerPortalRoutes.userDetails({
77
+ userId: user.id,
78
+ orgUnitId: orgUnitId,
79
+ })
80
+ );
77
81
  }}
78
82
  >
79
83
  View
@@ -108,14 +112,18 @@ export const CreateUserDrawer = ({
108
112
  },
109
113
  });
110
114
 
111
- const isAllFeldsFilled = Object.keys(form).every((key) =>
112
- form[key as keyof typeof form]?.trim()
113
- );
115
+ const isAllFieldsFilled = Object.keys(form).every((key) => {
116
+ if (key === "roles") {
117
+ return form[key as keyof typeof form]?.length > 0;
118
+ }
119
+
120
+ return (form[key as keyof typeof form] as string)?.trim();
121
+ });
114
122
 
115
123
  const handleConfirmClick = () => {
116
124
  setIsTouched(true);
117
125
 
118
- if (!isAllFeldsFilled) {
126
+ if (!isAllFieldsFilled) {
119
127
  return;
120
128
  }
121
129
 
@@ -123,12 +131,13 @@ export const CreateUserDrawer = ({
123
131
  addUserToOrgUnit({
124
132
  name,
125
133
  email,
126
- role,
134
+ roles,
127
135
  orgUnitId,
128
136
  });
129
137
  };
130
138
 
131
- const isConfirmButtonEnabled = isAllFeldsFilled && !isAddUserToOrgUnitLoading;
139
+ const isConfirmButtonEnabled =
140
+ isAllFieldsFilled && !isAddUserToOrgUnitLoading;
132
141
 
133
142
  const backToCreateUser = () => {
134
143
  setDrawerState("CREATE_USER");
@@ -136,7 +145,7 @@ export const CreateUserDrawer = ({
136
145
  setForm({
137
146
  name: "",
138
147
  email: "",
139
- role: "Buyer",
148
+ roles: [],
140
149
  });
141
150
  };
142
151
 
@@ -190,30 +199,33 @@ export const CreateUserDrawer = ({
190
199
  message="Email is required"
191
200
  />
192
201
 
193
- <AutocompleteDropdown
194
- label="Role"
195
- value={role}
196
- options={[...roles]}
197
- onConfirmKeyPress={(option) => updateField("role", option)}
198
- hasError={isTouched && !role?.trim()}
199
- renderOption={(optionRole, index) => (
200
- <AutocompleteDropdown.Item
201
- key={optionRole}
202
- closeOnClick
203
- index={index}
204
- isSelected={role === optionRole}
205
- onClick={() => updateField("role", optionRole)}
206
- >
207
- {optionRole}
208
- {role === optionRole && (
209
- <Icon name="Check" width={12} height={12} />
210
- )}
211
- </AutocompleteDropdown.Item>
212
- )}
213
- />
202
+ <div data-fs-bp-create-user-roles>
203
+ <span data-fs-bp-create-user-roles-label>Roles</span>
204
+ {rolesOptions.map((role) => {
205
+ const id = `role-${role.value.toLowerCase().replace(" ", "-")}`;
206
+ return (
207
+ <span data-fs-bp-create-user-role-wrapper key={role.value}>
208
+ <input
209
+ type="checkbox"
210
+ key={role.value}
211
+ value={role.value}
212
+ id={id}
213
+ checked={roles?.includes(role.value)}
214
+ onChange={(event) => {
215
+ const newRoles = event.target.checked
216
+ ? [...(roles ?? []), role.value]
217
+ : roles?.filter((r) => r !== role.value) ?? [];
218
+ updateField("roles", newRoles);
219
+ }}
220
+ />
221
+ <label htmlFor={id}>{role.label}</label>
222
+ </span>
223
+ );
224
+ })}
225
+ </div>
214
226
 
215
227
  <ErrorMessage
216
- show={isTouched && !role?.trim()}
228
+ show={isTouched && !roles?.length}
217
229
  message="Role is required"
218
230
  />
219
231
  </BasicDrawer.Body>
@@ -3,6 +3,7 @@
3
3
  [data-fs-bp-create-user-drawer] {
4
4
  @import "../../../shared/components/InputText/input-text.scss";
5
5
  @import "../../../shared/components/ErrorMessage/error-message.scss";
6
+ @import "@faststore/ui/src/components/molecules/CheckboxField/styles.scss";
6
7
 
7
8
  [data-fs-bp-create-user-drawer-back-icon] {
8
9
  vertical-align: middle;
@@ -10,4 +11,30 @@
10
11
  margin-right: 0.75rem;
11
12
  cursor: pointer;
12
13
  }
14
+
15
+ [data-fs-bp-create-user-roles] {
16
+ display: flex;
17
+ flex-direction: column;
18
+ gap: var(--fs-spacing-2);
19
+ margin-bottom: var(--fs-spacing-6);
20
+ margin-top: var(--fs-spacing-5);
21
+
22
+ [data-fs-bp-create-user-roles-label] {
23
+ font-weight: var(--fs-text-weight-regular);
24
+ font-size: var(--fs-text-size-1);
25
+ line-height: var(--fs-text-size-3);
26
+ }
27
+
28
+ [data-fs-bp-create-user-role-wrapper] {
29
+ input {
30
+ margin-right: var(--fs-spacing-1);
31
+ }
32
+
33
+ label {
34
+ font-weight: var(--fs-text-weight-regular);
35
+ font-size: var(--fs-text-size-1);
36
+ line-height: var(--fs-text-size-3);
37
+ }
38
+ }
39
+ }
13
40
  }
@@ -0,0 +1,214 @@
1
+ import { useState } from "react";
2
+
3
+ import { Checkbox, CheckboxField, Skeleton, useUI } from "@faststore/ui";
4
+ import {
5
+ type BasicDrawerProps,
6
+ BasicDrawer,
7
+ ErrorMessage,
8
+ InputText,
9
+ } from "../../../shared/components";
10
+ import { useGetUserById, useUpdateUser } from "../../hooks";
11
+ import { rolesOptions } from "../../utils";
12
+
13
+ export type UpdateUserDrawerProps = Omit<BasicDrawerProps, "children"> & {
14
+ onCreate?: () => void;
15
+ orgUnitId: string;
16
+ userId: string;
17
+ };
18
+
19
+ export const UpdateUserDrawer = ({
20
+ close,
21
+ onCreate,
22
+ userId,
23
+ orgUnitId,
24
+ ...props
25
+ }: UpdateUserDrawerProps) => {
26
+ const { pushToast } = useUI();
27
+
28
+ const { isUserLoading, user } = useGetUserById(
29
+ { orgUnitId, userId },
30
+ {
31
+ onSuccess: (data) => {
32
+ setForm({
33
+ name: data?.name ?? "",
34
+ email: data?.email ?? "",
35
+ roles: data?.roles ? data.roles : [],
36
+ });
37
+ },
38
+ onError: () => {
39
+ pushToast({
40
+ message: "Failed to fetch user details",
41
+ status: "ERROR",
42
+ });
43
+ },
44
+ }
45
+ );
46
+
47
+ const [form, setForm] = useState<{
48
+ name: string;
49
+ email: string;
50
+ roles: string[];
51
+ }>({
52
+ name: "",
53
+ email: "",
54
+ roles: [],
55
+ });
56
+
57
+ const [isTouched, setIsTouched] = useState(false);
58
+
59
+ const { name, email, roles } = form;
60
+
61
+ const updateField = (field: keyof typeof form, value: string | string[]) => {
62
+ setForm((prev) => ({
63
+ ...prev,
64
+ [field]: value,
65
+ }));
66
+ };
67
+
68
+ const handleAddUserSuccess = () => {
69
+ pushToast({
70
+ message: "User successfully updated",
71
+ status: "INFO",
72
+ });
73
+ onCreate?.();
74
+ close();
75
+ };
76
+
77
+ const { updateUser, isUpdateUserLoading } = useUpdateUser({
78
+ onSuccess: handleAddUserSuccess,
79
+ onError: () => {
80
+ pushToast({
81
+ message: "Failed to update user",
82
+ status: "ERROR",
83
+ });
84
+ },
85
+ });
86
+
87
+ const isAllFieldsFilled = Object.keys(form).every((key) => {
88
+ if (key === "roles") {
89
+ return form[key as keyof typeof form]?.length > 0;
90
+ }
91
+
92
+ return (form[key as keyof typeof form] as string)?.trim();
93
+ });
94
+
95
+ const handleConfirmClick = () => {
96
+ setIsTouched(true);
97
+
98
+ if (!isAllFieldsFilled) {
99
+ return;
100
+ }
101
+
102
+ updateUser({
103
+ userId,
104
+ name,
105
+ roles,
106
+ orgUnitId,
107
+ });
108
+ };
109
+
110
+ const isConfirmButtonEnabled = isAllFieldsFilled && !isUpdateUserLoading;
111
+
112
+ return (
113
+ <BasicDrawer data-fs-bp-update-user-drawer close={close} {...props}>
114
+ <BasicDrawer.Heading title="Edit user details" onClose={close} />
115
+
116
+ <BasicDrawer.Body>
117
+ {isUserLoading ? (
118
+ <Skeleton size={{ width: "100%", height: "3.5rem" }} />
119
+ ) : (
120
+ <InputText
121
+ label="Full Name"
122
+ value={name}
123
+ hasError={isTouched && !name?.trim()}
124
+ onChange={(event) => updateField("name", event.target.value)}
125
+ />
126
+ )}
127
+
128
+ <ErrorMessage
129
+ show={isTouched && !name?.trim()}
130
+ message="Full name is required"
131
+ />
132
+
133
+ {isUserLoading ? (
134
+ <Skeleton
135
+ size={{ width: "100%", height: "3.5rem" }}
136
+ style={{
137
+ marginTop: "1rem",
138
+ marginBottom: "1rem",
139
+ width: "100%",
140
+ height: "3.5rem",
141
+ }}
142
+ />
143
+ ) : (
144
+ <InputText
145
+ label="Email"
146
+ value={email}
147
+ wrapperProps={{ style: { marginTop: 16, marginBottom: 16 } }}
148
+ hasError={isTouched && !email?.trim()}
149
+ onChange={(event) => updateField("email", event.target.value)}
150
+ readOnly
151
+ disabled
152
+ />
153
+ )}
154
+
155
+ <ErrorMessage
156
+ show={isTouched && !email?.trim()}
157
+ message="Email is required"
158
+ />
159
+
160
+ <div data-fs-bp-update-user-roles>
161
+ <span data-fs-bp-update-user-roles-label>Roles</span>
162
+ {rolesOptions.map((role) => {
163
+ if (isUserLoading) {
164
+ return (
165
+ <Skeleton
166
+ key={role.value}
167
+ size={{ width: "100%", height: "1.25rem" }}
168
+ />
169
+ );
170
+ }
171
+
172
+ const id = `role-${role.value.toLowerCase().replace(" ", "-")}`;
173
+ return (
174
+ <span data-fs-bp-update-user-role-wrapper key={role.value}>
175
+ <input
176
+ type="checkbox"
177
+ key={role.value}
178
+ value={role.value}
179
+ id={id}
180
+ checked={roles?.includes(role.value)}
181
+ onChange={(event) => {
182
+ const newRoles = event.target.checked
183
+ ? [...(roles ?? []), role.value]
184
+ : roles?.filter((r) => r !== role.value) ?? [];
185
+ updateField("roles", newRoles);
186
+ }}
187
+ />
188
+ <label htmlFor={id}>{role.label}</label>
189
+ </span>
190
+ );
191
+ })}
192
+ </div>
193
+
194
+ <ErrorMessage
195
+ show={isTouched && !roles?.length}
196
+ message="Role is required"
197
+ />
198
+ </BasicDrawer.Body>
199
+ <BasicDrawer.Footer>
200
+ <BasicDrawer.Button variant="ghost" onClick={close}>
201
+ Cancel
202
+ </BasicDrawer.Button>
203
+ <BasicDrawer.Button
204
+ variant="confirm"
205
+ disabled={!isConfirmButtonEnabled}
206
+ onClick={handleConfirmClick}
207
+ isLoading={isUpdateUserLoading}
208
+ >
209
+ Save
210
+ </BasicDrawer.Button>
211
+ </BasicDrawer.Footer>
212
+ </BasicDrawer>
213
+ );
214
+ };
@@ -0,0 +1,40 @@
1
+ @import "../../../shared/components/BasicDrawer/basic-drawer.scss";
2
+
3
+ [data-fs-bp-update-user-drawer] {
4
+ @import "../../../shared/components/InputText/input-text.scss";
5
+ @import "../../../shared/components/ErrorMessage/error-message.scss";
6
+ @import "@faststore/ui/src/components/atoms/Skeleton/styles.scss";
7
+
8
+ [data-fs-bp-update-user-drawer-back-icon] {
9
+ vertical-align: middle;
10
+ margin-left: -3.125rem;
11
+ margin-right: 0.75rem;
12
+ cursor: pointer;
13
+ }
14
+
15
+ [data-fs-bp-update-user-roles] {
16
+ display: flex;
17
+ flex-direction: column;
18
+ gap: var(--fs-spacing-2);
19
+ margin-bottom: var(--fs-spacing-6);
20
+ margin-top: var(--fs-spacing-5);
21
+
22
+ [data-fs-bp-update-user-roles-label] {
23
+ font-weight: var(--fs-text-weight-regular);
24
+ font-size: var(--fs-text-size-1);
25
+ line-height: var(--fs-text-size-3);
26
+ }
27
+
28
+ [data-fs-bp-update-user-role-wrapper] {
29
+ input {
30
+ margin-right: var(--fs-spacing-1);
31
+ }
32
+
33
+ label {
34
+ font-weight: var(--fs-text-weight-regular);
35
+ font-size: var(--fs-text-size-1);
36
+ line-height: var(--fs-text-size-3);
37
+ }
38
+ }
39
+ }
40
+ }
@@ -5,6 +5,7 @@ import { ReassignOrgUnitDrawer } from "../ReassignOrgUnitDrawer/ReassignOrgUnitD
5
5
  import { useBuyerPortal, useDrawerProps } from "../../../shared/hooks";
6
6
  import { DeleteUserDrawer } from "../DeleteUserDrawer/DeleteUserDrawer";
7
7
  import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
8
+ import { UpdateUserDrawer } from "../UpdateUserDrawer/UpdateUserDrawer";
8
9
 
9
10
  export type UserDropdownMenuProps = {
10
11
  user: {
@@ -32,6 +33,12 @@ export const UserDropdownMenu = ({ user }: UserDropdownMenuProps) => {
32
33
  ...deleteUserDrawerProps
33
34
  } = useDrawerProps();
34
35
 
36
+ const {
37
+ open: openUpdateUser,
38
+ isOpen: isUpdateUserDrawerOpen,
39
+ ...updateUserDrawerProps
40
+ } = useDrawerProps();
41
+
35
42
  return (
36
43
  <>
37
44
  <BasicDropdownMenu>
@@ -48,9 +55,9 @@ export const UserDropdownMenu = ({ user }: UserDropdownMenuProps) => {
48
55
  <Icon name="OpenInNew" {...sizeProps} />
49
56
  Open
50
57
  </DropdownItem>
51
- <DropdownItem>
52
- <Icon name="FormatSize" {...sizeProps} />
53
- Rename
58
+ <DropdownItem onClick={openUpdateUser}>
59
+ <Icon name="Edit" {...sizeProps} />
60
+ Edit details
54
61
  </DropdownItem>
55
62
  <DropdownItem onClick={openReassignOrgUnitDrawer}>
56
63
  <Icon name="DriveFileMove" {...sizeProps} />
@@ -79,6 +86,14 @@ export const UserDropdownMenu = ({ user }: UserDropdownMenuProps) => {
79
86
  {...reassignOrgUnitDrawerProps}
80
87
  />
81
88
  )}
89
+ {isUpdateUserDrawerOpen && (
90
+ <UpdateUserDrawer
91
+ userId={user.id}
92
+ orgUnitId={currentOrgUnit?.id ?? ""}
93
+ isOpen={isUpdateUserDrawerOpen}
94
+ {...updateUserDrawerProps}
95
+ />
96
+ )}
82
97
  </>
83
98
  );
84
99
  };
@@ -2,3 +2,4 @@
2
2
  @import "../ReassignOrgUnitDrawer/reassign-org-unit-drawer.scss";
3
3
  @import "../DeleteUserDrawer/delete-user-drawer.scss";
4
4
  @import "../CreateUserDrawer/create-user-drawer.scss";
5
+ @import "../UpdateUserDrawer/update-user-drawer.scss";
@@ -7,3 +7,7 @@ export {
7
7
  ReassignOrgUnitDrawer,
8
8
  type ReassignOrgUnitDrawerProps,
9
9
  } from "./ReassignOrgUnitDrawer/ReassignOrgUnitDrawer";
10
+ export {
11
+ UpdateUserDrawer,
12
+ type UpdateUserDrawerProps,
13
+ } from "./UpdateUserDrawer/UpdateUserDrawer";
@@ -3,3 +3,5 @@ export { useRemoveUserFromOrgUnit } from "./useRemoveUserFromOrgUnit";
3
3
  export { useAddUserToOrgUnit } from "./useAddUserToOrgUnit";
4
4
  export { useReassignUser } from "./useReassignUser";
5
5
  export { useDebouncedSearchOrgUnit } from "./useDebouncedSearchOrgUnit";
6
+ export { useUpdateUser } from "./useUpdateUser";
7
+ export { useGetUserById } from "./useGetUserById";
@@ -0,0 +1,20 @@
1
+ import { type QueryOptions, useQuery } from "../../shared/hooks";
2
+ import { getUserByIdService } from "../services";
3
+
4
+ export const useGetUserById = (
5
+ { orgUnitId, userId }: { orgUnitId: string; userId: string },
6
+ options?: QueryOptions<AwaitedType<typeof getUserByIdService>>
7
+ ) => {
8
+ const { data, error, isLoading, refetch } = useQuery(
9
+ `org-unit/user/${orgUnitId}`,
10
+ ({ cookie }) => getUserByIdService({ orgUnitId, userId, cookie }),
11
+ options
12
+ );
13
+
14
+ return {
15
+ user: data,
16
+ hasUserError: error,
17
+ isUserLoading: isLoading,
18
+ refetchUser: refetch,
19
+ };
20
+ };
@@ -0,0 +1,23 @@
1
+ import { type MutationOptions, useMutation } from "../../shared/hooks";
2
+ import { updateUserService, type UpdateUserServiceProps } from "../services";
3
+
4
+ export const useUpdateUser = (
5
+ options?: MutationOptions<AwaitedType<typeof updateUserService>>
6
+ ) => {
7
+ const { mutate, isLoading, error } = useMutation<
8
+ AwaitedType<typeof updateUserService>,
9
+ Omit<UpdateUserServiceProps, "cookie">
10
+ >(
11
+ (variables, clientContext) =>
12
+ updateUserService({
13
+ ...variables,
14
+ cookie: clientContext.cookie,
15
+ }),
16
+ options
17
+ );
18
+ return {
19
+ updateUser: mutate,
20
+ isUpdateUserLoading: isLoading,
21
+ hasUpdateUserError: error,
22
+ };
23
+ };
@@ -7,7 +7,7 @@ import {
7
7
  Tag,
8
8
  } from "../../../shared/components";
9
9
  import { GlobalLayout } from "../../../shared/layouts";
10
- import { ReassignOrgUnitDrawer } from "../../components";
10
+ import { ReassignOrgUnitDrawer, UpdateUserDrawer } from "../../components";
11
11
  import { useBuyerPortal, useDrawerProps } from "../../../shared/hooks";
12
12
  import { OrgUnitTabsLayout } from "../../../shared/layouts/OrgUnitTabsLayout/OrgUnitTabLayout";
13
13
  import { UserDropdownMenu } from "../../components/UserDropdownMenu/UserDropdownMenu";
@@ -31,6 +31,12 @@ export const UserDetailsLayout = ({
31
31
  ...reassignDrawerProps
32
32
  } = useDrawerProps();
33
33
 
34
+ const {
35
+ open: openUpdateUser,
36
+ isOpen: isUpdateUserDrawerOpen,
37
+ ...updateUserDrawerProps
38
+ } = useDrawerProps();
39
+
34
40
  return (
35
41
  <GlobalLayout>
36
42
  {/* // TODO: Add org unit name and id */}
@@ -49,12 +55,15 @@ export const UserDetailsLayout = ({
49
55
  user={{ name: user?.name ?? "", id: user?.id ?? "" }}
50
56
  />
51
57
  </Dropdown>
52
- <HeaderInside.Button />
53
58
  </HeaderInside>
54
59
  <div data-fs-user-details>
55
60
  <div data-fs-buyer-portal-user-details-title>
56
61
  <span data-fs-buyer-portal-user-details-title-label>Details</span>
57
- <button type="button" data-fs-buyer-portal-user-details-edit>
62
+ <button
63
+ type="button"
64
+ data-fs-buyer-portal-user-details-edit
65
+ onClick={openUpdateUser}
66
+ >
58
67
  Edit
59
68
  </button>
60
69
  </div>
@@ -77,7 +86,11 @@ export const UserDetailsLayout = ({
77
86
  <div data-fs-user-details-row>
78
87
  <span data-fs-user-details-row-label>Role</span>
79
88
  <span data-fs-user-details-row-value>
80
- <Tag>{user?.userType}</Tag>
89
+ {user?.roles?.map((role) => (
90
+ <Tag key={role} data-fs-user-details-row-value-tag>
91
+ {role}
92
+ </Tag>
93
+ ))}
81
94
  </span>
82
95
  </div>
83
96
 
@@ -105,6 +118,14 @@ export const UserDetailsLayout = ({
105
118
  {...reassignDrawerProps}
106
119
  />
107
120
  )}
121
+ {isUpdateUserDrawerOpen && (
122
+ <UpdateUserDrawer
123
+ userId={user?.id ?? ""}
124
+ orgUnitId={currentOrgUnit?.id ?? ""}
125
+ isOpen={isUpdateUserDrawerOpen}
126
+ {...updateUserDrawerProps}
127
+ />
128
+ )}
108
129
  </OrgUnitTabsLayout>
109
130
  </GlobalLayout>
110
131
  );
@@ -1,5 +1,6 @@
1
1
  @import "@faststore/ui/src/components/molecules/Toggle/styles.scss";
2
2
  @import "../../components/ReassignOrgUnitDrawer/reassign-org-unit-drawer.scss";
3
+ @import "../../components/UpdateUserDrawer/update-user-drawer.scss";
3
4
  @import "../../components/DeleteUserDrawer/delete-user-drawer.scss";
4
5
  @import "../../components/UserDropdownMenu/user-dropdown-menu.scss";
5
6
 
@@ -1,7 +1,5 @@
1
- import { Dropdown, DropdownButton, Link } from "@faststore/ui";
2
-
3
1
  import type { UserData } from "../../types";
4
- import { InternalSearch, Icon, HeaderInside } from "../../../shared/components";
2
+ import { InternalSearch, HeaderInside, Tag } from "../../../shared/components";
5
3
  import {
6
4
  useBuyerPortal,
7
5
  useDrawerProps,
@@ -9,10 +7,11 @@ import {
9
7
  } from "../../../shared/hooks";
10
8
  import { GlobalLayout } from "../../../shared/layouts";
11
9
  import { CreateUserDrawer } from "../../components";
12
- import { Fragment } from "react";
13
10
  import { UserDropdownMenu } from "../../components/UserDropdownMenu/UserDropdownMenu";
14
11
  import { OrgUnitTabsLayout } from "../../../shared/layouts/OrgUnitTabsLayout/OrgUnitTabLayout";
15
12
  import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
13
+ import { Table } from "../../../shared/components/Table/Table";
14
+ import type { TableColumn } from "../../../shared/components/Table/TableHead/TableHead";
16
15
 
17
16
  export type UsersLayoutProps = {
18
17
  data: {
@@ -35,6 +34,28 @@ export const UsersLayout = ({
35
34
  ...createUserDrawerProps
36
35
  } = useDrawerProps();
37
36
 
37
+ const configTable: Array<TableColumn> = [
38
+ {
39
+ key: "name",
40
+ label: "Name",
41
+ align: "left",
42
+ colspan: 2,
43
+ size: "large",
44
+ },
45
+ {
46
+ key: "role",
47
+ label: "Role",
48
+ align: "left",
49
+ size: "large",
50
+ },
51
+ {
52
+ key: "actions",
53
+ label: "",
54
+ align: "right",
55
+ size: "large",
56
+ },
57
+ ];
58
+
38
59
  return (
39
60
  <GlobalLayout>
40
61
  <OrgUnitTabsLayout pageName="Organization">
@@ -57,40 +78,35 @@ export const UsersLayout = ({
57
78
  </div>
58
79
  </div>
59
80
 
60
- <div data-fs-user-table-heading>
81
+ {/* <div data-fs-user-table-heading>
61
82
  <span data-fs-user-table-title>Name</span>
62
83
  <span data-fs-user-table-title>Role</span>
63
84
  </div>
64
- <hr data-fs-user-divider />
65
- {users.map((user) => (
66
- <Fragment key={user.id}>
67
- <div data-fs-user-row>
68
- <Link
69
- data-fs-user-row-link
85
+ <hr data-fs-user-divider /> */}
86
+ <Table>
87
+ <Table.Head columns={configTable} />
88
+ <Table.Body>
89
+ {users.map((user) => (
90
+ <Table.Row
91
+ key={user.id}
92
+ title={user.name}
93
+ iconName="Profile"
94
+ iconSize={20}
70
95
  href={buyerPortalRoutes.userDetails({
71
96
  orgUnitId: currentOrgUnit?.id ?? "",
72
97
  userId: user.id,
73
98
  })}
74
- data-fs-user-row-information
99
+ dropdownMenu={<UserDropdownMenu user={user} />}
75
100
  >
76
- <span data-fs-user-row-icon-wrapper>
77
- <Icon name="Profile" width={16} height={16} />
78
- </span>
79
- <div data-fs-user-name>{user.name}</div>
80
- <span data-fs-user-usertype>{user.userType}</span>
81
- </Link>
82
- <div data-fs-user-row-action>
83
- <Dropdown>
84
- <DropdownButton data-fs-user-row-dropdown-button>
85
- <Icon name="MoreVert" data-fs-menu-action-button />
86
- </DropdownButton>
87
- <UserDropdownMenu user={user} />
88
- </Dropdown>
89
- </div>
90
- </div>
91
- <hr data-fs-user-divider />
92
- </Fragment>
93
- ))}
101
+ <Table.Cell>
102
+ {user.roles?.map((role) => (
103
+ <Tag key={role}>{role}</Tag>
104
+ ))}
105
+ </Table.Cell>
106
+ </Table.Row>
107
+ ))}
108
+ </Table.Body>
109
+ </Table>
94
110
  </div>
95
111
 
96
112
  {isCreateUserDrawerOpen && (
@@ -11,8 +11,11 @@
11
11
  @import "../../../shared/components/InternalSearch/internal-search.scss";
12
12
  @import "../../../shared/components/DropdownFilter/dropdown-filter.scss";
13
13
  @import "../../../shared/components/SortFilter/sort-filter.scss";
14
+ @import "../../../shared/components/Tag/tag.scss";
14
15
  @import "../../../shared/components/InternalTopbar/internal-top-bar.scss";
15
16
 
17
+ @import "../../../shared/components/Table/table.scss";
18
+
16
19
  --data-fs-users-table-width: 11.875rem;
17
20
 
18
21
  padding: 0 calc(var(--fs-spacing-9) - var(--fs-spacing-0));
@@ -128,7 +131,7 @@
128
131
  color: #5c5c5c;
129
132
  }
130
133
 
131
- [data-fs-user-userType] {
134
+ [data-fs-user-role] {
132
135
  width: 15.625rem;
133
136
  font-size: var(--fs-text-size-1);
134
137
  font-weight: var(--fs-text-weight-regular);
@@ -3,7 +3,7 @@ import type { UserData } from "../types";
3
3
  export const usersData: UserData[] = [
4
4
  {
5
5
  name: "Everton Ataide",
6
- userType: "Admin",
6
+ roles: ["Admin"],
7
7
  isActive: true,
8
8
  id: "1234",
9
9
  email: "everton@vtex.com",
@@ -14,7 +14,7 @@ export const usersData: UserData[] = [
14
14
  },
15
15
  {
16
16
  name: "Arthur Andrade",
17
- userType: "Buyer",
17
+ roles: ["Buyer"],
18
18
  isActive: false,
19
19
  id: "32141",
20
20
  email: "arthur@vtex.com",
@@ -2,7 +2,7 @@ import { usersClient } from "../clients/UsersClient";
2
2
 
3
3
  export type AddUserToOrgUnitServiceProps = {
4
4
  orgUnitId: string;
5
- role: string;
5
+ roles: string[];
6
6
  email: string;
7
7
  name: string;
8
8
  cookie: string;
@@ -10,9 +10,13 @@ export type AddUserToOrgUnitServiceProps = {
10
10
 
11
11
  export const addUserToOrgUnitService = async ({
12
12
  cookie,
13
+ roles,
13
14
  ...data
14
15
  }: AddUserToOrgUnitServiceProps) => {
15
- const response = await usersClient.addUserToOrgUnit(data, cookie);
16
+ const response = await usersClient.addUserToOrgUnit(
17
+ { ...data, role: roles.join(",") },
18
+ cookie
19
+ );
16
20
 
17
21
  if (response.message === "User already exists and is attached to a unit") {
18
22
  throw new Error(JSON.stringify(response));
@@ -18,8 +18,8 @@ export const getUserByIdService = async ({
18
18
  );
19
19
 
20
20
  return {
21
- name: `${name}`,
22
- userType: role,
21
+ name,
22
+ roles: role.split(","),
23
23
  id: userId,
24
24
  email: email ?? "",
25
25
  orgUnit: {
@@ -1,4 +1,5 @@
1
1
  import { usersClient } from "../clients/UsersClient";
2
+ import type { UserData } from "../types";
2
3
 
3
4
  export type GetUsersByOrgUnitIdServiceProps = {
4
5
  orgUnitId: string;
@@ -8,7 +9,10 @@ export type GetUsersByOrgUnitIdServiceProps = {
8
9
  export const getUsersByOrgUnitIdService = async ({
9
10
  orgUnitId,
10
11
  cookie,
11
- }: GetUsersByOrgUnitIdServiceProps) => {
12
+ }: GetUsersByOrgUnitIdServiceProps): Promise<{
13
+ users: UserData[];
14
+ total: number;
15
+ }> => {
12
16
  const { users, total } = await usersClient.getUsersByOrgUnitId(
13
17
  orgUnitId,
14
18
  cookie
@@ -16,9 +20,9 @@ export const getUsersByOrgUnitIdService = async ({
16
20
 
17
21
  return {
18
22
  users: users.map((user) => ({
23
+ id: user.userId,
19
24
  name: user.name,
20
- userType: user.role,
21
- userId: user.userId,
25
+ roles: user.role ? user.role.split(",") : [],
22
26
  email: user.email,
23
27
  orgUnit: { name: user.orgUnit },
24
28
  })),
@@ -1,4 +1,3 @@
1
- export { getUserDetailsService } from "./get-user-details.service";
2
1
  export {
3
2
  getUsersByOrgUnitIdService,
4
3
  type GetUsersByOrgUnitIdServiceProps,
@@ -15,4 +14,8 @@ export {
15
14
  reassignUserService,
16
15
  type ReassignUserServiceProps,
17
16
  } from "./reassign-user.service";
17
+ export {
18
+ updateUserService,
19
+ UpdateUserServiceProps,
20
+ } from "./update-user.service";
18
21
  export { getUserByIdService } from "./get-user-by-id.service";
@@ -0,0 +1,27 @@
1
+ import { usersClient } from "../clients/UsersClient";
2
+
3
+ export type UpdateUserServiceProps = {
4
+ orgUnitId: string;
5
+ userId: string;
6
+ name?: string;
7
+ roles?: string[];
8
+ cookie: string;
9
+ };
10
+
11
+ export const updateUserService = async ({
12
+ orgUnitId,
13
+ userId,
14
+ cookie,
15
+ name,
16
+ roles,
17
+ }: UpdateUserServiceProps) => {
18
+ return usersClient.updateUser(
19
+ {
20
+ orgUnitId,
21
+ userId,
22
+ name,
23
+ role: roles?.join(",") || undefined,
24
+ },
25
+ cookie
26
+ );
27
+ };
@@ -1,10 +1,9 @@
1
1
  export type UserData = {
2
2
  name: string;
3
- userType: string;
4
3
  isActive?: boolean;
5
4
  id: string;
6
5
  email?: string;
7
- role?: string;
6
+ roles?: string[];
8
7
  orgUnit: {
9
8
  id?: string;
10
9
  name: string;
@@ -0,0 +1 @@
1
+ export { rolesOptions } from "./roles";
@@ -0,0 +1,26 @@
1
+ export const rolesOptions = [
2
+ {
3
+ label: "Organizational Unit Admin",
4
+ value: "Admin",
5
+ },
6
+ {
7
+ label: "Order Approver",
8
+ value: "Approver",
9
+ },
10
+ {
11
+ label: "Contract Manager",
12
+ value: "Contract Manager",
13
+ },
14
+ {
15
+ label: "Organizational Unit Manager",
16
+ value: "Organizational Unit Manager",
17
+ },
18
+ {
19
+ label: "Contract Viewer",
20
+ value: "Contract Viewer",
21
+ },
22
+ {
23
+ label: "Address Manager",
24
+ value: "Manager",
25
+ },
26
+ ];
@@ -13,8 +13,6 @@ import { BuyerPortalProvider } from "../features/shared/components";
13
13
  import type { LoaderData } from "../features/shared/types";
14
14
  import { getOrgUnitBasicDataService } from "../features/org-units/services";
15
15
  import type { OrgUnitBasicData } from "../features/org-units/types";
16
- import { ContractData } from "../features/contracts/types";
17
- import { getContractDetailsService } from "../features/contracts/services";
18
16
 
19
17
  export type UsersPageData = {
20
18
  data: {
@@ -55,7 +53,8 @@ export async function loader(
55
53
 
56
54
  const mappedUsers = users.map((user) => ({
57
55
  ...user,
58
- id: user.userId,
56
+ id: user.id,
57
+ name: user.name || user.email,
59
58
  }));
60
59
 
61
60
  return {
@@ -1,6 +0,0 @@
1
- import { usersData } from "../mocks";
2
-
3
- export const getUserDetailsService = async (id: string) => {
4
- const userData = usersData.find((user) => user.id === id);
5
- return userData;
6
- };