Merge branch 'data-table' into dev

# Conflicts:
#	src/components/messages.js
This commit is contained in:
Mike Cao 2023-09-21 21:54:39 -07:00
commit 92ccc64e47
12 changed files with 206 additions and 37 deletions

View File

@ -0,0 +1,68 @@
import { createContext } from 'react';
import { SearchField } from 'react-basics';
import { useDataTable } from 'components/hooks/useDataTable';
import { useMessages } from 'components/hooks';
import Empty from 'components/common/Empty';
import Pager from 'components/common/Pager';
import styles from './DataTable.module.css';
const DEFAULT_SEARCH_DELAY = 1000;
export const DataTableStyles = styles;
export const DataTableContext = createContext(null);
export function DataTable({
searchDelay,
showSearch = true,
showPaging = true,
children,
onChange,
}) {
const { formatMessage, labels, messages } = useMessages();
const dataTable = useDataTable();
const { query, setQuery, data, pageInfo, setPageInfo } = dataTable;
const { page, pageSize, count } = pageInfo || {};
const noResults = Boolean(query && data?.length === 0);
const handleChange = () => {
onChange?.({ query, page });
};
const handleSearch = value => {
setQuery(value);
handleChange();
};
const handlePageChange = page => {
setPageInfo(state => ({ ...state, page }));
};
return (
<DataTableContext.Provider value={dataTable}>
{showSearch && (
<SearchField
className={styles.search}
value={query}
onChange={handleSearch}
delay={searchDelay || DEFAULT_SEARCH_DELAY}
autoFocus={true}
placeholder={formatMessage(labels.search)}
/>
)}
{noResults && <Empty message={formatMessage(messages.noResultsFound)} />}
<div className={styles.body}>{children}</div>
{showPaging && (
<Pager
className={styles.pager}
page={page}
pageSize={pageSize}
count={count}
onPageChange={handlePageChange}
/>
)}
</DataTableContext.Provider>
);
}
export default DataTable;

View File

@ -0,0 +1,17 @@
.search {
max-width: 300px;
margin: 20px 0;
}
.action {
justify-content: flex-end;
gap: 5px;
}
.body td {
align-items: center;
}
.pager {
margin-top: 20px;
}

View File

