Refactored intl messages.

This commit is contained in:
Mike Cao 2023-01-25 07:42:46 -08:00
parent fbccf4d3af
commit 7725b5c129
44 changed files with 558 additions and 485 deletions

View file

@ -1,16 +1,12 @@
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useIntl } from 'react-intl';
import { Button, Icon, Text, Modal, useToast, Icons } from 'react-basics';
import UserAddForm from './UserAddForm';
import { labels, messages } from 'components/messages';
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() {
export default function UserAddButton({ onSave }) {
const { formatMessage } = useIntl();
const [edit, setEdit] = useState(false);
const { toast, showToast } = useToast();
@ -18,6 +14,7 @@ export default function UserAddButton() {
const handleSave = () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
setEdit(false);
onSave();
};
const handleAdd = () => {
@ -35,11 +32,11 @@ export default function UserAddButton() {
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(messages.createUser)}</Text>
<Text>{formatMessage(labels.createUser)}</Text>
</Button>
{edit && (
<Modal title={formatMessage(messages.createUser)} onClose={handleClose}>
{() => <UserAddForm onSave={handleSave} onClose={handleClose} />}
<Modal title={formatMessage(labels.createUser)} onClose={handleClose}>
<UserAddForm onSave={handleSave} onClose={handleClose} />
</Modal>
)}
</>

View file

@ -10,19 +10,11 @@ import {
SubmitButton,
Button,
} from 'react-basics';
import { useIntl, defineMessages } from 'react-intl';
import { useIntl } from 'react-intl';
import useApi from 'hooks/useApi';
import { ROLES } from 'lib/constants';
import { labels } from 'components/messages';
const messages = defineMessages({
username: { id: 'label.username', defaultMessage: 'Username' },
password: { id: 'label.password', defaultMessage: 'Password' },
role: { id: 'label.role', defaultMessage: 'Role' },
user: { id: 'label.user', defaultMessage: 'User' },
admin: { id: 'label.admin', defaultMessage: 'Admin' },
});
export default function UserAddForm({ onSave, onClose }) {
const { post, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => post(`/users`, data));
@ -38,30 +30,30 @@ export default function UserAddForm({ onSave, onClose }) {
const renderValue = value => {
if (value === ROLES.user) {
return formatMessage(messages.user);
return formatMessage(labels.user);
}
if (value === ROLES.admin) {
return formatMessage(messages.admin);
return formatMessage(labels.admin);
}
};
return (
<Form onSubmit={handleSubmit} error={error}>
<FormRow label={formatMessage(messages.username)}>
<FormRow label={formatMessage(labels.username)}>
<FormInput name="username" rules={{ required: formatMessage(labels.required) }}>
<TextField autoComplete="new-username" />
</FormInput>
</FormRow>
<FormRow label={formatMessage(messages.password)}>
<FormRow label={formatMessage(labels.password)}>
<FormInput name="password" rules={{ required: formatMessage(labels.required) }}>
<PasswordField autoComplete="new-password" />
</FormInput>
</FormRow>
<FormRow label={formatMessage(messages.role)}>
<FormRow label={formatMessage(labels.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 renderValue={renderValue}>
<Item key={ROLES.user}>{formatMessage(labels.user)}</Item>
<Item key={ROLES.admin}>{formatMessage(labels.admin)}</Item>
</Dropdown>
</FormInput>
</FormRow>

View file

@ -1,36 +1,29 @@
import { useMutation } from '@tanstack/react-query';
import useApi from 'hooks/useApi';
import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
import { defineMessages, useIntl } from 'react-intl';
import { labels } from 'components/messages';
import { useIntl } from 'react-intl';
import { labels, messages } from 'components/messages';
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, username, onSave, onClose }) {
const { formatMessage } = useIntl();
const { del } = useApi();
const { mutate, error, isLoading } = useMutation(data => del(`/users/${userId}`, data));
const { mutate, error, isLoading } = useMutation(() => del(`/users/${userId}`));
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave();
onClose();
},
});
};
return (
<Form onSubmit={handleSubmit} error={error}>
<p>{formatMessage(messages.warning)}</p>
<p>{formatMessage(messages.deleteUserWarning, { username })}</p>
<FormButtons flex>
<SubmitButton variant="primary" disabled={isLoading}>
{formatMessage(labels.save)}
{formatMessage(labels.delete)}
</SubmitButton>
<Button disabled={isLoading} onClick={onClose}>
{formatMessage(labels.cancel)}

View file

@ -9,23 +9,10 @@ import {
SubmitButton,
PasswordField,
} from 'react-basics';
import { defineMessages, useIntl } from 'react-intl';
import { useIntl } from 'react-intl';
import useApi from 'hooks/useApi';
import { ROLES } from 'lib/constants';
import { labels } from 'components/messages';
const messages = defineMessages({
username: { id: 'label.username', defaultMessage: 'Username' },
password: { id: 'label.password', defaultMessage: 'Password' },
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',
},
});
import { labels, messages } from 'components/messages';
export default function UserEditForm({ userId, data, onSave }) {
const { formatMessage } = useIntl();
@ -42,35 +29,35 @@ export default function UserEditForm({ userId, data, onSave }) {
const renderValue = value => {
if (value === ROLES.user) {
return formatMessage(messages.user);
return formatMessage(labels.user);
}
if (value === ROLES.admin) {
return formatMessage(messages.admin);
return formatMessage(labels.admin);
}
};
return (
<Form onSubmit={handleSubmit} error={error} values={data} style={{ width: 600 }}>
<FormRow label={formatMessage(messages.username)}>
<Form onSubmit={handleSubmit} error={error} values={data} style={{ width: 300 }}>
<FormRow label={formatMessage(labels.username)}>
<FormInput name="username">
<TextField />
</FormInput>
</FormRow>
<FormRow label={formatMessage(messages.password)}>
<FormRow label={formatMessage(labels.password)}>
<FormInput
name="newPassword"
rules={{
minLength: { value: 8, message: formatMessage(messages.minLength) },
minLength: { value: 8, message: formatMessage(messages.minPasswordLength) },
}}
>
<PasswordField autoComplete="new-password" />
</FormInput>
</FormRow>
<FormRow label={formatMessage(messages.role)}>
<FormRow label={formatMessage(labels.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 renderValue={renderValue}>
<Item key={ROLES.user}>{formatMessage(labels.user)}</Item>
<Item key={ROLES.admin}>{formatMessage(labels.admin)}</Item>
</Dropdown>
</FormInput>
</FormRow>

View file

@ -1,23 +1,13 @@
import { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useIntl } from 'react-intl';
import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
import Link from 'next/link';
import { useRouter } from 'next/router';
import UserDeleteForm from 'components/pages/settings/users/UserDeleteForm';
import UserEditForm from 'components/pages/settings/users//UserEditForm';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import useApi from 'hooks/useApi';
import WebsitesTable from '../websites/WebsitesTable';
const messages = defineMessages({
users: { id: 'label.users', defaultMessage: 'Users' },
details: { id: 'label.details', defaultMessage: 'Details' },
websites: { id: 'label.websites', defaultMessage: 'Websites' },
actions: { id: 'label.actions', defaultMessage: 'Actions' },
saved: { id: 'message.saved-successfully', defaultMessage: 'Saved successfully.' },
delete: { id: 'message.delete-successfully', defaultMessage: 'Delete successfully.' },
});
import { labels, messages } from 'components/messages';
import UserWebsites from './UserWebsites';
export default function UserSettings({ userId }) {
const { formatMessage } = useIntl();
@ -26,7 +16,6 @@ export default function UserSettings({ userId }) {
const [tab, setTab] = useState('details');
const { get, useQuery } = useApi();
const { toast, showToast } = useToast();
const router = useRouter();
const { data, isLoading } = useQuery(
['user', userId],
() => {
@ -48,11 +37,6 @@ export default function UserSettings({ userId }) {
}
};
const handleDelete = async () => {
showToast({ message: formatMessage(messages.delete), variant: 'danger' });
await router.push('/users');
};
useEffect(() => {
if (data) {
setValues(data);
@ -65,19 +49,17 @@ export default function UserSettings({ userId }) {
<PageHeader>
<Breadcrumbs>
<Item>
<Link href="/settings/users">{formatMessage(messages.users)}</Link>
<Link href="/settings/users">{formatMessage(labels.users)}</Link>
</Item>
<Item>{values?.username}</Item>
</Breadcrumbs>
</PageHeader>
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
<Item key="details">{formatMessage(messages.details)}</Item>
<Item key="websites">{formatMessage(messages.websites)}</Item>
<Item key="delete">{formatMessage(messages.actions)}</Item>
<Item key="details">{formatMessage(labels.details)}</Item>
<Item key="websites">{formatMessage(labels.websites)}</Item>
</Tabs>
{tab === 'details' && <UserEditForm userId={userId} data={values} onSave={handleSave} />}
{tab === 'websites' && <WebsitesTable />}
{tab === 'delete' && <UserDeleteForm userId={userId} onSave={handleDelete} />}
{tab === 'websites' && <UserWebsites userId={userId} />}
</Page>
);
}

View file

@ -0,0 +1,25 @@
import { Loading } from 'react-basics';
import { useIntl } from 'react-intl';
import useApi from 'hooks/useApi';
import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
import { messages } from 'components/messages';
export default function UserWebsites({ userId }) {
const { formatMessage } = useIntl();
const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(['user/websites', userId], () =>
get(`/users/${userId}/websites`),
);
const hasData = data && data.length !== 0;
if (isLoading) {
return <Loading icon="dots" position="block" />;
}
return (
<div>
{hasData && <WebsitesTable data={data} />}
{!hasData && formatMessage(messages.noData)}
</div>
);
}

View file

@ -1,4 +1,4 @@
import { useIntl, defineMessages } from 'react-intl';
import { useIntl } from 'react-intl';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
@ -6,33 +6,35 @@ import UsersTable from './UsersTable';
import UserAddButton from './UserAddButton';
import useApi from 'hooks/useApi';
import useUser from 'hooks/useUser';
const messages = defineMessages({
noUsers: {
id: 'messages.no-users',
defaultMessage: "You don't have any users.",
},
users: { id: 'label.users', defaultMessage: 'Users' },
createUser: { id: 'label.create-user', defaultMessage: 'Create user' },
});
import { useToast } from 'react-basics';
import { labels, messages } from 'components/messages';
export default function UsersList() {
const { formatMessage } = useIntl();
const { user } = useUser();
const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(['user'], () => get(`/users`), {
const { data, isLoading, error, refetch } = useQuery(['user'], () => get(`/users`), {
enabled: !!user,
});
const { toast, showToast } = useToast();
const hasData = data && data.length !== 0;
const addButton = <UserAddButton />;
const handleSave = () => refetch();
const handleDelete = () =>
showToast({ message: formatMessage(messages.deleted), variant: 'success' });
return (
<Page loading={isLoading} error={error}>
<PageHeader title={formatMessage(messages.users)}>{addButton}</PageHeader>
{hasData && <UsersTable data={data} />}
{toast}
<PageHeader title={formatMessage(labels.users)}>
<UserAddButton onSave={handleSave} />
</PageHeader>
{hasData && <UsersTable data={data} onDelete={handleDelete} />}
{!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noUsers)}>{addButton}</EmptyPlaceholder>
<EmptyPlaceholder message={formatMessage(messages.noUsers)}>
<UserAddButton onSave={handleSave} />
</EmptyPlaceholder>
)}
</Page>
);

View file

@ -8,26 +8,38 @@ import {
TableColumn,
TableHeader,
TableRow,
Flexbox,
Icons,
ModalTrigger,
} from 'react-basics';
import { useIntl } from 'react-intl';
import { formatDistance } from 'date-fns';
import Link from 'next/link';
import { Edit } from 'components/icons';
import styles from './UsersTable.module.css';
import useUser from 'hooks/useUser';
import UserDeleteForm from './UserDeleteForm';
import { labels } from 'components/messages';
import { ROLES } from 'lib/constants';
const columns = [
{ name: 'username', label: 'Username', style: { flex: 2 } },
{ name: 'role', label: 'Role', style: { flex: 2 } },
{ name: 'created', label: 'Created' },
{ name: 'action', label: ' ' },
];
const { Trash } = Icons;
export default function UsersTable({ data = [], onDelete }) {
const { formatMessage } = useIntl();
const { user } = useUser();
const columns = [
{ name: 'username', label: formatMessage(labels.username), style: { flex: 2 } },
{ name: 'role', label: formatMessage(labels.role), style: { flex: 1 } },
{ name: 'created', label: formatMessage(labels.created), style: { flex: 1 } },
{ name: 'action', label: ' ', style: { flex: 2 } },
];
export default function UsersTable({ data = [] }) {
return (
<Table className={styles.table} columns={columns} rows={data}>
<Table columns={columns} rows={data}>
<TableHeader>
{(column, index) => {
return (
<TableColumn key={index} className={styles.header} style={{ ...column.style }}>
<TableColumn key={index} style={{ ...column.style }}>
{column.label}
</TableColumn>
);
@ -35,33 +47,57 @@ export default function UsersTable({ data = [] }) {
</TableHeader>
<TableBody>
{(row, keys, rowIndex) => {
row.created = formatDistance(new Date(row.createdAt), new Date(), {
addSuffix: true,
});
row.action = (
<div className={styles.actions}>
<Link href={`/settings/users/${row.id}`}>
<Button>
<Icon>
<Edit />
</Icon>
<Text>Edit</Text>
</Button>
</Link>
</div>
);
const rowData = {
...row,
created: formatDistance(new Date(row.createdAt), new Date(), {
addSuffix: true,
}),
role: formatMessage(
labels[Object.keys(ROLES).find(key => ROLES[key] === row.role) || labels.unknown],
),
action: (
<>
<Link href={`/settings/users/${row.id}`}>
<Button>
<Icon>
<Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
</Link>
<ModalTrigger disabled={row.id === user.id}>
<Button disabled={row.id === user.id}>
<Icon>
<Trash />
</Icon>
<Text>{formatMessage(labels.delete)}</Text>
</Button>
{close => (
<UserDeleteForm
userId={row.id}
username={row.username}
onSave={onDelete}
onClose={close}
/>
)}
</ModalTrigger>
</>
),
};
return (
<TableRow key={rowIndex} data={row} keys={keys}>
<TableRow key={rowIndex} data={rowData} keys={keys}>
{(data, key, colIndex) => {
return (
<TableCell
key={colIndex}
className={styles.cell}
style={{ ...columns[colIndex]?.style }}
>
{data[key]}
<TableCell key={colIndex} style={{ ...columns[colIndex]?.style }}>
<Flexbox
flex={1}
gap={10}
alignItems="center"
justifyContent={key === 'action' ? 'end' : undefined}
>
{data[key]}
</Flexbox>
</TableCell>
);
}}

View file

@ -1,26 +0,0 @@
.table th,
.table td {
flex: 2;
}
.cell {
display: flex;
align-items: center;
}
.input {
flex: 2;
}
.cell:last-child {
justify-content: flex-end;
}
.actions {
display: flex;
gap: 12px;
}
.empty {
min-height: 300px;
}