mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
12 Commits
feat/handl
...
v1.12.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
| 85ac65e405 | |||
| e07a497b69 | |||
| 21dc4eee62 | |||
| dc2042a1ee | |||
| bb9ba80edb | |||
| bfe8c674f2 | |||
| ebe1baf0a0 | |||
| 2345de679b | |||
| 1be0e2842c | |||
| 29a03d4ec7 | |||
| 039cd7d449 | |||
| 484f6c8b85 |
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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`),
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
249
apps/remix/app/components/general/folder/folder-grid.tsx
Normal file
249
apps/remix/app/components/general/folder/folder-grid.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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: {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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={_(
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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.4"
|
||||
"version": "1.12.0-rc.6"
|
||||
}
|
||||
|
||||
BIN
apps/remix/public/fonts/caveat-variablefont_wght.ttf
Normal file
BIN
apps/remix/public/fonts/caveat-variablefont_wght.ttf
Normal file
Binary file not shown.
BIN
apps/remix/public/fonts/inter-italic-variablefont_opsz,wght.ttf
Normal file
BIN
apps/remix/public/fonts/inter-italic-variablefont_opsz,wght.ttf
Normal file
Binary file not shown.
BIN
apps/remix/public/fonts/inter-variablefont_opsz,wght.ttf
Normal file
BIN
apps/remix/public/fonts/inter-variablefont_opsz,wght.ttf
Normal file
Binary file not shown.
@ -1,10 +1,15 @@
|
||||
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 { logger } from '@documenso/lib/utils/logger';
|
||||
import { openApiDocument } from '@documenso/trpc/server/open-api';
|
||||
|
||||
import { filesRoute } from './api/files';
|
||||
@ -14,13 +19,29 @@ 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) => {
|
||||
return c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown';
|
||||
},
|
||||
message: {
|
||||
error: 'Too many requests, please try again later.',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Attach session and context to requests.
|
||||
*/
|
||||
@ -31,6 +52,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);
|
||||
|
||||
208
package-lock.json
generated
208
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "1.12.0-rc.4",
|
||||
"version": "1.12.0-rc.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "1.12.0-rc.4",
|
||||
"version": "1.12.0-rc.6",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@ -89,7 +89,7 @@
|
||||
},
|
||||
"apps/remix": {
|
||||
"name": "@documenso/remix",
|
||||
"version": "1.12.0-rc.4",
|
||||
"version": "1.12.0-rc.6",
|
||||
"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",
|
||||
@ -13311,6 +13312,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 +14946,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 +16127,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 +18258,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 +18332,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 +20280,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 +21691,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"
|
||||
@ -26074,6 +26121,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 +26949,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 +27902,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 +28155,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 +29552,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 +30612,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 +30686,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 +31253,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",
|
||||
@ -32340,6 +32527,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",
|
||||
@ -36005,6 +36201,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",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.12.0-rc.4",
|
||||
"version": "1.12.0-rc.6",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --filter=@documenso/remix",
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -5,6 +5,7 @@ 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 { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { logger } from '@documenso/lib/utils/logger';
|
||||
|
||||
type B = {
|
||||
// appRoute: any;
|
||||
@ -62,6 +63,17 @@ export const authenticatedMiddleware = <
|
||||
},
|
||||
};
|
||||
|
||||
// Todo: Get from Hono context instead.
|
||||
logger.info({
|
||||
ipAddress: metadata.requestMetadata.ipAddress,
|
||||
userAgent: metadata.requestMetadata.userAgent,
|
||||
auth: 'api',
|
||||
source: 'apiV1',
|
||||
path: request.url,
|
||||
userId: apiToken.user.id,
|
||||
apiTokenId: apiToken.id,
|
||||
});
|
||||
|
||||
return await handler(
|
||||
{
|
||||
...args,
|
||||
|
||||
@ -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();
|
||||
|
||||
BIN
packages/assets/fonts/caveat-variablefont_wght.ttf
Normal file
BIN
packages/assets/fonts/caveat-variablefont_wght.ttf
Normal file
Binary file not shown.
BIN
packages/assets/fonts/inter-italic-variablefont_opsz,wght.ttf
Normal file
BIN
packages/assets/fonts/inter-italic-variablefont_opsz,wght.ttf
Normal file
Binary file not shown.
BIN
packages/assets/fonts/inter-variablefont_opsz,wght.ttf
Normal file
BIN
packages/assets/fonts/inter-variablefont_opsz,wght.ttf
Normal file
Binary file not shown.
@ -27,7 +27,6 @@
|
||||
"@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 +36,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 +44,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 +61,4 @@
|
||||
"@types/luxon": "^3.3.1",
|
||||
"@types/pg": "^8.11.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}));
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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 ""
|
||||
|
||||
#. placeholder {0}: folder._count.subfolders
|
||||
#: apps/remix/app/components/general/folder/folder-card.tsx
|
||||
msgid "{0, plural, one {# folder} other {# folders}}"
|
||||
msgstr ""
|
||||
|
||||
#. 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 ""
|
||||
|
||||
#. placeholder {0}: data.length
|
||||
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
|
||||
@ -1248,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."
|
||||
@ -1327,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 ""
|
||||
|
||||
#: 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?"
|
||||
@ -1632,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
|
||||
@ -1927,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>"
|
||||
@ -2071,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"
|
||||
@ -2140,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 ""
|
||||
|
||||
#: 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"
|
||||
@ -2149,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 ""
|
||||
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create now"
|
||||
msgstr "Jetzt erstellen"
|
||||
@ -2267,6 +2298,10 @@ msgstr "Erstellt am {0}"
|
||||
msgid "CSV Structure"
|
||||
msgstr "CSV-Struktur"
|
||||
|
||||
#: 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 ""
|
||||
@ -2353,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
|
||||
@ -2364,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"
|
||||
@ -2371,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}"
|
||||
@ -2406,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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
|
||||
msgid "Delete organisation"
|
||||
msgstr "Organisation löschen"
|
||||
@ -2845,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
|
||||
@ -3151,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 ""
|
||||
|
||||
#: apps/remix/app/components/forms/subscription-claim-form.tsx
|
||||
msgid "Enter claim name"
|
||||
msgstr "Anspruchsname eingeben"
|
||||
@ -3279,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"
|
||||
|
||||
@ -3288,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 ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
|
||||
msgid "Failed to delete subscription claim."
|
||||
msgstr "Fehler beim Löschen des Abonnementsanspruchs."
|
||||
@ -3296,6 +3342,10 @@ 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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
|
||||
msgid "Failed to reseal document"
|
||||
msgstr "Dokument konnte nicht erneut versiegelt werden"
|
||||
@ -3408,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 ""
|
||||
|
||||
#: 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 "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"
|
||||
@ -3655,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 ""
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "I am a signer of this document"
|
||||
msgstr "Ich bin ein Unterzeichner dieses Dokuments"
|
||||
@ -4137,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 ""
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
|
||||
msgid "Max"
|
||||
msgstr "Max"
|
||||
@ -4207,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"
|
||||
@ -4220,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 ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
|
||||
msgid "Move Template to Folder"
|
||||
msgstr "Vorlage in Ordner verschieben"
|
||||
@ -4233,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 ""
|
||||
|
||||
#: 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
|
||||
@ -4321,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 ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: 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."
|
||||
@ -4630,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 ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Organize your documents and templates"
|
||||
msgstr "Organisieren Sie Ihre Dokumente und Vorlagen"
|
||||
@ -4809,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 ""
|
||||
|
||||
#: 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
|
||||
@ -5384,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"
|
||||
@ -5440,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 ""
|
||||
|
||||
#: packages/ui/components/common/language-switcher-dialog.tsx
|
||||
msgid "Search languages..."
|
||||
msgstr "Sprachen suchen..."
|
||||
@ -5464,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 ""
|
||||
|
||||
#: 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."
|
||||
@ -5668,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"
|
||||
|
||||
@ -6365,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
|
||||
@ -6496,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."
|
||||
@ -6824,10 +6934,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 ""
|
||||
|
||||
#: 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."
|
||||
@ -7211,6 +7320,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 ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
|
||||
msgid "Untitled Group"
|
||||
msgstr "Unbetitelte Gruppe"
|
||||
@ -7916,7 +8029,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"
|
||||
@ -8276,7 +8388,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."
|
||||
@ -8378,7 +8489,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"
|
||||
|
||||
@ -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
|
||||
@ -1243,17 +1258,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."
|
||||
@ -1322,6 +1334,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?"
|
||||
@ -1627,6 +1643,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
|
||||
@ -1922,7 +1941,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>"
|
||||
@ -2066,6 +2084,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"
|
||||
@ -2135,6 +2154,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"
|
||||
@ -2144,6 +2171,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"
|
||||
@ -2262,6 +2293,10 @@ 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"
|
||||
@ -2348,6 +2383,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
|
||||
@ -2359,6 +2395,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"
|
||||
@ -2366,10 +2403,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}"
|
||||
@ -2401,6 +2437,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"
|
||||
@ -2840,7 +2880,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
|
||||
@ -3146,6 +3185,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"
|
||||
@ -3274,8 +3317,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"
|
||||
|
||||
@ -3283,6 +3325,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."
|
||||
@ -3291,6 +3337,10 @@ 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"
|
||||
@ -3403,16 +3453,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"
|
||||
@ -3650,9 +3712,16 @@ 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"
|
||||
@ -4132,6 +4201,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"
|
||||
@ -4202,7 +4275,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"
|
||||
@ -4215,6 +4290,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"
|
||||
@ -4228,6 +4307,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
|
||||
@ -4316,6 +4399,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."
|
||||
@ -4625,10 +4718,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"
|
||||
@ -4804,6 +4905,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
|
||||
@ -5379,11 +5484,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"
|
||||
@ -5435,6 +5535,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..."
|
||||
@ -5459,6 +5567,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."
|
||||
@ -5663,6 +5775,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"
|
||||
|
||||
@ -6360,7 +6473,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
|
||||
@ -6491,12 +6603,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."
|
||||
@ -6831,10 +6941,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."
|
||||
@ -7218,6 +7327,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"
|
||||
@ -7923,7 +8036,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"
|
||||
@ -8283,7 +8395,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."
|
||||
@ -8385,7 +8496,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"
|
||||
|
||||
@ -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 ""
|
||||
|
||||
#. placeholder {0}: folder._count.subfolders
|
||||
#: apps/remix/app/components/general/folder/folder-card.tsx
|
||||
msgid "{0, plural, one {# folder} other {# folders}}"
|
||||
msgstr ""
|
||||
|
||||
#. 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 ""
|
||||
|
||||
#. placeholder {0}: data.length
|
||||
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
|
||||
@ -1248,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."
|
||||
@ -1327,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 ""
|
||||
|
||||
#: 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?"
|
||||
@ -1632,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
|
||||
@ -1927,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>"
|
||||
@ -2071,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"
|
||||
@ -2140,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 ""
|
||||
|
||||
#: 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"
|
||||
@ -2149,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 ""
|
||||
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create now"
|
||||
msgstr "Crear ahora"
|
||||
@ -2267,6 +2298,10 @@ msgstr "Creado el {0}"
|
||||
msgid "CSV Structure"
|
||||
msgstr "Estructura 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 ""
|
||||
@ -2353,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
|
||||
@ -2364,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"
|
||||
@ -2371,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}"
|
||||
@ -2406,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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
|
||||
msgid "Delete organisation"
|
||||
msgstr "Eliminar organización"
|
||||
@ -2845,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
|
||||
@ -3151,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 ""
|
||||
|
||||
#: apps/remix/app/components/forms/subscription-claim-form.tsx
|
||||
msgid "Enter claim name"
|
||||
msgstr "Ingresar nombre de la reclamación"
|
||||
@ -3279,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"
|
||||
|
||||
@ -3288,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 ""
|
||||
|
||||
#: 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."
|
||||
@ -3296,6 +3342,10 @@ 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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
|
||||
msgid "Failed to reseal document"
|
||||
msgstr "Falló al volver a sellar el documento"
|
||||
@ -3408,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 ""
|
||||
|
||||
#: 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 "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"
|
||||
@ -3655,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 ""
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "I am a signer of this document"
|
||||
msgstr "Soy un firmante de este documento"
|
||||
@ -4137,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 ""
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
|
||||
msgid "Max"
|
||||
msgstr "Máx"
|
||||
@ -4207,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"
|
||||
@ -4220,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 ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
|
||||
msgid "Move Template to Folder"
|
||||
msgstr "Mover Plantilla a Carpeta"
|
||||
@ -4233,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 ""
|
||||
|
||||
#: 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
|
||||
@ -4321,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 ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: 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."
|
||||
@ -4630,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 ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Organize your documents and templates"
|
||||
msgstr "Organiza tus documentos y plantillas"
|
||||
@ -4809,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 ""
|
||||
|
||||
#: 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
|
||||
@ -5384,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"
|
||||
@ -5440,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 ""
|
||||
|
||||
#: packages/ui/components/common/language-switcher-dialog.tsx
|
||||
msgid "Search languages..."
|
||||
msgstr "Buscar idiomas..."
|
||||
@ -5464,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 ""
|
||||
|
||||
#: 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."
|
||||
@ -5668,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"
|
||||
|
||||
@ -6365,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
|
||||
@ -6496,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."
|
||||
@ -6826,10 +6936,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 ""
|
||||
|
||||
#: 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."
|
||||
@ -7213,6 +7322,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 ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
|
||||
msgid "Untitled Group"
|
||||
msgstr "Grupo sin título"
|
||||
@ -7918,7 +8031,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"
|
||||
@ -8278,7 +8390,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."
|
||||
@ -8380,7 +8491,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"
|
||||
|
||||
@ -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 ""
|
||||
|
||||
#. placeholder {0}: folder._count.subfolders
|
||||
#: apps/remix/app/components/general/folder/folder-card.tsx
|
||||
msgid "{0, plural, one {# folder} other {# folders}}"
|
||||
msgstr ""
|
||||
|
||||
#. 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 ""
|
||||
|
||||
#. placeholder {0}: data.length
|
||||
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
|
||||
@ -1248,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."
|
||||
@ -1327,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 ""
|
||||
|
||||
#: 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 ?"
|
||||
@ -1632,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
|
||||
@ -1927,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>"
|
||||
@ -2071,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"
|
||||
@ -2140,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 ""
|
||||
|
||||
#: 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"
|
||||
@ -2149,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 ""
|
||||
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create now"
|
||||
msgstr "Créer maintenant"
|
||||
@ -2267,6 +2298,10 @@ msgstr "Créé le {0}"
|
||||
msgid "CSV Structure"
|
||||
msgstr "Structure 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 ""
|
||||
@ -2353,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
|
||||
@ -2364,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"
|
||||
@ -2371,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}"
|
||||
@ -2406,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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
|
||||
msgid "Delete organisation"
|
||||
msgstr "Supprimer l'organisation"
|
||||
@ -2845,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
|
||||
@ -3151,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 ""
|
||||
|
||||
#: apps/remix/app/components/forms/subscription-claim-form.tsx
|
||||
msgid "Enter claim name"
|
||||
msgstr "Entrez le nom de la réclamation"
|
||||
@ -3279,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"
|
||||
|
||||
@ -3288,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 ""
|
||||
|
||||
#: 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."
|
||||
@ -3296,6 +3342,10 @@ 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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
|
||||
msgid "Failed to reseal document"
|
||||
msgstr "Échec du reseal du document"
|
||||
@ -3408,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 ""
|
||||
|
||||
#: 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 "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"
|
||||
@ -3655,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 ""
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "I am a signer of this document"
|
||||
msgstr "Je suis un signataire de ce document"
|
||||
@ -4137,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 ""
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
|
||||
msgid "Max"
|
||||
msgstr "Maximum"
|
||||
@ -4207,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"
|
||||
@ -4220,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 ""
|
||||
|
||||
#: 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"
|
||||
@ -4233,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 ""
|
||||
|
||||
#: 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
|
||||
@ -4321,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 ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: 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."
|
||||
@ -4630,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 ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Organize your documents and templates"
|
||||
msgstr "Organisez vos documents et modèles"
|
||||
@ -4809,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 ""
|
||||
|
||||
#: 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
|
||||
@ -5384,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"
|
||||
@ -5440,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 ""
|
||||
|
||||
#: packages/ui/components/common/language-switcher-dialog.tsx
|
||||
msgid "Search languages..."
|
||||
msgstr "Rechercher des langues..."
|
||||
@ -5464,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 ""
|
||||
|
||||
#: 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."
|
||||
@ -5668,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"
|
||||
|
||||
@ -6365,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
|
||||
@ -6496,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."
|
||||
@ -6824,10 +6934,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 ""
|
||||
|
||||
#: 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."
|
||||
@ -7211,6 +7320,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 ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
|
||||
msgid "Untitled Group"
|
||||
msgstr "Groupe sans titre"
|
||||
@ -7916,7 +8029,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"
|
||||
@ -8276,7 +8388,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."
|
||||
@ -8378,7 +8489,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"
|
||||
|
||||
@ -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 ""
|
||||
|
||||
#. placeholder {0}: folder._count.subfolders
|
||||
#: apps/remix/app/components/general/folder/folder-card.tsx
|
||||
msgid "{0, plural, one {# folder} other {# folders}}"
|
||||
msgstr ""
|
||||
|
||||
#. 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 ""
|
||||
|
||||
#. placeholder {0}: data.length
|
||||
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
|
||||
@ -1248,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."
|
||||
@ -1327,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 ""
|
||||
|
||||
#: 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?"
|
||||
@ -1632,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
|
||||
@ -1927,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>"
|
||||
@ -2071,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"
|
||||
@ -2140,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 ""
|
||||
|
||||
#: 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"
|
||||
@ -2149,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 ""
|
||||
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create now"
|
||||
msgstr "Crea ora"
|
||||
@ -2267,6 +2298,10 @@ 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 ""
|
||||
@ -2353,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
|
||||
@ -2364,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"
|
||||
@ -2371,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}"
|
||||
@ -2406,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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
|
||||
msgid "Delete organisation"
|
||||
msgstr "Elimina organizzazione"
|
||||
@ -2845,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
|
||||
@ -3151,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 ""
|
||||
|
||||
#: apps/remix/app/components/forms/subscription-claim-form.tsx
|
||||
msgid "Enter claim name"
|
||||
msgstr "Inserisci nome richiesta"
|
||||
@ -3279,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"
|
||||
|
||||
@ -3288,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 ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
|
||||
msgid "Failed to delete subscription claim."
|
||||
msgstr "Eliminazione di rivendicazione di abbonamento fallita."
|
||||
@ -3296,6 +3342,10 @@ 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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
|
||||
msgid "Failed to reseal document"
|
||||
msgstr "Fallito il risigillo del documento"
|
||||
@ -3408,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 ""
|
||||
|
||||
#: 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 "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"
|
||||
@ -3655,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 ""
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "I am a signer of this document"
|
||||
msgstr "Sono un firmatario di questo documento"
|
||||
@ -4137,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 ""
|
||||
@ -4207,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"
|
||||
@ -4220,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 ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
|
||||
msgid "Move Template to Folder"
|
||||
msgstr "Sposta modello nella cartella"
|
||||
@ -4233,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 ""
|
||||
|
||||
#: 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
|
||||
@ -4321,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 ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: 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."
|
||||
@ -4630,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 ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Organize your documents and templates"
|
||||
msgstr "Organizza i tuoi documenti e modelli."
|
||||
@ -4809,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 ""
|
||||
|
||||
#: 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
|
||||
@ -5384,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"
|
||||
@ -5440,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 ""
|
||||
|
||||
#: packages/ui/components/common/language-switcher-dialog.tsx
|
||||
msgid "Search languages..."
|
||||
msgstr "Cerca lingue..."
|
||||
@ -5464,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 ""
|
||||
|
||||
#: 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."
|
||||
@ -5668,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"
|
||||
|
||||
@ -6365,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
|
||||
@ -6496,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."
|
||||
@ -6836,10 +6946,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 ""
|
||||
|
||||
#: 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."
|
||||
@ -7223,6 +7332,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 ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
|
||||
msgid "Untitled Group"
|
||||
msgstr "Gruppo senza nome"
|
||||
@ -7928,7 +8041,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"
|
||||
@ -8288,7 +8400,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."
|
||||
@ -8390,7 +8501,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"
|
||||
|
||||
@ -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 ""
|
||||
|
||||
#. placeholder {0}: folder._count.subfolders
|
||||
#: apps/remix/app/components/general/folder/folder-card.tsx
|
||||
msgid "{0, plural, one {# folder} other {# folders}}"
|
||||
msgstr ""
|
||||
|
||||
#. 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 ""
|
||||
|
||||
#. placeholder {0}: data.length
|
||||
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
|
||||
@ -1248,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."
|
||||
@ -1327,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 ""
|
||||
|
||||
#: 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?"
|
||||
@ -1632,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
|
||||
@ -1927,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>"
|
||||
@ -2071,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"
|
||||
@ -2140,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 ""
|
||||
|
||||
#: 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"
|
||||
@ -2149,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 ""
|
||||
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create now"
|
||||
msgstr "Utwórz teraz"
|
||||
@ -2267,6 +2298,10 @@ msgstr "Utworzono {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 ""
|
||||
@ -2353,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
|
||||
@ -2364,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"
|
||||
@ -2371,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}"
|
||||
@ -2406,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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
|
||||
msgid "Delete organisation"
|
||||
msgstr "Usuń organizację"
|
||||
@ -2845,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
|
||||
@ -3151,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 ""
|
||||
|
||||
#: apps/remix/app/components/forms/subscription-claim-form.tsx
|
||||
msgid "Enter claim name"
|
||||
msgstr "Wprowadź nazwę roszczenia"
|
||||
@ -3279,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"
|
||||
|
||||
@ -3288,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 ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
|
||||
msgid "Failed to delete subscription claim."
|
||||
msgstr "Nie udało się usunąć roszczenia subskrypcyjnego."
|
||||
@ -3296,6 +3342,10 @@ 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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
|
||||
msgid "Failed to reseal document"
|
||||
msgstr "Nie udało się ponownie zaplombować dokumentu"
|
||||
@ -3408,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 ""
|
||||
|
||||
#: 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 "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"
|
||||
@ -3655,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 ""
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts
|
||||
msgid "I am a signer of this document"
|
||||
msgstr "Jestem sygnatariuszem tego dokumentu"
|
||||
@ -4137,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 ""
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
|
||||
msgid "Max"
|
||||
msgstr "Max"
|
||||
@ -4207,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ś"
|
||||
@ -4220,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 ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
|
||||
msgid "Move Template to Folder"
|
||||
msgstr "Przenieś szablon do folderu"
|
||||
@ -4233,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 ""
|
||||
|
||||
#: 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
|
||||
@ -4321,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 ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: 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."
|
||||
@ -4630,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 ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx
|
||||
msgid "Organize your documents and templates"
|
||||
msgstr "Organizuj swoje dokumenty i szablony"
|
||||
@ -4809,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 ""
|
||||
|
||||
#: 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
|
||||
@ -5384,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ę"
|
||||
@ -5440,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 ""
|
||||
|
||||
#: packages/ui/components/common/language-switcher-dialog.tsx
|
||||
msgid "Search languages..."
|
||||
msgstr "Szukaj języków..."
|
||||
@ -5464,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 ""
|
||||
|
||||
#: 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."
|
||||
@ -5668,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"
|
||||
|
||||
@ -6365,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
|
||||
@ -6496,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."
|
||||
@ -6824,10 +6934,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 ""
|
||||
|
||||
#: 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."
|
||||
@ -7211,6 +7320,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 ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
|
||||
msgid "Untitled Group"
|
||||
msgstr "Grupa bez tytułu"
|
||||
@ -7916,7 +8029,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"
|
||||
@ -8276,7 +8388,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ć."
|
||||
@ -8378,7 +8489,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ć"
|
||||
|
||||
112
packages/lib/utils/logger-legacy.ts
Normal file
112
packages/lib/utils/logger-legacy.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import Honeybadger from '@honeybadger-io/js';
|
||||
|
||||
import { env } from './env';
|
||||
|
||||
export const buildLogger = () => {
|
||||
if (env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY')) {
|
||||
return new HoneybadgerLogger();
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
error(error: Error, options?: LoggerDescriptionOptions): void;
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,112 +1,27 @@
|
||||
import Honeybadger from '@honeybadger-io/js';
|
||||
import { pino } from 'pino';
|
||||
|
||||
import { env } from './env';
|
||||
// const transports: TransportTargetOptions[] = [];
|
||||
|
||||
export const buildLogger = () => {
|
||||
if (env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY')) {
|
||||
return new HoneybadgerLogger();
|
||||
}
|
||||
// if (env('NEXT_PRIVATE_LOGGING_DEV')) {
|
||||
// transports.push({
|
||||
// target: 'pino-pretty',
|
||||
// level: 'info',
|
||||
// });
|
||||
// }
|
||||
|
||||
return new DefaultLogger();
|
||||
};
|
||||
// const loggingFilePath = env('NEXT_PRIVATE_LOGGING_FILE_PATH');
|
||||
|
||||
interface LoggerDescriptionOptions {
|
||||
method?: string;
|
||||
path?: string;
|
||||
context?: Record<string, unknown>;
|
||||
// if (loggingFilePath) {
|
||||
// transports.push({
|
||||
// target: 'pino/file',
|
||||
// level: 'info',
|
||||
// options: {
|
||||
// destination: loggingFilePath,
|
||||
// mkdir: true,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* The type of log to be captured.
|
||||
*
|
||||
* Defaults to `info`.
|
||||
*/
|
||||
level?: 'info' | 'error' | 'critical';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
error(error: Error, options?: LoggerDescriptionOptions): void;
|
||||
}
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import type { Session } from '@prisma/client';
|
||||
import type { Context } from 'hono';
|
||||
import type { Logger } from 'pino';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
// This is a bit nasty. Todo: Extract
|
||||
import type { HonoEnv } from '@documenso/remix/server/router';
|
||||
|
||||
type CreateTrpcContextOptions = {
|
||||
c: Context;
|
||||
c: Context<HonoEnv>;
|
||||
requestSource: 'app' | 'apiV1' | 'apiV2';
|
||||
};
|
||||
|
||||
@ -19,6 +22,7 @@ export const createTrpcContext = async ({
|
||||
const { session, user } = await getOptionalSession(c);
|
||||
|
||||
const req = c.req.raw;
|
||||
const logger = c.get('logger');
|
||||
|
||||
const metadata: ApiRequestMetadata = {
|
||||
requestMetadata: extractRequestMetadata(req),
|
||||
@ -36,6 +40,7 @@ export const createTrpcContext = async ({
|
||||
|
||||
if (!session || !user) {
|
||||
return {
|
||||
logger,
|
||||
session: null,
|
||||
user: null,
|
||||
teamId,
|
||||
@ -45,6 +50,7 @@ export const createTrpcContext = async ({
|
||||
}
|
||||
|
||||
return {
|
||||
logger,
|
||||
session,
|
||||
user,
|
||||
teamId,
|
||||
@ -66,4 +72,5 @@ export type TrpcContext = (
|
||||
teamId: number | undefined;
|
||||
req: Request;
|
||||
metadata: ApiRequestMetadata;
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
35
packages/trpc/server/document-router/get-inbox-count.ts
Normal file
35
packages/trpc/server/document-router/get-inbox-count.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { DocumentStatus, RecipientRole } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZGetInboxCountRequestSchema, ZGetInboxCountResponseSchema } from './get-inbox-count.types';
|
||||
|
||||
export const getInboxCountRoute = authenticatedProcedure
|
||||
.input(ZGetInboxCountRequestSchema)
|
||||
.output(ZGetInboxCountResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { readStatus } = input ?? {};
|
||||
|
||||
const userEmail = ctx.user.email;
|
||||
|
||||
const count = await prisma.recipient.count({
|
||||
where: {
|
||||
email: userEmail,
|
||||
readStatus,
|
||||
role: {
|
||||
not: RecipientRole.CC,
|
||||
},
|
||||
document: {
|
||||
status: {
|
||||
not: DocumentStatus.DRAFT,
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
count,
|
||||
};
|
||||
});
|
||||
@ -0,0 +1,15 @@
|
||||
// import type { OpenApiMeta } from 'trpc-to-openapi';
|
||||
import { ReadStatus } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZGetInboxCountRequestSchema = z
|
||||
.object({
|
||||
readStatus: z.nativeEnum(ReadStatus).optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export const ZGetInboxCountResponseSchema = z.object({
|
||||
count: z.number(),
|
||||
});
|
||||
|
||||
export type TGetInboxCountResponse = z.infer<typeof ZGetInboxCountResponseSchema>;
|
||||
@ -28,6 +28,7 @@ import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
|
||||
import { authenticatedProcedure, procedure, router } from '../trpc';
|
||||
import { findInboxRoute } from './find-inbox';
|
||||
import { getInboxCountRoute } from './get-inbox-count';
|
||||
import {
|
||||
ZCreateDocumentRequestSchema,
|
||||
ZCreateDocumentV2RequestSchema,
|
||||
@ -59,6 +60,7 @@ import { updateDocumentRoute } from './update-document';
|
||||
export const documentRouter = router({
|
||||
inbox: {
|
||||
find: findInboxRoute,
|
||||
getCount: getInboxCountRoute,
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -65,7 +65,13 @@ const t = initTRPC
|
||||
/**
|
||||
* Middlewares
|
||||
*/
|
||||
export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
|
||||
export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path }) => {
|
||||
const logger = ctx.logger.child({
|
||||
path,
|
||||
auth: ctx.metadata.auth,
|
||||
source: ctx.metadata.source,
|
||||
});
|
||||
|
||||
const authorizationHeader = ctx.req.headers.get('authorization');
|
||||
|
||||
// Taken from `authenticatedMiddleware` in `@documenso/api/v1/middleware/authenticated.ts`.
|
||||
@ -79,6 +85,11 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
|
||||
|
||||
const apiToken = await getApiTokenByToken({ token });
|
||||
|
||||
logger.info({
|
||||
userId: apiToken.user.id,
|
||||
apiTokenId: apiToken.id,
|
||||
});
|
||||
|
||||
return await next({
|
||||
ctx: {
|
||||
...ctx,
|
||||
@ -111,6 +122,11 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
|
||||
});
|
||||
}
|
||||
|
||||
logger.info({
|
||||
userId: ctx.user.id,
|
||||
apiTokenId: null,
|
||||
});
|
||||
|
||||
return await next({
|
||||
ctx: {
|
||||
...ctx,
|
||||
|
||||
0
packages/trpc/utils/logger.ts
Normal file
0
packages/trpc/utils/logger.ts
Normal file
@ -1,7 +1,7 @@
|
||||
import type { ErrorHandlerOptions } from '@trpc/server/unstable-core-do-not-import';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { buildLogger } from '@documenso/lib/utils/logger';
|
||||
import { buildLogger } from '@documenso/lib/utils/logger-legacy';
|
||||
|
||||
const logger = buildLogger();
|
||||
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { DocumentStatus, type Field, type Recipient, SendStatus } from '@prisma/client';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
DocumentStatus,
|
||||
DocumentVisibility,
|
||||
type Field,
|
||||
type Recipient,
|
||||
SendStatus,
|
||||
TeamMemberRole,
|
||||
} from '@prisma/client';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -23,7 +23,11 @@ export const ZAddSettingsFormSchema = z.object({
|
||||
.min(1, { message: msg`Title cannot be empty`.id }),
|
||||
externalId: z.string().optional(),
|
||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema),
|
||||
globalAccessAuth: z
|
||||
.array(z.union([ZDocumentAccessAuthTypesSchema, z.literal('-1')]))
|
||||
.transform((val) => (val.length === 1 && val[0] === '-1' ? [] : val))
|
||||
.optional()
|
||||
.default([]),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema),
|
||||
meta: z.object({
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||
|
||||
@ -22,7 +22,11 @@ export const ZAddTemplateSettingsFormSchema = z.object({
|
||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||
externalId: z.string().optional(),
|
||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||
globalAccessAuth: z
|
||||
.array(z.union([ZDocumentAccessAuthTypesSchema, z.literal('-1')]))
|
||||
.transform((val) => (val.length === 1 && val[0] === '-1' ? [] : val))
|
||||
.optional()
|
||||
.default([]),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||
meta: z.object({
|
||||
subject: z.string(),
|
||||
|
||||
Reference in New Issue
Block a user