chore: [app dir bootstrapping 4] check nullability of navigation hook return values (#12005)

This commit is contained in:
Greg Pabian
2023-10-20 16:47:05 -07:00
committed by GitHub
parent eac45c5e23
commit 39cfe18ffe
28 changed files with 114 additions and 86 deletions
@@ -1,4 +1,4 @@
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -26,9 +26,6 @@ type Props = {
};
export default function CancelBooking(props: Props) {
const pathname = usePathname();
const searchParams = useSearchParams();
const asPath = `${pathname}?${searchParams.toString()}`;
const [cancellationReason, setCancellationReason] = useState<string>("");
const { t } = useLocale();
const router = useRouter();
@@ -44,6 +41,7 @@ export default function CancelBooking(props: Props) {
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
{error && (
@@ -100,7 +98,8 @@ export default function CancelBooking(props: Props) {
});
if (res.status >= 200 && res.status < 300) {
router.replace(asPath);
// tested by apps/web/playwright/booking-pages.e2e.ts
router.refresh();
} else {
setLoading(false);
setError(
@@ -222,9 +222,9 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
onChange={(event) => {
event.preventDefault();
// Reset payment status
const _searchParams = new URLSearchParams(searchParams);
const _searchParams = new URLSearchParams(searchParams ?? undefined);
_searchParams.delete("paymentStatus");
if (searchParams.toString() !== _searchParams.toString()) {
if (searchParams?.toString() !== _searchParams.toString()) {
router.replace(`${pathname}?${_searchParams.toString()}`);
}
setInputUsernameValue(event.target.value);
+4 -4
View File
@@ -1,12 +1,12 @@
import { usePathname, useSearchParams } from "next/navigation";
export default function useIsBookingPage() {
export default function useIsBookingPage(): boolean {
const pathname = usePathname();
const isBookingPage = ["/booking/", "/cancel", "/reschedule"].some((route) => pathname?.startsWith(route));
const searchParams = useSearchParams();
const userParam = searchParams.get("user");
const teamParam = searchParams.get("team");
const userParam = Boolean(searchParams?.get("user"));
const teamParam = Boolean(searchParams?.get("team"));
return !!(isBookingPage || userParam || teamParam);
return isBookingPage || userParam || teamParam;
}
+10 -6
View File
@@ -1,17 +1,21 @@
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback } from "react";
export default function useRouterQuery<T extends string>(name: T) {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const setQuery = (newValue: string | number | null | undefined) => {
const _searchParams = new URLSearchParams(searchParams);
_searchParams.set(name, newValue as string);
router.replace(`${pathname}?${_searchParams.toString()}`);
};
const setQuery = useCallback(
(newValue: string | number | null | undefined) => {
const _searchParams = new URLSearchParams(searchParams ?? undefined);
_searchParams.set(name, newValue as string);
router.replace(`${pathname}?${_searchParams.toString()}`);
},
[name, pathname, router, searchParams]
);
return { [name]: searchParams.get(name), setQuery } as {
return { [name]: searchParams?.get(name), setQuery } as {
[K in T]: string | undefined;
} & { setQuery: typeof setQuery };
}
+6 -7
View File
@@ -51,8 +51,8 @@ export default function Custom404() {
const [url, setUrl] = useState(`${WEBSITE_URL}/signup`);
useEffect(() => {
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(window.location.host);
const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/);
if (!isValidOrgDomain || !currentOrgDomain) {
const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? [];
if (routerUsername && (!isValidOrgDomain || !currentOrgDomain)) {
const splitPath = routerUsername.split("/");
if (splitPath[1] === "team" && splitPath.length === 3) {
// Accessing a non-existent team
@@ -66,13 +66,12 @@ export default function Custom404() {
setUrl(`${WEBSITE_URL}/signup?username=${routerUsername.replace("/", "")}`);
}
} else {
setUsername(currentOrgDomain);
setUsername(currentOrgDomain ?? "");
setCurrentPageType(pageType.ORG);
setUrl(
`${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${currentOrgDomain.replace(
"/",
""
)}`
`${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${
currentOrgDomain?.replace("/", "") ?? ""
}`
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
+1 -1
View File
@@ -83,7 +83,7 @@ inferSSRProps<typeof _getServerSideProps> & WithNonceProps<{}>) {
const telemetry = useTelemetry();
let callbackUrl = searchParams.get("callbackUrl") || "";
let callbackUrl = searchParams?.get("callbackUrl") || "";
if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1);
+1 -1
View File
@@ -22,7 +22,7 @@ export default function Authorize() {
const state = searchParams?.get("state") as string;
const scope = searchParams?.get("scope") as string;
const queryString = searchParams.toString();
const queryString = searchParams?.toString();
const [selectedAccount, setSelectedAccount] = useState<{ value: string; label: string } | null>();
const scopes = scope ? scope.toString().split(",") : [];
+1 -1
View File
@@ -24,7 +24,7 @@ function useSetStep() {
const searchParams = useSearchParams();
const pathname = usePathname();
const setStep = (newStep = 1) => {
const _searchParams = new URLSearchParams(searchParams);
const _searchParams = new URLSearchParams(searchParams ?? undefined);
_searchParams.set("step", newStep.toString());
router.replace(`${pathname}?${_searchParams.toString()}`);
};
+1 -1
View File
@@ -164,7 +164,7 @@ export default function Verify() {
e.preventDefault();
setSecondsLeft(30);
// Update query params with t:timestamp, shallow: true doesn't re-render the page
const _searchParams = new URLSearchParams(searchParams.toString());
const _searchParams = new URLSearchParams(searchParams?.toString());
_searchParams.set("t", `${Date.now()}`);
router.replace(`${pathname}?${_searchParams.toString()}`);
return await sendVerificationLogin(customer.email, customer.username);
+1 -1
View File
@@ -155,7 +155,7 @@ export default function Success(props: SuccessProps) {
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
const { requiresLoginToUpdate } = props;
function setIsCancellationMode(value: boolean) {
const _searchParams = new URLSearchParams(searchParams);
const _searchParams = new URLSearchParams(searchParams ?? undefined);
if (value) {
_searchParams.set("cancel", "true");
+1 -1
View File
@@ -298,7 +298,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
// inject selection data into url for correct router history
const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => {
const newSearchParams = new URLSearchParams(searchParams);
const newSearchParams = new URLSearchParams(searchParams ?? undefined);
function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) {
if (value) newSearchParams.set(key, value.toString());
if (value === null) newSearchParams.delete(key);
@@ -134,7 +134,15 @@ function PaymentChecker(props: PaymentCheckerProps) {
const bookingSuccessRedirect = useBookingSuccessRedirect();
const utils = trpc.useContext();
const { t } = useLocale();
useEffect(() => {
if (searchParams === null) {
return;
}
// use closure to ensure non-nullability
const sp = searchParams;
const interval = setInterval(() => {
(async () => {
if (props.booking.status === "ACCEPTED") {
@@ -153,7 +161,7 @@ function PaymentChecker(props: PaymentCheckerProps) {
location: string;
} = {
uid: props.booking.uid,
email: searchParams.get("email"),
email: sp.get("email"),
location: t("web_conferencing_details_to_follow"),
};
@@ -165,6 +173,7 @@ function PaymentChecker(props: PaymentCheckerProps) {
}
})();
}, 1000);
return () => clearInterval(interval);
}, [
bookingSuccessRedirect,
@@ -178,5 +187,6 @@ function PaymentChecker(props: PaymentCheckerProps) {
t,
utils.viewer.bookings,
]);
return null;
}
@@ -30,15 +30,20 @@ export default function AlbySetup(props: IAlbySetupProps) {
function AlbySetupCallback() {
const [error, setError] = useState<string | null>(null);
const params = useSearchParams();
const searchParams = useSearchParams();
useEffect(() => {
if (!searchParams) {
return;
}
if (!window.opener) {
setError("Something went wrong. Opener not available. Please contact support@getalby.com");
return;
}
const code = params.get("code");
const error = params.get("error");
const code = searchParams?.get("code");
const error = searchParams?.get("error");
if (!code) {
setError("declined");
@@ -54,7 +59,7 @@ function AlbySetupCallback() {
payload: { code },
});
window.close();
}, []);
}, [searchParams]);
return (
<div>
@@ -48,7 +48,7 @@ export const useOpenModal = () => {
const pathname = usePathname();
const searchParams = useSearchParams();
const openModal = (option: z.infer<typeof newFormModalQuerySchema>) => {
const newQuery = new URLSearchParams(searchParams);
const newQuery = new URLSearchParams(searchParams ?? undefined);
newQuery.set("dialog", "new-form");
Object.keys(option).forEach((key) => {
newQuery.set(key, option[key as keyof typeof option] || "");
@@ -301,7 +301,7 @@ const usePrefilledResponse = (form: Props["form"]) => {
// Prefill the form from query params
form.fields?.forEach((field) => {
const valuesFromQuery = searchParams?.getAll(getFieldIdentifier(field)).filter(Boolean);
const valuesFromQuery = searchParams?.getAll(getFieldIdentifier(field)).filter(Boolean) ?? [];
// We only want to keep arrays if the field is a multi-select
const value = valuesFromQuery.length > 1 ? valuesFromQuery : valuesFromQuery[0];
+1 -1
View File
@@ -268,7 +268,7 @@ interface EditModalState extends Pick<App, "keys"> {
const AdminAppsListContainer = () => {
const searchParams = useSearchParams();
const { t } = useLocale();
const category = searchParams.get("category") || AppCategories.calendar;
const category = searchParams?.get("category") || AppCategories.calendar;
const { data: apps, isLoading } = trpc.viewer.appsRouter.listLocal.useQuery(
{ category },
@@ -38,7 +38,7 @@ function OverlayCalendarSwitch({ enabled }: OverlayCalendarSwitchProps) {
// Toggle query param for overlay calendar
const toggleOverlayCalendarQueryParam = useCallback(
(state: boolean) => {
const current = new URLSearchParams(Array.from(searchParams.entries()));
const current = new URLSearchParams(Array.from(searchParams?.entries() ?? []));
if (state) {
current.set("overlayCalendar", "true");
localStorage.setItem("overlayCalendarSwitchDefault", "true");
@@ -121,7 +121,7 @@ export function OverlayCalendarContainer() {
const { data: session, status: sessionStatus } = useSession();
const setOverlayBusyDates = useOverlayCalendarStore((state) => state.setOverlayBusyDates);
const switchEnabled =
searchParams.get("overlayCalendar") === "true" ||
searchParams?.get("overlayCalendar") === "true" ||
localStorage.getItem("overlayCalendarSwitchDefault") === "true";
const selectedDate = useBookerStore((state) => state.selectedDate);
@@ -62,7 +62,7 @@ export const useScheduleForEvent = ({
shallow
);
const searchParams = useSearchParams();
const rescheduleUid = searchParams.get("rescheduleUid");
const rescheduleUid = searchParams?.get("rescheduleUid");
const pathname = usePathname();
@@ -78,6 +78,6 @@ export const useScheduleForEvent = ({
rescheduleUid,
month: monthFromStore ?? month,
duration: durationFromStore ?? duration,
isTeamEvent: pathname.indexOf("/team/") !== -1 || isTeam,
isTeamEvent: pathname?.indexOf("/team/") !== -1 || isTeam,
});
};
@@ -61,7 +61,7 @@ export const EventMembers = ({ schedulingType, users, profile, entity }: EventMe
image: "logo" in profile && profile.logo ? `${profile.logo}` : undefined,
alt: profile.name || undefined,
href: profile.username
? `${CAL_URL}${pathname.indexOf("/team/") !== -1 ? "/team" : ""}/${profile.username}`
? `${CAL_URL}${pathname?.indexOf("/team/") !== -1 ? "/team" : ""}/${profile.username}`
: undefined,
});
@@ -58,7 +58,7 @@ const MembersView = () => {
const { t, i18n } = useLocale();
const router = useRouter();
const searchParams = useSearchParams();
const teamId = Number(searchParams.get("id"));
const teamId = Number(searchParams?.get("id"));
const session = useSession();
const utils = trpc.useContext();
const [offset, setOffset] = useState<number>(1);
@@ -74,6 +74,7 @@ const MembersView = () => {
const { data: team, isLoading: isTeamLoading } = trpc.viewer.organizations.getOtherTeam.useQuery(
{ teamId },
{
enabled: !Number.isNaN(teamId),
onError: () => {
router.push("/settings");
},
@@ -86,13 +87,14 @@ const MembersView = () => {
distinctUser: true,
},
{
enabled: searchParams !== null,
enabled: !Number.isNaN(teamId),
}
);
const { data: membersFetch, isLoading: isLoadingMembers } =
trpc.viewer.organizations.listOtherTeamMembers.useQuery(
{ teamId, limit, offset: (offset - 1) * limit },
{
enabled: !Number.isNaN(teamId),
onError: () => {
router.push("/settings");
},
@@ -101,12 +103,8 @@ const MembersView = () => {
useEffect(() => {
if (membersFetch) {
if (membersFetch.length < limit) {
setLoadMore(false);
} else {
setLoadMore(true);
}
setMembers(members.concat(membersFetch));
setLoadMore(membersFetch.length >= limit);
setMembers((m) => m.concat(membersFetch));
}
}, [membersFetch]);
@@ -77,11 +77,11 @@ const OtherTeamProfileView = () => {
resolver: zodResolver(teamProfileFormSchema),
});
const searchParams = useSearchParams();
const teamId = Number(searchParams.get("id"));
const teamId = Number(searchParams?.get("id"));
const { data: team, isLoading } = trpc.viewer.organizations.getOtherTeam.useQuery(
{ teamId: teamId },
{
enabled: !!teamId,
enabled: !Number.isNaN(teamId),
onError: () => {
router.push("/settings");
},
@@ -81,12 +81,17 @@ const PaymentForm = (props: Props) => {
const handleSubmit = async (ev: SyntheticEvent) => {
ev.preventDefault();
if (!stripe || !elements) return;
if (!stripe || !elements || searchParams === null) {
return;
}
setState({ status: "processing" });
let payload;
const params: {
[k: string]: any;
uid: string;
email: string | null;
location?: string;
} = {
uid: props.booking.uid,
email: searchParams.get("email"),
@@ -17,7 +17,10 @@ const UsersAddView = () => {
onSuccess: async () => {
showToast("User added successfully", "success");
await utils.viewer.users.list.invalidate();
router.replace(pathname?.replace("/add", ""));
if (pathname !== null) {
router.replace(pathname.replace("/add", ""));
}
},
onError: (err) => {
console.error(err.message);
+5 -4
View File
@@ -520,18 +520,19 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
(state) => [state.month, state.selectedDatesAndTimes],
shallow
);
const eventId = searchParams.get("eventId");
const eventId = searchParams?.get("eventId");
const parsedEventId = parseInt(eventId ?? "", 10);
const calLink = decodeURIComponent(embedUrl);
const { data: eventTypeData } = trpc.viewer.eventTypes.get.useQuery(
{ id: parseInt(eventId as string) },
{ enabled: !!eventId && embedType === "email", refetchOnWindowFocus: false }
{ id: parsedEventId },
{ enabled: !Number.isNaN(parsedEventId) && embedType === "email", refetchOnWindowFocus: false }
);
const s = (href: string) => {
const _searchParams = new URLSearchParams(searchParams);
const [a, b] = href.split("=");
_searchParams.set(a, b);
return `${pathname?.split("?")[0]}?${_searchParams.toString()}`;
return `${pathname?.split("?")[0] ?? ""}?${_searchParams.toString()}`;
};
const parsedTabs = tabs.map((t) => ({ ...t, href: s(t.href) }));
const embedCodeRefs: Record<(typeof tabs)[0]["name"], RefObject<HTMLTextAreaElement>> = {};
@@ -8,21 +8,22 @@ import { trpc } from "@calcom/trpc";
import type { FilterContextType } from "./provider";
import { FilterProvider } from "./provider";
const querySchema = z.object({
startTime: z.string().nullable(),
endTime: z.string().nullable(),
teamId: z.coerce.number().nullable(),
userId: z.coerce.number().nullable(),
memberUserId: z.coerce.number().nullable(),
eventTypeId: z.coerce.number().nullable(),
filter: z.enum(["event-type", "user"]).nullable(),
});
export function FiltersProvider({ children }: { children: React.ReactNode }) {
// searchParams to get initial values from query params
const utils = trpc.useContext();
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const querySchema = z.object({
startTime: z.string().nullable(),
endTime: z.string().nullable(),
teamId: z.coerce.number().nullable(),
userId: z.coerce.number().nullable(),
memberUserId: z.coerce.number().nullable(),
eventTypeId: z.coerce.number().nullable(),
filter: z.enum(["event-type", "user"]).nullable(),
});
let startTimeParsed,
endTimeParsed,
@@ -33,13 +34,13 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) {
memberUserIdParsed;
const safe = querySchema.safeParse({
startTime: searchParams.get("startTime"),
endTime: searchParams.get("endTime"),
teamId: searchParams.get("teamId"),
userId: searchParams.get("userId"),
eventTypeId: searchParams.get("eventTypeId"),
filter: searchParams.get("filter"),
memberUserId: searchParams.get("memberUserId"),
startTime: searchParams?.get("startTime") ?? null,
endTime: searchParams?.get("endTime") ?? null,
teamId: searchParams?.get("teamId") ?? null,
userId: searchParams?.get("userId") ?? null,
eventTypeId: searchParams?.get("eventTypeId") ?? null,
filter: searchParams?.get("filter") ?? null,
memberUserId: searchParams?.get("memberUserId") ?? null,
});
if (!safe.success) {
@@ -119,7 +120,7 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) {
initialConfig,
} = newConfigFilters;
const [startTime, endTime] = dateRange || [null, null];
const newSearchParams = new URLSearchParams(searchParams.toString());
const newSearchParams = new URLSearchParams(searchParams?.toString() ?? undefined);
function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) {
if (value !== undefined && value !== null) newSearchParams.set(key, value.toString());
}
+10 -7
View File
@@ -509,7 +509,7 @@ export type NavigationItemType = {
}: {
item: Pick<NavigationItemType, "href">;
isChild?: boolean;
pathname: string;
pathname: string | null;
}) => boolean;
};
@@ -527,7 +527,7 @@ const navigation: NavigationItemType[] = [
href: "/bookings/upcoming",
icon: Calendar,
badge: <UnconfirmedBookingBadge />,
isCurrent: ({ pathname }) => pathname?.startsWith("/bookings"),
isCurrent: ({ pathname }) => pathname?.startsWith("/bookings") ?? false,
},
{
name: "availability",
@@ -547,7 +547,7 @@ const navigation: NavigationItemType[] = [
icon: Grid,
isCurrent: ({ pathname: path, item }) => {
// During Server rendering path is /v2/apps but on client it becomes /apps(weird..)
return path?.startsWith(item.href) && !path?.includes("routing-forms/");
return (path?.startsWith(item.href) ?? false) && !(path?.includes("routing-forms/") ?? false);
},
child: [
{
@@ -556,7 +556,9 @@ const navigation: NavigationItemType[] = [
isCurrent: ({ pathname: path, item }) => {
// During Server rendering path is /v2/apps but on client it becomes /apps(weird..)
return (
path?.startsWith(item.href) && !path?.includes("routing-forms/") && !path?.includes("/installed")
(path?.startsWith(item.href) ?? false) &&
!(path?.includes("routing-forms/") ?? false) &&
!(path?.includes("/installed") ?? false)
);
},
},
@@ -564,7 +566,8 @@ const navigation: NavigationItemType[] = [
name: "installed_apps",
href: "/apps/installed/calendar",
isCurrent: ({ pathname: path }) =>
path?.startsWith("/apps/installed/") || path?.startsWith("/v2/apps/installed/"),
(path?.startsWith("/apps/installed/") ?? false) ||
(path?.startsWith("/v2/apps/installed/") ?? false),
},
],
},
@@ -577,7 +580,7 @@ const navigation: NavigationItemType[] = [
name: "Routing Forms",
href: "/apps/routing-forms/forms",
icon: FileText,
isCurrent: ({ pathname }) => pathname?.startsWith("/apps/routing-forms/"),
isCurrent: ({ pathname }) => pathname?.startsWith("/apps/routing-forms/") ?? false,
},
{
name: "workflows",
@@ -631,7 +634,7 @@ function useShouldDisplayNavigationItem(item: NavigationItemType) {
}
const defaultIsCurrent: NavigationItemType["isCurrent"] = ({ isChild, item, pathname }) => {
return isChild ? item.href === pathname : item.href ? pathname?.startsWith(item.href) : false;
return isChild ? item.href === pathname : item.href ? pathname?.startsWith(item.href) ?? false : false;
};
const NavigationItem: React.FC<{
@@ -12,7 +12,7 @@ import { subscriberUrlReserved } from "../lib/subscriberUrlReserved";
const EditWebhook = () => {
const searchParams = useSearchParams();
const id = searchParams.get("id");
const id = searchParams?.get("id");
if (!id) return <SkeletonContainer />;
+1 -1
View File
@@ -60,7 +60,7 @@ export const useBookingSuccessRedirect = () => {
...query,
...bookingExtraParams,
},
searchParams,
searchParams: searchParams ?? undefined,
});
window.parent.location.href = `${url.toString()}?${newSearchParams.toString()}`;
return;