Skip to content

Commit 5e11e18

Browse files
EPMRPP-113458 || Fix UI bugs related to Invite user modal
1 parent 354cc61 commit 5e11e18

File tree

11 files changed

+141
-19
lines changed

11 files changed

+141
-19
lines changed

app/src/pages/inside/common/assignments/instanceAssignment/instanceAssignment.scss

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,17 @@
1414
* limitations under the License.
1515
*/
1616

17+
.forms-wrapper {
18+
display: flex;
19+
flex-direction: column;
20+
gap: 8px;
21+
}
22+
1723
.organizations {
1824
display: flex;
1925
flex-direction: column;
2026
gap: 8px;
21-
margin-bottom: 16px;
27+
margin-bottom: 0;
2228
}
2329

2430
.instance-assignment {
@@ -27,6 +33,7 @@
2733
align-items: center;
2834
gap: 16px;
2935
padding: 16px;
36+
border-radius: 4px;
3037

3138
.autocomplete-wrapper {
3239
width: 270px;
@@ -44,12 +51,22 @@
4451
width: 36px;
4552
height: 36px;
4653
padding: 0;
54+
55+
svg {
56+
width: 14px;
57+
height: 14px;
58+
}
4759
}
4860

4961
.cancel-button {
5062
width: 36px;
5163
height: 36px;
5264
padding: 10px;
65+
66+
svg {
67+
width: 10px;
68+
height: 10px;
69+
}
5370
}
5471
}
5572

