feat: add organisations (#1820)

This commit is contained in:
David Nguyen
2025-06-10 11:49:52 +10:00
committed by GitHub
parent 0b37f19641
commit e6dc237ad2
631 changed files with 37616 additions and 25695 deletions

View File

@ -10,6 +10,7 @@ import { match } from 'ts-pattern';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
@ -21,7 +22,7 @@ import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
import { useCurrentTeam } from '~/providers/team';
export interface DocumentDropZoneWrapperProps {
children: ReactNode;
@ -34,10 +35,11 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
const { user } = useSession();
const { folderId } = useParams();
const team = useOptionalCurrentTeam();
const team = useCurrentTeam();
const navigate = useNavigate();
const analytics = useAnalytics();
const organisation = useCurrentOrganisation();
const [isLoading, setIsLoading] = useState(false);
@ -53,7 +55,7 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
const onFileDrop = async (file: File) => {
if (isUploadDisabled && IS_BILLING_ENABLED()) {
await navigate('/settings/billing');
await navigate(`/o/${organisation.url}/settings/billing`);
return;
}
@ -83,11 +85,7 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
timestamp: new Date().toISOString(),
});
await navigate(
folderId
? `${formatDocumentsPath(team?.url)}/f/${folderId}/${id}/edit`
: `${formatDocumentsPath(team?.url)}/${id}/edit`,
);
await navigate(`${formatDocumentsPath(team.url)}/${id}/edit`);
} catch (err) {
const error = AppError.parseError(err);
@ -156,7 +154,7 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
{isUploadDisabled && IS_BILLING_ENABLED() && (
<Link
to="/settings/billing"
to={`/o/${organisation.url}/settings/billing`}
className="mt-4 text-sm text-amber-500 hover:underline dark:text-amber-400"
>
<Trans>Upgrade your plan to upload more documents</Trans>

View File

@ -29,13 +29,12 @@ import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
import { useCurrentTeam } from '~/providers/team';
export type DocumentEditFormProps = {
className?: string;
initialDocument: TDocument;
documentRootPath: string;
isDocumentEnterprise: boolean;
};
type EditDocumentStep = 'settings' | 'signers' | 'fields' | 'subject';
@ -45,7 +44,6 @@ export const DocumentEditForm = ({
className,
initialDocument,
documentRootPath,
isDocumentEnterprise,
}: DocumentEditFormProps) => {
const { toast } = useToast();
const { _ } = useLingui();
@ -53,7 +51,7 @@ export const DocumentEditForm = ({
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const team = useOptionalCurrentTeam();
const team = useCurrentTeam();
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
@ -355,10 +353,9 @@ export const DocumentEditForm = ({
key={recipients.length}
documentFlow={documentFlow.settings}
document={document}
currentTeamMemberRole={team?.currentTeamMember?.role}
currentTeamMemberRole={team.currentTeamRole}
recipients={recipients}
fields={fields}
isDocumentEnterprise={isDocumentEnterprise}
isDocumentPdfLoaded={isDocumentPdfLoaded}
onSubmit={onAddSettingsFormSubmit}
/>
@ -370,7 +367,6 @@ export const DocumentEditForm = ({
signingOrder={document.documentMeta?.signingOrder}
allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner}
fields={fields}
isDocumentEnterprise={isDocumentEnterprise}
onSubmit={onAddSignersFormSubmit}
isDocumentPdfLoaded={isDocumentPdfLoaded}
/>
@ -382,7 +378,7 @@ export const DocumentEditForm = ({
fields={fields}
onSubmit={onAddFieldsFormSubmit}
isDocumentPdfLoaded={isDocumentPdfLoaded}
teamId={team?.id}
teamId={team.id}
/>
<AddSubjectFormPartial

View File

@ -19,7 +19,7 @@ export type DocumentPageViewButtonProps = {
document: Document & {
user: Pick<User, 'id' | 'name' | 'email'>;
recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null;
team: Pick<Team, 'id' | 'url'>;
};
};
@ -37,10 +37,8 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const role = recipient?.role;
const documentsPath = formatDocumentsPath(document.team?.url);
const formatPath = document.folderId
? `${documentsPath}/f/${document.folderId}/${document.id}/edit`
: `${documentsPath}/${document.id}/edit`;
const documentsPath = formatDocumentsPath(document.team.url);
const formatPath = `${documentsPath}/${document.id}/edit`;
const onDownloadClick = async () => {
try {

View File

@ -36,7 +36,7 @@ import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialo
import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
import { DocumentResendDialog } from '~/components/dialogs/document-resend-dialog';
import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog';
import { useOptionalCurrentTeam } from '~/providers/team';
import { useCurrentTeam } from '~/providers/team';
export type DocumentPageViewDropdownProps = {
document: Document & {
@ -52,7 +52,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
const { _ } = useLingui();
const navigate = useNavigate();
const team = useOptionalCurrentTeam();
const team = useCurrentTeam();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
@ -67,7 +67,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
const isCurrentTeamDocument = team && document.team?.url === team.url;
const canManageDocument = Boolean(isOwner || isCurrentTeamDocument);
const documentsPath = formatDocumentsPath(team?.url);
const documentsPath = formatDocumentsPath(team.url);
const onDownloadClick = async () => {
try {

View File

@ -3,12 +3,12 @@ import { useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import { useNavigate, useParams } from 'react-router';
import { match } from 'ts-pattern';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
@ -26,7 +26,7 @@ import {
} from '@documenso/ui/primitives/tooltip';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
import { useCurrentTeam } from '~/providers/team';
export type DocumentUploadDropzoneProps = {
className?: string;
@ -38,10 +38,11 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp
const { user } = useSession();
const { folderId } = useParams();
const team = useOptionalCurrentTeam();
const team = useCurrentTeam();
const navigate = useNavigate();
const analytics = useAnalytics();
const organisation = useCurrentOrganisation();
const userTimezone =
TIME_ZONES.find((timezone) => timezone === Intl.DateTimeFormat().resolvedOptions().timeZone) ??
@ -54,10 +55,12 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp
const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation();
const disabledMessage = useMemo(() => {
if (organisation.subscription && remaining.documents === 0) {
return msg`Document upload disabled due to unpaid invoices`;
}
if (remaining.documents === 0) {
return team
? msg`Document upload disabled due to unpaid invoices`
: msg`You have reached your document limit.`;
return msg`You have reached your document limit.`;
}
if (!user.emailVerified) {
@ -81,6 +84,8 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp
void refreshLimits();
await navigate(`${formatDocumentsPath(team.url)}/${id}/edit`);
toast({
title: _(msg`Document uploaded`),
description: _(msg`Your document has been uploaded successfully.`),
@ -92,12 +97,6 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp
documentId: id,
timestamp: new Date().toISOString(),
});
await navigate(
folderId
? `${formatDocumentsPath(team?.url)}/f/${folderId}/${id}/edit`
: `${formatDocumentsPath(team?.url)}/${id}/edit`,
);
} catch (err) {
const error = AppError.parseError(err);
@ -138,6 +137,7 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp
<TooltipTrigger asChild>
<div>
<DocumentDropzone
loading={isLoading}
disabled={remaining.documents === 0 || !user.emailVerified}
disabledMessage={disabledMessage}
onDrop={onFileDrop}
@ -145,6 +145,7 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp
/>
</div>
</TooltipTrigger>
{team?.id === undefined &&
remaining.documents > 0 &&
Number.isFinite(remaining.documents) && (
@ -158,12 +159,6 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp
)}
</Tooltip>
</TooltipProvider>
{isLoading && (
<div className="bg-background/50 absolute inset-0 flex items-center justify-center rounded-lg">
<Loader className="text-muted-foreground h-12 w-12 animate-spin" />
</div>
)}
</div>
);
};