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
parent
605e744a58
commit
a2d1dbebba
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>;
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue