Rewrite admin. (#1713)

* Rewrite admin.

* Clean up password forms.

* Fix naming issues.

* CSS Naming.
This commit is contained in:
Brian Cao 2022-12-26 16:57:59 -08:00 committed by GitHub
parent f4db04c3c6
commit e1f99a7d01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
113 changed files with 2054 additions and 1872 deletions

View file

@ -1,107 +0,0 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik';
import Button from 'components/common/Button';
import FormLayout, {
FormButtons,
FormError,
FormMessage,
FormRow,
} from 'components/layout/FormLayout';
import useApi from 'hooks/useApi';
import useUser from 'hooks/useUser';
const initialValues = {
current_password: '',
new_password: '',
confirm_password: '',
};
const validate = ({ current_password, new_password, confirm_password }) => {
const errors = {};
if (!current_password) {
errors.current_password = <FormattedMessage id="label.required" defaultMessage="Required" />;
}
if (!new_password) {
errors.new_password = <FormattedMessage id="label.required" defaultMessage="Required" />;
}
if (!confirm_password) {
errors.confirm_password = <FormattedMessage id="label.required" defaultMessage="Required" />;
} else if (new_password !== confirm_password) {
errors.confirm_password = (
<FormattedMessage id="label.passwords-dont-match" defaultMessage="Passwords don't match" />
);
}
return errors;
};
export default function ChangePasswordForm({ values, onSave, onClose }) {
const { post } = useApi();
const [message, setMessage] = useState();
const { user } = useUser();
const handleSubmit = async values => {
const { ok, error } = await post(`/users/${user.id}/password`, values);
if (ok) {
onSave();
} else {
setMessage(
error || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
);
}
};
return (
<FormLayout>
<Formik
initialValues={{ ...initialValues, ...values }}
validate={validate}
onSubmit={handleSubmit}
>
{() => (
<Form>
<FormRow>
<label htmlFor="current_password">
<FormattedMessage id="label.current-password" defaultMessage="Current password" />
</label>
<div>
<Field name="current_password" type="password" />
<FormError name="current_password" />
</div>
</FormRow>
<FormRow>
<label htmlFor="new_password">
<FormattedMessage id="label.new-password" defaultMessage="New password" />
</label>
<div>
<Field name="new_password" type="password" />
<FormError name="new_password" />
</div>
</FormRow>
<FormRow>
<label htmlFor="confirm_password">
<FormattedMessage id="label.confirm-password" defaultMessage="Confirm password" />
</label>
<div>
<Field name="confirm_password" type="password" />
<FormError name="confirm_password" />
</div>
</FormRow>
<FormButtons>
<Button type="submit" variant="action">
<FormattedMessage id="label.save" defaultMessage="Save" />
</Button>
<Button onClick={onClose}>
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button>
</FormButtons>
<FormMessage>{message}</FormMessage>
</Form>
)}
</Formik>
</FormLayout>
);
}

View file

@ -1,12 +1,11 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { isAfter, isBefore, isSameDay } from 'date-fns';
import Calendar from 'components/common/Calendar';
import Button from 'components/common/Button';
import { FormButtons } from 'components/layout/FormLayout';
import { isAfter, isBefore, isSameDay } from 'date-fns';
import { getDateRangeValues } from 'lib/date';
import React, { useState } from 'react';
import { Button, ButtonGroup } from 'react-basics';
import { FormattedMessage } from 'react-intl';
import styles from './DatePickerForm.module.css';
import ButtonGroup from 'components/common/ButtonGroup';
const FILTER_DAY = 0;
const FILTER_RANGE = 1;

View file

@ -1,105 +0,0 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik';
import Button from 'components/common/Button';
import FormLayout, {
FormButtons,
FormError,
FormMessage,
FormRow,
} from 'components/layout/FormLayout';
import Loading from 'components/common/Loading';
import useApi from 'hooks/useApi';
const CONFIRMATION_WORD = 'DELETE';
const validate = ({ confirmation }) => {
const errors = {};
if (confirmation !== CONFIRMATION_WORD) {
errors.confirmation = !confirmation ? (
<FormattedMessage id="label.required" defaultMessage="Required" />
) : (
<FormattedMessage id="label.invalid" defaultMessage="Invalid" />
);
}
return errors;
};
export default function DeleteForm({ values, onSave, onClose }) {
const { del } = useApi();
const [message, setMessage] = useState();
const [deleting, setDeleting] = useState(false);
const handleSubmit = async ({ type, id }) => {
setDeleting(true);
const { ok, data } = await del(`/${type}/${id}`);
if (ok) {
onSave();
} else {
setMessage(
data || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
);
setDeleting(false);
}
};
return (
<FormLayout>
{deleting && <Loading overlay />}
<Formik
initialValues={{ confirmation: '', ...values }}
validate={validate}
onSubmit={handleSubmit}
>
{props => (
<Form>
<div>
<FormattedMessage
id="message.confirm-delete"
defaultMessage="Are your sure you want to delete {target}?"
values={{ target: <b>{values.name}</b> }}
/>
</div>
<div>
<FormattedMessage
id="message.delete-warning"
defaultMessage="All associated data will be deleted as well."
/>
</div>
<p>
<FormattedMessage
id="message.type-delete"
defaultMessage="Type {delete} in the box below to confirm."
values={{ delete: <b>{CONFIRMATION_WORD}</b> }}
/>
</p>
<FormRow>
<div>
<Field name="confirmation" type="text" />
<FormError name="confirmation" />
</div>
</FormRow>
<FormButtons>
<Button
type="submit"
variant="danger"
disabled={props.values.confirmation !== CONFIRMATION_WORD}
>
<FormattedMessage id="label.delete" defaultMessage="Delete" />
</Button>
<Button onClick={onClose}>
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button>
</FormButtons>
<FormMessage>{message}</FormMessage>
</Form>
)}
</Formik>
</FormLayout>
);
}

View file

@ -1,5 +1,5 @@
import classNames from 'classnames';
import Button from 'components/common/Button';
import { Button } from 'react-basics';
import DateFilter from 'components/common/DateFilter';
import DropDown from 'components/common/DropDown';
import FormLayout, {

View file

@ -1,98 +0,0 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik';
import Button from 'components/common/Button';
import FormLayout, {
FormButtons,
FormError,
FormMessage,
FormRow,
} from 'components/layout/FormLayout';
import useApi from 'hooks/useApi';
const CONFIRMATION_WORD = 'RESET';
const validate = ({ confirmation }) => {
const errors = {};
if (confirmation !== CONFIRMATION_WORD) {
errors.confirmation = !confirmation ? (
<FormattedMessage id="label.required" defaultMessage="Required" />
) : (
<FormattedMessage id="label.invalid" defaultMessage="Invalid" />
);
}
return errors;
};
export default function ResetForm({ values, onSave, onClose }) {
const { post } = useApi();
const [message, setMessage] = useState();
const handleSubmit = async ({ type, id }) => {
const { ok, data } = await post(`/${type}/${id}/reset`);
if (ok) {
onSave();
} else {
setMessage(
data || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
);
}
};
return (
<FormLayout>
<Formik
initialValues={{ confirmation: '', ...values }}
validate={validate}
onSubmit={handleSubmit}
>
{props => (
<Form>
<div>
<FormattedMessage
id="message.confirm-reset"
defaultMessage="Are your sure you want to reset {target}'s statistics?"
values={{ target: <b>{values.name}</b> }}
/>
</div>
<div>
<FormattedMessage
id="message.reset-warning"
defaultMessage="All statistics for this website will be deleted, but your tracking code will remain intact."
/>
</div>
<p>
<FormattedMessage
id="message.type-reset"
defaultMessage="Type {reset} in the box below to confirm."
values={{ reset: <b>{CONFIRMATION_WORD}</b> }}
/>
</p>
<FormRow>
<div>
<Field name="confirmation" type="text" />
<FormError name="confirmation" />
</div>
</FormRow>
<FormButtons>
<Button
type="submit"
variant="danger"
disabled={props.values.confirmation !== CONFIRMATION_WORD}
>
<FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button>
<Button onClick={onClose}>
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button>
</FormButtons>
<FormMessage>{message}</FormMessage>
</Form>
)}
</Formik>
</FormLayout>
);
}

View file

@ -1,42 +1,85 @@
import React, { useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import Button from 'components/common/Button';
import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout';
import CopyButton from 'components/common/CopyButton';
import { useMutation } from '@tanstack/react-query';
import { getAuthToken } from 'lib/client';
import { getRandomChars, useApi } from 'next-basics';
import { useEffect, useMemo, useRef, useState } from 'react';
import {
Button,
Form,
FormButtons,
FormRow,
HiddenInput,
SubmitButton,
TextField,
Toggle,
} from 'react-basics';
export default function TrackingCodeForm({ values, onClose }) {
const ref = useRef();
const { basePath } = useRouter();
const { name, shareId } = values;
export default function ShareUrlForm({ websiteId, data, onSave }) {
const { name, shareId } = data;
const [id, setId] = useState(shareId);
const { post } = useApi(getAuthToken());
const { mutate, error } = useMutation(({ shareId }) =>
post(`/websites/${websiteId}`, { shareId }),
);
const ref = useRef(null);
const url = useMemo(
() => `${process.env.analyticsUrl}/share/${id}/${encodeURIComponent(name)}`,
[id, name],
);
const generateId = () => getRandomChars(16);
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave(data);
ref.current.reset(data);
},
});
};
const handleGenerate = () => {
const id = generateId();
ref.current.setValue('shareId', id, {
shouldValidate: true,
shouldDirty: true,
});
setId(id);
};
const handleChange = checked => {
const data = { shareId: checked ? generateId() : null };
mutate(data, {
onSuccess: async () => {
onSave(data);
},
});
setId(data.shareId);
};
useEffect(() => {
if (id && id !== shareId) {
ref.current.setValue('shareId', id);
}
}, [id, shareId]);
return (
<FormLayout>
<p>
<FormattedMessage
id="message.share-url"
defaultMessage="This is the publicly shared URL for {target}."
values={{ target: <b>{values.name}</b> }}
/>
</p>
<FormRow>
<textarea
ref={ref}
rows={3}
cols={60}
spellCheck={false}
defaultValue={`${
document.location.origin
}${basePath}/share/${shareId}/${encodeURIComponent(name)}`}
readOnly
/>
</FormRow>
<FormButtons>
<CopyButton type="submit" variant="action" element={ref} />
<Button onClick={onClose}>
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button>
</FormButtons>
</FormLayout>
<>
<Toggle checked={Boolean(id)} onChange={handleChange}>
Enable share URL
</Toggle>
{id && (
<Form key={websiteId} ref={ref} onSubmit={handleSubmit} error={error} values={data}>
<FormRow>
<p>Your website stats are publically available at the following URL:</p>
<TextField value={url} readOnly allowCopy />
</FormRow>
<HiddenInput name="shareId" />
<FormButtons>
<SubmitButton variant="primary">Save</SubmitButton>
<Button onClick={handleGenerate}>Regenerate URL</Button>
</FormButtons>
</Form>
)}
</>
);
}

