Replaced SettingsTable with DataTable.

This commit is contained in:
Mike Cao 2023-10-01 16:11:12 -07:00
parent 0d9b6e8355
commit 9bb89c7e8b
24 changed files with 134 additions and 381 deletions

View File

@ -1,3 +1,4 @@
'use client';
import { useState, useMemo } from 'react';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import classNames from 'classnames';
@ -7,7 +8,6 @@ import useDashboard, { saveDashboard } from 'store/dashboard';
import useMessages from 'components/hooks/useMessages';
import useApi from 'components/hooks/useApi';
import styles from './DashboardEdit.module.css';
import Page from 'components/layout/Page';
const dragId = 'dashboard-website-ordering';
@ -17,11 +17,7 @@ export function DashboardEdit() {
const { formatMessage, labels } = useMessages();
const [order, setOrder] = useState(websiteOrder || []);
const { get, useQuery } = useApi();
const {
data: result,
isLoading,
error,
} = useQuery(['websites'], () => get('/websites', { includeTeams: 1 }));
const { data: result } = useQuery(['websites'], () => get('/websites', { includeTeams: 1 }));
const { data: websites } = result || {};
const ordered = useMemo(() => {
@ -59,7 +55,7 @@ export function DashboardEdit() {
}
return (
<Page loading={isLoading} error={error}>
<>
<div className={styles.buttons}>
<Button onClick={handleSave} variant="action" size="small">
{formatMessage(labels.save)}
@ -105,7 +101,7 @@ export function DashboardEdit() {
</Droppable>
</DragDropContext>
</div>
</Page>
</>
);
}

View File

@ -1,4 +1,3 @@
'use client';
import Shell from './Shell';
import NavBar from './NavBar';
import Page from 'components/layout/Page';

View File

@ -4,7 +4,6 @@ import { useMessages } from 'components/hooks';
import useUser from 'components/hooks/useUser';
import {
Button,
Flexbox,
GridColumn,
GridTable,
Icon,
@ -43,7 +42,7 @@ export function ReportsTable({ data = [], onDelete, showDomain }) {
{row => {
const { id, name, userId, website } = row;
return (
<Flexbox gap={10}>
<>
<LinkButton href={`/reports/${id}`}>{formatMessage(labels.view)}</LinkButton>
{(user.id === userId || user.id === website?.userId) && (
<ModalTrigger>
@ -64,7 +63,7 @@ export function ReportsTable({ data = [], onDelete, showDomain }) {
</Modal>
</ModalTrigger>
)}
</Flexbox>
</>
);
}}
</GridColumn>

View File

@ -11,6 +11,8 @@
}
.content {
display: flex;
flex-direction: column;
min-height: 50vh;
}

View File

@ -3,7 +3,7 @@ import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser';
import { ROLES } from 'lib/constants';
import Link from 'next/link';
import { Button, Flexbox, GridColumn, GridTable, Icon, Icons, Text } from 'react-basics';
import { Button, GridColumn, GridTable, Icon, Icons, Text } from 'react-basics';
import TeamDeleteButton from './TeamDeleteButton';
import TeamLeaveButton from './TeamLeaveButton';
@ -24,7 +24,7 @@ export function TeamsTable({ data = [] }) {
const showDelete = user.id === owner?.userId;
return (
<Flexbox gap={10}>
<>
<Link href={`/settings/teams/${id}`}>
<Button>
<Icon>
@ -35,7 +35,7 @@ export function TeamsTable({ data = [] }) {
</Link>
{showDelete && <TeamDeleteButton teamId={id} teamName={name} />}
{!showDelete && <TeamLeaveButton teamId={id} teamName={name} />}
</Flexbox>
</>
);
}}
</GridColumn>

View File

@ -1,46 +1,25 @@
import { Loading, useToasts } from 'react-basics';
import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages';
import useApiFilter from 'components/hooks/useApiFilter';
import TeamMembersTable from './TeamMembersTable';
import useFilterQuery from 'components/hooks/useFilterQuery';
import DataTable from 'components/common/DataTable';
export function TeamMembers({ teamId, readOnly }) {
const { showToast } = useToasts();
const { formatMessage, messages } = useMessages();
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
useApiFilter();
const { get, useQuery } = useApi();
const { data, isLoading, refetch } = useQuery(
['teams:users', teamId, filter, page, pageSize],
() =>
get(`/teams/${teamId}/users`, {
filter,
page,
pageSize,
}),
const { get } = useApi();
const { getProps } = useFilterQuery(
['team:users', teamId],
params => {
return get(`/teams/${teamId}/users`, {
...params,
});
},
{ enabled: !!teamId },
);
if (isLoading) {
return <Loading icon="dots" style={{ minHeight: 300 }} />;
}
const handleSave = async () => {
await refetch();
showToast({ message: formatMessage(messages.saved), variant: 'success' });
};
return (
<>
<TeamMembersTable
onSave={handleSave}
teamId={teamId}
data={data}
readOnly={readOnly}
onFilterChange={handleFilterChange}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
filterValue={filter}
/>
<DataTable {...getProps()}>
{({ data }) => <TeamMembersTable data={data} readOnly={readOnly} />}
</DataTable>
</>
);
}

View File

@ -1,67 +1,36 @@
import { GridColumn, GridTable } from 'react-basics';
import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser';
import { ROLES } from 'lib/constants';
import TeamMemberRemoveButton from './TeamMemberRemoveButton';
import SettingsTable from 'components/common/SettingsTable';
export function TeamMembersTable({
data = [],
teamId,
onSave,
readOnly,
filterValue,
onFilterChange,
onPageChange,
onPageSizeChange,
}) {
export function TeamMembersTable({ data = [], teamId, readOnly, onChange }) {
const { formatMessage, labels } = useMessages();
const { user } = useUser();
const columns = [
{ name: 'username', label: formatMessage(labels.username) },
{ name: 'role', label: formatMessage(labels.role) },
{ name: 'action', label: ' ' },
];
const cellRender = (row, data, key) => {
if (key === 'username') {
return row?.username;
}
if (key === 'role') {
return formatMessage(
labels[
Object.keys(ROLES).find(key => ROLES[key] === row?.teamUser[0]?.role) || labels.unknown
],
);
}
return data[key];
const roles = {
[ROLES.teamOwner]: formatMessage(labels.teamOwner),
[ROLES.teamMember]: formatMessage(labels.teamMember),
};
return (
<SettingsTable
data={data}
columns={columns}
cellRender={cellRender}
showSearch={true}
showPaging={true}
onFilterChange={onFilterChange}
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
filterValue={filterValue}
>
{row => {
return (
!readOnly && (
<TeamMemberRemoveButton
teamId={teamId}
userId={row.id}
disabled={user.id === row?.user?.id || row.role === ROLES.teamOwner}
onSave={onSave}
/>
)
);
}}
</SettingsTable>
<GridTable data={data}>
<GridColumn name="username" label={formatMessage(labels.username)} />
<GridColumn name="role" label={formatMessage(labels.role)}>
{row => roles[row?.teamUser?.[0]?.role]}
</GridColumn>
<GridColumn name="action" label=" " alignment="end">
{row => {
return (
!readOnly &&
row?.teamUser?.[0]?.role !== ROLES.teamOwner &&
user?.id !== row?.id && (
<TeamMemberRemoveButton teamId={teamId} userId={row.id} onSave={onChange} />
)
);
}}
</GridColumn>
</GridTable>
);
}

View File

@ -1,6 +1,6 @@
'use client';
import { useEffect, useState } from 'react';
import { Item, Loading, Tabs, useToasts } from 'react-basics';
import { Item, Loading, Tabs, useToasts, Flexbox } from 'react-basics';
import PageHeader from 'components/layout/PageHeader';
import { ROLES } from 'lib/constants';
import useUser from 'components/hooks/useUser';
@ -46,7 +46,7 @@ export function TeamSettings({ teamId }) {
}
return (
<>
<Flexbox direction="column">
<PageHeader title={values?.name} />
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
<Item key="details">{formatMessage(labels.details)}</Item>
@ -58,7 +58,7 @@ export function TeamSettings({ teamId }) {
)}
{tab === 'members' && <TeamMembers teamId={teamId} readOnly={!canEdit} />}
{tab === 'websites' && <TeamWebsites teamId={teamId} readOnly={!canEdit} />}
</>
</Flexbox>
);
}

View File

@ -1,75 +1,49 @@
import {
ActionForm,
Button,
Icon,
Icons,
Loading,
Modal,
ModalTrigger,
Text,
useToasts,
} from 'react-basics';
import { ActionForm, Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
import TeamWebsitesTable from './TeamWebsitesTable';
import TeamAddWebsiteForm from './TeamAddWebsiteForm';
import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages';
import useApiFilter from 'components/hooks/useApiFilter';
import useUser from 'components/hooks/useUser';
import useFilterQuery from 'components/hooks/useFilterQuery';
import DataTable from 'components/common/DataTable';
export function TeamWebsites({ teamId }) {
const { showToast } = useToasts();
const { formatMessage, labels, messages } = useMessages();
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
useApiFilter();
const { get, useQuery } = useApi();
const { data, isLoading, refetch } = useQuery(
['teams:websites', teamId, filter, page, pageSize],
() =>
get(`/teams/${teamId}/websites`, {
filter,
page,
pageSize,
}),
const { user } = useUser();
const { get } = useApi();
const { getProps, refetch } = useFilterQuery(
['team:websites', teamId],
params => {
return get(`/teams/${teamId}/websites`, {
...params,
});
},
{ enabled: !!user },
);
const hasData = data && data.length !== 0;
if (isLoading) {
return <Loading icon="dots" style={{ minHeight: 300 }} />;
}
const handleSave = async () => {
await refetch();
showToast({ message: formatMessage(messages.saved), variant: 'success' });
const handleWebsiteAdd = () => {
refetch();
};
const addButton = (
<ModalTrigger>
<Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.addWebsite)}</Text>
</Button>
<Modal title={formatMessage(labels.addWebsite)}>
{close => <TeamAddWebsiteForm teamId={teamId} onSave={handleSave} onClose={close} />}
</Modal>
</ModalTrigger>
);
return (
<div>
<ActionForm description={formatMessage(messages.teamWebsitesInfo)}>{addButton}</ActionForm>
{hasData && (
<TeamWebsitesTable
teamId={teamId}
data={data}
onSave={handleSave}
onFilterChange={handleFilterChange}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
filterValue={filter}
/>
)}
</div>
<>
<ActionForm description={formatMessage(messages.teamWebsitesInfo)}>
<ModalTrigger>
<Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.addWebsite)}</Text>
</Button>
<Modal title={formatMessage(labels.addWebsite)}>
{close => (
<TeamAddWebsiteForm teamId={teamId} onSave={handleWebsiteAdd} onClose={close} />
)}
</Modal>
</ModalTrigger>
</ActionForm>
<DataTable {...getProps()}>{({ data }) => <TeamWebsitesTable data={data} />}</DataTable>
</>
);
}

View File

@ -1,65 +1,41 @@
import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser';
import Link from 'next/link';
import { Button, Icon, Icons, Text } from 'react-basics';
import { Button, GridColumn, GridTable, Icon, Icons, Text } from 'react-basics';
import TeamWebsiteRemoveButton from '../TeamWebsiteRemoveButton';
import SettingsTable from 'components/common/SettingsTable';
export function TeamWebsitesTable({
data = [],
onSave,
filterValue,
onFilterChange,
onPageChange,
onPageSizeChange,
openExternal = false,
}) {
export function TeamWebsitesTable({ data = [], onSave }) {
const { formatMessage, labels } = useMessages();
const { user } = useUser();
const columns = [
{ name: 'name', label: formatMessage(labels.name) },
{ name: 'domain', label: formatMessage(labels.domain) },
{ name: 'action', label: ' ' },
];
return (
<SettingsTable
columns={columns}
data={data}
showSearch={true}
showPaging={true}
onFilterChange={onFilterChange}
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
filterValue={filterValue}
>
{row => {
const { id: teamId, teamUser } = row.teamWebsite[0].team;
const { id: websiteId, name, domain, userId } = row;
const owner = teamUser[0];
const canRemove = user.id === userId || user.id === owner.userId;
row.name = name;
row.domain = domain;
return (
<>
<Link href={`/websites/${websiteId}`} target={openExternal ? '_blank' : null}>
<Button>
<Icon>
<Icons.External />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Button>
</Link>
{canRemove && (
<TeamWebsiteRemoveButton teamId={teamId} websiteId={websiteId} onSave={onSave} />
)}
</>
);
}}
</SettingsTable>
<GridTable data={data}>
<GridColumn name="name" label={formatMessage(labels.name)} />
<GridColumn name="domain" label={formatMessage(labels.domain)} />
<GridColumn name="action" label=" " alignment="end">
{row => {
const { id: teamId, teamUser } = row.teamWebsite[0].team;
const { id: websiteId, userId } = row;
const owner = teamUser[0];
const canRemove = user.id === userId || user.id === owner.userId;
return (
<>
<Link href={`/websites/${websiteId}`}>
<Button>
<Icon>
<Icons.External />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Button>
</Link>
{canRemove && (
<TeamWebsiteRemoveButton teamId={teamId} websiteId={websiteId} onSave={onSave} />
)}
</>
);
}}
</GridColumn>
</GridTable>
);
}

View File

@ -1,4 +1,4 @@
import { Button, Text, Icon, Icons, GridTable, GridColumn, Flexbox } from 'react-basics';
import { Button, Text, Icon, Icons, GridTable, GridColumn } from 'react-basics';
import { formatDistance } from 'date-fns';
import Link from 'next/link';
import { ROLES } from 'lib/constants';
@ -36,7 +36,7 @@ export function UsersTable({ data = [] }) {
{row => {
const { id, username } = row;
return (
<Flexbox gap={10}>
<>
<Link href={`/settings/users/${id}`}>
<Button>
<Icon>
@ -46,7 +46,7 @@ export function UsersTable({ data = [] }) {
</Button>
</Link>
<UserDeleteButton userId={id} username={username} />
</Flexbox>
</>
);
}}
</GridColumn>

View File

@ -6,7 +6,7 @@ import DataTable from 'components/common/DataTable';
import useFilterQuery from 'components/hooks/useFilterQuery';
import WebsitesHeader from './WebsitesHeader';
export function WebsitesList({
export function Websites({
showHeader = true,
showEditButton = true,
showTeam,
@ -40,4 +40,4 @@ export function WebsitesList({
);
}
export default WebsitesList;
export default Websites;

View File

@ -1,5 +1,5 @@
import Link from 'next/link';
import { Button, Text, Icon, Icons, GridTable, GridColumn, Flexbox } from 'react-basics';
import { Button, Text, Icon, Icons, GridTable, GridColumn } from 'react-basics';
import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser';
@ -29,7 +29,7 @@ export function WebsitesTable({ data = [], showTeam, showEditButton }) {
} = row;
return (
<Flexbox gap={10}>
<>
{showEditButton && (!showTeam || ownerId === user.id) && (
<Link href={`/settings/websites/${id}`}>
<Button>
@ -48,7 +48,7 @@ export function WebsitesTable({ data = [], showTeam, showEditButton }) {
<Text>{formatMessage(labels.view)}</Text>
</Button>
</Link>
</Flexbox>
</>
);
}}
</GridColumn>

View File

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

View File

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

View File

@ -23,6 +23,9 @@
}
.body td {
display: flex;
gap: 10px;
min-height: 70px;
align-items: center;
}

View File

@ -1,100 +0,0 @@
import Empty from 'components/common/Empty';
import useMessages from 'components/hooks/useMessages';
import { useState } from 'react';
import {
SearchField,
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
} from 'react-basics';
import styles from './SettingsTable.module.css';
import Pager from 'components/common/Pager';
export function SettingsTable({
columns = [],
data,
children,
cellRender,
showSearch,
showPaging,
onFilterChange,
onPageChange,
onPageSizeChange,
filterValue,
}) {
const { formatMessage, labels, messages } = useMessages();
const [filter, setFilter] = useState(filterValue);
const { data: value, page, count, pageSize } = data;
const handleFilterChange = value => {
setFilter(value);
onFilterChange(value);
};
return (
<>
{showSearch && (value.length > 0 || filterValue) && (
<SearchField
onChange={handleFilterChange}
delay={1000}
value={filter}
autoFocus={true}
placeholder={formatMessage(labels.search)}
style={{ maxWidth: '300px', marginBottom: '10px' }}
/>
)}
{value.length === 0 && filterValue && (
<Empty message={formatMessage(messages.noResultsFound)} />
)}
{value.length > 0 && (
<Table columns={columns} rows={value}>
<TableHeader className={styles.header}>
{(column, index) => {
return (
<TableColumn key={index} className={styles.cell} style={columns[index].style}>
{column.label}
</TableColumn>
);
}}
</TableHeader>
<TableBody className={styles.body}>
{(row, keys, rowIndex) => {
row.action = children(row, keys, rowIndex);
return (
<TableRow key={rowIndex} data={row} keys={keys} className={styles.row}>
{(data, key, colIndex) => {
return (
<TableCell
key={colIndex}
className={styles.cell}
style={columns[colIndex].style}
>
<label className={styles.label}>{columns[colIndex].label}</label>
{cellRender ? cellRender(row, data, key, colIndex) : data[key]}
</TableCell>
);
}}
</TableRow>
);
}}
</TableBody>
{showPaging && (
<Pager
page={page}
pageSize={pageSize}
count={count}
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
/>
)}
</Table>
)}
</>
);
}
export default SettingsTable;

View File

@ -1,44 +0,0 @@
.cell {
align-items: center;
}
.row .cell:last-child {
gap: 10px;
justify-content: flex-end;
}
.label {
display: none;
font-weight: 700;
}
@media screen and (max-width: 992px) {
.header .cell {
display: none;
}
.label {
display: block;
min-width: 100px;
}
.row .cell {
padding-left: 0;
flex-basis: 100%;
}
}
@media screen and (max-width: 1200px) {
.row {
flex-wrap: wrap;
}
.header .cell:last-child {
display: none;
}
.row .cell:last-child {
padding-left: 0;
flex-basis: 100%;
}
}

View File

@ -1,3 +1,4 @@
'use client';
import { ReactNode } from 'react';
import classNames from 'classnames';
import { Banner, Loading } from 'react-basics';

View File

@ -5,7 +5,7 @@ import styles from './PageHeader.module.css';
export interface PageHeaderProps {
title?: string;
className?: string;
children: ReactNode;
children?: ReactNode;
}
export function PageHeader({ title, className, children }: PageHeaderProps) {

View File

@ -12,7 +12,6 @@ export * from 'components/common/HoverTooltip';
export * from 'components/common/LinkButton';
export * from 'components/common/MobileMenu';
export * from 'components/common/Pager';
export * from 'components/common/SettingsTable';
export * from 'components/common/UpdateNotice';
export * from 'components/common/WorldMap';
@ -113,5 +112,5 @@ export * from 'app/(app)/settings/websites/[id]/WebsiteDeleteForm';
export * from 'app/(app)/settings/websites/[id]/WebsiteEditForm';
export * from 'app/(app)/settings/websites/[id]/WebsiteResetForm';
export * from 'app/(app)/settings/websites/WebsiteSettings';
export * from 'app/(app)/settings/websites/WebsitesList';
export * from './app/(app)/settings/websites/Websites';
export * from 'app/(app)/settings/websites/WebsitesTable';

View File

@ -189,7 +189,7 @@ function getPageFilters(filters: SearchFilter): [
return [
{
...(pageSize > 0 && { take: pageSize, skip: pageSize * (page - 1) }),
...(pageSize > 0 && { take: +pageSize, skip: +pageSize * (page - 1) }),
...(orderBy && {
orderBy: [
{

View File

@ -30,7 +30,7 @@ export default async (
await useValidate(schema, req, res);
const { user } = req.auth;
const { id: userId, page, query, includeTeams, onlyTeams } = req.query;
const { id: userId, page = 1, query = '', includeTeams, onlyTeams } = req.query;
if (req.method === 'GET') {
if (!user.isAdmin && user.id !== userId) {
@ -38,8 +38,8 @@ export default async (
}
const websites = await getWebsitesByUserId(userId, {
page: +page,
query: query as string,
page,
query,
includeTeams,
onlyTeams,
});