From b99a0bf51b5b5b4704f73c61922020ccfac12bd4 Mon Sep 17 00:00:00 2001 From: iacopolea Date: Wed, 22 Oct 2025 13:55:01 +0200 Subject: [PATCH 001/108] fix: add data-qa attributes for improved testing and accessibility --- src/pages/Campaign/pageHeader/Meta/index.tsx | 5 ++++- src/pages/Video/Actions.tsx | 2 +- src/pages/Video/components/SentimentOverview/index.tsx | 1 + src/pages/Video/components/Transcript/index.tsx | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/Campaign/pageHeader/Meta/index.tsx b/src/pages/Campaign/pageHeader/Meta/index.tsx index d610a3461..d5f151053 100644 --- a/src/pages/Campaign/pageHeader/Meta/index.tsx +++ b/src/pages/Campaign/pageHeader/Meta/index.tsx @@ -183,7 +183,10 @@ export const Metas = ({ {' '} {t('__INSIGHTS_PAGE_NAVIGATION_LABEL')} - + {
- + {t('__OBSERVATIONS_DRAWER_TOTAL')}: {observations.length} {observations && severities && severities.length > 0 && ( diff --git a/src/pages/Video/components/SentimentOverview/index.tsx b/src/pages/Video/components/SentimentOverview/index.tsx index 7ca0750e9..6188330b8 100644 --- a/src/pages/Video/components/SentimentOverview/index.tsx +++ b/src/pages/Video/components/SentimentOverview/index.tsx @@ -58,6 +58,7 @@ export const SentimentOverview = () => { }> diff --git a/src/pages/Video/components/Transcript/index.tsx b/src/pages/Video/components/Transcript/index.tsx index 745911baa..3618fc739 100644 --- a/src/pages/Video/components/Transcript/index.tsx +++ b/src/pages/Video/components/Transcript/index.tsx @@ -36,7 +36,7 @@ const TranscriptWrapper = ({ marginBottom: appTheme.space.xl, }} > - +
{video?.transcript ? children : } From 4f1e79c14bf55e005a16ee34cc610b186fe03608 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 29 Oct 2025 16:50:37 +0100 Subject: [PATCH 002/108] fix: Correct plan status --- src/pages/Plan/summary/components/BuyButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Plan/summary/components/BuyButton.tsx b/src/pages/Plan/summary/components/BuyButton.tsx index 27fbf6825..730235ead 100644 --- a/src/pages/Plan/summary/components/BuyButton.tsx +++ b/src/pages/Plan/summary/components/BuyButton.tsx @@ -60,7 +60,7 @@ const BuyButton = ({ isStretched={isStretched} disabled={ planComposedStatus && - ['AwaitingPayment', 'PurchasedPlan'].includes(planComposedStatus) + ['Accepted', 'PurchasedPlan'].includes(planComposedStatus) } onClick={handleBuyButtonClick} > From 1ae89f6e772b3685cbcf8296543e5be6e91fe6ac Mon Sep 17 00:00:00 2001 From: iacopolea Date: Mon, 20 Oct 2025 15:28:58 +0200 Subject: [PATCH 003/108] feat: add disposable email validation using fakefilter package --- package.json | 1 + src/common/disposableEmail.ts | 5 +++++ src/pages/JoinPage/Steps/Step1.tsx | 20 ++++++++++++++++++++ yarn.lock | 5 +++++ 4 files changed, 31 insertions(+) create mode 100644 src/common/disposableEmail.ts diff --git a/package.json b/package.json index d6eb98a67..031ffccda 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "analytics": "^0.8.16", "comuni-province-regioni": "^0.4.3", "date-fns": "^2.28.0", + "fakefilter": "^0.1.1351", "formik": "^2.2.9", "i18n-iso-countries": "^7.3.0", "i18next": "^23.15.1", diff --git a/src/common/disposableEmail.ts b/src/common/disposableEmail.ts new file mode 100644 index 000000000..dc86e9530 --- /dev/null +++ b/src/common/disposableEmail.ts @@ -0,0 +1,5 @@ +import { isFakeEmail } from 'fakefilter'; + +export function isDisposableEmail(email: string): any { + return isFakeEmail(email); +} diff --git a/src/pages/JoinPage/Steps/Step1.tsx b/src/pages/JoinPage/Steps/Step1.tsx index ff21b938c..8c2b112e0 100644 --- a/src/pages/JoinPage/Steps/Step1.tsx +++ b/src/pages/JoinPage/Steps/Step1.tsx @@ -20,6 +20,8 @@ import { PasswordRequirements } from 'src/common/components/PasswordRequirements import { useSendGTMevent } from 'src/hooks/useGTMevent'; import { JoinFormValues } from '../valuesType'; import { ButtonContainer } from './ButtonContainer'; +import { isDisposableEmail } from 'src/common/disposableEmail'; +import { is } from 'date-fns/locale'; export const Step1 = () => { const { setFieldValue, validateForm, setTouched, status, values } = @@ -66,6 +68,24 @@ export const Step1 = () => { let error; let error_event; if (status?.isInvited) return error; + const isDisposable = isDisposableEmail(value); + console.log('isDisposable', isDisposable); + // Disposable email check + if (isDisposable) { + error = t( + 'SIGNUP_FORM_EMAIL_DISPOSABLE_NOT_ALLOWED', + 'Non sono permesse email temporanee.' + ); + error_event = 'SIGNUP_FORM_EMAIL_DISPOSABLE_NOT_ALLOWED'; + sendGTMevent({ + event: 'sign-up-flow', + category: 'not set', + action: 'validate email', + content: `error: ${error_event}`, + target: `is invited: ${status?.isInvited}`, + }); + return error; + } const res = await fetch( `${process.env.REACT_APP_API_URL}/users/by-email/${value}`, { diff --git a/yarn.lock b/yarn.lock index ecde5e97b..67e07efdc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4819,6 +4819,11 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" +fakefilter@^0.1.1351: + version "0.1.1351" + resolved "https://registry.yarnpkg.com/fakefilter/-/fakefilter-0.1.1351.tgz#32f1b1daf16b91324099c4a8cae027eec3fed3e0" + integrity sha512-ZwKdX1JVa3P29kqHIhYesMapZ46OaUUtc6sP4kX4muf8FEC41D34qDipISBDSyDEGeeLbU9YuKaauAYXC+JzCA== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" From 9056eadcd8394d753b6486da16de7c3df0fe9276 Mon Sep 17 00:00:00 2001 From: iacopolea Date: Wed, 22 Oct 2025 10:46:42 +0200 Subject: [PATCH 004/108] refactor: clean up imports and remove unnecessary console log in Step1 component --- src/pages/JoinPage/Steps/Step1.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/JoinPage/Steps/Step1.tsx b/src/pages/JoinPage/Steps/Step1.tsx index 8c2b112e0..7a3397c18 100644 --- a/src/pages/JoinPage/Steps/Step1.tsx +++ b/src/pages/JoinPage/Steps/Step1.tsx @@ -17,11 +17,10 @@ import { useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { appTheme } from 'src/app/theme'; import { PasswordRequirements } from 'src/common/components/PasswordRequirements'; +import { isDisposableEmail } from 'src/common/disposableEmail'; import { useSendGTMevent } from 'src/hooks/useGTMevent'; import { JoinFormValues } from '../valuesType'; import { ButtonContainer } from './ButtonContainer'; -import { isDisposableEmail } from 'src/common/disposableEmail'; -import { is } from 'date-fns/locale'; export const Step1 = () => { const { setFieldValue, validateForm, setTouched, status, values } = @@ -69,7 +68,7 @@ export const Step1 = () => { let error_event; if (status?.isInvited) return error; const isDisposable = isDisposableEmail(value); - console.log('isDisposable', isDisposable); + // Disposable email check if (isDisposable) { error = t( From 1112227d26612e9bd933b382cc4291dce40717ab Mon Sep 17 00:00:00 2001 From: iacopolea Date: Wed, 22 Oct 2025 11:14:44 +0200 Subject: [PATCH 005/108] feat: add validation for disposable email addresses in signup process --- src/locales/en/translation.json | 1 + src/locales/it/translation.json | 1 + src/pages/JoinPage/Steps/Step1.tsx | 30 ++++++++++++++---------------- tests/e2e/join/index.spec.ts | 8 ++++++-- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index affefa462..44d642f79 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1454,6 +1454,7 @@ "SIGNUP_FORM_COMPANY_SIZE_PLACEHOLDER": "Select company size", "SIGNUP_FORM_CTA_RETURN_TO_UNGUESS_LANDING": "or visit UNGUESS website", "SIGNUP_FORM_EMAIL_ALREADY_TAKEN": "Error: User with provided email already exists", + "SIGNUP_FORM_EMAIL_DISPOSABLE_NOT_ALLOWED": "Temporary email addresses are not allowed", "SIGNUP_FORM_EMAIL_ERROR_SERVER_MAIL_CHECK": "Oops! Check your email format", "SIGNUP_FORM_EMAIL_IS_REQUIRED": "This field is required", "SIGNUP_FORM_EMAIL_LABEL": "Work Email", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index c41de16f3..ccf79bf24 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1498,6 +1498,7 @@ "SIGNUP_FORM_COMPANY_SIZE_PLACEHOLDER": "", "SIGNUP_FORM_CTA_RETURN_TO_UNGUESS_LANDING": "", "SIGNUP_FORM_EMAIL_ALREADY_TAKEN": "", + "SIGNUP_FORM_EMAIL_DISPOSABLE_NOT_ALLOWED": "", "SIGNUP_FORM_EMAIL_ERROR_SERVER_MAIL_CHECK": "", "SIGNUP_FORM_EMAIL_IS_REQUIRED": "", "SIGNUP_FORM_EMAIL_LABEL": "", diff --git a/src/pages/JoinPage/Steps/Step1.tsx b/src/pages/JoinPage/Steps/Step1.tsx index 7a3397c18..fdf5d1504 100644 --- a/src/pages/JoinPage/Steps/Step1.tsx +++ b/src/pages/JoinPage/Steps/Step1.tsx @@ -67,23 +67,21 @@ export const Step1 = () => { let error; let error_event; if (status?.isInvited) return error; - const isDisposable = isDisposableEmail(value); + if (value) { + const isDisposable = isDisposableEmail(value); - // Disposable email check - if (isDisposable) { - error = t( - 'SIGNUP_FORM_EMAIL_DISPOSABLE_NOT_ALLOWED', - 'Non sono permesse email temporanee.' - ); - error_event = 'SIGNUP_FORM_EMAIL_DISPOSABLE_NOT_ALLOWED'; - sendGTMevent({ - event: 'sign-up-flow', - category: 'not set', - action: 'validate email', - content: `error: ${error_event}`, - target: `is invited: ${status?.isInvited}`, - }); - return error; + if (isDisposable) { + error = t('SIGNUP_FORM_EMAIL_DISPOSABLE_NOT_ALLOWED'); + error_event = 'SIGNUP_FORM_EMAIL_DISPOSABLE_NOT_ALLOWED'; + sendGTMevent({ + event: 'sign-up-flow', + category: 'not set', + action: 'validate email', + content: `error: ${error_event}`, + target: `is invited: ${status?.isInvited}`, + }); + return error; + } } const res = await fetch( `${process.env.REACT_APP_API_URL}/users/by-email/${value}`, diff --git a/tests/e2e/join/index.spec.ts b/tests/e2e/join/index.spec.ts index 830bfd2c4..dc731c460 100644 --- a/tests/e2e/join/index.spec.ts +++ b/tests/e2e/join/index.spec.ts @@ -75,11 +75,16 @@ test.describe('The Join page first step - case new user', () => { await expect(step1.elements().emailError()).toHaveText( i18n.t('SIGNUP_FORM_EMAIL_IS_REQUIRED') ); - await step1.fillEmail('invalid-email'); + await step1.fillEmail('invalid-email@'); await expect( page.getByText(i18n.t('SIGNUP_FORM_EMAIL_MUST_BE_A_VALID_EMAIL')) ).toBeVisible(); + await step1.fillEmail('fake-email@mailinator.com'); + await expect( + page.getByText(i18n.t('SIGNUP_FORM_EMAIL_DISPOSABLE_NOT_ALLOWED')) + ).toBeVisible(); + await step1.fillRegisteredEmail(); await expect( page.getByText(i18n.t('SIGNUP_FORM_EMAIL_ALREADY_TAKEN')) @@ -94,7 +99,6 @@ test.describe('The Join page first step - case new user', () => { await expect(step1.elements().container()).not.toBeVisible(); await expect(step2.elements().container()).toBeVisible(); }); - test('display two links to go to app.unguess and a link to terms and conditions', async () => {}); }); test.describe('The Join page second step', () => { From 7a0063ea6411aea468a2c3cfb0860cd322b8d62c Mon Sep 17 00:00:00 2001 From: iacopolea Date: Wed, 22 Oct 2025 11:22:32 +0200 Subject: [PATCH 006/108] refactor: remove duplicate e2e tests for the Join page --- tests/e2e/login/index.spec.ts | 217 ---------------------------------- 1 file changed, 217 deletions(-) delete mode 100644 tests/e2e/login/index.spec.ts diff --git a/tests/e2e/login/index.spec.ts b/tests/e2e/login/index.spec.ts deleted file mode 100644 index 830bfd2c4..000000000 --- a/tests/e2e/login/index.spec.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { test, expect } from '../../fixtures/app'; -import { Join } from '../../fixtures/pages/Join'; -import { Step1 } from '../../fixtures/pages/Join/Step1'; -import { Step2 } from '../../fixtures/pages/Join/Step2'; -import { Step3 } from '../../fixtures/pages/Join/Step3'; - -test.describe('The Join page first step - case new user', () => { - let join: Join; - let step1: Step1; - let step2: Step2; - - test.beforeEach(async ({ page }) => { - join = new Join(page); - await join.notLoggedIn(); - step1 = new Step1(page); - step2 = new Step2(page); - await join.open(); - }); - - test('display a form with user email and password input, a CTA to go to the next step', async () => { - await expect(step1.elements().container()).toBeVisible(); - await expect(step1.elements().emailInput()).toBeVisible(); - await expect(step1.elements().passwordInput()).toBeVisible(); - await expect(step1.elements().buttonGoToStep2()).toBeVisible(); - }); - - test('the password input check if the password is strong enough', async ({ - page, - i18n, - }) => { - await step1.elements().buttonGoToStep2().click(); - await expect(step1.elements().passwordError()).toHaveText( - i18n.t('SIGNUP_FORM_PASSWORD_IS_A_REQUIRED_FIELD') - ); - - await expect(step1.elements().passwordRequirements()).toBeVisible(); - - await step1.fillPassword('weak'); - await expect( - page.getByText( - i18n.t('SIGNUP_FORM_PASSWORD_MUST_BE_AT_LEAST_6_CHARACTER_LONG') - ) - ).toBeVisible(); - - await step1.fillPassword('weakpassword'); - await expect( - page.getByText( - i18n.t('SIGNUP_FORM_PASSWORD_MUST_CONTAIN_AT_LEAST_A_NUMBER') - ) - ).toBeVisible(); - - await step1.fillPassword('weakpassword123'); - await expect( - page.getByText( - i18n.t('SIGNUP_FORM_PASSWORD_MUST_CONTAIN_AT_LEAST_AN_UPPERCASE_LETTER') - ) - ).toBeVisible(); - - await step1.fillPassword('WEAKPASSWORD123'); - await expect( - page.getByText( - i18n.t('SIGNUP_FORM_PASSWORD_MUST_CONTAIN_AT_LEAST_A_LOWERCASE_LETTER') - ) - ).toBeVisible(); - - await step1.fillValidPassword(); - await expect(page.getByTestId('message-error-password')).not.toBeVisible(); - }); - - test('the email input check if the email is valid', async ({ - page, - i18n, - }) => { - await step1.elements().buttonGoToStep2().click(); - await expect(step1.elements().emailError()).toHaveText( - i18n.t('SIGNUP_FORM_EMAIL_IS_REQUIRED') - ); - await step1.fillEmail('invalid-email'); - await expect( - page.getByText(i18n.t('SIGNUP_FORM_EMAIL_MUST_BE_A_VALID_EMAIL')) - ).toBeVisible(); - - await step1.fillRegisteredEmail(); - await expect( - page.getByText(i18n.t('SIGNUP_FORM_EMAIL_ALREADY_TAKEN')) - ).toBeVisible(); - - await step1.fillValidEmail(); - await expect(page.getByTestId('message-error-email')).not.toBeVisible(); - }); - - test('when the user click the next step cta we validate current inputs and if ok goes to the next step', async () => { - await step1.goToNextStep(); - await expect(step1.elements().container()).not.toBeVisible(); - await expect(step2.elements().container()).toBeVisible(); - }); - test('display two links to go to app.unguess and a link to terms and conditions', async () => {}); -}); - -test.describe('The Join page second step', () => { - let join: Join; - let step1: Step1; - let step2: Step2; - let step3: Step3; - - test.beforeEach(async ({ page }) => { - join = new Join(page); - await join.notLoggedIn(); - step1 = new Step1(page); - step2 = new Step2(page); - step3 = new Step3(page); - - await step2.mockGetRoles(); - await step2.mockGetCompanySizes(); - await join.open(); - await step1.goToNextStep(); - }); - test('display required inputs for name, surname, job role and company size dropdowns populated from api userRole and companySize', async ({ - i18n, - }) => { - await expect(step2.elements().nameInput()).toBeVisible(); - await expect(step2.elements().surnameInput()).toBeVisible(); - await expect(step2.elements().roleSelect()).toBeVisible(); - await step2.elements().roleSelect().click(); - await expect(step2.elements().roleSelectOptions()).toHaveCount(3); - - await expect(step2.elements().companySizeSelect()).toBeVisible(); - await step2.elements().companySizeSelect().click(); - await expect(step2.elements().companySizeSelectOptions()).toHaveCount(3); - - await step2.elements().buttonGoToStep3().click(); - - await expect(step2.elements().nameError()).toHaveText( - i18n.t('SIGNUP_FORM_NAME_IS_REQUIRED') - ); - await expect(step2.elements().surnameError()).toHaveText( - i18n.t('SIGNUP_FORM_SURNAME_IS_REQUIRED') - ); - await expect(step2.elements().roleSelectError()).toHaveText( - i18n.t('SIGNUP_FORM_ROLE_IS_REQUIRED') - ); - await expect(step2.elements().companySizeSelectError()).toHaveText( - i18n.t('SIGNUP_FORM_COMPANY_SIZE_IS_REQUIRED') - ); - - await step2.fillValidFields(); - await expect(step2.elements().nameError()).not.toBeVisible(); - await expect(step2.elements().surnameError()).not.toBeVisible(); - await expect(step2.elements().roleSelectError()).not.toBeVisible(); - }); - test('display back and next navigation, clicking on next validate this step and goes to step 3', async () => { - await expect(step2.elements().buttonBackToStep1()).toBeVisible(); - await expect(step2.elements().buttonGoToStep3()).toBeVisible(); - await step2.goToNextStep(); - await expect(step2.elements().container()).not.toBeVisible(); - await expect(step3.elements().container()).toBeVisible(); - }); -}); - -test.describe('The Join page third step', () => { - let join: Join; - let step1: Step1; - let step2: Step2; - let step3: Step3; - - test.beforeEach(async ({ page }) => { - join = new Join(page); - await join.notLoggedIn(); - step1 = new Step1(page); - step2 = new Step2(page); - step3 = new Step3(page); - - await join.open(); - await join.mockPostNewUser(); - await step1.goToNextStep(); - await step2.goToNextStep(); - }); - test('display a required text input for the workspace name and a back button to return to step 2', async () => { - await expect(step3.elements().workspaceInput()).toBeVisible(); - await expect(step3.elements().buttonBackToStep2()).toBeVisible(); - await step3.elements().buttonBackToStep2().click(); - await expect(step3.elements().container()).not.toBeVisible(); - await expect(step2.elements().container()).toBeVisible(); - }); - test('display a submit-button, clicking on submit-button validate the whole form and calls the api post', async ({ - page, - i18n, - }) => { - const postPromise = page.waitForResponse( - (response) => - /\/api\/users/.test(response.url()) && - response.status() === 200 && - response.request().method() === 'POST' - ); - await step3.elements().buttonSubmit().click(); - await expect(step3.elements().workspaceError()).toHaveText( - i18n.t('SIGNUP_FORM_WORKSPACE_IS_REQUIRED') - ); - await step3.fillValidWorkspace(); - await expect(step3.elements().workspaceError()).not.toBeVisible(); - await step3.elements().buttonSubmit().click(); - const response = await postPromise; - const data = response.request().postDataJSON(); - expect(data).toEqual( - expect.objectContaining({ - type: 'new', - email: 'new.user@example.com', - password: 'ValidPassword123', - name: step2.name, - surname: step2.surname, - roleId: step2.roleId, - companySizeId: step2.companySizeId, - workspace: step3.workspace, - }) - ); - }); -}); From a61ac46326f5e3bdf997d362dd75e8e9dea44bb4 Mon Sep 17 00:00:00 2001 From: iacopolea Date: Thu, 30 Oct 2025 10:59:19 +0100 Subject: [PATCH 007/108] fix: update fakefilter dependency to version 0.1.1361 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 031ffccda..55e269e97 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "analytics": "^0.8.16", "comuni-province-regioni": "^0.4.3", "date-fns": "^2.28.0", - "fakefilter": "^0.1.1351", + "fakefilter": "^0.1.1361", "formik": "^2.2.9", "i18n-iso-countries": "^7.3.0", "i18next": "^23.15.1", diff --git a/yarn.lock b/yarn.lock index 67e07efdc..d323602fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4819,10 +4819,10 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" -fakefilter@^0.1.1351: - version "0.1.1351" - resolved "https://registry.yarnpkg.com/fakefilter/-/fakefilter-0.1.1351.tgz#32f1b1daf16b91324099c4a8cae027eec3fed3e0" - integrity sha512-ZwKdX1JVa3P29kqHIhYesMapZ46OaUUtc6sP4kX4muf8FEC41D34qDipISBDSyDEGeeLbU9YuKaauAYXC+JzCA== +fakefilter@^0.1.1361: + version "0.1.1361" + resolved "https://registry.yarnpkg.com/fakefilter/-/fakefilter-0.1.1361.tgz#e62c4e8d06e2f969c687f5aeffa954ebbd93ff92" + integrity sha512-K4jTvfXsrt+vYCyGblcjlBrw16bMPNtWm0kRVfyVaHwc7I10WuosQL9d0sB16GD4RQTieEhOEJabR14MNkVMsQ== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" From 6f25838fdfb638868319c0f98597e7854e61dbf7 Mon Sep 17 00:00:00 2001 From: iacopolea Date: Thu, 30 Oct 2025 11:29:28 +0100 Subject: [PATCH 008/108] fix: include fakefilter in optimizeDeps for improved dependency management --- vite.config.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.mts b/vite.config.mts index 607b843b0..a64d9e8e3 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -15,7 +15,7 @@ export default defineConfig(({ mode }) => { setEnv(mode); return { optimizeDeps: { - include: ['react-dom'], + include: ['react-dom', 'fakefilter'], exclude: ['@appquality/unguess-design-system'], }, plugins: [ From 02fe00d8d4bb6c13d1ce2268e572c5912600c8da Mon Sep 17 00:00:00 2001 From: iacopolea Date: Thu, 30 Oct 2025 12:16:43 +0100 Subject: [PATCH 009/108] refactor: replace fakefilter with mailchecker for disposable email validation --- package.json | 4 ++-- src/common/disposableEmail.ts | 4 ++-- vite.config.mts | 2 +- yarn.lock | 10 +++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 55e269e97..1b288d626 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,11 @@ "analytics": "^0.8.16", "comuni-province-regioni": "^0.4.3", "date-fns": "^2.28.0", - "fakefilter": "^0.1.1361", "formik": "^2.2.9", "i18n-iso-countries": "^7.3.0", "i18next": "^23.15.1", "i18next-browser-languagedetector": "^8.0.0", + "mailchecker": "^6.0.19", "motion": "^12.16.0", "qs": "^6.10.3", "query-string": "^7.1.1", @@ -131,4 +131,4 @@ "*.{tsx,ts,js,css,md}": "prettier --write" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" -} \ No newline at end of file +} diff --git a/src/common/disposableEmail.ts b/src/common/disposableEmail.ts index dc86e9530..988009caf 100644 --- a/src/common/disposableEmail.ts +++ b/src/common/disposableEmail.ts @@ -1,5 +1,5 @@ -import { isFakeEmail } from 'fakefilter'; +import MailChecker from 'mailchecker'; export function isDisposableEmail(email: string): any { - return isFakeEmail(email); + return !MailChecker.isValid(email); } diff --git a/vite.config.mts b/vite.config.mts index a64d9e8e3..607b843b0 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -15,7 +15,7 @@ export default defineConfig(({ mode }) => { setEnv(mode); return { optimizeDeps: { - include: ['react-dom', 'fakefilter'], + include: ['react-dom'], exclude: ['@appquality/unguess-design-system'], }, plugins: [ diff --git a/yarn.lock b/yarn.lock index d323602fc..106faacc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4819,11 +4819,6 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" -fakefilter@^0.1.1361: - version "0.1.1361" - resolved "https://registry.yarnpkg.com/fakefilter/-/fakefilter-0.1.1361.tgz#e62c4e8d06e2f969c687f5aeffa954ebbd93ff92" - integrity sha512-K4jTvfXsrt+vYCyGblcjlBrw16bMPNtWm0kRVfyVaHwc7I10WuosQL9d0sB16GD4RQTieEhOEJabR14MNkVMsQ== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -6271,6 +6266,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +mailchecker@^6.0.19: + version "6.0.19" + resolved "https://registry.yarnpkg.com/mailchecker/-/mailchecker-6.0.19.tgz#7d0bc89e6f846a2dc1adb44e065e4d146e8f7d8d" + integrity sha512-a7yghUa0IC1n6OkSJIqCSw2HS8ujKJGBJcRkjhHiKvHqPU3JB28Yze9o90L52r6lA/sp/EBveDXOWbozcdmxYg== + make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" From 4de176479915259ebe7e4ee72ff14d9ce625df56 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Fri, 31 Oct 2025 13:39:24 +0100 Subject: [PATCH 010/108] feat: Add user watcher list --- src/assets/icons/eye-icon-fill.svg | 3 + src/assets/icons/eye-icon-slash.svg | 5 + src/assets/icons/eye-icon.svg | 3 + src/features/api/index.ts | 27 +++- .../WatcherList/MemberAddAutoComplete.tsx | 87 ++++++++++++ .../Plan/Controls/WatcherList/UserItem.tsx | 101 ++++++++++++++ .../Plan/Controls/WatcherList/UserList.tsx | 61 +++++++++ .../Plan/Controls/WatcherList/WatchButton.tsx | 29 ++++ .../WatcherList/hooks/useIsWatching.tsx | 22 +++ src/pages/Plan/Controls/WatcherList/index.tsx | 127 ++++++++++++++++++ src/pages/Plan/Controls/index.tsx | 4 +- 11 files changed, 466 insertions(+), 3 deletions(-) create mode 100644 src/assets/icons/eye-icon-fill.svg create mode 100644 src/assets/icons/eye-icon-slash.svg create mode 100644 src/assets/icons/eye-icon.svg create mode 100644 src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx create mode 100644 src/pages/Plan/Controls/WatcherList/UserItem.tsx create mode 100644 src/pages/Plan/Controls/WatcherList/UserList.tsx create mode 100644 src/pages/Plan/Controls/WatcherList/WatchButton.tsx create mode 100644 src/pages/Plan/Controls/WatcherList/hooks/useIsWatching.tsx create mode 100644 src/pages/Plan/Controls/WatcherList/index.tsx diff --git a/src/assets/icons/eye-icon-fill.svg b/src/assets/icons/eye-icon-fill.svg new file mode 100644 index 000000000..c1829260d --- /dev/null +++ b/src/assets/icons/eye-icon-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/eye-icon-slash.svg b/src/assets/icons/eye-icon-slash.svg new file mode 100644 index 000000000..a36fcfb6c --- /dev/null +++ b/src/assets/icons/eye-icon-slash.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/eye-icon.svg b/src/assets/icons/eye-icon.svg new file mode 100644 index 000000000..e7e4f55d0 --- /dev/null +++ b/src/assets/icons/eye-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/features/api/index.ts b/src/features/api/index.ts index 1a35d8b1e..cc8675690 100644 --- a/src/features/api/index.ts +++ b/src/features/api/index.ts @@ -786,6 +786,12 @@ const injectedRtkApi = api.injectEndpoints({ params: { limit: queryArg.limit, start: queryArg.start }, }), }), + getPlansByPidWatchers: build.query< + GetPlansByPidWatchersApiResponse, + GetPlansByPidWatchersApiArg + >({ + query: (queryArg) => ({ url: `/plans/${queryArg.pid}/watchers` }), + }), getWorkspacesByWidTemplates: build.query< GetWorkspacesByWidTemplatesApiResponse, GetWorkspacesByWidTemplatesApiArg @@ -896,12 +902,15 @@ export type PostBuyApiArg = { key: string; tag: string; }; + payment_status?: 'paid' | 'unpaid'; }; }; type: - | 'checkout.session.completed' + | 'checkout.session.async_payment_succeeded' | 'checkout.session.async_payment_failed' - | 'checkout.session.expired'; + | 'checkout.session.completed' + | 'checkout.session.expired' + | 'charge.refunded'; }; }; export type GetCampaignsByCidApiResponse = @@ -2005,6 +2014,19 @@ export type GetWorkspacesByWidProjectsAndPidCampaignsApiArg = { /** Start pagination parameter */ start?: number; }; +export type GetPlansByPidWatchersApiResponse = /** status 200 OK */ { + items: { + id: number; + name: string; + surname: string; + email: string; + image?: string; + isInternal: boolean; + }[]; +}; +export type GetPlansByPidWatchersApiArg = { + pid: string; +}; export type GetWorkspacesByWidTemplatesApiResponse = /** status 200 OK */ { items: CpReqTemplate[]; } & PaginationData; @@ -3050,6 +3072,7 @@ export const { useGetWorkspacesByWidProjectsQuery, useGetWorkspacesByWidProjectsAndPidQuery, useGetWorkspacesByWidProjectsAndPidCampaignsQuery, + useGetPlansByPidWatchersQuery, useGetWorkspacesByWidTemplatesQuery, usePostWorkspacesByWidTemplatesMutation, useDeleteWorkspacesByWidTemplatesAndTidMutation, diff --git a/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx b/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx new file mode 100644 index 000000000..f6a0996d0 --- /dev/null +++ b/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx @@ -0,0 +1,87 @@ +import { + Autocomplete, + DropdownFieldNew, + MD, +} from '@appquality/unguess-design-system'; +import { useState } from 'react'; + +import { + useGetPlansByPidWatchersQuery, + useGetUsersMeQuery, + useGetWorkspacesByWidUsersQuery, +} from 'src/features/api'; +import { useActiveWorkspace } from 'src/hooks/useActiveWorkspace'; +import { useTheme } from 'styled-components'; + +const ItemContent = ({ name, email }: { name: string; email: string }) => { + const appTheme = useTheme(); + return ( +
+
+ {name} + {email} +
+
+ ); +}; + +const MemberAddAutocomplete = ({ planId }: { planId: string }) => { + const { activeWorkspace, isLoading } = useActiveWorkspace(); + const { data } = useGetWorkspacesByWidUsersQuery( + { + wid: (activeWorkspace?.id || '0').toString(), + }, + { + skip: !activeWorkspace?.id, + } + ); + const { data: currentUser, isLoading: isLoadingCurrentUser } = + useGetUsersMeQuery(); + const { data: watchers, isLoading: isLoadingWatchers } = + useGetPlansByPidWatchersQuery({ pid: planId }); + const [inputValue, setInputValue] = useState(''); + + if (!data || isLoading || isLoadingWatchers || isLoadingCurrentUser) + return null; + + const users = data.items + .map((user) => ({ + name: user.name, + email: user.email, + id: user.profile_id, + })) + .filter( + (user) => + !watchers?.items.find((watcher) => watcher.id === user.id) && + user.id !== currentUser?.profile_id + ); + + return ( + + setInputValue(value)} + inputValue={inputValue} + selectionValue={null} + options={users.map((user) => ({ + children: , + id: `user-${user.id}`, + label: `${user.name} - ${user.email}`, + value: `${user.id}`, + }))} + onOptionClick={({ selectionValue }) => { + console.log('Selected user to add as watcher:', selectionValue); + setInputValue(''); + }} + /> + + ); +}; + +export { MemberAddAutocomplete }; diff --git a/src/pages/Plan/Controls/WatcherList/UserItem.tsx b/src/pages/Plan/Controls/WatcherList/UserItem.tsx new file mode 100644 index 000000000..4534eb42c --- /dev/null +++ b/src/pages/Plan/Controls/WatcherList/UserItem.tsx @@ -0,0 +1,101 @@ +import { + Avatar, + Ellipsis, + getColor, + IconButton, + MD, + SM, +} from '@appquality/unguess-design-system'; +import { t } from 'i18next'; +import { appTheme } from 'src/app/theme'; +import { ReactComponent as XStroke } from 'src/assets/icons/x-stroke.svg'; +import { getInitials } from 'src/common/components/navigation/header/utils'; +import { prepareGravatar } from 'src/common/utils'; +import styled from 'styled-components'; + +const StyledEllipsis = styled(Ellipsis)``; + +const UserListItem = styled.div` + display: flex; + padding: ${({ theme }) => `${theme.space.xs} 0`}; + align-items: center; + gap: ${({ theme }) => theme.space.sm}; + + ${StyledEllipsis} { + width: 250px; + } +`; + +const UserAvatar = ({ + image, + name, + isInternal, +}: { + image?: string; + name: string; + isInternal: boolean; +}) => { + if (isInternal) { + return ; + } + return ( + + {image ? prepareGravatar(image, 64) : getInitials(name)} + + ); +}; + +const UserItem = ({ + user, +}: { + user: { + id: number; + name: string; + email: string; + image?: string; + isInternal: boolean; + isMe?: boolean; + }; +}) => { + const { isMe } = user; + return ( + +
+ +
+
+ + + {user.name.length ? user.name : user.email}{' '} + {isMe && t('__WORKSPACE_SETTINGS_CURRENT_MEMBER_YOU_LABEL')} + + + {user.name.length > 0 && ( + + {user.email} + + )} +
+
+ alert('Remove watcher')}> + + +
+
+ ); +}; + +export { UserItem }; diff --git a/src/pages/Plan/Controls/WatcherList/UserList.tsx b/src/pages/Plan/Controls/WatcherList/UserList.tsx new file mode 100644 index 000000000..044415c3f --- /dev/null +++ b/src/pages/Plan/Controls/WatcherList/UserList.tsx @@ -0,0 +1,61 @@ +import { MD, Skeleton } from '@appquality/unguess-design-system'; + +import { styled } from 'styled-components'; + +import { useTranslation } from 'react-i18next'; +import { + useGetPlansByPidWatchersQuery, + useGetUsersMeQuery, +} from 'src/features/api'; +import { UserItem } from './UserItem'; + +const UserItemContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.space.xxs}; +`; + +const EmptyState = () => <>Empty; + +const UserList = ({ planId }: { planId: string }) => { + const { t } = useTranslation(); + const { data: currentUser } = useGetUsersMeQuery(); + const { data, isLoading } = useGetPlansByPidWatchersQuery({ + pid: planId, + }); + + if (isLoading) return ; + + if (!data || data.items.length === 0) return ; + + return ( + + + {t( + '__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE', + 'People following this activity' + )} + + + {[...data.items] + .sort((a, b) => { + const aName = `${a.name} ${a.surname}`.toLowerCase(); + const bName = `${b.name} ${b.surname}`.toLowerCase(); + + return aName.localeCompare(bName); + }) + .map((user) => ( + + ))} + + ); +}; + +export { UserList }; diff --git a/src/pages/Plan/Controls/WatcherList/WatchButton.tsx b/src/pages/Plan/Controls/WatcherList/WatchButton.tsx new file mode 100644 index 000000000..27938c72c --- /dev/null +++ b/src/pages/Plan/Controls/WatcherList/WatchButton.tsx @@ -0,0 +1,29 @@ +import { Button } from '@appquality/unguess-design-system'; +import { useTranslation } from 'react-i18next'; + +import { ReactComponent as EyeIconFill } from 'src/assets/icons/eye-icon-fill.svg'; +import { ReactComponent as EyeIconSlash } from 'src/assets/icons/eye-icon-slash.svg'; +import { useIsWatching } from './hooks/useIsWatching'; + +const WatchButton = ({ planId }: { planId: string }) => { + const isWatching = useIsWatching({ planId }); + const { t } = useTranslation(); + return ( + + ); +}; + +export { WatchButton }; diff --git a/src/pages/Plan/Controls/WatcherList/hooks/useIsWatching.tsx b/src/pages/Plan/Controls/WatcherList/hooks/useIsWatching.tsx new file mode 100644 index 000000000..cbb8098f4 --- /dev/null +++ b/src/pages/Plan/Controls/WatcherList/hooks/useIsWatching.tsx @@ -0,0 +1,22 @@ +import { + useGetPlansByPidWatchersQuery, + useGetUsersMeQuery, +} from 'src/features/api'; + +const useIsWatching = ({ planId }: { planId: string }) => { + const { data: currentUser } = useGetUsersMeQuery(); + + const { data } = useGetPlansByPidWatchersQuery({ + pid: planId, + }); + + if (!data || data.items.length === 0 || !currentUser) return false; + + if (data.items.some((user) => user.id === currentUser.profile_id)) { + return true; + } + + return false; +}; + +export { useIsWatching }; diff --git a/src/pages/Plan/Controls/WatcherList/index.tsx b/src/pages/Plan/Controls/WatcherList/index.tsx new file mode 100644 index 000000000..c60b8e117 --- /dev/null +++ b/src/pages/Plan/Controls/WatcherList/index.tsx @@ -0,0 +1,127 @@ +import { + Button, + MD, + Skeleton, + TooltipModal, +} from '@appquality/unguess-design-system'; + +import { useRef, useState } from 'react'; +import { Divider } from 'src/common/components/divider'; +import { styled, useTheme } from 'styled-components'; + +import { useTranslation } from 'react-i18next'; +import { ReactComponent as EyeIconFill } from 'src/assets/icons/eye-icon-fill.svg'; +import { ReactComponent as EyeIcon } from 'src/assets/icons/eye-icon.svg'; +import { ReactComponent as InfoIcon } from 'src/assets/icons/info-icon.svg'; +import { useGetPlansByPidWatchersQuery } from 'src/features/api'; +import { useIsWatching } from './hooks/useIsWatching'; +import { MemberAddAutocomplete } from './MemberAddAutoComplete'; +import { UserList } from './UserList'; +import { WatchButton } from './WatchButton'; + +const ModalBodyContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.space.md}; +`; + +const DropdownContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.space.xs}; + .title-with-icon { + display: flex; + align-items: center; + + gap: ${({ theme }) => theme.space.xs}; + } +`; + +const WatcherList = ({ planId }: { planId: string }) => { + const { t } = useTranslation(); + const ref = useRef(null); + const [referenceElement, setReferenceElement] = + useState(null); + const appTheme = useTheme(); + const isWatching = useIsWatching({ planId }); + const { data: watchers, isLoading } = useGetPlansByPidWatchersQuery({ + pid: planId, + }); + const watchersCount = watchers ? watchers.items.length : 0; + return ( + <> + + setReferenceElement(null)} + role="dialog" + > + + + {t( + '__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE', + 'Stay updated on setup progress' + )} + + + + + + {t( + '__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION', + 'Follow this activity and turn on notifications to receive important email updates about changes' + )} + + + + + + + + +
+ + {t( + '__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE', + 'Add workspace members' + )} + + +
+ +
+
+
+ + ); +}; + +export { WatcherList }; diff --git a/src/pages/Plan/Controls/index.tsx b/src/pages/Plan/Controls/index.tsx index 2a586d1be..2f2e05649 100644 --- a/src/pages/Plan/Controls/index.tsx +++ b/src/pages/Plan/Controls/index.tsx @@ -18,6 +18,7 @@ import { GoToCampaignButton } from './GoToCampaignButton'; import { IconButtonMenu } from './IconButtonMenu'; import { RequestQuotationButton } from './RequestQuotationButton'; import { SaveConfigurationButton } from './SaveConfigurationButton'; +import { WatcherList } from './WatcherList'; const StyledPipe = styled(Pipe)` display: inline; @@ -42,7 +43,7 @@ export const Controls = () => { const { addToast } = useToast(); const { handleSubmit } = useSubmit(planId || ''); - if (!plan) return null; + if (!plan || !planId) return null; const handleRequestQuotation = async () => { try { @@ -81,6 +82,7 @@ export const Controls = () => { planComposedStatus === 'OpsCheck' || planComposedStatus === 'Submitted') && } {planComposedStatus === 'Paying' && } + {(planComposedStatus === 'PurchasableDraft' || planComposedStatus === 'PrequotedDraft' || planComposedStatus === 'UnquotedDraft') && ( From 3e14b1787404d784858db7c09d2290e77c0b9077 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 3 Nov 2025 15:39:29 +0100 Subject: [PATCH 011/108] feat: Implement Plan Watchers functionality with add and remove capabilities --- src/features/api/api.ts | 1 + src/features/api/apiTags.ts | 9 +++ src/features/api/index.ts | 38 +++++++++++++ .../WatcherList/MemberAddAutoComplete.tsx | 30 +++++++++- .../Plan/Controls/WatcherList/UserItem.tsx | 13 ++++- .../Plan/Controls/WatcherList/UserList.tsx | 1 + .../Plan/Controls/WatcherList/WatchButton.tsx | 27 ++++++++- .../WatcherList/hooks/useRemoveWatcher.tsx | 55 +++++++++++++++++++ 8 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 src/pages/Plan/Controls/WatcherList/hooks/useRemoveWatcher.tsx diff --git a/src/features/api/api.ts b/src/features/api/api.ts index 09f20fe15..4f1b55b80 100644 --- a/src/features/api/api.ts +++ b/src/features/api/api.ts @@ -26,6 +26,7 @@ export const apiSlice = createApi({ 'Tags', 'CustomStatuses', 'Bug', + 'PlanWatchers', 'BugComments', 'Preferences', 'VideoTags', diff --git a/src/features/api/apiTags.ts b/src/features/api/apiTags.ts index 0b0253cfa..d4d59140d 100644 --- a/src/features/api/apiTags.ts +++ b/src/features/api/apiTags.ts @@ -330,6 +330,15 @@ unguessApi.enhanceEndpoints({ getPlansByPidRulesEvaluation: { providesTags: ['EvaluationRules'], }, + getPlansByPidWatchers: { + providesTags: ['PlanWatchers'], + }, + postPlansByPidWatchers: { + invalidatesTags: ['PlanWatchers'], + }, + deletePlansByPidWatchersAndProfileId: { + invalidatesTags: ['PlanWatchers'], + }, }, }); diff --git a/src/features/api/index.ts b/src/features/api/index.ts index cc8675690..090acac64 100644 --- a/src/features/api/index.ts +++ b/src/features/api/index.ts @@ -792,6 +792,16 @@ const injectedRtkApi = api.injectEndpoints({ >({ query: (queryArg) => ({ url: `/plans/${queryArg.pid}/watchers` }), }), + postPlansByPidWatchers: build.mutation< + PostPlansByPidWatchersApiResponse, + PostPlansByPidWatchersApiArg + >({ + query: (queryArg) => ({ + url: `/plans/${queryArg.pid}/watchers`, + method: 'POST', + body: queryArg.body, + }), + }), getWorkspacesByWidTemplates: build.query< GetWorkspacesByWidTemplatesApiResponse, GetWorkspacesByWidTemplatesApiArg @@ -868,6 +878,15 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.body, }), }), + deletePlansByPidWatchersAndProfileId: build.mutation< + DeletePlansByPidWatchersAndProfileIdApiResponse, + DeletePlansByPidWatchersAndProfileIdApiArg + >({ + query: (queryArg) => ({ + url: `/plans/${queryArg.pid}/watchers/${queryArg.profileId}`, + method: 'DELETE', + }), + }), }), overrideExisting: false, }); @@ -2027,6 +2046,15 @@ export type GetPlansByPidWatchersApiResponse = /** status 200 OK */ { export type GetPlansByPidWatchersApiArg = { pid: string; }; +export type PostPlansByPidWatchersApiResponse = /** status 200 OK */ void; +export type PostPlansByPidWatchersApiArg = { + pid: string; + body: { + users: { + id: number; + }[]; + }; +}; export type GetWorkspacesByWidTemplatesApiResponse = /** status 200 OK */ { items: CpReqTemplate[]; } & PaginationData; @@ -2126,6 +2154,14 @@ export type PostWorkspacesByWidUsersApiArg = { surname?: string; }; }; +export type DeletePlansByPidWatchersAndProfileIdApiResponse = + /** status 200 OK */ { + success?: boolean; + }; +export type DeletePlansByPidWatchersAndProfileIdApiArg = { + pid: string; + profileId: string; +}; export type Error = { code: number; error: boolean; @@ -3073,6 +3109,7 @@ export const { useGetWorkspacesByWidProjectsAndPidQuery, useGetWorkspacesByWidProjectsAndPidCampaignsQuery, useGetPlansByPidWatchersQuery, + usePostPlansByPidWatchersMutation, useGetWorkspacesByWidTemplatesQuery, usePostWorkspacesByWidTemplatesMutation, useDeleteWorkspacesByWidTemplatesAndTidMutation, @@ -3080,4 +3117,5 @@ export const { useDeleteWorkspacesByWidUsersMutation, useGetWorkspacesByWidUsersQuery, usePostWorkspacesByWidUsersMutation, + useDeletePlansByPidWatchersAndProfileIdMutation, } = injectedRtkApi; diff --git a/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx b/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx index f6a0996d0..ae024b5fe 100644 --- a/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx +++ b/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx @@ -2,13 +2,17 @@ import { Autocomplete, DropdownFieldNew, MD, + Notification, + useToast, } from '@appquality/unguess-design-system'; import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useGetPlansByPidWatchersQuery, useGetUsersMeQuery, useGetWorkspacesByWidUsersQuery, + usePostPlansByPidWatchersMutation, } from 'src/features/api'; import { useActiveWorkspace } from 'src/hooks/useActiveWorkspace'; import { useTheme } from 'styled-components'; @@ -33,6 +37,8 @@ const ItemContent = ({ name, email }: { name: string; email: string }) => { const MemberAddAutocomplete = ({ planId }: { planId: string }) => { const { activeWorkspace, isLoading } = useActiveWorkspace(); + + const [addUser] = usePostPlansByPidWatchersMutation(); const { data } = useGetWorkspacesByWidUsersQuery( { wid: (activeWorkspace?.id || '0').toString(), @@ -41,6 +47,8 @@ const MemberAddAutocomplete = ({ planId }: { planId: string }) => { skip: !activeWorkspace?.id, } ); + const { addToast } = useToast(); + const { t } = useTranslation(); const { data: currentUser, isLoading: isLoadingCurrentUser } = useGetUsersMeQuery(); const { data: watchers, isLoading: isLoadingWatchers } = @@ -76,7 +84,27 @@ const MemberAddAutocomplete = ({ planId }: { planId: string }) => { value: `${user.id}`, }))} onOptionClick={({ selectionValue }) => { - console.log('Selected user to add as watcher:', selectionValue); + addUser({ + pid: planId, + body: { users: [{ id: Number(selectionValue) }] }, + }) + .unwrap() + .catch(() => { + addToast( + ({ close }) => ( + + ), + { placement: 'top' } + ); + }); setInputValue(''); }} /> diff --git a/src/pages/Plan/Controls/WatcherList/UserItem.tsx b/src/pages/Plan/Controls/WatcherList/UserItem.tsx index 4534eb42c..71f0c3b40 100644 --- a/src/pages/Plan/Controls/WatcherList/UserItem.tsx +++ b/src/pages/Plan/Controls/WatcherList/UserItem.tsx @@ -5,6 +5,7 @@ import { IconButton, MD, SM, + useToast, } from '@appquality/unguess-design-system'; import { t } from 'i18next'; import { appTheme } from 'src/app/theme'; @@ -12,6 +13,7 @@ import { ReactComponent as XStroke } from 'src/assets/icons/x-stroke.svg'; import { getInitials } from 'src/common/components/navigation/header/utils'; import { prepareGravatar } from 'src/common/utils'; import styled from 'styled-components'; +import { useRemoveWatcher } from './hooks/useRemoveWatcher'; const StyledEllipsis = styled(Ellipsis)``; @@ -46,8 +48,10 @@ const UserAvatar = ({ }; const UserItem = ({ + planId, user, }: { + planId: string; user: { id: number; name: string; @@ -58,6 +62,9 @@ const UserItem = ({ }; }) => { const { isMe } = user; + const { addToast } = useToast(); + + const { removeWatcher } = useRemoveWatcher(); return (
@@ -90,7 +97,11 @@ const UserItem = ({ )}
- alert('Remove watcher')}> + + removeWatcher({ planId, profileId: user.id.toString() }) + } + >
diff --git a/src/pages/Plan/Controls/WatcherList/UserList.tsx b/src/pages/Plan/Controls/WatcherList/UserList.tsx index 044415c3f..75fd7d3a7 100644 --- a/src/pages/Plan/Controls/WatcherList/UserList.tsx +++ b/src/pages/Plan/Controls/WatcherList/UserList.tsx @@ -46,6 +46,7 @@ const UserList = ({ planId }: { planId: string }) => { }) .map((user) => ( { const isWatching = useIsWatching({ planId }); + const { removeWatcher } = useRemoveWatcher(); + const [addUser] = usePostPlansByPidWatchersMutation(); + const { data: currentUser } = useGetUsersMeQuery(); const { t } = useTranslation(); + + if (!currentUser) return null; return ( - + <> + {isLastOne && isWatching && ( + + {/* the following div is necessary to make Tooltip work with disabled Button */} +
+ +
+
+ )} + ); }; From eccc5f6d2062dab5187195571fe5c6fde47ff863 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Mon, 3 Nov 2025 17:16:51 +0100 Subject: [PATCH 017/108] feat: Add tooltip for adding members during setup phase in watcher list --- src/locales/en/translation.json | 1 + src/locales/it/translation.json | 1 + 2 files changed, 2 insertions(+) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 070533368..4803f9272 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1169,6 +1169,7 @@ "__PLAN_PAGE_TAB_TARGET_TAB_TITLE": "Screen participants", "__PLAN_PAGE_TITLE": "Plan your activity", "__PLAN_PAGE_WATCHER_LIST_ADD_USER_ERROR_TOAST_MESSAGE": "Error while adding user", + "__PLAN_PAGE_WATCHER_LIST_MODAL_ADD_MEMBERS_INFO_TOOLTIP": "Only workspace members can be added during the setup phase", "__PLAN_PAGE_WATCHER_LIST_MODAL_FOLLOW_BUTTON": "Follow this activity", "__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_DISABLED_TOOLTIP": "At least one person must follow this activity ", "__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE": "Add workspace members", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index cc7216f5a..fe99ab20b 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1203,6 +1203,7 @@ "__PLAN_PAGE_TAB_TARGET_TAB_TITLE": "", "__PLAN_PAGE_TITLE": "Pianifica la tua attività", "__PLAN_PAGE_WATCHER_LIST_ADD_USER_ERROR_TOAST_MESSAGE": "", + "__PLAN_PAGE_WATCHER_LIST_MODAL_ADD_MEMBERS_INFO_TOOLTIP": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_FOLLOW_BUTTON": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_DISABLED_TOOLTIP": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE": "", From ca4159fac82dae958a3c8f2358ba82c7fef16d2e Mon Sep 17 00:00:00 2001 From: Kariamos Date: Mon, 3 Nov 2025 17:17:11 +0100 Subject: [PATCH 018/108] feat: Add tooltip for adding workspace members during setup phase in watcher list --- src/pages/Plan/Controls/WatcherList/index.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/pages/Plan/Controls/WatcherList/index.tsx b/src/pages/Plan/Controls/WatcherList/index.tsx index c60b8e117..a29bda9f2 100644 --- a/src/pages/Plan/Controls/WatcherList/index.tsx +++ b/src/pages/Plan/Controls/WatcherList/index.tsx @@ -2,6 +2,7 @@ import { Button, MD, Skeleton, + Tooltip, TooltipModal, } from '@appquality/unguess-design-system'; @@ -114,7 +115,19 @@ const WatcherList = ({ planId }: { planId: string }) => { 'Add workspace members' )} - + +
+ +
+
From ad9ab03a4844411b7b5398691d3a645c29131f1d Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 3 Nov 2025 17:43:44 +0100 Subject: [PATCH 019/108] feat: Add plan approval status handling in watcher list and user item components --- src/hooks/usePlan.ts | 12 +++- .../Plan/Controls/WatcherList/UserItem.tsx | 20 ++++--- .../Plan/Controls/WatcherList/UserList.tsx | 15 +++-- src/pages/Plan/Controls/WatcherList/index.tsx | 58 +++++++++++++------ src/pages/Plan/Controls/index.tsx | 2 +- 5 files changed, 75 insertions(+), 32 deletions(-) diff --git a/src/hooks/usePlan.ts b/src/hooks/usePlan.ts index 9170b728e..ea1861baf 100644 --- a/src/hooks/usePlan.ts +++ b/src/hooks/usePlan.ts @@ -135,4 +135,14 @@ const usePlanIsPurchasable = (planId?: string) => { return isPurchasable; }; -export { usePlan, usePlanIsDraft, usePlanIsPurchasable }; +const usePlanIsApproved = (planId?: string) => { + const { planComposedStatus } = usePlan(planId); + + const isApproved = + !!planComposedStatus && + ['PurchasedPlan', 'Accepted'].includes(planComposedStatus); + + return isApproved; +}; + +export { usePlan, usePlanIsDraft, usePlanIsPurchasable, usePlanIsApproved }; diff --git a/src/pages/Plan/Controls/WatcherList/UserItem.tsx b/src/pages/Plan/Controls/WatcherList/UserItem.tsx index 71f0c3b40..e46995a2c 100644 --- a/src/pages/Plan/Controls/WatcherList/UserItem.tsx +++ b/src/pages/Plan/Controls/WatcherList/UserItem.tsx @@ -5,13 +5,13 @@ import { IconButton, MD, SM, - useToast, } from '@appquality/unguess-design-system'; import { t } from 'i18next'; import { appTheme } from 'src/app/theme'; import { ReactComponent as XStroke } from 'src/assets/icons/x-stroke.svg'; import { getInitials } from 'src/common/components/navigation/header/utils'; import { prepareGravatar } from 'src/common/utils'; +import { usePlanIsApproved } from 'src/hooks/usePlan'; import styled from 'styled-components'; import { useRemoveWatcher } from './hooks/useRemoveWatcher'; @@ -62,7 +62,7 @@ const UserItem = ({ }; }) => { const { isMe } = user; - const { addToast } = useToast(); + const isApproved = usePlanIsApproved(planId); const { removeWatcher } = useRemoveWatcher(); return ( @@ -97,13 +97,15 @@ const UserItem = ({ )}
- - removeWatcher({ planId, profileId: user.id.toString() }) - } - > - - + {!isApproved && ( + + removeWatcher({ planId, profileId: user.id.toString() }) + } + > + + + )}
); diff --git a/src/pages/Plan/Controls/WatcherList/UserList.tsx b/src/pages/Plan/Controls/WatcherList/UserList.tsx index 75fd7d3a7..5786d7b60 100644 --- a/src/pages/Plan/Controls/WatcherList/UserList.tsx +++ b/src/pages/Plan/Controls/WatcherList/UserList.tsx @@ -7,6 +7,7 @@ import { useGetPlansByPidWatchersQuery, useGetUsersMeQuery, } from 'src/features/api'; +import { usePlanIsApproved } from 'src/hooks/usePlan'; import { UserItem } from './UserItem'; const UserItemContainer = styled.div` @@ -24,6 +25,7 @@ const UserList = ({ planId }: { planId: string }) => { pid: planId, }); + const isApproved = usePlanIsApproved(planId); if (isLoading) return ; if (!data || data.items.length === 0) return ; @@ -31,10 +33,15 @@ const UserList = ({ planId }: { planId: string }) => { return ( - {t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE', - 'People following this activity' - )} + {isApproved + ? t( + '__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE_APPROVED', + 'People followed setup phase' + ) + : t( + '__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE', + 'People following this activity' + )} {[...data.items] diff --git a/src/pages/Plan/Controls/WatcherList/index.tsx b/src/pages/Plan/Controls/WatcherList/index.tsx index c60b8e117..591ca5639 100644 --- a/src/pages/Plan/Controls/WatcherList/index.tsx +++ b/src/pages/Plan/Controls/WatcherList/index.tsx @@ -1,4 +1,5 @@ import { + Alert, Button, MD, Skeleton, @@ -14,6 +15,7 @@ import { ReactComponent as EyeIconFill } from 'src/assets/icons/eye-icon-fill.sv import { ReactComponent as EyeIcon } from 'src/assets/icons/eye-icon.svg'; import { ReactComponent as InfoIcon } from 'src/assets/icons/info-icon.svg'; import { useGetPlansByPidWatchersQuery } from 'src/features/api'; +import { usePlanIsApproved } from 'src/hooks/usePlan'; import { useIsWatching } from './hooks/useIsWatching'; import { MemberAddAutocomplete } from './MemberAddAutoComplete'; import { UserList } from './UserList'; @@ -48,6 +50,9 @@ const WatcherList = ({ planId }: { planId: string }) => { pid: planId, }); const watchersCount = watchers ? watchers.items.length : 0; + + const isApproved = usePlanIsApproved(planId); + return ( <> - -
- )} - + + const button = ( + ); + + if (isLastOne && isWatching) { + return ( + + {/* the following div is necessary to make Tooltip work with disabled Button */} +
{button}
+
+ ); + } + + return button; }; export { WatchButton }; From ab7349957d4062658492125a5f8b1bef437bcfab Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 3 Nov 2025 17:59:45 +0100 Subject: [PATCH 022/108] feat: Implement code changes for feature enhancement and bug fixes --- .../Plan/Controls/WatcherList/UserList.tsx | 35 ++- .../Controls/WatcherList/assets/Empty.svg | 203 ++++++++++++++++++ 2 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 src/pages/Plan/Controls/WatcherList/assets/Empty.svg diff --git a/src/pages/Plan/Controls/WatcherList/UserList.tsx b/src/pages/Plan/Controls/WatcherList/UserList.tsx index 5786d7b60..c910f139a 100644 --- a/src/pages/Plan/Controls/WatcherList/UserList.tsx +++ b/src/pages/Plan/Controls/WatcherList/UserList.tsx @@ -1,6 +1,6 @@ -import { MD, Skeleton } from '@appquality/unguess-design-system'; - -import { styled } from 'styled-components'; +import { MD, Skeleton, SM } from '@appquality/unguess-design-system'; +import { styled, useTheme } from 'styled-components'; +import { ReactComponent as Empty } from './assets/Empty.svg'; import { useTranslation } from 'react-i18next'; import { @@ -16,7 +16,34 @@ const UserItemContainer = styled.div` gap: ${({ theme }) => theme.space.xxs}; `; -const EmptyState = () => <>Empty; +const EmptyState = () => { + const { t } = useTranslation(); + const appTheme = useTheme(); + return ( +
+ + + {t( + '__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_TITLE', + 'Add yourself as a workspace member' + )} + + + {t( + '__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_DESCRIPTION', + 'Add your team so they stay updated too' + )} + +
+ ); +}; const UserList = ({ planId }: { planId: string }) => { const { t } = useTranslation(); diff --git a/src/pages/Plan/Controls/WatcherList/assets/Empty.svg b/src/pages/Plan/Controls/WatcherList/assets/Empty.svg new file mode 100644 index 000000000..a8d1ba37c --- /dev/null +++ b/src/pages/Plan/Controls/WatcherList/assets/Empty.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 32e301999309c1984c5231f1d030bb0cb1766387 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 3 Nov 2025 18:12:51 +0100 Subject: [PATCH 023/108] rework: Format --- .../Plan/Controls/WatcherList/UserItem.tsx | 32 ++++++------ .../Plan/Controls/WatcherList/UserList.tsx | 2 +- src/pages/Plan/Controls/WatcherList/index.tsx | 50 ++++++++++--------- 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/pages/Plan/Controls/WatcherList/UserItem.tsx b/src/pages/Plan/Controls/WatcherList/UserItem.tsx index d79bd861b..04a31fb58 100644 --- a/src/pages/Plan/Controls/WatcherList/UserItem.tsx +++ b/src/pages/Plan/Controls/WatcherList/UserItem.tsx @@ -112,22 +112,22 @@ const UserItem = ({ )}
- {isLastOne ? !isApproved && ( - - {/* the following div is necessary to make Tooltip work with disabled IconButton */} -
{iconButton}
-
- ) : ( - !isApproved && iconButton - )} + {isLastOne + ? !isApproved && ( + + {/* the following div is necessary to make Tooltip work with disabled IconButton */} +
{iconButton}
+
+ ) + : !isApproved && iconButton}
); diff --git a/src/pages/Plan/Controls/WatcherList/UserList.tsx b/src/pages/Plan/Controls/WatcherList/UserList.tsx index c910f139a..9f645d2c5 100644 --- a/src/pages/Plan/Controls/WatcherList/UserList.tsx +++ b/src/pages/Plan/Controls/WatcherList/UserList.tsx @@ -1,6 +1,5 @@ import { MD, Skeleton, SM } from '@appquality/unguess-design-system'; import { styled, useTheme } from 'styled-components'; -import { ReactComponent as Empty } from './assets/Empty.svg'; import { useTranslation } from 'react-i18next'; import { @@ -8,6 +7,7 @@ import { useGetUsersMeQuery, } from 'src/features/api'; import { usePlanIsApproved } from 'src/hooks/usePlan'; +import { ReactComponent as Empty } from './assets/Empty.svg'; import { UserItem } from './UserItem'; const UserItemContainer = styled.div` diff --git a/src/pages/Plan/Controls/WatcherList/index.tsx b/src/pages/Plan/Controls/WatcherList/index.tsx index 8d6d83668..7bd2e61a6 100644 --- a/src/pages/Plan/Controls/WatcherList/index.tsx +++ b/src/pages/Plan/Controls/WatcherList/index.tsx @@ -128,30 +128,32 @@ const WatcherList = ({ planId }: { planId: string }) => { - {!isApproved && ( -
- - {t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE', - 'Add workspace members' - )} - - -
- -
-
-
- -
)} + {!isApproved && ( + +
+ + {t( + '__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE', + 'Add workspace members' + )} + + +
+ +
+
+
+ +
+ )}
From e48eb3b254892d1aff6f4fd4325063b3fe6f0fa5 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 10:57:10 +0100 Subject: [PATCH 024/108] feat: Add Italian translations for watcher list notifications and alerts --- src/locales/en/translation.json | 7 +++++++ src/locales/it/translation.json | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 4803f9272..95ccdc9a7 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1168,16 +1168,23 @@ "__PLAN_PAGE_TAB_SUMMARY_TAB_TITLE": "Get expert feedback", "__PLAN_PAGE_TAB_TARGET_TAB_TITLE": "Screen participants", "__PLAN_PAGE_TITLE": "Plan your activity", + "__PLAN_PAGE_WATCHER_LIST_ADD_SELF_TOAST_MESSAGE": "You’ve started following this activity. Make sure notifications are enabled in your settings", "__PLAN_PAGE_WATCHER_LIST_ADD_USER_ERROR_TOAST_MESSAGE": "Error while adding user", + "__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TEXT": "Future notifications will relate to execution. Add or remove followers from the dashboard", + "__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TITLE": "Info", "__PLAN_PAGE_WATCHER_LIST_MODAL_ADD_MEMBERS_INFO_TOOLTIP": "Only workspace members can be added during the setup phase", "__PLAN_PAGE_WATCHER_LIST_MODAL_FOLLOW_BUTTON": "Follow this activity", + "__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_DESCRIPTION": "Add your team so they stay updated too", + "__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_TITLE": "Add yourself as a workspace member", "__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_DISABLED_TOOLTIP": "At least one person must follow this activity ", "__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE": "Add workspace members", "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE": "Stay updated on setup progress", "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION": "Follow this activity and turn on notifications to receive important email updates about changes", + "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION_APPROVED": "Activity setup is complete", "__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON": "Unfollow this activity", "__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON_DISABLED_TOOLTIP": "At least one person must follow this activity ", "__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE": "People following this activity", + "__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE_APPROVED": "People followed setup phase", "__PLAN_PAGE_WATCHER_LIST_REMOVE_LAST_USER_ERROR_TOAST_MESSAGE": "At least one person must follow this activity", "__PLAN_PAGE_WATCHER_LIST_REMOVE_USER_ERROR_TOAST_MESSAGE": "Error while removing user", "__PLAN_REQUEST_QUOTATION_CTA": "Submit Request", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index fe99ab20b..e69977c56 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1202,16 +1202,23 @@ "__PLAN_PAGE_TAB_SUMMARY_TAB_TITLE": "", "__PLAN_PAGE_TAB_TARGET_TAB_TITLE": "", "__PLAN_PAGE_TITLE": "Pianifica la tua attività", + "__PLAN_PAGE_WATCHER_LIST_ADD_SELF_TOAST_MESSAGE": "", "__PLAN_PAGE_WATCHER_LIST_ADD_USER_ERROR_TOAST_MESSAGE": "", + "__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TEXT": "", + "__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TITLE": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_ADD_MEMBERS_INFO_TOOLTIP": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_FOLLOW_BUTTON": "", + "__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_DESCRIPTION": "", + "__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_TITLE": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_DISABLED_TOOLTIP": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION": "", + "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION_APPROVED": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON_DISABLED_TOOLTIP": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE": "", + "__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE_APPROVED": "", "__PLAN_PAGE_WATCHER_LIST_REMOVE_LAST_USER_ERROR_TOAST_MESSAGE": "", "__PLAN_PAGE_WATCHER_LIST_REMOVE_USER_ERROR_TOAST_MESSAGE": "", "__PLAN_REQUEST_QUOTATION_CTA": "", From 794e2dba82876b3282f52889f5e63bdbb485b9c4 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 10:57:44 +0100 Subject: [PATCH 025/108] feat: Add toast notification for successful follow action in WatchButton --- .../Plan/Controls/WatcherList/WatchButton.tsx | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/pages/Plan/Controls/WatcherList/WatchButton.tsx b/src/pages/Plan/Controls/WatcherList/WatchButton.tsx index c4fe440d6..bbd5b7d59 100644 --- a/src/pages/Plan/Controls/WatcherList/WatchButton.tsx +++ b/src/pages/Plan/Controls/WatcherList/WatchButton.tsx @@ -1,4 +1,9 @@ -import { Button, Tooltip } from '@appquality/unguess-design-system'; +import { + Button, + Tooltip, + useToast, + Notification, +} from '@appquality/unguess-design-system'; import { useTranslation } from 'react-i18next'; import { ReactComponent as EyeIconFill } from 'src/assets/icons/eye-icon-fill.svg'; @@ -14,6 +19,7 @@ import { useRemoveWatcher } from './hooks/useRemoveWatcher'; const WatchButton = ({ planId }: { planId: string }) => { const isWatching = useIsWatching({ planId }); const isLastOne = useIsLastOne({ planId }); + const { addToast } = useToast(); const { removeWatcher } = useRemoveWatcher(); const [addUser] = usePostPlansByPidWatchersMutation(); const { data: currentUser } = useGetUsersMeQuery(); @@ -36,7 +42,24 @@ const WatchButton = ({ planId }: { planId: string }) => { addUser({ pid: planId, body: { users: [{ id: currentUser.profile_id }] }, - }); + }) + .unwrap() + .then(() => { + addToast( + ({ close }) => ( + + ), + { placement: 'top' } + ); + }); } }} > From 8fe0d6ac4fa77f29458546ad6ec639fcb443244b Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 11:06:44 +0100 Subject: [PATCH 026/108] feat: Update toast messages for following and unfollowing activities in translations --- src/locales/en/translation.json | 3 ++- src/locales/it/translation.json | 1 + .../WatcherList/hooks/useRemoveWatcher.tsx | 24 ++++++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 95ccdc9a7..bd9f9eebf 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1168,7 +1168,7 @@ "__PLAN_PAGE_TAB_SUMMARY_TAB_TITLE": "Get expert feedback", "__PLAN_PAGE_TAB_TARGET_TAB_TITLE": "Screen participants", "__PLAN_PAGE_TITLE": "Plan your activity", - "__PLAN_PAGE_WATCHER_LIST_ADD_SELF_TOAST_MESSAGE": "You’ve started following this activity. Make sure notifications are enabled in your settings", + "__PLAN_PAGE_WATCHER_LIST_ADD_SELF_TOAST_MESSAGE": "You’ve started following this activity", "__PLAN_PAGE_WATCHER_LIST_ADD_USER_ERROR_TOAST_MESSAGE": "Error while adding user", "__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TEXT": "Future notifications will relate to execution. Add or remove followers from the dashboard", "__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TITLE": "Info", @@ -1186,6 +1186,7 @@ "__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE": "People following this activity", "__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE_APPROVED": "People followed setup phase", "__PLAN_PAGE_WATCHER_LIST_REMOVE_LAST_USER_ERROR_TOAST_MESSAGE": "At least one person must follow this activity", + "__PLAN_PAGE_WATCHER_LIST_REMOVE_SELF_TOAST_MESSAGE": "You’ve unfollowed this activity", "__PLAN_PAGE_WATCHER_LIST_REMOVE_USER_ERROR_TOAST_MESSAGE": "Error while removing user", "__PLAN_REQUEST_QUOTATION_CTA": "Submit Request", "__PLAN_RULE_DUPLICATE_TOUCHPOINT_FORM_FACTORS": "Multiple touchpoints of the same type", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index e69977c56..35bbac520 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1220,6 +1220,7 @@ "__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE_APPROVED": "", "__PLAN_PAGE_WATCHER_LIST_REMOVE_LAST_USER_ERROR_TOAST_MESSAGE": "", + "__PLAN_PAGE_WATCHER_LIST_REMOVE_SELF_TOAST_MESSAGE": "", "__PLAN_PAGE_WATCHER_LIST_REMOVE_USER_ERROR_TOAST_MESSAGE": "", "__PLAN_REQUEST_QUOTATION_CTA": "", "__PLAN_RULE_DUPLICATE_TOUCHPOINT_FORM_FACTORS": "", diff --git a/src/pages/Plan/Controls/WatcherList/hooks/useRemoveWatcher.tsx b/src/pages/Plan/Controls/WatcherList/hooks/useRemoveWatcher.tsx index 075401144..c397be796 100644 --- a/src/pages/Plan/Controls/WatcherList/hooks/useRemoveWatcher.tsx +++ b/src/pages/Plan/Controls/WatcherList/hooks/useRemoveWatcher.tsx @@ -1,11 +1,15 @@ import { Notification, useToast } from '@appquality/unguess-design-system'; import { useTranslation } from 'react-i18next'; -import { useDeletePlansByPidWatchersAndProfileIdMutation } from 'src/features/api'; +import { + useDeletePlansByPidWatchersAndProfileIdMutation, + useGetUsersMeQuery, +} from 'src/features/api'; const useRemoveWatcher = () => { const [removeUser] = useDeletePlansByPidWatchersAndProfileIdMutation(); const { addToast } = useToast(); const { t } = useTranslation(); + const { data: currentUser } = useGetUsersMeQuery(); const removeWatcher = async ({ planId, @@ -16,6 +20,24 @@ const useRemoveWatcher = () => { }) => removeUser({ pid: planId, profileId }) .unwrap() + .then(() => { + if (currentUser?.profile_id.toString() === profileId) { + addToast( + ({ close }) => ( + + ), + { placement: 'top' } + ); + } + }) .catch((error) => { if (error.status === 406) { addToast( From fd9a50f81c80e9dbdff1c5679088f2e63153116a Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 11:09:38 +0100 Subject: [PATCH 027/108] feat: Add placeholder text for member selection in WatcherList component --- src/locales/en/translation.json | 1 + src/locales/it/translation.json | 1 + src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx | 3 +++ 3 files changed, 5 insertions(+) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index bd9f9eebf..422eab632 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1188,6 +1188,7 @@ "__PLAN_PAGE_WATCHER_LIST_REMOVE_LAST_USER_ERROR_TOAST_MESSAGE": "At least one person must follow this activity", "__PLAN_PAGE_WATCHER_LIST_REMOVE_SELF_TOAST_MESSAGE": "You’ve unfollowed this activity", "__PLAN_PAGE_WATCHER_LIST_REMOVE_USER_ERROR_TOAST_MESSAGE": "Error while removing user", + "__PLAN_PAGE_WATCHER_LIST_SELECT_ADD_MEMBERS_PLACEHOLDER": "Search by name or email", "__PLAN_REQUEST_QUOTATION_CTA": "Submit Request", "__PLAN_RULE_DUPLICATE_TOUCHPOINT_FORM_FACTORS": "Multiple touchpoints of the same type", "__PLAN_RULE_MODULE_TYPE": "Custom features added", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 35bbac520..089ed50ec 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1222,6 +1222,7 @@ "__PLAN_PAGE_WATCHER_LIST_REMOVE_LAST_USER_ERROR_TOAST_MESSAGE": "", "__PLAN_PAGE_WATCHER_LIST_REMOVE_SELF_TOAST_MESSAGE": "", "__PLAN_PAGE_WATCHER_LIST_REMOVE_USER_ERROR_TOAST_MESSAGE": "", + "__PLAN_PAGE_WATCHER_LIST_SELECT_ADD_MEMBERS_PLACEHOLDER": "", "__PLAN_REQUEST_QUOTATION_CTA": "", "__PLAN_RULE_DUPLICATE_TOUCHPOINT_FORM_FACTORS": "", "__PLAN_RULE_MODULE_TYPE": "", diff --git a/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx b/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx index ae024b5fe..f76bb89c4 100644 --- a/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx +++ b/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx @@ -77,6 +77,9 @@ const MemberAddAutocomplete = ({ planId }: { planId: string }) => { onInputChange={(value) => setInputValue(value)} inputValue={inputValue} selectionValue={null} + placeholder={t( + '__PLAN_PAGE_WATCHER_LIST_SELECT_ADD_MEMBERS_PLACEHOLDER' + )} options={users.map((user) => ({ children: , id: `user-${user.id}`, From 819577175378d1f2cd2c9ce1148f3a7f8e415f55 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 11:13:44 +0100 Subject: [PATCH 028/108] feat: Add error toast notification for failed self-follow action in WatchButton --- src/locales/en/translation.json | 1 + src/locales/it/translation.json | 1 + .../Plan/Controls/WatcherList/WatchButton.tsx | 16 ++++++++++++++++ 3 files changed, 18 insertions(+) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 422eab632..30af2404d 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1168,6 +1168,7 @@ "__PLAN_PAGE_TAB_SUMMARY_TAB_TITLE": "Get expert feedback", "__PLAN_PAGE_TAB_TARGET_TAB_TITLE": "Screen participants", "__PLAN_PAGE_TITLE": "Plan your activity", + "__PLAN_PAGE_WATCHER_LIST_ADD_SELF_TOAST_ERROR_MESSAGE": "Ops! Something went wrong. Please try again", "__PLAN_PAGE_WATCHER_LIST_ADD_SELF_TOAST_MESSAGE": "You’ve started following this activity", "__PLAN_PAGE_WATCHER_LIST_ADD_USER_ERROR_TOAST_MESSAGE": "Error while adding user", "__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TEXT": "Future notifications will relate to execution. Add or remove followers from the dashboard", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 089ed50ec..37f88f45d 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1202,6 +1202,7 @@ "__PLAN_PAGE_TAB_SUMMARY_TAB_TITLE": "", "__PLAN_PAGE_TAB_TARGET_TAB_TITLE": "", "__PLAN_PAGE_TITLE": "Pianifica la tua attività", + "__PLAN_PAGE_WATCHER_LIST_ADD_SELF_TOAST_ERROR_MESSAGE": "", "__PLAN_PAGE_WATCHER_LIST_ADD_SELF_TOAST_MESSAGE": "", "__PLAN_PAGE_WATCHER_LIST_ADD_USER_ERROR_TOAST_MESSAGE": "", "__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TEXT": "", diff --git a/src/pages/Plan/Controls/WatcherList/WatchButton.tsx b/src/pages/Plan/Controls/WatcherList/WatchButton.tsx index bbd5b7d59..5a056530d 100644 --- a/src/pages/Plan/Controls/WatcherList/WatchButton.tsx +++ b/src/pages/Plan/Controls/WatcherList/WatchButton.tsx @@ -59,6 +59,22 @@ const WatchButton = ({ planId }: { planId: string }) => { ), { placement: 'top' } ); + }) + .catch(() => { + addToast( + ({ close }) => ( + + ), + { placement: 'top' } + ); }); } }} From fbdaa7ca3bf68dd9bc8fad6e768114b46bb899df Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 11:16:50 +0100 Subject: [PATCH 029/108] feat: Update watcher list modal title description for clarity --- src/locales/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 30af2404d..9b08fbfbb 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1180,7 +1180,7 @@ "__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_DISABLED_TOOLTIP": "At least one person must follow this activity ", "__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE": "Add workspace members", "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE": "Stay updated on setup progress", - "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION": "Follow this activity and turn on notifications to receive important email updates about changes", + "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION": "Follow this activity and turn on notifications to receive important email updates", "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION_APPROVED": "Activity setup is complete", "__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON": "Unfollow this activity", "__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON_DISABLED_TOOLTIP": "At least one person must follow this activity ", From 20337b285376167da8ca3d6708a865bee277b26e Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 11:30:10 +0100 Subject: [PATCH 030/108] refactor: removed default translations in UserItem, UserList, WatchButton, and WatcherList components --- .../Plan/Controls/WatcherList/UserItem.tsx | 3 +- .../Plan/Controls/WatcherList/UserList.tsx | 22 +++----------- .../Plan/Controls/WatcherList/WatchButton.tsx | 13 ++------ src/pages/Plan/Controls/WatcherList/index.tsx | 30 +++++-------------- 4 files changed, 15 insertions(+), 53 deletions(-) diff --git a/src/pages/Plan/Controls/WatcherList/UserItem.tsx b/src/pages/Plan/Controls/WatcherList/UserItem.tsx index 04a31fb58..52d7b63d7 100644 --- a/src/pages/Plan/Controls/WatcherList/UserItem.tsx +++ b/src/pages/Plan/Controls/WatcherList/UserItem.tsx @@ -119,8 +119,7 @@ const UserItem = ({ type="light" size="medium" content={t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_DISABLED_TOOLTIP', - 'At least one person must follow this activity ' + '__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_DISABLED_TOOLTIP' )} > {/* the following div is necessary to make Tooltip work with disabled IconButton */} diff --git a/src/pages/Plan/Controls/WatcherList/UserList.tsx b/src/pages/Plan/Controls/WatcherList/UserList.tsx index 9f645d2c5..f341a2e2d 100644 --- a/src/pages/Plan/Controls/WatcherList/UserList.tsx +++ b/src/pages/Plan/Controls/WatcherList/UserList.tsx @@ -29,17 +29,9 @@ const EmptyState = () => { }} > - - {t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_TITLE', - 'Add yourself as a workspace member' - )} - + {t('__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_TITLE')} - {t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_DESCRIPTION', - 'Add your team so they stay updated too' - )} + {t('__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_DESCRIPTION')} ); @@ -61,14 +53,8 @@ const UserList = ({ planId }: { planId: string }) => { {isApproved - ? t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE_APPROVED', - 'People followed setup phase' - ) - : t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE', - 'People following this activity' - )} + ? t('__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE_APPROVED') + : t('__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE')} {[...data.items] diff --git a/src/pages/Plan/Controls/WatcherList/WatchButton.tsx b/src/pages/Plan/Controls/WatcherList/WatchButton.tsx index 5a056530d..18ce2c9f5 100644 --- a/src/pages/Plan/Controls/WatcherList/WatchButton.tsx +++ b/src/pages/Plan/Controls/WatcherList/WatchButton.tsx @@ -82,14 +82,8 @@ const WatchButton = ({ planId }: { planId: string }) => {
{isWatching ? : } {isWatching - ? t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON', - 'Unfollow this activity' - ) - : t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_FOLLOW_BUTTON', - 'Follow this activity' - )} + ? t('__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON') + : t('__PLAN_PAGE_WATCHER_LIST_MODAL_FOLLOW_BUTTON')}
); @@ -101,8 +95,7 @@ const WatchButton = ({ planId }: { planId: string }) => { type="light" size="medium" content={t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON_DISABLED_TOOLTIP', - 'At least one person must follow this activity' + '__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON_DISABLED_TOOLTIP' )} > {/* the following div is necessary to make Tooltip work with disabled Button */} diff --git a/src/pages/Plan/Controls/WatcherList/index.tsx b/src/pages/Plan/Controls/WatcherList/index.tsx index 7bd2e61a6..0a51d13b1 100644 --- a/src/pages/Plan/Controls/WatcherList/index.tsx +++ b/src/pages/Plan/Controls/WatcherList/index.tsx @@ -92,34 +92,22 @@ const WatcherList = ({ planId }: { planId: string }) => { > - {t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE', - 'Stay updated on setup progress' - )} + {t('__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE')} {isApproved - ? t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION_APPROVED', - 'Activity setup is complete' - ) - : t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION', - 'Follow this activity and turn on notifications to receive important email updates about changes' - )} + ? t('__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION_APPROVED') + : t('__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION')} {isApproved && ( - {t('__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TITLE', 'Info')} + {t('__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TITLE')} - {t( - '__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TEXT', - 'Future notifications will relate to execution. Add or remove followers from the dashboard' - )} + {t('__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TEXT')} )} {!isApproved && } @@ -132,18 +120,14 @@ const WatcherList = ({ planId }: { planId: string }) => {
- {t( - '__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE', - 'Add workspace members' - )} + {t('__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE')}
From a75d7263031a0c319628174669948731ed047dc5 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 11:41:03 +0100 Subject: [PATCH 031/108] feat: Enhance watcher list modal title description with notification link --- src/locales/en/translation.json | 2 +- src/pages/Plan/Controls/WatcherList/index.tsx | 22 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 9b08fbfbb..f8c53319d 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1180,7 +1180,7 @@ "__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_DISABLED_TOOLTIP": "At least one person must follow this activity ", "__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE": "Add workspace members", "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE": "Stay updated on setup progress", - "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION": "Follow this activity and turn on notifications to receive important email updates", + "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION": "Follow this activity and turn on notifications to receive important email updates", "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION_APPROVED": "Activity setup is complete", "__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON": "Unfollow this activity", "__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON_DISABLED_TOOLTIP": "At least one person must follow this activity ", diff --git a/src/pages/Plan/Controls/WatcherList/index.tsx b/src/pages/Plan/Controls/WatcherList/index.tsx index 0a51d13b1..557e184a2 100644 --- a/src/pages/Plan/Controls/WatcherList/index.tsx +++ b/src/pages/Plan/Controls/WatcherList/index.tsx @@ -1,5 +1,6 @@ import { Alert, + Anchor, Button, MD, Skeleton, @@ -11,7 +12,7 @@ import { useRef, useState } from 'react'; import { Divider } from 'src/common/components/divider'; import { styled, useTheme } from 'styled-components'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { ReactComponent as EyeIconFill } from 'src/assets/icons/eye-icon-fill.svg'; import { ReactComponent as EyeIcon } from 'src/assets/icons/eye-icon.svg'; import { ReactComponent as InfoIcon } from 'src/assets/icons/info-icon.svg'; @@ -98,9 +99,22 @@ const WatcherList = ({ planId }: { planId: string }) => { - {isApproved - ? t('__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION_APPROVED') - : t('__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION')} + {isApproved ? ( + t('__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION_APPROVED') + ) : ( + + ), + }} + /> + )} {isApproved && ( From a18a4295b915605e525a9c06f46db31b8b7bc68a Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 11:56:32 +0100 Subject: [PATCH 032/108] refactor: remove unused toast notification hook from UserItem component --- src/pages/Plan/Controls/WatcherList/UserItem.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/Plan/Controls/WatcherList/UserItem.tsx b/src/pages/Plan/Controls/WatcherList/UserItem.tsx index 52d7b63d7..26219d13c 100644 --- a/src/pages/Plan/Controls/WatcherList/UserItem.tsx +++ b/src/pages/Plan/Controls/WatcherList/UserItem.tsx @@ -65,7 +65,6 @@ const UserItem = ({ }; }) => { const { isMe } = user; - const { addToast } = useToast(); const isLastOne = useIsLastOne({ planId }); const isApproved = usePlanIsApproved(planId); From dc7d7fd9fe54dd67beb8d5c79ffe4abcfc3ae6a1 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 13:03:48 +0100 Subject: [PATCH 033/108] fix: update eye-icon-slash.svg for improved visual consistency --- src/assets/icons/eye-icon-slash.svg | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/assets/icons/eye-icon-slash.svg b/src/assets/icons/eye-icon-slash.svg index a36fcfb6c..906f48a59 100644 --- a/src/assets/icons/eye-icon-slash.svg +++ b/src/assets/icons/eye-icon-slash.svg @@ -1,5 +1,4 @@ - - - + + From 618a42324b5ab3cd74c2e9418a2df69d86a84ec9 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 13:05:09 +0100 Subject: [PATCH 034/108] refactor: improve icon handling and color logic in WatchButton component --- src/pages/Plan/Controls/WatcherList/WatchButton.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pages/Plan/Controls/WatcherList/WatchButton.tsx b/src/pages/Plan/Controls/WatcherList/WatchButton.tsx index 18ce2c9f5..0d0a9e782 100644 --- a/src/pages/Plan/Controls/WatcherList/WatchButton.tsx +++ b/src/pages/Plan/Controls/WatcherList/WatchButton.tsx @@ -5,6 +5,7 @@ import { Notification, } from '@appquality/unguess-design-system'; import { useTranslation } from 'react-i18next'; +import { appTheme } from 'src/app/theme'; import { ReactComponent as EyeIconFill } from 'src/assets/icons/eye-icon-fill.svg'; import { ReactComponent as EyeIconSlash } from 'src/assets/icons/eye-icon-slash.svg'; @@ -25,6 +26,14 @@ const WatchButton = ({ planId }: { planId: string }) => { const { data: currentUser } = useGetUsersMeQuery(); const { t } = useTranslation(); + const iconColor = (() => { + if (!isWatching) return '#fff'; + if (isLastOne) return appTheme.palette.grey[400]; + return undefined; + })(); + + const EyeIcon = isWatching ? EyeIconSlash : EyeIconFill; + if (!currentUser) return null; const button = ( @@ -80,7 +89,8 @@ const WatchButton = ({ planId }: { planId: string }) => { }} >
- {isWatching ? : } + + {isWatching ? t('__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON') : t('__PLAN_PAGE_WATCHER_LIST_MODAL_FOLLOW_BUTTON')} From 62bbf874bd5e34c1b5b4c2fa963b92e9ddf788fb Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 13:57:45 +0100 Subject: [PATCH 035/108] feat: add Italian translations for watcher list modal messages --- src/locales/en/translation.json | 3 +++ src/locales/it/translation.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index f8c53319d..3c1c2266f 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1177,7 +1177,10 @@ "__PLAN_PAGE_WATCHER_LIST_MODAL_FOLLOW_BUTTON": "Follow this activity", "__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_DESCRIPTION": "Add your team so they stay updated too", "__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_TITLE": "Add yourself as a workspace member", + "__PLAN_PAGE_WATCHER_LIST_MODAL_ONLY_ONE_WATCHER_DESCRIPTION": "Add your team so they stay updated too", + "__PLAN_PAGE_WATCHER_LIST_MODAL_ONLY_ONE_WATCHER_TITLE": "Only one person is following this activity!", "__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_DISABLED_TOOLTIP": "At least one person must follow this activity ", + "__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_TOOLTIP": "If you remove this person, they will no longer receive email updates about this activity", "__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE": "Add workspace members", "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE": "Stay updated on setup progress", "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION": "Follow this activity and turn on notifications to receive important email updates", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 37f88f45d..aa23b0ac3 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1211,7 +1211,10 @@ "__PLAN_PAGE_WATCHER_LIST_MODAL_FOLLOW_BUTTON": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_DESCRIPTION": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_TITLE": "", + "__PLAN_PAGE_WATCHER_LIST_MODAL_ONLY_ONE_WATCHER_DESCRIPTION": "", + "__PLAN_PAGE_WATCHER_LIST_MODAL_ONLY_ONE_WATCHER_TITLE": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_DISABLED_TOOLTIP": "", + "__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_TOOLTIP": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE": "", "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION": "", From b1c7fdf2f26cd753597d6002145b5ba90ccad332 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 13:58:03 +0100 Subject: [PATCH 036/108] feat: enhance EmptyState component to display messages based on watcher count --- .../Plan/Controls/WatcherList/UserList.tsx | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/pages/Plan/Controls/WatcherList/UserList.tsx b/src/pages/Plan/Controls/WatcherList/UserList.tsx index f341a2e2d..3a2f606b4 100644 --- a/src/pages/Plan/Controls/WatcherList/UserList.tsx +++ b/src/pages/Plan/Controls/WatcherList/UserList.tsx @@ -16,7 +16,7 @@ const UserItemContainer = styled.div` gap: ${({ theme }) => theme.space.xxs}; `; -const EmptyState = () => { +const EmptyState = ({ watchers }: { watchers?: number }) => { const { t } = useTranslation(); const appTheme = useTheme(); return ( @@ -29,10 +29,27 @@ const EmptyState = () => { }} > - {t('__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_TITLE')} - - {t('__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_DESCRIPTION')} - + {(!watchers || watchers === 0) && ( + <> + + {t('__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_TITLE')} + + + {t('__PLAN_PAGE_WATCHER_LIST_MODAL_NO_WATCHERS_DESCRIPTION')} + + + )} + + {watchers && watchers === 1 && ( + <> + + {t('__PLAN_PAGE_WATCHER_LIST_MODAL_ONLY_ONE_WATCHER_TITLE')} + + + {t('__PLAN_PAGE_WATCHER_LIST_MODAL_ONLY_ONE_WATCHER_DESCRIPTION')} + + + )}
); }; @@ -75,6 +92,7 @@ const UserList = ({ planId }: { planId: string }) => { }} /> ))} + {data.items.length === 1 && } ); }; From f49e1c0c792143f9d8c461569bad21f2c9e82b59 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 13:58:11 +0100 Subject: [PATCH 037/108] feat: add tooltip to disabled remove button in UserItem component --- src/pages/Plan/Controls/WatcherList/UserItem.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/pages/Plan/Controls/WatcherList/UserItem.tsx b/src/pages/Plan/Controls/WatcherList/UserItem.tsx index 26219d13c..946bed3a8 100644 --- a/src/pages/Plan/Controls/WatcherList/UserItem.tsx +++ b/src/pages/Plan/Controls/WatcherList/UserItem.tsx @@ -125,7 +125,19 @@ const UserItem = ({
{iconButton}
) - : !isApproved && iconButton} + : !isApproved && ( + + {/* the following div is necessary to make Tooltip work with disabled IconButton */} +
{iconButton}
+
+ )}
); From d90aee99bee8446553485175a597de2ad9751585 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 14:02:13 +0100 Subject: [PATCH 038/108] fix: adjust button size in WatcherList component for consistency --- src/pages/Plan/Controls/WatcherList/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Plan/Controls/WatcherList/index.tsx b/src/pages/Plan/Controls/WatcherList/index.tsx index 557e184a2..acbd81a16 100644 --- a/src/pages/Plan/Controls/WatcherList/index.tsx +++ b/src/pages/Plan/Controls/WatcherList/index.tsx @@ -60,6 +60,7 @@ const WatcherList = ({ planId }: { planId: string }) => { + +
); }; From 9c92e79dcdb2e17a913b430cde9359e64a2657a4 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 15:40:51 +0100 Subject: [PATCH 041/108] feat: add tooltip to watcher list button for enhanced user guidance --- src/locales/en/translation.json | 2 + src/locales/it/translation.json | 2 + src/pages/Plan/Controls/WatcherList/index.tsx | 67 ++++++++++++------- 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 71ee21535..0e233f024 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1190,6 +1190,8 @@ "__PLAN_PAGE_WATCHER_LIST_REMOVE_SELF_TOAST_MESSAGE": "You’ve unfollowed this activity", "__PLAN_PAGE_WATCHER_LIST_REMOVE_USER_ERROR_TOAST_MESSAGE": "Error while removing user", "__PLAN_PAGE_WATCHER_LIST_SELECT_ADD_MEMBERS_PLACEHOLDER": "Search by name or email", + "__PLAN_PAGE_WATCHER_LIST_TOOLTIP": "Follow this activity", + "__PLAN_PAGE_WATCHER_LIST_TOOLTIP_DESCRIPTION": "Stay updated with important email notifications", "__PLAN_REQUEST_QUOTATION_CTA": "Submit", "__PLAN_RULE_DUPLICATE_TOUCHPOINT_FORM_FACTORS": "Multiple touchpoints of the same type", "__PLAN_RULE_MODULE_TYPE": "Custom features added", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 6be60dc05..424110298 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1224,6 +1224,8 @@ "__PLAN_PAGE_WATCHER_LIST_REMOVE_SELF_TOAST_MESSAGE": "", "__PLAN_PAGE_WATCHER_LIST_REMOVE_USER_ERROR_TOAST_MESSAGE": "", "__PLAN_PAGE_WATCHER_LIST_SELECT_ADD_MEMBERS_PLACEHOLDER": "", + "__PLAN_PAGE_WATCHER_LIST_TOOLTIP": "", + "__PLAN_PAGE_WATCHER_LIST_TOOLTIP_DESCRIPTION": "", "__PLAN_REQUEST_QUOTATION_CTA": "", "__PLAN_RULE_DUPLICATE_TOUCHPOINT_FORM_FACTORS": "", "__PLAN_RULE_MODULE_TYPE": "", diff --git a/src/pages/Plan/Controls/WatcherList/index.tsx b/src/pages/Plan/Controls/WatcherList/index.tsx index 557e184a2..78fdce1d4 100644 --- a/src/pages/Plan/Controls/WatcherList/index.tsx +++ b/src/pages/Plan/Controls/WatcherList/index.tsx @@ -4,6 +4,7 @@ import { Button, MD, Skeleton, + Span, Tooltip, TooltipModal, } from '@appquality/unguess-design-system'; @@ -57,33 +58,47 @@ const WatcherList = ({ planId }: { planId: string }) => { return ( <> - + + Date: Tue, 4 Nov 2025 15:41:00 +0100 Subject: [PATCH 042/108] refactor: remove unused StyledPipe component from Controls --- src/pages/Plan/Controls/index.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/pages/Plan/Controls/index.tsx b/src/pages/Plan/Controls/index.tsx index ddf30a6e8..246197528 100644 --- a/src/pages/Plan/Controls/index.tsx +++ b/src/pages/Plan/Controls/index.tsx @@ -20,12 +20,6 @@ import { RequestQuotationButton } from './RequestQuotationButton'; import { SaveConfigurationButton } from './SaveConfigurationButton'; import { WatcherList } from './WatcherList'; -const StyledPipe = styled(Pipe)` - display: inline; - margin: 0; - height: auto; -`; - export const Controls = () => { const { t } = useTranslation(); const [isRequestQuotationModalOpen, setRequestQuotationModalOpen] = @@ -88,7 +82,6 @@ export const Controls = () => { planComposedStatus === 'UnquotedDraft') && ( <> - )} From be60264a9a71ba1367efe2afa4187f25b1e8a306 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 4 Nov 2025 15:44:58 +0100 Subject: [PATCH 043/108] refactor: remove unused imports and optimize SaveConfigurationButton component --- src/pages/Plan/Controls/SaveConfigurationButton.tsx | 2 +- src/pages/Plan/Controls/WatcherList/index.tsx | 1 - src/pages/Plan/Controls/index.tsx | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/Plan/Controls/SaveConfigurationButton.tsx b/src/pages/Plan/Controls/SaveConfigurationButton.tsx index 7ac1c7b6a..8719e8d29 100644 --- a/src/pages/Plan/Controls/SaveConfigurationButton.tsx +++ b/src/pages/Plan/Controls/SaveConfigurationButton.tsx @@ -4,12 +4,12 @@ import { Tooltip, useToast, } from '@appquality/unguess-design-system'; +import { ReactComponent as SaveIcon } from 'src/assets/icons/save-icon.svg'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { useSubmit } from 'src/features/modules/useModuleConfiguration'; import { useValidateForm } from 'src/features/planModules'; import { usePlan, usePlanIsDraft } from '../../../hooks/usePlan'; -import { ReactComponent as SaveIcon } from 'src/assets/icons/save-icon.svg'; const SaveConfigurationButton = () => { const { t } = useTranslation(); diff --git a/src/pages/Plan/Controls/WatcherList/index.tsx b/src/pages/Plan/Controls/WatcherList/index.tsx index 78fdce1d4..799a91f78 100644 --- a/src/pages/Plan/Controls/WatcherList/index.tsx +++ b/src/pages/Plan/Controls/WatcherList/index.tsx @@ -4,7 +4,6 @@ import { Button, MD, Skeleton, - Span, Tooltip, TooltipModal, } from '@appquality/unguess-design-system'; diff --git a/src/pages/Plan/Controls/index.tsx b/src/pages/Plan/Controls/index.tsx index 246197528..0e3daa63d 100644 --- a/src/pages/Plan/Controls/index.tsx +++ b/src/pages/Plan/Controls/index.tsx @@ -3,9 +3,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; -import { Pipe } from 'src/common/components/Pipe'; import { useModule } from 'src/features/modules/useModule'; -import styled from 'styled-components'; import { useSubmit } from '../../../features/modules/useModuleConfiguration'; import { usePlan, usePlanIsPurchasable } from '../../../hooks/usePlan'; import { usePlanContext } from '../context/planContext'; From 068d056d8ea9718027b63cdffbbf5c918025c34a Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 4 Nov 2025 17:07:38 +0100 Subject: [PATCH 044/108] feat: Add basic structure for watchers on submit --- src/locales/en/translation.json | 5 + src/locales/it/translation.json | 5 + src/pages/Plan/modals/SendRequestModal.tsx | 23 ++++- src/pages/Plan/modals/Watchers.tsx | 103 +++++++++++++++++++++ 4 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 src/pages/Plan/modals/Watchers.tsx diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index f8c53319d..279384695 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -849,6 +849,11 @@ "__PLAN_PAGE_MODAL_SEND_REQUEST_TITLE_LABEL": "Activity title", "__PLAN_PAGE_MODAL_SEND_REQUEST_TOAST_ERROR": "Something went wrong while requesting the activity", "__PLAN_PAGE_MODAL_SEND_REQUEST_WAIT": "Setting up your requestWe're preparing everything for our experts to review. This will take just a few seconds.", + "__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_DESCRIPTION": "They’ll receive email updates at every stage", + "__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_ERROR": "Please add at least one team member to continue", + "__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_HINT": "You can add only workspace members in this phase", + "__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_LABEL": "Involve your team in this activity", + "__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_PLACEHOLDER": "Search for team members...", "__PLAN_PAGE_MODUL_GENERAL_REMOVE_MODAL_CANCEL": "Cancel", "__PLAN_PAGE_MODUL_GENERAL_REMOVE_MODAL_CONFIRM": "Confirm", "__PLAN_PAGE_MODUL_GENERAL_REMOVE_MODAL_DESCRIPTION": "If you delete the item, the entered information will no longer be recoverable.", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 37f88f45d..3498f0236 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -882,6 +882,11 @@ "__PLAN_PAGE_MODAL_SEND_REQUEST_TITLE_LABEL": "", "__PLAN_PAGE_MODAL_SEND_REQUEST_TOAST_ERROR": "", "__PLAN_PAGE_MODAL_SEND_REQUEST_WAIT": "", + "__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_DESCRIPTION": "", + "__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_ERROR": "", + "__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_HINT": "", + "__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_LABEL": "", + "__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_PLACEHOLDER": "", "__PLAN_PAGE_MODUL_GENERAL_REMOVE_MODAL_CANCEL": "", "__PLAN_PAGE_MODUL_GENERAL_REMOVE_MODAL_CONFIRM": "", "__PLAN_PAGE_MODUL_GENERAL_REMOVE_MODAL_DESCRIPTION": "", diff --git a/src/pages/Plan/modals/SendRequestModal.tsx b/src/pages/Plan/modals/SendRequestModal.tsx index 7b5135a60..de082e7a1 100644 --- a/src/pages/Plan/modals/SendRequestModal.tsx +++ b/src/pages/Plan/modals/SendRequestModal.tsx @@ -13,6 +13,7 @@ import { useToast, XL, } from '@appquality/unguess-design-system'; +import { useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; @@ -21,6 +22,7 @@ import { useRequestQuotation } from 'src/features/modules/useRequestQuotation'; import { useValidateForm } from 'src/features/planModules'; import { getModuleBySlug } from '../modules/Factory'; import { PurchasablePlanRulesGuide } from './PurchasablePlanRules'; +import { Watchers } from './Watchers'; const SendRequestModal = ({ onQuit, @@ -35,6 +37,7 @@ const SendRequestModal = ({ const { data, isLoading } = useGetPlansByPidRulesEvaluationQuery({ pid: planId || '', }); + const [watchers, setWatchers] = useState([]); const isFailed = isPurchasable && data && data.failed.length > 0; const { addToast } = useToast(); @@ -43,10 +46,13 @@ const SendRequestModal = ({ const { validateForm } = useValidateForm(); + if (!planId) return null; + const handleConfirm = async () => { try { - await validateForm(); - await handleQuoteRequest(); + console.log('Watchers IDs:', watchers); + // await validateForm(); + // await handleQuoteRequest(); } catch (e) { addToast( ({ close }) => ( @@ -154,6 +160,18 @@ const SendRequestModal = ({ {t('__PLAN_PAGE_MODAL_SEND_REQUEST_DATES_HINT')}
+
+ + + {t('__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_DESCRIPTION')} + + + + {t('__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_HINT')} + +
)} @@ -171,6 +189,7 @@ const SendRequestModal = ({ ); - if (isLastOne && isWatching) { + // condition is true only when one value is truthy and the other is falsy, otherwise it's false + if (!!hasWorkspaceAccess !== !!isLastWatcher) { return ( {/* the following div is necessary to make Tooltip work with disabled Button */}
{button}
From 5907d83d7f84e3503ed455c41cce416b8cfc111e Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 5 Nov 2025 11:05:22 +0100 Subject: [PATCH 047/108] feat: Add mutation for updating plan watchers and adjust watcher ID mapping --- src/features/api/index.ts | 20 ++++++++++++++++++++ src/pages/Plan/modals/SendRequestModal.tsx | 15 +++++++++++---- src/pages/Plan/modals/Watchers.tsx | 2 +- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/features/api/index.ts b/src/features/api/index.ts index 090acac64..fa9924f8b 100644 --- a/src/features/api/index.ts +++ b/src/features/api/index.ts @@ -802,6 +802,16 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.body, }), }), + putPlansByPidWatchers: build.mutation< + PutPlansByPidWatchersApiResponse, + PutPlansByPidWatchersApiArg + >({ + query: (queryArg) => ({ + url: `/plans/${queryArg.pid}/watchers`, + method: 'PUT', + body: queryArg.body, + }), + }), getWorkspacesByWidTemplates: build.query< GetWorkspacesByWidTemplatesApiResponse, GetWorkspacesByWidTemplatesApiArg @@ -2055,6 +2065,15 @@ export type PostPlansByPidWatchersApiArg = { }[]; }; }; +export type PutPlansByPidWatchersApiResponse = /** status 200 OK */ void; +export type PutPlansByPidWatchersApiArg = { + pid: string; + body: { + users: { + id: number; + }[]; + }; +}; export type GetWorkspacesByWidTemplatesApiResponse = /** status 200 OK */ { items: CpReqTemplate[]; } & PaginationData; @@ -3110,6 +3129,7 @@ export const { useGetWorkspacesByWidProjectsAndPidCampaignsQuery, useGetPlansByPidWatchersQuery, usePostPlansByPidWatchersMutation, + usePutPlansByPidWatchersMutation, useGetWorkspacesByWidTemplatesQuery, usePostWorkspacesByWidTemplatesMutation, useDeleteWorkspacesByWidTemplatesAndTidMutation, diff --git a/src/pages/Plan/modals/SendRequestModal.tsx b/src/pages/Plan/modals/SendRequestModal.tsx index de082e7a1..1ee7328ce 100644 --- a/src/pages/Plan/modals/SendRequestModal.tsx +++ b/src/pages/Plan/modals/SendRequestModal.tsx @@ -17,7 +17,10 @@ import { useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; -import { useGetPlansByPidRulesEvaluationQuery } from 'src/features/api'; +import { + useGetPlansByPidRulesEvaluationQuery, + usePutPlansByPidWatchersMutation, +} from 'src/features/api'; import { useRequestQuotation } from 'src/features/modules/useRequestQuotation'; import { useValidateForm } from 'src/features/planModules'; import { getModuleBySlug } from '../modules/Factory'; @@ -33,6 +36,7 @@ const SendRequestModal = ({ }) => { const { planId } = useParams(); const { t } = useTranslation(); + const [updateWatchers] = usePutPlansByPidWatchersMutation(); const { isRequestingQuote, handleQuoteRequest } = useRequestQuotation(); const { data, isLoading } = useGetPlansByPidRulesEvaluationQuery({ pid: planId || '', @@ -50,9 +54,12 @@ const SendRequestModal = ({ const handleConfirm = async () => { try { - console.log('Watchers IDs:', watchers); - // await validateForm(); - // await handleQuoteRequest(); + await updateWatchers({ + pid: planId, + body: { users: watchers.map((id) => ({ id })) }, + }).unwrap(); + await validateForm(); + await handleQuoteRequest(); } catch (e) { addToast( ({ close }) => ( diff --git a/src/pages/Plan/modals/Watchers.tsx b/src/pages/Plan/modals/Watchers.tsx index 3a3a0e601..1b55a9edf 100644 --- a/src/pages/Plan/modals/Watchers.tsx +++ b/src/pages/Plan/modals/Watchers.tsx @@ -36,7 +36,7 @@ const useOptions = (planId: string) => { if (data && users) { const watchersIds = (data?.items || []).map((watcher) => watcher.id); const options = (users?.items || []).map((user) => ({ - id: user.id, + id: user.profile_id, label: `${user.name}`, selected: watchersIds.includes(user.profile_id), })); From c45c0a5fee5182a64025d6e7f7ceeec529568828 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 5 Nov 2025 11:28:14 +0100 Subject: [PATCH 048/108] feat: Add put mutation for updating plan watchers with tag invalidation --- src/features/api/apiTags.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/features/api/apiTags.ts b/src/features/api/apiTags.ts index d4d59140d..74b6c4963 100644 --- a/src/features/api/apiTags.ts +++ b/src/features/api/apiTags.ts @@ -336,6 +336,9 @@ unguessApi.enhanceEndpoints({ postPlansByPidWatchers: { invalidatesTags: ['PlanWatchers'], }, + putPlansByPidWatchers: { + invalidatesTags: ['PlanWatchers'], + }, deletePlansByPidWatchersAndProfileId: { invalidatesTags: ['PlanWatchers'], }, From ecd78b3b20ab689c1df07468036ce3a502805311 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 5 Nov 2025 11:29:34 +0100 Subject: [PATCH 049/108] fix: Adjust condition in useEffect to simplify data dependency check --- src/pages/Plan/modals/Watchers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Plan/modals/Watchers.tsx b/src/pages/Plan/modals/Watchers.tsx index 1b55a9edf..11d9e2acd 100644 --- a/src/pages/Plan/modals/Watchers.tsx +++ b/src/pages/Plan/modals/Watchers.tsx @@ -33,7 +33,7 @@ const useOptions = (planId: string) => { ); const { data, isLoading } = useGetPlansByPidWatchersQuery({ pid: planId }); useEffect(() => { - if (data && users) { + if (users) { const watchersIds = (data?.items || []).map((watcher) => watcher.id); const options = (users?.items || []).map((user) => ({ id: user.profile_id, From 9185aa336668b3e83a692e1b1bd59555d6fee930 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 5 Nov 2025 15:04:28 +0100 Subject: [PATCH 050/108] feat: Remove pending users from select --- .../Controls/WatcherList/MemberAddAutoComplete.tsx | 2 ++ src/pages/Plan/modals/Watchers.tsx | 12 +++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx b/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx index f76bb89c4..3cf69923e 100644 --- a/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx +++ b/src/pages/Plan/Controls/WatcherList/MemberAddAutoComplete.tsx @@ -59,11 +59,13 @@ const MemberAddAutocomplete = ({ planId }: { planId: string }) => { return null; const users = data.items + .filter((user) => !user.invitationPending) .map((user) => ({ name: user.name, email: user.email, id: user.profile_id, })) + .filter( (user) => !watchers?.items.find((watcher) => watcher.id === user.id) && diff --git a/src/pages/Plan/modals/Watchers.tsx b/src/pages/Plan/modals/Watchers.tsx index 11d9e2acd..4f1c95849 100644 --- a/src/pages/Plan/modals/Watchers.tsx +++ b/src/pages/Plan/modals/Watchers.tsx @@ -35,11 +35,13 @@ const useOptions = (planId: string) => { useEffect(() => { if (users) { const watchersIds = (data?.items || []).map((watcher) => watcher.id); - const options = (users?.items || []).map((user) => ({ - id: user.profile_id, - label: `${user.name}`, - selected: watchersIds.includes(user.profile_id), - })); + const options = (users?.items || []) + .filter((user) => !user.invitationPending) + .map((user) => ({ + id: user.profile_id, + label: `${user.name}`, + selected: watchersIds.includes(user.profile_id), + })); setWatchers(options); } }, [data, users]); From 390b5dbce74b71d73372f8e5457e784d5fb1e3c8 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 5 Nov 2025 15:33:23 +0100 Subject: [PATCH 051/108] feat: Refactor WatchButton to use custom hook for workspace access --- .../Plan/Controls/WatcherList/WatchButton.tsx | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/pages/Plan/Controls/WatcherList/WatchButton.tsx b/src/pages/Plan/Controls/WatcherList/WatchButton.tsx index d81fedb49..e69cbfbeb 100644 --- a/src/pages/Plan/Controls/WatcherList/WatchButton.tsx +++ b/src/pages/Plan/Controls/WatcherList/WatchButton.tsx @@ -1,8 +1,8 @@ import { Button, + Notification, Tooltip, useToast, - Notification, } from '@appquality/unguess-design-system'; import { useTranslation } from 'react-i18next'; import { appTheme } from 'src/app/theme'; @@ -11,28 +11,50 @@ import { ReactComponent as EyeIconFill } from 'src/assets/icons/eye-icon-fill.sv import { ReactComponent as EyeIconSlash } from 'src/assets/icons/eye-icon-slash.svg'; import { useGetUsersMeQuery, + useGetWorkspacesByWidUsersQuery, usePostPlansByPidWatchersMutation, } from 'src/features/api'; -import { useCanAccessToActiveWorkspace } from 'src/hooks/useCanAccessToActiveWorkspace'; +import { useActiveWorkspace } from 'src/hooks/useActiveWorkspace'; import { useIsLastOne } from './hooks/useIsLastOne'; import { useIsWatching } from './hooks/useIsWatching'; import { useRemoveWatcher } from './hooks/useRemoveWatcher'; +const useHasWorkspaceAccess = () => { + const { activeWorkspace } = useActiveWorkspace(); + const { data: user } = useGetUsersMeQuery(); + + const { data } = useGetWorkspacesByWidUsersQuery( + { + wid: (activeWorkspace?.id || '0').toString(), + }, + { + skip: !activeWorkspace?.id, + } + ); + + return ( + (data?.items || []).find((u) => u.profile_id === user?.profile_id) !== + undefined + ); +}; + const WatchButton = ({ planId }: { planId: string }) => { const isWatching = useIsWatching({ planId }); const isLastOne = useIsLastOne({ planId }); const { addToast } = useToast(); const { removeWatcher } = useRemoveWatcher(); const [addUser] = usePostPlansByPidWatchersMutation(); - const hasWorkspaceAccess = useCanAccessToActiveWorkspace(); + const hasWorkspaceAccess = useHasWorkspaceAccess(); const { data: currentUser } = useGetUsersMeQuery(); const { t } = useTranslation(); const isLastWatcher = isWatching && isLastOne; + const isDisabled = !hasWorkspaceAccess || isLastWatcher; + const iconColor = (() => { + if (isDisabled) return appTheme.palette.grey[400]; if (!isWatching) return '#fff'; - if (isLastOne) return appTheme.palette.grey[400]; return undefined; })(); @@ -43,7 +65,7 @@ const WatchButton = ({ planId }: { planId: string }) => { const button = ( ); - // condition is true only when one value is truthy and the other is falsy, otherwise it's false - if (!!hasWorkspaceAccess !== !!isLastWatcher) { + if (isDisabled) { return ( { {t('__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE')} - + {isApproved ? ( diff --git a/src/pages/Plan/modals/SendRequestModal.tsx b/src/pages/Plan/modals/SendRequestModal.tsx index 1ee7328ce..123763485 100644 --- a/src/pages/Plan/modals/SendRequestModal.tsx +++ b/src/pages/Plan/modals/SendRequestModal.tsx @@ -10,6 +10,7 @@ import { Notification, Skeleton, SM, + Span, useToast, XL, } from '@appquality/unguess-design-system'; @@ -150,6 +151,8 @@ const SendRequestModal = ({ )}
+ * + <Message style={{ marginTop: appTheme.space.sm }}> {t('__PLAN_PAGE_MODAL_SEND_REQUEST_TITLE_HINT')} @@ -170,6 +173,7 @@ const SendRequestModal = ({ <div style={{ padding: `${appTheme.space.md} 0` }}> <Label style={{ marginBottom: appTheme.space.xxs }}> {t('__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_LABEL')} + <Span style={{ color: appTheme.palette.red[500] }}>*</Span> </Label> <SM style={{ marginBottom: appTheme.space.sm }}> {t('__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_DESCRIPTION')} From 4ecc6a45da99362e9e9e6e04cd9ee5ebb5af007a Mon Sep 17 00:00:00 2001 From: Kariamos <minucci9292@gmail.com> Date: Thu, 6 Nov 2025 10:47:55 +0100 Subject: [PATCH 056/108] feat: Update watcher list alert texts for clarity and improved user guidance --- src/locales/en/translation.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index b067e66ba..7674e6d7e 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1176,8 +1176,8 @@ "__PLAN_PAGE_WATCHER_LIST_ADD_SELF_TOAST_ERROR_MESSAGE": "Ops! Something went wrong. Please try again", "__PLAN_PAGE_WATCHER_LIST_ADD_SELF_TOAST_MESSAGE": "You’ve started following this activity", "__PLAN_PAGE_WATCHER_LIST_ADD_USER_ERROR_TOAST_MESSAGE": "Error while adding user", - "__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TEXT": "Future notifications will relate to execution. Add or remove followers from the dashboard", - "__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TITLE": "Info", + "__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TEXT": "Add or remove followers from the dashboard", + "__PLAN_PAGE_WATCHER_LIST_APPROVED_ALERT_TITLE": "You’ll keep receiving notifications while the activity is in progress", "__PLAN_PAGE_WATCHER_LIST_MODAL_ADD_MEMBERS_INFO_TOOLTIP": "Only workspace members can be added during the setup phase", "__PLAN_PAGE_WATCHER_LIST_MODAL_FOLLOW_BUTTON": "Follow this activity", "__PLAN_PAGE_WATCHER_LIST_MODAL_FOLLOW_BUTTON_DISABLED_TOOLTIP": "Become a workspace member to be included in updates", @@ -1187,10 +1187,10 @@ "__PLAN_PAGE_WATCHER_LIST_MODAL_ONLY_ONE_WATCHER_TITLE": "Only one person is following this activity!", "__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_DISABLED_TOOLTIP": "At least one person must follow this activity ", "__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_TOOLTIP": "If you remove this person, they will no longer receive email updates about this activity", - "__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE": "Add workspace members", - "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE": "Stay updated on setup progress", + "__PLAN_PAGE_WATCHER_LIST_MODAL_SUGGESTIONS_TITLE": "Add members from your workspace", + "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE": "Stay updated on the activity setup", "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION": "Follow this activity and <span>turn on notifications</span> to receive important email updates", - "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION_APPROVED": "Activity setup is complete", + "__PLAN_PAGE_WATCHER_LIST_MODAL_TITLE_DESCRIPTION_APPROVED": "Great job! You’ve completed the setup phase", "__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON": "Unfollow this activity", "__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON_DISABLED_TOOLTIP": "At least one person must follow this activity ", "__PLAN_PAGE_WATCHER_LIST_MODAL_WATCHERS_TITLE": "People following this activity", From 3fe939b0233634670b6c545370fc9e66b974f6ba Mon Sep 17 00:00:00 2001 From: Kariamos <minucci9292@gmail.com> Date: Thu, 6 Nov 2025 10:48:26 +0100 Subject: [PATCH 057/108] feat: Improve EmptyState rendering logic for single watcher scenario --- src/pages/Plan/Controls/WatcherList/UserList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/Plan/Controls/WatcherList/UserList.tsx b/src/pages/Plan/Controls/WatcherList/UserList.tsx index 3a2f606b4..876a87bfc 100644 --- a/src/pages/Plan/Controls/WatcherList/UserList.tsx +++ b/src/pages/Plan/Controls/WatcherList/UserList.tsx @@ -1,6 +1,5 @@ import { MD, Skeleton, SM } from '@appquality/unguess-design-system'; import { styled, useTheme } from 'styled-components'; - import { useTranslation } from 'react-i18next'; import { useGetPlansByPidWatchersQuery, @@ -19,6 +18,7 @@ const UserItemContainer = styled.div` const EmptyState = ({ watchers }: { watchers?: number }) => { const { t } = useTranslation(); const appTheme = useTheme(); + return ( <div style={{ @@ -92,7 +92,9 @@ const UserList = ({ planId }: { planId: string }) => { }} /> ))} - {data.items.length === 1 && <EmptyState watchers={data.items.length} />} + {data.items.length === 1 && !isApproved && ( + <EmptyState watchers={data.items.length} /> + )} </UserItemContainer> ); }; From 372afb63f6d06662cd1b80379c1899911e98728a Mon Sep 17 00:00:00 2001 From: ZecD <dominik.zecoli@gmail.com> Date: Thu, 6 Nov 2025 18:09:08 +0100 Subject: [PATCH 058/108] feat: add notification settings feature with UI components and translations --- src/assets/icons/email-icon.svg | 3 + src/locales/en/translation.json | 21 ++++ src/locales/it/translation.json | 21 ++++ .../Profile/FormNotificationSettings.tsx | 76 ++++++++++++ src/pages/Profile/index.tsx | 12 ++ .../FollowActivitiesPanel.tsx | 21 ++++ .../FollowActivitiesAccordion.tsx/index.tsx | 43 +++++++ .../parts/NotificationSettingsCard.tsx | 60 ++++++++++ .../CommunicationUpdatesPanel.tsx | 109 ++++++++++++++++++ .../parts/NotificationsAccordion/index.tsx | 35 ++++++ src/pages/Profile/parts/ProfileCard.tsx | 12 +- src/pages/Profile/parts/common.tsx | 8 ++ src/pages/Profile/valuesType.ts | 5 + 13 files changed, 418 insertions(+), 8 deletions(-) create mode 100644 src/assets/icons/email-icon.svg create mode 100644 src/pages/Profile/FormNotificationSettings.tsx create mode 100644 src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx create mode 100644 src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx create mode 100644 src/pages/Profile/parts/NotificationSettingsCard.tsx create mode 100644 src/pages/Profile/parts/NotificationsAccordion/CommunicationUpdatesPanel.tsx create mode 100644 src/pages/Profile/parts/NotificationsAccordion/index.tsx diff --git a/src/assets/icons/email-icon.svg b/src/assets/icons/email-icon.svg new file mode 100644 index 000000000..8cf0237ce --- /dev/null +++ b/src/assets/icons/email-icon.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M15 2H1C0.45 2 0 2.45 0 3V13C0 13.55 0.45 14 1 14H15C15.55 14 16 13.55 16 13V3C16 2.45 15.55 2 15 2ZM4.94 9.94L1.94 12.94C1.82 13.06 1.66 13.12 1.5 13.12C1.34 13.12 1.18 13.06 1.06 12.94C0.820119 12.6957 0.820119 12.3043 1.06 12.06L4.06 9.06C4.3 8.82 4.7 8.82 4.94 9.06C5.19 9.3 5.19 9.7 4.94 9.94ZM14.94 12.94C14.82 13.06 14.66 13.12 14.5 13.12C14.34 13.12 14.18 13.06 14.06 12.94L11.06 9.94C10.8201 9.69571 10.8201 9.30429 11.06 9.06C11.3 8.82 11.7 8.82 11.94 9.06L14.94 12.06C15.19 12.3 15.19 12.7 14.94 12.94ZM14.94 3.94L8.8 10.09C8.58 10.31 8.29 10.42 8 10.42C7.71 10.42 7.42 10.31 7.2 10.09L1.06 3.94C0.82 3.7 0.82 3.3 1.06 3.06C1.3 2.82 1.7 2.82 1.94 3.06L8 9.12L14.06 3.06C14.3 2.82 14.7 2.82 14.94 3.06C15.19 3.3 15.19 3.7 14.94 3.94Z" fill="#003A57"/> +</svg> diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index f8c53319d..5f3836c76 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1234,10 +1234,31 @@ "__PROFILE_PAGE_COMPANY_SIZE_REQUIRED_ERROR": "Company size is required", "__PROFILE_PAGE_CONFIRM_PASSWORD_MUST_MATCH_NEW_PASSWORD": "The confirmation password must match the new password", "__PROFILE_PAGE_NAME_REQUIRED_ERROR": "Name is required", + "__PROFILE_PAGE_NAV_ITEM_NOTIFICATION_SETTINGS": "Notification settings", "__PROFILE_PAGE_NAV_ITEM_PASSWORD": "Password settings", "__PROFILE_PAGE_NAV_ITEM_PROFILE": "Profile settings", "__PROFILE_PAGE_NAV_SECTION_PASSWORD": "PASSWORD", "__PROFILE_PAGE_NEW_PASSWORD_REQUIRED_ERROR": "New password is required", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_1": "When a quote is ready", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_2": "When a payment is confirmed", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_3": "When an activity is scheduled", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_4": "When an activity is completed", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_5": "When you’re mentioned in a comment", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_TITLE": "You’ll always receive:", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_CHECKBOX_HINT": "From execution to completion, including comments in threads you participate in", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_CHECKBOX_LABEL": "Activity progress", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_FORM_LABEL": "Receive updates for:", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_HINT": "Choose which progress notifications you’d like to receive by email for the activities you follow", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_LABEL": "Activity progress updates", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_TAG": "Recommended to keep enabled", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_HINT": "From configuration through planning", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_LABEL": "Activity setup", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_DESCRIPTION": "Manage which email updates you want to receive", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT": "You’ll receive updates for activities only for these activities", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_LABEL": "Activities you’re following", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_TAG": "tot.", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_LABEL": "Notification settings", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_SAVE_BUTTON_LABEL": "Save changes", "__PROFILE_PAGE_PASSWORD_ACCORDION_LABEL": "Password settings", "__PROFILE_PAGE_ROLE_REQUIRED_ERROR": "Role is required", "__PROFILE_PAGE_SURNAME_REQUIRED_ERROR": "Surname is required", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 37f88f45d..2296d3ced 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1268,10 +1268,31 @@ "__PROFILE_PAGE_COMPANY_SIZE_REQUIRED_ERROR": "", "__PROFILE_PAGE_CONFIRM_PASSWORD_MUST_MATCH_NEW_PASSWORD": "", "__PROFILE_PAGE_NAME_REQUIRED_ERROR": "", + "__PROFILE_PAGE_NAV_ITEM_NOTIFICATION_SETTINGS": "", "__PROFILE_PAGE_NAV_ITEM_PASSWORD": "", "__PROFILE_PAGE_NAV_ITEM_PROFILE": "", "__PROFILE_PAGE_NAV_SECTION_PASSWORD": "", "__PROFILE_PAGE_NEW_PASSWORD_REQUIRED_ERROR": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_1": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_2": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_3": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_4": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_5": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_TITLE": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_CHECKBOX_HINT": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_CHECKBOX_LABEL": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_FORM_LABEL": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_HINT": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_LABEL": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_TAG": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_HINT": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_LABEL": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_DESCRIPTION": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_LABEL": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_TAG": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_LABEL": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_SAVE_BUTTON_LABEL": "", "__PROFILE_PAGE_PASSWORD_ACCORDION_LABEL": "", "__PROFILE_PAGE_ROLE_REQUIRED_ERROR": "", "__PROFILE_PAGE_SURNAME_REQUIRED_ERROR": "", diff --git a/src/pages/Profile/FormNotificationSettings.tsx b/src/pages/Profile/FormNotificationSettings.tsx new file mode 100644 index 000000000..204fc42cb --- /dev/null +++ b/src/pages/Profile/FormNotificationSettings.tsx @@ -0,0 +1,76 @@ +import { useToast } from '@appquality/unguess-design-system'; +import { Formik } from 'formik'; +import { useTranslation } from 'react-i18next'; +import * as Yup from 'yup'; +import { NotificationSettingsFormValues } from './valuesType'; +import { NotificationSettingsCard } from './parts/NotificationSettingsCard'; + +export const FormNotificationSettings = () => { + const { t } = useTranslation(); + const { addToast } = useToast(); + + const initialValues: NotificationSettingsFormValues = { + activitySetupUpdates: true, + activityProgress: true, + }; + + /* if (isLoading) return <Loader />; */ + + const schema = Yup.object().shape({ + activitySetupUpdates: Yup.boolean(), + activityProgress: Yup.boolean(), + }); + + return ( + <Formik + initialValues={initialValues} + validationSchema={schema} + enableReinitialize + validateOnChange + onSubmit={async (values, actions) => { + actions.setSubmitting(true); + + /* updateProfile({ + body: { + name: values.name, + surname: values.surname, + roleId: values.roleId, + companySizeId: values.companySizeId, + }, + }) + .unwrap() + .then(() => { + addToast( + ({ close }) => ( + <Notification + onClose={close} + type="success" + message={t('__PROFILE_PAGE_UPDATE_SUCCESS')} + isPrimary + /> + ), + { placement: 'top' } + ); + }) + .catch(() => { + addToast( + ({ close }) => ( + <Notification + onClose={close} + type="error" + message={t('__PROFILE_PAGE_TOAST_ERROR_UPDATING_PROFILE')} + isPrimary + /> + ), + { placement: 'top' } + ); + }) + .finally(() => { + actions.setSubmitting(false); + }); */ + }} + > + <NotificationSettingsCard /> + </Formik> + ); +}; diff --git a/src/pages/Profile/index.tsx b/src/pages/Profile/index.tsx index e1c96cf73..186be6306 100644 --- a/src/pages/Profile/index.tsx +++ b/src/pages/Profile/index.tsx @@ -10,6 +10,7 @@ import { Page } from 'src/features/templates/Page'; import ProfilePageHeader from 'src/pages/Profile/Header'; import { FormPassword } from './FormPassword'; import { FormProfile } from './FormProfile'; +import { FormNotificationSettings } from './FormNotificationSettings'; const Profile = () => { const { t } = useTranslation(); @@ -37,6 +38,16 @@ const Profile = () => { > {t('__PROFILE_PAGE_NAV_ITEM_PROFILE')} </StickyNavItem> + <StickyNavItem + id="anchor-notification-settings" + to="anchor-notification-settings-id" + containerId="main" + smooth + duration={500} + offset={-30} + > + {t('__PROFILE_PAGE_NAV_ITEM_NOTIFICATION_SETTINGS')} + </StickyNavItem> <StickyNavItemLabel> {t('__PROFILE_PAGE_NAV_SECTION_PASSWORD')} @@ -68,6 +79,7 @@ const Profile = () => { }} > <FormProfile /> + <FormNotificationSettings /> <FormPassword /> </Col> </Row> diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx new file mode 100644 index 000000000..481d12210 --- /dev/null +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx @@ -0,0 +1,21 @@ +import { Label } from '@appquality/unguess-design-system'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +const StyledPanelCardContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.space.xs}; + margin-bottom: ${({ theme }) => theme.space.xxl}; +`; + +export const FollowActivitiesPanel = () => { + const { t } = useTranslation(); + return ( + <StyledPanelCardContainer> + <Label> + {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_DESCRIPTION')} + </Label> + </StyledPanelCardContainer> + ); +}; diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx new file mode 100644 index 000000000..5a1cb3afa --- /dev/null +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx @@ -0,0 +1,43 @@ +import { AccordionNew, Tag } from '@appquality/unguess-design-system'; +import { useTranslation } from 'react-i18next'; +import { appTheme } from 'src/app/theme'; +import { ReactComponent as EyeIcon } from 'src/assets/icons/eye-icon-fill.svg'; +import { FollowActivitiesPanel } from './FollowActivitiesPanel'; + +export const FollowActivitiesAccordion = () => { + const { t } = useTranslation(); + + return ( + <AccordionNew hasBorder={false} level={3} onChange={() => {}}> + <AccordionNew.Section> + <AccordionNew.Header + icon={ + <EyeIcon + style={{ + color: appTheme.palette.blue[600], + }} + /> + } + > + <AccordionNew.Label + label={t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_LABEL' + )} + subtitle={t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT' + )} + /> + <AccordionNew.Meta> + <Tag> + 4/5 + {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_TAG')} + </Tag> + </AccordionNew.Meta> + </AccordionNew.Header> + <AccordionNew.Panel> + <FollowActivitiesPanel /> + </AccordionNew.Panel> + </AccordionNew.Section> + </AccordionNew> + ); +}; diff --git a/src/pages/Profile/parts/NotificationSettingsCard.tsx b/src/pages/Profile/parts/NotificationSettingsCard.tsx new file mode 100644 index 000000000..fcc46ee49 --- /dev/null +++ b/src/pages/Profile/parts/NotificationSettingsCard.tsx @@ -0,0 +1,60 @@ +import { + Button, + ContainerCard, + LG, + MD, +} from '@appquality/unguess-design-system'; +import { useTranslation } from 'react-i18next'; +import { appTheme } from 'src/app/theme'; +import { ReactComponent as EmailIcon } from 'src/assets/icons/email-icon.svg'; + +import { Divider } from 'src/common/components/divider'; +import { + StyledCardHeader, + StyledNotificationsCardHeaderWrapper, +} from './common'; +import { NotificationSettingsAccordion } from './NotificationsAccordion'; +import { FollowActivitiesAccordion } from './FollowActivitiesAccordion.tsx'; + +export const NotificationSettingsCard = () => { + const { t } = useTranslation(); + return ( + <ContainerCard + id="anchor-notification-settings-id" + data-qa="notification-settings-card" + title={t('__PROFILE_PAGE_NOTIFICATIONS_CARD_LABEL')} + > + <div style={{ display: 'flex', flexDirection: 'column' }}> + <StyledNotificationsCardHeaderWrapper> + <StyledCardHeader> + <EmailIcon + style={{ + color: appTheme.palette.blue[600], + width: appTheme.iconSizes.md, + height: appTheme.iconSizes.md, + }} + /> + <LG isBold style={{ color: appTheme.palette.grey[800] }}> + {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_LABEL')} + </LG> + </StyledCardHeader> + <MD color={appTheme.palette.grey[600]}> + {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_DESCRIPTION')} + </MD> + </StyledNotificationsCardHeaderWrapper> + <Divider /> + <NotificationSettingsAccordion /> + <Button isAccent isPrimary style={{ alignSelf: 'flex-end' }}> + {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_SAVE_BUTTON_LABEL')} + </Button> + </div> + <Divider + style={{ + marginTop: appTheme.space.lg, + marginBottom: appTheme.space.lg, + }} + /> + <FollowActivitiesAccordion /> + </ContainerCard> + ); +}; diff --git a/src/pages/Profile/parts/NotificationsAccordion/CommunicationUpdatesPanel.tsx b/src/pages/Profile/parts/NotificationsAccordion/CommunicationUpdatesPanel.tsx new file mode 100644 index 000000000..faf96f893 --- /dev/null +++ b/src/pages/Profile/parts/NotificationsAccordion/CommunicationUpdatesPanel.tsx @@ -0,0 +1,109 @@ +import { + Alert, + Checkbox, + FormField, + Hint, + Label, + UnorderedList, +} from '@appquality/unguess-design-system'; +import { useTranslation } from 'react-i18next'; +import { appTheme } from 'src/app/theme'; +import styled from 'styled-components'; + +const StyledPanelContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.space.lg}; +`; + +const StyledCheckBoxContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.space.sm}; +`; + +export const CommunicationUpdatesPanel = () => { + const { t } = useTranslation(); + return ( + <StyledPanelContainer> + <StyledCheckBoxContainer> + <Label> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_FORM_LABEL' + )} + </Label> + + <FormField> + <Checkbox + role="checkbox" + key="all" + name="activitySetupUpdates" + onChange={() => {}} + > + <Label> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_LABEL' + )} + </Label> + <Hint> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_HINT' + )} + </Hint> + </Checkbox> + </FormField> + <FormField> + <Checkbox + role="checkbox" + key="all" + name="activityProgress" + onChange={() => {}} + > + <Label> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_CHECKBOX_LABEL' + )} + </Label> + <Hint> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_CHECKBOX_HINT' + )} + </Hint> + </Checkbox> + </FormField> + </StyledCheckBoxContainer> + <Alert type="info"> + <Alert.Title style={{ marginBottom: appTheme.space.xxs }}> + {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_TITLE')} + </Alert.Title> + <UnorderedList> + <UnorderedList.Item> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_1' + )} + </UnorderedList.Item> + <UnorderedList.Item> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_2' + )} + </UnorderedList.Item> + <UnorderedList.Item> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_3' + )} + </UnorderedList.Item> + <UnorderedList.Item> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_4' + )} + </UnorderedList.Item> + <UnorderedList.Item> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_ALERT_ITEM_5' + )} + </UnorderedList.Item> + </UnorderedList> + </Alert> + </StyledPanelContainer> + ); +}; diff --git a/src/pages/Profile/parts/NotificationsAccordion/index.tsx b/src/pages/Profile/parts/NotificationsAccordion/index.tsx new file mode 100644 index 000000000..3d64fdf42 --- /dev/null +++ b/src/pages/Profile/parts/NotificationsAccordion/index.tsx @@ -0,0 +1,35 @@ +import { AccordionNew, Tag } from '@appquality/unguess-design-system'; +import { useTranslation } from 'react-i18next'; +import { appTheme } from 'src/app/theme'; +import { CommunicationUpdatesPanel } from './CommunicationUpdatesPanel'; + +export const NotificationSettingsAccordion = () => { + const { t } = useTranslation(); + + return ( + <AccordionNew hasBorder={false} level={3} onChange={() => {}}> + <AccordionNew.Section> + <AccordionNew.Header> + <AccordionNew.Label + label={t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_LABEL' + )} + subtitle={t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_HINT' + )} + /> + <AccordionNew.Meta> + <Tag color={appTheme.palette.green[600]}> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_TAG' + )} + </Tag> + </AccordionNew.Meta> + </AccordionNew.Header> + <AccordionNew.Panel> + <CommunicationUpdatesPanel /> + </AccordionNew.Panel> + </AccordionNew.Section> + </AccordionNew> + ); +}; diff --git a/src/pages/Profile/parts/ProfileCard.tsx b/src/pages/Profile/parts/ProfileCard.tsx index 541298063..34a79a88d 100644 --- a/src/pages/Profile/parts/ProfileCard.tsx +++ b/src/pages/Profile/parts/ProfileCard.tsx @@ -1,5 +1,6 @@ import { Button, + ContainerCard, FormField, Input, Label, @@ -19,12 +20,7 @@ import { } from 'src/features/api'; import { ProfileFormValues } from '../valuesType'; import { Loader } from './cardLoader'; -import { - CardInnerPanel, - StyledCardHeader, - StyledContainerCard, - StyledFooter, -} from './common'; +import { CardInnerPanel, StyledCardHeader, StyledFooter } from './common'; export const ProfileCard = () => { const { t } = useTranslation(); @@ -40,7 +36,7 @@ export const ProfileCard = () => { if (userRoleIsLoading || userCompanySizesIsLoading) return <Loader />; return ( - <StyledContainerCard + <ContainerCard id="anchor-profile-id" data-qa="profile-card" title={t('__PROFILE_PAGE_USER_CARD_LABEL')} @@ -261,6 +257,6 @@ export const ProfileCard = () => { </Button> </StyledFooter> </CardInnerPanel> - </StyledContainerCard> + </ContainerCard> ); }; diff --git a/src/pages/Profile/parts/common.tsx b/src/pages/Profile/parts/common.tsx index d7d2f432d..6d75c52f1 100644 --- a/src/pages/Profile/parts/common.tsx +++ b/src/pages/Profile/parts/common.tsx @@ -10,6 +10,14 @@ export const StyledCardHeader = styled.div` align-items: center; gap: ${({ theme }) => theme.space.xs}; `; + +export const StyledNotificationsCardHeaderWrapper = styled(StyledCardHeader)` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: ${({ theme }) => theme.space.sm}; +`; + export const StyledContainerCard = styled(ContainerCard)` padding: ${({ theme }) => theme.space.md}; padding-bottom: ${({ theme }) => theme.space.lg}; diff --git a/src/pages/Profile/valuesType.ts b/src/pages/Profile/valuesType.ts index a436ec8f3..c265f73c7 100644 --- a/src/pages/Profile/valuesType.ts +++ b/src/pages/Profile/valuesType.ts @@ -8,6 +8,11 @@ export type ProfileFormValues = { companySizeId: number; }; +export type NotificationSettingsFormValues = { + activitySetupUpdates: boolean; + activityProgress: boolean; +}; + export type PasswordFormValues = { currentPassword: string; newPassword: string; From e51969998cb6b99ba44cc850f7b93bd1ac955e0b Mon Sep 17 00:00:00 2001 From: ZecD <dominik.zecoli@gmail.com> Date: Fri, 7 Nov 2025 09:33:16 +0100 Subject: [PATCH 059/108] feat: add follow activities section with button and description in FollowActivitiesPanel --- src/locales/en/translation.json | 3 +++ src/locales/it/translation.json | 3 +++ .../FollowActivitiesPanel.tsx | 20 ++++++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 3afe3ee5b..0cd7ae1cd 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1266,6 +1266,8 @@ "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_HINT": "From configuration through planning", "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_LABEL": "Activity setup", "__PROFILE_PAGE_NOTIFICATIONS_CARD_DESCRIPTION": "Manage which email updates you want to receive", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT": "Unfollow", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_DESCRIPTION": "Activity setup (3)", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT": "You’ll receive updates for activities only for these activities", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_LABEL": "Activities you’re following", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_TAG": "tot.", @@ -1469,6 +1471,7 @@ "_TOAST_UNPUBLISHED_MESSAGE": "successfully unpublished", "{{count}} bugs_one": "{{count}} bug", "{{count}} bugs_other": "{{count}} bugs", + "{{name}} (you)": "", "INSIGHT_PAGE_COLLECTION_OBSERVATIONS_LABEL": "Observations: <1>{{counter}}</1>", "INSIGHT_PAGE_COLLECTION_THEMES_LABEL": "Themes: <1>{{counter}}</1>", "INSIGHT_PAGE_COLLECTION_UNGROUPED_USECASE_SUBTITTLE": "Isolated Observations", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index d3445920b..c1caff29e 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1300,6 +1300,8 @@ "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_HINT": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_LABEL": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_DESCRIPTION": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_DESCRIPTION": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_LABEL": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_TAG": "", @@ -1512,6 +1514,7 @@ "{{count}} bugs_one": "", "{{count}} bugs_many": "", "{{count}} bugs_other": "", + "{{name}} (you)": "", "INSIGHT_PAGE_COLLECTION_OBSERVATIONS_LABEL": "Osservazioni: <1>{{counter}}</1>", "INSIGHT_PAGE_COLLECTION_THEMES_LABEL": "Temi: <1>{{counter}}</1>", "INSIGHT_PAGE_COLLECTION_UNGROUPED_USECASE_SUBTITTLE": "Osservazioni isolate", diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx index 481d12210..25e1bfed7 100644 --- a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx @@ -1,5 +1,6 @@ -import { Label } from '@appquality/unguess-design-system'; +import { Anchor, Button, Label, SM } from '@appquality/unguess-design-system'; import { useTranslation } from 'react-i18next'; + import styled from 'styled-components'; const StyledPanelCardContainer = styled.div` @@ -9,6 +10,14 @@ const StyledPanelCardContainer = styled.div` margin-bottom: ${({ theme }) => theme.space.xxl}; `; +const StyledActivityItem = styled.div` + padding-top: ${({ theme }) => theme.space.sm}; + padding-bottom: ${({ theme }) => theme.space.sm}; + display: flex; + align-items: center; + justify-content: space-between; +`; + export const FollowActivitiesPanel = () => { const { t } = useTranslation(); return ( @@ -16,6 +25,15 @@ export const FollowActivitiesPanel = () => { <Label> {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_DESCRIPTION')} </Label> + <StyledActivityItem> + <div> + <Anchor>Titolo Attivita</Anchor> + <SM>titolo progetto</SM> + </div> + <Button size="small" isBasic> + {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT')} + </Button> + </StyledActivityItem> </StyledPanelCardContainer> ); }; From 96456b86a061e222c392c6af46e1f623bcc09e4c Mon Sep 17 00:00:00 2001 From: ZecD <dominik.zecoli@gmail.com> Date: Fri, 7 Nov 2025 09:48:51 +0100 Subject: [PATCH 060/108] feat: update FollowActivitiesPanel with new descriptions and hint text for improved user guidance --- src/locales/en/translation.json | 4 +- src/locales/it/translation.json | 4 +- .../FollowActivitiesPanel.tsx | 77 +++++++++++++++---- 3 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 0cd7ae1cd..5597eb96e 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1267,9 +1267,11 @@ "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_LABEL": "Activity setup", "__PROFILE_PAGE_NOTIFICATIONS_CARD_DESCRIPTION": "Manage which email updates you want to receive", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT": "Unfollow", - "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_DESCRIPTION": "Activity setup (3)", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT": "You’ll receive updates for activities only for these activities", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT_TEXT": "Your changes are saved automatically", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_LABEL": "Activities you’re following", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_PROGRESS_DESCRIPTION": "Activity progress ", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_SETUP_DESCRIPTION": "Activity setup ", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_TAG": "tot.", "__PROFILE_PAGE_NOTIFICATIONS_CARD_LABEL": "Notification settings", "__PROFILE_PAGE_NOTIFICATIONS_CARD_SAVE_BUTTON_LABEL": "Save changes", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index c1caff29e..453afc667 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1301,9 +1301,11 @@ "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_LABEL": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_DESCRIPTION": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT": "", - "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_DESCRIPTION": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT_TEXT": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_LABEL": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_PROGRESS_DESCRIPTION": "", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_SETUP_DESCRIPTION": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_TAG": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_LABEL": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_SAVE_BUTTON_LABEL": "", diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx index 25e1bfed7..01d18a1f7 100644 --- a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx @@ -1,15 +1,28 @@ -import { Anchor, Button, Label, SM } from '@appquality/unguess-design-system'; +import { + Anchor, + Button, + Hint, + Label, + SM, +} from '@appquality/unguess-design-system'; import { useTranslation } from 'react-i18next'; +import { ReactComponent as InfoIcon } from 'src/assets/icons/info-icon.svg'; import styled from 'styled-components'; -const StyledPanelCardContainer = styled.div` +const StyledPanelSectionContainer = styled.div` display: flex; flex-direction: column; gap: ${({ theme }) => theme.space.xs}; margin-bottom: ${({ theme }) => theme.space.xxl}; `; +const StyledHintContainer = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.space.xs}; +`; + const StyledActivityItem = styled.div` padding-top: ${({ theme }) => theme.space.sm}; padding-bottom: ${({ theme }) => theme.space.sm}; @@ -21,19 +34,51 @@ const StyledActivityItem = styled.div` export const FollowActivitiesPanel = () => { const { t } = useTranslation(); return ( - <StyledPanelCardContainer> - <Label> - {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_DESCRIPTION')} - </Label> - <StyledActivityItem> - <div> - <Anchor>Titolo Attivita</Anchor> - <SM>titolo progetto</SM> - </div> - <Button size="small" isBasic> - {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT')} - </Button> - </StyledActivityItem> - </StyledPanelCardContainer> + <div> + <StyledPanelSectionContainer> + <Label> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_SETUP_DESCRIPTION' + )} + (3) + </Label> + <StyledActivityItem> + <div> + <Anchor>Titolo Attivita</Anchor> + <SM>titolo progetto</SM> + </div> + <Button size="small" isBasic> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT' + )} + </Button> + </StyledActivityItem> + </StyledPanelSectionContainer> + <StyledPanelSectionContainer> + <Label> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_PROGRESS_DESCRIPTION' + )} + (3) + </Label> + <StyledActivityItem> + <div> + <Anchor>Titolo Attivita</Anchor> + <SM>titolo progetto</SM> + </div> + <Button size="small" isBasic> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT' + )} + </Button> + </StyledActivityItem> + </StyledPanelSectionContainer> + <StyledHintContainer> + <InfoIcon /> + <Hint> + {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT_TEXT')} + </Hint> + </StyledHintContainer> + </div> ); }; From a132f512cc69e90375a00d1cd86e769d27af84e8 Mon Sep 17 00:00:00 2001 From: Kariamos <minucci9292@gmail.com> Date: Fri, 7 Nov 2025 10:39:10 +0100 Subject: [PATCH 061/108] feat: Add example JSON files for GET, POST, and PUT requests in watchers API --- .../pid/watchers/_get/200_Example_1.json | 19 +++++++++++++++++++ .../pid/watchers/_post/request_Example_1.json | 10 ++++++++++ .../pid/watchers/_put/request_Example_1.json | 10 ++++++++++ 3 files changed, 39 insertions(+) create mode 100644 tests/api/plans/pid/watchers/_get/200_Example_1.json create mode 100644 tests/api/plans/pid/watchers/_post/request_Example_1.json create mode 100644 tests/api/plans/pid/watchers/_put/request_Example_1.json diff --git a/tests/api/plans/pid/watchers/_get/200_Example_1.json b/tests/api/plans/pid/watchers/_get/200_Example_1.json new file mode 100644 index 000000000..9da3d6866 --- /dev/null +++ b/tests/api/plans/pid/watchers/_get/200_Example_1.json @@ -0,0 +1,19 @@ +{ + "items": [ + { + "id": 1, + "name": "Antonio", + "surname": "Arezzo", + "email": "antonio.arezzo@unguess.io", + "isInternal": true + }, + { + "id": 2, + "name": "Andrej", + "surname": "Kojmasky", + "email": "andrej.kojmasky@bear.org", + "isInternal": false, + "image": "https://picsum.photos/200" + } + ] +} diff --git a/tests/api/plans/pid/watchers/_post/request_Example_1.json b/tests/api/plans/pid/watchers/_post/request_Example_1.json new file mode 100644 index 000000000..feb744c28 --- /dev/null +++ b/tests/api/plans/pid/watchers/_post/request_Example_1.json @@ -0,0 +1,10 @@ +{ + "users": [ + { + "id": 1 + }, + { + "id": 2 + } + ] +} diff --git a/tests/api/plans/pid/watchers/_put/request_Example_1.json b/tests/api/plans/pid/watchers/_put/request_Example_1.json new file mode 100644 index 000000000..feb744c28 --- /dev/null +++ b/tests/api/plans/pid/watchers/_put/request_Example_1.json @@ -0,0 +1,10 @@ +{ + "users": [ + { + "id": 1 + }, + { + "id": 2 + } + ] +} From 25d1966a705ca4f8bc44358e87f434d47dfe9786 Mon Sep 17 00:00:00 2001 From: Davide Bizzi <davide.bizzi@unguess.io> Date: Fri, 7 Nov 2025 11:50:31 +0100 Subject: [PATCH 062/108] feat: Update JSON test files and add mock for watchers API --- tests/api/plans/pid/_get/200_approved.json | 8 +++----- .../plans/pid/_get/200_draft_complete.json | 8 +++----- .../_get/200_draft_complete_date_error.json | 6 ++---- .../pid/_get/200_draft_mandatory_only.json | 6 ++---- .../pid/_get/200_draft_missing_mandatory.json | 6 ++---- tests/api/plans/pid/_get/200_paying.json | 17 +++++------------ .../plans/pid/_get/200_pending_review.json | 6 ++---- .../pid/_get/200_pending_review_quoted.json | 6 ++---- .../pid/_get/200_pending_review_unquoted.json | 6 ++---- .../plans/pid/_patch/request_Example_1.json | 6 ++---- .../categories/_get/200_Example_1.json | 19 ++----------------- .../users/me/_get/200_Users_Me_example.json | 5 +---- .../wid/templates/_get/200_Example_1.json | 1 - .../200_global_and_private_templates.json | 9 --------- .../templates/_get/200_global_template.json | 1 - tests/e2e/plan/draft.spec.ts | 1 + tests/e2e/plan/draft_plan_info_card.spec.ts | 1 + tests/fixtures/pages/Plan/index.ts | 14 +++++++++++++- 18 files changed, 43 insertions(+), 83 deletions(-) diff --git a/tests/api/plans/pid/_get/200_approved.json b/tests/api/plans/pid/_get/200_approved.json index 0a8b1a508..d0e240062 100644 --- a/tests/api/plans/pid/_get/200_approved.json +++ b/tests/api/plans/pid/_get/200_approved.json @@ -132,14 +132,12 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs", - "id": "bug-1" + "title": "Search for bugs" }, { - "description": "description kind video", + "description": "description kind bug", "kind": "video", - "title": "Think aloud", - "id": "video-1" + "title": "Think aloud" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_draft_complete.json b/tests/api/plans/pid/_get/200_draft_complete.json index 8c640b96b..79cbe8b74 100644 --- a/tests/api/plans/pid/_get/200_draft_complete.json +++ b/tests/api/plans/pid/_get/200_draft_complete.json @@ -82,7 +82,7 @@ "output": [ { "gender": "female", - "percentage": 0 + "percentage": 100 } ], "type": "gender", @@ -155,14 +155,12 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs", - "id": "bug-1" + "title": "Search for bugs" }, { "description": "description kind bug", "kind": "video", - "title": "Think aloud", - "id": "video-1" + "title": "Think aloud" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_draft_complete_date_error.json b/tests/api/plans/pid/_get/200_draft_complete_date_error.json index 8197c8362..a1313ef1c 100644 --- a/tests/api/plans/pid/_get/200_draft_complete_date_error.json +++ b/tests/api/plans/pid/_get/200_draft_complete_date_error.json @@ -18,14 +18,12 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs", - "id": "bug-1" + "title": "Search for bugs" }, { "description": "description kind video", "kind": "video", - "title": "Think aloud", - "id": "video-1" + "title": "Think aloud" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_draft_mandatory_only.json b/tests/api/plans/pid/_get/200_draft_mandatory_only.json index 467c9c2df..17c5361ba 100644 --- a/tests/api/plans/pid/_get/200_draft_mandatory_only.json +++ b/tests/api/plans/pid/_get/200_draft_mandatory_only.json @@ -18,14 +18,12 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs", - "id": "bug-1" + "title": "Search for bugs" }, { "description": "description kind video", "kind": "video", - "title": "Think aloud", - "id": "video-1" + "title": "Think aloud" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_draft_missing_mandatory.json b/tests/api/plans/pid/_get/200_draft_missing_mandatory.json index d95c9d115..ae68fad70 100644 --- a/tests/api/plans/pid/_get/200_draft_missing_mandatory.json +++ b/tests/api/plans/pid/_get/200_draft_missing_mandatory.json @@ -6,14 +6,12 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs", - "id": "bug-1" + "title": "Search for bugs" }, { "description": "description kind bug", "kind": "video", - "title": "Think aloud", - "id": "video-1" + "title": "Think aloud" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_paying.json b/tests/api/plans/pid/_get/200_paying.json index 7b518c3c4..e19667113 100644 --- a/tests/api/plans/pid/_get/200_paying.json +++ b/tests/api/plans/pid/_get/200_paying.json @@ -132,14 +132,12 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs", - "id": "bug-1" + "title": "Search for bugs" }, { - "description": "description kind video", + "description": "description kind bug", "kind": "video", - "title": "Think aloud", - "id": "video-1" + "title": "Think aloud" } ], "type": "tasks", @@ -189,16 +187,11 @@ } ] }, - "id": 1, + "id": 13, "project": { "id": 90, "name": "MyProject" }, - "quote": { - "id": 98, - "status": "approved", - "value": "3600 €" - }, - "status": "paying", + "status": "pending_review", "workspace_id": 1 } diff --git a/tests/api/plans/pid/_get/200_pending_review.json b/tests/api/plans/pid/_get/200_pending_review.json index 3cbdec43b..43bcafe65 100644 --- a/tests/api/plans/pid/_get/200_pending_review.json +++ b/tests/api/plans/pid/_get/200_pending_review.json @@ -18,14 +18,12 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs", - "id": "bug-1" + "title": "Search for bugs" }, { "description": "description kind video", "kind": "video", - "title": "Think aloud", - "id": "video-1" + "title": "Think aloud" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_pending_review_quoted.json b/tests/api/plans/pid/_get/200_pending_review_quoted.json index 828316e4c..230163960 100644 --- a/tests/api/plans/pid/_get/200_pending_review_quoted.json +++ b/tests/api/plans/pid/_get/200_pending_review_quoted.json @@ -146,14 +146,12 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs", - "id": "bug-1" + "title": "Search for bugs" }, { "description": "description kind bug", "kind": "video", - "title": "Think aloud", - "id": "video-1" + "title": "Think aloud" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_pending_review_unquoted.json b/tests/api/plans/pid/_get/200_pending_review_unquoted.json index 85dc80a00..d449496f6 100644 --- a/tests/api/plans/pid/_get/200_pending_review_unquoted.json +++ b/tests/api/plans/pid/_get/200_pending_review_unquoted.json @@ -141,14 +141,12 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs", - "id": "bug-1" + "title": "Search for bugs" }, { "description": "description kind bug", "kind": "video", - "title": "Think aloud", - "id": "video-1" + "title": "Think aloud" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_patch/request_Example_1.json b/tests/api/plans/pid/_patch/request_Example_1.json index 8c31b6450..d3229c4d6 100644 --- a/tests/api/plans/pid/_patch/request_Example_1.json +++ b/tests/api/plans/pid/_patch/request_Example_1.json @@ -18,14 +18,12 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs", - "id": "bug-1" + "title": "Search for bugs" }, { "description": "description kind video", "kind": "video", - "title": "Think aloud", - "id": "video-1" + "title": "Think aloud" } ], "type": "tasks", diff --git a/tests/api/templates/categories/_get/200_Example_1.json b/tests/api/templates/categories/_get/200_Example_1.json index f9f3dca26..20b386b4b 100644 --- a/tests/api/templates/categories/_get/200_Example_1.json +++ b/tests/api/templates/categories/_get/200_Example_1.json @@ -1,22 +1,7 @@ [ { "description": "string", - "id": -1, - "name": "uncategorized" - }, - { - "description": "string", - "id": 10, - "name": "Accessibility" - }, - { - "description": "string", - "id": 20, - "name": "Best category ever" - }, - { - "description": "string", - "id": 30, - "name": "Security" + "id": 0, + "name": "string" } ] diff --git a/tests/api/users/me/_get/200_Users_Me_example.json b/tests/api/users/me/_get/200_Users_Me_example.json index 852bf2e77..df477885c 100644 --- a/tests/api/users/me/_get/200_Users_Me_example.json +++ b/tests/api/users/me/_get/200_Users_Me_example.json @@ -16,12 +16,9 @@ ], "id": 1, "name": "Luca Cannarozzo", - "first_name": "Luca", - "last_name": "Cannarozzo", - "customer_role": "Designer", "picture": "https://www.gravatar.com/avatar/04c5df6d27d87476fe9dee853e1142bd", "profile_id": 32, "role": "administrator", "tryber_wp_user_id": 1, "unguess_wp_user_id": 1 -} \ No newline at end of file +} diff --git a/tests/api/workspaces/wid/templates/_get/200_Example_1.json b/tests/api/workspaces/wid/templates/_get/200_Example_1.json index fc2185d65..f9309c295 100644 --- a/tests/api/workspaces/wid/templates/_get/200_Example_1.json +++ b/tests/api/workspaces/wid/templates/_get/200_Example_1.json @@ -1,7 +1,6 @@ { "items": [ { - "category_id": 0, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}]}", "description": "string", "id": 0, diff --git a/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json b/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json index c106ed897..68401401b 100644 --- a/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json +++ b/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json @@ -1,7 +1,6 @@ { "items": [ { - "category_id": -1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Gain qualitative insights by observing users as they interact. Identifies difficulties in the user experience, improves the interface. Identifies difficulties in the user experience, improves the interface.", "id": 1, @@ -11,7 +10,6 @@ "workspace_id": 1 }, { - "category_id": -1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Gain qualitative insights by observing users as they interact. Identifies difficulties in the user experience, improves the interface. Identifies difficulties in the user experience, improves the interface.", "id": 2, @@ -21,7 +19,6 @@ "workspace_id": 1 }, { - "category_id": -1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Find unexpected issues with exploratory testing.", "id": 3, @@ -30,7 +27,6 @@ "workspace_id": 1 }, { - "category_id": 1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Improve the user experience.", "id": 4, @@ -39,7 +35,6 @@ "workspace_id": 1 }, { - "category_id": 10, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Penetration testing.", "id": 5, @@ -63,7 +58,6 @@ } }, { - "category_id": 10, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Test system performance", "id": 6, @@ -88,7 +82,6 @@ } }, { - "category_id": 20, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Gain qualitative insights by observing users as they interact. Identifies difficulties in the user experience, improves the interface. Identifies difficulties in the user experience, improves the interface.", "id": 7, @@ -112,7 +105,6 @@ } }, { - "category_id": 20, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Test system performance.", "id": 8, @@ -137,7 +129,6 @@ } }, { - "category_id": 30, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Test system performance", "id": 9, diff --git a/tests/api/workspaces/wid/templates/_get/200_global_template.json b/tests/api/workspaces/wid/templates/_get/200_global_template.json index 268250936..1f963ad89 100644 --- a/tests/api/workspaces/wid/templates/_get/200_global_template.json +++ b/tests/api/workspaces/wid/templates/_get/200_global_template.json @@ -1,7 +1,6 @@ { "items": [ { - "category_id": 10, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}]}", "description": "Description of global template 1", "id": 1, diff --git a/tests/e2e/plan/draft.spec.ts b/tests/e2e/plan/draft.spec.ts index c01982b2e..7b3606707 100644 --- a/tests/e2e/plan/draft.spec.ts +++ b/tests/e2e/plan/draft.spec.ts @@ -17,6 +17,7 @@ test.describe('The module builder', () => { await planPage.loggedIn(); await planPage.mockPreferences(); await planPage.mockWorkspace(); + await planPage.mockGetWatchers(); await planPage.mockWorkspacesList(); await planPage.mockGetDraftWithOnlyMandatoryModulesPlan(); await requestQuotationModal.mockPatchStatus(); diff --git a/tests/e2e/plan/draft_plan_info_card.spec.ts b/tests/e2e/plan/draft_plan_info_card.spec.ts index 0c140f415..d8ed3f9ad 100644 --- a/tests/e2e/plan/draft_plan_info_card.spec.ts +++ b/tests/e2e/plan/draft_plan_info_card.spec.ts @@ -76,6 +76,7 @@ test.describe('A plan with template and price', () => { touchpointsModule = new TouchpointsModule(page); await planPage.loggedIn(); await planPage.mockPreferences(); + await planPage.mockGetWatchers(); await planPage.mockWorkspace(); await planPage.mockWorkspacesList(); await planPage.mockGetDraftPlanWithTemplateAndPrice(); diff --git a/tests/fixtures/pages/Plan/index.ts b/tests/fixtures/pages/Plan/index.ts index 9632cba24..a4217b5ec 100644 --- a/tests/fixtures/pages/Plan/index.ts +++ b/tests/fixtures/pages/Plan/index.ts @@ -2,6 +2,7 @@ import { type Page } from '@playwright/test'; import { UnguessPage } from '../../UnguessPage'; import { AgeModule } from './Module_age'; import { BankModule } from './Module_bank'; +import { BrowserModule } from './Module_browser'; import { DigitalLiteracyModule } from './Module_digital_literacy'; import { ElectricityModule } from './Module_electricity'; import { GasModule } from './Module_gas'; @@ -14,7 +15,6 @@ import { OutOfScopeModule } from './Module_out_of_scope'; import { TargetModule } from './Module_target'; import { TasksModule } from './Module_tasks'; import { TouchpointsModule } from './Module_touchpoints'; -import { BrowserModule } from './Module_browser'; interface TabModule { expectToBeReadonly(): Promise<void>; @@ -291,6 +291,18 @@ export class PlanPage extends UnguessPage { }); } + async mockGetWatchers() { + await this.page.route('*/**/api/plans/1/watchers*', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + path: 'tests/api/plans/pid/watchers/_get/200_Example_1.json', + }); + } else { + await route.fallback(); + } + }); + } + async mockGetDraftPlanWithDateError() { await this.page.route('*/**/api/plans/1', async (route) => { if (route.request().method() === 'GET') { From 1cfdb903a69d39914dfc760aecfc7b9d28d4b921 Mon Sep 17 00:00:00 2001 From: Davide Bizzi <davide.bizzi@unguess.io> Date: Fri, 7 Nov 2025 11:58:39 +0100 Subject: [PATCH 063/108] feat: Rename mockGetWatchers to mockWatchers and enhance its functionality for PUT and POST requests --- tests/e2e/plan/draft.spec.ts | 2 +- tests/e2e/plan/draft_plan_info_card.spec.ts | 2 +- tests/fixtures/pages/Plan/index.ts | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/e2e/plan/draft.spec.ts b/tests/e2e/plan/draft.spec.ts index 7b3606707..f7965db0e 100644 --- a/tests/e2e/plan/draft.spec.ts +++ b/tests/e2e/plan/draft.spec.ts @@ -17,7 +17,7 @@ test.describe('The module builder', () => { await planPage.loggedIn(); await planPage.mockPreferences(); await planPage.mockWorkspace(); - await planPage.mockGetWatchers(); + await planPage.mockWatchers(); await planPage.mockWorkspacesList(); await planPage.mockGetDraftWithOnlyMandatoryModulesPlan(); await requestQuotationModal.mockPatchStatus(); diff --git a/tests/e2e/plan/draft_plan_info_card.spec.ts b/tests/e2e/plan/draft_plan_info_card.spec.ts index d8ed3f9ad..04db0037c 100644 --- a/tests/e2e/plan/draft_plan_info_card.spec.ts +++ b/tests/e2e/plan/draft_plan_info_card.spec.ts @@ -76,7 +76,7 @@ test.describe('A plan with template and price', () => { touchpointsModule = new TouchpointsModule(page); await planPage.loggedIn(); await planPage.mockPreferences(); - await planPage.mockGetWatchers(); + await planPage.mockWatchers(); await planPage.mockWorkspace(); await planPage.mockWorkspacesList(); await planPage.mockGetDraftPlanWithTemplateAndPrice(); diff --git a/tests/fixtures/pages/Plan/index.ts b/tests/fixtures/pages/Plan/index.ts index a4217b5ec..8c647159a 100644 --- a/tests/fixtures/pages/Plan/index.ts +++ b/tests/fixtures/pages/Plan/index.ts @@ -291,12 +291,20 @@ export class PlanPage extends UnguessPage { }); } - async mockGetWatchers() { + async mockWatchers() { await this.page.route('*/**/api/plans/1/watchers*', async (route) => { if (route.request().method() === 'GET') { await route.fulfill({ path: 'tests/api/plans/pid/watchers/_get/200_Example_1.json', }); + } else if (route.request().method() === 'PUT') { + await route.fulfill({ + body: JSON.stringify({}), + }); + } else if (route.request().method() === 'POST') { + await route.fulfill({ + body: JSON.stringify({}), + }); } else { await route.fallback(); } From eb43ca7cb320908f9aab42097c8f057adaba38b9 Mon Sep 17 00:00:00 2001 From: Davide Bizzi <davide.bizzi@unguess.io> Date: Fri, 7 Nov 2025 12:13:24 +0100 Subject: [PATCH 064/108] feat: Update mock data and enhance testing setup for workspace users --- tests/api/plans/pid/watchers/_get/200_Example_1.json | 9 ++++----- tests/api/workspaces/wid/users/_get/200_Example_1.json | 7 +++++++ tests/e2e/plan/draft.spec.ts | 1 + tests/e2e/plan/draft_plan_info_card.spec.ts | 2 ++ tests/fixtures/UnguessPage.ts | 8 ++++++++ 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/tests/api/plans/pid/watchers/_get/200_Example_1.json b/tests/api/plans/pid/watchers/_get/200_Example_1.json index 9da3d6866..a647089c8 100644 --- a/tests/api/plans/pid/watchers/_get/200_Example_1.json +++ b/tests/api/plans/pid/watchers/_get/200_Example_1.json @@ -9,11 +9,10 @@ }, { "id": 2, - "name": "Andrej", - "surname": "Kojmasky", - "email": "andrej.kojmasky@bear.org", - "isInternal": false, - "image": "https://picsum.photos/200" + "name": "string", + "surname": "string", + "email": "user@example.com", + "isInternal": false } ] } diff --git a/tests/api/workspaces/wid/users/_get/200_Example_1.json b/tests/api/workspaces/wid/users/_get/200_Example_1.json index 2b9948f8d..ac3fdd597 100644 --- a/tests/api/workspaces/wid/users/_get/200_Example_1.json +++ b/tests/api/workspaces/wid/users/_get/200_Example_1.json @@ -1,5 +1,12 @@ { "items": [ + { + "email": "invited@example.com", + "id": 2, + "invitationPending": false, + "name": "string string", + "profile_id": 2 + }, { "email": "user@example.com", "id": 1234, diff --git a/tests/e2e/plan/draft.spec.ts b/tests/e2e/plan/draft.spec.ts index f7965db0e..4c1512f20 100644 --- a/tests/e2e/plan/draft.spec.ts +++ b/tests/e2e/plan/draft.spec.ts @@ -18,6 +18,7 @@ test.describe('The module builder', () => { await planPage.mockPreferences(); await planPage.mockWorkspace(); await planPage.mockWatchers(); + await planPage.mockWorkspaceUsers(); await planPage.mockWorkspacesList(); await planPage.mockGetDraftWithOnlyMandatoryModulesPlan(); await requestQuotationModal.mockPatchStatus(); diff --git a/tests/e2e/plan/draft_plan_info_card.spec.ts b/tests/e2e/plan/draft_plan_info_card.spec.ts index 04db0037c..216b3f4f6 100644 --- a/tests/e2e/plan/draft_plan_info_card.spec.ts +++ b/tests/e2e/plan/draft_plan_info_card.spec.ts @@ -43,6 +43,7 @@ test.describe('A plan without a template and price', () => { await planPage.loggedIn(); await planPage.mockPreferences(); await planPage.mockWorkspace(); + await planPage.mockWorkspaceUsers(); await planPage.mockWorkspacesList(); await planPage.mockGetDraftPlan(); await planPage.mockPatchPlan(); @@ -78,6 +79,7 @@ test.describe('A plan with template and price', () => { await planPage.mockPreferences(); await planPage.mockWatchers(); await planPage.mockWorkspace(); + await planPage.mockWorkspaceUsers(); await planPage.mockWorkspacesList(); await planPage.mockGetDraftPlanWithTemplateAndPrice(); await requestQuotationModal.mockPatchStatus(); diff --git a/tests/fixtures/UnguessPage.ts b/tests/fixtures/UnguessPage.ts index 30e307cb4..956585564 100644 --- a/tests/fixtures/UnguessPage.ts +++ b/tests/fixtures/UnguessPage.ts @@ -76,6 +76,14 @@ export class UnguessPage { }); } + async mockWorkspaceUsers() { + await this.page.route('*/**/api/workspaces/1/users*', async (route) => { + await route.fulfill({ + path: 'tests/api/workspaces/wid/users/_get/200_Example_1.json', + }); + }); + } + async mockWorkspacesList() { await this.page.route( '*/**/api/workspaces?orderBy=company', From 551889ab5cdd09f2cba8b2a54466fa8596e4110d Mon Sep 17 00:00:00 2001 From: Davide Bizzi <davide.bizzi@unguess.io> Date: Fri, 7 Nov 2025 13:09:15 +0100 Subject: [PATCH 065/108] feat: Update JSON test files with new IDs and descriptions for tasks in various scenarios --- tests/api/plans/pid/_get/200_approved.json | 8 +++++--- .../plans/pid/_get/200_draft_complete.json | 8 +++++--- .../_get/200_draft_complete_date_error.json | 6 ++++-- .../pid/_get/200_draft_mandatory_only.json | 6 ++++-- .../pid/_get/200_draft_missing_mandatory.json | 6 ++++-- tests/api/plans/pid/_get/200_paying.json | 17 ++++++++++++----- .../plans/pid/_get/200_pending_review.json | 6 ++++-- .../pid/_get/200_pending_review_quoted.json | 6 ++++-- .../pid/_get/200_pending_review_unquoted.json | 6 ++++-- .../plans/pid/_patch/request_Example_1.json | 6 ++++-- .../categories/_get/200_Example_1.json | 19 +++++++++++++++++-- .../users/me/_get/200_Users_Me_example.json | 3 +++ .../wid/templates/_get/200_Example_1.json | 1 + .../200_global_and_private_templates.json | 9 +++++++++ .../templates/_get/200_global_template.json | 1 + .../wid/users/_get/200_Example_1.json | 18 +++++++++--------- 16 files changed, 90 insertions(+), 36 deletions(-) diff --git a/tests/api/plans/pid/_get/200_approved.json b/tests/api/plans/pid/_get/200_approved.json index d0e240062..0a8b1a508 100644 --- a/tests/api/plans/pid/_get/200_approved.json +++ b/tests/api/plans/pid/_get/200_approved.json @@ -132,12 +132,14 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs" + "title": "Search for bugs", + "id": "bug-1" }, { - "description": "description kind bug", + "description": "description kind video", "kind": "video", - "title": "Think aloud" + "title": "Think aloud", + "id": "video-1" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_draft_complete.json b/tests/api/plans/pid/_get/200_draft_complete.json index 79cbe8b74..8c640b96b 100644 --- a/tests/api/plans/pid/_get/200_draft_complete.json +++ b/tests/api/plans/pid/_get/200_draft_complete.json @@ -82,7 +82,7 @@ "output": [ { "gender": "female", - "percentage": 100 + "percentage": 0 } ], "type": "gender", @@ -155,12 +155,14 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs" + "title": "Search for bugs", + "id": "bug-1" }, { "description": "description kind bug", "kind": "video", - "title": "Think aloud" + "title": "Think aloud", + "id": "video-1" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_draft_complete_date_error.json b/tests/api/plans/pid/_get/200_draft_complete_date_error.json index a1313ef1c..8197c8362 100644 --- a/tests/api/plans/pid/_get/200_draft_complete_date_error.json +++ b/tests/api/plans/pid/_get/200_draft_complete_date_error.json @@ -18,12 +18,14 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs" + "title": "Search for bugs", + "id": "bug-1" }, { "description": "description kind video", "kind": "video", - "title": "Think aloud" + "title": "Think aloud", + "id": "video-1" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_draft_mandatory_only.json b/tests/api/plans/pid/_get/200_draft_mandatory_only.json index 17c5361ba..467c9c2df 100644 --- a/tests/api/plans/pid/_get/200_draft_mandatory_only.json +++ b/tests/api/plans/pid/_get/200_draft_mandatory_only.json @@ -18,12 +18,14 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs" + "title": "Search for bugs", + "id": "bug-1" }, { "description": "description kind video", "kind": "video", - "title": "Think aloud" + "title": "Think aloud", + "id": "video-1" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_draft_missing_mandatory.json b/tests/api/plans/pid/_get/200_draft_missing_mandatory.json index ae68fad70..d95c9d115 100644 --- a/tests/api/plans/pid/_get/200_draft_missing_mandatory.json +++ b/tests/api/plans/pid/_get/200_draft_missing_mandatory.json @@ -6,12 +6,14 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs" + "title": "Search for bugs", + "id": "bug-1" }, { "description": "description kind bug", "kind": "video", - "title": "Think aloud" + "title": "Think aloud", + "id": "video-1" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_paying.json b/tests/api/plans/pid/_get/200_paying.json index e19667113..7b518c3c4 100644 --- a/tests/api/plans/pid/_get/200_paying.json +++ b/tests/api/plans/pid/_get/200_paying.json @@ -132,12 +132,14 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs" + "title": "Search for bugs", + "id": "bug-1" }, { - "description": "description kind bug", + "description": "description kind video", "kind": "video", - "title": "Think aloud" + "title": "Think aloud", + "id": "video-1" } ], "type": "tasks", @@ -187,11 +189,16 @@ } ] }, - "id": 13, + "id": 1, "project": { "id": 90, "name": "MyProject" }, - "status": "pending_review", + "quote": { + "id": 98, + "status": "approved", + "value": "3600 €" + }, + "status": "paying", "workspace_id": 1 } diff --git a/tests/api/plans/pid/_get/200_pending_review.json b/tests/api/plans/pid/_get/200_pending_review.json index 43bcafe65..3cbdec43b 100644 --- a/tests/api/plans/pid/_get/200_pending_review.json +++ b/tests/api/plans/pid/_get/200_pending_review.json @@ -18,12 +18,14 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs" + "title": "Search for bugs", + "id": "bug-1" }, { "description": "description kind video", "kind": "video", - "title": "Think aloud" + "title": "Think aloud", + "id": "video-1" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_pending_review_quoted.json b/tests/api/plans/pid/_get/200_pending_review_quoted.json index 230163960..828316e4c 100644 --- a/tests/api/plans/pid/_get/200_pending_review_quoted.json +++ b/tests/api/plans/pid/_get/200_pending_review_quoted.json @@ -146,12 +146,14 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs" + "title": "Search for bugs", + "id": "bug-1" }, { "description": "description kind bug", "kind": "video", - "title": "Think aloud" + "title": "Think aloud", + "id": "video-1" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_get/200_pending_review_unquoted.json b/tests/api/plans/pid/_get/200_pending_review_unquoted.json index d449496f6..85dc80a00 100644 --- a/tests/api/plans/pid/_get/200_pending_review_unquoted.json +++ b/tests/api/plans/pid/_get/200_pending_review_unquoted.json @@ -141,12 +141,14 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs" + "title": "Search for bugs", + "id": "bug-1" }, { "description": "description kind bug", "kind": "video", - "title": "Think aloud" + "title": "Think aloud", + "id": "video-1" } ], "type": "tasks", diff --git a/tests/api/plans/pid/_patch/request_Example_1.json b/tests/api/plans/pid/_patch/request_Example_1.json index d3229c4d6..8c31b6450 100644 --- a/tests/api/plans/pid/_patch/request_Example_1.json +++ b/tests/api/plans/pid/_patch/request_Example_1.json @@ -18,12 +18,14 @@ { "description": "description kind bug", "kind": "bug", - "title": "Search for bugs" + "title": "Search for bugs", + "id": "bug-1" }, { "description": "description kind video", "kind": "video", - "title": "Think aloud" + "title": "Think aloud", + "id": "video-1" } ], "type": "tasks", diff --git a/tests/api/templates/categories/_get/200_Example_1.json b/tests/api/templates/categories/_get/200_Example_1.json index 20b386b4b..f9f3dca26 100644 --- a/tests/api/templates/categories/_get/200_Example_1.json +++ b/tests/api/templates/categories/_get/200_Example_1.json @@ -1,7 +1,22 @@ [ { "description": "string", - "id": 0, - "name": "string" + "id": -1, + "name": "uncategorized" + }, + { + "description": "string", + "id": 10, + "name": "Accessibility" + }, + { + "description": "string", + "id": 20, + "name": "Best category ever" + }, + { + "description": "string", + "id": 30, + "name": "Security" } ] diff --git a/tests/api/users/me/_get/200_Users_Me_example.json b/tests/api/users/me/_get/200_Users_Me_example.json index df477885c..6a716a13a 100644 --- a/tests/api/users/me/_get/200_Users_Me_example.json +++ b/tests/api/users/me/_get/200_Users_Me_example.json @@ -16,6 +16,9 @@ ], "id": 1, "name": "Luca Cannarozzo", + "first_name": "Luca", + "last_name": "Cannarozzo", + "customer_role": "Designer", "picture": "https://www.gravatar.com/avatar/04c5df6d27d87476fe9dee853e1142bd", "profile_id": 32, "role": "administrator", diff --git a/tests/api/workspaces/wid/templates/_get/200_Example_1.json b/tests/api/workspaces/wid/templates/_get/200_Example_1.json index f9309c295..fc2185d65 100644 --- a/tests/api/workspaces/wid/templates/_get/200_Example_1.json +++ b/tests/api/workspaces/wid/templates/_get/200_Example_1.json @@ -1,6 +1,7 @@ { "items": [ { + "category_id": 0, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}]}", "description": "string", "id": 0, diff --git a/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json b/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json index 68401401b..c106ed897 100644 --- a/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json +++ b/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json @@ -1,6 +1,7 @@ { "items": [ { + "category_id": -1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Gain qualitative insights by observing users as they interact. Identifies difficulties in the user experience, improves the interface. Identifies difficulties in the user experience, improves the interface.", "id": 1, @@ -10,6 +11,7 @@ "workspace_id": 1 }, { + "category_id": -1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Gain qualitative insights by observing users as they interact. Identifies difficulties in the user experience, improves the interface. Identifies difficulties in the user experience, improves the interface.", "id": 2, @@ -19,6 +21,7 @@ "workspace_id": 1 }, { + "category_id": -1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Find unexpected issues with exploratory testing.", "id": 3, @@ -27,6 +30,7 @@ "workspace_id": 1 }, { + "category_id": 1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Improve the user experience.", "id": 4, @@ -35,6 +39,7 @@ "workspace_id": 1 }, { + "category_id": 10, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Penetration testing.", "id": 5, @@ -58,6 +63,7 @@ } }, { + "category_id": 10, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Test system performance", "id": 6, @@ -82,6 +88,7 @@ } }, { + "category_id": 20, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Gain qualitative insights by observing users as they interact. Identifies difficulties in the user experience, improves the interface. Identifies difficulties in the user experience, improves the interface.", "id": 7, @@ -105,6 +112,7 @@ } }, { + "category_id": 20, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Test system performance.", "id": 8, @@ -129,6 +137,7 @@ } }, { + "category_id": 30, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Test system performance", "id": 9, diff --git a/tests/api/workspaces/wid/templates/_get/200_global_template.json b/tests/api/workspaces/wid/templates/_get/200_global_template.json index 1f963ad89..268250936 100644 --- a/tests/api/workspaces/wid/templates/_get/200_global_template.json +++ b/tests/api/workspaces/wid/templates/_get/200_global_template.json @@ -1,6 +1,7 @@ { "items": [ { + "category_id": 10, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}]}", "description": "Description of global template 1", "id": 1, diff --git a/tests/api/workspaces/wid/users/_get/200_Example_1.json b/tests/api/workspaces/wid/users/_get/200_Example_1.json index ac3fdd597..1accbd220 100644 --- a/tests/api/workspaces/wid/users/_get/200_Example_1.json +++ b/tests/api/workspaces/wid/users/_get/200_Example_1.json @@ -1,22 +1,22 @@ { "items": [ - { - "email": "invited@example.com", - "id": 2, - "invitationPending": false, - "name": "string string", - "profile_id": 2 - }, { "email": "user@example.com", "id": 1234, "invitationPending": true, "name": "string", "profile_id": 999 + }, + { + "email": "invited@example.com", + "id": 2, + "invitationPending": false, + "name": "string string", + "profile_id": 2 } ], "limit": 0, - "size": 1, + "size": 2, "start": 0, - "total": 1 + "total": 2 } From 86302cba08c46566b63bd89f4962b5708481689b Mon Sep 17 00:00:00 2001 From: Davide Bizzi <davide.bizzi@unguess.io> Date: Fri, 7 Nov 2025 13:21:46 +0100 Subject: [PATCH 066/108] feat: Refactor tasks module tests to import API data and enhance mock setup --- tests/e2e/plan/modules/tasks.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/e2e/plan/modules/tasks.spec.ts b/tests/e2e/plan/modules/tasks.spec.ts index fd5f3face..d23f4371f 100644 --- a/tests/e2e/plan/modules/tasks.spec.ts +++ b/tests/e2e/plan/modules/tasks.spec.ts @@ -1,7 +1,7 @@ -import { test, expect } from '../../../fixtures/app'; +import apiGetDraftMandatoryPlan from '../../../api/plans/pid/_get/200_draft_mandatory_only.json'; +import { expect, test } from '../../../fixtures/app'; import { PlanPage } from '../../../fixtures/pages/Plan'; import { TasksModule } from '../../../fixtures/pages/Plan/Module_tasks'; -import apiGetDraftMandatoryPlan from '../../../api/plans/pid/_get/200_draft_mandatory_only.json'; import { RequestQuotationModal } from '../../../fixtures/pages/Plan/RequestQuotationModal'; test.describe('The tasks module defines a list of activities.', () => { @@ -17,6 +17,8 @@ test.describe('The tasks module defines a list of activities.', () => { await moduleBuilderPage.mockPreferences(); await moduleBuilderPage.mockWorkspace(); await moduleBuilderPage.mockPatchPlan(); + await moduleBuilderPage.mockWorkspaceUsers(); + await moduleBuilderPage.mockWatchers(); await moduleBuilderPage.mockWorkspacesList(); await moduleBuilderPage.mockGetDraftWithOnlyMandatoryModulesPlan(); await moduleBuilderPage.open(); From 17fb03d1eaab78ee1927432d4c720e4d597b61ce Mon Sep 17 00:00:00 2001 From: Kariamos <minucci9292@gmail.com> Date: Fri, 7 Nov 2025 15:00:50 +0100 Subject: [PATCH 067/108] feat: Limit maximum selectable watchers before +X to 3 in the Watchers component --- src/pages/Plan/modals/Watchers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Plan/modals/Watchers.tsx b/src/pages/Plan/modals/Watchers.tsx index 5f630835d..34db72794 100644 --- a/src/pages/Plan/modals/Watchers.tsx +++ b/src/pages/Plan/modals/Watchers.tsx @@ -89,7 +89,7 @@ const Watchers = ({ listboxAppendToNode={document.body} options={options} size="small" - maxItems={10} + maxItems={3} i18n={{ placeholder: t('__PLAN_PAGE_MODAL_SEND_REQUEST_WATCHERS_PLACEHOLDER'), }} From c8a3571435f1a675c6489ae7903d26490777c912 Mon Sep 17 00:00:00 2001 From: ZecD <dominik.zecoli@gmail.com> Date: Fri, 7 Nov 2025 17:18:43 +0100 Subject: [PATCH 068/108] feat: enhance notification settings with new fields and improved checkbox handling --- src/common/schema.ts | 148 ++++++++++++++++-- .../Profile/FormNotificationSettings.tsx | 16 +- .../FollowActivitiesPanel.tsx | 19 --- .../parts/NotificationSettingsCard.tsx | 13 +- .../CommunicationUpdatesPanel.tsx | 90 ++++++----- 5 files changed, 211 insertions(+), 75 deletions(-) diff --git a/src/common/schema.ts b/src/common/schema.ts index ed571c854..e7f1ad93e 100644 --- a/src/common/schema.ts +++ b/src/common/schema.ts @@ -24,10 +24,12 @@ export interface paths { }; "/buy": { /** - * Stripe webhook destination, listen three event types: - * - charge.failed, - * - checkout.session.completed - * - checkout.session.expired + * Stripe webhook destination, listen following event types: + * - checkout.session.completed (Occurs when a Checkout Session has been successfully completed) + * - checkout.session.expired (Occurs when a Checkout Session is expired) + * - checkout.session.async_payment_failed (Occurs when a payment intent using a delayed payment method fails) + * - checkout.session.async_payment_succeeded (Occurs when a payment intent using a delayed payment method finally succeeds) + * - charge.refunded (Occurs whenever a charge is refunded, including partial refunds. Listen to refund.created for information about the refund) */ post: operations["post-buy"]; }; @@ -622,6 +624,17 @@ export interface paths { }; }; }; + "/plans/{pid}/watchers": { + /** Returns all the watcher added to a plan. It always returns at least one item. */ + get: operations["get-plans-pid-watchers"]; + put: operations["put-plans-pid-watchers"]; + post: operations["post-plans-pid-watchers"]; + parameters: { + path: { + pid: string; + }; + }; + }; "/workspaces/{wid}/templates": { get: operations["get-workspaces-templates"]; post: operations["post-workspaces-wid-templates"]; @@ -656,6 +669,15 @@ export interface paths { }; }; }; + "/plans/{pid}/watchers/{profile_id}": { + delete: operations["delete-plans-pid-watchers-profile_id"]; + parameters: { + path: { + pid: string; + profile_id: string; + }; + }; + }; } export interface components { @@ -1332,8 +1354,7 @@ export interface components { kind: "app"; os: { ios?: string; - linux?: string; - windows?: string; + android?: string; }; }; /** OutputModuleTouchpointsWebDesktop */ @@ -1442,7 +1463,8 @@ export interface components { | "module_type" | "number_of_testers" | "number_of_tasks" - | "task_type"; + | "task_type" + | "duplicate_touchpoint_form_factors"; /** Report */ Report: { creation_date?: string; @@ -1999,10 +2021,12 @@ export interface operations { requestBody: components["requestBodies"]["Credentials"]; }; /** - * Stripe webhook destination, listen three event types: - * - charge.failed, - * - checkout.session.completed - * - checkout.session.expired + * Stripe webhook destination, listen following event types: + * - checkout.session.completed (Occurs when a Checkout Session has been successfully completed) + * - checkout.session.expired (Occurs when a Checkout Session is expired) + * - checkout.session.async_payment_failed (Occurs when a payment intent using a delayed payment method fails) + * - checkout.session.async_payment_succeeded (Occurs when a payment intent using a delayed payment method finally succeeds) + * - charge.refunded (Occurs whenever a charge is refunded, including partial refunds. Listen to refund.created for information about the refund) */ "post-buy": { responses: { @@ -2027,13 +2051,17 @@ export interface operations { key: string; tag: string; }; + /** @enum {string} */ + payment_status?: "paid" | "unpaid"; }; }; /** @enum {undefined} */ type: - | "checkout.session.completed" + | "checkout.session.async_payment_succeeded" | "checkout.session.async_payment_failed" - | "checkout.session.expired"; + | "checkout.session.completed" + | "checkout.session.expired" + | "charge.refunded"; }; }; }; @@ -4393,6 +4421,81 @@ export interface operations { 500: components["responses"]["Error"]; }; }; + /** Returns all the watcher added to a plan. It always returns at least one item. */ + "get-plans-pid-watchers": { + parameters: { + path: { + pid: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + items: { + id: number; + name: string; + surname: string; + email: string; + image?: string; + isInternal: boolean; + }[]; + }; + }; + }; + 403: components["responses"]["Error"]; + 404: components["responses"]["Error"]; + 500: components["responses"]["Error"]; + }; + }; + "put-plans-pid-watchers": { + parameters: { + path: { + pid: string; + }; + }; + responses: { + /** OK */ + 200: unknown; + 400: components["responses"]["Error"]; + 403: components["responses"]["Error"]; + 404: components["responses"]["Error"]; + 500: components["responses"]["Error"]; + }; + requestBody: { + content: { + "application/json": { + users: { + id: number; + }[]; + }; + }; + }; + }; + "post-plans-pid-watchers": { + parameters: { + path: { + pid: string; + }; + }; + responses: { + /** OK */ + 200: unknown; + 403: components["responses"]["Error"]; + 404: components["responses"]["Error"]; + 500: components["responses"]["Error"]; + }; + requestBody: { + content: { + "application/json": { + users: { + id: number; + }[]; + }; + }; + }; + }; "get-workspaces-templates": { parameters: { path: { @@ -4596,6 +4699,25 @@ export interface operations { }; }; }; + "delete-plans-pid-watchers-profile_id": { + parameters: { + path: { + pid: string; + profile_id: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + success?: boolean; + }; + }; + }; + 403: components["responses"]["Error"]; + }; + }; } export interface external {} diff --git a/src/pages/Profile/FormNotificationSettings.tsx b/src/pages/Profile/FormNotificationSettings.tsx index 204fc42cb..c7b331130 100644 --- a/src/pages/Profile/FormNotificationSettings.tsx +++ b/src/pages/Profile/FormNotificationSettings.tsx @@ -2,19 +2,29 @@ import { useToast } from '@appquality/unguess-design-system'; import { Formik } from 'formik'; import { useTranslation } from 'react-i18next'; import * as Yup from 'yup'; +import { useGetUsersMePreferencesQuery } from 'src/features/api'; import { NotificationSettingsFormValues } from './valuesType'; import { NotificationSettingsCard } from './parts/NotificationSettingsCard'; +import { Loader } from './parts/cardLoader'; export const FormNotificationSettings = () => { const { t } = useTranslation(); const { addToast } = useToast(); + const { data, isLoading } = useGetUsersMePreferencesQuery(); + const activitySetup = + data?.items?.find((pref) => pref.name === 'plan_notifications_enabled') + ?.value ?? '1'; + + const activityProgress = + data?.items?.find((pref) => pref.name === 'notifications_enabled')?.value ?? + '1'; const initialValues: NotificationSettingsFormValues = { - activitySetupUpdates: true, - activityProgress: true, + activitySetupUpdates: activitySetup === '1', + activityProgress: activityProgress === '1', }; - /* if (isLoading) return <Loader />; */ + if (isLoading) return <Loader />; const schema = Yup.object().shape({ activitySetupUpdates: Yup.boolean(), diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx index 01d18a1f7..8efe47e29 100644 --- a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx @@ -54,25 +54,6 @@ export const FollowActivitiesPanel = () => { </Button> </StyledActivityItem> </StyledPanelSectionContainer> - <StyledPanelSectionContainer> - <Label> - {t( - '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_PROGRESS_DESCRIPTION' - )} - (3) - </Label> - <StyledActivityItem> - <div> - <Anchor>Titolo Attivita</Anchor> - <SM>titolo progetto</SM> - </div> - <Button size="small" isBasic> - {t( - '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT' - )} - </Button> - </StyledActivityItem> - </StyledPanelSectionContainer> <StyledHintContainer> <InfoIcon /> <Hint> diff --git a/src/pages/Profile/parts/NotificationSettingsCard.tsx b/src/pages/Profile/parts/NotificationSettingsCard.tsx index fcc46ee49..8bb402e59 100644 --- a/src/pages/Profile/parts/NotificationSettingsCard.tsx +++ b/src/pages/Profile/parts/NotificationSettingsCard.tsx @@ -9,15 +9,20 @@ import { appTheme } from 'src/app/theme'; import { ReactComponent as EmailIcon } from 'src/assets/icons/email-icon.svg'; import { Divider } from 'src/common/components/divider'; +import { useFormikContext } from 'formik'; import { StyledCardHeader, StyledNotificationsCardHeaderWrapper, } from './common'; import { NotificationSettingsAccordion } from './NotificationsAccordion'; import { FollowActivitiesAccordion } from './FollowActivitiesAccordion.tsx'; +import { NotificationSettingsFormValues } from '../valuesType'; export const NotificationSettingsCard = () => { const { t } = useTranslation(); + const { dirty, isSubmitting, submitForm } = + useFormikContext<NotificationSettingsFormValues>(); + const canSave = dirty && !isSubmitting; return ( <ContainerCard id="anchor-notification-settings-id" @@ -44,7 +49,13 @@ export const NotificationSettingsCard = () => { </StyledNotificationsCardHeaderWrapper> <Divider /> <NotificationSettingsAccordion /> - <Button isAccent isPrimary style={{ alignSelf: 'flex-end' }}> + <Button + disabled={!canSave} + isAccent + isPrimary + style={{ alignSelf: 'flex-end' }} + onClick={submitForm} + > {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_SAVE_BUTTON_LABEL')} </Button> </div> diff --git a/src/pages/Profile/parts/NotificationsAccordion/CommunicationUpdatesPanel.tsx b/src/pages/Profile/parts/NotificationsAccordion/CommunicationUpdatesPanel.tsx index faf96f893..d8ede6c9b 100644 --- a/src/pages/Profile/parts/NotificationsAccordion/CommunicationUpdatesPanel.tsx +++ b/src/pages/Profile/parts/NotificationsAccordion/CommunicationUpdatesPanel.tsx @@ -6,9 +6,11 @@ import { Label, UnorderedList, } from '@appquality/unguess-design-system'; +import { Field, FieldProps, useFormikContext } from 'formik'; import { useTranslation } from 'react-i18next'; import { appTheme } from 'src/app/theme'; import styled from 'styled-components'; +import { NotificationSettingsFormValues } from '../../valuesType'; const StyledPanelContainer = styled.div` display: flex; @@ -24,6 +26,7 @@ const StyledCheckBoxContainer = styled.div` export const CommunicationUpdatesPanel = () => { const { t } = useTranslation(); + const { setFieldValue } = useFormikContext<NotificationSettingsFormValues>(); return ( <StyledPanelContainer> <StyledCheckBoxContainer> @@ -32,45 +35,54 @@ export const CommunicationUpdatesPanel = () => { '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_FORM_LABEL' )} </Label> - - <FormField> - <Checkbox - role="checkbox" - key="all" - name="activitySetupUpdates" - onChange={() => {}} - > - <Label> - {t( - '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_LABEL' - )} - </Label> - <Hint> - {t( - '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_HINT' - )} - </Hint> - </Checkbox> - </FormField> - <FormField> - <Checkbox - role="checkbox" - key="all" - name="activityProgress" - onChange={() => {}} - > - <Label> - {t( - '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_CHECKBOX_LABEL' - )} - </Label> - <Hint> - {t( - '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_CHECKBOX_HINT' - )} - </Hint> - </Checkbox> - </FormField> + <Field name="activitySetupUpdates"> + {({ field }: FieldProps) => ( + <FormField> + <Checkbox + role="checkbox" + key="all" + checked={field.value} + onChange={() => + setFieldValue('activitySetupUpdates', !field.value) + } + > + <Label> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_LABEL' + )} + </Label> + <Hint> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_HINT' + )} + </Hint> + </Checkbox> + </FormField> + )} + </Field> + <Field name="activityProgress"> + {({ field }: FieldProps) => ( + <FormField> + <Checkbox + role="checkbox" + key="all" + checked={field.value} + onChange={() => setFieldValue('activityProgress', !field.value)} + > + <Label> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_CHECKBOX_LABEL' + )} + </Label> + <Hint> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_CHECKBOX_HINT' + )} + </Hint> + </Checkbox> + </FormField> + )} + </Field> </StyledCheckBoxContainer> <Alert type="info"> <Alert.Title style={{ marginBottom: appTheme.space.xxs }}> From 0821fa49fed5730c34a02e44880cdd52b11ee8dd Mon Sep 17 00:00:00 2001 From: ZecD <dominik.zecoli@gmail.com> Date: Fri, 7 Nov 2025 18:05:44 +0100 Subject: [PATCH 069/108] feat: implement notification preferences update functionality with success and error handling --- .../Profile/FormNotificationSettings.tsx | 87 ++++++++++--------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/src/pages/Profile/FormNotificationSettings.tsx b/src/pages/Profile/FormNotificationSettings.tsx index c7b331130..7d325b07a 100644 --- a/src/pages/Profile/FormNotificationSettings.tsx +++ b/src/pages/Profile/FormNotificationSettings.tsx @@ -1,8 +1,11 @@ -import { useToast } from '@appquality/unguess-design-system'; +import { useToast, Notification } from '@appquality/unguess-design-system'; import { Formik } from 'formik'; import { useTranslation } from 'react-i18next'; import * as Yup from 'yup'; -import { useGetUsersMePreferencesQuery } from 'src/features/api'; +import { + useGetUsersMePreferencesQuery, + usePutUsersMePreferencesBySlugMutation, +} from 'src/features/api'; import { NotificationSettingsFormValues } from './valuesType'; import { NotificationSettingsCard } from './parts/NotificationSettingsCard'; import { Loader } from './parts/cardLoader'; @@ -11,6 +14,7 @@ export const FormNotificationSettings = () => { const { t } = useTranslation(); const { addToast } = useToast(); const { data, isLoading } = useGetUsersMePreferencesQuery(); + const [updatePreferences] = usePutUsersMePreferencesBySlugMutation(); const activitySetup = data?.items?.find((pref) => pref.name === 'plan_notifications_enabled') ?.value ?? '1'; @@ -31,6 +35,44 @@ export const FormNotificationSettings = () => { activityProgress: Yup.boolean(), }); + const handleUpdatePreferences = async ( + values: NotificationSettingsFormValues + ) => { + try { + if (values.activitySetupUpdates !== initialValues.activitySetupUpdates) { + await updatePreferences({ + slug: 'plan_notifications_enabled', + body: { + value: values.activitySetupUpdates ? '1' : '0', + }, + }).unwrap(); + } + if (values.activityProgress !== initialValues.activityProgress) { + await updatePreferences({ + slug: 'notifications_enabled', + body: { + value: values.activityProgress ? '1' : '0', + }, + }).unwrap(); + } + addToast(({ close }) => ( + <Notification + onClose={close} + type="success" + title={t('__PROFILE_PAGE_NOTIFICATIONS_UPDATE_SUCCESS_MESSAGE')} + /> + )); + } catch (error) { + addToast(({ close }) => ( + <Notification + onClose={close} + type="error" + title={t('__PROFILE_PAGE_NOTIFICATIONS_UPDATE_ERROR_MESSAGE')} + /> + )); + } + }; + return ( <Formik initialValues={initialValues} @@ -39,45 +81,8 @@ export const FormNotificationSettings = () => { validateOnChange onSubmit={async (values, actions) => { actions.setSubmitting(true); - - /* updateProfile({ - body: { - name: values.name, - surname: values.surname, - roleId: values.roleId, - companySizeId: values.companySizeId, - }, - }) - .unwrap() - .then(() => { - addToast( - ({ close }) => ( - <Notification - onClose={close} - type="success" - message={t('__PROFILE_PAGE_UPDATE_SUCCESS')} - isPrimary - /> - ), - { placement: 'top' } - ); - }) - .catch(() => { - addToast( - ({ close }) => ( - <Notification - onClose={close} - type="error" - message={t('__PROFILE_PAGE_TOAST_ERROR_UPDATING_PROFILE')} - isPrimary - /> - ), - { placement: 'top' } - ); - }) - .finally(() => { - actions.setSubmitting(false); - }); */ + await handleUpdatePreferences(values); + actions.setSubmitting(false); }} > <NotificationSettingsCard /> From 8faed437d340991b2b2133156376deb56c78ed75 Mon Sep 17 00:00:00 2001 From: ZecD <dominik.zecoli@gmail.com> Date: Fri, 7 Nov 2025 18:10:20 +0100 Subject: [PATCH 070/108] feat: update notification settings success and error messages for improved user feedback --- src/locales/en/translation.json | 3 +- src/locales/it/translation.json | 3 +- .../Profile/FormNotificationSettings.tsx | 36 +++++++++++-------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index f17f66f91..d093ab890 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1270,7 +1270,6 @@ "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT": "You’ll receive updates for activities only for these activities", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT_TEXT": "Your changes are saved automatically", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_LABEL": "Activities you’re following", - "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_PROGRESS_DESCRIPTION": "Activity progress ", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_SETUP_DESCRIPTION": "Activity setup ", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_TAG": "tot.", "__PROFILE_PAGE_NOTIFICATIONS_CARD_LABEL": "Notification settings", @@ -1282,7 +1281,9 @@ "__PROFILE_PAGE_TOAST_ERROR_INVALID_CURRENT_PASSWORD": "Invalid current password. Please try again.", "__PROFILE_PAGE_TOAST_ERROR_UPDATING_PASSWORD": "An error occurred while updating the password. Please try again.", "__PROFILE_PAGE_TOAST_ERROR_UPDATING_PROFILE": "An error occurred while updating your profile. Please try again.", + "__PROFILE_PAGE_TOAST_ERROR_UPDATING_SETTINGS": "Something went wrong, please try again later.", "__PROFILE_PAGE_TOAST_SUCCESS_PASSWORD_UPDATED": "Password updated successfully", + "__PROFILE_PAGE_UPDATE_SETTINGS_SUCCESS": "Settings updated successfully", "__PROFILE_PAGE_UPDATE_SUCCESS": "Profile updated successfully", "__PROFILE_PAGE_USER_CARD_COMPANY_SIZE_LABEL": "Company size", "__PROFILE_PAGE_USER_CARD_COMPANY_SIZE_PLACEHOLDER": "Select a Company size", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 410df31a8..2f50ffa7c 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1304,7 +1304,6 @@ "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT_TEXT": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_LABEL": "", - "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_PROGRESS_DESCRIPTION": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_SETUP_DESCRIPTION": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_TAG": "", "__PROFILE_PAGE_NOTIFICATIONS_CARD_LABEL": "", @@ -1316,7 +1315,9 @@ "__PROFILE_PAGE_TOAST_ERROR_INVALID_CURRENT_PASSWORD": "", "__PROFILE_PAGE_TOAST_ERROR_UPDATING_PASSWORD": "", "__PROFILE_PAGE_TOAST_ERROR_UPDATING_PROFILE": "", + "__PROFILE_PAGE_TOAST_ERROR_UPDATING_SETTINGS": "", "__PROFILE_PAGE_TOAST_SUCCESS_PASSWORD_UPDATED": "", + "__PROFILE_PAGE_UPDATE_SETTINGS_SUCCESS": "", "__PROFILE_PAGE_UPDATE_SUCCESS": "", "__PROFILE_PAGE_USER_CARD_COMPANY_SIZE_LABEL": "", "__PROFILE_PAGE_USER_CARD_COMPANY_SIZE_PLACEHOLDER": "", diff --git a/src/pages/Profile/FormNotificationSettings.tsx b/src/pages/Profile/FormNotificationSettings.tsx index 7d325b07a..878585c32 100644 --- a/src/pages/Profile/FormNotificationSettings.tsx +++ b/src/pages/Profile/FormNotificationSettings.tsx @@ -55,21 +55,29 @@ export const FormNotificationSettings = () => { }, }).unwrap(); } - addToast(({ close }) => ( - <Notification - onClose={close} - type="success" - title={t('__PROFILE_PAGE_NOTIFICATIONS_UPDATE_SUCCESS_MESSAGE')} - /> - )); + addToast( + ({ close }) => ( + <Notification + onClose={close} + type="success" + message={t('__PROFILE_PAGE_UPDATE_SETTINGS_SUCCESS')} + isPrimary + /> + ), + { placement: 'top' } + ); } catch (error) { - addToast(({ close }) => ( - <Notification - onClose={close} - type="error" - title={t('__PROFILE_PAGE_NOTIFICATIONS_UPDATE_ERROR_MESSAGE')} - /> - )); + addToast( + ({ close }) => ( + <Notification + onClose={close} + type="error" + message={t('__PROFILE_PAGE_TOAST_ERROR_UPDATING_SETTINGS')} + isPrimary + /> + ), + { placement: 'top' } + ); } }; From 65c2003f53d349e3f3a57912520884b6c4a0048d Mon Sep 17 00:00:00 2001 From: ZecD <dominik.zecoli@gmail.com> Date: Mon, 10 Nov 2025 09:20:44 +0100 Subject: [PATCH 071/108] feat: add functionality to unfollow activities with success and error notifications in FollowActivitiesPanel --- src/common/schema.ts | 28 +++++++++++ src/features/api/index.ts | 19 +++++++ src/locales/en/translation.json | 2 + src/locales/it/translation.json | 2 + .../FollowActivitiesPanel.tsx | 50 +++++++++++++++++-- .../FollowActivitiesAccordion.tsx/index.tsx | 17 ++++++- 6 files changed, 113 insertions(+), 5 deletions(-) diff --git a/src/common/schema.ts b/src/common/schema.ts index e7f1ad93e..4b7ef28ea 100644 --- a/src/common/schema.ts +++ b/src/common/schema.ts @@ -477,6 +477,10 @@ export interface paths { }; }; }; + "/users/me/watched/plans": { + get: operations["get-users-me-watched-plans"]; + parameters: {}; + }; "/users/roles": { get: operations["get-users-roles"]; parameters: {}; @@ -3892,6 +3896,30 @@ export interface operations { }; }; }; + "get-users-me-watched-plans": { + parameters: {}; + responses: { + 200: { + content: { + "application/json": { + items: { + id?: number; + name?: string; + project?: { + name?: string; + id?: number; + }; + }[]; + allItems: number; + }; + }; + }; + 400: components["responses"]["Error"]; + 403: components["responses"]["Error"]; + 404: components["responses"]["Error"]; + 500: components["responses"]["Error"]; + }; + }; "get-users-roles": { parameters: {}; responses: { diff --git a/src/features/api/index.ts b/src/features/api/index.ts index fa9924f8b..ce5b83e40 100644 --- a/src/features/api/index.ts +++ b/src/features/api/index.ts @@ -611,6 +611,12 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.body, }), }), + getUsersMeWatchedPlans: build.query< + GetUsersMeWatchedPlansApiResponse, + GetUsersMeWatchedPlansApiArg + >({ + query: () => ({ url: `/users/me/watched/plans` }), + }), getUsersRoles: build.query<GetUsersRolesApiResponse, GetUsersRolesApiArg>({ query: () => ({ url: `/users/roles` }), }), @@ -1816,6 +1822,18 @@ export type PutUsersMePreferencesBySlugApiArg = { value: string; }; }; +export type GetUsersMeWatchedPlansApiResponse = /** status 200 */ { + items: { + id?: number; + name?: string; + project?: { + name?: string; + id?: number; + }; + }[]; + allItems: number; +}; +export type GetUsersMeWatchedPlansApiArg = void; export type GetUsersRolesApiResponse = /** status 200 OK */ { id: number; name: string; @@ -3108,6 +3126,7 @@ export const { usePatchUsersMeMutation, useGetUsersMePreferencesQuery, usePutUsersMePreferencesBySlugMutation, + useGetUsersMeWatchedPlansQuery, useGetUsersRolesQuery, useGetVideosByVidQuery, useGetVideosByVidObservationsQuery, diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index d093ab890..e7752b578 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1283,6 +1283,8 @@ "__PROFILE_PAGE_TOAST_ERROR_UPDATING_PROFILE": "An error occurred while updating your profile. Please try again.", "__PROFILE_PAGE_TOAST_ERROR_UPDATING_SETTINGS": "Something went wrong, please try again later.", "__PROFILE_PAGE_TOAST_SUCCESS_PASSWORD_UPDATED": "Password updated successfully", + "__PROFILE_PAGE_UNFOLLOW_ERROR": "Ops! Something went wrong. Please try again", + "__PROFILE_PAGE_UNFOLLOW_SUCCESS": "You’ve unfollowed this activity", "__PROFILE_PAGE_UPDATE_SETTINGS_SUCCESS": "Settings updated successfully", "__PROFILE_PAGE_UPDATE_SUCCESS": "Profile updated successfully", "__PROFILE_PAGE_USER_CARD_COMPANY_SIZE_LABEL": "Company size", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 2f50ffa7c..82f1e2081 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1317,6 +1317,8 @@ "__PROFILE_PAGE_TOAST_ERROR_UPDATING_PROFILE": "", "__PROFILE_PAGE_TOAST_ERROR_UPDATING_SETTINGS": "", "__PROFILE_PAGE_TOAST_SUCCESS_PASSWORD_UPDATED": "", + "__PROFILE_PAGE_UNFOLLOW_ERROR": "", + "__PROFILE_PAGE_UNFOLLOW_SUCCESS": "", "__PROFILE_PAGE_UPDATE_SETTINGS_SUCCESS": "", "__PROFILE_PAGE_UPDATE_SUCCESS": "", "__PROFILE_PAGE_USER_CARD_COMPANY_SIZE_LABEL": "", diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx index 8efe47e29..c8eba8bcc 100644 --- a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx @@ -4,11 +4,17 @@ import { Hint, Label, SM, + useToast, + Notification, } from '@appquality/unguess-design-system'; import { useTranslation } from 'react-i18next'; import { ReactComponent as InfoIcon } from 'src/assets/icons/info-icon.svg'; import styled from 'styled-components'; +import { + GetUsersMeWatchedPlansApiResponse, + useDeletePlansByPidWatchersAndProfileIdMutation, +} from 'src/features/api'; const StyledPanelSectionContainer = styled.div` display: flex; @@ -31,8 +37,46 @@ const StyledActivityItem = styled.div` justify-content: space-between; `; -export const FollowActivitiesPanel = () => { +export const FollowActivitiesPanel = ({ + followedActivities, +}: { + followedActivities: GetUsersMeWatchedPlansApiResponse['items']; +}) => { const { t } = useTranslation(); + const { addToast } = useToast(); + + const [unfollowPlan] = useDeletePlansByPidWatchersAndProfileIdMutation(); + + const handleUnfollow = async (planId: number) => { + try { + await unfollowPlan({ pid: planId.toString(), profileId: '25' }).unwrap(); + + addToast( + ({ close }) => ( + <Notification + onClose={close} + type="success" + message={t('__PROFILE_PAGE_UNFOLLOW_SUCCESS')} + isPrimary + /> + ), + { placement: 'top' } + ); + } catch (error) { + addToast( + ({ close }) => ( + <Notification + onClose={close} + type="error" + message={t('__PROFILE_PAGE_UNFOLLOW_ERROR')} + isPrimary + /> + ), + { placement: 'top' } + ); + } + }; + return ( <div> <StyledPanelSectionContainer> @@ -40,14 +84,14 @@ export const FollowActivitiesPanel = () => { {t( '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_SETUP_DESCRIPTION' )} - (3) + {`(${followedActivities.length})`} </Label> <StyledActivityItem> <div> <Anchor>Titolo Attivita</Anchor> <SM>titolo progetto</SM> </div> - <Button size="small" isBasic> + <Button size="small" isBasic onClick={() => handleUnfollow(123)}> {t( '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT' )} diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx index 5a1cb3afa..63d4ec553 100644 --- a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx @@ -2,11 +2,20 @@ import { AccordionNew, Tag } from '@appquality/unguess-design-system'; import { useTranslation } from 'react-i18next'; import { appTheme } from 'src/app/theme'; import { ReactComponent as EyeIcon } from 'src/assets/icons/eye-icon-fill.svg'; +import { useGetUsersMeWatchedPlansQuery } from 'src/features/api'; import { FollowActivitiesPanel } from './FollowActivitiesPanel'; +import { Loader } from '../cardLoader'; export const FollowActivitiesAccordion = () => { const { t } = useTranslation(); + const { + data: followedActivities, + isLoading, + isError, + } = useGetUsersMeWatchedPlansQuery(); + if (isLoading) return <Loader />; + if (isError || !followedActivities) return null; return ( <AccordionNew hasBorder={false} level={3} onChange={() => {}}> <AccordionNew.Section> @@ -29,13 +38,17 @@ export const FollowActivitiesAccordion = () => { /> <AccordionNew.Meta> <Tag> - 4/5 + {`${followedActivities.items.length}/${followedActivities.allItems} `} {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_TAG')} </Tag> </AccordionNew.Meta> </AccordionNew.Header> <AccordionNew.Panel> - <FollowActivitiesPanel /> + {followedActivities.items.length > 0 && ( + <FollowActivitiesPanel + followedActivities={followedActivities.items} + /> + )} </AccordionNew.Panel> </AccordionNew.Section> </AccordionNew> From a85957adf2d03d3e90c6b8a074815d04328a3f8f Mon Sep 17 00:00:00 2001 From: ZecD <dominik.zecoli@gmail.com> Date: Mon, 10 Nov 2025 11:26:43 +0100 Subject: [PATCH 072/108] feat: update FollowActivitiesPanel to display activity names and project titles dynamically --- .../FollowActivitiesPanel.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx index c8eba8bcc..9a4c311c3 100644 --- a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx @@ -86,17 +86,19 @@ export const FollowActivitiesPanel = ({ )} {`(${followedActivities.length})`} </Label> - <StyledActivityItem> - <div> - <Anchor>Titolo Attivita</Anchor> - <SM>titolo progetto</SM> - </div> - <Button size="small" isBasic onClick={() => handleUnfollow(123)}> - {t( - '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT' - )} - </Button> - </StyledActivityItem> + {followedActivities.map((activity) => ( + <StyledActivityItem> + <div> + <Anchor href={`/plans/${activity.id}`}>{activity.name}</Anchor> + <SM>{activity?.project?.name}</SM> + </div> + <Button size="small" isBasic onClick={() => handleUnfollow(123)}> + {t( + '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT' + )} + </Button> + </StyledActivityItem> + ))} </StyledPanelSectionContainer> <StyledHintContainer> <InfoIcon /> From 78f140670fbf18dccd48b5884c1b75383d75242e Mon Sep 17 00:00:00 2001 From: ZecD <dominik.zecoli@gmail.com> Date: Mon, 10 Nov 2025 16:07:29 +0100 Subject: [PATCH 073/108] feat: enhance FollowActivitiesPanel to use dynamic profile ID for unfollowing plans and disable button for last activity --- src/common/schema.ts | 1 + src/features/api/apiTags.ts | 5 ++++- src/features/api/index.ts | 1 + .../FollowActivitiesPanel.tsx | 16 ++++++++++++---- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/common/schema.ts b/src/common/schema.ts index 4b7ef28ea..2797f8d5e 100644 --- a/src/common/schema.ts +++ b/src/common/schema.ts @@ -3909,6 +3909,7 @@ export interface operations { name?: string; id?: number; }; + isLast?: boolean; }[]; allItems: number; }; diff --git a/src/features/api/apiTags.ts b/src/features/api/apiTags.ts index 74b6c4963..ecbf95065 100644 --- a/src/features/api/apiTags.ts +++ b/src/features/api/apiTags.ts @@ -340,7 +340,10 @@ unguessApi.enhanceEndpoints({ invalidatesTags: ['PlanWatchers'], }, deletePlansByPidWatchersAndProfileId: { - invalidatesTags: ['PlanWatchers'], + invalidatesTags: ['PlanWatchers', 'Plans'], + }, + getUsersMeWatchedPlans: { + providesTags: ['Plans', 'PlanWatchers'], }, }, }); diff --git a/src/features/api/index.ts b/src/features/api/index.ts index ce5b83e40..900ba5a87 100644 --- a/src/features/api/index.ts +++ b/src/features/api/index.ts @@ -1830,6 +1830,7 @@ export type GetUsersMeWatchedPlansApiResponse = /** status 200 */ { name?: string; id?: number; }; + isLast?: boolean; }[]; allItems: number; }; diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx index 9a4c311c3..7f7f148ef 100644 --- a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx @@ -14,6 +14,7 @@ import styled from 'styled-components'; import { GetUsersMeWatchedPlansApiResponse, useDeletePlansByPidWatchersAndProfileIdMutation, + useGetUsersMeQuery, } from 'src/features/api'; const StyledPanelSectionContainer = styled.div` @@ -44,12 +45,14 @@ export const FollowActivitiesPanel = ({ }) => { const { t } = useTranslation(); const { addToast } = useToast(); - + const { data: userData } = useGetUsersMeQuery(); const [unfollowPlan] = useDeletePlansByPidWatchersAndProfileIdMutation(); - const handleUnfollow = async (planId: number) => { try { - await unfollowPlan({ pid: planId.toString(), profileId: '25' }).unwrap(); + await unfollowPlan({ + pid: planId.toString(), + profileId: userData?.profile_id.toString() ?? '', + }).unwrap(); addToast( ({ close }) => ( @@ -92,7 +95,12 @@ export const FollowActivitiesPanel = ({ <Anchor href={`/plans/${activity.id}`}>{activity.name}</Anchor> <SM>{activity?.project?.name}</SM> </div> - <Button size="small" isBasic onClick={() => handleUnfollow(123)}> + <Button + disabled={activity.isLast} + size="small" + isBasic + onClick={() => handleUnfollow(activity.id ?? 0)} + > {t( '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT' )} From 5c6d1ceee4c82202a80ef04c6d7434e33c2451f6 Mon Sep 17 00:00:00 2001 From: ZecD <dominik.zecoli@gmail.com> Date: Tue, 11 Nov 2025 10:27:59 +0100 Subject: [PATCH 074/108] feat: update @appquality/unguess-design-system to version 4.0.51 in package.json and yarn.lock --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1b288d626..f2f8c7269 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@analytics/google-tag-manager": "^0.6.0", "@analytics/hubspot": "^0.5.1", "@appquality/languages": "1.4.3", - "@appquality/unguess-design-system": "4.0.50", + "@appquality/unguess-design-system": "4.0.51", "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", diff --git a/yarn.lock b/yarn.lock index 106faacc3..26fb7edf2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,10 +122,10 @@ dependencies: hls.js "^1.4.8" -"@appquality/unguess-design-system@4.0.50": - version "4.0.50" - resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.50.tgz#1080f1963fc7430848d50f964a76c8fa903d0105" - integrity sha512-bfQnSWAHQyuDroaCRcpvmXn0V2Jtp2N15ukTuYiVAa7Z0g9tOsFskWsiR8IzLoinsEQ0qam0ySqIN6TCcnUbdQ== +"@appquality/unguess-design-system@4.0.51": + version "4.0.51" + resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.51.tgz#2a062a34a753e5b8ac453ebf32aaff3ab51442be" + integrity sha512-WKrvfIh5rSyMqRt2kRrBTljOinY8+g1oqrvDmgcYUrSlE912O6tRuPUjYjoqWN+B9j0fKXvmSrjx/YCh7zuJ6Q== dependencies: "@appquality/stream-player" "1.0.6" "@nivo/bar" "^0.87.0" From 7bdc6abbab11b708bd40e7e25ad7ca4a9d81d759 Mon Sep 17 00:00:00 2001 From: ZecD <dominik.zecoli@gmail.com> Date: Tue, 11 Nov 2025 12:34:13 +0100 Subject: [PATCH 075/108] feat: add UnfollowButton component with tooltip for disabled state and update translation files --- src/locales/en/translation.json | 1 + src/locales/it/translation.json | 1 + .../FollowActivitiesPanel.tsx | 22 ++++------ .../UnfollowButton.tsx | 40 +++++++++++++++++++ .../FollowActivitiesAccordion.tsx/index.tsx | 7 +++- .../parts/NotificationsAccordion/index.tsx | 7 +++- .../Profile/parts/PasswordAccordion/index.tsx | 1 + 7 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 src/pages/Profile/parts/FollowActivitiesAccordion.tsx/UnfollowButton.tsx diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index e7752b578..05f027318 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1283,6 +1283,7 @@ "__PROFILE_PAGE_TOAST_ERROR_UPDATING_PROFILE": "An error occurred while updating your profile. Please try again.", "__PROFILE_PAGE_TOAST_ERROR_UPDATING_SETTINGS": "Something went wrong, please try again later.", "__PROFILE_PAGE_TOAST_SUCCESS_PASSWORD_UPDATED": "Password updated successfully", + "__PROFILE_PAGE_UNFOLLOW_BUTTON_DISABLED_TOOLTIP": "At least one person must follow this activity", "__PROFILE_PAGE_UNFOLLOW_ERROR": "Ops! Something went wrong. Please try again", "__PROFILE_PAGE_UNFOLLOW_SUCCESS": "You’ve unfollowed this activity", "__PROFILE_PAGE_UPDATE_SETTINGS_SUCCESS": "Settings updated successfully", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 82f1e2081..f0b6ae5dc 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1317,6 +1317,7 @@ "__PROFILE_PAGE_TOAST_ERROR_UPDATING_PROFILE": "", "__PROFILE_PAGE_TOAST_ERROR_UPDATING_SETTINGS": "", "__PROFILE_PAGE_TOAST_SUCCESS_PASSWORD_UPDATED": "", + "__PROFILE_PAGE_UNFOLLOW_BUTTON_DISABLED_TOOLTIP": "", "__PROFILE_PAGE_UNFOLLOW_ERROR": "", "__PROFILE_PAGE_UNFOLLOW_SUCCESS": "", "__PROFILE_PAGE_UPDATE_SETTINGS_SUCCESS": "", diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx index 7f7f148ef..f613e5aea 100644 --- a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx @@ -1,7 +1,5 @@ import { Anchor, - Button, - Hint, Label, SM, useToast, @@ -16,6 +14,7 @@ import { useDeletePlansByPidWatchersAndProfileIdMutation, useGetUsersMeQuery, } from 'src/features/api'; +import { UnfollowButton } from './UnfollowButton'; const StyledPanelSectionContainer = styled.div` display: flex; @@ -95,24 +94,19 @@ export const FollowActivitiesPanel = ({ <Anchor href={`/plans/${activity.id}`}>{activity.name}</Anchor> <SM>{activity?.project?.name}</SM> </div> - <Button - disabled={activity.isLast} - size="small" - isBasic - onClick={() => handleUnfollow(activity.id ?? 0)} - > - {t( - '__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT' - )} - </Button> + <UnfollowButton + isDisabled={!!activity.isLast} + activityId={activity.id ?? 0} + handleUnfollow={handleUnfollow} + /> </StyledActivityItem> ))} </StyledPanelSectionContainer> <StyledHintContainer> <InfoIcon /> - <Hint> + <SM> {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_HINT_TEXT')} - </Hint> + </SM> </StyledHintContainer> </div> ); diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/UnfollowButton.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/UnfollowButton.tsx new file mode 100644 index 000000000..18386db1f --- /dev/null +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/UnfollowButton.tsx @@ -0,0 +1,40 @@ +import { Button, Tooltip } from '@appquality/unguess-design-system'; +import { useTranslation } from 'react-i18next'; + +const UnfollowButton = ({ + isDisabled, + activityId, + handleUnfollow, +}: { + isDisabled: boolean; + activityId: number; + handleUnfollow: (id: number) => void; +}) => { + const { t } = useTranslation(); + + const button = ( + <Button + disabled={isDisabled} + size="small" + isBasic + onClick={() => handleUnfollow(activityId)} + > + {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_FOLLOW_ACTIVITIES_BUTTON_TEXT')} + </Button> + ); + if (isDisabled) { + return ( + <Tooltip + placement="start" + type="light" + size="large" + content={t('__PROFILE_PAGE_UNFOLLOW_BUTTON_DISABLED_TOOLTIP')} + > + <div>{button}</div> + </Tooltip> + ); + } + return button; +}; + +export { UnfollowButton }; diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx index 63d4ec553..fc314d528 100644 --- a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx @@ -17,7 +17,12 @@ export const FollowActivitiesAccordion = () => { if (isLoading) return <Loader />; if (isError || !followedActivities) return null; return ( - <AccordionNew hasBorder={false} level={3} onChange={() => {}}> + <AccordionNew + isCompact + hasBorder={false} + level={3} + defaultExpandedSections={[]} + > <AccordionNew.Section> <AccordionNew.Header icon={ diff --git a/src/pages/Profile/parts/NotificationsAccordion/index.tsx b/src/pages/Profile/parts/NotificationsAccordion/index.tsx index 3d64fdf42..8aa03a353 100644 --- a/src/pages/Profile/parts/NotificationsAccordion/index.tsx +++ b/src/pages/Profile/parts/NotificationsAccordion/index.tsx @@ -7,7 +7,12 @@ export const NotificationSettingsAccordion = () => { const { t } = useTranslation(); return ( - <AccordionNew hasBorder={false} level={3} onChange={() => {}}> + <AccordionNew + isCompact + hasBorder={false} + level={3} + defaultExpandedSections={[]} + > <AccordionNew.Section> <AccordionNew.Header> <AccordionNew.Label diff --git a/src/pages/Profile/parts/PasswordAccordion/index.tsx b/src/pages/Profile/parts/PasswordAccordion/index.tsx index af4817601..a12e9c4de 100644 --- a/src/pages/Profile/parts/PasswordAccordion/index.tsx +++ b/src/pages/Profile/parts/PasswordAccordion/index.tsx @@ -84,6 +84,7 @@ export const PasswordAccordion = () => { <ContainerCard style={{ padding: 0 }}> <AccordionNew level={3} + isCompact id="anchor-password-id" data-qa="password-accordion" key={`password_accordion_${isOpen}`} From c1393c94d2caa6f4d7f442c3b1e5c4a597e39af0 Mon Sep 17 00:00:00 2001 From: ZecD <dominik.zecoli@gmail.com> Date: Tue, 11 Nov 2025 12:56:50 +0100 Subject: [PATCH 076/108] feat: enhance Profile page with active tab state management and update FollowActivitiesPanel to truncate activity names --- src/pages/Profile/index.tsx | 5 +++++ .../FollowActivitiesPanel.tsx | 14 +++++++++++++- .../parts/FollowActivitiesAccordion.tsx/index.tsx | 1 + .../Profile/parts/NotificationSettingsCard.tsx | 7 ++++++- .../Profile/parts/NotificationsAccordion/index.tsx | 1 + .../Profile/parts/PasswordAccordion/index.tsx | 1 - 6 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/pages/Profile/index.tsx b/src/pages/Profile/index.tsx index 186be6306..45794fc74 100644 --- a/src/pages/Profile/index.tsx +++ b/src/pages/Profile/index.tsx @@ -7,6 +7,7 @@ import { StickyNavItemLabel, } from 'src/common/components/navigation/asideNav'; import { Page } from 'src/features/templates/Page'; +import { useState } from 'react'; import ProfilePageHeader from 'src/pages/Profile/Header'; import { FormPassword } from './FormPassword'; import { FormProfile } from './FormProfile'; @@ -14,6 +15,7 @@ import { FormNotificationSettings } from './FormNotificationSettings'; const Profile = () => { const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState('profile'); return ( <Page @@ -35,6 +37,9 @@ const Profile = () => { smooth duration={500} offset={-30} + onSetActive={() => { + setActiveTab('profile'); + }} > {t('__PROFILE_PAGE_NAV_ITEM_PROFILE')} </StickyNavItem> diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx index f613e5aea..316372200 100644 --- a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/FollowActivitiesPanel.tsx @@ -46,6 +46,16 @@ export const FollowActivitiesPanel = ({ const { addToast } = useToast(); const { data: userData } = useGetUsersMeQuery(); const [unfollowPlan] = useDeletePlansByPidWatchersAndProfileIdMutation(); + + const truncateName = ( + name: string | undefined, + maxLength: number = 40 + ): string => { + if (!name) return ''; + return name.length > maxLength + ? `${name.substring(0, maxLength)}...` + : name; + }; const handleUnfollow = async (planId: number) => { try { await unfollowPlan({ @@ -91,7 +101,9 @@ export const FollowActivitiesPanel = ({ {followedActivities.map((activity) => ( <StyledActivityItem> <div> - <Anchor href={`/plans/${activity.id}`}>{activity.name}</Anchor> + <Anchor href={`/plans/${activity.id}`}> + {truncateName(activity?.name)} + </Anchor> <SM>{activity?.project?.name}</SM> </div> <UnfollowButton diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx index fc314d528..1793a8945 100644 --- a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx @@ -22,6 +22,7 @@ export const FollowActivitiesAccordion = () => { hasBorder={false} level={3} defaultExpandedSections={[]} + responsiveBreakpoint={730} > <AccordionNew.Section> <AccordionNew.Header diff --git a/src/pages/Profile/parts/NotificationSettingsCard.tsx b/src/pages/Profile/parts/NotificationSettingsCard.tsx index 8bb402e59..a403150c3 100644 --- a/src/pages/Profile/parts/NotificationSettingsCard.tsx +++ b/src/pages/Profile/parts/NotificationSettingsCard.tsx @@ -47,7 +47,12 @@ export const NotificationSettingsCard = () => { {t('__PROFILE_PAGE_NOTIFICATIONS_CARD_DESCRIPTION')} </MD> </StyledNotificationsCardHeaderWrapper> - <Divider /> + <Divider + style={{ + marginTop: appTheme.space.sm, + marginBottom: appTheme.space.sm, + }} + /> <NotificationSettingsAccordion /> <Button disabled={!canSave} diff --git a/src/pages/Profile/parts/NotificationsAccordion/index.tsx b/src/pages/Profile/parts/NotificationsAccordion/index.tsx index 8aa03a353..6d25440bc 100644 --- a/src/pages/Profile/parts/NotificationsAccordion/index.tsx +++ b/src/pages/Profile/parts/NotificationsAccordion/index.tsx @@ -12,6 +12,7 @@ export const NotificationSettingsAccordion = () => { hasBorder={false} level={3} defaultExpandedSections={[]} + responsiveBreakpoint={730} > <AccordionNew.Section> <AccordionNew.Header> diff --git a/src/pages/Profile/parts/PasswordAccordion/index.tsx b/src/pages/Profile/parts/PasswordAccordion/index.tsx index a12e9c4de..af4817601 100644 --- a/src/pages/Profile/parts/PasswordAccordion/index.tsx +++ b/src/pages/Profile/parts/PasswordAccordion/index.tsx @@ -84,7 +84,6 @@ export const PasswordAccordion = () => { <ContainerCard style={{ padding: 0 }}> <AccordionNew level={3} - isCompact id="anchor-password-id" data-qa="password-accordion" key={`password_accordion_${isOpen}`} From 5b52093dbe92ae7fc269d43ecae3eb4c02721a6b Mon Sep 17 00:00:00 2001 From: ZecD <dominik.zecoli@gmail.com> Date: Tue, 11 Nov 2025 16:46:55 +0100 Subject: [PATCH 077/108] feat: update responsive breakpoints for accordions and refine translation label for activity updates --- src/locales/en/translation.json | 2 +- src/pages/Profile/index.tsx | 23 +++++++++++-------- .../FollowActivitiesAccordion.tsx/index.tsx | 2 +- .../parts/NotificationsAccordion/index.tsx | 2 +- .../Profile/parts/PasswordAccordion/index.tsx | 2 +- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 05f027318..6d7df625d 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1261,7 +1261,7 @@ "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_CHECKBOX_LABEL": "Activity progress", "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_FORM_LABEL": "Receive updates for:", "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_HINT": "Choose which progress notifications you’d like to receive by email for the activities you follow", - "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_LABEL": "Activity progress updates", + "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_LABEL": "Activity updates", "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_PROGRESS_UPDATES_TAG": "Recommended to keep enabled", "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_HINT": "From configuration through planning", "__PROFILE_PAGE_NOTIFICATIONS_CARD_ACTIVITY_SETUP_CHECKBOX_LABEL": "Activity setup", diff --git a/src/pages/Profile/index.tsx b/src/pages/Profile/index.tsx index 45794fc74..5865a092c 100644 --- a/src/pages/Profile/index.tsx +++ b/src/pages/Profile/index.tsx @@ -7,7 +7,6 @@ import { StickyNavItemLabel, } from 'src/common/components/navigation/asideNav'; import { Page } from 'src/features/templates/Page'; -import { useState } from 'react'; import ProfilePageHeader from 'src/pages/Profile/Header'; import { FormPassword } from './FormPassword'; import { FormProfile } from './FormProfile'; @@ -15,7 +14,6 @@ import { FormNotificationSettings } from './FormNotificationSettings'; const Profile = () => { const { t } = useTranslation(); - const [activeTab, setActiveTab] = useState('profile'); return ( <Page @@ -28,28 +26,33 @@ const Profile = () => { <Grid gutters="xl" columns={12} style={{ marginTop: theme.space.xxl }}> <Row> <Col xs={12} lg={2} style={{ margin: 0 }}> - <AsideNav containerId="main"> + <AsideNav + containerId="main" + isSpy + isSmooth + duration={500} + offset={-30} + > <> <StickyNavItem - id="anchor-profile" to="anchor-profile-id" containerId="main" + spy smooth duration={500} offset={-30} - onSetActive={() => { - setActiveTab('profile'); - }} + activeClass="isCurrent" > {t('__PROFILE_PAGE_NAV_ITEM_PROFILE')} </StickyNavItem> <StickyNavItem - id="anchor-notification-settings" to="anchor-notification-settings-id" containerId="main" + spy smooth duration={500} offset={-30} + activeClass="isCurrent" > {t('__PROFILE_PAGE_NAV_ITEM_NOTIFICATION_SETTINGS')} </StickyNavItem> @@ -59,11 +62,13 @@ const Profile = () => { </StickyNavItemLabel> <StickyNavItem - id="anchor-password" to="anchor-password-id" containerId="main" + spy smooth duration={500} + offset={-30} + activeClass="isCurrent" > {t('__PROFILE_PAGE_NAV_ITEM_PASSWORD')} </StickyNavItem> diff --git a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx index 1793a8945..2d035e43d 100644 --- a/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx +++ b/src/pages/Profile/parts/FollowActivitiesAccordion.tsx/index.tsx @@ -22,7 +22,7 @@ export const FollowActivitiesAccordion = () => { hasBorder={false} level={3} defaultExpandedSections={[]} - responsiveBreakpoint={730} + responsiveBreakpoint={650} > <AccordionNew.Section> <AccordionNew.Header diff --git a/src/pages/Profile/parts/NotificationsAccordion/index.tsx b/src/pages/Profile/parts/NotificationsAccordion/index.tsx index 6d25440bc..4ab08a6f8 100644 --- a/src/pages/Profile/parts/NotificationsAccordion/index.tsx +++ b/src/pages/Profile/parts/NotificationsAccordion/index.tsx @@ -12,7 +12,7 @@ export const NotificationSettingsAccordion = () => { hasBorder={false} level={3} defaultExpandedSections={[]} - responsiveBreakpoint={730} + responsiveBreakpoint={650} > <AccordionNew.Section> <AccordionNew.Header> diff --git a/src/pages/Profile/parts/PasswordAccordion/index.tsx b/src/pages/Profile/parts/PasswordAccordion/index.tsx index af4817601..aa6fa0a54 100644 --- a/src/pages/Profile/parts/PasswordAccordion/index.tsx +++ b/src/pages/Profile/parts/PasswordAccordion/index.tsx @@ -78,7 +78,7 @@ export const PasswordAccordion = () => { submitForm, } = useFormikContext<PasswordFormValues>(); - const isOpen = false; // Temporary, as the context is not fully implemented yet + const isOpen = false; return ( <ContainerCard style={{ padding: 0 }}> From 6685fe4ad2611bafaabe551eb31c9e85e24d9f8b Mon Sep 17 00:00:00 2001 From: Kariamos <minucci9292@gmail.com> Date: Wed, 5 Nov 2025 14:36:39 +0100 Subject: [PATCH 078/108] refactor: Remove unused preferences query and related settings from profile modal --- .../Navigation/NavigationProfileModal.tsx | 51 +------------------ 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/src/features/navigation/Navigation/NavigationProfileModal.tsx b/src/features/navigation/Navigation/NavigationProfileModal.tsx index 045d376f4..ed0f99940 100644 --- a/src/features/navigation/Navigation/NavigationProfileModal.tsx +++ b/src/features/navigation/Navigation/NavigationProfileModal.tsx @@ -9,11 +9,7 @@ import { useAppDispatch, useAppSelector } from 'src/app/hooks'; import { isDev } from 'src/common/isDevEnvironment'; import { prepareGravatar } from 'src/common/utils'; import WPAPI from 'src/common/wpapi'; -import { - useGetUsersMePreferencesQuery, - useGetUsersMeQuery, - usePutUsersMePreferencesBySlugMutation, -} from 'src/features/api'; +import { useGetUsersMeQuery } from 'src/features/api'; import { useActiveWorkspace } from 'src/hooks/useActiveWorkspace'; import { setProfileModalOpen } from '../navigationSlice'; import { usePathWithoutLocale } from '../usePathWithoutLocale'; @@ -31,41 +27,12 @@ export const NavigationProfileModal = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); - const { data: preferences } = useGetUsersMePreferencesQuery(); - - const notificationsPreference = preferences?.items?.find( - (preference) => preference?.name === 'notifications_enabled' - ); const pathWithoutLocale = usePathWithoutLocale(); - const [updatePreference] = usePutUsersMePreferencesBySlugMutation(); - const onProfileModalClose = () => { dispatch(setProfileModalOpen(false)); }; - const onSetSettings = async (value: string) => { - await updatePreference({ - slug: `${notificationsPreference?.name}`, - body: { value }, - }) - .unwrap() - .then(() => { - addToast( - ({ close }) => ( - <Notification - onClose={close} - type="success" - message={t('__PROFILE_MODAL_NOTIFICATIONS_UPDATED')} - closeText={t('__TOAST_CLOSE_TEXT')} - isPrimary - /> - ), - { placement: 'top' } - ); - }); - }; - if (dataError || !user || isLoading) return null; const profileModal = { @@ -114,22 +81,6 @@ export const NavigationProfileModal = () => { title: t('__PROFILE_MODAL_PRIVACY_ITEM_LABEL'), url: 'https://www.iubenda.com/privacy-policy/833252/full-legal', }, - settingValue: notificationsPreference?.value ?? '0', - i18n: { - settingsTitle: t('__PROFILE_MODAL_NOTIFICATIONS_TITLE'), - settingsIntroText: t('__PROFILE_MODAL_NOTIFICATIONS_INTRO'), - settingsOutroText: { - paragraph_1: t('__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_1'), - paragraph_2: t('__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_2'), - paragraph_3: t('__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_3'), - }, - settingsToggle: { - title: t('__PROFILE_MODAL_NOTIFICATIONS_TOGGLE_TITLE'), - on: t('__PROFILE_MODAL_NOTIFICATIONS_TOGGLE_ON'), - off: t('__PROFILE_MODAL_NOTIFICATIONS_TOGGLE_OFF'), - }, - }, - onSetSettings, onSelectLanguage: (lang: string) => { if (pathWithoutLocale === false) return; if (lang === i18n.language) return; From 570c301f57c094b5a60a6221c809961e35b56a9b Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Mon, 20 Oct 2025 18:27:52 +0200 Subject: [PATCH 079/108] feat: add TooltipModalContext for managing modal references in ObservationForm and TitleDropdown components --- .../Video/components/ObservationForm.tsx | 5 +- .../Video/components/TitleDropdownNew.tsx | 62 +++++++++++++++++++ src/pages/Video/components/context.tsx | 40 ++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/pages/Video/components/context.tsx diff --git a/src/pages/Video/components/ObservationForm.tsx b/src/pages/Video/components/ObservationForm.tsx index 1ee728829..ec4c99acd 100644 --- a/src/pages/Video/components/ObservationForm.tsx +++ b/src/pages/Video/components/ObservationForm.tsx @@ -30,6 +30,7 @@ import { styled } from 'styled-components'; import * as Yup from 'yup'; import { ConfirmDeleteModal } from './ConfirmDeleteModal'; import { ObservationFormValues, TitleDropdown } from './TitleDropdownNew'; +import { TooltipModalContextProvider } from './context'; const FormContainer = styled.div` padding: ${({ theme }) => theme.space.md} ${({ theme }) => theme.space.xxs}; @@ -231,7 +232,7 @@ const ObservationForm = ({ }; return ( - <> + <TooltipModalContextProvider> <FormContainer> <Formik innerRef={formRef} @@ -479,7 +480,7 @@ const ObservationForm = ({ setIsConfirmationModalOpen={setIsConfirmationModalOpen} /> )} - </> + </TooltipModalContextProvider> ); }; diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index 7966e6506..d84e8b446 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -1,15 +1,22 @@ import { Autocomplete, DropdownFieldNew as Field, + Button, + MD, + TooltipModal, } from '@appquality/unguess-design-system'; import { FormikProps } from 'formik'; +import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; +import { appTheme } from 'src/app/theme'; import { ReactComponent as CopyIcon } from 'src/assets/icons/copy-icon.svg'; +import { ReactComponent as EditIcon } from 'src/assets/icons/edit-icon.svg'; import { GetCampaignsByCidVideoTagsApiResponse, usePostCampaignsByCidVideoTagsMutation, } from 'src/features/api'; +import { useTooltipModalContext } from './context'; export interface ObservationFormValues { title: number; @@ -26,6 +33,7 @@ export const TitleDropdown = ({ formProps: FormikProps<ObservationFormValues>; }) => { const { t } = useTranslation(); + const [isExpanded, setIsExpanded] = useState(false); const { campaignId } = useParams(); const [addVideoTags] = usePostCampaignsByCidVideoTagsMutation(); const titleMaxLength = 70; @@ -34,9 +42,62 @@ export const TitleDropdown = ({ return null; } + const Edit = () => { + const triggerRef = useRef<HTMLButtonElement>(null); + const { setModalRef } = useTooltipModalContext(); + const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { + // avoid select options to be closed when clicking on the edit icon + e.stopPropagation(); + e.preventDefault(); + + e.nativeEvent.stopImmediatePropagation(); + setModalRef(triggerRef.current); + }; + return ( + <> + <Button ref={triggerRef} onClick={handleClick}> + <EditIcon /> + </Button> + </> + ); + }; + + const EditModal = () => { + const { modalRef, setModalRef } = useTooltipModalContext(); + return ( + <TooltipModal + focusOnMount={false} + onBlur={() => { + setIsExpanded(false); + }} + referenceElement={modalRef} + appendToNode={document.querySelector('main') || undefined} + onClose={() => setModalRef(null)} + placement="auto" + hasArrow={false} + role="dialog" + style={{ zIndex: 900000000000000 }} + > + <TooltipModal.Title> + <MD isBold style={{ marginBottom: appTheme.space.sm }}> + titolo + </MD> + </TooltipModal.Title> + + <TooltipModal.Body> + <label htmlFor="title-input">Title</label> + <input type="text" id="title-input" /> + </TooltipModal.Body> + </TooltipModal> + ); + }; + return ( <Field> + <EditModal /> <Autocomplete + onClick={() => setIsExpanded(true)} + isExpanded={isExpanded} listboxAppendToNode={document.querySelector('main') || undefined} isCreatable renderValue={({ selection }) => { @@ -84,6 +145,7 @@ export const TitleDropdown = ({ value: i.id.toString(), label: `${i.name} (${i.usageNumber})`, isSelected: formProps.values.title === i.id, + icon: <Edit />, }))} startIcon={<CopyIcon />} placeholder={t( diff --git a/src/pages/Video/components/context.tsx b/src/pages/Video/components/context.tsx new file mode 100644 index 000000000..1d730d479 --- /dev/null +++ b/src/pages/Video/components/context.tsx @@ -0,0 +1,40 @@ +import { createContext, useContext, useMemo, useState } from 'react'; + +interface TooltipModalContextType { + modalRef: HTMLButtonElement | null; + setModalRef: (ref: HTMLButtonElement | null) => void; +} + +const TooltipModalContext = createContext<TooltipModalContextType | null>(null); + +export const TooltipModalContextProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [modalRef, setModalRef] = + useState<TooltipModalContextType['modalRef']>(null); + + const TooltipModalContextValue = useMemo( + () => ({ + modalRef, + setModalRef, + }), + [modalRef, setModalRef] + ); + + return ( + <TooltipModalContext.Provider value={TooltipModalContextValue}> + {children} + </TooltipModalContext.Provider> + ); +}; + +export const useTooltipModalContext = () => { + const context = useContext(TooltipModalContext); + + if (!context) + throw new Error('Provider not found for TooltipModalContextProvider'); + + return context; // Now we can use the context in the component, SAFELY. +}; From 0a2c27a28eca631178ee62969aa50dad6cfcb1a4 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Mon, 20 Oct 2025 18:32:34 +0200 Subject: [PATCH 080/108] fix: update unguess-design-system dependency to version 4.0.51 canary for testing --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f2f8c7269..deb8c9e44 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@analytics/google-tag-manager": "^0.6.0", "@analytics/hubspot": "^0.5.1", "@appquality/languages": "1.4.3", - "@appquality/unguess-design-system": "4.0.51", + "@appquality/unguess-design-system": "4.0.51--canary.551.38bfc82.0", "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", diff --git a/yarn.lock b/yarn.lock index 26fb7edf2..7d970cd87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,10 +122,10 @@ dependencies: hls.js "^1.4.8" -"@appquality/unguess-design-system@4.0.51": - version "4.0.51" - resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.51.tgz#2a062a34a753e5b8ac453ebf32aaff3ab51442be" - integrity sha512-WKrvfIh5rSyMqRt2kRrBTljOinY8+g1oqrvDmgcYUrSlE912O6tRuPUjYjoqWN+B9j0fKXvmSrjx/YCh7zuJ6Q== +"@appquality/unguess-design-system@4.0.51--canary.551.38bfc82.0": + version "4.0.51--canary.551.38bfc82.0" + resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.51--canary.551.38bfc82.0.tgz#51a94c7c2588d408c984c7be584c66231ee176d3" + integrity sha512-10jGFsoZD94TBs+RG1iWOjzXz5Cm+0b5gnLkRP4qnB0E87WAD3+wcOlO+La68dJUEO8W6l/jcfQg8j95chZyXg== dependencies: "@appquality/stream-player" "1.0.6" "@nivo/bar" "^0.87.0" From 05c86ccbb73e338439799deec1cad13afb743cb0 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Wed, 22 Oct 2025 10:44:59 +0200 Subject: [PATCH 081/108] fix: update TooltipModalContext to use HTMLDivElement instead of HTMLButtonElement for modal reference --- .../Video/components/TitleDropdownNew.tsx | 59 +++++++++++++------ src/pages/Video/components/context.tsx | 4 +- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index d84e8b446..ca6742e0b 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -1,7 +1,6 @@ import { Autocomplete, DropdownFieldNew as Field, - Button, MD, TooltipModal, } from '@appquality/unguess-design-system'; @@ -16,6 +15,7 @@ import { GetCampaignsByCidVideoTagsApiResponse, usePostCampaignsByCidVideoTagsMutation, } from 'src/features/api'; +import styled from 'styled-components'; import { useTooltipModalContext } from './context'; export interface ObservationFormValues { @@ -42,23 +42,43 @@ export const TitleDropdown = ({ return null; } - const Edit = () => { - const triggerRef = useRef<HTMLButtonElement>(null); + const EditAction = styled.div` + cursor: pointer; + border-radius: 50%; + height: 24px; + width: 24px; + display: flex; + align-items: center; + justify-content: center; + background-color: ${appTheme.palette.grey[200]}; + transition: background-color 0.2s ease-in-out; + + &:hover { + background-color: ${appTheme.palette.grey[400]}; + } + `; + + const Edit = ({ optionId }: { optionId: string }) => { + const triggerRef = useRef<HTMLDivElement>(null); const { setModalRef } = useTooltipModalContext(); - const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { + const handleClick = (e: React.MouseEvent<HTMLDivElement>) => { // avoid select options to be closed when clicking on the edit icon e.stopPropagation(); e.preventDefault(); e.nativeEvent.stopImmediatePropagation(); + const el = document.querySelector(`[itemid="${optionId}"]`); + + if (el) { + el.setAttribute('hover', 'true'); + el.parentElement?.style.setProperty('pointer-events', 'none'); + } setModalRef(triggerRef.current); }; return ( - <> - <Button ref={triggerRef} onClick={handleClick}> - <EditIcon /> - </Button> - </> + <EditAction ref={triggerRef} onClick={handleClick}> + <EditIcon /> + </EditAction> ); }; @@ -66,7 +86,6 @@ export const TitleDropdown = ({ const { modalRef, setModalRef } = useTooltipModalContext(); return ( <TooltipModal - focusOnMount={false} onBlur={() => { setIsExpanded(false); }} @@ -76,7 +95,7 @@ export const TitleDropdown = ({ placement="auto" hasArrow={false} role="dialog" - style={{ zIndex: 900000000000000 }} + style={{ maxWidth: 300, transform: 'translateX(-400px)' }} > <TooltipModal.Title> <MD isBold style={{ marginBottom: appTheme.space.sm }}> @@ -98,6 +117,7 @@ export const TitleDropdown = ({ <Autocomplete onClick={() => setIsExpanded(true)} isExpanded={isExpanded} + isDisabled={isExpanded} listboxAppendToNode={document.querySelector('main') || undefined} isCreatable renderValue={({ selection }) => { @@ -140,13 +160,16 @@ export const TitleDropdown = ({ if (!selectionValue || !inputValue) return; formProps.setFieldValue('title', Number(selectionValue)); }} - options={(titles || []).map((i) => ({ - id: i.id.toString(), - value: i.id.toString(), - label: `${i.name} (${i.usageNumber})`, - isSelected: formProps.values.title === i.id, - icon: <Edit />, - }))} + options={(titles || []).map((i) => { + return { + id: i.id.toString(), + value: i.id.toString(), + label: `${i.name} (${i.usageNumber})`, + isSelected: formProps.values.title === i.id, + action: <Edit optionId={i.id.toString()} />, + itemID: i.id.toString(), + }; + })} startIcon={<CopyIcon />} placeholder={t( '__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_PLACEHOLDER' diff --git a/src/pages/Video/components/context.tsx b/src/pages/Video/components/context.tsx index 1d730d479..78a2ee71c 100644 --- a/src/pages/Video/components/context.tsx +++ b/src/pages/Video/components/context.tsx @@ -1,8 +1,8 @@ import { createContext, useContext, useMemo, useState } from 'react'; interface TooltipModalContextType { - modalRef: HTMLButtonElement | null; - setModalRef: (ref: HTMLButtonElement | null) => void; + modalRef: HTMLDivElement | null; + setModalRef: (ref: HTMLDivElement | null) => void; } const TooltipModalContext = createContext<TooltipModalContextType | null>(null); From aae4d4072183a3a31cc6ef33d29824df0fa0f100 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Tue, 28 Oct 2025 12:41:12 +0100 Subject: [PATCH 082/108] fix: update unguess-design-system dependency version in package.json and yarn.lock --- package.json | 2 +- .../Video/components/TitleDropdownNew.tsx | 40 +++++++++---------- yarn.lock | 8 ++-- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index deb8c9e44..db076e37a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@analytics/google-tag-manager": "^0.6.0", "@analytics/hubspot": "^0.5.1", "@appquality/languages": "1.4.3", - "@appquality/unguess-design-system": "4.0.51--canary.551.38bfc82.0", + "@appquality/unguess-design-system": "4.0.51--canary.551.9d4d946.0", "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index ca6742e0b..9bd982328 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -1,7 +1,11 @@ import { Autocomplete, DropdownFieldNew as Field, + Input, + Label, MD, + Paragraph, + SM, TooltipModal, } from '@appquality/unguess-design-system'; import { FormikProps } from 'formik'; @@ -17,6 +21,7 @@ import { } from 'src/features/api'; import styled from 'styled-components'; import { useTooltipModalContext } from './context'; +import { Button } from '@appquality/unguess-design-system'; export interface ObservationFormValues { title: number; @@ -82,38 +87,33 @@ export const TitleDropdown = ({ ); }; - const EditModal = () => { - const { modalRef, setModalRef } = useTooltipModalContext(); + const editModal = ({ closeModal }: { closeModal: () => void }) => { return ( - <TooltipModal - onBlur={() => { - setIsExpanded(false); - }} - referenceElement={modalRef} - appendToNode={document.querySelector('main') || undefined} - onClose={() => setModalRef(null)} - placement="auto" - hasArrow={false} - role="dialog" - style={{ maxWidth: 300, transform: 'translateX(-400px)' }} - > + <> <TooltipModal.Title> <MD isBold style={{ marginBottom: appTheme.space.sm }}> - titolo + titolo della modale di modifica </MD> </TooltipModal.Title> <TooltipModal.Body> - <label htmlFor="title-input">Title</label> - <input type="text" id="title-input" /> + <Label htmlFor="title-input">Modifica Tema/Tag</Label> + <Input id="title-input" /> + <Paragraph style={{ margin: `${appTheme.space.md} 0` }}> + <SM> + Descrizione della modale di modifica del Tema/Tag o altro testo + </SM> + </Paragraph> + <Button size="small" isDanger onClick={closeModal}> + Elimina Tag + </Button> </TooltipModal.Body> - </TooltipModal> + </> ); }; return ( <Field> - <EditModal /> <Autocomplete onClick={() => setIsExpanded(true)} isExpanded={isExpanded} @@ -166,7 +166,7 @@ export const TitleDropdown = ({ value: i.id.toString(), label: `${i.name} (${i.usageNumber})`, isSelected: formProps.values.title === i.id, - action: <Edit optionId={i.id.toString()} />, + actions: editModal, itemID: i.id.toString(), }; })} diff --git a/yarn.lock b/yarn.lock index 7d970cd87..571c50324 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,10 +122,10 @@ dependencies: hls.js "^1.4.8" -"@appquality/unguess-design-system@4.0.51--canary.551.38bfc82.0": - version "4.0.51--canary.551.38bfc82.0" - resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.51--canary.551.38bfc82.0.tgz#51a94c7c2588d408c984c7be584c66231ee176d3" - integrity sha512-10jGFsoZD94TBs+RG1iWOjzXz5Cm+0b5gnLkRP4qnB0E87WAD3+wcOlO+La68dJUEO8W6l/jcfQg8j95chZyXg== +"@appquality/unguess-design-system@4.0.51--canary.551.9d4d946.0": + version "4.0.51--canary.551.9d4d946.0" + resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.51--canary.551.9d4d946.0.tgz#2f571ed32147be78bc4973722f6b747c7bcf64ba" + integrity sha512-RvOmH4ne51rrmfTtqCHSeOfRDUvcywsb5fvWJD+2Zk5d4S7UCsxuDxAsrRy7zLM/035KmjG9ogRmmdM+hBpLoQ== dependencies: "@appquality/stream-player" "1.0.6" "@nivo/bar" "^0.87.0" From 82117dd58f3ad45c5c67885057a5e06290a3c52e Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Tue, 28 Oct 2025 13:06:58 +0100 Subject: [PATCH 083/108] fix: refactor edit modal handling in TitleDropdown component --- .../Video/components/TitleDropdownNew.tsx | 51 +++---------------- 1 file changed, 8 insertions(+), 43 deletions(-) diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index 9bd982328..4f97d46b5 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -9,7 +9,7 @@ import { TooltipModal, } from '@appquality/unguess-design-system'; import { FormikProps } from 'formik'; -import { useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; @@ -22,6 +22,7 @@ import { import styled from 'styled-components'; import { useTooltipModalContext } from './context'; import { Button } from '@appquality/unguess-design-system'; +import { set } from 'date-fns'; export interface ObservationFormValues { title: number; @@ -47,47 +48,11 @@ export const TitleDropdown = ({ return null; } - const EditAction = styled.div` - cursor: pointer; - border-radius: 50%; - height: 24px; - width: 24px; - display: flex; - align-items: center; - justify-content: center; - background-color: ${appTheme.palette.grey[200]}; - transition: background-color 0.2s ease-in-out; - - &:hover { - background-color: ${appTheme.palette.grey[400]}; - } - `; - - const Edit = ({ optionId }: { optionId: string }) => { - const triggerRef = useRef<HTMLDivElement>(null); - const { setModalRef } = useTooltipModalContext(); - const handleClick = (e: React.MouseEvent<HTMLDivElement>) => { - // avoid select options to be closed when clicking on the edit icon - e.stopPropagation(); - e.preventDefault(); - - e.nativeEvent.stopImmediatePropagation(); - const el = document.querySelector(`[itemid="${optionId}"]`); - - if (el) { - el.setAttribute('hover', 'true'); - el.parentElement?.style.setProperty('pointer-events', 'none'); - } - setModalRef(triggerRef.current); + const editModal = ({ closeModal }: { closeModal: () => void }) => { + const handleSubmit = () => { + closeModal(); }; - return ( - <EditAction ref={triggerRef} onClick={handleClick}> - <EditIcon /> - </EditAction> - ); - }; - const editModal = ({ closeModal }: { closeModal: () => void }) => { return ( <> <TooltipModal.Title> @@ -104,7 +69,7 @@ export const TitleDropdown = ({ Descrizione della modale di modifica del Tema/Tag o altro testo </SM> </Paragraph> - <Button size="small" isDanger onClick={closeModal}> + <Button size="small" isDanger onClick={handleSubmit}> Elimina Tag </Button> </TooltipModal.Body> @@ -115,10 +80,10 @@ export const TitleDropdown = ({ return ( <Field> <Autocomplete - onClick={() => setIsExpanded(true)} + data-qa="video-title-dropdown" isExpanded={isExpanded} isDisabled={isExpanded} - listboxAppendToNode={document.querySelector('main') || undefined} + isEditable isCreatable renderValue={({ selection }) => { if (!selection) return ''; From 170ff467cf9199466a2b01ce1049c83f512ab451 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Tue, 28 Oct 2025 13:44:40 +0100 Subject: [PATCH 084/108] fix: update TitleDropdown component to toggle expansion on click and adjust listbox behavior --- src/pages/Video/components/TitleDropdownNew.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index 4f97d46b5..799e8068d 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -81,10 +81,11 @@ export const TitleDropdown = ({ <Field> <Autocomplete data-qa="video-title-dropdown" + onClick={() => setIsExpanded(!isExpanded)} isExpanded={isExpanded} isDisabled={isExpanded} - isEditable isCreatable + listboxAppendToNode={document.body} renderValue={({ selection }) => { if (!selection) return ''; // @ts-ignore From 355e286268ac1335dc76674d1a66e3c8f4f0abea Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Thu, 30 Oct 2025 10:40:25 +0100 Subject: [PATCH 085/108] test: add end-to-end tests for Video page edit dialog functionality --- tests/e2e/video/observation.spec.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/e2e/video/observation.spec.ts diff --git a/tests/e2e/video/observation.spec.ts b/tests/e2e/video/observation.spec.ts new file mode 100644 index 000000000..94c4b5442 --- /dev/null +++ b/tests/e2e/video/observation.spec.ts @@ -0,0 +1,26 @@ +import { expect, test } from '../../fixtures/app'; +import { VideoPage } from '../../fixtures/pages/Video'; + +test.describe('Video page', () => { + let videopage: VideoPage; + + test.beforeEach(async ({ page }) => { + videopage = new VideoPage(page); + await videopage.loggedIn(); + await videopage.mockPreferences(); + await videopage.mockWorkspace(); + await videopage.mockGetCampaign(); + await videopage.mockGetVideo(); + await videopage.mockGetVideoObservations(); + await videopage.open(); + }); + + test('should open the edit dialog in the themes combobox', async () => {}); + test('should open the edit dialog in the tags combobox', async () => {}); + test('edit dialog should have an input and a summary text for the current item and a save button', async () => {}); + test('should allow changing the name of a tag', async () => {}); + test('should allow changing the name of a theme', async () => {}); + test('should save and show a success toast after saving the edited name', async () => {}); + test('should show an error if trying to save with an existing name', async () => {}); + test('should not save and show an error if the input is empty', async () => {}); +}); From ba68f9fa2cec11a7e857c2204d809850fac8de30 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Tue, 4 Nov 2025 14:55:27 +0100 Subject: [PATCH 086/108] squash: all changes from UN-408-tooltip-modal-spike for new branch UN-1954 --- package.json | 2 +- src/assets/icons/save.svg | 3 + src/common/schema.ts | 40 +++++++++ src/features/api/index.ts | 20 +++++ src/locales/en/translation.json | 6 ++ src/locales/it/translation.json | 6 ++ src/pages/Video/components/Observation.tsx | 1 + .../Video/components/TitleDropdownNew.tsx | 81 +++++++++++++------ .../tagId/_patch/request_Example_1.json | 3 + .../categories/_get/200_Example_1.json | 19 +---- .../users/me/_get/200_Users_Me_example.json | 3 - .../wid/templates/_get/200_Example_1.json | 1 - .../200_global_and_private_templates.json | 9 --- .../templates/_get/200_global_template.json | 1 - tests/fixtures/pages/Video.ts | 2 + yarn.lock | 8 +- 16 files changed, 144 insertions(+), 61 deletions(-) create mode 100644 src/assets/icons/save.svg create mode 100644 tests/api/campaigns/cid/video-tags/tagId/_patch/request_Example_1.json diff --git a/package.json b/package.json index db076e37a..8dea09bbd 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@analytics/google-tag-manager": "^0.6.0", "@analytics/hubspot": "^0.5.1", "@appquality/languages": "1.4.3", - "@appquality/unguess-design-system": "4.0.51--canary.551.9d4d946.0", + "@appquality/unguess-design-system": "4.0.51--canary.551.2b300ae.0", "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", diff --git a/src/assets/icons/save.svg b/src/assets/icons/save.svg new file mode 100644 index 000000000..174d63f91 --- /dev/null +++ b/src/assets/icons/save.svg @@ -0,0 +1,3 @@ +<svg width="12" height="16" viewBox="0 0 12 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8.84915 3.05779C8.5904 2.84725 8.26699 2.7323 7.9334 2.7323H2.71383C2.07721 2.7323 1.56677 3.2573 1.56677 3.89897V12.0656C1.56677 12.7073 2.07721 13.2323 2.71383 13.2323H10.1697C10.8006 13.2323 11.3168 12.7073 11.3168 12.0656V5.75561C11.3168 5.31905 11.1202 4.90569 10.7816 4.63016L8.84915 3.05779ZM10.4565 12.3573H2.42707V3.6073H8.16236L10.4565 5.3573V12.3573ZM6.44177 8.56563C5.86824 8.56563 5.29471 8.91716 5.29471 9.7323C5.29471 10.5474 5.86824 10.899 6.44177 10.899C7.0153 10.899 7.58883 10.5169 7.58883 9.7323C7.58883 8.94765 7.0153 8.56563 6.44177 8.56563ZM3.28736 5.20777C3.28736 4.8071 3.61216 4.4823 4.01283 4.4823H7.15013C7.55079 4.4823 7.8756 4.8071 7.8756 5.20777V5.75683C7.8756 6.1575 7.55079 6.4823 7.15013 6.4823H4.01283C3.61216 6.4823 3.28736 6.1575 3.28736 5.75683V5.20777Z" fill="#F6F6F8"/> +</svg> diff --git a/src/common/schema.ts b/src/common/schema.ts index 2797f8d5e..c43532b86 100644 --- a/src/common/schema.ts +++ b/src/common/schema.ts @@ -298,6 +298,15 @@ export interface paths { }; }; }; + "/campaigns/{cid}/video-tags/{tagId}": { + patch: operations["patch-campaigns-cid-video-tags-tagId"]; + parameters: { + path: { + cid: string; + tagId: string; + }; + }; + }; "/campaigns/{cid}/videos": { /** Return all published video for a specific campaign */ get: operations["get-campaigns-cid-videos"]; @@ -3151,6 +3160,37 @@ export interface operations { }; }; }; + "patch-campaigns-cid-video-tags-tagId": { + parameters: { + path: { + cid: string; + tagId: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { [key: string]: unknown }; + }; + }; + /** Bad Request */ + 400: unknown; + /** Conflict */ + 409: { + content: { + "application/json": { [key: string]: unknown }; + }; + }; + }; + requestBody: { + content: { + "application/json": { + newTagName: string; + }; + }; + }; + }; /** Return all published video for a specific campaign */ "get-campaigns-cid-videos": { parameters: { diff --git a/src/features/api/index.ts b/src/features/api/index.ts index 900ba5a87..f367d74a2 100644 --- a/src/features/api/index.ts +++ b/src/features/api/index.ts @@ -349,6 +349,16 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.body, }), }), + patchCampaignsByCidVideoTagsAndTagId: build.mutation< + PatchCampaignsByCidVideoTagsAndTagIdApiResponse, + PatchCampaignsByCidVideoTagsAndTagIdApiArg + >({ + query: (queryArg) => ({ + url: `/campaigns/${queryArg.cid}/video-tags/${queryArg.tagId}`, + method: 'PATCH', + body: queryArg.body, + }), + }), getCampaignsByCidVideos: build.query< GetCampaignsByCidVideosApiResponse, GetCampaignsByCidVideosApiArg @@ -1485,6 +1495,15 @@ export type PostCampaignsByCidVideoTagsApiArg = { }; }; }; +export type PatchCampaignsByCidVideoTagsAndTagIdApiResponse = + /** status 200 OK */ {}; +export type PatchCampaignsByCidVideoTagsAndTagIdApiArg = { + cid: string; + tagId: string; + body: { + newTagName: string; + }; +}; export type GetCampaignsByCidVideosApiResponse = /** status 200 OK */ { items: (Video & { usecaseId: number; @@ -3095,6 +3114,7 @@ export const { useGetCampaignsByCidUxQuery, useGetCampaignsByCidVideoTagsQuery, usePostCampaignsByCidVideoTagsMutation, + usePatchCampaignsByCidVideoTagsAndTagIdMutation, useGetCampaignsByCidVideosQuery, useGetCampaignsByCidWidgetsQuery, usePostCheckoutMutation, diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 6d7df625d..a33d83f4c 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1409,6 +1409,7 @@ "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_PLACEHOLDER": "Select or add a theme", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_BUTTON": "Save", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_TOAST_SUCCESS": "Observation saved successfully", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SAVE_BUTTON": "Save", "__VIDEO_PAGE_NO_OBSERVATIONS": "Start watching the video and mark the highlights.", "__VIDEO_PAGE_NO_OBSERVATIONS_TITLE": "For this video, there are no observations yet.", "__VIDEO_PAGE_OBSERVATION_DESELECTED_TOAST_MESSAGE": "Observation deselected", @@ -1421,6 +1422,11 @@ "__VIDEO_PAGE_PLAYER_SHORTCUT_OBSERVATION_STARTED": "Observation started", "__VIDEO_PAGE_PLAYER_START_ADD_OBSERVATION": "Start observation", "__VIDEO_PAGE_PLAYER_STOP_ADD_OBSERVATION": "End observation", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_zero": "No observations related to this theme", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "1 Observation related to this theme", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "{{usageNumber}} Observations related to this theme", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL": "Theme", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE": "Edit Theme", "__VIDEO_PAGE_TITLE": "UX Tagging Tool", "__VIDEO_PAGE_TRANSCRIPT_EMPTY_STATE": "For this video, there is no available transcript.", "__VIDEO_PAGE_TRANSCRIPT_INFO": "Select some text to create an observation or use the button on the video player.", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index f0b6ae5dc..a78de6e7e 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1449,6 +1449,7 @@ "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_PLACEHOLDER": "Seleziona o crea un tema", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_BUTTON": "Salva", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_TOAST_SUCCESS": "Osservazione salvata con successo", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SAVE_BUTTON": "", "__VIDEO_PAGE_NO_OBSERVATIONS": "Inizia a guardare il video e segna i momenti salienti.", "__VIDEO_PAGE_NO_OBSERVATIONS_TITLE": "Per questo video non ci sono ancora evidenze.", "__VIDEO_PAGE_OBSERVATION_DESELECTED_TOAST_MESSAGE": "Osservazione de-selezionata", @@ -1461,6 +1462,11 @@ "__VIDEO_PAGE_PLAYER_SHORTCUT_OBSERVATION_STARTED": "Osservazione iniziata", "__VIDEO_PAGE_PLAYER_START_ADD_OBSERVATION": "Inizia osservazione", "__VIDEO_PAGE_PLAYER_STOP_ADD_OBSERVATION": "Fine osservazione", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_many": "", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL": "", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE": "", "__VIDEO_PAGE_TITLE": "Tagging Tool", "__VIDEO_PAGE_TRANSCRIPT_EMPTY_STATE": "Per questo video non è disponibile una trascrizione.", "__VIDEO_PAGE_TRANSCRIPT_INFO": "Seleziona del testo per creare un'osservazione o usa il pulsante sul lettore video.", diff --git a/src/pages/Video/components/Observation.tsx b/src/pages/Video/components/Observation.tsx index 824b12ad0..d78a45c66 100644 --- a/src/pages/Video/components/Observation.tsx +++ b/src/pages/Video/components/Observation.tsx @@ -147,6 +147,7 @@ const Observation = ({ onChange={handleAccordionChange} key={`observation_accordion_${observation.id}_${isOpen}`} id={`video-observation-accordion-${observation.id}`} + data-qa={`observation-accordion-${observation.id}`} > <AccordionNew.Section> <AccordionNew.Header diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index 799e8068d..f4edb5d26 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -1,5 +1,6 @@ import { Autocomplete, + Button, DropdownFieldNew as Field, Input, Label, @@ -8,21 +9,17 @@ import { SM, TooltipModal, } from '@appquality/unguess-design-system'; +import { ReactComponent as SaveIcon } from 'src/assets/icons/save.svg'; import { FormikProps } from 'formik'; -import { useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { useRef, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; import { ReactComponent as CopyIcon } from 'src/assets/icons/copy-icon.svg'; -import { ReactComponent as EditIcon } from 'src/assets/icons/edit-icon.svg'; import { GetCampaignsByCidVideoTagsApiResponse, usePostCampaignsByCidVideoTagsMutation, } from 'src/features/api'; -import styled from 'styled-components'; -import { useTooltipModalContext } from './context'; -import { Button } from '@appquality/unguess-design-system'; -import { set } from 'date-fns'; export interface ObservationFormValues { title: number; @@ -39,7 +36,6 @@ export const TitleDropdown = ({ formProps: FormikProps<ObservationFormValues>; }) => { const { t } = useTranslation(); - const [isExpanded, setIsExpanded] = useState(false); const { campaignId } = useParams(); const [addVideoTags] = usePostCampaignsByCidVideoTagsMutation(); const titleMaxLength = 70; @@ -48,30 +44,68 @@ export const TitleDropdown = ({ return null; } - const editModal = ({ closeModal }: { closeModal: () => void }) => { + interface EditModalProps { + option: { value: string | object; label: string }; + closeModal: () => void; + } + const editModal = ({ option, closeModal }: EditModalProps) => { + // Extract both the current title and the usage number in parentheses in two variables + const currentTitle = option.label.replace(/\s*\(\d+\)/, ''); + const usageNumber = option.label.match(/\((\d+)\)/)?.[1]; + const [newTitle, setNewTitle] = useState(currentTitle); + const inputRef = useRef<HTMLInputElement>(null); const handleSubmit = () => { + // Update the title in the form + alert('Title updated to: ' + newTitle); closeModal(); }; + const handleClick = () => { + inputRef.current?.focus(); + }; return ( <> <TooltipModal.Title> <MD isBold style={{ marginBottom: appTheme.space.sm }}> - titolo della modale di modifica + {t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE')} </MD> </TooltipModal.Title> - <TooltipModal.Body> - <Label htmlFor="title-input">Modifica Tema/Tag</Label> - <Input id="title-input" /> - <Paragraph style={{ margin: `${appTheme.space.md} 0` }}> - <SM> - Descrizione della modale di modifica del Tema/Tag o altro testo - </SM> - </Paragraph> - <Button size="small" isDanger onClick={handleSubmit}> - Elimina Tag - </Button> + <Label htmlFor="title-input"> + {t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL')} + <span style={{ color: appTheme.palette.red[500] }}>*</span> + </Label> + <Input + ref={inputRef} + id="title-input" + onClick={handleClick} + value={newTitle} + onChange={(e) => setNewTitle(e.target.value)} + /> + <div + style={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + margin: `${appTheme.space.md} 0 0 0`, + }} + > + <Paragraph style={{ margin: 0 }}> + <SM> + <Trans + i18nKey="__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION" + values={{ usageNumber }} + count={Number(usageNumber)} + /> + </SM> + </Paragraph> + <Button size="small" isPrimary isAccent onClick={handleSubmit}> + <Button.StartIcon> + <SaveIcon /> + </Button.StartIcon> + {t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SAVE_BUTTON')} + </Button> + </div> </TooltipModal.Body> </> ); @@ -81,10 +115,7 @@ export const TitleDropdown = ({ <Field> <Autocomplete data-qa="video-title-dropdown" - onClick={() => setIsExpanded(!isExpanded)} - isExpanded={isExpanded} - isDisabled={isExpanded} - isCreatable + isEditable listboxAppendToNode={document.body} renderValue={({ selection }) => { if (!selection) return ''; diff --git a/tests/api/campaigns/cid/video-tags/tagId/_patch/request_Example_1.json b/tests/api/campaigns/cid/video-tags/tagId/_patch/request_Example_1.json new file mode 100644 index 000000000..275526337 --- /dev/null +++ b/tests/api/campaigns/cid/video-tags/tagId/_patch/request_Example_1.json @@ -0,0 +1,3 @@ +{ + "newTagName": "Tag New" +} diff --git a/tests/api/templates/categories/_get/200_Example_1.json b/tests/api/templates/categories/_get/200_Example_1.json index f9f3dca26..20b386b4b 100644 --- a/tests/api/templates/categories/_get/200_Example_1.json +++ b/tests/api/templates/categories/_get/200_Example_1.json @@ -1,22 +1,7 @@ [ { "description": "string", - "id": -1, - "name": "uncategorized" - }, - { - "description": "string", - "id": 10, - "name": "Accessibility" - }, - { - "description": "string", - "id": 20, - "name": "Best category ever" - }, - { - "description": "string", - "id": 30, - "name": "Security" + "id": 0, + "name": "string" } ] diff --git a/tests/api/users/me/_get/200_Users_Me_example.json b/tests/api/users/me/_get/200_Users_Me_example.json index 6a716a13a..df477885c 100644 --- a/tests/api/users/me/_get/200_Users_Me_example.json +++ b/tests/api/users/me/_get/200_Users_Me_example.json @@ -16,9 +16,6 @@ ], "id": 1, "name": "Luca Cannarozzo", - "first_name": "Luca", - "last_name": "Cannarozzo", - "customer_role": "Designer", "picture": "https://www.gravatar.com/avatar/04c5df6d27d87476fe9dee853e1142bd", "profile_id": 32, "role": "administrator", diff --git a/tests/api/workspaces/wid/templates/_get/200_Example_1.json b/tests/api/workspaces/wid/templates/_get/200_Example_1.json index fc2185d65..f9309c295 100644 --- a/tests/api/workspaces/wid/templates/_get/200_Example_1.json +++ b/tests/api/workspaces/wid/templates/_get/200_Example_1.json @@ -1,7 +1,6 @@ { "items": [ { - "category_id": 0, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}]}", "description": "string", "id": 0, diff --git a/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json b/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json index c106ed897..68401401b 100644 --- a/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json +++ b/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json @@ -1,7 +1,6 @@ { "items": [ { - "category_id": -1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Gain qualitative insights by observing users as they interact. Identifies difficulties in the user experience, improves the interface. Identifies difficulties in the user experience, improves the interface.", "id": 1, @@ -11,7 +10,6 @@ "workspace_id": 1 }, { - "category_id": -1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Gain qualitative insights by observing users as they interact. Identifies difficulties in the user experience, improves the interface. Identifies difficulties in the user experience, improves the interface.", "id": 2, @@ -21,7 +19,6 @@ "workspace_id": 1 }, { - "category_id": -1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Find unexpected issues with exploratory testing.", "id": 3, @@ -30,7 +27,6 @@ "workspace_id": 1 }, { - "category_id": 1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Improve the user experience.", "id": 4, @@ -39,7 +35,6 @@ "workspace_id": 1 }, { - "category_id": 10, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Penetration testing.", "id": 5, @@ -63,7 +58,6 @@ } }, { - "category_id": 10, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Test system performance", "id": 6, @@ -88,7 +82,6 @@ } }, { - "category_id": 20, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Gain qualitative insights by observing users as they interact. Identifies difficulties in the user experience, improves the interface. Identifies difficulties in the user experience, improves the interface.", "id": 7, @@ -112,7 +105,6 @@ } }, { - "category_id": 20, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Test system performance.", "id": 8, @@ -137,7 +129,6 @@ } }, { - "category_id": 30, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Test system performance", "id": 9, diff --git a/tests/api/workspaces/wid/templates/_get/200_global_template.json b/tests/api/workspaces/wid/templates/_get/200_global_template.json index 268250936..1f963ad89 100644 --- a/tests/api/workspaces/wid/templates/_get/200_global_template.json +++ b/tests/api/workspaces/wid/templates/_get/200_global_template.json @@ -1,7 +1,6 @@ { "items": [ { - "category_id": 10, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}]}", "description": "Description of global template 1", "id": 1, diff --git a/tests/fixtures/pages/Video.ts b/tests/fixtures/pages/Video.ts index 04ed1f23a..53f230f80 100644 --- a/tests/fixtures/pages/Video.ts +++ b/tests/fixtures/pages/Video.ts @@ -20,6 +20,8 @@ export class VideoPage extends UnguessPage { this.page .getByTestId('transcript-sentiment') .locator('[data-garden-id="tags.tag_view"]'), + observationAccordion: (id: number) => + this.page.getByTestId(`observation-accordion-${id}`), }; } diff --git a/yarn.lock b/yarn.lock index 571c50324..dbffdf982 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,10 +122,10 @@ dependencies: hls.js "^1.4.8" -"@appquality/unguess-design-system@4.0.51--canary.551.9d4d946.0": - version "4.0.51--canary.551.9d4d946.0" - resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.51--canary.551.9d4d946.0.tgz#2f571ed32147be78bc4973722f6b747c7bcf64ba" - integrity sha512-RvOmH4ne51rrmfTtqCHSeOfRDUvcywsb5fvWJD+2Zk5d4S7UCsxuDxAsrRy7zLM/035KmjG9ogRmmdM+hBpLoQ== +"@appquality/unguess-design-system@4.0.51--canary.551.2b300ae.0": + version "4.0.51--canary.551.2b300ae.0" + resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.51--canary.551.2b300ae.0.tgz#528a588ef0febd09d8b2aa7adc2acc8296840198" + integrity sha512-KR4JoAx9/iXAaUynly86+sUG3wYexF5DMDeX1OUaz0M5WHDfN85PpI3kVPW8hggZPXsWxc3t/ChyuCQ2IJWGNA== dependencies: "@appquality/stream-player" "1.0.6" "@nivo/bar" "^0.87.0" From 533a4ca8db79d44c0662381c323544fb64981fbf Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Thu, 6 Nov 2025 15:36:25 +0100 Subject: [PATCH 087/108] feat: enhance video tags and themes editing functionality with new modals and error handling --- src/features/api/apiTags.ts | 3 + src/locales/en/translation.json | 4 + src/locales/it/translation.json | 4 + .../Video/components/ObservationForm.tsx | 22 ++- .../Video/components/TitleDropdownNew.tsx | 133 +++++++++++-- .../cid/video-tags/_get/200_Example_1.json | 93 +++++++++ .../vid/observations/_get/200_Example_1.json | 100 ++++++++++ tests/e2e/video/observation.spec.ts | 182 +++++++++++++++++- tests/fixtures/pages/Video.ts | 90 +++++++++ 9 files changed, 602 insertions(+), 29 deletions(-) diff --git a/src/features/api/apiTags.ts b/src/features/api/apiTags.ts index ecbf95065..5df8562ec 100644 --- a/src/features/api/apiTags.ts +++ b/src/features/api/apiTags.ts @@ -154,6 +154,9 @@ unguessApi.enhanceEndpoints({ getCampaignsByCidVideoTags: { providesTags: ['VideoTags'], }, + patchCampaignsByCidVideoTagsAndTagId: { + invalidatesTags: ['VideoTags'], + }, postCampaignsByCidVideoTags: { invalidatesTags: ['VideoTags'], }, diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index a33d83f4c..8b2390d19 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1403,10 +1403,12 @@ "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TAGS_SHOW_MORE_one": "Show more", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TAGS_SHOW_MORE_other": "Show more", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_DESCRIPTION": "Use themes to clusterize your observations", + "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_DUPLICATE_ERROR": "This option has already been used. Try another choice.", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_ERROR": "You must insert a theme", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_LABEL": "Theme", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_MAX_ERROR": "You must insert a theme between 1 and 70 characters", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_PLACEHOLDER": "Select or add a theme", + "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_REQUIRED_ERROR": "Theme is required", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_BUTTON": "Save", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_TOAST_SUCCESS": "Observation saved successfully", "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SAVE_BUTTON": "Save", @@ -1425,7 +1427,9 @@ "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_zero": "No observations related to this theme", "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "1 Observation related to this theme", "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "{{usageNumber}} Observations related to this theme", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE": "An error occurred while saving the Theme changes. Please try again.", "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL": "Theme", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE": "The Theme changes were successful.", "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE": "Edit Theme", "__VIDEO_PAGE_TITLE": "UX Tagging Tool", "__VIDEO_PAGE_TRANSCRIPT_EMPTY_STATE": "For this video, there is no available transcript.", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index a78de6e7e..f40f8bf98 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1443,10 +1443,12 @@ "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TAGS_SHOW_MORE_many": "", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TAGS_SHOW_MORE_other": "Mostra ancora", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_DESCRIPTION": "Usa i temi per raggruppare le tue osservazioni", + "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_DUPLICATE_ERROR": "", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_ERROR": "Devi inserire un tema", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_LABEL": "Tema", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_MAX_ERROR": "Devi inserire un tema compreso tra 1 e 70 caratteri", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_PLACEHOLDER": "Seleziona o crea un tema", + "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_REQUIRED_ERROR": "", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_BUTTON": "Salva", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_TOAST_SUCCESS": "Osservazione salvata con successo", "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SAVE_BUTTON": "", @@ -1465,7 +1467,9 @@ "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "", "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_many": "", "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE": "", "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL": "", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE": "", "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE": "", "__VIDEO_PAGE_TITLE": "Tagging Tool", "__VIDEO_PAGE_TRANSCRIPT_EMPTY_STATE": "Per questo video non è disponibile una trascrizione.", diff --git a/src/pages/Video/components/ObservationForm.tsx b/src/pages/Video/components/ObservationForm.tsx index ec4c99acd..a4148de92 100644 --- a/src/pages/Video/components/ObservationForm.tsx +++ b/src/pages/Video/components/ObservationForm.tsx @@ -10,6 +10,7 @@ import { Skeleton, Tag, Textarea, + TooltipModal, useToast, } from '@appquality/unguess-design-system'; import { Form, Formik, FormikHelpers, FormikProps } from 'formik'; @@ -73,7 +74,7 @@ const ObservationForm = ({ const formRef = useRef<FormikProps<ObservationFormValues>>(null); const { addToast } = useToast(); const [options, setOptions] = useState< - { id: number; label: string; selected?: boolean }[] + { id: number; label: string; selected?: boolean; actions: JSX.Element }[] >([]); const [selectedSeverity, setSelectedSeverity] = useState< GetCampaignsByCidVideoTagsApiResponse[number]['tags'][number] | undefined @@ -144,6 +145,23 @@ const ObservationForm = ({ id: tag.id, label: `${tag.name} (${tag.usageNumber})`, selected: selectedOptions.some((bt) => bt.id === tag.id), + actions: ( + <> + <TooltipModal.Title>{tag.name}</TooltipModal.Title> + <TooltipModal.Body> + {t( + '__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_TAGS_EDIT_MODAL_DESCRIPTION' + )} + <Button + isPrimary + isAccent + style={{ marginTop: appTheme.space.md }} + > + Save + </Button> + </TooltipModal.Body> + </> + ), })) ); } @@ -355,6 +373,8 @@ const ObservationForm = ({ <Skeleton /> ) : ( <MultiSelect + data-qa="video-tags-dropdown" + isEditable options={options} selectedItems={options.filter((o) => o.selected)} creatable diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index f4edb5d26..c1ee5ac1e 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -5,19 +5,24 @@ import { Input, Label, MD, + Message, + Notification, Paragraph, SM, TooltipModal, + useToast, } from '@appquality/unguess-design-system'; -import { ReactComponent as SaveIcon } from 'src/assets/icons/save.svg'; import { FormikProps } from 'formik'; -import { useRef, useState } from 'react'; +import { ErrorMessage } from 'formik/dist/ErrorMessage'; +import { useEffect, useRef, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; import { ReactComponent as CopyIcon } from 'src/assets/icons/copy-icon.svg'; +import { ReactComponent as SaveIcon } from 'src/assets/icons/save.svg'; import { GetCampaignsByCidVideoTagsApiResponse, + usePatchCampaignsByCidVideoTagsAndTagIdMutation, usePostCampaignsByCidVideoTagsMutation, } from 'src/features/api'; @@ -28,6 +33,11 @@ export interface ObservationFormValues { quotes?: string; } +interface EditModalProps { + option: { value: string | object; label: string }; + closeModal: () => void; +} + export const TitleDropdown = ({ titles, formProps, @@ -44,20 +54,83 @@ export const TitleDropdown = ({ return null; } - interface EditModalProps { - option: { value: string | object; label: string }; - closeModal: () => void; - } const editModal = ({ option, closeModal }: EditModalProps) => { // Extract both the current title and the usage number in parentheses in two variables const currentTitle = option.label.replace(/\s*\(\d+\)/, ''); const usageNumber = option.label.match(/\((\d+)\)/)?.[1]; const [newTitle, setNewTitle] = useState(currentTitle); const inputRef = useRef<HTMLInputElement>(null); - const handleSubmit = () => { + const [error, setError] = useState<string | null>(null); + const [patchVideoTag] = usePatchCampaignsByCidVideoTagsAndTagIdMutation({}); + const { addToast } = useToast(); + + useEffect(() => { + if (newTitle.trim() === '') { + setError( + t('__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_REQUIRED_ERROR') + ); + } else { + setError(null); + } + }, [newTitle]); + + const handleSubmit = async () => { + console.log('titledrop', error); + if (error) return; // Update the title in the form - alert('Title updated to: ' + newTitle); - closeModal(); + console.log('titledrop no error', newTitle); + try { + await patchVideoTag({ + cid: campaignId?.toString() || '0', + tagId: option.value.toString(), + body: { + newTagName: newTitle, + }, + }).unwrap(); + closeModal(); + addToast( + ({ close }) => ( + <Notification + onClose={close} + type="success" + message={t( + '__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE' + )} + closeText={t('__TOAST_CLOSE_TEXT')} + isPrimary + /> + ), + { placement: 'top' } + ); + } catch (error: any) { + // Handle error (e.g., show error toast) + // if status code is 409, conflict with another already saved name, show specific error + console.log('titledrop catch', error); + if (error.status === 409) { + console.log('titledrop 409', error); + setError( + t( + '__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_DUPLICATE_ERROR' + ) + ); + } else { + console.log('titledrop else', error); + addToast( + ({ close }) => ( + <Notification + onClose={close} + type="error" + message={t( + '__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE' + )} + closeText={t('__TOAST_CLOSE_TEXT')} + isPrimary + /> + ), + { placement: 'top' } + ); + } + } }; const handleClick = () => { inputRef.current?.focus(); @@ -82,6 +155,14 @@ export const TitleDropdown = ({ value={newTitle} onChange={(e) => setNewTitle(e.target.value)} /> + {error && ( + <Message + validation="error" + style={{ marginTop: appTheme.space.xs }} + > + {error} + </Message> + )} <div style={{ display: 'flex', @@ -99,7 +180,13 @@ export const TitleDropdown = ({ /> </SM> </Paragraph> - <Button size="small" isPrimary isAccent onClick={handleSubmit}> + <Button + size="small" + disabled={!!error} + isPrimary + isAccent + onClick={handleSubmit} + > <Button.StartIcon> <SaveIcon /> </Button.StartIcon> @@ -157,16 +244,22 @@ export const TitleDropdown = ({ if (!selectionValue || !inputValue) return; formProps.setFieldValue('title', Number(selectionValue)); }} - options={(titles || []).map((i) => { - return { - id: i.id.toString(), - value: i.id.toString(), - label: `${i.name} (${i.usageNumber})`, - isSelected: formProps.values.title === i.id, - actions: editModal, - itemID: i.id.toString(), - }; - })} + options={[ + { + id: 'id', + label: 'select or create', + options: (titles || []).map((i) => { + return { + id: i.id.toString(), + value: i.id.toString(), + label: `${i.name} (${i.usageNumber})`, + isSelected: formProps.values.title === i.id, + actions: editModal, + itemID: i.id.toString(), + }; + }), + }, + ]} startIcon={<CopyIcon />} placeholder={t( '__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_PLACEHOLDER' diff --git a/tests/api/campaigns/cid/video-tags/_get/200_Example_1.json b/tests/api/campaigns/cid/video-tags/_get/200_Example_1.json index 9ce5ec201..a356ecea0 100644 --- a/tests/api/campaigns/cid/video-tags/_get/200_Example_1.json +++ b/tests/api/campaigns/cid/video-tags/_get/200_Example_1.json @@ -13,5 +13,98 @@ "usageNumber": 0 } ] + }, + { + "group": { "id": 3, "name": "severity" }, + "tags": [ + { + "id": 1094, + "name": "Positive Finding", + "style": "#007D5A", + "usageNumber": 34 + }, + { + "id": 1099, + "name": "Observation", + "style": "#003A57", + "usageNumber": 29 + }, + { + "id": 1095, + "name": "Minor issue", + "style": "#955F1D", + "usageNumber": 5 + }, + { + "id": 1096, + "name": "Major Issue", + "style": "#932127", + "usageNumber": 5 + } + ] + }, + { + "group": { "id": 2, "name": "tags" }, + "tags": [ + { "id": 1114, "name": "marco", "style": "white", "usageNumber": 14 }, + { "id": 1097, "name": "tag 1", "style": "white", "usageNumber": 5 }, + { "id": 1103, "name": "Bloccante", "style": "white", "usageNumber": 5 }, + { "id": 1104, "name": "non b", "style": "white", "usageNumber": 3 }, + { "id": 20797, "name": "Confused", "style": "white", "usageNumber": 3 }, + { "id": 20756, "name": "gimmi test", "style": "white", "usageNumber": 2 }, + { "id": 20798, "name": "Layot", "style": "white", "usageNumber": 2 }, + { "id": 20800, "name": "Blocker", "style": "white", "usageNumber": 2 }, + { "id": 24855, "name": "new", "style": "white", "usageNumber": 1 }, + { "id": 1108, "name": "confu", "style": "white", "usageNumber": 0 }, + { "id": 1109, "name": "confusion", "style": "white", "usageNumber": 0 } + ] + }, + { + "group": { "id": 4, "name": "title" }, + "tags": [ + { + "id": 13390, + "name": "Home page navigation", + "style": "", + "usageNumber": 11 + }, + { + "id": 20767, + "name": "grey 500 andrebbe corretto in tutte le pagine per il testo perchè non", + "style": "white", + "usageNumber": 3 + }, + { + "id": 22099, + "name": "Out of scope", + "style": "white", + "usageNumber": 3 + }, + { "id": 22102, "name": "Vpn", "style": "white", "usageNumber": 3 }, + { + "id": 23125, + "name": "prova modifica quote", + "style": "white", + "usageNumber": 3 + }, + { + "id": 20795, + "name": "Login page inputs", + "style": "white", + "usageNumber": 1 + }, + { + "id": 20799, + "name": "Sign up information", + "style": "white", + "usageNumber": 1 + }, + { + "id": 22555, + "name": "Out of scope \\(3\\) test", + "style": "white", + "usageNumber": 0 + } + ] } ] diff --git a/tests/api/videos/vid/observations/_get/200_Example_1.json b/tests/api/videos/vid/observations/_get/200_Example_1.json index 14cfbca6c..25a476bb9 100644 --- a/tests/api/videos/vid/observations/_get/200_Example_1.json +++ b/tests/api/videos/vid/observations/_get/200_Example_1.json @@ -21,5 +21,105 @@ ], "title": "string", "uxNote": "string" + }, + { + "id": 1, + "title": "grey 500 andrebbe corretto in tutte le pagine per il testo perchè non", + "description": "", + "quotes": "che belle quotes modificate a dovere", + "start": 2, + "end": 5.62, + "tags": [ + { + "group": { "id": 2, "name": "tags" }, + "tag": { + "id": 1114, + "name": "marco", + "style": "white", + "usageNumber": 14 + } + }, + { + "group": { "id": 2, "name": "tags" }, + "tag": { + "id": 1098, + "name": "tag 2", + "style": "white", + "usageNumber": 13 + } + }, + { + "group": { "id": 2, "name": "tags" }, + "tag": { + "id": 1097, + "name": "tag 1", + "style": "white", + "usageNumber": 5 + } + }, + { + "group": { "id": 2, "name": "tags" }, + "tag": { + "id": 1104, + "name": "non b", + "style": "white", + "usageNumber": 3 + } + }, + { + "group": { "id": 2, "name": "tags" }, + "tag": { + "id": 20756, + "name": "gimmi test", + "style": "white", + "usageNumber": 2 + } + }, + { + "group": { "id": 2, "name": "tags" }, + "tag": { + "id": 20786, + "name": "prova\"dell'annoincredibile", + "style": "white", + "usageNumber": 2 + } + }, + { + "group": { "id": 2, "name": "tags" }, + "tag": { + "id": 1129, + "name": "GIOMMI - INT", + "style": "white", + "usageNumber": 1 + } + }, + { + "group": { "id": 2, "name": "tags" }, + "tag": { + "id": 24855, + "name": "new", + "style": "white", + "usageNumber": 1 + } + }, + { + "group": { "id": 3, "name": "severity" }, + "tag": { + "id": 1094, + "name": "Positive Finding", + "style": "#007D5A", + "usageNumber": 34 + } + }, + { + "group": { "id": 4, "name": "title" }, + "tag": { + "id": 20767, + "name": "grey 500 andrebbe corretto in tutte le pagine per il testo perchè non", + "style": "white", + "usageNumber": 3 + } + } + ] } ] diff --git a/tests/e2e/video/observation.spec.ts b/tests/e2e/video/observation.spec.ts index 94c4b5442..60b7d1403 100644 --- a/tests/e2e/video/observation.spec.ts +++ b/tests/e2e/video/observation.spec.ts @@ -11,16 +11,182 @@ test.describe('Video page', () => { await videopage.mockWorkspace(); await videopage.mockGetCampaign(); await videopage.mockGetVideo(); + await videopage.mockGetVideoTags(); await videopage.mockGetVideoObservations(); await videopage.open(); }); - test('should open the edit dialog in the themes combobox', async () => {}); - test('should open the edit dialog in the tags combobox', async () => {}); - test('edit dialog should have an input and a summary text for the current item and a save button', async () => {}); - test('should allow changing the name of a tag', async () => {}); - test('should allow changing the name of a theme', async () => {}); - test('should save and show a success toast after saving the edited name', async () => {}); - test('should show an error if trying to save with an existing name', async () => {}); - test('should not save and show an error if the input is empty', async () => {}); + test('should open the edit dialog in the themes combobox and display an input and a summary text for the current item and a save button', async ({ + i18n, + }) => { + await videopage.openObservationAccordion(1); + await videopage.openComboboxVideoTitle(1); + await videopage.clickOptionItemActions('20767'); + await expect( + videopage + .elements() + .tooltipModalOptions() + .getByText(i18n.t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE'), { + exact: true, + }) + ).toBeVisible(); + await expect( + videopage.elements().tooltipModalOptionsThemeInput() + ).toHaveValue( + 'grey 500 andrebbe corretto in tutte le pagine per il testo perchè non' + ); + await expect( + videopage + .elements() + .tooltipModalOptions() + .getByText( + i18n.t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION', { + count: 3, + usageNumber: '3', + }) + ) + ).toBeVisible(); + await expect( + videopage.elements().tooltipModalOptionsSaveButton() + ).toBeVisible(); + }); + + test('should open the edit dialog in the tags combobox', async () => { + await videopage.openObservationAccordion(1); + await videopage.openComboboxVideoTags(1); + await videopage.clickOptionItemActions('12345'); + await expect( + videopage + .elements() + .tooltipModalOptions() + .getByText( + videopage.i18n.t('__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_TITLE'), + { exact: true } + ) + ).toBeVisible(); + await expect( + videopage + .elements() + .tooltipModalOptions() + .getByLabel( + videopage.i18n.t('__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_LABEL') + ) + ).toHaveValue('Important Tag'); + await expect( + videopage + .elements() + .tooltipModalOptions() + .getByText( + videopage.i18n.t( + '__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_DESCRIPTION', + { count: 5, usageNumber: '5' } + ) + ) + ).toBeVisible(); + await expect( + videopage.elements().tooltipModalOptionsSaveButton() + ).toBeVisible(); + }); + + test('should allow changing the name of a theme', async ({ page }) => { + await videopage.mockPatchVideoTag('20767'); + const patchTitleTag = page.waitForResponse( + (response) => + /\/api\/campaigns\/1\/video-tags\/20767/.test(response.url()) && + response.status() === 200 && + response.request().method() === 'PATCH' + ); + const getVideoTags = page.waitForResponse( + (response) => + /\/api\/campaigns\/1\/video-tags/.test(response.url()) && + response.status() === 200 && + response.request().method() === 'GET' + ); + await videopage.openObservationAccordion(1); + await videopage.openComboboxVideoTitle(1); + await videopage.clickOptionItemActions('20767'); + await videopage + .elements() + .tooltipModalOptionsThemeInput() + .fill('New Theme Name'); + await videopage.elements().tooltipModalOptionsSaveButton().click(); + const patchRequest = await patchTitleTag; + const requestBody = await patchRequest.request().postDataJSON(); + expect(requestBody).toEqual({ + newTagName: 'New Theme Name', + }); + // show user a success toast + await expect( + videopage.elements().toastThemeEditSuccessMessage() + ).toBeVisible(); + // invalidate and refetch video tags + await getVideoTags; + }); + + test('in the theme edit modal should show an error if trying to save with empty name or an existing theme name', async ({ + i18n, + page, + }) => { + await videopage.mockPatchVideoTag('20767', 409); + await videopage.openObservationAccordion(1); + await videopage.openComboboxVideoTitle(1); + await videopage.clickOptionItemActions('20767'); + await videopage.elements().tooltipModalOptionsThemeInput().clear(); + await expect( + videopage + .elements() + .tooltipModalOptions() + .getByText( + i18n.t( + '__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_REQUIRED_ERROR' + ) + ) + ).toBeVisible(); + await expect( + videopage.elements().tooltipModalOptionsSaveButton() + ).toBeDisabled(); + await videopage + .elements() + .tooltipModalOptionsThemeInput() + .fill('Existing Theme Name'); + await expect( + videopage.elements().tooltipModalOptionsSaveButton() + ).not.toBeDisabled(); + await videopage.elements().tooltipModalOptionsSaveButton().click(); + await expect( + videopage + .elements() + .tooltipModalOptions() + .getByText( + i18n.t( + '__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_DUPLICATE_ERROR' + ) + ) + ).toBeVisible(); + await expect( + videopage.elements().tooltipModalOptionsSaveButton() + ).toBeDisabled(); + await videopage + .elements() + .tooltipModalOptionsThemeInput() + .fill('Now Unique Name'); + await expect( + videopage.elements().tooltipModalOptionsSaveButton() + ).not.toBeDisabled(); + }); + + test('should allow changing the name of a tag', async () => { + await videopage.openObservationAccordion(1); + await videopage.openComboboxVideoTags(1); + await videopage.clickOptionItemActions('12345'); + }); + + test('in the tag edit modal should show an error if trying to save with empty name or an existing tag name', async ({ + i18n, + page, + }) => { + await videopage.openObservationAccordion(1); + await videopage.openComboboxVideoTags(1); + await videopage.clickOptionItemActions('12345'); + }); }); diff --git a/tests/fixtures/pages/Video.ts b/tests/fixtures/pages/Video.ts index 53f230f80..602c9968a 100644 --- a/tests/fixtures/pages/Video.ts +++ b/tests/fixtures/pages/Video.ts @@ -22,9 +22,61 @@ export class VideoPage extends UnguessPage { .locator('[data-garden-id="tags.tag_view"]'), observationAccordion: (id: number) => this.page.getByTestId(`observation-accordion-${id}`), + comboboxVideoTitle: (id: number) => + this.elements() + .observationAccordion(id) + .getByTestId(`video-title-dropdown`), + comboboxVideoTags: (id: number) => + this.elements() + .observationAccordion(id) + .getByTestId(`video-tags-dropdown`), + optionsItem: (itemID: string) => + this.page.locator(`[itemid="${itemID}"]`), + tooltipModalOptions: () => this.page.getByTestId('tooltip-modal-option'), + tooltipModalOptionsSaveButton: () => + this.elements() + .tooltipModalOptions() + .getByRole('button', { + name: this.i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SAVE_BUTTON'), + }), + toastThemeEditSuccessMessage: () => + this.page.getByText( + this.i18n.t( + '__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE' + ) + ), + tooltipModalOptionsThemeInput: () => + this.elements() + .tooltipModalOptions() + .getByLabel( + this.i18n.t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL') + ), }; } + async openObservationAccordion(id: number) { + await this.elements().observationAccordion(id).click(); + } + + async openComboboxVideoTitle(id: number) { + await this.elements().comboboxVideoTitle(id).click(); + } + + async openComboboxVideoTags(id: number) { + await this.elements().comboboxVideoTags(id).click(); + } + + async clickOptionItem(itemID: string) { + await this.elements().optionsItem(itemID).click(); + } + + async clickOptionItemActions(itemID: string) { + await this.elements() + .optionsItem(itemID) + .getByTestId('select-option-actions') + .click(); + } + async mockGetCampaign() { await this.page.route('*/**/api/campaigns/1', async (route) => { await route.fulfill({ @@ -33,6 +85,44 @@ export class VideoPage extends UnguessPage { }); } + async mockPatchVideoTag(id: string, error?: number | boolean) { + await this.page.route( + `*/**/api/campaigns/1/video-tags/${id}`, + async (route) => { + if (route.request().method() === 'PATCH') { + console.log('titledrop api response', error); + if (error === 409) { + await route.fulfill({ + status: 409, + }); + } else if (error === true) { + await route.fulfill({ + status: 500, + }); + } else { + await route.fulfill({ + status: 200, + }); + } + } else { + await route.fallback(); + } + } + ); + } + + async mockGetVideoTags() { + await this.page.route('*/**/api/campaigns/1/video-tags', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + path: 'tests/api/campaigns/cid/video-tags/_get/200_Example_1.json', + }); + } else { + await route.fallback(); + } + }); + } + async mockGetVideo() { await this.page.route('*/**/api/videos/1', async (route) => { await route.fulfill({ From 7df4b305295f42757f7589db22ded369552df274 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Thu, 6 Nov 2025 15:37:20 +0100 Subject: [PATCH 088/108] fix: remove console logs from TitleDropdown and Video page mock API response --- src/pages/Video/components/TitleDropdownNew.tsx | 5 ----- tests/fixtures/pages/Video.ts | 1 - 2 files changed, 6 deletions(-) diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index c1ee5ac1e..4c96d4b5f 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -75,10 +75,8 @@ export const TitleDropdown = ({ }, [newTitle]); const handleSubmit = async () => { - console.log('titledrop', error); if (error) return; // Update the title in the form - console.log('titledrop no error', newTitle); try { await patchVideoTag({ cid: campaignId?.toString() || '0', @@ -105,16 +103,13 @@ export const TitleDropdown = ({ } catch (error: any) { // Handle error (e.g., show error toast) // if status code is 409, conflict with another already saved name, show specific error - console.log('titledrop catch', error); if (error.status === 409) { - console.log('titledrop 409', error); setError( t( '__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_DUPLICATE_ERROR' ) ); } else { - console.log('titledrop else', error); addToast( ({ close }) => ( <Notification diff --git a/tests/fixtures/pages/Video.ts b/tests/fixtures/pages/Video.ts index 602c9968a..8f4b1e979 100644 --- a/tests/fixtures/pages/Video.ts +++ b/tests/fixtures/pages/Video.ts @@ -90,7 +90,6 @@ export class VideoPage extends UnguessPage { `*/**/api/campaigns/1/video-tags/${id}`, async (route) => { if (route.request().method() === 'PATCH') { - console.log('titledrop api response', error); if (error === 409) { await route.fulfill({ status: 409, From fae5b0cd92ef085021f8469b6cb1adb781ab7f0c Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Fri, 7 Nov 2025 09:51:07 +0100 Subject: [PATCH 089/108] feat: update video tag editing functionality with new modal and improved translations --- src/locales/en/translation.json | 14 +- src/locales/it/translation.json | 14 +- src/pages/Video/components/EditTagModal.tsx | 162 ++++++++++++++++++ .../Video/components/ObservationForm.tsx | 28 +-- .../Video/components/TitleDropdownNew.tsx | 152 +--------------- src/pages/Video/index.tsx | 1 + tests/e2e/video/observation.spec.ts | 106 ++++++++++-- tests/fixtures/pages/Video.ts | 13 +- 8 files changed, 284 insertions(+), 206 deletions(-) create mode 100644 src/pages/Video/components/EditTagModal.tsx diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 8b2390d19..cda5d92c5 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1424,13 +1424,13 @@ "__VIDEO_PAGE_PLAYER_SHORTCUT_OBSERVATION_STARTED": "Observation started", "__VIDEO_PAGE_PLAYER_START_ADD_OBSERVATION": "Start observation", "__VIDEO_PAGE_PLAYER_STOP_ADD_OBSERVATION": "End observation", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_zero": "No observations related to this theme", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "1 Observation related to this theme", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "{{usageNumber}} Observations related to this theme", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE": "An error occurred while saving the Theme changes. Please try again.", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL": "Theme", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE": "The Theme changes were successful.", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE": "Edit Theme", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_zero": "No observations related", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "1 Observation related", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "{{usageNumber}} Observations related", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE": "An error occurred while saving the changes. Please try again.", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL": "New Name", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE": "The changes were successful.", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE": "Edit", "__VIDEO_PAGE_TITLE": "UX Tagging Tool", "__VIDEO_PAGE_TRANSCRIPT_EMPTY_STATE": "For this video, there is no available transcript.", "__VIDEO_PAGE_TRANSCRIPT_INFO": "Select some text to create an observation or use the button on the video player.", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index f40f8bf98..919f551c3 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1464,13 +1464,13 @@ "__VIDEO_PAGE_PLAYER_SHORTCUT_OBSERVATION_STARTED": "Osservazione iniziata", "__VIDEO_PAGE_PLAYER_START_ADD_OBSERVATION": "Inizia osservazione", "__VIDEO_PAGE_PLAYER_STOP_ADD_OBSERVATION": "Fine osservazione", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_many": "", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE": "", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL": "", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE": "", - "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_many": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE": "", "__VIDEO_PAGE_TITLE": "Tagging Tool", "__VIDEO_PAGE_TRANSCRIPT_EMPTY_STATE": "Per questo video non è disponibile una trascrizione.", "__VIDEO_PAGE_TRANSCRIPT_INFO": "Seleziona del testo per creare un'osservazione o usa il pulsante sul lettore video.", diff --git a/src/pages/Video/components/EditTagModal.tsx b/src/pages/Video/components/EditTagModal.tsx new file mode 100644 index 000000000..866dc69a8 --- /dev/null +++ b/src/pages/Video/components/EditTagModal.tsx @@ -0,0 +1,162 @@ +import { + Button, + Input, + Label, + MD, + Message, + Notification, + Paragraph, + SM, + TooltipModal, + useToast, +} from '@appquality/unguess-design-system'; +import { current } from '@reduxjs/toolkit'; +import { useEffect, useRef, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { appTheme } from 'src/app/theme'; +import { ReactComponent as SaveIcon } from 'src/assets/icons/save.svg'; +import { + GetCampaignsByCidVideoTagsApiResponse, + usePatchCampaignsByCidVideoTagsAndTagIdMutation, +} from 'src/features/api'; + +interface EditModalProps { + tag: GetCampaignsByCidVideoTagsApiResponse[number]['tags'][number]; + closeModal: () => void; +} + +export const EditTagModal = ({ closeModal, tag }: EditModalProps) => { + // Extract both the current title and the usage number in parentheses in two variables + + const [newName, setNewName] = useState(tag.name); + const inputRef = useRef<HTMLInputElement>(null); + const [error, setError] = useState<string | null>(null); + const [patchVideoTag] = usePatchCampaignsByCidVideoTagsAndTagIdMutation({}); + const { addToast } = useToast(); + const { t } = useTranslation(); + const { campaignId } = useParams(); + + useEffect(() => { + if (newName.trim() === '') { + setError( + t('__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_REQUIRED_ERROR') + ); + } else { + setError(null); + } + }, [newName]); + + const handleSubmit = async () => { + if (error) return; + // Update the title in the form + try { + await patchVideoTag({ + cid: campaignId?.toString() || '0', + tagId: tag.id.toString(), + body: { + newTagName: newName, + }, + }).unwrap(); + closeModal(); + addToast( + ({ close }) => ( + <Notification + onClose={close} + type="success" + message={t( + '__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE' + )} + closeText={t('__TOAST_CLOSE_TEXT')} + isPrimary + /> + ), + { placement: 'top' } + ); + } catch (error: any) { + // Handle error (e.g., show error toast) + // if status code is 409, conflict with another already saved name, show specific error + if (error.status === 409) { + setError( + t('__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_DUPLICATE_ERROR') + ); + } else { + addToast( + ({ close }) => ( + <Notification + onClose={close} + type="error" + message={t( + '__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE' + )} + closeText={t('__TOAST_CLOSE_TEXT')} + isPrimary + /> + ), + { placement: 'top' } + ); + } + } + }; + const handleClick = () => { + inputRef.current?.focus(); + }; + + return ( + <> + <TooltipModal.Title> + <MD isBold style={{ marginBottom: appTheme.space.sm }}> + {t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE')} + </MD> + </TooltipModal.Title> + <TooltipModal.Body> + <Label htmlFor="title-input"> + {t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL')} + <span style={{ color: appTheme.palette.red[500] }}>*</span> + </Label> + <Input + ref={inputRef} + id="title-input" + onClick={handleClick} + value={newName} + onChange={(e) => setNewName(e.target.value)} + /> + {error && ( + <Message validation="error" style={{ marginTop: appTheme.space.xs }}> + {error} + </Message> + )} + <div + style={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + margin: `${appTheme.space.md} 0 0 0`, + }} + > + <Paragraph style={{ margin: 0 }}> + <SM> + <Trans + i18nKey="__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION" + values={{ usageNumber: tag.usageNumber }} + count={Number(tag.usageNumber)} + /> + </SM> + </Paragraph> + <Button + size="small" + disabled={!!error} + isPrimary + isAccent + onClick={handleSubmit} + > + <Button.StartIcon> + <SaveIcon /> + </Button.StartIcon> + {t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SAVE_BUTTON')} + </Button> + </div> + </TooltipModal.Body> + </> + ); +}; diff --git a/src/pages/Video/components/ObservationForm.tsx b/src/pages/Video/components/ObservationForm.tsx index a4148de92..274c9602e 100644 --- a/src/pages/Video/components/ObservationForm.tsx +++ b/src/pages/Video/components/ObservationForm.tsx @@ -10,11 +10,10 @@ import { Skeleton, Tag, Textarea, - TooltipModal, useToast, } from '@appquality/unguess-design-system'; import { Form, Formik, FormikHelpers, FormikProps } from 'formik'; -import { useEffect, useRef, useState } from 'react'; +import { ComponentProps, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; @@ -30,8 +29,9 @@ import { import { styled } from 'styled-components'; import * as Yup from 'yup'; import { ConfirmDeleteModal } from './ConfirmDeleteModal'; -import { ObservationFormValues, TitleDropdown } from './TitleDropdownNew'; import { TooltipModalContextProvider } from './context'; +import { EditTagModal } from './EditTagModal'; +import { ObservationFormValues, TitleDropdown } from './TitleDropdownNew'; const FormContainer = styled.div` padding: ${({ theme }) => theme.space.md} ${({ theme }) => theme.space.xxs}; @@ -74,7 +74,7 @@ const ObservationForm = ({ const formRef = useRef<FormikProps<ObservationFormValues>>(null); const { addToast } = useToast(); const [options, setOptions] = useState< - { id: number; label: string; selected?: boolean; actions: JSX.Element }[] + ComponentProps<typeof MultiSelect>['options'] >([]); const [selectedSeverity, setSelectedSeverity] = useState< GetCampaignsByCidVideoTagsApiResponse[number]['tags'][number] | undefined @@ -143,24 +143,11 @@ const ObservationForm = ({ .sort((a, b) => b.usageNumber - a.usageNumber) .map((tag) => ({ id: tag.id, + itemID: tag.id.toString(), label: `${tag.name} (${tag.usageNumber})`, selected: selectedOptions.some((bt) => bt.id === tag.id), - actions: ( - <> - <TooltipModal.Title>{tag.name}</TooltipModal.Title> - <TooltipModal.Body> - {t( - '__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_TAGS_EDIT_MODAL_DESCRIPTION' - )} - <Button - isPrimary - isAccent - style={{ marginTop: appTheme.space.md }} - > - Save - </Button> - </TooltipModal.Body> - </> + actions: ({ closeModal }) => ( + <EditTagModal tag={tag} closeModal={closeModal} /> ), })) ); @@ -377,6 +364,7 @@ const ObservationForm = ({ isEditable options={options} selectedItems={options.filter((o) => o.selected)} + listboxAppendToNode={document.body} creatable maxItems={4} size="medium" diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index 4c96d4b5f..fa74f76f0 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -13,7 +13,6 @@ import { useToast, } from '@appquality/unguess-design-system'; import { FormikProps } from 'formik'; -import { ErrorMessage } from 'formik/dist/ErrorMessage'; import { useEffect, useRef, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; @@ -25,6 +24,7 @@ import { usePatchCampaignsByCidVideoTagsAndTagIdMutation, usePostCampaignsByCidVideoTagsMutation, } from 'src/features/api'; +import { EditTagModal } from './EditTagModal'; export interface ObservationFormValues { title: number; @@ -33,11 +33,6 @@ export interface ObservationFormValues { quotes?: string; } -interface EditModalProps { - option: { value: string | object; label: string }; - closeModal: () => void; -} - export const TitleDropdown = ({ titles, formProps, @@ -54,145 +49,6 @@ export const TitleDropdown = ({ return null; } - const editModal = ({ option, closeModal }: EditModalProps) => { - // Extract both the current title and the usage number in parentheses in two variables - const currentTitle = option.label.replace(/\s*\(\d+\)/, ''); - const usageNumber = option.label.match(/\((\d+)\)/)?.[1]; - const [newTitle, setNewTitle] = useState(currentTitle); - const inputRef = useRef<HTMLInputElement>(null); - const [error, setError] = useState<string | null>(null); - const [patchVideoTag] = usePatchCampaignsByCidVideoTagsAndTagIdMutation({}); - const { addToast } = useToast(); - - useEffect(() => { - if (newTitle.trim() === '') { - setError( - t('__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_REQUIRED_ERROR') - ); - } else { - setError(null); - } - }, [newTitle]); - - const handleSubmit = async () => { - if (error) return; - // Update the title in the form - try { - await patchVideoTag({ - cid: campaignId?.toString() || '0', - tagId: option.value.toString(), - body: { - newTagName: newTitle, - }, - }).unwrap(); - closeModal(); - addToast( - ({ close }) => ( - <Notification - onClose={close} - type="success" - message={t( - '__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE' - )} - closeText={t('__TOAST_CLOSE_TEXT')} - isPrimary - /> - ), - { placement: 'top' } - ); - } catch (error: any) { - // Handle error (e.g., show error toast) - // if status code is 409, conflict with another already saved name, show specific error - if (error.status === 409) { - setError( - t( - '__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_DUPLICATE_ERROR' - ) - ); - } else { - addToast( - ({ close }) => ( - <Notification - onClose={close} - type="error" - message={t( - '__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE' - )} - closeText={t('__TOAST_CLOSE_TEXT')} - isPrimary - /> - ), - { placement: 'top' } - ); - } - } - }; - const handleClick = () => { - inputRef.current?.focus(); - }; - - return ( - <> - <TooltipModal.Title> - <MD isBold style={{ marginBottom: appTheme.space.sm }}> - {t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE')} - </MD> - </TooltipModal.Title> - <TooltipModal.Body> - <Label htmlFor="title-input"> - {t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL')} - <span style={{ color: appTheme.palette.red[500] }}>*</span> - </Label> - <Input - ref={inputRef} - id="title-input" - onClick={handleClick} - value={newTitle} - onChange={(e) => setNewTitle(e.target.value)} - /> - {error && ( - <Message - validation="error" - style={{ marginTop: appTheme.space.xs }} - > - {error} - </Message> - )} - <div - style={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - margin: `${appTheme.space.md} 0 0 0`, - }} - > - <Paragraph style={{ margin: 0 }}> - <SM> - <Trans - i18nKey="__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION" - values={{ usageNumber }} - count={Number(usageNumber)} - /> - </SM> - </Paragraph> - <Button - size="small" - disabled={!!error} - isPrimary - isAccent - onClick={handleSubmit} - > - <Button.StartIcon> - <SaveIcon /> - </Button.StartIcon> - {t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SAVE_BUTTON')} - </Button> - </div> - </TooltipModal.Body> - </> - ); - }; - return ( <Field> <Autocomplete @@ -241,7 +97,7 @@ export const TitleDropdown = ({ }} options={[ { - id: 'id', + id: 'titles-group', label: 'select or create', options: (titles || []).map((i) => { return { @@ -249,7 +105,9 @@ export const TitleDropdown = ({ value: i.id.toString(), label: `${i.name} (${i.usageNumber})`, isSelected: formProps.values.title === i.id, - actions: editModal, + actions: ({ closeModal }) => ( + <EditTagModal tag={i} closeModal={closeModal} /> + ), itemID: i.id.toString(), }; }), diff --git a/src/pages/Video/index.tsx b/src/pages/Video/index.tsx index 6a842923e..448906a9c 100644 --- a/src/pages/Video/index.tsx +++ b/src/pages/Video/index.tsx @@ -16,6 +16,7 @@ import { useFeatureFlag } from 'src/hooks/useFeatureFlag'; import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; import VideoPageHeader from './components/PageHeader'; import VideoPageContent from './Content'; +import { Helmet } from 'react-helmet'; const VideoPage = () => { const { t } = useTranslation(); diff --git a/tests/e2e/video/observation.spec.ts b/tests/e2e/video/observation.spec.ts index 60b7d1403..ffc5f0a92 100644 --- a/tests/e2e/video/observation.spec.ts +++ b/tests/e2e/video/observation.spec.ts @@ -9,6 +9,7 @@ test.describe('Video page', () => { await videopage.loggedIn(); await videopage.mockPreferences(); await videopage.mockWorkspace(); + await videopage.mockWorkspacesList(); await videopage.mockGetCampaign(); await videopage.mockGetVideo(); await videopage.mockGetVideoTags(); @@ -26,7 +27,7 @@ test.describe('Video page', () => { videopage .elements() .tooltipModalOptions() - .getByText(i18n.t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE'), { + .getByText(i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE'), { exact: true, }) ).toBeVisible(); @@ -40,7 +41,7 @@ test.describe('Video page', () => { .elements() .tooltipModalOptions() .getByText( - i18n.t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION', { + i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION', { count: 3, usageNumber: '3', }) @@ -54,33 +55,30 @@ test.describe('Video page', () => { test('should open the edit dialog in the tags combobox', async () => { await videopage.openObservationAccordion(1); await videopage.openComboboxVideoTags(1); - await videopage.clickOptionItemActions('12345'); + await videopage.clickOptionItemActions('1103'); await expect( videopage .elements() .tooltipModalOptions() - .getByText( - videopage.i18n.t('__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_TITLE'), - { exact: true } - ) + .getByText(videopage.i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE'), { + exact: true, + }) ).toBeVisible(); await expect( videopage .elements() .tooltipModalOptions() - .getByLabel( - videopage.i18n.t('__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_LABEL') - ) - ).toHaveValue('Important Tag'); + .getByLabel(videopage.i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL')) + ).toHaveValue('Bloccante'); await expect( videopage .elements() .tooltipModalOptions() .getByText( - videopage.i18n.t( - '__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_DESCRIPTION', - { count: 5, usageNumber: '5' } - ) + videopage.i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION', { + count: 5, + usageNumber: '5', + }) ) ).toBeVisible(); await expect( @@ -175,18 +173,90 @@ test.describe('Video page', () => { ).not.toBeDisabled(); }); - test('should allow changing the name of a tag', async () => { + test('should allow changing the name of a tag', async ({ page }) => { + await videopage.mockPatchVideoTag('1103'); + const patchTitleTag = page.waitForResponse( + (response) => + /\/api\/campaigns\/1\/video-tags\/1103/.test(response.url()) && + response.status() === 200 && + response.request().method() === 'PATCH' + ); + const getVideoTags = page.waitForResponse( + (response) => + /\/api\/campaigns\/1\/video-tags/.test(response.url()) && + response.status() === 200 && + response.request().method() === 'GET' + ); await videopage.openObservationAccordion(1); await videopage.openComboboxVideoTags(1); - await videopage.clickOptionItemActions('12345'); + await videopage.clickOptionItemActions('1103'); + await videopage + .elements() + .tooltipModalOptionsThemeInput() + .fill('New Tag Name'); + await videopage.elements().tooltipModalOptionsSaveButton().click(); + const patchRequest = await patchTitleTag; + const requestBody = await patchRequest.request().postDataJSON(); + expect(requestBody).toEqual({ + newTagName: 'New Tag Name', + }); + // show user a success toast + await expect( + videopage.elements().toastThemeEditSuccessMessage() + ).toBeVisible(); + // invalidate and refetch video tags + await getVideoTags; }); test('in the tag edit modal should show an error if trying to save with empty name or an existing tag name', async ({ i18n, page, }) => { + await videopage.mockPatchVideoTag('1103', 409); await videopage.openObservationAccordion(1); await videopage.openComboboxVideoTags(1); - await videopage.clickOptionItemActions('12345'); + await videopage.clickOptionItemActions('1103'); + await videopage.elements().tooltipModalOptionsThemeInput().clear(); + await expect( + videopage + .elements() + .tooltipModalOptions() + .getByText( + i18n.t( + '__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_REQUIRED_ERROR' + ) + ) + ).toBeVisible(); + await expect( + videopage.elements().tooltipModalOptionsSaveButton() + ).toBeDisabled(); + await videopage + .elements() + .tooltipModalOptionsThemeInput() + .fill('Existing Tag Name'); + await expect( + videopage.elements().tooltipModalOptionsSaveButton() + ).not.toBeDisabled(); + await videopage.elements().tooltipModalOptionsSaveButton().click(); + await expect( + videopage + .elements() + .tooltipModalOptions() + .getByText( + i18n.t( + '__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_DUPLICATE_ERROR' + ) + ) + ).toBeVisible(); + await expect( + videopage.elements().tooltipModalOptionsSaveButton() + ).toBeDisabled(); + await videopage + .elements() + .tooltipModalOptionsThemeInput() + .fill('Now Unique Name'); + await expect( + videopage.elements().tooltipModalOptionsSaveButton() + ).not.toBeDisabled(); }); }); diff --git a/tests/fixtures/pages/Video.ts b/tests/fixtures/pages/Video.ts index 8f4b1e979..b6b507436 100644 --- a/tests/fixtures/pages/Video.ts +++ b/tests/fixtures/pages/Video.ts @@ -41,16 +41,12 @@ export class VideoPage extends UnguessPage { }), toastThemeEditSuccessMessage: () => this.page.getByText( - this.i18n.t( - '__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE' - ) + this.i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE') ), tooltipModalOptionsThemeInput: () => this.elements() .tooltipModalOptions() - .getByLabel( - this.i18n.t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL') - ), + .getByLabel(this.i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL')), }; } @@ -63,7 +59,10 @@ export class VideoPage extends UnguessPage { } async openComboboxVideoTags(id: number) { - await this.elements().comboboxVideoTags(id).click(); + await this.elements() + .comboboxVideoTags(id) + .locator('[data-garden-id="dropdowns.combobox.input_icon"]') + .click(); } async clickOptionItem(itemID: string) { From 8c23af0feabd71963c53ecc3e18637d0a4e7a921 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Fri, 7 Nov 2025 09:59:27 +0100 Subject: [PATCH 090/108] fix: ensure observation accordion is scrolled into view before opening edit dialogs --- tests/e2e/video/observation.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/video/observation.spec.ts b/tests/e2e/video/observation.spec.ts index ffc5f0a92..617534dd7 100644 --- a/tests/e2e/video/observation.spec.ts +++ b/tests/e2e/video/observation.spec.ts @@ -15,6 +15,7 @@ test.describe('Video page', () => { await videopage.mockGetVideoTags(); await videopage.mockGetVideoObservations(); await videopage.open(); + await videopage.elements().observationAccordion(1).scrollIntoViewIfNeeded(); }); test('should open the edit dialog in the themes combobox and display an input and a summary text for the current item and a save button', async ({ From 7d6f1ce11e4617fb8d55f6c6f2012bdaff65df3d Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Fri, 7 Nov 2025 10:12:23 +0100 Subject: [PATCH 091/108] chore validate --- src/pages/Video/components/EditTagModal.tsx | 5 +-- .../Video/components/TitleDropdownNew.tsx | 38 ++++++------------- src/pages/Video/index.tsx | 1 - tests/e2e/video/observation.spec.ts | 2 - 4 files changed, 13 insertions(+), 33 deletions(-) diff --git a/src/pages/Video/components/EditTagModal.tsx b/src/pages/Video/components/EditTagModal.tsx index 866dc69a8..730c884b8 100644 --- a/src/pages/Video/components/EditTagModal.tsx +++ b/src/pages/Video/components/EditTagModal.tsx @@ -10,7 +10,6 @@ import { TooltipModal, useToast, } from '@appquality/unguess-design-system'; -import { current } from '@reduxjs/toolkit'; import { useEffect, useRef, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; @@ -73,10 +72,10 @@ export const EditTagModal = ({ closeModal, tag }: EditModalProps) => { ), { placement: 'top' } ); - } catch (error: any) { + } catch (err: any) { // Handle error (e.g., show error toast) // if status code is 409, conflict with another already saved name, show specific error - if (error.status === 409) { + if (err.status === 409) { setError( t('__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_DUPLICATE_ERROR') ); diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index fa74f76f0..8ae35e06f 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -1,27 +1,13 @@ import { Autocomplete, - Button, DropdownFieldNew as Field, - Input, - Label, - MD, - Message, - Notification, - Paragraph, - SM, - TooltipModal, - useToast, } from '@appquality/unguess-design-system'; import { FormikProps } from 'formik'; -import { useEffect, useRef, useState } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; -import { appTheme } from 'src/app/theme'; import { ReactComponent as CopyIcon } from 'src/assets/icons/copy-icon.svg'; -import { ReactComponent as SaveIcon } from 'src/assets/icons/save.svg'; import { GetCampaignsByCidVideoTagsApiResponse, - usePatchCampaignsByCidVideoTagsAndTagIdMutation, usePostCampaignsByCidVideoTagsMutation, } from 'src/features/api'; import { EditTagModal } from './EditTagModal'; @@ -99,18 +85,16 @@ export const TitleDropdown = ({ { id: 'titles-group', label: 'select or create', - options: (titles || []).map((i) => { - return { - id: i.id.toString(), - value: i.id.toString(), - label: `${i.name} (${i.usageNumber})`, - isSelected: formProps.values.title === i.id, - actions: ({ closeModal }) => ( - <EditTagModal tag={i} closeModal={closeModal} /> - ), - itemID: i.id.toString(), - }; - }), + options: (titles || []).map((i) => ({ + id: i.id.toString(), + value: i.id.toString(), + label: `${i.name} (${i.usageNumber})`, + isSelected: formProps.values.title === i.id, + actions: ({ closeModal }) => ( + <EditTagModal tag={i} closeModal={closeModal} /> + ), + itemID: i.id.toString(), + })), }, ]} startIcon={<CopyIcon />} diff --git a/src/pages/Video/index.tsx b/src/pages/Video/index.tsx index 448906a9c..6a842923e 100644 --- a/src/pages/Video/index.tsx +++ b/src/pages/Video/index.tsx @@ -16,7 +16,6 @@ import { useFeatureFlag } from 'src/hooks/useFeatureFlag'; import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; import VideoPageHeader from './components/PageHeader'; import VideoPageContent from './Content'; -import { Helmet } from 'react-helmet'; const VideoPage = () => { const { t } = useTranslation(); diff --git a/tests/e2e/video/observation.spec.ts b/tests/e2e/video/observation.spec.ts index 617534dd7..2b28d103b 100644 --- a/tests/e2e/video/observation.spec.ts +++ b/tests/e2e/video/observation.spec.ts @@ -124,7 +124,6 @@ test.describe('Video page', () => { test('in the theme edit modal should show an error if trying to save with empty name or an existing theme name', async ({ i18n, - page, }) => { await videopage.mockPatchVideoTag('20767', 409); await videopage.openObservationAccordion(1); @@ -211,7 +210,6 @@ test.describe('Video page', () => { test('in the tag edit modal should show an error if trying to save with empty name or an existing tag name', async ({ i18n, - page, }) => { await videopage.mockPatchVideoTag('1103', 409); await videopage.openObservationAccordion(1); From c9f75024ecfc242faac191f79df619100388029e Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Fri, 7 Nov 2025 10:24:20 +0100 Subject: [PATCH 092/108] restore mock --- .../categories/_get/200_Example_1.json | 19 +++++++++++++++++-- .../users/me/_get/200_Users_Me_example.json | 3 +++ .../wid/templates/_get/200_Example_1.json | 1 + .../200_global_and_private_templates.json | 9 +++++++++ .../templates/_get/200_global_template.json | 1 + 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/tests/api/templates/categories/_get/200_Example_1.json b/tests/api/templates/categories/_get/200_Example_1.json index 20b386b4b..f9f3dca26 100644 --- a/tests/api/templates/categories/_get/200_Example_1.json +++ b/tests/api/templates/categories/_get/200_Example_1.json @@ -1,7 +1,22 @@ [ { "description": "string", - "id": 0, - "name": "string" + "id": -1, + "name": "uncategorized" + }, + { + "description": "string", + "id": 10, + "name": "Accessibility" + }, + { + "description": "string", + "id": 20, + "name": "Best category ever" + }, + { + "description": "string", + "id": 30, + "name": "Security" } ] diff --git a/tests/api/users/me/_get/200_Users_Me_example.json b/tests/api/users/me/_get/200_Users_Me_example.json index df477885c..6a716a13a 100644 --- a/tests/api/users/me/_get/200_Users_Me_example.json +++ b/tests/api/users/me/_get/200_Users_Me_example.json @@ -16,6 +16,9 @@ ], "id": 1, "name": "Luca Cannarozzo", + "first_name": "Luca", + "last_name": "Cannarozzo", + "customer_role": "Designer", "picture": "https://www.gravatar.com/avatar/04c5df6d27d87476fe9dee853e1142bd", "profile_id": 32, "role": "administrator", diff --git a/tests/api/workspaces/wid/templates/_get/200_Example_1.json b/tests/api/workspaces/wid/templates/_get/200_Example_1.json index f9309c295..fc2185d65 100644 --- a/tests/api/workspaces/wid/templates/_get/200_Example_1.json +++ b/tests/api/workspaces/wid/templates/_get/200_Example_1.json @@ -1,6 +1,7 @@ { "items": [ { + "category_id": 0, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}]}", "description": "string", "id": 0, diff --git a/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json b/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json index 68401401b..c106ed897 100644 --- a/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json +++ b/tests/api/workspaces/wid/templates/_get/200_global_and_private_templates.json @@ -1,6 +1,7 @@ { "items": [ { + "category_id": -1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Gain qualitative insights by observing users as they interact. Identifies difficulties in the user experience, improves the interface. Identifies difficulties in the user experience, improves the interface.", "id": 1, @@ -10,6 +11,7 @@ "workspace_id": 1 }, { + "category_id": -1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Gain qualitative insights by observing users as they interact. Identifies difficulties in the user experience, improves the interface. Identifies difficulties in the user experience, improves the interface.", "id": 2, @@ -19,6 +21,7 @@ "workspace_id": 1 }, { + "category_id": -1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Find unexpected issues with exploratory testing.", "id": 3, @@ -27,6 +30,7 @@ "workspace_id": 1 }, { + "category_id": 1, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Improve the user experience.", "id": 4, @@ -35,6 +39,7 @@ "workspace_id": 1 }, { + "category_id": 10, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Penetration testing.", "id": 5, @@ -58,6 +63,7 @@ } }, { + "category_id": 10, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Test system performance", "id": 6, @@ -82,6 +88,7 @@ } }, { + "category_id": 20, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Gain qualitative insights by observing users as they interact. Identifies difficulties in the user experience, improves the interface. Identifies difficulties in the user experience, improves the interface.", "id": 7, @@ -105,6 +112,7 @@ } }, { + "category_id": 20, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Test system performance.", "id": 8, @@ -129,6 +137,7 @@ } }, { + "category_id": 30, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}, {\"type\": \"target\", \"variant\": \"default\", \"output\": 12}]}", "description": "Test system performance", "id": 9, diff --git a/tests/api/workspaces/wid/templates/_get/200_global_template.json b/tests/api/workspaces/wid/templates/_get/200_global_template.json index 1f963ad89..268250936 100644 --- a/tests/api/workspaces/wid/templates/_get/200_global_template.json +++ b/tests/api/workspaces/wid/templates/_get/200_global_template.json @@ -1,6 +1,7 @@ { "items": [ { + "category_id": 10, "config": "{ \"modules\": [{\"type\": \"title\", \"variant\": \"default\", \"output\": \"cp-title\"}]}", "description": "Description of global template 1", "id": 1, From ca655cd07ac47a645104dbf23280973cb1290ec0 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Fri, 7 Nov 2025 12:41:31 +0100 Subject: [PATCH 093/108] fix: update unguess-design-system version and improve translation keys for video page modals --- package.json | 2 +- src/locales/en/translation.json | 15 ++++++++------- src/locales/it/translation.json | 15 ++++++++------- src/pages/Video/components/EditTagModal.tsx | 6 +++++- yarn.lock | 8 ++++---- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 8dea09bbd..30f0aed30 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@analytics/google-tag-manager": "^0.6.0", "@analytics/hubspot": "^0.5.1", "@appquality/languages": "1.4.3", - "@appquality/unguess-design-system": "4.0.51--canary.551.2b300ae.0", + "@appquality/unguess-design-system": "4.0.51--canary.551.1f1b008.0", "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index cda5d92c5..d52123b16 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1411,7 +1411,15 @@ "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_REQUIRED_ERROR": "Theme is required", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_BUTTON": "Save", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_TOAST_SUCCESS": "Observation saved successfully", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_zero": "No observations related", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "1 Observation related", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "{{usageNumber}} Observations related", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE": "An error occurred while saving the changes. Please try again.", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_INPUT_HELPER_TEXT": "enter [↵] to save", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL": "New Name", "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SAVE_BUTTON": "Save", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE": "The changes were successful.", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE": "Edit", "__VIDEO_PAGE_NO_OBSERVATIONS": "Start watching the video and mark the highlights.", "__VIDEO_PAGE_NO_OBSERVATIONS_TITLE": "For this video, there are no observations yet.", "__VIDEO_PAGE_OBSERVATION_DESELECTED_TOAST_MESSAGE": "Observation deselected", @@ -1424,13 +1432,6 @@ "__VIDEO_PAGE_PLAYER_SHORTCUT_OBSERVATION_STARTED": "Observation started", "__VIDEO_PAGE_PLAYER_START_ADD_OBSERVATION": "Start observation", "__VIDEO_PAGE_PLAYER_STOP_ADD_OBSERVATION": "End observation", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_zero": "No observations related", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "1 Observation related", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "{{usageNumber}} Observations related", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE": "An error occurred while saving the changes. Please try again.", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL": "New Name", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE": "The changes were successful.", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE": "Edit", "__VIDEO_PAGE_TITLE": "UX Tagging Tool", "__VIDEO_PAGE_TRANSCRIPT_EMPTY_STATE": "For this video, there is no available transcript.", "__VIDEO_PAGE_TRANSCRIPT_INFO": "Select some text to create an observation or use the button on the video player.", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 919f551c3..ac9bae11a 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1451,7 +1451,15 @@ "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_REQUIRED_ERROR": "", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_BUTTON": "Salva", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_TOAST_SUCCESS": "Osservazione salvata con successo", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_many": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_INPUT_HELPER_TEXT": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL": "", "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SAVE_BUTTON": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE": "", + "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE": "", "__VIDEO_PAGE_NO_OBSERVATIONS": "Inizia a guardare il video e segna i momenti salienti.", "__VIDEO_PAGE_NO_OBSERVATIONS_TITLE": "Per questo video non ci sono ancora evidenze.", "__VIDEO_PAGE_OBSERVATION_DESELECTED_TOAST_MESSAGE": "Osservazione de-selezionata", @@ -1464,13 +1472,6 @@ "__VIDEO_PAGE_PLAYER_SHORTCUT_OBSERVATION_STARTED": "Osservazione iniziata", "__VIDEO_PAGE_PLAYER_START_ADD_OBSERVATION": "Inizia osservazione", "__VIDEO_PAGE_PLAYER_STOP_ADD_OBSERVATION": "Fine osservazione", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_many": "", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE": "", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL": "", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE": "", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE": "", "__VIDEO_PAGE_TITLE": "Tagging Tool", "__VIDEO_PAGE_TRANSCRIPT_EMPTY_STATE": "Per questo video non è disponibile una trascrizione.", "__VIDEO_PAGE_TRANSCRIPT_INFO": "Seleziona del testo per creare un'osservazione o usa il pulsante sul lettore video.", diff --git a/src/pages/Video/components/EditTagModal.tsx b/src/pages/Video/components/EditTagModal.tsx index 730c884b8..3028c54df 100644 --- a/src/pages/Video/components/EditTagModal.tsx +++ b/src/pages/Video/components/EditTagModal.tsx @@ -120,10 +120,14 @@ export const EditTagModal = ({ closeModal, tag }: EditModalProps) => { value={newName} onChange={(e) => setNewName(e.target.value)} /> - {error && ( + {error ? ( <Message validation="error" style={{ marginTop: appTheme.space.xs }}> {error} </Message> + ) : ( + <Message style={{ marginTop: appTheme.space.xs }}> + {t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_INPUT_HELPER_TEXT')} + </Message> )} <div style={{ diff --git a/yarn.lock b/yarn.lock index dbffdf982..b79c1c0c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,10 +122,10 @@ dependencies: hls.js "^1.4.8" -"@appquality/unguess-design-system@4.0.51--canary.551.2b300ae.0": - version "4.0.51--canary.551.2b300ae.0" - resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.51--canary.551.2b300ae.0.tgz#528a588ef0febd09d8b2aa7adc2acc8296840198" - integrity sha512-KR4JoAx9/iXAaUynly86+sUG3wYexF5DMDeX1OUaz0M5WHDfN85PpI3kVPW8hggZPXsWxc3t/ChyuCQ2IJWGNA== +"@appquality/unguess-design-system@4.0.51--canary.551.1f1b008.0": + version "4.0.51--canary.551.1f1b008.0" + resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.51--canary.551.1f1b008.0.tgz#5cb19fd933eba8482c0ba6b43b62d2edc65cd187" + integrity sha512-MAHzoG4teqRdhZuiY3zvMj+cAj2iTRgfnbt7glYXH7SOeKpFL4dcTroxDwcF+eBEV4HgVLSUPOiuQGcyb8nqOg== dependencies: "@appquality/stream-player" "1.0.6" "@nivo/bar" "^0.87.0" From ea614e7722652aeece3e6dde2770af9afff70e67 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Fri, 7 Nov 2025 15:11:47 +0100 Subject: [PATCH 094/108] feat: add Enter key functionality to submit tag edits in EditTagModal --- src/pages/Video/components/EditTagModal.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pages/Video/components/EditTagModal.tsx b/src/pages/Video/components/EditTagModal.tsx index 3028c54df..f506d1719 100644 --- a/src/pages/Video/components/EditTagModal.tsx +++ b/src/pages/Video/components/EditTagModal.tsx @@ -119,6 +119,11 @@ export const EditTagModal = ({ closeModal, tag }: EditModalProps) => { onClick={handleClick} value={newName} onChange={(e) => setNewName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleSubmit(); + } + }} /> {error ? ( <Message validation="error" style={{ marginTop: appTheme.space.xs }}> From 817ec211307c23834a5509c6ca9ba4eace4c3c48 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Mon, 10 Nov 2025 11:52:03 +0100 Subject: [PATCH 095/108] feat: enhance EditTagModal and TitleDropdown with dynamic title, label, and description props --- src/locales/en/translation.json | 17 +++++++++----- src/locales/it/translation.json | 15 ++++++++---- src/pages/Video/components/EditTagModal.tsx | 23 +++++++++++-------- .../Video/components/ObservationForm.tsx | 14 ++++++++++- .../Video/components/TitleDropdownNew.tsx | 14 ++++++++++- 5 files changed, 60 insertions(+), 23 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index d52123b16..47d11da72 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1408,18 +1408,13 @@ "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_LABEL": "Theme", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_MAX_ERROR": "You must insert a theme between 1 and 70 characters", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_PLACEHOLDER": "Select or add a theme", - "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_REQUIRED_ERROR": "Theme is required", + "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_REQUIRED_ERROR": "This field is required", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_BUTTON": "Save", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_TOAST_SUCCESS": "Observation saved successfully", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_zero": "No observations related", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "1 Observation related", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "{{usageNumber}} Observations related", "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE": "An error occurred while saving the changes. Please try again.", "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_INPUT_HELPER_TEXT": "enter [↵] to save", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL": "New Name", "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SAVE_BUTTON": "Save", "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE": "The changes were successful.", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE": "Edit", "__VIDEO_PAGE_NO_OBSERVATIONS": "Start watching the video and mark the highlights.", "__VIDEO_PAGE_NO_OBSERVATIONS_TITLE": "For this video, there are no observations yet.", "__VIDEO_PAGE_OBSERVATION_DESELECTED_TOAST_MESSAGE": "Observation deselected", @@ -1432,6 +1427,16 @@ "__VIDEO_PAGE_PLAYER_SHORTCUT_OBSERVATION_STARTED": "Observation started", "__VIDEO_PAGE_PLAYER_START_ADD_OBSERVATION": "Start observation", "__VIDEO_PAGE_PLAYER_STOP_ADD_OBSERVATION": "End observation", + "__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_DESCRIPTION_zero": "No observations related to this tag", + "__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "1 Observation related to this tag", + "__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "{{usageNumber}} Observations related to this tag", + "__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_LABEL": "Tag name", + "__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_TITLE": "Edit Tag", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_zero": "No observations related to this theme", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "1 Observation related to this theme", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "{{usageNumber}} Observations related to this theme", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL": "Theme name", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE": "Edit Theme", "__VIDEO_PAGE_TITLE": "UX Tagging Tool", "__VIDEO_PAGE_TRANSCRIPT_EMPTY_STATE": "For this video, there is no available transcript.", "__VIDEO_PAGE_TRANSCRIPT_INFO": "Select some text to create an observation or use the button on the video player.", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index ac9bae11a..a685d1175 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1451,15 +1451,10 @@ "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_REQUIRED_ERROR": "", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_BUTTON": "Salva", "__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_SAVE_TOAST_SUCCESS": "Osservazione salvata con successo", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_many": "", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "", "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_ERROR_TOAST_MESSAGE": "", "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_INPUT_HELPER_TEXT": "", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL": "", "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SAVE_BUTTON": "", "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE": "", - "__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE": "", "__VIDEO_PAGE_NO_OBSERVATIONS": "Inizia a guardare il video e segna i momenti salienti.", "__VIDEO_PAGE_NO_OBSERVATIONS_TITLE": "Per questo video non ci sono ancora evidenze.", "__VIDEO_PAGE_OBSERVATION_DESELECTED_TOAST_MESSAGE": "Osservazione de-selezionata", @@ -1472,6 +1467,16 @@ "__VIDEO_PAGE_PLAYER_SHORTCUT_OBSERVATION_STARTED": "Osservazione iniziata", "__VIDEO_PAGE_PLAYER_START_ADD_OBSERVATION": "Inizia osservazione", "__VIDEO_PAGE_PLAYER_STOP_ADD_OBSERVATION": "Fine osservazione", + "__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "", + "__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_DESCRIPTION_many": "", + "__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "", + "__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_LABEL": "", + "__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_TITLE": "", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_one": "", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_many": "", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_other": "", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL": "", + "__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE": "", "__VIDEO_PAGE_TITLE": "Tagging Tool", "__VIDEO_PAGE_TRANSCRIPT_EMPTY_STATE": "Per questo video non è disponibile una trascrizione.", "__VIDEO_PAGE_TRANSCRIPT_INFO": "Seleziona del testo per creare un'osservazione o usa il pulsante sul lettore video.", diff --git a/src/pages/Video/components/EditTagModal.tsx b/src/pages/Video/components/EditTagModal.tsx index f506d1719..5df99359b 100644 --- a/src/pages/Video/components/EditTagModal.tsx +++ b/src/pages/Video/components/EditTagModal.tsx @@ -23,9 +23,18 @@ import { interface EditModalProps { tag: GetCampaignsByCidVideoTagsApiResponse[number]['tags'][number]; closeModal: () => void; + title: string; + label: string; + description: string; } -export const EditTagModal = ({ closeModal, tag }: EditModalProps) => { +export const EditTagModal = ({ + closeModal, + tag, + title, + label, + description, +}: EditModalProps) => { // Extract both the current title and the usage number in parentheses in two variables const [newName, setNewName] = useState(tag.name); @@ -105,12 +114,12 @@ export const EditTagModal = ({ closeModal, tag }: EditModalProps) => { <> <TooltipModal.Title> <MD isBold style={{ marginBottom: appTheme.space.sm }}> - {t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE')} + {title} </MD> </TooltipModal.Title> <TooltipModal.Body> <Label htmlFor="title-input"> - {t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL')} + {label} <span style={{ color: appTheme.palette.red[500] }}>*</span> </Label> <Input @@ -143,13 +152,7 @@ export const EditTagModal = ({ closeModal, tag }: EditModalProps) => { }} > <Paragraph style={{ margin: 0 }}> - <SM> - <Trans - i18nKey="__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION" - values={{ usageNumber: tag.usageNumber }} - count={Number(tag.usageNumber)} - /> - </SM> + <SM>{description}</SM> </Paragraph> <Button size="small" diff --git a/src/pages/Video/components/ObservationForm.tsx b/src/pages/Video/components/ObservationForm.tsx index 274c9602e..eff762335 100644 --- a/src/pages/Video/components/ObservationForm.tsx +++ b/src/pages/Video/components/ObservationForm.tsx @@ -147,7 +147,19 @@ const ObservationForm = ({ label: `${tag.name} (${tag.usageNumber})`, selected: selectedOptions.some((bt) => bt.id === tag.id), actions: ({ closeModal }) => ( - <EditTagModal tag={tag} closeModal={closeModal} /> + <EditTagModal + tag={tag} + closeModal={closeModal} + title={t('__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_TITLE')} + label={t('__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_LABEL')} + description={t( + '__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_DESCRIPTION', + { + usageNumber: tag.usageNumber, + count: Number(tag.usageNumber), + } + )} + /> ), })) ); diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index 8ae35e06f..d67de7f1a 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -91,7 +91,19 @@ export const TitleDropdown = ({ label: `${i.name} (${i.usageNumber})`, isSelected: formProps.values.title === i.id, actions: ({ closeModal }) => ( - <EditTagModal tag={i} closeModal={closeModal} /> + <EditTagModal + tag={i} + closeModal={closeModal} + title={t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE')} + label={t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL')} + description={t( + '__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION', + { + usageNumber: i.usageNumber, + count: Number(i.usageNumber), + } + )} + /> ), itemID: i.id.toString(), })), From fb90618fe8225a1501ee05b67c8452aa19bfce89 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Mon, 10 Nov 2025 12:14:02 +0100 Subject: [PATCH 096/108] fix: update @appquality/unguess-design-system version to correct canary build --- package.json | 2 +- src/pages/Video/components/EditTagModal.tsx | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 30f0aed30..9370f61dc 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@analytics/google-tag-manager": "^0.6.0", "@analytics/hubspot": "^0.5.1", "@appquality/languages": "1.4.3", - "@appquality/unguess-design-system": "4.0.51--canary.551.1f1b008.0", + "@appquality/unguess-design-system": "4.0.51--canary.551.2a494c1.0", "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", diff --git a/src/pages/Video/components/EditTagModal.tsx b/src/pages/Video/components/EditTagModal.tsx index 5df99359b..d0fe6d6bb 100644 --- a/src/pages/Video/components/EditTagModal.tsx +++ b/src/pages/Video/components/EditTagModal.tsx @@ -11,7 +11,7 @@ import { useToast, } from '@appquality/unguess-design-system'; import { useEffect, useRef, useState } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; import { ReactComponent as SaveIcon } from 'src/assets/icons/save.svg'; diff --git a/yarn.lock b/yarn.lock index b79c1c0c1..3ed8f9164 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,10 +122,10 @@ dependencies: hls.js "^1.4.8" -"@appquality/unguess-design-system@4.0.51--canary.551.1f1b008.0": - version "4.0.51--canary.551.1f1b008.0" - resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.51--canary.551.1f1b008.0.tgz#5cb19fd933eba8482c0ba6b43b62d2edc65cd187" - integrity sha512-MAHzoG4teqRdhZuiY3zvMj+cAj2iTRgfnbt7glYXH7SOeKpFL4dcTroxDwcF+eBEV4HgVLSUPOiuQGcyb8nqOg== +"@appquality/unguess-design-system@4.0.51--canary.551.2a494c1.0": + version "4.0.51--canary.551.2a494c1.0" + resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.51--canary.551.2a494c1.0.tgz#169e75db65712d20fc918e8d080ffb8effef72a3" + integrity sha512-DJczcnhOFnWZdzc/Q0MPGLcc+xhReXQBYtJTPOwPQYfYYDe5fuvq71HqNvzYKeOqgdA5iG8tJkoWyjMFG754Ag== dependencies: "@appquality/stream-player" "1.0.6" "@nivo/bar" "^0.87.0" From 664f385bace097cb82171854339066fa5ee2fe7d Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Tue, 11 Nov 2025 12:31:42 +0100 Subject: [PATCH 097/108] fix: update @appquality/unguess-design-system version to 4.0.52 canary --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9370f61dc..461f9a076 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@analytics/google-tag-manager": "^0.6.0", "@analytics/hubspot": "^0.5.1", "@appquality/languages": "1.4.3", - "@appquality/unguess-design-system": "4.0.51--canary.551.2a494c1.0", + "@appquality/unguess-design-system": "4.0.52--canary.551.da98e7c.0", "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", diff --git a/yarn.lock b/yarn.lock index 3ed8f9164..80c1881fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,10 +122,10 @@ dependencies: hls.js "^1.4.8" -"@appquality/unguess-design-system@4.0.51--canary.551.2a494c1.0": - version "4.0.51--canary.551.2a494c1.0" - resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.51--canary.551.2a494c1.0.tgz#169e75db65712d20fc918e8d080ffb8effef72a3" - integrity sha512-DJczcnhOFnWZdzc/Q0MPGLcc+xhReXQBYtJTPOwPQYfYYDe5fuvq71HqNvzYKeOqgdA5iG8tJkoWyjMFG754Ag== +"@appquality/unguess-design-system@4.0.52--canary.551.da98e7c.0": + version "4.0.52--canary.551.da98e7c.0" + resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.52--canary.551.da98e7c.0.tgz#1360e2485ffcf84e37a96aa7f465dafbb1b13f35" + integrity sha512-i0Au2hE9QtlpvxvVIV4SZzFvw2KMKhghJGvLF3KgMSh1RGeOPyA2VmNkkDP8ZtXqwOsZ1eRg7SvBilDxrCUMYQ== dependencies: "@appquality/stream-player" "1.0.6" "@nivo/bar" "^0.87.0" From 20f8245900d950f256438d1cbd9c59f4d1d67b6d Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Tue, 11 Nov 2025 17:22:35 +0100 Subject: [PATCH 098/108] feat: update video page test suite to improve themes and tags editing functionality --- tests/e2e/video/observation.spec.ts | 100 +++++++++++++++------------- tests/fixtures/pages/Video.ts | 12 +++- 2 files changed, 64 insertions(+), 48 deletions(-) diff --git a/tests/e2e/video/observation.spec.ts b/tests/e2e/video/observation.spec.ts index 2b28d103b..b5c11ccd5 100644 --- a/tests/e2e/video/observation.spec.ts +++ b/tests/e2e/video/observation.spec.ts @@ -18,6 +18,8 @@ test.describe('Video page', () => { await videopage.elements().observationAccordion(1).scrollIntoViewIfNeeded(); }); + // THEMES EDITING TESTS BELOW + test('should open the edit dialog in the themes combobox and display an input and a summary text for the current item and a save button', async ({ i18n, }) => { @@ -28,7 +30,7 @@ test.describe('Video page', () => { videopage .elements() .tooltipModalOptions() - .getByText(i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE'), { + .getByText(i18n.t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE'), { exact: true, }) ).toBeVisible(); @@ -42,7 +44,7 @@ test.describe('Video page', () => { .elements() .tooltipModalOptions() .getByText( - i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION', { + i18n.t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION_other', { count: 3, usageNumber: '3', }) @@ -53,40 +55,6 @@ test.describe('Video page', () => { ).toBeVisible(); }); - test('should open the edit dialog in the tags combobox', async () => { - await videopage.openObservationAccordion(1); - await videopage.openComboboxVideoTags(1); - await videopage.clickOptionItemActions('1103'); - await expect( - videopage - .elements() - .tooltipModalOptions() - .getByText(videopage.i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_TITLE'), { - exact: true, - }) - ).toBeVisible(); - await expect( - videopage - .elements() - .tooltipModalOptions() - .getByLabel(videopage.i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL')) - ).toHaveValue('Bloccante'); - await expect( - videopage - .elements() - .tooltipModalOptions() - .getByText( - videopage.i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_DESCRIPTION', { - count: 5, - usageNumber: '5', - }) - ) - ).toBeVisible(); - await expect( - videopage.elements().tooltipModalOptionsSaveButton() - ).toBeVisible(); - }); - test('should allow changing the name of a theme', async ({ page }) => { await videopage.mockPatchVideoTag('20767'); const patchTitleTag = page.waitForResponse( @@ -115,9 +83,7 @@ test.describe('Video page', () => { newTagName: 'New Theme Name', }); // show user a success toast - await expect( - videopage.elements().toastThemeEditSuccessMessage() - ).toBeVisible(); + await expect(videopage.elements().toastEditSuccessMessage()).toBeVisible(); // invalidate and refetch video tags await getVideoTags; }); @@ -173,6 +139,50 @@ test.describe('Video page', () => { ).not.toBeDisabled(); }); + // TAGS EDITING TESTS BELOW + + test('should open the edit dialog in the tags combobox', async () => { + await videopage.openObservationAccordion(1); + await videopage.openComboboxVideoTags(1); + await videopage.clickOptionItemActions('1103'); + await expect( + videopage + .elements() + .tooltipModalOptions() + .getByText( + videopage.i18n.t('__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_TITLE'), + { + exact: true, + } + ) + ).toBeVisible(); + await expect( + videopage + .elements() + .tooltipModalOptions() + .getByLabel( + videopage.i18n.t('__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_LABEL') + ) + ).toHaveValue('Bloccante'); + await expect( + videopage + .elements() + .tooltipModalOptions() + .getByText( + videopage.i18n.t( + '__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_DESCRIPTION_other', + { + count: 5, + usageNumber: '5', + } + ) + ) + ).toBeVisible(); + await expect( + videopage.elements().tooltipModalOptionsSaveButton() + ).toBeVisible(); + }); + test('should allow changing the name of a tag', async ({ page }) => { await videopage.mockPatchVideoTag('1103'); const patchTitleTag = page.waitForResponse( @@ -192,7 +202,7 @@ test.describe('Video page', () => { await videopage.clickOptionItemActions('1103'); await videopage .elements() - .tooltipModalOptionsThemeInput() + .tooltipModalOptionsTagInput() .fill('New Tag Name'); await videopage.elements().tooltipModalOptionsSaveButton().click(); const patchRequest = await patchTitleTag; @@ -201,9 +211,7 @@ test.describe('Video page', () => { newTagName: 'New Tag Name', }); // show user a success toast - await expect( - videopage.elements().toastThemeEditSuccessMessage() - ).toBeVisible(); + await expect(videopage.elements().toastEditSuccessMessage()).toBeVisible(); // invalidate and refetch video tags await getVideoTags; }); @@ -215,7 +223,7 @@ test.describe('Video page', () => { await videopage.openObservationAccordion(1); await videopage.openComboboxVideoTags(1); await videopage.clickOptionItemActions('1103'); - await videopage.elements().tooltipModalOptionsThemeInput().clear(); + await videopage.elements().tooltipModalOptionsTagInput().clear(); await expect( videopage .elements() @@ -231,7 +239,7 @@ test.describe('Video page', () => { ).toBeDisabled(); await videopage .elements() - .tooltipModalOptionsThemeInput() + .tooltipModalOptionsTagInput() .fill('Existing Tag Name'); await expect( videopage.elements().tooltipModalOptionsSaveButton() @@ -252,7 +260,7 @@ test.describe('Video page', () => { ).toBeDisabled(); await videopage .elements() - .tooltipModalOptionsThemeInput() + .tooltipModalOptionsTagInput() .fill('Now Unique Name'); await expect( videopage.elements().tooltipModalOptionsSaveButton() diff --git a/tests/fixtures/pages/Video.ts b/tests/fixtures/pages/Video.ts index b6b507436..2aa2ce43b 100644 --- a/tests/fixtures/pages/Video.ts +++ b/tests/fixtures/pages/Video.ts @@ -39,14 +39,22 @@ export class VideoPage extends UnguessPage { .getByRole('button', { name: this.i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SAVE_BUTTON'), }), - toastThemeEditSuccessMessage: () => + toastEditSuccessMessage: () => this.page.getByText( this.i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_SUCCESS_TOAST_MESSAGE') ), tooltipModalOptionsThemeInput: () => this.elements() .tooltipModalOptions() - .getByLabel(this.i18n.t('__VIDEO_PAGE_DROPDOWN_EDIT_MODAL_LABEL')), + .getByLabel( + this.i18n.t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL') + ), + tooltipModalOptionsTagInput: () => + this.elements() + .tooltipModalOptions() + .getByLabel( + this.i18n.t('__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_LABEL') + ), }; } From 8f5e7356bf2983b925c17cb29f1415bdbd8907d9 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Tue, 11 Nov 2025 17:31:11 +0100 Subject: [PATCH 099/108] refactor: simplify options mapping in TitleDropdown component --- .../Video/components/TitleDropdownNew.tsx | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index d67de7f1a..daee7a923 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -81,34 +81,28 @@ export const TitleDropdown = ({ if (!selectionValue || !inputValue) return; formProps.setFieldValue('title', Number(selectionValue)); }} - options={[ - { - id: 'titles-group', - label: 'select or create', - options: (titles || []).map((i) => ({ - id: i.id.toString(), - value: i.id.toString(), - label: `${i.name} (${i.usageNumber})`, - isSelected: formProps.values.title === i.id, - actions: ({ closeModal }) => ( - <EditTagModal - tag={i} - closeModal={closeModal} - title={t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE')} - label={t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL')} - description={t( - '__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION', - { - usageNumber: i.usageNumber, - count: Number(i.usageNumber), - } - )} - /> - ), - itemID: i.id.toString(), - })), - }, - ]} + options={(titles || []).map((i) => ({ + id: i.id.toString(), + value: i.id.toString(), + label: `${i.name} (${i.usageNumber})`, + isSelected: formProps.values.title === i.id, + actions: ({ closeModal }) => ( + <EditTagModal + tag={i} + closeModal={closeModal} + title={t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE')} + label={t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL')} + description={t( + '__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION', + { + usageNumber: i.usageNumber, + count: Number(i.usageNumber), + } + )} + /> + ), + itemID: i.id.toString(), + }))} startIcon={<CopyIcon />} placeholder={t( '__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_PLACEHOLDER' From c9c5a761477d050068f7425888d87b8829fd2502 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Tue, 11 Nov 2025 17:40:48 +0100 Subject: [PATCH 100/108] feat: enable option creation in TitleDropdown component --- src/pages/Video/components/TitleDropdownNew.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index daee7a923..0f568a81a 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -40,6 +40,7 @@ export const TitleDropdown = ({ <Autocomplete data-qa="video-title-dropdown" isEditable + isCreatable listboxAppendToNode={document.body} renderValue={({ selection }) => { if (!selection) return ''; From f6eb4dee76fe70c30327d61eb2ef36a62cb22109 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Wed, 12 Nov 2025 15:08:16 +0100 Subject: [PATCH 101/108] chore: update @appquality/unguess-design-system version --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 461f9a076..cce6e0b35 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@analytics/google-tag-manager": "^0.6.0", "@analytics/hubspot": "^0.5.1", "@appquality/languages": "1.4.3", - "@appquality/unguess-design-system": "4.0.52--canary.551.da98e7c.0", + "@appquality/unguess-design-system": "4.0.52--canary.551.a660c37.0", "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", diff --git a/yarn.lock b/yarn.lock index 80c1881fa..539233a41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,10 +122,10 @@ dependencies: hls.js "^1.4.8" -"@appquality/unguess-design-system@4.0.52--canary.551.da98e7c.0": - version "4.0.52--canary.551.da98e7c.0" - resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.52--canary.551.da98e7c.0.tgz#1360e2485ffcf84e37a96aa7f465dafbb1b13f35" - integrity sha512-i0Au2hE9QtlpvxvVIV4SZzFvw2KMKhghJGvLF3KgMSh1RGeOPyA2VmNkkDP8ZtXqwOsZ1eRg7SvBilDxrCUMYQ== +"@appquality/unguess-design-system@4.0.52--canary.551.a660c37.0": + version "4.0.52--canary.551.a660c37.0" + resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.52--canary.551.a660c37.0.tgz#29ab3931580a5bd8abdf401477808430559dda08" + integrity sha512-5OoCGFUimSsGbzgO3F4/gcMn7zNK2635kx1VLAIukxZTSrgN6blUkkFwMcTzgbLNCLajJIhQhvwZT8MG+Q6ZoA== dependencies: "@appquality/stream-player" "1.0.6" "@nivo/bar" "^0.87.0" From a9bd10eee5f6e0476a22b5494bdd039cd1411f28 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Thu, 13 Nov 2025 11:19:39 +0100 Subject: [PATCH 102/108] fix: remove focus on input element in handleAccordionChange function --- src/pages/Video/components/Observation.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/Video/components/Observation.tsx b/src/pages/Video/components/Observation.tsx index d78a45c66..1e48e1808 100644 --- a/src/pages/Video/components/Observation.tsx +++ b/src/pages/Video/components/Observation.tsx @@ -109,7 +109,6 @@ const Observation = ({ top: activeElement.offsetTop, behavior: 'smooth', }); - activeElement.querySelectorAll('input')[0]?.focus(); } setOpenAccordion(undefined); }, 100); From 1dbc501981d1187bc352b5a1e815bfb2f6bee0f6 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Thu, 13 Nov 2025 11:26:41 +0100 Subject: [PATCH 103/108] fix: adjust scroll position to account for header height in Observation component --- src/pages/Video/components/Observation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Video/components/Observation.tsx b/src/pages/Video/components/Observation.tsx index 1e48e1808..7d0bae4a3 100644 --- a/src/pages/Video/components/Observation.tsx +++ b/src/pages/Video/components/Observation.tsx @@ -106,7 +106,7 @@ const Observation = ({ ); if (activeElement) { refScroll.current.scrollTo({ - top: activeElement.offsetTop, + top: activeElement.offsetTop - 150, // account for header height behavior: 'smooth', }); } From 226f3ff1a0e5639258c54a2b7ac0153a9a90667e Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Fri, 14 Nov 2025 16:21:29 +0100 Subject: [PATCH 104/108] chore: update @appquality/unguess-design-system version --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index cce6e0b35..1a8aeaf8a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@analytics/google-tag-manager": "^0.6.0", "@analytics/hubspot": "^0.5.1", "@appquality/languages": "1.4.3", - "@appquality/unguess-design-system": "4.0.52--canary.551.a660c37.0", + "@appquality/unguess-design-system": "4.0.52", "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", diff --git a/yarn.lock b/yarn.lock index 539233a41..c5aad8cb2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,10 +122,10 @@ dependencies: hls.js "^1.4.8" -"@appquality/unguess-design-system@4.0.52--canary.551.a660c37.0": - version "4.0.52--canary.551.a660c37.0" - resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.52--canary.551.a660c37.0.tgz#29ab3931580a5bd8abdf401477808430559dda08" - integrity sha512-5OoCGFUimSsGbzgO3F4/gcMn7zNK2635kx1VLAIukxZTSrgN6blUkkFwMcTzgbLNCLajJIhQhvwZT8MG+Q6ZoA== +"@appquality/unguess-design-system@4.0.52": + version "4.0.52" + resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.52.tgz#833dd00a72d8d9abad4085de9a2fc948bbfd4841" + integrity sha512-zPp2aKapuwCaJdK1QRTDB/fGdiNjkCqyNHSV7VkeN6wfl/pJsoE2KGeIifcRbZvqx5jPMejWnW7Yl8XYRC1nMA== dependencies: "@appquality/stream-player" "1.0.6" "@nivo/bar" "^0.87.0" From b9c9a1c027f94fa6e30d0cbc3a8b877fc4a5a2cc Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Mon, 17 Nov 2025 10:18:26 +0100 Subject: [PATCH 105/108] feat: add edit icon to action items in ObservationForm and TitleDropdown components --- src/pages/Video/components/ObservationForm.tsx | 2 ++ src/pages/Video/components/TitleDropdownNew.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/pages/Video/components/ObservationForm.tsx b/src/pages/Video/components/ObservationForm.tsx index eff762335..887b6ddfd 100644 --- a/src/pages/Video/components/ObservationForm.tsx +++ b/src/pages/Video/components/ObservationForm.tsx @@ -12,6 +12,7 @@ import { Textarea, useToast, } from '@appquality/unguess-design-system'; +import { ReactComponent as EditIcon } from '@zendeskgarden/svg-icons/src/12/pencil-stroke.svg'; import { Form, Formik, FormikHelpers, FormikProps } from 'formik'; import { ComponentProps, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -161,6 +162,7 @@ const ObservationForm = ({ )} /> ), + actionIcon: <EditIcon />, })) ); } diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index 0f568a81a..79ce28bd9 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -2,6 +2,7 @@ import { Autocomplete, DropdownFieldNew as Field, } from '@appquality/unguess-design-system'; +import { ReactComponent as EditIcon } from '@zendeskgarden/svg-icons/src/12/pencil-stroke.svg'; import { FormikProps } from 'formik'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; @@ -103,6 +104,7 @@ export const TitleDropdown = ({ /> ), itemID: i.id.toString(), + actionIcon: <EditIcon />, }))} startIcon={<CopyIcon />} placeholder={t( From c218b06c067d6c98e81aae36bfbeadfbc86bd9d9 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Thu, 13 Nov 2025 15:44:52 +0100 Subject: [PATCH 106/108] feat: integrate analytics tracking for tag updates and dropdown interactions in EditTagModal and TitleDropdown components --- src/pages/Video/components/EditTagModal.tsx | 19 +++++ .../Video/components/ObservationForm.tsx | 18 +++++ .../Video/components/TitleDropdownNew.tsx | 69 ++++++++++++------- 3 files changed, 83 insertions(+), 23 deletions(-) diff --git a/src/pages/Video/components/EditTagModal.tsx b/src/pages/Video/components/EditTagModal.tsx index d0fe6d6bb..2d191b548 100644 --- a/src/pages/Video/components/EditTagModal.tsx +++ b/src/pages/Video/components/EditTagModal.tsx @@ -13,6 +13,7 @@ import { import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; +import analytics from 'src/analytics'; import { appTheme } from 'src/app/theme'; import { ReactComponent as SaveIcon } from 'src/assets/icons/save.svg'; import { @@ -26,6 +27,7 @@ interface EditModalProps { title: string; label: string; description: string; + type: 'theme' | 'extraTag'; } export const EditTagModal = ({ @@ -34,6 +36,7 @@ export const EditTagModal = ({ title, label, description, + type, }: EditModalProps) => { // Extract both the current title and the usage number in parentheses in two variables @@ -56,6 +59,13 @@ export const EditTagModal = ({ }, [newName]); const handleSubmit = async () => { + analytics.track('tagUpdateSubmitted', { + tagId: tag.id.toString(), + tagType: type, + associatedObservations: tag.usageNumber, + submissionTime: Date.now(), + }); + if (error) return; // Update the title in the form try { @@ -82,6 +92,15 @@ export const EditTagModal = ({ { placement: 'top' } ); } catch (err: any) { + analytics.track('tagUpdateFailed', { + tagId: tag.id.toString(), + tagType: type, + attemptedTagName: newName, + errorType: err.status === 409 ? 'duplicate' : 'other', + errorMessage: err.message, + associatedObservations: tag.usageNumber, + }); + // Handle error (e.g., show error toast) // if status code is 409, conflict with another already saved name, show specific error if (err.status === 409) { diff --git a/src/pages/Video/components/ObservationForm.tsx b/src/pages/Video/components/ObservationForm.tsx index 887b6ddfd..c93eab7c7 100644 --- a/src/pages/Video/components/ObservationForm.tsx +++ b/src/pages/Video/components/ObservationForm.tsx @@ -17,6 +17,7 @@ import { Form, Formik, FormikHelpers, FormikProps } from 'formik'; import { ComponentProps, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; +import analytics from 'src/analytics'; import { appTheme } from 'src/app/theme'; import { getColorWithAlpha } from 'src/common/utils'; import { @@ -149,6 +150,7 @@ const ObservationForm = ({ selected: selectedOptions.some((bt) => bt.id === tag.id), actions: ({ closeModal }) => ( <EditTagModal + type="extraTag" tag={tag} closeModal={closeModal} title={t('__VIDEO_PAGE_TAGS_DROPDOWN_EDIT_MODAL_TITLE')} @@ -163,6 +165,14 @@ const ObservationForm = ({ /> ), actionIcon: <EditIcon />, + onOptionActionClick: () => { + analytics.track('tagEditModalOpened', { + tagId: tag.id.toString(), + tagType: 'extraTag', + tagName: tag.name, + associatedObservations: tag.usageNumber, + }); + }, })) ); } @@ -381,6 +391,14 @@ const ObservationForm = ({ listboxAppendToNode={document.body} creatable maxItems={4} + onClick={() => + analytics.track('tagDropdownOpened', { + dropdownType: 'extraTags', + availableTagsCount: options.length, + selectedTagsCount: options.filter((o) => o.selected) + .length, + }) + } size="medium" i18n={{ placeholder: t( diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index 79ce28bd9..d60690be7 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -4,8 +4,10 @@ import { } from '@appquality/unguess-design-system'; import { ReactComponent as EditIcon } from '@zendeskgarden/svg-icons/src/12/pencil-stroke.svg'; import { FormikProps } from 'formik'; +import { ComponentProps, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; +import analytics from 'src/analytics'; import { ReactComponent as CopyIcon } from 'src/assets/icons/copy-icon.svg'; import { GetCampaignsByCidVideoTagsApiResponse, @@ -36,9 +38,47 @@ export const TitleDropdown = ({ return null; } + const options: ComponentProps<typeof Autocomplete>['options'] = useMemo( + () => + (titles || []).map((i) => ({ + id: i.id.toString(), + value: i.id.toString(), + label: `${i.name} (${i.usageNumber})`, + isSelected: formProps.values.title === i.id, + actions: ({ closeModal }) => ( + <EditTagModal + type="theme" + tag={i} + closeModal={closeModal} + title={t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE')} + label={t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL')} + description={t( + '__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION', + { + usageNumber: i.usageNumber, + count: Number(i.usageNumber), + } + )} + /> + ), + actionIcon: <EditIcon />, + itemID: i.id.toString(), + onOptionActionClick: () => { + analytics.track('tagEditModalOpened', { + tagId: i.id.toString(), + tagType: 'theme', + tagName: i.name, + associatedObservations: i.usageNumber, + }); + }, + })), + [titles, formProps.values.title] + ); + return ( <Field> <Autocomplete + options={options} data-qa="video-title-dropdown" isEditable isCreatable @@ -49,6 +89,12 @@ export const TitleDropdown = ({ const title = titles.find((i) => i.id === Number(selection.value)); return title?.name || ''; }} + onClick={() => + analytics.track('tagDropdownOpened', { + dropdownType: 'theme', + availableTagsCount: options.length, + }) + } selectionValue={formProps.values.title.toString()} onCreateNewOption={async (value) => { if (value.length > titleMaxLength) { @@ -83,29 +129,6 @@ export const TitleDropdown = ({ if (!selectionValue || !inputValue) return; formProps.setFieldValue('title', Number(selectionValue)); }} - options={(titles || []).map((i) => ({ - id: i.id.toString(), - value: i.id.toString(), - label: `${i.name} (${i.usageNumber})`, - isSelected: formProps.values.title === i.id, - actions: ({ closeModal }) => ( - <EditTagModal - tag={i} - closeModal={closeModal} - title={t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_TITLE')} - label={t('__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_LABEL')} - description={t( - '__VIDEO_PAGE_THEMES_DROPDOWN_EDIT_MODAL_DESCRIPTION', - { - usageNumber: i.usageNumber, - count: Number(i.usageNumber), - } - )} - /> - ), - itemID: i.id.toString(), - actionIcon: <EditIcon />, - }))} startIcon={<CopyIcon />} placeholder={t( '__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_FIELD_TITLE_PLACEHOLDER' From 10d01987b9657d6e4d31ee38554a37c541e9e1cb Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Fri, 14 Nov 2025 15:13:40 +0100 Subject: [PATCH 107/108] fix: move null check for titles to fix hooks error order --- src/pages/Video/components/TitleDropdownNew.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index d60690be7..966c49b12 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -33,11 +33,6 @@ export const TitleDropdown = ({ const { campaignId } = useParams(); const [addVideoTags] = usePostCampaignsByCidVideoTagsMutation(); const titleMaxLength = 70; - - if (!titles) { - return null; - } - const options: ComponentProps<typeof Autocomplete>['options'] = useMemo( () => (titles || []).map((i) => ({ @@ -75,6 +70,10 @@ export const TitleDropdown = ({ [titles, formProps.values.title] ); + if (!titles) { + return null; + } + return ( <Field> <Autocomplete From 6a689b6dcac665dfc1a396537847b4a0ed0f1621 Mon Sep 17 00:00:00 2001 From: iacopolea <iacopo.lea@gmail.com> Date: Mon, 17 Nov 2025 15:55:51 +0100 Subject: [PATCH 108/108] fix: change autocomplete inputvalue logic from label to children to address new des-sys version --- package.json | 2 +- src/pages/Video/components/TitleDropdownNew.tsx | 3 ++- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 1a8aeaf8a..349b4d797 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@analytics/google-tag-manager": "^0.6.0", "@analytics/hubspot": "^0.5.1", "@appquality/languages": "1.4.3", - "@appquality/unguess-design-system": "4.0.52", + "@appquality/unguess-design-system": "4.0.53", "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@atlaskit/pragmatic-drag-and-drop-flourish": "^2.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", diff --git a/src/pages/Video/components/TitleDropdownNew.tsx b/src/pages/Video/components/TitleDropdownNew.tsx index 966c49b12..d30615a33 100644 --- a/src/pages/Video/components/TitleDropdownNew.tsx +++ b/src/pages/Video/components/TitleDropdownNew.tsx @@ -38,7 +38,8 @@ export const TitleDropdown = ({ (titles || []).map((i) => ({ id: i.id.toString(), value: i.id.toString(), - label: `${i.name} (${i.usageNumber})`, + children: `${i.name} (${i.usageNumber})`, + label: i.name, isSelected: formProps.values.title === i.id, actions: ({ closeModal }) => ( <EditTagModal diff --git a/yarn.lock b/yarn.lock index c5aad8cb2..1deb9c090 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,10 +122,10 @@ dependencies: hls.js "^1.4.8" -"@appquality/unguess-design-system@4.0.52": - version "4.0.52" - resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.52.tgz#833dd00a72d8d9abad4085de9a2fc948bbfd4841" - integrity sha512-zPp2aKapuwCaJdK1QRTDB/fGdiNjkCqyNHSV7VkeN6wfl/pJsoE2KGeIifcRbZvqx5jPMejWnW7Yl8XYRC1nMA== +"@appquality/unguess-design-system@4.0.53": + version "4.0.53" + resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.53.tgz#d0b78c82ad3e3f9f0b7e0aee64aad6abc22bec4f" + integrity sha512-ZbsSBZR9AUY7DRZS/kApozaXVbOEArE6Io99/mpCStLiUKJcSdQoRBR2wQ36txZKXrVQ2CdmoyuJNTwT+Kbfkw== dependencies: "@appquality/stream-player" "1.0.6" "@nivo/bar" "^0.87.0"