View file

@ -0,0 +1,36 @@
import { useRef } from 'react';
import { Form, FormInput, FormButtons, TextField, Button } from 'react-basics';
import { useApi } from 'next-basics';
import styles from './Form.module.css';
import { useMutation } from '@tanstack/react-query';
import { getAuthToken } from 'lib/client';
export default function TeamAddForm({ onSave, onClose }) {
const { post } = useApi(getAuthToken());
const { mutate, error, isLoading } = useMutation(data => post('/teams', data));
const ref = useRef(null);
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave();
},
});
};
return (
<Form ref={ref} className={styles.form} onSubmit={handleSubmit} error={error}>
<FormInput name="name" label="Name" rules={{ required: 'Required' }}>
<TextField autoComplete="off" />
</FormInput>
<FormButtons flex>
<Button type="submit" variant="primary" disabled={isLoading}>
Save
</Button>
<Button disabled={isLoading} onClick={onClose}>
Cancel
</Button>
</FormButtons>
</Form>
);
}

View file

@ -0,0 +1,34 @@
import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics';
import { useMutation } from '@tanstack/react-query';
import { useRef } from 'react';
import { useApi } from 'next-basics';
import { getAuthToken } from 'lib/client';
export default function TeamEditForm({ teamId, data, onSave }) {
const { post } = useApi(getAuthToken());
const { mutate, error } = useMutation(data => post(`/teams/${teamId}`, data));
const ref = useRef(null);
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
ref.current.reset(data);
onSave(data);
},
});
};
return (
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data}>
<FormRow label="Team ID">
<TextField value={teamId} readOnly allowCopy />
</FormRow>
<FormInput name="name" label="Name" rules={{ required: 'Required' }}>
<TextField />
</FormInput>
<FormButtons>
<SubmitButton variant="primary">Save</SubmitButton>
</FormButtons>
</Form>
);
}

