feat: editmode availbility slider (#11197)

* Add loading data and banner

* [WIP] hasEditPerms middleware

* fix: type error in booker (#11011)

* New Crowdin translations by Github Action

* refactor: removed redundant test (#10785)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>

* feat: 2fa backup codes (#10600)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* Delete add-PRs-to-project-reviewing-PRs.yml (#11008)

Co-authored-by: alannnc <alannnc@gmail.com>

* New Crowdin translations by Github Action

* fix: multiple duration when booking (#11032)

* fix: other reported issues (#11015)

* fix: weird margin top in avatar

* fix: pending users are shown on booking page

* fix: avatar and naming issues

* fix: toast alignment and removing unneeded titles

* missing changes from toast improvements

* feat: empty state for teams without event types

* Removing console.log

* feat: cal ai (#10992)

Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
Co-authored-by: tedspare <ted.spare@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>

* New Crowdin translations by Github Action

* fix: meeting ended trigger for webhooks and zapier sometimes not working (#10946)

Co-authored-by: mohammed gehad <mohammed.gehad.1998@gmail.com>
Co-authored-by: Monto <138862352+monto7926@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* feat: team admin: see connected apps of team members (#11036)

* added feature:team admin can see connected apps of members

* fixed the type error

* Update packages/lib/server/queries/teams/index.ts

* Minor fixes

---------

Co-authored-by: alannnc <alannnc@gmail.com>

* fix: lower case slugs in teams (#11026)

* fix: lower case slugs in teams

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: use slugify

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* refactor: improvements on german translation (#10898)

* fix: fix-tablet-menu-not-centered-sidebar (#11020)

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>

* chore: add Popover in storybook (#11021)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: Set mobile availability (#11027)

* chore: add ColorPicker in storybook (CALCOM-10760) (#10866)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* feat: adds next cold start profiler (#11014)

* Handle disabling of ORGANIZATIONS_ENABLED flag (#11041)

* New Crowdin translations by Github Action

* styles:dark mode color fix (#11004)

* chore: sheet darkmode and improve responsive (#11047)

* fix: handle collective multiple host on destinationCalendar (#10967)

* fix: include app data and credentials from DB (#11048)

* include app data and credentials from DB

* Improve performance

* fix: Error when running storybook (#11037)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* fix: broken company profile link on team booking page (#10978)

* chore: add ErrorBoundary in storybook (CALCOM-10760) (#10872)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* feat: sorting for workflow and routing forms (#10780)

Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* resolve zod versions across child packages (#11052)

* fix: List storybook file is empty (fix-list) (#10965)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: admin org list without members (#11051)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* chore: add ToggleGroup in storybook (#10802)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

* fix: add metadata to stripe payment intent (#11053)

* fix: Logo storybook file with invalid icons (fix-logo) (#11018)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: Add controls for Select Field storybook file (#10936)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: Fix tooltip control on ButtonPlayground storybook file (fix-ButtonTooltip) (#10937)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: email embed – remove collapsible and permanently show times (#10996)

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Mehul <mehulzr@gmail.com>

* chore: add Timezone Select in storybook (CALCOM-10760) (#10966)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* chore: add Switch in storybook (CALCOM-10760) (#10804)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* Sync packages

* fix: zod utils due to upgrade

* easy fix (#11054)

* fix sub quantity update stripe (#11057)

* v3.2.7

* Revert "feat: adds next cold start profiler (#11014)" (#11072)

This reverts commit 05631d093f.

* fix: Fixes username invite issue (#10998)

* Fixes username invite issue

* Ensure we only suggest email invites in org members

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>

* Fix sheet layout

* WIP permissions when userschedule doesnt match

* WIP get handler weird behaviour

* Update toast

* add disabled - handle membership overlap

* Handle permissions + perf improvments

* use input uid

* Remove Console.log

* Clean up

* Revert changes accidental

* Fix merge artifacts

* Remove dead code

* Remove code after return

* Update read permission check

* Revert avatar changes as fixed elsewhere

* Handle if user has not completed onboarding

* Disable button

* Update packages/lib/hasEditPermissionForUser.ts

* Correct Error throwing

* Update packages/features/timezone-buddy/components/AvailabilityEditSheet.tsx

* Fix type erro

* Add i18n

* Improve Spacing

* Update yarn.lock

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: DexterStorey <36115192+DexterStorey@users.noreply.github.com>
Co-authored-by: tedspare <ted.spare@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Monto <138862352+montocoder@users.noreply.github.com>
Co-authored-by: mohammed gehad <mohammed.gehad.1998@gmail.com>
Co-authored-by: Monto <138862352+monto7926@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Abhijeet Singh <asingh9829@gmail.com>
Co-authored-by: Kamil B. Demirci <kamil.demirci@indyaner.ch>
Co-authored-by: Denzil Samuel <71846487+samueldenzil@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Sahil Padvi <71093044+Sahil25061999@users.noreply.github.com>
Co-authored-by: Patel Divyesh <pateldivyesh1323@gmail.com>
Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
Co-authored-by: Mehul <mehulzr@gmail.com>
pull/10713/head^2
sean-brydon 2023-09-14 09:45:13 +01:00 committed by GitHub
parent 605e744a58
commit a2d1dbebba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 502 additions and 42 deletions

View File

@ -2050,5 +2050,8 @@
"no_members_found": "No members found",
"event_setup_length_error":"Event Setup: The duration must be at least 1 minute.",
"availability_schedules":"Availability Schedules",
"view_only_edit_availability_not_onboarded":"This user has not completed onboarding. You will not be able to set their availability until they have completed onboarding.",
"view_only_edit_availability":"You are viewing this user's availability. You can only edit your own availability.",
"edit_users_availability":"Edit user's availability: {{username}}",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -43,11 +43,13 @@ const ScheduleDay = <TFieldValues extends FieldValues>({
weekday,
control,
CopyButton,
disabled,
}: {
name: ArrayPath<TFieldValues>;
weekday: string;
control: Control<TFieldValues>;
CopyButton: JSX.Element;
disabled?: boolean;
}) => {
const { watch, setValue } = useFormContext();
const watchDayRange = watch(name);
@ -60,7 +62,7 @@ const ScheduleDay = <TFieldValues extends FieldValues>({
<label className="text-default flex flex-row items-center space-x-2 rtl:space-x-reverse">
<div>
<Switch
disabled={!watchDayRange}
disabled={!watchDayRange || disabled}
defaultChecked={watchDayRange && watchDayRange.length > 0}
checked={watchDayRange && !!watchDayRange.length}
onCheckedChange={(isChecked) => {
@ -75,8 +77,8 @@ const ScheduleDay = <TFieldValues extends FieldValues>({
<>
{watchDayRange ? (
<div className="flex sm:ml-2">
<DayRanges control={control} name={name} />
{!!watchDayRange.length && <div className="block">{CopyButton}</div>}
<DayRanges control={control} name={name} disabled={disabled} />
{!!watchDayRange.length && !disabled && <div className="block">{CopyButton}</div>}
</div>
) : (
<SkeletonText className="ml-1 mt-2.5 h-6 w-48" />
@ -133,11 +135,13 @@ const Schedule = <
>({
name,
control,
disabled,
weekStart = 0,
}: {
name: TPath;
control: Control<TFieldValues>;
weekStart?: number;
disabled?: boolean;
}) => {
const { i18n } = useLocale();
@ -149,6 +153,7 @@ const Schedule = <
const dayRangeName = `${name}.${weekdayIndex}` as ArrayPath<TFieldValues>;
return (
<ScheduleDay
disabled={disabled}
name={dayRangeName}
key={weekday}
weekday={weekday}
@ -163,10 +168,12 @@ const Schedule = <
export const DayRanges = <TFieldValues extends FieldValues>({
name,
disabled,
control,
}: {
name: ArrayPath<TFieldValues>;
control?: Control<TFieldValues>;
disabled?: boolean;
}) => {
const { t } = useLocale();
const { getValues } = useFormContext();
@ -184,6 +191,7 @@ export const DayRanges = <TFieldValues extends FieldValues>({
<Controller name={`${name}.${index}`} render={({ field }) => <TimeRangeField {...field} />} />
{index === 0 && (
<Button
disabled={disabled}
tooltip={t("add_time_availability")}
className="text-default mx-2 "
type="button"
@ -220,15 +228,18 @@ export const DayRanges = <TFieldValues extends FieldValues>({
const RemoveTimeButton = ({
index,
remove,
disabled,
className,
}: {
index: number | number[];
remove: UseFieldArrayRemove;
className?: string;
disabled?: boolean;
}) => {
const { t } = useLocale();
return (
<Button
disabled={disabled}
type="button"
variant="icon"
color="destructive"
@ -240,12 +251,18 @@ const RemoveTimeButton = ({
);
};
const TimeRangeField = ({ className, value, onChange }: { className?: string } & ControllerRenderProps) => {
const TimeRangeField = ({
className,
value,
onChange,
disabled,
}: { className?: string; disabled?: boolean } & ControllerRenderProps) => {
// this is a controlled component anyway given it uses LazySelect, so keep it RHF agnostic.
return (
<div className={classNames("flex flex-row gap-1", className)}>
<LazySelect
className="inline-block w-[100px]"
isDisabled={disabled}
value={value.start}
max={value.end}
onChange={(option) => {
@ -255,6 +272,7 @@ const TimeRangeField = ({ className, value, onChange }: { className?: string } &
<span className="text-default mx-2 w-2 self-center"> - </span>
<LazySelect
className="inline-block w-[100px] rounded-md"
isDisabled={disabled}
value={value.end}
min={value.start}
onChange={(option) => {

View File

@ -0,0 +1,199 @@
import { useForm, useFieldArray } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
import Schedule from "@calcom/features/schedules/components/Schedule";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { trpc } from "@calcom/trpc/react";
import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
import {
Button,
Form,
Label,
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
TimezoneSelect,
showToast,
Alert,
} from "@calcom/ui";
import { Plus } from "@calcom/ui/components/icon";
import type { SliderUser } from "./AvailabilitySliderTable";
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
selectedUser?: SliderUser | null;
}
type AvailabilityFormValues = {
name: string;
schedule: ScheduleType;
dateOverrides: { ranges: TimeRange[] }[];
timeZone: string;
isDefault: boolean;
};
const DateOverride = ({ workingHours, disabled }: { workingHours: WorkingHours[]; disabled?: boolean }) => {
const { remove, append, replace, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
name: "dateOverrides",
});
const excludedDates = fields.map((field) => dayjs(field.ranges[0].start).utc().format("YYYY-MM-DD"));
const { t } = useLocale();
return (
<div className="">
<Label>{t("date_overrides")}</Label>
<div className="space-y-2">
<DateOverrideList
excludedDates={excludedDates}
remove={remove}
replace={replace}
items={fields}
workingHours={workingHours}
/>
<DateOverrideInputDialog
workingHours={workingHours}
excludedDates={excludedDates}
onChange={(ranges) => ranges.forEach((range) => append({ ranges: [range] }))}
Trigger={
<Button color="secondary" StartIcon={Plus} data-testid="add-override" disabled={disabled}>
{t("add_an_override")}
</Button>
}
/>
</div>
</div>
);
};
export function AvailabilityEditSheet(props: Props) {
// This sheet will not be rendered without a selected user
const userId = props.selectedUser?.id as number;
const { t } = useLocale();
const utils = trpc.useContext();
const { data: hasEditPermission, isLoading: loadingPermissions } =
trpc.viewer.teams.hasEditPermissionForUser.useQuery({
memberId: userId,
});
const { data, isLoading } = trpc.viewer.availability.schedule.getScheduleByUserId.useQuery({
userId: userId,
});
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
onSuccess: async () => {
utils.viewer.availability.listTeam.invalidate();
showToast(t("success"), "success");
props.onOpenChange(false);
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
}
},
});
const form = useForm<AvailabilityFormValues>({
values: data && {
...data,
timeZone: data?.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone,
schedule: data?.availability || [],
},
});
const watchTimezone = form.watch("timeZone");
return (
<Sheet open={props.open} onOpenChange={props.onOpenChange}>
<Form
form={form}
id="availability-form"
handleSubmit={async ({ dateOverrides, ...values }) => {
// Just blocking on a UI side -> Backend will also do the validation
if (!hasEditPermission) return;
data &&
updateMutation.mutate({
scheduleId: data?.id,
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
...values,
});
}}>
<SheetContent
bottomActions={
<>
<Button color="secondary" className="w-full justify-center">
{t("cancel")}
</Button>
<Button
disabled={!hasEditPermission || !data?.hasDefaultSchedule}
className="w-full justify-center"
type="submit"
loading={updateMutation.isLoading}
form="availability-form">
{t("save")}
</Button>
</>
}>
<SheetHeader>
<SheetTitle>
{t("edit_users_availability", {
username: props.selectedUser?.username ?? "Nameless user",
})}
</SheetTitle>
</SheetHeader>
{!data?.hasDefaultSchedule && !isLoading && hasEditPermission && (
<div className="my-2">
<Alert severity="warning" title={t("view_only_edit_availability_not_onboarded")} />
</div>
)}
{!hasEditPermission && !loadingPermissions && (
<div className="my-2">
<Alert severity="warning" title={t("view_only_edit_availability")} />
</div>
)}
<div className="mt-4 flex flex-col space-y-4">
<div>
<Label className="text-emphasis">
<>{t("timezone")}</>
</Label>
<TimezoneSelect
id="timezone"
isDisabled={!hasEditPermission || !data?.hasDefaultSchedule}
value={watchTimezone ?? "Europe/London"}
onChange={(event) => {
if (event) form.setValue("timeZone", event.value, { shouldDirty: true });
}}
/>
</div>
<div className="mt-4">
<Label className="text-emphasis">{t("members_default_schedule")}</Label>
{/* Remove padding from schedule without touching the component */}
<div className="[&>*:first-child]:!p-0">
<Schedule
control={form.control}
name="schedule"
weekStart={0}
disabled={!hasEditPermission || !data?.hasDefaultSchedule}
/>
</div>
</div>
<div className="mt-4">
{data?.workingHours && (
<DateOverride
workingHours={data.workingHours}
disabled={!hasEditPermission || !data.hasDefaultSchedule}
/>
)}
</div>
</div>
</SheetContent>
</Form>
</Sheet>
);
}

View File

@ -12,6 +12,7 @@ import { Avatar, Button, ButtonGroup, DataTable } from "@calcom/ui";
import { UpgradeTip } from "../../tips/UpgradeTip";
import { TBContext, createTimezoneBuddyStore } from "../store";
import { AvailabilityEditSheet } from "./AvailabilityEditSheet";
import { TimeDial } from "./TimeDial";
export interface SliderUser {
@ -20,6 +21,7 @@ export interface SliderUser {
email: string;
timeZone: string;
role: MembershipRole;
defaultScheduleId: number | null;
dateRanges: DateRange[];
}
@ -50,8 +52,11 @@ function UpgradeTeamTip() {
}
export function AvailabilitySliderTable() {
const { t } = useLocale();
const tableContainerRef = useRef<HTMLDivElement>(null);
const [browsingDate, setBrowsingDate] = useState(dayjs());
const [editSheetOpen, setEditSheetOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<SliderUser | null>(null);
const { data, isLoading, fetchNextPage, isFetching } = trpc.viewer.availability.listTeam.useInfiniteQuery(
{
@ -175,16 +180,32 @@ export function AvailabilitySliderTable() {
value={createTimezoneBuddyStore({
browsingDate: browsingDate.toDate(),
})}>
<div className="relative">
<DataTable
tableContainerRef={tableContainerRef}
columns={memorisedColumns}
data={flatData}
isLoading={isLoading}
// tableOverlay={<HoverOverview />}
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}
/>
</div>
<>
<div className="relative">
<DataTable
tableContainerRef={tableContainerRef}
columns={memorisedColumns}
onRowMouseclick={(row) => {
setEditSheetOpen(true);
setSelectedUser(row.original);
}}
data={flatData}
isLoading={isLoading}
// tableOverlay={<HoverOverview />}
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}
/>
</div>
{selectedUser && editSheetOpen ? (
<AvailabilityEditSheet
open={editSheetOpen}
onOpenChange={(e) => {
setEditSheetOpen(e);
setSelectedUser(null); // We need to clear the user here or else the sheet will not re-render when opening a new user
}}
selectedUser={selectedUser}
/>
) : null}
</>
</TBContext.Provider>
);
}

View File

@ -0,0 +1,64 @@
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
const ROLES_WITH_EDIT_PERMISSION = [MembershipRole.ADMIN, MembershipRole.OWNER] as MembershipRole[];
type InputOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: {
memberId: number;
};
};
export async function hasEditPermissionForUserID({ ctx, input }: InputOptions) {
const { user } = ctx;
const authedUsersTeams = await prisma.membership.findMany({
where: {
userId: user.id,
accepted: true,
role: {
in: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
},
});
const targetUsersTeams = await prisma.membership.findMany({
where: {
userId: input.memberId,
accepted: true,
},
});
const teamIdOverlaps = authedUsersTeams.some((authedTeam) => {
return targetUsersTeams.some((targetTeam) => targetTeam.teamId === authedTeam.teamId);
});
return teamIdOverlaps;
}
export async function hasReadPermissionsForUserId({ ctx, input }: InputOptions) {
const { user } = ctx;
const authedUsersTeams = await prisma.membership.findMany({
where: {
userId: user.id,
accepted: true,
},
});
const targetUsersTeams = await prisma.membership.findMany({
where: {
userId: input.memberId,
accepted: true,
},
});
const teamIdOverlaps = authedUsersTeams.some((authedTeam) => {
return targetUsersTeams.some((targetTeam) => targetTeam.teamId === authedTeam.teamId);
});
return teamIdOverlaps;
}

View File

@ -4,6 +4,7 @@ import { ZCreateInputSchema } from "./create.schema";
import { ZDeleteInputSchema } from "./delete.schema";
import { ZScheduleDuplicateSchema } from "./duplicate.schema";
import { ZGetInputSchema } from "./get.schema";
import { ZGetByUserIdInputSchema } from "./getScheduleByUserId.schema";
import { ZUpdateInputSchema } from "./update.schema";
type ScheduleRouterHandlerCache = {
@ -12,6 +13,7 @@ type ScheduleRouterHandlerCache = {
delete?: typeof import("./delete.handler").deleteHandler;
update?: typeof import("./update.handler").updateHandler;
duplicate?: typeof import("./duplicate.handler").duplicateHandler;
getScheduleByUserId?: typeof import("./getScheduleByUserId.handler").getScheduleByUserIdHandler;
};
const UNSTABLE_HANDLER_CACHE: ScheduleRouterHandlerCache = {};
@ -98,4 +100,22 @@ export const scheduleRouter = router({
input,
});
}),
getScheduleByUserId: authedProcedure.input(ZGetByUserIdInputSchema).query(async ({ input, ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.getScheduleByUserId) {
UNSTABLE_HANDLER_CACHE.getScheduleByUserId = await import("./getScheduleByUserId.handler").then(
(mod) => mod.getScheduleByUserIdHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.getScheduleByUserId) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.getScheduleByUserId({
ctx,
input,
});
}),
});

View File

@ -1,6 +1,7 @@
import dayjs from "@calcom/dayjs";
import { getWorkingHours } from "@calcom/lib/availability";
import { yyyymmdd } from "@calcom/lib/date-fns";
import { hasReadPermissionsForUserId } from "@calcom/lib/hasEditPermissionForUser";
import { prisma } from "@calcom/prisma";
import type { TimeRange } from "@calcom/types/schedule";
@ -30,25 +31,22 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
name: true,
availability: true,
timeZone: true,
eventType: {
select: {
id: true,
eventName: true,
team: {
select: { members: { select: { userId: true } } },
},
},
},
},
});
const isCurrentUserPartOfTeam = schedule?.eventType.some((eventType) =>
eventType.team?.members.some((teamMember) => teamMember.userId === user.id)
);
if (!schedule) {
throw new TRPCError({
code: "UNAUTHORIZED",
});
}
const isCurrentUserPartOfTeam = hasReadPermissionsForUserId({
ctx,
input: { memberId: schedule?.userId },
});
const isCurrentUserOwner = schedule?.userId === user.id;
if (!schedule || (!isCurrentUserOwner && !isCurrentUserPartOfTeam)) {
if (!isCurrentUserPartOfTeam && !isCurrentUserOwner) {
throw new TRPCError({
code: "UNAUTHORIZED",
});

View File

@ -0,0 +1,52 @@
import type { TrpcSessionUser } from "../../../../trpc";
import { getHandler } from "./get.handler";
import type { TGetByUserIdInputSchema } from "./getScheduleByUserId.schema";
type GetOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TGetByUserIdInputSchema;
};
const EMPTY_SCHEDULE = [[], [], [], [], [], [], []];
export const getScheduleByUserIdHandler = async ({ ctx, input }: GetOptions) => {
const foundUserDefaultId = await prisma.user.findUnique({
where: {
id: input.userId,
},
select: {
defaultScheduleId: true,
},
});
try {
// This looks kinda weird that we throw straight in the catch - its so that we can return a default schedule if the user has not completed onboarding @shiraz will loveme for this
if (!foundUserDefaultId?.defaultScheduleId) {
throw new Error("NOT_FOUND");
}
const schedule = await getHandler({
ctx,
input: {
scheduleId: foundUserDefaultId?.defaultScheduleId,
},
});
return {
...schedule,
hasDefaultSchedule: true,
};
} catch (e) {
return {
id: -1,
name: "Working Hourse",
availability: EMPTY_SCHEDULE,
dateOverrides: [],
timeZone: ctx.user.timeZone || "Europe/London",
workingHours: [],
isDefault: true,
hasDefaultSchedule: false, // This is the path that we take if the user has not completed onboarding
};
}
};

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const ZGetByUserIdInputSchema = z.object({
userId: z.optional(z.number()),
});
export type TGetByUserIdInputSchema = z.infer<typeof ZGetByUserIdInputSchema>;

View File

@ -1,4 +1,5 @@
import { getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { hasEditPermissionForUserID } from "@calcom/lib/hasEditPermissionForUser";
import { prisma } from "@calcom/prisma";
import { TRPCError } from "@trpc/server";
@ -38,14 +39,24 @@ export const updateHandler = async ({ input, ctx }: UpdateOptions) => {
},
});
if (userSchedule?.userId !== user.id) throw new TRPCError({ code: "UNAUTHORIZED" });
if (!userSchedule || userSchedule.userId !== user.id) {
if (!userSchedule) {
throw new TRPCError({
code: "UNAUTHORIZED",
});
}
if (userSchedule?.userId !== user.id) {
const hasEditPermission = await hasEditPermissionForUserID({
ctx,
input: { memberId: userSchedule.userId },
});
if (!hasEditPermission) {
throw new TRPCError({
code: "UNAUTHORIZED",
});
}
}
let updatedUser;
if (input.isDefault) {
const setupDefault = await setupDefaultSchedule(user.id, input.scheduleId, prisma);

View File

@ -65,6 +65,7 @@ async function buildMember(member: Member, dateFrom: Dayjs, dateTo: Dayjs) {
email: member.user.email,
timeZone: member.user.timeZone,
role: member.role,
defaultScheduleId: -1,
dateRanges: [] as DateRange[],
};
}
@ -88,6 +89,7 @@ async function buildMember(member: Member, dateFrom: Dayjs, dateTo: Dayjs) {
email: member.user.email,
timeZone,
role: member.role,
defaultScheduleId: member.user.defaultScheduleId ?? -1,
dateRanges,
};
}

View File

@ -9,6 +9,7 @@ import { ZDeleteInviteInputSchema } from "./deleteInvite.schema";
import { ZGetInputSchema } from "./get.schema";
import { ZGetMemberAvailabilityInputSchema } from "./getMemberAvailability.schema";
import { ZGetMembershipbyUserInputSchema } from "./getMembershipbyUser.schema";
import { ZHasEditPermissionForUserSchema } from "./hasEditPermissionForUser.schema";
import { ZInviteMemberInputSchema } from "./inviteMember/inviteMember.schema";
import { ZInviteMemberByTokenSchemaInputSchema } from "./inviteMemberByToken.schema";
import { ZListMembersInputSchema } from "./listMembers.schema";
@ -41,6 +42,7 @@ type TeamsRouterHandlerCache = {
setInviteExpiration?: typeof import("./setInviteExpiration.handler").setInviteExpirationHandler;
deleteInvite?: typeof import("./deleteInvite.handler").deleteInviteHandler;
inviteMemberByToken?: typeof import("./inviteMemberByToken.handler").inviteMemberByTokenHandler;
hasEditPermissionForUser?: typeof import("./hasEditPermissionForUser.handler").hasEditPermissionForUser;
};
const UNSTABLE_HANDLER_CACHE: TeamsRouterHandlerCache = {};
@ -433,4 +435,23 @@ export const viewerTeamsRouter = router({
input,
});
}),
hasEditPermissionForUser: authedProcedure
.input(ZHasEditPermissionForUserSchema)
.query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.hasEditPermissionForUser) {
UNSTABLE_HANDLER_CACHE.hasEditPermissionForUser = await import(
"./hasEditPermissionForUser.handler"
).then((mod) => mod.hasEditPermissionForUser);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.hasEditPermissionForUser) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.hasEditPermissionForUser({
ctx,
input,
});
}),
});

View File

@ -0,0 +1,19 @@
import { hasEditPermissionForUserID as $hasEditPermissionForUser } from "@calcom/lib/hasEditPermissionForUser";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { THasEditPermissionForUserSchema } from "./hasEditPermissionForUser.schema";
type HasEditPermissionForUserOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: THasEditPermissionForUserSchema;
};
export const hasEditPermissionForUser = async ({ ctx, input }: HasEditPermissionForUserOptions) => {
// Calculate if the logged in User has edit permission for the given User.
return $hasEditPermissionForUser({
ctx,
input,
});
};

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const ZHasEditPermissionForUserSchema = z.object({
memberId: z.number(),
});
export type THasEditPermissionForUserSchema = z.infer<typeof ZHasEditPermissionForUserSchema>;

View File

@ -17,6 +17,8 @@ import {
import { useState } from "react";
import { useVirtual } from "react-virtual";
import classNames from "@calcom/lib/classNames";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../table/TableNew";
import type { ActionItem } from "./DataTableSelectionBar";
import { DataTableSelectionBar } from "./DataTableSelectionBar";
@ -32,6 +34,7 @@ export interface DataTableProps<TData, TValue> {
selectionOptions?: ActionItem<TData>[];
tableCTA?: React.ReactNode;
isLoading?: boolean;
onRowMouseclick?: (row: Row<TData>) => void;
onScroll?: (e: React.UIEvent<HTMLDivElement, UIEvent>) => void;
CTA?: React.ReactNode;
tableOverlay?: React.ReactNode;
@ -47,6 +50,8 @@ export function DataTable<TData, TValue>({
tableContainerRef,
isLoading,
tableOverlay,
/** This should only really be used if you dont have actions in a row. */
onRowMouseclick,
onScroll,
}: DataTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = useState({});
@ -137,7 +142,11 @@ export function DataTable<TData, TValue>({
const row = rows[virtualRow.index] as Row<TData>;
return (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
onClick={() => onRowMouseclick && onRowMouseclick(row)}
className={classNames(onRowMouseclick && "hover:cursor-pointer")}>
{row.getVisibleCells().map((cell) => {
return (
<TableCell key={cell.id}>

View File

@ -49,7 +49,7 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 scale-100 gap-4 bg-default m-4 rounded-xl p-6 opacity-100 shadow-lg border border-default overflow-y-scroll ",
"fixed z-50 scale-100 gap-4 bg-default m-4 rounded-xl p-6 opacity-100 shadow-lg border border-default flex flex-col ",
{
variants: {
position: {
@ -138,17 +138,24 @@ const sheetVariants = cva(
export interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
VariantProps<typeof sheetVariants> {
bottomActions?: React.ReactNode;
}
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, DialogContentProps>(
({ position, size, className, children, ...props }, ref) => (
({ position, size, className, children, bottomActions, ...props }, ref) => (
<SheetPortal position={position}>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={classNames(sheetVariants({ position, size }), className)}
{...props}>
{children}
{...props}
className={classNames(sheetVariants({ position, size }), className)}>
<div className="h-full overflow-y-scroll">{children}</div>
{bottomActions && (
<div className="mt-auto flex justify-end">
<div className="flex gap-2">{bottomActions}</div>
</div>
)}
<SheetPrimitive.Close className="focus:ring-emphasis data-[state=open]:bg-deafult absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>

View File

@ -36,7 +36,10 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
({ className, ...props }, ref) => (
<tr
ref={ref}
className={classNames("hover:muted data-[state=selected]:bg-muted border-subtle border-b", className)}
className={classNames(
"hover:bg-muted data-[state=selected]:bg-subtle border-subtle border-b",
className
)}
{...props}
/>
)

View File

@ -4398,7 +4398,6 @@ __metadata:
next: ^13.4.6
next-auth: ^4.22.1
next-axiom: ^0.17.0
next-collect: ^0.2.1
next-i18next: ^13.2.2
next-seo: ^6.0.0
playwright: ^1.31.2
@ -27767,14 +27766,14 @@ __metadata:
linkType: hard
"mlly@npm:^1.4.0":
version: 1.4.1
resolution: "mlly@npm:1.4.1"
version: 1.4.2
resolution: "mlly@npm:1.4.2"
dependencies:
acorn: ^8.10.0
pathe: ^1.1.1
pkg-types: ^1.0.3
ufo: ^1.3.0
checksum: b2b59ab3d70196127be4e54609d2a442bd252345727138940fb245672a238b2fbdd431e8c75ec5c741ff90410ce488c5fd6446d5d3e6476d21dbf4c3fa35d4a0
checksum: ad0813eca133e59ac03b356b87deea57da96083dce7dda58a8eeb2dce92b7cc2315bedd9268f3ff8e98effe1867ddb1307486d4c5cd8be162daa8e0fa0a98ed4
languageName: node
linkType: hard