fix: wip
@ -18,7 +18,6 @@ import {
|
|||||||
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
SKIP_QUERY_BATCH_META,
|
SKIP_QUERY_BATCH_META,
|
||||||
} from '@documenso/lib/constants/trpc';
|
} from '@documenso/lib/constants/trpc';
|
||||||
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
|
|
||||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -288,9 +287,23 @@ const LanguageCommands = () => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await dynamicActivate(i18n, lang);
|
await dynamicActivate(lang);
|
||||||
await switchI18NLanguage(lang);
|
|
||||||
} catch (err) {
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('lang', lang);
|
||||||
|
|
||||||
|
const response = await fetch('/api/locale', {
|
||||||
|
method: 'post',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to set language: ${e}`);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
|
|||||||
@ -17,15 +17,14 @@ import { cn } from '@documenso/ui/lib/utils';
|
|||||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
export type DocumentUploadDropzoneProps = {
|
export type DocumentUploadDropzoneProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
team?: {
|
|
||||||
id: number;
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentUploadDropzone = ({ className, team }: DocumentUploadDropzoneProps) => {
|
export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProps) => {
|
||||||
|
const team = useOptionalCurrentTeam();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const userTimezone =
|
const userTimezone =
|
||||||
@ -70,7 +69,7 @@ export const DocumentUploadDropzone = ({ className, team }: DocumentUploadDropzo
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then(async (res) => res.json())
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error('Upload failed:', e);
|
console.error('Upload failed:', e);
|
||||||
throw new AppError('UPLOAD_FAILED');
|
throw new AppError('UPLOAD_FAILED');
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { useMemo, useTransition } from 'react';
|
|||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
import { msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import type { Team } from '@prisma/client';
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
@ -21,6 +20,7 @@ import { TableCell } from '@documenso/ui/primitives/table';
|
|||||||
|
|
||||||
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
import { DocumentsTableActionButton } from './documents-table-action-button';
|
import { DocumentsTableActionButton } from './documents-table-action-button';
|
||||||
import { DocumentsTableActionDropdown } from './documents-table-action-dropdown';
|
import { DocumentsTableActionDropdown } from './documents-table-action-dropdown';
|
||||||
@ -29,21 +29,14 @@ export type DocumentsTableProps = {
|
|||||||
data?: TFindDocumentsResponse;
|
data?: TFindDocumentsResponse;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
isLoadingError?: boolean;
|
isLoadingError?: boolean;
|
||||||
showSenderColumn?: boolean;
|
|
||||||
team?: Pick<Team, 'id' | 'url'> & { teamEmail?: string };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type DocumentsTableRow = TFindDocumentsResponse['data'][number];
|
type DocumentsTableRow = TFindDocumentsResponse['data'][number];
|
||||||
|
|
||||||
export const DocumentsTable = ({
|
export const DocumentsTable = ({ data, isLoading, isLoadingError }: DocumentsTableProps) => {
|
||||||
data,
|
|
||||||
showSenderColumn,
|
|
||||||
team,
|
|
||||||
isLoading,
|
|
||||||
isLoadingError,
|
|
||||||
}: DocumentsTableProps) => {
|
|
||||||
const { _, i18n } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
|
const team = useOptionalCurrentTeam();
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const updateSearchParams = useUpdateSearchParams();
|
const updateSearchParams = useUpdateSearchParams();
|
||||||
@ -120,7 +113,7 @@ export const DocumentsTable = ({
|
|||||||
totalPages={results.totalPages}
|
totalPages={results.totalPages}
|
||||||
onPaginationChange={onPaginationChange}
|
onPaginationChange={onPaginationChange}
|
||||||
columnVisibility={{
|
columnVisibility={{
|
||||||
sender: Boolean(showSenderColumn),
|
sender: team !== undefined,
|
||||||
}}
|
}}
|
||||||
error={{
|
error={{
|
||||||
enable: isLoadingError || false,
|
enable: isLoadingError || false,
|
||||||
|
|||||||
@ -1,24 +1,17 @@
|
|||||||
import { StrictMode, startTransition } from 'react';
|
import { StrictMode, startTransition } from 'react';
|
||||||
|
|
||||||
import { i18n } from '@lingui/core';
|
import { i18n } from '@lingui/core';
|
||||||
|
import { detect, fromHtmlTag } from '@lingui/detect-locale';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import { hydrateRoot } from 'react-dom/client';
|
import { hydrateRoot } from 'react-dom/client';
|
||||||
import { HydratedRouter } from 'react-router/dom';
|
import { HydratedRouter } from 'react-router/dom';
|
||||||
|
|
||||||
|
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// const locale = detect(fromHtmlTag('lang')) || 'en';
|
const locale = detect(fromHtmlTag('lang')) || 'en';
|
||||||
|
|
||||||
// await dynamicActivate(locale);
|
await dynamicActivate(locale);
|
||||||
|
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
// Todo: i18n
|
|
||||||
const locale = 'en';
|
|
||||||
|
|
||||||
// const { messages } = await import(`../../../packages/lib/translations/en/web.po`);
|
|
||||||
// const { messages } = await import(`../../../packages/lib/translations/${locale}/web.po`);
|
|
||||||
|
|
||||||
i18n.loadAndActivate({ locale, messages: {} });
|
|
||||||
|
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
hydrateRoot(
|
hydrateRoot(
|
||||||
|
|||||||
@ -8,6 +8,11 @@ import { renderToPipeableStream } from 'react-dom/server';
|
|||||||
import type { AppLoadContext, EntryContext } from 'react-router';
|
import type { AppLoadContext, EntryContext } from 'react-router';
|
||||||
import { ServerRouter } from 'react-router';
|
import { ServerRouter } from 'react-router';
|
||||||
|
|
||||||
|
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||||
|
import { dynamicActivate, extractLocaleData } from '@documenso/lib/utils/i18n';
|
||||||
|
|
||||||
|
import { langCookie } from './storage/lang-cookie.server';
|
||||||
|
|
||||||
export const streamTimeout = 5_000;
|
export const streamTimeout = 5_000;
|
||||||
|
|
||||||
export default async function handleRequest(
|
export default async function handleRequest(
|
||||||
@ -17,12 +22,13 @@ export default async function handleRequest(
|
|||||||
routerContext: EntryContext,
|
routerContext: EntryContext,
|
||||||
_loadContext: AppLoadContext,
|
_loadContext: AppLoadContext,
|
||||||
) {
|
) {
|
||||||
// Todo: i18n
|
let language = await langCookie.parse(request.headers.get('cookie') ?? '');
|
||||||
const locale = 'en';
|
|
||||||
// const { messages } = await import(`../../../packages/lib/translations/en/web.po`);
|
|
||||||
// const { messages } = await import(`../../../packages/lib/translations/${locale}/web.po`);
|
|
||||||
|
|
||||||
i18n.loadAndActivate({ locale, messages: {} });
|
if (!APP_I18N_OPTIONS.supportedLangs.includes(language)) {
|
||||||
|
language = extractLocaleData({ headers: request.headers }).lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dynamicActivate(language);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let shellRendered = false;
|
let shellRendered = false;
|
||||||
|
|||||||
@ -4,18 +4,22 @@ import {
|
|||||||
Outlet,
|
Outlet,
|
||||||
Scripts,
|
Scripts,
|
||||||
ScrollRestoration,
|
ScrollRestoration,
|
||||||
|
data,
|
||||||
isRouteErrorResponse,
|
isRouteErrorResponse,
|
||||||
useLoaderData,
|
useLoaderData,
|
||||||
} from 'react-router';
|
} from 'react-router';
|
||||||
import { ThemeProvider } from 'remix-themes';
|
import { ThemeProvider } from 'remix-themes';
|
||||||
|
|
||||||
import { SessionProvider } from '@documenso/lib/client-only/providers/session';
|
import { SessionProvider } from '@documenso/lib/client-only/providers/session';
|
||||||
|
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||||
|
import { extractLocaleData } from '@documenso/lib/utils/i18n';
|
||||||
import { TrpcProvider } from '@documenso/trpc/react';
|
import { TrpcProvider } from '@documenso/trpc/react';
|
||||||
import { Toaster } from '@documenso/ui/primitives/toaster';
|
import { Toaster } from '@documenso/ui/primitives/toaster';
|
||||||
import { TooltipProvider } from '@documenso/ui/primitives/tooltip';
|
import { TooltipProvider } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
import type { Route } from './+types/root';
|
import type { Route } from './+types/root';
|
||||||
import stylesheet from './app.css?url';
|
import stylesheet from './app.css?url';
|
||||||
|
import { langCookie } from './storage/lang-cookie.server';
|
||||||
import { themeSessionResolver } from './storage/theme-session.server';
|
import { themeSessionResolver } from './storage/theme-session.server';
|
||||||
|
|
||||||
export const links: Route.LinksFunction = () => [
|
export const links: Route.LinksFunction = () => [
|
||||||
@ -39,27 +43,42 @@ export const links: Route.LinksFunction = () => [
|
|||||||
export async function loader({ request, context }: Route.LoaderArgs) {
|
export async function loader({ request, context }: Route.LoaderArgs) {
|
||||||
const { getTheme } = await themeSessionResolver(request);
|
const { getTheme } = await themeSessionResolver(request);
|
||||||
|
|
||||||
return {
|
let lang = await langCookie.parse(request.headers.get('cookie') ?? '');
|
||||||
theme: getTheme(),
|
|
||||||
session: context.session,
|
if (!APP_I18N_OPTIONS.supportedLangs.includes(lang)) {
|
||||||
__ENV__: Object.fromEntries(
|
lang = extractLocaleData({ headers: request.headers });
|
||||||
Object.entries(process.env).filter(([key]) => key.startsWith('NEXT_')),
|
}
|
||||||
),
|
|
||||||
};
|
return data(
|
||||||
|
{
|
||||||
|
lang,
|
||||||
|
theme: getTheme(),
|
||||||
|
session: context.session,
|
||||||
|
__ENV__: Object.fromEntries(
|
||||||
|
Object.entries(process.env).filter(([key]) => key.startsWith('NEXT_')), // Todo: I'm pretty sure this will leak?
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Set-Cookie': await langCookie.serialize(lang),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Layout({ children }: { children: React.ReactNode }) {
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const { __ENV__, theme } = useLoaderData<typeof loader>() || {};
|
const { __ENV__, theme, lang } = useLoaderData<typeof loader>() || {};
|
||||||
|
|
||||||
// const [theme] = useTheme();
|
// const [theme] = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" data-theme={theme ?? ''}>
|
<html translate="no" lang={lang} data-theme={theme ?? ''}>
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
|
<meta name="google" content="notranslate" />
|
||||||
{/* <PreventFlashOnWrongTheme ssrTheme={Boolean(theme)} /> */}
|
{/* <PreventFlashOnWrongTheme ssrTheme={Boolean(theme)} /> */}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -1,23 +1,24 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useSearchParams } from 'react-router';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
|
||||||
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||||
import { parseToIntegerArray } from '@documenso/lib/utils/params';
|
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
|
||||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import {
|
||||||
|
type TFindDocumentsInternalResponse,
|
||||||
|
ZFindDocumentsInternalRequestSchema,
|
||||||
|
} from '@documenso/trpc/server/document-router/schema';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
|
|
||||||
import { DocumentSearch } from '~/components/(dashboard)/document-search/document-search';
|
import { DocumentSearch } from '~/components/(dashboard)/document-search/document-search';
|
||||||
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
||||||
import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
|
|
||||||
import { DocumentUploadDropzone } from '~/components/document/document-upload';
|
import { DocumentUploadDropzone } from '~/components/document/document-upload';
|
||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
import { UpcomingProfileClaimTeaser } from '~/components/general/upcoming-profile-claim-teaser';
|
|
||||||
import { DocumentsTable } from '~/components/tables/documents-table';
|
import { DocumentsTable } from '~/components/tables/documents-table';
|
||||||
import { DocumentsTableEmptyState } from '~/components/tables/documents-table-empty-state';
|
import { DocumentsTableEmptyState } from '~/components/tables/documents-table-empty-state';
|
||||||
import { DocumentsTableSenderFilter } from '~/components/tables/documents-table-sender-filter';
|
import { DocumentsTableSenderFilter } from '~/components/tables/documents-table-sender-filter';
|
||||||
@ -27,48 +28,35 @@ export function meta() {
|
|||||||
return [{ title: 'Documents' }];
|
return [{ title: 'Documents' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// searchParams?: {
|
const ZSearchParamsSchema = ZFindDocumentsInternalRequestSchema.pick({
|
||||||
// status?: ExtendedDocumentStatus;
|
status: true,
|
||||||
// period?: PeriodSelectorValue;
|
period: true,
|
||||||
// page?: string;
|
page: true,
|
||||||
// perPage?: string;
|
perPage: true,
|
||||||
// senderIds?: string;
|
senderIds: true,
|
||||||
// search?: string;
|
query: true,
|
||||||
// };
|
});
|
||||||
|
|
||||||
export default function DocumentsPage() {
|
export default function DocumentsPage() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const { user } = useSession();
|
|
||||||
const team = useOptionalCurrentTeam();
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
|
const [stats, setStats] = useState<TFindDocumentsInternalResponse['stats']>({
|
||||||
const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
|
[ExtendedDocumentStatus.DRAFT]: 0,
|
||||||
const page = Number(searchParams.page) || 1;
|
[ExtendedDocumentStatus.PENDING]: 0,
|
||||||
const perPage = Number(searchParams.perPage) || 20;
|
[ExtendedDocumentStatus.COMPLETED]: 0,
|
||||||
const senderIds = parseToIntegerArray(searchParams.senderIds ?? '');
|
[ExtendedDocumentStatus.INBOX]: 0,
|
||||||
const search = searchParams.search || '';
|
[ExtendedDocumentStatus.ALL]: 0,
|
||||||
const currentTeam = team
|
});
|
||||||
? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
|
|
||||||
: undefined;
|
|
||||||
const currentTeamMemberRole = team?.currentTeamMember?.role;
|
|
||||||
|
|
||||||
// const results = await findDocuments({
|
const findDocumentSearchParams = useMemo(
|
||||||
// status,
|
() => ZSearchParamsSchema.safeParse(Object.fromEntries(searchParams.entries())).data || {},
|
||||||
// orderBy: {
|
[searchParams],
|
||||||
// column: 'createdAt',
|
);
|
||||||
// direction: 'desc',
|
|
||||||
// },
|
|
||||||
// page,
|
|
||||||
// perPage,
|
|
||||||
// period,
|
|
||||||
// senderIds,
|
|
||||||
// query: search,
|
|
||||||
// });
|
|
||||||
|
|
||||||
const { data, isLoading, isLoadingError } = trpc.document.findDocuments.useQuery({
|
const { data, isLoading, isLoadingError } = trpc.document.findDocumentsInternal.useQuery({
|
||||||
page,
|
...findDocumentSearchParams,
|
||||||
perPage,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const getTabHref = (value: typeof status) => {
|
const getTabHref = (value: typeof status) => {
|
||||||
@ -83,83 +71,82 @@ export default function DocumentsPage() {
|
|||||||
return `${formatDocumentsPath(team?.url)}?${params.toString()}`;
|
return `${formatDocumentsPath(team?.url)}?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.stats) {
|
||||||
|
setStats(data.stats);
|
||||||
|
}
|
||||||
|
}, [data?.stats]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||||
<UpcomingProfileClaimTeaser />
|
<DocumentUploadDropzone />
|
||||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
|
||||||
<DocumentUploadDropzone team={currentTeam} />
|
|
||||||
|
|
||||||
<div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
|
<div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
{team && (
|
{team && (
|
||||||
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
|
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
|
||||||
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
|
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
|
||||||
<AvatarFallback className="text-xs text-gray-400">
|
<AvatarFallback className="text-xs text-gray-400">
|
||||||
{team.name.slice(0, 1)}
|
{team.name.slice(0, 1)}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<h1 className="text-4xl font-semibold">
|
<h1 className="text-4xl font-semibold">
|
||||||
<Trans>Documents</Trans>
|
<Trans>Documents</Trans>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="-m-1 flex flex-wrap gap-x-4 gap-y-6 overflow-hidden p-1">
|
|
||||||
<Tabs value={status} 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">todo</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={search} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="-m-1 flex flex-wrap gap-x-4 gap-y-6 overflow-hidden p-1">
|
||||||
<div>
|
<Tabs value={findDocumentSearchParams.status} className="overflow-x-auto">
|
||||||
{data && data.count === 0 ? (
|
<TabsList>
|
||||||
<DocumentsTableEmptyState status={status} />
|
{[
|
||||||
) : (
|
ExtendedDocumentStatus.INBOX,
|
||||||
<DocumentsTable
|
ExtendedDocumentStatus.PENDING,
|
||||||
data={data}
|
ExtendedDocumentStatus.COMPLETED,
|
||||||
isLoading={isLoading}
|
ExtendedDocumentStatus.DRAFT,
|
||||||
isLoadingError={isLoadingError}
|
ExtendedDocumentStatus.ALL,
|
||||||
showSenderColumn={team !== undefined}
|
].map((value) => (
|
||||||
team={currentTeam}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<div>
|
||||||
|
{data && data.count === 0 ? (
|
||||||
|
<DocumentsTableEmptyState
|
||||||
|
status={findDocumentSearchParams.status || ExtendedDocumentStatus.ALL}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DocumentsTable data={data} isLoading={isLoading} isLoadingError={isLoadingError} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
19
apps/remix/app/routes/api+/locale.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { ActionFunctionArgs } from 'react-router';
|
||||||
|
|
||||||
|
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||||
|
|
||||||
|
import { langCookie } from '~/storage/lang-cookie.server';
|
||||||
|
|
||||||
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const lang = formData.get('lang') || '';
|
||||||
|
|
||||||
|
if (!APP_I18N_OPTIONS.supportedLangs.find((l) => l === lang)) {
|
||||||
|
throw new Response('Unsupported language', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('OK', {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Set-Cookie': await langCookie.serialize(lang) },
|
||||||
|
});
|
||||||
|
};
|
||||||
6
apps/remix/app/storage/lang-cookie.server.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { createCookie } from 'react-router';
|
||||||
|
|
||||||
|
export const langCookie = createCookie('lang', {
|
||||||
|
path: '/',
|
||||||
|
maxAge: 60 * 60 * 24 * 365 * 2,
|
||||||
|
});
|
||||||
@ -16,6 +16,8 @@
|
|||||||
"@hono/node-server": "^1.13.7",
|
"@hono/node-server": "^1.13.7",
|
||||||
"@hono/trpc-server": "^0.3.4",
|
"@hono/trpc-server": "^0.3.4",
|
||||||
"@hono/zod-validator": "^0.4.2",
|
"@hono/zod-validator": "^0.4.2",
|
||||||
|
"@lingui/core": "^4.11.3",
|
||||||
|
"@lingui/detect-locale": "^4.11.1",
|
||||||
"@lingui/macro": "^4.11.3",
|
"@lingui/macro": "^4.11.3",
|
||||||
"@lingui/react": "^4.11.3",
|
"@lingui/react": "^4.11.3",
|
||||||
"@oslojs/crypto": "^1.0.1",
|
"@oslojs/crypto": "^1.0.1",
|
||||||
@ -37,7 +39,7 @@
|
|||||||
"ts-pattern": "^5.0.5"
|
"ts-pattern": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lingui/vite-plugin": "^5.1.2",
|
"@lingui/vite-plugin": "^5.2.0",
|
||||||
"@react-router/dev": "^7.1.1",
|
"@react-router/dev": "^7.1.1",
|
||||||
"@react-router/remix-routes-option-adapter": "^7.1.3",
|
"@react-router/remix-routes-option-adapter": "^7.1.3",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
@ -51,4 +53,4 @@
|
|||||||
"vite-plugin-babel-macros": "^1.0.6",
|
"vite-plugin-babel-macros": "^1.0.6",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 529 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 693 KiB |
22
apps/web/public/pdf.worker.min.js
vendored
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Documenso",
|
|
||||||
"short_name": "Documenso",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/android-chrome-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"theme_color": "#A2E771",
|
|
||||||
"background_color": "#FFFFFF",
|
|
||||||
"display": "standalone"
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 784 B |
@ -1,33 +0,0 @@
|
|||||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_1080_12656)">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.56772 0.890928C9.5882 -0.296974 11.4118 -0.296978 12.4323 0.890927L13.2272 1.81624C13.3589 1.96964 13.5596 2.0435 13.758 2.01166L14.955 1.81961C16.4917 1.57307 17.8887 2.75864 17.9154 4.33206L17.9363 5.55768C17.9398 5.76086 18.0465 5.94788 18.2188 6.0525L19.2578 6.68358C20.5916 7.49375 20.9083 9.31015 19.9288 10.5329L19.1659 11.4853C19.0394 11.6432 19.0023 11.8559 19.0678 12.048L19.4627 13.2069C19.9696 14.6947 19.0578 16.292 17.5304 16.5919L16.3406 16.8255C16.1434 16.8643 15.9798 17.0031 15.9079 17.1928L15.4738 18.3373C14.9166 19.8066 13.203 20.4374 11.8423 19.6741L10.7825 19.0796C10.6068 18.981 10.3932 18.981 10.2175 19.0796L9.15768 19.6741C7.79704 20.4374 6.08341 19.8066 5.52618 18.3373L5.09212 17.1928C5.02017 17.0031 4.8566 16.8643 4.65937 16.8255L3.46962 16.5919C1.94224 16.292 1.03044 14.6947 1.53734 13.2069L1.93219 12.048C1.99765 11.8559 1.96057 11.6432 1.8341 11.4853L1.07116 10.5329C0.0917119 9.31015 0.408373 7.49375 1.74223 6.68358L2.78123 6.0525C2.95348 5.94788 3.06024 5.76086 3.0637 5.55768L3.08456 4.33206C3.11133 2.75864 4.50829 1.57307 6.04498 1.81961L7.24197 2.01166C7.4404 2.0435 7.64105 1.96964 7.77282 1.81624L8.56772 0.890928Z" fill="url(#paint0_linear_1080_12656)"/>
|
|
||||||
<g filter="url(#filter0_di_1080_12656)">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3714 14.5609C13.5195 14.6358 13.6925 14.5149 13.6642 14.3563L13.1163 11.2805L15.4388 9.10299C15.5586 8.9907 15.4925 8.79506 15.327 8.77192L12.1176 8.32508L10.681 5.52519C10.6069 5.38093 10.3931 5.38093 10.319 5.52519L8.88116 8.32354L5.673 8.77192C5.50748 8.79506 5.44139 8.9907 5.56116 9.10299L7.8843 11.2803L7.33579 14.3563C7.30752 14.5149 7.48055 14.6358 7.62859 14.5609L10.5014 13.1083L13.3714 14.5609Z" fill="#FFFCEB"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<filter id="filter0_di_1080_12656" x="5.33521" y="5.41699" width="10.6591" height="9.90853" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
||||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
|
||||||
<feOffset dx="0.164785" dy="0.411963"/>
|
|
||||||
<feGaussianBlur stdDeviation="0.164785"/>
|
|
||||||
<feComposite in2="hardAlpha" operator="out"/>
|
|
||||||
<feColorMatrix type="matrix" values="0 0 0 0 0.414307 0 0 0 0 0.24341 0 0 0 0 0.0856598 0 0 0 0.1 0"/>
|
|
||||||
<feBlend mode="plus-darker" in2="BackgroundImageFix" result="effect1_dropShadow_1080_12656"/>
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1080_12656" result="shape"/>
|
|
||||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
|
||||||
<feOffset dx="0.164785" dy="0.164785"/>
|
|
||||||
<feGaussianBlur stdDeviation="0.0823927"/>
|
|
||||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
|
||||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0"/>
|
|
||||||
<feBlend mode="screen" in2="shape" result="effect2_innerShadow_1080_12656"/>
|
|
||||||
</filter>
|
|
||||||
<linearGradient id="paint0_linear_1080_12656" x1="12.5596" y1="-9.0568e-08" x2="6.25112" y2="19.9592" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#FFE76A"/>
|
|
||||||
<stop offset="1" stop-color="#E8C445"/>
|
|
||||||
</linearGradient>
|
|
||||||
<clipPath id="clip0_1080_12656">
|
|
||||||
<rect width="20" height="20" fill="white" transform="translate(0.5)"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
@ -1,9 +0,0 @@
|
|||||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.54474 0.890944C9.57689 -0.296979 11.4213 -0.296983 12.4535 0.890943L13.2575 1.81628C13.3908 1.96967 13.5937 2.04354 13.7944 2.0117L15.0051 1.81965C16.5593 1.57309 17.9723 2.75869 17.9994 4.33214L18.0205 5.55778C18.024 5.76096 18.1319 5.94799 18.3061 6.05261L19.357 6.6837C20.7061 7.49389 21.0264 9.31032 20.0358 10.5331L19.2641 11.4855C19.1362 11.6434 19.0987 11.8561 19.1649 12.0482L19.5643 13.2072C20.077 14.695 19.1547 16.2923 17.6099 16.5922L16.4065 16.8258C16.207 16.8646 16.0416 17.0034 15.9688 17.1931L15.5298 18.3376C14.9662 19.8069 13.233 20.4378 11.8568 19.6745L10.7848 19.08C10.6071 18.9814 10.3911 18.9814 10.2134 19.08L9.14145 19.6745C7.76525 20.4378 6.03203 19.8069 5.46842 18.3376L5.0294 17.1931C4.95662 17.0034 4.79119 16.8646 4.5917 16.8258L3.38834 16.5922C1.8435 16.2923 0.921268 14.695 1.43397 13.2072L1.83334 12.0482C1.89954 11.8561 1.86204 11.6434 1.73412 11.4855L0.962455 10.5331C-0.0281913 9.31032 0.292091 7.49389 1.6412 6.6837L2.69209 6.05261C2.8663 5.94799 2.97428 5.76096 2.97778 5.55778L2.99888 4.33214C3.02596 2.75869 4.4389 1.57309 5.99315 1.81965L7.20383 2.0117C7.40454 2.04354 7.60747 1.96967 7.74076 1.81628L8.54474 0.890944ZM13.7062 9.20711C14.0968 8.81658 14.0968 8.18342 13.7062 7.79289C13.3157 7.40237 12.6825 7.40237 12.292 7.79289L9.49912 10.5858L8.70622 9.79289C8.3157 9.40237 7.68253 9.40237 7.29201 9.79289C6.90148 10.1834 6.90148 10.8166 7.29201 11.2071L8.43846 12.3536C9.02425 12.9393 9.97399 12.9393 10.5598 12.3536L13.7062 9.20711Z" fill="url(#paint0_linear_1080_12647)"/>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="paint0_linear_1080_12647" x1="12.5823" y1="-9.05696e-08" x2="6.33214" y2="20.0004" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#96D766"/>
|
|
||||||
<stop offset="1" stop-color="#5AAE30"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 708 B |
|
Before Width: | Height: | Size: 367 B |
1371
package-lock.json
generated
@ -1,11 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await
|
|
||||||
export const switchI18NLanguage = async (lang: string) => {
|
|
||||||
// Two year expiry.
|
|
||||||
const maxAge = 60 * 60 * 24 * 365 * 2;
|
|
||||||
|
|
||||||
cookies().set('language', lang, { maxAge });
|
|
||||||
};
|
|
||||||
@ -1,19 +1,19 @@
|
|||||||
import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies';
|
|
||||||
|
|
||||||
import type { I18n, MessageDescriptor } from '@lingui/core';
|
import type { I18n, MessageDescriptor } from '@lingui/core';
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
|
||||||
import type { I18nLocaleData, SupportedLanguageCodes } from '../constants/i18n';
|
import type { I18nLocaleData, SupportedLanguageCodes } from '../constants/i18n';
|
||||||
import { APP_I18N_OPTIONS } from '../constants/i18n';
|
import { APP_I18N_OPTIONS } from '../constants/i18n';
|
||||||
import { env } from './env';
|
import { env } from './env';
|
||||||
|
|
||||||
export async function dynamicActivate(i18nInstance: I18n, locale: string) {
|
export async function dynamicActivate(locale: string) {
|
||||||
const extension = env('NODE_ENV') === 'development' ? 'po' : 'js';
|
const extension = env('NODE_ENV') === 'development' ? 'po' : 'js';
|
||||||
|
|
||||||
// const { messages } = await import(`../translations/${locale}/web.${extension}`);
|
// Todo: Use extension (currently breaks).
|
||||||
// todo
|
|
||||||
const messages = {};
|
|
||||||
|
|
||||||
i18nInstance.loadAndActivate({ locale, messages });
|
// const { messages } = await import(`../translations/${locale}/web.${extension}`);
|
||||||
|
const { messages } = await import(`../translations/${locale}/web.po`);
|
||||||
|
|
||||||
|
i18n.loadAndActivate({ locale, messages });
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseLanguageFromLocale = (locale: string): SupportedLanguageCodes | null => {
|
const parseLanguageFromLocale = (locale: string): SupportedLanguageCodes | null => {
|
||||||
@ -30,25 +30,6 @@ const parseLanguageFromLocale = (locale: string): SupportedLanguageCodes | null
|
|||||||
return foundSupportedLanguage;
|
return foundSupportedLanguage;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract the language if supported from the cookies header.
|
|
||||||
*
|
|
||||||
* Returns `null` if not supported or not found.
|
|
||||||
*/
|
|
||||||
export const extractLocaleDataFromCookies = (
|
|
||||||
cookies: ReadonlyRequestCookies,
|
|
||||||
): SupportedLanguageCodes | null => {
|
|
||||||
const preferredLocale = cookies.get('language')?.value || '';
|
|
||||||
|
|
||||||
const language = parseLanguageFromLocale(preferredLocale || '');
|
|
||||||
|
|
||||||
if (!language) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return language;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the language from the `accept-language` header.
|
* Extracts the language from the `accept-language` header.
|
||||||
*/
|
*/
|
||||||
@ -67,30 +48,24 @@ export const extractLocaleDataFromHeaders = (
|
|||||||
|
|
||||||
type ExtractLocaleDataOptions = {
|
type ExtractLocaleDataOptions = {
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
cookies: ReadonlyRequestCookies;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the supported language from the cookies, then header if not found.
|
* Extract the supported language from the header.
|
||||||
*
|
*
|
||||||
* Will return the default fallback language if not found.
|
* Will return the default fallback language if not found.
|
||||||
*/
|
*/
|
||||||
export const extractLocaleData = ({
|
export const extractLocaleData = ({ headers }: ExtractLocaleDataOptions): I18nLocaleData => {
|
||||||
headers,
|
const headerLocales = (headers.get('accept-language') ?? '').split(',');
|
||||||
cookies,
|
|
||||||
}: ExtractLocaleDataOptions): I18nLocaleData => {
|
|
||||||
let lang: SupportedLanguageCodes | null = extractLocaleDataFromCookies(cookies);
|
|
||||||
|
|
||||||
const langHeader = extractLocaleDataFromHeaders(headers);
|
const unknownLanguages = headerLocales
|
||||||
|
.map((locale) => parseLanguageFromLocale(locale))
|
||||||
if (!lang && langHeader?.lang) {
|
.filter((value): value is SupportedLanguageCodes => value !== null);
|
||||||
lang = langHeader.lang;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out locales that are not valid.
|
// Filter out locales that are not valid.
|
||||||
const locales = (langHeader?.locales ?? []).filter((locale) => {
|
const languages = (unknownLanguages ?? []).filter((language) => {
|
||||||
try {
|
try {
|
||||||
new Intl.Locale(locale);
|
new Intl.Locale(language);
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
@ -98,8 +73,8 @@ export const extractLocaleData = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lang: lang || APP_I18N_OPTIONS.sourceLang,
|
lang: languages[0] || APP_I18N_OPTIONS.sourceLang,
|
||||||
locales,
|
locales: headerLocales,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -18,11 +18,14 @@ import { findDocuments } from '@documenso/lib/server-only/document/find-document
|
|||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
||||||
|
import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats';
|
||||||
|
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
||||||
import { moveDocumentToTeam } from '@documenso/lib/server-only/document/move-document-to-team';
|
import { moveDocumentToTeam } from '@documenso/lib/server-only/document/move-document-to-team';
|
||||||
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
||||||
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
|
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
|
||||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||||
|
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||||
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
|
||||||
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||||
|
|
||||||
@ -39,6 +42,8 @@ import {
|
|||||||
ZDuplicateDocumentRequestSchema,
|
ZDuplicateDocumentRequestSchema,
|
||||||
ZDuplicateDocumentResponseSchema,
|
ZDuplicateDocumentResponseSchema,
|
||||||
ZFindDocumentAuditLogsQuerySchema,
|
ZFindDocumentAuditLogsQuerySchema,
|
||||||
|
ZFindDocumentsInternalRequestSchema,
|
||||||
|
ZFindDocumentsInternalResponseSchema,
|
||||||
ZFindDocumentsRequestSchema,
|
ZFindDocumentsRequestSchema,
|
||||||
ZFindDocumentsResponseSchema,
|
ZFindDocumentsResponseSchema,
|
||||||
ZGenericSuccessResponse,
|
ZGenericSuccessResponse,
|
||||||
@ -124,6 +129,82 @@ export const documentRouter = router({
|
|||||||
return documents;
|
return documents;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal endpoint for /documents page to additionally return getStats.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
findDocumentsInternal: authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/document',
|
||||||
|
summary: 'Find documents',
|
||||||
|
description: 'Find documents based on a search criteria',
|
||||||
|
tags: ['Document'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(ZFindDocumentsInternalRequestSchema)
|
||||||
|
.output(ZFindDocumentsInternalResponseSchema)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const { user, teamId } = ctx;
|
||||||
|
|
||||||
|
const {
|
||||||
|
query,
|
||||||
|
templateId,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
orderByDirection,
|
||||||
|
orderByColumn,
|
||||||
|
source,
|
||||||
|
status,
|
||||||
|
period,
|
||||||
|
senderIds,
|
||||||
|
} = input;
|
||||||
|
|
||||||
|
const getStatOptions: GetStatsInput = {
|
||||||
|
user,
|
||||||
|
period,
|
||||||
|
search: query,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (teamId) {
|
||||||
|
const team = await getTeamById({ userId: user.id, teamId });
|
||||||
|
|
||||||
|
getStatOptions.team = {
|
||||||
|
teamId: team.id,
|
||||||
|
teamEmail: team.teamEmail?.email,
|
||||||
|
senderIds,
|
||||||
|
currentTeamMemberRole: team.currentTeamMember?.role,
|
||||||
|
currentUserEmail: user.email,
|
||||||
|
userId: user.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [stats, documents] = await Promise.all([
|
||||||
|
getStats(getStatOptions),
|
||||||
|
findDocuments({
|
||||||
|
userId: user.id,
|
||||||
|
teamId,
|
||||||
|
templateId,
|
||||||
|
query,
|
||||||
|
source,
|
||||||
|
status,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
period,
|
||||||
|
orderBy: orderByColumn
|
||||||
|
? { column: orderByColumn, direction: orderByDirection }
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...documents,
|
||||||
|
stats,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import {
|
|||||||
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||||
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
|
|
||||||
import { ZCreateRecipientSchema } from '../recipient-router/schema';
|
import { ZCreateRecipientSchema } from '../recipient-router/schema';
|
||||||
|
|
||||||
@ -131,6 +132,25 @@ export const ZFindDocumentsResponseSchema = ZFindResultResponse.extend({
|
|||||||
|
|
||||||
export type TFindDocumentsResponse = z.infer<typeof ZFindDocumentsResponseSchema>;
|
export type TFindDocumentsResponse = z.infer<typeof ZFindDocumentsResponseSchema>;
|
||||||
|
|
||||||
|
export const ZFindDocumentsInternalRequestSchema = ZFindDocumentsRequestSchema.extend({
|
||||||
|
period: z.enum(['7d', '14d', '30d']).optional(),
|
||||||
|
senderIds: z.array(z.number()).optional(),
|
||||||
|
status: z.nativeEnum(ExtendedDocumentStatus).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ZFindDocumentsInternalResponseSchema = ZFindResultResponse.extend({
|
||||||
|
data: ZDocumentManySchema.array(),
|
||||||
|
stats: z.object({
|
||||||
|
[ExtendedDocumentStatus.DRAFT]: z.number(),
|
||||||
|
[ExtendedDocumentStatus.PENDING]: z.number(),
|
||||||
|
[ExtendedDocumentStatus.COMPLETED]: z.number(),
|
||||||
|
[ExtendedDocumentStatus.INBOX]: z.number(),
|
||||||
|
[ExtendedDocumentStatus.ALL]: z.number(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TFindDocumentsInternalResponse = z.infer<typeof ZFindDocumentsInternalResponseSchema>;
|
||||||
|
|
||||||
export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend({
|
export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend({
|
||||||
documentId: z.number().min(1),
|
documentId: z.number().min(1),
|
||||||
cursor: z.string().optional(),
|
cursor: z.string().optional(),
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { useLingui } from '@lingui/react';
|
|||||||
import { CheckIcon } from 'lucide-react';
|
import { CheckIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||||
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
|
|
||||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import {
|
import {
|
||||||
@ -25,8 +24,16 @@ export const LanguageSwitcherDialog = ({ open, setOpen }: LanguageSwitcherDialog
|
|||||||
const setLanguage = async (lang: string) => {
|
const setLanguage = async (lang: string) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
||||||
await dynamicActivate(i18n, lang);
|
await dynamicActivate(lang);
|
||||||
await switchI18NLanguage(lang);
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('lang', lang);
|
||||||
|
|
||||||
|
await fetch('/api/locale', {
|
||||||
|
method: 'post',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||