View file

@ -1,43 +1,23 @@
import React, { useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import Button from 'components/common/Button';
import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout';
import CopyButton from 'components/common/CopyButton';
import useConfig from 'hooks/useConfig';
import { useRef } from 'react';
import { Form, FormRow, TextArea } from 'react-basics';
export default function TrackingCodeForm({ values, onClose }) {
const ref = useRef();
const { basePath } = useRouter();
export default function TrackingCodeForm({ websiteId }) {
const ref = useRef(null);
const { trackerScriptName } = useConfig();
const code = `<script async defer src="${trackerScriptName}" data-website-id="${websiteId}"></script>`;
return (
<FormLayout>
<p>
<FormattedMessage
id="message.track-stats"
defaultMessage="To track stats for {target}, place the following code in the {head} section of your website."
values={{ head: '<head>', target: <b>{values.name}</b> }}
/>
</p>
<FormRow>
<textarea
ref={ref}
rows={3}
cols={60}
spellCheck={false}
defaultValue={`<script async defer data-website-id="${values.id}" src="${
document.location.origin
}${basePath}/${trackerScriptName ? `${trackerScriptName}.js` : 'umami.js'}"></script>`}
readOnly
/>
</FormRow>
<FormButtons>
<CopyButton type="submit" variant="action" element={ref} />
<Button onClick={onClose}>
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button>
</FormButtons>
</FormLayout>
<>
<Form ref={ref}>
<FormRow>
<p>
To track stats for this website, place the following code in the{' '}
<code>&lt;head&gt;</code> section of your HTML.
</p>
<TextArea rows={4} value={code} readOnly allowCopy />
</FormRow>
</Form>
</>
);
}

View file

@ -0,0 +1,43 @@
import { useMutation } from '@tanstack/react-query';
import { getAuthToken } from 'lib/client';
import { useApi } from 'next-basics';
import { Button, Form, FormButtons, FormInput, SubmitButton, TextField } from 'react-basics';
import styles from './Form.module.css';
const CONFIRM_VALUE = 'DELETE';
export default function UserDeleteForm({ userId, onSave, onClose }) {
const { del } = useApi(getAuthToken());
const { mutate, error, isLoading } = useMutation(data => del(`/users/${userId}`, data));
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave();
},
});
};
return (
<Form className={styles.form} onSubmit={handleSubmit} error={error}>
<div>
To delete this user, type <b>{CONFIRM_VALUE}</b> in the box below to confirm.
</div>
<FormInput
name="confirmation"
label="Confirm"
rules={{ validate: value => value === CONFIRM_VALUE }}
>
<TextField autoComplete="off" />
</FormInput>
<FormButtons flex>
<SubmitButton variant="primary" className={styles.button} disabled={isLoading}>
Save
</SubmitButton>
<Button className={styles.button} disabled={isLoading} onClick={onClose}>
Cancel
</Button>
</FormButtons>
</Form>
);
}

