From 3d809d5317582304675d3698a69e22f8d685b64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Sat, 24 Feb 2024 08:50:32 -0300 Subject: [PATCH] feat: add Display calendar settings (#4164) * feat: add Color calendar setting Closes #4067 * fix: fix wrong imports * feat: add Display calendar settings Closes #4068 * feat: add 12h/24h in Format option labels * fix tests * Fix * Fix --------- Co-authored-by: Charles Bochet --- package.json | 1 + ...tingsAccountsCalendarAccountsListCard.tsx} | 38 +- ...ettingsAccountsCalendarDisplaySettings.tsx | 47 ++ ...SettingsAccountsCalendarTimeZoneSelect.tsx | 25 + ...ettingsAccountsEmailsAccountsListCard.tsx} | 42 +- .../accounts/constants/ianaTimeZones.ts | 601 ++++++++++++++++++ .../constants/timeZoneSelectOptions.ts | 43 ++ .../settings/accounts/utils/detectTimeZone.ts | 6 + .../utils/findAvailableTimeZoneOption.ts | 10 + .../accounts/utils/formatTimeZoneLabel.ts | 29 + .../modules/ui/input/components/Select.tsx | 66 +- .../components/__stories__/Select.stories.tsx | 4 + .../layout/dropdown/components/Dropdown.tsx | 2 +- .../accounts/SettingsAccountsCalendars.tsx | 72 ++- .../accounts/SettingsAccountsEmails.tsx | 12 +- yarn.lock | 10 + 16 files changed, 926 insertions(+), 82 deletions(-) rename packages/twenty-front/src/modules/settings/accounts/components/{SettingsAccountsCalendarSettingsSection.tsx => SettingsAccountsCalendarAccountsListCard.tsx} (62%) create mode 100644 packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarDisplaySettings.tsx create mode 100644 packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarTimeZoneSelect.tsx rename packages/twenty-front/src/modules/settings/accounts/components/{SettingsAccountsEmailsSyncSection.tsx => SettingsAccountsEmailsAccountsListCard.tsx} (66%) create mode 100644 packages/twenty-front/src/modules/settings/accounts/constants/ianaTimeZones.ts create mode 100644 packages/twenty-front/src/modules/settings/accounts/constants/timeZoneSelectOptions.ts create mode 100644 packages/twenty-front/src/modules/settings/accounts/utils/detectTimeZone.ts create mode 100644 packages/twenty-front/src/modules/settings/accounts/utils/findAvailableTimeZoneOption.ts create mode 100644 packages/twenty-front/src/modules/settings/accounts/utils/formatTimeZoneLabel.ts diff --git a/package.json b/package.json index df96524779..048fce7c0d 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "danger-plugin-todos": "^1.3.1", "dataloader": "^2.2.2", "date-fns": "^2.30.0", + "date-fns-tz": "^2.0.0", "debounce": "^2.0.0", "deep-equal": "^2.2.2", "docusaurus-node-polyfills": "^1.0.0", diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarSettingsSection.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarAccountsListCard.tsx similarity index 62% rename from packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarSettingsSection.tsx rename to packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarAccountsListCard.tsx index b06954f3cf..5425976316 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarSettingsSection.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarAccountsListCard.tsx @@ -10,9 +10,7 @@ import { SettingsAccountsListCard } from '@/settings/accounts/components/Setting import { SettingsAccountsSynchronizationStatus } from '@/settings/accounts/components/SettingsAccountsSynchronizationStatus'; import { IconChevronRight } from '@/ui/display/icon'; import { IconGoogleCalendar } from '@/ui/display/icon/components/IconGoogleCalendar'; -import { H2Title } from '@/ui/display/typography/components/H2Title'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; -import { Section } from '@/ui/layout/section/components/Section'; import { mockedConnectedAccounts } from '~/testing/mock-data/accounts'; const StyledRowRightContainer = styled.div` @@ -21,7 +19,7 @@ const StyledRowRightContainer = styled.div` gap: ${({ theme }) => theme.spacing(1)}; `; -export const SettingsAccountsCalendarSettingsSection = () => { +export const SettingsAccountsCalendarAccountsListCard = () => { const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const navigate = useNavigate(); @@ -35,25 +33,19 @@ export const SettingsAccountsCalendarSettingsSection = () => { }); return ( -
- - - navigate(`/settings/accounts/calendars/${account.id}`) - } - RowIcon={IconGoogleCalendar} - RowRightComponent={({ account: _account }) => ( - - - - - )} - /> -
+ + navigate(`/settings/accounts/calendars/${account.id}`) + } + RowIcon={IconGoogleCalendar} + RowRightComponent={({ account: _account }) => ( + + + + + )} + /> ); }; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarDisplaySettings.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarDisplaySettings.tsx new file mode 100644 index 0000000000..880cf68c04 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarDisplaySettings.tsx @@ -0,0 +1,47 @@ +import { useState } from 'react'; +import styled from '@emotion/styled'; +import { formatInTimeZone } from 'date-fns-tz'; + +import { SettingsAccountsCalendarTimeZoneSelect } from '@/settings/accounts/components/SettingsAccountsCalendarTimeZoneSelect'; +import { detectTimeZone } from '@/settings/accounts/utils/detectTimeZone'; +import { Select } from '@/ui/input/components/Select'; + +const StyledContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(4)}; +`; + +export const SettingsAccountsCalendarDisplaySettings = () => { + // TODO: use the user's saved time zone. If undefined, default it with the user's detected time zone. + const [timeZone, setTimeZone] = useState(detectTimeZone()); + + // TODO: use the user's saved time format. + const [timeFormat, setTimeFormat] = useState<12 | 24>(24); + + return ( + + + +); diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsSyncSection.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsAccountsListCard.tsx similarity index 66% rename from packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsSyncSection.tsx rename to packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsAccountsListCard.tsx index e6d3cfa86d..2fc6154f1e 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsSyncSection.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsAccountsListCard.tsx @@ -12,8 +12,6 @@ import { SettingsAccountsListCard } from '@/settings/accounts/components/Setting import { SettingsAccountsSynchronizationStatus } from '@/settings/accounts/components/SettingsAccountsSynchronizationStatus'; import { IconChevronRight } from '@/ui/display/icon'; import { IconGmail } from '@/ui/display/icon/components/IconGmail'; -import { H2Title } from '@/ui/display/typography/components/H2Title'; -import { Section } from '@/ui/layout/section/components/Section'; const StyledRowRightContainer = styled.div` align-items: center; @@ -21,7 +19,7 @@ const StyledRowRightContainer = styled.div` gap: ${({ theme }) => theme.spacing(1)}; `; -export const SettingsAccountsEmailsSyncSection = () => { +export const SettingsAccountsEmailsAccountsListCard = () => { const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const navigate = useNavigate(); @@ -53,27 +51,21 @@ export const SettingsAccountsEmailsSyncSection = () => { ); return ( -
- - - navigate(`/settings/accounts/emails/${messageChannel.id}`) - } - RowIcon={IconGmail} - RowRightComponent={({ account: messageChannel }) => ( - - - - - )} - /> -
+ + navigate(`/settings/accounts/emails/${messageChannel.id}`) + } + RowIcon={IconGmail} + RowRightComponent={({ account: messageChannel }) => ( + + + + + )} + /> ); }; diff --git a/packages/twenty-front/src/modules/settings/accounts/constants/ianaTimeZones.ts b/packages/twenty-front/src/modules/settings/accounts/constants/ianaTimeZones.ts new file mode 100644 index 0000000000..2b5c179014 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/constants/ianaTimeZones.ts @@ -0,0 +1,601 @@ +/** + * Standard IANA time zones. + * @see https://www.iana.org/time-zones + */ +export const ianaTimeZones = [ + 'Africa/Abidjan', + 'Africa/Accra', + 'Africa/Addis_Ababa', + 'Africa/Algiers', + 'Africa/Asmara', + 'Africa/Asmera', + 'Africa/Bamako', + 'Africa/Bangui', + 'Africa/Banjul', + 'Africa/Bissau', + 'Africa/Blantyre', + 'Africa/Brazzaville', + 'Africa/Bujumbura', + 'Africa/Cairo', + 'Africa/Casablanca', + 'Africa/Ceuta', + 'Africa/Conakry', + 'Africa/Dakar', + 'Africa/Dar_es_Salaam', + 'Africa/Djibouti', + 'Africa/Douala', + 'Africa/El_Aaiun', + 'Africa/Freetown', + 'Africa/Gaborone', + 'Africa/Harare', + 'Africa/Johannesburg', + 'Africa/Juba', + 'Africa/Kampala', + 'Africa/Khartoum', + 'Africa/Kigali', + 'Africa/Kinshasa', + 'Africa/Lagos', + 'Africa/Libreville', + 'Africa/Lome', + 'Africa/Luanda', + 'Africa/Lubumbashi', + 'Africa/Lusaka', + 'Africa/Malabo', + 'Africa/Maputo', + 'Africa/Maseru', + 'Africa/Mbabane', + 'Africa/Mogadishu', + 'Africa/Monrovia', + 'Africa/Nairobi', + 'Africa/Ndjamena', + 'Africa/Niamey', + 'Africa/Nouakchott', + 'Africa/Ouagadougou', + 'Africa/Porto-Novo', + 'Africa/Sao_Tome', + 'Africa/Timbuktu', + 'Africa/Tripoli', + 'Africa/Tunis', + 'Africa/Windhoek', + 'America/Adak', + 'America/Anchorage', + 'America/Anguilla', + 'America/Antigua', + 'America/Araguaina', + 'America/Argentina/Buenos_Aires', + 'America/Argentina/Catamarca', + 'America/Argentina/ComodRivadavia', + 'America/Argentina/Cordoba', + 'America/Argentina/Jujuy', + 'America/Argentina/La_Rioja', + 'America/Argentina/Mendoza', + 'America/Argentina/Rio_Gallegos', + 'America/Argentina/Salta', + 'America/Argentina/San_Juan', + 'America/Argentina/San_Luis', + 'America/Argentina/Tucuman', + 'America/Argentina/Ushuaia', + 'America/Aruba', + 'America/Asuncion', + 'America/Atikokan', + 'America/Atka', + 'America/Bahia', + 'America/Bahia_Banderas', + 'America/Barbados', + 'America/Belem', + 'America/Belize', + 'America/Blanc-Sablon', + 'America/Boa_Vista', + 'America/Bogota', + 'America/Boise', + 'America/Buenos_Aires', + 'America/Cambridge_Bay', + 'America/Campo_Grande', + 'America/Cancun', + 'America/Caracas', + 'America/Catamarca', + 'America/Cayenne', + 'America/Cayman', + 'America/Chicago', + 'America/Chihuahua', + 'America/Coral_Harbour', + 'America/Cordoba', + 'America/Costa_Rica', + 'America/Creston', + 'America/Cuiaba', + 'America/Curacao', + 'America/Danmarkshavn', + 'America/Dawson', + 'America/Dawson_Creek', + 'America/Denver', + 'America/Detroit', + 'America/Dominica', + 'America/Edmonton', + 'America/Eirunepe', + 'America/El_Salvador', + 'America/Ensenada', + 'America/Fort_Nelson', + 'America/Fort_Wayne', + 'America/Fortaleza', + 'America/Glace_Bay', + 'America/Godthab', + 'America/Goose_Bay', + 'America/Grand_Turk', + 'America/Grenada', + 'America/Guadeloupe', + 'America/Guatemala', + 'America/Guayaquil', + 'America/Guyana', + 'America/Halifax', + 'America/Havana', + 'America/Hermosillo', + 'America/Indiana/Indianapolis', + 'America/Indiana/Knox', + 'America/Indiana/Marengo', + 'America/Indiana/Petersburg', + 'America/Indiana/Tell_City', + 'America/Indiana/Vevay', + 'America/Indiana/Vincennes', + 'America/Indiana/Winamac', + 'America/Indianapolis', + 'America/Inuvik', + 'America/Iqaluit', + 'America/Jamaica', + 'America/Jujuy', + 'America/Juneau', + 'America/Kentucky/Louisville', + 'America/Kentucky/Monticello', + 'America/Knox_IN', + 'America/Kralendijk', + 'America/La_Paz', + 'America/Lima', + 'America/Los_Angeles', + 'America/Louisville', + 'America/Lower_Princes', + 'America/Maceio', + 'America/Managua', + 'America/Manaus', + 'America/Marigot', + 'America/Martinique', + 'America/Matamoros', + 'America/Mazatlan', + 'America/Mendoza', + 'America/Menominee', + 'America/Merida', + 'America/Metlakatla', + 'America/Mexico_City', + 'America/Miquelon', + 'America/Moncton', + 'America/Monterrey', + 'America/Montevideo', + 'America/Montreal', + 'America/Montserrat', + 'America/Nassau', + 'America/New_York', + 'America/Nipigon', + 'America/Nome', + 'America/Noronha', + 'America/North_Dakota/Beulah', + 'America/North_Dakota/Center', + 'America/North_Dakota/New_Salem', + 'America/Nuuk', + 'America/Ojinaga', + 'America/Panama', + 'America/Pangnirtung', + 'America/Paramaribo', + 'America/Phoenix', + 'America/Port_of_Spain', + 'America/Port-au-Prince', + 'America/Porto_Acre', + 'America/Porto_Velho', + 'America/Puerto_Rico', + 'America/Punta_Arenas', + 'America/Rainy_River', + 'America/Rankin_Inlet', + 'America/Recife', + 'America/Regina', + 'America/Resolute', + 'America/Rio_Branco', + 'America/Rosario', + 'America/Santa_Isabel', + 'America/Santarem', + 'America/Santiago', + 'America/Santo_Domingo', + 'America/Sao_Paulo', + 'America/Scoresbysund', + 'America/Shiprock', + 'America/Sitka', + 'America/St_Barthelemy', + 'America/St_Johns', + 'America/St_Kitts', + 'America/St_Lucia', + 'America/St_Thomas', + 'America/St_Vincent', + 'America/Swift_Current', + 'America/Tegucigalpa', + 'America/Thule', + 'America/Thunder_Bay', + 'America/Tijuana', + 'America/Toronto', + 'America/Tortola', + 'America/Vancouver', + 'America/Virgin', + 'America/Whitehorse', + 'America/Winnipeg', + 'America/Yakutat', + 'America/Yellowknife', + 'Antarctica/Casey', + 'Antarctica/Davis', + 'Antarctica/DumontDUrville', + 'Antarctica/Macquarie', + 'Antarctica/Mawson', + 'Antarctica/McMurdo', + 'Antarctica/Palmer', + 'Antarctica/Rothera', + 'Antarctica/South_Pole', + 'Antarctica/Syowa', + 'Antarctica/Troll', + 'Antarctica/Vostok', + 'Arctic/Longyearbyen', + 'Asia/Aden', + 'Asia/Almaty', + 'Asia/Amman', + 'Asia/Anadyr', + 'Asia/Aqtau', + 'Asia/Aqtobe', + 'Asia/Ashgabat', + 'Asia/Ashkhabad', + 'Asia/Atyrau', + 'Asia/Baghdad', + 'Asia/Bahrain', + 'Asia/Baku', + 'Asia/Bangkok', + 'Asia/Barnaul', + 'Asia/Beirut', + 'Asia/Bishkek', + 'Asia/Brunei', + 'Asia/Calcutta', + 'Asia/Chita', + 'Asia/Choibalsan', + 'Asia/Chongqing', + 'Asia/Chungking', + 'Asia/Colombo', + 'Asia/Dacca', + 'Asia/Damascus', + 'Asia/Dhaka', + 'Asia/Dili', + 'Asia/Dubai', + 'Asia/Dushanbe', + 'Asia/Famagusta', + 'Asia/Gaza', + 'Asia/Harbin', + 'Asia/Hebron', + 'Asia/Ho_Chi_Minh', + 'Asia/Hong_Kong', + 'Asia/Hovd', + 'Asia/Irkutsk', + 'Asia/Istanbul', + 'Asia/Jakarta', + 'Asia/Jayapura', + 'Asia/Jerusalem', + 'Asia/Kabul', + 'Asia/Kamchatka', + 'Asia/Karachi', + 'Asia/Kashgar', + 'Asia/Kathmandu', + 'Asia/Katmandu', + 'Asia/Khandyga', + 'Asia/Kolkata', + 'Asia/Krasnoyarsk', + 'Asia/Kuala_Lumpur', + 'Asia/Kuching', + 'Asia/Kuwait', + 'Asia/Macao', + 'Asia/Macau', + 'Asia/Magadan', + 'Asia/Makassar', + 'Asia/Manila', + 'Asia/Muscat', + 'Asia/Nicosia', + 'Asia/Novokuznetsk', + 'Asia/Novosibirsk', + 'Asia/Omsk', + 'Asia/Oral', + 'Asia/Phnom_Penh', + 'Asia/Pontianak', + 'Asia/Pyongyang', + 'Asia/Qatar', + 'Asia/Qostanay', + 'Asia/Qyzylorda', + 'Asia/Rangoon', + 'Asia/Riyadh', + 'Asia/Saigon', + 'Asia/Sakhalin', + 'Asia/Samarkand', + 'Asia/Seoul', + 'Asia/Shanghai', + 'Asia/Singapore', + 'Asia/Srednekolymsk', + 'Asia/Taipei', + 'Asia/Tashkent', + 'Asia/Tbilisi', + 'Asia/Tehran', + 'Asia/Tel_Aviv', + 'Asia/Thimbu', + 'Asia/Thimphu', + 'Asia/Tokyo', + 'Asia/Tomsk', + 'Asia/Ujung_Pandang', + 'Asia/Ulaanbaatar', + 'Asia/Ulan_Bator', + 'Asia/Urumqi', + 'Asia/Ust-Nera', + 'Asia/Vientiane', + 'Asia/Vladivostok', + 'Asia/Yakutsk', + 'Asia/Yangon', + 'Asia/Yekaterinburg', + 'Asia/Yerevan', + 'Atlantic/Azores', + 'Atlantic/Bermuda', + 'Atlantic/Canary', + 'Atlantic/Cape_Verde', + 'Atlantic/Faeroe', + 'Atlantic/Faroe', + 'Atlantic/Jan_Mayen', + 'Atlantic/Madeira', + 'Atlantic/Reykjavik', + 'Atlantic/South_Georgia', + 'Atlantic/St_Helena', + 'Atlantic/Stanley', + 'Australia/ACT', + 'Australia/Adelaide', + 'Australia/Brisbane', + 'Australia/Broken_Hill', + 'Australia/Canberra', + 'Australia/Currie', + 'Australia/Darwin', + 'Australia/Eucla', + 'Australia/Hobart', + 'Australia/LHI', + 'Australia/Lindeman', + 'Australia/Lord_Howe', + 'Australia/Melbourne', + 'Australia/North', + 'Australia/NSW', + 'Australia/Perth', + 'Australia/Queensland', + 'Australia/South', + 'Australia/Sydney', + 'Australia/Tasmania', + 'Australia/Victoria', + 'Australia/West', + 'Australia/Yancowinna', + 'Brazil/Acre', + 'Brazil/DeNoronha', + 'Brazil/East', + 'Brazil/West', + 'Canada/Atlantic', + 'Canada/Central', + 'Canada/Eastern', + 'Canada/Mountain', + 'Canada/Newfoundland', + 'Canada/Pacific', + 'Canada/Saskatchewan', + 'Canada/Yukon', + 'CET', + 'Chile/Continental', + 'Chile/EasterIsland', + 'CST6CDT', + 'Cuba', + 'EET', + 'Egypt', + 'Eire', + 'EST', + 'EST5EDT', + 'Etc/GMT', + 'Etc/GMT-0', + 'Etc/GMT-1', + 'Etc/GMT-10', + 'Etc/GMT-11', + 'Etc/GMT-12', + 'Etc/GMT-13', + 'Etc/GMT-14', + 'Etc/GMT-2', + 'Etc/GMT-3', + 'Etc/GMT-4', + 'Etc/GMT-5', + 'Etc/GMT-6', + 'Etc/GMT-7', + 'Etc/GMT-8', + 'Etc/GMT-9', + 'Etc/GMT+0', + 'Etc/GMT+1', + 'Etc/GMT+10', + 'Etc/GMT+11', + 'Etc/GMT+12', + 'Etc/GMT+2', + 'Etc/GMT+3', + 'Etc/GMT+4', + 'Etc/GMT+5', + 'Etc/GMT+6', + 'Etc/GMT+7', + 'Etc/GMT+8', + 'Etc/GMT+9', + 'Etc/GMT0', + 'Etc/Greenwich', + 'Etc/UCT', + 'Etc/Universal', + 'Etc/UTC', + 'Etc/Zulu', + 'Europe/Amsterdam', + 'Europe/Andorra', + 'Europe/Astrakhan', + 'Europe/Athens', + 'Europe/Belfast', + 'Europe/Belgrade', + 'Europe/Berlin', + 'Europe/Bratislava', + 'Europe/Brussels', + 'Europe/Bucharest', + 'Europe/Budapest', + 'Europe/Busingen', + 'Europe/Chisinau', + 'Europe/Copenhagen', + 'Europe/Dublin', + 'Europe/Gibraltar', + 'Europe/Guernsey', + 'Europe/Helsinki', + 'Europe/Isle_of_Man', + 'Europe/Istanbul', + 'Europe/Jersey', + 'Europe/Kaliningrad', + 'Europe/Kiev', + 'Europe/Kirov', + 'Europe/Kyiv', + 'Europe/Lisbon', + 'Europe/Ljubljana', + 'Europe/London', + 'Europe/Luxembourg', + 'Europe/Madrid', + 'Europe/Malta', + 'Europe/Mariehamn', + 'Europe/Minsk', + 'Europe/Monaco', + 'Europe/Moscow', + 'Europe/Nicosia', + 'Europe/Oslo', + 'Europe/Paris', + 'Europe/Podgorica', + 'Europe/Prague', + 'Europe/Riga', + 'Europe/Rome', + 'Europe/Samara', + 'Europe/San_Marino', + 'Europe/Sarajevo', + 'Europe/Saratov', + 'Europe/Simferopol', + 'Europe/Skopje', + 'Europe/Sofia', + 'Europe/Stockholm', + 'Europe/Tallinn', + 'Europe/Tirane', + 'Europe/Tiraspol', + 'Europe/Ulyanovsk', + 'Europe/Uzhgorod', + 'Europe/Vaduz', + 'Europe/Vatican', + 'Europe/Vienna', + 'Europe/Vilnius', + 'Europe/Volgograd', + 'Europe/Warsaw', + 'Europe/Zagreb', + 'Europe/Zaporozhye', + 'Europe/Zurich', + 'GB', + 'GB-Eire', + 'GMT', + 'GMT-0', + 'GMT+0', + 'GMT0', + 'Greenwich', + 'Hongkong', + 'HST', + 'Iceland', + 'Indian/Antananarivo', + 'Indian/Chagos', + 'Indian/Christmas', + 'Indian/Cocos', + 'Indian/Comoro', + 'Indian/Kerguelen', + 'Indian/Mahe', + 'Indian/Maldives', + 'Indian/Mauritius', + 'Indian/Mayotte', + 'Indian/Reunion', + 'Iran', + 'Israel', + 'Jamaica', + 'Japan', + 'Kwajalein', + 'Libya', + 'MET', + 'Mexico/BajaNorte', + 'Mexico/BajaSur', + 'Mexico/General', + 'MST', + 'MST7MDT', + 'Navajo', + 'NZ', + 'NZ-CHAT', + 'Pacific/Apia', + 'Pacific/Auckland', + 'Pacific/Bougainville', + 'Pacific/Chatham', + 'Pacific/Chuuk', + 'Pacific/Easter', + 'Pacific/Efate', + 'Pacific/Enderbury', + 'Pacific/Fakaofo', + 'Pacific/Fiji', + 'Pacific/Funafuti', + 'Pacific/Galapagos', + 'Pacific/Gambier', + 'Pacific/Guadalcanal', + 'Pacific/Guam', + 'Pacific/Honolulu', + 'Pacific/Johnston', + 'Pacific/Kanton', + 'Pacific/Kiritimati', + 'Pacific/Kosrae', + 'Pacific/Kwajalein', + 'Pacific/Majuro', + 'Pacific/Marquesas', + 'Pacific/Midway', + 'Pacific/Nauru', + 'Pacific/Niue', + 'Pacific/Norfolk', + 'Pacific/Noumea', + 'Pacific/Pago_Pago', + 'Pacific/Palau', + 'Pacific/Pitcairn', + 'Pacific/Pohnpei', + 'Pacific/Ponape', + 'Pacific/Port_Moresby', + 'Pacific/Rarotonga', + 'Pacific/Saipan', + 'Pacific/Samoa', + 'Pacific/Tahiti', + 'Pacific/Tarawa', + 'Pacific/Tongatapu', + 'Pacific/Truk', + 'Pacific/Wake', + 'Pacific/Wallis', + 'Pacific/Yap', + 'Poland', + 'Portugal', + 'PRC', + 'PST8PDT', + 'ROC', + 'ROK', + 'Singapore', + 'Turkey', + 'UCT', + 'Universal', + 'US/Alaska', + 'US/Aleutian', + 'US/Arizona', + 'US/Central', + 'US/East-Indiana', + 'US/Eastern', + 'US/Hawaii', + 'US/Indiana-Starke', + 'US/Michigan', + 'US/Mountain', + 'US/Pacific', + 'US/Samoa', + 'UTC', + 'W-SU', + 'WET', + 'Zulu', +]; diff --git a/packages/twenty-front/src/modules/settings/accounts/constants/timeZoneSelectOptions.ts b/packages/twenty-front/src/modules/settings/accounts/constants/timeZoneSelectOptions.ts new file mode 100644 index 0000000000..ffb3d5e67a --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/constants/timeZoneSelectOptions.ts @@ -0,0 +1,43 @@ +import { getTimezoneOffset } from 'date-fns-tz'; + +import { ianaTimeZones } from '@/settings/accounts/constants/ianaTimeZones'; +import { formatTimeZoneLabel } from '@/settings/accounts/utils/formatTimeZoneLabel'; +import { SelectOption } from '@/ui/input/components/Select'; + +export const availableTimeZoneOptionsByLabel = ianaTimeZones.reduce< + Record> +>((result, ianaTimeZone) => { + const timeZoneLabel = formatTimeZoneLabel(ianaTimeZone); + + // Remove the '(GMT±00:00) ' prefix from the label. + const timeZoneName = timeZoneLabel.slice(11); + + // Skip time zones with GMT, UTC, or UCT in their name, + // and duplicates. + if ( + timeZoneName.includes('GMT') || + timeZoneName.includes('UTC') || + timeZoneName.includes('UCT') || + timeZoneLabel in result + ) { + return result; + } + + return { + ...result, + [timeZoneLabel]: { label: timeZoneLabel, value: ianaTimeZone }, + }; +}, {}); + +export const availableTimeZoneOptions = Object.values( + availableTimeZoneOptionsByLabel, +).sort((optionA, optionB) => { + const difference = + getTimezoneOffset(optionA.value) - getTimezoneOffset(optionB.value); + + return difference === 0 + ? // Sort alphabetically if the time zone offsets are the same. + optionA.label.localeCompare(optionB.label) + : // Sort by time zone offset if different. + difference; +}); diff --git a/packages/twenty-front/src/modules/settings/accounts/utils/detectTimeZone.ts b/packages/twenty-front/src/modules/settings/accounts/utils/detectTimeZone.ts new file mode 100644 index 0000000000..95bb9ccc62 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/utils/detectTimeZone.ts @@ -0,0 +1,6 @@ +/** + * Detects the user's time zone. + * @returns a IANA time zone + */ +export const detectTimeZone = () => + Intl.DateTimeFormat().resolvedOptions().timeZone; diff --git a/packages/twenty-front/src/modules/settings/accounts/utils/findAvailableTimeZoneOption.ts b/packages/twenty-front/src/modules/settings/accounts/utils/findAvailableTimeZoneOption.ts new file mode 100644 index 0000000000..8f5795cfe5 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/utils/findAvailableTimeZoneOption.ts @@ -0,0 +1,10 @@ +import { availableTimeZoneOptionsByLabel } from '@/settings/accounts/constants/timeZoneSelectOptions'; +import { formatTimeZoneLabel } from '@/settings/accounts/utils/formatTimeZoneLabel'; + +/** + * Finds the matching available IANA time zone select option from a given IANA time zone. + * @param value the IANA time zone to match + * @returns the matching available IANA time zone select option or undefined + */ +export const findAvailableTimeZoneOption = (value: string) => + availableTimeZoneOptionsByLabel[formatTimeZoneLabel(value)]; diff --git a/packages/twenty-front/src/modules/settings/accounts/utils/formatTimeZoneLabel.ts b/packages/twenty-front/src/modules/settings/accounts/utils/formatTimeZoneLabel.ts new file mode 100644 index 0000000000..e5f1ac97e0 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/utils/formatTimeZoneLabel.ts @@ -0,0 +1,29 @@ +import defaultLocale from 'date-fns/locale/en-US'; +import { formatInTimeZone } from 'date-fns-tz'; + +/** + * Formats a IANA time zone to a select option label. + * @param ianaTimeZone IANA time zone + * @returns Formatted label + * @example 'Europe/Paris' => '(GMT+01:00) Central European Time - Paris' + */ +export const formatTimeZoneLabel = (ianaTimeZone: string) => { + const timeZoneWithGmtOffset = formatInTimeZone( + Date.now(), + ianaTimeZone, + `(OOOO) zzzz`, + { locale: defaultLocale }, + ); + const ianaTimeZoneParts = ianaTimeZone.split('/'); + const location = + ianaTimeZoneParts.length > 1 + ? ianaTimeZoneParts.slice(-1)[0].replaceAll('_', ' ') + : undefined; + + const timeZoneLabel = + !location || timeZoneWithGmtOffset.includes(location) + ? timeZoneWithGmtOffset + : [timeZoneWithGmtOffset, location].join(' - '); + + return timeZoneLabel; +}; diff --git a/packages/twenty-front/src/modules/ui/input/components/Select.tsx b/packages/twenty-front/src/modules/ui/input/components/Select.tsx index 89ad89eb43..531f00f866 100644 --- a/packages/twenty-front/src/modules/ui/input/components/Select.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/Select.tsx @@ -1,3 +1,4 @@ +import { useMemo, useState } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; @@ -5,20 +6,30 @@ import { IconChevronDown } from '@/ui/display/icon'; import { IconComponent } from '@/ui/display/icon/types/IconComponent'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { SelectHotkeyScope } from '../types/SelectHotkeyScope'; +export type SelectOption = { + value: Value; + label: string; + Icon?: IconComponent; +}; + export type SelectProps = { className?: string; disabled?: boolean; dropdownId: string; + dropdownWidth?: `${string}px` | 'auto' | number; fullWidth?: boolean; label?: string; onChange?: (value: Value) => void; - options: { value: Value; label: string; Icon?: IconComponent }[]; + options: SelectOption[]; value?: Value; + withSearchInput?: boolean; }; const StyledControlContainer = styled.div<{ @@ -63,15 +74,28 @@ export const Select = ({ className, disabled, dropdownId, + dropdownWidth = 176, fullWidth, label, onChange, options, value, + withSearchInput, }: SelectProps) => { const theme = useTheme(); + const [searchInputValue, setSearchInputValue] = useState(''); + const selectedOption = options.find(({ value: key }) => key === value) || options[0]; + const filteredOptions = useMemo( + () => + searchInputValue + ? options.filter(({ label }) => + label.toLowerCase().includes(searchInputValue.toLowerCase()), + ) + : options, + [options, searchInputValue], + ); const { closeDropdown } = useDropdown(dropdownId); @@ -101,23 +125,37 @@ export const Select = ({ {!!label && {label}} - {options.map((option) => ( - { - onChange?.(option.value); - closeDropdown(); - }} + <> + {!!withSearchInput && ( + setSearchInputValue(event.target.value)} /> - ))} - + )} + {!!withSearchInput && !!filteredOptions.length && ( + + )} + {!!filteredOptions.length && ( + + {filteredOptions.map((option) => ( + { + onChange?.(option.value); + closeDropdown(); + }} + /> + ))} + + )} + } dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }} /> diff --git a/packages/twenty-front/src/modules/ui/input/components/__stories__/Select.stories.tsx b/packages/twenty-front/src/modules/ui/input/components/__stories__/Select.stories.tsx index 7dde7b4493..4c977c08c1 100644 --- a/packages/twenty-front/src/modules/ui/input/components/__stories__/Select.stories.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/__stories__/Select.stories.tsx @@ -53,3 +53,7 @@ export const Open: Story = { export const Disabled: Story = { args: { disabled: true }, }; + +export const WithSearch: Story = { + args: { withSearchInput: true }, +}; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx index 1169b1587a..dac09885fc 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx @@ -32,7 +32,7 @@ type DropdownProps = { dropdownHotkeyScope: HotkeyScope; dropdownId: string; dropdownPlacement?: Placement; - dropdownMenuWidth?: number; + dropdownMenuWidth?: `${string}px` | 'auto' | number; dropdownOffset?: { x?: number; y?: number }; onClickOutside?: () => void; onClose?: () => void; diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx index d77a9d6b98..145ff31008 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx @@ -1,24 +1,62 @@ -import { SettingsAccountsCalendarSettingsSection } from '@/settings/accounts/components/SettingsAccountsCalendarSettingsSection'; +import { useRecoilValue } from 'recoil'; + +import { ConnectedAccount } from '@/accounts/types/ConnectedAccount'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { SettingsAccountsCalendarAccountsListCard } from '@/settings/accounts/components/SettingsAccountsCalendarAccountsListCard'; +import { SettingsAccountsCalendarDisplaySettings } from '@/settings/accounts/components/SettingsAccountsCalendarDisplaySettings'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { IconSettings } from '@/ui/display/icon'; +import { H2Title } from '@/ui/display/typography/components/H2Title'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { Section } from '@/ui/layout/section/components/Section'; import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; +import { mockedConnectedAccounts } from '~/testing/mock-data/accounts'; -export const SettingsAccountsCalendars = () => ( - - - - - - -); +export const SettingsAccountsCalendars = () => { + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const { records: _accounts } = useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.ConnectedAccount, + filter: { + accountOwnerId: { + eq: currentWorkspaceMember?.id, + }, + }, + }); + + return ( + + + +
+ + +
+ {/* TODO: retrieve connected accounts data from back-end when the Calendar feature is ready. */} + {!!mockedConnectedAccounts.length && ( +
+ + +
+ )} +
+
+ ); +}; diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx index f46dc331ef..6aececd6a9 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx @@ -1,7 +1,9 @@ -import { SettingsAccountsEmailsSyncSection } from '@/settings/accounts/components/SettingsAccountsEmailsSyncSection'; +import { SettingsAccountsEmailsAccountsListCard } from '@/settings/accounts/components/SettingsAccountsEmailsAccountsListCard'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { IconSettings } from '@/ui/display/icon'; +import { H2Title } from '@/ui/display/typography/components/H2Title'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { Section } from '@/ui/layout/section/components/Section'; import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; export const SettingsAccountsEmails = () => ( @@ -13,7 +15,13 @@ export const SettingsAccountsEmails = () => ( { children: 'Emails' }, ]} /> - +
+ + +
); diff --git a/yarn.lock b/yarn.lock index 11f6746a38..035c4f388a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22702,6 +22702,15 @@ __metadata: languageName: node linkType: hard +"date-fns-tz@npm:^2.0.0": + version: 2.0.0 + resolution: "date-fns-tz@npm:2.0.0" + peerDependencies: + date-fns: ">=2.0.0" + checksum: 31cacb83c675ef8c2cf31d21c298ab6d74c5e975412dd804664d031ac0f2f03ed5bfb2a950fa15a321bbbcb90b833c300823fbceba0133680065a71894cc1170 + languageName: node + linkType: hard + "date-fns@npm:^2.0.1, date-fns@npm:^2.30.0": version: 2.30.0 resolution: "date-fns@npm:2.30.0" @@ -44303,6 +44312,7 @@ __metadata: danger-plugin-todos: "npm:^1.3.1" dataloader: "npm:^2.2.2" date-fns: "npm:^2.30.0" + date-fns-tz: "npm:^2.0.0" debounce: "npm:^2.0.0" deep-equal: "npm:^2.2.2" docusaurus-node-polyfills: "npm:^1.0.0"