@ -1,14 +1,15 @@
import styles from './Pager.module.css'; import classNames from 'classnames';
import { Button, Flexbox, Icon, Icons } from 'react-basics'; import { Button, Flexbox, Icon, Icons } from 'react-basics';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import styles from './Pager.module.css';
export function Pager({ page, pageSize, count, onPageChange }) { export function Pager({ page, pageSize, count, onPageChange, className }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const maxPage = Math.ceil(count / pageSize); const maxPage = pageSize && count ? Math.ceil(count / pageSize) : 0;
const lastPage = page === maxPage; const lastPage = page === maxPage;
const firstPage = page === 1; const firstPage = page === 1;
if (count === 0) { if (count === 0 || !maxPage) {
return null; return null;
} }
@ -24,7 +25,7 @@ export function Pager({ page, pageSize, count, onPageChange }) {
} }
return ( return (
<Flexbox justifyContent="center" className={styles.container}> <Flexbox justifyContent="center" className={classNames(styles.container, className)}>
<Button onClick={() => handlePageChange(-1)} disabled={firstPage}> <Button onClick={() => handlePageChange(-1)} disabled={firstPage}>
<Icon rotate={90}> <Icon rotate={90}>
<Icons.ChevronDown /> <Icons.ChevronDown />

View File

@ -1,7 +1,4 @@
.container {
margin-top: 20px;
}
.text { .text {
font-size: var(--font-size-md);
margin: 0 16px; margin: 0 16px;
} }

View File

@ -0,0 +1,13 @@
import { useState } from 'react';
import { usePaging } from 'components/hooks/usePaging';
export function useDataTable(config = {}) {
const { initialData, initialQuery, initialPageInfo } = config;
const [data, setData] = useState(initialData ?? null);
const [query, setQuery] = useState(initialQuery ?? '');
const { pageInfo, setPageInfo } = usePaging(initialPageInfo);
return { data, setData, query, setQuery, pageInfo, setPageInfo };
}
export default useDataTable;

View File

@ -0,0 +1,9 @@
import { useState } from 'react';
const DEFAULT_PAGE_INFO = { page: 1, pageSize: 10, total: 0 };
export function usePaging(initialPageInfo) {
const [pageInfo, setPageInfo] = useState(initialPageInfo ?? { ...DEFAULT_PAGE_INFO });
return { pageInfo, setPageInfo };
}

View File

@ -13,17 +13,17 @@ export function EventsChart({ websiteId, className, token }) {
const { locale } = useLocale(); const { locale } = useLocale();
const [timezone] = useTimezone(); const [timezone] = useTimezone();
const { const {
query: { url, eventName }, query: { url, event },
} = usePageQuery(); } = usePageQuery();
const { data, isLoading } = useQuery(['events', websiteId, modified, eventName], () => const { data, isLoading } = useQuery(['events', websiteId, modified, event], () =>
get(`/websites/${websiteId}/events`, { get(`/websites/${websiteId}/events`, {
startAt: +startDate, startAt: +startDate,
endAt: +endDate, endAt: +endDate,
unit, unit,
timezone, timezone,
url, url,
eventName, event,
token, token,
}), }),
); );

View File

@ -6,7 +6,7 @@ import classNames from 'classnames';
import Empty from 'components/common/Empty'; import Empty from 'components/common/Empty';
import { formatNumber, formatLongNumber } from 'lib/format'; import { formatNumber, formatLongNumber } from 'lib/format';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import styles from './DataTable.module.css'; import styles from './ListTable.module.css';
export function ListTable({ export function ListTable({
data = [], data = [],

View File

@ -3,11 +3,11 @@ import PageHeader from 'components/layout/PageHeader';
import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm';
import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
import useApi from 'components/hooks/useApi'; import useApi from 'components/hooks/useApi';
import useApiFilter from 'components/hooks/useApiFilter';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser'; import useUser from 'components/hooks/useUser';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
import { useState } from 'react';
export function WebsitesList({ export function WebsitesList({
showTeam, showTeam,
@ -15,28 +15,27 @@ export function WebsitesList({
showHeader = true, showHeader = true,
includeTeams, includeTeams,
onlyTeams, onlyTeams,
fetch,
}) { }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { user } = useUser(); const { user } = useUser();
const [params, setParams] = useState({});
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
useApiFilter();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading, error, refetch } = useQuery( const { data, isLoading, error, refetch } = useQuery(
['websites', fetch, user?.id, filter, page, pageSize, includeTeams, onlyTeams], ['websites', includeTeams, onlyTeams],
() => () =>
get(`/users/${user?.id}/websites`, { get(`/users/${user?.id}/websites`, {
filter,
page,
pageSize,
includeTeams, includeTeams,
onlyTeams, onlyTeams,
...params,
}), }),
{ enabled: !!user }, { enabled: !!user },
); );
const { showToast } = useToasts(); const { showToast } = useToasts();
const handleChange = params => {
setParams(params);
};
const handleSave = async () => { const handleSave = async () => {
await refetch(); await refetch();
showToast({ message: formatMessage(messages.saved), variant: 'success' }); showToast({ message: formatMessage(messages.saved), variant: 'success' });
@ -67,10 +66,7 @@ export function WebsitesList({
data={data} data={data}
showTeam={showTeam} showTeam={showTeam}
showEditButton={showEditButton} showEditButton={showEditButton}
onFilterChange={handleFilterChange} onChange={handleChange}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
filterValue={filter}
/> />
</Page> </Page>
); );

View File

@ -1,11 +1,78 @@
import Link from 'next/link'; import Link from 'next/link';
import { Button, Text, Icon, Icons } from 'react-basics'; import { Button, Text, Icon, Icons, GridTable, GridColumn } from 'react-basics';
import SettingsTable from 'components/common/SettingsTable'; import SettingsTable from 'components/common/SettingsTable';
import Empty from 'components/common/Empty'; import Empty from 'components/common/Empty';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser'; import useUser from 'components/hooks/useUser';
import DataTable, { DataTableStyles } from 'components/common/DataTable';
export function WebsitesTable({ export function WebsitesTable({
data = [],
filterValue,
showTeam,
showEditButton,
openExternal = false,
onChange,
}) {
const { formatMessage, labels } = useMessages();
const { user } = useUser();
const showTable = data && (filterValue || data?.data?.length !== 0);
return (
<DataTable onChange={onChange}>
{showTable && (
<GridTable data={data?.data}>
<GridColumn name="name" label={formatMessage(labels.name)} />
<GridColumn name="domain" label={formatMessage(labels.domain)} />
{showTeam && (
<GridColumn name="teamName" label={formatMessage(labels.teamName)}>
{row => row.teamWebsite[0]?.team.name}
</GridColumn>
)}
{showTeam && (
<GridColumn name="owner" label={formatMessage(labels.owner)}>
{row => row.user.username}
</GridColumn>
)}
<GridColumn name="action" label=" " className={DataTableStyles.action}>
{row => {
const {
id,
user: { id: ownerId },
} = row;
return (
<>
{showEditButton && (!showTeam || ownerId === user.id) && (
<Link href={`/settings/websites/${id}`}>
<Button>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
</Link>
)}
<Link href={`/websites/${id}`} target={openExternal ? '_blank' : null}>
<Button>
<Icon>
<Icons.External />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Button>
</Link>
</>
);
}}
</GridColumn>
</GridTable>
)}
</DataTable>
);
}
export function WebsitesTable2({
data = [], data = [],
filterValue, filterValue,
onFilterChange, onFilterChange,

View File

@ -18,16 +18,19 @@ import {
useToasts, useToasts,
} from 'react-basics'; } from 'react-basics';
const TABS = {
myWebsites: 'my-websites',
teamWebsites: 'team-websites',
};
export function WebsitesPage() { export function WebsitesPage() {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const [tab, setTab] = useState('my-websites'); const [tab, setTab] = useState(TABS.myWebsites);
const [fetch, setFetch] = useState(1);
const { user } = useUser(); const { user } = useUser();
const { showToast } = useToasts(); const { showToast } = useToasts();
const cloudMode = Boolean(process.env.cloudMode); const cloudMode = Boolean(process.env.cloudMode);
const handleSave = async () => { const handleSave = () => {
setFetch(fetch + 1);
showToast({ message: formatMessage(messages.saved), variant: 'success' }); showToast({ message: formatMessage(messages.saved), variant: 'success' });
}; };
@ -53,18 +56,16 @@ export function WebsitesPage() {
<Page> <Page>
<PageHeader title={formatMessage(labels.websites)}>{!cloudMode && addButton}</PageHeader> <PageHeader title={formatMessage(labels.websites)}>{!cloudMode && addButton}</PageHeader>
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}> <Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
<Item key="my-websites">{formatMessage(labels.myWebsites)}</Item> <Item key={TABS.myWebsites}>{formatMessage(labels.myWebsites)}</Item>
<Item key="team-webaites">{formatMessage(labels.teamWebsites)}</Item> <Item key={TABS.teamWebsites}>{formatMessage(labels.teamWebsites)}</Item>
</Tabs> </Tabs>
{tab === TABS.myWebsites && (
{tab === 'my-websites' && (
<WebsiteList showEditButton={!cloudMode} showHeader={false} fetch={fetch} /> <WebsiteList showEditButton={!cloudMode} showHeader={false} fetch={fetch} />
)} )}
{tab === 'team-webaites' && ( {tab === TABS.teamWebsites && (
<WebsiteList <WebsiteList
showEditButton={!cloudMode} showEditButton={!cloudMode}
showHeader={false} showHeader={false}
fetch={fetch}
showTeam={true} showTeam={true}
onlyTeams={true} onlyTeams={true}
/> />