View file

@ -1,89 +1,65 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik';
import Button from 'components/common/Button';
import FormLayout, {
import {
Dropdown,
Item,
Form,
FormButtons,
FormError,
FormMessage,
FormRow,
} from 'components/layout/FormLayout';
import useApi from 'hooks/useApi';
FormInput,
TextField,
SubmitButton,
} from 'react-basics';
import { useRef } from 'react';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'next-basics';
import { getAuthToken } from 'lib/client';
import { ROLES } from 'lib/constants';
import styles from './UserForm.module.css';
const initialValues = {
username: '',
password: '',
};
const items = [
{
value: ROLES.user,
label: 'User',
},
{
value: ROLES.admin,
label: 'Admin',
},
];
const validate = ({ id, username, password }) => {
const errors = {};
export default function UserEditForm({ data, onSave }) {
const { id } = data;
const { post } = useApi(getAuthToken());
const { mutate, error } = useMutation(({ username }) => post(`/user/${id}`, { username }));
const ref = useRef(null);
if (!username) {
errors.username = <FormattedMessage id="label.required" defaultMessage="Required" />;
}
if (!id && !password) {
errors.password = <FormattedMessage id="label.required" defaultMessage="Required" />;
}
return errors;
};
export default function UserEditForm({ values, onSave, onClose }) {
const { post } = useApi();
const [message, setMessage] = useState();
const handleSubmit = async values => {
const { id } = values;
const { ok, data } = await post(id ? `/users/${id}` : '/users', values);
if (ok) {
onSave();
} else {
setMessage(
data || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
);
}
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave(data);
ref.current.reset(data);
},
});
};
return (
<FormLayout>
<Formik
initialValues={{ ...initialValues, ...values }}
validate={validate}
onSubmit={handleSubmit}
>
{() => (
<Form>
<FormRow>
<label htmlFor="username">
<FormattedMessage id="label.username" defaultMessage="Username" />
</label>
<div>
<Field name="username" type="text" />
<FormError name="username" />
</div>
</FormRow>
<FormRow>
<label htmlFor="password">
<FormattedMessage id="label.password" defaultMessage="Password" />
</label>
<div>
<Field name="password" type="password" />
<FormError name="password" />
</div>
</FormRow>
<FormButtons>
<Button type="submit" variant="action">
<FormattedMessage id="label.save" defaultMessage="Save" />
</Button>
<Button onClick={onClose}>
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button>
</FormButtons>
<FormMessage>{message}</FormMessage>
</Form>
)}
</Formik>
</FormLayout>
<Form
key={id}
className={styles.form}
ref={ref}
onSubmit={handleSubmit}
error={error}
values={data}
>
<FormInput name="username" label="Username">
<TextField />
</FormInput>
<FormInput name="role" label="Role">
<Dropdown items={items} style={{ width: 200 }}>
{({ value, label }) => <Item key={value}>{label}</Item>}
</Dropdown>
</FormInput>
<FormButtons>
<SubmitButton variant="primary">Save</SubmitButton>
</FormButtons>
</Form>
);
}

