mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-16 02:05:04 +01:00
Add Search Api/Components.
This commit is contained in:
parent
45888fabe6
commit
dcf8b2edaa
@ -50,7 +50,8 @@
|
||||
"@next/next/no-img-element": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-var-requires": "off"
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off"
|
||||
},
|
||||
"globals": {
|
||||
"React": "writable"
|
||||
|
37
components/common/Pager.js
Normal file
37
components/common/Pager.js
Normal file
@ -0,0 +1,37 @@
|
||||
import styles from './Pager.module.css';
|
||||
import { Button, Flexbox, Icon, Icons } from 'react-basics';
|
||||
|
||||
export function Pager({ page, pageSize, count, onPageChange, onPageSizeChange }) {
|
||||
const maxPage = Math.ceil(count / pageSize);
|
||||
const lastPage = page === maxPage;
|
||||
const firstPage = page === 1;
|
||||
|
||||
if (count === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlePageChange = value => {
|
||||
const nextPage = page + value;
|
||||
if (nextPage > 0 && nextPage <= maxPage) {
|
||||
onPageChange(nextPage);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flexbox justifyContent="center" className={styles.container}>
|
||||
<Button onClick={() => handlePageChange(-1)} disabled={firstPage}>
|
||||
<Icon size="lg" className={styles.icon} rotate={90}>
|
||||
<Icons.ChevronDown />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Flexbox alignItems="center" className={styles.text}>{`Page ${page} of ${maxPage}`}</Flexbox>
|
||||
<Button onClick={() => handlePageChange(1)} disabled={lastPage}>
|
||||
<Icon size="lg" className={styles.icon} rotate={270}>
|
||||
<Icons.ChevronDown />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
export default Pager;
|
7
components/common/Pager.module.css
Normal file
7
components/common/Pager.module.css
Normal file
@ -0,0 +1,7 @@
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0 10px;
|
||||
}
|
@ -1,37 +1,98 @@
|
||||
import { Table, TableHeader, TableBody, TableRow, TableCell, TableColumn } from 'react-basics';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import useMessages from '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, messages } = useMessages();
|
||||
const [filter, setFilter] = useState(filterValue);
|
||||
const { data: value, page, count, pageSize } = data;
|
||||
|
||||
const handleFilterChange = value => {
|
||||
setFilter(value);
|
||||
onFilterChange(value);
|
||||
};
|
||||
|
||||
export function SettingsTable({ columns = [], data = [], children, cellRender }) {
|
||||
return (
|
||||
<Table columns={columns} rows={data}>
|
||||
<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);
|
||||
<>
|
||||
{showSearch && (
|
||||
<SearchField
|
||||
onChange={handleFilterChange}
|
||||
delay={1000}
|
||||
value={filter}
|
||||
placeholder="Search"
|
||||
style={{ maxWidth: '300px', 'margin-bottom': '10px' }}
|
||||
/>
|
||||
)}
|
||||
{value.length === 0 && filterValue && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noResultsFound)}></EmptyPlaceholder>
|
||||
)}
|
||||
{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>
|
||||
</Table>
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -8,12 +8,12 @@ export function WebsiteSelect({ websiteId, onSelect }) {
|
||||
const { data } = useQuery(['websites:me'], () => get('/me/websites'));
|
||||
|
||||
const renderValue = value => {
|
||||
return data?.find(({ id }) => id === value)?.name;
|
||||
return data?.data?.find(({ id }) => id === value)?.name;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
items={data}
|
||||
items={data?.data}
|
||||
value={websiteId}
|
||||
renderValue={renderValue}
|
||||
onChange={onSelect}
|
||||
|
@ -12,16 +12,17 @@ import useDashboard from 'store/dashboard';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export function Dashboard({ userId }) {
|
||||
export function Dashboard() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const dashboard = useDashboard();
|
||||
const { showCharts, limit, editing } = dashboard;
|
||||
const [max, setMax] = useState(limit);
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error } = useQuery(['websites'], () =>
|
||||
get('/websites', { userId, includeTeams: 1 }),
|
||||
get('/websites', { includeTeams: 1 }),
|
||||
);
|
||||
const hasData = data && data.length !== 0;
|
||||
const hasData = data && data?.data.length !== 0;
|
||||
|
||||
const { dir } = useLocale();
|
||||
|
||||
function handleMore() {
|
||||
@ -47,8 +48,10 @@ export function Dashboard({ userId }) {
|
||||
)}
|
||||
{hasData && (
|
||||
<>
|
||||
{editing && <DashboardEdit websites={data} />}
|
||||
{!editing && <WebsiteChartList websites={data} showCharts={showCharts} limit={max} />}
|
||||
{editing && <DashboardEdit websites={data?.data} />}
|
||||
{!editing && (
|
||||
<WebsiteChartList websites={data?.data} showCharts={showCharts} limit={max} />
|
||||
)}
|
||||
{max < data.length && (
|
||||
<Flexbox justifyContent="center">
|
||||
<Button onClick={handleMore}>
|
||||
|
@ -5,7 +5,14 @@ import SettingsTable from 'components/common/SettingsTable';
|
||||
import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm';
|
||||
import { useMessages } from 'hooks';
|
||||
|
||||
export function ReportsTable({ data = [], onDelete = () => {} }) {
|
||||
export function ReportsTable({
|
||||
data = [],
|
||||
onDelete = () => {},
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}) {
|
||||
const [report, setReport] = useState(null);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
@ -22,7 +29,16 @@ export function ReportsTable({ data = [], onDelete = () => {} }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsTable columns={columns} data={data}>
|
||||
<SettingsTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
showSearch={true}
|
||||
showPaging={true}
|
||||
onFilterChange={onFilterChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
filterValue={filterValue}
|
||||
>
|
||||
{row => {
|
||||
const { id } = row;
|
||||
|
||||
|
@ -12,6 +12,8 @@ export function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
|
||||
const [newWebsites, setNewWebsites] = useState([]);
|
||||
const formRef = useRef();
|
||||
|
||||
const hasData = websites && websites.data.length > 0;
|
||||
|
||||
const handleSubmit = () => {
|
||||
mutate(
|
||||
{ websiteIds: newWebsites },
|
||||
@ -42,20 +44,22 @@ export function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form onSubmit={handleSubmit} error={error} ref={formRef}>
|
||||
<FormRow label={formatMessage(labels.websites)}>
|
||||
<Dropdown items={websites} onChange={handleAddWebsite} style={{ width: 300 }}>
|
||||
{({ id, name }) => <Item key={id}>{name}</Item>}
|
||||
</Dropdown>
|
||||
</FormRow>
|
||||
<WebsiteTags items={websites} websites={newWebsites} onClick={handleRemoveWebsite} />
|
||||
<FormButtons flex>
|
||||
<SubmitButton disabled={newWebsites && newWebsites.length === 0}>
|
||||
{formatMessage(labels.addWebsite)}
|
||||
</SubmitButton>
|
||||
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
{hasData && (
|
||||
<Form onSubmit={handleSubmit} error={error} ref={formRef}>
|
||||
<FormRow label={formatMessage(labels.websites)}>
|
||||
<Dropdown items={websites.data} onChange={handleAddWebsite} style={{ width: 300 }}>
|
||||
{({ id, name }) => <Item key={id}>{name}</Item>}
|
||||
</Dropdown>
|
||||
</FormRow>
|
||||
<WebsiteTags items={websites.data} websites={newWebsites} onClick={handleRemoveWebsite} />
|
||||
<FormButtons flex>
|
||||
<SubmitButton disabled={newWebsites && newWebsites.length === 0}>
|
||||
{formatMessage(labels.addWebsite)}
|
||||
</SubmitButton>
|
||||
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -2,13 +2,22 @@ import { Loading, useToasts } from 'react-basics';
|
||||
import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useApiFilter from 'hooks/useApiFilter';
|
||||
|
||||
export function TeamMembers({ teamId, readOnly }) {
|
||||
const { showToast } = useToasts();
|
||||
const { get, useQuery } = useApi();
|
||||
const { formatMessage, messages } = useMessages();
|
||||
const { data, isLoading, refetch } = useQuery(['teams:users', teamId], () =>
|
||||
get(`/teams/${teamId}/users`),
|
||||
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,
|
||||
}),
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
@ -22,7 +31,15 @@ export function TeamMembers({ teamId, readOnly }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TeamMembersTable onSave={handleSave} data={data} readOnly={readOnly} />
|
||||
<TeamMembersTable
|
||||
onSave={handleSave}
|
||||
data={data}
|
||||
readOnly={readOnly}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -4,7 +4,15 @@ import { ROLES } from 'lib/constants';
|
||||
import TeamMemberRemoveButton from './TeamMemberRemoveButton';
|
||||
import SettingsTable from 'components/common/SettingsTable';
|
||||
|
||||
export function TeamMembersTable({ data = [], onSave, readOnly }) {
|
||||
export function TeamMembersTable({
|
||||
data = [],
|
||||
onSave,
|
||||
readOnly,
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
|
||||
@ -16,7 +24,7 @@ export function TeamMembersTable({ data = [], onSave, readOnly }) {
|
||||
|
||||
const cellRender = (row, data, key) => {
|
||||
if (key === 'username') {
|
||||
return row?.user?.username;
|
||||
return row?.username;
|
||||
}
|
||||
if (key === 'role') {
|
||||
return formatMessage(
|
||||
@ -27,13 +35,23 @@ export function TeamMembersTable({ data = [], onSave, readOnly }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsTable data={data} columns={columns} cellRender={cellRender}>
|
||||
<SettingsTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
cellRender={cellRender}
|
||||
showSearch={true}
|
||||
showPaging={true}
|
||||
onFilterChange={onFilterChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
filterValue={filterValue}
|
||||
>
|
||||
{row => {
|
||||
return (
|
||||
!readOnly && (
|
||||
<TeamMemberRemoveButton
|
||||
teamId={row.teamId}
|
||||
userId={row.userId}
|
||||
userId={row.id}
|
||||
disabled={user.id === row?.user?.id || row.role === ROLES.teamOwner}
|
||||
onSave={onSave}
|
||||
/>
|
||||
|
@ -13,13 +13,22 @@ import TeamWebsitesTable from 'components/pages/settings/teams/TeamWebsitesTable
|
||||
import TeamAddWebsiteForm from 'components/pages/settings/teams/TeamAddWebsiteForm';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useApiFilter from 'hooks/useApiFilter';
|
||||
|
||||
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], () =>
|
||||
get(`/teams/${teamId}/websites`),
|
||||
const { data, isLoading, refetch } = useQuery(
|
||||
['teams:websites', teamId, filter, page, pageSize],
|
||||
() =>
|
||||
get(`/teams/${teamId}/websites`, {
|
||||
filter,
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
);
|
||||
const hasData = data && data.length !== 0;
|
||||
|
||||
@ -49,7 +58,17 @@ export function TeamWebsites({ teamId }) {
|
||||
return (
|
||||
<div>
|
||||
<ActionForm description={formatMessage(messages.teamWebsitesInfo)}>{addButton}</ActionForm>
|
||||
{hasData && <TeamWebsitesTable teamId={teamId} data={data} onSave={handleSave} />}
|
||||
{hasData && (
|
||||
<TeamWebsitesTable
|
||||
teamId={teamId}
|
||||
data={data}
|
||||
onSave={handleSave}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -6,9 +6,17 @@ import TeamWebsiteRemoveButton from './TeamWebsiteRemoveButton';
|
||||
import SettingsTable from 'components/common/SettingsTable';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
|
||||
export function TeamWebsitesTable({ data = [], onSave }) {
|
||||
export function TeamWebsitesTable({
|
||||
data = [],
|
||||
onSave,
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { openExternal } = useConfig();
|
||||
|
||||
const { user } = useUser();
|
||||
const columns = [
|
||||
{ name: 'name', label: formatMessage(labels.name) },
|
||||
@ -17,11 +25,19 @@ export function TeamWebsitesTable({ data = [], onSave }) {
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsTable columns={columns} data={data}>
|
||||
<SettingsTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
showSearch={true}
|
||||
showPaging={true}
|
||||
onFilterChange={onFilterChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
filterValue={filterValue}
|
||||
>
|
||||
{row => {
|
||||
const { teamId } = row;
|
||||
const { id: websiteId, name, domain, userId } = row.website;
|
||||
const { teamUser } = row.team;
|
||||
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;
|
||||
|
||||
|
@ -1,24 +1,37 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Icon, Modal, ModalTrigger, useToasts, Text, Flexbox } from 'react-basics';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import TeamAddForm from 'components/pages/settings/teams/TeamAddForm';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import TeamsTable from 'components/pages/settings/teams/TeamsTable';
|
||||
import Page from 'components/layout/Page';
|
||||
import Icons from 'components/icons';
|
||||
import TeamJoinForm from './TeamJoinForm';
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import TeamAddForm from 'components/pages/settings/teams/TeamAddForm';
|
||||
import TeamsTable from 'components/pages/settings/teams/TeamsTable';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import useUser from 'hooks/useUser';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import { useState } from 'react';
|
||||
import { Button, Flexbox, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
|
||||
import TeamJoinForm from './TeamJoinForm';
|
||||
import useApiFilter from 'hooks/useApiFilter';
|
||||
|
||||
export default function TeamsList() {
|
||||
const { user } = useUser();
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
|
||||
useApiFilter();
|
||||
const [update, setUpdate] = useState(0);
|
||||
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error } = useQuery(['teams', update], () => get(`/teams`));
|
||||
const hasData = data && data.length !== 0;
|
||||
const { data, isLoading, error } = useQuery(['teams', update, filter, page, pageSize], () => {
|
||||
return get(`/teams`, {
|
||||
filter,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
});
|
||||
|
||||
const hasData = data && data?.data.length !== 0;
|
||||
const isFiltered = filter;
|
||||
|
||||
const { showToast } = useToasts();
|
||||
|
||||
const handleSave = () => {
|
||||
@ -71,15 +84,26 @@ export default function TeamsList() {
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
<PageHeader title={formatMessage(labels.teams)}>
|
||||
{hasData && (
|
||||
{(hasData || isFiltered) && (
|
||||
<Flexbox gap={10}>
|
||||
{joinButton}
|
||||
{createButton}
|
||||
</Flexbox>
|
||||
)}
|
||||
</PageHeader>
|
||||
{hasData && <TeamsTable data={data} onDelete={handleDelete} />}
|
||||
{!hasData && (
|
||||
|
||||
{(hasData || isFiltered) && (
|
||||
<TeamsTable
|
||||
data={data}
|
||||
onDelete={handleDelete}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!hasData && !isFiltered && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noTeams)}>
|
||||
<Flexbox gap={10}>
|
||||
{joinButton}
|
||||
|
@ -1,14 +1,21 @@
|
||||
import SettingsTable from 'components/common/SettingsTable';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useUser from 'hooks/useUser';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import Link from 'next/link';
|
||||
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
|
||||
import TeamDeleteForm from './TeamDeleteForm';
|
||||
import TeamLeaveForm from './TeamLeaveForm';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useUser from 'hooks/useUser';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import SettingsTable from 'components/common/SettingsTable';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export function TeamsTable({ data = [], onDelete }) {
|
||||
export function TeamsTable({
|
||||
data = { data: [] },
|
||||
onDelete,
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { dir } = useLocale();
|
||||
@ -27,7 +34,17 @@ export function TeamsTable({ data = [], onDelete }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsTable data={data} columns={columns} cellRender={cellRender}>
|
||||
<SettingsTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
cellRender={cellRender}
|
||||
showSearch={true}
|
||||
showPaging={true}
|
||||
onFilterChange={onFilterChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
filterValue={filterValue}
|
||||
>
|
||||
{row => {
|
||||
const { id, teamUser } = row;
|
||||
const owner = teamUser.find(({ role }) => role === ROLES.teamOwner);
|
||||
|
@ -7,14 +7,27 @@ import UserAddButton from './UserAddButton';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useUser from 'hooks/useUser';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useApiFilter from 'hooks/useApiFilter';
|
||||
|
||||
export function UsersList() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
|
||||
useApiFilter();
|
||||
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error, refetch } = useQuery(['user'], () => get(`/users`), {
|
||||
enabled: !!user,
|
||||
});
|
||||
const { data, isLoading, error, refetch } = useQuery(
|
||||
['user', filter, page, pageSize],
|
||||
() =>
|
||||
get(`/users`, {
|
||||
filter,
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
{
|
||||
enabled: !!user,
|
||||
},
|
||||
);
|
||||
const { showToast } = useToasts();
|
||||
const hasData = data && data.length !== 0;
|
||||
|
||||
@ -33,8 +46,17 @@ export function UsersList() {
|
||||
<PageHeader title={formatMessage(labels.users)}>
|
||||
<UserAddButton onSave={handleSave} />
|
||||
</PageHeader>
|
||||
{hasData && <UsersTable data={data} onDelete={handleDelete} />}
|
||||
{!hasData && (
|
||||
{(hasData || filter) && (
|
||||
<UsersTable
|
||||
data={data}
|
||||
onDelete={handleDelete}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
)}
|
||||
{!hasData && !filter && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noUsers)}>
|
||||
<UserAddButton onSave={handleSave} />
|
||||
</EmptyPlaceholder>
|
||||
|
@ -8,7 +8,14 @@ import useMessages from 'hooks/useMessages';
|
||||
import SettingsTable from 'components/common/SettingsTable';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export function UsersTable({ data = [], onDelete }) {
|
||||
export function UsersTable({
|
||||
data = { data: [] },
|
||||
onDelete,
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { dateLocale } = useLocale();
|
||||
@ -36,7 +43,17 @@ export function UsersTable({ data = [], onDelete }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsTable data={data} columns={columns} cellRender={cellRender}>
|
||||
<SettingsTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
cellRender={cellRender}
|
||||
showSearch={true}
|
||||
showPaging={true}
|
||||
onFilterChange={onFilterChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
filterValue={filterValue}
|
||||
>
|
||||
{(row, keys, rowIndex) => {
|
||||
return (
|
||||
<>
|
||||
|
@ -8,14 +8,22 @@ import useApi from 'hooks/useApi';
|
||||
import useUser from 'hooks/useUser';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import useApiFilter from 'hooks/useApiFilter';
|
||||
|
||||
export function WebsitesList() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
|
||||
useApiFilter();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error, refetch } = useQuery(
|
||||
['websites', user?.id],
|
||||
() => get(`/users/${user?.id}/websites`),
|
||||
['websites', user?.id, filter, page, pageSize],
|
||||
() =>
|
||||
get(`/users/${user?.id}/websites`, {
|
||||
filter,
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
{ enabled: !!user },
|
||||
);
|
||||
const { showToast } = useToasts();
|
||||
@ -47,7 +55,15 @@ export function WebsitesList() {
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
<PageHeader title={formatMessage(labels.websites)}>{addButton}</PageHeader>
|
||||
{hasData && <WebsitesTable data={data} />}
|
||||
{hasData && (
|
||||
<WebsitesTable
|
||||
data={data}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
)}
|
||||
{!hasData && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}>
|
||||
{addButton}
|
||||
|
@ -4,7 +4,13 @@ import SettingsTable from 'components/common/SettingsTable';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
|
||||
export function WebsitesTable({ data = [] }) {
|
||||
export function WebsitesTable({
|
||||
data = [],
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { openExternal } = useConfig();
|
||||
|
||||
@ -15,7 +21,16 @@ export function WebsitesTable({ data = [] }) {
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsTable columns={columns} data={data}>
|
||||
<SettingsTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
showSearch={true}
|
||||
showPaging={true}
|
||||
onFilterChange={onFilterChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
filterValue={filterValue}
|
||||
>
|
||||
{row => {
|
||||
const { id } = row;
|
||||
|
||||
|
@ -7,7 +7,16 @@ import WebsiteHeader from './WebsiteHeader';
|
||||
|
||||
export function WebsiteReportsPage({ websiteId }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { reports, error, isLoading, deleteReport } = useReports(websiteId);
|
||||
const {
|
||||
reports,
|
||||
error,
|
||||
isLoading,
|
||||
deleteReport,
|
||||
filter,
|
||||
handleFilterChange,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
} = useReports(websiteId);
|
||||
|
||||
const handleDelete = async id => {
|
||||
await deleteReport(id);
|
||||
@ -26,7 +35,14 @@ export function WebsiteReportsPage({ websiteId }) {
|
||||
</Button>
|
||||
</Link>
|
||||
</Flexbox>
|
||||
<ReportsTable data={reports} onDelete={handleDelete} />
|
||||
<ReportsTable
|
||||
data={reports}
|
||||
onDelete={handleDelete}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
28
hooks/useApiFilter.ts
Normal file
28
hooks/useApiFilter.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export function useApiFilter() {
|
||||
const [filter, setFilter] = useState();
|
||||
const [filterType, setFilterType] = useState('All');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const handleFilterChange = value => setFilter(value);
|
||||
const handlePageChange = value => setPage(value);
|
||||
const handlePageSizeChange = value => setPageSize(value);
|
||||
|
||||
return {
|
||||
filter,
|
||||
setFilter,
|
||||
filterType,
|
||||
setFilterType,
|
||||
page,
|
||||
setPage,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
handleFilterChange,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
};
|
||||
}
|
||||
|
||||
export default useApiFilter;
|
@ -1,12 +1,16 @@
|
||||
import { useState } from 'react';
|
||||
import useApi from './useApi';
|
||||
import useApiFilter from 'hooks/useApiFilter';
|
||||
|
||||
export function useReports(websiteId) {
|
||||
const [modified, setModified] = useState(Date.now());
|
||||
const { get, useQuery, del, useMutation } = useApi();
|
||||
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
|
||||
const { data, error, isLoading } = useQuery(['reports:website', { websiteId, modified }], () =>
|
||||
get(`/reports`, { websiteId }),
|
||||
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
|
||||
useApiFilter();
|
||||
const { data, error, isLoading } = useQuery(
|
||||
['reports:website', { websiteId, modified, filter, page, pageSize }],
|
||||
() => get(`/reports`, { websiteId, filter, page, pageSize }),
|
||||
);
|
||||
|
||||
const deleteReport = id => {
|
||||
@ -17,7 +21,18 @@ export function useReports(websiteId) {
|
||||
});
|
||||
};
|
||||
|
||||
return { reports: data, error, isLoading, deleteReport };
|
||||
return {
|
||||
reports: data,
|
||||
error,
|
||||
isLoading,
|
||||
deleteReport,
|
||||
filter,
|
||||
page,
|
||||
pageSize,
|
||||
handleFilterChange,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
};
|
||||
}
|
||||
|
||||
export default useReports;
|
||||
|
@ -30,6 +30,22 @@ export const FILTER_RANGE = 'filter-range';
|
||||
export const FILTER_REFERRERS = 'filter-referrers';
|
||||
export const FILTER_PAGES = 'filter-pages';
|
||||
|
||||
export const USER_FILTER_TYPES = {
|
||||
all: 'All',
|
||||
username: 'Username',
|
||||
} as const;
|
||||
export const WEBSITE_FILTER_TYPES = { all: 'All', name: 'Name', domain: 'Domain' } as const;
|
||||
export const TEAM_FILTER_TYPES = { all: 'All', name: 'Name', 'user:username': 'Owner' } as const;
|
||||
export const REPORT_FILTER_TYPES = {
|
||||
all: 'All',
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
type: 'Type',
|
||||
'user:username': 'Username',
|
||||
'website:name': 'Website Name',
|
||||
'website:domain': 'Website Domain',
|
||||
} as const;
|
||||
|
||||
export const EVENT_COLUMNS = ['url', 'referrer', 'title', 'query', 'event'];
|
||||
|
||||
export const SESSION_COLUMNS = [
|
||||
|
@ -4,7 +4,7 @@ import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
|
||||
import { FILTER_COLUMNS, SESSION_COLUMNS } from './constants';
|
||||
import { loadWebsite } from './load';
|
||||
import { maxDate } from './date';
|
||||
import { QueryFilters, QueryOptions } from './types';
|
||||
import { QueryFilters, QueryOptions, SearchFilter } from './types';
|
||||
|
||||
const MYSQL_DATE_FORMATS = {
|
||||
minute: '%Y-%m-%d %H:%i:00',
|
||||
@ -128,6 +128,37 @@ async function rawQuery(sql: string, data: object): Promise<any> {
|
||||
return prisma.rawQuery(query, params);
|
||||
}
|
||||
|
||||
function getPageFilters(filters: SearchFilter<any>): [
|
||||
{
|
||||
orderBy: {
|
||||
[x: string]: string;
|
||||
}[];
|
||||
take: number;
|
||||
skip: number;
|
||||
},
|
||||
{
|
||||
pageSize: number;
|
||||
page: number;
|
||||
orderBy: string;
|
||||
},
|
||||
] {
|
||||
const { pageSize = 10, page = 1, orderBy } = filters;
|
||||
|
||||
return [
|
||||
{
|
||||
...(pageSize > 0 && { take: pageSize, skip: pageSize * (page - 1) }),
|
||||
...(orderBy && {
|
||||
orderBy: [
|
||||
{
|
||||
[orderBy]: 'asc',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{ pageSize, page: +page, orderBy },
|
||||
];
|
||||
}
|
||||
|
||||
export default {
|
||||
...prisma,
|
||||
getAddMinutesQuery,
|
||||
@ -135,5 +166,6 @@ export default {
|
||||
getTimestampIntervalQuery,
|
||||
getFilterQuery,
|
||||
parseFilters,
|
||||
getPageFilters,
|
||||
rawQuery,
|
||||
};
|
||||
|
55
lib/types.ts
55
lib/types.ts
@ -1,17 +1,62 @@
|
||||
import { NextApiRequest } from 'next';
|
||||
import { COLLECTION_TYPE, DATA_TYPE, EVENT_TYPE, KAFKA_TOPIC, ROLES } from './constants';
|
||||
import {
|
||||
COLLECTION_TYPE,
|
||||
DATA_TYPE,
|
||||
EVENT_TYPE,
|
||||
KAFKA_TOPIC,
|
||||
REPORT_FILTER_TYPES,
|
||||
ROLES,
|
||||
TEAM_FILTER_TYPES,
|
||||
USER_FILTER_TYPES,
|
||||
WEBSITE_FILTER_TYPES,
|
||||
} from './constants';
|
||||
|
||||
type ObjectValues<T> = T[keyof T];
|
||||
|
||||
export type CollectionType = ObjectValues<typeof COLLECTION_TYPE>;
|
||||
|
||||
export type Role = ObjectValues<typeof ROLES>;
|
||||
|
||||
export type EventType = ObjectValues<typeof EVENT_TYPE>;
|
||||
|
||||
export type DynamicDataType = ObjectValues<typeof DATA_TYPE>;
|
||||
|
||||
export type KafkaTopic = ObjectValues<typeof KAFKA_TOPIC>;
|
||||
export type ReportSearchFilterType = ObjectValues<typeof REPORT_FILTER_TYPES>;
|
||||
export type UserSearchFilterType = ObjectValues<typeof USER_FILTER_TYPES>;
|
||||
export type WebsiteSearchFilterType = ObjectValues<typeof WEBSITE_FILTER_TYPES>;
|
||||
export type TeamSearchFilterType = ObjectValues<typeof TEAM_FILTER_TYPES>;
|
||||
|
||||
export interface WebsiteSearchFilter extends SearchFilter<WebsiteSearchFilterType> {
|
||||
userId?: string;
|
||||
teamId?: string;
|
||||
includeTeams?: boolean;
|
||||
}
|
||||
|
||||
export interface UserSearchFilter extends SearchFilter<UserSearchFilterType> {
|
||||
teamId?: string;
|
||||
}
|
||||
|
||||
export interface TeamSearchFilter extends SearchFilter<TeamSearchFilterType> {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export interface ReportSearchFilter extends SearchFilter<ReportSearchFilterType> {
|
||||
userId?: string;
|
||||
websiteId?: string;
|
||||
}
|
||||
|
||||
export interface SearchFilter<T> {
|
||||
filter?: string;
|
||||
filterType?: T;
|
||||
pageSize?: number;
|
||||
page?: number;
|
||||
orderBy?: string;
|
||||
}
|
||||
|
||||
export interface FilterResult<T> {
|
||||
data: T;
|
||||
count: number;
|
||||
pageSize: number;
|
||||
page: number;
|
||||
orderBy?: string;
|
||||
}
|
||||
|
||||
export interface DynamicData {
|
||||
[key: string]: number | string | DynamicData | number[] | string[] | DynamicData[];
|
||||
|
@ -94,7 +94,7 @@
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"react": "^18.2.0",
|
||||
"react-basics": "^0.91.0",
|
||||
"react-basics": "^0.92.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.4",
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { useAuth, useCors } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { createReport, getWebsiteReports } from 'queries';
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import { useAuth, useCors } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { createReport, getReportsByWebsiteId } from 'queries';
|
||||
|
||||
export interface ReportsRequestQuery extends SearchFilter<ReportSearchFilterType> {}
|
||||
|
||||
export interface ReportRequestBody {
|
||||
websiteId: string;
|
||||
@ -35,7 +37,13 @@ export default async (
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const data = await getWebsiteReports(websiteId);
|
||||
const { page, filter, pageSize } = req.query;
|
||||
|
||||
const data = await getReportsByWebsiteId(websiteId, {
|
||||
page,
|
||||
filter,
|
||||
pageSize: +pageSize || null,
|
||||
});
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { canUpdateTeam, canViewTeam } from 'lib/auth';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { createTeamUser, getTeamUsers, getUserByUsername } from 'queries';
|
||||
import { createTeamUser, getUserByUsername, getUsersByTeamId } from 'queries';
|
||||
|
||||
export interface TeamUserRequestQuery {
|
||||
export interface TeamUserRequestQuery extends SearchFilter<TeamSearchFilterType> {
|
||||
id: string;
|
||||
}
|
||||
|
||||
@ -27,7 +27,13 @@ export default async (
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const users = await getTeamUsers(teamId);
|
||||
const { page, filter, pageSize } = req.query;
|
||||
|
||||
const users = await getUsersByTeamId(teamId, {
|
||||
page,
|
||||
filter,
|
||||
pageSize: +pageSize || null,
|
||||
});
|
||||
|
||||
return ok(res, users);
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { canViewTeam } from 'lib/auth';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { createTeamWebsites, getTeamWebsites } from 'queries/admin/teamWebsite';
|
||||
import { getWebsites, getWebsitesByTeamId } from 'queries';
|
||||
import { createTeamWebsites } from 'queries/admin/teamWebsite';
|
||||
|
||||
export interface TeamWebsiteRequestQuery {
|
||||
export interface TeamWebsiteRequestQuery extends SearchFilter<WebsiteSearchFilterType> {
|
||||
id: string;
|
||||
}
|
||||
|
||||
@ -26,7 +27,13 @@ export default async (
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const websites = await getTeamWebsites(teamId);
|
||||
const { page, filter, pageSize } = req.query;
|
||||
|
||||
const websites = await getWebsitesByTeamId(teamId, {
|
||||
page,
|
||||
filter,
|
||||
pageSize: +pageSize || null,
|
||||
});
|
||||
|
||||
return ok(res, websites);
|
||||
}
|
||||
|
@ -1,18 +1,19 @@
|
||||
import { Team } from '@prisma/client';
|
||||
import { NextApiRequestQueryBody } from 'lib/types';
|
||||
import { canCreateTeam } from 'lib/auth';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { createTeam, getUserTeams } from 'queries';
|
||||
import { createTeam, getTeamsByUserId } from 'queries';
|
||||
|
||||
export interface TeamsRequestBody {
|
||||
export interface TeamsRequestQuery extends SearchFilter<TeamSearchFilterType> {}
|
||||
export interface TeamsRequestBody extends SearchFilter<TeamSearchFilterType> {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default async (
|
||||
req: NextApiRequestQueryBody<any, TeamsRequestBody>,
|
||||
req: NextApiRequestQueryBody<TeamsRequestQuery, TeamsRequestBody>,
|
||||
res: NextApiResponse<Team[] | Team>,
|
||||
) => {
|
||||
await useAuth(req, res);
|
||||
@ -22,9 +23,11 @@ export default async (
|
||||
} = req.auth;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const teams = await getUserTeams(userId);
|
||||
const { page, filter, pageSize } = req.query;
|
||||
|
||||
return ok(res, teams);
|
||||
const results = await getTeamsByUserId(userId, { page, filter, pageSize: +pageSize || null });
|
||||
|
||||
return ok(res, results);
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { useAuth, useCors } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { getUserWebsites } from 'queries';
|
||||
import { getWebsitesByUserId } from 'queries';
|
||||
|
||||
export interface UserWebsitesRequestQuery extends SearchFilter<WebsiteSearchFilterType> {
|
||||
id: string;
|
||||
}
|
||||
export interface UserWebsitesRequestBody {
|
||||
name: string;
|
||||
domain: string;
|
||||
@ -17,16 +20,19 @@ export default async (
|
||||
await useCors(req, res);
|
||||
await useAuth(req, res);
|
||||
const { user } = req.auth;
|
||||
const { id: userId } = req.query;
|
||||
const { id: userId, page, filter, pageSize, includeTeams } = req.query;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (!user.isAdmin && user.id !== userId) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { includeTeams } = req.query;
|
||||
|
||||
const websites = await getUserWebsites(userId, { includeTeams });
|
||||
const websites = await getWebsitesByUserId(userId, {
|
||||
page,
|
||||
filter,
|
||||
pageSize: +pageSize || null,
|
||||
includeTeams,
|
||||
});
|
||||
|
||||
return ok(res, websites);
|
||||
}
|
||||
|
@ -2,11 +2,12 @@ import { canCreateUser, canViewUsers } from 'lib/auth';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, Role, User } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, Role, SearchFilter, User, UserSearchFilterType } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { createUser, getUserByUsername, getUsers } from 'queries';
|
||||
|
||||
export interface UsersRequestQuery extends SearchFilter<UserSearchFilterType> {}
|
||||
export interface UsersRequestBody {
|
||||
username: string;
|
||||
password: string;
|
||||
@ -15,7 +16,7 @@ export interface UsersRequestBody {
|
||||
}
|
||||
|
||||
export default async (
|
||||
req: NextApiRequestQueryBody<any, UsersRequestBody>,
|
||||
req: NextApiRequestQueryBody<UsersRequestQuery, UsersRequestBody>,
|
||||
res: NextApiResponse<User[] | User>,
|
||||
) => {
|
||||
await useAuth(req, res);
|
||||
@ -25,7 +26,9 @@ export default async (
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const users = await getUsers();
|
||||
const { page, filter, pageSize } = req.query;
|
||||
|
||||
const users = await getUsers({ page, filter, pageSize: +pageSize || null });
|
||||
|
||||
return ok(res, users);
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { canCreateWebsite } from 'lib/auth';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import { useAuth, useCors } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { createWebsite } from 'queries';
|
||||
import userWebsites from 'pages/api/users/[id]/websites';
|
||||
|
||||
export interface WebsitesRequestQuery extends SearchFilter<WebsiteSearchFilterType> {}
|
||||
|
||||
export interface WebsitesRequestBody {
|
||||
name: string;
|
||||
domain: string;
|
||||
@ -14,7 +16,7 @@ export interface WebsitesRequestBody {
|
||||
}
|
||||
|
||||
export default async (
|
||||
req: NextApiRequestQueryBody<any, WebsitesRequestBody>,
|
||||
req: NextApiRequestQueryBody<WebsitesRequestQuery, WebsitesRequestBody>,
|
||||
res: NextApiResponse,
|
||||
) => {
|
||||
await useCors(req, res);
|
||||
@ -26,6 +28,7 @@ export default async (
|
||||
|
||||
if (req.method === 'GET') {
|
||||
req.query.id = userId;
|
||||
req.query.pageSize = 100;
|
||||
|
||||
return userWebsites(req, res);
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { Prisma, Report } from '@prisma/client';
|
||||
import { REPORT_FILTER_TYPES } from 'lib/constants';
|
||||
import prisma from 'lib/prisma';
|
||||
import { FilterResult, ReportSearchFilter, ReportSearchFilterType, SearchFilter } from 'lib/types';
|
||||
|
||||
export async function createReport(data: Prisma.ReportUncheckedCreateInput): Promise<Report> {
|
||||
return prisma.client.report.create({ data });
|
||||
@ -13,22 +15,6 @@ export async function getReportById(reportId: string): Promise<Report> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserReports(userId: string): Promise<Report[]> {
|
||||
return prisma.client.report.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWebsiteReports(websiteId: string): Promise<Report[]> {
|
||||
return prisma.client.report.findMany({
|
||||
where: {
|
||||
websiteId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateReport(
|
||||
reportId: string,
|
||||
data: Prisma.ReportUpdateInput,
|
||||
@ -39,3 +25,103 @@ export async function updateReport(
|
||||
export async function deleteReport(reportId: string): Promise<Report> {
|
||||
return prisma.client.report.delete({ where: { id: reportId } });
|
||||
}
|
||||
|
||||
export async function getReports(
|
||||
ReportSearchFilter: ReportSearchFilter,
|
||||
): Promise<FilterResult<Report[]>> {
|
||||
const { userId, websiteId, filter, filterType = REPORT_FILTER_TYPES.all } = ReportSearchFilter;
|
||||
const where: Prisma.ReportWhereInput = {
|
||||
...(userId && { userId: userId }),
|
||||
...(websiteId && { websiteId: websiteId }),
|
||||
...(filter && {
|
||||
AND: {
|
||||
OR: [
|
||||
{
|
||||
...((filterType === REPORT_FILTER_TYPES.all ||
|
||||
filterType === REPORT_FILTER_TYPES.name) && {
|
||||
name: {
|
||||
startsWith: filter,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === REPORT_FILTER_TYPES.all ||
|
||||
filterType === REPORT_FILTER_TYPES.description) && {
|
||||
description: {
|
||||
startsWith: filter,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === REPORT_FILTER_TYPES.all ||
|
||||
filterType === REPORT_FILTER_TYPES.type) && {
|
||||
type: {
|
||||
startsWith: filter,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === REPORT_FILTER_TYPES.all ||
|
||||
filterType === REPORT_FILTER_TYPES['user:username']) && {
|
||||
user: {
|
||||
username: {
|
||||
startsWith: filter,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === REPORT_FILTER_TYPES.all ||
|
||||
filterType === REPORT_FILTER_TYPES['website:name']) && {
|
||||
website: {
|
||||
name: {
|
||||
startsWith: filter,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === REPORT_FILTER_TYPES.all ||
|
||||
filterType === REPORT_FILTER_TYPES['website:domain']) && {
|
||||
website: {
|
||||
domain: {
|
||||
startsWith: filter,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const [pageFilters, getParameters] = prisma.getPageFilters(ReportSearchFilter);
|
||||
|
||||
const reports = await prisma.client.report.findMany({
|
||||
where,
|
||||
...pageFilters,
|
||||
});
|
||||
const count = await prisma.client.report.count({
|
||||
where,
|
||||
});
|
||||
|
||||
return {
|
||||
data: reports,
|
||||
count,
|
||||
...getParameters,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getReportsByUserId(
|
||||
userId: string,
|
||||
filter: SearchFilter<ReportSearchFilterType>,
|
||||
): Promise<FilterResult<Report[]>> {
|
||||
return getReports({ userId, ...filter });
|
||||
}
|
||||
|
||||
export async function getReportsByWebsiteId(
|
||||
websiteId: string,
|
||||
filter: SearchFilter<ReportSearchFilterType>,
|
||||
): Promise<FilterResult<Report[]>> {
|
||||
return getReports({ websiteId, ...filter });
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Prisma, Team } from '@prisma/client';
|
||||
import prisma from 'lib/prisma';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import { ROLES, TEAM_FILTER_TYPES } from 'lib/constants';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import { FilterResult, TeamSearchFilter, TeamSearchFilterType, SearchFilter } from 'lib/types';
|
||||
|
||||
export interface GetTeamOptions {
|
||||
includeTeamUser?: boolean;
|
||||
@ -26,12 +27,6 @@ export function getTeamByAccessCode(accessCode: string, options: GetTeamOptions
|
||||
return getTeam({ accessCode }, options);
|
||||
}
|
||||
|
||||
export async function getTeams(where: Prisma.TeamWhereInput): Promise<Team[]> {
|
||||
return prisma.client.team.findMany({
|
||||
where,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createTeam(data: Prisma.TeamCreateInput, userId: string): Promise<Team> {
|
||||
const { id } = data;
|
||||
|
||||
@ -85,3 +80,82 @@ export async function deleteTeam(
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function getTeams(
|
||||
TeamSearchFilter: TeamSearchFilter,
|
||||
options?: { include?: Prisma.TeamInclude },
|
||||
): Promise<FilterResult<Team[]>> {
|
||||
const { userId, filter, filterType = TEAM_FILTER_TYPES.all } = TeamSearchFilter;
|
||||
const where: Prisma.TeamWhereInput = {
|
||||
...(userId && {
|
||||
teamUser: {
|
||||
some: { userId },
|
||||
},
|
||||
}),
|
||||
...(filter && {
|
||||
AND: {
|
||||
OR: [
|
||||
{
|
||||
...((filterType === TEAM_FILTER_TYPES.all || filterType === TEAM_FILTER_TYPES.name) && {
|
||||
name: { startsWith: filter },
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === TEAM_FILTER_TYPES.all ||
|
||||
filterType === TEAM_FILTER_TYPES['user:username']) && {
|
||||
teamUser: {
|
||||
every: {
|
||||
role: ROLES.teamOwner,
|
||||
user: {
|
||||
username: {
|
||||
startsWith: filter,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const [pageFilters, getParameters] = prisma.getPageFilters({
|
||||
orderBy: 'name',
|
||||
...TeamSearchFilter,
|
||||
});
|
||||
|
||||
const teams = await prisma.client.team.findMany({
|
||||
where: {
|
||||
...where,
|
||||
},
|
||||
...pageFilters,
|
||||
...(options?.include && { include: options?.include }),
|
||||
});
|
||||
const count = await prisma.client.team.count({ where });
|
||||
|
||||
return { data: teams, count, ...getParameters };
|
||||
}
|
||||
|
||||
export async function getTeamsByUserId(
|
||||
userId: string,
|
||||
filter?: SearchFilter<TeamSearchFilterType>,
|
||||
): Promise<FilterResult<Team[]>> {
|
||||
return getTeams(
|
||||
{ userId, ...filter },
|
||||
{
|
||||
include: {
|
||||
teamUser: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Prisma, Team, TeamUser } from '@prisma/client';
|
||||
import { getRandomChars } from 'next-basics';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import cache from 'lib/cache';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import { ROLES, USER_FILTER_TYPES } from 'lib/constants';
|
||||
import prisma from 'lib/prisma';
|
||||
import { Website, User, Role } from 'lib/types';
|
||||
import { FilterResult, Role, User, UserSearchFilter } from 'lib/types';
|
||||
import { getRandomChars } from 'next-basics';
|
||||
|
||||
export interface GetUserOptions {
|
||||
includePassword?: boolean;
|
||||
@ -36,125 +36,59 @@ export async function getUserByUsername(username: string, options: GetUserOption
|
||||
return getUser({ username }, options);
|
||||
}
|
||||
|
||||
export async function getUsers(): Promise<User[]> {
|
||||
return prisma.client.user.findMany({
|
||||
take: 100,
|
||||
where: {
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
username: 'asc',
|
||||
},
|
||||
],
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
role: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserTeams(userId: string): Promise<
|
||||
(Team & {
|
||||
teamUser: (TeamUser & {
|
||||
user: { id: string; username: string };
|
||||
})[];
|
||||
})[]
|
||||
> {
|
||||
return prisma.client.team.findMany({
|
||||
where: {
|
||||
export async function getUsers(
|
||||
UserSearchFilter: UserSearchFilter = {},
|
||||
options?: { include?: Prisma.UserInclude },
|
||||
): Promise<FilterResult<User[]>> {
|
||||
const { teamId, filter, filterType = USER_FILTER_TYPES.all } = UserSearchFilter;
|
||||
const where: Prisma.UserWhereInput = {
|
||||
...(teamId && {
|
||||
teamUser: {
|
||||
some: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamUser: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
},
|
||||
}),
|
||||
...(filter && {
|
||||
AND: {
|
||||
OR: [
|
||||
{
|
||||
...((filterType === USER_FILTER_TYPES.all ||
|
||||
filterType === USER_FILTER_TYPES.username) && {
|
||||
username: {
|
||||
startsWith: filter,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
const [pageFilters, getParameters] = prisma.getPageFilters({
|
||||
orderBy: 'username',
|
||||
...UserSearchFilter,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserWebsites(
|
||||
userId: string,
|
||||
options?: { includeTeams: boolean },
|
||||
): Promise<Website[]> {
|
||||
const { rawQuery } = prisma;
|
||||
|
||||
if (options?.includeTeams) {
|
||||
const websites = await rawQuery(
|
||||
`
|
||||
select
|
||||
website_id as "id",
|
||||
name,
|
||||
domain,
|
||||
share_id as "shareId",
|
||||
reset_at as "resetAt",
|
||||
user_id as "userId",
|
||||
created_at as "createdAt",
|
||||
updated_at as "updatedAt",
|
||||
deleted_at as "deletedAt",
|
||||
null as "teamId",
|
||||
null as "teamName"
|
||||
from website
|
||||
where user_id = {{userId::uuid}}
|
||||
and deleted_at is null
|
||||
union
|
||||
select
|
||||
w.website_id as "id",
|
||||
w.name,
|
||||
w.domain,
|
||||
w.share_id as "shareId",
|
||||
w.reset_at as "resetAt",
|
||||
w.user_id as "userId",
|
||||
w.created_at as "createdAt",
|
||||
w.updated_at as "updatedAt",
|
||||
w.deleted_at as "deletedAt",
|
||||
t.team_id as "teamId",
|
||||
t.name as "teamName"
|
||||
from website w
|
||||
inner join team_website tw
|
||||
on tw.website_id = w.website_id
|
||||
inner join team t
|
||||
on t.team_id = tw.team_id
|
||||
inner join team_user tu
|
||||
on tu.team_id = tw.team_id
|
||||
where tu.user_id = {{userId::uuid}}
|
||||
and w.deleted_at is null
|
||||
`,
|
||||
{ userId },
|
||||
);
|
||||
|
||||
return websites.reduce((arr, item) => {
|
||||
if (!arr.find(({ id }) => id === item.id)) {
|
||||
return arr.concat(item);
|
||||
}
|
||||
return arr;
|
||||
}, []);
|
||||
}
|
||||
|
||||
return prisma.client.website.findMany({
|
||||
const users = await prisma.client.user.findMany({
|
||||
where: {
|
||||
userId,
|
||||
...where,
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
name: 'asc',
|
||||
},
|
||||
],
|
||||
...pageFilters,
|
||||
...(options?.include && { include: options.include }),
|
||||
});
|
||||
const count = await prisma.client.user.count({
|
||||
where: {
|
||||
...where,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
return { data: users as any, count, ...getParameters };
|
||||
}
|
||||
|
||||
export async function getUsersByTeamId(teamId: string, filter?: UserSearchFilter) {
|
||||
return getUsers({ teamId, ...filter });
|
||||
}
|
||||
|
||||
export async function createUser(data: {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Prisma, Website } from '@prisma/client';
|
||||
import cache from 'lib/cache';
|
||||
import { ROLES, WEBSITE_FILTER_TYPES } from 'lib/constants';
|
||||
import prisma from 'lib/prisma';
|
||||
import { FilterResult, WebsiteSearchFilter } from 'lib/types';
|
||||
|
||||
async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise<Website> {
|
||||
return prisma.client.website.findUnique({
|
||||
@ -16,11 +18,199 @@ export async function getWebsiteByShareId(shareId: string) {
|
||||
return getWebsite({ shareId });
|
||||
}
|
||||
|
||||
export async function getWebsites(): Promise<Website[]> {
|
||||
return prisma.client.website.findMany({
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
export async function getWebsites(
|
||||
WebsiteSearchFilter: WebsiteSearchFilter,
|
||||
options?: { include?: Prisma.WebsiteInclude },
|
||||
): Promise<FilterResult<Website[]>> {
|
||||
const {
|
||||
userId,
|
||||
teamId,
|
||||
includeTeams,
|
||||
filter,
|
||||
filterType = WEBSITE_FILTER_TYPES.all,
|
||||
} = WebsiteSearchFilter;
|
||||
|
||||
const filterQuery = {
|
||||
AND: {
|
||||
OR: [
|
||||
{
|
||||
...((filterType === WEBSITE_FILTER_TYPES.all ||
|
||||
filterType === WEBSITE_FILTER_TYPES.name) && {
|
||||
name: { startsWith: filter },
|
||||
}),
|
||||
},
|
||||
{
|
||||
...((filterType === WEBSITE_FILTER_TYPES.all ||
|
||||
filterType === WEBSITE_FILTER_TYPES.domain) && {
|
||||
domain: { startsWith: filter },
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const where: Prisma.WebsiteWhereInput = {
|
||||
...(teamId && {
|
||||
teamWebsite: {
|
||||
some: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
AND: {
|
||||
OR: [
|
||||
{
|
||||
...(userId && {
|
||||
userId,
|
||||
}),
|
||||
},
|
||||
{
|
||||
...(includeTeams && {
|
||||
teamWebsite: {
|
||||
some: {
|
||||
team: {
|
||||
teamUser: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
...(filter && filterQuery),
|
||||
};
|
||||
|
||||
const [pageFilters, getParameters] = prisma.getPageFilters({
|
||||
orderBy: 'name',
|
||||
...WebsiteSearchFilter,
|
||||
});
|
||||
|
||||
const websites = await prisma.client.website.findMany({
|
||||
where: {
|
||||
...where,
|
||||
deletedAt: null,
|
||||
},
|
||||
...pageFilters,
|
||||
...(options?.include && { include: options.include }),
|
||||
});
|
||||
const count = await prisma.client.website.count({ where });
|
||||
|
||||
return { data: websites, count, ...getParameters };
|
||||
}
|
||||
|
||||
export async function getWebsitesByUserId(
|
||||
userId: string,
|
||||
filter?: WebsiteSearchFilter,
|
||||
): Promise<FilterResult<Website[]>> {
|
||||
return getWebsites({ userId, ...filter });
|
||||
}
|
||||
|
||||
export async function getWebsitesByTeamId(
|
||||
teamId: string,
|
||||
filter?: WebsiteSearchFilter,
|
||||
): Promise<FilterResult<Website[]>> {
|
||||
return getWebsites(
|
||||
{
|
||||
teamId,
|
||||
...filter,
|
||||
includeTeams: true,
|
||||
},
|
||||
{
|
||||
include: {
|
||||
teamWebsite: {
|
||||
include: {
|
||||
team: {
|
||||
include: {
|
||||
teamUser: {
|
||||
where: { role: ROLES.teamOwner },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function getUserWebsites(
|
||||
userId: string,
|
||||
options?: { includeTeams: boolean },
|
||||
): Promise<Website[]> {
|
||||
const { rawQuery } = prisma;
|
||||
|
||||
if (options?.includeTeams) {
|
||||
const websites = await rawQuery(
|
||||
`
|
||||
select
|
||||
website_id as "id",
|
||||
name,
|
||||
domain,
|
||||
share_id as "shareId",
|
||||
reset_at as "resetAt",
|
||||
user_id as "userId",
|
||||
created_at as "createdAt",
|
||||
updated_at as "updatedAt",
|
||||
deleted_at as "deletedAt",
|
||||
null as "teamId",
|
||||
null as "teamName"
|
||||
from website
|
||||
where user_id = {{userId::uuid}}
|
||||
and deleted_at is null
|
||||
union
|
||||
select
|
||||
w.website_id as "id",
|
||||
w.name,
|
||||
w.domain,
|
||||
w.share_id as "shareId",
|
||||
w.reset_at as "resetAt",
|
||||
w.user_id as "userId",
|
||||
w.created_at as "createdAt",
|
||||
w.updated_at as "updatedAt",
|
||||
w.deleted_at as "deletedAt",
|
||||
t.team_id as "teamId",
|
||||
t.name as "teamName"
|
||||
from website w
|
||||
inner join team_website tw
|
||||
on tw.website_id = w.website_id
|
||||
inner join team t
|
||||
on t.team_id = tw.team_id
|
||||
inner join team_user tu
|
||||
on tu.team_id = tw.team_id
|
||||
where tu.user_id = {{userId::uuid}}
|
||||
and w.deleted_at is null
|
||||
`,
|
||||
{ userId },
|
||||
);
|
||||
|
||||
return websites.reduce((arr, item) => {
|
||||
if (!arr.find(({ id }) => id === item.id)) {
|
||||
return arr.concat(item);
|
||||
}
|
||||
return arr;
|
||||
}, []);
|
||||
}
|
||||
|
||||
return prisma.client.website.findMany({
|
||||
where: {
|
||||
userId,
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
name: 'asc',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -7557,10 +7557,10 @@ rc@^1.2.7:
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-basics@^0.91.0:
|
||||
version "0.91.0"
|
||||
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.91.0.tgz#2970529a22a455ec73a1be884eb93a109c9dafc0"
|
||||
integrity sha512-vP8LYWiFwA+eguMEuHvHct4Jl5R/2GUjWc1tMujDG0CsAAUGhx68tAJr0K3gBrWjmpJrTPVfX8SdBNKSDAjQsw==
|
||||
react-basics@^0.92.0:
|
||||
version "0.92.0"
|
||||
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.92.0.tgz#02bc6e88bdaf189c30cc6cbd8bbb1c9d12cd089b"
|
||||
integrity sha512-BVUWg5a7R88konA9NedYMBx1hl50d6h/MD7qlKOEO/Cnm8cOC7AYTRKAKhO6kHMWjY4ZpUuvlg0UcF+SJP/uXA==
|
||||
dependencies:
|
||||
classnames "^2.3.1"
|
||||
date-fns "^2.29.3"
|
||||
|
Loading…
Reference in New Issue
Block a user