fix: availability unlockedFields initialisation for team-managed event types (#25666)

* Fix: availability unlockedFields initialisation for team-managed event types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: init managed event type for local atoms testing

* chore: add changeset file

* chore: add tests

* fix: restore eslint-disable comments in managed-user.ts

Addresses review feedback from @anikdhabal to fix the eslint comment
issues where comments were accidentally replaced with whitespace.

Co-authored-by: gusdudey2k <45557946+gusdudey2k@users.noreply.github.com>
Co-Authored-By: unknown <>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
Co-authored-by: Ryukemeister <sahalrajiv6900@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Gus
2026-01-18 09:31:27 -03:00
committed by GitHub
co-authored by unknown Claude Opus 4.5 supalarry Anik Dhabal Babu Rajiv Sahal Ryukemeister Devin AI
parent 19f4677d95
commit da143ae546
4 changed files with 603 additions and 4 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@calcom/atoms": patch
---
fix: unlocked fields not saved for managed event type
@@ -0,0 +1,564 @@
/**
* @vitest-environment jsdom
*/
import { SchedulingType } from "@calcom/prisma/enums";
import { act, renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import useLockedFieldsManager from "./useLockedFieldsManager";
describe("useLockedFieldsManager", () => {
const mockTranslate = vi.fn((key: string) => key);
const createMockFormMethods = (getValuesReturn: unknown = undefined) => {
const setValueMock = vi.fn();
const getValuesMock = vi.fn().mockReturnValue(getValuesReturn);
return {
setValue: setValueMock,
getValues: getValuesMock,
watch: vi.fn(),
register: vi.fn(),
handleSubmit: vi.fn(),
formState: { errors: {} },
control: {},
} as unknown as ReturnType<typeof import("react-hook-form").useForm>;
};
beforeEach(() => {
vi.clearAllMocks();
});
describe("isManagedEventType", () => {
it("should return true when schedulingType is MANAGED", () => {
const mockFormMethods = createMockFormMethods();
const eventType = {
schedulingType: SchedulingType.MANAGED,
userId: 1,
metadata: {},
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
expect(result.current.isManagedEventType).toBe(true);
});
it("should return false when schedulingType is not MANAGED", () => {
const mockFormMethods = createMockFormMethods();
const eventType = {
schedulingType: null,
userId: 1,
metadata: {},
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
expect(result.current.isManagedEventType).toBe(false);
});
});
describe("isChildrenManagedEventType", () => {
it("should return true when metadata has managedEventConfig and schedulingType is not MANAGED", () => {
const mockFormMethods = createMockFormMethods();
const eventType = {
schedulingType: null,
userId: 1,
metadata: {
managedEventConfig: { unlockedFields: {} },
},
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
expect(result.current.isChildrenManagedEventType).toBe(true);
});
it("should return false when metadata has managedEventConfig but schedulingType is MANAGED", () => {
const mockFormMethods = createMockFormMethods();
const eventType = {
schedulingType: SchedulingType.MANAGED,
userId: 1,
metadata: {
managedEventConfig: { unlockedFields: {} },
},
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
expect(result.current.isChildrenManagedEventType).toBe(false);
});
it("should return false when metadata does not have managedEventConfig", () => {
const mockFormMethods = createMockFormMethods();
const eventType = {
schedulingType: null,
userId: 1,
metadata: {},
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
expect(result.current.isChildrenManagedEventType).toBe(false);
});
});
describe("setUnlockedFields", () => {
it("should initialize empty object when getValues returns undefined", () => {
const mockFormMethods = createMockFormMethods(undefined);
const eventType = {
schedulingType: SchedulingType.MANAGED,
userId: 1,
metadata: {},
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
const LockedSwitchComponent = result.current.useLockedSwitch("title");
const rendered = LockedSwitchComponent();
if (rendered?.props.onCheckedChange) {
rendered.props.onCheckedChange(false);
}
expect(mockFormMethods.setValue).toHaveBeenCalledWith(
"metadata.managedEventConfig.unlockedFields",
{ title: true },
{ shouldDirty: true }
);
});
it("should handle delete on empty object when getValues returns undefined", () => {
const mockFormMethods = createMockFormMethods(undefined);
const eventType = {
schedulingType: SchedulingType.MANAGED,
userId: 1,
metadata: {},
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
const LockedSwitchComponent = result.current.useLockedSwitch("title");
const rendered = LockedSwitchComponent();
if (rendered?.props.onCheckedChange) {
rendered.props.onCheckedChange(true);
}
expect(mockFormMethods.setValue).toHaveBeenCalledWith(
"metadata.managedEventConfig.unlockedFields",
{},
{ shouldDirty: true }
);
});
it("should preserve existing fields when adding a new unlocked field", () => {
const existingFields = { description: true };
const mockFormMethods = createMockFormMethods(existingFields);
const eventType = {
schedulingType: SchedulingType.MANAGED,
userId: 1,
metadata: { managedEventConfig: { unlockedFields: existingFields } },
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
const LockedSwitchComponent = result.current.useLockedSwitch("title");
const rendered = LockedSwitchComponent();
if (rendered?.props.onCheckedChange) {
rendered.props.onCheckedChange(false);
}
expect(mockFormMethods.setValue).toHaveBeenCalledWith(
"metadata.managedEventConfig.unlockedFields",
{ description: true, title: true },
{ shouldDirty: true }
);
});
it("should remove field from existing unlockedFields when locking", () => {
const existingFields = { title: true, description: true };
const mockFormMethods = createMockFormMethods(existingFields);
const eventType = {
schedulingType: SchedulingType.MANAGED,
userId: 1,
metadata: { managedEventConfig: { unlockedFields: existingFields } },
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
const LockedSwitchComponent = result.current.useLockedSwitch("title");
const rendered = LockedSwitchComponent();
if (rendered?.props.onCheckedChange) {
rendered.props.onCheckedChange(true);
}
expect(mockFormMethods.setValue).toHaveBeenCalledWith(
"metadata.managedEventConfig.unlockedFields",
{ description: true },
{ shouldDirty: true }
);
});
});
describe("useLockedSwitch", () => {
it("should return a component for managed event type", () => {
const mockFormMethods = createMockFormMethods();
const eventType = {
schedulingType: SchedulingType.MANAGED,
userId: 1,
metadata: {},
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
const LockedSwitchComponent = result.current.useLockedSwitch("title");
const rendered = LockedSwitchComponent();
expect(rendered).not.toBeNull();
expect(rendered?.props["data-testid"]).toBe("locked-indicator-title");
});
it("should return null for non-managed event type", () => {
const mockFormMethods = createMockFormMethods();
const eventType = {
schedulingType: SchedulingType.ROUND_ROBIN,
userId: 1,
metadata: {},
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
const LockedSwitchComponent = result.current.useLockedSwitch("title");
const rendered = LockedSwitchComponent();
expect(rendered).toBeNull();
});
});
describe("shouldLockDisableProps", () => {
it("should return isLocked=true for managed event type when field is not unlocked", () => {
const mockFormMethods = createMockFormMethods({
managedEventConfig: { unlockedFields: {} },
});
const eventType = {
schedulingType: SchedulingType.MANAGED,
userId: 1,
metadata: { managedEventConfig: { unlockedFields: {} } },
id: 1,
};
const { result, rerender } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
act(() => {
result.current.shouldLockDisableProps("title");
});
rerender();
const props = result.current.shouldLockDisableProps("title");
expect(props.isLocked).toBe(true);
expect(props.disabled).toBe(false);
});
it("should return isLocked=false for managed event type when field is unlocked", () => {
const mockFormMethods = createMockFormMethods({
managedEventConfig: { unlockedFields: { title: true } },
});
const eventType = {
schedulingType: SchedulingType.MANAGED,
userId: 1,
metadata: { managedEventConfig: { unlockedFields: { title: true } } },
id: 1,
};
const { result, rerender } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
act(() => {
result.current.shouldLockDisableProps("title");
});
rerender();
const props = result.current.shouldLockDisableProps("title");
expect(props.isLocked).toBe(false);
});
it("should return disabled=true for children managed event type when field is locked", () => {
const mockFormMethods = createMockFormMethods({
managedEventConfig: { unlockedFields: {} },
});
const eventType = {
schedulingType: null,
userId: 1,
metadata: { managedEventConfig: { unlockedFields: {} } },
id: 1,
};
const { result, rerender } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
act(() => {
result.current.shouldLockDisableProps("title");
});
rerender();
const props = result.current.shouldLockDisableProps("title");
expect(props.disabled).toBe(true);
expect(props.isLocked).toBe(true);
});
it("should return disabled=false for children managed event type when field is unlocked", () => {
const mockFormMethods = createMockFormMethods({
managedEventConfig: { unlockedFields: { title: true } },
});
const eventType = {
schedulingType: null,
userId: 1,
metadata: { managedEventConfig: { unlockedFields: { title: true } } },
id: 1,
};
const { result, rerender } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
act(() => {
result.current.shouldLockDisableProps("title");
});
rerender();
const props = result.current.shouldLockDisableProps("title");
expect(props.disabled).toBe(false);
expect(props.isLocked).toBe(false);
});
});
describe("useLockedLabel", () => {
it("should return correct properties for managed event type", () => {
const mockFormMethods = createMockFormMethods({
managedEventConfig: { unlockedFields: {} },
});
const eventType = {
schedulingType: SchedulingType.MANAGED,
userId: 1,
metadata: { managedEventConfig: { unlockedFields: {} } },
id: 1,
};
const { result, rerender } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
act(() => {
result.current.useLockedLabel("title");
});
rerender();
const labelProps = result.current.useLockedLabel("title");
expect(labelProps.isLocked).toBe(true);
expect(labelProps.disabled).toBe(false);
expect(labelProps.LockedIcon).toBeDefined();
});
it("should return disabled=true for children managed event type with locked field", () => {
const mockFormMethods = createMockFormMethods({
managedEventConfig: { unlockedFields: {} },
});
const eventType = {
schedulingType: null,
userId: 1,
metadata: { managedEventConfig: { unlockedFields: {} } },
id: 1,
};
const { result, rerender } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
act(() => {
result.current.useLockedLabel("title");
});
rerender();
const labelProps = result.current.useLockedLabel("title");
expect(labelProps.disabled).toBe(true);
expect(labelProps.isLocked).toBe(true);
});
});
describe("shouldLockIndicator", () => {
it("should return LockedIndicator for managed event type", () => {
const mockFormMethods = createMockFormMethods({
managedEventConfig: { unlockedFields: {} },
});
const eventType = {
schedulingType: SchedulingType.MANAGED,
userId: 1,
metadata: { managedEventConfig: { unlockedFields: {} } },
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
const indicator = result.current.shouldLockIndicator("title");
expect(indicator).toBeTruthy();
});
it("should return LockedIndicator for children managed event type", () => {
const mockFormMethods = createMockFormMethods({
managedEventConfig: { unlockedFields: {} },
});
const eventType = {
schedulingType: null,
userId: 1,
metadata: { managedEventConfig: { unlockedFields: {} } },
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
const indicator = result.current.shouldLockIndicator("title");
expect(indicator).toBeTruthy();
});
it("should return falsy for non-managed event type", () => {
const mockFormMethods = createMockFormMethods();
const eventType = {
schedulingType: SchedulingType.ROUND_ROBIN,
userId: 1,
metadata: {},
id: 1,
};
const { result } = renderHook(() =>
useLockedFieldsManager({
eventType,
translate: mockTranslate,
formMethods: mockFormMethods,
})
);
const indicator = result.current.shouldLockIndicator("title");
expect(indicator).toBeFalsy();
});
});
});
@@ -19,7 +19,7 @@ export const LockedSwitch = (
[fieldState, setFieldState]: [Record<string, boolean>, Dispatch<SetStateAction<Record<string, boolean>>>],
fieldName: string,
setUnlockedFields: (fieldName: string, val: boolean | undefined) => void,
options = { simple: false }
_options = { simple: false }
) => {
return isManagedEventType ? (
<Switch
@@ -114,8 +114,7 @@ const useLockedFieldsManager = ({
const setUnlockedFields = (fieldName: string, val: boolean | undefined) => {
const path = "metadata.managedEventConfig.unlockedFields";
const metaUnlockedFields = getValues(path);
if (!metaUnlockedFields) return;
const metaUnlockedFields = getValues(path) || {};
if (val === undefined) {
delete metaUnlockedFields[fieldName as keyof typeof metaUnlockedFields];
setValue(path, { ...metaUnlockedFields }, { shouldDirty: true });
@@ -130,6 +130,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
managedUserResponseFour.user.id,
]);
await createManagedEventType(+organizationId, team.id, [
managedUserResponseOne.user.id,
managedUserResponseTwo.user.id,
managedUserResponseThree.user.id,
managedUserResponseFour.user.id,
]);
await createOrgMembershipAdmin(+organizationId, managedUserResponseFive.user.id);
return res.status(200).json({
@@ -261,6 +268,31 @@ async function createRoundRobinEventType(orgId: number, teamId: number, userIds:
);
}
async function createManagedEventType(orgId: number, teamId: number, userIds: number[]) {
await fetch(
// eslint-disable-next-line turbo/no-undeclared-env-vars
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/organizations/${orgId}/teams/${teamId}/event-types`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
// eslint-disable-next-line turbo/no-undeclared-env-vars
[X_CAL_SECRET_KEY]: process.env.X_CAL_SECRET_KEY ?? "",
// eslint-disable-next-line turbo/no-undeclared-env-vars
[X_CAL_CLIENT_ID]: process.env.NEXT_PUBLIC_X_CAL_ID ?? "",
origin: "http://localhost:4321",
},
body: JSON.stringify({
lengthInMinutes: 60,
title: "Platform example managed",
slug: "platform-example-managed",
schedulingType: "managed",
hosts: userIds.map((userId) => ({ userId })),
}),
}
);
}
async function createDefaultSchedule(accessToken: string) {
const name = "Default Schedule";
const timeZone = "Europe/London";
@@ -273,7 +305,6 @@ async function createDefaultSchedule(accessToken: string) {
method: "POST",
headers: {
"Content-Type": "application/json",
// eslint-disable-next-line turbo/no-undeclared-env-vars
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({