View file

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

View file

@ -0,0 +1,81 @@
import { useRef } from 'react';
import { Form, FormInput, FormButtons, PasswordField, Button } from 'react-basics';
import { useApi } from 'next-basics';
import { useMutation } from '@tanstack/react-query';
import { getAuthToken } from 'lib/client';
import styles from './UserPasswordForm.module.css';
import useUser from 'hooks/useUser';
export default function UserPasswordForm({ onSave, userId }) {
const {
user: { id },
} = useUser();
const isCurrentUser = !userId || id === userId;
const url = isCurrentUser ? `/users/${id}/password` : `/users/${id}`;
const { post } = useApi(getAuthToken());
const { mutate, error, isLoading } = useMutation(data => post(url, data));
const ref = useRef(null);
const handleSubmit = async data => {
const payload = isCurrentUser
? data
: {
password: data.new_password,
};
mutate(payload, {
onSuccess: async () => {
onSave();
ref.current.reset();
},
});
};
const samePassword = value => {
if (value !== ref?.current?.getValues('new_password')) {
return "Passwords don't match";
}
return true;
};
return (
<Form ref={ref} className={styles.form} onSubmit={handleSubmit} error={error}>
{isCurrentUser && (
<FormInput
name="current_password"
label="Current password"
rules={{ required: 'Required' }}
>
<PasswordField autoComplete="off" />
</FormInput>
)}
<FormInput
name="new_password"
label="New password"
rules={{
required: 'Required',
minLength: { value: 8, message: 'Minimum length 8 characters' },
}}
>
<PasswordField autoComplete="off" />
</FormInput>
<FormInput
name="confirm_password"
label="Confirm password"
rules={{
required: 'Required',
minLength: { value: 8, message: 'Minimum length 8 characters' },
validate: samePassword,
}}
>
<PasswordField autoComplete="off" />
</FormInput>
<FormButtons flex>
<Button type="submit" disabled={isLoading}>
Save
</Button>
</FormButtons>
</Form>
);
}

