From b28e38ae294db391361c7b684bf7bfbe18b55b87 Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Thu, 27 Jul 2023 15:38:35 +0200 Subject: [PATCH 01/20] Use native URL class for URL validation --- .../steps/AdditionalInformationStep/ContactInfoStep.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx index 18817dbb5..d6592c46a 100644 --- a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx +++ b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx @@ -44,9 +44,16 @@ const isValidEmail = (email: string) => { typeof email === 'undefined' || email === '' || EMAIL_REGEX.test(email) ); }; + const isValidUrl = (url: string) => { - return typeof url === 'undefined' || url === '' || URL_REGEX.test(url); + try { + new URL(url); + return true; + } catch (e) { + return false; + } }; + const isValidPhone = (phone: string) => { return ( typeof phone === 'undefined' || phone === '' || PHONE_REGEX.test(phone) From 6c507b24616b64577659387fa5f1aafa4e7acfb4 Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Thu, 27 Jul 2023 15:50:12 +0200 Subject: [PATCH 02/20] Extract information validation utilities to avoid cyclomatic dependency ContactInfoStep -> AdditionalInformationStep -> BookingInfoStep -> ContactInfoStep --- .../BookingInfoStep.tsx | 3 +- .../ContactInfoStep.test.js | 0 .../ContactInfoStep.tsx | 36 ++----------------- src/pages/steps/LocationStep.tsx | 2 +- src/utils/isValidInfo.ts | 34 ++++++++++++++++++ 5 files changed, 39 insertions(+), 36 deletions(-) create mode 100644 src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js create mode 100644 src/utils/isValidInfo.ts diff --git a/src/pages/steps/AdditionalInformationStep/BookingInfoStep.tsx b/src/pages/steps/AdditionalInformationStep/BookingInfoStep.tsx index 0b1549c92..3afef0cac 100644 --- a/src/pages/steps/AdditionalInformationStep/BookingInfoStep.tsx +++ b/src/pages/steps/AdditionalInformationStep/BookingInfoStep.tsx @@ -24,7 +24,8 @@ import { formatDateToISO } from '@/utils/formatDateToISO'; import { prefixUrlWithHttps } from '@/utils/url'; import { TabContentProps, ValidationStatus } from './AdditionalInformationStep'; -import { isValidEmail, isValidPhone, isValidUrl } from './ContactInfoStep'; + +import { isValidEmail, isValidPhone, isValidUrl } from '@/utils/isValidInfo'; const schema = yup .object({ diff --git a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx index d6592c46a..0cad3b7bd 100644 --- a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx +++ b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx @@ -16,6 +16,7 @@ import { getStackProps, Stack, StackProps } from '@/ui/Stack'; import { prefixUrlWithHttps } from '@/utils/url'; import { TabContentProps, ValidationStatus } from './AdditionalInformationStep'; +import { isValidInfo } from '@/utils/isValidInfo'; const ContactInfoTypes = { EMAIL: 'email', @@ -34,39 +35,6 @@ type NewContactInfo = { value: string; }; -const EMAIL_REGEX: RegExp = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$/; -const URL_REGEX: RegExp = - /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?!&\/=]*)$/; -const PHONE_REGEX: RegExp = /^[0-9\/\-_.+ ]{0,15}$/; - -const isValidEmail = (email: string) => { - return ( - typeof email === 'undefined' || email === '' || EMAIL_REGEX.test(email) - ); -}; - -const isValidUrl = (url: string) => { - try { - new URL(url); - return true; - } catch (e) { - return false; - } -}; - -const isValidPhone = (phone: string) => { - return ( - typeof phone === 'undefined' || phone === '' || PHONE_REGEX.test(phone) - ); -}; - -const isValidInfo = (type: string, value: string): boolean => { - if (value === '') return true; - if (type === 'email') return isValidEmail(value); - if (type === 'url') return isValidUrl(value); - if (type === 'phone') return isValidPhone(value); -}; - type Props = StackProps & TabContentProps & { isOrganizer?: boolean; @@ -328,5 +296,5 @@ ContactInfoStep.defaultProps = { isOrganizer: false, }; -export { ContactInfoStep, isValidEmail, isValidPhone, isValidUrl }; +export { ContactInfoStep }; export type { ContactInfo }; diff --git a/src/pages/steps/LocationStep.tsx b/src/pages/steps/LocationStep.tsx index 38b8258e4..0fa7d122d 100644 --- a/src/pages/steps/LocationStep.tsx +++ b/src/pages/steps/LocationStep.tsx @@ -46,7 +46,6 @@ import { prefixUrlWithHttps } from '@/utils/url'; import { CityPicker } from '../CityPicker'; import { Features, NewFeatureTooltip } from '../NewFeatureTooltip'; -import { isValidUrl } from './AdditionalInformationStep/ContactInfoStep'; import { CountryPicker } from './CountryPicker'; import { UseEditArguments } from './hooks/useEditField'; import { PlaceStep } from './PlaceStep'; @@ -56,6 +55,7 @@ import { StepProps, StepsConfiguration, } from './Steps'; +import { isValidUrl } from '@/utils/isValidInfo'; const GERMAN_ZIP_REGEX: RegExp = /\b\d{5}\b/; const DUTCH_ZIP_REGEX: RegExp = /^\d{4}([A-Za-z0-9]{2})?$/; diff --git a/src/utils/isValidInfo.ts b/src/utils/isValidInfo.ts new file mode 100644 index 000000000..f05fe5d6c --- /dev/null +++ b/src/utils/isValidInfo.ts @@ -0,0 +1,34 @@ +const EMAIL_REGEX: RegExp = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$/; +const URL_REGEX: RegExp = + /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?!&\/=]*)$/; +const PHONE_REGEX: RegExp = /^[0-9\/\-_.+ ]{0,15}$/; + +const isValidEmail = (email: string) => { + return ( + typeof email === 'undefined' || email === '' || EMAIL_REGEX.test(email) + ); +}; + +const isValidUrl = (url: string) => { + try { + new URL(url); + return true; + } catch (e) { + return false; + } +}; + +const isValidPhone = (phone: string) => { + return ( + typeof phone === 'undefined' || phone === '' || PHONE_REGEX.test(phone) + ); +}; + +const isValidInfo = (type: string, value: string): boolean => { + if (value === '') return true; + if (type === 'email') return isValidEmail(value); + if (type === 'url') return isValidUrl(value); + if (type === 'phone') return isValidPhone(value); +}; + +export { isValidEmail, isValidInfo, isValidPhone, isValidUrl }; From ec43e2e22bd6b50d119279c1fbcf5796986a83bc Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Thu, 27 Jul 2023 15:54:14 +0200 Subject: [PATCH 03/20] Add small test to ensure ticket issue is solved --- .../ContactInfoStep.test.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js index e69de29bb..676616b46 100644 --- a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js +++ b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js @@ -0,0 +1,14 @@ +import { isValidUrl } from '@/utils/isValidInfo'; + +describe('isValidUrl', () => { + const tests = { + goobar: false, + 'goobar.com': false, + 'http://goobar.com': true, + 'https://speeltuin.vlaanderen/speeltuinen': true, + }; + + test.each(Object.entries(tests))('can check if url %p is %p', (url) => + expect(isValidUrl(url)).toBe(tests[url]), + ); +}); From 0fce3f76333742d28c41226f0c49496da89c4a44 Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Thu, 27 Jul 2023 15:54:57 +0200 Subject: [PATCH 04/20] Linting --- src/pages/steps/AdditionalInformationStep/BookingInfoStep.tsx | 3 +-- src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx | 2 +- src/pages/steps/LocationStep.tsx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/steps/AdditionalInformationStep/BookingInfoStep.tsx b/src/pages/steps/AdditionalInformationStep/BookingInfoStep.tsx index 3afef0cac..243f3891e 100644 --- a/src/pages/steps/AdditionalInformationStep/BookingInfoStep.tsx +++ b/src/pages/steps/AdditionalInformationStep/BookingInfoStep.tsx @@ -21,12 +21,11 @@ import { Text } from '@/ui/Text'; import { getGlobalBorderRadius, getValueFromTheme } from '@/ui/theme'; import { Title } from '@/ui/Title'; import { formatDateToISO } from '@/utils/formatDateToISO'; +import { isValidEmail, isValidPhone, isValidUrl } from '@/utils/isValidInfo'; import { prefixUrlWithHttps } from '@/utils/url'; import { TabContentProps, ValidationStatus } from './AdditionalInformationStep'; -import { isValidEmail, isValidPhone, isValidUrl } from '@/utils/isValidInfo'; - const schema = yup .object({ availabilityEnds: yup.string(), diff --git a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx index 0cad3b7bd..a3261c298 100644 --- a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx +++ b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx @@ -13,10 +13,10 @@ import { Inline } from '@/ui/Inline'; import { Input } from '@/ui/Input'; import { Select } from '@/ui/Select'; import { getStackProps, Stack, StackProps } from '@/ui/Stack'; +import { isValidInfo } from '@/utils/isValidInfo'; import { prefixUrlWithHttps } from '@/utils/url'; import { TabContentProps, ValidationStatus } from './AdditionalInformationStep'; -import { isValidInfo } from '@/utils/isValidInfo'; const ContactInfoTypes = { EMAIL: 'email', diff --git a/src/pages/steps/LocationStep.tsx b/src/pages/steps/LocationStep.tsx index 0fa7d122d..541897a91 100644 --- a/src/pages/steps/LocationStep.tsx +++ b/src/pages/steps/LocationStep.tsx @@ -41,6 +41,7 @@ import { Text, TextVariants } from '@/ui/Text'; import { getValueFromTheme } from '@/ui/theme'; import { ToggleBox } from '@/ui/ToggleBox'; import { getLanguageObjectOrFallback } from '@/utils/getLanguageObjectOrFallback'; +import { isValidUrl } from '@/utils/isValidInfo'; import { parseOfferId } from '@/utils/parseOfferId'; import { prefixUrlWithHttps } from '@/utils/url'; @@ -55,7 +56,6 @@ import { StepProps, StepsConfiguration, } from './Steps'; -import { isValidUrl } from '@/utils/isValidInfo'; const GERMAN_ZIP_REGEX: RegExp = /\b\d{5}\b/; const DUTCH_ZIP_REGEX: RegExp = /^\d{4}([A-Za-z0-9]{2})?$/; From b4c08ac3e69e80d1447e84adb94ad18cd895e7c8 Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Thu, 27 Jul 2023 15:56:32 +0200 Subject: [PATCH 05/20] Reword test phrasing so it reads better --- .../steps/AdditionalInformationStep/ContactInfoStep.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js index 676616b46..32c028134 100644 --- a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js +++ b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js @@ -8,7 +8,8 @@ describe('isValidUrl', () => { 'https://speeltuin.vlaanderen/speeltuinen': true, }; - test.each(Object.entries(tests))('can check if url %p is %p', (url) => - expect(isValidUrl(url)).toBe(tests[url]), + test.each(Object.entries(tests))( + 'can check if url %p being valid is %p', + (url, expected) => expect(isValidUrl(url)).toBe(expected), ); }); From d883875b17645a853e7c14d71d6fb83776c7678d Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Thu, 27 Jul 2023 15:57:02 +0200 Subject: [PATCH 06/20] Goodbye regex --- src/utils/isValidInfo.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/isValidInfo.ts b/src/utils/isValidInfo.ts index f05fe5d6c..1325f685e 100644 --- a/src/utils/isValidInfo.ts +++ b/src/utils/isValidInfo.ts @@ -1,6 +1,4 @@ const EMAIL_REGEX: RegExp = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$/; -const URL_REGEX: RegExp = - /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?!&\/=]*)$/; const PHONE_REGEX: RegExp = /^[0-9\/\-_.+ ]{0,15}$/; const isValidEmail = (email: string) => { From 1b205710c037534bf6031430f3c2745c54c0ec1d Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Thu, 27 Jul 2023 15:58:11 +0200 Subject: [PATCH 07/20] Remove redundant type hinting --- src/utils/isValidInfo.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/isValidInfo.ts b/src/utils/isValidInfo.ts index 1325f685e..666089702 100644 --- a/src/utils/isValidInfo.ts +++ b/src/utils/isValidInfo.ts @@ -1,5 +1,5 @@ -const EMAIL_REGEX: RegExp = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$/; -const PHONE_REGEX: RegExp = /^[0-9\/\-_.+ ]{0,15}$/; +const EMAIL_REGEX = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$/; +const PHONE_REGEX = /^[0-9\/\-_.+ ]{0,15}$/; const isValidEmail = (email: string) => { return ( From d70fb32b8c203b2f7900536845a19ad10fab6f4e Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Thu, 27 Jul 2023 16:44:58 +0200 Subject: [PATCH 08/20] Add missing help text for Members audience option --- src/i18n/de.json | 5 +++- src/i18n/fr.json | 5 +++- src/i18n/nl.json | 5 +++- src/pages/steps/AudienceStep.tsx | 44 ++++++++++++++++++-------------- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/i18n/de.json b/src/i18n/de.json index cd30c4cdc..0e8a6208a 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -293,7 +293,10 @@ "everyone": "Für alle", "members": "Nur für Mitglieder", "title": "Zugang", - "help": "Ihr Artikel wird nur auf kulturellen Bildungskanälen wie cultuurkuur.be veröffentlicht. Nach der Veröffentlichung können Sie spezifische Informationen für Schulen hinzufügen." + "help": { + "education": "Ihr Artikel wird nur auf kulturellen Bildungskanälen wie cultuurkuur.be veröffentlicht. Nach der Veröffentlichung können Sie spezifische Informationen für Schulen hinzufügen.", + "members": "Ihr Item wird nur auf Kanälen für Vereinigungen und deren Mitglieder veröffentlicht." + } }, "contact_info": { "add_more_singular": "Kontaktdaten hinzufügen", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index b509365be..3173bb853 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -295,7 +295,10 @@ "everyone": "Pour tout le monde", "members": "Seulement pour des membres", "title": "Accès", - "help": "Votre article ne sera publié que sur des chaînes d'éducation culturelle telles que cultuurkuur.be. Après la publication, vous pouvez ajouter des informations spécifiques pour les écoles." + "help": { + "education": "Votre article ne sera publié que sur des chaînes d'éducation culturelle telles que cultuurkuur.be. Après la publication, vous pouvez ajouter des informations spécifiques pour les écoles.", + "members": "Votre article est seulement publié sur des chaînes pour des associations et leurs membres." + } }, "contact_info": { "add_more_singular": "Ajouter des coordonnées", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 29a963120..43ba1693a 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -295,7 +295,10 @@ "education": "Specifiek voor scholen", "everyone": "Voor iedereen", "members": "Enkel voor leden", - "help": "Je item wordt enkel gepubliceerd op cultuureducatieve kanalen zoals cultuurkuur.be. Na het publiceren kan je nog specifieke informatie voor scholen toevoegen." + "help": { + "education": "Je item wordt enkel gepubliceerd op cultuureducatieve kanalen zoals cultuurkuur.be. Na het publiceren kan je nog specifieke informatie voor scholen toevoegen.", + "members": "Je item wordt enkel gepubliceerd op kanalen voor verenigingen en hun leden." + } }, "contact_info": { "add_more_singular": "Contactgegevens toevoegen", diff --git a/src/pages/steps/AudienceStep.tsx b/src/pages/steps/AudienceStep.tsx index 9ef7506bd..cd9534b35 100644 --- a/src/pages/steps/AudienceStep.tsx +++ b/src/pages/steps/AudienceStep.tsx @@ -1,5 +1,5 @@ import { yupResolver } from '@hookform/resolvers/yup'; -import { useEffect } from 'react'; +import { Fragment, useEffect } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import * as yup from 'yup'; @@ -87,25 +87,31 @@ const AudienceStep = ({ {t('create.additionalInformation.audience.title')} - {Object.values(AudienceType).map((type, index) => ( - handleOnChangeAudience(type)} + {Object.values(AudienceType).map((type, index) => { + return ( + + handleOnChangeAudience(type)} + /> + } /> - } - /> - ))} - {watchedAudienceType === AudienceType.EDUCATION && ( - - {t('create.additionalInformation.audience.help')} - - )} + {watchedAudienceType === type && + watchedAudienceType !== AudienceType.EVERYONE && ( + + {t( + `create.additionalInformation.audience.help.${watchedAudienceType}`, + )} + + )} + + ); + })} ); From 840d434a5d8af9bca936094f8b38d3346fe5ac6c Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Fri, 28 Jul 2023 12:20:22 +0200 Subject: [PATCH 09/20] Use testid to avoid non unique ids? --- .../AdditionalInformationStep/ContactInfoStep.tsx | 2 +- src/test/e2e/create-movie.spec.ts | 14 ++++++++++---- src/test/e2e/create-place.spec.ts | 6 ++++-- src/test/e2e/events/create-full-event.spec.ts | 6 ++++-- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx index 18817dbb5..4703f1280 100644 --- a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx +++ b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx @@ -284,7 +284,7 @@ const ContactInfoStep = ({ }} /> } - id="contact-info-value" + data-testid="contact-info-value" error={ !isFieldFocused && !isValidInfo(info.type, info.value) && diff --git a/src/test/e2e/create-movie.spec.ts b/src/test/e2e/create-movie.spec.ts index c728df434..e6bea1736 100644 --- a/src/test/e2e/create-movie.spec.ts +++ b/src/test/e2e/create-movie.spec.ts @@ -50,18 +50,24 @@ test('create a movie', async ({ baseURL, page }) => { const contactButton = await page.getByRole('button', { name: 'Meer contactgegevens toevoegen', }); - await page.locator('#contact-info-value').fill(faker.internet.email()); + await page.getByTestId('contact-info-value').fill(faker.internet.email()); await contactButton.click(); await page.locator('select').nth(2).selectOption('phone'); await page - .locator('#contact-info-value') + .getByTestId('contact-info-value') .nth(1) .fill(faker.phone.number('+336########')); await contactButton.click(); await page.locator('select').nth(3).selectOption('url'); - await page.locator('#contact-info-value').nth(2).fill(faker.internet.url()); + await page + .getByTestId('contact-info-value') + .nth(2) + .fill(faker.internet.url()); await contactButton.click(); - await page.locator('#contact-info-value').nth(3).fill(faker.internet.email()); + await page + .getByTestId('contact-info-value') + .nth(3) + .fill(faker.internet.email()); await page.getByRole('tabpanel').getByRole('button').nth(3).click(); // Publish diff --git a/src/test/e2e/create-place.spec.ts b/src/test/e2e/create-place.spec.ts index d17c8c8d3..b7813286f 100644 --- a/src/test/e2e/create-place.spec.ts +++ b/src/test/e2e/create-place.spec.ts @@ -116,8 +116,10 @@ test('create a place', async ({ baseURL, page }) => { // Contact await page.getByRole('tab', { name: 'Contact' }).click(); await page.getByRole('button', { name: 'Contactgegevens toevoegen' }).click(); - await page.locator('#contact-info-value').click(); - await page.locator('#contact-info-value').fill(dummyPlace.contactInfo.email); + await page.getByTestId('contact-info-value').click(); + await page + .getByTestId('contact-info-value') + .fill(dummyPlace.contactInfo.email); await page .getByRole('button', { name: 'Meer contactgegevens toevoegen' }) .click(); diff --git a/src/test/e2e/events/create-full-event.spec.ts b/src/test/e2e/events/create-full-event.spec.ts index bb366c65d..e9b1c56c5 100644 --- a/src/test/e2e/events/create-full-event.spec.ts +++ b/src/test/e2e/events/create-full-event.spec.ts @@ -137,8 +137,10 @@ test('create event with all possible fields filled in', async ({ // Contact await page.getByRole('tab', { name: 'Contact' }).click(); await page.getByRole('button', { name: 'Contactgegevens toevoegen' }).click(); - await page.locator('#contact-info-value').click(); - await page.locator('#contact-info-value').fill(dummyEvent.contactInfo.email); + await page.getByTestId('contact-info-value').click(); + await page + .getByTestId('contact-info-value') + .fill(dummyEvent.contactInfo.email); await page .getByRole('button', { name: 'Meer contactgegevens toevoegen' }) .click(); From d0b56860ac0cca893ce120f54501f1a1439b3ff9 Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Fri, 28 Jul 2023 13:00:05 +0200 Subject: [PATCH 10/20] Fix test id not being passed down to input element --- src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx | 3 ++- src/ui/Input.tsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx index 4703f1280..ce0917ba8 100644 --- a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx +++ b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx @@ -267,10 +267,12 @@ const ContactInfoStep = ({ ))} { const newContactInfoState = [...contactInfoState]; @@ -284,7 +286,6 @@ const ContactInfoStep = ({ }} /> } - data-testid="contact-info-value" error={ !isFieldFocused && !isValidInfo(info.type, info.value) && diff --git a/src/ui/Input.tsx b/src/ui/Input.tsx index 78aa4052c..3a0a5c314 100644 --- a/src/ui/Input.tsx +++ b/src/ui/Input.tsx @@ -92,6 +92,7 @@ const Input = forwardRef( disabled={disabled} onFocus={onFocus} onKeyDown={onKeyDown} + data-testid={props['data-testid']} {...getBoxProps(props)} /> ), From 72dcff096e04149ffd5cacd911b4568c37567c82 Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Tue, 1 Aug 2023 15:18:02 +0200 Subject: [PATCH 11/20] Update src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx Co-authored-by: Bram Pauwelyn --- src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx index ce0917ba8..e827d02c1 100644 --- a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx +++ b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.tsx @@ -272,7 +272,7 @@ const ContactInfoStep = ({ width="55%" Component={ { const newContactInfoState = [...contactInfoState]; From 7d0e0c8554a4bc74f0b286ca5cc639c09446a77a Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Tue, 1 Aug 2023 15:22:17 +0200 Subject: [PATCH 12/20] Add new test case with parameters --- .../steps/AdditionalInformationStep/ContactInfoStep.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js index 32c028134..f88f83be7 100644 --- a/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js +++ b/src/pages/steps/AdditionalInformationStep/ContactInfoStep.test.js @@ -6,6 +6,7 @@ describe('isValidUrl', () => { 'goobar.com': false, 'http://goobar.com': true, 'https://speeltuin.vlaanderen/speeltuinen': true, + 'https://speeltuin.vlaanderen/speeltuinen?foo[]=bar&baz=50': true, }; test.each(Object.entries(tests))( From d03c809fde52aaee8ad4a156c011a8416affa0e0 Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Fri, 4 Aug 2023 14:18:31 +0200 Subject: [PATCH 13/20] Move logic to function --- src/pages/steps/CalendarStep/CalendarStep.tsx | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/src/pages/steps/CalendarStep/CalendarStep.tsx b/src/pages/steps/CalendarStep/CalendarStep.tsx index 24a2496e5..b04bfabf4 100644 --- a/src/pages/steps/CalendarStep/CalendarStep.tsx +++ b/src/pages/steps/CalendarStep/CalendarStep.tsx @@ -73,6 +73,39 @@ const useEditCalendar = ({ offerId, onSuccess }: UseEditArguments) => { }; }; +const convertOfferToCalendarContext = (offer: Offer) => { + const initialContext = initialCalendarContext; + + const days = (offer.subEvent ?? []).map((subEvent) => ({ + id: createDayId(), + startDate: subEvent.startDate, + endDate: subEvent.endDate, + status: subEvent.status, + bookingAvailability: subEvent.bookingAvailability, + })); + + const openingHours = (offer.openingHours ?? []).map((openingHour) => ({ + id: createOpeninghoursId(), + opens: openingHour.opens, + closes: openingHour.closes, + dayOfWeek: openingHour.dayOfWeek, + })); + + const newContext = { + ...initialContext, + ...(days.length > 0 && { days }), + ...(openingHours.length > 0 && { openingHours }), + ...(offer?.startDate && { + startDate: offer.startDate, + }), + ...(offer?.endDate && { + endDate: offer.endDate, + }), + }; + + return { newContext, calendarType: offer.calendarType }; +}; + const convertStateToFormData = ( context: CalendarContext, calendarType: Values, @@ -211,38 +244,12 @@ const CalendarStep = ({ const offer: Offer | undefined = getOfferByIdQuery.data; useEffect(() => { - const initialContext = initialCalendarContext; if (!offer) return; - const days = (offer.subEvent ?? []).map((subEvent) => ({ - id: createDayId(), - startDate: subEvent.startDate, - endDate: subEvent.endDate, - status: subEvent.status, - bookingAvailability: subEvent.bookingAvailability, - })); - - const openingHours = (offer.openingHours ?? []).map((openingHour) => ({ - id: createOpeninghoursId(), - opens: openingHour.opens, - closes: openingHour.closes, - dayOfWeek: openingHour.dayOfWeek, - })); - - const newContext = { - ...initialContext, - ...(days.length > 0 && { days }), - ...(openingHours.length > 0 && { openingHours }), - ...(offer?.startDate && { - startDate: offer.startDate, - }), - ...(offer?.endDate && { - endDate: offer.endDate, - }), - }; + const { newContext, calendarType } = convertOfferToCalendarContext(offer); - handleLoadInitialContext({ newContext, calendarType: offer.calendarType }); + handleLoadInitialContext({ newContext, calendarType }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ handleLoadInitialContext, From 769ce2187c627f9da2458f24a419fe5b9e9f93d6 Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Fri, 4 Aug 2023 14:18:45 +0200 Subject: [PATCH 14/20] Add duplicate override --- src/pages/steps/CalendarStep/CalendarStep.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/pages/steps/CalendarStep/CalendarStep.tsx b/src/pages/steps/CalendarStep/CalendarStep.tsx index b04bfabf4..f383868c3 100644 --- a/src/pages/steps/CalendarStep/CalendarStep.tsx +++ b/src/pages/steps/CalendarStep/CalendarStep.tsx @@ -1,8 +1,10 @@ +import { useRouter } from 'next/router'; import { useEffect, useMemo, useRef } from 'react'; import { useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import * as yup from 'yup'; +import { BookingAvailabilityType } from '@/constants/BookingAvailabilityType'; import { CalendarType } from '@/constants/CalendarType'; import { eventTypesWithNoThemes } from '@/constants/EventTypes'; import { OfferStatus } from '@/constants/OfferStatus'; @@ -160,6 +162,7 @@ const CalendarStep = ({ onChange, ...props }: CalendarStepProps) => { + const router = useRouter(); const { t } = useTranslation(); const calendarStepContainer = useRef(null); @@ -244,10 +247,18 @@ const CalendarStep = ({ const offer: Offer | undefined = getOfferByIdQuery.data; useEffect(() => { + const isOnDuplicatePage = router.pathname.endsWith('/duplicate'); if (!offer) return; const { newContext, calendarType } = convertOfferToCalendarContext(offer); + if (isOnDuplicatePage) { + newContext.days = newContext.days.map((day) => ({ + ...day, + bookingAvailability: { type: BookingAvailabilityType.AVAILABLE }, + status: { type: OfferStatus.AVAILABLE }, + })); + } handleLoadInitialContext({ newContext, calendarType }); // eslint-disable-next-line react-hooks/exhaustive-deps From 472a118380db4f58f28d132c6921a51e835188b2 Mon Sep 17 00:00:00 2001 From: JonasVHG <4658984+JonasVHG@users.noreply.github.com> Date: Wed, 9 Aug 2023 11:50:14 +0200 Subject: [PATCH 15/20] Add info about running it with docker --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1135a62c3..9f530587a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Copy the `.env.example` and rename it to `.env.local`. Fill in the correct values for the variables. +For running it in combination with [udb3-backend](https://github.com/cultuurnet/udb3-backend) on [Docker](https://www.docker.com), +a sample `.env` is available in [appconfig](https://github.com/cultuurnet/appconfig/blob/main/files/udb3/docker/udb3-frontend/.env). ## Build Setup From a6ebbf61d80dd6ab90aa09e9a6ea6efc0d08d334 Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Thu, 10 Aug 2023 13:32:00 +0200 Subject: [PATCH 16/20] Prevent images from being stretched in the Announcements --- src/layouts/Announcements.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/layouts/Announcements.js b/src/layouts/Announcements.js index c67d33b00..8fe1318f2 100644 --- a/src/layouts/Announcements.js +++ b/src/layouts/Announcements.js @@ -92,9 +92,11 @@ const AnnouncementContent = ({ alt={callToActionLabel ?? ''} width="100%" maxHeight="30vh" + objectFit="contain" opacity={{ hover: 0.85 }} /> ); + return ( {title} From ab61a197ac519be456b3d5e02cb0645133b0a20d Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Thu, 10 Aug 2023 15:55:12 +0200 Subject: [PATCH 17/20] Update dashboard pagination border radii --- src/ui/Pagination.tsx | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/ui/Pagination.tsx b/src/ui/Pagination.tsx index 0ecd4f381..e7703b940 100644 --- a/src/ui/Pagination.tsx +++ b/src/ui/Pagination.tsx @@ -60,13 +60,26 @@ const Pagination = ({ forwardedAs="ul" justifyContent="center" css={` + .page-item:first-child .page-link, + .page-item:last-child .page-link { + border-radius: ${getGlobalBorderRadius}; + } + + .page-item:nth-child(2) .page-link { + border-top-left-radius: ${getGlobalBorderRadius}; + border-bottom-left-radius: ${getGlobalBorderRadius}; + } + + .page-item:nth-last-child(2) .page-link { + border-top-right-radius: ${getGlobalBorderRadius}; + border-bottom-right-radius: ${getGlobalBorderRadius}; + } + .page-link { color: ${getValue('color')}; border-color: ${getValue('borderColor')}; padding: ${getValue('paddingY')} ${getValue('paddingX')}; - border-radius: ${getGlobalBorderRadius}; - &:hover { background-color: ${getValue('hoverBackgroundColor')}; color: ${getValue('hoverColor')}; @@ -86,20 +99,10 @@ const Pagination = ({ .prev-btn { margin-right: 0.8rem; - - .page-link { - border-top-left-radius: 8px; - border-bottom-left-radius: 8px; - } } .next-btn { margin-left: 0.8rem; - - .page-link { - border-top-right-radius: 8px; - border-bottom-right-radius: 8px; - } } `} {...getInlineProps(props)} From 4fb4e52d63f133690da34557612384e293b7d966 Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Thu, 10 Aug 2023 16:11:53 +0200 Subject: [PATCH 18/20] Allow common accented letters in labels --- src/pages/steps/AdditionalInformationStep/LabelsStep.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/steps/AdditionalInformationStep/LabelsStep.tsx b/src/pages/steps/AdditionalInformationStep/LabelsStep.tsx index a796fbb2c..bb4850b8f 100644 --- a/src/pages/steps/AdditionalInformationStep/LabelsStep.tsx +++ b/src/pages/steps/AdditionalInformationStep/LabelsStep.tsx @@ -26,7 +26,7 @@ import { Typeahead } from '@/ui/Typeahead'; type LabelsStepProps = StackProps & TabContentProps; -const LABEL_PATTERN = /^[0-9a-zA-Z][0-9a-zA-Z-_\s]{1,49}$/; +const LABEL_PATTERN = /^[0-9a-zA-ZÀ-ÿ][0-9a-zA-ZÀ-ÿ\-_\s]{1,49}$/; function LabelsStep({ offerId, From b2242b838310a715dc5f800aaa00502e1f9b85a0 Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Tue, 19 Sep 2023 16:15:53 +0200 Subject: [PATCH 19/20] Add Delete button to organizers when permission is there --- src/layouts/Sidebar.tsx | 8 ++------ src/pages/dashboard/index.page.tsx | 10 ++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/layouts/Sidebar.tsx b/src/layouts/Sidebar.tsx index be9861aaf..ce8565b3c 100644 --- a/src/layouts/Sidebar.tsx +++ b/src/layouts/Sidebar.tsx @@ -18,11 +18,7 @@ import { useCookiesWithOptions } from '@/hooks/useCookiesWithOptions'; import { FeatureFlags, useFeatureFlag } from '@/hooks/useFeatureFlag'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useMatchBreakpoint } from '@/hooks/useMatchBreakpoint'; -import { - Features, - NewFeatureTooltip, - QuestionCircleIcon, -} from '@/pages/NewFeatureTooltip'; +import { QuestionCircleIcon } from '@/pages/NewFeatureTooltip'; import type { Values } from '@/types/Values'; import { Badge } from '@/ui/Badge'; import { Button, ButtonVariants } from '@/ui/Button'; @@ -641,4 +637,4 @@ const Sidebar = () => { ]; }; -export { Sidebar }; +export { PermissionTypes, Sidebar }; diff --git a/src/pages/dashboard/index.page.tsx b/src/pages/dashboard/index.page.tsx index bd20003df..e54b446f7 100644 --- a/src/pages/dashboard/index.page.tsx +++ b/src/pages/dashboard/index.page.tsx @@ -23,11 +23,13 @@ import { useGetPlacesByCreatorQuery, } from '@/hooks/api/places'; import { + useGetPermissionsQuery, useGetUserQuery, useGetUserQueryServerSide, User, } from '@/hooks/api/user'; import { FeatureFlags, useFeatureFlag } from '@/hooks/useFeatureFlag'; +import { PermissionTypes } from '@/layouts/Sidebar'; import { Footer } from '@/pages/Footer'; import type { Event } from '@/types/Event'; import { Offer } from '@/types/Offer'; @@ -361,6 +363,7 @@ const OrganizerRow = ({ const { t, i18n } = useTranslation(); const getUserQuery = useGetUserQuery(); + const getPermissionsQuery = useGetPermissionsQuery(); // @ts-expect-error const userId = getUserQuery.data?.sub; // @ts-expect-error @@ -373,6 +376,8 @@ const OrganizerRow = ({ const formattedAddress = address ? formatAddressInternal(address) : ''; const editUrl = `/organizer/${parseOfferId(organizer['@id'])}/edit`; const previewUrl = `/organizer/${parseOfferId(organizer['@id'])}/preview`; + // @ts-expect-error + const permissions = getPermissionsQuery?.data ?? []; return ( {t('dashboard.actions.edit')} , + permissions?.includes(PermissionTypes.ORGANISATIES_BEHEREN) && ( + onDelete(organizer)} key="delete"> + {t('dashboard.actions.delete')} + + ), ]} status={{ isExternalCreator, From b5815eb2f20e97ed5aeefe334a8e52ece1580d27 Mon Sep 17 00:00:00 2001 From: Emma Fabre Date: Wed, 20 Sep 2023 13:34:40 +0200 Subject: [PATCH 20/20] Refetch moderation count query on Moderated message --- src/hooks/useHandleWindowMessage.js | 1 + src/layouts/Sidebar.tsx | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/src/hooks/useHandleWindowMessage.js b/src/hooks/useHandleWindowMessage.js index 22baac276..ac1692056 100644 --- a/src/hooks/useHandleWindowMessage.js +++ b/src/hooks/useHandleWindowMessage.js @@ -11,6 +11,7 @@ const WindowMessageTypes = { URL_UNKNOWN: 'URL_UNKNOWN', JOB_ADDED: 'JOB_ADDED', HTTP_ERROR_CODE: 'HTTP_ERROR_CODE', + OFFER_MODERATED: 'OFFER_MODERATED', }; const useHandleWindowMessage = (eventsMap = {}) => { diff --git a/src/layouts/Sidebar.tsx b/src/layouts/Sidebar.tsx index be9861aaf..651cff67e 100644 --- a/src/layouts/Sidebar.tsx +++ b/src/layouts/Sidebar.tsx @@ -16,6 +16,10 @@ import { } from '@/hooks/api/user'; import { useCookiesWithOptions } from '@/hooks/useCookiesWithOptions'; import { FeatureFlags, useFeatureFlag } from '@/hooks/useFeatureFlag'; +import { + useHandleWindowMessage, + WindowMessageTypes, +} from '@/hooks/useHandleWindowMessage'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useMatchBreakpoint } from '@/hooks/useMatchBreakpoint'; import { @@ -317,6 +321,7 @@ const BetaVersionToggle = ({ const Sidebar = () => { const { t, i18n } = useTranslation(); + const queryClient = useQueryClient(); const storage = useLocalStorage(); const [isJobLoggerVisible, setIsJobLoggerVisible] = useState(true); @@ -422,6 +427,11 @@ const Sidebar = () => { // @ts-expect-error }, [getRolesQuery.data]); + useHandleWindowMessage({ + [WindowMessageTypes.OFFER_MODERATED]: () => + queryClient.invalidateQueries(['events']), + }); + const announcements = useMemo( () => rawAnnouncements.map((announcement) => {