mirror of
https://github.com/kremalicious/umami.git
synced 2024-06-30 13:41:50 +02:00
Refactored intl messages.
This commit is contained in:
parent
fbccf4d3af
commit
7725b5c129
|
@ -5,11 +5,108 @@ export const labels = defineMessages({
|
||||||
required: { id: 'label.required', defaultMessage: 'Required' },
|
required: { id: 'label.required', defaultMessage: 'Required' },
|
||||||
save: { id: 'label.save', defaultMessage: 'Save' },
|
save: { id: 'label.save', defaultMessage: 'Save' },
|
||||||
cancel: { id: 'label.cancel', defaultMessage: 'Cancel' },
|
cancel: { id: 'label.cancel', defaultMessage: 'Cancel' },
|
||||||
|
continue: { id: 'label.continue', defaultMessage: 'Continue' },
|
||||||
|
delete: { id: 'label.delete', defaultMessage: 'Delete' },
|
||||||
|
users: { id: 'label.users', defaultMessage: 'Users' },
|
||||||
|
createUser: { id: 'label.create-user', defaultMessage: 'Create user' },
|
||||||
|
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' },
|
||||||
|
confirm: { id: 'label.confirm', defaultMessage: 'Confirm' },
|
||||||
|
details: { id: 'label.details', defaultMessage: 'Details' },
|
||||||
|
websites: { id: 'label.websites', defaultMessage: 'Websites' },
|
||||||
|
created: { id: 'label.created', defaultMessage: 'Created' },
|
||||||
|
edit: { id: 'label.edit', defaultMessage: 'Edit' },
|
||||||
|
name: { id: 'label.name', defaultMessage: 'Name' },
|
||||||
|
members: { id: 'label.members', defaultMessage: 'Members' },
|
||||||
|
accessCode: { id: 'label.access-code', defaultMessage: 'Access code' },
|
||||||
|
teamId: { id: 'label.team-id', defaultMessage: 'Team ID' },
|
||||||
|
team: { id: 'label.team', defaultMessage: 'Team' },
|
||||||
|
regenerate: { id: 'label.regenerate', defaultMessage: 'Regenerate' },
|
||||||
|
remove: { id: 'label.remove', defaultMessage: 'Remove' },
|
||||||
|
createTeam: { id: 'label.create-team', defaultMessage: 'Create team' },
|
||||||
|
settings: { id: 'label.settings', defaultMessage: 'Settings' },
|
||||||
|
teamOwner: { id: 'label.team-owner', defaultMessage: 'Team owner' },
|
||||||
|
teamMember: { id: 'label.team-member', defaultMessage: 'Team member' },
|
||||||
|
teamGuest: { id: 'label.team-guest', defaultMessage: 'Team guest' },
|
||||||
|
enableShareUrl: { id: 'label.enable-share-url', defaultMessage: 'Enable share URL' },
|
||||||
|
data: { id: 'label.data', defaultMessage: 'Data' },
|
||||||
|
trackingCode: { id: 'label.tracking-code', defaultMessage: 'Tracking code' },
|
||||||
|
shareUrl: { id: 'label.share-url', defaultMessage: 'Share URL' },
|
||||||
|
actions: { id: 'label.actions', defaultMessage: 'Actions' },
|
||||||
|
view: { id: 'label.view', defaultMessage: 'View' },
|
||||||
|
domain: { id: 'label.domain', defaultMessage: 'Domain' },
|
||||||
|
websiteId: { id: 'label.website-id', defaultMessage: 'Website ID' },
|
||||||
|
resetWebsite: { id: 'label.reset-website', defaultMessage: 'Reset website' },
|
||||||
|
deleteWebsite: { id: 'label.delete-website', defaultMessage: 'Delete website' },
|
||||||
|
reset: { id: 'label.reset', defaultMessage: 'Reset' },
|
||||||
|
addWebsite: { id: 'label.add-website', defaultMessage: 'Add website' },
|
||||||
|
changePassword: { id: 'label.change-password', defaultMessage: 'Change password' },
|
||||||
|
currentPassword: { id: 'label.current-password', defaultMessage: 'Current password' },
|
||||||
|
newPassword: { id: 'label.new-password', defaultMessage: 'New password' },
|
||||||
|
confirmPassword: { id: 'label.confirm-password', defaultMessage: 'Confirm password' },
|
||||||
|
timezone: { id: 'label.timezone', defaultMessage: 'Timezone' },
|
||||||
|
dateRange: { id: 'label.default-date-range', defaultMessage: 'Default date range' },
|
||||||
|
language: { id: 'label.language', defaultMessage: 'Language' },
|
||||||
|
theme: { id: 'label.theme', defaultMessage: 'Theme' },
|
||||||
|
profile: { id: 'label.profile', defaultMessage: 'Profile' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
error: { id: 'message.error', defaultMessage: 'Something went wrong.' },
|
error: { id: 'message.error', defaultMessage: 'Something went wrong.' },
|
||||||
saved: { id: 'message.saved-successfully', defaultMessage: 'Saved successfully.' },
|
saved: { id: 'message.saved', defaultMessage: 'Saved successfully.' },
|
||||||
|
noUsers: { id: 'message.no-users', defaultMessage: 'There are no users.' },
|
||||||
|
userDeleted: { id: 'message.user-deleted', defaultMessage: 'User deleted successfully.' },
|
||||||
|
noData: { id: 'message.no-data', defaultMessage: 'No data available.' },
|
||||||
|
deleteUserWarning: {
|
||||||
|
id: 'message.delete-user-warning',
|
||||||
|
defaultMessage: 'Are you sure you want to delete {username}?',
|
||||||
|
},
|
||||||
|
minPasswordLength: {
|
||||||
|
id: 'message.min-password-length',
|
||||||
|
defaultMessage: 'Minimum length of 8 characters',
|
||||||
|
},
|
||||||
|
noTeams: {
|
||||||
|
id: 'message.no-teams',
|
||||||
|
defaultMessage: 'You have no created any teams.',
|
||||||
|
},
|
||||||
|
shareUrl: {
|
||||||
|
id: 'message.share-url',
|
||||||
|
defaultMessage: 'Your website stats are publically available at the following URL:',
|
||||||
|
},
|
||||||
|
trackingCode: {
|
||||||
|
id: 'message.tracking-code',
|
||||||
|
defaultMessage:
|
||||||
|
'To track stats for this website, place the following code in the <head> section of your HTML.',
|
||||||
|
},
|
||||||
|
deleteWebsite: {
|
||||||
|
id: 'message.delete-website',
|
||||||
|
defaultMessage: 'To delete this website, type {confirmation} in the box below to confirm.',
|
||||||
|
},
|
||||||
|
resetWebsite: {
|
||||||
|
id: 'message.reset-website',
|
||||||
|
defaultMessage: 'To reset this website, type {confirmation} in the box below to confirm.',
|
||||||
|
},
|
||||||
|
invalidDomain: {
|
||||||
|
id: 'message.invalid-domain',
|
||||||
|
defaultMessage: 'Invalid domain. Do not include http/https.',
|
||||||
|
},
|
||||||
|
resetWebsiteWarning: {
|
||||||
|
id: 'message.reset-website-warning',
|
||||||
|
defaultMessage:
|
||||||
|
'All statistics for this website will be deleted, but your settings will remain intact.',
|
||||||
|
},
|
||||||
|
deleteWebsiteWarning: {
|
||||||
|
id: 'message.delete-website-warning',
|
||||||
|
defaultMessage: 'All website data will be deleted.',
|
||||||
|
},
|
||||||
|
noWebsites: {
|
||||||
|
id: 'messages.no-websites',
|
||||||
|
defaultMessage: 'You do not have any websites configured.',
|
||||||
|
},
|
||||||
|
noMatchPassword: { id: 'message.no-match-password', defaultMessage: 'Passwords do not match.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const devices = defineMessages({
|
export const devices = defineMessages({
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import DateFilter from 'components/common/DateFilter';
|
import DateFilter from 'components/common/DateFilter';
|
||||||
import { Button, Flexbox } from 'react-basics';
|
import { Button, Flexbox } from 'react-basics';
|
||||||
import useDateRange from 'hooks/useDateRange';
|
import useDateRange from 'hooks/useDateRange';
|
||||||
import { DEFAULT_DATE_RANGE } from 'lib/constants';
|
import { DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
|
|
||||||
export default function DateRangeSetting() {
|
export default function DateRangeSetting() {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const [dateRange, setDateRange] = useDateRange();
|
const [dateRange, setDateRange] = useDateRange();
|
||||||
const { startDate, endDate, value } = dateRange;
|
const { startDate, endDate, value } = dateRange;
|
||||||
|
|
||||||
|
@ -13,9 +15,7 @@ export default function DateRangeSetting() {
|
||||||
return (
|
return (
|
||||||
<Flexbox width={400} gap={10}>
|
<Flexbox width={400} gap={10}>
|
||||||
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={setDateRange} />
|
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={setDateRange} />
|
||||||
<Button onClick={handleReset}>
|
<Button onClick={handleReset}>{formatMessage(labels.reset)}</Button>
|
||||||
<FormattedMessage id="label.reset" defaultMessage="Reset" />
|
|
||||||
</Button>
|
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { Button, Dropdown, Item, Flexbox } from 'react-basics';
|
import { Button, Dropdown, Item, Flexbox } from 'react-basics';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import { DEFAULT_LOCALE } from 'lib/constants';
|
import { DEFAULT_LOCALE } from 'lib/constants';
|
||||||
import { languages } from 'lib/lang';
|
import { languages } from 'lib/lang';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
const messages = defineMessages({
|
|
||||||
reset: { id: 'label.reset', defaultMessage: 'Reset' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function LanguageSetting() {
|
export default function LanguageSetting() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
@ -28,7 +25,7 @@ export default function LanguageSetting() {
|
||||||
>
|
>
|
||||||
{item => <Item key={item}>{languages[item].label}</Item>}
|
{item => <Item key={item}>{languages[item].label}</Item>}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Button onClick={handleReset}>{formatMessage(messages.reset)}</Button>
|
<Button onClick={handleReset}>{formatMessage(labels.reset)}</Button>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +1,29 @@
|
||||||
import { useState } from 'react';
|
import { useIntl } from 'react-intl';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { Button, Icon, Text, useToast, ModalTrigger } 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';
|
||||||
|
import { labels, messages } from 'components/messages';
|
||||||
const messages = defineMessages({
|
|
||||||
changePassword: { id: 'label.change-password', defaultMessage: 'Change password' },
|
|
||||||
saved: { id: 'message.saved-successfully', defaultMessage: 'Saved successfully.' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function PasswordChangeButton() {
|
export default function PasswordChangeButton() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const [edit, setEdit] = useState(false);
|
|
||||||
const { toast, showToast } = useToast();
|
const { toast, showToast } = useToast();
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
setEdit(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = () => {
|
|
||||||
setEdit(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setEdit(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{toast}
|
{toast}
|
||||||
<Button onClick={handleEdit}>
|
<ModalTrigger modalProps={{ title: formatMessage(labels.changePassword) }}>
|
||||||
<Icon>
|
<Button>
|
||||||
<Lock />
|
<Icon>
|
||||||
</Icon>
|
<Lock />
|
||||||
<Text>{formatMessage(messages.changePassword)}</Text>
|
</Icon>
|
||||||
</Button>
|
<Text>{formatMessage(labels.changePassword)}</Text>
|
||||||
{edit && (
|
</Button>
|
||||||
<Modal title={formatMessage(messages.changePassword)} onClose={handleClose}>
|
{close => <PasswordEditForm onSave={handleSave} onClose={close} />}
|
||||||
{() => <PasswordEditForm onSave={handleSave} onClose={handleClose} />}
|
</ModalTrigger>
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,75 +1,65 @@
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics';
|
import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import useUser from 'hooks/useUser';
|
import { labels, messages } from 'components/messages';
|
||||||
|
|
||||||
export default function PasswordEditForm({ userId, onSave, onClose }) {
|
export default function PasswordEditForm({ onSave, onClose }) {
|
||||||
const user = useUser();
|
const { formatMessage } = useIntl();
|
||||||
const isCurrentUser = !userId || user?.id === userId;
|
|
||||||
const url = isCurrentUser ? `/users/${user?.id}/password` : `/users/${user?.id}`;
|
|
||||||
const { post, useMutation } = useApi();
|
const { post, useMutation } = useApi();
|
||||||
const { mutate, error, isLoading } = useMutation(data => post(url, data));
|
const { mutate, error, isLoading } = useMutation(data => post('/me/password', data));
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|
||||||
const handleSubmit = async data => {
|
const handleSubmit = async data => {
|
||||||
const payload = isCurrentUser
|
mutate(data, {
|
||||||
? data
|
|
||||||
: {
|
|
||||||
password: data.newPassword,
|
|
||||||
};
|
|
||||||
|
|
||||||
mutate(payload, {
|
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
onSave();
|
onSave();
|
||||||
ref.current.reset();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const samePassword = value => {
|
const samePassword = value => {
|
||||||
if (value !== ref?.current?.getValues('newPassword')) {
|
if (value !== ref?.current?.getValues('newPassword')) {
|
||||||
return "Passwords don't match";
|
return formatMessage(messages.noMatchPassword);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form ref={ref} onSubmit={handleSubmit} error={error}>
|
<Form ref={ref} onSubmit={handleSubmit} error={error}>
|
||||||
{isCurrentUser && (
|
<FormRow label={formatMessage(labels.currentPassword)}>
|
||||||
<FormRow label="Current password">
|
<FormInput name="currentPassword" rules={{ required: 'Required' }}>
|
||||||
<FormInput name="currentPassword" rules={{ required: 'Required' }}>
|
<PasswordField autoComplete="current-password" />
|
||||||
<PasswordField autoComplete="off" />
|
</FormInput>
|
||||||
</FormInput>
|
</FormRow>
|
||||||
</FormRow>
|
<FormRow label={formatMessage(labels.newPassword)}>
|
||||||
)}
|
|
||||||
<FormRow label="New password">
|
|
||||||
<FormInput
|
<FormInput
|
||||||
name="newPassword"
|
name="newPassword"
|
||||||
rules={{
|
rules={{
|
||||||
required: 'Required',
|
required: 'Required',
|
||||||
minLength: { value: 8, message: 'Minimum length 8 characters' },
|
minLength: { value: 8, message: formatMessage(messages.minPasswordLength) },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PasswordField autoComplete="off" />
|
<PasswordField autoComplete="new-password" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label="Confirm password">
|
<FormRow label={formatMessage(labels.confirmPassword)}>
|
||||||
<FormInput
|
<FormInput
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
rules={{
|
rules={{
|
||||||
required: 'Required',
|
required: formatMessage(labels.required),
|
||||||
minLength: { value: 8, message: 'Minimum length 8 characters' },
|
minLength: { value: 8, message: formatMessage(messages.minPasswordLength) },
|
||||||
validate: samePassword,
|
validate: samePassword,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PasswordField autoComplete="off" />
|
<PasswordField autoComplete="confirm-password" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons flex>
|
<FormButtons flex>
|
||||||
<Button type="submit" variant="primary" disabled={isLoading}>
|
<Button type="submit" variant="primary" disabled={isLoading}>
|
||||||
Save
|
{formatMessage(labels.save)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onClose}>Close</Button>
|
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||||
</FormButtons>
|
</FormButtons>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,19 +1,11 @@
|
||||||
import { Form, FormRow } from 'react-basics';
|
import { Form, FormRow } from 'react-basics';
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
|
import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
|
||||||
import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
|
import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
|
||||||
import LanguageSetting from 'components/pages/settings/profile/LanguageSetting';
|
import LanguageSetting from 'components/pages/settings/profile/LanguageSetting';
|
||||||
import ThemeSetting from 'components/buttons/ThemeSetting';
|
import ThemeSetting from 'components/buttons/ThemeSetting';
|
||||||
import useUser from 'hooks/useUser';
|
import useUser from 'hooks/useUser';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
const messages = defineMessages({
|
|
||||||
username: { id: 'label.username', defaultMessage: 'Username' },
|
|
||||||
role: { id: 'label.role', defaultMessage: 'Role' },
|
|
||||||
timezone: { id: 'label.timezone', defaultMessage: 'Timezone' },
|
|
||||||
dateRange: { id: 'label.default-date-range', defaultMessage: 'Default date range' },
|
|
||||||
language: { id: 'label.language', defaultMessage: 'Language' },
|
|
||||||
theme: { id: 'label.theme', defaultMessage: 'Theme' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function ProfileDetails() {
|
export default function ProfileDetails() {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
@ -27,18 +19,18 @@ export default function ProfileDetails() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<FormRow label={formatMessage(messages.username)}>{username}</FormRow>
|
<FormRow label={formatMessage(labels.username)}>{username}</FormRow>
|
||||||
<FormRow label={formatMessage(messages.role)}>{role}</FormRow>
|
<FormRow label={formatMessage(labels.role)}>{role}</FormRow>
|
||||||
<FormRow label={formatMessage(messages.language)} inline>
|
<FormRow label={formatMessage(labels.language)} inline>
|
||||||
<LanguageSetting />
|
<LanguageSetting />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(messages.timezone)} inline>
|
<FormRow label={formatMessage(labels.timezone)} inline>
|
||||||
<TimezoneSetting />
|
<TimezoneSetting />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(messages.dateRange)} inline>
|
<FormRow label={formatMessage(labels.dateRange)} inline>
|
||||||
<DateRangeSetting />
|
<DateRangeSetting />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(messages.theme)}>
|
<FormRow label={formatMessage(labels.theme)}>
|
||||||
<ThemeSetting />
|
<ThemeSetting />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
.list dd {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
|
@ -1,13 +1,10 @@
|
||||||
import { Breadcrumbs, Item } from 'react-basics';
|
import { Breadcrumbs, Item } from 'react-basics';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { 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 PasswordChangeButton from './PasswordChangeButton';
|
import PasswordChangeButton from './PasswordChangeButton';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
const messages = defineMessages({
|
|
||||||
profile: { id: 'label.profile', defaultMessage: 'Profile' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function ProfileSettings() {
|
export default function ProfileSettings() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
@ -16,7 +13,7 @@ export default function ProfileSettings() {
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Item>{formatMessage(messages.profile)}</Item>
|
<Item>{formatMessage(labels.profile)}</Item>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<PasswordChangeButton />
|
<PasswordChangeButton />
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import { Dropdown, Item, Button, Flexbox } from 'react-basics';
|
import { Dropdown, Item, Button, Flexbox } from 'react-basics';
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { listTimeZones } from 'timezone-support';
|
import { listTimeZones } from 'timezone-support';
|
||||||
import useTimezone from 'hooks/useTimezone';
|
import useTimezone from 'hooks/useTimezone';
|
||||||
import { getTimezone } from 'lib/date';
|
import { getTimezone } from 'lib/date';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
const messages = defineMessages({
|
|
||||||
reset: { id: 'label.reset', defaultMessage: 'Reset' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function TimezoneSetting() {
|
export default function TimezoneSetting() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
@ -25,7 +22,7 @@ export default function TimezoneSetting() {
|
||||||
>
|
>
|
||||||
{item => <Item key={item}>{item}</Item>}
|
{item => <Item key={item}>{item}</Item>}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Button onClick={handleReset}>{formatMessage(messages.reset)}</Button>
|
<Button onClick={handleReset}>{formatMessage(labels.reset)}</Button>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Form, FormRow, FormInput, FormButtons, TextField, Button } from 'react-basics';
|
import { useIntl } from 'react-intl';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormRow,
|
||||||
|
FormInput,
|
||||||
|
FormButtons,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
SubmitButton,
|
||||||
|
} from 'react-basics';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
|
|
||||||
export default function TeamAddForm({ onSave, onClose }) {
|
export default function TeamAddForm({ onSave, onClose }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const { post, useMutation } = useApi();
|
const { post, useMutation } = useApi();
|
||||||
const { mutate, error, isLoading } = useMutation(data => post('/teams', data));
|
const { mutate, error, isLoading } = useMutation(data => post('/teams', data));
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
@ -17,17 +28,17 @@ export default function TeamAddForm({ onSave, onClose }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form ref={ref} onSubmit={handleSubmit} error={error}>
|
<Form ref={ref} onSubmit={handleSubmit} error={error}>
|
||||||
<FormRow label="Name">
|
<FormRow label={formatMessage(labels.name)}>
|
||||||
<FormInput name="name" rules={{ required: 'Required' }}>
|
<FormInput name="name" rules={{ required: 'Required' }}>
|
||||||
<TextField autoComplete="off" />
|
<TextField autoComplete="off" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons flex>
|
<FormButtons flex>
|
||||||
<Button type="submit" variant="primary" disabled={isLoading}>
|
<SubmitButton variant="primary" disabled={isLoading}>
|
||||||
Save
|
{formatMessage(labels.save)}
|
||||||
</Button>
|
</SubmitButton>
|
||||||
<Button disabled={isLoading} onClick={onClose}>
|
<Button disabled={isLoading} onClick={onClose}>
|
||||||
Cancel
|
{formatMessage(labels.cancel)}
|
||||||
</Button>
|
</Button>
|
||||||
</FormButtons>
|
</FormButtons>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -8,13 +8,16 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Flexbox,
|
Flexbox,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
import { getRandomChars } from 'next-basics';
|
import { getRandomChars } from 'next-basics';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
|
|
||||||
const generateId = () => getRandomChars(16);
|
const generateId = () => getRandomChars(16);
|
||||||
|
|
||||||
export default function TeamEditForm({ teamId, data, onSave }) {
|
export default function TeamEditForm({ teamId, data, onSave }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const { post, useMutation } = useApi();
|
const { post, useMutation } = useApi();
|
||||||
const { mutate, error } = useMutation(data => post(`/teams/${teamId}`, data));
|
const { mutate, error } = useMutation(data => post(`/teams/${teamId}`, data));
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
@ -40,22 +43,22 @@ export default function TeamEditForm({ teamId, data, onSave }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data}>
|
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data}>
|
||||||
<FormRow label="Team ID">
|
<FormRow label={formatMessage(labels.teamId)}>
|
||||||
<TextField value={teamId} readOnly allowCopy />
|
<TextField value={teamId} readOnly allowCopy />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label="Name">
|
<FormRow label={formatMessage(labels.name)}>
|
||||||
<FormInput name="name" rules={{ required: 'Required' }}>
|
<FormInput name="name" rules={{ required: 'Required' }}>
|
||||||
<TextField />
|
<TextField />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label="Access code">
|
<FormRow label={formatMessage(labels.accessCode)}>
|
||||||
<Flexbox gap={10}>
|
<Flexbox gap={10}>
|
||||||
<TextField value={accessCode} readOnly allowCopy />
|
<TextField value={accessCode} readOnly allowCopy />
|
||||||
<Button onClick={handleRegenerate}>Regenerate</Button>
|
<Button onClick={handleRegenerate}>{formatMessage(labels.regenerate)}</Button>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<SubmitButton variant="primary">Save</SubmitButton>
|
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
|
||||||
</FormButtons>
|
</FormButtons>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,18 +7,27 @@ import {
|
||||||
TableColumn,
|
TableColumn,
|
||||||
Button,
|
Button,
|
||||||
Icon,
|
Icon,
|
||||||
|
Icons,
|
||||||
|
Flexbox,
|
||||||
|
Text,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import styles from './TeamsTable.module.css';
|
import { ROLES } from 'lib/constants';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
const columns = [
|
const { Close } = Icons;
|
||||||
{ name: 'username', label: 'Username', style: { flex: 4 } },
|
|
||||||
{ name: 'role', label: 'Role' },
|
|
||||||
{ name: 'action', label: '' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function TeamMembersTable({ data = [] }) {
|
export default function TeamMembersTable({ data = [] }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ name: 'username', label: formatMessage(labels.username), style: { flex: 4 } },
|
||||||
|
{ name: 'role', label: formatMessage(labels.role) },
|
||||||
|
{ name: 'action', label: '' },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className={styles.table} columns={columns} rows={data}>
|
<Table columns={columns} rows={data}>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{(column, index) => {
|
{(column, index) => {
|
||||||
return (
|
return (
|
||||||
|
@ -30,25 +39,35 @@ export default function TeamMembersTable({ data = [] }) {
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(row, keys, rowIndex) => {
|
{(row, keys, rowIndex) => {
|
||||||
row.action = (
|
const rowData = {
|
||||||
<div className={styles.actions}>
|
username: row?.user?.username,
|
||||||
<Button>
|
role: formatMessage(
|
||||||
<Icon icon="cross" />
|
labels[Object.keys(ROLES).find(key => ROLES[key] === row.role) || labels.unknown],
|
||||||
Remove
|
),
|
||||||
</Button>
|
action: (
|
||||||
</div>
|
<div>
|
||||||
);
|
<Button>
|
||||||
|
<Icon>
|
||||||
|
<Close />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.remove)}</Text>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={rowIndex} data={row} keys={keys}>
|
<TableRow key={rowIndex} data={rowData} keys={keys}>
|
||||||
{(data, key, colIndex) => {
|
{(data, key, colIndex) => {
|
||||||
return (
|
return (
|
||||||
<TableCell
|
<TableCell key={colIndex} style={{ ...columns[colIndex]?.style }}>
|
||||||
key={colIndex}
|
<Flexbox
|
||||||
className={styles.cell}
|
flex={1}
|
||||||
style={{ ...columns[colIndex]?.style }}
|
alignItems="center"
|
||||||
>
|
justifyContent={key === 'action' ? 'end' : undefined}
|
||||||
{data[key] ?? data?.user?.[key]}
|
>
|
||||||
|
{data[key]}
|
||||||
|
</Flexbox>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
|
import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
@ -6,8 +7,11 @@ import Page from 'components/layout/Page';
|
||||||
import TeamEditForm from 'components/pages/settings/teams/TeamEditForm';
|
import TeamEditForm from 'components/pages/settings/teams/TeamEditForm';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import TeamMembers from 'components/pages/settings/teams/TeamMembers';
|
import TeamMembers from 'components/pages/settings/teams/TeamMembers';
|
||||||
|
import { labels, messages } from 'components/messages';
|
||||||
|
import TeamWebsites from './TeamWebsites';
|
||||||
|
|
||||||
export default function TeamDetails({ teamId }) {
|
export default function TeamSettings({ teamId }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const [values, setValues] = useState(null);
|
const [values, setValues] = useState(null);
|
||||||
const [tab, setTab] = useState('details');
|
const [tab, setTab] = useState('details');
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
|
@ -23,7 +27,7 @@ export default function TeamDetails({ teamId }) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSave = data => {
|
const handleSave = data => {
|
||||||
showToast({ message: 'Saved successfully.', variant: 'success' });
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
setValues(state => ({ ...state, ...data }));
|
setValues(state => ({ ...state, ...data }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -44,13 +48,14 @@ export default function TeamDetails({ teamId }) {
|
||||||
<Item>{values?.name}</Item>
|
<Item>{values?.name}</Item>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
|
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
|
||||||
<Item key="details">Details</Item>
|
<Item key="details">{formatMessage(labels.details)}</Item>
|
||||||
<Item key="members">Members</Item>
|
<Item key="members">{formatMessage(labels.members)}</Item>
|
||||||
<Item key="websites">Websites</Item>
|
<Item key="websites">{formatMessage(labels.websites)}</Item>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{tab === 'details' && <TeamEditForm teamId={teamId} data={values} onSave={handleSave} />}
|
{tab === 'details' && <TeamEditForm teamId={teamId} data={values} onSave={handleSave} />}
|
||||||
{tab === 'members' && <TeamMembers teamId={teamId} />}
|
{tab === 'members' && <TeamMembers teamId={teamId} />}
|
||||||
|
{tab === 'websites' && <TeamWebsites teamId={teamId} />}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
25
components/pages/settings/teams/TeamWebsites.js
Normal file
25
components/pages/settings/teams/TeamWebsites.js
Normal 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 TeamWebsites({ teamId }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const { get, useQuery } = useApi();
|
||||||
|
const { data, isLoading } = useQuery(['team/websites', teamId], () =>
|
||||||
|
get(`/teams/${teamId}/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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,13 +1,18 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Icon, Modal, useToast } from 'react-basics';
|
import { Button, Icon, Modal, useToast, Icons, Text } from 'react-basics';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
import TeamAddForm from 'components/pages/settings/teams/TeamAddForm';
|
import TeamAddForm from 'components/pages/settings/teams/TeamAddForm';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import TeamsTable from 'components/pages/settings/teams/TeamsTable';
|
import TeamsTable from 'components/pages/settings/teams/TeamsTable';
|
||||||
import Page from 'components/layout/Page';
|
import Page from 'components/layout/Page';
|
||||||
|
import { labels, messages } from 'components/messages';
|
||||||
|
|
||||||
|
const { Plus } = Icons;
|
||||||
|
|
||||||
export default function TeamsList() {
|
export default function TeamsList() {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const [edit, setEdit] = useState(false);
|
const [edit, setEdit] = useState(false);
|
||||||
const [update, setUpdate] = useState(0);
|
const [update, setUpdate] = useState(0);
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
|
@ -22,7 +27,7 @@ export default function TeamsList() {
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
setEdit(false);
|
setEdit(false);
|
||||||
setUpdate(state => state + 1);
|
setUpdate(state => state + 1);
|
||||||
showToast({ message: 'Team saved.', variant: 'success' });
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
@ -32,21 +37,27 @@ export default function TeamsList() {
|
||||||
return (
|
return (
|
||||||
<Page loading={isLoading} error={error}>
|
<Page loading={isLoading} error={error}>
|
||||||
{toast}
|
{toast}
|
||||||
<PageHeader title="Teams">
|
<PageHeader title={formatMessage(labels.team)}>
|
||||||
<Button onClick={handleAdd}>
|
<Button variant="primary" onClick={handleAdd}>
|
||||||
<Icon icon="plus" /> Create team
|
<Icon>
|
||||||
|
<Plus />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.createTeam)}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
{hasData && <TeamsTable data={data} />}
|
{hasData && <TeamsTable data={data} />}
|
||||||
{!hasData && (
|
{!hasData && (
|
||||||
<EmptyPlaceholder message="You don't have any teams configured.">
|
<EmptyPlaceholder message={formatMessage(messages.noTeams)}>
|
||||||
<Button variant="primary" onClick={handleAdd}>
|
<Button variant="primary" onClick={handleAdd}>
|
||||||
<Icon icon="plus" /> Create team
|
<Icon>
|
||||||
|
<Plus />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.createTeam)}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</EmptyPlaceholder>
|
</EmptyPlaceholder>
|
||||||
)}
|
)}
|
||||||
{edit && (
|
{edit && (
|
||||||
<Modal title="Create team" onClose={handleClose}>
|
<Modal title={formatMessage(labels.createTeam)} onClose={handleClose}>
|
||||||
{close => <TeamAddForm onSave={handleSave} onClose={close} />}
|
{close => <TeamAddForm onSave={handleSave} onClose={close} />}
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -8,17 +8,25 @@ import {
|
||||||
TableColumn,
|
TableColumn,
|
||||||
Button,
|
Button,
|
||||||
Icon,
|
Icon,
|
||||||
|
Flexbox,
|
||||||
|
Icons,
|
||||||
|
Text,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import styles from './TeamsTable.module.css';
|
import { useIntl } from 'react-intl';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
|
|
||||||
const columns = [
|
const { ArrowRight } = Icons;
|
||||||
{ name: 'name', label: 'Name', style: { flex: 2 } },
|
|
||||||
{ name: 'action', label: ' ' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function TeamsTable({ data = [] }) {
|
export default function TeamsTable({ data = [] }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ name: 'name', label: formatMessage(labels.name), style: { flex: 2 } },
|
||||||
|
{ name: 'action', label: ' ' },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className={styles.table} columns={columns} rows={data}>
|
<Table columns={columns} rows={data}>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{(column, index) => {
|
{(column, index) => {
|
||||||
return (
|
return (
|
||||||
|
@ -33,28 +41,28 @@ export default function TeamsTable({ data = [] }) {
|
||||||
const { id } = row;
|
const { id } = row;
|
||||||
|
|
||||||
row.action = (
|
row.action = (
|
||||||
<div className={styles.actions}>
|
<Flexbox flex={1} justifyContent="end">
|
||||||
<Link href={`/settings/teams/${id}`}>
|
<Link href={`/settings/teams/${id}`}>
|
||||||
<a>
|
<a>
|
||||||
<Button>
|
<Button>
|
||||||
<Icon icon="arrow-right" />
|
<Icon>
|
||||||
Settings
|
<ArrowRight />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.settings)}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</Flexbox>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={rowIndex} data={row} keys={keys}>
|
<TableRow key={rowIndex} data={row} keys={keys}>
|
||||||
{(data, key, colIndex) => {
|
{(data, key, colIndex) => {
|
||||||
return (
|
return (
|
||||||
<TableCell
|
<TableCell key={colIndex} style={{ ...columns[colIndex]?.style }}>
|
||||||
key={colIndex}
|
<Flexbox flex={1} alignItems="center">
|
||||||
className={styles.cell}
|
{data[key]}
|
||||||
style={{ ...columns[colIndex]?.style }}
|
</Flexbox>
|
||||||
>
|
|
||||||
{data[key]}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
.table th,
|
|
||||||
.table td {
|
|
||||||
flex: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell:last-child {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
|
@ -1,16 +1,12 @@
|
||||||
import { useState } from 'react';
|
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 { Button, Icon, Text, Modal, useToast, Icons } from 'react-basics';
|
||||||
import UserAddForm from './UserAddForm';
|
import UserAddForm from './UserAddForm';
|
||||||
|
import { labels, messages } from 'components/messages';
|
||||||
|
|
||||||
const { Plus } = Icons;
|
const { Plus } = Icons;
|
||||||
|
|
||||||
const messages = defineMessages({
|
export default function UserAddButton({ onSave }) {
|
||||||
createUser: { id: 'label.create-user', defaultMessage: 'Create user' },
|
|
||||||
saved: { id: 'message.saved-successfully', defaultMessage: 'Saved successfully.' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function UserAddButton() {
|
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const [edit, setEdit] = useState(false);
|
const [edit, setEdit] = useState(false);
|
||||||
const { toast, showToast } = useToast();
|
const { toast, showToast } = useToast();
|
||||||
|
@ -18,6 +14,7 @@ export default function UserAddButton() {
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
setEdit(false);
|
setEdit(false);
|
||||||
|
onSave();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
|
@ -35,11 +32,11 @@ export default function UserAddButton() {
|
||||||
<Icon>
|
<Icon>
|
||||||
<Plus />
|
<Plus />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Text>{formatMessage(messages.createUser)}</Text>
|
<Text>{formatMessage(labels.createUser)}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
{edit && (
|
{edit && (
|
||||||
<Modal title={formatMessage(messages.createUser)} onClose={handleClose}>
|
<Modal title={formatMessage(labels.createUser)} onClose={handleClose}>
|
||||||
{() => <UserAddForm onSave={handleSave} onClose={handleClose} />}
|
<UserAddForm onSave={handleSave} onClose={handleClose} />
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -10,19 +10,11 @@ import {
|
||||||
SubmitButton,
|
SubmitButton,
|
||||||
Button,
|
Button,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { 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';
|
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 }) {
|
export default function UserAddForm({ onSave, onClose }) {
|
||||||
const { post, useMutation } = useApi();
|
const { post, useMutation } = useApi();
|
||||||
const { mutate, error, isLoading } = useMutation(data => post(`/users`, data));
|
const { mutate, error, isLoading } = useMutation(data => post(`/users`, data));
|
||||||
|
@ -38,30 +30,30 @@ export default function UserAddForm({ onSave, onClose }) {
|
||||||
|
|
||||||
const renderValue = value => {
|
const renderValue = value => {
|
||||||
if (value === ROLES.user) {
|
if (value === ROLES.user) {
|
||||||
return formatMessage(messages.user);
|
return formatMessage(labels.user);
|
||||||
}
|
}
|
||||||
if (value === ROLES.admin) {
|
if (value === ROLES.admin) {
|
||||||
return formatMessage(messages.admin);
|
return formatMessage(labels.admin);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={error}>
|
<Form onSubmit={handleSubmit} error={error}>
|
||||||
<FormRow label={formatMessage(messages.username)}>
|
<FormRow label={formatMessage(labels.username)}>
|
||||||
<FormInput name="username" rules={{ required: formatMessage(labels.required) }}>
|
<FormInput name="username" rules={{ required: formatMessage(labels.required) }}>
|
||||||
<TextField autoComplete="new-username" />
|
<TextField autoComplete="new-username" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(messages.password)}>
|
<FormRow label={formatMessage(labels.password)}>
|
||||||
<FormInput name="password" rules={{ required: formatMessage(labels.required) }}>
|
<FormInput name="password" rules={{ required: formatMessage(labels.required) }}>
|
||||||
<PasswordField autoComplete="new-password" />
|
<PasswordField autoComplete="new-password" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(messages.role)}>
|
<FormRow label={formatMessage(labels.role)}>
|
||||||
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
|
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
|
||||||
<Dropdown renderValue={renderValue} style={{ width: 200 }}>
|
<Dropdown renderValue={renderValue}>
|
||||||
<Item key={ROLES.user}>{formatMessage(messages.user)}</Item>
|
<Item key={ROLES.user}>{formatMessage(labels.user)}</Item>
|
||||||
<Item key={ROLES.admin}>{formatMessage(messages.admin)}</Item>
|
<Item key={ROLES.admin}>{formatMessage(labels.admin)}</Item>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
|
|
|
@ -1,36 +1,29 @@
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
|
import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { labels } from 'components/messages';
|
import { labels, messages } from 'components/messages';
|
||||||
|
|
||||||
const messages = defineMessages({
|
export default function UserDeleteForm({ userId, username, onSave, onClose }) {
|
||||||
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 }) {
|
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { del } = useApi();
|
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 => {
|
const handleSubmit = async data => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
onSave();
|
onSave();
|
||||||
|
onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={error}>
|
<Form onSubmit={handleSubmit} error={error}>
|
||||||
<p>{formatMessage(messages.warning)}</p>
|
<p>{formatMessage(messages.deleteUserWarning, { username })}</p>
|
||||||
<FormButtons flex>
|
<FormButtons flex>
|
||||||
<SubmitButton variant="primary" disabled={isLoading}>
|
<SubmitButton variant="primary" disabled={isLoading}>
|
||||||
{formatMessage(labels.save)}
|
{formatMessage(labels.delete)}
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
<Button disabled={isLoading} onClick={onClose}>
|
<Button disabled={isLoading} onClick={onClose}>
|
||||||
{formatMessage(labels.cancel)}
|
{formatMessage(labels.cancel)}
|
||||||
|
|
|
@ -9,23 +9,10 @@ import {
|
||||||
SubmitButton,
|
SubmitButton,
|
||||||
PasswordField,
|
PasswordField,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { 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';
|
import { labels, messages } 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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function UserEditForm({ userId, data, onSave }) {
|
export default function UserEditForm({ userId, data, onSave }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
@ -42,35 +29,35 @@ export default function UserEditForm({ userId, data, onSave }) {
|
||||||
|
|
||||||
const renderValue = value => {
|
const renderValue = value => {
|
||||||
if (value === ROLES.user) {
|
if (value === ROLES.user) {
|
||||||
return formatMessage(messages.user);
|
return formatMessage(labels.user);
|
||||||
}
|
}
|
||||||
if (value === ROLES.admin) {
|
if (value === ROLES.admin) {
|
||||||
return formatMessage(messages.admin);
|
return formatMessage(labels.admin);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={error} values={data} style={{ width: 600 }}>
|
<Form onSubmit={handleSubmit} error={error} values={data} style={{ width: 300 }}>
|
||||||
<FormRow label={formatMessage(messages.username)}>
|
<FormRow label={formatMessage(labels.username)}>
|
||||||
<FormInput name="username">
|
<FormInput name="username">
|
||||||
<TextField />
|
<TextField />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(messages.password)}>
|
<FormRow label={formatMessage(labels.password)}>
|
||||||
<FormInput
|
<FormInput
|
||||||
name="newPassword"
|
name="newPassword"
|
||||||
rules={{
|
rules={{
|
||||||
minLength: { value: 8, message: formatMessage(messages.minLength) },
|
minLength: { value: 8, message: formatMessage(messages.minPasswordLength) },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PasswordField autoComplete="new-password" />
|
<PasswordField autoComplete="new-password" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(messages.role)}>
|
<FormRow label={formatMessage(labels.role)}>
|
||||||
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
|
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
|
||||||
<Dropdown renderValue={renderValue} style={{ width: 200 }}>
|
<Dropdown renderValue={renderValue}>
|
||||||
<Item key={ROLES.user}>{formatMessage(messages.user)}</Item>
|
<Item key={ROLES.user}>{formatMessage(labels.user)}</Item>
|
||||||
<Item key={ROLES.admin}>{formatMessage(messages.admin)}</Item>
|
<Item key={ROLES.admin}>{formatMessage(labels.admin)}</Item>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
|
|
|
@ -1,23 +1,13 @@
|
||||||
import { useEffect, useState } from 'react';
|
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 { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
|
||||||
import Link from 'next/link';
|
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 UserEditForm from 'components/pages/settings/users//UserEditForm';
|
||||||
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';
|
import { labels, messages } from 'components/messages';
|
||||||
|
import UserWebsites from './UserWebsites';
|
||||||
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.' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function UserSettings({ userId }) {
|
export default function UserSettings({ userId }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
@ -26,7 +16,6 @@ export default function UserSettings({ userId }) {
|
||||||
const [tab, setTab] = useState('details');
|
const [tab, setTab] = useState('details');
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { toast, showToast } = useToast();
|
const { toast, showToast } = useToast();
|
||||||
const router = useRouter();
|
|
||||||
const { data, isLoading } = useQuery(
|
const { data, isLoading } = useQuery(
|
||||||
['user', userId],
|
['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(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
setValues(data);
|
setValues(data);
|
||||||
|
@ -65,19 +49,17 @@ export default function UserSettings({ userId }) {
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Item>
|
<Item>
|
||||||
<Link href="/settings/users">{formatMessage(messages.users)}</Link>
|
<Link href="/settings/users">{formatMessage(labels.users)}</Link>
|
||||||
</Item>
|
</Item>
|
||||||
<Item>{values?.username}</Item>
|
<Item>{values?.username}</Item>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</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(labels.details)}</Item>
|
||||||
<Item key="websites">{formatMessage(messages.websites)}</Item>
|
<Item key="websites">{formatMessage(labels.websites)}</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 === 'websites' && <WebsitesTable />}
|
{tab === 'websites' && <UserWebsites userId={userId} />}
|
||||||
{tab === 'delete' && <UserDeleteForm userId={userId} onSave={handleDelete} />}
|
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
25
components/pages/settings/users/UserWebsites.js
Normal file
25
components/pages/settings/users/UserWebsites.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { 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 EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
|
@ -6,33 +6,35 @@ import UsersTable from './UsersTable';
|
||||||
import UserAddButton from './UserAddButton';
|
import UserAddButton from './UserAddButton';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import useUser from 'hooks/useUser';
|
import useUser from 'hooks/useUser';
|
||||||
|
import { useToast } from 'react-basics';
|
||||||
const messages = defineMessages({
|
import { labels, messages } from 'components/messages';
|
||||||
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' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function UsersList() {
|
export default function UsersList() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { data, isLoading, error } = useQuery(['user'], () => get(`/users`), {
|
const { data, isLoading, error, refetch } = useQuery(['user'], () => get(`/users`), {
|
||||||
enabled: !!user,
|
enabled: !!user,
|
||||||
});
|
});
|
||||||
|
const { toast, showToast } = useToast();
|
||||||
const hasData = data && data.length !== 0;
|
const hasData = data && data.length !== 0;
|
||||||
|
|
||||||
const addButton = <UserAddButton />;
|
const handleSave = () => refetch();
|
||||||
|
|
||||||
|
const handleDelete = () =>
|
||||||
|
showToast({ message: formatMessage(messages.deleted), variant: 'success' });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page loading={isLoading} error={error}>
|
<Page loading={isLoading} error={error}>
|
||||||
<PageHeader title={formatMessage(messages.users)}>{addButton}</PageHeader>
|
{toast}
|
||||||
{hasData && <UsersTable data={data} />}
|
<PageHeader title={formatMessage(labels.users)}>
|
||||||
|
<UserAddButton onSave={handleSave} />
|
||||||
|
</PageHeader>
|
||||||
|
{hasData && <UsersTable data={data} onDelete={handleDelete} />}
|
||||||
{!hasData && (
|
{!hasData && (
|
||||||
<EmptyPlaceholder message={formatMessage(messages.noUsers)}>{addButton}</EmptyPlaceholder>
|
<EmptyPlaceholder message={formatMessage(messages.noUsers)}>
|
||||||
|
<UserAddButton onSave={handleSave} />
|
||||||
|
</EmptyPlaceholder>
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,26 +8,38 @@ import {
|
||||||
TableColumn,
|
TableColumn,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
Flexbox,
|
||||||
|
Icons,
|
||||||
|
ModalTrigger,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
import { formatDistance } from 'date-fns';
|
import { formatDistance } from 'date-fns';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Edit } from 'components/icons';
|
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 = [
|
const { Trash } = Icons;
|
||||||
{ name: 'username', label: 'Username', style: { flex: 2 } },
|
|
||||||
{ name: 'role', label: 'Role', style: { flex: 2 } },
|
export default function UsersTable({ data = [], onDelete }) {
|
||||||
{ name: 'created', label: 'Created' },
|
const { formatMessage } = useIntl();
|
||||||
{ name: 'action', label: ' ' },
|
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 (
|
return (
|
||||||
<Table className={styles.table} columns={columns} rows={data}>
|
<Table columns={columns} rows={data}>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{(column, index) => {
|
{(column, index) => {
|
||||||
return (
|
return (
|
||||||
<TableColumn key={index} className={styles.header} style={{ ...column.style }}>
|
<TableColumn key={index} style={{ ...column.style }}>
|
||||||
{column.label}
|
{column.label}
|
||||||
</TableColumn>
|
</TableColumn>
|
||||||
);
|
);
|
||||||
|
@ -35,33 +47,57 @@ export default function UsersTable({ data = [] }) {
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(row, keys, rowIndex) => {
|
{(row, keys, rowIndex) => {
|
||||||
row.created = formatDistance(new Date(row.createdAt), new Date(), {
|
const rowData = {
|
||||||
addSuffix: true,
|
...row,
|
||||||
});
|
created: formatDistance(new Date(row.createdAt), new Date(), {
|
||||||
|
addSuffix: true,
|
||||||
row.action = (
|
}),
|
||||||
<div className={styles.actions}>
|
role: formatMessage(
|
||||||
<Link href={`/settings/users/${row.id}`}>
|
labels[Object.keys(ROLES).find(key => ROLES[key] === row.role) || labels.unknown],
|
||||||
<Button>
|
),
|
||||||
<Icon>
|
action: (
|
||||||
<Edit />
|
<>
|
||||||
</Icon>
|
<Link href={`/settings/users/${row.id}`}>
|
||||||
<Text>Edit</Text>
|
<Button>
|
||||||
</Button>
|
<Icon>
|
||||||
</Link>
|
<Edit />
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<TableRow key={rowIndex} data={row} keys={keys}>
|
<TableRow key={rowIndex} data={rowData} keys={keys}>
|
||||||
{(data, key, colIndex) => {
|
{(data, key, colIndex) => {
|
||||||
return (
|
return (
|
||||||
<TableCell
|
<TableCell key={colIndex} style={{ ...columns[colIndex]?.style }}>
|
||||||
key={colIndex}
|
<Flexbox
|
||||||
className={styles.cell}
|
flex={1}
|
||||||
style={{ ...columns[colIndex]?.style }}
|
gap={10}
|
||||||
>
|
alignItems="center"
|
||||||
{data[key]}
|
justifyContent={key === 'action' ? 'end' : undefined}
|
||||||
|
>
|
||||||
|
{data[key]}
|
||||||
|
</Flexbox>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -8,13 +8,16 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Toggle,
|
Toggle,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { getRandomChars } from 'next-basics';
|
import { getRandomChars } from 'next-basics';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
|
import { labels, messages } from 'components/messages';
|
||||||
|
|
||||||
const generateId = () => getRandomChars(16);
|
const generateId = () => getRandomChars(16);
|
||||||
|
|
||||||
export default function ShareUrl({ websiteId, data, onSave }) {
|
export default function ShareUrl({ websiteId, data, onSave }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const { name, shareId } = data;
|
const { name, shareId } = data;
|
||||||
const [id, setId] = useState(shareId);
|
const [id, setId] = useState(shareId);
|
||||||
const { post, useMutation } = useApi();
|
const { post, useMutation } = useApi();
|
||||||
|
@ -62,26 +65,24 @@ export default function ShareUrl({ websiteId, data, onSave }) {
|
||||||
}, [id, shareId]);
|
}, [id, shareId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form key={websiteId} ref={ref} onSubmit={handleSubmit} error={error} values={data}>
|
<>
|
||||||
<FormRow>
|
<Toggle checked={Boolean(id)} onChecked={handleCheck} style={{ marginBottom: 30 }}>
|
||||||
<Toggle checked={Boolean(id)} onChecked={handleCheck}>
|
{formatMessage(labels.enableShareUrl)}
|
||||||
Enable share URL
|
</Toggle>
|
||||||
</Toggle>
|
|
||||||
</FormRow>
|
|
||||||
{id && (
|
{id && (
|
||||||
<>
|
<Form key={websiteId} ref={ref} onSubmit={handleSubmit} error={error} values={data}>
|
||||||
<FormRow>
|
<FormRow>
|
||||||
<p>Your website stats are publically available at the following URL:</p>
|
<p>{formatMessage(messages.shareUrl)}</p>
|
||||||
<Flexbox gap={10}>
|
<Flexbox gap={10}>
|
||||||
<TextField value={url} readOnly allowCopy />
|
<TextField value={url} readOnly allowCopy />
|
||||||
<Button onClick={handleGenerate}>Regenerate URL</Button>
|
<Button onClick={handleGenerate}>{formatMessage(labels.regenerate)}</Button>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<SubmitButton variant="primary">Save</SubmitButton>
|
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
|
||||||
</FormButtons>
|
</FormButtons>
|
||||||
</>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { TextArea } from 'react-basics';
|
import { TextArea } from 'react-basics';
|
||||||
import { TRACKER_SCRIPT_URL } from 'lib/constants';
|
import { TRACKER_SCRIPT_URL } from 'lib/constants';
|
||||||
|
import { messages } from 'components/messages';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
export default function TrackingCode({ websiteId }) {
|
export default function TrackingCode({ websiteId }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const url = TRACKER_SCRIPT_URL.startsWith('http')
|
const url = TRACKER_SCRIPT_URL.startsWith('http')
|
||||||
? TRACKER_SCRIPT_URL
|
? TRACKER_SCRIPT_URL
|
||||||
: `${location.origin}${TRACKER_SCRIPT_URL}`;
|
: `${location.origin}${TRACKER_SCRIPT_URL}`;
|
||||||
|
@ -10,10 +13,7 @@ export default function TrackingCode({ websiteId }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>
|
<p>{formatMessage(messages.trackingCode)}</p>
|
||||||
To track stats for this website, place the following code in the <code><head></code>{' '}
|
|
||||||
section of your HTML.
|
|
||||||
</p>
|
|
||||||
<TextArea rows={4} value={code} readOnly allowCopy />
|
<TextArea rows={4} value={code} readOnly allowCopy />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -31,12 +31,12 @@ export default function WebsiteAddForm({ onSave, onClose }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={error}>
|
<Form onSubmit={handleSubmit} error={error}>
|
||||||
<FormRow label="Name">
|
<FormRow label={formatMessage(labels.name)}>
|
||||||
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
|
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
|
||||||
<TextField autoComplete="off" />
|
<TextField autoComplete="off" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label="Domain">
|
<FormRow label={formatMessage(labels.domain)}>
|
||||||
<FormInput
|
<FormInput
|
||||||
name="domain"
|
name="domain"
|
||||||
rules={{
|
rules={{
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import useApi from 'hooks/useApi';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Form,
|
Form,
|
||||||
|
@ -8,12 +7,16 @@ import {
|
||||||
SubmitButton,
|
SubmitButton,
|
||||||
TextField,
|
TextField,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { labels, messages } from 'components/messages';
|
||||||
|
import useApi from 'hooks/useApi';
|
||||||
|
|
||||||
const CONFIRM_VALUE = 'DELETE';
|
const CONFIRM_VALUE = 'DELETE';
|
||||||
|
|
||||||
export default function WebsiteDeleteForm({ websiteId, onSave, onClose }) {
|
export default function WebsiteDeleteForm({ websiteId, onSave, onClose }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const { del, useMutation } = useApi();
|
const { del, useMutation } = useApi();
|
||||||
const { mutate, error, isLoading } = useMutation(data => del(`/websites/${websiteId}`, data));
|
const { mutate, error } = useMutation(data => del(`/websites/${websiteId}`, data));
|
||||||
|
|
||||||
const handleSubmit = async data => {
|
const handleSubmit = async data => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
|
@ -25,21 +28,15 @@ export default function WebsiteDeleteForm({ websiteId, onSave, onClose }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={error}>
|
<Form onSubmit={handleSubmit} error={error}>
|
||||||
<div>
|
<p>{formatMessage(messages.deleteWebsite, { confirmation: CONFIRM_VALUE })}</p>
|
||||||
To delete this website, type <b>{CONFIRM_VALUE}</b> in the box below to confirm.
|
<FormRow label={formatMessage(labels.confirm)}>
|
||||||
</div>
|
|
||||||
<FormRow label="Confirm">
|
|
||||||
<FormInput name="confirmation" rules={{ validate: value => value === CONFIRM_VALUE }}>
|
<FormInput name="confirmation" rules={{ validate: value => value === CONFIRM_VALUE }}>
|
||||||
<TextField autoComplete="off" />
|
<TextField autoComplete="off" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons flex>
|
<FormButtons flex>
|
||||||
<SubmitButton variant="primary" disabled={isLoading}>
|
<SubmitButton variant="danger">{formatMessage(labels.delete)}</SubmitButton>
|
||||||
Save
|
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||||
</SubmitButton>
|
|
||||||
<Button disabled={isLoading} onClick={onClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</FormButtons>
|
</FormButtons>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics';
|
import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
import { 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, messages } from 'components/messages';
|
||||||
|
|
||||||
export default function WebsiteEditForm({ websiteId, data, onSave }) {
|
export default function WebsiteEditForm({ websiteId, data, onSave }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const { post, useMutation } = useApi();
|
const { post, useMutation } = useApi();
|
||||||
const { mutate, error } = useMutation(data => post(`/websites/${websiteId}`, data));
|
const { mutate, error } = useMutation(data => post(`/websites/${websiteId}`, data));
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
@ -19,22 +22,22 @@ export default function WebsiteEditForm({ websiteId, data, onSave }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data} style={{ width: 600 }}>
|
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data} style={{ width: 600 }}>
|
||||||
<FormRow label="Website ID">
|
<FormRow label={formatMessage(labels.websiteId)}>
|
||||||
<TextField value={websiteId} readOnly allowCopy />
|
<TextField value={websiteId} readOnly allowCopy />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label="Name">
|
<FormRow label={formatMessage(labels.name)}>
|
||||||
<FormInput name="name" rules={{ required: 'Required' }}>
|
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
|
||||||
<TextField />
|
<TextField />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label="Domain">
|
<FormRow label={formatMessage(labels.domain)}>
|
||||||
<FormInput
|
<FormInput
|
||||||
name="domain"
|
name="domain"
|
||||||
rules={{
|
rules={{
|
||||||
required: 'Required',
|
required: formatMessage(labels.required),
|
||||||
pattern: {
|
pattern: {
|
||||||
value: DOMAIN_REGEX,
|
value: DOMAIN_REGEX,
|
||||||
message: 'Invalid domain',
|
message: formatMessage(messages.invalidDomain),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -42,7 +45,7 @@ export default function WebsiteEditForm({ websiteId, data, onSave }) {
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<SubmitButton variant="primary">Save</SubmitButton>
|
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
|
||||||
</FormButtons>
|
</FormButtons>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import WebsiteDeleteForm from 'components/pages/settings/websites/WebsiteDeleteForm';
|
|
||||||
import WebsiteResetForm from 'components/pages/settings/websites/WebsiteResetForm';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Form, FormRow, Modal } from 'react-basics';
|
import { Button, Form, FormRow, Modal } from 'react-basics';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import WebsiteDeleteForm from 'components/pages/settings/websites/WebsiteDeleteForm';
|
||||||
|
import WebsiteResetForm from 'components/pages/settings/websites/WebsiteResetForm';
|
||||||
|
import { labels, messages } from 'components/messages';
|
||||||
|
|
||||||
export default function WebsiteReset({ websiteId, onSave }) {
|
export default function WebsiteReset({ websiteId, onSave }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const [modal, setModal] = useState(null);
|
const [modal, setModal] = useState(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -22,23 +25,21 @@ export default function WebsiteReset({ websiteId, onSave }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<FormRow label="Reset website">
|
<FormRow label={formatMessage(labels.resetWebsite)}>
|
||||||
<p>
|
<p>{formatMessage(messages.resetWebsiteWarning)}</p>
|
||||||
All statistics for this website will be deleted, but your settings will remain intact.
|
<Button onClick={() => setModal('reset')}>{formatMessage(labels.reset)}</Button>
|
||||||
</p>
|
|
||||||
<Button onClick={() => setModal('reset')}>Reset</Button>
|
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label="Delete website">
|
<FormRow label={formatMessage(labels.deleteWebsite)}>
|
||||||
<p>All website data will be deleted.</p>
|
<p>{formatMessage(messages.deleteWebsiteWarning)}</p>
|
||||||
<Button onClick={() => setModal('delete')}>Delete</Button>
|
<Button onClick={() => setModal('delete')}>Delete</Button>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
{modal === 'reset' && (
|
{modal === 'reset' && (
|
||||||
<Modal title="Reset website" onClose={handleClose}>
|
<Modal title={formatMessage(labels.resetWebsite)} onClose={handleClose}>
|
||||||
{close => <WebsiteResetForm websiteId={websiteId} onSave={handleReset} onClose={close} />}
|
{close => <WebsiteResetForm websiteId={websiteId} onSave={handleReset} onClose={close} />}
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
{modal === 'delete' && (
|
{modal === 'delete' && (
|
||||||
<Modal title="Delete website" onClose={handleClose}>
|
<Modal title={formatMessage(labels.deleteWebsite)} onClose={handleClose}>
|
||||||
{close => (
|
{close => (
|
||||||
<WebsiteDeleteForm websiteId={websiteId} onSave={handleDelete} onClose={close} />
|
<WebsiteDeleteForm websiteId={websiteId} onSave={handleDelete} onClose={close} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import useApi from 'hooks/useApi';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Form,
|
Form,
|
||||||
|
@ -8,14 +7,16 @@ import {
|
||||||
SubmitButton,
|
SubmitButton,
|
||||||
TextField,
|
TextField,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
|
import useApi from 'hooks/useApi';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { labels, messages } from 'components/messages';
|
||||||
|
|
||||||
const CONFIRM_VALUE = 'RESET';
|
const CONFIRM_VALUE = 'RESET';
|
||||||
|
|
||||||
export default function WebsiteResetForm({ websiteId, onSave, onClose }) {
|
export default function WebsiteResetForm({ websiteId, onSave, onClose }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const { post, useMutation } = useApi();
|
const { post, useMutation } = useApi();
|
||||||
const { mutate, error, isLoading } = useMutation(data =>
|
const { mutate, error } = useMutation(data => post(`/websites/${websiteId}/reset`, data));
|
||||||
post(`/websites/${websiteId}/reset`, data),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSubmit = async data => {
|
const handleSubmit = async data => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
|
@ -27,21 +28,15 @@ export default function WebsiteResetForm({ websiteId, onSave, onClose }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={error}>
|
<Form onSubmit={handleSubmit} error={error}>
|
||||||
<div>
|
<p>{formatMessage(messages.resetWebsite, { confirmation: CONFIRM_VALUE })}</p>
|
||||||
To reset this website, type <b>{CONFIRM_VALUE}</b> in the box below to confirm.
|
<FormRow label={formatMessage(labels.confirm)}>
|
||||||
</div>
|
|
||||||
<FormRow label="Confirmation">
|
|
||||||
<FormInput name="confirm" rules={{ validate: value => value === CONFIRM_VALUE }}>
|
<FormInput name="confirm" rules={{ validate: value => value === CONFIRM_VALUE }}>
|
||||||
<TextField autoComplete="off" />
|
<TextField autoComplete="off" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons flex>
|
<FormButtons flex>
|
||||||
<SubmitButton variant="primary" disabled={isLoading}>
|
<SubmitButton variant="danger">{formatMessage(labels.reset)}</SubmitButton>
|
||||||
Save
|
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||||
</SubmitButton>
|
|
||||||
<Button disabled={isLoading} onClick={onClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</FormButtons>
|
</FormButtons>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Breadcrumbs, Item, Tabs, useToast, Button, Text, Icon, Icons } from 'react-basics';
|
import { Breadcrumbs, Item, Tabs, useToast, Button, Text, Icon, Icons } from 'react-basics';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Page from 'components/layout/Page';
|
import Page from 'components/layout/Page';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
|
@ -9,19 +9,10 @@ import WebsiteReset from 'components/pages/settings/websites/WebsiteReset';
|
||||||
import TrackingCode from 'components/pages/settings/websites/TrackingCode';
|
import TrackingCode from 'components/pages/settings/websites/TrackingCode';
|
||||||
import ShareUrl from 'components/pages/settings/websites/ShareUrl';
|
import ShareUrl from 'components/pages/settings/websites/ShareUrl';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
|
import { labels, messages } from 'components/messages';
|
||||||
|
|
||||||
const { External } = Icons;
|
const { External } = Icons;
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
websites: { id: 'label.websites', defaultMessage: 'Websites' },
|
|
||||||
details: { id: 'label.details', defaultMessage: 'Details' },
|
|
||||||
trackingCode: { id: 'label.tracking-code', defaultMessage: 'Tracking code' },
|
|
||||||
shareUrl: { id: 'label.share-url', defaultMessage: 'Share URL' },
|
|
||||||
actions: { id: 'label.actions', defaultMessage: 'Actions' },
|
|
||||||
view: { id: 'label.view', defaultMessage: 'View' },
|
|
||||||
saved: { id: 'message.saved-successfully', defaultMessage: 'Save successfully.' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function WebsiteSettings({ websiteId }) {
|
export default function WebsiteSettings({ websiteId }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const [values, setValues] = useState(null);
|
const [values, setValues] = useState(null);
|
||||||
|
@ -55,7 +46,7 @@ export default function WebsiteSettings({ websiteId }) {
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Item>
|
<Item>
|
||||||
<Link href="/settings/websites">{formatMessage(messages.websites)}</Link>
|
<Link href="/settings/websites">{formatMessage(labels.websites)}</Link>
|
||||||
</Item>
|
</Item>
|
||||||
<Item>{values?.name}</Item>
|
<Item>{values?.name}</Item>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
|
@ -65,23 +56,23 @@ export default function WebsiteSettings({ websiteId }) {
|
||||||
<Icon>
|
<Icon>
|
||||||
<External />
|
<External />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Text>{formatMessage(messages.view)}</Text>
|
<Text>{formatMessage(labels.view)}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
|
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
|
||||||
<Item key="details">{formatMessage(messages.details)}</Item>
|
<Item key="details">{formatMessage(labels.details)}</Item>
|
||||||
<Item key="tracking">{formatMessage(messages.trackingCode)}</Item>
|
<Item key="tracking">{formatMessage(labels.trackingCode)}</Item>
|
||||||
<Item key="share">{formatMessage(messages.shareUrl)}</Item>
|
<Item key="share">{formatMessage(labels.shareUrl)}</Item>
|
||||||
<Item key="actions">{formatMessage(messages.actions)}</Item>
|
<Item key="data">{formatMessage(labels.data)}</Item>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{tab === 'details' && (
|
{tab === 'details' && (
|
||||||
<WebsiteEditForm websiteId={websiteId} data={values} onSave={handleSave} />
|
<WebsiteEditForm websiteId={websiteId} data={values} onSave={handleSave} />
|
||||||
)}
|
)}
|
||||||
{tab === 'tracking' && <TrackingCode websiteId={websiteId} data={values} />}
|
{tab === 'tracking' && <TrackingCode websiteId={websiteId} data={values} />}
|
||||||
{tab === 'share' && <ShareUrl websiteId={websiteId} data={values} onSave={handleSave} />}
|
{tab === 'share' && <ShareUrl websiteId={websiteId} data={values} onSave={handleSave} />}
|
||||||
{tab === 'actions' && <WebsiteReset websiteId={websiteId} onSave={handleSave} />}
|
{tab === 'data' && <WebsiteReset websiteId={websiteId} onSave={handleSave} />}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Icon, Text, Modal, useToast, Icons } from 'react-basics';
|
import { Button, Icon, Text, Modal, useToast, Icons } from 'react-basics';
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { 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 EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
|
@ -8,16 +8,7 @@ import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm';
|
||||||
import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
|
import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import useUser from 'hooks/useUser';
|
import useUser from 'hooks/useUser';
|
||||||
|
import { labels, messages } from 'components/messages';
|
||||||
const messages = defineMessages({
|
|
||||||
saved: { id: 'messages.website-saved', defaultMessage: 'Website saved.' },
|
|
||||||
noWebsites: {
|
|
||||||
id: 'messages.no-websites',
|
|
||||||
defaultMessage: "You don't have any websites configured.",
|
|
||||||
},
|
|
||||||
websites: { id: 'label.websites', defaultMessage: 'Websites' },
|
|
||||||
addWebsite: { id: 'label.add-website', defaultMessage: 'Add website' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const { Plus } = Icons;
|
const { Plus } = Icons;
|
||||||
|
|
||||||
|
@ -49,14 +40,14 @@ export default function WebsitesList() {
|
||||||
<Icon>
|
<Icon>
|
||||||
<Plus />
|
<Plus />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Text>{formatMessage(messages.addWebsite)}</Text>
|
<Text>{formatMessage(labels.addWebsite)}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page loading={isLoading} error={error}>
|
<Page loading={isLoading} error={error}>
|
||||||
{toast}
|
{toast}
|
||||||
<PageHeader title={formatMessage(messages.websites)}>{addButton}</PageHeader>
|
<PageHeader title={formatMessage(labels.websites)}>{addButton}</PageHeader>
|
||||||
{hasData && <WebsitesTable data={data} />}
|
{hasData && <WebsitesTable data={data} />}
|
||||||
{!hasData && (
|
{!hasData && (
|
||||||
<EmptyPlaceholder message={formatMessage(messages.noWebsites)}>
|
<EmptyPlaceholder message={formatMessage(messages.noWebsites)}>
|
||||||
|
@ -64,7 +55,7 @@ export default function WebsitesList() {
|
||||||
</EmptyPlaceholder>
|
</EmptyPlaceholder>
|
||||||
)}
|
)}
|
||||||
{edit && (
|
{edit && (
|
||||||
<Modal title={formatMessage(messages.addWebsite)} onClose={handleClose}>
|
<Modal title={formatMessage(labels.addWebsite)} onClose={handleClose}>
|
||||||
{close => <WebsiteAddForm onSave={handleSave} onClose={close} />}
|
{close => <WebsiteAddForm onSave={handleSave} onClose={close} />}
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -9,20 +9,28 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Icon,
|
Icon,
|
||||||
Icons,
|
Icons,
|
||||||
|
Flexbox,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import styles from './WebsitesTable.module.css';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
const { ArrowRight, External } = Icons;
|
const { ArrowRight, External } = Icons;
|
||||||
|
|
||||||
const columns = [
|
const messages = defineMessages({
|
||||||
{ name: 'name', label: 'Name', style: { flex: 2 } },
|
name: { id: 'label.name', defaultMessage: 'Name' },
|
||||||
{ name: 'domain', label: 'Domain' },
|
domain: { id: 'label.domain', defaultMessage: 'Domain' },
|
||||||
{ name: 'action', label: ' ' },
|
});
|
||||||
];
|
|
||||||
|
|
||||||
export default function WebsitesTable({ data = [] }) {
|
export default function WebsitesTable({ data = [] }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ name: 'name', label: formatMessage(messages.name), style: { flex: 2 } },
|
||||||
|
{ name: 'domain', label: formatMessage(messages.domain) },
|
||||||
|
{ name: 'action', label: ' ' },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className={styles.table} columns={columns} rows={data}>
|
<Table columns={columns} rows={data}>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{(column, index) => {
|
{(column, index) => {
|
||||||
return (
|
return (
|
||||||
|
@ -37,7 +45,7 @@ export default function WebsitesTable({ data = [] }) {
|
||||||
const { id } = row;
|
const { id } = row;
|
||||||
|
|
||||||
row.action = (
|
row.action = (
|
||||||
<div className={styles.actions}>
|
<Flexbox flex={1} justifyContent="end" gap={10}>
|
||||||
<Link href={`/settings/websites/${id}`}>
|
<Link href={`/settings/websites/${id}`}>
|
||||||
<a>
|
<a>
|
||||||
<Button>
|
<Button>
|
||||||
|
@ -58,19 +66,17 @@ export default function WebsitesTable({ data = [] }) {
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</Flexbox>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={rowIndex} data={row} keys={keys}>
|
<TableRow key={rowIndex} data={row} keys={keys}>
|
||||||
{(data, key, colIndex) => {
|
{(data, key, colIndex) => {
|
||||||
return (
|
return (
|
||||||
<TableCell
|
<TableCell key={colIndex} style={{ ...columns[colIndex]?.style }}>
|
||||||
key={colIndex}
|
<Flexbox flex={1} alignItems="center">
|
||||||
className={styles.cell}
|
{data[key]}
|
||||||
style={{ ...columns[colIndex]?.style }}
|
</Flexbox>
|
||||||
>
|
|
||||||
{data[key]}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
.table th,
|
|
||||||
.table td {
|
|
||||||
flex: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell:last-child {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
|
@ -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.55.0",
|
"react-basics": "^0.62.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",
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default async (
|
||||||
const { id: teamId } = req.query;
|
const { id: teamId } = req.query;
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
if (await canViewTeam(req.auth, teamId)) {
|
if (!(await canViewTeam(req.auth, teamId))) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,7 @@ export default async (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === userId) {
|
if (id === userId) {
|
||||||
return badRequest(res, 'You cannot delete your own user.');
|
return badRequest(res, 'You cannot delete yourself.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteUser(id);
|
await deleteUser(id);
|
||||||
|
|
|
@ -26,9 +26,10 @@ export default async (
|
||||||
const {
|
const {
|
||||||
user: { id: userId },
|
user: { id: userId },
|
||||||
} = req.auth;
|
} = req.auth;
|
||||||
|
const { id } = req.query;
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const websites = await getUserWebsites(userId);
|
const websites = await getUserWebsites(id as string);
|
||||||
|
|
||||||
return ok(res, websites);
|
return ok(res, websites);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import AppLayout from 'components/layout/AppLayout';
|
import AppLayout from 'components/layout/AppLayout';
|
||||||
import TeamDetails from 'components/pages/settings/teams/TeamDetails';
|
import TeamSettings from 'components/pages/settings/teams/TeamSettings';
|
||||||
import useUser from 'hooks/useUser';
|
import useUser from 'hooks/useUser';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ export default function TeamDetailPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<TeamDetails teamId={id} />
|
<TeamSettings teamId={id} />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,9 @@ export async function getUser(
|
||||||
|
|
||||||
export async function getUsers(): Promise<User[]> {
|
export async function getUsers(): Promise<User[]> {
|
||||||
return prisma.client.user.findMany({
|
return prisma.client.user.findMany({
|
||||||
|
where: {
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{
|
{
|
||||||
username: 'asc',
|
username: 'asc',
|
||||||
|
|
|
@ -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.55.0:
|
react-basics@^0.62.0:
|
||||||
version "0.55.0"
|
version "0.62.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.55.0.tgz#b9c3dbba33a3ce118e63322ddcb822139a3ea547"
|
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.62.0.tgz#7d4890632d9a0086e54f7472b9e59db0c973dd86"
|
||||||
integrity sha512-30ygRxj7l0KjbmOGF2xcptCOSr8Bw85n6U7I4RzHI5VKvzq7CliKaXr+xvGrRFPvNQ6TnrY25uFdUd7BFDHvGA==
|
integrity sha512-/m9LXHwRCX3uqT8exJp+D0PsDqQBAfg4BXpqRuXmQBMmEYjixQo+qBLHcbnJ0/6qLrsyb/5y4VBrorWQ2HtKiQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
classnames "^2.3.1"
|
classnames "^2.3.1"
|
||||||
react "^18.2.0"
|
react "^18.2.0"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user