fix: add document visibility to template (#1566)

Adds the visibility property to templates
This commit is contained in:
Catalin Pit
2025-01-09 01:14:24 +02:00
committed by GitHub
parent 07c852744b
commit 6fc5e565d0
12 changed files with 316 additions and 26 deletions

View File

@ -166,6 +166,7 @@ export const EditTemplateForm = ({
data: { data: {
title: data.title, title: data.title,
externalId: data.externalId || null, externalId: data.externalId || null,
visibility: data.visibility,
globalAccessAuth: data.globalAccessAuth ?? null, globalAccessAuth: data.globalAccessAuth ?? null,
globalActionAuth: data.globalActionAuth ?? null, globalActionAuth: data.globalActionAuth ?? null,
}, },
@ -296,6 +297,7 @@ export const EditTemplateForm = ({
<AddTemplateSettingsFormPartial <AddTemplateSettingsFormPartial
key={recipients.length} key={recipients.length}
template={template} template={template}
currentTeamMemberRole={team?.currentTeamMember?.role}
documentFlow={documentFlow.settings} documentFlow={documentFlow.settings}
recipients={recipients} recipients={recipients}
fields={fields} fields={fields}

View File

@ -1,5 +1,7 @@
import { expect, test } from '@playwright/test'; 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 { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
import { seedTeam } from '@documenso/prisma/seed/teams'; import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
@ -157,3 +159,109 @@ test('[TEMPLATE_FLOW]: add settings', async ({ page }) => {
await expect(page.getByLabel('Title')).toHaveValue('New Title'); await expect(page.getByLabel('Title')).toHaveValue('New Title');
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account'); 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');
});

View File

@ -5,7 +5,7 @@ import path from 'path';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma'; 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 { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
import { seedTeam } from '@documenso/prisma/seed/teams'; import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
@ -529,3 +529,90 @@ test('[TEMPLATE]: should create a document from a template using template docume
); );
expect(document.documentData.type).toEqual(templateWithData.templateDocumentData.type); 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();
});

View File

@ -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: 'Enable direct link signing' }).click();
await page.getByRole('button', { name: 'Create one automatically' }).click(); await page.getByRole('button', { name: 'Create one automatically' }).click();
await expect(page.getByRole('heading', { name: 'Direct Link Signing' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Direct Link Signing' })).toBeVisible();
await page.waitForTimeout(1000);
await page.getByTestId('btn-dialog-close').click(); await page.getByTestId('btn-dialog-close').click();
// Expect badge to appear. // Expect badge to appear.

View File

@ -213,7 +213,7 @@ export const createDocumentFromTemplate = async ({
globalAccessAuth: templateAuthOptions.globalAccessAuth, globalAccessAuth: templateAuthOptions.globalAccessAuth,
globalActionAuth: templateAuthOptions.globalActionAuth, globalActionAuth: templateAuthOptions.globalActionAuth,
}), }),
visibility: template.team?.teamGlobalSettings?.documentVisibility, visibility: template.visibility || template.team?.teamGlobalSettings?.documentVisibility,
documentMeta: { documentMeta: {
create: { create: {
subject: override?.subject || template.templateMeta?.subject, subject: override?.subject || template.templateMeta?.subject,

View File

@ -1,7 +1,13 @@
import { match } from 'ts-pattern';
import type { z } from 'zod'; import type { z } from 'zod';
import { prisma } from '@documenso/prisma'; 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 { import {
DocumentDataSchema, DocumentDataSchema,
FieldSchema, FieldSchema,
@ -12,6 +18,7 @@ import {
TemplateSchema, TemplateSchema,
} from '@documenso/prisma/generated/zod'; } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { type FindResultResponse, ZFindResultResponse } from '../../types/search-params'; import { type FindResultResponse, ZFindResultResponse } from '../../types/search-params';
export type FindTemplatesOptions = { export type FindTemplatesOptions = {
@ -52,28 +59,58 @@ export const findTemplates = async ({
page = 1, page = 1,
perPage = 10, perPage = 10,
}: FindTemplatesOptions): Promise<TFindTemplatesResponse> => { }: FindTemplatesOptions): Promise<TFindTemplatesResponse> => {
let whereFilter: Prisma.TemplateWhereInput = { const whereFilter: Prisma.TemplateWhereInput[] = [];
userId,
teamId: null, if (teamId === undefined) {
type, whereFilter.push({ userId, teamId: null });
}; }
if (teamId !== undefined) { if (teamId !== undefined) {
whereFilter = { const teamMember = await prisma.teamMember.findFirst({
team: { where: {
id: teamId, userId,
members: { teamId,
some: {
userId,
},
},
}, },
}; });
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([ const [data, count] = await Promise.all([
prisma.template.findMany({ prisma.template.findMany({
where: whereFilter, where: {
type,
AND: whereFilter,
},
include: { include: {
templateDocumentData: true, templateDocumentData: true,
team: { team: {
@ -103,7 +140,9 @@ export const findTemplates = async ({
}, },
}), }),
prisma.template.count({ prisma.template.count({
where: whereFilter, where: {
AND: whereFilter,
},
}), }),
]); ]);

View File

@ -5,7 +5,7 @@ import type { z } from 'zod';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise'; import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma'; 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 { TemplateSchema } from '@documenso/prisma/generated/zod';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
@ -19,6 +19,7 @@ export type UpdateTemplateSettingsOptions = {
data: { data: {
title?: string; title?: string;
externalId?: string | null; externalId?: string | null;
visibility?: DocumentVisibility;
globalAccessAuth?: TDocumentAccessAuthTypes | null; globalAccessAuth?: TDocumentAccessAuthTypes | null;
globalActionAuth?: TDocumentActionAuthTypes | null; globalActionAuth?: TDocumentActionAuthTypes | null;
publicTitle?: string; publicTitle?: string;
@ -110,6 +111,7 @@ export const updateTemplateSettings = async ({
title: data.title, title: data.title,
externalId: data.externalId, externalId: data.externalId,
type: data.type, type: data.type,
visibility: data.visibility,
publicDescription: data.publicDescription, publicDescription: data.publicDescription,
publicTitle: data.publicTitle, publicTitle: data.publicTitle,
authOptions, authOptions,

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Template" ADD COLUMN "visibility" "DocumentVisibility" NOT NULL DEFAULT 'EVERYONE';

View File

@ -654,19 +654,20 @@ model TemplateMeta {
} }
model Template { model Template {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
externalId String? externalId String?
type TemplateType @default(PRIVATE) type TemplateType @default(PRIVATE)
title String title String
userId Int userId Int
teamId Int? teamId Int?
visibility DocumentVisibility @default(EVERYONE)
authOptions Json? authOptions Json?
templateMeta TemplateMeta? templateMeta TemplateMeta?
templateDocumentDataId String templateDocumentDataId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
publicTitle String @default("") publicTitle String @default("")
publicDescription String @default("") publicDescription String @default("")
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade) templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade)

View File

@ -11,6 +11,7 @@ import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import { import {
DocumentDistributionMethod, DocumentDistributionMethod,
DocumentSigningOrder, DocumentSigningOrder,
DocumentVisibility,
TemplateType, TemplateType,
} from '@documenso/prisma/client'; } from '@documenso/prisma/client';
@ -84,6 +85,7 @@ export const ZUpdateTemplateSettingsMutationSchema = z.object({
data: z.object({ data: z.object({
title: z.string().min(1).optional(), title: z.string().min(1).optional(),
externalId: z.string().nullish(), externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(), globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(), globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
publicTitle: z.string().trim().min(1).max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH).optional(), publicTitle: z.string().trim().min(1).max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH).optional(),

View File

@ -7,6 +7,7 @@ import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { InfoIcon } from 'lucide-react'; import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { DOCUMENT_DISTRIBUTION_METHODS } from '@documenso/lib/constants/document'; import { DOCUMENT_DISTRIBUTION_METHODS } from '@documenso/lib/constants/document';
@ -14,6 +15,7 @@ import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
import { DocumentDistributionMethod, type Field, type Recipient } from '@documenso/prisma/client'; import { DocumentDistributionMethod, type Field, type Recipient } from '@documenso/prisma/client';
import type { TemplateWithData } from '@documenso/prisma/types/template'; import type { TemplateWithData } from '@documenso/prisma/types/template';
import { import {
@ -25,6 +27,10 @@ import {
DocumentGlobalAuthActionTooltip, DocumentGlobalAuthActionTooltip,
} from '@documenso/ui/components/document/document-global-auth-action-select'; } from '@documenso/ui/components/document/document-global-auth-action-select';
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper'; import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
import {
DocumentVisibilitySelect,
DocumentVisibilityTooltip,
} from '@documenso/ui/components/document/document-visibility-select';
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
@ -66,6 +72,7 @@ export type AddTemplateSettingsFormProps = {
isEnterprise: boolean; isEnterprise: boolean;
isDocumentPdfLoaded: boolean; isDocumentPdfLoaded: boolean;
template: TemplateWithData; template: TemplateWithData;
currentTeamMemberRole?: TeamMemberRole;
onSubmit: (_data: TAddTemplateSettingsFormSchema) => void; onSubmit: (_data: TAddTemplateSettingsFormSchema) => void;
}; };
@ -76,6 +83,7 @@ export const AddTemplateSettingsFormPartial = ({
isEnterprise, isEnterprise,
isDocumentPdfLoaded, isDocumentPdfLoaded,
template, template,
currentTeamMemberRole,
onSubmit, onSubmit,
}: AddTemplateSettingsFormProps) => { }: AddTemplateSettingsFormProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
@ -89,6 +97,7 @@ export const AddTemplateSettingsFormPartial = ({
defaultValues: { defaultValues: {
title: template.title, title: template.title,
externalId: template.externalId || undefined, externalId: template.externalId || undefined,
visibility: template.visibility || '',
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined, globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
globalActionAuth: documentAuthOption?.globalActionAuth || undefined, globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
meta: { meta: {
@ -110,6 +119,16 @@ export const AddTemplateSettingsFormPartial = ({
const distributionMethod = form.watch('meta.distributionMethod'); const distributionMethod = form.watch('meta.distributionMethod');
const emailSettings = form.watch('meta.emailSettings'); const emailSettings = form.watch('meta.emailSettings');
const canUpdateVisibility = match(currentTeamMemberRole)
.with(TeamMemberRole.ADMIN, () => true)
.with(
TeamMemberRole.MANAGER,
() =>
template.visibility === DocumentVisibility.EVERYONE ||
template.visibility === DocumentVisibility.MANAGER_AND_ABOVE,
)
.otherwise(() => false);
// We almost always want to set the timezone to the user's local timezone to avoid confusion // We almost always want to set the timezone to the user's local timezone to avoid confusion
// when the document is signed. // when the document is signed.
useEffect(() => { useEffect(() => {
@ -210,6 +229,30 @@ export const AddTemplateSettingsFormPartial = ({
)} )}
/> />
{currentTeamMemberRole && (
<FormField
control={form.control}
name="visibility"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
Document visibility
<DocumentVisibilityTooltip />
</FormLabel>
<FormControl>
<DocumentVisibilitySelect
canUpdateVisibility={canUpdateVisibility}
currentTeamMemberRole={currentTeamMemberRole}
{...field}
onValueChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
<FormField <FormField
control={form.control} control={form.control}
name="meta.distributionMethod" name="meta.distributionMethod"

View File

@ -9,6 +9,7 @@ import {
} from '@documenso/lib/types/document-auth'; } from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url'; import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import { DocumentVisibility } from '@documenso/prisma/client';
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types'; import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
import { DocumentDistributionMethod } from '.prisma/client'; import { DocumentDistributionMethod } from '.prisma/client';
@ -16,6 +17,7 @@ import { DocumentDistributionMethod } from '.prisma/client';
export const ZAddTemplateSettingsFormSchema = z.object({ export const ZAddTemplateSettingsFormSchema = z.object({
title: z.string().trim().min(1, { message: "Title can't be empty" }), title: z.string().trim().min(1, { message: "Title can't be empty" }),
externalId: z.string().optional(), externalId: z.string().optional(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe( globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
ZDocumentAccessAuthTypesSchema.optional(), ZDocumentAccessAuthTypesSchema.optional(),
), ),