feat: lock event types for org users (#14000)
* chroe: org settings schema * wip: UI work + modal on toggle * chore: update i18n * fix: list handler * fix: update handler on untoggle * chore: extract * feat: update handler for hide and delete logic * chore: radio translations * chore: fix modal state from flickering * feat: update copy & add danger colour on delete focus * chore: update copy * design: remove modal icon * chore: include more seeds * fix: update handler * feat: hide create for profile user * feat: use readonly instead * feat: hide personal event type header when read only * chore: move away form readOnly as it was used in other ways * chore: fix read-only bug * fix: hide readonly create screen * fix: show managed event types when events are locked * fix: hide if not admin * Update packages/trpc/server/routers/viewer/eventTypes/create.handler.ts Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * fix: feedback * fix: lock eventtype single switch * fix: empty state create button * fix: more profileGroup check outside CTA to pass value to filter too --------- Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
co-authored by
Carina Wollendorfer
Udit Takkar
CarinaWolli
parent
3aff79e7d1
commit
09ca75eb1f
@@ -204,6 +204,7 @@ function EventTypeSingleLayout({
|
||||
activeWebhooksNumber,
|
||||
}: Props) {
|
||||
const { t } = useLocale();
|
||||
const eventTypesLockedByOrg = eventType.team?.parent?.organizationSettings?.lockEventTypeCreationForUsers;
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
@@ -341,6 +342,7 @@ function EventTypeSingleLayout({
|
||||
<div className="self-center rounded-md p-2">
|
||||
<Switch
|
||||
id="hiddenSwitch"
|
||||
disabled={eventTypesLockedByOrg}
|
||||
checked={!formMethods.watch("hidden")}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hidden", !e, { shouldDirty: true });
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
import type { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
@@ -109,6 +110,7 @@ interface EventTypeListProps {
|
||||
readOnly: boolean;
|
||||
bookerUrl: string | null;
|
||||
types: DeNormalizedEventType[];
|
||||
lockedByOrg?: boolean;
|
||||
}
|
||||
|
||||
interface MobileTeamsTabProps {
|
||||
@@ -230,6 +232,7 @@ export const EventTypeList = ({
|
||||
readOnly,
|
||||
types,
|
||||
bookerUrl,
|
||||
lockedByOrg,
|
||||
}: EventTypeListProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
@@ -390,8 +393,10 @@ export const EventTypeList = ({
|
||||
if (!types.length) {
|
||||
return group.teamId ? (
|
||||
<EmptyEventTypeList group={group} />
|
||||
) : (
|
||||
) : !group.profile.eventTypesLockedByOrg ? (
|
||||
<CreateFirstEventTypeView slug={group.profile.slug ?? ""} />
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -450,6 +455,7 @@ export const EventTypeList = ({
|
||||
<div className="self-center rounded-md p-2">
|
||||
<Switch
|
||||
name="Hidden"
|
||||
disabled={lockedByOrg}
|
||||
checked={!type.hidden}
|
||||
onCheckedChange={() => {
|
||||
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
|
||||
@@ -803,21 +809,22 @@ const CreateFirstEventTypeView = ({ slug }: { slug: string }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const CTA = ({ data, isOrganization }: { data: GetByViewerResponse; isOrganization: boolean }) => {
|
||||
const CTA = ({
|
||||
profileOptions,
|
||||
isOrganization,
|
||||
}: {
|
||||
profileOptions: {
|
||||
teamId: number | null | undefined;
|
||||
label: string | null;
|
||||
image: string;
|
||||
membershipRole: MembershipRole | null | undefined;
|
||||
slug: string | null;
|
||||
}[];
|
||||
isOrganization: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
if (!data) return null;
|
||||
const profileOptions = data.profiles
|
||||
.filter((profile) => !profile.readOnly)
|
||||
.map((profile) => {
|
||||
return {
|
||||
teamId: profile.teamId,
|
||||
label: profile.name || profile.slug,
|
||||
image: profile.image,
|
||||
membershipRole: profile.membershipRole,
|
||||
slug: profile.slug,
|
||||
};
|
||||
});
|
||||
if (!profileOptions.length) return null;
|
||||
|
||||
return (
|
||||
<CreateButton
|
||||
@@ -831,10 +838,10 @@ const CTA = ({ data, isOrganization }: { data: GetByViewerResponse; isOrganizati
|
||||
);
|
||||
};
|
||||
|
||||
const Actions = () => {
|
||||
const Actions = (props: { showDivider: boolean }) => {
|
||||
return (
|
||||
<div className="hidden items-center md:flex">
|
||||
<TeamsFilter useProfileFilter popoverTriggerClassNames="mb-0" showVerticalDivider={true} />
|
||||
<TeamsFilter useProfileFilter popoverTriggerClassNames="mb-0" showVerticalDivider={props.showDivider} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -885,7 +892,6 @@ const Main = ({
|
||||
rawData.eventTypeGroups.length === 1;
|
||||
|
||||
const data = denormalizePayload(rawData);
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.eventTypeGroups.length > 1 || isFilteredByOnlyOneItem ? (
|
||||
@@ -893,33 +899,45 @@ const Main = ({
|
||||
{isMobile ? (
|
||||
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
|
||||
) : (
|
||||
data.eventTypeGroups.map((group, index: number) => (
|
||||
<div
|
||||
className="mt-4 flex flex-col"
|
||||
data-testid={`slug-${group.profile.slug}`}
|
||||
key={group.profile.slug}>
|
||||
<EventTypeListHeading
|
||||
profile={group.profile}
|
||||
membershipCount={group.metadata.membershipCount}
|
||||
teamId={group.teamId}
|
||||
bookerUrl={group.bookerUrl}
|
||||
/>
|
||||
data.eventTypeGroups.map((group, index: number) => {
|
||||
const eventsLockedByOrg = group.profile.eventTypesLockedByOrg;
|
||||
const userHasManagedOrHiddenEventTypes = group.eventTypes.find(
|
||||
(event) => event.metadata?.managedEventConfig || event.hidden
|
||||
);
|
||||
if (eventsLockedByOrg && !userHasManagedOrHiddenEventTypes) return null;
|
||||
return (
|
||||
<div
|
||||
className="mt-4 flex flex-col"
|
||||
data-testid={`slug-${group.profile.slug}`}
|
||||
key={group.profile.slug}>
|
||||
{/* If the group is readonly and empty don't leave a floating header when the user cant see the create box due
|
||||
to it being readonly for that user */}
|
||||
{group.eventTypes.length === 0 && group.metadata.readOnly ? null : (
|
||||
<EventTypeListHeading
|
||||
profile={group.profile}
|
||||
membershipCount={group.metadata.membershipCount}
|
||||
teamId={group.teamId}
|
||||
bookerUrl={group.bookerUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group.eventTypes.length ? (
|
||||
<EventTypeList
|
||||
types={group.eventTypes}
|
||||
group={group}
|
||||
bookerUrl={group.bookerUrl}
|
||||
groupIndex={index}
|
||||
readOnly={group.metadata.readOnly}
|
||||
/>
|
||||
) : group.teamId ? (
|
||||
<EmptyEventTypeList group={group} />
|
||||
) : (
|
||||
<CreateFirstEventTypeView slug={data.profiles[0].slug ?? ""} />
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
{group.eventTypes.length ? (
|
||||
<EventTypeList
|
||||
types={group.eventTypes}
|
||||
group={group}
|
||||
bookerUrl={group.bookerUrl}
|
||||
groupIndex={index}
|
||||
readOnly={group.metadata.readOnly}
|
||||
lockedByOrg={eventsLockedByOrg}
|
||||
/>
|
||||
) : group.teamId && !group.metadata.readOnly ? (
|
||||
<EmptyEventTypeList group={group} />
|
||||
) : !group.metadata.readOnly ? (
|
||||
<CreateFirstEventTypeView slug={data.profiles[0].slug ?? ""} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
@@ -974,6 +992,21 @@ const EventTypesPage: React.FC & {
|
||||
);
|
||||
}, [orgBranding, user]);
|
||||
|
||||
const profileOptions = data
|
||||
? data?.profiles
|
||||
.filter((profile) => !profile.readOnly)
|
||||
.filter((profile) => !profile.eventTypesLockedByOrg)
|
||||
.map((profile) => {
|
||||
return {
|
||||
teamId: profile.teamId,
|
||||
label: profile.name || profile.slug,
|
||||
image: profile.image,
|
||||
membershipRole: profile.membershipRole,
|
||||
slug: profile.slug,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Shell
|
||||
withoutMain={false}
|
||||
@@ -983,8 +1016,8 @@ const EventTypesPage: React.FC & {
|
||||
heading={t("event_types_page_title")}
|
||||
hideHeadingOnMobile
|
||||
subtitle={t("event_types_page_subtitle")}
|
||||
beforeCTAactions={<Actions />}
|
||||
CTA={<CTA data={data} isOrganization={!!user?.organizationId} />}>
|
||||
beforeCTAactions={<Actions showDivider={profileOptions.length > 0} />}
|
||||
CTA={<CTA profileOptions={profileOptions} isOrganization={!!user?.organizationId} />}>
|
||||
<HeadSeo
|
||||
title="Event Types"
|
||||
description="Create events to share for people to book on your calendar."
|
||||
|
||||
@@ -2313,5 +2313,12 @@
|
||||
"confirm_email": "Confirm your email",
|
||||
"confirm_email_description": "We sent an email to <strong>{{email}}</strong>. Click the link in the email to verify this address.",
|
||||
"send_event_details_to": "Send event details to",
|
||||
"lock_event_types_modal_header":"What should we do with your member's existing event types?",
|
||||
"org_delete_event_types_org_admin":"All of your members individual event types (except managed ones) will be permanently deleted. They will not be able to create new ones",
|
||||
"org_hide_event_types_org_admin":"Your members individual event types will be hidden (except managed ones) from profiles but the links will still be active. They will not be able to create new ones. ",
|
||||
"hide_org_eventtypes":"Hide individual event types",
|
||||
"delete_org_eventtypes":"Delete individual event types",
|
||||
"lock_org_users_eventtypes":"Lock individual event type creation",
|
||||
"lock_org_users_eventtypes_description":"Prevent members from creating their own event types.",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import {
|
||||
showToast,
|
||||
Form,
|
||||
SettingsToggle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Button,
|
||||
RadioGroup as RadioArea,
|
||||
} from "@calcom/ui";
|
||||
|
||||
enum CurrentEventTypeOptions {
|
||||
DELETE = "DELETE",
|
||||
HIDE = "HIDE",
|
||||
}
|
||||
|
||||
interface GeneralViewProps {
|
||||
currentOrg: RouterOutputs["viewer"]["organizations"]["listCurrent"];
|
||||
isAdminOrOwner: boolean;
|
||||
}
|
||||
|
||||
interface FormValues {
|
||||
currentEventTypeOptions: CurrentEventTypeOptions;
|
||||
}
|
||||
|
||||
export const LockEventTypeSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewProps) => {
|
||||
const [lockEventTypeCreationForUsers, setLockEventTypeCreationForUsers] = useState(
|
||||
!!currentOrg.organizationSettings.lockEventTypeCreationForUsers
|
||||
);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const { t } = useLocale();
|
||||
|
||||
const mutation = trpc.viewer.organizations.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
reset(getValues());
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
},
|
||||
});
|
||||
|
||||
const formMethods = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
currentEventTypeOptions: CurrentEventTypeOptions.HIDE,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isAdminOrOwner) return null;
|
||||
|
||||
const currentLockedOption = formMethods.watch("currentEventTypeOptions");
|
||||
|
||||
const { reset, getValues } = formMethods;
|
||||
|
||||
const onSubmit = (values: FormValues) => {
|
||||
mutation.mutate({
|
||||
lockEventTypeCreation: lockEventTypeCreationForUsers,
|
||||
lockEventTypeCreationOptions: values.currentEventTypeOptions,
|
||||
});
|
||||
setShowModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("lock_org_users_eventtypes")}
|
||||
disabled={mutation?.isPending || !isAdminOrOwner}
|
||||
description={t("lock_org_users_eventtypes_description")}
|
||||
checked={lockEventTypeCreationForUsers}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!checked) {
|
||||
mutation.mutate({
|
||||
lockEventTypeCreation: checked,
|
||||
});
|
||||
} else {
|
||||
setShowModal(true);
|
||||
}
|
||||
setLockEventTypeCreationForUsers(checked);
|
||||
}}
|
||||
switchContainerClassName="mt-6"
|
||||
/>
|
||||
{showModal && (
|
||||
<Dialog
|
||||
open={showModal}
|
||||
onOpenChange={(e) => {
|
||||
if (!e) {
|
||||
setLockEventTypeCreationForUsers(
|
||||
!!currentOrg.organizationSettings.lockEventTypeCreationForUsers
|
||||
);
|
||||
setShowModal(false);
|
||||
}
|
||||
}}>
|
||||
<DialogContent enableOverflow>
|
||||
<Form form={formMethods} handleSubmit={onSubmit}>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="w-full pt-1">
|
||||
<DialogHeader title={t("lock_event_types_modal_header")} />
|
||||
<RadioArea.Group
|
||||
id="currentEventTypeOptions"
|
||||
onValueChange={(val: CurrentEventTypeOptions) => {
|
||||
formMethods.setValue("currentEventTypeOptions", val);
|
||||
}}
|
||||
className={classNames("min-h-24 mt-1 flex flex-col gap-4")}>
|
||||
<RadioArea.Item
|
||||
checked={currentLockedOption === CurrentEventTypeOptions.HIDE}
|
||||
value={CurrentEventTypeOptions.HIDE}
|
||||
className={classNames("h-full text-sm")}>
|
||||
<strong className="mb-1 block">{t("hide_org_eventtypes")}</strong>
|
||||
<p>{t("org_hide_event_types_org_admin")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item
|
||||
checked={currentLockedOption === CurrentEventTypeOptions.DELETE}
|
||||
value={CurrentEventTypeOptions.DELETE}
|
||||
className={classNames("[&:has(input:checked)]:border-error h-full text-sm")}>
|
||||
<strong className="mb-1 block">{t("delete_org_eventtypes")}</strong>
|
||||
<p>{t("org_delete_event_types_org_admin")}</p>
|
||||
</RadioArea.Item>
|
||||
</RadioArea.Group>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button disabled={!isAdminOrOwner} type="submit">
|
||||
{t("submit")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
TimezoneSelect,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { LockEventTypeSwitch } from "../components/LockEventTypeSwitch";
|
||||
|
||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
@@ -81,6 +83,8 @@ const OrgGeneralView = () => {
|
||||
isAdminOrOwner={isAdminOrOwner}
|
||||
localeProp={user?.locale ?? "en"}
|
||||
/>
|
||||
|
||||
<LockEventTypeSwitch currentOrg={currentOrg} isAdminOrOwner={isAdminOrOwner} />
|
||||
</LicenseRequired>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -133,6 +133,11 @@ export default async function getEventTypeById({
|
||||
parent: {
|
||||
select: {
|
||||
slug: true,
|
||||
organizationSettings: {
|
||||
select: {
|
||||
lockEventTypeCreationForUsers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
members: {
|
||||
|
||||
@@ -349,6 +349,11 @@ export class ProfileRepository {
|
||||
movedFromUser: true,
|
||||
organization: {
|
||||
include: {
|
||||
organizationSettings: {
|
||||
select: {
|
||||
lockEventTypeCreationForUsers: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
select: membershipSelect,
|
||||
},
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "OrganizationSettings" ADD COLUMN "lockEventTypeCreationForUsers" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -374,13 +374,14 @@ model Team {
|
||||
}
|
||||
|
||||
model OrganizationSettings {
|
||||
id Int @id @default(autoincrement())
|
||||
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
organizationId Int @unique
|
||||
isOrganizationConfigured Boolean @default(false)
|
||||
isOrganizationVerified Boolean @default(false)
|
||||
orgAutoAcceptEmail String
|
||||
dSyncData DSyncData?
|
||||
id Int @id @default(autoincrement())
|
||||
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
organizationId Int @unique
|
||||
isOrganizationConfigured Boolean @default(false)
|
||||
isOrganizationVerified Boolean @default(false)
|
||||
orgAutoAcceptEmail String
|
||||
lockEventTypeCreationForUsers Boolean @default(false)
|
||||
dSyncData DSyncData?
|
||||
}
|
||||
|
||||
enum MembershipRole {
|
||||
|
||||
@@ -852,6 +852,51 @@ async function main() {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
memberData: {
|
||||
email: "member1-acme@example.com",
|
||||
password: {
|
||||
create: {
|
||||
hash: "member1-acme",
|
||||
},
|
||||
},
|
||||
username: "member1-acme",
|
||||
name: "Member 1",
|
||||
},
|
||||
orgMembership: {
|
||||
role: "MEMBER",
|
||||
accepted: true,
|
||||
},
|
||||
orgProfile: {
|
||||
username: "member1",
|
||||
},
|
||||
inTeams: [
|
||||
{
|
||||
slug: "team1",
|
||||
role: "ADMIN",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
memberData: {
|
||||
email: "member2-acme@example.com",
|
||||
password: {
|
||||
create: {
|
||||
hash: "member2-acme",
|
||||
},
|
||||
},
|
||||
username: "member2-acme",
|
||||
name: "Member 2",
|
||||
},
|
||||
orgMembership: {
|
||||
role: "MEMBER",
|
||||
accepted: true,
|
||||
},
|
||||
orgProfile: {
|
||||
username: "member2",
|
||||
},
|
||||
inTeams: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
teams: [
|
||||
|
||||
@@ -28,6 +28,7 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
|
||||
|
||||
const userId = ctx.user.id;
|
||||
const isManagedEventType = schedulingType === SchedulingType.MANAGED;
|
||||
const isOrgAdmin = !!ctx.user?.organization?.isOrgAdmin;
|
||||
// Get Users default conferencing app
|
||||
|
||||
const defaultConferencingData = userMetadataSchema.parse(ctx.user.metadata)?.defaultConferencingApp;
|
||||
@@ -70,8 +71,6 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
|
||||
},
|
||||
});
|
||||
|
||||
const isOrgAdmin = !!ctx.user?.organization?.isOrgAdmin;
|
||||
|
||||
if (!hasMembership?.role || !(["ADMIN", "OWNER"].includes(hasMembership.role) || isOrgAdmin)) {
|
||||
console.warn(`User ${userId} does not have permission to create this new event type`);
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
@@ -85,6 +84,27 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
|
||||
data.schedulingType = schedulingType;
|
||||
}
|
||||
|
||||
// If we are in an organization & they are not admin & they are not creating an event on a teamID
|
||||
// Check if evenTypes are locked.
|
||||
if (ctx.user.organizationId && !ctx.user?.organization?.isOrgAdmin && !teamId) {
|
||||
const orgSettings = await ctx.prisma.organizationSettings.findUnique({
|
||||
where: {
|
||||
organizationId: ctx.user.organizationId,
|
||||
},
|
||||
select: {
|
||||
lockEventTypeCreationForUsers: true,
|
||||
},
|
||||
});
|
||||
|
||||
const orgHasLockedEventTypes = !!orgSettings?.lockEventTypeCreationForUsers;
|
||||
if (orgHasLockedEventTypes) {
|
||||
console.warn(
|
||||
`User ${userId} does not have permission to create this new event type - Locked status: ${orgHasLockedEventTypes}`
|
||||
);
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
|
||||
const profile = ctx.user.profile;
|
||||
try {
|
||||
const eventType = await EventTypeRepository.create({
|
||||
|
||||
@@ -44,8 +44,9 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
|
||||
rateLimitingType: "common",
|
||||
});
|
||||
const lightProfile = ctx.user.profile;
|
||||
|
||||
const profile = await ProfileRepository.findByUpId(lightProfile.upId);
|
||||
const parentOrgHasLockedEventTypes =
|
||||
profile?.organization?.organizationSettings?.lockEventTypeCreationForUsers;
|
||||
const isFilterSet = input?.filters && hasFilter(input.filters);
|
||||
const isUpIdInFilter = input?.filters?.upIds?.includes(lightProfile.upId);
|
||||
const shouldListUserEvents = !isFilterSet || isUpIdInFilter;
|
||||
@@ -150,6 +151,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
|
||||
slug: (typeof profile)["username"] | null;
|
||||
name: (typeof profile)["name"];
|
||||
image: string;
|
||||
eventTypesLockedByOrg?: boolean;
|
||||
};
|
||||
metadata: {
|
||||
membershipCount: number;
|
||||
@@ -188,6 +190,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
|
||||
avatarUrl: profile.avatarUrl,
|
||||
profile: profile,
|
||||
}),
|
||||
eventTypesLockedByOrg: parentOrgHasLockedEventTypes,
|
||||
},
|
||||
eventTypes: orderBy(unmanagedEventTypes, ["position", "id"], ["desc", "asc"]),
|
||||
metadata: {
|
||||
|
||||
@@ -29,6 +29,15 @@ export const listHandler = async ({ ctx }: ListHandlerInput) => {
|
||||
},
|
||||
});
|
||||
|
||||
const organizationSettings = await ctx.prisma.organizationSettings.findUnique({
|
||||
where: {
|
||||
organizationId: ctx.user.organization.id,
|
||||
},
|
||||
select: {
|
||||
lockEventTypeCreationForUsers: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -39,6 +48,9 @@ export const listHandler = async ({ ctx }: ListHandlerInput) => {
|
||||
const metadata = teamMetadataSchema.parse(membership?.team.metadata);
|
||||
|
||||
return {
|
||||
organizationSettings: {
|
||||
lockEventTypeCreationForUsers: organizationSettings?.lockEventTypeCreationForUsers,
|
||||
},
|
||||
user: {
|
||||
role: membership?.role,
|
||||
accepted: membership?.accepted,
|
||||
|
||||
@@ -116,9 +116,63 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
}
|
||||
}
|
||||
|
||||
const updatedOrganisation = await prisma.team.update({
|
||||
where: { id: currentOrgId },
|
||||
data,
|
||||
const updatedOrganisation = await prisma.$transaction(async (tx) => {
|
||||
const updatedOrganisation = await tx.team.update({
|
||||
where: { id: currentOrgId },
|
||||
data,
|
||||
});
|
||||
|
||||
await tx.organizationSettings.update({
|
||||
where: {
|
||||
organizationId: currentOrgId,
|
||||
},
|
||||
data: {
|
||||
lockEventTypeCreationForUsers: !!input.lockEventTypeCreation,
|
||||
},
|
||||
});
|
||||
|
||||
if (input.lockEventTypeCreation) {
|
||||
switch (input.lockEventTypeCreationOptions) {
|
||||
case "HIDE":
|
||||
await tx.eventType.updateMany({
|
||||
where: {
|
||||
teamId: null, // Not assigned to a team
|
||||
parentId: null, // Not a managed event type
|
||||
owner: {
|
||||
profiles: {
|
||||
some: {
|
||||
organizationId: currentOrgId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
hidden: true,
|
||||
},
|
||||
});
|
||||
|
||||
break;
|
||||
case "DELETE":
|
||||
await tx.eventType.deleteMany({
|
||||
where: {
|
||||
teamId: null, // Not assigned to a team
|
||||
parentId: null, // Not a managed event type
|
||||
owner: {
|
||||
profiles: {
|
||||
some: {
|
||||
organizationId: currentOrgId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return updatedOrganisation;
|
||||
});
|
||||
|
||||
// Sync Services: Close.com
|
||||
|
||||
@@ -33,6 +33,8 @@ export const ZUpdateInputSchema = z.object({
|
||||
weekStart: z.string().optional(),
|
||||
timeFormat: z.number().optional(),
|
||||
metadata: teamMetadataSchema.unwrap().optional(),
|
||||
lockEventTypeCreation: z.boolean().optional(),
|
||||
lockEventTypeCreationOptions: z.enum(["DELETE", "HIDE"]).optional(),
|
||||
});
|
||||
|
||||
export type TUpdateInputSchema = z.infer<typeof ZUpdateInputSchema>;
|
||||
|
||||
Reference in New Issue
Block a user