mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
feat: search documents by name or recipient name or recipient email (#1384)
This commit is contained in:
@ -16,6 +16,7 @@ import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-documen
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
|
|
||||||
|
import { DocumentSearch } from '~/components/(dashboard)/document-search/document-search';
|
||||||
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
||||||
import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
|
import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
|
||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
@ -25,16 +26,17 @@ import { DataTableSenderFilter } from './data-table-sender-filter';
|
|||||||
import { EmptyDocumentState } from './empty-state';
|
import { EmptyDocumentState } from './empty-state';
|
||||||
import { UploadDocument } from './upload-document';
|
import { UploadDocument } from './upload-document';
|
||||||
|
|
||||||
export type DocumentsPageViewProps = {
|
export interface DocumentsPageViewProps {
|
||||||
searchParams?: {
|
searchParams?: {
|
||||||
status?: ExtendedDocumentStatus;
|
status?: ExtendedDocumentStatus;
|
||||||
period?: PeriodSelectorValue;
|
period?: PeriodSelectorValue;
|
||||||
page?: string;
|
page?: string;
|
||||||
perPage?: string;
|
perPage?: string;
|
||||||
senderIds?: string;
|
senderIds?: string;
|
||||||
|
search?: string;
|
||||||
};
|
};
|
||||||
team?: Team & { teamEmail?: TeamEmail | null } & { currentTeamMember?: { role: TeamMemberRole } };
|
team?: Team & { teamEmail?: TeamEmail | null } & { currentTeamMember?: { role: TeamMemberRole } };
|
||||||
};
|
}
|
||||||
|
|
||||||
export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPageViewProps) => {
|
export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPageViewProps) => {
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
@ -44,6 +46,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
|||||||
const page = Number(searchParams.page) || 1;
|
const page = Number(searchParams.page) || 1;
|
||||||
const perPage = Number(searchParams.perPage) || 20;
|
const perPage = Number(searchParams.perPage) || 20;
|
||||||
const senderIds = parseToIntegerArray(searchParams.senderIds ?? '');
|
const senderIds = parseToIntegerArray(searchParams.senderIds ?? '');
|
||||||
|
const search = searchParams.search || '';
|
||||||
const currentTeam = team
|
const currentTeam = team
|
||||||
? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
|
? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
|
||||||
: undefined;
|
: undefined;
|
||||||
@ -52,6 +55,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
|||||||
const getStatOptions: GetStatsInput = {
|
const getStatOptions: GetStatsInput = {
|
||||||
user,
|
user,
|
||||||
period,
|
period,
|
||||||
|
search,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (team) {
|
if (team) {
|
||||||
@ -79,6 +83,7 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
|||||||
perPage,
|
perPage,
|
||||||
period,
|
period,
|
||||||
senderIds,
|
senderIds,
|
||||||
|
search,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getTabHref = (value: typeof status) => {
|
const getTabHref = (value: typeof status) => {
|
||||||
@ -148,6 +153,9 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa
|
|||||||
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
|
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
|
||||||
<PeriodSelector />
|
<PeriodSelector />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
|
||||||
|
<DocumentSearch initialValue={search} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
|
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
|
||||||
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
|
||||||
|
export const DocumentSearch = ({ initialValue = '' }: { initialValue?: string }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const [searchTerm, setSearchTerm] = useState(initialValue);
|
||||||
|
const debouncedSearchTerm = useDebouncedValue(searchTerm, 500);
|
||||||
|
|
||||||
|
const handleSearch = useCallback(
|
||||||
|
(term: string) => {
|
||||||
|
const params = new URLSearchParams(searchParams?.toString() ?? '');
|
||||||
|
if (term) {
|
||||||
|
params.set('search', term);
|
||||||
|
} else {
|
||||||
|
params.delete('search');
|
||||||
|
}
|
||||||
|
router.push(`?${params.toString()}`);
|
||||||
|
},
|
||||||
|
[router, searchParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleSearch(searchTerm);
|
||||||
|
}, [debouncedSearchTerm]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="search"
|
||||||
|
placeholder="Search documents..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
249
packages/app-tests/e2e/teams/search-documents.spec.ts
Normal file
249
packages/app-tests/e2e/teams/search-documents.spec.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||||
|
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
|
||||||
|
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
|
||||||
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
|
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||||
|
import { checkDocumentTabCount } from '../fixtures/documents';
|
||||||
|
|
||||||
|
test('[TEAMS]: search respects team document visibility', async ({ page }) => {
|
||||||
|
const team = await seedTeam();
|
||||||
|
const adminUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.ADMIN });
|
||||||
|
const managerUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MANAGER });
|
||||||
|
const memberUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER });
|
||||||
|
|
||||||
|
await seedDocuments([
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'EVERYONE',
|
||||||
|
title: 'Searchable Document for Everyone',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'MANAGER_AND_ABOVE',
|
||||||
|
title: 'Searchable Document for Managers',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'ADMIN',
|
||||||
|
title: 'Searchable Document for Admins',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{ user: adminUser, visibleDocs: 3 },
|
||||||
|
{ user: managerUser, visibleDocs: 2 },
|
||||||
|
{ user: memberUser, visibleDocs: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { user, visibleDocs } of testCases) {
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Search documents...').fill('Searchable');
|
||||||
|
await page.waitForURL(/search=Searchable/);
|
||||||
|
|
||||||
|
await checkDocumentTabCount(page, 'All', visibleDocs);
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[TEAMS]: search does not reveal documents from other teams', async ({ page }) => {
|
||||||
|
const { team: teamA, teamMember2: teamAMember } = await seedTeamDocuments();
|
||||||
|
const { team: teamB } = await seedTeamDocuments();
|
||||||
|
|
||||||
|
await seedDocuments([
|
||||||
|
{
|
||||||
|
sender: teamA.owner,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: teamA.id,
|
||||||
|
visibility: 'EVERYONE',
|
||||||
|
title: 'Unique Team A Document',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sender: teamB.owner,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: teamB.id,
|
||||||
|
visibility: 'EVERYONE',
|
||||||
|
title: 'Unique Team B Document',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: teamAMember.email,
|
||||||
|
redirectPath: `/t/${teamA.url}/documents`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Search documents...').fill('Unique');
|
||||||
|
await page.waitForURL(/search=Unique/);
|
||||||
|
|
||||||
|
await checkDocumentTabCount(page, 'All', 1);
|
||||||
|
await expect(page.getByRole('link', { name: 'Unique Team A Document' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: 'Unique Team B Document' })).not.toBeVisible();
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[PERSONAL]: search does not reveal team documents in personal account', async ({ page }) => {
|
||||||
|
const { team, teamMember2 } = await seedTeamDocuments();
|
||||||
|
|
||||||
|
await seedDocuments([
|
||||||
|
{
|
||||||
|
sender: teamMember2,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: null,
|
||||||
|
title: 'Personal Unique Document',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'EVERYONE',
|
||||||
|
title: 'Team Unique Document',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: teamMember2.email,
|
||||||
|
redirectPath: '/documents',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Search documents...').fill('Unique');
|
||||||
|
await page.waitForURL(/search=Unique/);
|
||||||
|
|
||||||
|
await checkDocumentTabCount(page, 'All', 1);
|
||||||
|
await expect(page.getByRole('link', { name: 'Personal Unique Document' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: 'Team Unique Document' })).not.toBeVisible();
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[TEAMS]: search respects recipient visibility regardless of team visibility', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const team = await seedTeam();
|
||||||
|
const memberUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER });
|
||||||
|
|
||||||
|
await seedDocuments([
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [memberUser],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'ADMIN',
|
||||||
|
title: 'Admin Document with Member Recipient',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: memberUser.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Search documents...').fill('Admin Document');
|
||||||
|
await page.waitForURL(/search=Admin(%20|\+|\s)Document/);
|
||||||
|
|
||||||
|
await checkDocumentTabCount(page, 'All', 1);
|
||||||
|
await expect(
|
||||||
|
page.getByRole('link', { name: 'Admin Document with Member Recipient' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[TEAMS]: search by recipient name respects visibility', async ({ page }) => {
|
||||||
|
const team = await seedTeam();
|
||||||
|
const adminUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.ADMIN });
|
||||||
|
const memberUser = await seedTeamMember({
|
||||||
|
teamId: team.id,
|
||||||
|
role: TeamMemberRole.MEMBER,
|
||||||
|
name: 'Team Member',
|
||||||
|
});
|
||||||
|
|
||||||
|
const uniqueRecipient = await seedUser();
|
||||||
|
|
||||||
|
await seedDocuments([
|
||||||
|
{
|
||||||
|
sender: team.owner,
|
||||||
|
recipients: [uniqueRecipient],
|
||||||
|
type: DocumentStatus.COMPLETED,
|
||||||
|
documentOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
visibility: 'ADMIN',
|
||||||
|
title: 'Admin Document for Unique Recipient',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Admin should see the document when searching by recipient name
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: adminUser.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Search documents...').fill('Unique Recipient');
|
||||||
|
await page.waitForURL(/search=Unique(%20|\+|\s)Recipient/);
|
||||||
|
|
||||||
|
await checkDocumentTabCount(page, 'All', 1);
|
||||||
|
await expect(
|
||||||
|
page.getByRole('link', { name: 'Admin Document for Unique Recipient' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
|
||||||
|
// Member should not see the document when searching by recipient name
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: memberUser.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Search documents...').fill('Unique Recipient');
|
||||||
|
await page.waitForURL(/search=Unique(%20|\+|\s)Recipient/);
|
||||||
|
|
||||||
|
await checkDocumentTabCount(page, 'All', 0);
|
||||||
|
await expect(
|
||||||
|
page.getByRole('link', { name: 'Admin Document for Unique Recipient' }),
|
||||||
|
).not.toBeVisible();
|
||||||
|
|
||||||
|
await apiSignout({ page });
|
||||||
|
});
|
||||||
@ -25,6 +25,7 @@ export type FindDocumentsOptions = {
|
|||||||
};
|
};
|
||||||
period?: PeriodSelectorValue;
|
period?: PeriodSelectorValue;
|
||||||
senderIds?: number[];
|
senderIds?: number[];
|
||||||
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findDocuments = async ({
|
export const findDocuments = async ({
|
||||||
@ -37,6 +38,7 @@ export const findDocuments = async ({
|
|||||||
orderBy,
|
orderBy,
|
||||||
period,
|
period,
|
||||||
senderIds,
|
senderIds,
|
||||||
|
search,
|
||||||
}: FindDocumentsOptions) => {
|
}: FindDocumentsOptions) => {
|
||||||
const { user, team } = await prisma.$transaction(async (tx) => {
|
const { user, team } = await prisma.$transaction(async (tx) => {
|
||||||
const user = await tx.user.findFirstOrThrow({
|
const user = await tx.user.findFirstOrThrow({
|
||||||
@ -92,6 +94,14 @@ export const findDocuments = async ({
|
|||||||
})
|
})
|
||||||
.otherwise(() => undefined);
|
.otherwise(() => undefined);
|
||||||
|
|
||||||
|
const searchFilter: Prisma.DocumentWhereInput = {
|
||||||
|
OR: [
|
||||||
|
{ title: { contains: search, mode: 'insensitive' } },
|
||||||
|
{ Recipient: { some: { name: { contains: search, mode: 'insensitive' } } } },
|
||||||
|
{ Recipient: { some: { email: { contains: search, mode: 'insensitive' } } } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const visibilityFilters = [
|
const visibilityFilters = [
|
||||||
match(teamMemberRole)
|
match(teamMemberRole)
|
||||||
.with(TeamMemberRole.ADMIN, () => ({
|
.with(TeamMemberRole.ADMIN, () => ({
|
||||||
@ -188,7 +198,7 @@ export const findDocuments = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const whereClause: Prisma.DocumentWhereInput = {
|
const whereClause: Prisma.DocumentWhereInput = {
|
||||||
AND: [{ ...termFilters }, { ...filters }, { ...deletedFilter }],
|
AND: [{ ...termFilters }, { ...filters }, { ...deletedFilter }, { ...searchFilter }],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (period) {
|
if (period) {
|
||||||
|
|||||||
@ -15,9 +15,10 @@ export type GetStatsInput = {
|
|||||||
user: User;
|
user: User;
|
||||||
team?: Omit<GetTeamCountsOption, 'createdAt'>;
|
team?: Omit<GetTeamCountsOption, 'createdAt'>;
|
||||||
period?: PeriodSelectorValue;
|
period?: PeriodSelectorValue;
|
||||||
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStats = async ({ user, period, ...options }: GetStatsInput) => {
|
export const getStats = async ({ user, period, search, ...options }: GetStatsInput) => {
|
||||||
let createdAt: Prisma.DocumentWhereInput['createdAt'];
|
let createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||||
|
|
||||||
if (period) {
|
if (period) {
|
||||||
@ -31,8 +32,14 @@ export const getStats = async ({ user, period, ...options }: GetStatsInput) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [ownerCounts, notSignedCounts, hasSignedCounts] = await (options.team
|
const [ownerCounts, notSignedCounts, hasSignedCounts] = await (options.team
|
||||||
? getTeamCounts({ ...options.team, createdAt, currentUserEmail: user.email, userId: user.id })
|
? getTeamCounts({
|
||||||
: getCounts({ user, createdAt }));
|
...options.team,
|
||||||
|
createdAt,
|
||||||
|
currentUserEmail: user.email,
|
||||||
|
userId: user.id,
|
||||||
|
search,
|
||||||
|
})
|
||||||
|
: getCounts({ user, createdAt, search }));
|
||||||
|
|
||||||
const stats: Record<ExtendedDocumentStatus, number> = {
|
const stats: Record<ExtendedDocumentStatus, number> = {
|
||||||
[ExtendedDocumentStatus.DRAFT]: 0,
|
[ExtendedDocumentStatus.DRAFT]: 0,
|
||||||
@ -72,9 +79,18 @@ export const getStats = async ({ user, period, ...options }: GetStatsInput) => {
|
|||||||
type GetCountsOption = {
|
type GetCountsOption = {
|
||||||
user: User;
|
user: User;
|
||||||
createdAt: Prisma.DocumentWhereInput['createdAt'];
|
createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||||
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCounts = async ({ user, createdAt }: GetCountsOption) => {
|
const getCounts = async ({ user, createdAt, search }: GetCountsOption) => {
|
||||||
|
const searchFilter: Prisma.DocumentWhereInput = {
|
||||||
|
OR: [
|
||||||
|
{ title: { contains: search, mode: 'insensitive' } },
|
||||||
|
{ Recipient: { some: { name: { contains: search, mode: 'insensitive' } } } },
|
||||||
|
{ Recipient: { some: { email: { contains: search, mode: 'insensitive' } } } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
// Owner counts.
|
// Owner counts.
|
||||||
prisma.document.groupBy({
|
prisma.document.groupBy({
|
||||||
@ -87,6 +103,7 @@ const getCounts = async ({ user, createdAt }: GetCountsOption) => {
|
|||||||
createdAt,
|
createdAt,
|
||||||
teamId: null,
|
teamId: null,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
|
AND: [searchFilter],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
// Not signed counts.
|
// Not signed counts.
|
||||||
@ -105,6 +122,7 @@ const getCounts = async ({ user, createdAt }: GetCountsOption) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
createdAt,
|
createdAt,
|
||||||
|
AND: [searchFilter],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
// Has signed counts.
|
// Has signed counts.
|
||||||
@ -142,6 +160,7 @@ const getCounts = async ({ user, createdAt }: GetCountsOption) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
AND: [searchFilter],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@ -155,6 +174,7 @@ type GetTeamCountsOption = {
|
|||||||
userId: number;
|
userId: number;
|
||||||
createdAt: Prisma.DocumentWhereInput['createdAt'];
|
createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||||
currentTeamMemberRole?: TeamMemberRole;
|
currentTeamMemberRole?: TeamMemberRole;
|
||||||
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTeamCounts = async (options: GetTeamCountsOption) => {
|
const getTeamCounts = async (options: GetTeamCountsOption) => {
|
||||||
@ -169,6 +189,14 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
|||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const searchFilter: Prisma.DocumentWhereInput = {
|
||||||
|
OR: [
|
||||||
|
{ title: { contains: options.search, mode: 'insensitive' } },
|
||||||
|
{ Recipient: { some: { name: { contains: options.search, mode: 'insensitive' } } } },
|
||||||
|
{ Recipient: { some: { email: { contains: options.search, mode: 'insensitive' } } } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
let ownerCountsWhereInput: Prisma.DocumentWhereInput = {
|
let ownerCountsWhereInput: Prisma.DocumentWhereInput = {
|
||||||
userId: userIdWhereClause,
|
userId: userIdWhereClause,
|
||||||
createdAt,
|
createdAt,
|
||||||
@ -220,6 +248,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
...searchFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (teamEmail) {
|
if (teamEmail) {
|
||||||
|
|||||||
@ -1316,7 +1316,7 @@ msgstr "Dokument wird dauerhaft gelöscht"
|
|||||||
#: apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx:109
|
#: apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx:109
|
||||||
#: apps/web/src/app/(dashboard)/documents/[id]/loading.tsx:16
|
#: apps/web/src/app/(dashboard)/documents/[id]/loading.tsx:16
|
||||||
#: apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx:15
|
#: apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx:15
|
||||||
#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:114
|
#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:119
|
||||||
#: apps/web/src/app/(profile)/p/[url]/page.tsx:166
|
#: apps/web/src/app/(profile)/p/[url]/page.tsx:166
|
||||||
#: apps/web/src/app/not-found.tsx:21
|
#: apps/web/src/app/not-found.tsx:21
|
||||||
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:205
|
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:205
|
||||||
|
|||||||
@ -1311,7 +1311,7 @@ msgstr "Document will be permanently deleted"
|
|||||||
#: apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx:109
|
#: apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx:109
|
||||||
#: apps/web/src/app/(dashboard)/documents/[id]/loading.tsx:16
|
#: apps/web/src/app/(dashboard)/documents/[id]/loading.tsx:16
|
||||||
#: apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx:15
|
#: apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx:15
|
||||||
#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:114
|
#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:119
|
||||||
#: apps/web/src/app/(profile)/p/[url]/page.tsx:166
|
#: apps/web/src/app/(profile)/p/[url]/page.tsx:166
|
||||||
#: apps/web/src/app/not-found.tsx:21
|
#: apps/web/src/app/not-found.tsx:21
|
||||||
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:205
|
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:205
|
||||||
|
|||||||
@ -1316,7 +1316,7 @@ msgstr "Le document sera supprimé de manière permanente"
|
|||||||
#: apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx:109
|
#: apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx:109
|
||||||
#: apps/web/src/app/(dashboard)/documents/[id]/loading.tsx:16
|
#: apps/web/src/app/(dashboard)/documents/[id]/loading.tsx:16
|
||||||
#: apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx:15
|
#: apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx:15
|
||||||
#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:114
|
#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:119
|
||||||
#: apps/web/src/app/(profile)/p/[url]/page.tsx:166
|
#: apps/web/src/app/(profile)/p/[url]/page.tsx:166
|
||||||
#: apps/web/src/app/not-found.tsx:21
|
#: apps/web/src/app/not-found.tsx:21
|
||||||
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:205
|
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:205
|
||||||
|
|||||||
@ -105,13 +105,15 @@ export const unseedTeam = async (teamUrl: string) => {
|
|||||||
type SeedTeamMemberOptions = {
|
type SeedTeamMemberOptions = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
role?: TeamMemberRole;
|
role?: TeamMemberRole;
|
||||||
|
name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const seedTeamMember = async ({
|
export const seedTeamMember = async ({
|
||||||
teamId,
|
teamId,
|
||||||
|
name,
|
||||||
role = TeamMemberRole.ADMIN,
|
role = TeamMemberRole.ADMIN,
|
||||||
}: SeedTeamMemberOptions) => {
|
}: SeedTeamMemberOptions) => {
|
||||||
const user = await seedUser();
|
const user = await seedUser({ name });
|
||||||
|
|
||||||
await prisma.teamMember.create({
|
await prisma.teamMember.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@ -21,8 +21,13 @@ export const seedUser = async ({
|
|||||||
password = 'password',
|
password = 'password',
|
||||||
verified = true,
|
verified = true,
|
||||||
}: SeedUserOptions = {}) => {
|
}: SeedUserOptions = {}) => {
|
||||||
if (!name) {
|
let url = name;
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
url = nanoid();
|
||||||
|
} else {
|
||||||
name = nanoid();
|
name = nanoid();
|
||||||
|
url = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
@ -35,7 +40,7 @@ export const seedUser = async ({
|
|||||||
email,
|
email,
|
||||||
password: hashSync(password),
|
password: hashSync(password),
|
||||||
emailVerified: verified ? new Date() : undefined,
|
emailVerified: verified ? new Date() : undefined,
|
||||||
url: name,
|
url,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user