fix:CityNotShownInOption (#17064)

Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
Shrey Tanna
2024-10-24 09:10:27 -04:00
committed by GitHub
co-authored by Alex van Andel
parent 0949bbf167
commit 14ae664941
10 changed files with 104 additions and 219 deletions
+34 -94
View File
@@ -1,88 +1,28 @@
import { expect, beforeEach, afterEach, it, vi, describe } from "vitest";
import { filterByCities, addCitiesToDropdown, handleOptionLabel } from "@calcom/lib/timezone";
import { addTimezonesToDropdown, handleOptionLabel, filterBySearchText } from "@calcom/lib/timezone";
const cityData = [
{
city: "San Francisco",
timezone: "America/Argentina/Cordoba",
},
{
city: "Sao Francisco do Sul",
timezone: "America/Sao_Paulo",
},
{
city: "San Francisco de Macoris",
timezone: "America/Santo_Domingo",
},
{
city: "San Francisco Gotera",
timezone: "America/El_Salvador",
},
{
city: "San Francisco",
timezone: "America/Los_Angeles",
},
{
city: "Eastern Time - US & Canada",
timezone: "America/New_York",
},
{
city: "Pacific Time - US & Canada",
timezone: "America/Los_Angeles",
},
{
city: "Central Time - US & Canada",
timezone: "America/Chicago",
},
{
city: "Mountain Time - US & Canada",
timezone: "America/Denver",
},
{
city: "Atlantic Time - Canada",
timezone: "America/Halifax",
},
{
city: "Eastern European Time",
timezone: "Europe/Bucharest",
},
{
city: "Central European Time",
timezone: "Europe/Berlin",
},
{
city: "Western European Time",
timezone: "Europe/London",
},
{
city: "Australian Eastern Time",
timezone: "Australia/Sydney",
},
{
city: "Japan Standard Time",
timezone: "Asia/Tokyo",
},
{
city: "India Standard Time",
timezone: "Asia/Kolkata",
},
{
city: "Gulf Standard Time",
timezone: "Asia/Dubai",
},
{
city: "South Africa Standard Time",
timezone: "Africa/Johannesburg",
},
{
city: "Brazil Time",
timezone: "America/Sao_Paulo",
},
{
city: "Hawaii-Aleutian Standard Time",
timezone: "Pacific/Honolulu",
},
const timezonesFixture = [
{ label: "San Francisco", timezone: "America/Argentina/Cordoba" },
{ label: "San Francisco", timezone: "America/Los_Angeles" },
{ label: "Sao Francisco do Sul", timezone: "America/Sao_Paulo" },
{ label: "San Francisco de Macoris", timezone: "America/Santo_Domingo" },
{ label: "San Francisco Gotera", timezone: "America/El_Salvador" },
{ label: "Eastern Time - US & Canada", timezone: "America/New_York" },
{ label: "Pacific Time - US & Canada", timezone: "America/Los_Angeles" },
{ label: "Central Time - US & Canada", timezone: "America/Chicago" },
{ label: "Mountain Time - US & Canada", timezone: "America/Denver" },
{ label: "Atlantic Time - Canada", timezone: "America/Halifax" },
{ label: "Eastern European Time", timezone: "Europe/Bucharest" },
{ label: "Central European Time", timezone: "Europe/Berlin" },
{ label: "Western European Time", timezone: "Europe/London" },
{ label: "Australian Eastern Time", timezone: "Australia/Sydney" },
{ label: "Japan Standard Time", timezone: "Asia/Tokyo" },
{ label: "India Standard Time", timezone: "Asia/Kolkata" },
{ label: "Gulf Standard Time", timezone: "Asia/Dubai" },
{ label: "South Africa Standard Time", timezone: "Africa/Johannesburg" },
{ label: "Brazil Time", timezone: "America/Sao_Paulo" },
{ label: "Hawaii-Aleutian Standard Time", timezone: "Pacific/Honolulu" },
];
const option = {
@@ -104,34 +44,34 @@ describe("getTimezone", () => {
});
it("should return empty array for an empty string", () => {
expect(filterByCities("", cityData)).toMatchInlineSnapshot(`[]`);
expect(filterBySearchText("", timezonesFixture)).toMatchInlineSnapshot(`[]`);
});
it("should filter cities for a valid city name", () => {
expect(filterByCities("San Francisco", cityData)).toMatchInlineSnapshot(`
expect(filterBySearchText("San Francisco", timezonesFixture)).toMatchInlineSnapshot(`
[
{
"city": "San Francisco",
"label": "San Francisco",
"timezone": "America/Argentina/Cordoba",
},
{
"city": "San Francisco de Macoris",
"label": "San Francisco",
"timezone": "America/Los_Angeles",
},
{
"label": "San Francisco de Macoris",
"timezone": "America/Santo_Domingo",
},
{
"city": "San Francisco Gotera",
"label": "San Francisco Gotera",
"timezone": "America/El_Salvador",
},
{
"city": "San Francisco",
"timezone": "America/Los_Angeles",
},
]
`);
});
it("should return appropriate timezone(s) for a given city name array", () => {
expect(addCitiesToDropdown(cityData)).toMatchInlineSnapshot(`
expect(addTimezonesToDropdown(timezonesFixture)).toMatchInlineSnapshot(`
{
"Africa/Johannesburg": "South Africa Standard Time",
"America/Argentina/Cordoba": "San Francisco",
@@ -156,9 +96,9 @@ describe("getTimezone", () => {
});
it("should render city name as option label if cityData is not empty", () => {
expect(handleOptionLabel(option, cityData)).toMatchInlineSnapshot(`"San Francisco GMT -8:00"`);
expect(handleOptionLabel(option, timezonesFixture)).toMatchInlineSnapshot(`"San Francisco GMT -8:00"`);
vi.setSystemTime(new Date("2020-06-01"));
expect(handleOptionLabel(option, cityData)).toMatchInlineSnapshot(`"San Francisco GMT -7:00"`);
expect(handleOptionLabel(option, timezonesFixture)).toMatchInlineSnapshot(`"San Francisco GMT -7:00"`);
});
it("should return timezone as option label if cityData is empty", () => {
@@ -1,64 +0,0 @@
{
"timezones": [
{
"name": "Eastern Time - US & Canada",
"identifier": "America/New_York"
},
{
"name": "Pacific Time - US & Canada",
"identifier": "America/Los_Angeles"
},
{
"name": "Central Time - US & Canada",
"identifier": "America/Chicago"
},
{
"name": "Mountain Time - US & Canada",
"identifier": "America/Denver"
},
{
"name": "Atlantic Time - Canada",
"identifier": "America/Halifax"
},
{
"name": "Eastern European Time",
"identifier": "Europe/Bucharest"
},
{
"name": "Central European Time",
"identifier": "Europe/Berlin"
},
{
"name": "Western European Time",
"identifier": "Europe/London"
},
{
"name": "Australian Eastern Time",
"identifier": "Australia/Sydney"
},
{
"name": "Japan Standard Time",
"identifier": "Asia/Tokyo"
},
{
"name": "India Standard Time",
"identifier": "Asia/Kolkata"
},
{
"name": "Gulf Standard Time",
"identifier": "Asia/Dubai"
},
{
"name": "South Africa Standard Time",
"identifier": "Africa/Johannesburg"
},
{
"name": "Brazil Time",
"identifier": "America/Sao_Paulo"
},
{
"name": "Hawaii-Aleutian Standard Time",
"identifier": "Pacific/Honolulu"
}
]
}
@@ -1,10 +0,0 @@
import * as additionalTimezones from "./additionalTimezones.json";
export interface Timezone {
name: string;
identifier: string;
}
export const timezones: Timezone[] = additionalTimezones.timezones;
export default additionalTimezones;
+16 -31
View File
@@ -1,53 +1,38 @@
import type { ITimezoneOption } from "react-timezone-select";
import dayjs from "@calcom/dayjs";
import type { ICity } from "@calcom/ui/components/form/timezone-select";
import { timezones as additionalTimezones } from "../features/additionalTimezones/additionalTimezones";
import isProblematicTimezone from "./isProblematicTimezone";
function findPartialMatch(itemsToSearch: string, searchString: string) {
const searchItems = searchString.split(" ");
return searchItems.every((i) => itemsToSearch.toLowerCase().indexOf(i.toLowerCase()) >= 0);
}
export type Timezones = { label: string; timezone: string }[];
function findFromCity(searchString: string, data: ICity[]): ICity[] {
if (searchString) {
const cityLookup = data.filter((o) => findPartialMatch(o.city, searchString));
return cityLookup?.length ? cityLookup : [];
}
return [];
}
export const filterByCities = (tz: string, data: ICity[]): ICity[] => {
const cityLookup = findFromCity(tz, data);
return cityLookup.map(({ city, timezone }) => ({ city, timezone }));
const searchTextFilter = (tzOption: Timezones[number], searchText: string) => {
return searchText && tzOption.label.toLowerCase().includes(searchText.toLowerCase());
};
export const addCitiesToDropdown = (cities: ICity[]) => {
const cityTimezones = cities?.reduce((acc: { [key: string]: string }, city: ICity) => {
if (city.timezone !== null && !isProblematicTimezone(city.timezone)) {
acc[city.timezone] = city.city;
}
return acc;
}, {});
export const filterBySearchText = (searchText: string, timezones: Timezones) => {
return timezones.filter((tzOption) => searchTextFilter(tzOption, searchText));
};
additionalTimezones.forEach((tz) => {
cityTimezones[tz.identifier] = tz.name;
});
return cityTimezones || {};
export const addTimezonesToDropdown = (timezones: Timezones) => {
return Object.fromEntries(
timezones
.filter(({ timezone }) => {
return timezone !== null && !isProblematicTimezone(timezone);
})
.map(({ label, timezone }) => [timezone, label])
);
};
const formatOffset = (offset: string) =>
offset.replace(/^([-+])(0)(\d):00$/, (_, sign, _zero, hour) => `${sign}${hour}:00`);
export const handleOptionLabel = (option: ITimezoneOption, cities: ICity[]) => {
export const handleOptionLabel = (option: ITimezoneOption, timezones: Timezones) => {
const offsetUnit = option.label.split("-")[0].substring(1);
const cityName = option.label.split(") ")[1];
const timezoneValue = ` ${offsetUnit} ${formatOffset(dayjs.tz(undefined, option.value).format("Z"))}`;
return cities.length > 0
return timezones.length > 0
? `${cityName}${timezoneValue}`
: `${option.value.replace(/_/g, " ")}${timezoneValue}`;
};
+2 -2
View File
@@ -88,8 +88,8 @@ export type { AppsStatus } from "@calcom/types/Calendar";
export { MINUTES_TO_BOOK } from "@calcom/lib/constants";
export { cityTimezonesHandler } from "@calcom/lib/cityTimezonesHandler";
export type { CityTimezones } from "@calcom/lib/cityTimezonesHandler";
export { cityTimezonesHandler } from "@calcom/features/cityTimezones/cityTimezonesHandler";
export type { CityTimezones } from "@calcom/features/cityTimezones/cityTimezonesHandler";
export { TRPCError } from "@trpc/server";
export type { TUpdateInputSchema } from "@calcom/trpc/server/routers/viewer/availability/schedule/update.schema";
@@ -11,7 +11,7 @@ export const timezonesRouter = router({
cityTimezones: publicProcedure.input(cityTimezonesSchema).query(async () => {
const handler = await importHandler(
namespaced("cityTimezones"),
() => import("@calcom/lib/cityTimezonesHandler")
() => import("@calcom/features/cityTimezones/cityTimezonesHandler")
);
return handler();
}),
@@ -6,22 +6,40 @@ import BaseSelect from "react-timezone-select";
import { classNames } from "@calcom/lib";
import { CALCOM_VERSION } from "@calcom/lib/constants";
import { filterByCities, addCitiesToDropdown, handleOptionLabel } from "@calcom/lib/timezone";
import { filterBySearchText, addTimezonesToDropdown, handleOptionLabel } from "@calcom/lib/timezone";
import type { Timezones } from "@calcom/lib/timezone";
import { trpc } from "@calcom/trpc/react";
import { getReactSelectProps } from "../select";
export interface ICity {
city: string;
timezone: string;
}
const SELECT_SEARCH_DATA: Timezones = [
{ label: "San Francisco", timezone: "America/Los_Angeles" },
{ label: "Sao Francisco do Sul", timezone: "America/Sao_Paulo" },
{ label: "San Francisco de Macoris", timezone: "America/Santo_Domingo" },
{ label: "San Francisco Gotera", timezone: "America/El_Salvador" },
{ label: "Eastern Time - US & Canada", timezone: "America/New_York" },
{ label: "Pacific Time - US & Canada", timezone: "America/Los_Angeles" },
{ label: "Central Time - US & Canada", timezone: "America/Chicago" },
{ label: "Mountain Time - US & Canada", timezone: "America/Denver" },
{ label: "Atlantic Time - Canada", timezone: "America/Halifax" },
{ label: "Eastern European Time", timezone: "Europe/Bucharest" },
{ label: "Central European Time", timezone: "Europe/Berlin" },
{ label: "Western European Time", timezone: "Europe/London" },
{ label: "Australian Eastern Time", timezone: "Australia/Sydney" },
{ label: "Japan Standard Time", timezone: "Asia/Tokyo" },
{ label: "India Standard Time", timezone: "Asia/Kolkata" },
{ label: "Gulf Standard Time", timezone: "Asia/Dubai" },
{ label: "South Africa Standard Time", timezone: "Africa/Johannesburg" },
{ label: "Brazil Time", timezone: "America/Sao_Paulo" },
{ label: "Hawaii-Aleutian Standard Time", timezone: "Pacific/Honolulu" },
];
export type TimezoneSelectProps = SelectProps & {
variant?: "default" | "minimal";
timezoneSelectCustomClassname?: string;
};
export function TimezoneSelect(props: TimezoneSelectProps) {
const { data, isPending } = trpc.viewer.timezones.cityTimezones.useQuery(
const { data = [], isPending } = trpc.viewer.timezones.cityTimezones.useQuery(
{
CalComVersion: CALCOM_VERSION,
},
@@ -29,14 +47,19 @@ export function TimezoneSelect(props: TimezoneSelectProps) {
trpc: { context: { skipBatch: true } },
}
);
return <TimezoneSelectComponent data={data} isPending={isPending} {...props} />;
return (
<TimezoneSelectComponent
data={data.map(({ city, timezone }) => ({ label: city, timezone }))}
isPending={isPending}
{...props}
/>
);
}
export type TimezoneSelectComponentProps = SelectProps & {
variant?: "default" | "minimal";
isPending: boolean;
data: ICity[] | undefined;
data?: Timezones;
timezoneSelectCustomClassname?: string;
};
export function TimezoneSelectComponent({
@@ -45,14 +68,21 @@ export function TimezoneSelectComponent({
timezoneSelectCustomClassname,
components,
variant = "default",
data,
isPending,
value,
...props
}: TimezoneSelectComponentProps) {
const [cities, setCities] = useState<ICity[]>([]);
const handleInputChange = (tz: string) => {
if (data) setCities(filterByCities(tz, data));
const data = [...(props.data || []), ...SELECT_SEARCH_DATA];
/*
* we support multiple timezones for the different labels
* e.g. 'Sao Paulo' and 'Brazil Time' both being 'America/Sao_Paulo'
* but react-timezone-select does not.
*
* We make sure to be able to search through both options, and flip the key/value on final display.
*/
const [additionalTimezones, setAdditionalTimezones] = useState<Timezones>([]);
const handleInputChange = (searchText: string) => {
if (data) setAdditionalTimezones(filterBySearchText(searchText, data));
};
const reactSelectProps = useMemo(() => {
@@ -70,15 +100,15 @@ export function TimezoneSelectComponent({
isDisabled={isPending}
{...reactSelectProps}
timezones={{
...(data ? addCitiesToDropdown(data) : {}),
...addCitiesToDropdown(cities),
...(props.data ? addTimezonesToDropdown(data) : {}),
...addTimezonesToDropdown(additionalTimezones),
}}
onInputChange={handleInputChange}
{...props}
formatOptionLabel={(option) => (
<p className="truncate">{(option as ITimezoneOption).value.replace(/_/g, " ")}</p>
)}
getOptionLabel={(option) => handleOptionLabel(option as ITimezoneOption, cities)}
getOptionLabel={(option) => handleOptionLabel(option as ITimezoneOption, additionalTimezones)}
classNames={{
...timezoneClassNames,
input: (state) =>
@@ -2,7 +2,6 @@ export { TimezoneSelect, TimezoneSelectComponent } from "./TimezoneSelect";
export type {
ITimezone,
ITimezoneOption,
ICity,
TimezoneSelectProps,
TimezoneSelectComponentProps,
} from "./TimezoneSelect";
+5
View File
@@ -6,6 +6,11 @@
"main": "./index.tsx",
"exports": {
".": "./index.tsx",
"./button": "./components/button/index.ts",
"./form/timezone-select": "./components/form/timezone-select/index.ts",
"./toast": "./components/toast/index.tsx",
"./icon": "./components/icon/index.ts",
"./unpublished-entity/UnpublishedEntity": "./components/unpublished-entity/index.ts",
"./components/toast": "./components/toast/index.tsx",
"./components/icon": "./components/icon/index.ts",
"./components/icon/SatSymbol": "./components/icon/SatSymbol.tsx",