Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,16 @@ export const EnvelopeEditorRecipientForm = () => {
removeSigner(formStateIndex);

const updatedSigners = form.getValues('signers').filter((s) => s.formId !== signer.formId);
const normalizedSigners = normalizeSigningOrders(updatedSigners);

form.setValue('signers', normalizeSigningOrders(updatedSigners), {
form.setValue('signers', normalizedSigners, {
shouldValidate: true,
shouldDirty: true,
});

// Enqueue the removal immediately so rapid step navigation cannot flush
// before the form-change effect has observed the updated recipients.
setRecipientsDebounced(normalizedSigners);
}
};

Expand Down Expand Up @@ -515,8 +520,7 @@ export const EnvelopeEditorRecipientForm = () => {

const updatedSigner = updatedSigners.find((s) => s.formId === signer.formId);
const isLastSlot =
updatedSigner?.signingOrder ===
Math.max(...updatedSigners.map((s) => s.signingOrder ?? 0));
updatedSigner?.signingOrder === Math.max(...updatedSigners.map((s) => s.signingOrder ?? 0));

if (signer.role === RecipientRole.ASSISTANT && isLastSlot) {
toast({
Expand Down
2 changes: 1 addition & 1 deletion apps/remix/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function LayoutContent({ children }: { children: React.ReactNode }) {
const [theme] = useTheme();

return (
<html translate="no" lang={lang} data-theme={theme} className={theme ?? ''}>
<html translate="no" lang={lang} className={theme ?? ''}>
<head>
<meta charSet="utf-8" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
Expand Down
19 changes: 13 additions & 6 deletions apps/remix/app/routes/_authenticated+/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
import { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams-translations';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { isAdmin } from '@documenso/lib/utils/is-admin';
import { canExecuteOrganisationAction } from '@documenso/lib/utils/organisations';
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
Expand Down Expand Up @@ -67,15 +68,21 @@ export default function DashboardPage() {
<Trans>No organisations found</Trans>
</p>
<p className="text-sm text-muted-foreground">
<Trans>Create an organisation to get started.</Trans>
{isAdmin(user) ? (
<Trans>Create an organisation to get started.</Trans>
) : (
<Trans>Contact your administrator to request organisation access.</Trans>
)}
</p>
</div>

<Button asChild className="mt-4" variant="outline">
<Link to="/settings/organisations?action=add-organisation">
<Trans>Create organisation</Trans>
</Link>
</Button>
{isAdmin(user) && (
<Button asChild className="mt-4" variant="outline">
<Link to="/settings/organisations?action=add-organisation">
<Trans>Create organisation</Trans>
</Link>
</Button>
)}
</div>
)}

Expand Down
2 changes: 1 addition & 1 deletion docker/development/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
volumes:
- documenso_database:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
interval: 10s
timeout: 5s
retries: 5
Expand Down
Binary file not shown.
6 changes: 3 additions & 3 deletions packages/app-tests/e2e/api/v1/document-sending.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ test.describe('Document API', () => {
expect(response.status()).toBe(400);
});

test('sendDocument: should fail when signer has only non-signature fields', async ({
test('sendDocument: should succeed when signer has only non-signature fields', async ({
request,
}) => {
const { user, team } = await seedUser();
Expand Down Expand Up @@ -251,8 +251,8 @@ test.describe('Document API', () => {
},
);

expect(response.ok()).toBeFalsy();
expect(response.status()).toBe(400);
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
});

test('sendDocument: should succeed when signer has signature field', async ({ request }) => {
Expand Down
20 changes: 12 additions & 8 deletions packages/app-tests/e2e/document-flow/stepper-component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,14 +466,18 @@ test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a
const { status } = await getDocumentByToken(token);
expect(status).toBe(DocumentStatus.PENDING);

await page.getByRole('button', { name: 'Approve' }).click();
await expect(
page
.getByRole('dialog')
.getByText('You are about to complete approving the following document')
.first(),
).toBeVisible();
await page.getByRole('button', { name: 'Approve' }).click();
const approvalDialog = page.getByRole('dialog');

await expect(async () => {
await page.getByRole('button', { name: 'Approve' }).first().click();
await expect(
approvalDialog
.getByText('You are about to complete approving the following document')
.first(),
).toBeVisible({ timeout: 5000 });
}).toPass({ timeout: 30_000 });

await approvalDialog.getByRole('button', { name: 'Approve' }).click();

await page.waitForURL('https://documenso.com');

Expand Down
11 changes: 8 additions & 3 deletions packages/app-tests/e2e/documents/bulk-document-actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ test('[BULK_ACTIONS]: header checkbox selects all documents on page', async ({ p
redirectPath: `/t/${sender.team.url}/documents`,
});

await expect(page.getByRole('link', { name: 'Bulk Test Doc 1' })).toBeVisible();
await page.locator('thead').getByRole('checkbox').click();

await expect(page.getByText(`${documents.length} selected`)).toBeVisible();
Expand All @@ -77,6 +78,7 @@ test('[BULK_ACTIONS]: can clear selection with X button', async ({ page }) => {
redirectPath: `/t/${sender.team.url}/documents`,
});

await expect(page.getByRole('link', { name: 'Bulk Test Doc 1' })).toBeVisible();
await page.locator('thead').getByRole('checkbox').click();
await expect(page.getByText(/\d+ selected/)).toBeVisible();

Expand All @@ -102,9 +104,10 @@ test('[BULK_ACTIONS]: can move multiple documents to a folder', async ({ page })
await expect(page.getByText('Move Documents to Folder')).toBeVisible();

await page.getByRole('button', { name: folder.name }).click();
const moveToast = expectToastTextToBeVisible(page, 'Selected items have been moved.');
await page.getByRole('button', { name: 'Move' }).click();

await expectToastTextToBeVisible(page, 'Selected items have been moved.');
await moveToast;

await page.goto(`/t/${sender.team.url}/documents/f/${folder.id}`);
await expect(page.getByRole('link', { name: 'Bulk Test Doc 1' })).toBeVisible();
Expand Down Expand Up @@ -162,9 +165,10 @@ test('[BULK_ACTIONS]: selection clears after successful move', async ({ page })

await page.getByRole('button', { name: 'Move to Folder' }).click();
await page.getByRole('button', { name: folder.name }).click();
const moveToast = expectToastTextToBeVisible(page, 'Selected items have been moved.');
await page.getByRole('button', { name: 'Move' }).click();

await expectToastTextToBeVisible(page, 'Selected items have been moved.');
await moveToast;
await expect(page.getByText(/\d+ selected/)).not.toBeVisible();
});

Expand Down Expand Up @@ -250,9 +254,10 @@ test('[BULK_ACTIONS]: can move documents from folder to home (root)', async ({ p

await page.getByRole('button', { name: 'Home (No Folder)' }).click();

const moveToast = expectToastTextToBeVisible(page, 'Selected items have been moved.');
await page.getByRole('button', { name: 'Move' }).click();

await expectToastTextToBeVisible(page, 'Selected items have been moved.');
await moveToast;

await page.goto(`/t/${sender.team.url}/documents`);
await expect(page.getByRole('link', { name: 'Bulk Test Doc 1' })).toBeVisible();
Expand Down
12 changes: 7 additions & 5 deletions packages/app-tests/e2e/documents/find-documents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1167,8 +1167,9 @@ test.describe('Find Documents UI - Sender Filter', () => {
include: { members: { include: { user: true }, orderBy: { id: 'asc' } } },
});

const member1 = org.members[1].user;
const member2 = org.members[2].user;
const [member1, member2] = org.members
.filter((member) => member.userId !== owner.id)
.map((member) => member.user);

const { user: outsideUser } = await seedUser();

Expand Down Expand Up @@ -1207,11 +1208,12 @@ test.describe('Find Documents UI - Sender Filter', () => {

// Filter by member1
await page.locator('button').filter({ hasText: 'Sender: All' }).click();
await page.getByRole('option', { name: member1.name ?? '' }).click();
await page.waitForURL(/senderIds/);
await page.getByRole('option', { name: member1.name ?? '', exact: true }).click();
await expect(page).toHaveURL(new RegExp(`senderIds=${member1.id}`));
await expect(page.locator('button').filter({ hasText: member1.name ?? '' })).toBeVisible();

// Should only show member1's doc
await checkDocumentTabCount(page, 'All', 1);
await expect(page.getByTestId('data-table-count')).toContainText('Showing 1');
await expect(page.getByRole('link', { name: 'Member1 Sent Doc' })).toBeVisible();
});
});
51 changes: 21 additions & 30 deletions packages/app-tests/e2e/envelope-editor-v2/envelope-fields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,21 +120,18 @@ const runAddAndPersistSignatureTextFields = async (
await expect(surface.root.locator('.konva-container canvas').first()).toBeVisible();

await placeFieldOnPdf(surface.root, 'Signature', { x: 120, y: 140 });
let fieldCount = await getKonvaElementCountForPage(surface.root, 1, '.field-group');
expect(fieldCount).toBe(1);
await expectFieldCount(surface.root, 1);

await placeFieldOnPdf(surface.root, 'Text', { x: 220, y: 240 });
fieldCount = await getKonvaElementCountForPage(surface.root, 1, '.field-group');
expect(fieldCount).toBe(2);
await expectFieldCount(surface.root, 2);

await clickEnvelopeEditorStep(surface.root, 'upload');
await expect(surface.root.getByRole('heading', { name: 'Recipients' })).toBeVisible();

await clickEnvelopeEditorStep(surface.root, 'addFields');
await surface.root.locator('.konva-container canvas').first().waitFor({ state: 'visible' });
await expect(surface.root.getByText('Selected Recipient')).toBeVisible();
fieldCount = await getKonvaElementCountForPage(surface.root, 1, '.field-group');
expect(fieldCount).toBe(2);
await expectFieldCount(surface.root, 2);

return {
externalId,
Expand Down Expand Up @@ -214,6 +211,12 @@ const MULTI_RECIPIENT_VALUES = {
},
};

const expectFieldCount = async (root: Page, expectedCount: number) => {
await expect
.poll(async () => getKonvaElementCountForPage(root, 1, '.field-group'))
.toBe(expectedCount);
};

const runMultiRecipientFieldFlow = async (
surface: TEnvelopeEditorSurface,
): Promise<TMultiRecipientFlowResult> => {
Expand Down Expand Up @@ -247,25 +250,21 @@ const runMultiRecipientFieldFlow = async (
await expect(root.getByText('Selected Recipient')).toBeVisible();
await expect(root.locator('.konva-container canvas').first()).toBeVisible();

let fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
expect(fieldCount).toBe(0);
await expectFieldCount(root, 0);

// Place Signature for recipient #1 (auto-selected).
await placeFieldOnPdf(root, 'Signature', { x: 120, y: 140 });
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
expect(fieldCount).toBe(1);
await expectFieldCount(root, 1);

// Switch recipient and place text field for recipient #2.
await selectRecipientInFieldsStep(root, MULTI_RECIPIENT_VALUES.secondSigner.email);
await placeFieldOnPdf(root, 'Text', { x: 220, y: 240 });
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
expect(fieldCount).toBe(2);
await expectFieldCount(root, 2);

// Navigate away and back to ensure fields are persisted in the UI.
await clickEnvelopeEditorStep(root, 'upload');
await clickEnvelopeEditorStep(root, 'addFields');
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
expect(fieldCount).toBe(2);
await expectFieldCount(root, 2);

// Phase 2: cascade deletion — go back to recipients and remove the second one.
await clickEnvelopeEditorStep(root, 'upload');
Expand All @@ -276,8 +275,7 @@ const runMultiRecipientFieldFlow = async (

// Go back to fields and verify cascade removal.
await clickEnvelopeEditorStep(root, 'addFields');
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
expect(fieldCount).toBe(1);
await expectFieldCount(root, 1);

return {
externalId,
Expand Down Expand Up @@ -436,17 +434,15 @@ const runAllFieldTypesFlow = async (
await root.locator('[data-testid="field-form-defaultValue"]').click();
await root.getByRole('option', { name: 'Red' }).click();

let fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
expect(fieldCount).toBe(10);
await expectFieldCount(root, 10);

// Wait briefly for auto-save to fire on the last configured field.
await root.waitForTimeout(500);

// Navigate away and back to verify persistence.
await clickEnvelopeEditorStep(root, 'upload');
await clickEnvelopeEditorStep(root, 'addFields');
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
expect(fieldCount).toBe(10);
await expectFieldCount(root, 10);

return { externalId };
};
Expand Down Expand Up @@ -580,36 +576,31 @@ const runDuplicateDeleteFieldFlow = async (

// Place a Signature field.
await placeFieldOnPdf(root, 'Signature', { x: 150, y: 150 });
let fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
expect(fieldCount).toBe(1);
await expectFieldCount(root, 1);

// Select the field on canvas to show the action toolbar.
await selectFieldOnCanvas(root, { x: 150, y: 150 });
await expect(root.locator('button[title="Duplicate"]')).toBeVisible();

// Duplicate the field.
await root.locator('button[title="Duplicate"]').click();
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
expect(fieldCount).toBe(2);
await expectFieldCount(root, 2);

// Navigate away and back to persist changes.
await clickEnvelopeEditorStep(root, 'upload');
await clickEnvelopeEditorStep(root, 'addFields');
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
expect(fieldCount).toBe(2);
await expectFieldCount(root, 2);

// Select a field and delete it via the Remove button.
await selectFieldOnCanvas(root, { x: 150, y: 150 });
await expect(root.locator('button[title="Remove"]')).toBeVisible();
await root.locator('button[title="Remove"]').click();
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
expect(fieldCount).toBe(1);
await expectFieldCount(root, 1);

// Navigate away and back to verify persistence.
await clickEnvelopeEditorStep(root, 'upload');
await clickEnvelopeEditorStep(root, 'addFields');
fieldCount = await getKonvaElementCountForPage(root, 1, '.field-group');
expect(fieldCount).toBe(1);
await expectFieldCount(root, 1);

return { externalId };
};
Expand Down
Loading
Loading