app/src/pages/inside/common/assignments/instanceAssignment/instanceAssignment.tsx

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { useState, useRef } from 'react';
17+
import { useState, useRef, useEffect } from 'react';
1818
import { useDispatch, useSelector } from 'react-redux';
1919
import {
2020
WrappedFieldArrayProps,
@@ -69,7 +69,7 @@ const messages = defineMessages({
6969
},
7070
project: {
7171
id: 'InstanceAssignment.project',
72-
defaultMessage: 'Project (optional)',
72+
defaultMessage: 'Project',
7373
},
7474
organizationPlaceholder: {
7575
id: 'InstanceAssignment.organizationPlaceholder',
@@ -156,6 +156,7 @@ export const InstanceAssignment = ({
156156
const dispatch = useDispatch();
157157
const { formatMessage } = useIntl();
158158
const selector = formValueSelector(formName);
159+
const isAddingProject = useSelector((state) => selector(state, 'isAddingProject') as boolean | undefined);
159160
const errors = useSelector((state) => getFormSyncErrors(formName)(state)) as {
160161
organization: { name: string };
161162
};
@@ -174,9 +175,15 @@ export const InstanceAssignment = ({
174175
const [organizationProjects, setOrganizationProjects] = useState<ProjectsSearchesItem[]>([]);
175176
const [selectedProjectId, setSelectedProjectId] = useState<number | null>(null);
176177
const [selectedOrganizationId, setSelectedOrganizationId] = useState<number | null>(null);
178+
const [totalProjects, setTotalProjects] = useState(0);
177179
const [isOpen, setIsOpen] = useState<boolean>(true);
178180
const allOrganizations = fields.getAll();
179181

182+
useEffect(() => {
183+
const shouldFormBeOpen = isOpen || allOrganizations?.length === 0;
184+
dispatch(change(formName, 'isAddingOrganization', shouldFormBeOpen));
185+
}, [isOpen, allOrganizations?.length, dispatch, formName]);
186+
180187
const resetOrganization = () => {
181188
dispatch(
182189
change(formName, formNamespace, {
@@ -266,9 +273,9 @@ export const InstanceAssignment = ({
266273
};
267274

268275
return (
269-
<div>
276+
<div className={cx('forms-wrapper')}>
270277
<FieldElement name={ORGANIZATIONS} className={cx('organizations')}>
271-
<OrganizationAssignment isMultiple />
278+
<OrganizationAssignment isMultiple formName={formName} />
272279
</FieldElement>
273280
{isOpen || allOrganizations?.length === 0 ? (
274281
<div className={cx('instance-assignment')}>
@@ -278,17 +285,25 @@ export const InstanceAssignment = ({
278285
<AsyncAutocompleteV2
279286
inputProps={{
280287
label: formatMessage(messages.organization),
288+
clearable: true,
289+
onClear: () => {
290+
dispatch(change(formName, FORM_FIELDS.ORGANIZATION.NAME, null));
291+
setSelectedOrganizationId(null);
292+
},
281293
}}
282294
placeholder={formatMessage(messages.organizationPlaceholder)}
283295
getURI={URLS.organizationSearches}
284296
getRequestParams={getRequestOrganizationsParams}
285297
makeOptions={makeOrganizationsOptions}
286298
createWithoutConfirmation
299+
skipOptionCreation
287300
popoverClassName={cx('popover-organization')}
288301
onChange={(organizationName: string) => {
289-
setSelectedOrganizationId(
290-
notAssignedOrganizations.find(({ name }) => name === organizationName)?.id,
291-
);
302+
const selectedOrg = notAssignedOrganizations.find(({ name }) => name === organizationName);
303+
setSelectedOrganizationId(selectedOrg?.id || null);
304+
if (selectedOrg?.relationships?.projects?.meta?.count !== undefined) {
305+
setTotalProjects(selectedOrg.relationships.projects.meta.count);
306+
}
292307
}}
293308
isRequired={isOrganizationRequired}
294309
useFixedPositioning
@@ -315,17 +330,28 @@ export const InstanceAssignment = ({
315330
<FieldProvider name={FORM_FIELDS.ORGANIZATION.PROJECTS.NAME}>
316331
<FieldErrorHint provideHint={false}>
317332
<AsyncAutocompleteV2
333+
key={`project-${selectedOrganizationId}`}
318334
inputProps={{
319335
label: formatMessage(messages.project),
336+
clearable: true,
337+
placeholder: formatMessage(messages.projectPlaceholder),
338+
onClear: () => {
339+
dispatch(change(formName, FORM_FIELDS.ORGANIZATION.PROJECTS.NAME, null));
340+
setSelectedProjectId(null);
341+
},
320342
}}
321343
placeholder={formatMessage(messages.projectPlaceholder)}
322344
getURI={() => URLS.organizationProjectsSearches(selectedOrganizationId)}
323345
getRequestParams={getRequestOrganizationsParams}
324346
makeOptions={makeProjectsOptions}
325347
onChange={handleProjectChange}
326348
createWithoutConfirmation
349+
skipOptionCreation
327350
className={cx('autocomplete')}
328351
disabled={!selectedOrganizationId}
352+
customEmptyListMessage={totalProjects === 0 && selectedOrganizationId ? 'No projects created yet' : undefined}
353+
isDropdownMode={totalProjects === 0 && selectedOrganizationId}
354+
icon={totalProjects === 0 && selectedOrganizationId ? <div /> : undefined}
329355
useFixedPositioning
330356
dropdownMatchInputWidth
331357
/>
@@ -389,8 +415,8 @@ export const InstanceAssignment = ({
389415
<AddItemButton
390416
tooltipClassname={cx('tooltip')}
391417
onClick={() => setIsOpen(true)}
392-
tooltipContent={messages.availableOrganizations}
393-
disabled={areOrganizationsExhausted}
418+
tooltipContent={areOrganizationsExhausted ? messages.availableOrganizations : undefined}
419+
disabled={areOrganizationsExhausted || !!isAddingProject}
394420
text={formatMessage(messages.addOrganization)}
395421
/>
396422
</div>

app/src/pages/inside/common/assignments/organizationAssignment/organizationAssignment.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ interface OrganizationAssignmentProps {
2121
value?: Organization | Organization[];
2222
isMultiple?: boolean;
2323
organizationRoleDisabledTooltip?: string | null;
24+
formName?: string;
2425
}
2526

2627
export const OrganizationAssignment = ({
2728
value,
2829
onChange,
2930
isMultiple = false,
3031
organizationRoleDisabledTooltip = null,
32+
formName,
3133
}: OrganizationAssignmentProps) => {
3234
const updateItem = (updates: Partial<Organization>, index?: number) => {
3335
if (isMultiple) {
@@ -61,6 +63,7 @@ export const OrganizationAssignment = ({
6163
onChange={(updates) => updateItem(updates, index)}
6264
onRemove={() => removeItem(index)}
6365
collapsable
66+
formName={formName}
6467
/>
6568
</div>
6669
))}
@@ -73,6 +76,7 @@ export const OrganizationAssignment = ({
7376
value={value as Organization}
7477
onChange={(updates) => updateItem(updates)}
7578
organizationRoleDisabledTooltip={organizationRoleDisabledTooltip}
79+
formName={formName}
7680
/>
7781
);
7882
};

app/src/pages/inside/common/assignments/organizationAssignment/organizationItem/addItemButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Button, PlusIcon, Tooltip } from '@reportportal/ui-kit';
1919

2020
interface AddItemButtonProps {
2121
onClick: () => void;
22-
tooltipContent: MessageDescriptor;
22+
tooltipContent?: MessageDescriptor;
2323
text: string;
2424
tooltipClassname?: string;
2525
disabled?: boolean;
@@ -46,7 +46,7 @@ export const AddItemButton = ({
4646
</Button>
4747
);
4848

49-
return disabled ? (
49+
return disabled && tooltipContent ? (
5050
<Tooltip
5151
wrapperClassName={tooltipClassname}
5252
placement="top"

app/src/pages/inside/common/assignments/organizationAssignment/organizationItem/organizationItem.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
*/
1616

1717
import { useEffect, useState } from 'react';
18+
import { useDispatch } from 'react-redux';
1819
import { useIntl } from 'react-intl';
1920
import { BaseIconButton, CloseIcon, Dropdown, DropdownIcon, Tooltip } from '@reportportal/ui-kit';
21+
import { change } from 'redux-form';
2022

2123
import { createClassnames, fetch } from 'common/utils';
2224
import { EDITOR, MANAGER, MEMBER } from 'common/constants/projectRoles';
@@ -47,6 +49,7 @@ interface OrganizationItemProps {
4749
onRemove?: () => void;
4850
collapsable?: boolean;
4951
organizationRoleDisabledTooltip?: string | null;
52+
formName?: string;
5053
}
5154

5255
export const OrganizationItem = ({
@@ -55,7 +58,9 @@ export const OrganizationItem = ({
5558
onRemove,
5659
collapsable,
5760
organizationRoleDisabledTooltip = null,
61+
formName,
5862
}: OrganizationItemProps) => {
63+
const dispatch = useDispatch();
5964
const disableOrganizationRole = Boolean(organizationRoleDisabledTooltip);
6065
const { formatMessage } = useIntl();
6166
const { id, name, role, projects } = value;
@@ -69,6 +74,12 @@ export const OrganizationItem = ({
6974
const noProjects = totalProjects === 0;
7075
const allProjectsAdded = projects?.length === totalProjects;
7176

77+
useEffect(() => {
78+
if (formName) {
79+
dispatch(change(formName, 'isAddingProject', addProjectFormOpen));
80+
}
81+
}, [addProjectFormOpen, formName, dispatch]);
82+
7283
useEffect(() => {
7384
const data = { method: 'post', data: { limit: PROJECTS_LIMIT } };
7485
fetch(URLS.organizationProjectsSearches(id), data)

app/src/pages/inside/common/invitations/inviteUserModal/InviteUserEmailAutocompleteField.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
flex-shrink: 0;
3636
margin-right: 8px;
3737
overflow: hidden;
38+
width: 40px;
39+
height: 40px;
3840
}
3941

4042
.img {

app/src/pages/inside/common/invitations/inviteUserModal/InviteUserEmailAutocompleteField.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import { useCallback, useRef } from 'react';
1818
import type { MutableRefObject, ReactNode } from 'react';
1919
import { FormattedMessage, useIntl } from 'react-intl';
20+
import { useDispatch } from 'react-redux';
21+
import { untouch } from 'redux-form';
2022
import { createClassnames } from 'common/utils';
2123
import { AsyncAutocompleteV2 } from 'componentLibrary/autocompletes/asyncAutocompleteV2';
2224
import { FieldErrorHint } from 'components/fields/fieldErrorHint';
@@ -26,6 +28,7 @@ import { URLS } from 'common/urls';
2628
import { messages } from 'common/constants/localization/invitationsLocalization';
2729
import { email as emailValidator } from 'common/utils/validation/validate';
2830
import { MailIcon } from '@reportportal/ui-kit';
31+
import { INSTANCE_FORM_NAME } from './utils';
2932
import styles from './InviteUserEmailAutocompleteField.scss';
3033

3134
const cx = createClassnames(styles);
@@ -141,9 +144,19 @@ function renderOption(
141144

142145
const InviteUserEmailAutocompleteFieldContent = ({
143146
inputValueRef,
147+
input,
148+
onFocus,
149+
onResetTouched,
144150
...rest
145151
}: {
146152
inputValueRef: MutableRefObject<string>;
153+
input?: {
154+
onChange: (value: unknown) => void;
155+
setTouched?: (touched: boolean) => void;
156+
onFocus?: () => void;
157+
};
158+
onFocus?: () => void;
159+
onResetTouched?: () => void;
147160
[key: string]: unknown;
148161
}) => {
149162
const { formatMessage } = useIntl();
@@ -171,6 +184,11 @@ const InviteUserEmailAutocompleteFieldContent = ({
171184
return baseOptions;
172185
}, [inputValueRef]);
173186

187+
const handleFocus = useCallback(() => {
188+
onFocus?.();
189+
onResetTouched?.();
190+
}, [onFocus, onResetTouched]);
191+
174192
const placeholder = formatMessage(messages.inputPlaceholderInstance);
175193
const customEmptyListMessage = formatMessage(messages.noMatchesContinueTyping);
176194
const label = formatMessage(messages.email);
@@ -187,7 +205,16 @@ const InviteUserEmailAutocompleteFieldContent = ({
187205
createWithoutConfirmation
188206
minLength={1}
189207
customEmptyListMessage={customEmptyListMessage}
190-
inputProps={{ label, autoComplete: 'one-time-code' }}
208+
inputProps={{
209+
label,
210+
autoComplete: 'one-time-code',
211+
clearable: true,
212+
onClear: () => {
213+
inputValueRef.current = '';
214+
input?.onChange(null);
215+
},
216+
onFocus: handleFocus,
217+
}}
191218
isRequired
192219
useFixedPositioning={false}
193220
{...rest}
@@ -197,11 +224,16 @@ const InviteUserEmailAutocompleteFieldContent = ({
197224

198225
export const InviteUserEmailAutocompleteField = () => {
199226
const inputValueRef = useRef('');
227+
const dispatch = useDispatch();
228+
229+
const handleResetTouched = useCallback(() => {
230+
dispatch(untouch(INSTANCE_FORM_NAME, 'email'));
231+
}, [dispatch]);
200232

201233
return (
202234
<FieldProvider name="email">
203235
<FieldErrorHint provideHint={false}>
204-
<InviteUserEmailAutocompleteFieldContent inputValueRef={inputValueRef} />
236+
<InviteUserEmailAutocompleteFieldContent inputValueRef={inputValueRef} onResetTouched={handleResetTouched} />
205237
</FieldErrorHint>
206238
</FieldProvider>
207239
);

app/src/pages/inside/common/invitations/inviteUserModal/inviteUserModalValidate.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ export function validateInstance(formData: InviteUserInstanceFormData): InviteUs
3232
const errors: InviteUserFormErrors = {
3333
email: emailValidator(emailStr.trim()),
3434
};
35+
36+
if (formData.isAddingOrganization || formData.isAddingProject) {
37+
errors.organizations = 'Form is being edited';
38+
return errors;
39+
}
40+
3541
const organizations = formData.organizations ?? [];
3642
if (organizations.length === 0) {
3743
errors.organizations = commonValidators.requiredField(organizations);

app/src/pages/inside/common/invitations/inviteUserModal/inviteUserOrganizationForm/inviteUserOrganizationForm.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
OrganizationAssignment,
66
} from 'pages/inside/common/assignments/organizationAssignment';
77

8-
import { InviteUserEmailAutocompleteField } from '../InviteUserEmailAutocompleteField';
8+
import { InviteUserEmailField } from '../inviteUserEmailField';
9+
import { getFormName } from '../utils';
10+
import { Level } from '../constants';
911

1012
import styles from './inviteUserOrganizationForm.scss';
1113

@@ -19,7 +21,7 @@ export interface InviteUserOrganizationFormData {
1921
export const InviteUserOrganizationForm = () => {
2022
return (
2123
<form className={cx('form')}>
22-
<InviteUserEmailAutocompleteField />
24+
<InviteUserEmailField formName={getFormName(Level.ORGANIZATION)} />
2325
<FieldElement name="organization">
2426
<OrganizationAssignment />
2527
</FieldElement>

0 commit comments

Comments
 (0)