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>
This commit is contained in:
sean-brydon
2023-09-14 09:45:13 +01:00
committed by GitHub
co-authored by gitstart-calcom Shivam Kalra Peer Richelsen alannnc nicktrn tedspare Alex van Andel mohammed gehad Monto Carina Wollendorfer Udit Takkar Udit Takkar Hariom Balhara Mehul Crowdin Bot GitStart-Cal.com Leo Giovanetti DexterStorey Monto Abhijeet Singh Kamil B. Demirci Denzil Samuel Anik Dhabal Babu Omar López Sahil Padvi Patel Divyesh neo773
parent 605e744a58
commit a2d1dbebba
18 changed files with 502 additions and 42 deletions
@@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -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) => {
@@ -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>
);
}
@@ -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>
);
}
+64
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;
}
@@ -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,
});
}),
});
@@ -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",
});
@@ -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
};
}
};
@@ -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>;
@@ -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);
@@ -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,
};
}
@@ -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,
});
}),
});
@@ -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,
});
};
@@ -0,0 +1,7 @@
import { z } from "zod";
export const ZHasEditPermissionForUserSchema = z.object({
memberId: z.number(),
});
export type THasEditPermissionForUserSchema = z.infer<typeof ZHasEditPermissionForUserSchema>;
+10 -1
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}>
+13 -6
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>
+4 -1
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}
/>
)
+3 -4
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