View file

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

View file

@ -0,0 +1,47 @@
import { useRef } from 'react';
import { Form, FormInput, FormButtons, TextField, Button, SubmitButton } from 'react-basics';
import { useApi } from 'next-basics';
import styles from './Form.module.css';
import { useMutation } from '@tanstack/react-query';
import { getAuthToken } from 'lib/client';
import { DOMAIN_REGEX } from 'lib/constants';
export default function WebsiteAddForm({ onSave, onClose }) {
const { post } = useApi(getAuthToken());
const { mutate, error, isLoading } = useMutation(data => post('/websites', data));
const ref = useRef(null);
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave();
},
});
};
return (
<Form ref={ref} className={styles.form} onSubmit={handleSubmit} error={error}>
<FormInput name="name" label="Name" rules={{ required: 'Required' }}>
<TextField autoComplete="off" />
</FormInput>
<FormInput
name="domain"
label="Domain"
rules={{
required: 'Required',
pattern: { value: DOMAIN_REGEX, message: 'Invalid domain' },
}}
>
<TextField autoComplete="off" />
</FormInput>
<FormButtons flex>
<SubmitButton variant="primary" disabled={false}>
Save
</SubmitButton>
<Button disabled={isLoading} onClick={onClose}>
Cancel
</Button>
</FormButtons>
</Form>
);
}

View file

@ -0,0 +1,43 @@
import { useMutation } from '@tanstack/react-query';
import { getAuthToken } from 'lib/client';
import { useApi } from 'next-basics';
import { Button, Form, FormButtons, FormInput, SubmitButton, TextField } from 'react-basics';
import styles from './Form.module.css';
const CONFIRM_VALUE = 'DELETE';
export default function WebsiteDeleteForm({ websiteId, onSave, onClose }) {
const { del } = useApi(getAuthToken());
const { mutate, error, isLoading } = useMutation(data => del(`/websites/${websiteId}`, data));
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave();
},
});
};
return (
<Form className={styles.form} onSubmit={handleSubmit} error={error}>
<div>
To delete this website, type <b>{CONFIRM_VALUE}</b> in the box below to confirm.
</div>
<FormInput
name="confirmation"
label="Confirm"
rules={{ validate: value => value === CONFIRM_VALUE }}
>
<TextField autoComplete="off" />
</FormInput>
<FormButtons flex>
<SubmitButton variant="primary" className={styles.button} disabled={isLoading}>
Save
</SubmitButton>
<Button className={styles.button} disabled={isLoading} onClick={onClose}>
Cancel
</Button>
</FormButtons>
</Form>
);
}

View file

@ -1,159 +1,48 @@
import React, { useEffect, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field, useFormikContext } from 'formik';
import Button from 'components/common/Button';
import FormLayout, {
FormButtons,
FormError,
FormMessage,
FormRow,
} from 'components/layout/FormLayout';
import Checkbox from 'components/common/Checkbox';
import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics';
import { useMutation } from '@tanstack/react-query';
import { useRef } from 'react';
import { useApi } from 'next-basics';
import { getAuthToken } from 'lib/client';
import { DOMAIN_REGEX } from 'lib/constants';
import useApi from 'hooks/useApi';
import useFetch from 'hooks/useFetch';
import useUser from 'hooks/useUser';
import styles from './WebsiteEditForm.module.css';
const initialValues = {
name: '',
domain: '',
owner: '',
public: false,
};
export default function WebsiteEditForm({ websiteId, data, onSave }) {
const { post } = useApi(getAuthToken());
const { mutate, error } = useMutation(data => post(`/websites/${websiteId}`, data));
const ref = useRef(null);
const validate = ({ name, domain }) => {
const errors = {};
if (!name) {
errors.name = <FormattedMessage id="label.required" defaultMessage="Required" />;
}
if (!domain) {
errors.domain = <FormattedMessage id="label.required" defaultMessage="Required" />;
} else if (!DOMAIN_REGEX.test(domain)) {
errors.domain = <FormattedMessage id="label.invalid-domain" defaultMessage="Invalid domain" />;
}
return errors;
};
const OwnerDropDown = ({ user, users }) => {
const { setFieldValue, values } = useFormikContext();
useEffect(() => {
if (values.userId != null && values.owner === '') {
setFieldValue('owner', values.userId.toString());
} else if (user?.id && values.owner === '') {
setFieldValue('owner', user.id.toString());
}
}, [users, setFieldValue, user, values]);
if (user?.isAdmin) {
return (
<FormRow>
<label htmlFor="owner">
<FormattedMessage id="label.owner" defaultMessage="Owner" />
</label>
<div>
<Field as="select" name="owner" className={styles.dropdown}>
{users?.map(acc => (
<option key={acc.id} value={acc.id}>
{acc.username}
</option>
))}
</Field>
<FormError name="owner" />
</div>
</FormRow>
);
} else {
return null;
}
};
export default function WebsiteEditForm({ values, onSave, onClose }) {
const { post } = useApi();
const { data: users } = useFetch(`/users`);
const { user } = useUser();
const [message, setMessage] = useState();
const handleSubmit = async values => {
const { id } = values;
const { ok, data } = await post(id ? `/websites/${id}` : '/websites', values);
if (ok) {
onSave();
} else {
setMessage(
data || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
);
}
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
ref.current.reset(data);
onSave(data);
},
});
};
return (
<FormLayout>
<Formik
initialValues={{ ...initialValues, ...values, enableShareUrl: !!values?.shareId }}
validate={validate}
onSubmit={handleSubmit}
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data}>
<FormRow label="Website ID">
<TextField value={websiteId} readOnly allowCopy />
</FormRow>
<FormInput name="name" label="Name" rules={{ required: 'Required' }}>
<TextField />
</FormInput>
<FormInput
name="domain"
label="Domain"
rules={{
required: 'Required',
pattern: {
value: DOMAIN_REGEX,
message: 'Invalid domain',
},
}}
>
{() => (
<Form>
<FormRow>
<label htmlFor="name">
<FormattedMessage id="label.name" defaultMessage="Name" />
</label>
<div>
<Field name="name" type="text" />
<FormError name="name" />
</div>
</FormRow>
<FormRow>
<label htmlFor="domain">
<FormattedMessage id="label.domain" defaultMessage="Domain" />
</label>
<div>
<Field
name="domain"
type="text"
placeholder="example.com"
spellCheck="false"
autoCapitalize="off"
autoCorrect="off"
/>
<FormError name="domain" />
</div>
</FormRow>
<OwnerDropDown users={users} user={user} />
<FormRow>
<label />
<Field name="enableShareUrl">
{({ field }) => (
<Checkbox
{...field}
label={
<FormattedMessage
id="label.enable-share-url"
defaultMessage="Enable share URL"
/>
}
/>
)}
</Field>
</FormRow>
<FormButtons>
<Button type="submit" variant="action">
<FormattedMessage id="label.save" defaultMessage="Save" />
</Button>
<Button onClick={onClose}>
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
</Button>
</FormButtons>
<FormMessage>{message}</FormMessage>
</Form>
)}
</Formik>
</FormLayout>
<TextField />
</FormInput>
<FormButtons>
<SubmitButton variant="primary">Save</SubmitButton>
</FormButtons>
</Form>
);
}

