fix: element visible race condition (#1996)

On larger documents we could accidentally start trying to render fields
while not all pages of the PDF have loaded due to us checking for a
single page existing. This would cause an error to be thrown, hard
locking those documents.

This change resolves this by grabbing the highest page number from the
given fields and using it for the visibility check instead.
This commit is contained in:
Lucas Smith
2025-08-26 11:08:43 +10:00
committed by GitHub
parent 44f5da95b3
commit 4012022f55
9 changed files with 37 additions and 8 deletions

View File

@ -172,6 +172,8 @@ export const ConfigureFieldsView = ({
name: 'fields', name: 'fields',
}); });
const highestPageNumber = Math.max(...localFields.map((field) => field.pageNumber));
const onFieldCopy = useCallback( const onFieldCopy = useCallback(
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => { (event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
const { duplicate = false, duplicateAll = false } = options ?? {}; const { duplicate = false, duplicateAll = false } = options ?? {};
@ -540,7 +542,9 @@ export const ConfigureFieldsView = ({
<div> <div>
<PDFViewer documentData={normalizedDocumentData} /> <PDFViewer documentData={normalizedDocumentData} />
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}> <ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
>
{localFields.map((field, index) => { {localFields.map((field, index) => {
const recipientIndex = recipients.findIndex( const recipientIndex = recipients.findIndex(
(r) => r.id === field.recipientId, (r) => r.id === field.recipientId,

View File

@ -91,6 +91,8 @@ export const EmbedDirectTemplateClientPage = ({
localFields.filter((field) => field.inserted), localFields.filter((field) => field.inserted),
]; ];
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE); const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
const { mutateAsync: createDocumentFromDirectTemplate, isPending: isSubmitting } = const { mutateAsync: createDocumentFromDirectTemplate, isPending: isSubmitting } =
@ -442,7 +444,9 @@ export const EmbedDirectTemplateClientPage = ({
</div> </div>
</div> </div>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}> <ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
>
{showPendingFieldTooltip && pendingFields.length > 0 && ( {showPendingFieldTooltip && pendingFields.length > 0 && (
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning"> <FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
<Trans>Click to insert field</Trans> <Trans>Click to insert field</Trans>

View File

@ -50,8 +50,10 @@ export const EmbedDocumentFields = ({
onSignField, onSignField,
onUnsignField, onUnsignField,
}: EmbedDocumentFieldsProps) => { }: EmbedDocumentFieldsProps) => {
const highestPageNumber = Math.max(...fields.map((field) => field.page));
return ( return (
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}> <ElementVisible target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}>
{fields.map((field) => {fields.map((field) =>
match(field.type) match(field.type)
.with(FieldType.SIGNATURE, () => ( .with(FieldType.SIGNATURE, () => (

View File

@ -106,6 +106,8 @@ export const EmbedSignDocumentClientPage = ({
fields.filter((field) => field.inserted), fields.filter((field) => field.inserted),
]; ];
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } = const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
trpc.recipient.completeDocumentWithToken.useMutation(); trpc.recipient.completeDocumentWithToken.useMutation();
@ -465,7 +467,9 @@ export const EmbedSignDocumentClientPage = ({
</div> </div>
</div> </div>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}> <ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
>
{showPendingFieldTooltip && pendingFields.length > 0 && ( {showPendingFieldTooltip && pendingFields.length > 0 && (
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning"> <FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
<Trans>Click to insert field</Trans> <Trans>Click to insert field</Trans>

View File

@ -92,6 +92,8 @@ export const MultiSignDocumentSigningView = ({
[], [],
]; ];
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
const uninsertedFields = document?.fields.filter((field) => !field.inserted) ?? []; const uninsertedFields = document?.fields.filter((field) => !field.inserted) ?? [];
const onSignField = async (payload: TSignFieldWithTokenMutationSchema) => { const onSignField = async (payload: TSignFieldWithTokenMutationSchema) => {
@ -357,7 +359,9 @@ export const MultiSignDocumentSigningView = ({
</div> </div>
{hasDocumentLoaded && ( {hasDocumentLoaded && (
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}> <ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
>
{showPendingFieldTooltip && pendingFields.length > 0 && ( {showPendingFieldTooltip && pendingFields.length > 0 && (
<FieldToolTip <FieldToolTip
key={pendingFields[0].id} key={pendingFields[0].id}

View File

@ -79,6 +79,8 @@ export const DirectTemplateSigningForm = ({
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false); const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const highestPageNumber = Math.max(...localFields.map((field) => field.page));
const fieldsRequiringValidation = useMemo(() => { const fieldsRequiringValidation = useMemo(() => {
return localFields.filter((field) => isFieldUnsignedAndRequired(field)); return localFields.filter((field) => isFieldUnsignedAndRequired(field));
}, [localFields]); }, [localFields]);
@ -221,7 +223,9 @@ export const DirectTemplateSigningForm = ({
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} /> <DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}> <ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
>
{validateUninsertedFields && uninsertedFields[0] && ( {validateUninsertedFields && uninsertedFields[0] && (
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning"> <FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
<Trans>Click to insert field</Trans> <Trans>Click to insert field</Trans>

View File

@ -78,6 +78,8 @@ export const DocumentSigningPageView = ({
const targetSigner = const targetSigner =
recipient.role === RecipientRole.ASSISTANT && selectedSigner ? selectedSigner : null; recipient.role === RecipientRole.ASSISTANT && selectedSigner ? selectedSigner : null;
const highestPageNumber = Math.max(...fields.map((field) => field.page));
return ( return (
<DocumentSigningRecipientProvider recipient={recipient} targetSigner={targetSigner}> <DocumentSigningRecipientProvider recipient={recipient} targetSigner={targetSigner}>
<div className="mx-auto w-full max-w-screen-xl sm:px-6"> <div className="mx-auto w-full max-w-screen-xl sm:px-6">
@ -224,7 +226,9 @@ export const DocumentSigningPageView = ({
<DocumentSigningAutoSign recipient={recipient} fields={fields} /> <DocumentSigningAutoSign recipient={recipient} fields={fields} />
)} )}
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}> <ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
>
{fields {fields
.filter( .filter(
(field) => (field) =>

View File

@ -67,6 +67,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
const documentVisibility = document?.visibility; const documentVisibility = document?.visibility;
const currentTeamMemberRole = team.currentTeamRole; const currentTeamMemberRole = team.currentTeamRole;
const isRecipient = document?.recipients.find((recipient) => recipient.email === user.email); const isRecipient = document?.recipients.find((recipient) => recipient.email === user.email);
let canAccessDocument = true; let canAccessDocument = true;
if (!isRecipient && document?.userId !== user.id) { if (!isRecipient && document?.userId !== user.id) {

View File

@ -95,8 +95,10 @@ export const DocumentReadOnlyFields = ({
setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true })); setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
}; };
const highestPageNumber = Math.max(...fields.map((field) => field.page));
return ( return (
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}> <ElementVisible target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}>
{fields.map( {fields.map(
(field) => (field) =>
!hiddenFieldIds[field.secondaryId] && ( !hiddenFieldIds[field.secondaryId] && (