mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
feat: add delegate document ownership option (#2272)
When using an API key created in a team context, the documents/templates’ owner always defaults to the team API token creator, rather than the actual uploader. For example, John creates the API key for the team "Lawyers". Tom and Maria use the API key to upload documents. All the uploaded documents are attributed to John. This makes it impossible to see who actually uploaded a document. The new feature allows users to enable document ownership delegation from the organization/team settings.
This commit is contained in:
@@ -137,12 +137,12 @@ export const TemplateBulkSendDialog = ({
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
|
||||
<div className="bg-muted/70 rounded-lg border p-4">
|
||||
<div className="rounded-lg border bg-muted/70 p-4">
|
||||
<h3 className="text-sm font-medium">
|
||||
<Trans>CSV Structure</Trans>
|
||||
</h3>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<Trans>
|
||||
For each recipient, provide their email (required) and name (optional) in separate
|
||||
columns. Download the template CSV below for the correct format.
|
||||
@@ -153,7 +153,7 @@ export const TemplateBulkSendDialog = ({
|
||||
<Trans>Current recipients:</Trans>
|
||||
</p>
|
||||
|
||||
<ul className="text-muted-foreground mt-2 list-inside list-disc text-sm">
|
||||
<ul className="mt-2 list-inside list-disc text-sm text-muted-foreground">
|
||||
{recipients.map((recipient, index) => (
|
||||
<li key={index}>
|
||||
{recipient.name ? `${recipient.name} (${recipient.email})` : recipient.email}
|
||||
@@ -167,7 +167,7 @@ export const TemplateBulkSendDialog = ({
|
||||
<Trans>Download Template CSV</Trans>
|
||||
</Button>
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<Trans>Pre-formatted CSV template with example data.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
@@ -200,14 +200,14 @@ export const TemplateBulkSendDialog = ({
|
||||
) : (
|
||||
<div className="flex h-10 items-center rounded-md border px-3">
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<FileIcon className="text-muted-foreground h-4 w-4" />
|
||||
<FileIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="flex-1 truncate text-sm">{value.name}</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
className="text-destructive hover:text-destructive p-0 text-xs"
|
||||
className="p-0 text-xs text-destructive hover:text-destructive"
|
||||
onClick={() => onChange(null)}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
@@ -220,9 +220,9 @@ export const TemplateBulkSendDialog = ({
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
{error && <p className="text-destructive text-sm">{error.message}</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<Trans>
|
||||
Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use
|
||||
template defaults.
|
||||
@@ -247,7 +247,7 @@ export const TemplateBulkSendDialog = ({
|
||||
|
||||
<label
|
||||
htmlFor="send-immediately"
|
||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||
className="ml-2 flex items-center text-sm text-muted-foreground"
|
||||
>
|
||||
<Trans>Send documents to recipients immediately</Trans>
|
||||
</label>
|
||||
|
||||
@@ -58,6 +58,7 @@ export type TDocumentPreferencesFormSchema = {
|
||||
includeSigningCertificate: boolean | null;
|
||||
includeAuditLog: boolean | null;
|
||||
signatureTypes: DocumentSignatureType[];
|
||||
delegateDocumentOwnership: boolean | null;
|
||||
aiFeaturesEnabled: boolean | null;
|
||||
};
|
||||
|
||||
@@ -73,6 +74,7 @@ type SettingsSubset = Pick<
|
||||
| 'typedSignatureEnabled'
|
||||
| 'uploadSignatureEnabled'
|
||||
| 'drawSignatureEnabled'
|
||||
| 'delegateDocumentOwnership'
|
||||
| 'aiFeaturesEnabled'
|
||||
>;
|
||||
|
||||
@@ -109,6 +111,7 @@ export const DocumentPreferencesForm = ({
|
||||
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(canInherit ? 0 : 1, {
|
||||
message: msg`At least one signature type must be enabled`.id,
|
||||
}),
|
||||
delegateDocumentOwnership: z.boolean().nullable(),
|
||||
aiFeaturesEnabled: z.boolean().nullable(),
|
||||
});
|
||||
|
||||
@@ -125,6 +128,7 @@ export const DocumentPreferencesForm = ({
|
||||
includeSigningCertificate: settings.includeSigningCertificate,
|
||||
includeAuditLog: settings.includeAuditLog,
|
||||
signatureTypes: extractTeamSignatureSettings({ ...settings }),
|
||||
delegateDocumentOwnership: settings.delegateDocumentOwnership,
|
||||
aiFeaturesEnabled: settings.aiFeaturesEnabled,
|
||||
},
|
||||
resolver: zodResolver(ZDocumentPreferencesFormSchema),
|
||||
@@ -515,6 +519,52 @@ export const DocumentPreferencesForm = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="delegateDocumentOwnership"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Delegate Document Ownership</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value.toString()}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'true' ? true : value === 'false' ? false : null)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="bg-background text-muted-foreground">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="true">
|
||||
<Trans>Yes</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="false">
|
||||
<Trans>No</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{canInherit && (
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>Inherit from organisation</Trans>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Enable team API tokens to delegate document ownership to another team member.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{isAiFeaturesConfigured && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -368,7 +368,7 @@ export const SignInForm = ({
|
||||
<p className="mt-2 text-right">
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
|
||||
className="text-sm text-muted-foreground duration-200 hover:opacity-70"
|
||||
>
|
||||
<Trans>Forgot your password?</Trans>
|
||||
</Link>
|
||||
@@ -390,11 +390,11 @@ export const SignInForm = ({
|
||||
<>
|
||||
{hasSocialAuthEnabled && (
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<span className="bg-transparent text-muted-foreground">
|
||||
<Trans>Or continue with</Trans>
|
||||
</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -403,7 +403,7 @@ export const SignInForm = ({
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="bg-background text-muted-foreground border"
|
||||
className="border bg-background text-muted-foreground"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignInWithGoogleClick}
|
||||
>
|
||||
@@ -417,7 +417,7 @@ export const SignInForm = ({
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="bg-background text-muted-foreground border"
|
||||
className="border bg-background text-muted-foreground"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignInWithMicrosoftClick}
|
||||
>
|
||||
@@ -435,7 +435,7 @@ export const SignInForm = ({
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="bg-background text-muted-foreground border"
|
||||
className="border bg-background text-muted-foreground"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignInWithOIDCClick}
|
||||
>
|
||||
@@ -452,7 +452,7 @@ export const SignInForm = ({
|
||||
variant="outline"
|
||||
disabled={isSubmitting}
|
||||
loading={isPasskeyLoading}
|
||||
className="bg-background text-muted-foreground border"
|
||||
className="border bg-background text-muted-foreground"
|
||||
onClick={onSignInWithPasskey}
|
||||
>
|
||||
{!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TableCell } from '@documenso/ui/primitives/table';
|
||||
|
||||
export type DocumentLogsTableProps = {
|
||||
documentId: number;
|
||||
userId?: number;
|
||||
};
|
||||
|
||||
const dateFormat: DateTimeFormatOptions = {
|
||||
@@ -26,7 +27,7 @@ const dateFormat: DateTimeFormatOptions = {
|
||||
hourCycle: 'h12',
|
||||
};
|
||||
|
||||
export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
|
||||
export const DocumentLogsTable = ({ documentId, userId }: DocumentLogsTableProps) => {
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -93,7 +94,9 @@ export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
|
||||
{
|
||||
header: _(msg`Action`),
|
||||
accessorKey: 'type',
|
||||
cell: ({ row }) => <span>{formatDocumentAuditLogAction(_, row.original).description}</span>,
|
||||
cell: ({ row }) => (
|
||||
<span>{formatDocumentAuditLogAction(_, row.original, userId).description}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: _(msg`IP Address`),
|
||||
|
||||
@@ -57,6 +57,7 @@ export default function OrganisationSettingsDocumentPage() {
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
signatureTypes,
|
||||
delegateDocumentOwnership,
|
||||
aiFeaturesEnabled,
|
||||
} = data;
|
||||
|
||||
@@ -85,6 +86,7 @@ export default function OrganisationSettingsDocumentPage() {
|
||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
delegateDocumentOwnership: delegateDocumentOwnership,
|
||||
aiFeaturesEnabled,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -75,11 +75,12 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
},
|
||||
recipients: envelope.recipients,
|
||||
documentRootPath,
|
||||
userId: user.id,
|
||||
};
|
||||
}
|
||||
|
||||
export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps) {
|
||||
const { document, recipients, documentRootPath } = loaderData;
|
||||
const { document, recipients, documentRootPath, userId } = loaderData;
|
||||
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
@@ -171,15 +172,15 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
||||
<section className="mt-6">
|
||||
<Card className="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2" degrees={45} gradient>
|
||||
{documentInformation.map((info, i) => (
|
||||
<div className="text-foreground text-sm" key={i}>
|
||||
<div className="text-sm text-foreground" key={i}>
|
||||
<h3 className="font-semibold">{_(info.description)}</h3>
|
||||
<p className="text-muted-foreground truncate">{info.value}</p>
|
||||
<p className="truncate text-muted-foreground">{info.value}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="text-foreground text-sm">
|
||||
<div className="text-sm text-foreground">
|
||||
<h3 className="font-semibold">Recipients</h3>
|
||||
<ul className="text-muted-foreground list-inside list-disc">
|
||||
<ul className="list-inside list-disc text-muted-foreground">
|
||||
{recipients.map((recipient) => (
|
||||
<li key={`recipient-${recipient.id}`}>
|
||||
<span>{formatRecipientText(recipient)}</span>
|
||||
@@ -191,7 +192,7 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
|
||||
</section>
|
||||
|
||||
<section className="mt-6">
|
||||
<DocumentLogsTable documentId={document.id} />
|
||||
<DocumentLogsTable documentId={document.id} userId={userId} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -50,6 +50,7 @@ export default function TeamsSettingsPage() {
|
||||
includeSigningCertificate,
|
||||
includeAuditLog,
|
||||
signatureTypes,
|
||||
delegateDocumentOwnership,
|
||||
aiFeaturesEnabled,
|
||||
} = data;
|
||||
|
||||
@@ -75,6 +76,7 @@ export default function TeamsSettingsPage() {
|
||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
}),
|
||||
delegateDocumentOwnership: delegateDocumentOwnership,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -193,11 +193,11 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
<CardContent className="space-y-6">
|
||||
{/* Current User Section */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-muted-foreground flex items-center gap-2 font-semibold">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-muted-foreground">
|
||||
<UserCircle2 className="h-4 w-4" />
|
||||
<Trans>Your Account</Trans>
|
||||
</h3>
|
||||
<div className="bg-muted/50 flex items-center justify-between gap-3 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg bg-muted/50 p-3">
|
||||
<AvatarWithText
|
||||
avatarSrc={formatAvatarUrl(user.avatar)}
|
||||
avatarFallback={extractInitials(user.name || user.email)}
|
||||
@@ -215,11 +215,11 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
|
||||
{/* Organisation Section */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-muted-foreground flex items-center gap-2 font-semibold">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-muted-foreground">
|
||||
<Building2 className="h-4 w-4" />
|
||||
<Trans>Requesting Organisation</Trans>
|
||||
</h3>
|
||||
<div className="bg-muted/50 flex items-center justify-between gap-3 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg bg-muted/50 p-3">
|
||||
<AvatarWithText
|
||||
avatarSrc={formatAvatarUrl(organisation.avatar)}
|
||||
avatarFallback={extractInitials(organisation.name)}
|
||||
@@ -237,7 +237,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
|
||||
{/* Warnings Section */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-muted-foreground flex items-center gap-2 font-semibold">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-muted-foreground">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<Trans>Important: What This Means</Trans>
|
||||
</h3>
|
||||
@@ -253,7 +253,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
<Eye className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="text-muted-foreground font-semibold">
|
||||
<span className="font-semibold text-muted-foreground">
|
||||
Full account access:
|
||||
</span>{' '}
|
||||
View all your profile information, settings, and activity
|
||||
@@ -264,7 +264,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
<Settings className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="text-muted-foreground font-semibold">
|
||||
<span className="font-semibold text-muted-foreground">
|
||||
Account management:
|
||||
</span>{' '}
|
||||
Modify your account settings, permissions, and preferences
|
||||
@@ -275,7 +275,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
<Database className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="text-muted-foreground font-semibold">Data access:</span>{' '}
|
||||
<span className="font-semibold text-muted-foreground">Data access:</span>{' '}
|
||||
Access all data associated with your account
|
||||
</Trans>
|
||||
</span>
|
||||
@@ -304,7 +304,7 @@ export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Rou
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={`accept-conditions`}
|
||||
>
|
||||
<Trans>I agree to link my account with this organization</Trans>
|
||||
|
||||
Reference in New Issue
Block a user