Compare commits

...

37 Commits

Author SHA1 Message Date
37ed5ad222 v1.12.2-rc.1 2025-07-11 12:55:56 +10:00
d6c11bd195 fix: sign-able readonly fields (#1885) 2025-07-10 16:47:36 +10:00
cb73d21e05 chore: api tests (#1856) 2025-07-10 12:56:46 +10:00
106f796fea fix: readonly field styling (#1887)
Changes:
- Updating styling of read only fields
- Removed truncation for fields and used overflow hidden instead
2025-07-10 12:35:18 +10:00
9917def0ca v1.12.2-rc.0 2025-07-03 10:31:22 +10:00
cdb9b9ee03 chore: add certificate error logs (#1875)
Add certificate logs
2025-07-03 10:13:12 +10:00
8d1d098e3a v1.12.1 2025-07-03 10:07:54 +10:00
b682d2785f chore: add translations (#1835)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-07-03 10:07:11 +10:00
1a1a30791e v1.12.0 2025-07-03 10:01:03 +10:00
ea1cf481eb chore: extract translations 2025-07-01 21:27:35 +10:00
eda0d5eeb6 fix: open advanced settings when fields are added to templates (#1855) 2025-07-01 21:21:13 +10:00
8da4ab533f fix(add-subject): remove superfluous word (#1866) 2025-07-01 12:34:14 +10:00
8695ef766e v1.12.0-rc.8 2025-06-30 19:47:37 +10:00
7487399123 feat: add more api logs (#1870)
Adds more detailed API logging using Pino
2025-06-30 19:46:32 +10:00
0cc729e9bd feat: add sequential document view logs (#1871)
## Description

Add a new document audit log to detect when the document is viewed. This
should only be visible in the document audit log page

Notes:
1. I wanted to reuse the `DOCUMENT_OPENED` event and add an additional
paramter to track sequential views, but it's not query-able
2. This will log "DOCUMENT_VIEWED" before "DOCUMENT_OPENED" but i don't
think it matters
2025-06-30 19:11:16 +10:00
58d97518c8 v1.12.0-rc.7 2025-06-27 22:17:45 +10:00
20c8969272 fix: get real ip for rate limit key 2025-06-27 22:17:02 +10:00
85ac65e405 v1.12.0-rc.6 2025-06-27 21:46:16 +10:00
e07a497b69 feat: api logging by pino (#1865)
experiemental
2025-06-27 21:44:51 +10:00
21dc4eee62 v1.12.0-rc.5 2025-06-27 18:53:45 +10:00
dc2042a1ee fix: rate limit api endpoints (#1863)
Rate limit API endpoint
2025-06-27 18:50:22 +10:00
bb9ba80edb fix: duplicate fields and recipients when you duplicate a document (#1852) 2025-06-23 16:43:07 +10:00
bfe8c674f2 fix: globalAccessAuth error (#1851) 2025-06-23 10:10:57 +10:00
ebe1baf0a0 chore: extract translations 2025-06-19 15:16:44 +10:00
2345de679b feat: admin monthly active users metric (#1724) 2025-06-19 15:12:17 +10:00
1be0e2842c fix: refactor folders UI/UX (#1770)
- Add folder search
- Used correct HTML elements
- Added missing translations
- Removed automatic folder redirects
- Removed duplicate code
- Added folder loading skeletons and empty states
2025-06-19 14:57:32 +10:00
29a03d4ec7 feat: add inbox counter (#1849) 2025-06-18 13:30:01 +10:00
039cd7d449 fix: remove preconnect font links (#1798) 2025-06-18 12:42:54 +10:00
484f6c8b85 fix: admin metrics broken (#1845) 2025-06-17 21:15:11 +10:00
4fd8a767b2 chore: Update README.md (#1840) 2025-06-13 22:42:38 +10:00
b8e08e88ac fix: api keys not showing (#1839) 2025-06-13 17:20:03 +10:00
031a7b9e36 fix: visibility 2025-06-13 01:02:40 +10:00
12fe045195 fix: visiblity 2025-06-13 00:05:08 +10:00
614106a5e4 fix: rework documents limits logic (#1836) 2025-06-12 13:42:31 +10:00
8be7137b59 v1.12.0-rc.4 2025-06-12 10:27:41 +10:00
31e2a6443e fix: legacy authOptions support for api v1 2025-06-12 10:21:41 +10:00
400d2a2b1a feat: sign out of all sessions (#1797) 2025-06-11 17:57:38 +10:00
172 changed files with 7516 additions and 3911 deletions

View File

@ -127,4 +127,6 @@ E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com"
E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
# [[LOGGER]]
NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY=
# OPTIONAL: The file to save the logger output to. Will disable stdout if provided.
NEXT_PRIVATE_LOGGER_FILE_PATH=

3
.gitignore vendored
View File

@ -50,3 +50,6 @@ yarn-error.log*
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# logs
logs.json

View File

@ -49,8 +49,6 @@ Join us in creating the next generation of open trust infrastructure.
## Community and Next Steps 🎯
We're currently working on a redesign of the application, including a revamp of the codebase, so Documenso can be more intuitive to use and robust to develop upon.
- Check out the first source code release in this repository and test it.
- Tell us what you think in the [Discussions](https://github.com/documenso/documenso/discussions).
- Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members.

View File

@ -1,19 +1,30 @@
@import '@documenso/ui/styles/theme.css';
/* Inter Variable Fonts */
@font-face {
font-family: 'Inter';
src: url('/public/fonts/inter-regular.ttf') format('ttf');
/* font-weight: 400;
src: url('/fonts/inter-variablefont_opsz,wght.ttf') format('truetype-variations');
font-weight: 100 900;
font-style: normal;
font-display: swap; */
font-display: swap;
}
/* Inter Italic Variable Fonts */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-italic-variablefont_opsz,wght.ttf') format('truetype-variations');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
/* Caveat Variable Font */
@font-face {
font-family: 'Caveat';
src: url('/public/fonts/caveat.ttf') format('ttf');
/* font-weight: 400;
src: url('/fonts/caveat-variablefont_wght.ttf') format('truetype-variations');
font-weight: 400 600;
font-style: normal;
font-display: swap; */
font-display: swap;
}
@layer base {

View File

@ -1,11 +1,11 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { FolderIcon, HomeIcon, Loader2 } from 'lucide-react';
import { FolderIcon, HomeIcon, Loader2, Search } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { z } from 'zod';
@ -31,6 +31,7 @@ import {
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
@ -61,6 +62,8 @@ export const DocumentMoveToFolderDialog = ({
const navigate = useNavigate();
const team = useCurrentTeam();
const [searchTerm, setSearchTerm] = useState('');
const form = useForm<TMoveDocumentFormSchema>({
resolver: zodResolver(ZMoveDocumentFormSchema),
defaultValues: {
@ -83,6 +86,7 @@ export const DocumentMoveToFolderDialog = ({
useEffect(() => {
if (!open) {
form.reset();
setSearchTerm('');
} else {
form.reset({ folderId: currentFolderId });
}
@ -131,6 +135,10 @@ export const DocumentMoveToFolderDialog = ({
}
};
const filteredFolders = folders?.data.filter((folder) =>
folder.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
return (
<Dialog {...props} open={open} onOpenChange={onOpenChange}>
<DialogContent>
@ -144,8 +152,18 @@ export const DocumentMoveToFolderDialog = ({
</DialogDescription>
</DialogHeader>
<div className="relative">
<Search className="text-muted-foreground absolute left-2 top-3 h-4 w-4" />
<Input
placeholder={_(msg`Search folders...`)}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8"
/>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-4 flex flex-col gap-y-4">
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
<FormField
control={form.control}
name="folderId"
@ -154,8 +172,9 @@ export const DocumentMoveToFolderDialog = ({
<FormLabel>
<Trans>Folder</Trans>
</FormLabel>
<FormControl>
<div className="space-y-2">
<div className="max-h-96 space-y-2 overflow-y-auto">
{isFoldersLoading ? (
<div className="flex h-10 items-center justify-center">
<Loader2 className="h-4 w-4 animate-spin" />
@ -170,10 +189,10 @@ export const DocumentMoveToFolderDialog = ({
disabled={currentFolderId === null}
>
<HomeIcon className="mr-2 h-4 w-4" />
<Trans>Root (No Folder)</Trans>
<Trans>Home (No Folder)</Trans>
</Button>
{folders?.data.map((folder) => (
{filteredFolders?.map((folder) => (
<Button
key={folder.id}
type="button"
@ -186,6 +205,12 @@ export const DocumentMoveToFolderDialog = ({
{folder.name}
</Button>
))}
{searchTerm && filteredFolders?.length === 0 && (
<div className="text-muted-foreground px-2 py-2 text-center text-sm">
<Trans>No folders found</Trans>
</div>
)}
</>
)}
</div>

View File

@ -1,17 +1,14 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans, useLingui } from '@lingui/react/macro';
import type { FolderType } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { FolderPlusIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router';
import { useParams } from 'react-router';
import { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { FolderType } from '@documenso/lib/types/folder-type';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
@ -34,26 +31,22 @@ import {
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
const ZCreateFolderFormSchema = z.object({
name: z.string().min(1, { message: 'Folder name is required' }),
});
type TCreateFolderFormSchema = z.infer<typeof ZCreateFolderFormSchema>;
export type CreateFolderDialogProps = {
export type FolderCreateDialogProps = {
type: FolderType;
trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
export const CreateFolderDialog = ({ trigger, ...props }: CreateFolderDialogProps) => {
const { _ } = useLingui();
export const FolderCreateDialog = ({ type, trigger, ...props }: FolderCreateDialogProps) => {
const { t } = useLingui();
const { toast } = useToast();
const { folderId } = useParams();
const navigate = useNavigate();
const team = useCurrentTeam();
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
const { mutateAsync: createFolder } = trpc.folder.createFolder.useMutation();
@ -67,37 +60,21 @@ export const CreateFolderDialog = ({ trigger, ...props }: CreateFolderDialogProp
const onSubmit = async (data: TCreateFolderFormSchema) => {
try {
const newFolder = await createFolder({
await createFolder({
name: data.name,
parentId: folderId,
type: FolderType.DOCUMENT,
type,
});
setIsCreateFolderOpen(false);
const documentsPath = formatDocumentsPath(team.url);
await navigate(`${documentsPath}/f/${newFolder.id}`);
toast({
description: 'Folder created successfully',
description: t`Folder created successfully`,
});
} catch (err) {
const error = AppError.parseError(err);
if (error.code === AppErrorCode.ALREADY_EXISTS) {
toast({
title: 'Failed to create folder',
description: _(msg`This folder name is already taken.`),
variant: 'destructive',
});
return;
}
toast({
title: 'Failed to create folder',
description: _(msg`An unknown error occurred while creating the folder.`),
title: t`Failed to create folder`,
description: t`An unknown error occurred while creating the folder.`,
variant: 'destructive',
});
}
@ -113,48 +90,60 @@ export const CreateFolderDialog = ({ trigger, ...props }: CreateFolderDialogProp
<Dialog {...props} open={isCreateFolderOpen} onOpenChange={setIsCreateFolderOpen}>
<DialogTrigger asChild>
{trigger ?? (
<Button variant="outline" className="flex items-center space-x-2">
<FolderPlusIcon className="h-4 w-4" />
<span>Create Folder</span>
<Button
variant="outline"
className="flex items-center"
data-testid="folder-create-button"
>
<FolderPlusIcon className="mr-2 h-4 w-4" />
<Trans>Create Folder</Trans>
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Folder</DialogTitle>
<DialogTitle>
<Trans>Create New Folder</Trans>
</DialogTitle>
<DialogDescription>
Enter a name for your new folder. Folders help you organise your documents.
<Trans>Enter a name for your new folder. Folders help you organise your items.</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Folder Name</FormLabel>
<FormControl>
<Input placeholder="My Folder" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset disabled={form.formState.isSubmitting} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Folder Name</Trans>
</FormLabel>
<FormControl>
<Input placeholder={t`My Folder`} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type="button"
variant="secondary"
onClick={() => setIsCreateFolderOpen(false)}
>
Cancel
</Button>
<DialogFooter>
<Button
type="button"
variant="secondary"
onClick={() => setIsCreateFolderOpen(false)}
>
<Trans>Cancel</Trans>
</Button>
<Button type="submit">Create</Button>
</DialogFooter>
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</fieldset>
</form>
</Form>
</DialogContent>

View File

@ -1,8 +1,7 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form';
@ -11,6 +10,7 @@ import { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
@ -32,22 +32,22 @@ import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type FolderDeleteDialogProps = {
folder: TFolderWithSubfolders | null;
folder: TFolderWithSubfolders;
isOpen: boolean;
onOpenChange: (open: boolean) => void;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDeleteDialogProps) => {
const { _ } = useLingui();
const { t } = useLingui();
const { toast } = useToast();
const { mutateAsync: deleteFolder } = trpc.folder.deleteFolder.useMutation();
const deleteMessage = _(msg`delete ${folder?.name ?? 'folder'}`);
const deleteMessage = t`delete ${folder.name}`;
const ZDeleteFolderFormSchema = z.object({
confirmText: z.literal(deleteMessage, {
errorMap: () => ({ message: _(msg`You must type '${deleteMessage}' to confirm`) }),
errorMap: () => ({ message: t`You must type '${deleteMessage}' to confirm` }),
}),
});
@ -61,8 +61,6 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet
});
const onFormSubmit = async () => {
if (!folder) return;
try {
await deleteFolder({
id: folder.id,
@ -71,15 +69,15 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet
onOpenChange(false);
toast({
title: 'Folder deleted successfully',
title: t`Folder deleted successfully`,
});
} catch (err) {
const error = AppError.parseError(err);
if (error.code === AppErrorCode.NOT_FOUND) {
toast({
title: 'Folder not found',
description: _(msg`The folder you are trying to delete does not exist.`),
title: t`Folder not found`,
description: t`The folder you are trying to delete does not exist.`,
variant: 'destructive',
});
@ -87,8 +85,8 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet
}
toast({
title: 'Failed to delete folder',
description: _(msg`An unknown error occurred while deleting the folder.`),
title: t`Failed to delete folder`,
description: t`An unknown error occurred while deleting the folder.`,
variant: 'destructive',
});
}
@ -104,53 +102,65 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Folder</DialogTitle>
<DialogTitle>
<Trans>Delete Folder</Trans>
</DialogTitle>
<DialogDescription>
Are you sure you want to delete this folder?
{folder && folder._count.documents > 0 && (
<span className="text-destructive mt-2 block">
This folder contains {folder._count.documents} document(s). Deleting it will also
delete all documents in the folder.
</span>
)}
{folder && folder._count.subfolders > 0 && (
<span className="text-destructive mt-2 block">
This folder contains {folder._count.subfolders} subfolder(s). Deleting it will
delete all subfolders and their contents.
</span>
)}
<Trans>Are you sure you want to delete this folder?</Trans>
</DialogDescription>
</DialogHeader>
{(folder._count.documents > 0 ||
folder._count.templates > 0 ||
folder._count.subfolders > 0) && (
<Alert variant="destructive">
<AlertDescription>
<Trans>
This folder contains multiple items. Deleting it will also delete all items in the
folder, including nested folders and their contents.
</Trans>
</AlertDescription>
</Alert>
)}
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)} className="space-y-4">
<FormField
control={form.control}
name="confirmText"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>
Confirm by typing:{' '}
<span className="font-sm text-destructive font-semibold">
{deleteMessage}
</span>
</Trans>
</FormLabel>
<FormControl>
<Input {...field} placeholder={deleteMessage} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button variant="destructive" type="submit" disabled={!form.formState.isValid}>
Delete
</Button>
</DialogFooter>
<form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset disabled={form.formState.isSubmitting} className="space-y-4">
<FormField
control={form.control}
name="confirmText"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>
Confirm by typing:{' '}
<span className="font-sm text-destructive font-semibold">
{deleteMessage}
</span>
</Trans>
</FormLabel>
<FormControl>
<Input placeholder={deleteMessage} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
<Trans>Cancel</Trans>
</Button>
<Button
variant="destructive"
type="submit"
disabled={!form.formState.isValid}
loading={form.formState.isSubmitting}
>
<Trans>Delete</Trans>
</Button>
</DialogFooter>
</fieldset>
</form>
</Form>
</DialogContent>

View File

@ -1,10 +1,10 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { FolderIcon, HomeIcon } from 'lucide-react';
import { FolderIcon, HomeIcon, Search } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@ -27,6 +27,7 @@ import {
FormItem,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type FolderMoveDialogProps = {
@ -48,9 +49,10 @@ export const FolderMoveDialog = ({
isOpen,
onOpenChange,
}: FolderMoveDialogProps) => {
const { _ } = useLingui();
const { t } = useLingui();
const { toast } = useToast();
const [searchTerm, setSearchTerm] = useState('');
const { mutateAsync: moveFolder } = trpc.folder.moveFolder.useMutation();
const form = useForm<TMoveFolderFormSchema>({
@ -72,15 +74,15 @@ export const FolderMoveDialog = ({
onOpenChange(false);
toast({
title: 'Folder moved successfully',
title: t`Folder moved successfully`,
});
} catch (err) {
const error = AppError.parseError(err);
if (error.code === AppErrorCode.NOT_FOUND) {
toast({
title: 'Folder not found',
description: _(msg`The folder you are trying to move does not exist.`),
title: t`Folder not found`,
description: t`The folder you are trying to move does not exist.`,
variant: 'destructive',
});
@ -88,8 +90,8 @@ export const FolderMoveDialog = ({
}
toast({
title: 'Failed to move folder',
description: _(msg`An unknown error occurred while moving the folder.`),
title: t`Failed to move folder`,
description: t`An unknown error occurred while moving the folder.`,
variant: 'destructive',
});
}
@ -98,69 +100,91 @@ export const FolderMoveDialog = ({
useEffect(() => {
if (!isOpen) {
form.reset();
setSearchTerm('');
}
}, [isOpen, form]);
// Filter out the current folder and only show folders of the same type
// Filter out the current folder, only show folders of the same type, and filter by search term
const filteredFolders = foldersData?.filter(
(f) => f.id !== folder?.id && f.type === folder?.type,
(f) =>
f.id !== folder?.id &&
f.type === folder?.type &&
(searchTerm === '' || f.name.toLowerCase().includes(searchTerm.toLowerCase())),
);
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Move Folder</DialogTitle>
<DialogDescription>Select a destination for this folder.</DialogDescription>
<DialogTitle>
<Trans>Move Folder</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Select a destination for this folder.</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)} className="space-y-4 py-4">
<FormField
control={form.control}
name="targetFolderId"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="space-y-2">
<Button
type="button"
variant={!field.value ? 'default' : 'outline'}
className="w-full justify-start"
disabled={!folder?.parentId}
onClick={() => field.onChange(null)}
>
<HomeIcon className="mr-2 h-4 w-4" />
Root
</Button>
{filteredFolders &&
filteredFolders.map((f) => (
<Button
key={f.id}
type="button"
disabled={f.id === folder?.parentId}
variant={field.value === f.id ? 'default' : 'outline'}
className="w-full justify-start"
onClick={() => field.onChange(f.id)}
>
<FolderIcon className="mr-2 h-4 w-4" />
{f.name}
</Button>
))}
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
Move Folder
</Button>
</DialogFooter>
<div className="relative">
<Search className="text-muted-foreground absolute left-2 top-3 h-4 w-4" />
<Input
placeholder={t`Search folders...`}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8"
/>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset disabled={form.formState.isSubmitting} className="space-y-4">
<FormField
control={form.control}
name="targetFolderId"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="max-h-96 space-y-2 overflow-y-auto">
<Button
type="button"
variant={!field.value ? 'default' : 'outline'}
className="w-full justify-start"
disabled={!folder?.parentId}
onClick={() => field.onChange(null)}
>
<HomeIcon className="mr-2 h-4 w-4" />
<Trans>Home</Trans>
</Button>
{filteredFolders &&
filteredFolders.map((f) => (
<Button
key={f.id}
type="button"
disabled={f.id === folder?.parentId}
variant={field.value === f.id ? 'default' : 'outline'}
className="w-full justify-start"
onClick={() => field.onChange(f.id)}
>
<FolderIcon className="mr-2 h-4 w-4" />
{f.name}
</Button>
))}
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Move</Trans>
</Button>
</DialogFooter>
</fieldset>
</form>
</Form>
</DialogContent>

View File

@ -83,7 +83,9 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
const { mutateAsync: createOrganisation } = trpc.organisation.create.useMutation();
const { data: plansData } = trpc.billing.plans.get.useQuery();
const { data: plansData } = trpc.billing.plans.get.useQuery(undefined, {
enabled: IS_BILLING_ENABLED(),
});
const onFormSubmit = async ({ name }: TCreateOrganisationFormSchema) => {
try {

View File

@ -0,0 +1,94 @@
import { useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { authClient } from '@documenso/auth/client';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
type SessionLogoutAllDialogProps = {
onSuccess?: () => Promise<unknown>;
disabled?: boolean;
};
export const SessionLogoutAllDialog = ({ onSuccess, disabled }: SessionLogoutAllDialogProps) => {
const { t } = useLingui();
const { toast } = useToast();
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleSignOutAllSessions = async () => {
setIsLoading(true);
try {
await authClient.signOutAllSessions();
if (onSuccess) {
await onSuccess();
}
toast({
title: t`Sessions have been revoked`,
});
setIsOpen(false);
} catch (error) {
console.error(error);
toast({
title: t`Error`,
description: t`Failed to sign out all sessions`,
variant: 'destructive',
});
}
setIsLoading(false);
};
return (
<Dialog open={isOpen} onOpenChange={(value) => (isLoading ? undefined : setIsOpen(value))}>
<DialogTrigger asChild>
<Button variant="secondary" disabled={disabled}>
<Trans>Revoke all sessions</Trans>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Revoke all sessions</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
This will sign you out of all other devices. You will need to sign in again on those
devices to continue using your account.
</Trans>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="secondary" disabled={isLoading}>
<Trans>Cancel</Trans>
</Button>
</DialogClose>
<Button loading={isLoading} variant="destructive" onClick={handleSignOutAllSessions}>
<Trans>Revoke all sessions</Trans>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@ -8,6 +8,7 @@ import { useNavigate } from 'react-router';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
@ -23,18 +24,21 @@ import {
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
type TemplateCreateDialogProps = {
templateRootPath: string;
folderId?: string;
};
export const TemplateCreateDialog = ({ templateRootPath, folderId }: TemplateCreateDialogProps) => {
export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) => {
const navigate = useNavigate();
const { user } = useSession();
const { toast } = useToast();
const { _ } = useLingui();
const team = useCurrentTeam();
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
const [showTemplateCreateDialog, setShowTemplateCreateDialog] = useState(false);
@ -66,7 +70,7 @@ export const TemplateCreateDialog = ({ templateRootPath, folderId }: TemplateCre
setShowTemplateCreateDialog(false);
await navigate(`${templateRootPath}/${id}/edit`);
await navigate(`${formatTemplatesPath(team.url)}/${id}/edit`);
} catch {
toast({
title: _(msg`Something went wrong`),

View File

@ -1,165 +0,0 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { FolderPlusIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router';
import { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { FolderType } from '@documenso/lib/types/folder-type';
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
const ZCreateFolderFormSchema = z.object({
name: z.string().min(1, { message: 'Folder name is required' }),
});
type TCreateFolderFormSchema = z.infer<typeof ZCreateFolderFormSchema>;
export type TemplateFolderCreateDialogProps = {
trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
export const TemplateFolderCreateDialog = ({
trigger,
...props
}: TemplateFolderCreateDialogProps) => {
const { toast } = useToast();
const { _ } = useLingui();
const { folderId } = useParams();
const navigate = useNavigate();
const team = useCurrentTeam();
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
const { mutateAsync: createFolder } = trpc.folder.createFolder.useMutation();
const form = useForm<TCreateFolderFormSchema>({
resolver: zodResolver(ZCreateFolderFormSchema),
defaultValues: {
name: '',
},
});
const onSubmit = async (data: TCreateFolderFormSchema) => {
try {
const newFolder = await createFolder({
name: data.name,
parentId: folderId,
type: FolderType.TEMPLATE,
});
setIsCreateFolderOpen(false);
toast({
description: _(msg`Folder created successfully`),
});
const templatesPath = formatTemplatesPath(team.url);
void navigate(`${templatesPath}/f/${newFolder.id}`);
} catch (err) {
const error = AppError.parseError(err);
if (error.code === AppErrorCode.ALREADY_EXISTS) {
toast({
title: _(msg`Failed to create folder`),
description: _(msg`This folder name is already taken.`),
variant: 'destructive',
});
return;
}
toast({
title: _(msg`Failed to create folder`),
description: _(msg`An unknown error occurred while creating the folder.`),
variant: 'destructive',
});
}
};
useEffect(() => {
if (!isCreateFolderOpen) {
form.reset();
}
}, [isCreateFolderOpen, form]);
return (
<Dialog {...props} open={isCreateFolderOpen} onOpenChange={setIsCreateFolderOpen}>
<DialogTrigger asChild>
{trigger ?? (
<Button variant="outline" className="flex items-center space-x-2">
<FolderPlusIcon className="h-4 w-4" />
<span>Create Folder</span>
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Folder</DialogTitle>
<DialogDescription>
Enter a name for your new folder. Folders help you organise your templates.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Folder Name</FormLabel>
<FormControl>
<Input placeholder="My Folder" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type="button"
variant="secondary"
onClick={() => setIsCreateFolderOpen(false)}
>
Cancel
</Button>
<Button type="submit">Create</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@ -1,163 +0,0 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type TemplateFolderDeleteDialogProps = {
folder: TFolderWithSubfolders | null;
isOpen: boolean;
onOpenChange: (open: boolean) => void;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
export const TemplateFolderDeleteDialog = ({
folder,
isOpen,
onOpenChange,
}: TemplateFolderDeleteDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { mutateAsync: deleteFolder } = trpc.folder.deleteFolder.useMutation();
const deleteMessage = _(msg`delete ${folder?.name ?? 'folder'}`);
const ZDeleteFolderFormSchema = z.object({
confirmText: z.literal(deleteMessage, {
errorMap: () => ({ message: _(msg`You must type '${deleteMessage}' to confirm`) }),
}),
});
type TDeleteFolderFormSchema = z.infer<typeof ZDeleteFolderFormSchema>;
const form = useForm<TDeleteFolderFormSchema>({
resolver: zodResolver(ZDeleteFolderFormSchema),
defaultValues: {
confirmText: '',
},
});
const onFormSubmit = async () => {
if (!folder) return;
try {
await deleteFolder({
id: folder.id,
});
onOpenChange(false);
toast({
title: 'Folder deleted successfully',
});
} catch (err) {
const error = AppError.parseError(err);
if (error.code === AppErrorCode.NOT_FOUND) {
toast({
title: 'Folder not found',
description: _(msg`The folder you are trying to delete does not exist.`),
variant: 'destructive',
});
return;
}
toast({
title: 'Failed to delete folder',
description: _(msg`An unknown error occurred while deleting the folder.`),
variant: 'destructive',
});
}
};
useEffect(() => {
if (!isOpen) {
form.reset();
}
}, [isOpen]);
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Folder</DialogTitle>
<DialogDescription>
Are you sure you want to delete this folder?
{folder && folder._count.documents > 0 && (
<span className="text-destructive mt-2 block">
This folder contains {folder._count.documents} document(s). Deleting it will also
delete all documents in the folder.
</span>
)}
{folder && folder._count.subfolders > 0 && (
<span className="text-destructive mt-2 block">
This folder contains {folder._count.subfolders} subfolder(s). Deleting it will
delete all subfolders and their contents.
</span>
)}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)} className="space-y-4">
<FormField
control={form.control}
name="confirmText"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>
Confirm by typing:{' '}
<span className="font-sm text-destructive font-semibold">
{deleteMessage}
</span>
</Trans>
</FormLabel>
<FormControl>
<Input {...field} placeholder={deleteMessage} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button variant="destructive" type="submit" disabled={!form.formState.isValid}>
Delete
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@ -1,175 +0,0 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { FolderIcon, HomeIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type TemplateFolderMoveDialogProps = {
foldersData: TFolderWithSubfolders[] | undefined;
folder: TFolderWithSubfolders | null;
isOpen: boolean;
onOpenChange: (open: boolean) => void;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
const ZMoveFolderFormSchema = z.object({
targetFolderId: z.string().optional(),
});
type TMoveFolderFormSchema = z.infer<typeof ZMoveFolderFormSchema>;
export const TemplateFolderMoveDialog = ({
foldersData,
folder,
isOpen,
onOpenChange,
}: TemplateFolderMoveDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { mutateAsync: moveFolder } = trpc.folder.moveFolder.useMutation();
const form = useForm<TMoveFolderFormSchema>({
resolver: zodResolver(ZMoveFolderFormSchema),
defaultValues: {
targetFolderId: folder?.parentId ?? '',
},
});
const onFormSubmit = async ({ targetFolderId }: TMoveFolderFormSchema) => {
if (!folder) return;
try {
await moveFolder({
id: folder.id,
parentId: targetFolderId ?? '',
});
onOpenChange(false);
toast({
title: 'Folder moved successfully',
});
} catch (err) {
const error = AppError.parseError(err);
if (error.code === AppErrorCode.NOT_FOUND) {
toast({
title: 'Folder not found',
description: _(msg`The folder you are trying to move does not exist.`),
variant: 'destructive',
});
return;
}
toast({
title: 'Failed to move folder',
description: _(msg`An unknown error occurred while moving the folder.`),
variant: 'destructive',
});
}
};
useEffect(() => {
if (!isOpen) {
form.reset();
}
}, [isOpen, form]);
// Filter out the current folder and only show folders of the same type
const filteredFolders = foldersData?.filter(
(f) => f.id !== folder?.id && f.type === folder?.type,
);
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Move Folder</DialogTitle>
<DialogDescription>Select a destination for this folder.</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)} className="space-y-4 py-4">
<FormField
control={form.control}
name="targetFolderId"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="space-y-2">
<Button
type="button"
variant={!field.value ? 'default' : 'outline'}
className="w-full justify-start"
disabled={!folder?.parentId}
onClick={() => field.onChange(undefined)}
>
<HomeIcon className="mr-2 h-4 w-4" />
Root
</Button>
{filteredFolders &&
filteredFolders.map((f) => (
<Button
key={f.id}
type="button"
disabled={f.id === folder?.parentId}
variant={field.value === f.id ? 'default' : 'outline'}
className="w-full justify-start"
onClick={() => field.onChange(f.id)}
>
<FolderIcon className="mr-2 h-4 w-4" />
{f.name}
</Button>
))}
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
type="submit"
disabled={
form.formState.isSubmitting ||
form.getValues('targetFolderId') === folder?.parentId
}
>
Move Folder
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@ -1,176 +0,0 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { FolderType } from '@documenso/lib/types/folder-type';
import { trpc } from '@documenso/trpc/react';
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
export type TemplateFolderSettingsDialogProps = {
folder: TFolderWithSubfolders | null;
isOpen: boolean;
onOpenChange: (open: boolean) => void;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
export const ZUpdateFolderFormSchema = z.object({
name: z.string().min(1),
visibility: z.nativeEnum(DocumentVisibility).optional(),
});
export type TUpdateFolderFormSchema = z.infer<typeof ZUpdateFolderFormSchema>;
export const TemplateFolderSettingsDialog = ({
folder,
isOpen,
onOpenChange,
}: TemplateFolderSettingsDialogProps) => {
const { _ } = useLingui();
const team = useOptionalCurrentTeam();
const { toast } = useToast();
const { mutateAsync: updateFolder } = trpc.folder.updateFolder.useMutation();
const isTeamContext = !!team;
const isTemplateFolder = folder?.type === FolderType.TEMPLATE;
const form = useForm<z.infer<typeof ZUpdateFolderFormSchema>>({
resolver: zodResolver(ZUpdateFolderFormSchema),
defaultValues: {
name: folder?.name ?? '',
visibility: folder?.visibility ?? DocumentVisibility.EVERYONE,
},
});
useEffect(() => {
if (folder) {
form.reset({
name: folder.name,
visibility: folder.visibility ?? DocumentVisibility.EVERYONE,
});
}
}, [folder, form]);
const onFormSubmit = async (data: TUpdateFolderFormSchema) => {
if (!folder) return;
try {
await updateFolder({
id: folder.id,
name: data.name,
visibility:
isTeamContext && !isTemplateFolder
? (data.visibility ?? DocumentVisibility.EVERYONE)
: DocumentVisibility.EVERYONE,
});
toast({
title: _(msg`Folder updated successfully`),
});
onOpenChange(false);
} catch (err) {
const error = AppError.parseError(err);
if (error.code === AppErrorCode.NOT_FOUND) {
toast({
title: _(msg`Folder not found`),
});
}
}
};
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Folder Settings</DialogTitle>
<DialogDescription>Manage the settings for this folder.</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isTeamContext && !isTemplateFolder && (
<FormField
control={form.control}
name="visibility"
render={({ field }) => (
<FormItem>
<FormLabel>Visibility</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select visibility" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={DocumentVisibility.EVERYONE}>Everyone</SelectItem>
<SelectItem value={DocumentVisibility.MANAGER_AND_ABOVE}>
Managers and above
</SelectItem>
<SelectItem value={DocumentVisibility.ADMIN}>Admins only</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<DialogFooter>
<Button type="submit">Save Changes</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@ -1,11 +1,11 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { FolderIcon, HomeIcon, Loader2 } from 'lucide-react';
import { FolderIcon, HomeIcon, Loader2, Search } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { z } from 'zod';
@ -31,6 +31,7 @@ import {
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
@ -59,9 +60,12 @@ export function TemplateMoveToFolderDialog({
}: TemplateMoveToFolderDialogProps) {
const { _ } = useLingui();
const { toast } = useToast();
const navigate = useNavigate();
const team = useCurrentTeam();
const [searchTerm, setSearchTerm] = useState('');
const form = useForm<TMoveTemplateFormSchema>({
resolver: zodResolver(ZMoveTemplateFormSchema),
defaultValues: {
@ -84,6 +88,7 @@ export function TemplateMoveToFolderDialog({
useEffect(() => {
if (!isOpen) {
form.reset();
setSearchTerm('');
} else {
form.reset({ folderId: currentFolderId ?? null });
}
@ -132,6 +137,10 @@ export function TemplateMoveToFolderDialog({
}
};
const filteredFolders = folders?.data?.filter((folder) =>
folder.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
return (
<Dialog {...props} open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
@ -145,6 +154,16 @@ export function TemplateMoveToFolderDialog({
</DialogDescription>
</DialogHeader>
<div className="relative">
<Search className="text-muted-foreground absolute left-2 top-3 h-4 w-4" />
<Input
placeholder={_(msg`Search folders...`)}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8"
/>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-4 flex flex-col gap-y-4">
<FormField
@ -155,8 +174,9 @@ export function TemplateMoveToFolderDialog({
<FormLabel>
<Trans>Folder</Trans>
</FormLabel>
<FormControl>
<div className="space-y-2">
<div className="max-h-96 space-y-2 overflow-y-auto">
{isFoldersLoading ? (
<div className="flex h-10 items-center justify-center">
<Loader2 className="h-4 w-4 animate-spin" />
@ -171,10 +191,10 @@ export function TemplateMoveToFolderDialog({
disabled={currentFolderId === null}
>
<HomeIcon className="mr-2 h-4 w-4" />
<Trans>Root (No Folder)</Trans>
<Trans>Home (No Folder)</Trans>
</Button>
{folders?.data?.map((folder) => (
{filteredFolders?.map((folder) => (
<Button
key={folder.id}
type="button"
@ -187,6 +207,12 @@ export function TemplateMoveToFolderDialog({
{folder.name}
</Button>
))}
{searchTerm && filteredFolders?.length === 0 && (
<div className="text-muted-foreground px-2 py-2 text-center text-sm">
<Trans>No folders found</Trans>
</div>
)}
</>
)}
</div>

View File

@ -0,0 +1,73 @@
'use client';
import { DateTime } from 'luxon';
import type { TooltipProps } from 'recharts';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent';
import type { GetMonthlyActiveUsersResult } from '@documenso/lib/server-only/admin/get-users-stats';
export type MonthlyActiveUsersChartProps = {
className?: string;
title: string;
cummulative?: boolean;
data: GetMonthlyActiveUsersResult;
};
const CustomTooltip = ({ active, payload, label }: TooltipProps<ValueType, NameType>) => {
if (active && payload && payload.length) {
return (
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
<p>{label}</p>
<p className="text-documenso">
{payload[0].name === 'cume_count' ? 'Cumulative MAU' : 'Monthly Active Users'}:{' '}
<span className="text-black">{Number(payload[0].value).toLocaleString('en-US')}</span>
</p>
</div>
);
}
return null;
};
export const MonthlyActiveUsersChart = ({
className,
data,
title,
cummulative = false,
}: MonthlyActiveUsersChartProps) => {
const formattedData = [...data].reverse().map(({ month, count, cume_count }) => {
return {
month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('MMM yyyy'),
count: Number(count),
cume_count: Number(cume_count),
};
});
return (
<div className={className}>
<div className="border-border flex flex-1 flex-col justify-center rounded-2xl border p-6 pl-2">
<div className="mb-6 flex px-4">
<h3 className="text-lg font-semibold">{title}</h3>
</div>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={formattedData}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'hsl(var(--primary) / 10%)' }} />
<Bar
dataKey={cummulative ? 'cume_count' : 'count'}
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
maxBarSize={60}
label={cummulative ? 'Cumulative MAU' : 'Monthly Active Users'}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};

View File

@ -1,11 +1,13 @@
import { type HTMLAttributes, useEffect, useState } from 'react';
import { ReadStatus } from '@prisma/client';
import { InboxIcon, MenuIcon, SearchIcon } from 'lucide-react';
import { Link, useParams } from 'react-router';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
import { getRootHref } from '@documenso/lib/utils/params';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@ -28,6 +30,15 @@ export const Header = ({ className, ...props }: HeaderProps) => {
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
const [scrollY, setScrollY] = useState(0);
const { data: unreadCountData } = trpc.document.inbox.getCount.useQuery(
{
readStatus: ReadStatus.NOT_OPENED,
},
{
// refetchInterval: 30000, // Refetch every 30 seconds
},
);
useEffect(() => {
const onScroll = () => {
setScrollY(window.scrollY);
@ -61,12 +72,11 @@ export const Header = ({ className, ...props }: HeaderProps) => {
<Link to="/inbox" className="relative block h-10 w-10">
<InboxIcon className="text-muted-foreground hover:text-foreground h-5 w-5 flex-shrink-0 transition-colors" />
{/* Todo: Add counter. */}
{/* {unreadCount > 0 && (
<span className="bg-muted text-muted-foreground absolute -right-2 -top-2 flex h-5 w-5 items-center justify-center rounded-full text-[10px]">
1
{unreadCountData && unreadCountData.count > 0 && (
<span className="bg-primary text-primary-foreground absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-semibold">
{unreadCountData.count > 99 ? '99+' : unreadCountData.count}
</span>
)} */}
)}
</Link>
</Button>

View File

@ -2,12 +2,14 @@ import { useMemo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { ReadStatus } from '@prisma/client';
import { Link } from 'react-router';
import LogoImage from '@documenso/assets/logo.png';
import { authClient } from '@documenso/auth/client';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
import { trpc } from '@documenso/trpc/react';
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
@ -25,6 +27,15 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
const currentTeam = useOptionalCurrentTeam();
const { data: unreadCountData } = trpc.document.inbox.getCount.useQuery(
{
readStatus: ReadStatus.NOT_OPENED,
},
{
// refetchInterval: 30000, // Refetch every 30 seconds
},
);
const handleMenuItemClick = () => {
onMenuOpenChange?.(false);
};
@ -52,11 +63,11 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
return [
{
href: `/t/${teamUrl}/documents`,
label: t`Documents`,
text: t`Documents`,
},
{
href: `/t/${teamUrl}/templates`,
label: t`Templates`,
text: t`Templates`,
},
{
href: '/inbox',
@ -86,11 +97,16 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
{menuNavigationLinks.map(({ href, text }) => (
<Link
key={href}
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
className="text-foreground hover:text-foreground/80 flex items-center gap-2 text-2xl font-semibold"
to={href}
onClick={() => handleMenuItemClick()}
>
{text}
{href === '/inbox' && unreadCountData && unreadCountData.count > 0 && (
<span className="bg-primary text-primary-foreground flex h-6 min-w-[1.5rem] items-center justify-center rounded-full px-1.5 text-xs font-semibold">
{unreadCountData.count > 99 ? '99+' : unreadCountData.count}
</span>
)}
</Link>
))}

View File

@ -10,6 +10,7 @@ import { useRevalidator } from 'react-router';
import { P, match } from 'ts-pattern';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import { AUTO_SIGNABLE_FIELD_TYPES } from '@documenso/lib/constants/autosign';
import { DocumentAuth } from '@documenso/lib/types/document-auth';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { trpc } from '@documenso/trpc/react';
@ -30,13 +31,6 @@ import { DocumentSigningDisclosure } from '~/components/general/document-signing
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
import { useRequiredDocumentSigningContext } from './document-signing-provider';
const AUTO_SIGNABLE_FIELD_TYPES: string[] = [
FieldType.NAME,
FieldType.INITIALS,
FieldType.EMAIL,
FieldType.DATE,
];
// The action auth types that are not allowed to be auto signed
//
// Reasoning: If the action auth is a passkey or 2FA, it's likely that the owner of the document

View File

@ -286,6 +286,7 @@ export const DocumentSigningCheckboxField = ({
className="h-3 w-3"
id={`checkbox-${field.id}-${item.id}`}
checked={checkedValues.includes(itemValue)}
disabled={isReadOnly}
onCheckedChange={() => handleCheckboxChange(item.value, item.id)}
/>
{!item.value.includes('empty-value-') && item.value && (
@ -314,7 +315,7 @@ export const DocumentSigningCheckboxField = ({
className="h-3 w-3"
id={`checkbox-${field.id}-${item.id}`}
checked={parsedCheckedValues.includes(itemValue)}
disabled={isLoading}
disabled={isLoading || isReadOnly}
onCheckedChange={() => void handleCheckboxOptionClick(item)}
/>
{!item.value.includes('empty-value-') && item.value && (

View File

@ -131,7 +131,12 @@ export const DocumentSigningFieldContainer = ({
return (
<div className={cn('[container-type:size]')}>
<FieldRootContainer color={RECIPIENT_COLOR_STYLES.green} field={field}>
<FieldRootContainer
color={
field.fieldMeta?.readOnly ? RECIPIENT_COLOR_STYLES.readOnly : RECIPIENT_COLOR_STYLES.green
}
field={field}
>
{!field.inserted && !loading && !readOnlyField && (
<button
type="submit"
@ -140,14 +145,6 @@ export const DocumentSigningFieldContainer = ({
/>
)}
{readOnlyField && (
<button className="bg-background/40 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 duration-200 group-hover:opacity-100">
<span className="bg-foreground/50 text-background rounded-xl p-2">
<Trans>Read only field</Trans>
</span>
</button>
)}
{type === 'Checkbox' && field.inserted && !loading && !readOnlyField && (
<button
className="absolute -bottom-10 flex items-center justify-evenly rounded-md border bg-gray-900 opacity-0 group-hover:opacity-100"

View File

@ -34,7 +34,7 @@ export const DocumentSigningFieldsInserted = ({
textAlign = 'left',
}: DocumentSigningFieldsInsertedProps) => {
return (
<div className="flex h-full w-full items-center">
<div className="flex h-full w-full items-center overflow-hidden">
<p
className={cn(
'text-foreground w-full text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',

View File

@ -41,6 +41,7 @@ export const DocumentSigningRadioField = ({
const { recipient, targetSigner, isAssistantMode } = useDocumentSigningRecipientContext();
const parsedFieldMeta = ZRadioFieldMeta.parse(field.fieldMeta);
const isReadOnly = parsedFieldMeta.readOnly;
const values = parsedFieldMeta.values?.map((item) => ({
...item,
value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
@ -164,6 +165,7 @@ export const DocumentSigningRadioField = ({
value={item.value}
id={`option-${field.id}-${item.id}`}
checked={item.checked}
disabled={isReadOnly}
/>
{!item.value.includes('empty-value-') && item.value && (
<Label
@ -187,6 +189,7 @@ export const DocumentSigningRadioField = ({
value={item.value}
id={`option-${field.id}-${item.id}`}
checked={item.value === field.customText}
disabled={isReadOnly}
/>
{!item.value.includes('empty-value-') && item.value && (
<Label

View File

@ -262,9 +262,7 @@ export const DocumentSigningTextField = ({
{field.inserted && (
<DocumentSigningFieldsInserted textAlign={parsedFieldMeta?.textAlign}>
{field.customText.length < 20
? field.customText
: field.customText.substring(0, 20) + '...'}
{field.customText}
</DocumentSigningFieldsInserted>
)}

View File

@ -4,6 +4,7 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
import { useNavigate, useSearchParams } from 'react-router';
import { z } from 'zod';
import { DocumentSignatureType } from '@documenso/lib/constants/document';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
@ -12,6 +13,7 @@ import {
SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc';
import type { TDocument } from '@documenso/lib/types/document';
import { ZDocumentAccessAuthTypesSchema } from '@documenso/lib/types/document-auth';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -175,13 +177,17 @@ export const DocumentEditForm = ({
try {
const { timezone, dateFormat, redirectUrl, language, signatureTypes } = data.meta;
const parsedGlobalAccessAuth = z
.array(ZDocumentAccessAuthTypesSchema)
.safeParse(data.globalAccessAuth);
await updateDocument({
documentId: document.id,
data: {
title: data.title,
externalId: data.externalId || null,
visibility: data.visibility,
globalAccessAuth: data.globalAccessAuth ?? [],
globalAccessAuth: parsedGlobalAccessAuth.success ? parsedGlobalAccessAuth.data : [],
globalActionAuth: data.globalActionAuth ?? [],
},
meta: {

View File

@ -1,26 +0,0 @@
import React from 'react';
import { Badge } from '@documenso/ui/primitives/badge';
export type DocumentHistorySheetChangesProps = {
values: {
key: string | React.ReactNode;
value: string | React.ReactNode;
}[];
};
export const DocumentHistorySheetChanges = ({ values }: DocumentHistorySheetChangesProps) => {
return (
<Badge
className="text-muted-foreground mt-3 block w-full space-y-0.5 text-xs"
variant="neutral"
>
{values.map(({ key, value }, i) => (
<p key={typeof key === 'string' ? key : i}>
<span>{key}: </span>
<span className="font-normal">{value}</span>
</p>
))}
</Badge>
);
};

View File

@ -1,410 +0,0 @@
import { useMemo, useState } from 'react';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { ArrowRightIcon, Loader } from 'lucide-react';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { UAParser } from 'ua-parser-js';
import { DOCUMENT_AUDIT_LOG_EMAIL_FORMAT } from '@documenso/lib/constants/document-audit-logs';
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { Sheet, SheetContent, SheetTrigger } from '@documenso/ui/primitives/sheet';
import { DocumentHistorySheetChanges } from './document-history-sheet-changes';
export type DocumentHistorySheetProps = {
documentId: number;
userId: number;
isMenuOpen?: boolean;
onMenuOpenChange?: (_value: boolean) => void;
children?: React.ReactNode;
};
export const DocumentHistorySheet = ({
documentId,
userId,
isMenuOpen,
onMenuOpenChange,
children,
}: DocumentHistorySheetProps) => {
const { _, i18n } = useLingui();
const [isUserDetailsVisible, setIsUserDetailsVisible] = useState(false);
const {
data,
isLoading,
isLoadingError,
refetch,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = trpc.document.findDocumentAuditLogs.useInfiniteQuery(
{
documentId,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
placeholderData: (previousData) => previousData,
},
);
const documentAuditLogs = useMemo(() => (data?.pages ?? []).flatMap((page) => page.data), [data]);
const extractBrowser = (userAgent?: string | null) => {
if (!userAgent) {
return 'Unknown';
}
const parser = new UAParser(userAgent);
parser.setUA(userAgent);
const result = parser.getResult();
return result.browser.name;
};
/**
* Applies the following formatting for a given text:
* - Uppercase first lower, lowercase rest
* - Replace _ with spaces
*
* @param text The text to format
* @returns The formatted text
*/
const formatGenericText = (text?: string | string[] | null): string => {
if (!text) {
return '';
}
if (Array.isArray(text)) {
return text.map((t) => formatGenericText(t)).join(', ');
}
return (text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()).replaceAll('_', ' ');
};
return (
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
{children && <SheetTrigger asChild>{children}</SheetTrigger>}
<SheetContent
sheetClass="backdrop-blur-none"
className="flex w-full max-w-[500px] flex-col overflow-y-auto p-0"
>
<div className="text-foreground px-6 pt-6">
<h1 className="text-lg font-medium">
<Trans>Document history</Trans>
</h1>
<button
className="text-muted-foreground text-sm"
onClick={() => setIsUserDetailsVisible(!isUserDetailsVisible)}
>
{isUserDetailsVisible ? (
<Trans>Hide additional information</Trans>
) : (
<Trans>Show additional information</Trans>
)}
</button>
</div>
{isLoading && (
<div className="flex h-full items-center justify-center">
<Loader className="text-muted-foreground h-6 w-6 animate-spin" />
</div>
)}
{isLoadingError && (
<div className="flex h-full flex-col items-center justify-center">
<p className="text-foreground/80 text-sm">
<Trans>Unable to load document history</Trans>
</p>
<button
onClick={async () => refetch()}
className="text-foreground/70 hover:text-muted-foreground mt-2 text-sm"
>
<Trans>Click here to retry</Trans>
</button>
</div>
)}
{data && (
<ul
className={cn('divide-y border-t', {
'mb-4 border-b': !hasNextPage,
})}
>
{documentAuditLogs.map((auditLog) => (
<li className="px-4 py-2.5" key={auditLog.id}>
<div className="flex flex-row items-center">
<Avatar className="mr-2 h-9 w-9">
<AvatarFallback className="text-xs text-gray-400">
{(auditLog?.email ?? auditLog?.name ?? '?').slice(0, 1).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<p className="text-foreground text-xs font-bold">
{formatDocumentAuditLogAction(_, auditLog, userId).description}
</p>
<p className="text-foreground/50 text-xs">
{DateTime.fromJSDate(auditLog.createdAt)
.setLocale(i18n.locales?.[0] || i18n.locale)
.toFormat('d MMM, yyyy HH:MM a')}
</p>
</div>
</div>
{match(auditLog)
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM },
() => null,
)
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED },
({ data }) => {
const values = [
{
key: 'Email',
value: data.recipientEmail,
},
{
key: 'Role',
value: formatGenericText(data.recipientRole),
},
];
// Insert the name to the start of the array if available.
if (data.recipientName) {
values.unshift({
key: 'Name',
value: data.recipientName,
});
}
return <DocumentHistorySheetChanges values={values} />;
},
)
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED }, ({ data }) => {
if (data.changes.length === 0) {
return null;
}
return (
<DocumentHistorySheetChanges
values={data.changes.map(({ type, from, to }) => ({
key: formatGenericText(type),
value: (
<span className="inline-flex flex-row items-center">
<span>{type === 'ROLE' ? formatGenericText(from) : from}</span>
<ArrowRightIcon className="h-4 w-4" />
<span>{type === 'ROLE' ? formatGenericText(to) : to}</span>
</span>
),
}))}
/>
);
})
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED },
({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Field',
value: formatGenericText(data.fieldType),
},
{
key: 'Recipient',
value: formatGenericText(data.fieldRecipientEmail),
},
]}
/>
),
)
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED },
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED },
({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Old',
value: Array.isArray(data.from)
? data.from
.map((f) => DOCUMENT_AUTH_TYPES[f]?.value || 'None')
.join(', ')
: DOCUMENT_AUTH_TYPES[data.from || '']?.value || 'None',
},
{
key: 'New',
value: Array.isArray(data.to)
? data.to
.map((f) => DOCUMENT_AUTH_TYPES[f]?.value || 'None')
.join(', ')
: DOCUMENT_AUTH_TYPES[data.to || '']?.value || 'None',
},
]}
/>
),
)
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED }, ({ data }) => {
if (data.changes.length === 0) {
return null;
}
return (
<DocumentHistorySheetChanges
values={data.changes.map((change) => ({
key: formatGenericText(change.type),
value: change.type === 'PASSWORD' ? '*********' : change.to,
}))}
/>
);
})
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED }, ({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Old',
value: data.from,
},
{
key: 'New',
value: data.to,
},
]}
/>
))
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED },
({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Old',
value: data.from,
},
{
key: 'New',
value: data.to,
},
]}
/>
),
)
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED }, ({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Field inserted',
value: formatGenericText(data.field.type),
},
]}
/>
))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED }, ({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Field uninserted',
value: formatGenericText(data.field),
},
]}
/>
))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT }, ({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Type',
value: DOCUMENT_AUDIT_LOG_EMAIL_FORMAT[data.emailType].description,
},
{
key: 'Sent to',
value: data.recipientEmail,
},
]}
/>
))
.with(
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED },
({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Old',
value: data.from,
},
{
key: 'New',
value: data.to,
},
]}
/>
),
)
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED }, ({ data }) => (
<DocumentHistorySheetChanges
values={[
{
key: 'Field prefilled',
value: formatGenericText(data.field.type),
},
]}
/>
))
.exhaustive()}
{isUserDetailsVisible && (
<>
<div className="mb-1 mt-2 flex flex-row space-x-2">
<Badge variant="neutral" className="text-muted-foreground">
IP: {auditLog.ipAddress ?? 'Unknown'}
</Badge>
<Badge variant="neutral" className="text-muted-foreground">
Browser: {extractBrowser(auditLog.userAgent)}
</Badge>
</div>
</>
)}
</li>
))}
{hasNextPage && (
<div className="flex items-center justify-center py-4">
<Button
variant="outline"
loading={isFetchingNextPage}
onClick={async () => fetchNextPage()}
>
Show more
</Button>
</div>
)}
</ul>
)}
</SheetContent>
</Sheet>
);
};

View File

@ -30,8 +30,12 @@ export const DocumentSearch = ({ initialValue = '' }: { initialValue?: string })
);
useEffect(() => {
handleSearch(searchTerm);
}, [debouncedSearchTerm]);
const currentQueryParam = searchParams.get('query') || '';
if (debouncedSearchTerm !== currentQueryParam) {
handleSearch(debouncedSearchTerm);
}
}, [debouncedSearchTerm, searchParams]);
return (
<Input

View File

@ -1,19 +1,32 @@
import { FolderIcon, PinIcon } from 'lucide-react';
import { Plural, Trans } from '@lingui/react/macro';
import { FolderType } from '@prisma/client';
import {
ArrowRightIcon,
FolderIcon,
FolderPlusIcon,
MoreVerticalIcon,
PinIcon,
SettingsIcon,
TrashIcon,
} from 'lucide-react';
import { Link } from 'react-router';
import { FolderType } from '@documenso/lib/types/folder-type';
import { formatFolderCount } from '@documenso/lib/utils/format-folder-count';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
import { useCurrentTeam } from '~/providers/team';
export type FolderCardProps = {
folder: TFolderWithSubfolders;
onNavigate: (folderId: string) => void;
onMove: (folder: TFolderWithSubfolders) => void;
onPin: (folderId: string) => void;
onUnpin: (folderId: string) => void;
@ -23,66 +36,132 @@ export type FolderCardProps = {
export const FolderCard = ({
folder,
onNavigate,
onMove,
onPin,
onUnpin,
onSettings,
onDelete,
}: FolderCardProps) => {
const team = useCurrentTeam();
const formatPath = () => {
const rootPath =
folder.type === FolderType.DOCUMENT
? formatDocumentsPath(team.url)
: formatTemplatesPath(team.url);
return `${rootPath}/f/${folder.id}`;
};
return (
<div
key={folder.id}
className="border-border hover:border-muted-foreground/40 group relative flex flex-col rounded-lg border p-4 transition-all hover:shadow-sm"
>
<div className="flex items-start justify-between">
<button
className="flex items-center space-x-2 text-left"
onClick={() => onNavigate(folder.id)}
>
<FolderIcon className="text-documenso h-6 w-6" />
<div>
<div className="flex items-center gap-2">
<h3 className="font-medium">{folder.name}</h3>
{folder.pinned && <PinIcon className="text-documenso h-3 w-3" />}
</div>
<div className="mt-1 flex space-x-2 text-xs text-gray-500">
<span>
{formatFolderCount(
folder.type === FolderType.TEMPLATE
? folder._count.templates
: folder._count.documents,
folder.type === FolderType.TEMPLATE ? 'template' : 'document',
folder.type === FolderType.TEMPLATE ? 'templates' : 'documents',
)}
</span>
<span></span>
<span>{formatFolderCount(folder._count.subfolders, 'folder', 'folders')}</span>
<Link to={formatPath()} key={folder.id}>
<Card className="hover:bg-muted/50 border-border h-full border transition-all">
<CardContent className="p-4">
<div className="flex min-w-0 items-center gap-3">
<FolderIcon className="text-documenso h-6 w-6 flex-shrink-0" />
<div className="flex w-full min-w-0 items-center justify-between">
<div className="min-w-0 flex-1">
<h3 className="flex min-w-0 items-center gap-2 font-medium">
<span className="truncate">{folder.name}</span>
{folder.pinned && <PinIcon className="text-documenso h-3 w-3 flex-shrink-0" />}
</h3>
<div className="text-muted-foreground mt-1 flex space-x-2 truncate text-xs">
<span>
{folder.type === FolderType.TEMPLATE ? (
<Plural
value={folder._count.templates}
one={<Trans># template</Trans>}
other={<Trans># templates</Trans>}
/>
) : (
<Plural
value={folder._count.documents}
one={<Trans># document</Trans>}
other={<Trans># documents</Trans>}
/>
)}
</span>
<span></span>
<span>
<Plural
value={folder._count.subfolders}
one={<Trans># folder</Trans>}
other={<Trans># folders</Trans>}
/>
</span>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
data-testid="folder-card-more-button"
>
<MoreVerticalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent onClick={(e) => e.stopPropagation()} align="end">
<DropdownMenuItem onClick={() => onMove(folder)}>
<ArrowRightIcon className="mr-2 h-4 w-4" />
<Trans>Move</Trans>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => (folder.pinned ? onUnpin(folder.id) : onPin(folder.id))}
>
<PinIcon className="mr-2 h-4 w-4" />
{folder.pinned ? <Trans>Unpin</Trans> : <Trans>Pin</Trans>}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onSettings(folder)}>
<SettingsIcon className="mr-2 h-4 w-4" />
<Trans>Settings</Trans>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onDelete(folder)}>
<TrashIcon className="mr-2 h-4 w-4" />
<Trans>Delete</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="opacity-0 group-hover:opacity-100">
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onMove(folder)}>Move</DropdownMenuItem>
{folder.pinned ? (
<DropdownMenuItem onClick={() => onUnpin(folder.id)}>Unpin</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={() => onPin(folder.id)}>Pin</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => onSettings(folder)}>Settings</DropdownMenuItem>
<DropdownMenuItem className="text-red-500" onClick={() => onDelete(folder)}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</CardContent>
</Card>
</Link>
);
};
export const FolderCardEmpty = ({ type }: { type: FolderType }) => {
return (
<Card className="hover:bg-muted/50 border-border h-full border transition-all">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<FolderPlusIcon className="text-muted-foreground/60 h-6 w-6" />
<div>
<h3 className="text-muted-foreground flex items-center gap-2 font-medium">
<Trans>Create folder</Trans>
</h3>
<div className="text-muted-foreground/60 mt-1 flex space-x-2 truncate text-xs">
{type === FolderType.DOCUMENT ? (
<Trans>Organise your documents</Trans>
) : (
<Trans>Organise your templates</Trans>
)}
</div>
</div>
</div>
</CardContent>
</Card>
);
};

View File

@ -0,0 +1,249 @@
import { useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { FolderType } from '@prisma/client';
import { FolderIcon, HomeIcon } from 'lucide-react';
import { Link } from 'react-router';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
import { TemplateCreateDialog } from '~/components/dialogs/template-create-dialog';
import { DocumentUploadDropzone } from '~/components/general/document/document-upload';
import { FolderCard, FolderCardEmpty } from '~/components/general/folder/folder-card';
import { useCurrentTeam } from '~/providers/team';
export type FolderGridProps = {
type: FolderType;
parentId: string | null;
};
export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
const team = useCurrentTeam();
const [isMovingFolder, setIsMovingFolder] = useState(false);
const [folderToMove, setFolderToMove] = useState<TFolderWithSubfolders | null>(null);
const [isDeletingFolder, setIsDeletingFolder] = useState(false);
const [folderToDelete, setFolderToDelete] = useState<TFolderWithSubfolders | null>(null);
const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false);
const [folderToSettings, setFolderToSettings] = useState<TFolderWithSubfolders | null>(null);
const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation();
const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation();
const { data: foldersData, isPending } = trpc.folder.getFolders.useQuery({
type,
parentId,
});
const formatBreadCrumbPath = (folderId: string) => {
const rootPath =
type === FolderType.DOCUMENT ? formatDocumentsPath(team.url) : formatTemplatesPath(team.url);
return `${rootPath}/f/${folderId}`;
};
const formatViewAllFoldersPath = () => {
const rootPath =
type === FolderType.DOCUMENT ? formatDocumentsPath(team.url) : formatTemplatesPath(team.url);
return `${rootPath}/folders`;
};
const formatRootPath = () => {
return type === FolderType.DOCUMENT
? formatDocumentsPath(team.url)
: formatTemplatesPath(team.url);
};
const pinnedFolders = foldersData?.folders.filter((folder) => folder.pinned) || [];
const unpinnedFolders = foldersData?.folders.filter((folder) => !folder.pinned) || [];
return (
<div>
<div className="mb-4 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div
className="text-muted-foreground hover:text-muted-foreground/80 flex flex-1 items-center text-sm font-medium"
data-testid="folder-grid-breadcrumbs"
>
<Link to={formatRootPath()} className="flex items-center">
<HomeIcon className="mr-2 h-4 w-4" />
<Trans>Home</Trans>
</Link>
{isPending && parentId ? (
<div className="flex items-center">
<Skeleton className="mx-3 h-4 w-1 rotate-12" />
<Skeleton className="h-4 w-20" />
</div>
) : (
foldersData?.breadcrumbs.map((folder) => (
<div key={folder.id} className="flex items-center">
<span className="px-3">/</span>
<Link to={formatBreadCrumbPath(folder.id)} className="flex items-center">
<FolderIcon className="mr-2 h-4 w-4" />
<span>{folder.name}</span>
</Link>
</div>
))
)}
</div>
<div className="flex gap-4 sm:flex-row sm:justify-end">
{type === FolderType.DOCUMENT ? (
<DocumentUploadDropzone />
) : (
<TemplateCreateDialog folderId={parentId ?? undefined} />
)}
<FolderCreateDialog type={type} />
</div>
</div>
{isPending ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="border-border bg-card h-full rounded-lg border px-4 py-5">
<div className="flex items-center gap-3">
<Skeleton className="h-8 w-8 rounded" />
<div className="flex w-full items-center justify-between">
<div className="flex-1">
<Skeleton className="mb-2 h-4 w-24" />
<div className="flex space-x-2">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-3 w-3" />
<Skeleton className="h-3 w-12" />
</div>
</div>
<Skeleton className="h-8 w-2 rounded" />
</div>
</div>
</div>
))}
</div>
) : foldersData && foldersData.folders.length === 0 ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<FolderCreateDialog
type={type}
trigger={
<button>
<FolderCardEmpty type={type} />
</button>
}
/>
</div>
) : (
foldersData && (
<div key="content" className="space-y-4">
{pinnedFolders.length > 0 && (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{pinnedFolders.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onPin={(folderId) => void pinFolder({ folderId })}
onUnpin={(folderId) => void unpinFolder({ folderId })}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
)}
{unpinnedFolders.length > 0 && (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{unpinnedFolders.slice(0, 12).map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onPin={(folderId) => void pinFolder({ folderId })}
onUnpin={(folderId) => void unpinFolder({ folderId })}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
)}
{foldersData.folders.length > 12 && (
<div className="mt-2 flex items-center justify-center">
<Link
className="text-muted-foreground hover:text-foreground text-sm font-medium"
to={formatViewAllFoldersPath()}
>
View all folders
</Link>
</div>
)}
</div>
)
)}
<FolderMoveDialog
foldersData={foldersData?.folders}
folder={folderToMove}
isOpen={isMovingFolder}
onOpenChange={(open) => {
setIsMovingFolder(open);
if (!open) {
setFolderToMove(null);
}
}}
/>
<FolderSettingsDialog
folder={folderToSettings}
isOpen={isSettingsFolderOpen}
onOpenChange={(open) => {
setIsSettingsFolderOpen(open);
if (!open) {
setFolderToSettings(null);
}
}}
/>
{folderToDelete && (
<FolderDeleteDialog
folder={folderToDelete}
isOpen={isDeletingFolder}
onOpenChange={(open) => {
setIsDeletingFolder(open);
if (!open) {
setFolderToDelete(null);
}
}}
/>
)}
</div>
);
};

View File

@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { useNavigate } from 'react-router';
import { z } from 'zod';
import { DocumentSignatureType } from '@documenso/lib/constants/document';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
@ -10,6 +11,7 @@ import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
SKIP_QUERY_BATCH_META,
} from '@documenso/lib/constants/trpc';
import { ZDocumentAccessAuthTypesSchema } from '@documenso/lib/types/document-auth';
import type { TTemplate } from '@documenso/lib/types/template';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
@ -125,6 +127,10 @@ export const TemplateEditForm = ({
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
const { signatureTypes } = data.meta;
const parsedGlobalAccessAuth = z
.array(ZDocumentAccessAuthTypesSchema)
.safeParse(data.globalAccessAuth);
try {
await updateTemplateSettings({
templateId: template.id,
@ -132,7 +138,7 @@ export const TemplateEditForm = ({
title: data.title,
externalId: data.externalId || null,
visibility: data.visibility,
globalAccessAuth: data.globalAccessAuth ?? [],
globalAccessAuth: parsedGlobalAccessAuth.success ? parsedGlobalAccessAuth.data : [],
globalActionAuth: data.globalActionAuth ?? [],
},
meta: {

View File

@ -36,23 +36,7 @@ const { trackPageview } = Plausible({
trackLocalhost: false,
});
export const links: Route.LinksFunction = () => [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{
rel: 'preconnect',
href: 'https://fonts.gstatic.com',
crossOrigin: 'anonymous',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400..600&display=swap',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
},
{ rel: 'stylesheet', href: stylesheet },
];
export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: stylesheet }];
export function meta() {
return appMetaTags();

View File

@ -18,11 +18,14 @@ import {
import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
import {
getMonthlyActiveUsers,
getOrganisationsWithSubscriptionsCount,
getUserWithSignedDocumentMonthlyGrowth,
getUsersCount,
} from '@documenso/lib/server-only/admin/get-users-stats';
import { getSignerConversionMonthly } from '@documenso/lib/server-only/user/get-signer-conversion';
import { MonthlyActiveUsersChart } from '~/components/general/admin-monthly-active-user-charts';
import { AdminStatsSignerConversionChart } from '~/components/general/admin-stats-signer-conversion-chart';
import { AdminStatsUsersWithDocumentsChart } from '~/components/general/admin-stats-users-with-documents';
import { CardMetric } from '~/components/general/metric-card';
@ -37,18 +40,16 @@ export async function loader() {
docStats,
recipientStats,
signerConversionMonthly,
// userWithAtLeastOneDocumentPerMonth,
// userWithAtLeastOneDocumentSignedPerMonth,
// MONTHLY_USERS_SIGNED,
monthlyUsersWithDocuments,
monthlyActiveUsers,
] = await Promise.all([
getUsersCount(),
getOrganisationsWithSubscriptionsCount(),
getDocumentStats(),
getRecipientsStats(),
getSignerConversionMonthly(),
// getUserWithAtLeastOneDocumentPerMonth(),
// getUserWithAtLeastOneDocumentSignedPerMonth(),
// getUserWithSignedDocumentMonthlyGrowth(),
getUserWithSignedDocumentMonthlyGrowth(),
getMonthlyActiveUsers(),
]);
return {
@ -57,7 +58,8 @@ export async function loader() {
docStats,
recipientStats,
signerConversionMonthly,
// MONTHLY_USERS_SIGNED,
monthlyUsersWithDocuments,
monthlyActiveUsers,
};
}
@ -70,7 +72,8 @@ export default function AdminStatsPage({ loaderData }: Route.ComponentProps) {
docStats,
recipientStats,
signerConversionMonthly,
// MONTHLY_USERS_SIGNED,
monthlyUsersWithDocuments,
monthlyActiveUsers,
} = loaderData;
return (
@ -147,15 +150,21 @@ export default function AdminStatsPage({ loaderData }: Route.ComponentProps) {
<Trans>Charts</Trans>
</h3>
<div className="mt-5 grid grid-cols-2 gap-8">
<MonthlyActiveUsersChart title={_(msg`MAU (signed in)`)} data={monthlyActiveUsers} />
<MonthlyActiveUsersChart
title={_(msg`Cumulative MAU (signed in)`)}
data={monthlyActiveUsers}
cummulative
/>
<AdminStatsUsersWithDocumentsChart
data={[]}
// data={MONTHLY_USERS_SIGNED}
data={monthlyUsersWithDocuments}
title={_(msg`MAU (created document)`)}
tooltip={_(msg`Monthly Active Users: Users that created at least one Document`)}
/>
<AdminStatsUsersWithDocumentsChart
data={[]}
// data={MONTHLY_USERS_SIGNED}
data={monthlyUsersWithDocuments}
completed
title={_(msg`MAU (had document completed)`)}
tooltip={_(

View File

@ -171,6 +171,27 @@ export default function SettingsSecurity({ loaderData }: Route.ComponentProps) {
</Link>
</Button>
</Alert>
<Alert
className="mt-6 flex flex-col justify-between p-6 sm:flex-row sm:items-center"
variant="neutral"
>
<div className="mb-4 mr-4 sm:mb-0">
<AlertTitle>
<Trans>Active sessions</Trans>
</AlertTitle>
<AlertDescription className="mr-2">
<Trans>View and manage all active sessions for your account.</Trans>
</AlertDescription>
</div>
<Button asChild variant="outline" className="bg-background">
<Link to="/settings/security/sessions">
<Trans>Manage sessions</Trans>
</Link>
</Button>
</Alert>
</div>
);
}

View File

@ -0,0 +1,192 @@
import { useMemo, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { useQuery } from '@tanstack/react-query';
import { DateTime } from 'luxon';
import { UAParser } from 'ua-parser-js';
import { authClient } from '@documenso/auth/client';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { SessionLogoutAllDialog } from '~/components/dialogs/session-logout-all-dialog';
import { SettingsHeader } from '~/components/general/settings-header';
import { appMetaTags } from '~/utils/meta';
export function meta() {
return appMetaTags('Active Sessions');
}
const parser = new UAParser();
export default function SettingsSecuritySessions() {
const { t } = useLingui();
const { data, isLoading, isLoadingError, refetch } = useQuery({
queryKey: ['active-sessions'],
queryFn: async () => await authClient.getSessions(),
});
const { session } = useSession();
const results = data?.sessions ?? [];
const columns = useMemo(() => {
return [
{
header: t`Device`,
accessorKey: 'userAgent',
cell: ({ row }) => {
const userAgent = row.original.userAgent || '';
parser.setUA(userAgent);
const result = parser.getResult();
const browser = result.browser.name || t`Unknown`;
const os = result.os.name || t`Unknown`;
const isCurrentSession = row.original.id === session?.id;
return (
<div className="flex items-center gap-2">
<span>
{browser} ({os})
</span>
{isCurrentSession && (
<Badge>
<Trans>Current</Trans>
</Badge>
)}
</div>
);
},
},
{
header: t`IP Address`,
accessorKey: 'ipAddress',
cell: ({ row }) => row.original.ipAddress || t`Unknown`,
},
{
header: t`Last Active`,
accessorKey: 'updatedAt',
cell: ({ row }) => DateTime.fromJSDate(row.original.updatedAt).toRelative(),
},
{
header: t`Created`,
accessorKey: 'createdAt',
cell: ({ row }) => DateTime.fromJSDate(row.original.createdAt).toRelative(),
},
{
id: 'actions',
cell: ({ row }) => (
<SessionRevokeButton
sessionId={row.original.id}
isCurrentSession={row.original.id === session?.id}
onSuccess={refetch}
/>
),
},
] satisfies DataTableColumnDef<(typeof results)[number]>[];
}, []);
return (
<div>
<SettingsHeader
title={t`Active sessions`}
subtitle={t`View and manage all active sessions for your account.`}
>
<SessionLogoutAllDialog onSuccess={refetch} disabled={results.length === 1 || isLoading} />
</SettingsHeader>
<div className="mt-4">
<DataTable
columns={columns}
data={results}
hasFilters={false}
error={{
enable: isLoadingError,
}}
skeleton={{
enable: isLoading,
rows: 3,
component: (
<>
<TableCell>
<Skeleton className="h-4 w-40 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-24 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-24 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-24 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-8 w-16 rounded" />
</TableCell>
</>
),
}}
/>
</div>
</div>
);
}
type SessionRevokeButtonProps = {
sessionId: string;
isCurrentSession: boolean;
onSuccess: () => Promise<unknown>;
};
const SessionRevokeButton = ({
sessionId,
isCurrentSession,
onSuccess,
}: SessionRevokeButtonProps) => {
const { toast } = useToast();
const { t } = useLingui();
const [isLoading, setIsLoading] = useState(false);
const handleRevoke = async () => {
setIsLoading(true);
try {
await authClient.signOutSession({
sessionId,
redirectPath: isCurrentSession ? '/signin' : undefined,
});
if (!isCurrentSession) {
await onSuccess();
}
toast({
title: t`Session revoked`,
});
} catch (error) {
console.error(error);
toast({
title: t`Error`,
description: t`Failed to revoke session`,
variant: 'destructive',
});
}
setIsLoading(false);
};
return (
<Button variant="destructive" size="sm" onClick={handleRevoke} loading={isLoading}>
<Trans>Revoke</Trans>
</Button>
);
};

View File

@ -1,7 +1,7 @@
import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
import { ChevronLeft, Users2 } from 'lucide-react';
import { Link, redirect } from 'react-router';
import { match } from 'ts-pattern';
@ -13,11 +13,9 @@ import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { DocumentHistorySheet } from '~/components/general/document/document-history-sheet';
import { DocumentPageViewButton } from '~/components/general/document/document-page-view-button';
import { DocumentPageViewDropdown } from '~/components/general/document/document-page-view-dropdown';
import { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information';
@ -101,9 +99,6 @@ export default function DocumentPage() {
const { recipients, documentData, documentMeta } = document;
// This was a feature flag. Leave to false since it's not ready.
const isDocumentHistoryEnabled = false;
return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
{document.status === DocumentStatus.PENDING && (
@ -154,17 +149,6 @@ export default function DocumentPage() {
)}
</div>
</div>
{isDocumentHistoryEnabled && (
<div className="self-end">
<DocumentHistorySheet documentId={document.id} userId={user.id}>
<Button variant="outline">
<Clock9 className="mr-1.5 h-4 w-4" />
<Trans>Document history</Trans>
</Button>
</DocumentHistorySheet>
</div>
)}
</div>
<div className="mt-6 grid w-full grid-cols-12 gap-8">

View File

@ -1,14 +1,12 @@
import { useEffect, useMemo, useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { OrganisationType } from '@prisma/client';
import { FolderIcon, HomeIcon, Loader2 } from 'lucide-react';
import { useNavigate, useSearchParams } from 'react-router';
import { FolderType, OrganisationType } from '@prisma/client';
import { useParams, useSearchParams } from 'react-router';
import { Link } from 'react-router';
import { z } from 'zod';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { FolderType } from '@documenso/lib/types/folder-type';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { parseToIntegerArray } from '@documenso/lib/utils/params';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
@ -18,21 +16,14 @@ import {
type TFindDocumentsInternalResponse,
ZFindDocumentsInternalRequestSchema,
} from '@documenso/trpc/server/document-router/schema';
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { DocumentMoveToFolderDialog } from '~/components/dialogs/document-move-to-folder-dialog';
import { CreateFolderDialog } from '~/components/dialogs/folder-create-dialog';
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper';
import { DocumentSearch } from '~/components/general/document/document-search';
import { DocumentStatus } from '~/components/general/document/document-status';
import { DocumentUploadDropzone } from '~/components/general/document/document-upload';
import { FolderCard } from '~/components/general/folder/folder-card';
import { FolderGrid } from '~/components/general/folder/folder-grid';
import { PeriodSelector } from '~/components/general/period-selector';
import { DocumentsTable } from '~/components/tables/documents-table';
import { DocumentsTableEmptyState } from '~/components/tables/documents-table-empty-state';
@ -55,23 +46,14 @@ const ZSearchParamsSchema = ZFindDocumentsInternalRequestSchema.pick({
});
export default function DocumentsPage() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const organisation = useCurrentOrganisation();
const team = useCurrentTeam();
const { folderId } = useParams();
const [searchParams] = useSearchParams();
const [isMovingDocument, setIsMovingDocument] = useState(false);
const [documentToMove, setDocumentToMove] = useState<number | null>(null);
const [isMovingFolder, setIsMovingFolder] = useState(false);
const [folderToMove, setFolderToMove] = useState<TFolderWithSubfolders | null>(null);
const [isDeletingFolder, setIsDeletingFolder] = useState(false);
const [folderToDelete, setFolderToDelete] = useState<TFolderWithSubfolders | null>(null);
const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false);
const [folderToSettings, setFolderToSettings] = useState<TFolderWithSubfolders | null>(null);
const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation();
const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation();
const [stats, setStats] = useState<TFindDocumentsInternalResponse['stats']>({
[ExtendedDocumentStatus.DRAFT]: 0,
@ -87,26 +69,11 @@ export default function DocumentsPage() {
[searchParams],
);
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocumentsInternal.useQuery(
{
...findDocumentSearchParams,
},
);
const {
data: foldersData,
isLoading: isFoldersLoading,
refetch: refetchFolders,
} = trpc.folder.getFolders.useQuery({
type: FolderType.DOCUMENT,
parentId: null,
const { data, isLoading, isLoadingError } = trpc.document.findDocumentsInternal.useQuery({
...findDocumentSearchParams,
folderId,
});
useEffect(() => {
void refetch();
void refetchFolders();
}, [team?.url]);
const getTabHref = (value: keyof typeof ExtendedDocumentStatus) => {
const params = new URLSearchParams(searchParams);
@ -124,7 +91,17 @@ export default function DocumentsPage() {
params.delete('page');
}
return `${formatDocumentsPath(team.url)}?${params.toString()}`;
let path = formatDocumentsPath(team.url);
if (folderId) {
path += `/f/${folderId}`;
}
if (params.toString()) {
path += `?${params.toString()}`;
}
return path;
};
useEffect(() => {
@ -133,147 +110,19 @@ export default function DocumentsPage() {
}
}, [data?.stats]);
const navigateToFolder = (folderId?: string | null) => {
const documentsPath = formatDocumentsPath(team.url);
if (folderId) {
void navigate(`${documentsPath}/f/${folderId}`);
} else {
void navigate(documentsPath);
}
};
const handleViewAllFolders = () => {
void navigate(`${formatDocumentsPath(team.url)}/folders`);
};
return (
<DocumentDropZoneWrapper>
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex flex-1 items-center">
<Button
variant="ghost"
size="sm"
className="flex items-center space-x-2 pl-0 hover:bg-transparent"
onClick={() => navigateToFolder(null)}
>
<HomeIcon className="h-4 w-4" />
<span>Home</span>
</Button>
<FolderGrid type={FolderType.DOCUMENT} parentId={folderId ?? null} />
{foldersData?.breadcrumbs.map((folder) => (
<div key={folder.id} className="flex items-center space-x-2">
<span>/</span>
<Button
variant="ghost"
size="sm"
className="flex items-center space-x-2 pl-1 hover:bg-transparent"
onClick={() => navigateToFolder(folder.id)}
>
<FolderIcon className="h-4 w-4" />
<span>{folder.name}</span>
</Button>
</div>
))}
</div>
<div className="flex gap-4 sm:flex-row sm:justify-end">
<DocumentUploadDropzone />
<CreateFolderDialog />
</div>
</div>
{isFoldersLoading ? (
<div className="mt-6 flex justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
) : (
<>
{foldersData?.folders?.some((folder) => folder.pinned) && (
<div className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData.folders
.filter((folder) => folder.pinned)
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onNavigate={navigateToFolder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onPin={(folderId) => void pinFolder({ folderId })}
onUnpin={(folderId) => void unpinFolder({ folderId })}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
</div>
)}
<div className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData?.folders
?.filter((folder) => !folder.pinned)
.slice(0, 12)
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onNavigate={navigateToFolder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onPin={(folderId) => void pinFolder({ folderId })}
onUnpin={(folderId) => void unpinFolder({ folderId })}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
<div className="mt-6 flex items-center justify-center">
{foldersData && foldersData.folders?.length > 12 && (
<Button
variant="link"
size="sm"
className="text-muted-foreground hover:text-foreground"
onClick={() => void handleViewAllFolders()}
>
View all folders
</Button>
)}
</div>
</div>
</>
)}
<div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
<div className="mt-8 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
<div className="flex flex-row items-center">
{team && (
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
<AvatarFallback className="text-muted-foreground text-xs">
{team.name.slice(0, 1)}
</AvatarFallback>
</Avatar>
)}
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
<AvatarFallback className="text-muted-foreground text-xs">
{team.name.slice(0, 1)}
</AvatarFallback>
</Avatar>
<h2 className="text-4xl font-semibold">
<Trans>Documents</Trans>
@ -329,9 +178,7 @@ export default function DocumentsPage() {
<div className="mt-8">
<div>
{data &&
data.count === 0 &&
(!foldersData?.folders.length || foldersData.folders.length === 0) ? (
{data && data.count === 0 ? (
<DocumentsTableEmptyState
status={findDocumentSearchParams.status || ExtendedDocumentStatus.ALL}
/>
@ -353,6 +200,7 @@ export default function DocumentsPage() {
<DocumentMoveToFolderDialog
documentId={documentToMove}
open={isMovingDocument}
currentFolderId={folderId}
onOpenChange={(open) => {
setIsMovingDocument(open);
@ -362,43 +210,6 @@ export default function DocumentsPage() {
}}
/>
)}
<FolderMoveDialog
foldersData={foldersData?.folders}
folder={folderToMove}
isOpen={isMovingFolder}
onOpenChange={(open) => {
setIsMovingFolder(open);
if (!open) {
setFolderToMove(null);
}
}}
/>
<FolderSettingsDialog
folder={folderToSettings}
isOpen={isSettingsFolderOpen}
onOpenChange={(open) => {
setIsSettingsFolderOpen(open);
if (!open) {
setFolderToSettings(null);
}
}}
/>
<FolderDeleteDialog
folder={folderToDelete}
isOpen={isDeletingFolder}
onOpenChange={(open) => {
setIsDeletingFolder(open);
if (!open) {
setFolderToDelete(null);
}
}}
/>
</div>
</DocumentDropZoneWrapper>
);

View File

@ -1,374 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import DocumentPage, { meta } from './documents._index';
import { Trans } from '@lingui/react/macro';
import { FolderIcon, HomeIcon, Loader2 } from 'lucide-react';
import { useNavigate, useParams, useSearchParams } from 'react-router';
import { Link } from 'react-router';
import { z } from 'zod';
export { meta };
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { parseToIntegerArray } from '@documenso/lib/utils/params';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { trpc } from '@documenso/trpc/react';
import {
type TFindDocumentsInternalResponse,
ZFindDocumentsInternalRequestSchema,
} from '@documenso/trpc/server/document-router/schema';
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { DocumentMoveToFolderDialog } from '~/components/dialogs/document-move-to-folder-dialog';
import { CreateFolderDialog } from '~/components/dialogs/folder-create-dialog';
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
import { DocumentDropZoneWrapper } from '~/components/general/document/document-drop-zone-wrapper';
import { DocumentSearch } from '~/components/general/document/document-search';
import { DocumentStatus } from '~/components/general/document/document-status';
import { DocumentUploadDropzone } from '~/components/general/document/document-upload';
import { FolderCard } from '~/components/general/folder/folder-card';
import { PeriodSelector } from '~/components/general/period-selector';
import { DocumentsTable } from '~/components/tables/documents-table';
import { DocumentsTableEmptyState } from '~/components/tables/documents-table-empty-state';
import { DocumentsTableSenderFilter } from '~/components/tables/documents-table-sender-filter';
import { useCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta';
export function meta() {
return appMetaTags('Documents');
}
const ZSearchParamsSchema = ZFindDocumentsInternalRequestSchema.pick({
status: true,
period: true,
page: true,
perPage: true,
query: true,
}).extend({
senderIds: z.string().transform(parseToIntegerArray).optional().catch([]),
});
export default function DocumentsPage() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [isMovingDocument, setIsMovingDocument] = useState(false);
const [documentToMove, setDocumentToMove] = useState<number | null>(null);
const [isMovingFolder, setIsMovingFolder] = useState(false);
const [folderToMove, setFolderToMove] = useState<TFolderWithSubfolders | null>(null);
const [isDeletingFolder, setIsDeletingFolder] = useState(false);
const [folderToDelete, setFolderToDelete] = useState<TFolderWithSubfolders | null>(null);
const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false);
const [folderToSettings, setFolderToSettings] = useState<TFolderWithSubfolders | null>(null);
const { folderId } = useParams();
const team = useCurrentTeam();
const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation();
const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation();
const [stats, setStats] = useState<TFindDocumentsInternalResponse['stats']>({
[ExtendedDocumentStatus.DRAFT]: 0,
[ExtendedDocumentStatus.PENDING]: 0,
[ExtendedDocumentStatus.COMPLETED]: 0,
[ExtendedDocumentStatus.REJECTED]: 0,
[ExtendedDocumentStatus.INBOX]: 0,
[ExtendedDocumentStatus.ALL]: 0,
});
const findDocumentSearchParams = useMemo(
() => ZSearchParamsSchema.safeParse(Object.fromEntries(searchParams.entries())).data || {},
[searchParams],
);
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocumentsInternal.useQuery(
{
...findDocumentSearchParams,
folderId,
},
);
const {
data: foldersData,
isLoading: isFoldersLoading,
refetch: refetchFolders,
} = trpc.folder.getFolders.useQuery({
parentId: folderId,
});
useEffect(() => {
void refetch();
void refetchFolders();
}, [team.url]);
const getTabHref = (value: keyof typeof ExtendedDocumentStatus) => {
const params = new URLSearchParams(searchParams);
params.set('status', value);
if (value === ExtendedDocumentStatus.ALL) {
params.delete('status');
}
if (params.has('page')) {
params.delete('page');
}
return `${formatDocumentsPath(team.url)}/f/${folderId}?${params.toString()}`;
};
useEffect(() => {
if (data?.stats) {
setStats(data.stats);
}
}, [data?.stats]);
const navigateToFolder = (folderId?: string | null) => {
const documentsPath = formatDocumentsPath(team.url);
if (folderId) {
void navigate(`${documentsPath}/f/${folderId}`);
} else {
void navigate(documentsPath);
}
};
return (
<DocumentDropZoneWrapper>
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex flex-1 items-center">
<Button
variant="ghost"
size="sm"
className="flex items-center space-x-2 pl-0 hover:bg-transparent"
onClick={() => navigateToFolder(null)}
>
<HomeIcon className="h-4 w-4" />
<span>Home</span>
</Button>
{foldersData?.breadcrumbs.map((folder) => (
<div key={folder.id} className="flex items-center space-x-2">
<span>/</span>
<Button
variant="ghost"
size="sm"
className="flex items-center space-x-2 pl-1 hover:bg-transparent"
onClick={() => navigateToFolder(folder.id)}
>
<FolderIcon className="h-4 w-4" />
<span>{folder.name}</span>
</Button>
</div>
))}
</div>
<div className="flex gap-4 sm:flex-row sm:justify-end">
<DocumentUploadDropzone />
<CreateFolderDialog />
</div>
</div>
{isFoldersLoading ? (
<div className="mt-6 flex justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
) : (
<>
{foldersData?.folders && foldersData.folders.some((folder) => folder.pinned) && (
<div className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData.folders
.filter((folder) => folder.pinned)
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onNavigate={navigateToFolder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onPin={(folderId) => void pinFolder({ folderId })}
onUnpin={(folderId) => void unpinFolder({ folderId })}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
</div>
)}
<div className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData?.folders
.filter((folder) => !folder.pinned)
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onNavigate={navigateToFolder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onPin={(folderId) => void pinFolder({ folderId })}
onUnpin={(folderId) => void unpinFolder({ folderId })}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
</div>
</>
)}
<div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
<div className="flex flex-row items-center">
{team && (
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
<AvatarFallback className="text-muted-foreground text-xs">
{team.name.slice(0, 1)}
</AvatarFallback>
</Avatar>
)}
<h2 className="text-4xl font-semibold">
<Trans>Documents</Trans>
</h2>
</div>
<div className="-m-1 flex flex-wrap gap-x-4 gap-y-6 overflow-hidden p-1">
<Tabs value={findDocumentSearchParams.status || 'ALL'} className="overflow-x-auto">
<TabsList>
{[
ExtendedDocumentStatus.INBOX,
ExtendedDocumentStatus.PENDING,
ExtendedDocumentStatus.COMPLETED,
ExtendedDocumentStatus.DRAFT,
ExtendedDocumentStatus.ALL,
].map((value) => (
<TabsTrigger
key={value}
className="hover:text-foreground min-w-[60px]"
value={value}
asChild
>
<Link to={getTabHref(value)} preventScrollReset>
<DocumentStatus status={value} />
{value !== ExtendedDocumentStatus.ALL && (
<span className="ml-1 inline-block opacity-50">{stats[value]}</span>
)}
</Link>
</TabsTrigger>
))}
</TabsList>
</Tabs>
{team && <DocumentsTableSenderFilter teamId={team.id} />}
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<PeriodSelector />
</div>
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<DocumentSearch initialValue={findDocumentSearchParams.query} />
</div>
</div>
</div>
<div className="mt-8">
<div>
{data &&
data.count === 0 &&
(!foldersData?.folders.length || foldersData.folders.length === 0) ? (
<DocumentsTableEmptyState
status={findDocumentSearchParams.status || ExtendedDocumentStatus.ALL}
/>
) : (
<DocumentsTable
data={data}
isLoading={isLoading}
isLoadingError={isLoadingError}
onMoveDocument={(documentId) => {
setDocumentToMove(documentId);
setIsMovingDocument(true);
}}
/>
)}
</div>
</div>
{documentToMove && (
<DocumentMoveToFolderDialog
documentId={documentToMove}
open={isMovingDocument}
onOpenChange={(open) => {
setIsMovingDocument(open);
if (!open) {
setDocumentToMove(null);
}
}}
currentFolderId={folderId}
/>
)}
<FolderMoveDialog
foldersData={foldersData?.folders}
folder={folderToMove}
isOpen={isMovingFolder}
onOpenChange={(open) => {
setIsMovingFolder(open);
if (!open) {
setFolderToMove(null);
}
}}
/>
<FolderSettingsDialog
folder={folderToSettings}
isOpen={isSettingsFolderOpen}
onOpenChange={(open) => {
setIsSettingsFolderOpen(open);
if (!open) {
setFolderToSettings(null);
}
}}
/>
<FolderDeleteDialog
folder={folderToDelete}
isOpen={isDeletingFolder}
onOpenChange={(open) => {
setIsDeletingFolder(open);
if (!open) {
setFolderToDelete(null);
}
}}
/>
</div>
</DocumentDropZoneWrapper>
);
}
export default DocumentPage;

View File

@ -1,7 +1,7 @@
import { useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { HomeIcon, Loader2 } from 'lucide-react';
import { Trans, useLingui } from '@lingui/react/macro';
import { HomeIcon, Loader2, SearchIcon } from 'lucide-react';
import { useNavigate } from 'react-router';
import { FolderType } from '@documenso/lib/types/folder-type';
@ -9,8 +9,9 @@ import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import { Input } from '@documenso/ui/primitives/input';
import { CreateFolderDialog } from '~/components/dialogs/folder-create-dialog';
import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
@ -23,6 +24,8 @@ export function meta() {
}
export default function DocumentsFoldersPage() {
const { t } = useLingui();
const navigate = useNavigate();
const team = useCurrentTeam();
@ -32,6 +35,7 @@ export default function DocumentsFoldersPage() {
const [folderToDelete, setFolderToDelete] = useState<TFolderWithSubfolders | null>(null);
const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false);
const [folderToSettings, setFolderToSettings] = useState<TFolderWithSubfolders | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const { data: foldersData, isLoading: isFoldersLoading } = trpc.folder.getFolders.useQuery({
type: FolderType.DOCUMENT,
@ -51,6 +55,9 @@ export default function DocumentsFoldersPage() {
}
};
const isFolderMatchingSearch = (folder: TFolderWithSubfolders) =>
folder.name.toLowerCase().includes(searchTerm.toLowerCase());
return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<div className="flex w-full items-center justify-between">
@ -67,60 +74,41 @@ export default function DocumentsFoldersPage() {
</div>
<div className="flex flex-col gap-y-4 sm:flex-row sm:justify-end sm:gap-x-4">
<CreateFolderDialog />
<FolderCreateDialog type={FolderType.DOCUMENT} />
</div>
</div>
<div className="mt-6">
{isFoldersLoading ? (
<div className="mt-6 flex justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
) : (
<>
{foldersData?.folders?.some((folder) => folder.pinned) && (
<div className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData.folders
.filter((folder) => folder.pinned)
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onNavigate={navigateToFolder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onPin={(folderId) => void pinFolder({ folderId })}
onUnpin={(folderId) => void unpinFolder({ folderId })}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
</div>
)}
<div className="relative w-full max-w-md py-6">
<SearchIcon className="text-muted-foreground absolute left-2 top-9 h-4 w-4" />
<Input
placeholder={t`Search folders...`}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8"
/>
</div>
<div className="mt-12">
<h1 className="truncate text-2xl font-semibold md:text-3xl">
<Trans>All Folders</Trans>
</h1>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl">
<Trans>All Folders</Trans>
</h1>
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData?.folders
.filter((folder) => !folder.pinned)
{isFoldersLoading ? (
<div className="mt-6 flex justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
) : (
<>
{foldersData?.folders?.some(
(folder) => folder.pinned && isFolderMatchingSearch(folder),
) && (
<div className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData.folders
.filter((folder) => folder.pinned && isFolderMatchingSearch(folder))
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onNavigate={navigateToFolder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
@ -139,9 +127,42 @@ export default function DocumentsFoldersPage() {
))}
</div>
</div>
</>
)}
</div>
)}
<div>
{searchTerm && foldersData?.folders.filter(isFolderMatchingSearch).length === 0 && (
<div className="text-muted-foreground mt-6 text-center">
<Trans>No folders found matching "{searchTerm}"</Trans>
</div>
)}
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData?.folders
.filter((folder) => !folder.pinned)
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onPin={(folderId) => void pinFolder({ folderId })}
onUnpin={(folderId) => void unpinFolder({ folderId })}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
</div>
</>
)}
<FolderMoveDialog
foldersData={foldersData?.folders}
@ -168,17 +189,19 @@ export default function DocumentsFoldersPage() {
}}
/>
<FolderDeleteDialog
folder={folderToDelete}
isOpen={isDeletingFolder}
onOpenChange={(open) => {
setIsDeletingFolder(open);
{folderToDelete && (
<FolderDeleteDialog
folder={folderToDelete}
isOpen={isDeletingFolder}
onOpenChange={(open) => {
setIsDeletingFolder(open);
if (!open) {
setFolderToDelete(null);
}
}}
/>
if (!open) {
setFolderToDelete(null);
}
}}
/>
)}
</div>
);
}

View File

@ -1,23 +1,14 @@
import { useEffect, useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { Bird, FolderIcon, HomeIcon, Loader2 } from 'lucide-react';
import { useNavigate, useSearchParams } from 'react-router';
import { Bird } from 'lucide-react';
import { useParams, useSearchParams } from 'react-router';
import { FolderType } from '@documenso/lib/types/folder-type';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
import { TemplateCreateDialog } from '~/components/dialogs/template-create-dialog';
import { TemplateFolderCreateDialog } from '~/components/dialogs/template-folder-create-dialog';
import { TemplateFolderDeleteDialog } from '~/components/dialogs/template-folder-delete-dialog';
import { TemplateFolderMoveDialog } from '~/components/dialogs/template-folder-move-dialog';
import { TemplateFolderSettingsDialog } from '~/components/dialogs/template-folder-settings-dialog';
import { FolderCard } from '~/components/general/folder/folder-card';
import { FolderGrid } from '~/components/general/folder/folder-grid';
import { TemplatesTable } from '~/components/tables/templates-table';
import { useCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta';
@ -27,20 +18,10 @@ export function meta() {
}
export default function TemplatesPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [isMovingFolder, setIsMovingFolder] = useState(false);
const [folderToMove, setFolderToMove] = useState<TFolderWithSubfolders | null>(null);
const [isDeletingFolder, setIsDeletingFolder] = useState(false);
const [folderToDelete, setFolderToDelete] = useState<TFolderWithSubfolders | null>(null);
const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false);
const [folderToSettings, setFolderToSettings] = useState<TFolderWithSubfolders | null>(null);
const team = useCurrentTeam();
const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation();
const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation();
const { folderId } = useParams();
const [searchParams] = useSearchParams();
const page = Number(searchParams.get('page')) || 1;
const perPage = Number(searchParams.get('perPage')) || 10;
@ -48,174 +29,24 @@ export default function TemplatesPage() {
const documentRootPath = formatDocumentsPath(team.url);
const templateRootPath = formatTemplatesPath(team.url);
const { data, isLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery({
const { data, isLoading, isLoadingError } = trpc.template.findTemplates.useQuery({
page: page,
perPage: perPage,
folderId,
});
const {
data: foldersData,
isLoading: isFoldersLoading,
refetch: refetchFolders,
} = trpc.folder.getFolders.useQuery({
type: FolderType.TEMPLATE,
parentId: null,
});
useEffect(() => {
void refetch();
void refetchFolders();
}, [team.url]);
const navigateToFolder = (folderId?: string | null) => {
const templatesPath = formatTemplatesPath(team.url);
if (folderId) {
void navigate(`${templatesPath}/f/${folderId}`);
} else {
void navigate(templatesPath);
}
};
const handleNavigate = (folderId: string) => {
navigateToFolder(folderId);
};
const handleMove = (folder: TFolderWithSubfolders) => {
setFolderToMove(folder);
setIsMovingFolder(true);
};
const handlePin = (folderId: string) => {
void pinFolder({ folderId });
};
const handleUnpin = (folderId: string) => {
void unpinFolder({ folderId });
};
const handleSettings = (folder: TFolderWithSubfolders) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
};
const handleDelete = (folder: TFolderWithSubfolders) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
};
const handleViewAllFolders = () => {
void navigate(`${formatTemplatesPath(team.url)}/folders`);
};
return (
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex flex-1 items-center">
<Button
variant="ghost"
size="sm"
className="flex items-center space-x-2 pl-0 hover:bg-transparent"
onClick={() => navigateToFolder(null)}
>
<HomeIcon className="h-4 w-4" />
<span>Home</span>
</Button>
<FolderGrid type={FolderType.TEMPLATE} parentId={folderId ?? null} />
{foldersData?.breadcrumbs.map((folder) => (
<div key={folder.id} className="flex items-center space-x-2">
<span>/</span>
<Button
variant="ghost"
size="sm"
className="flex items-center space-x-2 pl-1 hover:bg-transparent"
onClick={() => navigateToFolder(folder.id)}
>
<FolderIcon className="h-4 w-4" />
<span>{folder.name}</span>
</Button>
</div>
))}
</div>
<div className="flex gap-4 sm:flex-row sm:justify-end">
<TemplateCreateDialog templateRootPath={templateRootPath} />
<TemplateFolderCreateDialog />
</div>
</div>
{isFoldersLoading ? (
<div className="mt-6 flex justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
) : (
<>
{foldersData?.folders && foldersData.folders.some((folder) => folder.pinned) && (
<div className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData.folders
.filter((folder) => folder.pinned)
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onNavigate={handleNavigate}
onMove={handleMove}
onPin={handlePin}
onUnpin={handleUnpin}
onSettings={handleSettings}
onDelete={handleDelete}
/>
))}
</div>
</div>
)}
<div className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData?.folders
?.filter((folder) => !folder.pinned)
.slice(0, 12)
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onNavigate={handleNavigate}
onMove={handleMove}
onPin={handlePin}
onUnpin={handleUnpin}
onSettings={handleSettings}
onDelete={handleDelete}
/>
))}
</div>
<div className="mt-6 flex items-center justify-center">
{foldersData && foldersData.folders?.length > 12 && (
<Button
variant="link"
size="sm"
className="text-muted-foreground hover:text-foreground"
onClick={() => void handleViewAllFolders()}
>
View all folders
</Button>
)}
</div>
</div>
</>
)}
<div className="mt-12">
<div className="mt-8">
<div className="flex flex-row items-center">
{team && (
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
<AvatarFallback className="text-muted-foreground text-xs">
{team.name.slice(0, 1)}
</AvatarFallback>
</Avatar>
)}
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
<AvatarFallback className="text-muted-foreground text-xs">
{team.name.slice(0, 1)}
</AvatarFallback>
</Avatar>
<h1 className="truncate text-2xl font-semibold md:text-3xl">
<Trans>Templates</Trans>
@ -250,43 +81,6 @@ export default function TemplatesPage() {
)}
</div>
</div>
<TemplateFolderMoveDialog
foldersData={foldersData?.folders}
folder={folderToMove}
isOpen={isMovingFolder}
onOpenChange={(open) => {
setIsMovingFolder(open);
if (!open) {
setFolderToMove(null);
}
}}
/>
<TemplateFolderSettingsDialog
folder={folderToSettings}
isOpen={isSettingsFolderOpen}
onOpenChange={(open) => {
setIsSettingsFolderOpen(open);
if (!open) {
setFolderToSettings(null);
}
}}
/>
<TemplateFolderDeleteDialog
folder={folderToDelete}
isOpen={isDeletingFolder}
onOpenChange={(open) => {
setIsDeletingFolder(open);
if (!open) {
setFolderToDelete(null);
}
}}
/>
</div>
);
}

View File

@ -1,369 +1,5 @@
import { useEffect, useState } from 'react';
import TemplatesPage, { meta } from './templates._index';
import { Trans } from '@lingui/react/macro';
import { Bird, FolderIcon, HomeIcon, Loader2, PinIcon } from 'lucide-react';
import { useNavigate, useParams, useSearchParams } from 'react-router';
export { meta };
import { FolderType } from '@documenso/lib/types/folder-type';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
import { TemplateCreateDialog } from '~/components/dialogs/template-create-dialog';
import { TemplateFolderCreateDialog } from '~/components/dialogs/template-folder-create-dialog';
import { TemplatesTable } from '~/components/tables/templates-table';
import { useCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta';
export function meta() {
return appMetaTags('Templates');
}
export default function TemplatesPage() {
const [searchParams] = useSearchParams();
const { folderId } = useParams();
const navigate = useNavigate();
const team = useCurrentTeam();
const page = Number(searchParams.get('page')) || 1;
const perPage = Number(searchParams.get('perPage')) || 10;
const documentRootPath = formatDocumentsPath(team.url);
const templateRootPath = formatTemplatesPath(team.url);
const { data, isLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery({
page: page,
perPage: perPage,
folderId: folderId,
});
const {
data: foldersData,
isLoading: isFoldersLoading,
refetch: refetchFolders,
} = trpc.folder.getFolders.useQuery({
parentId: folderId,
type: FolderType.TEMPLATE,
});
const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation();
const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation();
const [folderToMove, setFolderToMove] = useState<TFolderWithSubfolders | null>(null);
const [isMovingFolder, setIsMovingFolder] = useState(false);
const [folderToSettings, setFolderToSettings] = useState<TFolderWithSubfolders | null>(null);
const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false);
const [folderToDelete, setFolderToDelete] = useState<TFolderWithSubfolders | null>(null);
const [isDeletingFolder, setIsDeletingFolder] = useState(false);
useEffect(() => {
void refetch();
void refetchFolders();
}, [team?.url]);
const navigateToFolder = (folderId?: string) => {
const templatesPath = formatTemplatesPath(team.url);
if (folderId) {
void navigate(`${templatesPath}/f/${folderId}`);
} else {
void navigate(templatesPath);
}
};
return (
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex flex-1 items-center">
<Button
variant="ghost"
size="sm"
className="flex items-center space-x-2 pl-0 hover:bg-transparent"
onClick={() => navigateToFolder()}
>
<HomeIcon className="h-4 w-4" />
<span>Home</span>
</Button>
{foldersData?.breadcrumbs.map((folder) => (
<div key={folder.id} className="flex items-center space-x-2">
<span>/</span>
<Button
variant="ghost"
size="sm"
className="flex items-center space-x-2 pl-1 hover:bg-transparent"
onClick={() => navigateToFolder(folder.id)}
>
<FolderIcon className="h-4 w-4" />
<span>{folder.name}</span>
</Button>
</div>
))}
</div>
<div className="flex gap-4 sm:flex-row sm:justify-end">
<TemplateFolderCreateDialog />
<TemplateCreateDialog templateRootPath={templateRootPath} folderId={folderId} />
</div>
</div>
{isFoldersLoading ? (
<div className="mt-6 flex justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
) : (
<>
{foldersData?.folders.some((folder) => folder.pinned) && (
<div className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData?.folders
.filter((folder) => folder.pinned)
.map((folder) => (
<div
key={folder.id}
className="border-border hover:border-muted-foreground/40 group relative flex flex-col rounded-lg border p-4 transition-all hover:shadow-sm"
>
<div className="flex items-start justify-between">
<button
className="flex items-center space-x-2 text-left"
onClick={() => navigateToFolder(folder.id)}
>
<FolderIcon className="text-documenso h-6 w-6" />
<div>
<div className="flex items-center gap-2">
<h3 className="font-medium">{folder.name}</h3>
<PinIcon className="text-documenso h-3 w-3" />
</div>
<div className="mt-1 flex space-x-2 text-xs text-gray-500">
<span>{folder._count.templates || 0} templates</span>
<span></span>
<span>{folder._count.subfolders} folders</span>
</div>
</div>
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="opacity-0 group-hover:opacity-100"
>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
>
Move
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
void unpinFolder({ folderId: folder.id });
}}
>
Unpin
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
>
Settings
</DropdownMenuItem>
<DropdownMenuItem
className="text-red-500"
onClick={() => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
</div>
</div>
)}
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData?.folders
.filter((folder) => !folder.pinned)
.map((folder) => (
<div
key={folder.id}
className="border-border hover:border-muted-foreground/40 group relative flex flex-col rounded-lg border p-4 transition-all hover:shadow-sm"
>
<div className="flex items-start justify-between">
<button
className="flex items-center space-x-2 text-left"
onClick={() => navigateToFolder(folder.id)}
>
<FolderIcon className="text-documenso h-6 w-6" />
<div>
<h3 className="font-medium">{folder.name}</h3>
<div className="mt-1 flex space-x-2 text-xs text-gray-500">
<span>{folder._count.templates || 0} templates</span>
<span></span>
<span>{folder._count.subfolders} folders</span>
</div>
</div>
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="opacity-0 group-hover:opacity-100"
>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
>
Move
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
void pinFolder({ folderId: folder.id });
}}
>
Pin
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
>
Settings
</DropdownMenuItem>
<DropdownMenuItem
className="text-red-500"
onClick={() => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
</div>
</>
)}
<div className="relative mt-12">
<div className="flex flex-row items-center">
{team && (
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
<AvatarFallback className="text-muted-foreground text-xs">
{team.name.slice(0, 1)}
</AvatarFallback>
</Avatar>
)}
<h1 className="truncate text-2xl font-semibold md:text-3xl">
<Trans>Templates</Trans>
</h1>
</div>
<div className="mt-8">
{data && data.count === 0 ? (
<div className="text-muted-foreground/60 flex h-96 flex-col items-center justify-center gap-y-4">
<Bird className="h-12 w-12" strokeWidth={1.5} />
<div className="text-center">
<h3 className="text-lg font-semibold">
<Trans>We're all empty</Trans>
</h3>
<p className="mt-2 max-w-[50ch]">
<Trans>
You have not yet created any templates. To create a template please upload one.
</Trans>
</p>
</div>
</div>
) : (
<TemplatesTable
data={data}
isLoading={isLoading}
isLoadingError={isLoadingError}
documentRootPath={documentRootPath}
templateRootPath={templateRootPath}
/>
)}
</div>
</div>
<FolderMoveDialog
foldersData={foldersData?.folders}
folder={folderToMove}
isOpen={isMovingFolder}
onOpenChange={(open) => {
setIsMovingFolder(open);
if (!open) {
setFolderToMove(null);
}
}}
/>
<FolderSettingsDialog
folder={folderToSettings}
isOpen={isSettingsFolderOpen}
onOpenChange={(open) => {
setIsSettingsFolderOpen(open);
if (!open) {
setFolderToSettings(null);
}
}}
/>
<FolderDeleteDialog
folder={folderToDelete}
isOpen={isDeletingFolder}
onOpenChange={(open) => {
setIsDeletingFolder(open);
if (!open) {
setFolderToDelete(null);
}
}}
/>
</div>
);
}
export default TemplatesPage;

View File

@ -1,7 +1,7 @@
import { useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { HomeIcon, Loader2 } from 'lucide-react';
import { Trans, useLingui } from '@lingui/react/macro';
import { HomeIcon, Loader2, SearchIcon } from 'lucide-react';
import { useNavigate } from 'react-router';
import { FolderType } from '@documenso/lib/types/folder-type';
@ -9,11 +9,12 @@ import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import { Input } from '@documenso/ui/primitives/input';
import { TemplateFolderCreateDialog } from '~/components/dialogs/template-folder-create-dialog';
import { TemplateFolderDeleteDialog } from '~/components/dialogs/template-folder-delete-dialog';
import { TemplateFolderMoveDialog } from '~/components/dialogs/template-folder-move-dialog';
import { TemplateFolderSettingsDialog } from '~/components/dialogs/template-folder-settings-dialog';
import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
import { FolderCard } from '~/components/general/folder/folder-card';
import { useCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta';
@ -23,6 +24,8 @@ export function meta() {
}
export default function TemplatesFoldersPage() {
const { t } = useLingui();
const navigate = useNavigate();
const team = useCurrentTeam();
@ -32,6 +35,7 @@ export default function TemplatesFoldersPage() {
const [folderToDelete, setFolderToDelete] = useState<TFolderWithSubfolders | null>(null);
const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false);
const [folderToSettings, setFolderToSettings] = useState<TFolderWithSubfolders | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const { data: foldersData, isLoading: isFoldersLoading } = trpc.folder.getFolders.useQuery({
type: FolderType.TEMPLATE,
@ -51,6 +55,9 @@ export default function TemplatesFoldersPage() {
}
};
const isFolderMatchingSearch = (folder: TFolderWithSubfolders) =>
folder.name.toLowerCase().includes(searchTerm.toLowerCase());
return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<div className="flex w-full items-center justify-between">
@ -67,60 +74,41 @@ export default function TemplatesFoldersPage() {
</div>
<div className="flex flex-col gap-y-4 sm:flex-row sm:justify-end sm:gap-x-4">
<TemplateFolderCreateDialog />
<FolderCreateDialog type={FolderType.TEMPLATE} />
</div>
</div>
<div className="mt-6">
{isFoldersLoading ? (
<div className="mt- flex justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
) : (
<>
{foldersData?.folders?.some((folder) => folder.pinned) && (
<div className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData.folders
.filter((folder) => folder.pinned)
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onNavigate={navigateToFolder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onPin={(folderId) => void pinFolder({ folderId })}
onUnpin={(folderId) => void unpinFolder({ folderId })}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
</div>
)}
<div className="relative w-full max-w-md py-6">
<SearchIcon className="text-muted-foreground absolute left-2 top-9 h-4 w-4" />
<Input
placeholder={t`Search folders...`}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8"
/>
</div>
<div className="mt-12">
<h1 className="truncate text-2xl font-semibold md:text-3xl">
<Trans>All Folders</Trans>
</h1>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl">
<Trans>All Folders</Trans>
</h1>
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData?.folders
.filter((folder) => !folder.pinned)
{isFoldersLoading ? (
<div className="mt- flex justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
) : (
<>
{foldersData?.folders?.some(
(folder) => folder.pinned && isFolderMatchingSearch(folder),
) && (
<div className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData.folders
.filter((folder) => folder.pinned && isFolderMatchingSearch(folder))
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onNavigate={navigateToFolder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
@ -139,11 +127,44 @@ export default function TemplatesFoldersPage() {
))}
</div>
</div>
</>
)}
</div>
)}
<TemplateFolderMoveDialog
<div>
{searchTerm && foldersData?.folders.filter(isFolderMatchingSearch).length === 0 && (
<div className="text-muted-foreground mt-6 text-center">
<Trans>No folders found matching "{searchTerm}"</Trans>
</div>
)}
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData?.folders
.filter((folder) => !folder.pinned && isFolderMatchingSearch(folder))
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onPin={(folderId) => void pinFolder({ folderId })}
onUnpin={(folderId) => void unpinFolder({ folderId })}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
</div>
</>
)}
<FolderMoveDialog
foldersData={foldersData?.folders}
folder={folderToMove}
isOpen={isMovingFolder}
@ -156,7 +177,7 @@ export default function TemplatesFoldersPage() {
}}
/>
<TemplateFolderSettingsDialog
<FolderSettingsDialog
folder={folderToSettings}
isOpen={isSettingsFolderOpen}
onOpenChange={(open: boolean) => {
@ -168,17 +189,19 @@ export default function TemplatesFoldersPage() {
}}
/>
<TemplateFolderDeleteDialog
folder={folderToDelete}
isOpen={isDeletingFolder}
onOpenChange={(open: boolean) => {
setIsDeletingFolder(open);
{folderToDelete && (
<FolderDeleteDialog
folder={folderToDelete}
isOpen={isDeletingFolder}
onOpenChange={(open: boolean) => {
setIsDeletingFolder(open);
if (!open) {
setFolderToDelete(null);
}
}}
/>
if (!open) {
setFolderToDelete(null);
}
}}
/>
)}
</div>
);
}

View File

@ -41,6 +41,7 @@
"colord": "^2.9.3",
"framer-motion": "^10.12.8",
"hono": "4.7.0",
"hono-rate-limiter": "^0.4.2",
"hono-react-router-adapter": "^0.6.2",
"input-otp": "^1.2.4",
"isbot": "^5.1.17",
@ -100,5 +101,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
"version": "1.12.0-rc.3"
"version": "1.12.2-rc.1"
}

Binary file not shown.

View File

@ -1,10 +1,16 @@
import { Hono } from 'hono';
import { rateLimiter } from 'hono-rate-limiter';
import { contextStorage } from 'hono/context-storage';
import { requestId } from 'hono/request-id';
import type { RequestIdVariables } from 'hono/request-id';
import type { Logger } from 'pino';
import { tsRestHonoApp } from '@documenso/api/hono';
import { auth } from '@documenso/auth/server';
import { API_V2_BETA_URL } from '@documenso/lib/constants/app';
import { jobsClient } from '@documenso/lib/jobs/client';
import { getIpAddress } from '@documenso/lib/universal/get-ip-address';
import { logger } from '@documenso/lib/utils/logger';
import { openApiDocument } from '@documenso/trpc/server/open-api';
import { filesRoute } from './api/files';
@ -14,13 +20,33 @@ import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api';
import { reactRouterTrpcServer } from './trpc/hono-trpc-remix';
export interface HonoEnv {
Variables: {
Variables: RequestIdVariables & {
context: AppContext;
logger: Logger;
};
}
const app = new Hono<HonoEnv>();
/**
* Rate limiting for v1 and v2 API routes only.
* - 100 requests per minute per IP address
*/
const rateLimitMiddleware = rateLimiter({
windowMs: 60 * 1000, // 1 minute
limit: 100, // 100 requests per window
keyGenerator: (c) => {
try {
return getIpAddress(c.req.raw);
} catch (error) {
return 'unknown';
}
},
message: {
error: 'Too many requests, please try again later.',
},
});
/**
* Attach session and context to requests.
*/
@ -31,6 +57,24 @@ app.use(appContext);
* RR7 app middleware.
*/
app.use('*', appMiddleware);
app.use('*', requestId());
app.use(async (c, next) => {
const metadata = c.get('context').requestMetadata;
const honoLogger = logger.child({
requestId: c.var.requestId,
ipAddress: metadata.ipAddress,
userAgent: metadata.userAgent,
});
c.set('logger', honoLogger);
await next();
});
// Apply rate limit to /api/v1/*
app.use('/api/v1/*', rateLimitMiddleware);
app.use('/api/v2/*', rateLimitMiddleware);
// Auth server.
app.route('/api/auth', auth);

345
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "1.12.0-rc.3",
"version": "1.12.2-rc.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "1.12.0-rc.3",
"version": "1.12.2-rc.1",
"workspaces": [
"apps/*",
"packages/*"
@ -89,7 +89,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
"version": "1.12.0-rc.3",
"version": "1.12.2-rc.1",
"dependencies": {
"@documenso/api": "*",
"@documenso/assets": "*",
@ -118,6 +118,7 @@
"colord": "^2.9.3",
"framer-motion": "^10.12.8",
"hono": "4.7.0",
"hono-rate-limiter": "^0.4.2",
"hono-react-router-adapter": "^0.6.2",
"input-otp": "^1.2.4",
"isbot": "^5.1.17",
@ -3072,36 +3073,6 @@
"integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==",
"license": "MIT"
},
"node_modules/@honeybadger-io/core": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@honeybadger-io/core/-/core-6.7.0.tgz",
"integrity": "sha512-bEXRe2UVbfr9q3434/2eO3AHguUT0froYEqrHfTPphR4Aw6+AlFac0YE8elqDZqUSgRQ6m1OXqxmq/HOF+W6LQ==",
"license": "MIT",
"dependencies": {
"json-nd": "^1.0.0",
"stacktrace-parser": "^0.1.10"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@honeybadger-io/js": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@honeybadger-io/js/-/js-6.11.0.tgz",
"integrity": "sha512-nSibKUr9ccrs6Jb3Ql7uO/4MdEEv3ONGP1CrD0w3zSMHUvQKHe43NPYfESA7btxjrf9PVeV+m6ETP/193BSILg==",
"license": "MIT",
"dependencies": {
"@honeybadger-io/core": "^6.7.0",
"@types/aws-lambda": "^8.10.89",
"@types/express": "^4.17.13"
},
"bin": {
"honeybadger-checkins-sync": "scripts/check-ins-sync-bin.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@hono/node-server": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.2.tgz",
@ -11714,16 +11685,6 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"license": "MIT",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/bunyan": {
"version": "1.8.11",
"resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.11.tgz",
@ -11844,30 +11805,6 @@
"@types/estree": "*"
}
},
"node_modules/@types/express": {
"version": "4.17.22",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz",
"integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==",
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.19.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/@types/formidable": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-2.0.6.tgz",
@ -11897,12 +11834,6 @@
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"license": "MIT"
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@ -11976,12 +11907,6 @@
"@types/node": "*"
}
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"license": "MIT"
},
"node_modules/@types/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
@ -12069,12 +11994,6 @@
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"license": "MIT"
},
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"license": "MIT"
},
"node_modules/@types/ramda": {
"version": "0.30.2",
"resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.30.2.tgz",
@ -12084,12 +12003,6 @@
"types-ramda": "^0.30.1"
}
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.5",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz",
@ -12123,27 +12036,6 @@
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
"license": "MIT"
},
"node_modules/@types/send": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*",
"@types/send": "*"
}
},
"node_modules/@types/shimmer": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
@ -13311,6 +13203,15 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/attr-accept": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
@ -14936,7 +14837,6 @@
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/colors": {
@ -16118,6 +16018,15 @@
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/dateformat": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
@ -18240,6 +18149,12 @@
"node": ">=4"
}
},
"node_modules/fast-copy": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
"integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -18308,6 +18223,21 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"license": "MIT"
},
"node_modules/fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@ -20241,6 +20171,15 @@
"node": ">=16.9.0"
}
},
"node_modules/hono-rate-limiter": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/hono-rate-limiter/-/hono-rate-limiter-0.4.2.tgz",
"integrity": "sha512-AAtFqgADyrmbDijcRTT/HJfwqfvhalya2Zo+MgfdrMPas3zSMD8SU03cv+ZsYwRU1swv7zgVt0shwN059yzhjw==",
"license": "MIT",
"peerDependencies": {
"hono": "^4.1.1"
}
},
"node_modules/hono-react-router-adapter": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/hono-react-router-adapter/-/hono-react-router-adapter-0.6.5.tgz",
@ -21643,7 +21582,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@ -21852,12 +21790,6 @@
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"license": "MIT"
},
"node_modules/json-nd": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-nd/-/json-nd-1.0.0.tgz",
"integrity": "sha512-8TIp0HZAY0VVrwRQJJPb4+nOTSPoOWZeEKBTLizUfQO4oym5Fc/MKqN8vEbLCxcyxDf2vwNxOQ1q84O49GWPyQ==",
"license": "BSD-3-Clause"
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@ -26074,6 +26006,15 @@
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
"license": "MIT"
},
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@ -26893,6 +26834,82 @@
"node": ">=0.10.0"
}
},
"node_modules/pino": {
"version": "9.7.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.7.0.tgz",
"integrity": "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^5.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^3.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pino-abstract-transport/node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/pino-pretty": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz",
"integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==",
"license": "MIT",
"dependencies": {
"colorette": "^2.0.7",
"dateformat": "^4.6.3",
"fast-copy": "^3.0.2",
"fast-safe-stringify": "^2.1.1",
"help-me": "^5.0.0",
"joycon": "^3.1.1",
"minimist": "^1.2.6",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pump": "^3.0.0",
"secure-json-parse": "^2.4.0",
"sonic-boom": "^4.0.1",
"strip-json-comments": "^3.1.1"
},
"bin": {
"pino-pretty": "bin.js"
}
},
"node_modules/pino-pretty/node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
"license": "MIT"
},
"node_modules/pino-std-serializers": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
"license": "MIT"
},
"node_modules/pirates": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
@ -27770,6 +27787,22 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@ -28007,6 +28040,12 @@
],
"license": "MIT"
},
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
"license": "MIT"
},
"node_modules/quick-lru": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
@ -29398,6 +29437,15 @@
"integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==",
"license": "MIT"
},
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"license": "MIT",
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/recharts": {
"version": "2.15.3",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz",
@ -30449,6 +30497,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@ -30514,6 +30571,12 @@
"node": ">=4"
}
},
"node_modules/secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
"license": "BSD-3-Clause"
},
"node_modules/selderee": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
@ -31075,6 +31138,15 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/sonic-boom": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
},
"node_modules/sort-keys": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.1.0.tgz",
@ -31304,27 +31376,6 @@
"integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==",
"license": "MIT"
},
"node_modules/stacktrace-parser": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz",
"integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==",
"license": "MIT",
"dependencies": {
"type-fest": "^0.7.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/stacktrace-parser/node_modules/type-fest": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
"integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=8"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -32340,6 +32391,15 @@
"node": ">=0.8"
}
},
"node_modules/thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"license": "MIT",
"dependencies": {
"real-require": "^0.2.0"
}
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@ -35984,7 +36044,6 @@
"@documenso/email": "*",
"@documenso/prisma": "*",
"@documenso/signing": "*",
"@honeybadger-io/js": "^6.10.1",
"@lingui/core": "^5.2.0",
"@lingui/macro": "^5.2.0",
"@lingui/react": "^5.2.0",
@ -36005,6 +36064,8 @@
"oslo": "^0.17.0",
"pdf-lib": "^1.17.1",
"pg": "^8.11.3",
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
"playwright": "1.52.0",
"posthog-js": "^1.245.0",
"posthog-node": "^4.17.0",

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "1.12.0-rc.3",
"version": "1.12.2-rc.1",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/remix",

View File

@ -8,10 +8,12 @@ import { testCredentialsHandler } from '@documenso/lib/server-only/public-api/te
import { listDocumentsHandler } from '@documenso/lib/server-only/webhooks/zapier/list-documents';
import { subscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/subscribe';
import { unsubscribeHandler } from '@documenso/lib/server-only/webhooks/zapier/unsubscribe';
// This is a bit nasty. Todo: Extract
import type { HonoEnv } from '@documenso/remix/server/router';
// This is bad, ts-router will be created on each request.
// But don't really have a choice here.
export const tsRestHonoApp = new Hono();
export const tsRestHonoApp = new Hono<HonoEnv>();
tsRestHonoApp
.get('/openapi', (c) => c.redirect('https://openapi-v1.documenso.com'))

View File

@ -77,9 +77,15 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}),
getDocument: authenticatedMiddleware(async (args, user, team) => {
getDocument: authenticatedMiddleware(async (args, user, team, { logger }) => {
const { id: documentId } = args.params;
logger.info({
input: {
id: documentId,
},
});
try {
const document = await getDocumentById({
documentId: Number(documentId),
@ -139,10 +145,16 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
downloadSignedDocument: authenticatedMiddleware(async (args, user, team) => {
downloadSignedDocument: authenticatedMiddleware(async (args, user, team, { logger }) => {
const { id: documentId } = args.params;
const { downloadOriginalDocument } = args.query;
logger.info({
input: {
id: documentId,
},
});
try {
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
return {
@ -204,9 +216,15 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
deleteDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
deleteDocument: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId } = args.params;
logger.info({
input: {
id: documentId,
},
});
try {
const document = await getDocumentById({
documentId: Number(documentId),
@ -382,9 +400,15 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
deleteTemplate: authenticatedMiddleware(async (args, user, team) => {
deleteTemplate: authenticatedMiddleware(async (args, user, team, { logger }) => {
const { id: templateId } = args.params;
logger.info({
input: {
id: templateId,
},
});
try {
const deletedTemplate = await deleteTemplate({
id: Number(templateId),
@ -406,9 +430,15 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
getTemplate: authenticatedMiddleware(async (args, user, team) => {
getTemplate: authenticatedMiddleware(async (args, user, team, { logger }) => {
const { id: templateId } = args.params;
logger.info({
input: {
id: templateId,
},
});
try {
const template = await getTemplateById({
id: Number(templateId),
@ -463,202 +493,224 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { body, params } = args;
createDocumentFromTemplate: authenticatedMiddleware(
async (args, user, team, { logger, metadata }) => {
const { body, params } = args;
const { remaining } = await getServerLimits({ userId: user.id, teamId: team?.id });
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
logger.info({
input: {
templateId: params.templateId,
},
};
}
const templateId = Number(params.templateId);
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
const document = await createDocumentFromTemplateLegacy({
templateId,
userId: user.id,
teamId: team?.id,
recipients: body.recipients,
});
let documentDataId = document.documentDataId;
if (body.formValues) {
const pdf = await getFileServerSide(document.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
const { remaining } = await getServerLimits({ userId: user.id, teamId: team?.id });
documentDataId = newDocumentData.id;
}
await updateDocument({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: {
title: fileName,
externalId: body.externalId || null,
formValues: body.formValues,
documentData: {
connect: {
id: documentDataId,
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
},
},
},
});
};
}
if (body.meta) {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
teamId: team?.id,
...body.meta,
requestMetadata: metadata,
});
}
const templateId = Number(params.templateId);
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: metadata,
});
}
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
return {
status: 200,
body: {
documentId: document.id,
recipients: document.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
}),
generateDocumentFromTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
const { body, params } = args;
const { remaining } = await getServerLimits({ userId: user.id, teamId: team?.id });
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
},
};
}
const templateId = Number(params.templateId);
let document: Awaited<ReturnType<typeof createDocumentFromTemplate>> | null = null;
try {
document = await createDocumentFromTemplate({
const document = await createDocumentFromTemplateLegacy({
templateId,
externalId: body.externalId || null,
userId: user.id,
teamId: team?.id,
recipients: body.recipients,
prefillFields: body.prefillFields,
override: {
title: body.title,
...body.meta,
},
requestMetadata: metadata,
});
} catch (err) {
return AppError.toRestAPIError(err);
}
if (body.formValues) {
const fileName = document.title.endsWith('.pdf') ? document.title : `${document.title}.pdf`;
const pdf = await getFileServerSide(document.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
let documentDataId = document.documentDataId;
if (body.formValues) {
const pdf = await getFileServerSide(document.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
documentDataId = newDocumentData.id;
}
await updateDocument({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: {
title: fileName,
externalId: body.externalId || null,
formValues: body.formValues,
documentData: {
connect: {
id: newDocumentData.id,
id: documentDataId,
},
},
},
});
}
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: metadata,
if (body.meta) {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
teamId: team?.id,
...body.meta,
requestMetadata: metadata,
});
}
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: metadata,
});
}
return {
status: 200,
body: {
documentId: document.id,
recipients: document.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
},
),
generateDocumentFromTemplate: authenticatedMiddleware(
async (args, user, team, { logger, metadata }) => {
const { body, params } = args;
logger.info({
input: {
templateId: params.templateId,
},
});
}
return {
status: 200,
body: {
documentId: document.id,
recipients: document.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
}),
const { remaining } = await getServerLimits({ userId: user.id, teamId: team?.id });
sendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
if (remaining.documents <= 0) {
return {
status: 400,
body: {
message: 'You have reached the maximum number of documents allowed for this month',
},
};
}
const templateId = Number(params.templateId);
let document: Awaited<ReturnType<typeof createDocumentFromTemplate>> | null = null;
try {
document = await createDocumentFromTemplate({
templateId,
externalId: body.externalId || null,
userId: user.id,
teamId: team?.id,
recipients: body.recipients,
prefillFields: body.prefillFields,
override: {
title: body.title,
...body.meta,
},
requestMetadata: metadata,
});
} catch (err) {
return AppError.toRestAPIError(err);
}
if (body.formValues) {
const fileName = document.title.endsWith('.pdf') ? document.title : `${document.title}.pdf`;
const pdf = await getFileServerSide(document.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
const newDocumentData = await putPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
await updateDocument({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: {
formValues: body.formValues,
documentData: {
connect: {
id: newDocumentData.id,
},
},
},
});
}
if (body.authOptions) {
await updateDocumentSettings({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: body.authOptions,
requestMetadata: metadata,
});
}
return {
status: 200,
body: {
documentId: document.id,
recipients: document.recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
})),
},
};
},
),
sendDocument: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId } = args.params;
const { sendEmail, sendCompletionEmails } = args.body;
logger.info({
input: {
id: documentId,
},
});
try {
const document = await getDocumentById({
documentId: Number(documentId),
@ -730,10 +782,16 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
resendDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
resendDocument: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId } = args.params;
const { recipients } = args.body;
logger.info({
input: {
id: documentId,
},
});
try {
await resendDocument({
userId: user.id,
@ -759,10 +817,16 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
createRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
createRecipient: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId } = args.params;
const { name, email, role, authOptions, signingOrder } = args.body;
logger.info({
input: {
id: documentId,
},
});
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
@ -850,10 +914,17 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
updateRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
updateRecipient: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId, recipientId } = args.params;
const { name, email, role, authOptions, signingOrder } = args.body;
logger.info({
input: {
id: documentId,
recipientId,
},
});
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
@ -916,9 +987,16 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}),
deleteRecipient: authenticatedMiddleware(async (args, user, team, { metadata }) => {
deleteRecipient: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId, recipientId } = args.params;
logger.info({
input: {
id: documentId,
recipientId,
},
});
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
@ -970,8 +1048,15 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}),
createField: authenticatedMiddleware(async (args, user, team, { metadata }) => {
createField: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId } = args.params;
logger.info({
input: {
id: documentId,
},
});
const fields = Array.isArray(args.body) ? args.body : [args.body];
const document = await prisma.document.findFirst({
@ -1131,11 +1216,18 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
updateField: authenticatedMiddleware(async (args, user, team, { metadata }) => {
updateField: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId, fieldId } = args.params;
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } =
args.body;
logger.info({
input: {
id: documentId,
fieldId,
},
});
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,
@ -1222,9 +1314,16 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}),
deleteField: authenticatedMiddleware(async (args, user, team, { metadata }) => {
deleteField: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
const { id: documentId, fieldId } = args.params;
logger.info({
input: {
id: documentId,
fieldId,
},
});
const document = await getDocumentById({
documentId: Number(documentId),
userId: user.id,

View File

@ -1,10 +1,14 @@
import type { Team, User } from '@prisma/client';
import type { TsRestRequest } from '@ts-rest/serverless';
import type { Logger } from 'pino';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
import type { BaseApiLog, RootApiLog } from '@documenso/lib/types/api-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { nanoid } from '@documenso/lib/universal/id';
import { logger } from '@documenso/lib/utils/logger';
type B = {
// appRoute: any;
@ -27,10 +31,24 @@ export const authenticatedMiddleware = <
args: T & { req: TsRestRequest },
user: Pick<User, 'id' | 'email' | 'name' | 'disabled'>,
team: Team,
options: { metadata: ApiRequestMetadata },
options: { metadata: ApiRequestMetadata; logger: Logger },
) => Promise<R>,
) => {
return async (args: T, { request }: B) => {
const requestMetadata = extractRequestMetadata(request);
const apiLogger = logger.child({
ipAddress: requestMetadata.ipAddress,
userAgent: requestMetadata.userAgent,
requestId: nanoid(),
} satisfies RootApiLog);
const infoToLog: BaseApiLog = {
auth: 'api',
source: 'apiV1',
path: request.url,
};
try {
const { authorization } = args.headers;
@ -51,8 +69,14 @@ export const authenticatedMiddleware = <
});
}
apiLogger.info({
...infoToLog,
userId: apiToken.user.id,
apiTokenId: apiToken.id,
} satisfies BaseApiLog);
const metadata: ApiRequestMetadata = {
requestMetadata: extractRequestMetadata(request),
requestMetadata,
source: 'apiV1',
auth: 'api',
auditUser: {
@ -69,10 +93,12 @@ export const authenticatedMiddleware = <
},
apiToken.user,
apiToken.team,
{ metadata },
{ metadata, logger: apiLogger },
);
} catch (err) {
console.log({ err: err });
console.log({ err });
apiLogger.info(infoToLog);
let message = 'Unauthorized';

View File

@ -176,8 +176,16 @@ export const ZCreateDocumentMutationSchema = z.object({
.default({}),
authOptions: z
.object({
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
globalAccessAuth: z
.union([ZDocumentAccessAuthTypesSchema, z.array(ZDocumentAccessAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
globalActionAuth: z
.union([ZDocumentActionAuthTypesSchema, z.array(ZDocumentActionAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
})
.optional()
.openapi({
@ -236,8 +244,16 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
.optional(),
authOptions: z
.object({
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
globalAccessAuth: z
.union([ZDocumentAccessAuthTypesSchema, z.array(ZDocumentAccessAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
globalActionAuth: z
.union([ZDocumentActionAuthTypesSchema, z.array(ZDocumentActionAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
})
.optional(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
@ -309,8 +325,16 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
.optional(),
authOptions: z
.object({
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
globalAccessAuth: z
.union([ZDocumentAccessAuthTypesSchema, z.array(ZDocumentAccessAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
globalActionAuth: z
.union([ZDocumentActionAuthTypesSchema, z.array(ZDocumentActionAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
})
.optional(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
@ -349,7 +373,11 @@ export const ZCreateRecipientMutationSchema = z.object({
signingOrder: z.number().nullish(),
authOptions: z
.object({
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
actionAuth: z
.union([ZRecipientActionAuthTypesSchema, z.array(ZRecipientActionAuthTypesSchema)])
.transform((val) => (Array.isArray(val) ? val : [val]))
.optional()
.default([]),
})
.optional()
.openapi({

View File

@ -0,0 +1,540 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { prisma } from '@documenso/prisma';
import { FieldType } from '@documenso/prisma/client';
import {
seedBlankDocument,
seedCompletedDocument,
seedDraftDocument,
seedPendingDocumentWithFullFields,
} from '@documenso/prisma/seed/documents';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
test.describe.configure({
mode: 'parallel',
});
test.describe('Document Access API V1', () => {
test('should block unauthorized access to documents not owned by the user', async ({
request,
}) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const documentA = await seedBlankDocument(userA, teamA.id);
// User B cannot access User A's document
const resB = await request.get(`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}`, {
headers: { Authorization: `Bearer ${tokenB}` },
});
expect(resB.ok()).toBeFalsy();
expect(resB.status()).toBe(404);
});
test('should block unauthorized access to document download endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const documentA = await seedCompletedDocument(userA, teamA.id, ['test@example.com'], {
createDocumentOptions: { title: 'Document 1 - Completed' },
});
const resB = await request.get(`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/download`, {
headers: { Authorization: `Bearer ${tokenB}` },
});
expect(resB.ok()).toBeFalsy();
expect(resB.status()).toBe(500);
});
test('should block unauthorized access to document delete endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const documentA = await seedBlankDocument(userA, teamA.id);
const resB = await request.delete(`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}`, {
headers: { Authorization: `Bearer ${tokenB}` },
});
expect(resB.ok()).toBeFalsy();
expect(resB.status()).toBe(404);
});
test('should block unauthorized access to document send endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const { document: documentA } = await seedPendingDocumentWithFullFields({
owner: userA,
recipients: ['test@example.com'],
teamId: teamA.id,
});
const resB = await request.post(`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/send`, {
headers: { Authorization: `Bearer ${tokenB}` },
data: {},
});
expect(resB.ok()).toBeFalsy();
expect(resB.status()).toBe(500);
});
test('should block unauthorized access to document resend endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const { user: recipientUser } = await seedUser();
const { document: documentA, recipients } = await seedPendingDocumentWithFullFields({
owner: userA,
recipients: [recipientUser.email],
teamId: teamA.id,
});
const resB = await request.post(`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/resend`, {
headers: { Authorization: `Bearer ${tokenB}` },
data: {
recipients: [recipients[0].id],
},
});
expect(resB.ok()).toBeFalsy();
expect(resB.status()).toBe(500);
});
test('should block unauthorized access to document recipients endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const documentA = await seedBlankDocument(userA, teamA.id);
const resB = await request.post(
`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/recipients`,
{
headers: { Authorization: `Bearer ${tokenB}` },
data: { name: 'Test', email: 'test@example.com' },
},
);
expect(resB.ok()).toBeFalsy();
expect(resB.status()).toBe(401);
});
test('should block unauthorized access to PATCH on recipients endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const { user: userRecipient } = await seedUser();
const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]);
const recipient = await prisma.recipient.findFirst({
where: {
documentId: documentA.id,
email: userRecipient.email,
},
});
const patchRes = await request.patch(
`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/recipients/${recipient!.id}`,
{
headers: { Authorization: `Bearer ${tokenB}` },
data: {
name: 'New Name',
email: 'new@example.com',
role: 'SIGNER',
signingOrder: null,
authOptions: {
accessAuth: [],
actionAuth: [],
},
},
},
);
expect(patchRes.ok()).toBeFalsy();
expect(patchRes.status()).toBe(401);
});
test('should block unauthorized access to DELETE on recipients endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const { user: userRecipient } = await seedUser();
const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]);
const recipient = await prisma.recipient.findFirst({
where: {
documentId: documentA.id,
email: userRecipient.email,
},
});
const deleteRes = await request.delete(
`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/recipients/${recipient!.id}`,
{
headers: { Authorization: `Bearer ${tokenB}` },
data: {},
},
);
expect(deleteRes.ok()).toBeFalsy();
expect(deleteRes.status()).toBe(401);
});
test('should block unauthorized access to document fields endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const { user: recipientUser } = await seedUser();
const documentA = await seedDraftDocument(userA, teamA.id, [recipientUser.email]);
const documentRecipient = await prisma.recipient.findFirst({
where: {
documentId: documentA.id,
email: recipientUser.email,
},
});
const resB = await request.post(`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/fields`, {
headers: { Authorization: `Bearer ${tokenB}` },
data: {
recipientId: documentRecipient!.id,
type: 'SIGNATURE',
pageNumber: 1,
pageX: 1,
pageY: 1,
pageWidth: 1,
pageHeight: 1,
},
});
expect(resB.ok()).toBeFalsy();
expect(resB.status()).toBe(404);
});
test('should block unauthorized access to template get endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const templateA = await seedBlankTemplate(userA, teamA.id);
const resB = await request.get(`${WEBAPP_BASE_URL}/api/v1/templates/${templateA.id}`, {
headers: { Authorization: `Bearer ${tokenB}` },
});
expect(resB.ok()).toBeFalsy();
expect(resB.status()).toBe(404);
});
test('should block unauthorized access to template delete endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const templateA = await seedBlankTemplate(userA, teamA.id);
const resB = await request.delete(`${WEBAPP_BASE_URL}/api/v1/templates/${templateA.id}`, {
headers: { Authorization: `Bearer ${tokenB}` },
});
expect(resB.ok()).toBeFalsy();
expect(resB.status()).toBe(404);
});
test('should block unauthorized access to PATCH on fields endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const { user: userRecipient } = await seedUser();
const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]);
const recipient = await prisma.recipient.findFirst({
where: {
documentId: documentA.id,
email: userRecipient.email,
},
});
const field = await prisma.field.create({
data: {
documentId: documentA.id,
recipientId: recipient!.id,
type: FieldType.TEXT,
page: 1,
positionX: 5,
positionY: 5,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'text',
label: 'Default Text Field',
},
},
});
const patchRes = await request.patch(
`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/fields/${field.id}`,
{
headers: { Authorization: `Bearer ${tokenB}` },
data: {
recipientId: recipient!.id,
type: FieldType.TEXT,
pageNumber: 1,
pageX: 99,
pageY: 99,
pageWidth: 99,
pageHeight: 99,
fieldMeta: {
type: 'text',
label: 'My new field',
},
},
},
);
expect(patchRes.ok()).toBeFalsy();
expect(patchRes.status()).toBe(401);
});
test('should block unauthorized access to DELETE on fields endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const { user: userRecipient } = await seedUser();
const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]);
const recipient = await prisma.recipient.findFirst({
where: {
documentId: documentA.id,
email: userRecipient.email,
},
});
const field = await prisma.field.create({
data: {
documentId: documentA.id,
recipientId: recipient!.id,
type: FieldType.NUMBER,
page: 1,
positionX: 5,
positionY: 5,
width: 10,
height: 5,
customText: '',
inserted: false,
fieldMeta: {
type: 'number',
label: 'Default Number Field',
},
},
});
const deleteRes = await request.delete(
`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/fields/${field.id}`,
{
headers: { Authorization: `Bearer ${tokenB}` },
data: {},
},
);
expect(deleteRes.ok()).toBeFalsy();
expect(deleteRes.status()).toBe(401);
});
test('should block unauthorized access to documents list endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
await seedBlankDocument(userA, teamA.id);
const resB = await request.get(`${WEBAPP_BASE_URL}/api/v1/documents`, {
headers: { Authorization: `Bearer ${tokenB}` },
});
const reqData = await resB.json();
expect(resB.ok()).toBeTruthy();
expect(resB.status()).toBe(200);
expect(reqData.documents.every((doc: { userId: number }) => doc.userId !== userA.id)).toBe(
true,
);
expect(reqData.documents.length).toBe(0);
expect(reqData.totalPages).toBe(0);
});
test('should block unauthorized access to templates list endpoint', async ({ request }) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
await seedBlankTemplate(userA, teamA.id);
const resB = await request.get(`${WEBAPP_BASE_URL}/api/v1/templates`, {
headers: { Authorization: `Bearer ${tokenB}` },
});
const reqData = await resB.json();
expect(resB.ok()).toBeTruthy();
expect(resB.status()).toBe(200);
expect(reqData.templates.every((tpl: { userId: number }) => tpl.userId !== userA.id)).toBe(
true,
);
expect(reqData.templates.length).toBe(0);
expect(reqData.totalPages).toBe(0);
});
test('should block unauthorized access to create-document-from-template endpoint', async ({
request,
}) => {
const { user: userA, team: teamA } = await seedUser();
const { user: userB, team: teamB } = await seedUser();
const { token: tokenB } = await createApiToken({
userId: userB.id,
teamId: teamB.id,
tokenName: 'userB',
expiresIn: null,
});
const templateA = await seedBlankTemplate(userA, teamA.id);
const resB = await request.post(
`${WEBAPP_BASE_URL}/api/v1/templates/${templateA.id}/create-document`,
{
headers: {
Authorization: `Bearer ${tokenB}`,
'Content-Type': 'application/json',
},
data: {
title: 'Should not work',
recipients: [{ name: 'Test user', email: 'test@example.com' }],
meta: {
subject: 'Test',
message: 'Test',
timezone: 'UTC',
dateFormat: 'yyyy-MM-dd',
redirectUrl: 'https://example.com',
},
},
},
);
expect(resB.ok()).toBeFalsy();
expect(resB.status()).toBe(401);
});
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,100 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import {
seedBlankDocument,
seedCompletedDocument,
seedPendingDocument,
} from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
test.describe.configure({
mode: 'parallel',
});
test.describe('Unauthorized Access to Documents', () => {
test('should block unauthorized access to the draft document page', async ({ page }) => {
const { user, team } = await seedUser();
const document = await seedBlankDocument(user, team.id);
const { user: unauthorizedUser } = await seedUser();
await apiSignin({
page,
email: unauthorizedUser.email,
redirectPath: `/t/${team.url}/documents`,
});
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${document.id}`);
await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible();
});
test('should block unauthorized access to the draft document edit page', async ({ page }) => {
const { user, team } = await seedUser();
const document = await seedBlankDocument(user, team.id);
const { user: unauthorizedUser } = await seedUser();
await apiSignin({
page,
email: unauthorizedUser.email,
redirectPath: `/t/${team.url}/documents/${document.id}/edit`,
});
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${document.id}/edit`);
await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible();
});
test('should block unauthorized access to the pending document page', async ({ page }) => {
const { user, team } = await seedUser();
const { user: recipient } = await seedUser();
const document = await seedPendingDocument(user, team.id, [recipient]);
const { user: unauthorizedUser } = await seedUser();
await apiSignin({
page,
email: unauthorizedUser.email,
redirectPath: `/t/${team.url}/documents/${document.id}`,
});
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${document.id}`);
await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible();
});
test('should block unauthorized access to pending document edit page', async ({ page }) => {
const { user, team } = await seedUser();
const { user: recipient } = await seedUser();
const document = await seedPendingDocument(user, team.id, [recipient]);
const { user: unauthorizedUser } = await seedUser();
await apiSignin({
page,
email: unauthorizedUser.email,
redirectPath: `/t/${team.url}/documents/${document.id}/edit`,
});
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${document.id}/edit`);
await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible();
});
test('should block unauthorized access to completed document page', async ({ page }) => {
const { user, team } = await seedUser();
const { user: recipient } = await seedUser();
const document = await seedCompletedDocument(user, team.id, [recipient]);
const { user: unauthorizedUser } = await seedUser();
await apiSignin({
page,
email: unauthorizedUser.email,
redirectPath: `/t/${team.url}/documents/${document.id}`,
});
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${document.id}`);
await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible();
});
});

View File

@ -21,7 +21,7 @@ test('[TEAMS]: create document folder button is visible', async ({ page }) => {
redirectPath: `/t/${team.url}`,
});
await expect(page.getByRole('button', { name: 'Create Folder' })).toBeVisible();
await expect(page.getByTestId('folder-create-button')).toBeVisible();
});
test('[TEAMS]: can create document folder', async ({ page }) => {
@ -33,7 +33,7 @@ test('[TEAMS]: can create document folder', async ({ page }) => {
redirectPath: `/t/${team.url}`,
});
await page.getByRole('button', { name: 'Create Folder' }).click();
await page.getByTestId('folder-create-button').click();
await page.getByLabel('Folder name').fill('Team Folder');
await page.getByRole('button', { name: 'Create' }).click();
@ -59,7 +59,7 @@ test('[TEAMS]: can create document subfolder within a document folder', async ({
await page.goto(`/t/${team.url}/documents/f/${teamFolder.id}`);
await page.getByRole('button', { name: 'Create Folder' }).click();
await page.getByTestId('folder-create-button').click();
await page.getByLabel('Folder name').fill('Subfolder');
await page.getByRole('button', { name: 'Create' }).click();
@ -115,7 +115,7 @@ test('[TEAMS]: can pin a document folder', async ({ page }) => {
redirectPath: `/t/${team.url}/documents`,
});
await page.getByRole('button', { name: '•••' }).click();
await page.getByTestId('folder-card-more-button').click();
await page.getByRole('menuitem', { name: 'Pin' }).click();
await page.reload();
@ -140,7 +140,7 @@ test('[TEAMS]: can unpin a document folder', async ({ page }) => {
redirectPath: `/t/${team.url}/documents`,
});
await page.getByRole('button', { name: '•••' }).click();
await page.getByTestId('folder-card-more-button').click();
await page.getByRole('menuitem', { name: 'Unpin' }).click();
await page.reload();
@ -164,7 +164,7 @@ test('[TEAMS]: can rename a document folder', async ({ page }) => {
redirectPath: `/t/${team.url}/documents`,
});
await page.getByRole('button', { name: '•••' }).click();
await page.getByTestId('folder-card-more-button').click();
await page.getByRole('menuitem', { name: 'Settings' }).click();
await page.getByLabel('Name').fill('Team Archive');
@ -189,7 +189,7 @@ test('[TEAMS]: document folder visibility is visible to team member', async ({ p
redirectPath: `/t/${team.url}/documents`,
});
await page.getByRole('button', { name: '•••' }).click();
await page.getByTestId('folder-card-more-button').click();
await page.getByRole('menuitem', { name: 'Settings' }).click();
await expect(page.getByRole('combobox', { name: 'Visibility' })).toBeVisible();
@ -218,11 +218,11 @@ test('[TEAMS]: document folder can be moved to another document folder', async (
redirectPath: `/t/${team.url}/documents`,
});
await page.getByRole('button', { name: '•••' }).nth(0).click();
await page.getByTestId('folder-card-more-button').nth(0).click();
await page.getByRole('menuitem', { name: 'Move' }).click();
await page.getByRole('button', { name: 'Team Clients' }).click();
await page.getByRole('button', { name: 'Move Folder' }).click();
await page.getByRole('button', { name: 'Move' }).click();
await page.waitForTimeout(1000);
@ -269,7 +269,7 @@ test('[TEAMS]: document folder and its contents can be deleted', async ({ page }
redirectPath: `/t/${team.url}/documents`,
});
await page.getByRole('button', { name: '•••' }).click();
await page.getByTestId('folder-card-more-button').click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('textbox').fill(`delete ${folder.name}`);
@ -295,7 +295,7 @@ test('[TEAMS]: create folder button is visible on templates page', async ({ page
redirectPath: `/t/${team.url}/templates`,
});
await expect(page.getByRole('button', { name: 'Create Folder' })).toBeVisible();
await expect(page.getByTestId('folder-create-button')).toBeVisible();
});
test('[TEAMS]: can create a template folder', async ({ page }) => {
@ -307,7 +307,7 @@ test('[TEAMS]: can create a template folder', async ({ page }) => {
redirectPath: `/t/${team.url}/templates`,
});
await page.getByRole('button', { name: 'Create Folder' }).click();
await page.getByTestId('folder-create-button').click();
await expect(page.getByRole('dialog', { name: 'Create New folder' })).toBeVisible();
await page.getByLabel('Folder name').fill('Team template folder');
@ -342,7 +342,7 @@ test('[TEAMS]: can create a template subfolder inside a template folder', async
await expect(page.getByText('Team Client Templates')).toBeVisible();
await page.getByRole('button', { name: 'Create Folder' }).click();
await page.getByTestId('folder-create-button').click();
await expect(page.getByRole('dialog', { name: 'Create New folder' })).toBeVisible();
await page.getByLabel('Folder name').fill('Team Contract Templates');
@ -414,7 +414,7 @@ test('[TEAMS]: can pin a template folder', async ({ page }) => {
redirectPath: `/t/${team.url}/templates`,
});
await page.getByRole('button', { name: '•••' }).click();
await page.getByTestId('folder-card-more-button').click();
await page.getByRole('menuitem', { name: 'Pin' }).click();
await page.reload();
@ -440,7 +440,7 @@ test('[TEAMS]: can unpin a template folder', async ({ page }) => {
redirectPath: `/t/${team.url}/templates`,
});
await page.getByRole('button', { name: '•••' }).click();
await page.getByTestId('folder-card-more-button').click();
await page.getByRole('menuitem', { name: 'Unpin' }).click();
await page.reload();
@ -466,7 +466,7 @@ test('[TEAMS]: can rename a template folder', async ({ page }) => {
redirectPath: `/t/${team.url}/templates`,
});
await page.getByRole('button', { name: '•••' }).click();
await page.getByTestId('folder-card-more-button').click();
await page.getByRole('menuitem', { name: 'Settings' }).click();
await page.getByLabel('Name').fill('Updated Team Template Folder');
@ -492,7 +492,7 @@ test('[TEAMS]: template folder visibility is not visible to team member', async
redirectPath: `/t/${team.url}/templates`,
});
await page.getByRole('button', { name: '•••' }).click();
await page.getByTestId('folder-card-more-button').click();
await page.getByRole('menuitem', { name: 'Settings' }).click();
await expect(page.getByRole('menuitem', { name: 'Visibility' })).not.toBeVisible();
@ -523,11 +523,11 @@ test('[TEAMS]: template folder can be moved to another template folder', async (
redirectPath: `/t/${team.url}/templates`,
});
await page.getByRole('button', { name: '•••' }).nth(0).click();
await page.getByTestId('folder-card-more-button').nth(0).click();
await page.getByRole('menuitem', { name: 'Move' }).click();
await page.getByRole('button', { name: 'Team Client Templates' }).click();
await page.getByRole('button', { name: 'Move Folder' }).click();
await page.getByRole('button', { name: 'Move' }).click();
await page.waitForTimeout(1000);
@ -576,7 +576,7 @@ test('[TEAMS]: template folder and its contents can be deleted', async ({ page }
redirectPath: `/t/${team.url}/templates`,
});
await page.getByRole('button', { name: '•••' }).click();
await page.getByTestId('folder-card-more-button').click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('textbox').fill(`delete ${folder.name}`);
@ -633,10 +633,10 @@ test('[TEAMS]: can navigate between template folders', async ({ page }) => {
await page.getByText('Team Contract Templates').click();
await expect(page.getByText('Team Contract Template 1')).toBeVisible();
await page.getByRole('button', { name: parentFolder.name }).click();
await page.getByRole('link', { name: parentFolder.name }).click();
await expect(page.getByText('Team Contract Templates')).toBeVisible();
await page.getByRole('button', { name: subfolder.name }).click();
await page.getByRole('link', { name: subfolder.name }).click();
await expect(page.getByText('Team Contract Template 1')).toBeVisible();
});
@ -754,7 +754,7 @@ test('[TEAMS]: folder inherits team visibility settings', async ({ page }) => {
redirectPath: `/t/${team.url}/documents`,
});
await page.getByRole('button', { name: 'Create Folder' }).click();
await page.getByTestId('folder-create-button').click();
await page.getByLabel('Name').fill('Admin Only Folder');
await page.getByRole('button', { name: 'Create' }).click();
@ -762,7 +762,7 @@ test('[TEAMS]: folder inherits team visibility settings', async ({ page }) => {
await page.goto(`/t/${team.url}/documents/`);
await page.getByRole('button', { name: '•••' }).click();
await page.getByTestId('folder-card-more-button').click();
await page.getByRole('menuitem', { name: 'Settings' }).click();
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Admins only');
@ -774,15 +774,15 @@ test('[TEAMS]: folder inherits team visibility settings', async ({ page }) => {
await page.reload();
await page.getByRole('button', { name: 'Create Folder' }).click();
await page.getByTestId('folder-create-button').click();
await page.getByLabel('Name').fill('Manager and above Folder');
await page.getByRole('button', { name: 'Create' }).click();
await expect(page.getByText('Manager and above Folder')).toBeVisible();
await page.goto(`/t/${team.url}/documents/`);
await page.goto(`/t/${team.url}/documents`);
await page.getByRole('button', { name: '•••' }).nth(0).click();
await page.getByTestId('folder-card-more-button').nth(0).click();
await page.getByRole('menuitem', { name: 'Settings' }).click();
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Managers and above');
@ -794,7 +794,7 @@ test('[TEAMS]: folder inherits team visibility settings', async ({ page }) => {
await page.reload();
await page.getByRole('button', { name: 'Create Folder' }).click();
await page.getByTestId('folder-create-button').click();
await page.getByLabel('Name').fill('Everyone Folder');
await page.getByRole('button', { name: 'Create' }).click();
@ -802,7 +802,7 @@ test('[TEAMS]: folder inherits team visibility settings', async ({ page }) => {
await page.goto(`/t/${team.url}/documents/`);
await page.getByRole('button', { name: '•••' }).nth(0).click();
await page.getByTestId('folder-card-more-button').nth(0).click();
await page.getByRole('menuitem', { name: 'Settings' }).click();
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Everyone');
@ -834,7 +834,7 @@ test('[TEAMS]: documents inherit folder visibility', async ({ page }) => {
redirectPath: `/t/${team.url}/documents`,
});
await page.getByRole('button', { name: 'Create Folder' }).click();
await page.getByTestId('folder-create-button').click();
await page.getByLabel('Name').fill('Admin Only Folder');
await page.getByRole('button', { name: 'Create' }).click();
@ -2368,7 +2368,10 @@ test('[TEAMS]: team manager can see manager and everyone documents in manager fo
redirectPath: `/t/${team.url}/documents/f/${managerFolder.id}`,
});
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
await expect(
page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Manager Folder' }),
).toBeVisible();
await expect(page.getByText('Manager Folder - Everyone Document')).toBeVisible();
await expect(page.getByText('Manager Folder - Manager Document')).toBeVisible();
await expect(page.getByText('Manager Folder - Admin Document')).not.toBeVisible();
@ -2426,7 +2429,10 @@ test('[TEAMS]: team manager can see manager and everyone documents in everyone f
redirectPath: `/t/${team.url}/documents/f/${everyoneFolder.id}`,
});
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
await expect(
page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Everyone Folder' }),
).toBeVisible();
await expect(page.getByText('Everyone Folder - Everyone Document')).toBeVisible();
await expect(page.getByText('Everyone Folder - Manager Document')).toBeVisible();
await expect(page.getByText('Everyone Folder - Admin Document')).not.toBeVisible();
@ -2572,7 +2578,10 @@ test('[TEAMS]: team owner can see all documents in admin folder', async ({ page
redirectPath: `/t/${team.url}/documents/f/${adminFolder.id}`,
});
await expect(page.getByRole('button', { name: 'Admin Only Folder' })).toBeVisible();
await expect(
page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Admin Only Folder' }),
).toBeVisible();
await expect(page.getByText('Admin Folder - Everyone Document')).toBeVisible();
await expect(page.getByText('Admin Folder - Manager Document')).toBeVisible();
await expect(page.getByText('Admin Folder - Admin Document')).toBeVisible();
@ -2622,7 +2631,9 @@ test('[TEAMS]: team owner can see all documents in manager folder', async ({ pag
redirectPath: `/t/${team.url}/documents/f/${managerFolder.id}`,
});
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
await expect(
page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Manager Folder' }),
).toBeVisible();
await expect(page.getByText('Manager Folder - Everyone Document')).toBeVisible();
await expect(page.getByText('Manager Folder - Manager Document')).toBeVisible();
await expect(page.getByText('Manager Folder - Admin Document')).toBeVisible();
@ -2672,7 +2683,9 @@ test('[TEAMS]: team owner can see all documents in everyone folder', async ({ pa
redirectPath: `/t/${team.url}/documents/f/${everyoneFolder.id}`,
});
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
await expect(
page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Everyone Folder' }),
).toBeVisible();
await expect(page.getByText('Everyone Folder - Everyone Document')).toBeVisible();
await expect(page.getByText('Everyone Folder - Manager Document')).toBeVisible();
await expect(page.getByText('Everyone Folder - Admin Document')).toBeVisible();
@ -2772,7 +2785,10 @@ test('[TEAMS]: team admin can see all documents in admin folder', async ({ page
redirectPath: `/t/${team.url}/documents/f/${adminFolder.id}`,
});
await expect(page.getByRole('button', { name: 'Admin Only Folder' })).toBeVisible();
await expect(
page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Admin Only Folder' }),
).toBeVisible();
await expect(page.getByText('Admin Folder - Everyone Document')).toBeVisible();
await expect(page.getByText('Admin Folder - Manager Document')).toBeVisible();
await expect(page.getByText('Admin Folder - Admin Document')).toBeVisible();
@ -2828,7 +2844,9 @@ test('[TEAMS]: team admin can see all documents in manager folder', async ({ pag
redirectPath: `/t/${team.url}/documents/f/${managerFolder.id}`,
});
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
await expect(
page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Manager Folder' }),
).toBeVisible();
await expect(page.getByText('Manager Folder - Everyone Document')).toBeVisible();
await expect(page.getByText('Manager Folder - Manager Document')).toBeVisible();
await expect(page.getByText('Manager Folder - Admin Document')).toBeVisible();
@ -2884,7 +2902,9 @@ test('[TEAMS]: team admin can see all documents in everyone folder', async ({ pa
redirectPath: `/t/${team.url}/documents/f/${everyoneFolder.id}`,
});
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
await expect(
page.getByTestId('folder-grid-breadcrumbs').getByRole('link', { name: 'Everyone Folder' }),
).toBeVisible();
await expect(page.getByText('Everyone Folder - Everyone Document')).toBeVisible();
await expect(page.getByText('Everyone Folder - Manager Document')).toBeVisible();
await expect(page.getByText('Everyone Folder - Admin Document')).toBeVisible();

View File

@ -0,0 +1,45 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
test.describe.configure({
mode: 'parallel',
});
test.describe('Unauthorized Access to Templates', () => {
test('should block unauthorized access to the template page', async ({ page }) => {
const { user, team } = await seedUser();
const template = await seedBlankTemplate(user, team.id);
const { user: unauthorizedUser } = await seedUser();
await apiSignin({
page,
email: unauthorizedUser.email,
redirectPath: `/t/${team.url}/templates/${template.id}`,
});
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates/${template.id}`);
await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible();
});
test('should block unauthorized access to the template edit page', async ({ page }) => {
const { user, team } = await seedUser();
const template = await seedBlankTemplate(user, team.id);
const { user: unauthorizedUser } = await seedUser();
await apiSignin({
page,
email: unauthorizedUser.email,
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
});
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates/${template.id}/edit`);
await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible();
});
});

View File

@ -0,0 +1,82 @@
import { type Page, expect, test } from '@playwright/test';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
import { expectTextToBeVisible } from '../fixtures/generic';
test('[USER] revoke sessions', async ({ page }: { page: Page }) => {
const { user, team } = await seedUser();
await apiSignin({
page,
email: user.email,
password: 'password',
redirectPath: '/settings/security/sessions',
});
// Expect 2 rows length (header + 1)
await expect(page.getByRole('row')).toHaveCount(2);
// Clear cookies
await page.context().clearCookies();
await apiSignin({
page,
email: user.email,
password: 'password',
redirectPath: '/settings/security/sessions',
});
await page.context().clearCookies();
await apiSignin({
page,
email: user.email,
password: 'password',
redirectPath: '/settings/security/sessions',
});
// Expect 4 (3 sessions + 1 header) rows length
await expect(page.getByRole('row')).toHaveCount(4);
// Revoke all sessions
await page.getByRole('button', { name: 'Revoke all sessions' }).click();
await page.getByRole('button', { name: 'Revoke all sessions' }).click();
await expectTextToBeVisible(page, 'Sessions have been revoked');
// Expect (1 sessions + 1 header) rows length
await expect(page.getByRole('row')).toHaveCount(2);
await page.context().clearCookies();
await apiSignin({
page,
email: user.email,
password: 'password',
redirectPath: '/settings/security/sessions',
});
// Find table row which does not have text 'Current' and click the button called Revoke within the row.
await page
.getByRole('row')
.filter({ hasNotText: 'Current' })
.nth(1)
.getByRole('button', { name: 'Revoke' })
.click();
await expectTextToBeVisible(page, 'Session revoked');
// Expect (1 sessions + 1 header) rows length
await expect(page.getByRole('row')).toHaveCount(2);
// Revoke own session.
await page
.getByRole('row')
.filter({ hasText: 'Current' })
.first()
.getByRole('button', { name: 'Revoke' })
.click();
await expect(page).toHaveURL('/signin');
});

Binary file not shown.

Binary file not shown.

View File

@ -7,6 +7,7 @@ import { AppError } from '@documenso/lib/errors/app-error';
import type { AuthAppType } from '../server';
import type { SessionValidationResult } from '../server/lib/session/session';
import type { ActiveSession } from '../server/lib/utils/get-session';
import { handleSignInRedirect } from '../server/lib/utils/redirect';
import type {
TDisableTwoFactorRequestSchema,
@ -47,6 +48,26 @@ export class AuthClient {
window.location.href = redirectPath ?? this.signOutredirectPath;
}
public async signOutAllSessions() {
await this.client['signout-all'].$post();
}
public async signOutSession({
sessionId,
redirectPath,
}: {
sessionId: string;
redirectPath?: string;
}) {
await this.client['signout-session'].$post({
json: { sessionId },
});
if (redirectPath) {
window.location.href = redirectPath;
}
}
public async getSession() {
const response = await this.client['session-json'].$get();
@ -57,6 +78,16 @@ export class AuthClient {
return superjson.deserialize<SessionValidationResult>(result);
}
public async getSessions() {
const response = await this.client['sessions'].$get();
await this.handleError(response);
const result = await response.json();
return superjson.deserialize<{ sessions: ActiveSession[] }>(result);
}
private async handleError<T>(response: ClientResponse<T>): Promise<void> {
if (!response.ok) {
const error = await response.json();

View File

@ -1,2 +1 @@
export * from './server/lib/errors/errors';
export * from './server/lib/errors/error-codes';

View File

@ -2,6 +2,7 @@ import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import { type Session, type User, UserSecurityAuditLogType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
@ -129,18 +130,46 @@ export const validateSessionToken = async (token: string): Promise<SessionValida
return { session, user, isAuthenticated: true };
};
export const invalidateSession = async (
sessionId: string,
metadata: RequestMetadata,
): Promise<void> => {
const session = await prisma.session.delete({ where: { id: sessionId } });
type InvalidateSessionsOptions = {
userId: number;
sessionIds: string[];
metadata: RequestMetadata;
isRevoke?: boolean;
};
await prisma.userSecurityAuditLog.create({
data: {
userId: session.userId,
ipAddress: metadata.ipAddress,
userAgent: metadata.userAgent,
type: UserSecurityAuditLogType.SIGN_OUT,
},
export const invalidateSessions = async ({
userId,
sessionIds,
metadata,
isRevoke,
}: InvalidateSessionsOptions): Promise<void> => {
if (sessionIds.length === 0) {
return;
}
await prisma.$transaction(async (tx) => {
const { count } = await tx.session.deleteMany({
where: {
userId,
id: { in: sessionIds },
},
});
if (count !== sessionIds.length) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'One or more sessions are not valid.',
});
}
await tx.userSecurityAuditLog.createMany({
data: sessionIds.map(() => ({
userId,
ipAddress: metadata.ipAddress,
userAgent: metadata.userAgent,
type: isRevoke
? UserSecurityAuditLogType.SESSION_REVOKED
: UserSecurityAuditLogType.SIGN_OUT,
})),
});
});
};

View File

@ -1,6 +1,8 @@
import type { Session } from '@prisma/client';
import type { Context } from 'hono';
import { AppError } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { AuthenticationErrorCode } from '../errors/error-codes';
import type { SessionValidationResult } from '../session/session';
@ -37,6 +39,33 @@ export const getOptionalSession = async (
return await validateSessionToken(sessionId);
};
export type ActiveSession = Omit<Session, 'sessionToken'>;
export const getActiveSessions = async (c: Context | Request): Promise<ActiveSession[]> => {
const { user } = await getSession(c);
return await prisma.session.findMany({
where: {
userId: user.id,
expiresAt: {
gt: new Date(),
},
},
orderBy: {
updatedAt: 'desc',
},
select: {
id: true,
userId: true,
expiresAt: true,
updatedAt: true,
createdAt: true,
ipAddress: true,
userAgent: true,
},
});
};
/**
* Todo: (RR7) Rethink, this is pretty sketchy.
*/

View File

@ -2,7 +2,7 @@ import { Hono } from 'hono';
import superjson from 'superjson';
import type { SessionValidationResult } from '../lib/session/session';
import { getOptionalSession } from '../lib/utils/get-session';
import { getActiveSessions, getOptionalSession } from '../lib/utils/get-session';
export const sessionRoute = new Hono()
.get('/session', async (c) => {
@ -10,6 +10,11 @@ export const sessionRoute = new Hono()
return c.json(session);
})
.get('/sessions', async (c) => {
const sessions = await getActiveSessions(c);
return c.json(superjson.serialize({ sessions }));
})
.get('/session-json', async (c) => {
const session: SessionValidationResult = await getOptionalSession(c);

View File

@ -1,27 +1,114 @@
import { sValidator } from '@hono/standard-validator';
import { Hono } from 'hono';
import { z } from 'zod';
import { invalidateSession, validateSessionToken } from '../lib/session/session';
import { prisma } from '@documenso/prisma';
import { invalidateSessions, validateSessionToken } from '../lib/session/session';
import { deleteSessionCookie, getSessionCookie } from '../lib/session/session-cookies';
import type { HonoAuthContext } from '../types/context';
export const signOutRoute = new Hono<HonoAuthContext>().post('/signout', async (c) => {
const metadata = c.get('requestMetadata');
const sessionId = await getSessionCookie(c);
if (!sessionId) {
return new Response('No session found', { status: 401 });
}
const { session } = await validateSessionToken(sessionId);
if (!session) {
return new Response('No session found', { status: 401 });
}
await invalidateSession(session.id, metadata);
deleteSessionCookie(c);
return c.status(200);
const ZSignoutSessionSchema = z.object({
sessionId: z.string().trim().min(1),
});
export const signOutRoute = new Hono<HonoAuthContext>()
.post('/signout', async (c) => {
const metadata = c.get('requestMetadata');
const sessionToken = await getSessionCookie(c);
if (!sessionToken) {
return new Response('No session found', { status: 401 });
}
const { session } = await validateSessionToken(sessionToken);
if (!session) {
deleteSessionCookie(c);
return new Response('No session found', { status: 401 });
}
await invalidateSessions({
userId: session.userId,
sessionIds: [session.id],
metadata,
isRevoke: false,
});
deleteSessionCookie(c);
return c.status(200);
})
.post('/signout-all', async (c) => {
const metadata = c.get('requestMetadata');
const sessionToken = await getSessionCookie(c);
if (!sessionToken) {
return new Response('No session found', { status: 401 });
}
const { session } = await validateSessionToken(sessionToken);
if (!session) {
deleteSessionCookie(c);
return new Response('No session found', { status: 401 });
}
const userId = session.userId;
const userSessionIds = await prisma.session
.findMany({
where: {
userId,
id: {
not: session.id,
},
},
select: {
id: true,
},
})
.then((sessions) => sessions.map((session) => session.id));
await invalidateSessions({
userId,
sessionIds: userSessionIds,
metadata,
isRevoke: true,
});
return c.status(200);
})
.post('/signout-session', sValidator('json', ZSignoutSessionSchema), async (c) => {
const metadata = c.get('requestMetadata');
const { sessionId: sessionIdToRevoke } = c.req.valid('json');
const sessionToken = await getSessionCookie(c);
if (!sessionToken) {
return new Response('No session found', { status: 401 });
}
const { session } = await validateSessionToken(sessionToken);
if (!session) {
deleteSessionCookie(c);
return new Response('No session found', { status: 401 });
}
await invalidateSessions({
userId: session.userId,
sessionIds: [sessionIdToRevoke],
metadata,
isRevoke: true,
});
if (session.id === sessionIdToRevoke) {
deleteSessionCookie(c);
}
return c.status(200);
});

View File

@ -66,7 +66,7 @@ export const getServerLimits = async ({
};
}
// If plan expired.
// Early return for users with an expired subscription.
if (subscription && subscription.status !== SubscriptionStatus.ACTIVE) {
return {
quota: INACTIVE_PLAN_LIMITS,
@ -74,52 +74,46 @@ export const getServerLimits = async ({
};
}
if (subscription && organisation.organisationClaim.flags.unlimitedDocuments) {
// Allow unlimited documents for users with an unlimited documents claim.
// This also allows "free" claim users without subscriptions if they have this flag.
if (organisation.organisationClaim.flags.unlimitedDocuments) {
return {
quota: PAID_PLAN_LIMITS,
remaining: PAID_PLAN_LIMITS,
};
}
// If free tier or plan does not have unlimited documents.
if (!subscription || !organisation.organisationClaim.flags.unlimitedDocuments) {
const [documents, directTemplates] = await Promise.all([
prisma.document.count({
where: {
team: {
organisationId: organisation.id,
},
createdAt: {
gte: DateTime.utc().startOf('month').toJSDate(),
},
source: {
not: DocumentSource.TEMPLATE_DIRECT_LINK,
},
const [documents, directTemplates] = await Promise.all([
prisma.document.count({
where: {
team: {
organisationId: organisation.id,
},
}),
prisma.template.count({
where: {
team: {
organisationId: organisation.id,
},
directLink: {
isNot: null,
},
createdAt: {
gte: DateTime.utc().startOf('month').toJSDate(),
},
}),
]);
source: {
not: DocumentSource.TEMPLATE_DIRECT_LINK,
},
},
}),
prisma.template.count({
where: {
team: {
organisationId: organisation.id,
},
directLink: {
isNot: null,
},
},
}),
]);
remaining.documents = Math.max(remaining.documents - documents, 0);
remaining.directTemplates = Math.max(remaining.directTemplates - directTemplates, 0);
return {
quota,
remaining,
};
}
remaining.documents = Math.max(remaining.documents - documents, 0);
remaining.directTemplates = Math.max(remaining.directTemplates - directTemplates, 0);
return {
quota: PAID_PLAN_LIMITS,
remaining: PAID_PLAN_LIMITS,
quota,
remaining,
};
};

View File

@ -31,6 +31,7 @@ export const USER_SECURITY_AUDIT_LOG_MAP: Record<string, string> = {
PASSKEY_UPDATED: 'Passkey updated',
PASSWORD_RESET: 'Password reset',
PASSWORD_UPDATE: 'Password updated',
SESSION_REVOKED: 'Session revoked',
SIGN_OUT: 'Signed Out',
SIGN_IN: 'Signed In',
SIGN_IN_FAIL: 'Sign in attempt failed',

View File

@ -0,0 +1,8 @@
import { FieldType } from '@prisma/client';
export const AUTO_SIGNABLE_FIELD_TYPES: FieldType[] = [
FieldType.NAME,
FieldType.INITIALS,
FieldType.EMAIL,
FieldType.DATE,
];

View File

@ -23,11 +23,9 @@
"@documenso/email": "*",
"@documenso/prisma": "*",
"@documenso/signing": "*",
"@honeybadger-io/js": "^6.10.1",
"@lingui/core": "^5.2.0",
"@lingui/macro": "^5.2.0",
"@lingui/react": "^5.2.0",
"jose": "^6.0.0",
"@noble/ciphers": "0.4.0",
"@noble/hashes": "1.3.2",
"@node-rs/bcrypt": "^1.10.0",
@ -37,6 +35,7 @@
"@vvo/tzdb": "^6.117.0",
"csv-parse": "^5.6.0",
"inngest": "^3.19.13",
"jose": "^6.0.0",
"kysely": "0.26.3",
"luxon": "^3.4.0",
"micro": "^10.0.1",
@ -44,6 +43,8 @@
"oslo": "^0.17.0",
"pdf-lib": "^1.17.1",
"pg": "^8.11.3",
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
"playwright": "1.52.0",
"posthog-js": "^1.245.0",
"posthog-node": "^4.17.0",
@ -59,4 +60,4 @@
"@types/luxon": "^3.3.1",
"@types/pg": "^8.11.4"
}
}
}

View File

@ -1,7 +1,7 @@
import { DocumentStatus, SubscriptionStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
import { kyselyPrisma, prisma, sql } from '@documenso/prisma';
import { SubscriptionStatus, UserSecurityAuditLogType } from '@documenso/prisma/client';
export const getUsersCount = async () => {
return await prisma.user.count();
@ -17,37 +17,6 @@ export const getOrganisationsWithSubscriptionsCount = async () => {
});
};
export const getUserWithAtLeastOneDocumentPerMonth = async () => {
return await prisma.user.count({
where: {
documents: {
some: {
createdAt: {
gte: DateTime.now().minus({ months: 1 }).toJSDate(),
},
},
},
},
});
};
export const getUserWithAtLeastOneDocumentSignedPerMonth = async () => {
return await prisma.user.count({
where: {
documents: {
some: {
status: {
equals: DocumentStatus.COMPLETED,
},
completedAt: {
gte: DateTime.now().minus({ months: 1 }).toJSDate(),
},
},
},
},
});
};
export type GetUserWithDocumentMonthlyGrowth = Array<{
month: string;
count: number;
@ -67,6 +36,8 @@ export const getUserWithSignedDocumentMonthlyGrowth = async () => {
COUNT(DISTINCT "Document"."userId") as "count",
COUNT(DISTINCT CASE WHEN "Document"."status" = 'COMPLETED' THEN "Document"."userId" END) as "signed_count"
FROM "Document"
INNER JOIN "Team" ON "Document"."teamId" = "Team"."id"
INNER JOIN "Organisation" ON "Team"."organisationId" = "Organisation"."id"
GROUP BY "month"
ORDER BY "month" DESC
LIMIT 12
@ -78,3 +49,37 @@ export const getUserWithSignedDocumentMonthlyGrowth = async () => {
signed_count: Number(row.signed_count),
}));
};
export type GetMonthlyActiveUsersResult = Array<{
month: string;
count: number;
cume_count: number;
}>;
export const getMonthlyActiveUsers = async () => {
const qb = kyselyPrisma.$kysely
.selectFrom('UserSecurityAuditLog')
.select(({ fn }) => [
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'UserSecurityAuditLog.createdAt']).as('month'),
fn.count('userId').distinct().as('count'),
fn
.sum(fn.count('userId').distinct())
.over((ob) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions
ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'UserSecurityAuditLog.createdAt']) as any),
)
.as('cume_count'),
])
.where(sql`type = ${UserSecurityAuditLogType.SIGN_IN}::"UserSecurityAuditLogType"`)
.groupBy(({ fn }) => fn('DATE_TRUNC', [sql.lit('MONTH'), 'UserSecurityAuditLog.createdAt']))
.orderBy('month', 'desc')
.limit(12);
const result = await qb.execute();
return result.map((row) => ({
month: DateTime.fromJSDate(row.month).toFormat('yyyy-MM'),
count: Number(row.count),
cume_count: Number(row.cume_count),
}));
};

View File

@ -1,4 +1,6 @@
import { DocumentSource, type Prisma, WebhookTriggerEvents } from '@prisma/client';
import type { Prisma, Recipient } from '@prisma/client';
import { DocumentSource, WebhookTriggerEvents } from '@prisma/client';
import { omit } from 'remeda';
import { prisma } from '@documenso/prisma';
@ -7,7 +9,7 @@ import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { prefixedId } from '../../universal/id';
import { nanoid, prefixedId } from '../../universal/id';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { getDocumentWhereInput } from './get-document-by-id';
@ -40,14 +42,16 @@ export const duplicateDocument = async ({
type: true,
},
},
documentMeta: {
authOptions: true,
visibility: true,
documentMeta: true,
recipients: {
select: {
message: true,
subject: true,
dateFormat: true,
password: true,
timezone: true,
redirectUrl: true,
email: true,
name: true,
role: true,
signingOrder: true,
fields: true,
},
},
},
@ -59,56 +63,83 @@ export const duplicateDocument = async ({
});
}
const createDocumentArguments: Prisma.DocumentCreateArgs = {
const documentData = await prisma.documentData.create({
data: {
title: document.title,
qrToken: prefixedId('qr'),
user: {
connect: {
id: document.userId,
},
},
team: {
connect: {
id: teamId,
},
},
documentData: {
create: {
...document.documentData,
data: document.documentData.initialData,
},
},
documentMeta: {
create: {
...document.documentMeta,
},
},
source: DocumentSource.DOCUMENT,
type: document.documentData.type,
data: document.documentData.initialData,
initialData: document.documentData.initialData,
},
};
});
if (teamId !== undefined) {
createDocumentArguments.data.team = {
connect: {
id: teamId,
let documentMeta: Prisma.DocumentCreateArgs['data']['documentMeta'] | undefined = undefined;
if (document.documentMeta) {
documentMeta = {
create: {
...omit(document.documentMeta, ['id', 'documentId']),
emailSettings: document.documentMeta.emailSettings || undefined,
},
};
}
const createdDocument = await prisma.document.create({
...createDocumentArguments,
data: {
userId: document.userId,
teamId: teamId,
title: document.title,
documentDataId: documentData.id,
authOptions: document.authOptions || undefined,
visibility: document.visibility,
qrToken: prefixedId('qr'),
documentMeta,
source: DocumentSource.DOCUMENT,
},
include: {
recipients: true,
documentMeta: true,
},
});
const recipientsToCreate = document.recipients.map((recipient) => ({
documentId: createdDocument.id,
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
fields: {
createMany: {
data: recipient.fields.map((field) => ({
documentId: createdDocument.id,
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
})),
},
},
}));
const recipients: Recipient[] = [];
for (const recipientData of recipientsToCreate) {
const newRecipient = await prisma.recipient.create({
data: recipientData,
});
recipients.push(newRecipient);
}
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_CREATED,
data: ZWebhookDocumentSchema.parse({
...mapDocumentToWebhookDocumentPayload(createdDocument),
recipients: createdDocument.recipients,
recipients,
documentMeta: createdDocument.documentMeta,
}),
userId: userId,

View File

@ -1,5 +1,5 @@
import type { Prisma } from '@prisma/client';
import { TeamMemberRole } from '@prisma/client';
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
@ -83,9 +83,46 @@ export const getDocumentWhereInput = async ({
}: GetDocumentWhereInputOptions) => {
const team = await getTeamById({ teamId, userId });
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const teamVisibilityFilters = match(team.currentTeamRole)
.with(TeamMemberRole.ADMIN, () => [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
DocumentVisibility.ADMIN,
])
.with(TeamMemberRole.MANAGER, () => [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
])
.otherwise(() => [DocumentVisibility.EVERYONE]);
const documentOrInput: Prisma.DocumentWhereInput[] = [
// Allow access if they own the document.
{
teamId: team.id,
userId,
},
// Or, if they belong to the team that the document is associated with.
{
visibility: {
in: teamVisibilityFilters,
},
teamId,
},
// Or, if they are a recipient of the document.
{
status: {
not: DocumentStatus.DRAFT,
},
recipients: {
some: {
email: user.email,
},
},
},
];
@ -112,45 +149,8 @@ export const getDocumentWhereInput = async ({
OR: documentOrInput,
};
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const visibilityFilters = [
...match(team.currentTeamRole)
.with(TeamMemberRole.ADMIN, () => [
{ visibility: DocumentVisibility.EVERYONE },
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
{ visibility: DocumentVisibility.ADMIN },
])
.with(TeamMemberRole.MANAGER, () => [
{ visibility: DocumentVisibility.EVERYONE },
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
])
.otherwise(() => [{ visibility: DocumentVisibility.EVERYONE }]),
{
OR: [
{
recipients: {
some: {
email: user.email,
},
},
},
{
userId: user.id,
},
],
},
];
return {
documentWhereInput: {
...documentWhereInput,
OR: [...visibilityFilters],
},
documentWhereInput,
team,
};
};

View File

@ -117,7 +117,12 @@ export const sealDocument = async ({
? await getCertificatePdf({
documentId,
language: document.documentMeta?.language,
}).catch(() => null)
}).catch((e) => {
console.log('Failed to get certificate PDF');
console.error(e);
return null;
})
: null;
const doc = await PDFDocument.load(pdfData);

View File

@ -27,7 +27,6 @@ export const viewedDocument = async ({
const recipient = await prisma.recipient.findFirst({
where: {
token,
readStatus: ReadStatus.NOT_OPENED,
},
});
@ -37,6 +36,30 @@ export const viewedDocument = async ({
const { documentId } = recipient;
await prisma.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED,
documentId,
user: {
name: recipient.name,
email: recipient.email,
},
requestMetadata,
data: {
recipientEmail: recipient.email,
recipientId: recipient.id,
recipientName: recipient.name,
recipientRole: recipient.role,
accessAuth: recipientAccessAuth ?? [],
},
}),
});
// Early return if already opened.
if (recipient.readStatus === ReadStatus.OPENED) {
return;
}
await prisma.$transaction(async (tx) => {
await tx.recipient.update({
where: {

View File

@ -1,5 +1,6 @@
import { DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { isDeepEqual } from 'remeda';
import { match } from 'ts-pattern';
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
@ -10,6 +11,7 @@ import { validateTextField } from '@documenso/lib/advanced-fields-validation/val
import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
import { prisma } from '@documenso/prisma';
import { AUTO_SIGNABLE_FIELD_TYPES } from '../../constants/autosign';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../../constants/time-zones';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
@ -205,6 +207,29 @@ export const signFieldWithToken = async ({
throw new Error('Typed signatures are not allowed. Please draw your signature');
}
if (field.fieldMeta?.readOnly && !AUTO_SIGNABLE_FIELD_TYPES.includes(field.type)) {
// !: This is a bit of a hack at the moment, readonly fields with default values
// !: should be inserted with their default value on document creation instead of
// !: this weird programattic approach. Until that's fixed though this will verify
// !: that the programmatic signed value is only that of its default.
const isAutomaticSigningValueValid = match(field.fieldMeta)
.with({ type: 'text' }, (meta) => customText === meta.text)
.with({ type: 'number' }, (meta) => customText === meta.value)
.with({ type: 'checkbox' }, (meta) =>
isDeepEqual(
fromCheckboxValue(customText ?? ''),
meta.values?.filter((v) => v.checked).map((v) => v.value) ?? [],
),
)
.with({ type: 'radio' }, (meta) => customText === meta.values?.find((v) => v.checked)?.value)
.with({ type: 'dropdown' }, (meta) => customText === meta.defaultValue)
.otherwise(() => false);
if (!isAutomaticSigningValueValid) {
throw new Error('Field is read only and only accepts its default value for signing.');
}
}
const assistant = recipient.role === RecipientRole.ASSISTANT ? recipient : undefined;
return await prisma.$transaction(async (tx) => {

View File

@ -1,15 +0,0 @@
import { prisma } from '@documenso/prisma';
export type GetApiTokenByIdOptions = {
id: number;
userId: number;
};
export const getApiTokenById = async ({ id, userId }: GetApiTokenByIdOptions) => {
return await prisma.apiToken.findFirstOrThrow({
where: {
id,
userId,
},
});
};

View File

@ -11,7 +11,6 @@ export type GetApiTokensOptions = {
export const getApiTokens = async ({ userId, teamId }: GetApiTokensOptions) => {
return await prisma.apiToken.findMany({
where: {
userId,
team: buildTeamWhereQuery({
teamId,
userId,

View File

@ -23,8 +23,15 @@ export const duplicateTemplate = async ({
team: buildTeamWhereQuery({ teamId, userId }),
},
include: {
recipients: true,
fields: true,
recipients: {
select: {
email: true,
name: true,
role: true,
signingOrder: true,
fields: true,
},
},
templateDocumentData: true,
templateMeta: true,
},
@ -59,13 +66,8 @@ export const duplicateTemplate = async ({
teamId,
title: template.title + ' (copy)',
templateDocumentDataId: documentData.id,
recipients: {
create: template.recipients.map((recipient) => ({
email: recipient.email,
name: recipient.name,
token: nanoid(),
})),
},
authOptions: template.authOptions || undefined,
visibility: template.visibility,
templateMeta,
},
include: {
@ -73,32 +75,36 @@ export const duplicateTemplate = async ({
},
});
await prisma.field.createMany({
data: template.fields.map((field) => {
const recipient = template.recipients.find((recipient) => recipient.id === field.recipientId);
const recipientsToCreate = template.recipients.map((recipient) => ({
templateId: duplicatedTemplate.id,
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
fields: {
createMany: {
data: recipient.fields.map((field) => ({
templateId: duplicatedTemplate.id,
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta as PrismaJson.FieldMeta,
})),
},
},
}));
const duplicatedTemplateRecipient = duplicatedTemplate.recipients.find(
(doc) => doc.email === recipient?.email,
);
if (!duplicatedTemplateRecipient) {
throw new Error('Recipient not found.');
}
return {
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: field.customText,
inserted: field.inserted,
templateId: duplicatedTemplate.id,
recipientId: duplicatedTemplateRecipient.id,
};
}),
});
for (const recipientData of recipientsToCreate) {
await prisma.recipient.create({
data: recipientData,
});
}
return duplicatedTemplate;
};

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-06-10 02:27\n"
"PO-Revision-Date: 2025-06-19 06:05\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -72,6 +72,16 @@ msgstr "{0, plural, one {(1 Zeichen über dem Limit)} other {(# Zeichen über de
msgid "{0, plural, one {# character over the limit} other {# characters over the limit}}"
msgstr "{0, plural, one {# Zeichen über dem Limit} other {# Zeichen über dem Limit}}"
#. placeholder {0}: folder._count.documents
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# document} other {# documents}}"
msgstr "{0, plural, one {# Dokument} other {# Dokumente}}"
#. placeholder {0}: folder._count.subfolders
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# folder} other {# folders}}"
msgstr "{0, plural, one {# Ordner} other {# Ordner}}"
#. placeholder {0}: template.recipients.length
#: apps/remix/app/routes/_recipient+/d.$token+/_index.tsx
msgid "{0, plural, one {# recipient} other {# recipients}}"
@ -82,6 +92,11 @@ msgstr "{0, plural, one {# Empfänger} other {# Empfänger}}"
msgid "{0, plural, one {# team} other {# teams}}"
msgstr "{0, plural, one {# Team} other {# Teams}}"
#. placeholder {0}: folder._count.templates
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# template} other {# templates}}"
msgstr "{0, plural, one {# Vorlage} other {# Vorlagen}}"
#. placeholder {0}: data.length
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
@ -752,6 +767,11 @@ msgstr "Aktionen"
msgid "Active"
msgstr "Aktiv"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Active sessions"
msgstr "Aktive Sitzungen"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Active Subscriptions"
msgstr "Aktive Abonnements"
@ -817,13 +837,13 @@ msgstr "Felder hinzufügen"
msgid "Add group roles"
msgstr "Gruppenrollen hinzufügen"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add groups"
msgstr "Gruppen hinzufügen"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add members"
msgstr "Mitglieder hinzufügen"
@ -1243,17 +1263,14 @@ msgstr "Ein unerwarteter Fehler ist aufgetreten."
msgid "An unknown error occurred"
msgstr "Es ist ein unbekannter Fehler aufgetreten"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "An unknown error occurred while creating the folder."
msgstr "Ein unbekannter Fehler ist beim Erstellen des Ordners aufgetreten."
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "An unknown error occurred while deleting the folder."
msgstr "Ein unbekannter Fehler ist beim Löschen des Ordners aufgetreten."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "An unknown error occurred while moving the folder."
msgstr "Ein unbekannter Fehler ist beim Verschieben des Ordners aufgetreten."
@ -1322,6 +1339,10 @@ msgstr "Sind Sie sicher, dass Sie das Dokument abschließen möchten? Diese Akti
msgid "Are you sure you want to delete the following claim?"
msgstr "Sind Sie sicher, dass Sie den folgenden Antrag löschen möchten?"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Are you sure you want to delete this folder?"
msgstr "Möchten Sie diesen Ordner wirklich löschen?"
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
msgid "Are you sure you want to delete this token?"
msgstr "Bist du sicher, dass du dieses Token löschen möchtest?"
@ -1614,6 +1635,7 @@ msgstr "Kann vorbereiten"
#: apps/remix/app/components/dialogs/team-delete-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
@ -1626,6 +1648,9 @@ msgstr "Kann vorbereiten"
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
@ -1921,7 +1946,6 @@ msgstr "Bestätigen Sie durch Eingabe von <0>{deleteMessage}</0>"
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Confirm by typing: <0>{deleteMessage}</0>"
msgstr "Bestätigen Sie durch Eingabe: <0>{deleteMessage}</0>"
@ -2065,6 +2089,7 @@ msgstr "Token kopieren"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
msgid "Create"
msgstr "Erstellen"
@ -2134,6 +2159,14 @@ msgstr "Direkten Signatur-Link erstellen"
msgid "Create document from template"
msgstr "Dokument aus der Vorlage erstellen"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Create folder"
msgstr "Ordner erstellen"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create Folder"
msgstr "Ordner erstellen"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Create group"
@ -2143,6 +2176,10 @@ msgstr "Gruppe erstellen"
msgid "Create Groups"
msgstr "Gruppen erstellen"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create New Folder"
msgstr "Neuen Ordner erstellen"
#: apps/remix/app/routes/_profile+/_layout.tsx
msgid "Create now"
msgstr "Jetzt erstellen"
@ -2221,6 +2258,7 @@ msgstr "Erstellen Sie Ihr Konto und beginnen Sie mit dem modernen Dokumentensign
msgid "Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp."
msgstr "Erstellen Sie Ihr Konto und beginnen Sie mit dem modernen Dokumentensignieren. Offenes und schönes Signieren liegt in Ihrer Reichweite."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
@ -2260,6 +2298,14 @@ msgstr "Erstellt am {0}"
msgid "CSV Structure"
msgstr "CSV-Struktur"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Cumulative MAU (signed in)"
msgstr "Kumulative MAU (angemeldet)"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Current"
msgstr "Aktuell"
#: apps/remix/app/components/forms/password.tsx
msgid "Current Password"
msgstr "Aktuelles Passwort"
@ -2342,6 +2388,7 @@ msgstr "löschen"
#: apps/remix/app/components/tables/organisation-groups-table.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/admin-claims-table.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
@ -2353,6 +2400,7 @@ msgstr "löschen"
#: apps/remix/app/components/dialogs/organisation-group-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Delete"
@ -2360,10 +2408,9 @@ msgstr "Löschen"
#. placeholder {0}: webhook.webhookUrl
#. placeholder {0}: token.name
#. placeholder {0}: folder?.name ?? 'folder'
#. placeholder {0}: organisation.name
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "delete {0}"
@ -2395,6 +2442,10 @@ msgstr "Dokument löschen"
msgid "Delete Document"
msgstr "Dokument löschen"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Delete Folder"
msgstr "Ordner löschen"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
msgid "Delete organisation"
msgstr "Organisation löschen"
@ -2453,6 +2504,7 @@ msgid "Details"
msgstr "Einzelheiten"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/settings-security-activity-table.tsx
msgid "Device"
msgstr "Gerät"
@ -2833,7 +2885,6 @@ msgid "Document will be permanently deleted"
msgstr "Dokument wird dauerhaft gelöscht"
#: apps/remix/app/routes/_profile+/p.$url.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
@ -3139,6 +3190,10 @@ msgstr "Das Aktivieren des Kontos führt dazu, dass der Benutzer das Konto wiede
msgid "Enclosed Document"
msgstr "Beigefügte Dokument"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Geben Sie einen Namen für Ihren neuen Ordner ein. Ordner helfen Ihnen, Ihre Dateien zu organisieren."
#: apps/remix/app/components/forms/subscription-claim-form.tsx
msgid "Enter claim name"
msgstr "Anspruchsname eingeben"
@ -3179,6 +3234,7 @@ msgstr "Unternehmen"
#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
@ -3221,6 +3277,7 @@ msgstr "Unternehmen"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx
@ -3265,8 +3322,7 @@ msgstr "Läuft ab am {0}"
msgid "External ID"
msgstr "Externe ID"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Failed to create folder"
msgstr "Ordner konnte nicht erstellt werden"
@ -3274,6 +3330,10 @@ msgstr "Ordner konnte nicht erstellt werden"
msgid "Failed to create subscription claim."
msgstr "Fehler beim Erstellen des Abonnementsanspruchs."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Failed to delete folder"
msgstr "Ordner konnte nicht gelöscht werden"
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Failed to delete subscription claim."
msgstr "Fehler beim Löschen des Abonnementsanspruchs."
@ -3282,14 +3342,26 @@ msgstr "Fehler beim Löschen des Abonnementsanspruchs."
msgid "Failed to load document"
msgstr "Fehler beim Laden des Dokuments"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Failed to move folder"
msgstr "Ordner konnte nicht verschoben werden"
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Failed to reseal document"
msgstr "Dokument konnte nicht erneut versiegelt werden"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Failed to revoke session"
msgstr "Sitzung konnte nicht widerrufen werden"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Failed to save settings."
msgstr "Einstellungen konnten nicht gespeichert werden."
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Failed to sign out all sessions"
msgstr "Fehler beim Abmelden aller Sitzungen"
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
msgid "Failed to update document"
msgstr "Dokument konnte nicht aktualisiert werden"
@ -3386,16 +3458,28 @@ msgstr "Füllen Sie die Details aus, um einen neuen Abonnementsanspruch zu erste
msgid "Folder"
msgstr "Ordner"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder created successfully"
msgstr "Ordner erfolgreich erstellt"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder deleted successfully"
msgstr "Ordner erfolgreich gelöscht"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Folder moved successfully"
msgstr "Ordner erfolgreich verschoben"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder Name"
msgstr "Ordnername"
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder not found"
msgstr "Ordner nicht gefunden"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
msgid "Folder updated successfully"
msgstr "Ordner erfolgreich aktualisiert"
@ -3633,9 +3717,16 @@ msgid "Hide additional information"
msgstr "Zusätzliche Informationen ausblenden"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Home"
msgstr "Startseite"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Home (No Folder)"
msgstr "Startseite (kein Ordner)"
#: packages/lib/constants/recipient-roles.ts
msgid "I am a signer of this document"
msgstr "Ich bin ein Unterzeichner dieses Dokuments"
@ -3747,7 +3838,6 @@ msgstr "Ungültiger Code. Bitte versuchen Sie es erneut."
msgid "Invalid email"
msgstr "Ungültige E-Mail"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "Invalid link"
msgstr "Ungültiger Link"
@ -3811,6 +3901,7 @@ msgid "Invoice"
msgstr "Rechnung"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
msgid "IP Address"
msgstr "IP-Adresse"
@ -3881,6 +3972,10 @@ msgstr "Die letzten 30 Tage"
msgid "Last 7 days"
msgstr "Die letzten 7 Tage"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Last Active"
msgstr "Zuletzt aktiv"
#: apps/remix/app/components/general/template/template-page-view-information.tsx
#: apps/remix/app/components/general/document/document-page-view-information.tsx
msgid "Last modified"
@ -4030,6 +4125,10 @@ msgstr "Passkeys verwalten"
msgid "Manage permissions and access controls"
msgstr "Verwalten Sie Berechtigungen und Zugangskontrollen"
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Manage sessions"
msgstr "Sitzungen verwalten"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage subscription"
@ -4107,6 +4206,10 @@ msgstr "MAU (erstellt Dokument)"
msgid "MAU (had document completed)"
msgstr "MAU (hat Dokument abgeschlossen)"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "MAU (signed in)"
msgstr "MAU (angemeldet)"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
msgid "Max"
msgstr "Max"
@ -4177,7 +4280,9 @@ msgstr "Monatlich aktive Benutzer: Benutzer, die mindestens ein Dokument erstell
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Monatlich aktive Benutzer: Benutzer, die mindestens eines ihrer Dokumente abgeschlossen haben"
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Move"
msgstr "Verschieben"
@ -4190,6 +4295,10 @@ msgstr "\"{templateTitle}\" in einen Ordner verschieben"
msgid "Move Document to Folder"
msgstr "Dokument in Ordner verschieben"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Move Folder"
msgstr "Ordner verschieben"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
msgid "Move Template to Folder"
msgstr "Vorlage in Ordner verschieben"
@ -4203,6 +4312,10 @@ msgstr "In Ordner verschieben"
msgid "Multiple access methods can be selected."
msgstr "Es können mehrere Zugriffsmethoden ausgewählt werden."
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "My Folder"
msgstr "Mein Ordner"
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
@ -4291,6 +4404,16 @@ msgstr "Nein"
msgid "No active drafts"
msgstr "Keine aktiven Entwürfe"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "No folders found"
msgstr "Keine Ordner gefunden"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
msgid "No folders found matching \"{searchTerm}\""
msgstr "Keine Ordner gefunden, die \"{searchTerm}\" entsprechen"
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
msgid "No further action is required from you at this time."
@ -4600,10 +4723,18 @@ msgstr "Organisationen"
msgid "Organisations that the user is a member of."
msgstr "Organisationen, in denen der Benutzer Mitglied ist."
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your documents"
msgstr "Organisiere deine Dokumente"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Organise your members into groups which can be assigned to teams"
msgstr "Organisieren Sie Ihre Mitglieder in Gruppen, die Teams zugewiesen werden können"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your templates"
msgstr "Organisiere deine Vorlagen"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
msgid "Organize your documents and templates"
msgstr "Organisieren Sie Ihre Dokumente und Vorlagen"
@ -4779,6 +4910,10 @@ msgstr "Wählen Sie ein Passwort"
msgid "Pick any of the following agreements below and start signing to get started"
msgstr "Wählen Sie eine der folgenden Vereinbarungen aus und beginnen Sie das Signieren, um loszulegen"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Pin"
msgstr "Anheften"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@ -5305,7 +5440,6 @@ msgstr "Aufbewahrung von Dokumenten"
msgid "Retry"
msgstr "Wiederholen"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.decline.$token.tsx
@ -5321,6 +5455,7 @@ msgstr "Zurück zur Startseite"
msgid "Return to sign in"
msgstr "Zurück zur Anmeldung"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/general/teams/team-email-usage.tsx
msgid "Revoke"
msgstr "Zugriff widerrufen"
@ -5329,6 +5464,12 @@ msgstr "Zugriff widerrufen"
msgid "Revoke access"
msgstr "Zugriff widerrufen"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Revoke all sessions"
msgstr "Alle Sitzungen widerrufen"
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/components/tables/team-members-table.tsx
#: apps/remix/app/components/tables/team-groups-table.tsx
@ -5348,11 +5489,6 @@ msgstr "Rolle"
msgid "Roles"
msgstr "Rollen"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Root (No Folder)"
msgstr "Wurzel (kein Ordner)"
#: packages/ui/primitives/data-table-pagination.tsx
msgid "Rows per page"
msgstr "Zeilen pro Seite"
@ -5404,6 +5540,14 @@ msgstr "Suche nach Organisations-ID, Name, Kunden-ID oder E-Mail des Inhabers"
msgid "Search documents..."
msgstr "Dokumente suchen..."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Search folders..."
msgstr "Ordner durchsuchen..."
#: packages/ui/components/common/language-switcher-dialog.tsx
msgid "Search languages..."
msgstr "Sprachen suchen..."
@ -5428,6 +5572,10 @@ msgstr "Sicherheitsaktivität"
msgid "Select"
msgstr "Auswählen"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Select a destination for this folder."
msgstr "Wählen Sie ein Ziel für diesen Ordner aus."
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Select a folder to move this document to."
msgstr "Wählen Sie einen Ordner, um dieses Dokument zu verschieben."
@ -5481,6 +5629,10 @@ msgstr "Standardoption auswählen"
msgid "Select groups"
msgstr "Gruppen auswählen"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups of members to add to the team."
msgstr "Mitgliedsgruppen auswählen, die dem Team hinzugefügt werden sollen."
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups to add to this team"
msgstr "Wählen Sie Gruppen aus, die diesem Team hinzugefügt werden sollen"
@ -5492,7 +5644,6 @@ msgid "Select members"
msgstr "Mitglieder auswählen"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select members or groups of members to add to the team."
msgstr "Wählen Sie Mitglieder oder Gruppen von Mitgliedern, die dem Team hinzugefügt werden sollen."
@ -5602,6 +5753,14 @@ msgstr "Senden..."
msgid "Sent"
msgstr "Gesendet"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Session revoked"
msgstr "Sitzung widerrufen"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Sessions have been revoked"
msgstr "Sitzungen wurden widerrufen"
#: apps/remix/app/components/general/claim-account.tsx
msgid "Set a password"
msgstr "Ein Passwort festlegen"
@ -5621,6 +5780,7 @@ msgstr "Richten Sie Ihre Vorlageneigenschaften und Empfängerinformationen ein"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Settings"
msgstr "Einstellungen"
@ -6318,7 +6478,6 @@ msgstr "Vorlagentitel"
msgid "Template updated successfully"
msgstr "Vorlage erfolgreich aktualisiert"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
@ -6449,12 +6608,10 @@ msgstr "Die Ereignisse, die einen Webhook auslösen, der an Ihre URL gesendet wi
msgid "The fields have been updated to the new field insertion method successfully"
msgstr "Die Felder wurden erfolgreich auf die neue Feld-Integrationsmethode aktualisiert"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "The folder you are trying to delete does not exist."
msgstr "Der Order, den Sie löschen möchten, existiert nicht."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "The folder you are trying to move does not exist."
msgstr "Der Order, den Sie verschieben möchten, existiert nicht."
@ -6771,10 +6928,9 @@ msgstr "Diese E-Mail wird an den Empfänger gesendet, der das Dokument gerade un
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
msgstr "Dieses Feld kann nicht geändert oder gelöscht werden. Wenn Sie den direkten Link dieser Vorlage teilen oder zu Ihrem öffentlichen Profil hinzufügen, kann jeder, der darauf zugreift, seinen Namen und seine E-Mail-Adresse eingeben und die ihm zugewiesenen Felder ausfüllen."
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "This folder name is already taken."
msgstr "Dieser Ordnername ist bereits vergeben."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "This folder contains multiple items. Deleting it will also delete all items in the folder, including nested folders and their contents."
msgstr "Dieser Ordner enthält mehrere Elemente. Wenn Sie ihn löschen, werden auch alle Elemente im Ordner gelöscht, einschließlich verschachtelter Ordner und deren Inhalt."
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "This is how the document will reach the recipients once the document is ready for signing."
@ -6784,10 +6940,6 @@ msgstr "So wird das Dokument die Empfänger erreichen, sobald es zum Unterschrei
msgid "This is the claim that this organisation was initially created with. Any feature flag changes to this claim will be backported into this organisation."
msgstr "Dies ist der Anspruch, mit dem diese Organisation ursprünglich erstellt wurde. Alle Änderungen an Funktionsflags für diesen Anspruch werden in diese Organisation übernommen."
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
msgid "This link is invalid or has expired."
msgstr "Dieser Link ist ungültig oder abgelaufen."
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
msgstr "Dieser Link ist ungültig oder abgelaufen. Bitte kontaktieren Sie Ihr Team, um eine neue Bestätigungsanfrage zu senden."
@ -6858,6 +7010,10 @@ msgstr "Dies wird an den Dokumenteneigentümer gesendet, sobald das Dokument vol
msgid "This will ONLY backport feature flags which are set to true, anything disabled in the initial claim will not be backported"
msgstr "Diese werden NUR Funktionsflags zurückspielen, die auf wahr gesetzt sind; alles, was im ursprünglichen Anspruch deaktiviert ist, wird nicht zurückportiert."
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "This will sign you out of all other devices. You will need to sign in again on those devices to continue using your account."
msgstr "Dies meldet Sie auf allen anderen Geräten ab. Sie müssen sich erneut auf diesen Geräten anmelden, um Ihr Konto weiter zu nutzen."
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
#: apps/remix/app/components/tables/document-logs-table.tsx
msgid "Time"
@ -7144,6 +7300,9 @@ msgstr "Unvollendet"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Unknown"
msgstr "Unbekannt"
@ -7155,6 +7314,10 @@ msgstr "Unbegrenzt"
msgid "Unlimited documents, API and more"
msgstr "Unbegrenzte Dokumente, API und mehr"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Unpin"
msgstr "Lösen"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Untitled Group"
msgstr "Unbetitelte Gruppe"
@ -7497,6 +7660,11 @@ msgstr "Alle verwandten Dokumente anzeigen"
msgid "View all security activity related to your account."
msgstr "Sehen Sie sich alle Sicherheitsaktivitäten in Ihrem Konto an."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "View and manage all active sessions for your account."
msgstr "Alle aktiven Sitzungen Ihres Kontos anzeigen und verwalten."
#: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx
msgid "View Codes"
msgstr "Codes ansehen"
@ -7855,7 +8023,6 @@ msgstr "Wir werden Unterzeichnungslinks für Sie erstellen, die Sie an die Empf
msgid "We won't send anything to notify recipients."
msgstr "Wir werden nichts senden, um die Empfänger zu benachrichtigen."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/components/tables/documents-table-empty-state.tsx
msgid "We're all empty"
@ -8215,7 +8382,6 @@ msgstr "Du hast das Dokument {0} initiiert, das erfordert, dass du {recipientAct
msgid "You have no webhooks yet. Your webhooks will be shown here once you create them."
msgstr "Sie haben noch keine Webhooks. Ihre Webhooks werden hier angezeigt, sobald Sie sie erstellt haben."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
msgid "You have not yet created any templates. To create a template please upload one."
msgstr "Sie haben noch keine Vorlagen erstellt. Bitte laden Sie eine Datei hoch, um eine Vorlage zu erstellen."
@ -8317,7 +8483,6 @@ msgstr "Sie haben Ihre E-Mail-Adresse für <0>{0}</0> bestätigt."
msgid "You must enter '{deleteMessage}' to proceed"
msgstr "Sie müssen '{deleteMessage}' eingeben, um fortzufahren"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "You must type '{deleteMessage}' to confirm"
msgstr "Sie müssen '{deleteMessage}' eingeben, um zu bestätigen"

View File

@ -67,6 +67,16 @@ msgstr "{0, plural, one {(1 character over)} other {(# characters over)}}"
msgid "{0, plural, one {# character over the limit} other {# characters over the limit}}"
msgstr "{0, plural, one {# character over the limit} other {# characters over the limit}}"
#. placeholder {0}: folder._count.documents
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# document} other {# documents}}"
msgstr "{0, plural, one {# document} other {# documents}}"
#. placeholder {0}: folder._count.subfolders
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# folder} other {# folders}}"
msgstr "{0, plural, one {# folder} other {# folders}}"
#. placeholder {0}: template.recipients.length
#: apps/remix/app/routes/_recipient+/d.$token+/_index.tsx
msgid "{0, plural, one {# recipient} other {# recipients}}"
@ -77,6 +87,11 @@ msgstr "{0, plural, one {# recipient} other {# recipients}}"
msgid "{0, plural, one {# team} other {# teams}}"
msgstr "{0, plural, one {# team} other {# teams}}"
#. placeholder {0}: folder._count.templates
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# template} other {# templates}}"
msgstr "{0, plural, one {# template} other {# templates}}"
#. placeholder {0}: data.length
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
@ -293,6 +308,10 @@ msgstr "{prefix} updated the document title"
msgid "{prefix} updated the document visibility"
msgstr "{prefix} updated the document visibility"
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} viewed the document"
msgstr "{prefix} viewed the document"
#: apps/remix/app/components/general/direct-template/direct-template-page.tsx
msgid "{recipientActionVerb} document"
msgstr "{recipientActionVerb} document"
@ -747,6 +766,11 @@ msgstr "Actions"
msgid "Active"
msgstr "Active"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Active sessions"
msgstr "Active sessions"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Active Subscriptions"
msgstr "Active Subscriptions"
@ -812,13 +836,13 @@ msgstr "Add Fields"
msgid "Add group roles"
msgstr "Add group roles"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add groups"
msgstr "Add groups"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add members"
msgstr "Add members"
@ -1238,17 +1262,14 @@ msgstr "An unexpected error occurred."
msgid "An unknown error occurred"
msgstr "An unknown error occurred"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "An unknown error occurred while creating the folder."
msgstr "An unknown error occurred while creating the folder."
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "An unknown error occurred while deleting the folder."
msgstr "An unknown error occurred while deleting the folder."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "An unknown error occurred while moving the folder."
msgstr "An unknown error occurred while moving the folder."
@ -1317,6 +1338,10 @@ msgstr "Are you sure you want to complete the document? This action cannot be un
msgid "Are you sure you want to delete the following claim?"
msgstr "Are you sure you want to delete the following claim?"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Are you sure you want to delete this folder?"
msgstr "Are you sure you want to delete this folder?"
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
msgid "Are you sure you want to delete this token?"
msgstr "Are you sure you want to delete this token?"
@ -1609,6 +1634,7 @@ msgstr "Can prepare"
#: apps/remix/app/components/dialogs/team-delete-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
@ -1621,6 +1647,9 @@ msgstr "Can prepare"
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
@ -1737,7 +1766,6 @@ msgstr "Click here to get started"
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
#: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Click here to retry"
msgstr "Click here to retry"
@ -1916,7 +1944,6 @@ msgstr "Confirm by typing <0>{deleteMessage}</0>"
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Confirm by typing: <0>{deleteMessage}</0>"
msgstr "Confirm by typing: <0>{deleteMessage}</0>"
@ -2060,6 +2087,7 @@ msgstr "Copy token"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
msgid "Create"
msgstr "Create"
@ -2129,6 +2157,14 @@ msgstr "Create Direct Signing Link"
msgid "Create document from template"
msgstr "Create document from template"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Create folder"
msgstr "Create folder"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create Folder"
msgstr "Create Folder"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Create group"
@ -2138,6 +2174,10 @@ msgstr "Create group"
msgid "Create Groups"
msgstr "Create Groups"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create New Folder"
msgstr "Create New Folder"
#: apps/remix/app/routes/_profile+/_layout.tsx
msgid "Create now"
msgstr "Create now"
@ -2216,6 +2256,7 @@ msgstr "Create your account and start using state-of-the-art document signing."
msgid "Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp."
msgstr "Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
@ -2255,6 +2296,14 @@ msgstr "Created on {0}"
msgid "CSV Structure"
msgstr "CSV Structure"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Cumulative MAU (signed in)"
msgstr "Cumulative MAU (signed in)"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Current"
msgstr "Current"
#: apps/remix/app/components/forms/password.tsx
msgid "Current Password"
msgstr "Current Password"
@ -2337,6 +2386,7 @@ msgstr "delete"
#: apps/remix/app/components/tables/organisation-groups-table.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/admin-claims-table.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
@ -2348,6 +2398,7 @@ msgstr "delete"
#: apps/remix/app/components/dialogs/organisation-group-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Delete"
@ -2355,10 +2406,9 @@ msgstr "Delete"
#. placeholder {0}: webhook.webhookUrl
#. placeholder {0}: token.name
#. placeholder {0}: folder?.name ?? 'folder'
#. placeholder {0}: organisation.name
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "delete {0}"
@ -2390,6 +2440,10 @@ msgstr "Delete document"
msgid "Delete Document"
msgstr "Delete Document"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Delete Folder"
msgstr "Delete Folder"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
msgid "Delete organisation"
msgstr "Delete organisation"
@ -2448,6 +2502,7 @@ msgid "Details"
msgstr "Details"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/settings-security-activity-table.tsx
msgid "Device"
msgstr "Device"
@ -2692,11 +2747,6 @@ msgstr "Document external ID updated"
msgid "Document found in your account"
msgstr "Document found in your account"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Document history"
msgstr "Document history"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx
msgid "Document ID"
@ -2815,6 +2865,10 @@ msgstr "Document upload disabled due to unpaid invoices"
msgid "Document uploaded"
msgstr "Document uploaded"
#: packages/lib/utils/document-audit-logs.ts
msgid "Document viewed"
msgstr "Document viewed"
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
msgid "Document Viewed"
msgstr "Document Viewed"
@ -2828,7 +2882,6 @@ msgid "Document will be permanently deleted"
msgstr "Document will be permanently deleted"
#: apps/remix/app/routes/_profile+/p.$url.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
@ -3134,6 +3187,10 @@ msgstr "Enabling the account results in the user being able to use the account a
msgid "Enclosed Document"
msgstr "Enclosed Document"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Enter a name for your new folder. Folders help you organise your items."
#: apps/remix/app/components/forms/subscription-claim-form.tsx
msgid "Enter claim name"
msgstr "Enter claim name"
@ -3174,6 +3231,7 @@ msgstr "Enterprise"
#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
@ -3216,6 +3274,7 @@ msgstr "Enterprise"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx
@ -3260,8 +3319,7 @@ msgstr "Expires on {0}"
msgid "External ID"
msgstr "External ID"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Failed to create folder"
msgstr "Failed to create folder"
@ -3269,6 +3327,10 @@ msgstr "Failed to create folder"
msgid "Failed to create subscription claim."
msgstr "Failed to create subscription claim."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Failed to delete folder"
msgstr "Failed to delete folder"
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Failed to delete subscription claim."
msgstr "Failed to delete subscription claim."
@ -3277,14 +3339,26 @@ msgstr "Failed to delete subscription claim."
msgid "Failed to load document"
msgstr "Failed to load document"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Failed to move folder"
msgstr "Failed to move folder"
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Failed to reseal document"
msgstr "Failed to reseal document"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Failed to revoke session"
msgstr "Failed to revoke session"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Failed to save settings."
msgstr "Failed to save settings."
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Failed to sign out all sessions"
msgstr "Failed to sign out all sessions"
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
msgid "Failed to update document"
msgstr "Failed to update document"
@ -3381,16 +3455,28 @@ msgstr "Fill in the details to create a new subscription claim."
msgid "Folder"
msgstr "Folder"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder created successfully"
msgstr "Folder created successfully"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder deleted successfully"
msgstr "Folder deleted successfully"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Folder moved successfully"
msgstr "Folder moved successfully"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder Name"
msgstr "Folder Name"
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder not found"
msgstr "Folder not found"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
msgid "Folder updated successfully"
msgstr "Folder updated successfully"
@ -3623,14 +3709,17 @@ msgstr "Hi, {userName} <0>({userEmail})</0>"
msgid "Hide"
msgstr "Hide"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Hide additional information"
msgstr "Hide additional information"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Home"
msgstr "Home"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Home (No Folder)"
msgstr "Home (No Folder)"
#: packages/lib/constants/recipient-roles.ts
msgid "I am a signer of this document"
msgstr "I am a signer of this document"
@ -3742,7 +3831,6 @@ msgstr "Invalid code. Please try again."
msgid "Invalid email"
msgstr "Invalid email"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "Invalid link"
msgstr "Invalid link"
@ -3806,6 +3894,7 @@ msgid "Invoice"
msgstr "Invoice"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
msgid "IP Address"
msgstr "IP Address"
@ -3876,6 +3965,10 @@ msgstr "Last 30 days"
msgid "Last 7 days"
msgstr "Last 7 days"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Last Active"
msgstr "Last Active"
#: apps/remix/app/components/general/template/template-page-view-information.tsx
#: apps/remix/app/components/general/document/document-page-view-information.tsx
msgid "Last modified"
@ -4025,6 +4118,10 @@ msgstr "Manage passkeys"
msgid "Manage permissions and access controls"
msgstr "Manage permissions and access controls"
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Manage sessions"
msgstr "Manage sessions"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage subscription"
@ -4035,10 +4132,10 @@ msgstr "Manage subscription"
msgid "Manage the {0} organisation"
msgstr "Manage the {0} organisation"
#. placeholder {0}: organisation.name
#. placeholder {1}: organisation.name
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage the {0} organisation subscription"
msgstr "Manage the {0} organisation subscription"
msgid "Manage the {1} organisation subscription"
msgstr "Manage the {1} organisation subscription"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx
msgid "Manage the custom groups of members for your organisation."
@ -4102,6 +4199,10 @@ msgstr "MAU (created document)"
msgid "MAU (had document completed)"
msgstr "MAU (had document completed)"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "MAU (signed in)"
msgstr "MAU (signed in)"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
msgid "Max"
msgstr "Max"
@ -4172,7 +4273,9 @@ msgstr "Monthly Active Users: Users that created at least one Document"
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Monthly Active Users: Users that had at least one of their documents completed"
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Move"
msgstr "Move"
@ -4185,6 +4288,10 @@ msgstr "Move \"{templateTitle}\" to a folder"
msgid "Move Document to Folder"
msgstr "Move Document to Folder"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Move Folder"
msgstr "Move Folder"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
msgid "Move Template to Folder"
msgstr "Move Template to Folder"
@ -4198,6 +4305,10 @@ msgstr "Move to Folder"
msgid "Multiple access methods can be selected."
msgstr "Multiple access methods can be selected."
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "My Folder"
msgstr "My Folder"
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
@ -4286,6 +4397,16 @@ msgstr "No"
msgid "No active drafts"
msgstr "No active drafts"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "No folders found"
msgstr "No folders found"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
msgid "No folders found matching \"{searchTerm}\""
msgstr "No folders found matching \"{searchTerm}\""
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
msgid "No further action is required from you at this time."
@ -4595,10 +4716,18 @@ msgstr "Organisations"
msgid "Organisations that the user is a member of."
msgstr "Organisations that the user is a member of."
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your documents"
msgstr "Organise your documents"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Organise your members into groups which can be assigned to teams"
msgstr "Organise your members into groups which can be assigned to teams"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your templates"
msgstr "Organise your templates"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
msgid "Organize your documents and templates"
msgstr "Organize your documents and templates"
@ -4774,6 +4903,10 @@ msgstr "Pick a password"
msgid "Pick any of the following agreements below and start signing to get started"
msgstr "Pick any of the following agreements below and start signing to get started"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Pin"
msgstr "Pin"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@ -5300,7 +5433,6 @@ msgstr "Retention of Documents"
msgid "Retry"
msgstr "Retry"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.decline.$token.tsx
@ -5316,6 +5448,7 @@ msgstr "Return to Home"
msgid "Return to sign in"
msgstr "Return to sign in"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/general/teams/team-email-usage.tsx
msgid "Revoke"
msgstr "Revoke"
@ -5324,6 +5457,12 @@ msgstr "Revoke"
msgid "Revoke access"
msgstr "Revoke access"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Revoke all sessions"
msgstr "Revoke all sessions"
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/components/tables/team-members-table.tsx
#: apps/remix/app/components/tables/team-groups-table.tsx
@ -5343,11 +5482,6 @@ msgstr "Role"
msgid "Roles"
msgstr "Roles"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Root (No Folder)"
msgstr "Root (No Folder)"
#: packages/ui/primitives/data-table-pagination.tsx
msgid "Rows per page"
msgstr "Rows per page"
@ -5399,6 +5533,14 @@ msgstr "Search by organisation ID, name, customer ID or owner email"
msgid "Search documents..."
msgstr "Search documents..."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Search folders..."
msgstr "Search folders..."
#: packages/ui/components/common/language-switcher-dialog.tsx
msgid "Search languages..."
msgstr "Search languages..."
@ -5423,6 +5565,10 @@ msgstr "Security activity"
msgid "Select"
msgstr "Select"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Select a destination for this folder."
msgstr "Select a destination for this folder."
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Select a folder to move this document to."
msgstr "Select a folder to move this document to."
@ -5476,6 +5622,10 @@ msgstr "Select default option"
msgid "Select groups"
msgstr "Select groups"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups of members to add to the team."
msgstr "Select groups of members to add to the team."
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups to add to this team"
msgstr "Select groups to add to this team"
@ -5487,7 +5637,6 @@ msgid "Select members"
msgstr "Select members"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select members or groups of members to add to the team."
msgstr "Select members or groups of members to add to the team."
@ -5597,6 +5746,14 @@ msgstr "Sending..."
msgid "Sent"
msgstr "Sent"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Session revoked"
msgstr "Session revoked"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Sessions have been revoked"
msgstr "Sessions have been revoked"
#: apps/remix/app/components/general/claim-account.tsx
msgid "Set a password"
msgstr "Set a password"
@ -5616,6 +5773,7 @@ msgstr "Set up your template properties and recipient information"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Settings"
msgstr "Settings"
@ -5649,10 +5807,6 @@ msgstr "Share your signing experience!"
msgid "Show"
msgstr "Show"
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Show additional information"
msgstr "Show additional information"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Show advanced settings"
@ -6313,7 +6467,6 @@ msgstr "Template title"
msgid "Template updated successfully"
msgstr "Template updated successfully"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
@ -6444,12 +6597,10 @@ msgstr "The events that will trigger a webhook to be sent to your URL."
msgid "The fields have been updated to the new field insertion method successfully"
msgstr "The fields have been updated to the new field insertion method successfully"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "The folder you are trying to delete does not exist."
msgstr "The folder you are trying to delete does not exist."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "The folder you are trying to move does not exist."
msgstr "The folder you are trying to move does not exist."
@ -6784,10 +6935,9 @@ msgstr "This email will be sent to the recipient who has just signed the documen
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
msgstr "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "This folder name is already taken."
msgstr "This folder name is already taken."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "This folder contains multiple items. Deleting it will also delete all items in the folder, including nested folders and their contents."
msgstr "This folder contains multiple items. Deleting it will also delete all items in the folder, including nested folders and their contents."
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "This is how the document will reach the recipients once the document is ready for signing."
@ -6797,10 +6947,6 @@ msgstr "This is how the document will reach the recipients once the document is
msgid "This is the claim that this organisation was initially created with. Any feature flag changes to this claim will be backported into this organisation."
msgstr "This is the claim that this organisation was initially created with. Any feature flag changes to this claim will be backported into this organisation."
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
msgid "This link is invalid or has expired."
msgstr "This link is invalid or has expired."
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
msgstr "This link is invalid or has expired. Please contact your team to resend a verification."
@ -6871,6 +7017,10 @@ msgstr "This will be sent to the document owner once the document has been fully
msgid "This will ONLY backport feature flags which are set to true, anything disabled in the initial claim will not be backported"
msgstr "This will ONLY backport feature flags which are set to true, anything disabled in the initial claim will not be backported"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "This will sign you out of all other devices. You will need to sign in again on those devices to continue using your account."
msgstr "This will sign you out of all other devices. You will need to sign in again on those devices to continue using your account."
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
#: apps/remix/app/components/tables/document-logs-table.tsx
msgid "Time"
@ -7098,7 +7248,6 @@ msgid "Unable to join this organisation at this time."
msgstr "Unable to join this organisation at this time."
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
#: apps/remix/app/components/general/document/document-history-sheet.tsx
msgid "Unable to load document history"
msgstr "Unable to load document history"
@ -7157,6 +7306,9 @@ msgstr "Uncompleted"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Unknown"
msgstr "Unknown"
@ -7168,6 +7320,10 @@ msgstr "Unlimited"
msgid "Unlimited documents, API and more"
msgstr "Unlimited documents, API and more"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Unpin"
msgstr "Unpin"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Untitled Group"
msgstr "Untitled Group"
@ -7510,6 +7666,11 @@ msgstr "View all related documents"
msgid "View all security activity related to your account."
msgstr "View all security activity related to your account."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "View and manage all active sessions for your account."
msgstr "View and manage all active sessions for your account."
#: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx
msgid "View Codes"
msgstr "View Codes"
@ -7855,11 +8016,8 @@ msgstr "We were unable to verify your email at this time."
msgid "We were unable to verify your email. If your email is not verified already, please try again."
msgstr "We were unable to verify your email. If your email is not verified already, please try again."
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for with you, which you can send to the recipients through your method of choice."
msgstr "We will generate signing links for with you, which you can send to the recipients through your method of choice."
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "We will generate signing links for you, which you can send to the recipients through your method of choice."
msgstr "We will generate signing links for you, which you can send to the recipients through your method of choice."
@ -7868,7 +8026,6 @@ msgstr "We will generate signing links for you, which you can send to the recipi
msgid "We won't send anything to notify recipients."
msgstr "We won't send anything to notify recipients."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/components/tables/documents-table-empty-state.tsx
msgid "We're all empty"
@ -8228,7 +8385,6 @@ msgstr "You have initiated the document {0} that requires you to {recipientActio
msgid "You have no webhooks yet. Your webhooks will be shown here once you create them."
msgstr "You have no webhooks yet. Your webhooks will be shown here once you create them."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
msgid "You have not yet created any templates. To create a template please upload one."
msgstr "You have not yet created any templates. To create a template please upload one."
@ -8330,7 +8486,6 @@ msgstr "You have verified your email address for <0>{0}</0>."
msgid "You must enter '{deleteMessage}' to proceed"
msgstr "You must enter '{deleteMessage}' to proceed"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "You must type '{deleteMessage}' to confirm"
msgstr "You must type '{deleteMessage}' to confirm"

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-06-10 02:27\n"
"PO-Revision-Date: 2025-06-19 06:05\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -72,6 +72,16 @@ msgstr "{0, plural, one {(1 carácter excedido)} other {(# caracteres excedidos)
msgid "{0, plural, one {# character over the limit} other {# characters over the limit}}"
msgstr "{0, plural, one {# carácter sobre el límite} other {# caracteres sobre el límite}}"
#. placeholder {0}: folder._count.documents
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# document} other {# documents}}"
msgstr "{0, plural, one {# documento} other {# documentos}}"
#. placeholder {0}: folder._count.subfolders
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# folder} other {# folders}}"
msgstr "{0, plural, one {# carpeta} other {# carpetas}}"
#. placeholder {0}: template.recipients.length
#: apps/remix/app/routes/_recipient+/d.$token+/_index.tsx
msgid "{0, plural, one {# recipient} other {# recipients}}"
@ -82,6 +92,11 @@ msgstr "{0, plural, one {# destinatario} other {# destinatarios}}"
msgid "{0, plural, one {# team} other {# teams}}"
msgstr "{0, plural, one {# equipo} other {# equipos}}"
#. placeholder {0}: folder._count.templates
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# template} other {# templates}}"
msgstr "{0, plural, one {# plantilla} other {# plantillas}}"
#. placeholder {0}: data.length
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
@ -752,6 +767,11 @@ msgstr "Acciones"
msgid "Active"
msgstr "Activo"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Active sessions"
msgstr "Sesiones activas"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Active Subscriptions"
msgstr "Suscripciones Activas"
@ -817,13 +837,13 @@ msgstr "Agregar Campos"
msgid "Add group roles"
msgstr "Agregar roles de grupo"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add groups"
msgstr "Agregar grupos"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add members"
msgstr "Agregar miembros"
@ -1243,17 +1263,14 @@ msgstr "Ocurrió un error inesperado."
msgid "An unknown error occurred"
msgstr "Ocurrió un error desconocido"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "An unknown error occurred while creating the folder."
msgstr "Se produjo un error desconocido al crear la carpeta."
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "An unknown error occurred while deleting the folder."
msgstr "Se produjo un error desconocido al eliminar la carpeta."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "An unknown error occurred while moving the folder."
msgstr "Se produjo un error desconocido al mover la carpeta."
@ -1322,6 +1339,10 @@ msgstr "¿Estás seguro de que quieres completar el documento? Esta acción no s
msgid "Are you sure you want to delete the following claim?"
msgstr "¿Estás seguro de que quieres eliminar la siguiente solicitud?"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Are you sure you want to delete this folder?"
msgstr "¿Está seguro de que quiere eliminar esta carpeta?"
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
msgid "Are you sure you want to delete this token?"
msgstr "¿Estás seguro de que deseas eliminar este token?"
@ -1614,6 +1635,7 @@ msgstr "Puede preparar"
#: apps/remix/app/components/dialogs/team-delete-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
@ -1626,6 +1648,9 @@ msgstr "Puede preparar"
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
@ -1921,7 +1946,6 @@ msgstr "Confirme escribiendo <0>{deleteMessage}</0>"
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Confirm by typing: <0>{deleteMessage}</0>"
msgstr "Confirme escribiendo: <0>{deleteMessage}</0>"
@ -2065,6 +2089,7 @@ msgstr "Copiar token"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
msgid "Create"
msgstr "Crear"
@ -2134,6 +2159,14 @@ msgstr "Crear enlace de firma directo"
msgid "Create document from template"
msgstr "Crear documento a partir de la plantilla"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Create folder"
msgstr "Crear carpeta"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create Folder"
msgstr "Crear Carpeta"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Create group"
@ -2143,6 +2176,10 @@ msgstr "Crear grupo"
msgid "Create Groups"
msgstr "Crear Grupos"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create New Folder"
msgstr "Crear Nueva Carpeta"
#: apps/remix/app/routes/_profile+/_layout.tsx
msgid "Create now"
msgstr "Crear ahora"
@ -2221,6 +2258,7 @@ msgstr "Crea tu cuenta y comienza a utilizar la firma de documentos de última g
msgid "Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp."
msgstr "Crea tu cuenta y comienza a utilizar la firma de documentos de última generación. La firma abierta y hermosa está al alcance de tu mano."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
@ -2260,6 +2298,14 @@ msgstr "Creado el {0}"
msgid "CSV Structure"
msgstr "Estructura CSV"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Cumulative MAU (signed in)"
msgstr "MAU acumulativo (con sesión iniciada)"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Current"
msgstr "Actual"
#: apps/remix/app/components/forms/password.tsx
msgid "Current Password"
msgstr "Contraseña actual"
@ -2342,6 +2388,7 @@ msgstr "eliminar"
#: apps/remix/app/components/tables/organisation-groups-table.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/admin-claims-table.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
@ -2353,6 +2400,7 @@ msgstr "eliminar"
#: apps/remix/app/components/dialogs/organisation-group-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Delete"
@ -2360,10 +2408,9 @@ msgstr "Eliminar"
#. placeholder {0}: webhook.webhookUrl
#. placeholder {0}: token.name
#. placeholder {0}: folder?.name ?? 'folder'
#. placeholder {0}: organisation.name
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "delete {0}"
@ -2395,6 +2442,10 @@ msgstr "Eliminar documento"
msgid "Delete Document"
msgstr "Eliminar Documento"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Delete Folder"
msgstr "Eliminar Carpeta"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
msgid "Delete organisation"
msgstr "Eliminar organización"
@ -2453,6 +2504,7 @@ msgid "Details"
msgstr "Detalles"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/settings-security-activity-table.tsx
msgid "Device"
msgstr "Dispositivo"
@ -2833,7 +2885,6 @@ msgid "Document will be permanently deleted"
msgstr "El documento será eliminado permanentemente"
#: apps/remix/app/routes/_profile+/p.$url.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
@ -3139,6 +3190,10 @@ msgstr "Habilitar la cuenta permite al usuario usar la cuenta de nuevo, junto co
msgid "Enclosed Document"
msgstr "Documento Adjunto"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Ingrese un nombre para su nueva carpeta. Las carpetas le ayudan a organizar sus elementos."
#: apps/remix/app/components/forms/subscription-claim-form.tsx
msgid "Enter claim name"
msgstr "Ingresar nombre de la reclamación"
@ -3179,6 +3234,7 @@ msgstr "Enterprise"
#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
@ -3221,6 +3277,7 @@ msgstr "Enterprise"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx
@ -3265,8 +3322,7 @@ msgstr "Expira el {0}"
msgid "External ID"
msgstr "ID externo"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Failed to create folder"
msgstr "No se pudo crear la carpeta"
@ -3274,6 +3330,10 @@ msgstr "No se pudo crear la carpeta"
msgid "Failed to create subscription claim."
msgstr "Error al crear reclamación de suscripción."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Failed to delete folder"
msgstr "Error al eliminar la carpeta"
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Failed to delete subscription claim."
msgstr "Error al eliminar la reclamación de suscripción."
@ -3282,14 +3342,26 @@ msgstr "Error al eliminar la reclamación de suscripción."
msgid "Failed to load document"
msgstr "Error al cargar el documento"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Failed to move folder"
msgstr "Error al mover la carpeta"
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Failed to reseal document"
msgstr "Falló al volver a sellar el documento"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Failed to revoke session"
msgstr "Error al revocar la sesión"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Failed to save settings."
msgstr "Fallo al guardar configuraciones."
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Failed to sign out all sessions"
msgstr "Error al cerrar sesión en todas las sesiones"
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
msgid "Failed to update document"
msgstr "No se pudo actualizar el documento"
@ -3386,16 +3458,28 @@ msgstr "Rellena los detalles para crear una nueva reclamación de suscripción."
msgid "Folder"
msgstr "Carpeta"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder created successfully"
msgstr "Carpeta creada exitosamente"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder deleted successfully"
msgstr "Carpeta eliminada correctamente"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Folder moved successfully"
msgstr "Carpeta movida correctamente"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder Name"
msgstr "Nombre de la Carpeta"
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder not found"
msgstr "Carpeta no encontrada"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
msgid "Folder updated successfully"
msgstr "Carpeta actualizada con éxito"
@ -3633,9 +3717,16 @@ msgid "Hide additional information"
msgstr "Ocultar información adicional"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Home"
msgstr "Inicio"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Home (No Folder)"
msgstr "Inicio (Sin Carpeta)"
#: packages/lib/constants/recipient-roles.ts
msgid "I am a signer of this document"
msgstr "Soy un firmante de este documento"
@ -3747,7 +3838,6 @@ msgstr "Código inválido. Por favor, intenta nuevamente."
msgid "Invalid email"
msgstr "Email inválido"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "Invalid link"
msgstr "Enlace inválido"
@ -3811,6 +3901,7 @@ msgid "Invoice"
msgstr "Factura"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
msgid "IP Address"
msgstr "Dirección IP"
@ -3881,6 +3972,10 @@ msgstr "Últimos 30 días"
msgid "Last 7 days"
msgstr "Últimos 7 días"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Last Active"
msgstr "Última actividad"
#: apps/remix/app/components/general/template/template-page-view-information.tsx
#: apps/remix/app/components/general/document/document-page-view-information.tsx
msgid "Last modified"
@ -4030,6 +4125,10 @@ msgstr "Gestionar claves de acceso"
msgid "Manage permissions and access controls"
msgstr "Gestiona permisos y controles de acceso"
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Manage sessions"
msgstr "Gestionar sesiones"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage subscription"
@ -4107,6 +4206,10 @@ msgstr "MAU (documento creado)"
msgid "MAU (had document completed)"
msgstr "MAU (documento completado)"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "MAU (signed in)"
msgstr "MAU (con sesión iniciada)"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
msgid "Max"
msgstr "Máx"
@ -4177,7 +4280,9 @@ msgstr "Usuarios activos mensuales: Usuarios que crearon al menos un documento"
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Usuarios activos mensuales: Usuarios que completaron al menos uno de sus documentos"
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Move"
msgstr "Mover"
@ -4190,6 +4295,10 @@ msgstr "Mover \"{templateTitle}\" a una carpeta"
msgid "Move Document to Folder"
msgstr "Mover Documento a Carpeta"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Move Folder"
msgstr "Mover Carpeta"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
msgid "Move Template to Folder"
msgstr "Mover Plantilla a Carpeta"
@ -4203,6 +4312,10 @@ msgstr "Mover a Carpeta"
msgid "Multiple access methods can be selected."
msgstr "Se pueden seleccionar varios métodos de acceso."
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "My Folder"
msgstr "Mi Carpeta"
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
@ -4291,6 +4404,16 @@ msgstr "No"
msgid "No active drafts"
msgstr "No hay borradores activos"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "No folders found"
msgstr "No se encontraron carpetas"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
msgid "No folders found matching \"{searchTerm}\""
msgstr "No se encontraron carpetas que coincidan con \"{searchTerm}\""
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
msgid "No further action is required from you at this time."
@ -4600,10 +4723,18 @@ msgstr "Organizaciones"
msgid "Organisations that the user is a member of."
msgstr "Organizaciones de las que el usuario es miembro."
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your documents"
msgstr "Organiza tus documentos"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Organise your members into groups which can be assigned to teams"
msgstr "Organiza a tus miembros en grupos que se puedan asignar a equipos"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your templates"
msgstr "Organiza tus plantillas"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
msgid "Organize your documents and templates"
msgstr "Organiza tus documentos y plantillas"
@ -4779,6 +4910,10 @@ msgstr "Elige una contraseña"
msgid "Pick any of the following agreements below and start signing to get started"
msgstr "Elige cualquiera de los siguientes acuerdos a continuación y comience a firmar para empezar"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Pin"
msgstr "Fijar"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@ -5305,7 +5440,6 @@ msgstr "Retención de Documentos"
msgid "Retry"
msgstr "Reintentar"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.decline.$token.tsx
@ -5321,6 +5455,7 @@ msgstr "Regresar a la página de inicio"
msgid "Return to sign in"
msgstr "Regresar para iniciar sesión"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/general/teams/team-email-usage.tsx
msgid "Revoke"
msgstr "Revocar"
@ -5329,6 +5464,12 @@ msgstr "Revocar"
msgid "Revoke access"
msgstr "Revocar acceso"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Revoke all sessions"
msgstr "Revocar todas las sesiones"
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/components/tables/team-members-table.tsx
#: apps/remix/app/components/tables/team-groups-table.tsx
@ -5348,11 +5489,6 @@ msgstr "Rol"
msgid "Roles"
msgstr "Roles"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Root (No Folder)"
msgstr "Raíz (Sin Carpeta)"
#: packages/ui/primitives/data-table-pagination.tsx
msgid "Rows per page"
msgstr "Filas por página"
@ -5404,6 +5540,14 @@ msgstr "Buscar por ID de organización, nombre, ID de cliente o correo electrón
msgid "Search documents..."
msgstr "Buscar documentos..."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Search folders..."
msgstr "Buscar carpetas..."
#: packages/ui/components/common/language-switcher-dialog.tsx
msgid "Search languages..."
msgstr "Buscar idiomas..."
@ -5428,6 +5572,10 @@ msgstr "Actividad de seguridad"
msgid "Select"
msgstr "Seleccionar"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Select a destination for this folder."
msgstr "Selecciona un destino para esta carpeta."
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Select a folder to move this document to."
msgstr "Selecciona una carpeta para mover este documento."
@ -5481,6 +5629,10 @@ msgstr "Seleccionar opción predeterminada"
msgid "Select groups"
msgstr "Seleccionar grupos"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups of members to add to the team."
msgstr "Seleccionar grupos de miembros para añadir al equipo."
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups to add to this team"
msgstr "Seleccionar grupos para añadir a este equipo"
@ -5492,7 +5644,6 @@ msgid "Select members"
msgstr "Seleccione miembros"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select members or groups of members to add to the team."
msgstr "Seleccione miembros o grupos de miembros para agregar al equipo."
@ -5602,6 +5753,14 @@ msgstr "Enviando..."
msgid "Sent"
msgstr "Enviado"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Session revoked"
msgstr "Sesión revocada"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Sessions have been revoked"
msgstr "Las sesiones han sido revocadas"
#: apps/remix/app/components/general/claim-account.tsx
msgid "Set a password"
msgstr "Establecer una contraseña"
@ -5621,6 +5780,7 @@ msgstr "Configura las propiedades de tu plantilla y la información del destinat
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Settings"
msgstr "Configuraciones"
@ -6318,7 +6478,6 @@ msgstr "Título de plantilla"
msgid "Template updated successfully"
msgstr "Plantilla actualizada con éxito"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
@ -6449,12 +6608,10 @@ msgstr "Los eventos que activarán un webhook para ser enviado a tu URL."
msgid "The fields have been updated to the new field insertion method successfully"
msgstr "Los campos se han actualizado al nuevo método de inserción de campo con éxito"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "The folder you are trying to delete does not exist."
msgstr "La carpeta que intenta eliminar no existe."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "The folder you are trying to move does not exist."
msgstr "La carpeta que intenta mover no existe."
@ -6772,10 +6929,9 @@ msgstr "Este correo electrónico se enviará al destinatario que acaba de firmar
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
msgstr "Este campo no se puede modificar ni eliminar. Cuando comparta el enlace directo de esta plantilla o lo agregue a su perfil público, cualquiera que acceda podrá ingresar su nombre y correo electrónico, y completar los campos que se le hayan asignado."
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "This folder name is already taken."
msgstr "Este nombre de carpeta ya está en uso."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "This folder contains multiple items. Deleting it will also delete all items in the folder, including nested folders and their contents."
msgstr "Esta carpeta contiene múltiples elementos. Eliminándola también se eliminarán todos los elementos de la carpeta, incluidas las carpetas anidadas y sus contenidos."
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "This is how the document will reach the recipients once the document is ready for signing."
@ -6785,10 +6941,6 @@ msgstr "Así es como el documento llegará a los destinatarios una vez que esté
msgid "This is the claim that this organisation was initially created with. Any feature flag changes to this claim will be backported into this organisation."
msgstr "Esta es la reclamo con la que se creó inicialmente esta organización. Cualquier cambio en la bandera de características de esta reclamo se retroalimentará en esta organización."
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
msgid "This link is invalid or has expired."
msgstr "Este enlace no es válido o ha expirado."
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
msgstr "Este enlace es inválido o ha expirado. Por favor, contacta a tu equipo para reenviar una verificación."
@ -6859,6 +7011,10 @@ msgstr "Esto se enviará al propietario del documento una vez que el documento s
msgid "This will ONLY backport feature flags which are set to true, anything disabled in the initial claim will not be backported"
msgstr "Esto solo retroalimentará las banderas de características que estén configuradas como verdaderas, cualquier cosa desactivada en la reclamo inicial no será retroalimentada"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "This will sign you out of all other devices. You will need to sign in again on those devices to continue using your account."
msgstr "Esto cerrará la sesión en todos los demás dispositivos. Necesitarás iniciar sesión nuevamente en esos dispositivos para continuar usando tu cuenta."
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
#: apps/remix/app/components/tables/document-logs-table.tsx
msgid "Time"
@ -7145,6 +7301,9 @@ msgstr "Incompleto"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Unknown"
msgstr "Desconocido"
@ -7156,6 +7315,10 @@ msgstr "Ilimitado"
msgid "Unlimited documents, API and more"
msgstr "Documentos ilimitados, API y más"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Unpin"
msgstr "Desanclar"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Untitled Group"
msgstr "Grupo sin título"
@ -7498,6 +7661,11 @@ msgstr "Ver todos los documentos relacionados"
msgid "View all security activity related to your account."
msgstr "Ver toda la actividad de seguridad relacionada con tu cuenta."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "View and manage all active sessions for your account."
msgstr "Ver y gestionar todas las sesiones activas de tu cuenta."
#: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx
msgid "View Codes"
msgstr "Ver Códigos"
@ -7856,7 +8024,6 @@ msgstr "Generaremos enlaces de firma para ti, que podrás enviar a los destinata
msgid "We won't send anything to notify recipients."
msgstr "No enviaremos nada para notificar a los destinatarios."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/components/tables/documents-table-empty-state.tsx
msgid "We're all empty"
@ -8216,7 +8383,6 @@ msgstr "Has iniciado el documento {0} que requiere que {recipientActionVerb}."
msgid "You have no webhooks yet. Your webhooks will be shown here once you create them."
msgstr "Aún no tienes webhooks. Tus webhooks se mostrarán aquí una vez que los crees."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
msgid "You have not yet created any templates. To create a template please upload one."
msgstr "Aún no has creado plantillas. Para crear una plantilla, por favor carga una."
@ -8318,7 +8484,6 @@ msgstr "Has verificado tu dirección de correo electrónico para <0>{0}</0>."
msgid "You must enter '{deleteMessage}' to proceed"
msgstr "Debes ingresar '{deleteMessage}' para continuar"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "You must type '{deleteMessage}' to confirm"
msgstr "Debes escribir '{deleteMessage}' para confirmar"

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-06-10 02:27\n"
"PO-Revision-Date: 2025-06-19 06:05\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@ -72,6 +72,16 @@ msgstr "{0, plural, one {(1 caractère de trop)} other {(# caractères de trop)}
msgid "{0, plural, one {# character over the limit} other {# characters over the limit}}"
msgstr "{0, plural, one {# caractère au-dessus de la limite} other {# caractères au-dessus de la limite}}"
#. placeholder {0}: folder._count.documents
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# document} other {# documents}}"
msgstr "{0, plural, one {# document} other {# documents}}"
#. placeholder {0}: folder._count.subfolders
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# folder} other {# folders}}"
msgstr "{0, plural, one {# dossier} other {# dossiers}}"
#. placeholder {0}: template.recipients.length
#: apps/remix/app/routes/_recipient+/d.$token+/_index.tsx
msgid "{0, plural, one {# recipient} other {# recipients}}"
@ -82,6 +92,11 @@ msgstr "{0, plural, one {# destinataire} other {# destinataires}}"
msgid "{0, plural, one {# team} other {# teams}}"
msgstr "{0, plural, one {# équipe} other {# équipes}}"
#. placeholder {0}: folder._count.templates
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# template} other {# templates}}"
msgstr "{0, plural, one {# modèle} other {# modèles}}"
#. placeholder {0}: data.length
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
@ -752,6 +767,11 @@ msgstr "Actions"
msgid "Active"
msgstr "Actif"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Active sessions"
msgstr "Sessions actives"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Active Subscriptions"
msgstr "Abonnements actifs"
@ -817,13 +837,13 @@ msgstr "Ajouter des champs"
msgid "Add group roles"
msgstr "Ajouter des rôles de groupe"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add groups"
msgstr "Ajouter des groupes"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add members"
msgstr "Ajouter des membres"
@ -1243,17 +1263,14 @@ msgstr "Une erreur inattendue est survenue."
msgid "An unknown error occurred"
msgstr "Une erreur inconnue est survenue"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "An unknown error occurred while creating the folder."
msgstr "Une erreur inconnue s'est produite lors de la création du dossier."
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "An unknown error occurred while deleting the folder."
msgstr "Une erreur inconnue s'est produite lors de la suppression du dossier."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "An unknown error occurred while moving the folder."
msgstr "Une erreur inconnue s'est produite lors du déplacement du dossier."
@ -1322,6 +1339,10 @@ msgstr "Êtes-vous sûr de vouloir terminer le document ? Cette action ne peut
msgid "Are you sure you want to delete the following claim?"
msgstr "Êtes-vous sûr de vouloir supprimer la réclamation suivante?"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Are you sure you want to delete this folder?"
msgstr "Êtes-vous sûr de vouloir supprimer ce dossier ?"
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
msgid "Are you sure you want to delete this token?"
msgstr "Êtes-vous sûr de vouloir supprimer ce token ?"
@ -1614,6 +1635,7 @@ msgstr "Peut préparer"
#: apps/remix/app/components/dialogs/team-delete-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
@ -1626,6 +1648,9 @@ msgstr "Peut préparer"
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
@ -1921,7 +1946,6 @@ msgstr "Confirmer en tapant <0>{deleteMessage}</0>"
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Confirm by typing: <0>{deleteMessage}</0>"
msgstr "Confirmer en tapant : <0>{deleteMessage}</0>"
@ -2065,6 +2089,7 @@ msgstr "Copier le token"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
msgid "Create"
msgstr "Créer"
@ -2134,6 +2159,14 @@ msgstr "Créer un lien de signature directe"
msgid "Create document from template"
msgstr "Créer un document à partir du modèle"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Create folder"
msgstr "Créer un dossier"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create Folder"
msgstr "Créer un Dossier"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Create group"
@ -2143,6 +2176,10 @@ msgstr "Créer un groupe"
msgid "Create Groups"
msgstr "Créer des groupes"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create New Folder"
msgstr "Créer un Nouveau Dossier"
#: apps/remix/app/routes/_profile+/_layout.tsx
msgid "Create now"
msgstr "Créer maintenant"
@ -2221,6 +2258,7 @@ msgstr "Créez votre compte et commencez à utiliser la signature de documents
msgid "Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp."
msgstr "Créez votre compte et commencez à utiliser la signature de documents à la pointe de la technologie. Une signature ouverte et magnifique est à votre portée."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
@ -2260,6 +2298,14 @@ msgstr "Créé le {0}"
msgid "CSV Structure"
msgstr "Structure CSV"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Cumulative MAU (signed in)"
msgstr "MAU cumulatif (connecté)"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Current"
msgstr "Actuel"
#: apps/remix/app/components/forms/password.tsx
msgid "Current Password"
msgstr "Mot de passe actuel"
@ -2342,6 +2388,7 @@ msgstr "supprimer"
#: apps/remix/app/components/tables/organisation-groups-table.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/admin-claims-table.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
@ -2353,6 +2400,7 @@ msgstr "supprimer"
#: apps/remix/app/components/dialogs/organisation-group-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Delete"
@ -2360,10 +2408,9 @@ msgstr "Supprimer"
#. placeholder {0}: webhook.webhookUrl
#. placeholder {0}: token.name
#. placeholder {0}: folder?.name ?? 'folder'
#. placeholder {0}: organisation.name
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "delete {0}"
@ -2395,6 +2442,10 @@ msgstr "Supprimer le document"
msgid "Delete Document"
msgstr "Supprimer le document"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Delete Folder"
msgstr "Supprimer le Dossier"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
msgid "Delete organisation"
msgstr "Supprimer l'organisation"
@ -2453,6 +2504,7 @@ msgid "Details"
msgstr "Détails"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/settings-security-activity-table.tsx
msgid "Device"
msgstr "Appareil"
@ -2833,7 +2885,6 @@ msgid "Document will be permanently deleted"
msgstr "Le document sera supprimé de manière permanente"
#: apps/remix/app/routes/_profile+/p.$url.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
@ -3139,6 +3190,10 @@ msgstr "Activer le compte permet à l'utilisateur de pouvoir utiliser le compte
msgid "Enclosed Document"
msgstr "Document joint"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Entrez un nom pour votre nouveau dossier. Les dossiers vous aident à organiser vos éléments."
#: apps/remix/app/components/forms/subscription-claim-form.tsx
msgid "Enter claim name"
msgstr "Entrez le nom de la réclamation"
@ -3179,6 +3234,7 @@ msgstr "Entreprise"
#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
@ -3221,6 +3277,7 @@ msgstr "Entreprise"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx
@ -3265,8 +3322,7 @@ msgstr "Expire le {0}"
msgid "External ID"
msgstr "ID externe"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Failed to create folder"
msgstr "Échec de la création du dossier"
@ -3274,6 +3330,10 @@ msgstr "Échec de la création du dossier"
msgid "Failed to create subscription claim."
msgstr "Échec de la création de la réclamation d'abonnement."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Failed to delete folder"
msgstr "Échec de la suppression du dossier"
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Failed to delete subscription claim."
msgstr "Échec de la suppression de la réclamation d'abonnement."
@ -3282,14 +3342,26 @@ msgstr "Échec de la suppression de la réclamation d'abonnement."
msgid "Failed to load document"
msgstr "Échec du chargement du document"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Failed to move folder"
msgstr "Échec du déplacement du dossier"
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Failed to reseal document"
msgstr "Échec du reseal du document"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Failed to revoke session"
msgstr "Échec de la révocation de la session"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Failed to save settings."
msgstr "Échec de l'enregistrement des paramètres."
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Failed to sign out all sessions"
msgstr "Impossible de se déconnecter de toutes les sessions"
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
msgid "Failed to update document"
msgstr "Échec de la mise à jour du document"
@ -3386,16 +3458,28 @@ msgstr "Remplissez les détails pour créer une nouvelle réclamation d'abonneme
msgid "Folder"
msgstr "Dossier"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder created successfully"
msgstr "Dossier créé avec succès"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder deleted successfully"
msgstr "Dossier supprimé avec succès"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Folder moved successfully"
msgstr "Dossier déplacé avec succès"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder Name"
msgstr "Nom du Dossier"
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder not found"
msgstr "Dossier introuvable"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
msgid "Folder updated successfully"
msgstr "Dossier mis à jour avec succès"
@ -3633,9 +3717,16 @@ msgid "Hide additional information"
msgstr "Cacher des informations supplémentaires"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Home"
msgstr "Accueil"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Home (No Folder)"
msgstr "Accueil (Pas de Dossier)"
#: packages/lib/constants/recipient-roles.ts
msgid "I am a signer of this document"
msgstr "Je suis un signataire de ce document"
@ -3747,7 +3838,6 @@ msgstr "Code invalide. Veuillez réessayer."
msgid "Invalid email"
msgstr "Email invalide"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "Invalid link"
msgstr "Lien invalide"
@ -3811,6 +3901,7 @@ msgid "Invoice"
msgstr "Facture"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
msgid "IP Address"
msgstr "Adresse IP"
@ -3881,6 +3972,10 @@ msgstr "30 derniers jours"
msgid "Last 7 days"
msgstr "7 derniers jours"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Last Active"
msgstr "Dernière activité"
#: apps/remix/app/components/general/template/template-page-view-information.tsx
#: apps/remix/app/components/general/document/document-page-view-information.tsx
msgid "Last modified"
@ -4030,6 +4125,10 @@ msgstr "Gérer les clés d'accès"
msgid "Manage permissions and access controls"
msgstr "Gérez les autorisations et les contrôles d'accès"
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Manage sessions"
msgstr "Gérer les sessions"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage subscription"
@ -4107,6 +4206,10 @@ msgstr "MAU (document créé)"
msgid "MAU (had document completed)"
msgstr "MAU (document terminé)"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "MAU (signed in)"
msgstr "MAU (connecté)"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
msgid "Max"
msgstr "Maximum"
@ -4177,7 +4280,9 @@ msgstr "Utilisateurs actifs mensuels : utilisateurs ayant créé au moins un doc
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Utilisateurs actifs mensuels : utilisateurs ayant terminé au moins un de leurs documents"
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Move"
msgstr "Déplacer"
@ -4190,6 +4295,10 @@ msgstr "Déplacer «{templateTitle}» vers un dossier"
msgid "Move Document to Folder"
msgstr "Déplacer le document vers un dossier"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Move Folder"
msgstr "Déplacer le Dossier"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
msgid "Move Template to Folder"
msgstr "Déplacer le modèle vers un dossier"
@ -4203,6 +4312,10 @@ msgstr "Déplacer vers le dossier"
msgid "Multiple access methods can be selected."
msgstr "Plusieurs méthodes d'accès peuvent être sélectionnées."
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "My Folder"
msgstr "Mon Dossier"
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
@ -4291,6 +4404,16 @@ msgstr "Non"
msgid "No active drafts"
msgstr "Pas de brouillons actifs"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "No folders found"
msgstr "Aucun dossier trouvé"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
msgid "No folders found matching \"{searchTerm}\""
msgstr "Aucun dossier correspondant à \"{searchTerm}\" trouvé"
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
msgid "No further action is required from you at this time."
@ -4600,10 +4723,18 @@ msgstr "Organisations"
msgid "Organisations that the user is a member of."
msgstr "Organisations dont l'utilisateur est membre."
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your documents"
msgstr "Organisez vos documents"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Organise your members into groups which can be assigned to teams"
msgstr "Organisez vos membres en groupes qui peuvent être assignés à des équipes"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your templates"
msgstr "Organisez vos modèles"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
msgid "Organize your documents and templates"
msgstr "Organisez vos documents et modèles"
@ -4779,6 +4910,10 @@ msgstr "Choisir un mot de passe"
msgid "Pick any of the following agreements below and start signing to get started"
msgstr "Choisissez l'un des accords suivants ci-dessous et commencez à signer pour commencer"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Pin"
msgstr "Épingler"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@ -5305,7 +5440,6 @@ msgstr "Conservation des documents"
msgid "Retry"
msgstr "Réessayer"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.decline.$token.tsx
@ -5321,6 +5455,7 @@ msgstr "Retour à l'accueil"
msgid "Return to sign in"
msgstr "Retour à la connexion"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/general/teams/team-email-usage.tsx
msgid "Revoke"
msgstr "Révoquer"
@ -5329,6 +5464,12 @@ msgstr "Révoquer"
msgid "Revoke access"
msgstr "Révoquer l'accès"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Revoke all sessions"
msgstr "Révoquer toutes les sessions"
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/components/tables/team-members-table.tsx
#: apps/remix/app/components/tables/team-groups-table.tsx
@ -5348,11 +5489,6 @@ msgstr "Rôle"
msgid "Roles"
msgstr "Rôles"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Root (No Folder)"
msgstr "Racine (Pas de dossier)"
#: packages/ui/primitives/data-table-pagination.tsx
msgid "Rows per page"
msgstr "Lignes par page"
@ -5404,6 +5540,14 @@ msgstr "Rechercher par ID d'organisation, nom, ID client ou e-mail du propriéta
msgid "Search documents..."
msgstr "Rechercher des documents..."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Search folders..."
msgstr "Rechercher dans les dossiers..."
#: packages/ui/components/common/language-switcher-dialog.tsx
msgid "Search languages..."
msgstr "Rechercher des langues..."
@ -5428,6 +5572,10 @@ msgstr "Activité de sécurité"
msgid "Select"
msgstr "Sélectionner"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Select a destination for this folder."
msgstr "Sélectionnez une destination pour ce dossier."
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Select a folder to move this document to."
msgstr "Sélectionnez un dossier pour déplacer ce document."
@ -5481,6 +5629,10 @@ msgstr "Sélectionner l'option par défaut"
msgid "Select groups"
msgstr "Sélectionnez des groupes"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups of members to add to the team."
msgstr "Sélectionnez des groupes de membres à ajouter à l'équipe."
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups to add to this team"
msgstr "Sélectionnez des groupes à ajouter à cette équipe"
@ -5492,7 +5644,6 @@ msgid "Select members"
msgstr "Sélectionnez des membres"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select members or groups of members to add to the team."
msgstr "Sélectionnez des membres ou groupes de membres à ajouter à l'équipe."
@ -5602,6 +5753,14 @@ msgstr "Envoi..."
msgid "Sent"
msgstr "Envoyé"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Session revoked"
msgstr "Session révoquée"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Sessions have been revoked"
msgstr "Les sessions ont été révoquées"
#: apps/remix/app/components/general/claim-account.tsx
msgid "Set a password"
msgstr "Définir un mot de passe"
@ -5621,6 +5780,7 @@ msgstr "Configurez les propriétés de votre modèle et les informations du dest
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Settings"
msgstr "Paramètres"
@ -6318,7 +6478,6 @@ msgstr "Titre du modèle"
msgid "Template updated successfully"
msgstr "Modèle mis à jour avec succès"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
@ -6449,12 +6608,10 @@ msgstr "Les événements qui déclencheront un webhook à envoyer à votre URL."
msgid "The fields have been updated to the new field insertion method successfully"
msgstr "Les champs ont été mis à jour avec la nouvelle méthode d'insertion de champ avec succès"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "The folder you are trying to delete does not exist."
msgstr "Le dossier que vous essayez de supprimer n'existe pas."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "The folder you are trying to move does not exist."
msgstr "Le dossier que vous essayez de déplacer n'existe pas."
@ -6771,10 +6928,9 @@ msgstr "Cet e-mail sera envoyé au destinataire qui vient de signer le document,
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
msgstr "Ce champ ne peut pas être modifié ou supprimé. Lorsque vous partagez le lien direct de ce modèle ou l'ajoutez à votre profil public, toute personne qui y accède peut saisir son nom et son email, et remplir les champs qui lui sont attribués."
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "This folder name is already taken."
msgstr "Ce nom de dossier est déjà pris."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "This folder contains multiple items. Deleting it will also delete all items in the folder, including nested folders and their contents."
msgstr "Ce dossier contient plusieurs éléments. Le supprimer supprimera également tous les éléments du dossier, y compris les dossiers imbriqués et leur contenu."
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "This is how the document will reach the recipients once the document is ready for signing."
@ -6784,10 +6940,6 @@ msgstr "Voici comment le document atteindra les destinataires une fois qu'il ser
msgid "This is the claim that this organisation was initially created with. Any feature flag changes to this claim will be backported into this organisation."
msgstr "Il s'agit de la réclamation avec laquelle cette organisation a initialement été créée. Tout changement de drapeau de fonctionnalité à cette revendication sera rétroporté dans cette organisation."
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
msgid "This link is invalid or has expired."
msgstr "Ce lien est invalide ou a expiré."
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
msgstr "Ce lien est invalide ou a expiré. Veuillez contacter votre équipe pour renvoyer une vérification."
@ -6858,6 +7010,10 @@ msgstr "Cela sera envoyé au propriétaire du document une fois que le document
msgid "This will ONLY backport feature flags which are set to true, anything disabled in the initial claim will not be backported"
msgstr "Cela ne fera que rétroporter les drapeaux de fonctionnalité qui sont activés, tout ce qui est désactivé dans la réclamation initiale ne sera pas rétroporté"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "This will sign you out of all other devices. You will need to sign in again on those devices to continue using your account."
msgstr "Cela entraînera votre déconnexion de tous les autres appareils. Vous devrez vous reconnecter sur ces appareils pour continuer à utiliser votre compte."
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
#: apps/remix/app/components/tables/document-logs-table.tsx
msgid "Time"
@ -7144,6 +7300,9 @@ msgstr "Non complet"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Unknown"
msgstr "Inconnu"
@ -7155,6 +7314,10 @@ msgstr "Illimité"
msgid "Unlimited documents, API and more"
msgstr "Documents illimités, API et plus"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Unpin"
msgstr "Détacher"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Untitled Group"
msgstr "Groupe sans titre"
@ -7497,6 +7660,11 @@ msgstr "Voir tous les documents associés"
msgid "View all security activity related to your account."
msgstr "Voir toute l'activité de sécurité liée à votre compte."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "View and manage all active sessions for your account."
msgstr "Afficher et gérer toutes les sessions actives de votre compte."
#: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx
msgid "View Codes"
msgstr "Voir les codes"
@ -7855,7 +8023,6 @@ msgstr "Nous allons générer des liens de signature pour vous, que vous pouvez
msgid "We won't send anything to notify recipients."
msgstr "Nous n'enverrons rien pour notifier les destinataires."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/components/tables/documents-table-empty-state.tsx
msgid "We're all empty"
@ -8215,7 +8382,6 @@ msgstr "Vous avez initié le document {0} qui nécessite que vous {recipientActi
msgid "You have no webhooks yet. Your webhooks will be shown here once you create them."
msgstr "Vous n'avez pas encore de webhooks. Vos webhooks seront affichés ici une fois que vous les aurez créés."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
msgid "You have not yet created any templates. To create a template please upload one."
msgstr "Vous n'avez pas encore créé de modèles. Pour créer un modèle, veuillez en importer un."
@ -8317,7 +8483,6 @@ msgstr "Vous avez vérifié votre adresse e-mail pour <0>{0}</0>."
msgid "You must enter '{deleteMessage}' to proceed"
msgstr "Vous devez entrer '{deleteMessage}' pour continuer"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "You must type '{deleteMessage}' to confirm"
msgstr "Vous devez taper '{deleteMessage}' pour confirmer"

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: it\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-06-10 02:27\n"
"PO-Revision-Date: 2025-06-19 06:05\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -72,6 +72,16 @@ msgstr "{0, plural, one {(1 carattere in eccesso)} other {(# caratteri in eccess
msgid "{0, plural, one {# character over the limit} other {# characters over the limit}}"
msgstr "{0, plural, one {# carattere oltre il limite} other {# caratteri oltre il limite}}"
#. placeholder {0}: folder._count.documents
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# document} other {# documents}}"
msgstr "{0, plural, one {# documento} other {# documenti}}"
#. placeholder {0}: folder._count.subfolders
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# folder} other {# folders}}"
msgstr "{0, plural, one {# cartella} other {# cartelle}}"
#. placeholder {0}: template.recipients.length
#: apps/remix/app/routes/_recipient+/d.$token+/_index.tsx
msgid "{0, plural, one {# recipient} other {# recipients}}"
@ -82,6 +92,11 @@ msgstr "{0, plural, one {# destinatario} other {# destinatari}}"
msgid "{0, plural, one {# team} other {# teams}}"
msgstr "{0, plural, one {# squadra} other {# squadre}}"
#. placeholder {0}: folder._count.templates
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# template} other {# templates}}"
msgstr "{0, plural, one {# modello} other {# modelli}}"
#. placeholder {0}: data.length
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
@ -752,6 +767,11 @@ msgstr "Azioni"
msgid "Active"
msgstr "Attivo"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Active sessions"
msgstr "Sessioni attive"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Active Subscriptions"
msgstr "Abbonamenti attivi"
@ -817,13 +837,13 @@ msgstr "Aggiungi campi"
msgid "Add group roles"
msgstr "Aggiungi ruoli di gruppo"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add groups"
msgstr "Aggiungi gruppi"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add members"
msgstr "Aggiungi membri"
@ -1243,17 +1263,14 @@ msgstr "Si è verificato un errore inaspettato."
msgid "An unknown error occurred"
msgstr "Si è verificato un errore sconosciuto"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "An unknown error occurred while creating the folder."
msgstr "Si è verificato un errore sconosciuto durante la creazione della cartella."
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "An unknown error occurred while deleting the folder."
msgstr "Si è verificato un errore sconosciuto durante l'eliminazione della cartella."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "An unknown error occurred while moving the folder."
msgstr "Si è verificato un errore sconosciuto durante lo spostamento della cartella."
@ -1322,6 +1339,10 @@ msgstr "Sei sicuro di voler completare il documento? Questa azione non può esse
msgid "Are you sure you want to delete the following claim?"
msgstr "Sei sicuro di voler eliminare la seguente richiesta?"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Are you sure you want to delete this folder?"
msgstr "Sei sicuro di voler eliminare questa cartella?"
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
msgid "Are you sure you want to delete this token?"
msgstr "Sei sicuro di voler eliminare questo token?"
@ -1614,6 +1635,7 @@ msgstr "Può preparare"
#: apps/remix/app/components/dialogs/team-delete-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
@ -1626,6 +1648,9 @@ msgstr "Può preparare"
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
@ -1921,7 +1946,6 @@ msgstr "Conferma digitando <0>{deleteMessage}</0>"
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Confirm by typing: <0>{deleteMessage}</0>"
msgstr "Conferma digitando: <0>{deleteMessage}</0>"
@ -2065,6 +2089,7 @@ msgstr "Copia il token"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
msgid "Create"
msgstr "Crea"
@ -2134,6 +2159,14 @@ msgstr "Crea Link di Firma Diretto"
msgid "Create document from template"
msgstr "Crea documento da modello"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Create folder"
msgstr "Crea cartella"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create Folder"
msgstr "Crea Cartella"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Create group"
@ -2143,6 +2176,10 @@ msgstr "Crea gruppo"
msgid "Create Groups"
msgstr "Crea Gruppi"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create New Folder"
msgstr "Crea Nuova Cartella"
#: apps/remix/app/routes/_profile+/_layout.tsx
msgid "Create now"
msgstr "Crea ora"
@ -2221,6 +2258,7 @@ msgstr "Crea il tuo account e inizia a utilizzare firme digitali all'avanguardia
msgid "Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp."
msgstr "Crea il tuo account e inizia a utilizzare firme digitali all'avanguardia. Una firma aperta e bella è a tua portata."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
@ -2260,6 +2298,14 @@ msgstr "Creato il {0}"
msgid "CSV Structure"
msgstr "Struttura CSV"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Cumulative MAU (signed in)"
msgstr ""
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Current"
msgstr "Corrente"
#: apps/remix/app/components/forms/password.tsx
msgid "Current Password"
msgstr "Password attuale"
@ -2342,6 +2388,7 @@ msgstr "elimina"
#: apps/remix/app/components/tables/organisation-groups-table.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/admin-claims-table.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
@ -2353,6 +2400,7 @@ msgstr "elimina"
#: apps/remix/app/components/dialogs/organisation-group-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Delete"
@ -2360,10 +2408,9 @@ msgstr "Elimina"
#. placeholder {0}: webhook.webhookUrl
#. placeholder {0}: token.name
#. placeholder {0}: folder?.name ?? 'folder'
#. placeholder {0}: organisation.name
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "delete {0}"
@ -2395,6 +2442,10 @@ msgstr "Elimina documento"
msgid "Delete Document"
msgstr "Elimina Documento"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Delete Folder"
msgstr "Elimina Cartella"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
msgid "Delete organisation"
msgstr "Elimina organizzazione"
@ -2453,6 +2504,7 @@ msgid "Details"
msgstr "Dettagli"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/settings-security-activity-table.tsx
msgid "Device"
msgstr "Dispositivo"
@ -2833,7 +2885,6 @@ msgid "Document will be permanently deleted"
msgstr "Il documento sarà eliminato definitivamente"
#: apps/remix/app/routes/_profile+/p.$url.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
@ -3139,6 +3190,10 @@ msgstr "Abilitare l'account consente all'utente di utilizzare nuovamente l'accou
msgid "Enclosed Document"
msgstr "Documento Allegato"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Inserisci un nome per la tua nuova cartella. Le cartelle ti aiutano a organizzare i tuoi elementi."
#: apps/remix/app/components/forms/subscription-claim-form.tsx
msgid "Enter claim name"
msgstr "Inserisci nome richiesta"
@ -3179,6 +3234,7 @@ msgstr "Impresa"
#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
@ -3221,6 +3277,7 @@ msgstr "Impresa"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx
@ -3265,8 +3322,7 @@ msgstr "Scade il {0}"
msgid "External ID"
msgstr "ID esterno"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Failed to create folder"
msgstr "Creazione cartella fallita"
@ -3274,6 +3330,10 @@ msgstr "Creazione cartella fallita"
msgid "Failed to create subscription claim."
msgstr "Creazione della richiesta di abbonamento non riuscita."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Failed to delete folder"
msgstr "Impossibile eliminare la cartella"
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Failed to delete subscription claim."
msgstr "Eliminazione di rivendicazione di abbonamento fallita."
@ -3282,14 +3342,26 @@ msgstr "Eliminazione di rivendicazione di abbonamento fallita."
msgid "Failed to load document"
msgstr "Caricamento documento fallito."
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Failed to move folder"
msgstr "Impossibile spostare la cartella"
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Failed to reseal document"
msgstr "Fallito il risigillo del documento"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Failed to revoke session"
msgstr "Impossibile revocare la sessione"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Failed to save settings."
msgstr "Impossibile salvare le impostazioni."
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Failed to sign out all sessions"
msgstr "Non è stato possibile disconnettere tutte le sessioni"
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
msgid "Failed to update document"
msgstr "Aggiornamento documento fallito"
@ -3386,16 +3458,28 @@ msgstr "Compila i dettagli per creare una nuova rivendicazione di abbonamento."
msgid "Folder"
msgstr "Cartella"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder created successfully"
msgstr "Cartella creata con successo"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder deleted successfully"
msgstr "Cartella eliminata con successo"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Folder moved successfully"
msgstr "Cartella spostata con successo"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder Name"
msgstr "Nome Cartella"
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder not found"
msgstr "Cartella non trovata"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
msgid "Folder updated successfully"
msgstr "Cartella aggiornata con successo"
@ -3633,9 +3717,16 @@ msgid "Hide additional information"
msgstr "Nascondi informazioni aggiuntive"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Home"
msgstr "Home"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Home (No Folder)"
msgstr "Home (Nessuna Cartella)"
#: packages/lib/constants/recipient-roles.ts
msgid "I am a signer of this document"
msgstr "Sono un firmatario di questo documento"
@ -3747,7 +3838,6 @@ msgstr "Codice non valido. Riprova."
msgid "Invalid email"
msgstr "Email non valida"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "Invalid link"
msgstr "Link non valido"
@ -3811,6 +3901,7 @@ msgid "Invoice"
msgstr "Fattura"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
msgid "IP Address"
msgstr "Indirizzo IP"
@ -3881,6 +3972,10 @@ msgstr "Ultimi 30 giorni"
msgid "Last 7 days"
msgstr "Ultimi 7 giorni"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Last Active"
msgstr "Ultimo accesso"
#: apps/remix/app/components/general/template/template-page-view-information.tsx
#: apps/remix/app/components/general/document/document-page-view-information.tsx
msgid "Last modified"
@ -4030,6 +4125,10 @@ msgstr "Gestisci chiavi di accesso"
msgid "Manage permissions and access controls"
msgstr "Gestisci le autorizzazioni e i controlli di accesso"
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Manage sessions"
msgstr "Gestisci le sessioni"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage subscription"
@ -4107,6 +4206,10 @@ msgstr "MAU (documento creato)"
msgid "MAU (had document completed)"
msgstr "MAU (ha completato il documento)"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "MAU (signed in)"
msgstr ""
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
msgid "Max"
msgstr ""
@ -4177,7 +4280,9 @@ msgstr "Utenti attivi mensili: Utenti che hanno creato almeno un documento"
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Utenti attivi mensili: Utenti con almeno uno dei loro documenti completati"
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Move"
msgstr "Sposta"
@ -4190,6 +4295,10 @@ msgstr "Sposta \"{templateTitle}\" in una cartella"
msgid "Move Document to Folder"
msgstr "Sposta documento nella cartella"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Move Folder"
msgstr "Sposta Cartella"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
msgid "Move Template to Folder"
msgstr "Sposta modello nella cartella"
@ -4203,6 +4312,10 @@ msgstr "Sposta nella cartella"
msgid "Multiple access methods can be selected."
msgstr "Possono essere selezionati più metodi di accesso."
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "My Folder"
msgstr "La Mia Cartella"
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
@ -4291,6 +4404,16 @@ msgstr "No"
msgid "No active drafts"
msgstr "Nessuna bozza attiva"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "No folders found"
msgstr "Nessuna cartella trovata"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
msgid "No folders found matching \"{searchTerm}\""
msgstr "Nessuna cartella trovata corrispondente a \"{searchTerm}\""
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
msgid "No further action is required from you at this time."
@ -4600,10 +4723,18 @@ msgstr "Organizzazioni"
msgid "Organisations that the user is a member of."
msgstr "Organizzazioni di cui l'utente è membro."
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your documents"
msgstr "Organizza i tuoi documenti"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Organise your members into groups which can be assigned to teams"
msgstr "Organizza i tuoi membri in gruppi che possono essere assegnati ai team."
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your templates"
msgstr "Organizza i tuoi modelli"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
msgid "Organize your documents and templates"
msgstr "Organizza i tuoi documenti e modelli."
@ -4779,6 +4910,10 @@ msgstr "Inserisci una password"
msgid "Pick any of the following agreements below and start signing to get started"
msgstr "Scegli uno dei seguenti accordi e inizia a firmare per iniziare"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Pin"
msgstr "Fissa"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@ -5305,7 +5440,6 @@ msgstr "Conservazione dei documenti"
msgid "Retry"
msgstr "Riprova"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.decline.$token.tsx
@ -5321,6 +5455,7 @@ msgstr "Torna alla home"
msgid "Return to sign in"
msgstr "Torna a accedere"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/general/teams/team-email-usage.tsx
msgid "Revoke"
msgstr "Revoca"
@ -5329,6 +5464,12 @@ msgstr "Revoca"
msgid "Revoke access"
msgstr "Revoca l'accesso"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Revoke all sessions"
msgstr "Revoca tutte le sessioni"
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/components/tables/team-members-table.tsx
#: apps/remix/app/components/tables/team-groups-table.tsx
@ -5348,11 +5489,6 @@ msgstr "Ruolo"
msgid "Roles"
msgstr "Ruoli"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Root (No Folder)"
msgstr "Radice (Nessuna cartella)"
#: packages/ui/primitives/data-table-pagination.tsx
msgid "Rows per page"
msgstr "Righe per pagina"
@ -5404,6 +5540,14 @@ msgstr "Cerca per ID dell'organizzazione, nome, ID cliente o email del proprieta
msgid "Search documents..."
msgstr "Cerca documenti..."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Search folders..."
msgstr "Cerca cartelle..."
#: packages/ui/components/common/language-switcher-dialog.tsx
msgid "Search languages..."
msgstr "Cerca lingue..."
@ -5428,6 +5572,10 @@ msgstr "Attività di sicurezza"
msgid "Select"
msgstr "Seleziona"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Select a destination for this folder."
msgstr "Seleziona una destinazione per questa cartella."
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Select a folder to move this document to."
msgstr "Seleziona una cartella in cui spostare questo documento."
@ -5481,6 +5629,10 @@ msgstr "Seleziona opzione predefinita"
msgid "Select groups"
msgstr "Seleziona gruppi"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups of members to add to the team."
msgstr "Seleziona i gruppi di membri da aggiungere al team."
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups to add to this team"
msgstr "Seleziona i gruppi da aggiungere a questo team"
@ -5492,7 +5644,6 @@ msgid "Select members"
msgstr "Seleziona membri"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select members or groups of members to add to the team."
msgstr "Seleziona membri o gruppi di membri da aggiungere al team."
@ -5602,6 +5753,14 @@ msgstr "Invio..."
msgid "Sent"
msgstr "Inviato"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Session revoked"
msgstr "Sessione revocata"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Sessions have been revoked"
msgstr "Le sessioni sono state revocate"
#: apps/remix/app/components/general/claim-account.tsx
msgid "Set a password"
msgstr "Imposta una password"
@ -5621,6 +5780,7 @@ msgstr "Configura le proprietà del modello e le informazioni sui destinatari"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Settings"
msgstr "Impostazioni"
@ -6318,7 +6478,6 @@ msgstr "Titolo del modello"
msgid "Template updated successfully"
msgstr "Modello aggiornato con successo"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
@ -6449,12 +6608,10 @@ msgstr "Gli eventi che scateneranno un webhook da inviare al tuo URL."
msgid "The fields have been updated to the new field insertion method successfully"
msgstr "I campi sono stati aggiornati con successo al nuovo metodo di inserimento campo"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "The folder you are trying to delete does not exist."
msgstr "La cartella che stai cercando di eliminare non esiste."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "The folder you are trying to move does not exist."
msgstr "La cartella che stai cercando di spostare non esiste."
@ -6777,10 +6934,9 @@ msgstr "Questa email sarà inviata al destinatario che ha appena firmato il docu
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
msgstr "Questo campo non può essere modificato o eliminato. Quando condividi il link diretto di questo modello o lo aggiungi al tuo profilo pubblico, chiunque vi acceda può inserire il proprio nome e email, e compilare i campi assegnati."
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "This folder name is already taken."
msgstr "Questo nome di cartella è già usato."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "This folder contains multiple items. Deleting it will also delete all items in the folder, including nested folders and their contents."
msgstr "Questa cartella contiene più elementi. Cancellarla eliminerà anche tutti gli elementi nella cartella, incluse le cartelle nidificate e i loro contenuti."
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "This is how the document will reach the recipients once the document is ready for signing."
@ -6790,10 +6946,6 @@ msgstr "È così che il documento raggiungerà i destinatari una volta pronto pe
msgid "This is the claim that this organisation was initially created with. Any feature flag changes to this claim will be backported into this organisation."
msgstr "Questo è il reclamo con cui questa organizzazione è stata inizialmente creata. Qualunque cambiamento agli indicatori delle funzionalità di questo reclamo sarà retroportato in questa organizzazione."
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
msgid "This link is invalid or has expired."
msgstr "Questo link non è valido o è scaduto."
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
msgstr "Questo link è invalido o è scaduto. Si prega di contattare il tuo team per inviare nuovamente una verifica."
@ -6864,6 +7016,10 @@ msgstr "Questo sarà inviato al proprietario del documento una volta che il docu
msgid "This will ONLY backport feature flags which are set to true, anything disabled in the initial claim will not be backported"
msgstr "Questo farà SOLO il retroporting degli indicatori delle funzionalità impostati su vero, qualsiasi cosa disabilitata nel reclamo iniziale non sarà retroportata"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "This will sign you out of all other devices. You will need to sign in again on those devices to continue using your account."
msgstr "Questa azione ti disconnetterà da tutti gli altri dispositivi. Dovrai accedere nuovamente su quei dispositivi per continuare a usare il tuo account."
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
#: apps/remix/app/components/tables/document-logs-table.tsx
msgid "Time"
@ -7150,6 +7306,9 @@ msgstr "Incompleto"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Unknown"
msgstr "Sconosciuto"
@ -7161,6 +7320,10 @@ msgstr "Illimitato"
msgid "Unlimited documents, API and more"
msgstr "Documenti illimitati, API e altro"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Unpin"
msgstr "Rimuovi"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Untitled Group"
msgstr "Gruppo senza nome"
@ -7503,6 +7666,11 @@ msgstr "Visualizza tutti i documenti correlati"
msgid "View all security activity related to your account."
msgstr "Visualizza tutte le attività di sicurezza relative al tuo account."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "View and manage all active sessions for your account."
msgstr "Visualizza e gestisci tutte le sessioni attive per il tuo account."
#: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx
msgid "View Codes"
msgstr "Visualizza Codici"
@ -7861,7 +8029,6 @@ msgstr "Genereremo link di firma per te, che potrai inviare ai destinatari trami
msgid "We won't send anything to notify recipients."
msgstr "Non invieremo nulla per notificare i destinatari."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/components/tables/documents-table-empty-state.tsx
msgid "We're all empty"
@ -8221,7 +8388,6 @@ msgstr "Hai avviato il documento {0} che richiede che tu lo {recipientActionVerb
msgid "You have no webhooks yet. Your webhooks will be shown here once you create them."
msgstr "Non hai ancora webhook. I tuoi webhook verranno visualizzati qui una volta creati."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
msgid "You have not yet created any templates. To create a template please upload one."
msgstr "Non hai ancora creato alcun modello. Per creare un modello, caricane uno."
@ -8323,7 +8489,6 @@ msgstr "Hai verificato il tuo indirizzo email per <0>{0}</0>."
msgid "You must enter '{deleteMessage}' to proceed"
msgstr "Devi inserire '{deleteMessage}' per procedere"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "You must type '{deleteMessage}' to confirm"
msgstr "Devi digitare '{deleteMessage}' per confermare"

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: ko\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-06-10 02:27\n"
"PO-Revision-Date: 2025-06-19 06:05\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@ -72,6 +72,16 @@ msgstr "{0, plural, other {(#자 초과)}}"
msgid "{0, plural, one {# character over the limit} other {# characters over the limit}}"
msgstr "{0, plural, other {#자 제한 초과}}"
#. placeholder {0}: folder._count.documents
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# document} other {# documents}}"
msgstr "{0, plural, other {# 문서}}"
#. placeholder {0}: folder._count.subfolders
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# folder} other {# folders}}"
msgstr "{0, plural, other {# 폴더}}"
#. placeholder {0}: template.recipients.length
#: apps/remix/app/routes/_recipient+/d.$token+/_index.tsx
msgid "{0, plural, one {# recipient} other {# recipients}}"
@ -82,6 +92,11 @@ msgstr "{0, plural, other {#명의 수신자}}"
msgid "{0, plural, one {# team} other {# teams}}"
msgstr ""
#. placeholder {0}: folder._count.templates
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# template} other {# templates}}"
msgstr "{0, plural, other {# 템플릿}}"
#. placeholder {0}: data.length
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
@ -752,6 +767,11 @@ msgstr "행동들"
msgid "Active"
msgstr "활성화됨"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Active sessions"
msgstr "활성 세션"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Active Subscriptions"
msgstr "활성 구독"
@ -817,13 +837,13 @@ msgstr "필드 추가"
msgid "Add group roles"
msgstr "그룹 역할 추가"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add groups"
msgstr "그룹 추가"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add members"
msgstr "멤버 추가"
@ -1243,17 +1263,14 @@ msgstr "예상치 못한 오류가 발생했습니다."
msgid "An unknown error occurred"
msgstr "알 수 없는 오류가 발생했습니다"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "An unknown error occurred while creating the folder."
msgstr "폴더를 생성하는 동안 알 수 없는 오류가 발생했습니다."
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "An unknown error occurred while deleting the folder."
msgstr "폴더를 삭제하는 동안 알 수 없는 오류가 발생했습니다."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "An unknown error occurred while moving the folder."
msgstr "폴더를 이동하는 동안 알 수 없는 오류가 발생했습니다."
@ -1322,6 +1339,10 @@ msgstr "문서를 완료하시겠습니까? 이 작업은 되돌릴 수 없습
msgid "Are you sure you want to delete the following claim?"
msgstr "정말 이 클레임을 삭제하시겠습니까?"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Are you sure you want to delete this folder?"
msgstr "이 폴더를 삭제하시겠습니까?"
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
msgid "Are you sure you want to delete this token?"
msgstr "정말 이 토큰을 삭제하시겠습니까?"
@ -1614,6 +1635,7 @@ msgstr "준비 가능"
#: apps/remix/app/components/dialogs/team-delete-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
@ -1626,6 +1648,9 @@ msgstr "준비 가능"
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
@ -1921,7 +1946,6 @@ msgstr "확인을 위해 입력하세요 <0>{deleteMessage}</0>"
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Confirm by typing: <0>{deleteMessage}</0>"
msgstr "확인을 위해 입력하세요: <0>{deleteMessage}</0>"
@ -2065,6 +2089,7 @@ msgstr "토큰 복사"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
msgid "Create"
msgstr "만들기"
@ -2134,6 +2159,14 @@ msgstr "직접 서명 링크 생성"
msgid "Create document from template"
msgstr "템플릿에서 문서 생성"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Create folder"
msgstr "폴더 만들기"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create Folder"
msgstr "폴더 생성"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Create group"
@ -2143,6 +2176,10 @@ msgstr "그룹 생성"
msgid "Create Groups"
msgstr "그룹 생성"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create New Folder"
msgstr "새 폴더 생성"
#: apps/remix/app/routes/_profile+/_layout.tsx
msgid "Create now"
msgstr "지금 생성"
@ -2221,6 +2258,7 @@ msgstr "계정을 만들고 최첨단 문서 서명을 사용해보세요."
msgid "Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp."
msgstr "계정을 만들고 최첨단 문서 서명을 사용해보세요. 개방적이고 아름다운 서명은 당신의 손 안에 있습니다."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
@ -2260,6 +2298,14 @@ msgstr "{0}에 생성됨"
msgid "CSV Structure"
msgstr "CSV 구조"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Cumulative MAU (signed in)"
msgstr "누적 MAU (로그인 완료)"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Current"
msgstr "현재"
#: apps/remix/app/components/forms/password.tsx
msgid "Current Password"
msgstr "현재 비밀번호"
@ -2342,6 +2388,7 @@ msgstr "삭제"
#: apps/remix/app/components/tables/organisation-groups-table.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/admin-claims-table.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
@ -2353,6 +2400,7 @@ msgstr "삭제"
#: apps/remix/app/components/dialogs/organisation-group-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Delete"
@ -2360,10 +2408,9 @@ msgstr "삭제"
#. placeholder {0}: webhook.webhookUrl
#. placeholder {0}: token.name
#. placeholder {0}: folder?.name ?? 'folder'
#. placeholder {0}: organisation.name
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "delete {0}"
@ -2395,6 +2442,10 @@ msgstr "문서 삭제"
msgid "Delete Document"
msgstr "문서 삭제"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Delete Folder"
msgstr "폴더 삭제"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
msgid "Delete organisation"
msgstr "조직 삭제"
@ -2453,6 +2504,7 @@ msgid "Details"
msgstr "세부 정보"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/settings-security-activity-table.tsx
msgid "Device"
msgstr "장치"
@ -2833,7 +2885,6 @@ msgid "Document will be permanently deleted"
msgstr "문서가 영구적으로 삭제될 것입니다"
#: apps/remix/app/routes/_profile+/p.$url.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
@ -3139,6 +3190,10 @@ msgstr "계정을 활성화하면 사용자가 다시 계정을 사용할 수
msgid "Enclosed Document"
msgstr "첨부 문서"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "새 폴더의 이름을 입력하세요. 폴더는 항목을 정리하는 데 도움을 줍니다."
#: apps/remix/app/components/forms/subscription-claim-form.tsx
msgid "Enter claim name"
msgstr "클레임 이름 입력"
@ -3179,6 +3234,7 @@ msgstr "엔터프라이즈"
#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
@ -3221,6 +3277,7 @@ msgstr "엔터프라이즈"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx
@ -3265,8 +3322,7 @@ msgstr "{0}에 만료됨"
msgid "External ID"
msgstr "외부 ID"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Failed to create folder"
msgstr "폴더 생성에 실패했습니다"
@ -3274,6 +3330,10 @@ msgstr "폴더 생성에 실패했습니다"
msgid "Failed to create subscription claim."
msgstr "구독 클레임 생성에 실패했습니다."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Failed to delete folder"
msgstr "폴더 삭제 실패"
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Failed to delete subscription claim."
msgstr "구독 클레임 삭제에 실패했습니다."
@ -3282,14 +3342,26 @@ msgstr "구독 클레임 삭제에 실패했습니다."
msgid "Failed to load document"
msgstr "문서 로드에 실패했습니다"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Failed to move folder"
msgstr "폴더 이동 실패"
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Failed to reseal document"
msgstr "문서 걸쇠 잠금 실패"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Failed to revoke session"
msgstr "세션을 취소하는 데 실패했습니다"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Failed to save settings."
msgstr "설정 저장 실패."
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Failed to sign out all sessions"
msgstr "모든 세션에서 로그아웃에 실패했습니다."
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
msgid "Failed to update document"
msgstr "문서 업데이트에 실패했습니다"
@ -3386,16 +3458,28 @@ msgstr "새로운 구독 클레임을 만들기 위한 세부 정보를 입력
msgid "Folder"
msgstr "폴더"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder created successfully"
msgstr "폴더가 성공적으로 생성되었습니다"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder deleted successfully"
msgstr "폴더가 성공적으로 삭제되었습니다"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Folder moved successfully"
msgstr "폴더가 성공적으로 이동되었습니다"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder Name"
msgstr "폴더 이름"
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder not found"
msgstr "폴더를 찾을 수 없습니다"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
msgid "Folder updated successfully"
msgstr "폴더가 성공적으로 업데이트되었습니다"
@ -3633,9 +3717,16 @@ msgid "Hide additional information"
msgstr "추가 정보 숨기기"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Home"
msgstr "홈"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Home (No Folder)"
msgstr "홈 (폴더 없음)"
#: packages/lib/constants/recipient-roles.ts
msgid "I am a signer of this document"
msgstr "저는 이 문서의 서명자입니다"
@ -3747,7 +3838,6 @@ msgstr "잘못된 코드입니다. 다시 시도해 주세요."
msgid "Invalid email"
msgstr "잘못된 이메일"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "Invalid link"
msgstr "잘못된 링크"
@ -3811,6 +3901,7 @@ msgid "Invoice"
msgstr "송장"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
msgid "IP Address"
msgstr "IP 주소"
@ -3881,6 +3972,10 @@ msgstr "지난 30일"
msgid "Last 7 days"
msgstr "지난 7일"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Last Active"
msgstr "마지막 활동"
#: apps/remix/app/components/general/template/template-page-view-information.tsx
#: apps/remix/app/components/general/document/document-page-view-information.tsx
msgid "Last modified"
@ -4030,6 +4125,10 @@ msgstr "패스키 관리"
msgid "Manage permissions and access controls"
msgstr "권한 및 접근 제어 관리"
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Manage sessions"
msgstr "세션 관리"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage subscription"
@ -4107,6 +4206,10 @@ msgstr "MAU (생성된 문서)"
msgid "MAU (had document completed)"
msgstr "MAU (문서 완료)"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "MAU (signed in)"
msgstr "MAU (로그인 완료)"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
msgid "Max"
msgstr "최대"
@ -4177,7 +4280,9 @@ msgstr "월간 활성 사용자: 최소 하나의 문서를 생성한 사용자"
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "월간 활성 사용자: 최소 한 개의 문서가 완료된 사용자"
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Move"
msgstr "이동"
@ -4190,6 +4295,10 @@ msgstr "\"{templateTitle}\"를 폴더로 이동"
msgid "Move Document to Folder"
msgstr "문서를 폴더로 이동"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Move Folder"
msgstr "폴더 이동"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
msgid "Move Template to Folder"
msgstr "템플릿을 폴더로 이동"
@ -4203,6 +4312,10 @@ msgstr "폴더로 이동"
msgid "Multiple access methods can be selected."
msgstr "다중 접근 방법을 선택할 수 있습니다."
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "My Folder"
msgstr "내 폴더"
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
@ -4291,6 +4404,16 @@ msgstr "아니요"
msgid "No active drafts"
msgstr "활성화된 초안이 없음"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "No folders found"
msgstr "폴더를 찾을 수 없습니다"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
msgid "No folders found matching \"{searchTerm}\""
msgstr "'{searchTerm}'와 일치하는 폴더를 찾을 수 없습니다"
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
msgid "No further action is required from you at this time."
@ -4600,10 +4723,18 @@ msgstr "조직들"
msgid "Organisations that the user is a member of."
msgstr "이 사용자가 멤버로 있는 조직들."
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your documents"
msgstr "문서를 정리하세요"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Organise your members into groups which can be assigned to teams"
msgstr "멤버들을 팀에 할당할 수 있는 그룹으로 구성하세요"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your templates"
msgstr "템플릿을 정리하세요"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
msgid "Organize your documents and templates"
msgstr "문서 및 템플릿을 정리하세요"
@ -4779,6 +4910,10 @@ msgstr "비밀번호 설정"
msgid "Pick any of the following agreements below and start signing to get started"
msgstr "다음의 동의서 중 하나를 선택하고 서명을 시작하여 시작하세요"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Pin"
msgstr "고정"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@ -5305,7 +5440,6 @@ msgstr "문서 보유"
msgid "Retry"
msgstr "다시 시도"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.decline.$token.tsx
@ -5321,6 +5455,7 @@ msgstr "홈으로 돌아가기"
msgid "Return to sign in"
msgstr "로그인 화면으로 돌아가기"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/general/teams/team-email-usage.tsx
msgid "Revoke"
msgstr "취소"
@ -5329,6 +5464,12 @@ msgstr "취소"
msgid "Revoke access"
msgstr "접근 권한 취소"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Revoke all sessions"
msgstr "모든 세션 취소"
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/components/tables/team-members-table.tsx
#: apps/remix/app/components/tables/team-groups-table.tsx
@ -5348,11 +5489,6 @@ msgstr "역할"
msgid "Roles"
msgstr "역할들"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Root (No Folder)"
msgstr "루트 (폴더 없음)"
#: packages/ui/primitives/data-table-pagination.tsx
msgid "Rows per page"
msgstr "페이지당 행 수"
@ -5404,6 +5540,14 @@ msgstr "조직 ID, 이름, 고객 ID 또는 소유자 이메일로 검색"
msgid "Search documents..."
msgstr "문서 검색…"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Search folders..."
msgstr "폴더 검색..."
#: packages/ui/components/common/language-switcher-dialog.tsx
msgid "Search languages..."
msgstr "언어 검색…"
@ -5428,6 +5572,10 @@ msgstr "보안 활동"
msgid "Select"
msgstr "선택"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Select a destination for this folder."
msgstr "이 폴더의 목적지를 선택하세요."
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Select a folder to move this document to."
msgstr "이 문서를 이동할 폴더를 선택하세요."
@ -5481,6 +5629,10 @@ msgstr "기본 옵션 선택"
msgid "Select groups"
msgstr "그룹 선택"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups of members to add to the team."
msgstr "팀에 추가할 구성원 그룹을 선택하세요."
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups to add to this team"
msgstr "이 팀에 추가할 그룹 선택"
@ -5492,7 +5644,6 @@ msgid "Select members"
msgstr "멤버 선택"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select members or groups of members to add to the team."
msgstr "팀에 추가할 멤버 또는 멤버 그룹을 선택하세요."
@ -5602,6 +5753,14 @@ msgstr "보내는 중…"
msgid "Sent"
msgstr "보냈음"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Session revoked"
msgstr "세션이 해제되었습니다."
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Sessions have been revoked"
msgstr "세션이 취소되었습니다."
#: apps/remix/app/components/general/claim-account.tsx
msgid "Set a password"
msgstr "비밀번호 설정"
@ -5621,6 +5780,7 @@ msgstr "템플릿 속성과 수신자 정보를 설정하세요"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Settings"
msgstr "설정"
@ -6318,7 +6478,6 @@ msgstr "템플릿 제목"
msgid "Template updated successfully"
msgstr "템플릿이 성공적으로 업데이트되었습니다"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
@ -6449,12 +6608,10 @@ msgstr "URL로 전송될 웹훅을 트리거할 이벤트입니다."
msgid "The fields have been updated to the new field insertion method successfully"
msgstr "필드가 새로운 필드 삽입 방법으로 성공적으로 업데이트되었습니다"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "The folder you are trying to delete does not exist."
msgstr "삭제하려는 폴더가 존재하지 않습니다."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "The folder you are trying to move does not exist."
msgstr "이동하려는 폴더가 존재하지 않습니다."
@ -6771,10 +6928,9 @@ msgstr "다른 수신자가 아직 서명하지 않았을 경우 문서에 서
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
msgstr "이 필드는 수정되거나 삭제될 수 없습니다. 이 템플릿의 직접 링크를 공유하거나 공개 프로필에 추가하면, 누구나 이름과 이메일을 입력하고 할당된 필드를 기입할 수 있습니다."
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "This folder name is already taken."
msgstr "이 폴더 이름은 이미 사용 중입니다."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "This folder contains multiple items. Deleting it will also delete all items in the folder, including nested folders and their contents."
msgstr "이 폴더에는 여러 항목이 포함되어 있습니다. 삭제하면 중첩 폴더 및 그 내용물을 포함한 모든 항목이 삭제됩니다."
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "This is how the document will reach the recipients once the document is ready for signing."
@ -6784,10 +6940,6 @@ msgstr "이 문서를 서명할 준비가 되면 수신자에게 이렇게 도
msgid "This is the claim that this organisation was initially created with. Any feature flag changes to this claim will be backported into this organisation."
msgstr "이것이 조직 초기 설립 당시의 클레임입니다. 이 클레임으로의 기능 플래그 변경은 조직에 백포팅됩니다."
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
msgid "This link is invalid or has expired."
msgstr "이 링크는 유효하지 않거나 만료되었습니다."
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
msgstr "이 링크는 유효하지 않거나 만료되었습니다. 팀에 문의하여 확인을 다시 요청하십시오."
@ -6858,6 +7010,10 @@ msgstr "문서가 완전히 완료된 후 문서 소유자에게 이것이 발
msgid "This will ONLY backport feature flags which are set to true, anything disabled in the initial claim will not be backported"
msgstr "기능 플래그 기본 설정은 사실의 경우에만 백포팅됩니다. 초기 클레임에 비활성화된 사항은 백포팅되지 않습니다."
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "This will sign you out of all other devices. You will need to sign in again on those devices to continue using your account."
msgstr "이 작업은 다른 모든 기기에서 로그아웃됩니다. 계정을 계속 사용하려면 해당 기기에서 다시 로그인해야 합니다."
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
#: apps/remix/app/components/tables/document-logs-table.tsx
msgid "Time"
@ -7144,6 +7300,9 @@ msgstr "미완료"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Unknown"
msgstr "알 수 없음"
@ -7155,6 +7314,10 @@ msgstr "무제한"
msgid "Unlimited documents, API and more"
msgstr "무제한 문서, API 및 기타"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Unpin"
msgstr "고정 해제"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Untitled Group"
msgstr "제목없는 그룹"
@ -7497,6 +7660,11 @@ msgstr "모든 관련 문서 보기"
msgid "View all security activity related to your account."
msgstr "귀하의 계정과 관련된 모든 보안 활동 보기."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "View and manage all active sessions for your account."
msgstr "계정의 모든 활성 세션을 보고 관리하십시오."
#: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx
msgid "View Codes"
msgstr "코드 보기"
@ -7855,7 +8023,6 @@ msgstr "저희가 서명 링크를 생성해드리며, 원하시는 방법으로
msgid "We won't send anything to notify recipients."
msgstr "수신자에게 알리기 위해 아무것도 보내지 않습니다."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/components/tables/documents-table-empty-state.tsx
msgid "We're all empty"
@ -8215,7 +8382,6 @@ msgstr "귀하에게 {recipientActionVerb}을 요구하는 문서 {0}을 시작
msgid "You have no webhooks yet. Your webhooks will be shown here once you create them."
msgstr "아직 웹훅이 없습니다.웹훅을 만들면 여기에 표시됩니다."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
msgid "You have not yet created any templates. To create a template please upload one."
msgstr "아직 템플릿을 생성하지 않았습니다.템플릿을 만들려면 하나를 업로드하세요."
@ -8317,7 +8483,6 @@ msgstr "<0>{0}</0>에 대한 이메일 주소를 확인했습니다."
msgid "You must enter '{deleteMessage}' to proceed"
msgstr "계속하려면 '{deleteMessage}'을(를) 입력해야 합니다"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "You must type '{deleteMessage}' to confirm"
msgstr "확인을 위해 '{deleteMessage}'를 입력해야 합니다"

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-06-10 12:05\n"
"PO-Revision-Date: 2025-06-19 06:05\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
@ -72,6 +72,16 @@ msgstr "{0, plural, one {(1 znak przekroczony)} few {(# znaki przekroczone)} man
msgid "{0, plural, one {# character over the limit} other {# characters over the limit}}"
msgstr "{0, plural, one {# znak przekroczony} few {# znaki przekroczone} many {# znaków przekroczonych} other {# znaków przekroczonych}}"
#. placeholder {0}: folder._count.documents
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# document} other {# documents}}"
msgstr "{0, plural, one {# dokument} few {# dokumenty} many {# dokumentów} other {# dokumentów}}"
#. placeholder {0}: folder._count.subfolders
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# folder} other {# folders}}"
msgstr "{0, plural, one {# folder} few {# foldery} many {# folderów} other {# folderów}}"
#. placeholder {0}: template.recipients.length
#: apps/remix/app/routes/_recipient+/d.$token+/_index.tsx
msgid "{0, plural, one {# recipient} other {# recipients}}"
@ -82,6 +92,11 @@ msgstr "{0, plural, one {# odbiorca} few {# odbiorców} many {# odbiorców} othe
msgid "{0, plural, one {# team} other {# teams}}"
msgstr "{0, plural, one {# zespół} few {# zespoły} many {# zespołów} other {# zespołów}}"
#. placeholder {0}: folder._count.templates
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# template} other {# templates}}"
msgstr "{0, plural, one {# szablon} few {# szablony} many {# szablonów} other {# szablonów}}"
#. placeholder {0}: data.length
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
@ -268,7 +283,7 @@ msgstr "{prefix} niepodpisane pole"
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} updated a field"
msgstr "{prefix} zaktualizowane pole"
msgstr "Użytkownik {prefix} zaktualizował pole"
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} updated a recipient"
@ -355,7 +370,7 @@ msgstr "{teamName} zaprosił cię do {action} {documentName}"
#: packages/lib/utils/document-audit-logs.ts
msgid "{userName} approved the document"
msgstr "{userName} zatwierdził dokument"
msgstr "Użytkownik {userName} zatwierdził dokument"
#: packages/lib/utils/document-audit-logs.ts
msgid "{userName} CC'd the document"
@ -367,15 +382,15 @@ msgstr "{userName} zakończył swoje zadanie"
#: packages/lib/utils/document-audit-logs.ts
msgid "{userName} rejected the document"
msgstr "{userName} odrzucił dokument"
msgstr "Użytkownik {userName} odrzucił dokument"
#: packages/lib/utils/document-audit-logs.ts
msgid "{userName} signed the document"
msgstr "{userName} podpisał dokument"
msgstr "Użytkownik {userName} podpisał dokument"
#: packages/lib/utils/document-audit-logs.ts
msgid "{userName} viewed the document"
msgstr "{userName} wyświetlił dokument"
msgstr "Użytkownik {userName} wyświetlił dokument"
#: packages/ui/primitives/data-table-pagination.tsx
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
@ -471,11 +486,11 @@ msgstr "<0>Jesteś na drodze do zakończenia przeglądania \"<1>{documentTitle}<
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
msgid "0 Free organisations left"
msgstr "0 Wolnych organizacji pozostało"
msgstr "Pozostało 0 darmowych organizacji"
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
msgid "1 Free organisations left"
msgstr "1 Darmowa organizacja pozostała"
msgstr "Pozostała 1 darmowa organizacja"
#: apps/remix/app/components/forms/token.tsx
msgid "1 month"
@ -530,7 +545,7 @@ msgstr "5 dokumentów miesięcznie"
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
msgid "5 Documents a month"
msgstr "5 Dokumentów miesięcznie"
msgstr "5 dokumentów miesięcznie"
#: apps/remix/app/components/general/generic-error-layout.tsx
msgid "500 Internal Server Error"
@ -706,11 +721,11 @@ msgstr "Konto zostało usunięte"
#: apps/remix/app/components/dialogs/admin-user-disable-dialog.tsx
msgid "Account disabled"
msgstr "Konto wyłączone"
msgstr "Konto zostało wyłączone"
#: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx
msgid "Account enabled"
msgstr "Konto włączone"
msgstr "Konto zostało włączone"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
msgid "Account Re-Authentication"
@ -752,6 +767,11 @@ msgstr "Akcje"
msgid "Active"
msgstr "Aktywne"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Active sessions"
msgstr "Aktywne sesje"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Active Subscriptions"
msgstr "Aktywne subskrypcje"
@ -817,13 +837,13 @@ msgstr "Dodaj pola"
msgid "Add group roles"
msgstr "Dodaj role grupowe"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add groups"
msgstr "Dodaj grupy"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add members"
msgstr "Dodaj członków"
@ -1243,17 +1263,14 @@ msgstr "Wystąpił nieoczekiwany błąd."
msgid "An unknown error occurred"
msgstr "Wystąpił nieznany błąd"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "An unknown error occurred while creating the folder."
msgstr "Wystąpił nieznany błąd podczas tworzenia folderu."
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "An unknown error occurred while deleting the folder."
msgstr "Wystąpił nieznany błąd podczas usuwania folderu."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "An unknown error occurred while moving the folder."
msgstr "Wystąpił nieznany błąd podczas przenoszenia folderu."
@ -1322,6 +1339,10 @@ msgstr "Czy na pewno chcesz zakończyć dokument? Tego działania nie można cof
msgid "Are you sure you want to delete the following claim?"
msgstr "Czy na pewno chcesz usunąć następujący wniosek?"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Are you sure you want to delete this folder?"
msgstr "Czy na pewno chcesz usunąć ten folder?"
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
msgid "Are you sure you want to delete this token?"
msgstr "Czy na pewno chcesz usunąć ten token?"
@ -1614,6 +1635,7 @@ msgstr "Może przygotować"
#: apps/remix/app/components/dialogs/team-delete-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
@ -1626,6 +1648,9 @@ msgstr "Może przygotować"
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
@ -1921,7 +1946,6 @@ msgstr "Potwierdź, wpisując <0>{deleteMessage}</0>"
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Confirm by typing: <0>{deleteMessage}</0>"
msgstr "Potwierdź, wpisując: <0>{deleteMessage}</0>"
@ -2065,6 +2089,7 @@ msgstr "Kopiuj token"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
msgid "Create"
msgstr "Utwórz"
@ -2134,6 +2159,14 @@ msgstr "Utwórz bezpośredni link do podpisu"
msgid "Create document from template"
msgstr "Utwórz dokument z szablonu"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Create folder"
msgstr "Utwórz folder"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create Folder"
msgstr "Utwórz folder"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Create group"
@ -2143,6 +2176,10 @@ msgstr "Utwórz grupę"
msgid "Create Groups"
msgstr "Utwórz grupy"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create New Folder"
msgstr "Utwórz nowy folder"
#: apps/remix/app/routes/_profile+/_layout.tsx
msgid "Create now"
msgstr "Utwórz teraz"
@ -2221,6 +2258,7 @@ msgstr "Utwórz swoje konto i zacznij korzystać z nowoczesnego podpisywania dok
msgid "Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp."
msgstr "Utwórz swoje konto i zacznij korzystać z nowoczesnego podpisywania dokumentów. Otwarty i piękny podpis jest w zasięgu ręki."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
@ -2260,6 +2298,14 @@ msgstr "Utworzono {0}"
msgid "CSV Structure"
msgstr "Struktura CSV"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Cumulative MAU (signed in)"
msgstr "Łączne MAU (zalogowani)"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Current"
msgstr "Bieżący"
#: apps/remix/app/components/forms/password.tsx
msgid "Current Password"
msgstr "Obecne hasło"
@ -2342,6 +2388,7 @@ msgstr "usuń"
#: apps/remix/app/components/tables/organisation-groups-table.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/admin-claims-table.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
@ -2353,6 +2400,7 @@ msgstr "usuń"
#: apps/remix/app/components/dialogs/organisation-group-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Delete"
@ -2360,10 +2408,9 @@ msgstr "Usuń"
#. placeholder {0}: webhook.webhookUrl
#. placeholder {0}: token.name
#. placeholder {0}: folder?.name ?? 'folder'
#. placeholder {0}: organisation.name
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "delete {0}"
@ -2395,6 +2442,10 @@ msgstr "Usuń dokument"
msgid "Delete Document"
msgstr "Usuń dokument"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Delete Folder"
msgstr "Usuń folder"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
msgid "Delete organisation"
msgstr "Usuń organizację"
@ -2453,6 +2504,7 @@ msgid "Details"
msgstr "Szczegóły"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/settings-security-activity-table.tsx
msgid "Device"
msgstr "Urządzenie"
@ -2637,7 +2689,7 @@ msgstr "Dokument Zakończony!"
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: packages/lib/utils/document-audit-logs.ts
msgid "Document created"
msgstr "Dokument utworzony"
msgstr "Utworzono dokument"
#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx
msgid "Document Created"
@ -2741,7 +2793,7 @@ msgstr "Dokument nie jest już dostępny do podpisania"
#: packages/lib/utils/document-audit-logs.ts
msgid "Document opened"
msgstr "Dokument otwarty"
msgstr "Otwarto dokument"
#: apps/remix/app/components/general/document/document-status.tsx
msgid "Document pending"
@ -2777,7 +2829,7 @@ msgstr "Dokument ponownie zaplombowany"
#: apps/remix/app/components/general/document/document-edit-form.tsx
#: packages/lib/utils/document-audit-logs.ts
msgid "Document sent"
msgstr "Dokument wysłany"
msgstr "Wysłano dokument"
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
msgid "Document Signed"
@ -2833,7 +2885,6 @@ msgid "Document will be permanently deleted"
msgstr "Dokument zostanie trwale usunięty"
#: apps/remix/app/routes/_profile+/p.$url.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
@ -3049,11 +3100,11 @@ msgstr "Opcje e-mail"
#: packages/lib/utils/document-audit-logs.ts
msgid "Email resent"
msgstr "E-mail wysłany ponownie"
msgstr "Wysłano ponownie wiadomość"
#: packages/lib/utils/document-audit-logs.ts
msgid "Email sent"
msgstr "E-mail wysłany"
msgstr "Wysłano wiadomość"
#: apps/remix/app/routes/_unauthenticated+/check-email.tsx
msgid "Email sent!"
@ -3139,6 +3190,10 @@ msgstr "Włączenie konta pozwala użytkownikowi na ponowne korzystanie z niego
msgid "Enclosed Document"
msgstr "Załączony dokument"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Wprowadź nazwę dla nowego folderu. Foldery pomogą ci zorganizować przedmioty."
#: apps/remix/app/components/forms/subscription-claim-form.tsx
msgid "Enter claim name"
msgstr "Wprowadź nazwę roszczenia"
@ -3179,6 +3234,7 @@ msgstr "Enterprise"
#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
@ -3221,6 +3277,7 @@ msgstr "Enterprise"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx
@ -3265,8 +3322,7 @@ msgstr "Wygasa {0}"
msgid "External ID"
msgstr "Zewnętrzny ID"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Failed to create folder"
msgstr "Nie udało się utworzyć folderu"
@ -3274,6 +3330,10 @@ msgstr "Nie udało się utworzyć folderu"
msgid "Failed to create subscription claim."
msgstr "Nie udało się utworzyć roszczenia subskrypcyjnego."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Failed to delete folder"
msgstr "Nie udało się usunąć folder"
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Failed to delete subscription claim."
msgstr "Nie udało się usunąć roszczenia subskrypcyjnego."
@ -3282,14 +3342,26 @@ msgstr "Nie udało się usunąć roszczenia subskrypcyjnego."
msgid "Failed to load document"
msgstr "Nie udało się załadować dokumentu"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Failed to move folder"
msgstr "Nie udało się przenieść folderu"
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Failed to reseal document"
msgstr "Nie udało się ponownie zaplombować dokumentu"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Failed to revoke session"
msgstr "Nie udało się unieważnić sesji"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Failed to save settings."
msgstr "Nie udało się zapisać ustawień."
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Failed to sign out all sessions"
msgstr "Nie udało się wylogować ze wszystkich sesji"
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
msgid "Failed to update document"
msgstr "Nie udało się aktualizować dokumentu"
@ -3353,11 +3425,11 @@ msgstr "Pole wstępnie wypełnione przez asystenta"
#: packages/lib/utils/document-audit-logs.ts
msgid "Field signed"
msgstr "Pole podpisane"
msgstr "Podpisano pole"
#: packages/lib/utils/document-audit-logs.ts
msgid "Field unsigned"
msgstr "Pole niepodpisane"
msgstr "Niepodpisano pole"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Fields"
@ -3386,16 +3458,28 @@ msgstr "Wypełnij szczegóły, aby utworzyć nowe roszczenie subskrypcyjne."
msgid "Folder"
msgstr "Folder"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder created successfully"
msgstr "Folder utworzony pomyślnie"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder deleted successfully"
msgstr "Folder został pomyślnie usunięty"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Folder moved successfully"
msgstr "Folder został pomyślnie przeniesiony"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder Name"
msgstr "Nazwa foldera"
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder not found"
msgstr "Folder nie znaleziony"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
msgid "Folder updated successfully"
msgstr "Folder zaktualizowany pomyślnie"
@ -3633,9 +3717,16 @@ msgid "Hide additional information"
msgstr "Ukryj dodatkowe informacje"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Home"
msgstr "Dom"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Home (No Folder)"
msgstr "Strona główna (Brak folderu)"
#: packages/lib/constants/recipient-roles.ts
msgid "I am a signer of this document"
msgstr "Jestem sygnatariuszem tego dokumentu"
@ -3747,7 +3838,6 @@ msgstr "Nieprawidłowy kod. Proszę spróbuj ponownie."
msgid "Invalid email"
msgstr "Nieprawidłowy email"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "Invalid link"
msgstr "Nieprawidłowy link"
@ -3811,6 +3901,7 @@ msgid "Invoice"
msgstr "Faktura"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
msgid "IP Address"
msgstr "Adres IP"
@ -3881,6 +3972,10 @@ msgstr "Ostatnie 30 dni"
msgid "Last 7 days"
msgstr "Ostatnie 7 dni"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Last Active"
msgstr "Ostatnia aktywność"
#: apps/remix/app/components/general/template/template-page-view-information.tsx
#: apps/remix/app/components/general/document/document-page-view-information.tsx
msgid "Last modified"
@ -4030,6 +4125,10 @@ msgstr "Zarządzaj kluczami dostępu"
msgid "Manage permissions and access controls"
msgstr "Zarządzaj uprawnieniami i kontrolą dostępu"
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Manage sessions"
msgstr "Zarządzaj sesjami"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage subscription"
@ -4107,6 +4206,10 @@ msgstr "MAU (utworzony dokument)"
msgid "MAU (had document completed)"
msgstr "MAU (zakończony dokument)"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "MAU (signed in)"
msgstr "MAU (zalogowani)"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
msgid "Max"
msgstr "Max"
@ -4177,7 +4280,9 @@ msgstr "Miesięczni aktywni użytkownicy: Użytkownicy, którzy utworzyli przyna
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Miesięczni aktywni użytkownicy: Użytkownicy, którzy mieli przynajmniej jeden z ukończonych dokumentów"
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Move"
msgstr "Przenieś"
@ -4190,6 +4295,10 @@ msgstr "Przenieś \"{templateTitle}\" do folderu"
msgid "Move Document to Folder"
msgstr "Przenieś dokument do folderu"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Move Folder"
msgstr "Przenieś folder"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
msgid "Move Template to Folder"
msgstr "Przenieś szablon do folderu"
@ -4203,6 +4312,10 @@ msgstr "Przenieś do folderu"
msgid "Multiple access methods can be selected."
msgstr "Można wybrać wiele metod dostępu."
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "My Folder"
msgstr "Mój folder"
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
@ -4291,6 +4404,16 @@ msgstr "Nie"
msgid "No active drafts"
msgstr "Brak aktywnych szkiców"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "No folders found"
msgstr "Nie znaleziono folderów"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
msgid "No folders found matching \"{searchTerm}\""
msgstr "Nie znaleziono folderów zgodnych z \"{searchTerm}\""
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
msgid "No further action is required from you at this time."
@ -4600,10 +4723,18 @@ msgstr "Organizacje"
msgid "Organisations that the user is a member of."
msgstr "Organizacje, których użytkownik jest członkiem."
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your documents"
msgstr "Zorganizuj swoje dokumenty"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Organise your members into groups which can be assigned to teams"
msgstr "Zorganizuj swoich członków w grupy, które można przypisać do zespołów"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your templates"
msgstr "Zorganizuj swoje szablony"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
msgid "Organize your documents and templates"
msgstr "Organizuj swoje dokumenty i szablony"
@ -4779,6 +4910,10 @@ msgstr "Wybierz hasło"
msgid "Pick any of the following agreements below and start signing to get started"
msgstr "Wybierz dowolną z poniższych umów i zacznij podpisywanie, aby rozpocząć"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Pin"
msgstr "Przypnij"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@ -5305,7 +5440,6 @@ msgstr "Przechowywanie dokumentów"
msgid "Retry"
msgstr "Spróbuj ponownie"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.decline.$token.tsx
@ -5321,6 +5455,7 @@ msgstr "Powrót do strony głównej"
msgid "Return to sign in"
msgstr "Powrót do logowania"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/general/teams/team-email-usage.tsx
msgid "Revoke"
msgstr "Cofnij"
@ -5329,6 +5464,12 @@ msgstr "Cofnij"
msgid "Revoke access"
msgstr "Cofnij dostęp"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Revoke all sessions"
msgstr "Cofnij wszystkie sesje"
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/components/tables/team-members-table.tsx
#: apps/remix/app/components/tables/team-groups-table.tsx
@ -5348,11 +5489,6 @@ msgstr "Rola"
msgid "Roles"
msgstr "Role"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Root (No Folder)"
msgstr "Root (Bez folderu)"
#: packages/ui/primitives/data-table-pagination.tsx
msgid "Rows per page"
msgstr "Wiersze na stronę"
@ -5404,6 +5540,14 @@ msgstr "Wyszukaj według ID organizacji, nazwy, ID klienta lub adresu e-mail wł
msgid "Search documents..."
msgstr "Szukaj dokumentów..."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Search folders..."
msgstr "Szukaj folderów..."
#: packages/ui/components/common/language-switcher-dialog.tsx
msgid "Search languages..."
msgstr "Szukaj języków..."
@ -5428,6 +5572,10 @@ msgstr "Aktywność bezpieczeństwa"
msgid "Select"
msgstr "Wybierz"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Select a destination for this folder."
msgstr "Wybierz miejsce docelowe dla tego folderu."
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Select a folder to move this document to."
msgstr "Wybierz folder, do którego chcesz przenieść ten dokument."
@ -5481,6 +5629,10 @@ msgstr "Wybierz domyślną opcję"
msgid "Select groups"
msgstr "Wybierz grupy"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups of members to add to the team."
msgstr "Wybierz grupy członków do dodania do zespołu."
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups to add to this team"
msgstr "Wybierz grupy, które chcesz dodać do tego zespołu"
@ -5492,7 +5644,6 @@ msgid "Select members"
msgstr "Wybierz członków"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select members or groups of members to add to the team."
msgstr "Wybierz członków lub grupy członków, aby dodać do zespołu."
@ -5602,6 +5753,14 @@ msgstr "Wysyłanie..."
msgid "Sent"
msgstr "Wysłano"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Session revoked"
msgstr "Sesja odwołana"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Sessions have been revoked"
msgstr "Sesje zostały odwołane"
#: apps/remix/app/components/general/claim-account.tsx
msgid "Set a password"
msgstr "Ustaw hasło"
@ -5621,6 +5780,7 @@ msgstr "Skonfiguruj właściwości szablonu i informacje o odbiorcach"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Settings"
msgstr "Ustawienia"
@ -6318,7 +6478,6 @@ msgstr "Tytuł szablonu"
msgid "Template updated successfully"
msgstr "Szablon zaktualizowany pomyślnie"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
@ -6449,12 +6608,10 @@ msgstr "Wydarzenia, które wyzwolą webhook do wysłania do Twojego URL."
msgid "The fields have been updated to the new field insertion method successfully"
msgstr "Pola zostały zaktualizowane do nowej metody wstawiania."
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "The folder you are trying to delete does not exist."
msgstr "Folder, który próbujesz usunąć, nie istnieje."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "The folder you are trying to move does not exist."
msgstr "Folder, który próbujesz przenieść, nie istnieje."
@ -6771,10 +6928,9 @@ msgstr "Ten e-mail zostanie wysłany do odbiorcy, który właśnie podpisał dok
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
msgstr "To pole nie może być modyfikowane ani usuwane. Po udostępnieniu bezpośredniego linku do tego szablonu lub dodaniu go do swojego publicznego profilu, każdy, kto się w nim dostanie, może wpisać swoje imię i email oraz wypełnić przypisane mu pola."
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "This folder name is already taken."
msgstr "Nazwa tego folderu jest już zajęta."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "This folder contains multiple items. Deleting it will also delete all items in the folder, including nested folders and their contents."
msgstr "Ten folder zawiera wiele elementów. Usunięcie go również usunie wszystkie elementy w folderze, w tym zagnieżdżone foldery i ich zawartość."
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "This is how the document will reach the recipients once the document is ready for signing."
@ -6784,10 +6940,6 @@ msgstr "W ten sposób dokument dotrze do odbiorców, gdy tylko dokument będzie
msgid "This is the claim that this organisation was initially created with. Any feature flag changes to this claim will be backported into this organisation."
msgstr "To jest roszczenie, z którym pierwotnie została utworzona ta organizacja. Wszelkie zmiany flag funkcji w tym roszczeniu zostaną przeniesione do tej organizacji."
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
msgid "This link is invalid or has expired."
msgstr "Ten link jest nieprawidłowy lub wygasł."
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
msgstr "Ten link jest nieprawidłowy lub wygasł. Proszę skontaktować się ze swoim zespołem, aby ponownie wysłać weryfikację."
@ -6858,6 +7010,10 @@ msgstr "To zostanie wysłane do właściciela dokumentu, gdy dokument zostanie w
msgid "This will ONLY backport feature flags which are set to true, anything disabled in the initial claim will not be backported"
msgstr "To będzie TYLKO przenoś funkcje flag, które są ustawione na true, wszystko, co wyłączone w początkowym roszczeniu, nie zostanie przeniesione"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "This will sign you out of all other devices. You will need to sign in again on those devices to continue using your account."
msgstr "Zostaniesz wylogowany z wszystkich innych urządzeń. Aby dalej korzystać z konta, musisz ponownie się zalogować na tych urządzeniach."
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
#: apps/remix/app/components/tables/document-logs-table.tsx
msgid "Time"
@ -7144,6 +7300,9 @@ msgstr "Niezakończony"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Unknown"
msgstr "Nieznany"
@ -7155,6 +7314,10 @@ msgstr "Nieograniczone"
msgid "Unlimited documents, API and more"
msgstr "Nieograniczone dokumenty, API i więcej"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Unpin"
msgstr "Odepnij"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Untitled Group"
msgstr "Grupa bez tytułu"
@ -7497,6 +7660,11 @@ msgstr "Zobacz wszystkie powiązane dokumenty"
msgid "View all security activity related to your account."
msgstr "Wyświetl wszystkie aktywności związane z bezpieczeństwem twojego konta."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "View and manage all active sessions for your account."
msgstr "Przeglądaj i zarządzaj wszystkimi aktywnymi sesjami swojego konta."
#: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx
msgid "View Codes"
msgstr "Wyświetl kody"
@ -7855,7 +8023,6 @@ msgstr "Wygenerujemy dla Ciebie linki do podpisania, które możesz wysłać do
msgid "We won't send anything to notify recipients."
msgstr "Nie wyślemy nic, aby powiadomić odbiorców."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/components/tables/documents-table-empty-state.tsx
msgid "We're all empty"
@ -8215,7 +8382,6 @@ msgstr "Rozpocząłeś dokument {0}, który wymaga, abyś go {recipientActionVer
msgid "You have no webhooks yet. Your webhooks will be shown here once you create them."
msgstr "Nie masz jeszcze żadnych webhooków. Twoje webhooki będą tutaj widoczne, gdy je utworzysz."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
msgid "You have not yet created any templates. To create a template please upload one."
msgstr "Brak utworzonych szablonów. Prześlij, aby utworzyć."
@ -8317,7 +8483,6 @@ msgstr "Zweryfikowałeś swój adres e-mail dla <0>{0}</0>."
msgid "You must enter '{deleteMessage}' to proceed"
msgstr "Musisz wpisać '{deleteMessage}' aby kontynuować"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "You must type '{deleteMessage}' to confirm"
msgstr "Musisz wpisać '{deleteMessage}', by potwierdzić"

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: sq\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-06-10 02:27\n"
"PO-Revision-Date: 2025-06-19 06:05\n"
"Last-Translator: \n"
"Language-Team: Albanian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -72,6 +72,16 @@ msgstr "{0, plural, one {(1 karakter më shumë)} other {(# karaktere më shumë
msgid "{0, plural, one {# character over the limit} other {# characters over the limit}}"
msgstr "{0, plural, one {# karakter mbi kufi} other {# karaktere mbi kufi}}"
#. placeholder {0}: folder._count.documents
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# document} other {# documents}}"
msgstr "{0, plural, one {# dokument} other {# dokumente}}"
#. placeholder {0}: folder._count.subfolders
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# folder} other {# folders}}"
msgstr "{0, plural, one {# dosje} other {# dosje}}"
#. placeholder {0}: template.recipients.length
#: apps/remix/app/routes/_recipient+/d.$token+/_index.tsx
msgid "{0, plural, one {# recipient} other {# recipients}}"
@ -82,6 +92,11 @@ msgstr "{0, plural, one {# marrës} other {# marrësa}}"
msgid "{0, plural, one {# team} other {# teams}}"
msgstr "{0, plural, one {# ekip} other {# ekipe}}"
#. placeholder {0}: folder._count.templates
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "{0, plural, one {# template} other {# templates}}"
msgstr "{0, plural, one {# model} other {# modele}}"
#. placeholder {0}: data.length
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
@ -752,6 +767,11 @@ msgstr "Veprime"
msgid "Active"
msgstr "Aktive"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Active sessions"
msgstr "Seanca aktive"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Active Subscriptions"
msgstr "Abonime aktive"
@ -817,13 +837,13 @@ msgstr "Shto fusha"
msgid "Add group roles"
msgstr "Shto rolet e grupeve"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add groups"
msgstr "Shto grupet"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Add members"
msgstr "Shto anëtarët"
@ -1243,17 +1263,14 @@ msgstr "Ndodhi një gabim i papritur."
msgid "An unknown error occurred"
msgstr "Ndodhi një gabim i panjohur"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "An unknown error occurred while creating the folder."
msgstr "Një gabim i panjohur ndodhi gjatë krijimit të dosjes."
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "An unknown error occurred while deleting the folder."
msgstr "Një gabim i panjohur ndodhi gjatë fshirjes së dosjes."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "An unknown error occurred while moving the folder."
msgstr "Një gabim i panjohur ndodhi gjatë zhvendosjes së dosjes."
@ -1322,6 +1339,10 @@ msgstr "A jeni të sigurt se doni të përfundoni dokumentin? Ky veprim nuk mund
msgid "Are you sure you want to delete the following claim?"
msgstr "A jeni të sigurt që doni të fshini ankesën e mëposhtme?"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Are you sure you want to delete this folder?"
msgstr "A jeni i sigurt që doni ta fshini këtë dosje?"
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
msgid "Are you sure you want to delete this token?"
msgstr "A jeni i sigurt që doni të fshini këtë token?"
@ -1614,6 +1635,7 @@ msgstr "Mund të përgatitë"
#: apps/remix/app/components/dialogs/team-delete-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
@ -1626,6 +1648,9 @@ msgstr "Mund të përgatitë"
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/document-resend-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
@ -1921,7 +1946,6 @@ msgstr "Konfirmo duke shtypur <0>{deleteMessage}</0>"
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Confirm by typing: <0>{deleteMessage}</0>"
msgstr "Konfirmo duke shtypur: <0>{deleteMessage}</0>"
@ -2065,6 +2089,7 @@ msgstr "Kopjo token"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
msgid "Create"
msgstr "Krijo"
@ -2134,6 +2159,14 @@ msgstr "Krijo Lidhje të Drejtpërdrejtë për Nënshkrim"
msgid "Create document from template"
msgstr "Krijo dokument nga modeli"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Create folder"
msgstr "Krijo dosje"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create Folder"
msgstr "Krijo Dosje"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Create group"
@ -2143,6 +2176,10 @@ msgstr "Krijo grup"
msgid "Create Groups"
msgstr "Krijo Grupet"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Create New Folder"
msgstr "Krijo Dosje të Re"
#: apps/remix/app/routes/_profile+/_layout.tsx
msgid "Create now"
msgstr "Krijo tani"
@ -2221,6 +2258,7 @@ msgstr "Krijoni llogarinë tuaj dhe filloni të përdorni nënshkrimin e dokumen
msgid "Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp."
msgstr "Krijoni llogarinë tuaj dhe filloni të përdorni nënshkrimin e dokumenteve në gjendje të artit. Nënshkrimi i hapur dhe i bukur është brenda arritjes tuaj."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
@ -2260,6 +2298,14 @@ msgstr "Krijuar më {0}"
msgid "CSV Structure"
msgstr "Struktura CSV"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Cumulative MAU (signed in)"
msgstr ""
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Current"
msgstr "Aktual"
#: apps/remix/app/components/forms/password.tsx
msgid "Current Password"
msgstr "Fjalëkalimi aktual"
@ -2342,6 +2388,7 @@ msgstr "fshi"
#: apps/remix/app/components/tables/organisation-groups-table.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
#: apps/remix/app/components/tables/admin-claims-table.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
@ -2353,6 +2400,7 @@ msgstr "fshi"
#: apps/remix/app/components/dialogs/organisation-group-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Delete"
@ -2360,10 +2408,9 @@ msgstr "Fshi"
#. placeholder {0}: webhook.webhookUrl
#. placeholder {0}: token.name
#. placeholder {0}: folder?.name ?? 'folder'
#. placeholder {0}: organisation.name
#: apps/remix/app/components/dialogs/webhook-delete-dialog.tsx
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "delete {0}"
@ -2395,6 +2442,10 @@ msgstr "Fshi dokumentin"
msgid "Delete Document"
msgstr "Fshi Dokumentin"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Delete Folder"
msgstr "Fshij Dosjen"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
msgid "Delete organisation"
msgstr "Fshij organizatën"
@ -2453,6 +2504,7 @@ msgid "Details"
msgstr "Detaje"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/settings-security-activity-table.tsx
msgid "Device"
msgstr "Pajisje"
@ -2833,7 +2885,6 @@ msgid "Document will be permanently deleted"
msgstr "Dokumenti do të Fshihet Përgjithmonë"
#: apps/remix/app/routes/_profile+/p.$url.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
@ -3139,6 +3190,10 @@ msgstr "Aktivizimi i llogarisë rezulton që përdoruesi të jetë në gjendje t
msgid "Enclosed Document"
msgstr "Dokumenti i Bashkangjitur"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Enter a name for your new folder. Folders help you organise your items."
msgstr "Shkruani emrin për dosjen tuaj të re. Dosjet ju ndihmojnë të organizoni sendet tuaja."
#: apps/remix/app/components/forms/subscription-claim-form.tsx
msgid "Enter claim name"
msgstr "Vendos emrin e pretendimit"
@ -3179,6 +3234,7 @@ msgstr "Ndërmarrje"
#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
@ -3221,6 +3277,7 @@ msgstr "Ndërmarrje"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx
@ -3265,8 +3322,7 @@ msgstr "Skadon më {0}"
msgid "External ID"
msgstr "ID i jashtëm"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Failed to create folder"
msgstr "Dështoi krijimi i dosjes"
@ -3274,6 +3330,10 @@ msgstr "Dështoi krijimi i dosjes"
msgid "Failed to create subscription claim."
msgstr "Dështoi krijimi i pretendimit të abonimit."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Failed to delete folder"
msgstr "Deshtim në fshirjen e dosjes"
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Failed to delete subscription claim."
msgstr "Dështoi fshirja e pretendimit të abonimit."
@ -3282,14 +3342,26 @@ msgstr "Dështoi fshirja e pretendimit të abonimit."
msgid "Failed to load document"
msgstr "Dështoi ngarkimi i dokumentit"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Failed to move folder"
msgstr "Deshtim në zhvendosjen e dosjes"
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Failed to reseal document"
msgstr "Dështoi rihapja e dokumentit"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Failed to revoke session"
msgstr "Dështoi të revokojë seancën"
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Failed to save settings."
msgstr "Dështoi të ruajë parametrat."
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Failed to sign out all sessions"
msgstr "Dështoi të dilni nga të gjitha sesionet"
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
msgid "Failed to update document"
msgstr "Dështoi përditësimi i dokumentit"
@ -3386,16 +3458,28 @@ msgstr "Plotësoni detajet për të krijuar një pretendim të ri të abonimit."
msgid "Folder"
msgstr "Dosje"
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder created successfully"
msgstr "Dosja u krijua me sukses"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder deleted successfully"
msgstr "Dosja u fshi me sukses"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Folder moved successfully"
msgstr "Dosja u zhvendos me sukses"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder Name"
msgstr "Emri i Dosjes"
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder not found"
msgstr "Dosja nuk u gjet"
#: apps/remix/app/components/dialogs/template-folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
msgid "Folder updated successfully"
msgstr "Dosja përditësuar me sukses"
@ -3633,9 +3717,16 @@ msgid "Hide additional information"
msgstr "Fshih informacionin shtesë"
#: apps/remix/app/components/general/generic-error-layout.tsx
#: apps/remix/app/components/general/folder/folder-grid.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Home"
msgstr "Shtëpia"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Home (No Folder)"
msgstr "Kryefaqe (Pa Dosje)"
#: packages/lib/constants/recipient-roles.ts
msgid "I am a signer of this document"
msgstr "Unë jam një nënshkrues i këtij dokumenti"
@ -3747,7 +3838,6 @@ msgstr "Kodi i pavlefshëm. Ju lutemi provoni përsëri."
msgid "Invalid email"
msgstr "Email i pavlefshëm"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "Invalid link"
msgstr "Lidhja e pavlefshme"
@ -3811,6 +3901,7 @@ msgid "Invoice"
msgstr "Fatura"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
msgid "IP Address"
msgstr "Adresa IP"
@ -3881,6 +3972,10 @@ msgstr "30 ditët e fundit"
msgid "Last 7 days"
msgstr "7 ditët e fundit"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Last Active"
msgstr "Aktiviteti i fundit"
#: apps/remix/app/components/general/template/template-page-view-information.tsx
#: apps/remix/app/components/general/document/document-page-view-information.tsx
msgid "Last modified"
@ -4030,6 +4125,10 @@ msgstr "Menaxho çelësat e aksesit"
msgid "Manage permissions and access controls"
msgstr "Menaxhoni lejet dhe kontrollin e qasjes"
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Manage sessions"
msgstr "Menaxho seancat"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Manage subscription"
@ -4107,6 +4206,10 @@ msgstr "MAU (dokument i krijuar)"
msgid "MAU (had document completed)"
msgstr "MAU (dokument i përfunduar)"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "MAU (signed in)"
msgstr ""
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
msgid "Max"
msgstr "Maks"
@ -4177,7 +4280,9 @@ msgstr "Përdorues Aktivë Mujorë: Përdorues që kanë krijuar të paktën nj
msgid "Monthly Active Users: Users that had at least one of their documents completed"
msgstr "Përdorues Aktivë Mujorë: Përdorues që kanë përfunduar të paktën një nga dokumentet e tyre"
#: apps/remix/app/components/general/folder/folder-card.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Move"
msgstr "Zhvendos"
@ -4190,6 +4295,10 @@ msgstr "Zhvendos \"{templateTitle}\" në një dosje"
msgid "Move Document to Folder"
msgstr "Zhvendos Dokumentin në Dosje"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Move Folder"
msgstr "Zhvendos Dosjen"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
msgid "Move Template to Folder"
msgstr "Zhvendos Shabllonin në Dosje"
@ -4203,6 +4312,10 @@ msgstr "Zhvendos në Dosje"
msgid "Multiple access methods can be selected."
msgstr "Mund të zgjedhni metoda të shumta të qasje."
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "My Folder"
msgstr "Dosja Ime"
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table.tsx
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
@ -4291,6 +4404,16 @@ msgstr "Jo"
msgid "No active drafts"
msgstr "Asnjë draft aktiv"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "No folders found"
msgstr "Nuk u gjetën dosje"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
msgid "No folders found matching \"{searchTerm}\""
msgstr "Nuk u gjetën dosje që përputhen me \"{searchTerm}\""
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
msgid "No further action is required from you at this time."
@ -4600,10 +4723,18 @@ msgstr "Organizatat"
msgid "Organisations that the user is a member of."
msgstr "Organizatat të cilat përdoruesi është anëtar i. "
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your documents"
msgstr "Organizoni dokumentet tuaja"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Organise your members into groups which can be assigned to teams"
msgstr "Organizoni anëtarët tuaj në grupe që mund të caktohen në ekipe"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Organise your templates"
msgstr "Organizoni shabllonet tuaja"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
msgid "Organize your documents and templates"
msgstr "Organizoni dokumentet dhe shabllonet tuaj"
@ -4779,6 +4910,10 @@ msgstr "Zgjidhni një fjalëkalim"
msgid "Pick any of the following agreements below and start signing to get started"
msgstr "Zgjidhni ndonjë nga marrëveshjet e mëposhtme dhe filloni të nënshkruani për të nisur"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Pin"
msgstr "Vendos"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
@ -5305,7 +5440,6 @@ msgstr "Ruajtja e Dokumenteve"
msgid "Retry"
msgstr "Provoni përsëri"
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.decline.$token.tsx
@ -5321,6 +5455,7 @@ msgstr "Kthehu në Faqen Kryesore"
msgid "Return to sign in"
msgstr "Kthehu te Hyrja"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/components/general/teams/team-email-usage.tsx
msgid "Revoke"
msgstr "Revoko"
@ -5329,6 +5464,12 @@ msgstr "Revoko"
msgid "Revoke access"
msgstr "Revoko qasje"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Revoke all sessions"
msgstr "Revoko të gjitha seancat"
#: apps/remix/app/components/tables/user-organisations-table.tsx
#: apps/remix/app/components/tables/team-members-table.tsx
#: apps/remix/app/components/tables/team-groups-table.tsx
@ -5348,11 +5489,6 @@ msgstr "Roli"
msgid "Roles"
msgstr "Rolet"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Root (No Folder)"
msgstr "Rrënjë (Pa Dosje)"
#: packages/ui/primitives/data-table-pagination.tsx
msgid "Rows per page"
msgstr "Rreshta për faqe"
@ -5404,6 +5540,14 @@ msgstr "Kërko sipas ID të organizatës, emrit, ID të klientit ose email i pro
msgid "Search documents..."
msgstr "Kërko dokumente..."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Search folders..."
msgstr "Kërkoni dosje..."
#: packages/ui/components/common/language-switcher-dialog.tsx
msgid "Search languages..."
msgstr "Kërko gjuhë..."
@ -5428,6 +5572,10 @@ msgstr "Aktiviteti i Sigurisë"
msgid "Select"
msgstr "Zgjidh"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Select a destination for this folder."
msgstr "Zgjidhni një destinacion për këtë dosje."
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Select a folder to move this document to."
msgstr "Zgjidhni një dosje për të zhvendosur këtë dokument."
@ -5481,6 +5629,10 @@ msgstr "Zgjidh opsionin e paracaktuar"
msgid "Select groups"
msgstr "Zgjidh grupet"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups of members to add to the team."
msgstr "Zgjidh grupet e anëtarëve për t'i shtuar në ekip."
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select groups to add to this team"
msgstr "Zgjidh grupet për t'u shtuar në këtë ekip"
@ -5492,7 +5644,6 @@ msgid "Select members"
msgstr "Zgjidh anëtarët"
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Select members or groups of members to add to the team."
msgstr "Zgjidh anëtarët ose grupet e anëtarëve për t'i shtuar në ekip."
@ -5602,6 +5753,14 @@ msgstr "Duke Dërguar..."
msgid "Sent"
msgstr "Dërguar"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Session revoked"
msgstr "Sesion i revokuar"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "Sessions have been revoked"
msgstr "Sesione janë revokuar"
#: apps/remix/app/components/general/claim-account.tsx
msgid "Set a password"
msgstr "Vendosni një fjalëkalim"
@ -5621,6 +5780,7 @@ msgstr "Vendosni cilësitë e shabllonit tuaj dhe informacionin e marrësit"
#: apps/remix/app/components/general/app-nav-mobile.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Settings"
msgstr "Cilësimet"
@ -6318,7 +6478,6 @@ msgstr "Titulli i shabllonit"
msgid "Template updated successfully"
msgstr "Shablloni u përditësua me sukses"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
@ -6449,12 +6608,10 @@ msgstr "Ngjarjet që do të nxisin dërgimin e një webhook në URL-në tuaj."
msgid "The fields have been updated to the new field insertion method successfully"
msgstr "Fushat janë përditësuar në metodën e re të futjes së fushës me sukses"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "The folder you are trying to delete does not exist."
msgstr "Dosja që po përpiqeni të fshini nuk ekziston."
#: apps/remix/app/components/dialogs/template-folder-move-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "The folder you are trying to move does not exist."
msgstr "Dosja që po përpiqeni të zhvendosni nuk ekziston."
@ -6771,10 +6928,9 @@ msgstr "Ky email do t'i ndërlidhet marrësit që sapo ka nënshkruar dokumentin
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
msgstr "Ky fushë nuk mund të modifikohet apo të fshihet. Kur ndani lidhjen direkte të këtij model ose e shtoni në profilin tuaj publik, kushdo që e akseson mund të shkruajë emrin dhe email-in e tij, dhe të plotësojë fushat e caktuara për ta."
#: apps/remix/app/components/dialogs/template-folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "This folder name is already taken."
msgstr "Ky emër dosjeje është marrë tashmë."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "This folder contains multiple items. Deleting it will also delete all items in the folder, including nested folders and their contents."
msgstr "Kjo dosje përmban disa artikuj. Fshirja e saj do të fshijë gjithashtu të gjitha artikujt në dosje, duke përfshirë dosjet e mbivendosura dhe përmbajtjet e tyre."
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "This is how the document will reach the recipients once the document is ready for signing."
@ -6784,10 +6940,6 @@ msgstr "Kështu do të arrijë dokumenti te marrësit sapo dokumenti të jetë g
msgid "This is the claim that this organisation was initially created with. Any feature flag changes to this claim will be backported into this organisation."
msgstr "Kjo është kërkesa që kjo organizatë fillimisht u krijua me të. Çdo ndryshim në flamurët e veçorive në këtë kërkesë do të transferohet në këtë organizatë."
#: apps/remix/app/routes/_unauthenticated+/team.verify.transfer.$token.tsx
msgid "This link is invalid or has expired."
msgstr "Ky lidhje është e pavlefshme ose ka skaduar."
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "This link is invalid or has expired. Please contact your team to resend a verification."
msgstr "Ky link është i pavlefshëm ose ka skaduar. Ju lutem kontaktoni ekipin tuaj për të dërguar përsëri një verifikim."
@ -6858,6 +7010,10 @@ msgstr "Kjo do të dërgohet pronarit të dokumentit sapo dokumenti të jetë pl
msgid "This will ONLY backport feature flags which are set to true, anything disabled in the initial claim will not be backported"
msgstr "Kjo do të TRANSFEROHET VETËM flamujt e veçorive që janë vendosur në të vërtetë, çdo gjë që është çaktivizuar në kërkesën fillestare nuk do të transferohet"
#: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx
msgid "This will sign you out of all other devices. You will need to sign in again on those devices to continue using your account."
msgstr "Kjo do t'ju nxjerrë nga të gjitha pajisjet e tjera. Do të duhet të hyni përsëri në ato pajisje për të vazhduar përdorimin e llogarisë suaj."
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
#: apps/remix/app/components/tables/document-logs-table.tsx
msgid "Time"
@ -7144,6 +7300,9 @@ msgstr "I papërfunduar"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Unknown"
msgstr "E panjohur"
@ -7155,6 +7314,10 @@ msgstr "E pakufizuar"
msgid "Unlimited documents, API and more"
msgstr "Dokumente pa kufi, API dhe më shumë"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Unpin"
msgstr "Çaktivizo"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Untitled Group"
msgstr "Grup pa titull"
@ -7497,6 +7660,11 @@ msgstr "Shiko të gjitha dokumentet e lidhura"
msgid "View all security activity related to your account."
msgstr "Shiko të gjitha aktivitetet e sigurisë lidhur me llogarinë tuaj."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "View and manage all active sessions for your account."
msgstr "Shiko dhe menaxho të gjitha seancat aktive për llogarinë tuaj."
#: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx
msgid "View Codes"
msgstr "Shiko Kodet"
@ -7855,7 +8023,6 @@ msgstr "Ne do të gjenerojmë lidhje nënshkrimi për ju, të cilat mund t'i dë
msgid "We won't send anything to notify recipients."
msgstr "Ne nuk do të dërgojmë asgjë për të njoftuar pranuesit."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/components/tables/documents-table-empty-state.tsx
msgid "We're all empty"
@ -8215,7 +8382,6 @@ msgstr "Ju e keni iniciuar dokumentin {0} që ju kërkon të {recipientActionVer
msgid "You have no webhooks yet. Your webhooks will be shown here once you create them."
msgstr "Ju nuk keni ende ndonjë webhook. Webhook-et tuaja do të shfaqen këtu sapo t'i krijoni."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.f.$folderId._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
msgid "You have not yet created any templates. To create a template please upload one."
msgstr "Ju ende nuk keni krijuar ndonjë shabllon. Për të krijuar një shabllon ju lutemi ngarkoni një."
@ -8317,7 +8483,6 @@ msgstr "Ju keni verifikuar adresën tuaj të emailit për <0>{0}</0>."
msgid "You must enter '{deleteMessage}' to proceed"
msgstr "Duhet të shkruani '{deleteMessage}' për të vazhduar"
#: apps/remix/app/components/dialogs/template-folder-delete-dialog.tsx
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "You must type '{deleteMessage}' to confirm"
msgstr "Ju duhet të shkruani '{deleteMessage}' për të konfirmuar"

View File

@ -0,0 +1,29 @@
import type { ApiRequestMetadata } from '../universal/extract-request-metadata';
/**
* The minimum required fields that the parent API logger must contain.
*/
export type RootApiLog = {
ipAddress?: string;
userAgent?: string;
requestId: string;
};
/**
* The minimum API log that must be logged at the start of every API request.
*/
export type BaseApiLog = Partial<RootApiLog> & {
path: string;
auth: ApiRequestMetadata['auth'];
source: ApiRequestMetadata['source'];
userId?: number | null;
apiTokenId?: number | null;
};
/**
* The TRPC API log.
*/
export type TrpcApiLog = BaseApiLog & {
trpcMiddleware: string;
unverifiedTeamId?: number | null;
};

View File

@ -33,6 +33,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
'DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED', // When the global action authentication is updated.
'DOCUMENT_META_UPDATED', // When the document meta data is updated.
'DOCUMENT_OPENED', // When the document is opened by a recipient.
'DOCUMENT_VIEWED', // When the document is viewed by a recipient.
'DOCUMENT_RECIPIENT_REJECTED', // When a recipient rejects the document.
'DOCUMENT_RECIPIENT_COMPLETED', // When a recipient completes all their required tasks for the document.
'DOCUMENT_SENT', // When the document transitions from DRAFT to PENDING.
@ -438,6 +439,22 @@ export const ZDocumentAuditLogEventDocumentOpenedSchema = z.object({
}),
});
/**
* Event: Document viewed.
*/
export const ZDocumentAuditLogEventDocumentViewedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED),
data: ZBaseRecipientDataSchema.extend({
accessAuth: z.preprocess((unknownValue) => {
if (!unknownValue) {
return [];
}
return Array.isArray(unknownValue) ? unknownValue : [unknownValue];
}, z.array(ZRecipientAccessAuthTypesSchema)),
}),
});
/**
* Event: Document recipient completed the document (the recipient has fully actioned and completed their required steps for the document).
*/
@ -601,6 +618,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
ZDocumentAuditLogEventDocumentGlobalAuthActionUpdatedSchema,
ZDocumentAuditLogEventDocumentMetaUpdatedSchema,
ZDocumentAuditLogEventDocumentOpenedSchema,
ZDocumentAuditLogEventDocumentViewedSchema,
ZDocumentAuditLogEventDocumentRecipientCompleteSchema,
ZDocumentAuditLogEventDocumentRecipientRejectedSchema,
ZDocumentAuditLogEventDocumentSentSchema,

View File

@ -1,5 +1,7 @@
import { z } from 'zod';
import { getIpAddress } from './get-ip-address';
const ZIpSchema = z.string().ip();
export const ZRequestMetadataSchema = z.object({
@ -40,11 +42,13 @@ export type ApiRequestMetadata = {
};
export const extractRequestMetadata = (req: Request): RequestMetadata => {
const forwardedFor = req.headers.get('x-forwarded-for');
const ip = forwardedFor
?.split(',')
.map((ip) => ip.trim())
.at(0);
let ip: string | undefined = undefined;
try {
ip = getIpAddress(req);
} catch {
// Do nothing.
}
const parsedIp = ZIpSchema.safeParse(ip);

View File

@ -0,0 +1,39 @@
export const getIpAddress = (req: Request) => {
// Check for forwarded headers first (common in proxy setups)
const forwarded = req.headers.get('x-forwarded-for');
if (forwarded) {
// x-forwarded-for can contain multiple IPs, take the first one
return forwarded.split(',')[0].trim();
}
// Check for real IP header (used by some proxies)
const realIp = req.headers.get('x-real-ip');
if (realIp) {
return realIp;
}
// Check for client IP header
const clientIp = req.headers.get('x-client-ip');
if (clientIp) {
return clientIp;
}
// Check for CF-Connecting-IP (Cloudflare)
const cfConnectingIp = req.headers.get('cf-connecting-ip');
if (cfConnectingIp) {
return cfConnectingIp;
}
// Check for True-Client-IP (Akamai and Cloudflare)
const trueClientIp = req.headers.get('true-client-ip');
if (trueClientIp) {
return trueClientIp;
}
throw new Error('No IP address found');
};

View File

@ -338,6 +338,10 @@ export const formatDocumentAuditLogAction = (
anonymous: msg`Document opened`,
identified: msg`${prefix} opened the document`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED }, () => ({
anonymous: msg`Document viewed`,
identified: msg`${prefix} viewed the document`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED }, () => ({
anonymous: msg`Document title updated`,
identified: msg`${prefix} updated the document title`,

View File

@ -1,112 +1,35 @@
import Honeybadger from '@honeybadger-io/js';
import { type TransportTargetOptions, pino } from 'pino';
import { env } from './env';
export const buildLogger = () => {
if (env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY')) {
return new HoneybadgerLogger();
}
const transports: TransportTargetOptions[] = [];
return new DefaultLogger();
};
interface LoggerDescriptionOptions {
method?: string;
path?: string;
context?: Record<string, unknown>;
/**
* The type of log to be captured.
*
* Defaults to `info`.
*/
level?: 'info' | 'error' | 'critical';
if (env('NODE_ENV') !== 'production' && !env('INTERNAL_FORCE_JSON_LOGGER')) {
transports.push({
target: 'pino-pretty',
level: 'info',
});
}
/**
* Basic logger implementation intended to be used in the server side for capturing
* explicit errors and other logs.
*
* Not intended to capture the request and responses.
*/
interface Logger {
log(message: string, options?: LoggerDescriptionOptions): void;
const loggingFilePath = env('NEXT_PRIVATE_LOGGER_FILE_PATH');
error(error: Error, options?: LoggerDescriptionOptions): void;
if (loggingFilePath) {
transports.push({
target: 'pino/file',
level: 'info',
options: {
destination: loggingFilePath,
mkdir: true,
},
});
}
class DefaultLogger implements Logger {
log(_message: string, _options?: LoggerDescriptionOptions) {
// Do nothing.
}
error(_error: Error, _options?: LoggerDescriptionOptions): void {
// Do nothing.
}
}
class HoneybadgerLogger implements Logger {
constructor() {
if (!env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY')) {
throw new Error('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY is not set');
}
Honeybadger.configure({
apiKey: env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY'),
});
}
/**
* Honeybadger doesn't really have a non-error logging system.
*/
log(message: string, options?: LoggerDescriptionOptions) {
const { context = {}, level = 'info' } = options || {};
try {
Honeybadger.event({
message,
context: {
level,
...context,
},
});
} catch (err) {
console.error(err);
// Do nothing.
}
}
error(error: Error, options?: LoggerDescriptionOptions): void {
const { context = {}, level = 'error', method, path } = options || {};
// const tags = [`level:${level}`];
const tags = [];
let errorMessage = error.message;
if (method) {
tags.push(`method:${method}`);
errorMessage = `[${method}]: ${error.message}`;
}
if (path) {
tags.push(`path:${path}`);
}
try {
Honeybadger.notify(errorMessage, {
context: {
level,
...context,
},
tags,
});
} catch (err) {
console.error(err);
// Do nothing.
}
}
}
export const logger = pino({
level: 'info',
transport:
transports.length > 0
? {
targets: transports,
}
: undefined,
});

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "UserSecurityAuditLogType" ADD VALUE 'SESSION_REVOKED';

View File

@ -97,6 +97,7 @@ enum UserSecurityAuditLogType {
PASSKEY_UPDATED
PASSWORD_RESET
PASSWORD_UPDATE
SESSION_REVOKED
SIGN_OUT
SIGN_IN
SIGN_IN_FAIL

View File

@ -12,9 +12,15 @@ import {
export const createAdminOrganisationRoute = adminProcedure
.input(ZCreateAdminOrganisationRequestSchema)
.output(ZCreateAdminOrganisationResponseSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const { ownerUserId, data } = input;
ctx.logger.info({
input: {
ownerUserId,
},
});
const organisation = await createOrganisation({
userId: ownerUserId,
name: data.name,

View File

@ -11,9 +11,15 @@ import {
export const createStripeCustomerRoute = adminProcedure
.input(ZCreateStripeCustomerRequestSchema)
.output(ZCreateStripeCustomerResponseSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const { organisationId } = input;
ctx.logger.info({
input: {
organisationId,
},
});
const organisation = await prisma.organisation.findUnique({
where: {
id: organisationId,

Some files were not shown because too many files have changed in this diff Show More