Updated add team website form.

This commit is contained in:
Mike Cao 2023-10-04 01:46:00 -07:00
parent c990459238
commit a14e11bae2
18 changed files with 149 additions and 129 deletions

View File

@ -7,30 +7,30 @@ import DataTable from 'components/common/DataTable';
function useReports() { function useReports() {
const { get, del, useMutation } = useApi(); const { get, del, useMutation } = useApi();
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`)); const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
const reports = useFilterQuery(['reports'], params => get(`/reports`, params)); const queryResult = useFilterQuery(['reports'], params => get(`/reports`, params));
const deleteReport = id => { const deleteReport = id => {
mutate(id, { mutate(id, {
onSuccess: () => { onSuccess: () => {
reports.refetch(); queryResult.refetch();
}, },
}); });
}; };
return { reports, deleteReport }; return { queryResult, deleteReport };
} }
export default function ReportsList() { export default function ReportsList() {
const { reports, deleteReport } = useReports(); const { queryResult, deleteReport } = useReports();
const handleDelete = async (id, callback) => { const handleDelete = async (id, callback) => {
await deleteReport(id); await deleteReport(id);
await reports.refetch(); await queryResult.refetch();
callback?.(); callback?.();
}; };
return ( return (
<DataTable {...reports.getProps()}> <DataTable queryResult={queryResult}>
{({ data }) => <ReportsTable data={data} showDomain={true} onDelete={handleDelete} />} {({ data }) => <ReportsTable data={data} showDomain={true} onDelete={handleDelete} />}
</DataTable> </DataTable>
); );

View File

@ -4,16 +4,21 @@ import TeamsTable from 'app/(main)/settings/teams/TeamsTable';
import useApi from 'components/hooks/useApi'; import useApi from 'components/hooks/useApi';
import useFilterQuery from 'components/hooks/useFilterQuery'; import useFilterQuery from 'components/hooks/useFilterQuery';
export function TeamsList() { export function TeamsDataTable() {
const { get } = useApi(); const { get } = useApi();
const filterQuery = useFilterQuery(['teams'], params => { const queryResult = useFilterQuery(['teams'], params => {
return get(`/teams`, { return get(`/teams`, {
...params, ...params,
}); });
}); });
const { getProps } = filterQuery;
return <DataTable {...getProps()}>{({ data }) => <TeamsTable data={data} />}</DataTable>; return (
<DataTable queryResult={queryResult}>
{({ data }) => {
return <TeamsTable data={data} />;
}}
</DataTable>
);
} }
export default TeamsList; export default TeamsDataTable;

View File

@ -1,22 +1,20 @@
import useApi from 'components/hooks/useApi'; import useApi from 'components/hooks/useApi';
import { useRef, useState } from 'react'; import { useState } from 'react';
import { Button, Dropdown, Form, FormButtons, FormRow, Item, SubmitButton } from 'react-basics'; import { Button, Form, FormButtons, GridColumn, Loading, SubmitButton, Toggle } from 'react-basics';
import WebsiteTags from '../WebsiteTags';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import WebsitesDataTable from '../../websites/WebsitesDataTable';
export function TeamAddWebsiteForm({ teamId, onSave, onClose }) { export function TeamAddWebsiteForm({ 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));
const { data: websites } = useQuery(['websites'], () => get('/websites')); const { data: websites } = useQuery(['websites'], () => get('/websites'));
const [newWebsites, setNewWebsites] = useState([]); const [selected, setSelected] = useState([]);
const formRef = useRef();
const hasData = websites && websites.data.length > 0; const hasData = websites && websites.data.length > 0;
const handleSubmit = () => { const handleSubmit = () => {
mutate( mutate(
{ websiteIds: newWebsites }, { websiteIds: selected },
{ {
onSuccess: async () => { onSuccess: async () => {
onSave(); onSave();
@ -26,34 +24,29 @@ export function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
); );
}; };
const handleAddWebsite = value => { const handleSelect = id => {
if (!newWebsites.some(a => a === value)) { setSelected(state => (state.includes(id) ? state.filter(n => n !== id) : state.concat(id)));
const nextValue = [...newWebsites];
nextValue.push(value);
setNewWebsites(nextValue);
}
};
const handleRemoveWebsite = value => {
const newValue = newWebsites.filter(a => a !== value);
setNewWebsites(newValue);
}; };
return ( return (
<> <>
{!hasData && <Loading />}
{hasData && ( {hasData && (
<Form onSubmit={handleSubmit} error={error} ref={formRef}> <Form onSubmit={handleSubmit} error={error}>
<FormRow label={formatMessage(labels.websites)}> <WebsitesDataTable showHeader={false} showActions={false}>
<Dropdown items={websites.data} onChange={handleAddWebsite} style={{ width: 300 }}> <GridColumn name="select" label={formatMessage(labels.selectWebsite)} alignment="end">
{({ id, name }) => <Item key={id}>{name}</Item>} {row => (
</Dropdown> <Toggle
</FormRow> key={row.id}
<WebsiteTags items={websites.data} websites={newWebsites} onClick={handleRemoveWebsite} /> value={row.id}
checked={selected?.includes(row.id)}
onChange={handleSelect.bind(null, row.id)}
/>
)}
</GridColumn>
</WebsitesDataTable>
<FormButtons flex> <FormButtons flex>
<SubmitButton disabled={newWebsites && newWebsites.length === 0}> <SubmitButton disabled={selected?.length === 0}>
{formatMessage(labels.addWebsite)} {formatMessage(labels.addWebsite)}
</SubmitButton> </SubmitButton>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button> <Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>

View File

@ -5,7 +5,7 @@ import DataTable from 'components/common/DataTable';
export function TeamMembers({ teamId, readOnly }) { export function TeamMembers({ teamId, readOnly }) {
const { get } = useApi(); const { get } = useApi();
const { getProps } = useFilterQuery( const queryResult = useFilterQuery(
['team:users', teamId], ['team:users', teamId],
params => { params => {
return get(`/teams/${teamId}/users`, { return get(`/teams/${teamId}/users`, {
@ -17,7 +17,7 @@ export function TeamMembers({ teamId, readOnly }) {
return ( return (
<> <>
<DataTable {...getProps()}> <DataTable queryResult={queryResult}>
{({ data }) => <TeamMembersTable data={data} readOnly={readOnly} />} {({ data }) => <TeamMembersTable data={data} readOnly={readOnly} />}
</DataTable> </DataTable>
</> </>

View File

@ -19,7 +19,7 @@ export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) {
}; };
return ( return (
<LoadingButton onClick={() => handleRemoveTeamMember()} isLoading={isLoading}> <LoadingButton variant="quiet" onClick={() => handleRemoveTeamMember()} isLoading={isLoading}>
<Icon> <Icon>
<Icons.Close /> <Icons.Close />
</Icon> </Icon>

View File

@ -11,7 +11,7 @@ export function TeamWebsites({ teamId }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { user } = useUser(); const { user } = useUser();
const { get } = useApi(); const { get } = useApi();
const { getProps, refetch } = useFilterQuery( const queryResult = useFilterQuery(
['team:websites', teamId], ['team:websites', teamId],
params => { params => {
return get(`/teams/${teamId}/websites`, { return get(`/teams/${teamId}/websites`, {
@ -21,8 +21,8 @@ export function TeamWebsites({ teamId }) {
{ enabled: !!user }, { enabled: !!user },
); );
const handleWebsiteAdd = () => { const handleChange = () => {
refetch(); queryResult.refetch();
}; };
return ( return (
@ -36,13 +36,13 @@ 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 => ( {close => <TeamAddWebsiteForm teamId={teamId} onSave={handleChange} onClose={close} />}
<TeamAddWebsiteForm teamId={teamId} onSave={handleWebsiteAdd} onClose={close} />
)}
</Modal> </Modal>
</ModalTrigger> </ModalTrigger>
</ActionForm> </ActionForm>
<DataTable {...getProps()}>{({ data }) => <TeamWebsitesTable data={data} />}</DataTable> <DataTable queryResult={queryResult}>
{({ data }) => <TeamWebsitesTable data={data} onRemove={handleChange} />}
</DataTable>
</> </>
); );
} }

View File

@ -1,10 +1,10 @@
import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser';
import Link from 'next/link'; import Link from 'next/link';
import { Button, GridColumn, GridTable, Icon, Icons, Text } from 'react-basics'; import { Button, GridColumn, GridTable, Icon, Icons, Text } from 'react-basics';
import TeamWebsiteRemoveButton from '../TeamWebsiteRemoveButton'; import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser';
import TeamWebsiteRemoveButton from './TeamWebsiteRemoveButton';
export function TeamWebsitesTable({ data = [], onSave }) { export function TeamWebsitesTable({ data = [], onRemove }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { user } = useUser(); const { user } = useUser();
@ -20,6 +20,9 @@ export function TeamWebsitesTable({ data = [], onSave }) {
const canRemove = user.id === userId || user.id === owner.userId; const canRemove = user.id === userId || user.id === owner.userId;
return ( return (
<> <>
{canRemove && (
<TeamWebsiteRemoveButton teamId={teamId} websiteId={websiteId} onSave={onRemove} />
)}
<Link href={`/websites/${websiteId}`}> <Link href={`/websites/${websiteId}`}>
<Button> <Button>
<Icon> <Icon>
@ -28,9 +31,6 @@ export function TeamWebsitesTable({ data = [], onSave }) {
<Text>{formatMessage(labels.view)}</Text> <Text>{formatMessage(labels.view)}</Text>
</Button> </Button>
</Link> </Link>
{canRemove && (
<TeamWebsiteRemoveButton teamId={teamId} websiteId={websiteId} onSave={onSave} />
)}
</> </>
); );
}} }}

View File

@ -1,4 +1,4 @@
import TeamsList from 'app/(main)/settings/teams/TeamsList'; import TeamsDataTable from './TeamsDataTable';
import TeamsHeader from './TeamsHeader'; import TeamsHeader from './TeamsHeader';
export default function () { export default function () {
@ -9,7 +9,7 @@ export default function () {
return ( return (
<> <>
<TeamsHeader /> <TeamsHeader />
<TeamsList /> <TeamsDataTable />
</> </>
); );
} }

View File

@ -5,21 +5,20 @@ import DataTable from 'components/common/DataTable';
import UsersTable from './UsersTable'; import UsersTable from './UsersTable';
import UsersHeader from './UsersHeader'; import UsersHeader from './UsersHeader';
export function UsersList() { export function UsersDataTable() {
const { get } = useApi(); const { get } = useApi();
const filterQuery = useFilterQuery(['users'], params => { const queryResult = useFilterQuery(['users'], params => {
return get(`/users`, { return get(`/users`, {
...params, ...params,
}); });
}); });
const { getProps } = filterQuery;
return ( return (
<> <>
<UsersHeader /> <UsersHeader />
<DataTable {...getProps()}>{({ data }) => <UsersTable data={data} />}</DataTable> <DataTable queryResult={queryResult}>{({ data }) => <UsersTable data={data} />}</DataTable>
</> </>
); );
} }
export default UsersList; export default UsersDataTable;

View File

@ -1,4 +1,4 @@
import UsersList from 'app/(main)/settings/users/UsersList'; import UsersDataTable from './UsersDataTable';
import { Metadata } from 'next'; import { Metadata } from 'next';
export default function () { export default function () {
@ -6,7 +6,7 @@ export default function () {
return null; return null;
} }
return <UsersList />; return <UsersDataTable />;
} }
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Users | umami', title: 'Users | umami',

View File

@ -6,16 +6,10 @@ import DataTable from 'components/common/DataTable';
import useFilterQuery from 'components/hooks/useFilterQuery'; import useFilterQuery from 'components/hooks/useFilterQuery';
import WebsitesHeader from './WebsitesHeader'; import WebsitesHeader from './WebsitesHeader';
export function Websites({ function useWebsites({ includeTeams, onlyTeams }) {
showHeader = true,
showEditButton = true,
showTeam,
includeTeams,
onlyTeams,
}) {
const { user } = useUser(); const { user } = useUser();
const { get } = useApi(); const { get } = useApi();
const filterQuery = useFilterQuery( return useFilterQuery(
['websites', { includeTeams, onlyTeams }], ['websites', { includeTeams, onlyTeams }],
params => { params => {
return get(`/users/${user?.id}/websites`, { return get(`/users/${user?.id}/websites`, {
@ -26,18 +20,38 @@ export function Websites({
}, },
{ enabled: !!user }, { enabled: !!user },
); );
const { getProps } = filterQuery; }
export function WebsitesDataTable({
showHeader = true,
showEditButton = true,
showViewButton = true,
showActions = true,
showTeam,
includeTeams,
onlyTeams,
children,
}) {
const queryResult = useWebsites({ includeTeams, onlyTeams });
return ( return (
<> <>
{showHeader && <WebsitesHeader />} {showHeader && <WebsitesHeader />}
<DataTable {...getProps()}> <DataTable queryResult={queryResult}>
{({ data }) => ( {({ data }) => (
<WebsitesTable data={data} showTeam={showTeam} showEditButton={showEditButton} /> <WebsitesTable
data={data}
showTeam={showTeam}
showActions={showActions}
showEditButton={showEditButton}
showViewButton={showViewButton}
>
{children}
</WebsitesTable>
)} )}
</DataTable> </DataTable>
</> </>
); );
} }
export default Websites; export default WebsitesDataTable;

View File

@ -3,7 +3,14 @@ import { Button, Text, Icon, Icons, GridTable, GridColumn } from 'react-basics';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser'; import useUser from 'components/hooks/useUser';
export function WebsitesTable({ data = [], showTeam, showEditButton }) { export function WebsitesTable({
data = [],
showTeam,
showActions,
showEditButton,
showViewButton,
children,
}) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { user } = useUser(); const { user } = useUser();
@ -30,7 +37,7 @@ export function WebsitesTable({ data = [], showTeam, showEditButton }) {
return ( return (
<> <>
{showEditButton && (!showTeam || ownerId === user.id) && ( {showActions && showEditButton && (!showTeam || ownerId === user.id) && (
<Link href={`/settings/websites/${id}`}> <Link href={`/settings/websites/${id}`}>
<Button> <Button>
<Icon> <Icon>
@ -40,18 +47,21 @@ export function WebsitesTable({ data = [], showTeam, showEditButton }) {
</Button> </Button>
</Link> </Link>
)} )}
<Link href={`/websites/${id}`}> {showActions && showViewButton && (
<Button> <Link href={`/websites/${id}`}>
<Icon> <Button>
<Icons.External /> <Icon>
</Icon> <Icons.External />
<Text>{formatMessage(labels.view)}</Text> </Icon>
</Button> <Text>{formatMessage(labels.view)}</Text>
</Link> </Button>
</Link>
)}
</> </>
); );
}} }}
</GridColumn> </GridColumn>
{children}
</GridTable> </GridTable>
); );
} }

View File

@ -1,9 +1,9 @@
import Websites from './Websites'; import WebsitesDataTable from './WebsitesDataTable';
export default function () { export default function () {
if (process.env.cloudMode) { if (process.env.cloudMode) {
return null; return null;
} }
return <Websites />; return <WebsitesDataTable />;
} }

View File

@ -1,5 +1,5 @@
'use client'; 'use client';
import WebsiteList from '../../(main)/settings/websites/Websites'; import WebsiteList from '../settings/websites/WebsitesDataTable';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
import { useState } from 'react'; import { useState } from 'react';
import { Item, Tabs } from 'react-basics'; import { Item, Tabs } from 'react-basics';

View File

@ -9,19 +9,21 @@ import styles from './DataTable.module.css';
const DEFAULT_SEARCH_DELAY = 600; const DEFAULT_SEARCH_DELAY = 600;
export interface DataTableProps { export interface DataTableProps {
result: { queryResult: {
page: number; result: {
pageSize: number; page: number;
count: number; pageSize: number;
data: any[]; count: number;
data: any[];
};
params: {
query: string;
page: number;
};
setParams: Dispatch<SetStateAction<{ query: string; page: number }>>;
isLoading: boolean;
error: unknown;
}; };
params: {
query: string;
page: number;
};
setParams: Dispatch<SetStateAction<{ query: string; page: number }>>;
isLoading: boolean;
error: unknown;
searchDelay?: number; searchDelay?: number;
showSearch?: boolean; showSearch?: boolean;
showPaging?: boolean; showPaging?: boolean;
@ -29,20 +31,17 @@ export interface DataTableProps {
} }
export function DataTable({ export function DataTable({
result, queryResult,
params, searchDelay = 600,
setParams,
isLoading,
error,
searchDelay,
showSearch = true, showSearch = true,
showPaging = true, showPaging = true,
children, children,
}: DataTableProps) { }: DataTableProps) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { pageSize, count } = result || {}; const { result, error, isLoading, params, setParams } = queryResult || {};
const { query, page } = params || {}; const { page, pageSize, count, data } = result || {};
const hasData = Boolean(!isLoading && result?.data?.length); const { query } = params || {};
const hasData = Boolean(!isLoading && data?.length);
const noResults = Boolean(!isLoading && query && !hasData); const noResults = Boolean(!isLoading && query && !hasData);
const handleSearch = query => { const handleSearch = query => {

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react'; import { useState } from 'react';
import { useApi } from 'components/hooks/useApi'; import { useApi } from 'components/hooks/useApi';
export function useFilterQuery(key: any[], fn, options?: any) { export function useFilterQuery(key: any[], fn, options?: any) {
@ -8,19 +8,19 @@ export function useFilterQuery(key: any[], fn, options?: any) {
}); });
const { useQuery } = useApi(); const { useQuery } = useApi();
const result = useQuery<{ const { data, ...other } = useQuery([...key, params], fn.bind(null, params), options);
page: number;
pageSize: number;
count: number;
data: any[];
}>([...key, params], fn.bind(null, params), options);
const getProps = useCallback(() => { return {
const { data, isLoading, error } = result; result: data as {
return { result: data, isLoading, error, params, setParams }; page: number;
}, [result, params, setParams]); pageSize: number;
count: number;
return { ...result, getProps }; data: any[];
},
...other,
params,
setParams,
};
} }
export default useFilterQuery; export default useFilterQuery;

View File

@ -32,9 +32,9 @@ 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/TeamsList'; export * from 'app/(main)/settings/teams/TeamsDataTable';
export * from 'app/(main)/settings/teams/TeamsTable'; export * from 'app/(main)/settings/teams/TeamsTable';
export * from 'app/(main)/settings/teams/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/WebsiteTags'; export * from 'app/(main)/settings/teams/WebsiteTags';
@ -46,5 +46,5 @@ 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/WebsiteSettings'; export * from 'app/(main)/settings/websites/WebsiteSettings';
export * from 'app/(main)/settings/websites/Websites'; export * from 'app/(main)/settings/websites/WebsitesDataTable';
export * from 'app/(main)/settings/websites/WebsitesTable'; export * from 'app/(main)/settings/websites/WebsitesTable';

View File

@ -88,7 +88,7 @@ export const useValidate = async (schema, req, res) => {
const rules = schema[req.method]; const rules = schema[req.method];
if (rules) { if (rules) {
rules.validateSync(req.method === 'GET' ? { ...req.query } : { ...req.body }); rules.validateSync({ ...req.query, ...req.body });
} }
} catch (e: any) { } catch (e: any) {
return badRequest(res, e.message); return badRequest(res, e.message);