Skip to content

Commit

Permalink
feat: ✨ Implement "user created" step for new user form
Browse files Browse the repository at this point in the history
Implemented a new step for the users page user creation contextual bar in which the user will be able to choose from creating another user right away or finishing the user creation process and being redirected to the most recently created user. Also fixed some tests, created new translations and changed some logic to improve the roles filter.
  • Loading branch information
rique223 committed Jul 30, 2024
1 parent aa9eb6d commit 4161066
Show file tree
Hide file tree
Showing 15 changed files with 129 additions and 61 deletions.
2 changes: 1 addition & 1 deletion apps/meteor/client/components/InfoPanel/InfoPanelTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type InfoPanelTitleProps = {
const isValidIcon = (icon: ReactNode): icon is IconName => typeof icon === 'string';

const InfoPanelTitle = ({ title, icon }: InfoPanelTitleProps) => (
<Box display='flex' flexShrink={0} alignItems='center' fontScale='h4' color='default' withTruncatedText>
<Box display='flex' flexShrink={0} alignItems='center' fontScale='p1m' color='default' withTruncatedText>
{isValidIcon(icon) ? <Icon name={icon} size='x22' /> : icon}
<Box mis={8} withTruncatedText title={title}>
{title}
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/components/UserCard/UserCardInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ReactElement, ComponentProps } from 'react';
import React from 'react';

const UserCardInfo = (props: ComponentProps<typeof Box>): ReactElement => (
<Box mbe={8} is='span' fontScale='p2' color='hint' withTruncatedText {...props} />
<Box mb={8} is='span' fontScale='p2' color='hint' withTruncatedText {...props} />
);

export default UserCardInfo;
4 changes: 2 additions & 2 deletions apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ const RoomsTableFilters = ({ setFilters }: { setFilters: Dispatch<SetStateAction
</Box>
<Box minWidth='x224' m='x4'>
<MultiSelectCustom
dropdownOptions={roomTypeFilterStructure as OptionProp[]}
defaultTitle={'All_rooms' as any}
dropdownOptions={roomTypeFilterStructure}
defaultTitle='All_rooms'
selectedOptionsTitle='Rooms'
setSelectedOptions={handleRoomTypeChange}
selectedOptions={roomTypeSelectedOptions}
Expand Down
29 changes: 29 additions & 0 deletions apps/meteor/client/views/admin/users/AdminUserCreated.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Button, ButtonGroup, ContextualbarFooter } from '@rocket.chat/fuselage';
import { useRouter } from '@rocket.chat/ui-contexts';
import React from 'react';
import { useTranslation } from 'react-i18next';

import { ContextualbarEmptyContent } from '../../../components/Contextualbar';

const AdminUserCreated = ({ uid }: { uid: string }) => {
const { t } = useTranslation();
const router = useRouter();

return (
<>
<ContextualbarEmptyContent icon='user' title={t('You_have_created_user', { count: 1 })} />
<ContextualbarFooter>
<ButtonGroup stretch>
<Button type='reset' w='50%' onClick={() => router.navigate(`/admin/users/new`)}>
{t('Add_more_users')}
</Button>
<Button primary w='50%' onClick={() => router.navigate(`/admin/users/info/${uid}`)}>
{t('Done')}
</Button>
</ButtonGroup>
</ContextualbarFooter>
</>
);
};

export default AdminUserCreated;
6 changes: 3 additions & 3 deletions apps/meteor/client/views/admin/users/AdminUserForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ type AdminUserFormProps = {
roleError: unknown;
};

export type userFormProps = Omit<UserCreateParamsPOST & { avatar: AvatarObject; passwordConfirmation: string }, 'fields'>;
export type UserFormProps = Omit<UserCreateParamsPOST & { avatar: AvatarObject; passwordConfirmation: string }, 'fields'>;

const getInitialValue = ({
data,
Expand All @@ -64,7 +64,7 @@ const getInitialValue = ({
defaultUserRoles?: IUser['roles'];
isSmtpEnabled?: boolean;
isNewUserPage?: boolean;
}): userFormProps => ({
}): UserFormProps => ({
roles: data?.roles ?? defaultUserRoles,
name: data?.name ?? '',
password: '',
Expand Down Expand Up @@ -151,7 +151,7 @@ const AdminUserForm = ({ userData, onReload, context, refetchUserFormData, roleD
},
});

const handleSaveUser = useMutableCallback(async (userFormPayload: userFormProps) => {
const handleSaveUser = useMutableCallback(async (userFormPayload: UserFormProps) => {
const { avatar, passwordConfirmation, ...userFormData } = userFormPayload;

if (!isNewUserPage && userData?._id) {
Expand Down
20 changes: 16 additions & 4 deletions apps/meteor/client/views/admin/users/AdminUserFormWithData.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IUser } from '@rocket.chat/core-typings';
import type { IRole, IUser } from '@rocket.chat/core-typings';
import { isUserFederated } from '@rocket.chat/core-typings';
import { Box, Callout } from '@rocket.chat/fuselage';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
Expand All @@ -13,9 +13,12 @@ import AdminUserForm from './AdminUserForm';
type AdminUserFormWithDataProps = {
uid: IUser['_id'];
onReload: () => void;
context: string;
roleData: { roles: IRole[] } | undefined;
roleError: unknown;
};

const AdminUserFormWithData = ({ uid, onReload }: AdminUserFormWithDataProps): ReactElement => {
const AdminUserFormWithData = ({ uid, onReload, context, roleData, roleError }: AdminUserFormWithDataProps): ReactElement => {
const t = useTranslation();
const { data, isLoading, isError, refetch } = useUserInfoQuery({ userId: uid });

Expand All @@ -40,15 +43,24 @@ const AdminUserFormWithData = ({ uid, onReload }: AdminUserFormWithDataProps): R
);
}

if (data?.user && isUserFederated(data?.user as unknown as IUser)) {
if (data?.user && isUserFederated(data?.user)) {
return (
<Callout m={16} type='danger'>
{t('Edit_Federated_User_Not_Allowed')}
</Callout>
);
}

return <AdminUserForm userData={data?.user} onReload={handleReload} />;
return (
<AdminUserForm
userData={data?.user}
onReload={onReload}
context={context}
refetchUserFormData={handleReload}
roleData={roleData}
roleError={roleError}
/>
);
};

export default AdminUserFormWithData;
20 changes: 15 additions & 5 deletions apps/meteor/client/views/admin/users/AdminUsersPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { LicenseInfo } from '@rocket.chat/core-typings';
import { Button, ButtonGroup, Callout, ContextualbarIcon, Skeleton, Tabs, TabsItem } from '@rocket.chat/fuselage';
import { Button, ButtonGroup, Callout, ContextualbarIcon, Icon, Skeleton, Tabs, TabsItem } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import type { OptionProp } from '@rocket.chat/ui-client';
import { ExternalLink } from '@rocket.chat/ui-client';
Expand All @@ -23,6 +23,7 @@ import { useLicenseLimitsByBehavior } from '../../../hooks/useLicenseLimitsByBeh
import { useShouldPreventAction } from '../../../hooks/useShouldPreventAction';
import { useCheckoutUrl } from '../subscription/hooks/useCheckoutUrl';
import AdminInviteUsers from './AdminInviteUsers';
import AdminUserCreated from './AdminUserCreated';
import AdminUserForm from './AdminUserForm';
import AdminUserFormWithData from './AdminUserFormWithData';
import AdminUserInfoWithData from './AdminUserInfoWithData';
Expand Down Expand Up @@ -61,7 +62,7 @@ const AdminUsersPage = (): ReactElement => {
const isCreateUserDisabled = useShouldPreventAction('activeUsers');

const getRoles = useEndpoint('GET', '/v1/roles.list');
const { data } = useQuery(['roles'], async () => getRoles());
const { data, error } = useQuery(['roles'], async () => getRoles());

const paginationData = usePagination();
const sortData = useSort<UsersTableSortingOptions>('name');
Expand Down Expand Up @@ -181,14 +182,23 @@ const AdminUsersPage = (): ReactElement => {
<ContextualbarTitle>
{context === 'info' && t('User_Info')}
{context === 'edit' && t('Edit_User')}
{context === 'new' && t('Add_User')}
{(context === 'new' || context === 'created') && (
<>
<Icon name='user-plus' size={20} /> {t('New_user')}
</>
)}
{context === 'invite' && t('Invite_Users')}
</ContextualbarTitle>
<ContextualbarClose onClick={() => router.navigate('/admin/users')} />
</ContextualbarHeader>
{context === 'info' && id && <AdminUserInfoWithData uid={id} onReload={handleReload} tab={tab} />}
{context === 'edit' && id && <AdminUserFormWithData uid={id} onReload={handleReload} />}
{!isRoutePrevented && context === 'new' && <AdminUserForm onReload={handleReload} context={context} />}
{context === 'edit' && id && (
<AdminUserFormWithData uid={id} onReload={handleReload} context={context} roleData={data} roleError={error} />
)}
{!isRoutePrevented && context === 'new' && (
<AdminUserForm onReload={handleReload} context={context} roleData={data} roleError={error} />
)}
{!isRoutePrevented && context === 'created' && id && <AdminUserCreated uid={id} />}
{!isRoutePrevented && context === 'invite' && <AdminInviteUsers />}
{isRoutePrevented && <AdminUserUpgrade />}
</Contextualbar>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ const UsersTable = ({
divider
current={current}
itemsPerPage={itemsPerPage}
count={data?.total || 0}
count={data.total || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
{...paginationProps}
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/tests/e2e/administration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ test.describe.parallel('administration', () => {
await poAdmin.tabs.users.btnNewUser.click();
await poAdmin.tabs.users.inputName.type(faker.person.firstName());
await poAdmin.tabs.users.inputUserName.type(faker.internet.userName());
await poAdmin.tabs.users.inputSetManually.click();
await poAdmin.tabs.users.inputEmail.type(faker.internet.email());
await poAdmin.tabs.users.checkboxVerified.click();
await poAdmin.tabs.users.inputPassword.type('any_password');
await poAdmin.tabs.users.inputConfirmPassword.type('any_password');
await expect(poAdmin.tabs.users.userRole).toBeVisible();
await poAdmin.tabs.users.btnSave.click();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class AdminFlextabUsers {
}

get btnSave(): Locator {
return this.page.locator('role=button[name="Save"]');
return this.page.locator('role=button[name="Add user"]');
}

get btnInvite(): Locator {
Expand All @@ -31,12 +31,20 @@ export class AdminFlextabUsers {
return this.page.locator('//label[text()="Email"]/following-sibling::span//input').first();
}

get inputSetManually(): Locator {
return this.page.locator('//label[text()="Set manually"]');
}

get inputPassword(): Locator {
return this.page.locator('//label[text()="Password"]/following-sibling::span//input');
return this.page.locator('input[placeholder="Password"]');
}

get inputConfirmPassword(): Locator {
return this.page.locator('input[placeholder="Confirm password"]');
}

get checkboxVerified(): Locator {
return this.page.locator('//label[text()="Verified"]');
return this.page.locator('//label[text()="Mark email as verified"]');
}

get joinDefaultChannels(): Locator {
Expand Down
4 changes: 1 addition & 3 deletions packages/core-typings/src/IUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,7 @@ export interface IUser extends IRocketChatRecord {
private_key: string;
public_key: string;
};
customFields?: {
[key: string]: any;
};
customFields?: Record<string, any>;
settings?: IUserSettings;
defaultRoom?: string;
ldap?: boolean;
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@
"Add_files_from": "Add files from",
"Add_manager": "Add manager",
"Add_monitor": "Add monitor",
"Add_more_users": "Add more users",
"Add_link": "Add link",
"Add_Reaction": "Add reaction",
"Add_Role": "Add Role",
Expand Down Expand Up @@ -6049,6 +6050,8 @@
"You_have_a_new_message": "You have a new message",
"You_have_been_muted": "You have been muted and cannot speak in this room",
"You_have_been_removed_from__roomName_": "You've been removed from the room {{roomName}}",
"You_have_created_user_one": "You’ve created {{count}} user",
"You_have_created_user_other": "You’ve created {{count}} users",
"You_have_joined_a_new_call_with": "You have joined a new call with",
"You_have_n_codes_remaining": "You have {{number}} codes remaining.",
"You_have_not_verified_your_email": "You have not verified your email.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Box, Button } from '@rocket.chat/fuselage';
import { useOutsideClick, useToggle } from '@rocket.chat/fuselage-hooks';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import type { ComponentProps, FormEvent, ReactElement, RefObject } from 'react';
import { useCallback, useRef } from 'react';

Expand Down Expand Up @@ -33,7 +32,6 @@ export type OptionProp = {
@param selectedOptionsTitle dropdown text after clicking one or more options. For example: 'Rooms (3)'
* @param selectedOptions array with clicked options. This is used in the useFilteredTypeRooms hook, to filter the Rooms' table, for example. This array joins all of the individual clicked options from all available MultiSelectCustom components in the page. It helps to create a union filter for all the selections.
* @param setSelectedOptions part of an useState hook to set the previous selectedOptions
* @param customSetSelected part of an useState hook to set the individual selected checkboxes from this instance.
* @param searchBarText optional text prop that creates a search bar inside the dropdown, when added.
* @returns a React Component that should be used with a custom hook for filters, such as useFilteredTypeRooms.tsx.
* Check out the following files, for examples:
Expand All @@ -43,11 +41,11 @@ export type OptionProp = {
*/
type DropDownProps = {
dropdownOptions: OptionProp[];
defaultTitle: TranslationKey;
selectedOptionsTitle: TranslationKey;
defaultTitle: string;
selectedOptionsTitle: string;
selectedOptions: OptionProp[];
setSelectedOptions: (roles: OptionProp[]) => void;
searchBarText?: TranslationKey;
searchBarText?: string;
} & ComponentProps<typeof Button>;

export const MultiSelectCustom = ({
Expand Down Expand Up @@ -77,20 +75,26 @@ export const MultiSelectCustom = ({

useOutsideClick([target], onClose);

const onSelect = (item: OptionProp, e?: FormEvent<HTMLElement>): void => {
e?.stopPropagation();
item.checked = !item.checked;
const onSelect = useCallback(
(selectedOption: OptionProp, e?: FormEvent<HTMLElement>): void => {
e?.stopPropagation();

if (item.checked === true) {
setSelectedOptions([...new Set([...selectedOptions, item])]);
return;
}
if (selectedOption.hasOwnProperty('checked')) {
selectedOption.checked = !selectedOption.checked;

// the user has disabled this option -> remove this from the selected options list
setSelectedOptions(selectedOptions.filter((option: OptionProp) => option.id !== item.id));
};
if (selectedOption.checked) {
setSelectedOptions([...new Set([...selectedOptions, selectedOption])]);
return;
}

const count = dropdownOptions.filter((option) => option.checked).length;
// the user has disabled this option -> remove this from the selected options list
setSelectedOptions(selectedOptions.filter((option: OptionProp) => option.id !== selectedOption.id));
}
},
[selectedOptions, setSelectedOptions],
);

const selectedOptionsCount = dropdownOptions.filter((option) => option.hasOwnProperty('checked') && option.checked).length;

return (
<Box display='flex' position='relative'>
Expand All @@ -101,7 +105,7 @@ export const MultiSelectCustom = ({
onKeyDown={(e) => (e.code === 'Enter' || e.code === 'Space') && toggleCollapsed(!collapsed)}
defaultTitle={defaultTitle}
selectedOptionsTitle={selectedOptionsTitle}
selectedOptionsCount={count}
selectedOptionsCount={selectedOptionsCount}
maxCount={dropdownOptions.length}
{...props}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { forwardRef } from 'react';

type MultiSelectCustomAnchorProps = {
collapsed: boolean;
defaultTitle: TranslationKey;
selectedOptionsTitle: TranslationKey;
defaultTitle: string;
selectedOptionsTitle: string;
selectedOptionsCount: number;
maxCount: number;
} & ComponentProps<typeof Box>;
Expand Down Expand Up @@ -37,7 +37,7 @@ const MultiSelectCustomAnchor = forwardRef<HTMLElement, MultiSelectCustomAnchorP
className={['rcx-input-box__wrapper', customStyle, ...(Array.isArray(className) ? className : [className])].filter(Boolean)}
{...props}
>
{isDirty ? `${t(selectedOptionsTitle)} (${selectedOptionsCount})` : t(defaultTitle)}
{isDirty ? `${t(selectedOptionsTitle as TranslationKey)} (${selectedOptionsCount})` : t(defaultTitle as TranslationKey)}
<Icon name={collapsed ? 'chevron-up' : 'chevron-down'} fontSize='x20' color='hint' />
</Box>
);
Expand Down
Loading

0 comments on commit 4161066

Please sign in to comment.