* mv * wip * wip * wip * wip * plural * fix: resolve TypeScript type errors for attribute refactoring - Add TransformedAttributeOption type to handle transformed contains field - Update imports to use routing-forms Attribute type where needed - Fix getValueOfAttributeOption to use TransformedAttributeOption type - Update AttributeOptionValueWithType to use TransformedAttributeOption - Fix pre-existing lint warnings (any -> unknown, proper type casting) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix * cleanup * cleanup * fix * fix * fix * fix * fix * fix: add explicit type annotation to reduce callback in getAttributes.ts Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: use RouterOutputs from @calcom/trpc/react for consistent type inference - Update AppPage.tsx to use RouterOutputs from @calcom/trpc/react instead of inferRouterOutputs<AppRouter> - Update MultiDisconnectIntegration.tsx to use the same RouterOutputs type source - Add eslint-disable comments for pre-existing warnings (useEffect deps, img elements) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: explicitly type attribute property in AttributeOptionAssignment The attribute property from Pick<AttributeOption, 'attribute'> was typed as 'unknown' because Prisma relations don't have explicit types when picked. This explicitly defines the attribute shape with id, type, and isLocked properties that are used in assignValueToUser.ts. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: remove unused assignedUsers from AttributeOptionAssignment Pick The assignedUsers property was not being used in the code but was required by the Pick type, causing a type mismatch when the actual data from the database query didn't include it. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: update import path for AttributeOptionAssignment and BulkAttributeAssigner types The types.d.ts file was moved from packages/lib/service/attribute/ to packages/app-store/routing-forms/types/. Updated the import path in utils.ts to point to the new location. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
481 lines
15 KiB
TypeScript
481 lines
15 KiB
TypeScript
import prismock from "../../../../tests/libs/__mocks__/prisma";
|
|
|
|
import { describe, expect, it, beforeEach } from "vitest";
|
|
|
|
import type { Attribute } from "@calcom/app-store/routing-forms/types/types";
|
|
import type { AttributeOption } from "@calcom/prisma/client";
|
|
import { AttributeType, MembershipRole } from "@calcom/prisma/enums";
|
|
|
|
import { getAttributesForTeam, getAttributesAssignmentData, getUsersAttributes } from "./getAttributes";
|
|
|
|
// Helper functions to create test data
|
|
async function createMockAttribute({
|
|
orgId,
|
|
options,
|
|
...attributeData
|
|
}: {
|
|
orgId: number;
|
|
id: string;
|
|
type: AttributeType;
|
|
name: string;
|
|
slug: string;
|
|
options: Pick<AttributeOption, "id" | "value" | "slug" | "contains" | "isGroup">[];
|
|
}) {
|
|
const attribute = await prismock.attribute.create({
|
|
data: {
|
|
...attributeData,
|
|
teamId: orgId,
|
|
enabled: true,
|
|
usersCanEditRelation: true,
|
|
isWeightsEnabled: false,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
});
|
|
|
|
await prismock.attributeOption.createMany({
|
|
data: options.map((option) => ({
|
|
...option,
|
|
attributeId: attribute.id,
|
|
})),
|
|
});
|
|
|
|
const result = await prismock.attribute.findUnique({
|
|
where: { id: attribute.id },
|
|
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
|
include: { options: true },
|
|
});
|
|
if (!result) {
|
|
throw new Error("Some error in creating mock attribute");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function createMockUserHavingMembershipWithBothTeamAndOrg({
|
|
orgId,
|
|
teamId,
|
|
}: {
|
|
orgId: number;
|
|
teamId: number;
|
|
}) {
|
|
const user = await prismock.user.create({
|
|
data: {
|
|
name: "Test User",
|
|
email: "test@test.com",
|
|
},
|
|
});
|
|
|
|
// Create org membership
|
|
const orgMembership = await prismock.membership.create({
|
|
data: {
|
|
role: MembershipRole.MEMBER,
|
|
disableImpersonation: false,
|
|
accepted: true,
|
|
teamId: orgId,
|
|
userId: user.id,
|
|
},
|
|
});
|
|
|
|
// Create team membership
|
|
const teamMembership = await prismock.membership.create({
|
|
data: {
|
|
role: MembershipRole.MEMBER,
|
|
disableImpersonation: false,
|
|
accepted: true,
|
|
teamId: teamId,
|
|
userId: user.id,
|
|
},
|
|
});
|
|
|
|
return { user, teamMembership, orgMembership };
|
|
}
|
|
|
|
async function createMockTeam({ orgId }: { orgId: number | null }) {
|
|
return await prismock.team.create({
|
|
data: {
|
|
name: "Test Team",
|
|
slug: "test-team",
|
|
parentId: orgId,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function createMockAttributeAssignment({
|
|
orgMembershipId,
|
|
attributeOptionId,
|
|
}: {
|
|
orgMembershipId: number;
|
|
attributeOptionId: string;
|
|
}) {
|
|
return await prismock.attributeToUser.create({
|
|
data: {
|
|
memberId: orgMembershipId,
|
|
attributeOptionId,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function expectAttributeToMatch(attribute: Attribute, expected: Attribute) {
|
|
expect(attribute).toMatchObject({
|
|
id: expected.id,
|
|
name: expected.name,
|
|
slug: expected.slug,
|
|
type: expected.type,
|
|
});
|
|
}
|
|
|
|
async function expectAttributesToMatch(attributes: Attribute[], expected: Attribute[]) {
|
|
expect(attributes).toHaveLength(expected.length);
|
|
for (let i = 0; i < attributes.length; i++) {
|
|
expectAttributeToMatch(attributes[i], expected[i]);
|
|
}
|
|
}
|
|
|
|
describe("getAttributes", () => {
|
|
const orgId = 1001;
|
|
|
|
beforeEach(async () => {
|
|
// @ts-expect-error reset is a method on Prismock
|
|
await prismock.reset();
|
|
});
|
|
|
|
describe("getAttributesForTeam", () => {
|
|
it("should return empty array for team without parent (non-org team)", async () => {
|
|
const teamWithoutParent = await createMockTeam({ orgId: null });
|
|
await createMockAttribute({
|
|
orgId,
|
|
id: "attr1",
|
|
name: "Department",
|
|
slug: "department",
|
|
type: AttributeType.SINGLE_SELECT,
|
|
options: [
|
|
{ id: "opt1", value: "Engineering", slug: "engineering", isGroup: false, contains: [] },
|
|
{ id: "opt2", value: "Sales", slug: "sales", isGroup: false, contains: [] },
|
|
],
|
|
});
|
|
|
|
const attributes = await getAttributesForTeam({ teamId: teamWithoutParent.id });
|
|
expect(attributes).toHaveLength(0);
|
|
});
|
|
|
|
it("should return attributes assigned to team members", async () => {
|
|
const team = await createMockTeam({ orgId });
|
|
const { teamMembership } = await createMockUserHavingMembershipWithBothTeamAndOrg({
|
|
orgId,
|
|
teamId: team.id,
|
|
});
|
|
|
|
const attribute = await createMockAttribute({
|
|
orgId,
|
|
id: "attr1",
|
|
name: "Department",
|
|
slug: "department",
|
|
type: AttributeType.SINGLE_SELECT,
|
|
options: [
|
|
{ id: "opt1", value: "Engineering", slug: "engineering", isGroup: false, contains: [] },
|
|
{ id: "opt2", value: "Sales", slug: "sales", isGroup: false, contains: [] },
|
|
],
|
|
});
|
|
|
|
await createMockAttributeAssignment({
|
|
orgMembershipId: teamMembership.id,
|
|
attributeOptionId: "opt1",
|
|
});
|
|
|
|
const attributes = await getAttributesForTeam({ teamId: team.id });
|
|
|
|
expect(attributes).toHaveLength(1);
|
|
expect(attributes[0]).toMatchObject({
|
|
id: attribute.id,
|
|
name: attribute.name,
|
|
type: attribute.type,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getAttributesAssignmentData", () => {
|
|
it("should return team members with their assigned attributes", async () => {
|
|
const team = await createMockTeam({ orgId });
|
|
const { user, orgMembership } = await createMockUserHavingMembershipWithBothTeamAndOrg({
|
|
orgId,
|
|
teamId: team.id,
|
|
});
|
|
|
|
const attr1 = await createMockAttribute({
|
|
orgId,
|
|
id: "attr1",
|
|
name: "Department",
|
|
slug: "department",
|
|
type: AttributeType.MULTI_SELECT,
|
|
options: [
|
|
{ id: "engineering-id", value: "Engineering", slug: "engineering", isGroup: false, contains: [] },
|
|
{ id: "sales-id", value: "Sales", slug: "sales", isGroup: false, contains: [] },
|
|
{
|
|
id: "engineering-and-sales-id",
|
|
value: "EngineeringAndSales",
|
|
slug: "engineering-and-sales",
|
|
isGroup: true,
|
|
contains: ["engineering-id", "sales-id"],
|
|
},
|
|
],
|
|
});
|
|
|
|
const attr2 = await createMockAttribute({
|
|
orgId,
|
|
id: "attr2",
|
|
name: "Territory",
|
|
slug: "territory",
|
|
type: AttributeType.SINGLE_SELECT,
|
|
options: [
|
|
{ id: "india-id", value: "India", slug: "india", isGroup: false, contains: [] },
|
|
{ id: "china-id", value: "China", slug: "china", isGroup: false, contains: [] },
|
|
],
|
|
});
|
|
|
|
// Unassigned attribute
|
|
const attr3 = await createMockAttribute({
|
|
orgId,
|
|
id: "attr3",
|
|
name: "Attr3",
|
|
slug: "attr3",
|
|
type: AttributeType.SINGLE_SELECT,
|
|
options: [
|
|
{ id: "opt1", value: "Opt1", slug: "opt1", isGroup: false, contains: [] },
|
|
{ id: "opt2", value: "Opt2", slug: "opt2", isGroup: false, contains: [] },
|
|
],
|
|
});
|
|
|
|
await createMockAttributeAssignment({
|
|
orgMembershipId: orgMembership.id,
|
|
// Assigning a group option that has its sub-options not assigned directly to the user
|
|
attributeOptionId: "engineering-and-sales-id",
|
|
});
|
|
|
|
await createMockAttributeAssignment({
|
|
orgMembershipId: orgMembership.id,
|
|
attributeOptionId: "india-id",
|
|
});
|
|
|
|
await createMockAttributeAssignment({
|
|
orgMembershipId: orgMembership.id,
|
|
attributeOptionId: "engineering-id",
|
|
});
|
|
|
|
const { attributesOfTheOrg, attributesAssignedToTeamMembersWithOptions } =
|
|
await getAttributesAssignmentData({ teamId: team.id, orgId });
|
|
|
|
expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(1);
|
|
expect(attributesAssignedToTeamMembersWithOptions[0]).toEqual({
|
|
userId: user.id,
|
|
attributes: {
|
|
attr1: {
|
|
type: AttributeType.MULTI_SELECT,
|
|
attributeOption: [
|
|
{
|
|
isGroup: true,
|
|
value: "EngineeringAndSales",
|
|
contains: [
|
|
{
|
|
id: "engineering-id",
|
|
slug: "engineering",
|
|
value: "Engineering",
|
|
},
|
|
{ id: "sales-id", slug: "sales", value: "Sales" },
|
|
],
|
|
},
|
|
{
|
|
isGroup: false,
|
|
value: "Engineering",
|
|
contains: [],
|
|
},
|
|
],
|
|
},
|
|
attr2: {
|
|
type: AttributeType.SINGLE_SELECT,
|
|
attributeOption: { isGroup: false, value: "India", contains: [] },
|
|
},
|
|
// attr3 - unassigned isn't here
|
|
},
|
|
});
|
|
|
|
expectAttributesToMatch(attributesOfTheOrg, [attr1, attr2, attr3]);
|
|
});
|
|
|
|
it("should return contains correctly for group options that have sub-options not assigned directly to the user", async () => {
|
|
const team = await createMockTeam({ orgId });
|
|
const { user, orgMembership } = await createMockUserHavingMembershipWithBothTeamAndOrg({
|
|
orgId,
|
|
teamId: team.id,
|
|
});
|
|
|
|
const attr1 = await createMockAttribute({
|
|
orgId,
|
|
id: "attr1",
|
|
name: "Department",
|
|
slug: "department",
|
|
type: AttributeType.MULTI_SELECT,
|
|
options: [
|
|
{ id: "engineering-id", value: "Engineering", slug: "engineering", isGroup: false, contains: [] },
|
|
{ id: "sales-id", value: "Sales", slug: "sales", isGroup: false, contains: [] },
|
|
{
|
|
id: "engineering-and-sales-id",
|
|
value: "EngineeringAndSales",
|
|
slug: "engineering-and-sales",
|
|
isGroup: true,
|
|
contains: ["engineering-id", "sales-id"],
|
|
},
|
|
],
|
|
});
|
|
|
|
await createMockAttributeAssignment({
|
|
orgMembershipId: orgMembership.id,
|
|
// Assigning a group option that has its sub-options not assigned directly to the user
|
|
attributeOptionId: "engineering-and-sales-id",
|
|
});
|
|
|
|
const { attributesOfTheOrg, attributesAssignedToTeamMembersWithOptions } =
|
|
await getAttributesAssignmentData({ teamId: team.id, orgId });
|
|
|
|
expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(1);
|
|
expect(attributesAssignedToTeamMembersWithOptions[0]).toEqual({
|
|
userId: user.id,
|
|
attributes: {
|
|
attr1: {
|
|
type: AttributeType.MULTI_SELECT,
|
|
attributeOption: {
|
|
isGroup: true,
|
|
value: "EngineeringAndSales",
|
|
contains: [
|
|
{
|
|
id: "engineering-id",
|
|
slug: "engineering",
|
|
value: "Engineering",
|
|
},
|
|
{ id: "sales-id", slug: "sales", value: "Sales" },
|
|
],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expectAttributesToMatch(attributesOfTheOrg, [attr1]);
|
|
});
|
|
|
|
it("should return no attributes for a different org", async () => {
|
|
const team = await createMockTeam({ orgId });
|
|
const otherOrgId = 1002;
|
|
const otherOrgsTeam = await createMockTeam({ orgId: otherOrgId });
|
|
const { orgMembership } = await createMockUserHavingMembershipWithBothTeamAndOrg({
|
|
orgId,
|
|
teamId: team.id,
|
|
});
|
|
|
|
await createMockAttribute({
|
|
orgId,
|
|
id: "attr1",
|
|
name: "Department",
|
|
slug: "department",
|
|
type: AttributeType.MULTI_SELECT,
|
|
options: [
|
|
{ id: "engineering-id", value: "Engineering", slug: "engineering", isGroup: false, contains: [] },
|
|
{ id: "sales-id", value: "Sales", slug: "sales", isGroup: false, contains: [] },
|
|
{
|
|
id: "engineering-and-sales-id",
|
|
value: "EngineeringAndSales",
|
|
slug: "engineering-and-sales",
|
|
isGroup: true,
|
|
contains: ["engineering-id", "sales-id"],
|
|
},
|
|
],
|
|
});
|
|
|
|
await createMockAttributeAssignment({
|
|
orgMembershipId: orgMembership.id,
|
|
// Assigning a group option that has its sub-options not assigned directly to the user
|
|
attributeOptionId: "engineering-and-sales-id",
|
|
});
|
|
|
|
const { attributesOfTheOrg, attributesAssignedToTeamMembersWithOptions } =
|
|
await getAttributesAssignmentData({ teamId: otherOrgsTeam.id, orgId: otherOrgId });
|
|
|
|
expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(0);
|
|
// Because created attribute is of other org, it should not be returned
|
|
expect(attributesOfTheOrg).toHaveLength(0);
|
|
});
|
|
|
|
it("should handle multi-select attributes correctly", async () => {
|
|
const team = await createMockTeam({ orgId });
|
|
const { orgMembership } = await createMockUserHavingMembershipWithBothTeamAndOrg({
|
|
orgId,
|
|
teamId: team.id,
|
|
});
|
|
|
|
const attr1 = await createMockAttribute({
|
|
orgId,
|
|
id: "attr1",
|
|
name: "Skills",
|
|
slug: "skills",
|
|
type: AttributeType.MULTI_SELECT,
|
|
options: [
|
|
{ id: "opt1", value: "JavaScript", slug: "javascript", isGroup: false, contains: [] },
|
|
{ id: "opt2", value: "Python", slug: "python", isGroup: false, contains: [] },
|
|
],
|
|
});
|
|
|
|
// Assign multiple skills
|
|
await createMockAttributeAssignment({
|
|
orgMembershipId: orgMembership.id,
|
|
attributeOptionId: "opt1",
|
|
});
|
|
await createMockAttributeAssignment({
|
|
orgMembershipId: orgMembership.id,
|
|
attributeOptionId: "opt2",
|
|
});
|
|
|
|
const { attributesOfTheOrg, attributesAssignedToTeamMembersWithOptions } =
|
|
await getAttributesAssignmentData({ teamId: team.id, orgId });
|
|
|
|
expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(1);
|
|
expect(attributesAssignedToTeamMembersWithOptions[0].attributes.attr1.attributeOption).toEqual([
|
|
{ value: "JavaScript", isGroup: false, contains: [] },
|
|
{ value: "Python", isGroup: false, contains: [] },
|
|
]);
|
|
|
|
expectAttributesToMatch(attributesOfTheOrg, [attr1]);
|
|
});
|
|
});
|
|
|
|
describe("getUsersAttributes", () => {
|
|
it("should return attributes assigned to specific user in team", async () => {
|
|
const team = await createMockTeam({ orgId });
|
|
const { user, orgMembership } = await createMockUserHavingMembershipWithBothTeamAndOrg({
|
|
orgId,
|
|
teamId: team.id,
|
|
});
|
|
|
|
const attribute = await createMockAttribute({
|
|
orgId,
|
|
id: "attr1",
|
|
name: "Department",
|
|
slug: "department",
|
|
type: AttributeType.SINGLE_SELECT,
|
|
options: [{ id: "opt1", value: "Engineering", slug: "engineering", isGroup: false, contains: [] }],
|
|
});
|
|
|
|
await createMockAttributeAssignment({
|
|
orgMembershipId: orgMembership.id,
|
|
attributeOptionId: "opt1",
|
|
});
|
|
|
|
const userAttributes = await getUsersAttributes({ userId: user.id, teamId: team.id });
|
|
|
|
expect(userAttributes).toHaveLength(1);
|
|
expect(userAttributes[0]).toMatchObject({
|
|
id: attribute.id,
|
|
name: attribute.name,
|
|
type: attribute.type,
|
|
});
|
|
});
|
|
});
|
|
});
|