@vtex/faststore-plugin-buyer-portal 1.1.39 → 1.1.41

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.
@@ -36,6 +36,31 @@ export const TEST_DATA = {
36
36
  TEST_CONTRACT_NAME: "teste_fields",
37
37
  TEST_CONTRACT_ID: "27e05218-d807-11ef-b37f-8dc7bb2ee7fa",
38
38
  },
39
+ USERS: {
40
+ USER_ORG_UNIT_ADMIN: {
41
+ NAME: "User Org Unit Admin",
42
+ ROLE: "Organizational Unit Admin",
43
+ },
44
+ USER_ORG_UNIT_BUYER: {
45
+ NAME: "User Org Unit Buyer",
46
+ ROLE: "Buyer",
47
+ },
48
+ USER_NAME_FOR_ROLES: "User for Test Roles",
49
+ NEW_USER: {
50
+ NAME: `New User ${Date.now().toString(36)}-${Math.random()
51
+ .toString(36)
52
+ .substring(2, 5)}`,
53
+ EMAIL: `new_user+e2e_test${Date.now().toString(36)}-${Math.random()
54
+ .toString(36)
55
+ .substring(2, 5)}@vtex.com`,
56
+ ROLE: "Buyer",
57
+ },
58
+ USER_FROM_OTHER_ORG_UNIT: {
59
+ NAME: "User from other unit",
60
+ EMAIL: "rodrigo.tavares+devb2b@vtex.com",
61
+ ROLE: "Order Approver",
62
+ },
63
+ },
39
64
  BUYING_POLICIES: {
40
65
  NEW_POLICY: {
41
66
  NAME: "Test Buying Policy E2E",
@@ -44,4 +69,14 @@ export const TEST_DATA = {
44
69
  ACTION_TYPE: "Bypass all buying policies",
45
70
  },
46
71
  },
72
+ ADDRESS: {
73
+ EXISTING_ADDRESS: "Existing Test Address",
74
+ NEW_ADDRESS_NAME: "VTEX RJ",
75
+ NEW_ADDRESS_STREET: "Praia de Botafogo, 300",
76
+ EDIT_ADDRESS_STREET: "Praia de Botafogo, 400",
77
+ NEW_ADDRESS_COMPLEMENT: "Segundo Andar",
78
+ NEW_ADDRESS_CITY: "Rio de Janeiro",
79
+ NEW_ADDRESS_STATE: "RJ",
80
+ NEW_ADDRESS_POSTAL_CODE: "22250-905",
81
+ },
47
82
  };
