Files
cal.diy/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx
T
6d5d2a1c4d feat: new App install flow (#11975)
* feat(appStore): add isOAuth config

* refactor(appStore): EventTypeAppSettngsInterface

* refactor(qr_code): split EventTypeAppCardInterface and EventTypeAppSettingsInterface

* feat(appStore): redirect to onboarding for stripe and basecam

* fix(ui/ScrollableArea): overflow indicator not working

* feat(apps): new install app flow

* fix(configureStep): get disabled props for settings

* fix: getAppInstallsBySlug now use teamIds

* chore: Alby to AppSettingsInterface

* chore: Giphy to appsettings interface

* chore: GTM migration

* chore: GT4 - migration

* chore: fathom migration

* chore: paypal

* feat: basecamp migration

* feat: metapixel migration

* feat:plausable

* feat:stripe

* fix merge issues

* fix merge issues in new app install flow steps

* wip: callback to onboarding

* fix: imports

* fix: more imports

* feat: use redirect to onboarding on install flow

* feat: New app install flow 2 (#14077)

* fix: redirect to event-type after oauth flow

* When clicking on install new app button, only redirect to the new app install flow if it is an oAuth app or an app that extends the EventType feature in it's config

* code refactor, skip event-type step

* removed extra consoles

* so that no one can change the eventTypeId in url

* theme support enabled

* only pass eventTypeId instead of whole evetType obj to OmniInstallAppButton

* fix: clicking on event-type gives error and redirects to /apps

* saved appOnboardingRedirectUrl to OAuth State

* fix: a user having no team breaks the onboarding flow

* add optional chaining to appMetadata.extendsFeature and appMetadata.isOAuth;

* encode appOnboardingRedirectUrl

* fix: dark theme ui issues

* skip configure step if no-event types

if user/team does not have event-type then skip configure step and goto apps/installed/[category]

* exclude calendars from apps- onboarding flow

* Update getAppOnboardingRedirectUrl.ts

* fix: On configure step, inputs aren't showing errors

* change route from `apps/onboarding` to `apps/installation`

* fix: flow not correctly working for teams

* should only allow team ADMIN or OWER to install an app to team

* Prevent two payment apps to be enabled

* removed loading state

* Fix type error

* undo new-app-flow from OmniInstallAppButton

* remove OAuth Step, handle step numbering

* fix: redirect url

* getAppOnboardingRedirectUrl refactor

* for debugging

* Revert "for debugging"

This reverts commit b9362c1a8f47f2280e684c213d7ec08d04a8ac4c.

* fix: added toaster

* feat: added support to select multiple events (almost)

* hide button

* Move inner form outside outer form using portal

Due to limitations with nested forms in React Hook Form, moved the inner form outside the outer form using a portal to ensure proper form handling.

* submit buttons refs madness

* removed configure step

we had to use query params to manage selected event-types, between routes which was causing 2 issues:
* each time we selected or unselected an event the whole page was reloaded including getserversideprops being called again and again
* the form was reloaded causing more issues

* removed extra unwanted code

* small fix

* fix: type error

* fix: types

* implemented suggested changes

* removed step footer
* fixed error.message
* removed commented styles

* removed step footer

* fix: Display team event types instead of user's on event types step when team selected

* fix: typo

* removed shouldLockDisableProps

* refactor setUpdatedEventTypesStatus, fix type

* removed extra code

* can't delete while saving

* added translations

* fix: loading in select account step, fix handle onSelect params type

* Fix typo

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>

* fix: type errors

* fix: failing tests

---------

Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-04-10 16:24:31 +00:00

227 lines
7.9 KiB
TypeScript

import { keepPreviousData } from "@tanstack/react-query";
import type { ColumnDef } from "@tanstack/react-table";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import dayjs from "@calcom/dayjs";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import type { DateRange } from "@calcom/lib/date-ranges";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc";
import type { UserProfile } from "@calcom/types/UserProfile";
import { Button, ButtonGroup, DataTable, UserAvatar } from "@calcom/ui";
import { UpgradeTip } from "../../tips/UpgradeTip";
import { createTimezoneBuddyStore, TBContext } from "../store";
import { AvailabilityEditSheet } from "./AvailabilityEditSheet";
import { TimeDial } from "./TimeDial";
export interface SliderUser {
id: number;
username: string | null;
name: string | null;
organizationId: number;
avatarUrl: string | null;
email: string;
timeZone: string;
role: MembershipRole;
defaultScheduleId: number | null;
dateRanges: DateRange[];
profile: UserProfile;
}
function UpgradeTeamTip() {
const { t } = useLocale();
return (
<UpgradeTip
plan="team"
title={t("calcom_is_better_with_team", { appName: APP_NAME }) as string}
description="add_your_team_members"
background="/tips/teams"
features={[]}
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
{t("create_team")}
</Button>
<Button color="minimal" href="https://go.cal.com/teams-video" target="_blank">
{t("learn_more")}
</Button>
</ButtonGroup>
</div>
}>
<></>
</UpgradeTip>
);
}
export function AvailabilitySliderTable(props: { userTimeFormat: number | null }) {
const tableContainerRef = useRef<HTMLDivElement>(null);
const [browsingDate, setBrowsingDate] = useState(dayjs());
const [editSheetOpen, setEditSheetOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<SliderUser | null>(null);
const { data, isPending, fetchNextPage, isFetching } = trpc.viewer.availability.listTeam.useInfiniteQuery(
{
limit: 10,
loggedInUsersTz: dayjs.tz.guess() || "Europe/London",
startDate: browsingDate.startOf("day").toISOString(),
endDate: browsingDate.endOf("day").toISOString(),
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
placeholderData: keepPreviousData,
}
);
const memorisedColumns = useMemo(() => {
const cols: ColumnDef<SliderUser>[] = [
{
id: "member",
accessorFn: (data) => data.email,
header: "Member",
cell: ({ row }) => {
const { username, email, timeZone, name, avatarUrl, profile } = row.original;
return (
<div className="max-w-64 flex flex-shrink-0 items-center gap-2 overflow-hidden">
<UserAvatar
size="sm"
user={{
username,
name,
avatarUrl,
profile,
}}
/>
<div className="">
<div className="text-emphasis max-w-64 truncate text-sm font-medium" title={email}>
{username || "No username"}
</div>
<div className="text-subtle text-xs leading-none">{timeZone}</div>
</div>
</div>
);
},
},
{
id: "timezone",
accessorFn: (data) => data.timeZone,
header: "Timezone",
cell: ({ row }) => {
const { timeZone } = row.original;
const timeRaw = dayjs().tz(timeZone);
const time = timeRaw.format("HH:mm");
const utcOffsetInMinutes = timeRaw.utcOffset();
const hours = Math.abs(Math.floor(utcOffsetInMinutes / 60));
const minutes = Math.abs(utcOffsetInMinutes % 60);
const offsetFormatted = `${utcOffsetInMinutes < 0 ? "-" : "+"}${hours
.toString()
.padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
return (
<div className="flex flex-col text-center">
<span className="text-default text-sm font-medium">{time}</span>
<span className="text-subtle text-xs leading-none">GMT {offsetFormatted}</span>
</div>
);
},
},
{
id: "slider",
header: () => {
return (
<div className="flex items-center space-x-2">
<ButtonGroup containerProps={{ className: "space-x-0" }}>
<Button
color="minimal"
variant="icon"
StartIcon="chevron-left"
onClick={() => setBrowsingDate(browsingDate.subtract(1, "day"))}
/>
<Button
onClick={() => setBrowsingDate(browsingDate.add(1, "day"))}
color="minimal"
StartIcon="chevron-right"
variant="icon"
/>
</ButtonGroup>
<span>{browsingDate.format("LL")}</span>
</div>
);
},
cell: ({ row }) => {
const { timeZone, dateRanges } = row.original;
// return <pre>{JSON.stringify(dateRanges, null, 2)}</pre>;
return <TimeDial timezone={timeZone} dateRanges={dateRanges} />;
},
},
];
return cols;
}, [browsingDate]);
//we must flatten the array of arrays from the useInfiniteQuery hook
const flatData = useMemo(() => data?.pages?.flatMap((page) => page.rows) ?? [], [data]) as SliderUser[];
const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;
const totalFetched = flatData.length;
//called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table
const fetchMoreOnBottomReached = useCallback(
(containerRefElement?: HTMLDivElement | null) => {
if (containerRefElement) {
const { scrollHeight, scrollTop, clientHeight } = containerRefElement;
//once the user has scrolled within 300px of the bottom of the table, fetch more data if there is any
if (scrollHeight - scrollTop - clientHeight < 300 && !isFetching && totalFetched < totalDBRowCount) {
fetchNextPage();
}
}
},
[fetchNextPage, isFetching, totalFetched, totalDBRowCount]
);
useEffect(() => {
fetchMoreOnBottomReached(tableContainerRef.current);
}, [fetchMoreOnBottomReached]);
// This means they are not apart of any teams so we show the upgrade tip
if (!flatData.length) return <UpgradeTeamTip />;
return (
<TBContext.Provider
value={createTimezoneBuddyStore({
browsingDate: browsingDate.toDate(),
})}>
<>
<div className="relative -mx-2 w-[calc(100%+16px)] overflow-x-scroll px-2 lg:-mx-6 lg:w-[calc(100%+48px)] lg:px-6">
<DataTable
variant="compact"
searchKey="member"
tableContainerRef={tableContainerRef}
columns={memorisedColumns}
onRowMouseclick={(row) => {
setEditSheetOpen(true);
setSelectedUser(row.original);
}}
data={flatData}
isPending={isPending}
// 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>
);
}