mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 18:00:17 +01:00
Refactored forms and pages.
This commit is contained in:
parent
1325abe31d
commit
6253d55790
@ -60,7 +60,7 @@ const redirects = [
|
|||||||
{
|
{
|
||||||
source: '/settings',
|
source: '/settings',
|
||||||
destination: process.env.CLOUD_MODE
|
destination: process.env.CLOUD_MODE
|
||||||
? `${process.env.CLOUD_URL}/settings/websites`
|
? `${process.env.CLOUD_URL}/websites`
|
||||||
: '/settings/websites',
|
: '/settings/websites',
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
|
42
src/app/(main)/reports/ReportDeleteButton.js
Normal file
42
src/app/(main)/reports/ReportDeleteButton.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
|
||||||
|
import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm';
|
||||||
|
import { useApi, useMessages } from 'components/hooks';
|
||||||
|
import { setValue } from 'store/cache';
|
||||||
|
|
||||||
|
export function ReportDeleteButton({ reportId, reportName, onDelete }) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { del, useMutation } = useApi();
|
||||||
|
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
|
||||||
|
|
||||||
|
const handleConfirm = close => {
|
||||||
|
mutate(reportId, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setValue('reports', Date.now());
|
||||||
|
onDelete?.();
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalTrigger>
|
||||||
|
<Button>
|
||||||
|
<Icon>
|
||||||
|
<Icons.Trash />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.delete)}</Text>
|
||||||
|
</Button>
|
||||||
|
<Modal>
|
||||||
|
{close => (
|
||||||
|
<ConfirmDeleteForm
|
||||||
|
name={reportName}
|
||||||
|
onConfirm={handleConfirm.bind(null, close)}
|
||||||
|
onClose={close}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</ModalTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReportDeleteButton;
|
22
src/app/(main)/reports/ReportsDataTable.js
Normal file
22
src/app/(main)/reports/ReportsDataTable.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
'use client';
|
||||||
|
import { useApi } from 'components/hooks';
|
||||||
|
import ReportsTable from './ReportsTable';
|
||||||
|
import useFilterQuery from 'components/hooks/useFilterQuery';
|
||||||
|
import DataTable from 'components/common/DataTable';
|
||||||
|
import useCache from 'store/cache';
|
||||||
|
|
||||||
|
function useReports() {
|
||||||
|
const { get } = useApi();
|
||||||
|
const modified = useCache(state => state?.reports);
|
||||||
|
return useFilterQuery(['reports', modified], params => get(`/reports`, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReportsDataTable() {
|
||||||
|
const queryResult = useReports();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable queryResult={queryResult}>
|
||||||
|
{({ data }) => <ReportsTable data={data} showDomain={true} />}
|
||||||
|
</DataTable>
|
||||||
|
);
|
||||||
|
}
|
@ -1,22 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import Link from 'next/link';
|
|
||||||
import { Button, Icon, Icons, Text } from 'react-basics';
|
import { Button, Icon, Icons, Text } from 'react-basics';
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export function ReportsHeader() {
|
export function ReportsHeader() {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleClick = () => router.push('/reports/create');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader title={formatMessage(labels.reports)}>
|
<PageHeader title={formatMessage(labels.reports)}>
|
||||||
<Link href="/reports/create">
|
<Button variant="primary" onClick={handleClick}>
|
||||||
<Button variant="primary">
|
<Icon>
|
||||||
<Icon>
|
<Icons.Plus />
|
||||||
<Icons.Plus />
|
</Icon>
|
||||||
</Icon>
|
<Text>{formatMessage(labels.createReport)}</Text>
|
||||||
<Text>{formatMessage(labels.createReport)}</Text>
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import { useApi } from 'components/hooks';
|
|
||||||
import ReportsTable from './ReportsTable';
|
|
||||||
import useFilterQuery from 'components/hooks/useFilterQuery';
|
|
||||||
import DataTable from 'components/common/DataTable';
|
|
||||||
|
|
||||||
function useReports() {
|
|
||||||
const { get, del, useMutation } = useApi();
|
|
||||||
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
|
|
||||||
const queryResult = useFilterQuery(['reports'], params => get(`/reports`, params));
|
|
||||||
|
|
||||||
const deleteReport = id => {
|
|
||||||
mutate(id, {
|
|
||||||
onSuccess: () => {
|
|
||||||
queryResult.refetch();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return { queryResult, deleteReport };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ReportsList() {
|
|
||||||
const { queryResult, deleteReport } = useReports();
|
|
||||||
|
|
||||||
const handleDelete = async (id, callback) => {
|
|
||||||
await deleteReport(id);
|
|
||||||
await queryResult.refetch();
|
|
||||||
callback?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataTable queryResult={queryResult}>
|
|
||||||
{({ data }) => <ReportsTable data={data} showDomain={true} onDelete={handleDelete} />}
|
|
||||||
</DataTable>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,27 +1,14 @@
|
|||||||
import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm';
|
|
||||||
import LinkButton from 'components/common/LinkButton';
|
import LinkButton from 'components/common/LinkButton';
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
import useUser from 'components/hooks/useUser';
|
import useUser from 'components/hooks/useUser';
|
||||||
import {
|
import { GridColumn, GridTable, Icon, Icons, Text } from 'react-basics';
|
||||||
Button,
|
|
||||||
GridColumn,
|
|
||||||
GridTable,
|
|
||||||
Icon,
|
|
||||||
Icons,
|
|
||||||
Modal,
|
|
||||||
ModalTrigger,
|
|
||||||
Text,
|
|
||||||
} from 'react-basics';
|
|
||||||
import { REPORT_TYPES } from 'lib/constants';
|
import { REPORT_TYPES } from 'lib/constants';
|
||||||
|
import ReportDeleteButton from './ReportDeleteButton';
|
||||||
|
|
||||||
export function ReportsTable({ data = [], onDelete, showDomain }) {
|
export function ReportsTable({ data = [], showDomain }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
const handleConfirm = (id, callback) => {
|
|
||||||
onDelete?.(id, callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridTable data={data}>
|
<GridTable data={data}>
|
||||||
<GridColumn name="name" label={formatMessage(labels.name)} />
|
<GridColumn name="name" label={formatMessage(labels.name)} />
|
||||||
@ -43,26 +30,15 @@ export function ReportsTable({ data = [], onDelete, showDomain }) {
|
|||||||
const { id, name, userId, website } = row;
|
const { id, name, userId, website } = row;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LinkButton href={`/reports/${id}`}>{formatMessage(labels.view)}</LinkButton>
|
|
||||||
{(user.id === userId || user.id === website?.userId) && (
|
{(user.id === userId || user.id === website?.userId) && (
|
||||||
<ModalTrigger>
|
<ReportDeleteButton reportId={id} reportName={name} />
|
||||||
<Button>
|
|
||||||
<Icon>
|
|
||||||
<Icons.Trash />
|
|
||||||
</Icon>
|
|
||||||
<Text>{formatMessage(labels.delete)}</Text>
|
|
||||||
</Button>
|
|
||||||
<Modal>
|
|
||||||
{close => (
|
|
||||||
<ConfirmDeleteForm
|
|
||||||
name={name}
|
|
||||||
onConfirm={handleConfirm.bind(null, id, close)}
|
|
||||||
onClose={close}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
</ModalTrigger>
|
|
||||||
)}
|
)}
|
||||||
|
<LinkButton href={`/reports/${id}`}>
|
||||||
|
<Icon>
|
||||||
|
<Icons.ArrowRight />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.view)}</Text>
|
||||||
|
</LinkButton>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
import { FormRow } from 'react-basics';
|
import { FormRow } from 'react-basics';
|
||||||
|
import { parseDateRange } from 'lib/date';
|
||||||
import DateFilter from 'components/input/DateFilter';
|
import DateFilter from 'components/input/DateFilter';
|
||||||
import WebsiteSelect from 'components/input/WebsiteSelect';
|
import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||||
import { parseDateRange } from 'lib/date';
|
|
||||||
import { useContext } from 'react';
|
|
||||||
import { ReportContext } from './Report';
|
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
|
import { ReportContext } from './Report';
|
||||||
|
|
||||||
export function BaseParameters({
|
export function BaseParameters({
|
||||||
showWebsiteSelect = true,
|
showWebsiteSelect = true,
|
@ -1,13 +1,13 @@
|
|||||||
import { useContext, useRef } from 'react';
|
import { useContext, useRef } from 'react';
|
||||||
import { useApi, useMessages } from 'components/hooks';
|
|
||||||
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
|
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
|
||||||
import { ReportContext } from '../Report';
|
|
||||||
import Empty from 'components/common/Empty';
|
import Empty from 'components/common/Empty';
|
||||||
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import FieldAddForm from '../FieldAddForm';
|
import { useApi, useMessages } from 'components/hooks';
|
||||||
import BaseParameters from '../BaseParameters';
|
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
||||||
import ParameterList from '../ParameterList';
|
import { ReportContext } from '../[id]/Report';
|
||||||
|
import FieldAddForm from '../[id]/FieldAddForm';
|
||||||
|
import ParameterList from '../[id]/ParameterList';
|
||||||
|
import BaseParameters from '../[id]/BaseParameters';
|
||||||
import styles from './EventDataParameters.module.css';
|
import styles from './EventDataParameters.module.css';
|
||||||
|
|
||||||
function useFields(websiteId, startDate, endDate) {
|
function useFields(websiteId, startDate, endDate) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Report from '../Report';
|
import Report from '../[id]/Report';
|
||||||
import ReportHeader from '../ReportHeader';
|
import ReportHeader from '../[id]/ReportHeader';
|
||||||
import ReportMenu from '../ReportMenu';
|
import ReportMenu from '../[id]/ReportMenu';
|
||||||
import ReportBody from '../ReportBody';
|
import ReportBody from '../[id]/ReportBody';
|
||||||
import EventDataParameters from './EventDataParameters';
|
import EventDataParameters from './EventDataParameters';
|
||||||
import EventDataTable from './EventDataTable';
|
import EventDataTable from './EventDataTable';
|
||||||
import Nodes from 'assets/nodes.svg';
|
import Nodes from 'assets/nodes.svg';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { GridTable, GridColumn } from 'react-basics';
|
import { GridTable, GridColumn } from 'react-basics';
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
import { ReportContext } from '../Report';
|
import { ReportContext } from '../[id]/Report';
|
||||||
|
|
||||||
export function EventDataTable() {
|
export function EventDataTable() {
|
||||||
const { report } = useContext(ReportContext);
|
const { report } = useContext(ReportContext);
|
||||||
|
@ -5,7 +5,7 @@ import useTheme from 'components/hooks/useTheme';
|
|||||||
import BarChart from 'components/metrics/BarChart';
|
import BarChart from 'components/metrics/BarChart';
|
||||||
import { formatLongNumber } from 'lib/format';
|
import { formatLongNumber } from 'lib/format';
|
||||||
import styles from './FunnelChart.module.css';
|
import styles from './FunnelChart.module.css';
|
||||||
import { ReportContext } from '../Report';
|
import { ReportContext } from '../[id]/Report';
|
||||||
|
|
||||||
export function FunnelChart({ className, loading }) {
|
export function FunnelChart({ className, loading }) {
|
||||||
const { report } = useContext(ReportContext);
|
const { report } = useContext(ReportContext);
|
||||||
|
@ -13,10 +13,10 @@ import {
|
|||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import UrlAddForm from './UrlAddForm';
|
import UrlAddForm from './UrlAddForm';
|
||||||
import { ReportContext } from '../Report';
|
import { ReportContext } from '../[id]/Report';
|
||||||
import BaseParameters from '../BaseParameters';
|
import BaseParameters from '../[id]/BaseParameters';
|
||||||
import ParameterList from '../ParameterList';
|
import ParameterList from '../[id]/ParameterList';
|
||||||
import PopupForm from '../PopupForm';
|
import PopupForm from '../[id]/PopupForm';
|
||||||
|
|
||||||
export function FunnelParameters() {
|
export function FunnelParameters() {
|
||||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
import FunnelChart from './FunnelChart';
|
import FunnelChart from './FunnelChart';
|
||||||
import FunnelTable from './FunnelTable';
|
import FunnelTable from './FunnelTable';
|
||||||
import FunnelParameters from './FunnelParameters';
|
import FunnelParameters from './FunnelParameters';
|
||||||
import Report from '../Report';
|
import Report from '../[id]/Report';
|
||||||
import ReportHeader from '../ReportHeader';
|
import ReportHeader from '../[id]/ReportHeader';
|
||||||
import ReportMenu from '../ReportMenu';
|
import ReportMenu from '../[id]/ReportMenu';
|
||||||
import ReportBody from '../ReportBody';
|
import ReportBody from '../[id]/ReportBody';
|
||||||
import Funnel from 'assets/funnel.svg';
|
import Funnel from 'assets/funnel.svg';
|
||||||
import { REPORT_TYPES } from 'lib/constants';
|
import { REPORT_TYPES } from 'lib/constants';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import ListTable from 'components/metrics/ListTable';
|
import ListTable from 'components/metrics/ListTable';
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
import { ReportContext } from '../Report';
|
import { ReportContext } from '../[id]/Report';
|
||||||
|
|
||||||
export function FunnelTable() {
|
export function FunnelTable() {
|
||||||
const { report } = useContext(ReportContext);
|
const { report } = useContext(ReportContext);
|
||||||
|
@ -10,14 +10,14 @@ import {
|
|||||||
Popup,
|
Popup,
|
||||||
TooltipPopup,
|
TooltipPopup,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import { ReportContext } from '../Report';
|
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import BaseParameters from '../BaseParameters';
|
import BaseParameters from '../[id]/BaseParameters';
|
||||||
import ParameterList from '../ParameterList';
|
import { ReportContext } from '../[id]/Report';
|
||||||
|
import ParameterList from '../[id]/ParameterList';
|
||||||
|
import FilterSelectForm from '../[id]/FilterSelectForm';
|
||||||
|
import FieldSelectForm from '../[id]/FieldSelectForm';
|
||||||
|
import PopupForm from '../[id]/PopupForm';
|
||||||
import styles from './InsightsParameters.module.css';
|
import styles from './InsightsParameters.module.css';
|
||||||
import PopupForm from '../PopupForm';
|
|
||||||
import FilterSelectForm from '../FilterSelectForm';
|
|
||||||
import FieldSelectForm from '../FieldSelectForm';
|
|
||||||
|
|
||||||
export function InsightsParameters() {
|
export function InsightsParameters() {
|
||||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import Report from '../Report';
|
import Report from '../[id]/Report';
|
||||||
import ReportHeader from '../ReportHeader';
|
import ReportHeader from '../[id]/ReportHeader';
|
||||||
import ReportMenu from '../ReportMenu';
|
import ReportMenu from '../[id]/ReportMenu';
|
||||||
import ReportBody from '../ReportBody';
|
import ReportBody from '../[id]/ReportBody';
|
||||||
import InsightsParameters from './InsightsParameters';
|
import InsightsParameters from './InsightsParameters';
|
||||||
import InsightsTable from './InsightsTable';
|
import InsightsTable from './InsightsTable';
|
||||||
import Lightbulb from 'assets/lightbulb.svg';
|
import Lightbulb from 'assets/lightbulb.svg';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
import { GridTable, GridColumn } from 'react-basics';
|
import { GridTable, GridColumn } from 'react-basics';
|
||||||
import { useFormat, useMessages } from 'components/hooks';
|
import { useFormat, useMessages } from 'components/hooks';
|
||||||
import { ReportContext } from '../Report';
|
import { ReportContext } from '../[id]/Report';
|
||||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
|
|
||||||
export function InsightsTable() {
|
export function InsightsTable() {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import ReportsHeader from './ReportsHeader';
|
import ReportsHeader from './ReportsHeader';
|
||||||
import ReportsList from './ReportsList';
|
import ReportsDataTable from './ReportsDataTable';
|
||||||
|
|
||||||
export default function ReportsPage() {
|
export default function ReportsPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ReportsHeader />
|
<ReportsHeader />
|
||||||
<ReportsList />
|
<ReportsDataTable />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { useContext, useRef } from 'react';
|
import { useContext, useRef } from 'react';
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics';
|
import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics';
|
||||||
import { ReportContext } from '../Report';
|
import { ReportContext } from '../[id]/Report';
|
||||||
import { MonthSelect } from 'components/input/MonthSelect';
|
import { MonthSelect } from 'components/input/MonthSelect';
|
||||||
import BaseParameters from '../BaseParameters';
|
import BaseParameters from '../[id]/BaseParameters';
|
||||||
import { parseDateRange } from 'lib/date';
|
import { parseDateRange } from 'lib/date';
|
||||||
|
|
||||||
export function RetentionParameters() {
|
export function RetentionParameters() {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import RetentionTable from './RetentionTable';
|
import RetentionTable from './RetentionTable';
|
||||||
import RetentionParameters from './RetentionParameters';
|
import RetentionParameters from './RetentionParameters';
|
||||||
import Report from '../Report';
|
import Report from '../[id]/Report';
|
||||||
import ReportHeader from '../ReportHeader';
|
import ReportHeader from '../[id]/ReportHeader';
|
||||||
import ReportMenu from '../ReportMenu';
|
import ReportMenu from '../[id]/ReportMenu';
|
||||||
import ReportBody from '../ReportBody';
|
import ReportBody from '../[id]/ReportBody';
|
||||||
import Magnet from 'assets/magnet.svg';
|
import Magnet from 'assets/magnet.svg';
|
||||||
import { REPORT_TYPES } from 'lib/constants';
|
import { REPORT_TYPES } from 'lib/constants';
|
||||||
import { parseDateRange } from 'lib/date';
|
import { parseDateRange } from 'lib/date';
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { ReportContext } from '../Report';
|
import { ReportContext } from '../[id]/Report';
|
||||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages, useLocale } from 'components/hooks';
|
||||||
import { useLocale } from 'components/hooks';
|
|
||||||
import { formatDate } from 'lib/date';
|
import { formatDate } from 'lib/date';
|
||||||
import styles from './RetentionTable.module.css';
|
import styles from './RetentionTable.module.css';
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
SubmitButton,
|
SubmitButton,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
|
import { setValue } from 'store/cache';
|
||||||
import useApi from 'components/hooks/useApi';
|
import useApi from 'components/hooks/useApi';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
|
|
||||||
@ -20,8 +21,9 @@ export function TeamAddForm({ onSave, onClose }) {
|
|||||||
const handleSubmit = async data => {
|
const handleSubmit = async data => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
onSave();
|
setValue('teams', Date.now());
|
||||||
onClose();
|
onSave?.();
|
||||||
|
onClose?.();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
|
import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
|
||||||
import useApi from 'components/hooks/useApi';
|
import useApi from 'components/hooks/useApi';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
|
import { setValue } from 'store/cache';
|
||||||
|
|
||||||
export function TeamDeleteForm({ teamId, teamName, onSave, onClose }) {
|
export function TeamDeleteForm({ teamId, teamName, onSave, onClose }) {
|
||||||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||||
@ -10,8 +11,9 @@ export function TeamDeleteForm({ teamId, teamName, onSave, onClose }) {
|
|||||||
const handleSubmit = async data => {
|
const handleSubmit = async data => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
onSave();
|
setValue('teams', Date.now());
|
||||||
onClose();
|
onSave?.();
|
||||||
|
onClose?.();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -25,16 +25,16 @@ export function TeamsTable({ data = [] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{showDelete && <TeamDeleteButton teamId={id} teamName={name} />}
|
||||||
|
{!showDelete && <TeamLeaveButton teamId={id} teamName={name} />}
|
||||||
<Link href={`/settings/teams/${id}`}>
|
<Link href={`/settings/teams/${id}`}>
|
||||||
<Button>
|
<Button>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Icons.Edit />
|
<Icons.Edit />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Text>{formatMessage(labels.view)}</Text>
|
<Text>{formatMessage(labels.edit)}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{showDelete && <TeamDeleteButton teamId={id} teamName={name} />}
|
|
||||||
{!showDelete && <TeamLeaveButton teamId={id} teamName={name} />}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -4,7 +4,7 @@ import { Button, Form, FormButtons, GridColumn, Loading, SubmitButton, Toggle }
|
|||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
import WebsitesDataTable from '../../websites/WebsitesDataTable';
|
import WebsitesDataTable from '../../websites/WebsitesDataTable';
|
||||||
|
|
||||||
export function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
|
export function TeamWebsiteAddForm({ teamId, onSave, onClose }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { get, post, useQuery, useMutation } = useApi();
|
const { get, post, useQuery, useMutation } = useApi();
|
||||||
const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data));
|
const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data));
|
||||||
@ -57,4 +57,4 @@ export function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TeamAddWebsiteForm;
|
export default TeamWebsiteAddForm;
|
@ -7,15 +7,12 @@ export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) {
|
|||||||
const { del, useMutation } = useApi();
|
const { del, useMutation } = useApi();
|
||||||
const { mutate, isLoading } = useMutation(() => del(`/teams/${teamId}/websites/${websiteId}`));
|
const { mutate, isLoading } = useMutation(() => del(`/teams/${teamId}/websites/${websiteId}`));
|
||||||
|
|
||||||
const handleRemoveTeamMember = () => {
|
const handleRemoveTeamMember = async () => {
|
||||||
mutate(
|
await mutate(null, {
|
||||||
{},
|
onSuccess: () => {
|
||||||
{
|
onSave();
|
||||||
onSuccess: () => {
|
|
||||||
onSave();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ActionForm, Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
|
import { ActionForm, Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
|
||||||
import TeamWebsitesTable from './TeamWebsitesTable';
|
import TeamWebsitesTable from './TeamWebsitesTable';
|
||||||
import TeamAddWebsiteForm from './TeamAddWebsiteForm';
|
import TeamWebsiteAddForm from './TeamWebsiteAddForm';
|
||||||
import useApi from 'components/hooks/useApi';
|
import useApi from 'components/hooks/useApi';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
import useUser from 'components/hooks/useUser';
|
import useUser from 'components/hooks/useUser';
|
||||||
@ -36,7 +36,7 @@ export function TeamWebsites({ teamId }) {
|
|||||||
<Text>{formatMessage(labels.addWebsite)}</Text>
|
<Text>{formatMessage(labels.addWebsite)}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
<Modal title={formatMessage(labels.addWebsite)}>
|
<Modal title={formatMessage(labels.addWebsite)}>
|
||||||
{close => <TeamAddWebsiteForm teamId={teamId} onSave={handleChange} onClose={close} />}
|
{close => <TeamWebsiteAddForm teamId={teamId} onSave={handleChange} onClose={close} />}
|
||||||
</Modal>
|
</Modal>
|
||||||
</ModalTrigger>
|
</ModalTrigger>
|
||||||
</ActionForm>
|
</ActionForm>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
|
import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
|
||||||
import WebsiteAddForm from './WebsiteAddForm';
|
import WebsiteAddForm from './WebsiteAddForm';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
|
import { setValue } from 'store/cache';
|
||||||
|
|
||||||
export function WebsiteAddButton({ onSave }) {
|
export function WebsiteAddButton({ onSave }) {
|
||||||
const { formatMessage, labels, messages } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
@ -8,6 +9,7 @@ export function WebsiteAddButton({ onSave }) {
|
|||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
|
setValue('websites', Date.now());
|
||||||
onSave?.();
|
onSave?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,13 +4,15 @@ import useUser from 'components/hooks/useUser';
|
|||||||
import useApi from 'components/hooks/useApi';
|
import useApi from 'components/hooks/useApi';
|
||||||
import DataTable from 'components/common/DataTable';
|
import DataTable from 'components/common/DataTable';
|
||||||
import useFilterQuery from 'components/hooks/useFilterQuery';
|
import useFilterQuery from 'components/hooks/useFilterQuery';
|
||||||
import WebsitesHeader from './WebsitesHeader';
|
import useCache from 'store/cache';
|
||||||
|
|
||||||
function useWebsites({ includeTeams, onlyTeams }) {
|
function useWebsites({ includeTeams, onlyTeams }) {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { get } = useApi();
|
const { get } = useApi();
|
||||||
|
const modified = useCache(state => state?.websites);
|
||||||
|
|
||||||
return useFilterQuery(
|
return useFilterQuery(
|
||||||
['websites', { includeTeams, onlyTeams }],
|
['websites', { includeTeams, onlyTeams, modified }],
|
||||||
params => {
|
params => {
|
||||||
return get(`/users/${user?.id}/websites`, {
|
return get(`/users/${user?.id}/websites`, {
|
||||||
includeTeams,
|
includeTeams,
|
||||||
@ -23,9 +25,8 @@ function useWebsites({ includeTeams, onlyTeams }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WebsitesDataTable({
|
export function WebsitesDataTable({
|
||||||
showHeader = true,
|
allowEdit = true,
|
||||||
showEditButton = true,
|
allowView = true,
|
||||||
showViewButton = true,
|
|
||||||
showActions = true,
|
showActions = true,
|
||||||
showTeam,
|
showTeam,
|
||||||
includeTeams,
|
includeTeams,
|
||||||
@ -35,22 +36,19 @@ export function WebsitesDataTable({
|
|||||||
const queryResult = useWebsites({ includeTeams, onlyTeams });
|
const queryResult = useWebsites({ includeTeams, onlyTeams });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DataTable queryResult={queryResult}>
|
||||||
{showHeader && <WebsitesHeader />}
|
{({ data }) => (
|
||||||
<DataTable queryResult={queryResult}>
|
<WebsitesTable
|
||||||
{({ data }) => (
|
data={data}
|
||||||
<WebsitesTable
|
showTeam={showTeam}
|
||||||
data={data}
|
showActions={showActions}
|
||||||
showTeam={showTeam}
|
allowEdit={allowEdit}
|
||||||
showActions={showActions}
|
allowView={allowView}
|
||||||
showEditButton={showEditButton}
|
>
|
||||||
showViewButton={showViewButton}
|
{children}
|
||||||
>
|
</WebsitesTable>
|
||||||
{children}
|
)}
|
||||||
</WebsitesTable>
|
</DataTable>
|
||||||
)}
|
|
||||||
</DataTable>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,8 +7,8 @@ export function WebsitesTable({
|
|||||||
data = [],
|
data = [],
|
||||||
showTeam,
|
showTeam,
|
||||||
showActions,
|
showActions,
|
||||||
showEditButton,
|
allowEdit,
|
||||||
showViewButton,
|
allowView,
|
||||||
children,
|
children,
|
||||||
}) {
|
}) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
@ -37,7 +37,7 @@ export function WebsitesTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showActions && showEditButton && (!showTeam || ownerId === user.id) && (
|
{showActions && allowEdit && (!showTeam || ownerId === user.id) && (
|
||||||
<Link href={`/settings/websites/${id}`}>
|
<Link href={`/settings/websites/${id}`}>
|
||||||
<Button>
|
<Button>
|
||||||
<Icon>
|
<Icon>
|
||||||
@ -47,7 +47,7 @@ export function WebsitesTable({
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{showActions && showViewButton && (
|
{showActions && allowView && (
|
||||||
<Link href={`/websites/${id}`}>
|
<Link href={`/websites/${id}`}>
|
||||||
<Button>
|
<Button>
|
||||||
<Icon>
|
<Icon>
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import WebsitesDataTable from './WebsitesDataTable';
|
import WebsitesDataTable from './WebsitesDataTable';
|
||||||
|
import WebsitesHeader from './WebsitesHeader';
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
if (process.env.cloudMode) {
|
if (process.env.cloudMode) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <WebsitesDataTable />;
|
return (
|
||||||
|
<>
|
||||||
|
<WebsitesHeader />
|
||||||
|
<WebsitesDataTable />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@ import { useApi, useDateRange, useMessages, useNavigation, useSticky } from 'com
|
|||||||
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
|
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
|
||||||
import MetricCard from 'components/metrics/MetricCard';
|
import MetricCard from 'components/metrics/MetricCard';
|
||||||
import MetricsBar from 'components/metrics/MetricsBar';
|
import MetricsBar from 'components/metrics/MetricsBar';
|
||||||
import FilterSelectForm from '../../../(main)/reports/FilterSelectForm';
|
import FilterSelectForm from 'app/(main)/reports/[id]/FilterSelectForm';
|
||||||
import PopupForm from '../../../(main)/reports/PopupForm';
|
import PopupForm from 'app/(main)/reports/[id]/PopupForm';
|
||||||
import { formatShortTime } from 'lib/format';
|
import { formatShortTime } from 'lib/format';
|
||||||
import { Button, Icon, Icons, Popup, PopupTrigger } from 'react-basics';
|
import { Button, Icon, Icons, Popup, PopupTrigger } from 'react-basics';
|
||||||
import styles from './WebsiteMetricsBar.module.css';
|
import styles from './WebsiteMetricsBar.module.css';
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { Flexbox } from 'react-basics';
|
import { Flexbox } from 'react-basics';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
|
|
||||||
export default function Custom404() {
|
export default function () {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Icon, Icons, Text } from 'react-basics';
|
|
||||||
import styles from './LinkButton.module.css';
|
|
||||||
import { useLocale } from 'components/hooks';
|
import { useLocale } from 'components/hooks';
|
||||||
|
import styles from './LinkButton.module.css';
|
||||||
|
|
||||||
export function LinkButton({ href, icon, className, children }) {
|
export function LinkButton({ href, className, children }) {
|
||||||
const { dir } = useLocale();
|
const { dir } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link className={classNames(styles.button, className)} href={href}>
|
<Link className={classNames(styles.button, className)} href={href} dir={dir}>
|
||||||
<Icon rotate={dir === 'rtl' ? 0 : 180}>{icon || <Icons.ArrowRight />}</Icon>
|
{children}
|
||||||
<Text>{children}</Text>
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--base50);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 1320px;
|
max-width: 1320px;
|
||||||
min-height: calc(100vh - 60px);
|
min-height: calc(100vh - 60px);
|
||||||
|
20
src/index.ts
20
src/index.ts
@ -22,29 +22,31 @@ export * from 'components/hooks/useUser';
|
|||||||
export * from 'components/hooks/useWebsite';
|
export * from 'components/hooks/useWebsite';
|
||||||
export * from 'components/hooks/useWebsiteReports';
|
export * from 'components/hooks/useWebsiteReports';
|
||||||
|
|
||||||
export * from 'app/(main)/settings/teams/TeamAddForm';
|
export * from './app/(main)/settings/teams/[id]/TeamWebsiteAddForm';
|
||||||
export * from 'app/(main)/settings/teams/[id]/TeamAddWebsiteForm';
|
|
||||||
export * from 'app/(main)/settings/teams/TeamDeleteForm';
|
|
||||||
export * from 'app/(main)/settings/teams/[id]/TeamEditForm';
|
export * from 'app/(main)/settings/teams/[id]/TeamEditForm';
|
||||||
export * from 'app/(main)/settings/teams/TeamJoinForm';
|
|
||||||
export * from 'app/(main)/settings/teams/TeamLeaveForm';
|
|
||||||
export * from 'app/(main)/settings/teams/[id]/TeamMemberRemoveButton';
|
export * from 'app/(main)/settings/teams/[id]/TeamMemberRemoveButton';
|
||||||
export * from 'app/(main)/settings/teams/[id]/TeamMembers';
|
export * from 'app/(main)/settings/teams/[id]/TeamMembers';
|
||||||
export * from 'app/(main)/settings/teams/[id]/TeamMembersTable';
|
export * from 'app/(main)/settings/teams/[id]/TeamMembersTable';
|
||||||
export * from 'app/(main)/settings/teams/[id]/TeamSettings';
|
export * from 'app/(main)/settings/teams/[id]/TeamSettings';
|
||||||
export * from 'app/(main)/settings/teams/TeamsDataTable';
|
|
||||||
export * from 'app/(main)/settings/teams/TeamsTable';
|
|
||||||
export * from 'app/(main)/settings/teams/[id]/TeamWebsiteRemoveButton';
|
export * from 'app/(main)/settings/teams/[id]/TeamWebsiteRemoveButton';
|
||||||
export * from 'app/(main)/settings/teams/[id]/TeamWebsites';
|
export * from 'app/(main)/settings/teams/[id]/TeamWebsites';
|
||||||
export * from 'app/(main)/settings/teams/[id]/TeamWebsitesTable';
|
export * from 'app/(main)/settings/teams/[id]/TeamWebsitesTable';
|
||||||
|
export * from 'app/(main)/settings/teams/TeamAddForm';
|
||||||
|
export * from 'app/(main)/settings/teams/TeamDeleteForm';
|
||||||
|
export * from 'app/(main)/settings/teams/TeamsHeader';
|
||||||
|
export * from 'app/(main)/settings/teams/TeamJoinForm';
|
||||||
|
export * from 'app/(main)/settings/teams/TeamLeaveForm';
|
||||||
|
export * from 'app/(main)/settings/teams/TeamsDataTable';
|
||||||
|
export * from 'app/(main)/settings/teams/TeamsTable';
|
||||||
export * from 'app/(main)/settings/teams/WebsiteTags';
|
export * from 'app/(main)/settings/teams/WebsiteTags';
|
||||||
|
|
||||||
export * from 'app/(main)/settings/websites/[id]/ShareUrl';
|
export * from 'app/(main)/settings/websites/[id]/ShareUrl';
|
||||||
export * from 'app/(main)/settings/websites/[id]/TrackingCode';
|
export * from 'app/(main)/settings/websites/[id]/TrackingCode';
|
||||||
export * from 'app/(main)/settings/websites/WebsiteAddForm';
|
|
||||||
export * from 'app/(main)/settings/websites/[id]/WebsiteDeleteForm';
|
export * from 'app/(main)/settings/websites/[id]/WebsiteDeleteForm';
|
||||||
export * from 'app/(main)/settings/websites/[id]/WebsiteEditForm';
|
export * from 'app/(main)/settings/websites/[id]/WebsiteEditForm';
|
||||||
export * from 'app/(main)/settings/websites/[id]/WebsiteResetForm';
|
export * from 'app/(main)/settings/websites/[id]/WebsiteResetForm';
|
||||||
|
export * from 'app/(main)/settings/websites/WebsiteAddForm';
|
||||||
|
export * from 'app/(main)/settings/websites/WebsitesHeader';
|
||||||
export * from 'app/(main)/settings/websites/WebsiteSettings';
|
export * from 'app/(main)/settings/websites/WebsiteSettings';
|
||||||
export * from 'app/(main)/settings/websites/WebsitesDataTable';
|
export * from './app/(main)/settings/websites/WebsitesDataTable';
|
||||||
export * from 'app/(main)/settings/websites/WebsitesTable';
|
export * from 'app/(main)/settings/websites/WebsitesTable';
|
||||||
|
9
src/store/cache.js
Normal file
9
src/store/cache.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
const store = create(() => ({}));
|
||||||
|
|
||||||
|
export function setValue(key, value) {
|
||||||
|
store.setState({ [key]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default store;
|
@ -1,13 +0,0 @@
|
|||||||
import { create } from 'zustand';
|
|
||||||
|
|
||||||
const store = create(() => ({}));
|
|
||||||
|
|
||||||
export function saveQuery(key, data) {
|
|
||||||
store.setState({ [key]: data });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getQuery(key) {
|
|
||||||
return store.getState()[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default store;
|
|
@ -4,10 +4,6 @@ import { DateRange } from 'lib/types';
|
|||||||
|
|
||||||
const store = create(() => ({}));
|
const store = create(() => ({}));
|
||||||
|
|
||||||
export function getWebsiteDateRange(websiteId: string) {
|
|
||||||
return store.getState()?.[websiteId];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setWebsiteDateRange(websiteId: string, dateRange: DateRange) {
|
export function setWebsiteDateRange(websiteId: string, dateRange: DateRange) {
|
||||||
store.setState(
|
store.setState(
|
||||||
produce(state => {
|
produce(state => {
|
||||||
|
Loading…
Reference in New Issue
Block a user