mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 12:47:13 +01:00
Rewrite admin. (#1713)
* Rewrite admin. * Clean up password forms. * Fix naming issues. * CSS Naming.
This commit is contained in:
parent
f4db04c3c6
commit
e1f99a7d01
113 changed files with 2054 additions and 1872 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
36
components/forms/TeamAddForm.js
Normal file
36
components/forms/TeamAddForm.js
Normal 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>
|
||||
);
|
||||
}
|
||||
34
components/forms/TeamEditForm.js
Normal file
34
components/forms/TeamEditForm.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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><head></code> section of your HTML.
|
||||
</p>
|
||||
<TextArea rows={4} value={code} readOnly allowCopy />
|
||||
</FormRow>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
43
components/forms/UserDeleteForm.js
Normal file
43
components/forms/UserDeleteForm.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
6
components/forms/UserForm.module.css
Normal file
6
components/forms/UserForm.module.css
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
width: 300px;
|
||||
}
|
||||
81
components/forms/UserPasswordForm.js
Normal file
81
components/forms/UserPasswordForm.js
Normal 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>
|
||||
);
|
||||
}
|
||||
6
components/forms/UserPasswordForm.module.css
Normal file
6
components/forms/UserPasswordForm.module.css
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
width: 300px;
|
||||
}
|
||||
47
components/forms/WebsiteAddForm.js
Normal file
47
components/forms/WebsiteAddForm.js
Normal 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>
|
||||
);
|
||||
}
|
||||
43
components/forms/WebsiteDeleteForm.js
Normal file
43
components/forms/WebsiteDeleteForm.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
.dropdown {
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
49
components/forms/WebsiteReset.js
Normal file
49
components/forms/WebsiteReset.js
Normal 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>
|
||||
);
|
||||
}
|
||||
45
components/forms/WebsiteResetForm.js
Normal file
45
components/forms/WebsiteResetForm.js
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue