Handle children on event types, managed events, readonly and team count
This commit is contained in:
parent
9d1ebbebd1
commit
2a2cb14f6c
|
@ -29,7 +29,6 @@ import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import { SchedulingType } from "@calcom/prisma/enums";
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||||
import {
|
import {
|
||||||
|
@ -92,10 +91,12 @@ type EventType = EventTypeList[number];
|
||||||
|
|
||||||
interface EventTypeListProps {
|
interface EventTypeListProps {
|
||||||
data: EventTypeList;
|
data: EventTypeList;
|
||||||
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MobileTeamsTabProps {
|
interface MobileTeamsTabProps {
|
||||||
teamEventTypes: EventTypeList[];
|
teamEventTypes: EventTypeList[];
|
||||||
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
|
@ -103,7 +104,7 @@ const querySchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const MobileTeamsTab: FC<MobileTeamsTabProps> = (props: MobileTeamsTabProps) => {
|
const MobileTeamsTab: FC<MobileTeamsTabProps> = (props: MobileTeamsTabProps) => {
|
||||||
const { teamEventTypes } = props;
|
const { teamEventTypes, readonly } = props;
|
||||||
const orgBranding = useOrgBranding();
|
const orgBranding = useOrgBranding();
|
||||||
const tabs = teamEventTypes
|
const tabs = teamEventTypes
|
||||||
.filter((item) => item !== undefined)
|
.filter((item) => item !== undefined)
|
||||||
|
@ -130,7 +131,7 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props: MobileTeamsTabProps) =>
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<HorizontalTabs tabs={tabs} />
|
<HorizontalTabs tabs={tabs} />
|
||||||
{events && events.length && <EventTypeList data={events} />}
|
{events && events.length && <EventTypeList data={events} readonly={readonly} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -147,9 +148,8 @@ const getEventTypeSlug = (eventType: EventType, t: TFunction) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Item = ({ eventType }: { eventType: EventType }) => {
|
const Item = ({ eventType, readonly }: { eventType: EventType; readonly?: boolean }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const readOnly = false;
|
|
||||||
|
|
||||||
const content = () => (
|
const content = () => (
|
||||||
<div>
|
<div>
|
||||||
|
@ -165,7 +165,7 @@ const Item = ({ eventType }: { eventType: EventType }) => {
|
||||||
{getEventTypeSlug(eventType, t)}
|
{getEventTypeSlug(eventType, t)}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
{readOnly && (
|
{readonly && (
|
||||||
<Badge variant="gray" className="ml-2">
|
<Badge variant="gray" className="ml-2">
|
||||||
{t("readonly")}
|
{t("readonly")}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
@ -173,7 +173,7 @@ const Item = ({ eventType }: { eventType: EventType }) => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return readOnly ? (
|
return readonly ? (
|
||||||
<div className="flex-1 overflow-hidden pr-4 text-sm">
|
<div className="flex-1 overflow-hidden pr-4 text-sm">
|
||||||
{content()}
|
{content()}
|
||||||
<EventTypeDescription
|
<EventTypeDescription
|
||||||
|
@ -200,7 +200,7 @@ const Item = ({ eventType }: { eventType: EventType }) => {
|
||||||
{getEventTypeSlug(eventType, t)}
|
{getEventTypeSlug(eventType, t)}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
{readOnly && (
|
{readonly && (
|
||||||
<Badge variant="gray" className="ml-2">
|
<Badge variant="gray" className="ml-2">
|
||||||
{t("readonly")}
|
{t("readonly")}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
@ -220,7 +220,7 @@ const Item = ({ eventType }: { eventType: EventType }) => {
|
||||||
|
|
||||||
const MemoizedItem = memo(Item);
|
const MemoizedItem = memo(Item);
|
||||||
|
|
||||||
export const EventTypeList = ({ data }: EventTypeListProps): JSX.Element => {
|
export const EventTypeList = ({ data, readonly }: EventTypeListProps): JSX.Element => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
@ -391,11 +391,10 @@ export const EventTypeList = ({ data }: EventTypeListProps): JSX.Element => {
|
||||||
const calLink = `${orgBranding?.fullDomain ?? CAL_URL}/${embedLink}`;
|
const calLink = `${orgBranding?.fullDomain ?? CAL_URL}/${embedLink}`;
|
||||||
|
|
||||||
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
|
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
|
||||||
const eventTypeMetadata = EventTypeMetaDataSchema.safeParse(eventType.metadata);
|
|
||||||
const isChildrenManagedEventType =
|
const isChildrenManagedEventType = !!eventType.parentId;
|
||||||
eventTypeMetadata.success &&
|
const isReadOnly = readonly || isChildrenManagedEventType;
|
||||||
eventTypeMetadata.data?.managedEventConfig !== undefined &&
|
|
||||||
eventType.schedulingType !== SchedulingType.MANAGED;
|
|
||||||
return (
|
return (
|
||||||
<li key={eventType.id}>
|
<li key={eventType.id}>
|
||||||
<div className="hover:bg-muted flex w-full items-center justify-between">
|
<div className="hover:bg-muted flex w-full items-center justify-between">
|
||||||
|
@ -406,7 +405,7 @@ export const EventTypeList = ({ data }: EventTypeListProps): JSX.Element => {
|
||||||
{!(lastItem && lastItem.id === eventType.id) && (
|
{!(lastItem && lastItem.id === eventType.id) && (
|
||||||
<ArrowButton onClick={() => moveEventType(index, 1)} arrowDirection="down" />
|
<ArrowButton onClick={() => moveEventType(index, 1)} arrowDirection="down" />
|
||||||
)}
|
)}
|
||||||
<MemoizedItem eventType={eventType} />
|
<MemoizedItem eventType={eventType} readonly={isReadOnly} />
|
||||||
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
||||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||||
{eventType.team && !isManagedEventType && (
|
{eventType.team && !isManagedEventType && (
|
||||||
|
@ -425,16 +424,20 @@ export const EventTypeList = ({ data }: EventTypeListProps): JSX.Element => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isManagedEventType && type?.children && type.children?.length > 0 && (
|
{isManagedEventType && eventType.children?.length > 0 && (
|
||||||
<AvatarGroup
|
<AvatarGroup
|
||||||
className="relative right-3 top-1"
|
className="relative right-3 top-1"
|
||||||
size="sm"
|
size="sm"
|
||||||
truncateAfter={4}
|
truncateAfter={4}
|
||||||
items={eventType.users.map((user: Pick<User, "name" | "username">) => ({
|
items={eventType.children.map(
|
||||||
alt: user.name || "",
|
({ users }: { users: Pick<User, "name" | "username">[] }) => ({
|
||||||
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${user.username}/avatar.png`,
|
alt: users[0].name || "",
|
||||||
title: user.name || "",
|
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${
|
||||||
}))}
|
users[0].username
|
||||||
|
}/avatar.png`,
|
||||||
|
title: users[0].name || "",
|
||||||
|
})
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
|
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
|
||||||
|
@ -485,7 +488,7 @@ export const EventTypeList = ({ data }: EventTypeListProps): JSX.Element => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Dropdown modal={false}>
|
<Dropdown modal={isReadOnly}>
|
||||||
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + eventType.id}>
|
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + eventType.id}>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -496,7 +499,7 @@ export const EventTypeList = ({ data }: EventTypeListProps): JSX.Element => {
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
{!false && (
|
{!isReadOnly && (
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -534,8 +537,7 @@ export const EventTypeList = ({ data }: EventTypeListProps): JSX.Element => {
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
||||||
{/* @TODO: */}
|
{isReadOnly && !isChildrenManagedEventType && (
|
||||||
{false && !isChildrenManagedEventType && (
|
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
|
@ -612,7 +614,7 @@ export const EventTypeList = ({ data }: EventTypeListProps): JSX.Element => {
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
{!false && (
|
{!isReadOnly && (
|
||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={() => router.push("/event-types/" + eventType.id)}
|
onClick={() => router.push("/event-types/" + eventType.id)}
|
||||||
|
@ -633,8 +635,7 @@ export const EventTypeList = ({ data }: EventTypeListProps): JSX.Element => {
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
||||||
{/* Readonly @TODO: */}
|
{isReadOnly && !isChildrenManagedEventType && (
|
||||||
{false && !isChildrenManagedEventType && (
|
|
||||||
<>
|
<>
|
||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
|
@ -913,7 +914,7 @@ const Main = ({ filters }: { filters: ReturnType<typeof getTeamsFiltersFromQuery
|
||||||
const teamEventTypesForTabs = eventTypePaginate
|
const teamEventTypesForTabs = eventTypePaginate
|
||||||
.map((trpcFetch) => {
|
.map((trpcFetch) => {
|
||||||
const { data } = trpcFetch;
|
const { data } = trpcFetch;
|
||||||
return data;
|
return data || [];
|
||||||
})
|
})
|
||||||
.filter((item) => item && item.length > 0);
|
.filter((item) => item && item.length > 0);
|
||||||
return (
|
return (
|
||||||
|
@ -925,7 +926,8 @@ const Main = ({ filters }: { filters: ReturnType<typeof getTeamsFiltersFromQuery
|
||||||
<EventTypeListHeading
|
<EventTypeListHeading
|
||||||
teamSlugOrUsername={mainUser.username || ""}
|
teamSlugOrUsername={mainUser.username || ""}
|
||||||
teamNameOrUserName={mainUser.name || ""}
|
teamNameOrUserName={mainUser.name || ""}
|
||||||
membershipCount={firstElementPersonalEventTypes.team?.members.length || 0}
|
// Single event types have no teamMembershipCount
|
||||||
|
membershipCount={0}
|
||||||
teamId={firstElementPersonalEventTypes.teamId}
|
teamId={firstElementPersonalEventTypes.teamId}
|
||||||
orgSlug={orgBranding?.slug}
|
orgSlug={orgBranding?.slug}
|
||||||
/>
|
/>
|
||||||
|
@ -939,11 +941,13 @@ const Main = ({ filters }: { filters: ReturnType<typeof getTeamsFiltersFromQuery
|
||||||
{/* Then we list team event types */}
|
{/* Then we list team event types */}
|
||||||
{eventTypePaginate.map((trpcFetch, index) => {
|
{eventTypePaginate.map((trpcFetch, index) => {
|
||||||
const { data: teamEventTypes } = trpcFetch;
|
const { data: teamEventTypes } = trpcFetch;
|
||||||
const [firstElementTeamEventTypes] = data || [];
|
const [firstElementTeamEventTypes] = teamEventTypes || [];
|
||||||
|
|
||||||
if (!teamEventTypes || teamEventTypes.length === 0 || !firstElementTeamEventTypes.team) {
|
if (!teamEventTypes || teamEventTypes.length === 0 || !firstElementTeamEventTypes.team) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const teamDataMatchWithEventType = teams && teams.length > 0 && teams[index];
|
||||||
|
const membershipCount = teamDataMatchWithEventType?.membershipCount || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -951,13 +955,17 @@ const Main = ({ filters }: { filters: ReturnType<typeof getTeamsFiltersFromQuery
|
||||||
key={index}
|
key={index}
|
||||||
teamSlugOrUsername={firstElementTeamEventTypes.team.slug || ""}
|
teamSlugOrUsername={firstElementTeamEventTypes.team.slug || ""}
|
||||||
teamNameOrUserName={firstElementTeamEventTypes.team.name || ""}
|
teamNameOrUserName={firstElementTeamEventTypes.team.name || ""}
|
||||||
membershipCount={firstElementTeamEventTypes.team?.members.length || 0}
|
membershipCount={membershipCount}
|
||||||
teamId={firstElementTeamEventTypes.teamId}
|
teamId={firstElementTeamEventTypes.teamId}
|
||||||
orgSlug={orgBranding?.slug}
|
orgSlug={orgBranding?.slug}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{teamEventTypes.length > 0 ? (
|
{teamEventTypes.length > 0 ? (
|
||||||
<EventTypeList data={teamEventTypes} key={index} />
|
<EventTypeList
|
||||||
|
data={teamEventTypes}
|
||||||
|
key={index}
|
||||||
|
readonly={"MEMBER" === teamDataMatchWithEventType.role}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyEventTypeList
|
<EmptyEventTypeList
|
||||||
teamId={firstElementTeamEventTypes.teamId}
|
teamId={firstElementTeamEventTypes.teamId}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
-- View: public.TeamMemberCount
|
||||||
|
|
||||||
|
-- DROP VIEW public."TeamMemberCount";
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW public."TeamMemberCount"
|
||||||
|
AS
|
||||||
|
SELECT t.id,
|
||||||
|
count(*) AS count
|
||||||
|
FROM "Membership" m
|
||||||
|
LEFT JOIN "Team" t ON t.id = m."teamId"
|
||||||
|
WHERE m.accepted = true
|
||||||
|
GROUP BY t.id;
|
|
@ -900,6 +900,7 @@ model SelectedSlots {
|
||||||
@@unique(fields: [userId, slotUtcStartDate, slotUtcEndDate, uid], name: "selectedSlotUnique")
|
@@unique(fields: [userId, slotUtcStartDate, slotUtcEndDate, uid], name: "selectedSlotUnique")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Let's keep views at the bottom of the schema
|
||||||
view BookingTimeStatus {
|
view BookingTimeStatus {
|
||||||
id Int @unique
|
id Int @unique
|
||||||
uid String?
|
uid String?
|
||||||
|
@ -919,3 +920,8 @@ view BookingTimeStatus {
|
||||||
timeStatus String?
|
timeStatus String?
|
||||||
eventParentId Int?
|
eventParentId Int?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
view TeamMemberCount {
|
||||||
|
id Int @unique
|
||||||
|
count Int
|
||||||
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ export const paginateHandler = async ({ ctx, input }: EventTypesPaginateProps) =
|
||||||
hidden: true,
|
hidden: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
teamId: true,
|
teamId: true,
|
||||||
|
parentId: true,
|
||||||
users: {
|
users: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
@ -62,23 +63,29 @@ export const paginateHandler = async ({ ctx, input }: EventTypesPaginateProps) =
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
children: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
users: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
team: {
|
team: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
name: true,
|
name: true,
|
||||||
members: {
|
|
||||||
select: {
|
|
||||||
userId: true,
|
|
||||||
role: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
skip,
|
skip,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
});
|
});
|
||||||
console.log("result", result);
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
|
@ -59,6 +59,17 @@ export const listHandler = async ({ ctx }: ListOptions) => {
|
||||||
orderBy: { role: "desc" },
|
orderBy: { role: "desc" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// This can be optimized by using a custom view between membership and team and teamMemberCount
|
||||||
|
const membershipCount = await prisma.teamMemberCount.findMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: memberships.map((m) => m.teamId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log({ membershipCount });
|
||||||
|
|
||||||
return memberships
|
return memberships
|
||||||
.filter((mmship) => {
|
.filter((mmship) => {
|
||||||
const metadata = teamMetadataSchema.parse(mmship.team.metadata);
|
const metadata = teamMetadataSchema.parse(mmship.team.metadata);
|
||||||
|
@ -70,5 +81,6 @@ export const listHandler = async ({ ctx }: ListOptions) => {
|
||||||
..._team,
|
..._team,
|
||||||
/** To prevent breaking we only return non-email attached token here, if we have one */
|
/** To prevent breaking we only return non-email attached token here, if we have one */
|
||||||
inviteToken: inviteTokens.find((token) => token.identifier === "invite-link-for-teamId-" + _team.id),
|
inviteToken: inviteTokens.find((token) => token.identifier === "invite-link-for-teamId-" + _team.id),
|
||||||
|
membershipCount: membershipCount.find((m) => m.id === _team.id)?.count || 0,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user