View file

@ -1,5 +0,0 @@
.dropdown {
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
}

View file

@ -0,0 +1,49 @@
import WebsiteDeleteForm from 'components/forms/WebsiteDeleteForm';
import WebsiteResetForm from 'components/forms/WebsiteResetForm';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { Button, Form, FormRow, Modal } from 'react-basics';
export default function WebsiteReset({ websiteId, onSave }) {
const [modal, setModal] = useState(null);
const router = useRouter();
const handleReset = async () => {
setModal(null);
onSave();
};
const handleDelete = async () => {
onSave();
await router.push('/websites');
};
const handleClose = () => setModal(null);
return (
<Form>
<FormRow label="Reset website">
<p>
All statistics for this website will be deleted, but your settings will remain intact.
</p>
<Button onClick={() => setModal('reset')}>Reset</Button>
</FormRow>
<FormRow label="Delete website">
<p>All website data will be deleted.</p>
<Button onClick={() => setModal('delete')}>Delete</Button>
</FormRow>
{modal === 'reset' && (
<Modal title="Reset website" onClose={handleClose}>
{close => <WebsiteResetForm websiteId={websiteId} onSave={handleReset} onClose={close} />}
</Modal>
)}
{modal === 'delete' && (
<Modal title="Delete website" onClose={handleClose}>
{close => (
<WebsiteDeleteForm websiteId={websiteId} onSave={handleDelete} onClose={close} />
)}
</Modal>
)}
</Form>
);
}

View file

@ -0,0 +1,45 @@
import { useMutation } from '@tanstack/react-query';
import { getAuthToken } from 'lib/client';
import { useApi } from 'next-basics';
import { Button, Form, FormButtons, FormInput, SubmitButton, TextField } from 'react-basics';
import styles from './Form.module.css';
const CONFIRM_VALUE = 'RESET';
export default function WebsiteResetForm({ websiteId, onSave, onClose }) {
const { post } = useApi(getAuthToken());
const { mutate, error, isLoading } = useMutation(data =>
post(`/websites/${websiteId}/reset`, data),
);
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave();
},
});
};
return (
<Form className={styles.form} onSubmit={handleSubmit} error={error}>
<div>
To reset this website, type <b>{CONFIRM_VALUE}</b> in the box below to confirm.
</div>
<FormInput
name="confirm"
label="Confirmation"
rules={{ validate: value => value === CONFIRM_VALUE }}
>
<TextField autoComplete="off" />
</FormInput>
<FormButtons flex>
<SubmitButton variant="primary" className={styles.button} disabled={isLoading}>
Save
</SubmitButton>
<Button className={styles.button} disabled={isLoading} onClick={onClose}>
Cancel
</Button>
</FormButtons>
</Form>
);
}