c1PV_R*@^RR?(+_<6bYb>Qah|le5^QA-PRfj)?xkPc}2PtlT}$gmRYu?kBJ1iT5s%yV3JLx&_PC&B$Y{4
zIhuFXHqh(dS#ellgxSM-s3CBt*I^@Wq@GJ0>Q*Q
zLpG%PhrBo{jeW`vmQGSU=OL-^Ntu@8vwFburj7IPTzm>3HVH;M+uPd<*ZiX(7Owf*
z1?*RvaDQcGklM
z4f7d*QF`72j@v=o^=ev0%WE*MI)Pxoc!@v8bGNgurd#&WVTSoe@@ib!Fa#Iy6SM{@YiQQntX!Fg
zSEN;ZO)T$iFu=}SXgqoM(9n?IKb_@;Y)yHa<+;e%N~Kakbs(^EU#~lF$!uHPl($(o
zJ#`Jj$46!hq^W7Ma;SIoXgM5}@hRbuRD`wNBW;LRrfa!c3UaL^osxEmbLB9_fbS~ll-)B$
zf_gg9g9<^3LM#S2F4LA@D_zAemaGJV_ze!ybbuqYpmddRre^jkEKQEfzOLGt)J9Q_
zps@d8(hx>+(Q?0Ejl7`i*7?8>b=joam8)Y325hNRRY(XTL|H`0LbVAF(-1+Vif~G)
zP!D-w6+~HiQ|+jhW6P*6d(Gd_8Sjbf8-nrmAVV(beAsZEWdH?T*Rlz+9j)m~r)#+#
zIg7Lc$O5gBhF8b`y6)E*9;bD1h#yV@@{i847CQ}2f*o=R`C4S33%ft;dQcqI?zh7P
r9fzHBaBy&NaBy&NaBy&NsEOYJQ|7+=Yxnzq00000NkvXXu0mjfB)pO|
literal 0
HcmV?d00001
diff --git a/apps/web/public/static/mail-open-alert.png b/apps/web/public/static/mail-open-alert.png
new file mode 100644
index 0000000000000000000000000000000000000000..1511f0bc539302bf3fc9de7e18283d8882861ffb
GIT binary patch
literal 3818
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
BjST<*
literal 0
HcmV?d00001
diff --git a/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx
index 83ad81ca1..0fc660968 100644
--- a/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx
+++ b/apps/web/src/app/(dashboard)/admin/documents/data-table.tsx
@@ -7,9 +7,9 @@ import Link from 'next/link';
import { Loader } from 'lucide-react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { FindResultSet } from '@documenso/lib/types/find-result-set';
-import { recipientInitials } from '@documenso/lib/utils/recipient-formatter';
-import { Document, User } from '@documenso/prisma/client';
+import type { FindResultSet } from '@documenso/lib/types/find-result-set';
+import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
+import type { Document, User } from '@documenso/prisma/client';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
@@ -65,7 +65,7 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
accessorKey: 'owner',
cell: ({ row }) => {
const avatarFallbackText = row.original.User.name
- ? recipientInitials(row.original.User.name)
+ ? extractInitials(row.original.User.name)
: row.original.User.email.slice(0, 1).toUpperCase();
return (
diff --git a/packages/ui/primitives/multiselect-combobox.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/multiselect-role-combobox.tsx
similarity index 95%
rename from packages/ui/primitives/multiselect-combobox.tsx
rename to apps/web/src/app/(dashboard)/admin/users/[id]/multiselect-role-combobox.tsx
index bac87ce0b..9a25af897 100644
--- a/packages/ui/primitives/multiselect-combobox.tsx
+++ b/apps/web/src/app/(dashboard)/admin/users/[id]/multiselect-role-combobox.tsx
@@ -19,7 +19,7 @@ type ComboboxProps = {
onChange: (_values: string[]) => void;
};
-const MultiSelectCombobox = ({ listValues, onChange }: ComboboxProps) => {
+const MultiSelectRoleCombobox = ({ listValues, onChange }: ComboboxProps) => {
const [open, setOpen] = React.useState(false);
const [selectedValues, setSelectedValues] = React.useState([]);
const dbRoles = Object.values(Role);
@@ -79,4 +79,4 @@ const MultiSelectCombobox = ({ listValues, onChange }: ComboboxProps) => {
);
};
-export { MultiSelectCombobox };
+export { MultiSelectRoleCombobox };
diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx
index 9ae270d28..3bd909623 100644
--- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx
+++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx
@@ -18,9 +18,10 @@ import {
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
-import { MultiSelectCombobox } from '@documenso/ui/primitives/multiselect-combobox';
import { useToast } from '@documenso/ui/primitives/use-toast';
+import { MultiSelectRoleCombobox } from './multiselect-role-combobox';
+
const ZUserFormSchema = ZUpdateProfileMutationByAdminSchema.omit({ id: true });
type TUserFormSchema = z.infer;
@@ -117,7 +118,7 @@ export default function UserPage({ params }: { params: { id: number } }) {
+ }
+ enableClearAllButton={true}
+ inputPlaceholder="Search"
+ loading={!isMounted || isInitialLoading}
+ options={comboBoxOptions}
+ selectedValues={senderIds}
+ onChange={onChange}
+ />
+ );
+};
diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx
index c8adb1422..13b85d526 100644
--- a/apps/web/src/app/(dashboard)/documents/data-table.tsx
+++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx
@@ -7,7 +7,7 @@ import { useSession } from 'next-auth/react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { FindResultSet } from '@documenso/lib/types/find-result-set';
-import type { Document, Recipient, User } from '@documenso/prisma/client';
+import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
@@ -25,11 +25,18 @@ export type DocumentsDataTableProps = {
Document & {
Recipient: Recipient[];
User: Pick;
+ team: Pick | null;
}
>;
+ showSenderColumn?: boolean;
+ team?: Pick;
};
-export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
+export const DocumentsDataTable = ({
+ results,
+ showSenderColumn,
+ team,
+}: DocumentsDataTableProps) => {
const { data: session } = useSession();
const [isPending, startTransition] = useTransition();
@@ -61,6 +68,11 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
header: 'Title',
cell: ({ row }) => ,
},
+ {
+ id: 'sender',
+ header: 'Sender',
+ cell: ({ row }) => row.original.User.name ?? row.original.User.email,
+ },
{
header: 'Recipient',
accessorKey: 'recipient',
@@ -79,8 +91,8 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
(!row.original.deletedAt ||
row.original.status === ExtendedDocumentStatus.COMPLETED) && (
-
-
+
+
),
},
@@ -90,6 +102,9 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
currentPage={results.currentPage}
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
+ columnVisibility={{
+ sender: Boolean(showSenderColumn),
+ }}
>
{(table) => }
diff --git a/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx b/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx
new file mode 100644
index 000000000..ead3e8f4f
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx
@@ -0,0 +1,158 @@
+import Link from 'next/link';
+
+import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
+import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
+import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
+import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats';
+import { getStats } from '@documenso/lib/server-only/document/get-stats';
+import { parseToIntegerArray } from '@documenso/lib/utils/params';
+import { formatDocumentsPath } from '@documenso/lib/utils/teams';
+import type { Team, TeamEmail } from '@documenso/prisma/client';
+import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
+import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
+import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
+import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
+
+import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
+import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
+import { DocumentStatus } from '~/components/formatter/document-status';
+
+import { DocumentsDataTable } from './data-table';
+import { DataTableSenderFilter } from './data-table-sender-filter';
+import { EmptyDocumentState } from './empty-state';
+import { UploadDocument } from './upload-document';
+
+export type DocumentsPageViewProps = {
+ searchParams?: {
+ status?: ExtendedDocumentStatus;
+ period?: PeriodSelectorValue;
+ page?: string;
+ perPage?: string;
+ senderIds?: string;
+ };
+ team?: Team & { teamEmail?: TeamEmail | null };
+};
+
+export default async function DocumentsPageView({
+ searchParams = {},
+ team,
+}: DocumentsPageViewProps) {
+ const { user } = await getRequiredServerComponentSession();
+
+ const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
+ const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
+ const page = Number(searchParams.page) || 1;
+ const perPage = Number(searchParams.perPage) || 20;
+ const senderIds = parseToIntegerArray(searchParams.senderIds ?? '');
+ const currentTeam = team ? { id: team.id, url: team.url } : undefined;
+
+ const getStatOptions: GetStatsInput = {
+ user,
+ period,
+ };
+
+ if (team) {
+ getStatOptions.team = {
+ teamId: team.id,
+ teamEmail: team.teamEmail?.email,
+ senderIds,
+ };
+ }
+
+ const stats = await getStats(getStatOptions);
+
+ const results = await findDocuments({
+ userId: user.id,
+ teamId: team?.id,
+ status,
+ orderBy: {
+ column: 'createdAt',
+ direction: 'desc',
+ },
+ page,
+ perPage,
+ period,
+ senderIds,
+ });
+
+ const getTabHref = (value: typeof status) => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('status', value);
+
+ if (params.has('page')) {
+ params.delete('page');
+ }
+
+ return `${formatDocumentsPath(team?.url)}?${params.toString()}`;
+ };
+
+ return (
+
+
+
+
+
+ {team && (
+
+
+ {team.name.slice(0, 1)}
+
+
+ )}
+
+
Documents
+
+
+
+
+
+ {[
+ ExtendedDocumentStatus.INBOX,
+ ExtendedDocumentStatus.PENDING,
+ ExtendedDocumentStatus.COMPLETED,
+ ExtendedDocumentStatus.DRAFT,
+ ExtendedDocumentStatus.ALL,
+ ].map((value) => (
+
+
+
+
+ {value !== ExtendedDocumentStatus.ALL && (
+
+ {Math.min(stats[value], 99)}
+ {stats[value] > 99 && '+'}
+
+ )}
+
+
+ ))}
+
+
+
+ {team &&
}
+
+
+
+
+
+
+ {results.count > 0 && (
+
+ )}
+ {results.count === 0 && }
+
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx b/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx
index 56c112d75..14370cff8 100644
--- a/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx
+++ b/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx
@@ -1,5 +1,7 @@
import { useRouter } from 'next/navigation';
+import { formatDocumentsPath } from '@documenso/lib/utils/teams';
+import type { Team } from '@documenso/prisma/client';
import { trpc as trpcReact } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
@@ -16,18 +18,21 @@ type DuplicateDocumentDialogProps = {
id: number;
open: boolean;
onOpenChange: (_open: boolean) => void;
+ team?: Pick;
};
export const DuplicateDocumentDialog = ({
id,
open,
onOpenChange,
+ team,
}: DuplicateDocumentDialogProps) => {
const router = useRouter();
const { toast } = useToast();
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
id,
+ teamId: team?.id,
});
const documentData = document?.documentData
@@ -37,10 +42,12 @@ export const DuplicateDocumentDialog = ({
}
: undefined;
+ const documentsPath = formatDocumentsPath(team?.url);
+
const { mutateAsync: duplicateDocument, isLoading: isDuplicateLoading } =
trpcReact.document.duplicateDocument.useMutation({
onSuccess: (newId) => {
- router.push(`/documents/${newId}`);
+ router.push(`${documentsPath}/${newId}`);
toast({
title: 'Document Duplicated',
@@ -54,7 +61,7 @@ export const DuplicateDocumentDialog = ({
const onDuplicate = async () => {
try {
- await duplicateDocument({ id });
+ await duplicateDocument({ id, teamId: team?.id });
} catch {
toast({
title: 'Something went wrong',
diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx
index 5780df1dc..b67ed6f02 100644
--- a/apps/web/src/app/(dashboard)/documents/page.tsx
+++ b/apps/web/src/app/(dashboard)/documents/page.tsx
@@ -1,119 +1,16 @@
import type { Metadata } from 'next';
-import Link from 'next/link';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
-import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
-import { getStats } from '@documenso/lib/server-only/document/get-stats';
-import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
-import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
-import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
-
-import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
-import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
-import { DocumentStatus } from '~/components/formatter/document-status';
-
-import { DocumentsDataTable } from './data-table';
-import { EmptyDocumentState } from './empty-state';
-import { UploadDocument } from './upload-document';
+import type { DocumentsPageViewProps } from './documents-page-view';
+import DocumentsPageView from './documents-page-view';
export type DocumentsPageProps = {
- searchParams?: {
- status?: ExtendedDocumentStatus;
- period?: PeriodSelectorValue;
- page?: string;
- perPage?: string;
- };
+ searchParams?: DocumentsPageViewProps['searchParams'];
};
export const metadata: Metadata = {
title: 'Documents',
};
-export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
- const { user } = await getRequiredServerComponentSession();
- const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
- const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
- const page = Number(searchParams.page) || 1;
- const perPage = Number(searchParams.perPage) || 20;
-
- const stats = await getStats({
- user,
- period,
- });
-
- const results = await findDocuments({
- userId: user.id,
- status,
- orderBy: {
- column: 'createdAt',
- direction: 'desc',
- },
- page,
- perPage,
- period,
- });
-
- const getTabHref = (value: typeof status) => {
- const params = new URLSearchParams(searchParams);
-
- params.set('status', value);
-
- if (params.has('page')) {
- params.delete('page');
- }
-
- return `/documents?${params.toString()}`;
- };
-
- return (
-
-
-
-
-
Documents
-
-
-
-
- {[
- ExtendedDocumentStatus.INBOX,
- ExtendedDocumentStatus.PENDING,
- ExtendedDocumentStatus.COMPLETED,
- ExtendedDocumentStatus.DRAFT,
- ExtendedDocumentStatus.ALL,
- ].map((value) => (
-
-
-
-
- {value !== ExtendedDocumentStatus.ALL && (
-
- {Math.min(stats[value], 99)}
- {stats[value] > 99 && '+'}
-
- )}
-
-
- ))}
-
-
-
-
-
-
-
-
- {results.count > 0 && }
- {results.count === 0 && }
-
-
- );
+export default function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
+ return ;
}
diff --git a/apps/web/src/app/(dashboard)/documents/upload-document.tsx b/apps/web/src/app/(dashboard)/documents/upload-document.tsx
index 444bd1db0..ed91620dc 100644
--- a/apps/web/src/app/(dashboard)/documents/upload-document.tsx
+++ b/apps/web/src/app/(dashboard)/documents/upload-document.tsx
@@ -13,6 +13,7 @@ import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { putFile } from '@documenso/lib/universal/upload/put-file';
+import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
@@ -21,9 +22,13 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export type UploadDocumentProps = {
className?: string;
+ team?: {
+ id: number;
+ url: string;
+ };
};
-export const UploadDocument = ({ className }: UploadDocumentProps) => {
+export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
const router = useRouter();
const analytics = useAnalytics();
@@ -39,13 +44,15 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
const disabledMessage = useMemo(() => {
if (remaining.documents === 0) {
- return 'You have reached your document limit.';
+ return team
+ ? 'Document upload disabled due to unpaid invoices'
+ : 'You have reached your document limit.';
}
if (!session?.user.emailVerified) {
return 'Verify your email to upload documents.';
}
- }, [remaining.documents, session?.user.emailVerified]);
+ }, [remaining.documents, session?.user.emailVerified, team]);
const onFileDrop = async (file: File) => {
try {
@@ -61,6 +68,7 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
const { id } = await createDocument({
title: file.name,
documentDataId,
+ teamId: team?.id,
});
toast({
@@ -75,7 +83,7 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
timestamp: new Date().toISOString(),
});
- router.push(`/documents/${id}`);
+ router.push(`${formatDocumentsPath(team?.url)}/${id}`);
} catch (error) {
console.error(error);
@@ -117,11 +125,13 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
/>
- {remaining.documents > 0 && Number.isFinite(remaining.documents) && (
-
- {remaining.documents} of {quota.documents} documents remaining this month.
-
- )}
+ {team?.id === undefined &&
+ remaining.documents > 0 &&
+ Number.isFinite(remaining.documents) && (
+
+ {remaining.documents} of {quota.documents} documents remaining this month.
+
+ )}
{isLoading && (
@@ -130,7 +140,7 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {