Skip to content

Commit

Permalink
fix: add document visibility to template (#1566)
Browse files Browse the repository at this point in the history
Adds the visibility property to templates
  • Loading branch information
catalinpit authored Jan 8, 2025
1 parent 07c8527 commit 6fc5e56
Show file tree
Hide file tree
Showing 12 changed files with 316 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export const EditTemplateForm = ({
data: {
title: data.title,
externalId: data.externalId || null,
visibility: data.visibility,
globalAccessAuth: data.globalAccessAuth ?? null,
globalActionAuth: data.globalActionAuth ?? null,
},
Expand Down Expand Up @@ -296,6 +297,7 @@ export const EditTemplateForm = ({
<AddTemplateSettingsFormPartial
key={recipients.length}
template={template}
currentTeamMemberRole={team?.currentTeamMember?.role}
documentFlow={documentFlow.settings}
recipients={recipients}
fields={fields}
Expand Down
108 changes: 108 additions & 0 deletions packages/app-tests/e2e/templates-flow/template-settings-step.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { expect, test } from '@playwright/test';

import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
Expand Down Expand Up @@ -157,3 +159,109 @@ test('[TEMPLATE_FLOW]: add settings', async ({ page }) => {
await expect(page.getByLabel('Title')).toHaveValue('New Title');
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
});

test('[TEMPLATE_FLOW] add document visibility settings', async ({ page }) => {
const { owner, ...team } = await seedTeam({
createTeamMembers: 1,
});

const template = await seedBlankTemplate(owner, {
createTemplateOptions: {
teamId: team.id,
},
});

await apiSignin({
page,
email: owner.email,
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
});

// Set document visibility.
await page.getByTestId('documentVisibilitySelectValue').click();
await page.getByLabel('Managers and above').click();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
'Managers and above',
);

// Save the settings by going to the next step.
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();

// Navigate back to the edit page to check that the settings are saved correctly.
await page.goto(`/t/${team.url}/templates/${template.id}/edit`);

await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
'Managers and above',
);
});

test('[TEMPLATE_FLOW] team member visibility permissions', async ({ page }) => {
const team = await seedTeam({
createTeamMembers: 2, // Create an additional member to test different roles
});

await prisma.teamMember.update({
where: {
id: team.members[1].id,
},
data: {
role: TeamMemberRole.MANAGER,
},
});

const owner = team.owner;
const managerUser = team.members[1].user;
const memberUser = team.members[2].user;

const template = await seedBlankTemplate(owner, {
createTemplateOptions: {
teamId: team.id,
},
});

// Test as manager
await apiSignin({
page,
email: managerUser.email,
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
});

// Manager should be able to set visibility to managers and above
await page.getByTestId('documentVisibilitySelectValue').click();
await page.getByLabel('Managers and above').click();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
'Managers and above',
);
await expect(page.getByText('Admins only')).toBeDisabled();

// Save and verify
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();

// Test as regular member
await apiSignin({
page,
email: memberUser.email,
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
});

// Regular member should not be able to modify visibility when set to managers and above
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();

// Create a new template with 'everyone' visibility
const everyoneTemplate = await seedBlankTemplate(owner, {
createTemplateOptions: {
teamId: team.id,
visibility: 'EVERYONE',
},
});

// Navigate to the new template
await page.goto(`/t/${team.url}/templates/${everyoneTemplate.id}/edit`);

// Regular member should be able to see but not modify visibility
await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText('Everyone');
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import path from 'path';

import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { DocumentDataType } from '@documenso/prisma/client';
import { DocumentDataType, TeamMemberRole } from '@documenso/prisma/client';
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
Expand Down Expand Up @@ -529,3 +529,90 @@ test('[TEMPLATE]: should create a document from a template using template docume
);
expect(document.documentData.type).toEqual(templateWithData.templateDocumentData.type);
});

test('[TEMPLATE]: should persist document visibility when creating from template', async ({
page,
}) => {
const { owner, ...team } = await seedTeam({
createTeamMembers: 2,
});

const template = await seedBlankTemplate(owner, {
createTemplateOptions: {
teamId: team.id,
},
});

await apiSignin({
page,
email: owner.email,
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
});

// Set template title and visibility
await page.getByLabel('Title').fill('TEMPLATE_WITH_VISIBILITY');
await page.getByTestId('documentVisibilitySelectValue').click();
await page.getByLabel('Managers and above').click();
await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText(
'Managers and above',
);

await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();

// Add a signer
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
await page.getByPlaceholder('Name').fill('Recipient');

await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();

await page.getByRole('button', { name: 'Save template' }).click();

// Test creating document as team manager
await prisma.teamMember.update({
where: {
id: team.members[1].id,
},
data: {
role: TeamMemberRole.MANAGER,
},
});

const managerUser = team.members[1].user;

await apiSignin({
page,
email: managerUser.email,
redirectPath: `/t/${team.url}/templates`,
});

await page.getByRole('button', { name: 'Use Template' }).click();
await page.getByRole('button', { name: 'Create as draft' }).click();

// Review that the document was created with the correct visibility
await page.waitForURL(/documents/);

const documentId = Number(page.url().split('/').pop());

const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
},
});

expect(document.title).toEqual('TEMPLATE_WITH_VISIBILITY');
expect(document.visibility).toEqual('MANAGER_AND_ABOVE');
expect(document.teamId).toEqual(team.id);

// Test that regular member cannot create document from restricted template
const memberUser = team.members[2].user;
await apiSignin({
page,
email: memberUser.email,
redirectPath: `/t/${team.url}/templates`,
});

// Template should not be visible to regular member
await expect(page.getByRole('button', { name: 'Use Template' })).not.toBeVisible();
});
2 changes: 2 additions & 0 deletions packages/app-tests/e2e/templates/direct-templates.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ test('[DIRECT_TEMPLATES]: create direct link for template', async ({ page }) =>
await page.getByRole('button', { name: 'Enable direct link signing' }).click();
await page.getByRole('button', { name: 'Create one automatically' }).click();
await expect(page.getByRole('heading', { name: 'Direct Link Signing' })).toBeVisible();

await page.waitForTimeout(1000);
await page.getByTestId('btn-dialog-close').click();

// Expect badge to appear.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export const createDocumentFromTemplate = async ({
globalAccessAuth: templateAuthOptions.globalAccessAuth,
globalActionAuth: templateAuthOptions.globalActionAuth,
}),
visibility: template.team?.teamGlobalSettings?.documentVisibility,
visibility: template.visibility || template.team?.teamGlobalSettings?.documentVisibility,
documentMeta: {
create: {
subject: override?.subject || template.templateMeta?.subject,
Expand Down
73 changes: 56 additions & 17 deletions packages/lib/server-only/template/find-templates.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { match } from 'ts-pattern';
import type { z } from 'zod';

import { prisma } from '@documenso/prisma';
import type { Prisma, Template } from '@documenso/prisma/client';
import {
DocumentVisibility,
type Prisma,
TeamMemberRole,
type Template,
} from '@documenso/prisma/client';
import {
DocumentDataSchema,
FieldSchema,
Expand All @@ -12,6 +18,7 @@ import {
TemplateSchema,
} from '@documenso/prisma/generated/zod';

import { AppError, AppErrorCode } from '../../errors/app-error';
import { type FindResultResponse, ZFindResultResponse } from '../../types/search-params';

export type FindTemplatesOptions = {
Expand Down Expand Up @@ -52,28 +59,58 @@ export const findTemplates = async ({
page = 1,
perPage = 10,
}: FindTemplatesOptions): Promise<TFindTemplatesResponse> => {
let whereFilter: Prisma.TemplateWhereInput = {
userId,
teamId: null,
type,
};
const whereFilter: Prisma.TemplateWhereInput[] = [];

if (teamId === undefined) {
whereFilter.push({ userId, teamId: null });
}

if (teamId !== undefined) {
whereFilter = {
team: {
id: teamId,
members: {
some: {
userId,
},
},
const teamMember = await prisma.teamMember.findFirst({
where: {
userId,
teamId,
},
};
});

if (!teamMember) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You are not a member of this team.',
});
}

whereFilter.push(
{ teamId },
{
OR: [
match(teamMember.role)
.with(TeamMemberRole.ADMIN, () => ({
visibility: {
in: [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
DocumentVisibility.ADMIN,
],
},
}))
.with(TeamMemberRole.MANAGER, () => ({
visibility: {
in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE],
},
}))
.otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })),
{ userId, teamId },
],
},
);
}

const [data, count] = await Promise.all([
prisma.template.findMany({
where: whereFilter,
where: {
type,
AND: whereFilter,
},
include: {
templateDocumentData: true,
team: {
Expand Down Expand Up @@ -103,7 +140,9 @@ export const findTemplates = async ({
},
}),
prisma.template.count({
where: whereFilter,
where: {
AND: whereFilter,
},
}),
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { z } from 'zod';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import type { Template, TemplateMeta } from '@documenso/prisma/client';
import type { DocumentVisibility, Template, TemplateMeta } from '@documenso/prisma/client';
import { TemplateSchema } from '@documenso/prisma/generated/zod';

import { AppError, AppErrorCode } from '../../errors/app-error';
Expand All @@ -19,6 +19,7 @@ export type UpdateTemplateSettingsOptions = {
data: {
title?: string;
externalId?: string | null;
visibility?: DocumentVisibility;
globalAccessAuth?: TDocumentAccessAuthTypes | null;
globalActionAuth?: TDocumentActionAuthTypes | null;
publicTitle?: string;
Expand Down Expand Up @@ -110,6 +111,7 @@ export const updateTemplateSettings = async ({
title: data.title,
externalId: data.externalId,
type: data.type,
visibility: data.visibility,
publicDescription: data.publicDescription,
publicTitle: data.publicTitle,
authOptions,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Template" ADD COLUMN "visibility" "DocumentVisibility" NOT NULL DEFAULT 'EVERYONE';
Loading

0 comments on commit 6fc5e56

Please sign in to comment.