Refactored user components.

This commit is contained in:
Mike Cao 2023-01-23 15:32:35 -08:00
parent 91af593ff5
commit cdd54df8f6
16 changed files with 180 additions and 231 deletions

View File

@ -9,6 +9,7 @@ import Logo from 'assets/logo.svg';
import Moon from 'assets/moon.svg'; import Moon from 'assets/moon.svg';
import Profile from 'assets/profile.svg'; import Profile from 'assets/profile.svg';
import Sun from 'assets/sun.svg'; import Sun from 'assets/sun.svg';
import Trash from 'assets/trash.svg';
import User from 'assets/user.svg'; import User from 'assets/user.svg';
import Users from 'assets/users.svg'; import Users from 'assets/users.svg';
@ -24,6 +25,7 @@ export {
Moon, Moon,
Profile, Profile,
Sun, Sun,
Trash,
User, User,
Users, Users,
}; };

View File

@ -2,6 +2,14 @@ import { defineMessages } from 'react-intl';
export const labels = defineMessages({ export const labels = defineMessages({
unknown: { id: 'label.unknown', defaultMessage: 'Unknown' }, unknown: { id: 'label.unknown', defaultMessage: 'Unknown' },
required: { id: 'label.required', defaultMessage: 'Required' },
save: { id: 'label.save', defaultMessage: 'Save' },
cancel: { id: 'label.cancel', defaultMessage: 'Cancel' },
});
export const messages = defineMessages({
error: { id: 'message.error', defaultMessage: 'Something went wrong.' },
saved: { id: 'message.saved-successfully', defaultMessage: 'Saved successfully.' },
}); });
export const devices = defineMessages({ export const devices = defineMessages({

View File

@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { Button, Icon, Modal, useToast } from 'react-basics'; import { Button, Icon, Text, Modal, useToast } from 'react-basics';
import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm'; import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm';
import { Lock } from 'components/icons'; import { Lock } from 'components/icons';
@ -9,7 +9,7 @@ const messages = defineMessages({
saved: { id: 'message.saved-successfully', defaultMessage: 'Saved successfully.' }, saved: { id: 'message.saved-successfully', defaultMessage: 'Saved successfully.' },
}); });
export default function ChangePasswordButton() { export default function PasswordChangeButton() {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [edit, setEdit] = useState(false); const [edit, setEdit] = useState(false);
const { toast, showToast } = useToast(); const { toast, showToast } = useToast();
@ -19,7 +19,7 @@ export default function ChangePasswordButton() {
setEdit(false); setEdit(false);
}; };
const handleAdd = () => { const handleEdit = () => {
setEdit(true); setEdit(true);
}; };
@ -30,11 +30,11 @@ export default function ChangePasswordButton() {
return ( return (
<> <>
{toast} {toast}
<Button onClick={handleAdd}> <Button onClick={handleEdit}>
<Icon> <Icon>
<Lock /> <Lock />
</Icon> </Icon>
Change Password <Text>{formatMessage(messages.changePassword)}</Text>
</Button> </Button>
{edit && ( {edit && (
<Modal title={formatMessage(messages.changePassword)} onClose={handleClose}> <Modal title={formatMessage(messages.changePassword)} onClose={handleClose}>

View File

@ -3,7 +3,7 @@ import { defineMessages, useIntl } from 'react-intl';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import ProfileDetails from './ProfileDetails'; import ProfileDetails from './ProfileDetails';
import ChangePasswordButton from './ChangePasswordButton'; import PasswordChangeButton from './PasswordChangeButton';
const messages = defineMessages({ const messages = defineMessages({
profile: { id: 'label.profile', defaultMessage: 'Profile' }, profile: { id: 'label.profile', defaultMessage: 'Profile' },
@ -18,7 +18,7 @@ export default function ProfileSettings() {
<Breadcrumbs> <Breadcrumbs>
<Item>{formatMessage(messages.profile)}</Item> <Item>{formatMessage(messages.profile)}</Item>
</Breadcrumbs> </Breadcrumbs>
<ChangePasswordButton /> <PasswordChangeButton />
</PageHeader> </PageHeader>
<ProfileDetails /> <ProfileDetails />
</Page> </Page>

View File

@ -0,0 +1,47 @@
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Button, Icon, Text, Modal, useToast, Icons } from 'react-basics';
import UserAddForm from './UserAddForm';
const { Plus } = Icons;
const messages = defineMessages({
createUser: { id: 'label.create-user', defaultMessage: 'Create user' },
saved: { id: 'message.saved-successfully', defaultMessage: 'Saved successfully.' },
});
export default function UserAddButton() {
const { formatMessage } = useIntl();
const [edit, setEdit] = useState(false);
const { toast, showToast } = useToast();
const handleSave = () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
setEdit(false);
};
const handleAdd = () => {
setEdit(true);
};
const handleClose = () => {
setEdit(false);
};
return (
<>
{toast}
<Button variant="primary" onClick={handleAdd}>
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(messages.createUser)}</Text>
</Button>
{edit && (
<Modal title={formatMessage(messages.createUser)} onClose={handleClose}>
{() => <UserAddForm onSave={handleSave} onClose={handleClose} />}
</Modal>
)}
</>
);
}

View File

@ -8,10 +8,12 @@ import {
TextField, TextField,
PasswordField, PasswordField,
SubmitButton, SubmitButton,
Button,
} from 'react-basics'; } from 'react-basics';
import { useIntl, defineMessages } from 'react-intl'; import { useIntl, defineMessages } from 'react-intl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import { labels } from 'components/messages';
const messages = defineMessages({ const messages = defineMessages({
username: { id: 'label.username', defaultMessage: 'Username' }, username: { id: 'label.username', defaultMessage: 'Username' },
@ -19,14 +21,11 @@ const messages = defineMessages({
role: { id: 'label.role', defaultMessage: 'Role' }, role: { id: 'label.role', defaultMessage: 'Role' },
user: { id: 'label.user', defaultMessage: 'User' }, user: { id: 'label.user', defaultMessage: 'User' },
admin: { id: 'label.admin', defaultMessage: 'Admin' }, admin: { id: 'label.admin', defaultMessage: 'Admin' },
save: { id: 'label.save', defaultMessage: 'Save' },
cancel: { id: 'label.cancel', defaultMessage: 'Cancel' },
required: { id: 'label.required', defaultMessage: 'Required' },
}); });
export default function UserAddForm({ onSave }) { export default function UserAddForm({ onSave, onClose }) {
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation(data => post(`/users`, data)); const { mutate, error, isLoading } = useMutation(data => post(`/users`, data));
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const handleSubmit = async data => { const handleSubmit = async data => {
@ -37,28 +36,42 @@ export default function UserAddForm({ onSave }) {
}); });
}; };
const renderValue = value => {
if (value === ROLES.user) {
return formatMessage(messages.user);
}
if (value === ROLES.admin) {
return formatMessage(messages.admin);
}
};
return ( return (
<Form onSubmit={handleSubmit} error={error}> <Form onSubmit={handleSubmit} error={error}>
<FormRow label={formatMessage(messages.username)}> <FormRow label={formatMessage(messages.username)}>
<FormInput name="username" rules={{ required: formatMessage(messages.required) }}> <FormInput name="username" rules={{ required: formatMessage(labels.required) }}>
<TextField /> <TextField autoComplete="new-username" />
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormRow label={formatMessage(messages.password)}> <FormRow label={formatMessage(messages.password)}>
<FormInput name="password" rules={{ required: formatMessage(messages.required) }}> <FormInput name="password" rules={{ required: formatMessage(labels.required) }}>
<PasswordField /> <PasswordField autoComplete="new-password" />
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormRow label={formatMessage(messages.role)}> <FormRow label={formatMessage(messages.role)}>
<FormInput name="role" rules={{ required: formatMessage(messages.required) }}> <FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
<Dropdown style={{ width: 200 }}> <Dropdown renderValue={renderValue} style={{ width: 200 }}>
<Item key={ROLES.user}>{formatMessage(messages.user)}</Item> <Item key={ROLES.user}>{formatMessage(messages.user)}</Item>
<Item key={ROLES.admin}>{formatMessage(messages.admin)}</Item> <Item key={ROLES.admin}>{formatMessage(messages.admin)}</Item>
</Dropdown> </Dropdown>
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormButtons> <FormButtons flex>
<SubmitButton variant="primary">{formatMessage(messages.save)}</SubmitButton> <SubmitButton variant="primary" disabled={false}>
{formatMessage(labels.save)}
</SubmitButton>
<Button disabled={isLoading} onClick={onClose}>
{formatMessage(labels.cancel)}
</Button>
</FormButtons> </FormButtons>
</Form> </Form>
); );

View File

@ -1,30 +0,0 @@
import UserDeleteForm from 'components/pages/settings/users/UserDeleteForm';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { Button, Form, FormRow, Modal } from 'react-basics';
export default function UserDelete({ userId, onSave }) {
const [modal, setModal] = useState(null);
const router = useRouter();
const handleDelete = async () => {
onSave();
await router.push('/users');
};
const handleClose = () => setModal(null);
return (
<Form>
<FormRow label="Delete user">
<p>All user data will be deleted.</p>
<Button onClick={() => setModal('delete')}>Delete</Button>
</FormRow>
{modal === 'delete' && (
<Modal title="Delete user" onClose={handleClose}>
{close => <UserDeleteForm userId={userId} onSave={handleDelete} onClose={close} />}
</Modal>
)}
</Form>
);
}

View File

@ -1,18 +1,19 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
Button, import { defineMessages, useIntl } from 'react-intl';
Form, import { labels } from 'components/messages';
FormRow,
FormButtons,
FormInput,
SubmitButton,
TextField,
} from 'react-basics';
const CONFIRM_VALUE = 'DELETE'; const messages = defineMessages({
confirm: { id: 'label.confirm', defaultMessage: 'Confirm' },
warning: {
id: 'message.confirm-delete-user',
defaultMessage: 'Are you sure you want to delete this user?',
},
});
export default function UserDeleteForm({ userId, onSave, onClose }) { export default function UserDeleteForm({ userId, onSave, onClose }) {
const { formatMessage } = useIntl();
const { del } = useApi(); const { del } = useApi();
const { mutate, error, isLoading } = useMutation(data => del(`/users/${userId}`, data)); const { mutate, error, isLoading } = useMutation(data => del(`/users/${userId}`, data));
@ -26,20 +27,13 @@ export default function UserDeleteForm({ userId, onSave, onClose }) {
return ( return (
<Form onSubmit={handleSubmit} error={error}> <Form onSubmit={handleSubmit} error={error}>
<p> <p>{formatMessage(messages.warning)}</p>
To delete this user, type <b>{CONFIRM_VALUE}</b> in the box below to confirm.
</p>
<FormRow label="Confirm">
<FormInput name="confirmation" rules={{ validate: value => value === CONFIRM_VALUE }}>
<TextField autoComplete="off" />
</FormInput>
</FormRow>
<FormButtons flex> <FormButtons flex>
<SubmitButton variant="primary" disabled={isLoading}> <SubmitButton variant="primary" disabled={isLoading}>
Save {formatMessage(labels.save)}
</SubmitButton> </SubmitButton>
<Button disabled={isLoading} onClick={onClose}> <Button disabled={isLoading} onClick={onClose}>
Cancel {formatMessage(labels.cancel)}
</Button> </Button>
</FormButtons> </FormButtons>
</Form> </Form>

View File

@ -7,53 +7,75 @@ import {
FormInput, FormInput,
TextField, TextField,
SubmitButton, SubmitButton,
PasswordField,
} from 'react-basics'; } from 'react-basics';
import { useRef } from 'react'; import { defineMessages, useIntl } from 'react-intl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import { labels } from 'components/messages';
const items = [ const messages = defineMessages({
{ username: { id: 'label.username', defaultMessage: 'Username' },
value: ROLES.user,
label: 'User',
},
{
value: ROLES.admin,
label: 'Admin',
},
];
export default function UserEditForm({ data, onSave }) { password: { id: 'label.password', defaultMessage: 'Password' },
const { id } = data; role: { id: 'label.role', defaultMessage: 'Role' },
user: { id: 'label.user', defaultMessage: 'User' },
admin: { id: 'label.admin', defaultMessage: 'Admin' },
minLength: {
id: 'message.min-password-length',
defaultMessage: 'Minimum length of 8 characters',
},
});
export default function UserEditForm({ userId, data, onSave }) {
const { formatMessage } = useIntl();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation(({ username }) => post(`/user/${id}`, { username })); const { mutate, error } = useMutation(({ username }) => post(`/users/${userId}`, { username }));
const ref = useRef(null);
const handleSubmit = async data => { const handleSubmit = async data => {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(data); onSave(data);
ref.current.reset(data);
}, },
}); });
}; };
const renderValue = value => {
if (value === ROLES.user) {
return formatMessage(messages.user);
}
if (value === ROLES.admin) {
return formatMessage(messages.admin);
}
};
return ( return (
<Form key={id} ref={ref} onSubmit={handleSubmit} error={error} values={data}> <Form onSubmit={handleSubmit} error={error} values={data} style={{ width: 600 }}>
<FormRow label="Username"> <FormRow label={formatMessage(messages.username)}>
<FormInput name="username"> <FormInput name="username">
<TextField /> <TextField />
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormRow label="Role"> <FormRow label={formatMessage(messages.password)}>
<FormInput name="role"> <FormInput
<Dropdown items={items} style={{ width: 200 }}> name="newPassword"
{({ value, label }) => <Item key={value}>{label}</Item>} rules={{
minLength: { value: 8, message: formatMessage(messages.minLength) },
}}
>
<PasswordField autoComplete="new-password" />
</FormInput>
</FormRow>
<FormRow label={formatMessage(messages.role)}>
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
<Dropdown renderValue={renderValue} style={{ width: 200 }}>
<Item key={ROLES.user}>{formatMessage(messages.user)}</Item>
<Item key={ROLES.admin}>{formatMessage(messages.admin)}</Item>
</Dropdown> </Dropdown>
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormButtons> <FormButtons>
<SubmitButton variant="primary">Save</SubmitButton> <SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
</FormButtons> </FormButtons>
</Form> </Form>
); );

View File

@ -1,6 +0,0 @@
.form {
display: flex;
flex-direction: column;
gap: 30px;
width: 300px;
}

View File

@ -1,76 +0,0 @@
import { useRef } from 'react';
import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics';
import useApi from 'hooks/useApi';
import useUser from 'hooks/useUser';
export default function UserPasswordForm({ onSave, onClose, userId }) {
const user = useUser();
const isCurrentUser = !userId || user?.id === userId;
const url = isCurrentUser ? `/users/${user?.id}/password` : `/users/${user?.id}`;
const { post, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => post(url, data));
const ref = useRef(null);
const handleSubmit = async data => {
const payload = isCurrentUser
? data
: {
password: data.newPassword,
};
mutate(payload, {
onSuccess: async () => {
onSave();
ref.current.reset();
},
});
};
const samePassword = value => {
if (value !== ref?.current?.getValues('newPassword')) {
return "Passwords don't match";
}
return true;
};
return (
<Form ref={ref} onSubmit={handleSubmit} error={error}>
{isCurrentUser && (
<FormRow label="Current password">
<FormInput name="currentPassword" rules={{ required: 'Required' }}>
<PasswordField autoComplete="off" />
</FormInput>
</FormRow>
)}
<FormRow label="New password">
<FormInput
name="newPassword"
rules={{
required: 'Required',
minLength: { value: 8, message: 'Minimum length 8 characters' },
}}
>
<PasswordField autoComplete="off" />
</FormInput>
</FormRow>
<FormRow label="Confirm password">
<FormInput
name="confirmPassword"
rules={{
required: 'Required',
minLength: { value: 8, message: 'Minimum length 8 characters' },
validate: samePassword,
}}
>
<PasswordField autoComplete="off" />
</FormInput>
</FormRow>
<FormButtons flex>
<Button type="submit" variant="primary" disabled={isLoading}>
Save
</Button>
<Button onClick={onClose}>Close</Button>
</FormButtons>
</Form>
);
}

View File

@ -3,17 +3,17 @@ import { defineMessages, useIntl } from 'react-intl';
import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics'; import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import UserDelete from 'components/pages/settings/users/UserDelete'; import UserDeleteForm from 'components/pages/settings/users/UserDeleteForm';
import UserEditForm from 'components/pages/settings/users//UserEditForm'; import UserEditForm from 'components/pages/settings/users//UserEditForm';
import UserPasswordForm from 'components/pages/settings/users/UserPasswordForm';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import WebsitesTable from '../websites/WebsitesTable';
const messages = defineMessages({ const messages = defineMessages({
users: { id: 'label.users', defaultMessage: 'Users' }, users: { id: 'label.users', defaultMessage: 'Users' },
details: { id: 'label.details', defaultMessage: 'Details' }, details: { id: 'label.details', defaultMessage: 'Details' },
changePassword: { id: 'label.change-password', defaultMessage: 'Change password' }, websites: { id: 'label.websites', defaultMessage: 'Websites' },
actions: { id: 'label.actions', defaultMessage: 'Actions' }, actions: { id: 'label.actions', defaultMessage: 'Actions' },
saved: { id: 'message.saved-successfully', defaultMessage: 'Saved successfully.' }, saved: { id: 'message.saved-successfully', defaultMessage: 'Saved successfully.' },
delete: { id: 'message.delete-successfully', defaultMessage: 'Delete successfully.' }, delete: { id: 'message.delete-successfully', defaultMessage: 'Delete successfully.' },
@ -38,7 +38,7 @@ export default function UserSettings({ userId }) {
); );
const handleSave = data => { const handleSave = data => {
showToast({ message: 'Saved successfully.', variant: 'success' }); showToast({ message: formatMessage(messages.saved), variant: 'success' });
if (data) { if (data) {
setValues(state => ({ ...state, ...data })); setValues(state => ({ ...state, ...data }));
} }
@ -49,7 +49,7 @@ export default function UserSettings({ userId }) {
}; };
const handleDelete = async () => { const handleDelete = async () => {
showToast({ message: 'Deleted successfully.', variant: 'danger' }); showToast({ message: formatMessage(messages.delete), variant: 'danger' });
await router.push('/users'); await router.push('/users');
}; };
@ -72,12 +72,12 @@ export default function UserSettings({ userId }) {
</PageHeader> </PageHeader>
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}> <Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
<Item key="details">{formatMessage(messages.details)}</Item> <Item key="details">{formatMessage(messages.details)}</Item>
<Item key="password">{formatMessage(messages.changePassword)}</Item> <Item key="websites">{formatMessage(messages.websites)}</Item>
<Item key="delete">{formatMessage(messages.actions)}</Item> <Item key="delete">{formatMessage(messages.actions)}</Item>
</Tabs> </Tabs>
{tab === 'details' && <UserEditForm userId={userId} data={values} onSave={handleSave} />} {tab === 'details' && <UserEditForm userId={userId} data={values} onSave={handleSave} />}
{tab === 'password' && <UserPasswordForm userId={userId} onSave={handleSave} />} {tab === 'websites' && <WebsitesTable />}
{tab === 'delete' && <UserDelete userId={userId} onSave={handleDelete} />} {tab === 'delete' && <UserDeleteForm userId={userId} onSave={handleDelete} />}
</Page> </Page>
); );
} }

View File

@ -1,20 +1,15 @@
import { useState } from 'react';
import { Button, Text, Icon, useToast, Icons, Modal } from 'react-basics';
import { useIntl, defineMessages } from 'react-intl'; import { useIntl, defineMessages } from 'react-intl';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import UsersTable from 'components/pages/settings/users/UsersTable'; import UsersTable from './UsersTable';
import UserEditForm from 'components/pages/settings/users/UserEditForm'; import UserAddButton from './UserAddButton';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
const { Plus } = Icons;
const messages = defineMessages({ const messages = defineMessages({
saved: { id: 'messages.api-key-saved', defaultMessage: 'API key saved.' },
noUsers: { noUsers: {
id: 'messages.no-useres', id: 'messages.no-users',
defaultMessage: "You don't have any users.", defaultMessage: "You don't have any users.",
}, },
users: { id: 'label.users', defaultMessage: 'Users' }, users: { id: 'label.users', defaultMessage: 'Users' },
@ -22,48 +17,23 @@ const messages = defineMessages({
}); });
export default function UsersList() { export default function UsersList() {
const [edit, setEdit] = useState(false);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { toast, showToast } = useToast();
const { user } = useUser(); const { user } = useUser();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading, error, refetch } = useQuery(['user'], () => get(`/users`), { const { data, isLoading, error } = useQuery(['user'], () => get(`/users`), {
enabled: !!user, enabled: !!user,
}); });
const hasData = data && data.length !== 0; const hasData = data && data.length !== 0;
const handleSave = async () => { const addButton = <UserAddButton />;
await refetch();
setEdit(false);
showToast({ message: formatMessage(messages.saved), variant: 'success' });
};
const handleAdd = () => setEdit(true);
const handleClose = () => setEdit(false);
const addButton = (
<Button variant="primary" onClick={handleAdd}>
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(messages.createUser)}</Text>
</Button>
);
return ( return (
<Page loading={isLoading} error={error}> <Page loading={isLoading} error={error}>
{toast}
<PageHeader title={formatMessage(messages.users)}>{addButton}</PageHeader> <PageHeader title={formatMessage(messages.users)}>{addButton}</PageHeader>
{hasData && <UsersTable data={data} />} {hasData && <UsersTable data={data} />}
{!hasData && ( {!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noUsers)}>{addButton}</EmptyPlaceholder> <EmptyPlaceholder message={formatMessage(messages.noUsers)}>{addButton}</EmptyPlaceholder>
)} )}
{edit && (
<Modal title={formatMessage(messages.createUser)} onClose={handleClose}>
{close => <UserEditForm onSave={handleSave} onClose={close} />}
</Modal>
)}
</Page> </Page>
); );
} }

View File

@ -1,4 +1,3 @@
import { useRef } from 'react';
import { import {
Form, Form,
FormRow, FormRow,
@ -8,13 +7,19 @@ import {
Button, Button,
SubmitButton, SubmitButton,
} from 'react-basics'; } from 'react-basics';
import { defineMessages, useIntl } from 'react-intl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { DOMAIN_REGEX } from 'lib/constants'; import { DOMAIN_REGEX } from 'lib/constants';
import { labels } from 'components/messages';
const messages = defineMessages({
invalidDomain: { id: 'label.invalid-domain', defaultMessage: 'Invalid domain' },
});
export default function WebsiteAddForm({ onSave, onClose }) { export default function WebsiteAddForm({ onSave, onClose }) {
const { formatMessage } = useIntl();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => post('/websites', data)); const { mutate, error, isLoading } = useMutation(data => post('/websites', data));
const ref = useRef(null);
const handleSubmit = async data => { const handleSubmit = async data => {
mutate(data, { mutate(data, {
@ -25,9 +30,9 @@ export default function WebsiteAddForm({ onSave, onClose }) {
}; };
return ( return (
<Form ref={ref} onSubmit={handleSubmit} error={error}> <Form onSubmit={handleSubmit} error={error}>
<FormRow label="Name"> <FormRow label="Name">
<FormInput name="name" rules={{ required: 'Required' }}> <FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
<TextField autoComplete="off" /> <TextField autoComplete="off" />
</FormInput> </FormInput>
</FormRow> </FormRow>
@ -35,8 +40,8 @@ export default function WebsiteAddForm({ onSave, onClose }) {
<FormInput <FormInput
name="domain" name="domain"
rules={{ rules={{
required: 'Required', required: formatMessage(labels.required),
pattern: { value: DOMAIN_REGEX, message: 'Invalid domain' }, pattern: { value: DOMAIN_REGEX, message: formatMessage(messages.invalidDomain) },
}} }}
> >
<TextField autoComplete="off" /> <TextField autoComplete="off" />
@ -44,10 +49,10 @@ export default function WebsiteAddForm({ onSave, onClose }) {
</FormRow> </FormRow>
<FormButtons flex> <FormButtons flex>
<SubmitButton variant="primary" disabled={false}> <SubmitButton variant="primary" disabled={false}>
Save {formatMessage(labels.save)}
</SubmitButton> </SubmitButton>
<Button disabled={isLoading} onClick={onClose}> <Button disabled={isLoading} onClick={onClose}>
Cancel {formatMessage(labels.cancel)}
</Button> </Button>
</FormButtons> </FormButtons>
</Form> </Form>

View File

@ -94,7 +94,7 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-basics": "^0.53.0", "react-basics": "^0.55.0",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-intl": "^5.24.7", "react-intl": "^5.24.7",

View File

@ -6562,10 +6562,10 @@ rc@^1.2.7:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
react-basics@^0.53.0: react-basics@^0.55.0:
version "0.53.0" version "0.55.0"
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.53.0.tgz#2215ca47bca99a316cd129875682960d6519611f" resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.55.0.tgz#b9c3dbba33a3ce118e63322ddcb822139a3ea547"
integrity sha512-XsG38JsumhHvqGaVnY7oRErs7Ub1Td1GjFE0VG4Uc7SlwRzK1idmtaZGW6dbPpHhmwkDNa7xnohRaKgRBmP2pg== integrity sha512-30ygRxj7l0KjbmOGF2xcptCOSr8Bw85n6U7I4RzHI5VKvzq7CliKaXr+xvGrRFPvNQ6TnrY25uFdUd7BFDHvGA==
dependencies: dependencies:
classnames "^2.3.1" classnames "^2.3.1"
react "^18.2.0" react "^18.2.0"