@@ -0,0 +1,275 @@
1
+ import { TEST_CONFIG, TEST_DATA } from "../constants";
2
+ import { clickConfirmButton } from "../shared";
3
+
4
+ function navigateToAddressByName(addressName: string) {
5
+ cy.get("[data-fs-addresses-table]").within(() => {
6
+ cy.contains(addressName).should("be.visible").parent().parent().click();
7
+ });
8
+
9
+ cy.wait(8000);
10
+ }
11
+
12
+ function goToAddressPage() {
13
+ // Navigate to Addresses page
14
+ cy.contains("Addresses").click();
15
+
16
+ // Address Page is loading slowly, custom wait untill we get the request optimized
17
+ cy.wait(10000);
18
+
19
+ // Check to see if the address section loaded
20
+ cy.get("[data-fs-addresses-section]", { timeout: 100000 }).should("exist");
21
+
22
+ // Check to see if contract details loaded correctly
23
+ cy.get("[data-fs-bp-header-inside]").should("exist");
24
+ cy.contains(TEST_DATA.PROFILE.TEST_CONTRACT_NAME).should("be.visible");
25
+ }
26
+
27
+ function openCreateAddressDrawer() {
28
+ cy.get("[data-fs-bp-header-inside]").within(() => {
29
+ cy.get("button").eq(1).click(); // Dropdown triggers
30
+ });
31
+
32
+ cy.get("[data-fs-bp-create-address-drawer]").should("be.visible");
33
+ }
34
+
35
+ function openActionsDropdown(addressName: string) {
36
+ cy.get("[data-fs-addresses-table]")
37
+ .contains(addressName)
38
+ .parent()
39
+ .parent()
40
+ .find("[data-fs-bp-basic-dropdown-menu-trigger]")
41
+ .click();
42
+ }
43
+
44
+ describe("Address Page", () => {
45
+ beforeEach(() => {
46
+ cy.login();
47
+ });
48
+ it("Should load current addresses, create a new one and be able to see it's details", () => {
49
+ // Visit the homepage
50
+ cy.visit(TEST_CONFIG.ROUTES.BUYER_PORTAL);
51
+
52
+ goToAddressPage();
53
+
54
+ // Check Existing Address
55
+ cy.get("[data-fs-addresses-table]").within(() => {
56
+ cy.contains(TEST_DATA.ADDRESS.EXISTING_ADDRESS).should("exist");
57
+ });
58
+
59
+ openCreateAddressDrawer();
60
+
61
+ // Fill The Address Form
62
+ // Open Country Selector
63
+ cy.get("[data-fs-bp-autocomplete-dropdown]")
64
+ .eq(0)
65
+ .click()
66
+ .within(() => {
67
+ cy.get("[data-fs-bp-autocomplete-dropdown-option]").eq(0).click();
68
+ });
69
+
70
+ // Address Name
71
+ cy.get("[data-fs-bp-autocomplete-dropdown]")
72
+ .eq(1)
73
+ .type(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME);
74
+
75
+ // Street
76
+ cy.get("[class=street-address]").type(TEST_DATA.ADDRESS.NEW_ADDRESS_STREET);
77
+
78
+ // Complement
79
+ cy.get("[class=street-address2]").type(
80
+ TEST_DATA.ADDRESS.NEW_ADDRESS_COMPLEMENT
81
+ );
82
+
83
+ // City
84
+ cy.get("[class=address-city]").type(TEST_DATA.ADDRESS.NEW_ADDRESS_CITY);
85
+
86
+ // Rio de Janeiro as State
87
+ cy.get("[data-fs-bp-autocomplete-dropdown]")
88
+ .eq(2)
89
+ .click()
90
+ .within(() => {
91
+ cy.get("[data-fs-bp-autocomplete-dropdown-option]").eq(18).click();
92
+ });
93
+
94
+ // Postal Code
95
+ cy.get("[class=address-postal-code]")
96
+ .click()
97
+ .type(TEST_DATA.ADDRESS.NEW_ADDRESS_POSTAL_CODE);
98
+
99
+ // Mark as Shipping Address
100
+ cy.get("[data-fs-bp-autocomplete-dropdown]")
101
+ .eq(3)
102
+ .click()
103
+ .within(() => {
104
+ cy.get("[data-fs-bp-autocomplete-dropdown-option]").eq(0).click();
105
+ });
106
+
107
+ clickConfirmButton();
108
+
109
+ cy.wait(TEST_CONFIG.TIMEOUTS.PAGE_LOAD);
110
+
111
+ //Sometimes masterdata has cache, the reload helps with that
112
+ cy.reload();
113
+
114
+ // Check to see if address was created
115
+ cy.get("[data-fs-addresses-table]")
116
+ .contains(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME)
117
+ .should("be.visible");
118
+ });
119
+ it("Should be able to edit street in specific Address via address details", () => {
120
+ // Visit the homepage
121
+ cy.visit(TEST_CONFIG.ROUTES.BUYER_PORTAL);
122
+
123
+ // Navigate to Addresses page
124
+ goToAddressPage();
125
+
126
+ // Navigate to Address Details
127
+ navigateToAddressByName(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME);
128
+
129
+ // // Check if sections are rendered correctly
130
+ cy.contains(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME).should("be.visible");
131
+ cy.contains(TEST_DATA.ADDRESS.NEW_ADDRESS_STREET).should("be.visible");
132
+ cy.contains(TEST_DATA.ADDRESS.NEW_ADDRESS_COMPLEMENT).should("be.visible");
133
+ cy.contains(TEST_DATA.ADDRESS.NEW_ADDRESS_POSTAL_CODE).should("be.visible");
134
+
135
+ // Open Edit Drawer
136
+ cy.get("[data-fs-address-details-button]").click();
137
+
138
+ cy.wait(TEST_CONFIG.TIMEOUTS.PAGE_LOAD);
139
+
140
+ // Edit street
141
+ cy.get("[class=street-address]")
142
+ .clear()
143
+ .type(TEST_DATA.ADDRESS.EDIT_ADDRESS_STREET);
144
+
145
+ clickConfirmButton();
146
+
147
+ cy.wait(TEST_CONFIG.TIMEOUTS.PAGE_LOAD);
148
+ cy.reload();
149
+
150
+ // Check to see if information is edited
151
+ cy.contains(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME).should("be.visible");
152
+ cy.contains(TEST_DATA.ADDRESS.EDIT_ADDRESS_STREET).should("be.visible");
153
+ cy.contains(TEST_DATA.ADDRESS.NEW_ADDRESS_COMPLEMENT).should("be.visible");
154
+ cy.contains(TEST_DATA.ADDRESS.NEW_ADDRESS_POSTAL_CODE).should("be.visible");
155
+ });
156
+ it("Should be able to edit the address from the address list page", () => {
157
+ // Visit the homepage
158
+ cy.visit(TEST_CONFIG.ROUTES.BUYER_PORTAL);
159
+
160
+ // Navigate to Addresses page
161
+ goToAddressPage();
162
+
163
+ openActionsDropdown(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME);
164
+
165
+ cy.contains("Edit details").click();
166
+
167
+ // Edit street
168
+ cy.get("[class=street-address]")
169
+ .clear()
170
+ .type(TEST_DATA.ADDRESS.NEW_ADDRESS_STREET);
171
+
172
+ clickConfirmButton();
173
+ cy.wait(TEST_CONFIG.TIMEOUTS.PAGE_LOAD);
174
+ });
175
+
176
+ it("Should be able to set the address as default", () => {
177
+ // Visit the homepage
178
+ cy.visit(TEST_CONFIG.ROUTES.BUYER_PORTAL);
179
+
180
+ // Navigate to Addresses page
181
+ goToAddressPage();
182
+
183
+ openActionsDropdown(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME);
184
+
185
+ cy.contains("Set as default").click();
186
+
187
+ cy.wait(TEST_CONFIG.TIMEOUTS.PAGE_LOAD);
188
+
189
+ // Check to see if is set as default
190
+ cy.get("[data-fs-addresses-table]")
191
+ .contains(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME)
192
+ .parent()
193
+ .parent()
194
+ .get("[data-fs-bp-table-row-default-icon]")
195
+ .should("be.visible");
196
+ });
197
+ it("Should be able to Remove the address from the unit", () => {
198
+ // Visit the homepage
199
+ cy.visit(TEST_CONFIG.ROUTES.BUYER_PORTAL);
200
+
201
+ // Navigate to Addresses page
202
+ goToAddressPage();
203
+
204
+ openActionsDropdown(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME);
205
+
206
+ // Inside the Action dropdown, remove from unit
207
+ cy.contains("Remove from Unit").click();
208
+
209
+ clickConfirmButton();
210
+
211
+ cy.wait(TEST_CONFIG.TIMEOUTS.PAGE_LOAD);
212
+ cy.reload();
213
+
214
+ // Check to see if the address has been removed
215
+ cy.contains(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME).should("not.exist");
216
+ });
217
+
218
+ it("Should be able to Add existing Address", () => {
219
+ // Visit the homepage
220
+ cy.visit(TEST_CONFIG.ROUTES.BUYER_PORTAL);
221
+
222
+ // Navigate to Addresses page
223
+ goToAddressPage();
224
+
225
+ // Check to see if address is really absent
226
+ cy.contains(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME).should("not.exist");
227
+
228
+ openCreateAddressDrawer();
229
+
230
+ // Address Name with existing address
231
+ cy.get('[data-fs-bp-input-text="true"]')
232
+ .eq(1)
233
+ .should("be.visible")
234
+ .type(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME, { delay: 500 });
235
+
236
+ // Select existing address
237
+ cy.get('[data-fs-bp-autocomplete-dropdown-menu="true"]')
238
+ .contains(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME)
239
+ .should("exist")
240
+ .click();
241
+
242
+ clickConfirmButton();
243
+ cy.wait(TEST_CONFIG.TIMEOUTS.PAGE_LOAD);
244
+
245
+ cy.reload();
246
+
247
+ // Check to see if address has been added
248
+ cy.contains(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME).should("be.visible");
249
+ });
250
+
251
+ it("Should be able to delete the address", () => {
252
+ // Visit the homepage
253
+ cy.visit(TEST_CONFIG.ROUTES.BUYER_PORTAL);
254
+
255
+ // Navigate to Addresses page
256
+ goToAddressPage();
257
+
258
+ openActionsDropdown(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME);
259
+
260
+ cy.contains("Delete").click();
261
+
262
+ // Fill check to allow deletion
263
+ cy.get("[class=address-name-confirmation]").type(
264
+ TEST_DATA.ADDRESS.NEW_ADDRESS_NAME
265
+ );
266
+
267
+ cy.get('[data-fs-bp-basic-drawer-button-variant="danger"]').click();
268
+
269
+ cy.wait(TEST_CONFIG.TIMEOUTS.PAGE_LOAD);
270
+ cy.reload();
271
+
272
+ // Check if address has been deleted
273
+ cy.contains(TEST_DATA.ADDRESS.NEW_ADDRESS_NAME).should("not.exist");
274
+ });
275
+ });
@@ -0,0 +1,551 @@
1
+ import { TEST_CONFIG, TEST_DATA } from "../constants";
2
+
3
+ const navigateToUsers = () => {
4
+ cy.visit(TEST_CONFIG.ROUTES.BUYER_PORTAL);
5
+ cy.contains("Users").click();
6
+
7
+ cy.wait(TEST_CONFIG.TIMEOUTS.RETRY_DELAY);
8
+ cy.get("[data-fs-users-section]").should("exist");
9
+ };
10
+
11
+ const verifyUserState = (
12
+ userName: string,
13
+ shouldExist: boolean,
14
+ maxRetries = TEST_CONFIG.RETRY.DEFAULT_MAX_ATTEMPTS
15
+ ) => {
16
+ const verifyAndRetry = (currentRetry = 0) => {
17
+ cy.get("[data-fs-users-section]").should("exist");
18
+ cy.get("[data-fs-bp-base-tabs-layout-content]").scrollTo("bottom");
19
+
20
+ cy.get("body").then(($body) => {
21
+ const exists = $body.text().includes(userName);
22
+ if (exists !== shouldExist && currentRetry < maxRetries) {
23
+ cy.log(`Retry attempt ${currentRetry + 1} of ${maxRetries}`);
24
+ cy.wait(TEST_CONFIG.TIMEOUTS.RETRY_DELAY);
25
+ cy.reload();
26
+ verifyAndRetry(currentRetry + 1);
27
+ } else if (exists !== shouldExist) {
28
+ throw new Error(
29
+ `Failed to verify user state after ${maxRetries} retries`
30
+ );
31
+ }
32
+ });
33
+ };
34
+
35
+ verifyAndRetry();
36
+
37
+ if (shouldExist) {
38
+ cy.contains(userName).should("be.visible");
39
+ } else {
40
+ cy.contains(userName).should("not.exist");
41
+ }
42
+ };
43
+
44
+ const fillUserForm = (
45
+ userData: typeof TEST_DATA.USERS.NEW_USER,
46
+ shouldUpdateEmailAndRole = true
47
+ ) => {
48
+ // Fill the user name
49
+ cy.get("label")
50
+ .contains("Full Name")
51
+ .parent()
52
+ .find("[data-fs-bp-input-text-input]")
53
+ .clear()
54
+ .type(userData.NAME);
55
+
56
+ if (shouldUpdateEmailAndRole) {
57
+ // Fill the user email
58
+ cy.get("label")
59
+ .contains("Email")
60
+ .parent()
61
+ .find("[data-fs-bp-input-text-input]")
62
+ .clear()
63
+ .type(userData.EMAIL);
64
+
65
+ cy.wait(500);
66
+ // Fill the user role
67
+ cy.get('[data-fs-bp-create-user-roles="true"]')
68
+ .contains(userData.ROLE)
69
+ .parents('[data-fs-bp-create-user-role-wrapper="true"]')
70
+ .within(() => {
71
+ cy.get('[type="checkbox"]').check();
72
+ });
73
+ }
74
+ };
75
+
76
+ const openUserDropdown = (userName: string) => {
77
+ cy.get("[data-fs-bp-table-row]")
78
+ .contains(userName)
79
+ .parents("[data-fs-bp-table-row]")
80
+ .within(() => {
81
+ cy.get("[data-fs-bp-basic-dropdown-menu-trigger]").click();
82
+ });
83
+ };
84
+
85
+ const confirmDeletion = (userName: string) => {
86
+ const tryDelete = (attempt = 0) => {
87
+ cy.contains("Delete user").should("be.visible");
88
+ cy.contains("Confirm by typing the user name below:").should("be.visible");
89
+
90
+ cy.get("label")
91
+ .contains("User name")
92
+ .parent()
93
+ .find("[data-fs-bp-input-text-input]")
94
+ .clear()
95
+ .type(userName);
96
+
97
+ cy.contains("button", "Delete").click();
98
+
99
+ cy.wait(TEST_CONFIG.TIMEOUTS.PAGE_LOAD);
100
+
101
+ cy.get("body").then(($body) => {
102
+ const errorOccurred = $body
103
+ .text()
104
+ .includes("An error occurred while deleting the user");
105
+ if (errorOccurred && attempt < TEST_CONFIG.RETRY.DEFAULT_MAX_ATTEMPTS) {
106
+ cy.log(`Retrying delete attempt ${attempt + 1}`);
107
+ cy.wait(TEST_CONFIG.TIMEOUTS.RETRY_DELAY);
108
+ tryDelete(attempt + 1);
109
+ } else if (errorOccurred) {
110
+ throw new Error(
111
+ `Failed to delete user after ${TEST_CONFIG.RETRY.DEFAULT_MAX_ATTEMPTS} retries due to error`
112
+ );
113
+ }
114
+ });
115
+ };
116
+
117
+ tryDelete();
118
+ };
119
+
120
+ describe("Users", () => {
121
+ beforeEach(() => {
122
+ cy.login();
123
+ });
124
+
125
+ it("Should load and display users information correctly (User List)", () => {
126
+ navigateToUsers();
127
+
128
+ // Check that the table columns are present
129
+ cy.contains("Name").should("be.visible");
130
+ cy.contains("Role").should("be.visible");
131
+
132
+ // Check that the expected users are present in the table
133
+ cy.contains(TEST_DATA.USERS.USER_ORG_UNIT_ADMIN.NAME)
134
+ .scrollIntoView()
135
+ .should("be.visible")
136
+ .parent()
137
+ .parent()
138
+ .within(() => {
139
+ cy.contains(TEST_DATA.USERS.USER_ORG_UNIT_ADMIN.ROLE)
140
+ .scrollIntoView()
141
+ .should("be.visible");
142
+ cy.get("[data-fs-bp-basic-dropdown-menu-trigger]")
143
+ .should("be.visible")
144
+ .click();
145
+ });
146
+
147
+ // Check if the menu options are visible
148
+ cy.contains("Open").should("be.visible");
149
+ cy.contains("Edit details").should("be.visible");
150
+ cy.contains("Change organizational unit").should("be.visible");
151
+ cy.contains("Delete").should("be.visible");
152
+ });
153
+
154
+ it("Should load user details page successfully", () => {
155
+ navigateToUsers();
156
+
157
+ // Click on User Test 1 to go to details page
158
+ cy.contains(TEST_DATA.USERS.USER_ORG_UNIT_ADMIN.NAME)
159
+ .scrollIntoView()
160
+ .should("be.visible")
161
+ .parents("[data-fs-bp-table-row]")
162
+ .click();
163
+
164
+ cy.wait(TEST_CONFIG.TIMEOUTS.PAGE_LOAD);
165
+
166
+ // Check that the user details section is visible
167
+ cy.get("[data-fs-user-details-section]").should("exist");
168
+
169
+ // Check that the user's name is displayed in the details
170
+ cy.get("[data-fs-user-details-row-value]")
171
+ .contains(TEST_DATA.USERS.USER_ORG_UNIT_ADMIN.NAME)
172
+ .should("be.visible");
173
+
174
+ cy.get("[data-fs-user-details-row-value]")
175
+ .contains(TEST_DATA.USERS.USER_ORG_UNIT_ADMIN.ROLE)
176
+ .should("be.visible");
177
+
178
+ // Verify labels and buttons are present
179
+ cy.contains("Name").should("be.visible");
180
+ cy.contains("Email").should("be.visible");
181
+ cy.contains("Role").should("be.visible");
182
+ cy.contains("Organizational Unit").should("be.visible");
183
+ cy.contains("Edit").should("be.visible");
184
+ });
185
+
186
+ it("Should add a new user", () => {
187
+ navigateToUsers();
188
+
189
+ // === CREATE USER ===
190
+ cy.log("=== Creating new user ===");
191
+
192
+ // Click to add a new user
193
+ cy.get("[data-fs-header-inside-button]").should("be.visible").click();
194
+
195
+ // Fill user form
196
+ fillUserForm(TEST_DATA.USERS.NEW_USER);
197
+
198
+ // Submit form
199
+ cy.contains("button", "Add").click();
200
+
201
+ // Verify success message
202
+ cy.contains("User successfully added").should("be.visible");
203
+
204
+ // Reload and verify user was created
205
+ verifyUserState(TEST_DATA.USERS.NEW_USER.NAME, true);
206
+ });
207
+
208
+ it("Should view and edit the newly created user", () => {
209
+ navigateToUsers();
210
+
211
+ // === VIEW USER DETAILS ===
212
+ cy.log("=== Viewing user details ===");
213
+ cy.get("[data-fs-bp-base-tabs-layout-content]").scrollTo("bottom");
214
+
215
+ cy.contains(TEST_DATA.USERS.NEW_USER.NAME)
216
+ .should("be.visible")
217
+ .parents("[data-fs-bp-table-row]")
218
+ .click();
219
+ cy.wait(TEST_CONFIG.TIMEOUTS.PAGE_LOAD);
220
+
221
+ // Verify user details page
222
+ cy.get("[data-fs-user-details-section]").should("exist");
223
+ cy.get("[data-fs-user-details-row-value]")
224
+ .contains(TEST_DATA.USERS.NEW_USER.ROLE)
225
+ .should("be.visible");
226
+
227
+ // Go back to users list
228
+ navigateToUsers();
229
+
230
+ // === EDIT USER ===
231
+ cy.log("=== Editing user ===");
232
+
233
+ openUserDropdown(TEST_DATA.USERS.NEW_USER.NAME);
234
+ cy.contains("Edit details").click();
235
+
236
+ // Verify edit drawer is open
237
+ cy.contains("Edit user details").should("be.visible");
238
+
239
+ cy.wait(TEST_CONFIG.TIMEOUTS.PAGE_LOAD);
240
+
241
+ // Update user information
242
+ const updatedUserData = {
243
+ NAME: `${TEST_DATA.USERS.NEW_USER.NAME} Updated`,
244
+ EMAIL: TEST_DATA.USERS.NEW_USER.EMAIL,
245
+ ROLE: TEST_DATA.USERS.NEW_USER.ROLE,
246
+ };
247
+
248
+ fillUserForm(updatedUserData, false);
249
+
250
+ // Save changes
251
+ cy.contains("button", "Save").click();
252
+
253
+ // Verify update
254
+ verifyUserState(updatedUserData.NAME, true);
255
+ });
256
+
257
+ it("Should delete the updated user", () => {
258
+ navigateToUsers();
259
+
260
+ // === DELETE USER ===
261
+ cy.log("=== Deleting user ===");
262
+ cy.get("[data-fs-bp-base-tabs-layout-content]").scrollTo("bottom");
263
+
264
+ const updatedUserData = {
265
+ NAME: `${TEST_DATA.USERS.NEW_USER.NAME} Updated`,
266
+ EMAIL: TEST_DATA.USERS.NEW_USER.EMAIL,
267
+ ROLE: TEST_DATA.USERS.NEW_USER.ROLE,
268
+ };
269
+
270
+ openUserDropdown(updatedUserData.NAME);
271
+ cy.contains("Delete").click();
272
+
273
+ cy.get(`a:contains("${updatedUserData.NAME}")`)
274
+ .invoke("text")
275
+ .then((userNameText) => {
276
+ confirmDeletion(userNameText);
277
+ });
278
+
279
+ // Verify if the user was deleted
280
+ verifyUserState(updatedUserData.NAME, false);
281
+ });
282
+
283
+ it("Should not allow adding a user already in another org unit", () => {
284
+ navigateToUsers();
285
+
286
+ // Click to add a new user
287
+ cy.get("[data-fs-header-inside-button]").should("be.visible").click();
288
+
289
+ // Fill form with existing user data - Org Unit for E2E Tests Child 3
290
+ fillUserForm(TEST_DATA.USERS.USER_FROM_OTHER_ORG_UNIT);
291
+
292
+ // Submit form
293
+ cy.contains("button", "Add").click();
294
+
295
+ // Assert error message is shown
296
+ cy.contains("User cannot be added").should("be.visible");
297
+ cy.contains("This user is assigned to another organizational unit.").should(
298
+ "be.visible"
299
+ );
300
+ cy.contains(
301
+ "Users can only belong to one organizational unit at a time."
302
+ ).should("be.visible");
303
+
304
+ // Close error dialog
305
+ cy.contains("Ok").click();
306
+ });
307
+
308
+ it("Should create a new user add, edit and remove roles", () => {
309
+ // Visit the buyer portal home page
310
+ cy.visit(TEST_CONFIG.ROUTES.BUYER_PORTAL);
311
+
312
+ // Click on the "Users" link in the sidebar menu
313
+ cy.get("a[data-fs-vertical-nav-menu-link]").contains("Users").click();
314
+
315
+ // Check that the Users page is rendered
316
+ cy.get('h1[data-fs-bp-header-inside-title="true"]').should(
317
+ "contain",
318
+ "Users"
319
+ );
320
+
321
+ // Check that at least one user row exists
322
+ cy.get('a[data-fs-bp-table-row-link="true"]').should("exist");
323
+
324
+ // Click the add user button at the top
325
+ cy.get('button[data-fs-header-inside-button="true"]').click();
326
+
327
+ // Check that the user creation modal is open
328
+ cy.get(
329
+ 'div[data-fs-modal-content="true"][data-fs-bp-create-user-drawer="true"]'
330
+ ).should("be.visible");
331
+ cy.get('h2[data-fs-bp-basic-drawer-heading-title="true"]').should(
332
+ "contain",
333
+ "Add User"
334
+ );
335
+
336
+ // Fill out the user creation form
337
+ cy.get('input[data-fs-bp-input-text-input="true"]')
338
+ .first()
339
+ .type(TEST_DATA.USERS.USER_NAME_FOR_ROLES); // Full Name
340
+ cy.get('input[data-fs-bp-input-text-input="true"]')
341
+ .eq(1)
342
+ .type("userfortestroles@vtex.com"); // Email
343
+ cy.get('input[type="checkbox"][value="Buyer"]').check(); // Assign Buyer role
344
+
345
+ // Click the "Add" button to submit the form
346
+ cy.get(
347
+ 'button[data-fs-bp-basic-drawer-button="true"][data-fs-bp-basic-drawer-button-variant="confirm"]'
348
+ )
349
+ .should("not.be.disabled")
350
+ .click();
351
+
352
+ cy.wait(3000);
353
+
354
+ // Retry logic to check if user appears in the list
355
+ const checkUserExists = (attempt = 1) => {
356
+ cy.reload();
357
+ cy.get("body").then(($body) => {
358
+ if ($body.text().includes(TEST_DATA.USERS.USER_NAME_FOR_ROLES)) {
359
+ // User found, verify it exists
360
+ cy.contains("td", TEST_DATA.USERS.USER_NAME_FOR_ROLES).should(
361
+ "exist"
362
+ );
363
+ } else if (attempt < 3) {
364
+ cy.log(`User not found, retry attempt ${attempt + 1} of 3`);
365
+ cy.wait(2000);
366
+ checkUserExists(attempt + 1);
367
+ } else {
368
+ throw new Error(
369
+ `User ${TEST_DATA.USERS.USER_NAME_FOR_ROLES} not found after 3 attempts`
370
+ );
371
+ }
372
+ });
373
+ };
374
+
375
+ checkUserExists();
376
+
377
+ // Check that the 'Buyer' badge is present in the same row
378
+ cy.contains(
379
+ 'tr[data-fs-bp-table-row="true"]',
380
+ TEST_DATA.USERS.USER_NAME_FOR_ROLES
381
+ )
382
+ .find('span[data-fs-tag="true"]')
383
+ .should("contain.text", "Buyer");
384
+
385
+ // --- Edit user roles: add Organizational Unit Admin ---
386
+ cy.contains(
387
+ 'tr[data-fs-bp-table-row="true"]',
388
+ TEST_DATA.USERS.USER_NAME_FOR_ROLES
389
+ )
390
+ .find('button[data-fs-bp-basic-dropdown-menu-trigger="true"]')
391
+ .click();
392
+ cy.get(
393
+ 'div[data-fs-dropdown-menu="true"] button[data-fs-dropdown-item="true"]'
394
+ )
395
+ .contains("Edit details")
396
+ .click();
397
+ cy.get(
398
+ 'div[data-fs-modal-content="true"][data-fs-bp-update-user-drawer="true"]'
399
+ ).should("be.visible");
400
+ cy.get('h2[data-fs-bp-basic-drawer-heading-title="true"]').should(
401
+ "contain",
402
+ "Edit user details"
403
+ );
404
+ cy.get("input#role-organizational-unit\\ admin").check({ force: true });
405
+ cy.get(
406
+ 'button[data-fs-bp-basic-drawer-button="true"][data-fs-bp-basic-drawer-button-variant="confirm"]'
407
+ )
408
+ .should("not.be.disabled")
409
+ .click();
410
+
411
+ cy.wait(1000);
412
+ cy.reload();
413
+ cy.contains(
414
+ 'tr[data-fs-bp-table-row="true"]',
415
+ TEST_DATA.USERS.USER_NAME_FOR_ROLES
416
+ )
417
+ .find('span[data-fs-tag="true"]')
418
+ .should("contain.text", "Organizational Unit Admin");
419
+
420
+ // --- Edit user roles: remove Organizational Unit Admin ---
421
+ cy.contains(
422
+ 'tr[data-fs-bp-table-row="true"]',
423
+ TEST_DATA.USERS.USER_NAME_FOR_ROLES
424
+ )
425
+ .find('button[data-fs-bp-basic-dropdown-menu-trigger="true"]')
426
+ .click();
427
+ cy.get(
428
+ 'div[data-fs-dropdown-menu="true"] button[data-fs-dropdown-item="true"]'
429
+ )
430
+ .contains("Edit details")
431
+ .click();
432
+ cy.get(
433
+ 'div[data-fs-modal-content="true"][data-fs-bp-update-user-drawer="true"]'
434
+ ).should("be.visible");
435
+ cy.get("input#role-organizational-unit\\ admin").uncheck({ force: true });
436
+ cy.get(
437
+ 'button[data-fs-bp-basic-drawer-button="true"][data-fs-bp-basic-drawer-button-variant="confirm"]'
438
+ )
439
+ .should("not.be.disabled")
440
+ .click();
441
+
442
+ cy.wait(1000);
443
+ cy.reload();
444
+ cy.contains(
445
+ 'tr[data-fs-bp-table-row="true"]',
446
+ TEST_DATA.USERS.USER_NAME_FOR_ROLES
447
+ )
448
+ .find('span[data-fs-tag="true"]')
449
+ .should("not.contain.text", "Organizational Unit Admin");
450
+
451
+ // Open the dropdown menu (three dots) for the created user
452
+ cy.contains(
453
+ 'tr[data-fs-bp-table-row="true"]',
454
+ TEST_DATA.USERS.USER_NAME_FOR_ROLES
455
+ )
456
+ .find('button[data-fs-bp-basic-dropdown-menu-trigger="true"]')
457
+ .click();
458
+
459
+ // Click the "Delete" option in the dropdown menu
460
+ cy.get(
461
+ 'div[data-fs-dropdown-menu="true"] button[data-fs-dropdown-item="true"]'
462
+ )
463
+ .contains("Delete")
464
+ .click();
465
+
466
+ // Check that the delete user modal is open
467
+ cy.get(
468
+ 'div[data-fs-modal-content="true"][data-fs-bp-delete-user-drawer="true"]'
469
+ ).should("be.visible");
470
+ cy.get('h2[data-fs-bp-basic-drawer-heading-title="true"]').should(
471
+ "contain",
472
+ "Delete user"
473
+ );
474
+
475
+ // Type the user name to confirm deletion
476
+ cy.get('input[data-fs-bp-input-text-input="true"]').type(
477
+ TEST_DATA.USERS.USER_NAME_FOR_ROLES
478
+ );
479
+
480
+ cy.wait(1000);
481
+
482
+ // Click the "Delete" button to confirm
483
+ cy.get(
484
+ 'button[data-fs-bp-basic-drawer-button="true"][data-fs-bp-basic-drawer-button-variant="danger"]'
485
+ )
486
+ .should("not.be.disabled")
487
+ .click();
488
+
489
+ cy.wait(1000);
490
+ // Reload the users page
491
+ cy.reload();
492
+
493
+ // Check that the user no longer exists in the list
494
+ cy.contains("td", TEST_DATA.USERS.USER_NAME_FOR_ROLES).should("not.exist");
495
+ });
496
+
497
+ it("Should allow changing the organizational unit from the list dropdown menu", () => {
498
+ navigateToUsers();
499
+
500
+ // Open dropdown for the test user
501
+ cy.get("[data-fs-header-inside-button]").should("be.visible").click();
502
+ fillUserForm(TEST_DATA.USERS.NEW_USER);
503
+
504
+ cy.contains("button", "Add").click();
505
+ cy.wait(TEST_CONFIG.TIMEOUTS.PAGE_LOAD);
506
+
507
+ verifyUserState(TEST_DATA.USERS.NEW_USER.NAME, true);
508
+
509
+ openUserDropdown(TEST_DATA.USERS.NEW_USER.NAME);
510
+ cy.contains("Change organizational unit").click();
511
+
512
+ // Verify change org unit modal/drawer is open
513
+ cy.contains("Change user organizational unit").should("be.visible");
514
+ cy.contains(
515
+ `Users can belong to only one Organizational Unit. Search and select a new unit for ${TEST_DATA.USERS.NEW_USER.NAME}`
516
+ ).should("be.visible");
517
+
518
+ cy.get('[data-fs-bp-input-text="true"]')
519
+ .should("be.visible")
520
+ .type(TEST_DATA.ORG_UNITS.ROOT, { delay: 1000 });
521
+
522
+ cy.wait(0);
523
+
524
+ // Wait for and click on the option in the dropdown
525
+ cy.get('[data-fs-bp-autocomplete-dropdown-menu="true"]')
526
+ .contains(TEST_DATA.ORG_UNITS.CHILD_2, { timeout: 10000 })
527
+ .should("exist")
528
+ .click();
529
+
530
+ // Confirm change
531
+ cy.contains("button", "Save").click();
532
+
533
+ // Reload and verify user is now in the new org unit
534
+ cy.reload();
535
+
536
+ // Check if the user is not on the same org unit
537
+ verifyUserState(TEST_DATA.USERS.NEW_USER.NAME, false);
538
+ });
539
+
540
+ it.skip("Should not allow deleting the last admin user", () => {
541
+ // TODO: Implement this test
542
+ });
543
+
544
+ it.skip("Should not allow moving the last admin user", () => {
545
+ // TODO: Implement this test
546
+ });
547
+
548
+ it.skip("Should not allow removing the role from the last admin user", () => {
549
+ // TODO: Implement this test
550
+ });
551
+ });
@@ -0,0 +1,3 @@
1
+ export function clickConfirmButton() {
2
+ cy.get('[data-fs-bp-basic-drawer-button-variant="confirm"]').click();
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.1.39",
3
+ "version": "1.1.41",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -10,7 +10,7 @@ import {
10
10
  DeleteAddressDrawer,
11
11
  } from "..";
12
12
  import { BasicDropdownMenu, Icon } from "../../../shared/components";
13
- import { useDrawerProps, useBuyerPortal } from "../../../shared/hooks";
13
+ import { useDrawerProps } from "../../../shared/hooks";
14
14
  import { ADDRESS_MESSAGES } from "../../constants/messages";
15
15
  import { useSetDefaultAddress } from "../../hooks/useSetDefaultAddress";
16
16
  import { useUnsetDefaultAddress } from "../../hooks/useUnsetDefaultAddress";
@@ -28,7 +28,6 @@ export const AddressDropdownMenu = ({
28
28
  currentAddress,
29
29
  onUpdate,
30
30
  }: AddressDropdownMenuProps) => {
31
- const { currentOrgUnit } = useBuyerPortal();
32
31
  const router = useRouter();
33
32
  const { pushToast } = useUI();
34
33
 
@@ -107,9 +106,8 @@ export const AddressDropdownMenu = ({
107
106
  });
108
107
  };
109
108
 
110
- const idsPath = currentOrgUnit?.path?.ids?.split("/") ?? [];
111
-
112
- const isRootLevelOrgUnit = idsPath.length === 2;
109
+ // const idsPath = currentOrgUnit?.path?.ids?.split("/") ?? [];
110
+ // const isRootLevelOrgUnit = idsPath.length === 2;
113
111
 
114
112
  const {
115
113
  open: openEditDrawerProps,
@@ -175,15 +173,16 @@ export const AddressDropdownMenu = ({
175
173
  <UIIcon name="MinusCircle" {...sizeProps} />
176
174
  Remove from Unit
177
175
  </DropdownItem>
178
- {isRootLevelOrgUnit && (
179
- <DropdownItem
180
- onClick={openDeleteAddressDrawer}
181
- data-fs-bp-dropdown-menu-item-mode="danger"
182
- >
183
- <Icon name="Delete" {...sizeProps} data-fs-bp-delete-address />
184
- <span data-fs-bp-delete-address>Delete</span>
185
- </DropdownItem>
186
- )}
176
+ {/* Remove validation while we reevaluate the rule */}
177
+ {/* {isRootLevelOrgUnit && ( */}
178
+ <DropdownItem
179
+ onClick={openDeleteAddressDrawer}
180
+ data-fs-bp-dropdown-menu-item-mode="danger"
181
+ >
182
+ <Icon name="Delete" {...sizeProps} data-fs-bp-delete-address />
183
+ <span data-fs-bp-delete-address>Delete</span>
184
+ </DropdownItem>
185
+ {/* )} */}
187
186
  </BasicDropdownMenu>
188
187
  {isEditDrawerOpen && (
189
188
  <EditAddressDrawer
@@ -11,7 +11,7 @@ import { useBuyerPortal } from "../../../shared/hooks";
11
11
  import { maskPostalCode } from "../../../shared/utils/postalCode";
12
12
  import { CountryOptions } from "../../data/countries";
13
13
  import { states as StateOptions } from "../../data/states";
14
- import { useDebouncedSearchAddressByUnitId } from "../../hooks/useDebouncedSearchAddressByUnitId";
14
+ import { useDebouncedSearchAddress } from "../../hooks/useDebouncedSearchAddress";
15
15
  import { AddressData, AddressInput } from "../../types";
16
16
 
17
17
  export type AddressFormProps = {
@@ -45,10 +45,10 @@ export const AddressForm = ({
45
45
 
46
46
  const [autoCompleteAddressName, setAutoCompleteAddressName] = useState("");
47
47
 
48
- const { searchedAddresses } = useDebouncedSearchAddressByUnitId({
49
- orgUnitId: currentOrgUnit?.id ?? "",
50
- search: autoCompleteAddressName,
51
- });
48
+ const { searchedAddresses } = useDebouncedSearchAddress(
49
+ autoCompleteAddressName,
50
+ currentOrgUnit?.id ?? ""
51
+ );
52
52
 
53
53
  const cleanCompletedAddress = () => {
54
54
  if (setCompletedAddress && setUseExistingAddress) {
@@ -75,6 +75,7 @@ export const AddressForm = ({
75
75
  <AutocompleteDropdown
76
76
  data-fs-bp-create-address-country-selector
77
77
  label="Country"
78
+ className="address-country"
78
79
  value={address.country}
79
80
  options={CountryOptions}
80
81
  onConfirmKeyPress={(option) =>
@@ -110,6 +111,7 @@ export const AddressForm = ({
110
111
  <InputText
111
112
  label="Address Name"
112
113
  value={address.name}
114
+ className="address-name"
113
115
  wrapperProps={{ style: { marginTop: 16 } }}
114
116
  hasError={isTouched && !address.name?.trim()}
115
117
  onChange={(event) =>
@@ -120,6 +122,7 @@ export const AddressForm = ({
120
122
  <AutocompleteDropdown
121
123
  label="Address Name"
122
124
  value={autoCompleteAddressName}
125
+ className="address-name"
123
126
  options={searchedAddresses}
124
127
  onChange={(event) => {
125
128
  setAutoCompleteAddressName(event.currentTarget.value);
@@ -157,6 +160,7 @@ export const AddressForm = ({
157
160
  <InputText
158
161
  label="Street Address"
159
162
  value={address.streetAddress}
163
+ className="street-address"
160
164
  wrapperProps={{ style: { marginTop: 16 } }}
161
165
  hasError={isTouched && !address.streetAddress?.trim()}
162
166
  onChange={(event) =>
@@ -171,6 +175,7 @@ export const AddressForm = ({
171
175
 
172
176
  <InputText
173
177
  label="Apt, Suite, Building (optional)"
178
+ className="street-address2"
174
179
  value={address.streetAddress2}
175
180
  wrapperProps={{ style: { marginTop: 16, marginBottom: 16 } }}
176
181
  onChange={(event) =>
@@ -180,6 +185,7 @@ export const AddressForm = ({
180
185
 
181
186
  <InputText
182
187
  label="City"
188
+ className="address-city"
183
189
  value={address.city}
184
190
  wrapperProps={{ style: { marginTop: 16 } }}
185
191
  hasError={isTouched && !address.city?.trim()}
@@ -191,6 +197,7 @@ export const AddressForm = ({
191
197
  <AutocompleteDropdown
192
198
  data-fs-bp-create-address-state-selector
193
199
  label="State"
200
+ className="address-state"
194
201
  value={address.state}
195
202
  options={
196
203
  address.countryCode ? StateOptions[address.countryCode] : []
@@ -221,6 +228,7 @@ export const AddressForm = ({
221
228
  <InputText
222
229
  label="Postal Code"
223
230
  value={address.zip}
231
+ className="address-postal-code"
224
232
  wrapperProps={{ style: { marginTop: 16, marginBottom: 16 } }}
225
233
  hasError={isTouched && !address.zip?.trim()}
226
234
  onChange={(event) => handleChangePostalCode(event.target.value)}
@@ -252,6 +260,7 @@ export const AddressForm = ({
252
260
  <AutocompleteDropdown
253
261
  label="Address Type"
254
262
  value={address.types}
263
+ className="address-type"
255
264
  options={addressTypeOptions}
256
265
  onConfirmKeyPress={(option) =>
257
266
  setAddress({ ...address, types: [option] })
@@ -77,6 +77,7 @@ export const DeleteAddressDrawer = ({
77
77
  <br />
78
78
  <InputText
79
79
  label="Address name"
80
+ className="address-name-confirmation"
80
81
  value={addressNameConfirmation}
81
82
  onChange={(event) =>
82
83
  setAddressNameConfirmation(event.currentTarget.value)
@@ -1,3 +1,4 @@
1
+ import { scopesClient } from "../../shared/clients/ScopeClient";
1
2
  import { addressesClient } from "../clients/AddressesClient";
2
3
 
3
4
  import type { AddressInput } from "../types";
@@ -8,14 +9,26 @@ export type CreateNewAddressServiceProps = {
8
9
  unitId: string;
9
10
  } & AddressInput;
10
11
 
12
+ type NewAddress = {
13
+ newAddressId: string;
14
+ };
15
+
11
16
  export const createNewAddressService = async ({
12
17
  cookie,
13
18
  ...data
14
19
  }: CreateNewAddressServiceProps) => {
15
- return addressesClient.createNewAddress(
20
+ const newAddress = (await addressesClient.createNewAddress(
16
21
  data,
17
22
  data.unitId,
18
23
  data.customerId,
19
24
  cookie
25
+ )) as NewAddress;
26
+
27
+ await scopesClient.addToScope(
28
+ data.unitId,
29
+ { scopeName: "addresses", ids: [newAddress.newAddressId] },
30
+ cookie
20
31
  );
32
+
33
+ return newAddress;
21
34
  };
@@ -20,8 +20,8 @@ export const searchAddressByNameService = async (
20
20
  }
21
21
 
22
22
  const { addresses } = await addressesClient.searchAddressesByName(
23
- customerId,
24
23
  unitId,
24
+ customerId,
25
25
  name,
26
26
  cookie
27
27
  );
@@ -3,7 +3,7 @@ export type CollectionsSummary = {
3
3
  name: string;
4
4
  };
5
5
 
6
- export type ContractCollection = CollectionsSummary & { inContract: boolean };
6
+ export type ContractCollection = CollectionsSummary & { isEnabled: boolean };
7
7
  export type ScopeCollection = CollectionsSummary & { isEnabled: boolean };
8
8
 
9
9
  export type GetCollectionsFromContractResponse = {
@@ -31,7 +31,7 @@ export const DeleteUserDrawer = ({
31
31
 
32
32
  const handleRemoveSuccess = () => {
33
33
  pushToast({
34
- message: "User successfully edited",
34
+ message: "User successfully deleted",
35
35
  status: "INFO",
36
36
  });
37
37
  onDelete?.();
@@ -49,15 +49,16 @@ export const DeleteUserDrawer = ({
49
49
  });
50
50
  };
51
51
 
52
- const { removeUserFromOrgUnit } = useRemoveUserFromOrgUnit({
53
- onSuccess: handleRemoveSuccess,
54
- onError: () => {
55
- pushToast({
56
- message: "An error occurred while deleting the user",
57
- status: "ERROR",
58
- });
59
- },
60
- });
52
+ const { removeUserFromOrgUnit, isRemoveUserFromOrgUnitLoading } =
53
+ useRemoveUserFromOrgUnit({
54
+ onSuccess: handleRemoveSuccess,
55
+ onError: () => {
56
+ pushToast({
57
+ message: "An error occurred while deleting the user",
58
+ status: "ERROR",
59
+ });
60
+ },
61
+ });
61
62
 
62
63
  const isDeleteButtonEnabled = userNameConfirmation === user.name;
63
64
 
@@ -72,6 +73,7 @@ export const DeleteUserDrawer = ({
72
73
  orgUnitId: orgUnit?.id || "",
73
74
  userId: user.id,
74
75
  })}
76
+ data-fs-bp-delete-user-drawer-user-name
75
77
  >
76
78
  {user.name}
77
79
  </a>
@@ -100,7 +102,8 @@ export const DeleteUserDrawer = ({
100
102
  Cancel
101
103
  </BasicDrawer.Button>
102
104
  <BasicDrawer.Button
103
- variant="confirm"
105
+ variant="danger"
106
+ isLoading={isRemoveUserFromOrgUnitLoading}
104
107
  onClick={handleDeleteClick}
105
108
  disabled={!isDeleteButtonEnabled}
106
109
  >
@@ -43,6 +43,10 @@ export const UserDropdownMenu = ({ user }: UserDropdownMenuProps) => {
43
43
  ...updateUserDrawerProps
44
44
  } = useDrawerProps();
45
45
 
46
+ const handleUpdate = () => {
47
+ route.reload();
48
+ };
49
+
46
50
  return (
47
51
  <>
48
52
  <BasicDropdownMenu>
@@ -80,6 +84,7 @@ export const UserDropdownMenu = ({ user }: UserDropdownMenuProps) => {
80
84
  <DeleteUserDrawer
81
85
  user={user}
82
86
  isOpen={isDeleteUserDrawerOpen}
87
+ onDelete={handleUpdate}
83
88
  {...deleteUserDrawerProps}
84
89
  />
85
90
  )}
@@ -87,6 +92,7 @@ export const UserDropdownMenu = ({ user }: UserDropdownMenuProps) => {
87
92
  <ReassignOrgUnitDrawer
88
93
  user={user}
89
94
  isOpen={isReassignOrgUnitDrawerOpen}
95
+ onReassing={handleUpdate}
90
96
  {...reassignOrgUnitDrawerProps}
91
97
  />
92
98
  )}
@@ -96,6 +102,7 @@ export const UserDropdownMenu = ({ user }: UserDropdownMenuProps) => {
96
102
  userId={user.id}
97
103
  orgUnitId={currentOrgUnit?.id ?? ""}
98
104
  isOpen={isUpdateUserDrawerOpen}
105
+ onCreate={handleUpdate}
99
106
  {...updateUserDrawerProps}
100
107
  />
101
108
  )}
@@ -89,7 +89,10 @@ export const UserDetailsLayout = ({
89
89
 
90
90
  <div data-fs-user-details-row>
91
91
  <span data-fs-user-details-row-label>Role</span>
92
- <span data-fs-user-details-row-value>
92
+ <span
93
+ data-fs-user-details-row-value
94
+ data-fs-user-details-row-value-roles
95
+ >
93
96
  {user?.roles?.map((role) => (
94
97
  <Tag key={role} data-fs-user-details-row-value-tag>
95
98
  {role}
@@ -130,6 +130,7 @@
130
130
  color: #707070;
131
131
  font-size: var(--fs-text-size-1);
132
132
  font-weight: 500;
133
+ min-width: 12.5rem;
133
134
  }
134
135
 
135
136
  [data-fs-user-details-row-value] {
@@ -138,6 +139,12 @@
138
139
  font-weight: 500;
139
140
  }
140
141
 
142
+ [data-fs-user-details-row-value-roles] {
143
+ display: flex;
144
+ flex-wrap: wrap;
145
+ gap: var(--fs-spacing-1);
146
+ }
147
+
141
148
  [data-fs-user-details-change] {
142
149
  margin-left: auto;
143
150
  font-weight: 600;
@@ -103,9 +103,11 @@ export const UsersLayout = ({ data: { users, search } }: UsersLayoutProps) => {
103
103
  dropdownMenu={<UserDropdownMenu user={user} />}
104
104
  >
105
105
  <Table.Cell>
106
- {user.roles?.map((role) => (
107
- <Tag key={role}>{role}</Tag>
108
- ))}
106
+ <div data-fs-bp-table-user-roles>
107
+ {user.roles?.map((role) => (
108
+ <Tag key={role}>{role}</Tag>
109
+ ))}
110
+ </div>
109
111
  </Table.Cell>
110
112
  </Table.Row>
111
113
  ))}
@@ -158,4 +158,16 @@
158
158
  }
159
159
  }
160
160
  }
161
+
162
+ [data-fs-bp-table] {
163
+ margin-bottom: var(--fs-spacing-8);
164
+
165
+ [data-fs-bp-table-row] {
166
+ [data-fs-bp-table-user-roles] {
167
+ display: flex;
168
+ flex-wrap: wrap;
169
+ gap: var(--fs-spacing-1);
170
+ }
171
+ }
172
+ }
161
173
  }
@@ -73,7 +73,7 @@ export async function loader(
73
73
  }),
74
74
  ]);
75
75
 
76
- const isContractEmpty = contractCollections.every((c) => !c.inContract);
76
+ const isContractEmpty = contractCollections.every((c) => !c.isEnabled);
77
77
  const drawerCollections = allCollections.filter((col) => !col.isEnabled);
78
78
 
79
79
  return {