mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 17:04:12 +10:00
feat: support language in embedding (#2364)
This commit is contained in:
@@ -17,6 +17,7 @@ import { useSearchParams } from 'react-router';
|
||||
|
||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||
import { ZDirectTemplateEmbedDataSchema } from '@documenso/lib/types/embed-direct-template-schema';
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
} from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
|
||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@@ -290,12 +292,19 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
cssVars: data.cssVars,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.language && data.language !== APP_I18N_OPTIONS.sourceLang) {
|
||||
void dynamicActivate(data.language).finally(() => {
|
||||
setHasFinishedInit(true);
|
||||
});
|
||||
} else {
|
||||
setHasFinishedInit(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setHasFinishedInit(true);
|
||||
}
|
||||
|
||||
setHasFinishedInit(true);
|
||||
|
||||
// !: While the two setters are stable we still want to ensure we're avoiding
|
||||
// !: re-renders.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -8,11 +8,13 @@ import { type Field, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
|
||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { ZSignDocumentEmbedDataSchema } from '@documenso/lib/types/embed-document-sign-schema';
|
||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
|
||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@@ -232,12 +234,19 @@ export const EmbedSignDocumentV1ClientPage = ({
|
||||
cssVars: data.cssVars,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.language && data.language !== APP_I18N_OPTIONS.sourceLang) {
|
||||
void dynamicActivate(data.language).finally(() => {
|
||||
setHasFinishedInit(true);
|
||||
});
|
||||
} else {
|
||||
setHasFinishedInit(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setHasFinishedInit(true);
|
||||
}
|
||||
|
||||
setHasFinishedInit(true);
|
||||
|
||||
// !: While the two setters are stable we still want to ensure we're avoiding
|
||||
// !: re-renders.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -3,8 +3,10 @@ import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
import { ZSignDocumentEmbedDataSchema } from '@documenso/lib/types/embed-document-sign-schema';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||
|
||||
import { injectCss } from '~/utils/css-vars';
|
||||
|
||||
@@ -162,12 +164,19 @@ export const EmbedSignDocumentV2ClientPage = ({
|
||||
cssVars: data.cssVars,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.language && data.language !== APP_I18N_OPTIONS.sourceLang) {
|
||||
void dynamicActivate(data.language).finally(() => {
|
||||
setHasFinishedInit(true);
|
||||
});
|
||||
} else {
|
||||
setHasFinishedInit(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setHasFinishedInit(true);
|
||||
}
|
||||
|
||||
setHasFinishedInit(true);
|
||||
|
||||
// !: While the setters are stable we still want to ensure we're avoiding
|
||||
// !: re-renders.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -37,6 +37,7 @@ export default function EmbedPlaygroundPage() {
|
||||
() => (searchParams.get('envelopeType') as 'DOCUMENT' | 'TEMPLATE') || 'DOCUMENT',
|
||||
);
|
||||
const [folderId, setFolderId] = useState(() => searchParams.get('folderId') || '');
|
||||
const [language, setLanguage] = useState(() => searchParams.get('language') || '');
|
||||
|
||||
// Auto-launch if query params are present on mount
|
||||
const [iframeSrc, setIframeSrc] = useState<string | null>(null);
|
||||
@@ -203,6 +204,7 @@ export default function EmbedPlaygroundPage() {
|
||||
envelopeId: string;
|
||||
envelopeType: string;
|
||||
folderId: string;
|
||||
language: string;
|
||||
}) => {
|
||||
const newParams = new URLSearchParams();
|
||||
|
||||
@@ -230,6 +232,10 @@ export default function EmbedPlaygroundPage() {
|
||||
newParams.set('folderId', params.folderId);
|
||||
}
|
||||
|
||||
if (params.language) {
|
||||
newParams.set('language', params.language);
|
||||
}
|
||||
|
||||
const qs = newParams.toString();
|
||||
|
||||
void navigate(qs ? `?${qs}` : '.', { replace: true });
|
||||
@@ -270,6 +276,7 @@ export default function EmbedPlaygroundPage() {
|
||||
externalId: externalId || undefined,
|
||||
type: mode === 'create' ? envelopeType : undefined,
|
||||
folderId: mode === 'create' && folderId ? folderId : undefined,
|
||||
language: language || undefined,
|
||||
darkModeDisabled: darkModeDisabled || undefined,
|
||||
css: rawCss || undefined,
|
||||
cssVars: Object.keys(filteredCssVars).length > 0 ? filteredCssVars : undefined,
|
||||
@@ -299,7 +306,15 @@ export default function EmbedPlaygroundPage() {
|
||||
setIframeSrc(buildIframeSrc(basePath, presignToken, hash));
|
||||
setIframeKey((prev) => prev + 1);
|
||||
|
||||
updateQueryParams({ token: inputToken, externalId, mode, envelopeId, envelopeType, folderId });
|
||||
updateQueryParams({
|
||||
token: inputToken,
|
||||
externalId,
|
||||
mode,
|
||||
envelopeId,
|
||||
envelopeType,
|
||||
folderId,
|
||||
language,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
@@ -314,6 +329,7 @@ export default function EmbedPlaygroundPage() {
|
||||
envelopeId,
|
||||
envelopeType,
|
||||
folderId,
|
||||
language,
|
||||
generalFeatures,
|
||||
settingsFeatures,
|
||||
actionsFeatures,
|
||||
@@ -333,6 +349,7 @@ export default function EmbedPlaygroundPage() {
|
||||
setEnvelopeId('');
|
||||
setEnvelopeType('DOCUMENT');
|
||||
setFolderId('');
|
||||
setLanguage('');
|
||||
setIframeSrc(null);
|
||||
setMessages([]);
|
||||
setTokenError(null);
|
||||
@@ -477,6 +494,30 @@ export default function EmbedPlaygroundPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: 'bold' }}>
|
||||
Language (optional)
|
||||
</label>
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => setLanguage(e.target.value)}
|
||||
style={{ width: '100%', padding: '4px', fontSize: '12px' }}
|
||||
>
|
||||
<option value="">Default (en)</option>
|
||||
<option value="de">German (de)</option>
|
||||
<option value="en">English (en)</option>
|
||||
<option value="es">Spanish (es)</option>
|
||||
<option value="fr">French (fr)</option>
|
||||
<option value="it">Italian (it)</option>
|
||||
<option value="ja">Japanese (ja)</option>
|
||||
<option value="ko">Korean (ko)</option>
|
||||
<option value="nl">Dutch (nl)</option>
|
||||
<option value="pl">Polish (pl)</option>
|
||||
<option value="pt-BR">Portuguese - Brazil (pt-BR)</option>
|
||||
<option value="zh">Chinese (zh)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<h3 style={{ fontSize: '14px', margin: '12px 0 4px' }}>Feature Flags</h3>
|
||||
|
||||
{renderCheckboxGroup('General', generalFeatures, setGeneralFeatures)}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Outlet, useLoaderData } from 'react-router';
|
||||
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
|
||||
import { ZBaseEmbedAuthoringSchema } from '@documenso/lib/types/embed-authoring-base-schema';
|
||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||
import { TrpcProvider } from '@documenso/trpc/react';
|
||||
import { Spinner } from '@documenso/ui/primitives/spinner';
|
||||
|
||||
import { injectCss } from '~/utils/css-vars';
|
||||
|
||||
@@ -46,6 +49,8 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
|
||||
export default function AuthoringLayout() {
|
||||
const { token, hasValidToken, allowEmbedAuthoringWhiteLabel } = useLoaderData<typeof loader>();
|
||||
|
||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
try {
|
||||
const hash = window.location.hash.slice(1);
|
||||
@@ -55,10 +60,11 @@ export default function AuthoringLayout() {
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
setHasFinishedInit(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const { css, cssVars, darkModeDisabled } = result.data;
|
||||
const { css, cssVars, darkModeDisabled, language } = result.data;
|
||||
|
||||
if (darkModeDisabled) {
|
||||
document.documentElement.classList.add('dark-mode-disabled');
|
||||
@@ -70,11 +76,28 @@ export default function AuthoringLayout() {
|
||||
cssVars,
|
||||
});
|
||||
}
|
||||
|
||||
if (language && language !== APP_I18N_OPTIONS.sourceLang) {
|
||||
void dynamicActivate(language).finally(() => {
|
||||
setHasFinishedInit(true);
|
||||
});
|
||||
} else {
|
||||
setHasFinishedInit(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setHasFinishedInit(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!hasFinishedInit) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasValidToken) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { OrganisationMemberRole, OrganisationType, TeamMemberRole } from '@prisma/client';
|
||||
@@ -8,12 +8,15 @@ import { match } from 'ts-pattern';
|
||||
import { PAID_PLAN_LIMITS } from '@documenso/ee/server-only/limits/constants';
|
||||
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { OrganisationProvider } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
|
||||
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
|
||||
import { ZBaseEmbedDataSchema } from '@documenso/lib/types/embed-base-schemas';
|
||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||
import { TrpcProvider } from '@documenso/trpc/react';
|
||||
import type { OrganisationSession } from '@documenso/trpc/server/organisation-router/get-organisation-session.types';
|
||||
import { Spinner } from '@documenso/ui/primitives/spinner';
|
||||
|
||||
import { TeamProvider } from '~/providers/team';
|
||||
import { injectCss } from '~/utils/css-vars';
|
||||
@@ -60,6 +63,8 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
|
||||
export default function AuthoringLayout() {
|
||||
const { token, teamId, organisationClaim, preferences } = useLoaderData<typeof loader>();
|
||||
|
||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||
|
||||
const allowEmbedAuthoringWhiteLabel = organisationClaim.flags.embedAuthoringWhiteLabel ?? false;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -69,10 +74,11 @@ export default function AuthoringLayout() {
|
||||
const result = ZBaseEmbedDataSchema.safeParse(JSON.parse(decodeURIComponent(atob(hash))));
|
||||
|
||||
if (!result.success) {
|
||||
setHasFinishedInit(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const { css, cssVars, darkModeDisabled } = result.data;
|
||||
const { css, cssVars, darkModeDisabled, language } = result.data;
|
||||
|
||||
if (darkModeDisabled) {
|
||||
document.documentElement.classList.add('dark-mode-disabled');
|
||||
@@ -84,8 +90,17 @@ export default function AuthoringLayout() {
|
||||
cssVars,
|
||||
});
|
||||
}
|
||||
|
||||
if (language && language !== APP_I18N_OPTIONS.sourceLang) {
|
||||
void dynamicActivate(language).finally(() => {
|
||||
setHasFinishedInit(true);
|
||||
});
|
||||
} else {
|
||||
setHasFinishedInit(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setHasFinishedInit(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -139,7 +154,13 @@ export default function AuthoringLayout() {
|
||||
}}
|
||||
teamId={team.id}
|
||||
>
|
||||
<Outlet />
|
||||
{hasFinishedInit ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</LimitsProvider>
|
||||
</TrpcProvider>
|
||||
</TeamProvider>
|
||||
|
||||
Reference in New Issue
Block a user