Skip to content

Commit f906dba

Browse files
feat(editor): Include pending users for project users search (#15389)
Co-authored-by: Csaba Tuncsik <[email protected]>
1 parent 65c07f1 commit f906dba

File tree

2 files changed

+368
-6
lines changed

2 files changed

+368
-6
lines changed
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
import userEvent from '@testing-library/user-event';
2+
import { screen, waitFor } from '@testing-library/vue';
3+
4+
import N8nUserSelect from '.';
5+
import { createComponentRenderer } from '../../__tests__/render';
6+
import type { IUser } from '../../types/user';
7+
8+
const renderComponent = createComponentRenderer(N8nUserSelect);
9+
10+
const getRenderedOptions = async () => {
11+
const dropdown = await waitFor(() => screen.getByRole('listbox'));
12+
expect(dropdown).toBeInTheDocument();
13+
return dropdown.querySelectorAll('.el-select-dropdown__item');
14+
};
15+
16+
const filterInput = async (filterText: string) => {
17+
const input = screen.getByRole('combobox');
18+
await userEvent.type(input, filterText);
19+
};
20+
21+
const sampleUsers: IUser[] = [
22+
{
23+
id: 'u1',
24+
25+
firstName: 'Alice',
26+
lastName: 'Smith',
27+
fullName: 'Alice Smith',
28+
isOwner: true,
29+
isPendingUser: false,
30+
disabled: false,
31+
signInType: 'email',
32+
},
33+
{
34+
id: 'u2',
35+
36+
firstName: 'Bob',
37+
lastName: 'Johnson',
38+
fullName: 'Bob Johnson',
39+
isOwner: true,
40+
isPendingUser: false,
41+
disabled: false,
42+
signInType: 'email',
43+
},
44+
{
45+
id: 'u3',
46+
47+
firstName: 'Charlie',
48+
lastName: 'Brown',
49+
fullName: 'Charlie Brown',
50+
isOwner: true,
51+
isPendingUser: false,
52+
disabled: false,
53+
signInType: 'email',
54+
},
55+
{
56+
id: 'u4',
57+
58+
firstName: 'Dave',
59+
lastName: 'Smith',
60+
fullName: 'Dave Smith',
61+
isOwner: true,
62+
isPendingUser: false,
63+
disabled: false,
64+
signInType: 'email',
65+
},
66+
{
67+
id: 'u5',
68+
69+
isOwner: true,
70+
isPendingUser: false,
71+
disabled: false,
72+
signInType: 'email',
73+
},
74+
{
75+
id: 'u6',
76+
77+
fullName: 'Frank Castle',
78+
isOwner: true,
79+
isPendingUser: false,
80+
disabled: false,
81+
signInType: 'email',
82+
},
83+
{
84+
id: 'u7',
85+
86+
firstName: 'Gina',
87+
lastName: 'Davis',
88+
fullName: 'Gina Davis',
89+
isOwner: true,
90+
isPendingUser: false,
91+
disabled: false,
92+
signInType: 'email',
93+
},
94+
];
95+
96+
describe('UserSelect', () => {
97+
it('should render user select with all users (even pending ones)', async () => {
98+
const { getByRole } = renderComponent({
99+
props: {
100+
users: sampleUsers,
101+
},
102+
});
103+
104+
// ACT
105+
const selectInput = getByRole('combobox'); // Find the select input
106+
expect(selectInput).toBeInTheDocument();
107+
108+
// Simulate clicking the select input to open the dropdown
109+
await userEvent.click(selectInput);
110+
111+
// ASSERT
112+
// Wait for the dropdown to appear in the DOM
113+
const options = await getRenderedOptions();
114+
expect(options).toHaveLength(sampleUsers.length);
115+
});
116+
117+
it('filters users by full name (case-insensitive)', async () => {
118+
renderComponent({
119+
props: {
120+
users: sampleUsers,
121+
},
122+
});
123+
124+
await filterInput('alice');
125+
await waitFor(async () => {
126+
const options = await getRenderedOptions();
127+
expect(options.length).toBe(1);
128+
expect(options[0]).toHaveAttribute('id', 'user-select-option-id-u1');
129+
});
130+
131+
await userEvent.click(document.body);
132+
133+
await filterInput('SMITH');
134+
await waitFor(async () => {
135+
const options = await getRenderedOptions();
136+
expect(options.length).toBe(2); // Alice Smith, Dave Smith
137+
expect(Array.from(options).map((o) => o.getAttribute('id'))).toEqual([
138+
'user-select-option-id-u1',
139+
'user-select-option-id-u4',
140+
]); // Sorted by first name
141+
});
142+
});
143+
144+
it('filters users by email (case-sensitive for filter term, if full name does not match)', async () => {
145+
renderComponent({
146+
props: {
147+
users: sampleUsers,
148+
},
149+
});
150+
151+
await filterInput('[email protected]');
152+
await waitFor(async () => {
153+
const options = await getRenderedOptions();
154+
expect(options.length).toBe(1);
155+
expect(options[0]).toHaveAttribute('id', 'user-select-option-id-u1');
156+
});
157+
158+
await userEvent.click(document.body);
159+
160+
await filterInput('Example.com'); // Email part of filter is case-sensitive in the component's logic
161+
await waitFor(() => {
162+
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
163+
});
164+
165+
await userEvent.click(document.body);
166+
167+
await filterInput('example.com'); // Matches all users with email containing 'example.com'
168+
await waitFor(async () => {
169+
const options = await getRenderedOptions();
170+
expect(options.length).toBe(sampleUsers.length);
171+
});
172+
});
173+
174+
it('filters by full name and email and sorts by last name', async () => {
175+
const specificUsers: IUser[] = [
176+
{
177+
id: 's1',
178+
179+
fullName: 'Alice TestName',
180+
isOwner: true,
181+
isPendingUser: false,
182+
disabled: false,
183+
signInType: 'email',
184+
},
185+
{
186+
id: 's2',
187+
188+
fullName: 'Bob Something',
189+
isOwner: true,
190+
isPendingUser: false,
191+
disabled: false,
192+
signInType: 'email',
193+
},
194+
];
195+
renderComponent({
196+
props: {
197+
users: specificUsers,
198+
},
199+
});
200+
await filterInput('alice'); // Should match "Alice TestName" by fullName and "Bob Something" by email
201+
await waitFor(async () => {
202+
const options = await getRenderedOptions();
203+
expect(options.length).toBe(2);
204+
expect(Array.from(options).map((o) => o.getAttribute('id'))).toEqual([
205+
'user-select-option-id-s2',
206+
'user-select-option-id-s1',
207+
]);
208+
});
209+
});
210+
211+
it('excludes users without an email from filtered results', async () => {
212+
const usersWithNoEmail: IUser[] = [
213+
sampleUsers[0], // Alice
214+
{
215+
id: 'noemail',
216+
fullName: 'No Email User',
217+
isOwner: true,
218+
isPendingUser: false,
219+
disabled: false,
220+
signInType: 'email',
221+
},
222+
];
223+
const { getByRole } = renderComponent({
224+
props: {
225+
users: usersWithNoEmail,
226+
},
227+
});
228+
229+
const selectInput = getByRole('combobox');
230+
expect(selectInput).toBeInTheDocument();
231+
232+
await userEvent.click(selectInput);
233+
234+
await waitFor(async () => {
235+
const options = await getRenderedOptions();
236+
expect(options.length).toBe(1);
237+
expect(options[0]).toHaveAttribute('id', 'user-select-option-id-u1');
238+
});
239+
240+
await filterInput('No Email User'); // Try to filter by name
241+
await waitFor(() => {
242+
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
243+
});
244+
});
245+
246+
it('excludes users in ignoreIds from filtered results', async () => {
247+
const { getByRole } = renderComponent({
248+
props: {
249+
users: sampleUsers,
250+
ignoreIds: ['u1', 'u3'], // Exclude Alice and Bob
251+
},
252+
});
253+
254+
const selectInput = getByRole('combobox');
255+
expect(selectInput).toBeInTheDocument();
256+
257+
await userEvent.click(selectInput);
258+
259+
await waitFor(async () => {
260+
const options = await getRenderedOptions();
261+
expect(options.length).toBe(5);
262+
});
263+
264+
await userEvent.click(document.body);
265+
266+
await filterInput('smith'); // Would normally match Alice Smith (u1) and Dave Smith (u4)
267+
await waitFor(async () => {
268+
const options = await getRenderedOptions();
269+
expect(options.length).toBe(1);
270+
expect(options[0]).toHaveAttribute('id', 'user-select-option-id-u4'); // Only Dave Smith
271+
});
272+
});
273+
274+
it('sorts users by lastName, then firstName, then email', async () => {
275+
const usersToSort: IUser[] = [
276+
{
277+
id: 'a',
278+
279+
firstName: 'Zeta',
280+
lastName: 'Able',
281+
fullName: 'Zeta Able',
282+
isOwner: true,
283+
isPendingUser: false,
284+
disabled: false,
285+
signInType: 'email',
286+
},
287+
{
288+
id: 'b',
289+
290+
firstName: 'Alpha',
291+
lastName: 'Baker',
292+
fullName: 'Alpha Baker',
293+
isOwner: true,
294+
isPendingUser: false,
295+
disabled: false,
296+
signInType: 'email',
297+
},
298+
{
299+
id: 'c',
300+
301+
firstName: 'Beta',
302+
lastName: 'Able',
303+
fullName: 'Beta Able',
304+
isOwner: true,
305+
isPendingUser: false,
306+
disabled: false,
307+
signInType: 'email',
308+
},
309+
{
310+
id: 'd',
311+
312+
firstName: 'Gamma',
313+
lastName: 'Able',
314+
fullName: 'Gamma Able',
315+
isOwner: true,
316+
isPendingUser: false,
317+
disabled: false,
318+
signInType: 'email',
319+
},
320+
{
321+
id: 'e',
322+
323+
isOwner: true,
324+
isPendingUser: false,
325+
disabled: false,
326+
signInType: 'email',
327+
}, // No names, sort by email
328+
{
329+
id: 'f',
330+
331+
firstName: 'Charlie',
332+
lastName: 'Baker',
333+
fullName: 'Charlie Baker',
334+
isOwner: true,
335+
isPendingUser: false,
336+
disabled: false,
337+
signInType: 'email',
338+
},
339+
];
340+
const { getByRole } = renderComponent({
341+
props: {
342+
users: usersToSort,
343+
},
344+
});
345+
346+
const selectInput = getByRole('combobox');
347+
expect(selectInput).toBeInTheDocument();
348+
349+
await userEvent.click(selectInput);
350+
351+
const dropdown = await waitFor(() => getByRole('listbox'));
352+
expect(dropdown).toBeInTheDocument();
353+
const options = dropdown.querySelectorAll('.el-select-dropdown__item');
354+
const sortedIds = Array.from(options).map((option) => option.getAttribute('id'));
355+
356+
expect(sortedIds).toEqual([
357+
'user-select-option-id-c',
358+
'user-select-option-id-e',
359+
'user-select-option-id-d',
360+
'user-select-option-id-a',
361+
'user-select-option-id-b',
362+
'user-select-option-id-f',
363+
]);
364+
});
365+
});

packages/frontend/@n8n/design-system/src/components/N8nUserSelect/UserSelect.vue

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,18 @@ const filter = ref('');
3434
3535
const filteredUsers = computed(() =>
3636
props.users.filter((user) => {
37-
if (user.isPendingUser || !user.email) {
38-
return false;
39-
}
40-
4137
if (props.ignoreIds.includes(user.id)) {
4238
return false;
4339
}
4440
45-
if (user.fullName) {
41+
if (user.fullName && user.email) {
4642
const match = user.fullName.toLowerCase().includes(filter.value.toLowerCase());
4743
if (match) {
4844
return true;
4945
}
5046
}
5147
52-
return user.email.includes(filter.value);
48+
return user.email?.includes(filter.value) ?? false;
5349
}),
5450
);
5551
@@ -102,6 +98,7 @@ const getLabel = (user: IUser) =>
10298
</template>
10399
<N8nOption
104100
v-for="user in sortedUsers"
101+
:id="`user-select-option-id-${user.id}`"
105102
:key="user.id"
106103
:value="user.id"
107104
:class="$style.itemContainer"

0 commit comments

Comments
 (0)