diff --git a/apps/demo/src/Views/Ui/components/FormWidgets/CountryPicker.tsx b/apps/demo/src/Views/Ui/components/FormWidgets/CountryPicker.tsx index 4d8594d6f..ef37e5c66 100644 --- a/apps/demo/src/Views/Ui/components/FormWidgets/CountryPicker.tsx +++ b/apps/demo/src/Views/Ui/components/FormWidgets/CountryPicker.tsx @@ -20,11 +20,11 @@ export const CountryPickerDemo = () => { const data = [ { - default: "{ en: defaultEnCatalogue }", - description: t("countryPicker.propertiesDescription.i18n"), + default: "[]", + description: t("countryPicker.propertiesDescription.exclude"), id: 1, - prop: "i18n", - type: "Record>", + prop: "exclude", + type: "string[]", }, { default: '"en"', @@ -33,82 +33,82 @@ export const CountryPickerDemo = () => { prop: "fallbackLocale", type: "string", }, + { + default: "[]", + description: t("countryPicker.propertiesDescription.favorites"), + id: 3, + prop: "favorites", + type: "string[]", + }, { default: "true", description: t("countryPicker.propertiesDescription.flags"), - id: 3, + id: 4, prop: "flags", type: "Boolean", }, { default: "-", description: t("countryPicker.propertiesDescription.flagsPath"), - id: 4, + id: 5, prop: "flagsPath", type: "(code: string) => string", }, { default: "left", description: t("countryPicker.propertiesDescription.flagsPosition"), - id: 5, + id: 6, prop: "flagsPosition", type: "left | right | right-edge", }, { default: "rectangular", description: t("countryPicker.propertiesDescription.flagsStyle"), - id: 6, + id: 7, prop: "flagsStyle", type: "circle | rectangular | square", }, { - default: '"en"', - description: t("countryPicker.propertiesDescription.locale"), - id: 7, - prop: "locale", - type: "string", - }, - { - default: "[]", - description: t("countryPicker.propertiesDescription.include"), + default: "-", + description: t("countryPicker.propertiesDescription.groups"), id: 8, - prop: "include", - type: "string[]", + prop: "groups", + type: "GroupData", }, { default: "[]", - description: t("countryPicker.propertiesDescription.exclude"), + description: t("countryPicker.propertiesDescription.include"), id: 9, - prop: "exclude", - type: "string[]", - }, - { - default: "[]", - description: t("countryPicker.propertiesDescription.favorites"), - id: 10, - prop: "favorites", + prop: "include", type: "string[]", }, { default: "true", description: t("countryPicker.propertiesDescription.includeFavorites"), - id: 11, + id: 10, prop: "includeFavorites", type: "boolean", }, { default: "-", - description: t("countryPicker.propertiesDescription.groups"), + description: t("countryPicker.propertiesDescription.label"), + id: 11, + prop: "label", + type: "string", + }, + { + default: '"en"', + description: t("countryPicker.propertiesDescription.locale"), id: 12, - prop: "groups", - type: "GroupData", + prop: "locale", + type: "string", }, { - default: "-", - description: t("countryPicker.propertiesDescription.label"), + default: "{ en: defaultEnCatalogue }", + description: t("countryPicker.propertiesDescription.i18n"), id: 13, - prop: "label", - type: "string", + prop: "locales", + type: "Record>", }, { default: "false", @@ -240,7 +240,7 @@ const selectedLocale = i18n.language;
setSingleSelectValue(value)} @@ -270,7 +270,7 @@ const selectedLocale = "np";
{ setSingleSelectValue(value)} @@ -607,19 +607,19 @@ const myRegions = {
; +type Translation = Record; -type I18nData = Record; +type Locales = Record; -type GroupData = Record; +type Groups = Record; -Example I18n: +Example Locales: { en:{ "US": "USA" }, fr: { "US": "États-Unis" } } -Example Group: +Example Groups: { "European Union": ["AT", "BE", "FR", "DE"], "North America": ["US", "CA", "MX"] diff --git a/packages/ui/src/FormWidgets/CountryPicker/index.tsx b/packages/ui/src/FormWidgets/CountryPicker/index.tsx index af7c5778d..7fd01c325 100644 --- a/packages/ui/src/FormWidgets/CountryPicker/index.tsx +++ b/packages/ui/src/FormWidgets/CountryPicker/index.tsx @@ -1,14 +1,14 @@ -import React, { useMemo } from "react"; +import React, { useCallback, useMemo } from "react"; +import { getFallbackTranslation } from "../../utils/CountryPicker"; import { Select, ISelectProperties } from "../Select"; -import defaultEnglishCatalogue from "./en.json"; import defaultGroups from "./groups.json"; import type { Option } from "../Select"; -export type TranslationCatalogue = Record; -export type I18nData = Record; -export type GroupData = Record; +export type Translation = Record; +export type Locales = Record; +export type Groups = Record; export type CountryPickerLabels = { favorites?: string; @@ -26,146 +26,192 @@ export type CountryPickerProperties = Omit< flagsPath?: (code: string) => string; flagsPosition?: "left" | "right" | "right-edge"; flagsStyle?: "circle" | "rectangular" | "square"; - groups?: GroupData; - i18n?: I18nData; + groups?: Groups; include?: string[]; includeFavorites?: boolean; labels?: CountryPickerLabels; locale?: string; + locales?: Locales; }; export { defaultGroups }; -const getAllCountryCodes = ( - i18n: I18nData | undefined, - locale: string, +const getBaseOptions = ( + exclude: string[] | undefined, fallbackLocale: string, -): string[] => { - if (!i18n) return Object.keys(defaultEnglishCatalogue); - - const localeData = i18n[locale]; - const fallbackData = i18n[fallbackLocale]; + include: string[] | undefined, + locale: string, + locales: Locales | undefined, +): Option[] => { + const fallbackData = getFallbackTranslation(fallbackLocale, locales); - if (!localeData && !fallbackData) { - return Object.keys(defaultEnglishCatalogue); + if (!fallbackData) { + return []; } - return Array.from( - new Set([ - ...(fallbackData ? Object.keys(fallbackData) : []), - ...(localeData ? Object.keys(localeData) : []), - ]), - ); -}; - -const getFilteredCountryCodes = ( - codes: string[], - include?: string[], - exclude?: string[], -): string[] => { - const includeSet = include && include.length > 0 ? new Set(include) : null; - const excludeSet = exclude && exclude.length > 0 ? new Set(exclude) : null; + const baseOptions: Option[] = []; - if (!includeSet && !excludeSet) return codes; - - return codes.filter((code) => { - if (excludeSet && excludeSet.has(code)) { - return false; + Object.entries(fallbackData).forEach(([code, fallbackLabel]) => { + if (exclude && exclude.includes(code)) { + return; } - if (includeSet && !includeSet.has(code)) { - return false; + if (include && !include.includes(code)) { + return; } - return true; + const label = locales?.[locale]?.[code] || fallbackLabel || code; + baseOptions.push({ value: code as unknown as T, label }); }); + + return baseOptions; }; -const countryLabel = ( - code: string, - i18n: I18nData | undefined, - locale: string, - fallbackLocale: string, -): string => { - return ( - i18n?.[locale]?.[code] || - i18n?.[fallbackLocale]?.[code] || - defaultEnglishCatalogue[code as keyof typeof defaultEnglishCatalogue] || - code +const getFlagClass = ( + code: string | undefined, + position: string, + style: string, +) => + [ + "flag-icon", + code && `flag-icon-${code.trim().toLowerCase()}`, + position === "right" && "flag-icon-right", + position === "right-edge" && "flag-icon-right-edge", + style === "circle" && "flag-icon-rounded", + style === "square" && "flag-icon-squared", + ] + .filter(Boolean) + .join(" "); + +const getGroups = ( + groups: Groups, + list: Option[], +): { label: string; options: Option[] }[] => { + const groupedResult: { label: string; options: Option[] }[] = []; + + Object.entries(groups).forEach(([groupLabel, groupCodes]) => { + const groupOptions = list.filter((option) => + groupCodes.includes(String(option.value)), + ); + + if (groupOptions.length > 0) { + groupedResult.push({ + label: groupLabel, + options: groupOptions, + }); + } + }); + + return groupedResult; +}; + +const getOptions = ({ + exclude, + fallbackLocale = "en", + favorites, + groups, + include, + includeFavorites = true, + labels, + locale = "en", + locales, +}: Pick< + CountryPickerProperties, + | "exclude" + | "fallbackLocale" + | "favorites" + | "groups" + | "include" + | "includeFavorites" + | "labels" + | "locale" + | "locales" +>) => { + const baseOptions = getBaseOptions( + exclude, + fallbackLocale, + include, + locale, + locales, ); + + if (favorites && favorites.length > 0) { + return getOptionsWithFavorites( + baseOptions, + favorites, + includeFavorites, + groups, + labels, + ); + } + + if (groups && Object.keys(groups).length > 0) { + return getGroups(groups, baseOptions); + } + + return baseOptions; }; const getOptionsWithFavorites = ( baseOptions: Option[], - favorites: string[] | undefined, - labels: CountryPickerLabels | undefined, + favorites: string[], includeFavorites: boolean, + groups?: Groups, + labels?: CountryPickerLabels, ) => { - if (!favorites || favorites.length === 0) { - return baseOptions; - } - - const favoriteSet = new Set(favorites); - const favoriteList = baseOptions.filter((item) => - favoriteSet.has(String(item.value)), + const favoriteOptions = baseOptions.filter((option) => + favorites.includes(String(option.value)), ); - if (favoriteList.length === 0) { - return baseOptions; - } - - const favoritesLabel = labels?.favorites || "Favorites"; - const allCountriesLabel = labels?.allCountries || "All countries"; - - const remainingList = includeFavorites + const mainOptions = includeFavorites ? baseOptions - : baseOptions.filter((item) => !favoriteSet.has(String(item.value))); + : baseOptions.filter((option) => !favorites.includes(String(option.value))); + + const mainGroups = + groups && Object.keys(groups).length > 0 + ? getGroups(groups, mainOptions) + : [ + { + label: labels?.allCountries || "All countries", + options: mainOptions, + }, + ]; return [ - { label: favoritesLabel, options: favoriteList }, - { label: allCountriesLabel, options: remainingList }, + { + label: labels?.favorites || "Favorites", + options: favoriteOptions, + }, + ...mainGroups, ]; }; -const getOptionsWithGroups = ( - baseOptions: Option[], - groups: GroupData, - favorites: string[] | undefined, - labels: CountryPickerLabels | undefined, +const renderCountryOption = ( + option: Option, + { + flags, + flagsPath, + flagsPosition = "left", + flagsStyle = "rectangular", + }: Pick< + CountryPickerProperties, + "flags" | "flagsPath" | "flagsPosition" | "flagsStyle" + >, ) => { - const finalGroupedOptions: { label: string; options: Option[] }[] = []; + const code = String(option.value); + const flagClass = getFlagClass(code, flagsPosition, flagsStyle); - const optionsMap = new Map( - baseOptions.map((option) => [String(option.value), option]), + return ( +
+ {flags && + (flagsPath ? ( + {option.label} + ) : ( + + ))} + {option.label} +
); - - if (favorites && favorites.length > 0) { - const favoriteList = favorites - .map((code) => optionsMap.get(code)) - .filter((option): option is Option => !!option); - - if (favoriteList.length > 0) { - finalGroupedOptions.push({ - label: labels?.favorites || "Favorites", - options: favoriteList, - }); - } - } - - Object.entries(groups).forEach(([groupLabel, groupCodes]) => { - const groupOptions = groupCodes - .map((code) => optionsMap.get(code)) - .filter((option): option is Option => !!option); - - if (groupOptions.length > 0) { - finalGroupedOptions.push({ - label: groupLabel, - options: groupOptions, - }); - } - }); - - return finalGroupedOptions; }; export const CountryPicker = ({ @@ -177,7 +223,7 @@ export const CountryPicker = ({ flagsPosition = "left", flagsStyle = "rectangular", groups, - i18n, + locales, include, includeFavorites = true, labels, @@ -185,89 +231,59 @@ export const CountryPicker = ({ ...properties }: CountryPickerProperties) => { const options = useMemo(() => { - const countryCodes = getAllCountryCodes(i18n, locale, fallbackLocale); - - const filteredCountryCodes = getFilteredCountryCodes( - countryCodes, - include, + return getOptions({ exclude, - ); - const baseOptions: Option[] = filteredCountryCodes.map((code) => ({ - value: code as unknown as T, - label: countryLabel(code, i18n, locale, fallbackLocale), - })); - - if (groups && Object.keys(groups).length > 0) { - return getOptionsWithGroups(baseOptions, groups, favorites, labels); - } - - return getOptionsWithFavorites( - baseOptions, + fallbackLocale, favorites, - labels, + groups, + include, includeFavorites, - ); + labels, + locale, + locales, + }); }, [ exclude, fallbackLocale, favorites, groups, - i18n, include, includeFavorites, labels, locale, + locales, ]); - const getFlagClass = (code?: string) => - [ - "flag-icon", - code && `flag-icon-${code.trim().toLowerCase()}`, - flagsPosition === "right" && "flag-icon-right", - flagsPosition === "right-edge" && "flag-icon-right-edge", - flagsStyle === "circle" && "flag-icon-rounded", - flagsStyle === "square" && "flag-icon-squared", - ] - .filter(Boolean) - .join(" "); - - const handleOnChange = (value: T | T[]) => { - if (!properties.onChange) return; - const result = Array.isArray(value) - ? (Array.from(new Set(value)) as T[]) - : value; - (properties.onChange as (value: T | T[]) => void)(result); - }; - - const renderOptionWithFlags = ( - option: Option & { - groupLabel?: string; + const handleOnChange = useCallback( + (value: T | T[]) => { + if (!properties.onChange) { + return; + } + + const result = Array.isArray(value) + ? (Array.from(new Set(value)) as T[]) + : value; + (properties.onChange as (value: T | T[]) => void)(result); }, - ) => { - const code = String(option.value); - - return ( -
- {flags && - (flagsPath ? ( - {option.label} - ) : ( - - ))} - {option.label} -
- ); - }; + [properties.onChange], + ); + + const handleRenderOption = useCallback( + (option: Option) => + renderCountryOption(option, { + flags, + flagsPath, + flagsPosition, + flagsStyle, + }), + [flags, flagsPath, flagsPosition, flagsStyle], + ); return (