fix:CityNotShownInOption (#17064)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
co-authored by
Alex van Andel
parent
0949bbf167
commit
14ae664941
@@ -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
@@ -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}`;
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user