Refactor of team websites.

This commit is contained in:
Mike Cao 2024-01-18 16:46:40 -08:00
parent 2552e1d495
commit dc3818baaa
22 changed files with 61 additions and 208 deletions

View File

@ -63,7 +63,7 @@ export function Dashboard() {
{editing && <DashboardEdit />} {editing && <DashboardEdit />}
{!editing && ( {!editing && (
<> <>
<WebsiteChartList websites={data} showCharts={showCharts} limit={pageSize} /> <WebsiteChartList websites={data as any} showCharts={showCharts} limit={pageSize} />
<Pager <Pager
page={page} page={page}
pageSize={pageSize} pageSize={pageSize}

View File

@ -19,7 +19,7 @@ export function DashboardEdit() {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data: result } = useQuery({ const { data: result } = useQuery({
queryKey: ['websites'], queryKey: ['websites'],
queryFn: () => get('/websites', { includeTeams: 1 }), queryFn: () => get('/websites'),
}); });
const { data: websites } = result || {}; const { data: websites } = result || {};

View File

@ -41,7 +41,7 @@ export default function FieldFilterForm({
format(values[0]); format(values[0]);
} }
return formatted; return formatted;
}, [values]); }, [formatValue, locale, name, values]);
const renderFilterValue = value => { const renderFilterValue = value => {
return filters.find(f => f.value === value)?.label; return filters.find(f => f.value === value)?.label;
@ -65,7 +65,7 @@ export default function FieldFilterForm({
items={filters} items={filters}
value={filter} value={filter}
renderValue={renderFilterValue} renderValue={renderFilterValue}
onSelect={(key: any) => setFilter(key)} onChange={(key: any) => setFilter(key)}
> >
{({ value, label }) => { {({ value, label }) => {
return <Item key={value}>{label}</Item>; return <Item key={value}>{label}</Item>;
@ -78,7 +78,7 @@ export default function FieldFilterForm({
items={values} items={values}
value={value} value={value}
renderValue={renderValue} renderValue={renderValue}
onSelect={(key: any) => setValue(key)} onChange={(key: any) => setValue(key)}
style={{ style={{
minWidth: '250px', minWidth: '250px',
}} }}

View File

@ -29,7 +29,7 @@ export function LanguageSetting() {
items={options} items={options}
value={locale} value={locale}
renderValue={renderValue} renderValue={renderValue}
onSelect={saveLocale} onChange={saveLocale}
allowSearch={true} allowSearch={true}
onSearch={setSearch} onSearch={setSearch}
menuProps={{ className: styles.menu }} menuProps={{ className: styles.menu }}

View File

@ -21,7 +21,7 @@ export function TimezoneSetting() {
<Dropdown <Dropdown
items={options} items={options}
value={timezone} value={timezone}
onSelect={saveTimezone} onChange={saveTimezone}
menuProps={{ className: styles.menu }} menuProps={{ className: styles.menu }}
allowSearch={true} allowSearch={true}
onSearch={setSearch} onSearch={setSearch}

View File

@ -1,33 +0,0 @@
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
import useMessages from 'components/hooks/useMessages';
import TeamDeleteForm from './TeamDeleteForm';
export function TeamDeleteButton({
teamId,
teamName,
onDelete,
}: {
teamId: string;
teamName: string;
onDelete?: () => void;
}) {
const { formatMessage, labels } = useMessages();
return (
<ModalTrigger>
<Button>
<Icon>
<Icons.Trash />
</Icon>
<Text>{formatMessage(labels.delete)}</Text>
</Button>
<Modal title={formatMessage(labels.deleteTeam)}>
{(close: any) => (
<TeamDeleteForm teamId={teamId} teamName={teamName} onSave={onDelete} onClose={close} />
)}
</Modal>
</ModalTrigger>
);
}
export default TeamDeleteButton;

View File

@ -4,7 +4,6 @@ import useUser from 'components/hooks/useUser';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import Link from 'next/link'; import Link from 'next/link';
import { Button, GridColumn, GridTable, Icon, Icons, Text, useBreakpoint } from 'react-basics'; import { Button, GridColumn, GridTable, Icon, Icons, Text, useBreakpoint } from 'react-basics';
import TeamDeleteButton from './TeamDeleteButton';
import TeamLeaveButton from './TeamLeaveButton'; import TeamLeaveButton from './TeamLeaveButton';
export function TeamsTable({ data = [] }: { data: any[] }) { export function TeamsTable({ data = [] }: { data: any[] }) {
@ -26,13 +25,10 @@ export function TeamsTable({ data = [] }: { data: any[] }) {
return ( return (
<> <>
{isOwner && <TeamDeleteButton teamId={id} teamName={name} />}
{!isOwner && <TeamLeaveButton teamId={id} teamName={name} />} {!isOwner && <TeamLeaveButton teamId={id} teamName={name} />}
<Link href={`/settings/teams/${id}`}> <Link href={`/settings/teams/${id}`}>
<Button> <Button>
<Icon> <Icon>{isOwner ? <Icons.Edit /> : <Icons.ArrowRight />}</Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(isOwner ? labels.edit : labels.view)}</Text> <Text>{formatMessage(isOwner ? labels.edit : labels.view)}</Text>
</Button> </Button>
</Link> </Link>

View File

@ -0,0 +1,27 @@
import { ActionForm, Button, Modal, ModalTrigger } from 'react-basics';
import useMessages from 'components/hooks/useMessages';
import TeamDeleteForm from '../TeamDeleteForm';
export function TeamData({ teamId }: { teamId: string; onSave?: (value: string) => void }) {
const { formatMessage, labels, messages } = useMessages();
const handleSave = () => {};
return (
<ActionForm
label={formatMessage(labels.deleteTeam)}
description={formatMessage(messages.deleteTeamWarning)}
>
<ModalTrigger>
<Button variant="danger">{formatMessage(labels.delete)}</Button>
<Modal title={formatMessage(labels.deleteTeam)}>
{(close: () => void) => (
<TeamDeleteForm teamId={teamId} teamName={''} onSave={handleSave} onClose={close} />
)}
</Modal>
</ModalTrigger>
</ActionForm>
);
}
export default TeamData;

View File

@ -9,6 +9,7 @@ import useMessages from 'components/hooks/useMessages';
import TeamEditForm from './TeamEditForm'; import TeamEditForm from './TeamEditForm';
import TeamMembers from './TeamMembers'; import TeamMembers from './TeamMembers';
import TeamWebsites from './TeamWebsites'; import TeamWebsites from './TeamWebsites';
import TeamData from './TeamData';
export function TeamSettings({ teamId }: { teamId: string }) { export function TeamSettings({ teamId }: { teamId: string }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
@ -52,12 +53,14 @@ export function TeamSettings({ teamId }: { teamId: string }) {
<Item key="details">{formatMessage(labels.details)}</Item> <Item key="details">{formatMessage(labels.details)}</Item>
<Item key="members">{formatMessage(labels.members)}</Item> <Item key="members">{formatMessage(labels.members)}</Item>
<Item key="websites">{formatMessage(labels.websites)}</Item> <Item key="websites">{formatMessage(labels.websites)}</Item>
<Item key="data">{formatMessage(labels.data)}</Item>
</Tabs> </Tabs>
{tab === 'details' && ( {tab === 'details' && (
<TeamEditForm teamId={teamId} data={values} onSave={handleSave} readOnly={!canEdit} /> <TeamEditForm teamId={teamId} data={values} onSave={handleSave} readOnly={!canEdit} />
)} )}
{tab === 'members' && <TeamMembers teamId={teamId} readOnly={!canEdit} />} {tab === 'members' && <TeamMembers teamId={teamId} readOnly={!canEdit} />}
{tab === 'websites' && <TeamWebsites teamId={teamId} readOnly={!canEdit} />} {tab === 'websites' && <TeamWebsites teamId={teamId} readOnly={!canEdit} />}
{canEdit && tab === 'data' && <TeamData teamId={teamId} />}
</Flexbox> </Flexbox>
); );
} }

View File

@ -1,15 +1,11 @@
import { ActionForm, Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
import TeamWebsitesTable from './TeamWebsitesTable'; import TeamWebsitesTable from './TeamWebsitesTable';
import TeamWebsiteAddForm from './TeamWebsiteAddForm';
import useApi from 'components/hooks/useApi'; import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser'; import useUser from 'components/hooks/useUser';
import useFilterQuery from 'components/hooks/useFilterQuery'; import useFilterQuery from 'components/hooks/useFilterQuery';
import DataTable from 'components/common/DataTable'; import DataTable from 'components/common/DataTable';
import useCache from 'store/cache'; import useCache from 'store/cache';
export function TeamWebsites({ teamId, readOnly }: { teamId: string; readOnly: boolean }) { export function TeamWebsites({ teamId, readOnly }: { teamId: string; readOnly: boolean }) {
const { formatMessage, labels, messages } = useMessages();
const { user } = useUser(); const { user } = useUser();
const { get } = useApi(); const { get } = useApi();
const modified = useCache(state => state?.['team:websites']); const modified = useCache(state => state?.['team:websites']);
@ -28,26 +24,9 @@ export function TeamWebsites({ teamId, readOnly }: { teamId: string; readOnly: b
}; };
return ( return (
<> <DataTable queryResult={queryResult}>
<ActionForm description={formatMessage(messages.teamWebsitesInfo)}> {({ data }) => <TeamWebsitesTable data={data} onRemove={handleChange} readOnly={readOnly} />}
<ModalTrigger> </DataTable>
<Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.addWebsite)}</Text>
</Button>
<Modal title={formatMessage(labels.addWebsite)}>
{close => <TeamWebsiteAddForm teamId={teamId} onSave={handleChange} onClose={close} />}
</Modal>
</ModalTrigger>
</ActionForm>
<DataTable queryResult={queryResult}>
{({ data }) => (
<TeamWebsitesTable data={data} onRemove={handleChange} readOnly={readOnly} />
)}
</DataTable>
</>
); );
} }

View File

@ -12,9 +12,6 @@ export interface WebsitesDataTableProps {
allowEdit?: boolean; allowEdit?: boolean;
allowView?: boolean; allowView?: boolean;
showActions?: boolean; showActions?: boolean;
showTeam?: boolean;
includeTeams?: boolean;
onlyTeams?: boolean;
children?: ReactNode; children?: ReactNode;
} }
@ -23,9 +20,6 @@ export function WebsitesDataTable({
allowEdit = true, allowEdit = true,
allowView = true, allowView = true,
showActions = true, showActions = true,
showTeam,
includeTeams,
onlyTeams,
children, children,
}: WebsitesDataTableProps) { }: WebsitesDataTableProps) {
const { get } = useApi(); const { get } = useApi();
@ -33,11 +27,9 @@ export function WebsitesDataTable({
const { websitesUrl } = useContext(SettingsContext); const { websitesUrl } = useContext(SettingsContext);
const queryResult = useFilterQuery({ const queryResult = useFilterQuery({
queryKey: ['websites', { includeTeams, onlyTeams, modified }], queryKey: ['websites', { modified }],
queryFn: (params: any) => { queryFn: (params: any) => {
return get(websitesUrl, { return get(websitesUrl, {
includeTeams,
onlyTeams,
...params, ...params,
}); });
}, },
@ -49,7 +41,6 @@ export function WebsitesDataTable({
{({ data }) => ( {({ data }) => (
<WebsitesTable <WebsitesTable
data={data} data={data}
showTeam={showTeam}
showActions={showActions} showActions={showActions}
allowEdit={allowEdit} allowEdit={allowEdit}
allowView={allowView} allowView={allowView}

View File

@ -7,7 +7,6 @@ import SettingsContext from '../SettingsContext';
export interface WebsitesTableProps { export interface WebsitesTableProps {
data: any[]; data: any[];
showTeam?: boolean;
showActions?: boolean; showActions?: boolean;
allowEdit?: boolean; allowEdit?: boolean;
allowView?: boolean; allowView?: boolean;
@ -16,7 +15,6 @@ export interface WebsitesTableProps {
export function WebsitesTable({ export function WebsitesTable({
data = [], data = [],
showTeam,
showActions, showActions,
allowEdit, allowEdit,
allowView, allowView,
@ -31,16 +29,6 @@ export function WebsitesTable({
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}> <GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
<GridColumn name="name" label={formatMessage(labels.name)} /> <GridColumn name="name" label={formatMessage(labels.name)} />
<GridColumn name="domain" label={formatMessage(labels.domain)} /> <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>
)}
{showActions && ( {showActions && (
<GridColumn name="action" label=" " alignment="end"> <GridColumn name="action" label=" " alignment="end">
{row => { {row => {
@ -51,7 +39,7 @@ export function WebsitesTable({
return ( return (
<> <>
{allowEdit && (!showTeam || ownerId === user.id) && ( {allowEdit && ownerId === user.id && (
<Link href={`${settingsPath}/${id}`}> <Link href={`${settingsPath}/${id}`}>
<Button> <Button>
<Icon> <Icon>

View File

@ -43,7 +43,7 @@ export function WebsiteDeleteForm({
<Form onSubmit={handleSubmit} error={error}> <Form onSubmit={handleSubmit} error={error}>
<p> <p>
<FormattedMessage <FormattedMessage
{...messages.deleteWebsite} {...messages.actionConfirmation}
values={{ confirmation: <b>{CONFIRM_VALUE}</b> }} values={{ confirmation: <b>{CONFIRM_VALUE}</b> }}
/> />
</p> </p>

View File

@ -43,7 +43,7 @@ export function WebsiteResetForm({
<Form onSubmit={handleSubmit} error={error}> <Form onSubmit={handleSubmit} error={error}>
<p> <p>
<FormattedMessage <FormattedMessage
{...messages.resetWebsite} {...messages.actionConfirmation}
values={{ confirmation: <b>{CONFIRM_VALUE}</b> }} values={{ confirmation: <b>{CONFIRM_VALUE}</b> }}
/> />
</p> </p>

View File

@ -1,37 +1,12 @@
'use client'; 'use client';
import WebsitesDataTable from '../settings/websites/WebsitesDataTable'; import WebsitesDataTable from '../settings/websites/WebsitesDataTable';
import { useMessages, useUser } from 'components/hooks'; import { useUser } from 'components/hooks';
import { useState } from 'react';
import { Item, Tabs } from 'react-basics';
const TABS = {
myWebsites: 'my-websites',
teamWebsites: 'team-websites',
};
export function WebsitesBrowse() { export function WebsitesBrowse() {
const { user } = useUser(); const { user } = useUser();
const { formatMessage, labels } = useMessages();
const [tab, setTab] = useState(TABS.myWebsites);
const allowEdit = !process.env.cloudMode; const allowEdit = !process.env.cloudMode;
return ( return <WebsitesDataTable userId={user.id} allowEdit={allowEdit} />;
<>
<Tabs selectedKey={tab} onSelect={(tab: any) => setTab(tab)} style={{ marginBottom: 30 }}>
<Item key={TABS.myWebsites}>{formatMessage(labels.myWebsites)}</Item>
<Item key={TABS.teamWebsites}>{formatMessage(labels.teamWebsites)}</Item>
</Tabs>
{tab === TABS.myWebsites && <WebsitesDataTable userId={user.id} allowEdit={allowEdit} />}
{tab === TABS.teamWebsites && (
<WebsitesDataTable
userId={user.id}
showTeam={true}
onlyTeams={true}
allowEdit={allowEdit}
/>
)}
</>
);
} }
export default WebsitesBrowse; export default WebsitesBrowse;

View File

@ -134,7 +134,7 @@ export default function WebsiteExpandedView({
items={items} items={items}
value={view} value={view}
renderValue={renderValue} renderValue={renderValue}
onSelect={handleChange} onChange={handleChange}
alignment="end" alignment="end"
> >
{({ key, label }) => <Item key={key}>{label}</Item>} {({ key, label }) => <Item key={key}>{label}</Item>}

View File

@ -113,7 +113,7 @@ export function DateFilter({
value={value} value={value}
alignment={alignment} alignment={alignment}
placeholder={formatMessage(labels.selectDate)} placeholder={formatMessage(labels.selectDate)}
onSelect={key => handleChange(key as any)} onChange={key => handleChange(key as any)}
> >
{({ label, value, divider }) => ( {({ label, value, divider }) => (
<Item key={value} divider={divider}> <Item key={value} divider={divider}>

View File

@ -50,7 +50,7 @@ export function WebsiteSelect({
value={selectedId as string} value={selectedId as string}
renderValue={renderValue} renderValue={renderValue}
renderEmpty={renderEmpty} renderEmpty={renderEmpty}
onSelect={handleSelect} onChange={handleSelect}
alignment="end" alignment="end"
placeholder={formatMessage(labels.selectWebsite)} placeholder={formatMessage(labels.selectWebsite)}
allowSearch={true} allowSearch={true}

View File

@ -238,13 +238,9 @@ export const messages = defineMessages({
id: 'message.team-already-member', id: 'message.team-already-member',
defaultMessage: 'You are already a member of the team.', defaultMessage: 'You are already a member of the team.',
}, },
deleteAccount: { actionConfirmation: {
id: 'message.delete-account', id: 'message.action-confirmation',
defaultMessage: 'To delete this account, type {confirmation} in the box below to confirm.', defaultMessage: 'Type {confirmation} in the box below to confirm.',
},
deleteWebsite: {
id: 'message.delete-website',
defaultMessage: 'To delete this website, type {confirmation} in the box below to confirm.',
}, },
resetWebsite: { resetWebsite: {
id: 'message.reset-website', id: 'message.reset-website',
@ -263,6 +259,10 @@ export const messages = defineMessages({
id: 'message.delete-website-warning', id: 'message.delete-website-warning',
defaultMessage: 'All website data will be deleted.', defaultMessage: 'All website data will be deleted.',
}, },
deleteTeamWarning: {
id: 'message.delete-team-warning',
defaultMessage: 'Deleting a team will also delete all team websites.',
},
noResultsFound: { noResultsFound: {
id: 'message.no-results-found', id: 'message.no-results-found',
defaultMessage: 'No results found.', defaultMessage: 'No results found.',

View File

@ -41,7 +41,6 @@ export interface TeamSearchFilter extends SearchFilter {
export interface ReportSearchFilter extends SearchFilter { export interface ReportSearchFilter extends SearchFilter {
userId?: string; userId?: string;
websiteId?: string; websiteId?: string;
includeTeams?: boolean;
} }
export interface SearchFilter { export interface SearchFilter {

View File

@ -29,7 +29,7 @@ export async function getReports(
params: ReportSearchFilter, params: ReportSearchFilter,
options?: { include?: Prisma.ReportInclude }, options?: { include?: Prisma.ReportInclude },
): Promise<FilterResult<Report[]>> { ): Promise<FilterResult<Report[]>> {
const { query, userId, websiteId, includeTeams } = params; const { query, userId, websiteId } = params;
const mode = prisma.getQueryMode(); const mode = prisma.getQueryMode();
@ -42,23 +42,6 @@ export async function getReports(
{ {
userId, userId,
}, },
{
...(includeTeams && {
website: {
teamWebsite: {
some: {
team: {
teamUser: {
some: {
userId,
},
},
},
},
},
},
}),
},
], ],
}, },
{ {

View File

@ -1,6 +1,5 @@
import { Prisma, Website } from '@prisma/client'; import { Prisma, Website } from '@prisma/client';
import cache from 'lib/cache'; import cache from 'lib/cache';
import { ROLES } from 'lib/constants';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import { FilterResult, WebsiteSearchFilter } from 'lib/types'; import { FilterResult, WebsiteSearchFilter } from 'lib/types';
@ -22,49 +21,16 @@ export async function getWebsites(
filters: WebsiteSearchFilter, filters: WebsiteSearchFilter,
options?: { include?: Prisma.WebsiteInclude }, options?: { include?: Prisma.WebsiteInclude },
): Promise<FilterResult<Website[]>> { ): Promise<FilterResult<Website[]>> {
const { userId, teamId, includeTeams, onlyTeams, query } = filters; const { userId, teamId, query } = filters;
const mode = prisma.getQueryMode(); const mode = prisma.getQueryMode();
const where: Prisma.WebsiteWhereInput = { const where: Prisma.WebsiteWhereInput = {
...(teamId && {
teamWebsite: {
some: {
teamId,
},
},
}),
AND: [ AND: [
{ {
OR: [ OR: [
{ {
...(userId && ...(userId && { userId }),
!onlyTeams && { ...(teamId && { teamId }),
userId,
}),
},
{
...((includeTeams || onlyTeams) && {
AND: [
{
teamWebsite: {
some: {
team: {
teamUser: {
some: {
userId,
},
},
},
},
},
},
{
userId: {
not: userId,
},
},
],
}),
}, },
], ],
}, },
@ -110,15 +76,6 @@ export async function getWebsitesByUserId(
{ userId, ...filters }, { userId, ...filters },
{ {
include: { include: {
teamWebsite: {
include: {
team: {
select: {
name: true,
},
},
},
},
user: { user: {
select: { select: {
username: true, username: true,
@ -138,21 +95,9 @@ export async function getWebsitesByTeamId(
{ {
teamId, teamId,
...filters, ...filters,
includeTeams: true,
}, },
{ {
include: { include: {
teamWebsite: {
include: {
team: {
include: {
teamUser: {
where: { role: ROLES.teamOwner },
},
},
},
},
},
user: { user: {
select: { select: {
id: true, id: true,