diff --git a/package.json b/package.json index f5700e080..35f998665 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@appquality/languages": "1.4.3", - "@appquality/unguess-design-system": "4.0.35", + "@appquality/unguess-design-system": "4.0.36", "@headwayapp/react-widget": "^0.0.4", "@reduxjs/toolkit": "^1.8.0", "@sentry/react": "^8.32.0", diff --git a/src/assets/icons/info.svg b/src/assets/icons/info.svg new file mode 100644 index 000000000..b4798c63e --- /dev/null +++ b/src/assets/icons/info.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/components/inviteUsers/modalFooter.tsx b/src/common/components/inviteUsers/modalFooter.tsx index f52752075..7a38e4654 100644 --- a/src/common/components/inviteUsers/modalFooter.tsx +++ b/src/common/components/inviteUsers/modalFooter.tsx @@ -1,8 +1,8 @@ -import { Modal, Button } from '@appquality/unguess-design-system'; +import { Button, Modal } from '@appquality/unguess-design-system'; +import { useTranslation } from 'react-i18next'; import { ReactComponent as LinkIcon } from 'src/assets/icons/link-stroke.svg'; +import { useCopyLink } from 'src/hooks/useCopyLink'; import styled from 'styled-components'; -import { useTranslation } from 'react-i18next'; -import { useCopyLink } from '../utils/useCopyLink'; const FooterWithBorder = styled(Modal.Footer)` padding: ${({ theme }) => diff --git a/src/common/components/navigation/asideNav/index.tsx b/src/common/components/navigation/asideNav/index.tsx index 1dbc71380..87762928f 100644 --- a/src/common/components/navigation/asideNav/index.tsx +++ b/src/common/components/navigation/asideNav/index.tsx @@ -27,8 +27,8 @@ export const StickyNavContainer = styled(ContainerCard)` `; export const StyledDivider = styled(Divider)` - margin-top: ${({ theme }) => theme.space.base * 6}px; - margin-bottom: ${({ theme }) => theme.space.base * 6}px; + margin-top: ${({ theme }) => theme.space.base * 4}px; + margin-bottom: ${({ theme }) => theme.space.base * 4}px; `; export const StickyNavItem = styled(Link)` diff --git a/src/common/components/utils/useCopyLink.tsx b/src/common/components/utils/useCopyLink.tsx deleted file mode 100644 index e5aedcbd7..000000000 --- a/src/common/components/utils/useCopyLink.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useToast, Notification } from '@appquality/unguess-design-system'; -import { t } from 'i18next'; - -export const useCopyLink = () => { - const { addToast } = useToast(); - - const copyLinkToClipboard = () => { - const url = new URL(window.location.href); - navigator.clipboard.writeText(`${url.origin}${url.pathname}`); - addToast( - ({ close }) => ( - - ), - { placement: 'top' } - ); - }; - - return copyLinkToClipboard; -}; diff --git a/src/features/api/index.ts b/src/features/api/index.ts index d0d567c71..20ac6e677 100644 --- a/src/features/api/index.ts +++ b/src/features/api/index.ts @@ -2280,6 +2280,16 @@ export type Transcript = { speakers: number; paragraphs: Paragraph[]; }; +export type MediaSentiment = { + value: number; + reason: string; + paragraphs: { + start: number; + end: number; + value: number; + reason: string; + }[]; +}; export type Video = { id: number; url: string; @@ -2295,6 +2305,7 @@ export type Video = { }; }; transcript?: Transcript; + sentiment?: MediaSentiment; }; export type PaginationData = { start?: number; diff --git a/src/features/navigation/Navigation.tsx b/src/features/navigation/Navigation.tsx index 674343745..f695b3f08 100644 --- a/src/features/navigation/Navigation.tsx +++ b/src/features/navigation/Navigation.tsx @@ -104,9 +104,6 @@ export const Navigation = ({ case 'insights': dispatch(setSidebarOpen(false)); break; - case 'templates': - dispatch(setSidebarOpen(false)); - break; case 'template': dispatch(setSidebarOpen(false)); break; diff --git a/src/hooks/useCopy.tsx b/src/hooks/useCopy.tsx new file mode 100644 index 000000000..38c1f0040 --- /dev/null +++ b/src/hooks/useCopy.tsx @@ -0,0 +1,46 @@ +import { Notification, useToast } from '@appquality/unguess-design-system'; +import { useTranslation } from 'react-i18next'; + +export const useCopy = ({ + text, + notification, +}: { + text: string; + notification?: string; +}) => { + const { t } = useTranslation(); + const { addToast } = useToast(); + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(text); + addToast( + ({ close }) => ( + + ), + { placement: 'top' } + ); + } catch (error) { + addToast( + ({ close }) => ( + + ), + { placement: 'top' } + ); + } + }; + + return copyToClipboard; +}; diff --git a/src/hooks/useCopyLink.tsx b/src/hooks/useCopyLink.tsx new file mode 100644 index 000000000..574722bbf --- /dev/null +++ b/src/hooks/useCopyLink.tsx @@ -0,0 +1,13 @@ +import { useTranslation } from 'react-i18next'; +import { useCopy } from './useCopy'; + +export const useCopyLink = () => { + const { t } = useTranslation(); + const url = new URL(window.location.href); + const text = `${url.origin}${url.pathname}`; + const copyLinkToClipboard = useCopy({ + text, + notification: t('__PERMISSION_SETTINGS_TOAST_COPY_MESSAGE'), + }); + return copyLinkToClipboard; +}; diff --git a/src/hooks/useLocalizeDashboardUrl.ts b/src/hooks/useLocalizeDashboardUrl.ts index 85ff09e3f..85d66d44b 100644 --- a/src/hooks/useLocalizeDashboardUrl.ts +++ b/src/hooks/useLocalizeDashboardUrl.ts @@ -88,3 +88,13 @@ export const getLocalizedCampaignUrl = ( ? `${protocol}//${host}/campaigns/${aCampaignId}` : `${protocol}//${host}/it/campaigns/${aCampaignId}`; }; + +export const getLocalizedPlanUrl = ( + aPlanId: number, + aLanguage: string +): string => { + const { host, protocol } = window.location; + return aLanguage === 'en' + ? `${protocol}//${host}/plans/${aPlanId}` + : `${protocol}//${host}/it/plans/${aPlanId}`; +}; diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index ead3e2e90..e9c5b7428 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -23,6 +23,23 @@ "__APP_SIDEBAR_SHARED_WORKSPACE_HOME_ITEM_LABEL": "Activities", "__APP_SIDEBAR_SHARED_WORKSPACE_LABEL": "Shared with me", "__APP_SIDEBAR_TEMPLATES_ITEM_LABEL": "Templates", + "__ARCHIVE_PAGE_TOTAL_CAMPAIGN_TITLE": "All", + "__ASIDE_NAVIGATION_MODULE_ADDITIONAL_TARGET_SUBTITLE": " ", + "__ASIDE_NAVIGATION_MODULE_AGE_SUBTITLE": "Participant age groups", + "__ASIDE_NAVIGATION_MODULE_BROWSER_SUBTITLE": "Compatible browsers", + "__ASIDE_NAVIGATION_MODULE_DATES_BLOCK_SUBTITLE": " ", + "__ASIDE_NAVIGATION_MODULE_DIGITAL_LITERACY_ACCORDION_SUBTITLE": "Participant technical proficiency", + "__ASIDE_NAVIGATION_MODULE_GENDER_ACCORDION_SUBTITLE": "Participant gender criteria", + "__ASIDE_NAVIGATION_MODULE_GOAL_SUBTITLE": "Activity objective", + "__ASIDE_NAVIGATION_MODULE_INSTRUCTIONS_NOTE_BLOCK_SUBTITLE": " ", + "__ASIDE_NAVIGATION_MODULE_LANGUAGE_SUBTITLE": "Feedback language", + "__ASIDE_NAVIGATION_MODULE_OUT_OF_SCOPE_SUBTITLE": " ", + "__ASIDE_NAVIGATION_MODULE_SETUP_NOTE_BLOCK_SUBTITLE": " ", + "__ASIDE_NAVIGATION_MODULE_SUBTITLE_BLOCK_SUBTITLE": " ", + "__ASIDE_NAVIGATION_MODULE_TARGET_NOTE_BLOCK_SUBTITLE": " ", + "__ASIDE_NAVIGATION_MODULE_TARGET_SUBTITLE": "Participant number", + "__ASIDE_NAVIGATION_MODULE_TASKS_SUBTITLE": " ", + "__ASIDE_NAVIGATION_MODULE_TOUCHPOINTS_SUBTITLE": "Interaction points", "__BANNER_CROSS_FUNCTIONAL_CTA_AUTOMATION": "Get in touch", "__BANNER_CROSS_FUNCTIONAL_CTA_EXPERIENCE": "Get in touch", "__BANNER_CROSS_FUNCTIONAL_CTA_LOADING": "sending...", @@ -410,6 +427,7 @@ "__CAMPAIGN_PAGE_NAVIGATION_MEDIA_ITEM_INSIGHTS_LABEL": "Key points", "__CAMPAIGN_PAGE_NAVIGATION_MEDIA_ITEM_METHODOLOGY_LABEL": "Activity", "__CAMPAIGN_PAGE_NAVIGATION_MEDIA_ITEM_OVERVIEW_LABEL": "Overview", + "__CAMPAIGN_PAGE_NAVIGATION_PLAN_EXTERNAL_LINK_LABEL": "Go to Activity Setup", "__CAMPAIGN_PAGE_REPORTS_CARDS_DOWNLOAD_LABEL": "Download now", "__CAMPAIGN_PAGE_REPORTS_CARDS_OPEN_LINK_LABEL": "Open link", "__CAMPAIGN_PAGE_REPORTS_CARDS_UPDATED_ON_LABEL": "Last edit", @@ -550,6 +568,8 @@ "__CATALOG_PAGE_TITLE": "Services", "__CATALOG_STICKY_CONTAINER_NAV_CATEGORIES_LABEL": "BY CATEGORY", "__CATALOG_STICKY_CONTAINER_NAV_EXTERNAL_LINK_LABEL": "Discover services", + "__COPY_ERROR": "Failed to copy text to clipboard", + "__COPY_SUCCESS": "Text copied to clipboard", "__DASHABOARD_CAMPAIGN_CAMPAIGN_TYPE_FILTER_LABEL Max:10": "Type", "__DASHABOARD_CAMPAIGN_STATUS_FILTER_ALL": "All", "__DASHABOARD_CAMPAIGN_STATUS_FILTER_COMPLETED": "Completed", @@ -952,6 +972,7 @@ "__PAGE_TITLE_CATALOG": "Services", "__PAGE_TITLE_LOGIN": "Log in", "__PAGE_TITLE_PRIMARY_DASHBOARD": "My activities", + "__PAGE_TITLE_PRIMARY_DASHBOARD_ARCHIVE": "Archive", "__PAGE_TITLE_PRIMARY_DASHBOARD_SINGLE_PROJECT": "Projects", "__PAGE_TITLE_TEMPLATES": "Templates", "__PAGE_VIDEOS_EMPTY_STATE": "The videos for this project have not been uploaded yet.", @@ -978,6 +999,7 @@ "__PLAN_CREATION_PROJECT_DROPDOWN_PLACEHOLDER": "Select a project", "__PLAN_DATE_ERROR_REQUIRED": "Required field: enter a date to continue", "__PLAN_DATE_IN_FUTURE_ERROR": "Date must be at least one business day in the future", + "__PLAN_DELETE_PLAN_CTA": "Delete draft", "__PLAN_GOAL_SIZE_ERROR_REQUIRED": "Required field: enter a goal to continue", "__PLAN_GOAL_SIZE_ERROR_TOO_LONG": "Character limit exceeded: Please reduce your text to 256 characters", "__PLAN_INSTRUCTION_NOTE_SIZE_ERROR_EMPTY": "Required field: enter a text to continue", @@ -987,6 +1009,11 @@ "__PLAN_PAGE_ADD_MODULE_BLOCK_BUTTON": "Add item", "__PLAN_PAGE_ADD_MODULE_BLOCK_MODAL_SUBTITLE": "Provide the necessary details to describe this activity", "__PLAN_PAGE_ADD_MODULE_BLOCK_MODAL_TITLE": "Available informations", + "__PLAN_PAGE_DELETE_PLAN_MODAL_BODY": "You're about to delete \"{{planTitle}}\".

This action will permanently remove all information you've entered.
After deletion, you'll be redirected to your Activities dashboard.
", + "__PLAN_PAGE_DELETE_PLAN_MODAL_BUTTON_CANCEL": "Keep editing", + "__PLAN_PAGE_DELETE_PLAN_MODAL_BUTTON_CONFIRM": "Delete permanently", + "__PLAN_PAGE_DELETE_PLAN_MODAL_ERROR": "We couldn't delete your draft: please try again later.", + "__PLAN_PAGE_DELETE_PLAN_MODAL_TITLE": "Delete this draft?", "__PLAN_PAGE_HEADER_BREADCRUMBS_INSTRUCTIONS_TAB": "Assign Tasks", "__PLAN_PAGE_HEADER_BREADCRUMBS_SETUP_TAB": "Set up", "__PLAN_PAGE_HEADER_BREADCRUMBS_SUMMARY_TAB": "Get expert feedback", @@ -1160,7 +1187,7 @@ "__PLAN_PAGE_MODULE_TOUCHPOINTS_REMOVE_TOUCHPOINT_BUTTON": "Remove", "__PLAN_PAGE_MODULE_TOUCHPOINTS_SUBTITLE": "Add touchpoints & devices for the test", "__PLAN_PAGE_MODULE_TOUCHPOINTS_TITLE": "Touchpoints & Devices", - "__PLAN_PAGE_MODULE_TOUCHPOINTS_TOUCHPOINT_APP_LINK_ANDROID_DESCRIPTION": "Add Google Play Store or Test Flight link for activity partecipants", + "__PLAN_PAGE_MODULE_TOUCHPOINTS_TOUCHPOINT_APP_LINK_ANDROID_DESCRIPTION": "Add Play Store or beta testing link for the test", "__PLAN_PAGE_MODULE_TOUCHPOINTS_TOUCHPOINT_APP_LINK_ANDROID_HINT": "Add accessible URL with http:// or https:// prefix", "__PLAN_PAGE_MODULE_TOUCHPOINTS_TOUCHPOINT_APP_LINK_ANDROID_LABEL": "Product link", "__PLAN_PAGE_MODULE_TOUCHPOINTS_TOUCHPOINT_APP_LINK_ANDROID_PLACEHOLDER": "https://example.com", @@ -1245,9 +1272,9 @@ "__PROFILE_MODAL_LANGUAGES_TITLE": "Change Language", "__PROFILE_MODAL_LOGOUT_TITLE": "Log Out", "__PROFILE_MODAL_NOTIFICATIONS_INTRO": "Manage the notifications we send you by email.", - "__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_1": "By turning on notifications, you will be updated on: ", - "__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_2": "comments, mentions, activity starting, activity ending, and invitations.", - "__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_3": "By turning off notifications, you will be only updated on mentions and invitations", + "__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_1": " ", + "__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_2": "Enable to receive all updates: comments, mentions, quotes on the platform, confirmations, activity timelines, and invitations.", + "__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_3": "Disable to receive only essentials: mentions, quotes on the platform, activity confirmations, and invitations.", "__PROFILE_MODAL_NOTIFICATIONS_TITLE": "Notifications settings", "__PROFILE_MODAL_NOTIFICATIONS_TOGGLE_OFF": "No", "__PROFILE_MODAL_NOTIFICATIONS_TOGGLE_ON": "Yes", @@ -1269,6 +1296,13 @@ "__PUBLIC_MANUAL_HELP_MODAL_TITLE": "Got questions?", "__PUBLIC_MANUAL_NOT_FOUND_TEXT": "This page does not exist yet. Ask your CSM or explore our Support Center.", "__PUBLIC_MANUAL_NOT_FOUND_TITLE": "There is no manual here", + "__SENTIMENT_COPY_BUTTON_LABEL": "Copy", + "__SENTIMENT_OVERVIEW_ALERT_DISCLAIMER": "AI helps you explore. Just remember to verify what you find.", + "__SENTIMENT_OVERVIEW_ALERT_SUBTITLE": "See how your user felt paragraph by paragraph", + "__SENTIMENT_OVERVIEW_ALERT_TITLE": "Explore sentiment journey:", + "__SENTIMENT_OVERVIEW_SUBTITLE": "Generated from transcript", + "__SENTIMENT_OVERVIEW_TITLE": "Experience Summary", + "__SENTIMENT_TOAST_COPY_MESSAGE": "Sentiment copied to clipboard", "__SERVICE_DETAIL_PAGE_TAG_RESULTS_DAYS_LABEL": "First results in <0>{{hours}}h", "__SERVICE_TILES_HEADER": "Explore new ways of testing", "__SERVICE_TILES_SUBTITLE": "Launch lean tests autonomosly, get expert-verified results", @@ -1296,7 +1330,9 @@ "__TOAST_CLOSE_TEXT": "Dismiss", "__TOAST_GENERIC_ERROR_MESSAGE": "Something went wrong. Please try again later.", "__TOOLS_MENU_ITEM_BUTTON_LABEL": "Translate", + "__TOOLS_MENU_ITEM_HIDE_SENTIMENT_LABEL": "Hide Sentiment", "__TOOLS_MENU_ITEM_LANGUAGE_SETTINGS_TOOLTIP": "Language settings", + "__TOOLS_MENU_ITEM_SHOW_SENTIMENT_LABEL": "Show Sentiment", "__TOOLS_MENU_ITEM_TRANSLATE_PREFERENCE_TITLE": "Translate in", "__TOOLS_TRANSLATE_BUTTON_CANCEL": "Cancel", "__TOOLS_TRANSLATE_BUTTON_SEND": "Translate", @@ -1310,6 +1346,11 @@ "__TOOLS_TRANSLATE_TOAST_LANGUAGE_ERROR_MESSAGE": "Something went wrong during the preferred language update, try again", "__TOOLS_TRANSLATE_TOAST_LANGUAGE_SUCCESS_MESSAGE": "Preferred language updated", "__TOOLS_TRANSLATE_TOGGLE_TEXT": "Set as preferred language", + "__TRANSCRIPT_SENTIMENT_VALUE_NEGATIVE": "Negative", + "__TRANSCRIPT_SENTIMENT_VALUE_NEUTRAL": "Neutral", + "__TRANSCRIPT_SENTIMENT_VALUE_POSITIVE": "Positive", + "__TRANSCRIPT_SENTIMENT_VALUE_VERY_NEGATIVE": "Very Negative", + "__TRANSCRIPT_SENTIMENT_VALUE_VERY_POSITIVE": "Very Positive", "__UX_CAMPAIGN_PAGE_NAVIGATION_DASHBOARD_TOOLTIP": "Dashboard", "__UX_CAMPAIGN_PAGE_NAVIGATION_INSIGHTS_TOOLTIP": "Topics & insights", "__UX_CAMPAIGN_PAGE_NAVIGATION_VIDEO_LIST_TOOLTIP": "Playlist", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 3f60a44fd..8c1c448bd 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -23,6 +23,23 @@ "__APP_SIDEBAR_SHARED_WORKSPACE_HOME_ITEM_LABEL": "Campagne", "__APP_SIDEBAR_SHARED_WORKSPACE_LABEL": "Condivisi con me", "__APP_SIDEBAR_TEMPLATES_ITEM_LABEL": "", + "__ARCHIVE_PAGE_TOTAL_CAMPAIGN_TITLE": "", + "__ASIDE_NAVIGATION_MODULE_ADDITIONAL_TARGET_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_AGE_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_BROWSER_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_DATES_BLOCK_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_DIGITAL_LITERACY_ACCORDION_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_GENDER_ACCORDION_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_GOAL_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_INSTRUCTIONS_NOTE_BLOCK_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_LANGUAGE_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_OUT_OF_SCOPE_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_SETUP_NOTE_BLOCK_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_SUBTITLE_BLOCK_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_TARGET_NOTE_BLOCK_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_TARGET_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_TASKS_SUBTITLE": "", + "__ASIDE_NAVIGATION_MODULE_TOUCHPOINTS_SUBTITLE": "", "__BANNER_CROSS_FUNCTIONAL_CTA_AUTOMATION": "Contattaci", "__BANNER_CROSS_FUNCTIONAL_CTA_EXPERIENCE": "Contattaci", "__BANNER_CROSS_FUNCTIONAL_CTA_LOADING": "invio in corso...", @@ -432,6 +449,7 @@ "__CAMPAIGN_PAGE_NAVIGATION_MEDIA_ITEM_INSIGHTS_LABEL": "Punti principali", "__CAMPAIGN_PAGE_NAVIGATION_MEDIA_ITEM_METHODOLOGY_LABEL": "Sulla campagna", "__CAMPAIGN_PAGE_NAVIGATION_MEDIA_ITEM_OVERVIEW_LABEL": "Panoramica", + "__CAMPAIGN_PAGE_NAVIGATION_PLAN_EXTERNAL_LINK_LABEL": "", "__CAMPAIGN_PAGE_REPORTS_CARDS_DOWNLOAD_LABEL": "Scarica ora", "__CAMPAIGN_PAGE_REPORTS_CARDS_OPEN_LINK_LABEL": "Apri link", "__CAMPAIGN_PAGE_REPORTS_CARDS_UPDATED_ON_LABEL": "Ultima modifica", @@ -576,6 +594,8 @@ "__CATALOG_PAGE_TITLE": "Servizi", "__CATALOG_STICKY_CONTAINER_NAV_CATEGORIES_LABEL": "PER TIPOLOGIA", "__CATALOG_STICKY_CONTAINER_NAV_EXTERNAL_LINK_LABEL": "Scopri tutti i servizi", + "__COPY_ERROR": "", + "__COPY_SUCCESS": "", "__DASHABOARD_CAMPAIGN_CAMPAIGN_TYPE_FILTER_LABEL Max:10": "Tipo", "__DASHABOARD_CAMPAIGN_STATUS_FILTER_ALL": "Tutte", "__DASHABOARD_CAMPAIGN_STATUS_FILTER_COMPLETED": "Completate", @@ -981,6 +1001,7 @@ "__PAGE_TITLE_CATALOG": "Servizi", "__PAGE_TITLE_LOGIN": "Accedi", "__PAGE_TITLE_PRIMARY_DASHBOARD": "Le mie campagne", + "__PAGE_TITLE_PRIMARY_DASHBOARD_ARCHIVE": "", "__PAGE_TITLE_PRIMARY_DASHBOARD_SINGLE_PROJECT": "Progetti", "__PAGE_TITLE_TEMPLATES": "", "__PAGE_VIDEOS_EMPTY_STATE": "I video per questo progetto non sono ancora stati caricati.", @@ -1007,6 +1028,7 @@ "__PLAN_CREATION_PROJECT_DROPDOWN_PLACEHOLDER": "", "__PLAN_DATE_ERROR_REQUIRED": "", "__PLAN_DATE_IN_FUTURE_ERROR": "", + "__PLAN_DELETE_PLAN_CTA": "", "__PLAN_GOAL_SIZE_ERROR_REQUIRED": "", "__PLAN_GOAL_SIZE_ERROR_TOO_LONG": "", "__PLAN_INSTRUCTION_NOTE_SIZE_ERROR_EMPTY": "", @@ -1016,6 +1038,11 @@ "__PLAN_PAGE_ADD_MODULE_BLOCK_BUTTON": "", "__PLAN_PAGE_ADD_MODULE_BLOCK_MODAL_SUBTITLE": "", "__PLAN_PAGE_ADD_MODULE_BLOCK_MODAL_TITLE": "", + "__PLAN_PAGE_DELETE_PLAN_MODAL_BODY": "", + "__PLAN_PAGE_DELETE_PLAN_MODAL_BUTTON_CANCEL": "", + "__PLAN_PAGE_DELETE_PLAN_MODAL_BUTTON_CONFIRM": "", + "__PLAN_PAGE_DELETE_PLAN_MODAL_ERROR": "", + "__PLAN_PAGE_DELETE_PLAN_MODAL_TITLE": "", "__PLAN_PAGE_HEADER_BREADCRUMBS_INSTRUCTIONS_TAB": "", "__PLAN_PAGE_HEADER_BREADCRUMBS_SETUP_TAB": "", "__PLAN_PAGE_HEADER_BREADCRUMBS_SUMMARY_TAB": "", @@ -1274,9 +1301,9 @@ "__PROFILE_MODAL_LANGUAGES_TITLE": "Cambia Lingua", "__PROFILE_MODAL_LOGOUT_TITLE": "Esci", "__PROFILE_MODAL_NOTIFICATIONS_INTRO": "Gestisci le notifiche che ti mandiamo per email.", - "__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_1": "Con le notifiche attive, ti aggiorneremo su: ", - "__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_2": "un commento, l’inizio di una campagna, la fine di una campagna, una menzione e un invito.", - "__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_3": "Se disattivi le notifiche, riceverai solo comunicazioni su menzioni e inviti", + "__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_1": "", + "__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_2": "", + "__PROFILE_MODAL_NOTIFICATIONS_OUTRO_P_3": "", "__PROFILE_MODAL_NOTIFICATIONS_TITLE": "Gestisci notifiche", "__PROFILE_MODAL_NOTIFICATIONS_TOGGLE_OFF": "No", "__PROFILE_MODAL_NOTIFICATIONS_TOGGLE_ON": "Si", @@ -1298,6 +1325,13 @@ "__PUBLIC_MANUAL_HELP_MODAL_TITLE": "I nostri Articoli", "__PUBLIC_MANUAL_NOT_FOUND_TEXT": "A quanto pare la pagina che stai cercando non esiste ancora.", "__PUBLIC_MANUAL_NOT_FOUND_TITLE": "Manuale non trovato", + "__SENTIMENT_COPY_BUTTON_LABEL": "", + "__SENTIMENT_OVERVIEW_ALERT_DISCLAIMER": "", + "__SENTIMENT_OVERVIEW_ALERT_SUBTITLE": "", + "__SENTIMENT_OVERVIEW_ALERT_TITLE": "", + "__SENTIMENT_OVERVIEW_SUBTITLE": "", + "__SENTIMENT_OVERVIEW_TITLE": "", + "__SENTIMENT_TOAST_COPY_MESSAGE": "", "__SERVICE_DETAIL_PAGE_TAG_RESULTS_DAYS_LABEL": "Primi risultati in <0>{{hours}}h", "__SERVICE_TILES_HEADER": "", "__SERVICE_TILES_SUBTITLE": "", @@ -1326,7 +1360,9 @@ "__TOAST_CLOSE_TEXT": "Chiudi", "__TOAST_GENERIC_ERROR_MESSAGE": "Qualcosa è andato storto, riprova più tardi.", "__TOOLS_MENU_ITEM_BUTTON_LABEL": "Traduci", + "__TOOLS_MENU_ITEM_HIDE_SENTIMENT_LABEL": "", "__TOOLS_MENU_ITEM_LANGUAGE_SETTINGS_TOOLTIP": "Impostazioni lingua", + "__TOOLS_MENU_ITEM_SHOW_SENTIMENT_LABEL": "", "__TOOLS_MENU_ITEM_TRANSLATE_PREFERENCE_TITLE": "Traduci in", "__TOOLS_TRANSLATE_BUTTON_CANCEL": "Annulla", "__TOOLS_TRANSLATE_BUTTON_SEND": "Traduci", @@ -1340,6 +1376,11 @@ "__TOOLS_TRANSLATE_TOAST_LANGUAGE_ERROR_MESSAGE": "Qualcosa è andato storto durante l'aggiornamento della lingua preferita, riprova", "__TOOLS_TRANSLATE_TOAST_LANGUAGE_SUCCESS_MESSAGE": "Lingua preferita aggiornata", "__TOOLS_TRANSLATE_TOGGLE_TEXT": "Imposta come lingua preferita", + "__TRANSCRIPT_SENTIMENT_VALUE_NEGATIVE": "", + "__TRANSCRIPT_SENTIMENT_VALUE_NEUTRAL": "", + "__TRANSCRIPT_SENTIMENT_VALUE_POSITIVE": "", + "__TRANSCRIPT_SENTIMENT_VALUE_VERY_NEGATIVE": "", + "__TRANSCRIPT_SENTIMENT_VALUE_VERY_POSITIVE": "", "__UX_CAMPAIGN_PAGE_NAVIGATION_DASHBOARD_TOOLTIP": "Dashboard", "__UX_CAMPAIGN_PAGE_NAVIGATION_INSIGHTS_TOOLTIP": "Temi e scoperte", "__UX_CAMPAIGN_PAGE_NAVIGATION_VIDEO_LIST_TOOLTIP": "Lista Video", diff --git a/src/pages/Bug/Header/ActionsMenu.tsx b/src/pages/Bug/Header/ActionsMenu.tsx index fb9e7ebb8..07a3eed8e 100644 --- a/src/pages/Bug/Header/ActionsMenu.tsx +++ b/src/pages/Bug/Header/ActionsMenu.tsx @@ -2,12 +2,12 @@ import { DotsMenu } from '@appquality/unguess-design-system'; import { t } from 'i18next'; import { useState } from 'react'; import { appTheme } from 'src/app/theme'; +import { ReactComponent as CopyIcon } from 'src/assets/icons/copy-icon.svg'; import { ReactComponent as KeyboardIcon } from 'src/assets/icons/keyboard.svg'; import { ReactComponent as ShareIcon } from 'src/assets/icons/share-stroke.svg'; -import { ReactComponent as CopyIcon } from 'src/assets/icons/copy-icon.svg'; -import { useCopyLink } from 'src/common/components/utils/useCopyLink'; import { ShareModal } from 'src/common/components/BugDetail/ShareModal'; import { GetCampaignsByCidBugsAndBidApiResponse } from 'src/features/api'; +import { useCopyLink } from 'src/hooks/useCopyLink'; import { useSendGTMevent } from 'src/hooks/useGTMevent'; import { BugShortcutHelper } from '../BugShortcutHelper'; diff --git a/src/pages/Bugs/Content/BugsTable/components/SingleGroupAccordion.tsx b/src/pages/Bugs/Content/BugsTable/components/SingleGroupAccordion.tsx index 15519b405..812565825 100644 --- a/src/pages/Bugs/Content/BugsTable/components/SingleGroupAccordion.tsx +++ b/src/pages/Bugs/Content/BugsTable/components/SingleGroupAccordion.tsx @@ -97,7 +97,7 @@ const SingleGroupAccordion = ({ style={{ marginRight: appTheme.space.xs }} /> {t('__BUGS_PAGE_TABLE_SEE_ALL', 'see all')} - {` (${item.bugs.length})`} + {` (${item.bugs.length - 3})`} ) : ( <> diff --git a/src/pages/Campaign/ExternalLink.tsx b/src/pages/Campaign/ExternalLink.tsx index 250f1e187..dbcfa73b3 100644 --- a/src/pages/Campaign/ExternalLink.tsx +++ b/src/pages/Campaign/ExternalLink.tsx @@ -3,7 +3,6 @@ import styled from 'styled-components'; const StyledAnchor = styled(Anchor)` display: block; - margin-bottom: ${({ theme }) => theme.space.sm}; `; export const ExternalLink = ({ diff --git a/src/pages/Campaign/useWidgets/Functional/widgets.tsx b/src/pages/Campaign/useWidgets/Functional/widgets.tsx index 9067281e6..665d160d3 100644 --- a/src/pages/Campaign/useWidgets/Functional/widgets.tsx +++ b/src/pages/Campaign/useWidgets/Functional/widgets.tsx @@ -1,11 +1,21 @@ import { useTranslation } from 'react-i18next'; import { useGetCampaignsByCidQuery } from 'src/features/api'; -import { getLocalizedFunctionalDashboardUrl } from 'src/hooks/useLocalizeDashboardUrl'; +import { + getLocalizedFunctionalDashboardUrl, + getLocalizedPlanUrl, +} from 'src/hooks/useLocalizeDashboardUrl'; +import styled from 'styled-components'; import { ExternalLink } from '../../ExternalLink'; import { CampaignOverview } from './CampaignOverview'; import { DevicesAndTypes } from './DevicesAndTypes'; import { UniqueBugsSection } from './UniqueBugsSection'; +const NavFooterCTAContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.space.sm}; +`; + export const widgets = ({ campaignId }: { campaignId: number }) => { const { t, i18n } = useTranslation(); const { data: campaign } = useGetCampaignsByCidQuery({ @@ -44,15 +54,25 @@ export const widgets = ({ campaignId }: { campaignId: number }) => { }, { content: ( - + {campaign.plan && ( + + {t('__CAMPAIGN_PAGE_NAVIGATION_PLAN_EXTERNAL_LINK_LABEL')} + )} - > - {t('__CAMPAIGN_PAGE_NAVIGATION_BUG_EXTERNAL_LINK_LABEL')} - + + {t('__CAMPAIGN_PAGE_NAVIGATION_BUG_EXTERNAL_LINK_LABEL')} + + ), type: 'footer' as const, }, diff --git a/src/pages/Dashboard/Project.tsx b/src/pages/Dashboard/Project.tsx index f8efe5de9..75e56b1f0 100644 --- a/src/pages/Dashboard/Project.tsx +++ b/src/pages/Dashboard/Project.tsx @@ -48,14 +48,19 @@ const Items = ({ ); } + const isArchiveProject = project.is_archive === 1; + if (project.campaigns_count > 0 || items.length > 0) { return ( - + - + {!isArchiveProject && } ); } @@ -130,7 +135,11 @@ const Project = () => { return ( } excludeMarginBottom={isEmpty} diff --git a/src/pages/Dashboard/project-items/index.tsx b/src/pages/Dashboard/project-items/index.tsx index 199798305..b74b0f71e 100644 --- a/src/pages/Dashboard/project-items/index.tsx +++ b/src/pages/Dashboard/project-items/index.tsx @@ -23,7 +23,13 @@ const FloatRight = styled.div` margin-bottom: ${theme.space.xs}; `; -export const ProjectItems = ({ projectId }: { projectId: number }) => { +export const ProjectItems = ({ + isArchive, + projectId, +}: { + isArchive: boolean; + projectId: number; +}) => { const { t } = useTranslation(); const { width } = useWindowSize(); const breakpointMd = parseInt(theme.breakpoints.md, 10); @@ -75,8 +81,16 @@ export const ProjectItems = ({ projectId }: { projectId: number }) => { > {width >= breakpointMd && ( diff --git a/src/pages/Plan/Controls.tsx b/src/pages/Plan/Controls.tsx index 64a4ea05d..9e4f815f8 100644 --- a/src/pages/Plan/Controls.tsx +++ b/src/pages/Plan/Controls.tsx @@ -1,18 +1,25 @@ -import { Button } from '@appquality/unguess-design-system'; +import { + Button, + ButtonMenu, + IconButton, +} from '@appquality/unguess-design-system'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; import { Pipe } from 'src/common/components/Pipe'; +import { ReactComponent as DotsIcon } from '@zendeskgarden/svg-icons/src/16/overflow-vertical-stroke.svg'; import { usePatchPlansByPidStatusMutation } from 'src/features/api'; import { useSubmit } from 'src/features/modules/useModuleConfiguration'; import { useRequestQuotation } from 'src/features/modules/useRequestQuotation'; import { useValidateForm } from 'src/features/planModules'; import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; import styled from 'styled-components'; +import { useModule } from 'src/features/modules/useModule'; import { getPlanStatus } from '../Dashboard/hooks/getPlanStatus'; import { usePlan } from './hooks/usePlan'; import { SendRequestModal } from './modals/SendRequestModal'; +import { DeletePlanModal } from './modals/DeletePlanModal'; const StyledPipe = styled(Pipe)` display: inline; @@ -24,6 +31,7 @@ export const Controls = () => { const { t } = useTranslation(); const navigate = useNavigate(); const [isModalOpen, setIsModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const { planId } = useParams(); const { plan } = usePlan(planId); const { handleSubmit: submitModuleConfiguration, isLoading: isSubmitting } = @@ -31,6 +39,7 @@ export const Controls = () => { const { validateForm } = useValidateForm(); const [isSubmitted, setIsSubmitted] = useState(false); const [patchStatus] = usePatchPlansByPidStatusMutation(); + const { value: titleValue } = useModule('title'); // to use the current changed title value (also if plan is not saved) in delete modal const campaignRoute = useLocalizeRoute( `campaigns/${plan?.campaign?.id ?? 0}` @@ -45,6 +54,16 @@ export const Controls = () => { setIsModalOpen(true); }; + const handleQuitDeletePlanModal = () => { + setIsDeleteModalOpen(false); + }; + + const handleMenuClick = (value?: string) => { + if (value === 'delete') { + setIsDeleteModalOpen(true); + } + }; + if (!plan) return null; const { status } = getPlanStatus(plan, t); @@ -94,7 +113,9 @@ export const Controls = () => { } return ( -
+
+ { + handleMenuClick(value ?? ''); + }} + label={(props) => ( + + + + )} + > + + {t('__PLAN_DELETE_PLAN_CTA')} + + + + {isDeleteModalOpen && planId && ( + + )} + {isModalOpen && setIsModalOpen(false)} />}
); diff --git a/src/pages/Plan/ModulesList.tsx b/src/pages/Plan/ModulesList.tsx index 75618668d..44317c21e 100644 --- a/src/pages/Plan/ModulesList.tsx +++ b/src/pages/Plan/ModulesList.tsx @@ -30,6 +30,10 @@ export const ModulesList = ({ tabId }: { tabId: PlanTab }) => { id={`module-${type}`} style={{ marginBottom: appTheme.space.lg, + paddingLeft: + type === 'tasks' || type === 'target' ? 0 : appTheme.space.xs, + paddingRight: + type === 'tasks' || type === 'target' ? 0 : appTheme.space.xs, display: isVisible ? 'block' : 'none', }} > @@ -37,8 +41,13 @@ export const ModulesList = ({ tabId }: { tabId: PlanTab }) => {
); })} -
{tabId !== 'summary' && } +
); }; diff --git a/src/pages/Plan/PlanBody.tsx b/src/pages/Plan/PlanBody.tsx index 1e95bd8f5..9fb9082af 100644 --- a/src/pages/Plan/PlanBody.tsx +++ b/src/pages/Plan/PlanBody.tsx @@ -1,5 +1,6 @@ import { Col, Grid, Row } from '@appquality/unguess-design-system'; import { appTheme } from 'src/app/theme'; +import { LayoutWrapper } from 'src/common/components/LayoutWrapper'; import { StickyCol } from './common/StickyCol'; import { usePlanTab } from './context/planContext'; import { ModulesList } from './ModulesList'; @@ -15,20 +16,22 @@ export const PlanBody = () => { const debug = params.get('debug'); return ( - - {activeTab === 'summary' ? ( - - ) : ( - - -
{hasFeatureFlag(FEATURE_FLAG_CHANGE_MODULES_VARIANTS) && getPlanStatus() === 'draft' && ( -
{hasFeatureFlag(FEATURE_FLAG_CHANGE_MODULES_VARIANTS) && getPlanStatus() === 'draft' && ( - )} diff --git a/src/pages/Plan/modules/Tasks/parts/modal/ExperientialTasks.tsx b/src/pages/Plan/modules/Tasks/parts/modal/ExperientialTasks.tsx index 66fa6e054..5bd86508f 100644 --- a/src/pages/Plan/modules/Tasks/parts/modal/ExperientialTasks.tsx +++ b/src/pages/Plan/modules/Tasks/parts/modal/ExperientialTasks.tsx @@ -1,6 +1,7 @@ import { Button, SM } from '@appquality/unguess-design-system'; import { useTranslation } from 'react-i18next'; import { ReactComponent as ThinkingAloudTaskIcon } from 'src/assets/icons/thinking-aloud-task-icon.svg'; +import { appTheme } from 'src/app/theme'; import { useHandleModalItemClick } from '../../utils'; import { ButtonsContainer } from './ButtonsContainer'; @@ -10,8 +11,10 @@ const ExperientialTasks = () => { return ( <> - - {t('__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_EXPERIENTIAL_TASKS_LABEL')} + + {t( + '__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_EXPERIENTIAL_TASKS_LABEL' + ).toUpperCase()} )} diff --git a/src/pages/Plan/modules/Touchpoints/parts/nav/TouchpointItemNav.tsx b/src/pages/Plan/modules/Touchpoints/parts/nav/TouchpointItemNav.tsx index bc43f546c..aa898c782 100644 --- a/src/pages/Plan/modules/Touchpoints/parts/nav/TouchpointItemNav.tsx +++ b/src/pages/Plan/modules/Touchpoints/parts/nav/TouchpointItemNav.tsx @@ -14,8 +14,8 @@ import { useModuleTouchpoints } from '../../hooks'; import { getIconFromTouchpointOutput } from '../../utils'; const StyledCard = styled(Card)` - padding: ${({ theme }) => theme.space.md}; - margin: ${({ theme }) => theme.space.sm} 0; + padding: ${({ theme }) => `${theme.space.sm} ${theme.space.md}`}; + margin: ${({ theme }) => theme.space.xs} 0; background-color: transparent; `; @@ -59,6 +59,7 @@ const TouchpointItemNav = ({ offset={-200} smooth spy + isDynamic style={{ textDecoration: 'none' }} activeClass="isCurrent" > @@ -67,14 +68,14 @@ const TouchpointItemNav = ({ data-qa="touchpoint-item-nav" {...(hasErrors && { style: { - borderColor: appTheme.palette.red[600], + borderColor: appTheme.palette.red[900], }, })} > {getIconFromTouchpointOutput(touchpoint)} - + {key + 1}.{' '} {form_factor} {kind} diff --git a/src/pages/Plan/navigation/aside/AddBlockButton.tsx b/src/pages/Plan/navigation/aside/AddBlockButton.tsx index 70edabbf4..21b5d6679 100644 --- a/src/pages/Plan/navigation/aside/AddBlockButton.tsx +++ b/src/pages/Plan/navigation/aside/AddBlockButton.tsx @@ -4,10 +4,18 @@ import { useTranslation } from 'react-i18next'; import { useAppSelector } from 'src/app/hooks'; import { ReactComponent as PlusIcon } from 'src/assets/icons/plus-icon.svg'; import { useModuleConfiguration } from 'src/features/modules/useModuleConfiguration'; +import styled from 'styled-components'; import { usePlanTab } from '../../context/planContext'; import { MODULES_BY_TAB } from '../../modulesMap'; import { usePlanNavContext } from './context'; +const ButtonContainer = styled.div` + padding-top: ${({ theme }) => theme.space.sm}; + padding-bottom: ${({ theme }) => theme.space.sm}; + padding-left: ${({ theme }) => theme.space.xxs}; + padding-right: ${({ theme }) => theme.space.xxs}; +`; + const AddBlockButton = () => { const { t } = useTranslation(); const triggerRef = useRef(null); @@ -24,19 +32,21 @@ const AddBlockButton = () => { }); return ( - + + + ); }; diff --git a/src/pages/Plan/navigation/aside/NavItem.tsx b/src/pages/Plan/navigation/aside/NavItem.tsx index 7f5934c7c..24979cd18 100644 --- a/src/pages/Plan/navigation/aside/NavItem.tsx +++ b/src/pages/Plan/navigation/aside/NavItem.tsx @@ -3,6 +3,7 @@ import { Ellipsis, MD, Message, + SM, Span, } from '@appquality/unguess-design-system'; import { useTranslation } from 'react-i18next'; @@ -12,17 +13,24 @@ import { appTheme } from 'src/app/theme'; import { components } from 'src/common/schema'; import styled from 'styled-components'; import { getIconFromModuleType, getTitleFromModuleType } from '../../utils'; +import { getSubtitleFromModuleType } from '../../utils/getSubtitleFromModuleType'; const StyledCard = styled(Card)` - padding: ${({ theme }) => theme.space.md}; - margin-bottom: ${({ theme }) => theme.space.xs}; background-color: transparent; + padding: 0; + margin-top: ${({ theme }) => theme.space.xs}; + margin-bottom: ${({ theme }) => theme.space.xs}; `; const StyledContainer = styled.div` display: flex; justify-content: flex-start; align-items: center; + padding-left: ${({ theme }) => theme.space.md}; + padding-right: ${({ theme }) => theme.space.md}; + padding-top: ${({ theme }) => theme.space.sm}; + padding-bottom: ${({ theme }) => theme.space.sm}; + gap: ${({ theme }) => theme.space.sm}; `; @@ -67,19 +75,24 @@ const NavItem = ({ data-qa="task-item-nav" {...(hasErrors && { style: { - borderColor: appTheme.palette.red[600], + borderColor: appTheme.palette.red[900], }, })} {...(!children && { className: 'no-children', })} > - + {getIconFromModuleType(type)} - + {getTitleFromModuleType(type)} + + {getSubtitleFromModuleType(type)} + {children && children} diff --git a/src/pages/Plan/navigation/aside/NavItemChildren.tsx b/src/pages/Plan/navigation/aside/NavItemChildren.tsx index 65cc277de..b9fefee30 100644 --- a/src/pages/Plan/navigation/aside/NavItemChildren.tsx +++ b/src/pages/Plan/navigation/aside/NavItemChildren.tsx @@ -6,6 +6,7 @@ const ChildrenContainer = styled.div` max-height: 300px; overflow-y: auto; padding-right: ${({ theme }) => theme.space.sm}; + padding-left: ${({ theme }) => theme.space.sm}; `; const NavItemChildren = ({ diff --git a/src/pages/Plan/utils/getSubtitleFromModuleType.tsx b/src/pages/Plan/utils/getSubtitleFromModuleType.tsx new file mode 100644 index 000000000..db04419f1 --- /dev/null +++ b/src/pages/Plan/utils/getSubtitleFromModuleType.tsx @@ -0,0 +1,47 @@ +import { useTranslation } from 'react-i18next'; +import { components } from 'src/common/schema'; + +const getSubtitleFromModuleType = ( + type: components['schemas']['Module']['type'] +) => { + const { t } = useTranslation(); + + switch (type) { + case 'dates': + return t('__ASIDE_NAVIGATION_MODULE_DATES_BLOCK_SUBTITLE'); + case 'age': + return t('__ASIDE_NAVIGATION_MODULE_AGE_SUBTITLE'); + case 'gender': + return t('__ASIDE_NAVIGATION_MODULE_GENDER_ACCORDION_SUBTITLE'); + case 'goal': + return t('__ASIDE_NAVIGATION_MODULE_GOAL_SUBTITLE'); + case 'language': + return t('__ASIDE_NAVIGATION_MODULE_LANGUAGE_SUBTITLE'); + case 'literacy': + return t('__ASIDE_NAVIGATION_MODULE_DIGITAL_LITERACY_ACCORDION_SUBTITLE'); + case 'out_of_scope': + return t('__ASIDE_NAVIGATION_MODULE_OUT_OF_SCOPE_SUBTITLE'); + case 'target': + return t('__ASIDE_NAVIGATION_MODULE_TARGET_SUBTITLE'); + case 'target_note': + return t('__ASIDE_NAVIGATION_MODULE_TARGET_NOTE_BLOCK_SUBTITLE'); + case 'tasks': + return t('__ASIDE_NAVIGATION_MODULE_TASKS_SUBTITLE'); + case 'title': + return t('__ASIDE_NAVIGATION_MODULE_SUBTITLE_BLOCK_SUBTITLE'); + case 'setup_note': + return t('__ASIDE_NAVIGATION_MODULE_SETUP_NOTE_BLOCK_SUBTITLE'); + case 'instruction_note': + return t('__ASIDE_NAVIGATION_MODULE_INSTRUCTIONS_NOTE_BLOCK_SUBTITLE'); + case 'browser': + return t('__ASIDE_NAVIGATION_MODULE_BROWSER_SUBTITLE'); + case 'touchpoints': + return t('__ASIDE_NAVIGATION_MODULE_TOUCHPOINTS_SUBTITLE'); + case 'additional_target': + return t('__ASIDE_NAVIGATION_MODULE_ADDITIONAL_TARGET_SUBTITLE'); + default: + return ''; + } +}; + +export { getSubtitleFromModuleType }; diff --git a/src/pages/Templates/Body.tsx b/src/pages/Templates/Body.tsx index 7ba6e84b8..b6e72cf82 100644 --- a/src/pages/Templates/Body.tsx +++ b/src/pages/Templates/Body.tsx @@ -1,6 +1,5 @@ import { MD, Separator, XXL } from '@appquality/unguess-design-system'; import { useTranslation } from 'react-i18next'; -import { LayoutWrapper } from 'src/common/components/LayoutWrapper'; import styled from 'styled-components'; import { useTemplatesContext } from './Context'; import { TemplateCardsGrid } from './TemplateCardsGrid'; @@ -21,7 +20,7 @@ const Body = () => { const { t } = useTranslation(); const { templatesByCategory } = useTemplatesContext(); return ( - + <> {templatesByCategory.tailored.length > 0 && ( { )} - + ); }; diff --git a/src/pages/Templates/TemplateCardsGrid.tsx b/src/pages/Templates/TemplateCardsGrid.tsx index 81ae5ea26..6b7ff6ca9 100644 --- a/src/pages/Templates/TemplateCardsGrid.tsx +++ b/src/pages/Templates/TemplateCardsGrid.tsx @@ -13,10 +13,10 @@ const CardsGrid = styled.div` row-gap: ${appTheme.space.lg}; column-gap: ${appTheme.space.md}; @container cardsWrapper (min-width: 450px) { - grid-template-columns: 1fr 1fr; + grid-template-columns: repeat(2, minmax(0, 1fr)); } @container cardsWrapper (min-width: 900px) { - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: repeat(3, minmax(0, 1fr)); } `; diff --git a/src/pages/Templates/index.tsx b/src/pages/Templates/index.tsx index 90d88b968..86324fad5 100644 --- a/src/pages/Templates/index.tsx +++ b/src/pages/Templates/index.tsx @@ -7,6 +7,7 @@ import { PageLoader } from 'src/common/components/PageLoader'; import PlanCreationInterface from 'src/common/components/PlanCreationInterface'; import { useGetWorkspacesByWidTemplatesQuery } from 'src/features/api'; import { Page } from 'src/features/templates/Page'; +import { LayoutWrapper } from 'src/common/components/LayoutWrapper'; import { useActiveWorkspace } from 'src/hooks/useActiveWorkspace'; import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; import Body from './Body'; @@ -21,7 +22,7 @@ const PageInner = () => { setIsDrawerOpen(false); }, [setIsDrawerOpen]); return ( - <> + @@ -39,7 +40,7 @@ const PageInner = () => { template={selectedTemplate} /> )} - + ); }; diff --git a/src/pages/Video/Actions.tsx b/src/pages/Video/Actions.tsx index 2dc07ff0a..9381b14ec 100644 --- a/src/pages/Video/Actions.tsx +++ b/src/pages/Video/Actions.tsx @@ -1,23 +1,24 @@ import { LG, Skeleton, Tag, XL } from '@appquality/unguess-design-system'; +import { useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; -import { Meta } from 'src/common/components/Meta'; -import { Pipe } from 'src/common/components/Pipe'; -import { getDeviceIcon } from 'src/common/components/BugDetail/Meta'; import { ReactComponent as ClockIcon } from 'src/assets/icons/time-icon.svg'; -import { Divider } from 'src/common/components/divider'; -import { useTranslation } from 'react-i18next'; import { capitalizeFirstLetter } from 'src/common/capitalizeFirstLetter'; +import { getDeviceIcon } from 'src/common/components/BugDetail/Meta'; +import { Divider } from 'src/common/components/divider'; +import { Meta } from 'src/common/components/Meta'; +import { Pipe } from 'src/common/components/Pipe'; import { useGetVideosByVidObservationsQuery, useGetVideosByVidQuery, } from 'src/features/api'; import styled from 'styled-components'; -import { useRef } from 'react'; -import { getSeverityTagsByVideoCount } from '../Videos/utils/getSeverityTagsWithCount'; import { formatDuration } from '../Videos/utils/formatDuration'; +import { getSeverityTagsByVideoCount } from '../Videos/utils/getSeverityTagsWithCount'; import { NoObservations } from './components/NoObservations'; import { Observation } from './components/Observation'; +import { SentimentOverview } from './components/SentimentOverview'; const Container = styled.div` display: flex; @@ -103,6 +104,7 @@ const Actions = () => { )} +
{t('__OBSERVATIONS_DRAWER_TOTAL')}: {observations.length} diff --git a/src/pages/Video/components/SentimentOverview/index.tsx b/src/pages/Video/components/SentimentOverview/index.tsx new file mode 100644 index 000000000..7ca0750e9 --- /dev/null +++ b/src/pages/Video/components/SentimentOverview/index.tsx @@ -0,0 +1,90 @@ +import { + AccordionNew, + Button, + GlobalAlert, + SM, + Tag, +} from '@appquality/unguess-design-system'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { ReactComponent as AiIcon } from 'src/assets/icons/ai-icon.svg'; +import { ReactComponent as CopyIcon } from 'src/assets/icons/copy-icon.svg'; +import { useCopy } from 'src/hooks/useCopy'; +import styled, { useTheme } from 'styled-components'; +import { useContent } from '../Transcript/useContent'; + +const SentimentOverviewWrapper = styled.div` + margin-top: ${({ theme }) => theme.space.md}; +`; + +const CopyButton = styled(Button)` + margin-top: ${({ theme }) => theme.space.lg}; +`; + +const StyledHeader = styled(AccordionNew.Header)` + .accordion-header-inner-wrapper { + grid-template-areas: + 'supertitle supertitle' + 'label label' + 'meta meta'; + } +`; + +const StyledSM = styled(SM)` + font-style: italic; +`; + +export const SentimentOverview = () => { + const { videoId } = useParams(); + const theme = useTheme(); + const [isOpen, setIsOpen] = useState(true); + const { t } = useTranslation(); + const { generalSentiment } = useContent(videoId || ''); + const copy = useCopy({ text: generalSentiment || '' }); + + if (!generalSentiment) return null; + const handleAccordionChange = () => { + setIsOpen((prev) => !prev); + }; + + return ( + + + + }> + + + Beta + + + +
+
{generalSentiment}
+ + + + + {t('__SENTIMENT_COPY_BUTTON_LABEL')} + +
+ + {t('__SENTIMENT_OVERVIEW_ALERT_DISCLAIMER')} +
+
+
+
+ ); +}; diff --git a/src/pages/Video/components/Transcript/TranscriptTheme/ParagraphWrapper.tsx b/src/pages/Video/components/Transcript/TranscriptTheme/ParagraphWrapper.tsx index a12fe8e56..8c8cefb4c 100644 --- a/src/pages/Video/components/Transcript/TranscriptTheme/ParagraphWrapper.tsx +++ b/src/pages/Video/components/Transcript/TranscriptTheme/ParagraphWrapper.tsx @@ -1,7 +1,25 @@ +import { ReactNode } from 'react'; import styled from 'styled-components'; -const Component = styled.div` +const ComponentContainer = ({ + className, + children, +}: { + className: string; + children: ReactNode; +}) => ( +
+ {children} +
+); + +const Component = styled(ComponentContainer)` padding: ${({ theme }) => theme.space.sm} 0; + .paragraph-topbar { + align-items: center; + display: flex; + justify-content: space-between; + } `; export default Component; diff --git a/src/pages/Video/components/Transcript/TranscriptTheme/SentencesWrapper.tsx b/src/pages/Video/components/Transcript/TranscriptTheme/SentencesWrapper.tsx index c4abf8b65..43aea65f8 100644 --- a/src/pages/Video/components/Transcript/TranscriptTheme/SentencesWrapper.tsx +++ b/src/pages/Video/components/Transcript/TranscriptTheme/SentencesWrapper.tsx @@ -2,8 +2,7 @@ import { ReactNode } from 'react'; import styled from 'styled-components'; const Wrapper = styled.div` - margin: ${({ theme }) => theme.space.sm} 0; - padding-top: ${({ theme }) => theme.space.lg}; + margin-bottom: ${({ theme }) => theme.space.sm}; font-size: ${({ theme }) => theme.fontSizes.md}; position: relative; color: ${({ theme }) => theme.palette.blue[600]}; diff --git a/src/pages/Video/components/Transcript/TranscriptTheme/SentimentWrapper.tsx b/src/pages/Video/components/Transcript/TranscriptTheme/SentimentWrapper.tsx new file mode 100644 index 000000000..f7398d5a0 --- /dev/null +++ b/src/pages/Video/components/Transcript/TranscriptTheme/SentimentWrapper.tsx @@ -0,0 +1,149 @@ +import { + Button, + IconButton, + MD, + SM, + Tag, + TooltipModal, +} from '@appquality/unguess-design-system'; +import { useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { ReactComponent as LeafIcon } from 'src/assets/icons/ai-icon.svg'; +import { ReactComponent as CopyIcon } from 'src/assets/icons/copy-icon.svg'; +import { ReactComponent as InfoIcon } from 'src/assets/icons/info.svg'; +import { useCopy } from 'src/hooks/useCopy'; +import { styled, useTheme } from 'styled-components'; +import { useToolsContext } from '../../tools/context/ToolsContext'; +import { ReactComponent as NegativeIcon } from '../assets/negative.svg'; +import { ReactComponent as NeutralIcon } from '../assets/neutral.svg'; +import { ReactComponent as PositiveIcon } from '../assets/positive.svg'; +import { ReactComponent as VeryNegativeIcon } from '../assets/very_negative.svg'; +import { ReactComponent as VeryPositiveIcon } from '../assets/very_positive.svg'; + +const useTagData = (value: number) => { + const { t } = useTranslation(); + const theme = useTheme(); + + switch (value) { + case 1: + return { + text: t('__TRANSCRIPT_SENTIMENT_VALUE_VERY_NEGATIVE'), + color: theme.palette.red[800], + textColor: theme.palette.red[100], + titleColor: theme.palette.red[900], + Icon: VeryNegativeIcon, + }; + case 2: + return { + text: t('__TRANSCRIPT_SENTIMENT_VALUE_NEGATIVE'), + color: theme.palette.red[100], + textColor: theme.palette.red[800], + titleColor: theme.palette.red[900], + Icon: NegativeIcon, + }; + case 3: + return { + text: t('__TRANSCRIPT_SENTIMENT_VALUE_NEUTRAL'), + color: 'transparent', + textColor: theme.palette.grey[600], + titleColor: theme.palette.grey[800], + Icon: NeutralIcon, + }; + case 4: + return { + text: t('__TRANSCRIPT_SENTIMENT_VALUE_POSITIVE'), + color: theme.palette.green[10], + textColor: theme.palette.green[600], + titleColor: theme.palette.green[800], + Icon: PositiveIcon, + }; + case 5: + return { + text: t('__TRANSCRIPT_SENTIMENT_VALUE_VERY_POSITIVE'), + color: theme.palette.green[800], + textColor: theme.palette.green[50], + titleColor: theme.palette.green[800], + Icon: VeryPositiveIcon, + }; + default: + return { + text: '', + color: 'grey', + textColor: 'white', + titleColor: 'grey', + Icon: LeafIcon, + }; + } +}; + +const StyledSM = styled(SM)` + color: ${({ theme }) => theme.palette.grey[600]}; +`; + +const TagWrapper = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.space.xxs}; +`; + +const Component = ({ value, text }: { value: number; text: string }) => { + const tagData = useTagData(value); + const { t } = useTranslation(); + const { showSentiment } = useToolsContext(); + + const copy = useCopy({ + text, + notification: t('__SENTIMENT_TOAST_COPY_MESSAGE'), + }); + const theme = useTheme(); + const { Icon } = tagData; + const ref = useRef(null); + const [referenceElement, setReferenceElement] = + useState(null); + + if (!showSentiment) return ; + + return ( + <> + + + + + + {tagData.text} + + setReferenceElement(ref.current)} + > + + + + setReferenceElement(null)} + > + + {tagData.text} + + + {text} + + +
+ +
+
+
+ + ); +}; + +export default Component; diff --git a/src/pages/Video/components/Transcript/TranscriptTheme/index.tsx b/src/pages/Video/components/Transcript/TranscriptTheme/index.tsx index 95d30b0f2..5ff57a748 100644 --- a/src/pages/Video/components/Transcript/TranscriptTheme/index.tsx +++ b/src/pages/Video/components/Transcript/TranscriptTheme/index.tsx @@ -3,8 +3,9 @@ import { styled } from 'styled-components'; import ActiveWrapper from './ActiveWrapper'; import ObservationWrapper from './ObservationWrapper'; import ParagraphWrapper from './ParagraphWrapper'; -import SentenceWrapper from './SentenceWrapper'; import SentencesWrapper from './SentencesWrapper'; +import SentenceWrapper from './SentenceWrapper'; +import SentimentWrapper from './SentimentWrapper'; import SpeakerWrapper from './SpeakerWrapper'; import TranslationWrapper from './TranslationsWrapper'; @@ -12,6 +13,7 @@ export const TranscriptTheme = Theme.configure({ speakerWrapper: SpeakerWrapper, activeWrapper: ActiveWrapper, observationWrapper: ObservationWrapper, + sentimentWrapper: SentimentWrapper, searchStyleWrapper: styled.div` .search-result { background-color: ${({ theme }) => theme.palette.product.talk}; diff --git a/src/pages/Video/components/Transcript/assets/negative.svg b/src/pages/Video/components/Transcript/assets/negative.svg new file mode 100644 index 000000000..3a3eb0852 --- /dev/null +++ b/src/pages/Video/components/Transcript/assets/negative.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pages/Video/components/Transcript/assets/neutral.svg b/src/pages/Video/components/Transcript/assets/neutral.svg new file mode 100644 index 000000000..a2624c137 --- /dev/null +++ b/src/pages/Video/components/Transcript/assets/neutral.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pages/Video/components/Transcript/assets/positive.svg b/src/pages/Video/components/Transcript/assets/positive.svg new file mode 100644 index 000000000..c57384a9f --- /dev/null +++ b/src/pages/Video/components/Transcript/assets/positive.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pages/Video/components/Transcript/assets/very_negative.svg b/src/pages/Video/components/Transcript/assets/very_negative.svg new file mode 100644 index 000000000..e7ddb844c --- /dev/null +++ b/src/pages/Video/components/Transcript/assets/very_negative.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pages/Video/components/Transcript/assets/very_positive.svg b/src/pages/Video/components/Transcript/assets/very_positive.svg new file mode 100644 index 000000000..6f7c70678 --- /dev/null +++ b/src/pages/Video/components/Transcript/assets/very_positive.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pages/Video/components/Transcript/index.tsx b/src/pages/Video/components/Transcript/index.tsx index 44d2a2911..745911baa 100644 --- a/src/pages/Video/components/Transcript/index.tsx +++ b/src/pages/Video/components/Transcript/index.tsx @@ -54,7 +54,7 @@ export const Transcript = ({ setCurrentTime: (time: number) => void; }) => { const handleAddObservation = useAddObservation({ videoId: videoId || '' }); - const { data: content, speakers } = useContent(videoId || ''); + const { data: content, speakers, sentiments } = useContent(videoId || ''); const { data: observations } = useObservations(videoId || ''); const { data: translationData } = useTranslationTools(); @@ -66,8 +66,8 @@ export const Transcript = ({ translations: translationData.translation?.sentences, themeExtension: TranscriptTheme, observations, - // @ts-ignore - numberOfSpeakers: speakers, + sentiments, + numberOfSpeakers: speakers || undefined, }, [observations, translationData.translation?.sentences] ); diff --git a/src/pages/Video/components/Transcript/useContent.tsx b/src/pages/Video/components/Transcript/useContent.tsx index d27cfbcb0..099ca0975 100644 --- a/src/pages/Video/components/Transcript/useContent.tsx +++ b/src/pages/Video/components/Transcript/useContent.tsx @@ -22,10 +22,23 @@ export const useContent = (videoId: string) => { [video] ); + const sentiments = useMemo( + () => + video && video?.sentiment + ? video.sentiment.paragraphs.map((s) => ({ + ...s, + text: s.reason, + })) + : undefined, + [video] + ); + const speakers = useMemo(() => video?.transcript?.speakers || null, [video]); return { data: content, + sentiments, + generalSentiment: video?.sentiment?.reason, speakers, isError: isErrorVideo, isFetching: isFetchingVideo, diff --git a/src/pages/Video/components/tools/assets/showSentimentIcon.svg b/src/pages/Video/components/tools/assets/showSentimentIcon.svg new file mode 100644 index 000000000..807e9e869 --- /dev/null +++ b/src/pages/Video/components/tools/assets/showSentimentIcon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/pages/Video/components/tools/context/ToolsContext.tsx b/src/pages/Video/components/tools/context/ToolsContext.tsx index c8f0bd73e..3713ea70b 100644 --- a/src/pages/Video/components/tools/context/ToolsContext.tsx +++ b/src/pages/Video/components/tools/context/ToolsContext.tsx @@ -5,6 +5,8 @@ interface ToolsContextType { setIsOpen: (isOpen: boolean) => void; language: string; setLanguage: (lang: string) => void; + showSentiment: boolean; + setShowSentiment: (showSentiment: boolean) => void; } const ToolsContext = createContext(null); @@ -16,6 +18,7 @@ export const ToolsContextProvider = ({ }) => { const [isOpen, setIsOpen] = useState(false); const [language, setLanguage] = useState(''); + const [showSentiment, setShowSentiment] = useState(true); const toolsContextValue = useMemo( () => ({ @@ -23,8 +26,10 @@ export const ToolsContextProvider = ({ setLanguage, isOpen, setIsOpen, + showSentiment, + setShowSentiment, }), - [language, setLanguage, isOpen, setIsOpen] + [language, setLanguage, isOpen, setIsOpen, showSentiment, setShowSentiment] ); return ( diff --git a/src/pages/Video/components/tools/hooks/useRequestTranslation.tsx b/src/pages/Video/components/tools/hooks/useRequestTranslation.tsx new file mode 100644 index 000000000..b1f49968e --- /dev/null +++ b/src/pages/Video/components/tools/hooks/useRequestTranslation.tsx @@ -0,0 +1,51 @@ +import { Notification, useToast } from '@appquality/unguess-design-system'; +import { useTranslation } from 'react-i18next'; +import { usePostVideosByVidTranslationMutation } from 'src/features/api'; +import { useToolsContext } from '../context/ToolsContext'; + +export const useRequestTranslation = ({ + language, + videoId, +}: { + language?: string; + videoId: string; +}) => { + const { t } = useTranslation(); + const { addToast } = useToast(); + const { setLanguage } = useToolsContext(); + + const [requestTranslation] = usePostVideosByVidTranslationMutation(); + if (!language) return () => null; + + const translate = () => { + requestTranslation({ + vid: videoId || '', + body: { + language, + }, + }) + .unwrap() + .then(() => { + setLanguage(language); + }) + .catch((e) => { + // eslint-disable-next-line no-console + console.error(e); + + addToast( + ({ close }) => ( + + ), + { placement: 'top' } + ); + }); + }; + + return translate; +}; diff --git a/src/pages/Video/components/tools/index.tsx b/src/pages/Video/components/tools/index.tsx index 90060630b..40851ecbc 100644 --- a/src/pages/Video/components/tools/index.tsx +++ b/src/pages/Video/components/tools/index.tsx @@ -2,129 +2,135 @@ import { getLanguageNameByFullTag } from '@appquality/languages'; import { Button, IconButton, - Notification, Span, Tooltip, - useToast, } from '@appquality/unguess-design-system'; -import { ReactComponent as TranslateIcon } from '@zendeskgarden/svg-icons/src/16/translation-exists-fill.svg'; import { ReactComponent as SettingsIcon } from '@zendeskgarden/svg-icons/src/16/gear-fill.svg'; +import { ReactComponent as TranslateIcon } from '@zendeskgarden/svg-icons/src/16/translation-exists-fill.svg'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; -import { usePostVideosByVidTranslationMutation } from 'src/features/api'; -import { ToolsTranslate } from './ToolsTranslate'; +import { styled } from 'styled-components'; +import { useContent } from '../Transcript/useContent'; +import { ReactComponent as ShowSentimentIcon } from './assets/showSentimentIcon.svg'; import { useToolsContext } from './context/ToolsContext'; +import { useRequestTranslation } from './hooks/useRequestTranslation'; import { useTranslationTools } from './hooks/useTranslationTools'; +import { ToolsTranslate } from './ToolsTranslate'; -export const Tools = () => { +const TranslateModal = () => { + const { data } = useTranslationTools(); + const { isOpen } = useToolsContext(); + + if (!isOpen) return null; + + return ( + + ); +}; + +const TranslateButton = () => { const { t } = useTranslation(); const { videoId } = useParams(); - const { addToast } = useToast(); + const { isProcessing, data } = useTranslationTools(); + const { setIsOpen } = useToolsContext(); + + const translate = useRequestTranslation({ + language: data.preferredLanguage, + videoId: videoId || '', + }); + + const openModal = () => { + setIsOpen(true); + }; + + return ( + + ); +}; + +const OpenModalButton = () => { + const { t } = useTranslation(); const { isError, isProcessing, data } = useTranslationTools(); - const { isOpen, setIsOpen, setLanguage } = useToolsContext(); - const [requestTranslation] = usePostVideosByVidTranslationMutation(); + const { isOpen, setIsOpen } = useToolsContext(); + + if (isError || !data.hasQuickTranslate || !data.preferredLanguage) + return null; + + return ( + e.stopPropagation()} + > + setIsOpen(!isOpen)}> + + + + ); +}; + +const ShowHideSentiment = () => { + const { videoId } = useParams(); + const { t } = useTranslation(); + const { sentiments } = useContent(videoId || ''); + const { showSentiment, setShowSentiment } = useToolsContext(); + + if (!sentiments) return null; + + return ( + + ); +}; + +const ToolsWrapper = styled.div` + display: flex; + gap: ${({ theme }) => theme.space.xs}; +`; + +export const Tools = () => { + const { isError } = useTranslationTools(); if (isError) return null; - if (data.hasQuickTranslate && data.preferredLanguage) { - // 2 buttons - return ( -
- - {isOpen && ( - - )} - { - e.stopPropagation(); - }} - > - { - setIsOpen(!isOpen); - }} - > - - - -
- ); - } - - // 1 button return ( -
- - {isOpen && ( - - )} -
+ + + + + + ); }; diff --git a/tests/api/projects/pid/campaigns/_get/200_Example_Project_Campaigns.json b/tests/api/projects/pid/campaigns/_get/200_Example_Project_Campaigns.json index 80b9ae015..ba0907357 100644 --- a/tests/api/projects/pid/campaigns/_get/200_Example_Project_Campaigns.json +++ b/tests/api/projects/pid/campaigns/_get/200_Example_Project_Campaigns.json @@ -122,13 +122,11 @@ "id": 0, "name": "Functional" }, - "outputs": [ - "bugs" - ] + "outputs": ["bugs"] } ], "total": 4, "start": 0, "limit": 10000, "size": 4 -} \ No newline at end of file +} diff --git a/tests/api/users/me/_get/200_Customer.json b/tests/api/users/me/_get/200_Customer.json new file mode 100644 index 000000000..fcf98be54 --- /dev/null +++ b/tests/api/users/me/_get/200_Customer.json @@ -0,0 +1,24 @@ +{ + "id": 10, + "email": "customer@example.com", + "role": "customer", + "name": "customer", + "profile_id": 10, + "tryber_wp_user_id": 10, + "unguess_wp_user_id": 10, + "picture": "https://www.gravatar.com/avatar/04c5df6d27d87476fe9dee853e1142bd", + "features": [ + { + "slug": "exploratory-express", + "name": "Exploratory Express" + }, + { + "slug": "catalog-pages", + "name": "Catalog pages" + }, + { + "slug": "tagging-tool", + "name": "UX Tagging Tool" + } + ] +} diff --git a/tests/api/videos/vid/_get/200_Example_1.json b/tests/api/videos/vid/_get/200_Example_1.json deleted file mode 100644 index 293ef9b5d..000000000 --- a/tests/api/videos/vid/_get/200_Example_1.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "id": 0, - "url": "string", - "streamUrl": "string", - "poster": "string", - "duration": 0, - "tester": { - "id": 0, - "name": "string", - "surname": "string", - "device": { - "type": "smartphone" - } - }, - "transcript": { - "speakers": 0, - "paragraphs": [ - { - "text": "string", - "start": 0, - "end": 0, - "speaker": 0, - "words": [ - { - "start": 0, - "end": 0, - "speaker": 0, - "word": "string" - } - ] - } - ] - }, - "usecase": { - "id": 0, - "name": "string" - }, - "language": "string" -} diff --git a/tests/api/videos/vid/_get/200_consistency_paragraphs_sentiments.json b/tests/api/videos/vid/_get/200_consistency_paragraphs_sentiments.json new file mode 100644 index 000000000..f18732f12 --- /dev/null +++ b/tests/api/videos/vid/_get/200_consistency_paragraphs_sentiments.json @@ -0,0 +1,131 @@ +{ + "id": 0, + "url": "string", + "streamUrl": "string", + "poster": "string", + "duration": 0, + "tester": { + "id": 0, + "name": "string", + "surname": "string", + "device": { + "type": "smartphone" + } + }, + "transcript": { + "speakers": 0, + "paragraphs": [ + { + "text": "ciao", + "start": 1, + "end": 2.01, + "speaker": 0, + "words": [ + { + "start": 1, + "end": 2.01, + "speaker": 0, + "word": "ciao" + } + ] + }, + { + "text": "ciao2", + "start": 2.01, + "end": 10, + "speaker": 0, + "words": [ + { + "start": 2.01, + "end": 10, + "speaker": 0, + "word": "ciao2" + } + ] + }, + { + "text": "ciao3", + "start": 10.01, + "end": 20, + "speaker": 0, + "words": [ + { + "start": 10.01, + "end": 20, + "speaker": 0, + "word": "ciao3" + } + ] + }, + { + "text": "ciao4", + "start": 20.01, + "end": 30, + "speaker": 0, + "words": [ + { + "start": 20.01, + "end": 30, + "speaker": 0, + "word": "ciao4" + } + ] + }, + { + "text": "ciao5", + "start": 30.01, + "end": 40, + "speaker": 0, + "words": [ + { + "start": 30.01, + "end": 40, + "speaker": 0, + "word": "ciao5" + } + ] + } + ] + }, + "sentiment": { + "value": 2, + "reason": "string", + "paragraphs": [ + { + "start": 1, + "end": 2.01, + "value": 1, + "reason": "very negative" + }, + { + "start": 2.01, + "end": 10, + "value": 2, + "reason": "negative" + }, + { + "start": 10.01, + "end": 20, + "value": 3, + "reason": "neutral" + }, + { + "start": 20.01, + "end": 30, + "value": 4, + "reason": "positive" + }, + { + "start": 30.01, + "end": 40, + "value": 5, + "reason": "very positive" + } + ] + }, + "usecase": { + "id": 0, + "name": "string" + }, + "language": "string" +} diff --git a/tests/api/workspaces/wid/projects/pid/_get/200_200_Archived_Empty_Project.json b/tests/api/workspaces/wid/projects/pid/_get/200_200_Archived_Empty_Project.json new file mode 100644 index 000000000..ed4fb9cc4 --- /dev/null +++ b/tests/api/workspaces/wid/projects/pid/_get/200_200_Archived_Empty_Project.json @@ -0,0 +1,8 @@ +{ + "id": 1, + "name": "My archived project", + "campaigns_count": 0, + "workspaceId": 1, + "description": "An empty archived project", + "is_archive": 1 +} diff --git a/tests/api/workspaces/wid/projects/pid/_get/200_200_Archived_Project.json b/tests/api/workspaces/wid/projects/pid/_get/200_200_Archived_Project.json new file mode 100644 index 000000000..cfef9ab3e --- /dev/null +++ b/tests/api/workspaces/wid/projects/pid/_get/200_200_Archived_Project.json @@ -0,0 +1,8 @@ +{ + "id": 1, + "name": "My archived project", + "campaigns_count": 4, + "workspaceId": 1, + "description": "An archived project with some campaigns", + "is_archive": 1 +} diff --git a/tests/api/workspaces/wid/projects/pid/_get/200_Archived_Empty_Project.json b/tests/api/workspaces/wid/projects/pid/_get/200_Archived_Empty_Project.json new file mode 100644 index 000000000..ed4fb9cc4 --- /dev/null +++ b/tests/api/workspaces/wid/projects/pid/_get/200_Archived_Empty_Project.json @@ -0,0 +1,8 @@ +{ + "id": 1, + "name": "My archived project", + "campaigns_count": 0, + "workspaceId": 1, + "description": "An empty archived project", + "is_archive": 1 +} diff --git a/tests/api/workspaces/wid/projects/pid/_get/200_Archived_Project.json b/tests/api/workspaces/wid/projects/pid/_get/200_Archived_Project.json new file mode 100644 index 000000000..cfef9ab3e --- /dev/null +++ b/tests/api/workspaces/wid/projects/pid/_get/200_Archived_Project.json @@ -0,0 +1,8 @@ +{ + "id": 1, + "name": "My archived project", + "campaigns_count": 4, + "workspaceId": 1, + "description": "An archived project with some campaigns", + "is_archive": 1 +} diff --git a/tests/e2e/archive.spec.ts b/tests/e2e/archive.spec.ts new file mode 100644 index 000000000..ae2bbfcca --- /dev/null +++ b/tests/e2e/archive.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '../fixtures/app'; +import { Archive } from '../fixtures/pages/Archive'; + +test.describe('Archive page with some campaigns', () => { + let archive: Archive; + + test.beforeEach(async ({ page }) => { + archive = new Archive(page); + + await archive.loggedIn(); + await archive.mockPreferences(); + await archive.mockWorkspace(); + await archive.mockArchive(); + await archive.mockProjectCampaigns(); + await archive.mockWorkspacesList(); + await archive.open(); + }); + + test('Should only display the campaigns table', async () => { + await expect(archive.elements().emptyState1()).not.toBeVisible(); + await expect(archive.elements().emptyState2()).not.toBeVisible(); + await expect(archive.elements().campaignTable()).toBeVisible(); + await expect(archive.elements().campaignRow()).toHaveCount( + archive.campaignCount + 1 // +1 for the header row + ); + }); +}); + +test.describe('Archive page without campaigns', () => { + let archive: Archive; + + test.beforeEach(async ({ page }) => { + archive = new Archive(page); + + await archive.loggedIn(); + await archive.mockPreferences(); + await archive.mockWorkspace(); + await archive.mockEmptyArchive(); + await archive.mockProjectCampaigns(); + await archive.mockWorkspacesList(); + await archive.open(); + }); + + test('Should display an empty state', async () => { + await expect(archive.elements().emptyState1()).toBeVisible(); + await expect(archive.elements().emptyState2()).toBeVisible(); + await expect(archive.elements().title()).toHaveText( + archive.emptyArchive.name + ); + }); +}); diff --git a/tests/e2e/plan/draft.spec.ts b/tests/e2e/plan/draft.spec.ts index 4ee6b856a..96790580e 100644 --- a/tests/e2e/plan/draft.spec.ts +++ b/tests/e2e/plan/draft.spec.ts @@ -14,10 +14,11 @@ test.describe('The module builder', () => { await planPage.mockWorkspacesList(); await planPage.mockGetDraftWithOnlyMandatoryModulesPlan(); await planPage.mockPatchStatus(); + await planPage.open(); }); - test('has a list of saved modules and not the others, a save button and a request quote cta', async () => { + test('has a list of saved modules and not the others, a save button, a request quote cta and a dots menu cta', async () => { // Click the "Setup" tab await planPage.elements().setupTab().click(); @@ -28,11 +29,13 @@ test.describe('The module builder', () => { // Check if other modules are not visible await expect(planPage.elements().tasksModule()).not.toBeVisible(); - // Check if the save button and request quote CTA are visible and enabled + // Check if the save button, request quote CTA and dots menu are visible and enabled await expect(planPage.elements().saveConfigurationCTA()).toBeVisible(); await expect(planPage.elements().saveConfigurationCTA()).not.toBeDisabled(); await expect(planPage.elements().requestQuotationCTA()).toBeVisible(); await expect(planPage.elements().requestQuotationCTA()).not.toBeDisabled(); + await expect(planPage.elements().extraActionsMenu()).toBeVisible(); + await expect(planPage.elements().extraActionsMenu()).not.toBeDisabled(); }); test('The task module is visible if instructionTab is clicked', async () => { @@ -120,6 +123,7 @@ test.describe('When there is an error in the module configuration (e.g. a date i await planPage.mockWorkspace(); await planPage.mockWorkspacesList(); await planPage.mockGetDraftPlanWithDateError(); + await planPage.open(); }); @@ -130,3 +134,47 @@ test.describe('When there is an error in the module configuration (e.g. a date i }); test('when a user click Request Quotation we trigger all fields validation, display error messages and trigger PATCH plan but not the PATCH status', async () => {}); }); + +test.describe('When the user clicks on the dots menu', () => { + let planPage: PlanPage; + + test.beforeEach(async ({ page }, testinfo) => { + testinfo.setTimeout(60000); + planPage = new PlanPage(page); + await planPage.loggedIn(); + await planPage.mockPreferences(); + await planPage.mockWorkspace(); + await planPage.mockWorkspacesList(); + await planPage.mockGetDraftWithOnlyMandatoryModulesPlan(); + + await planPage.open(); + }); + + test("The menu opens and the user can see the 'Delete' option", async () => { + await planPage.elements().extraActionsMenu().click(); + await expect(planPage.elements().deletePlanActionItem()).toBeVisible(); + await expect(planPage.elements().deletePlanActionItem()).toBeEnabled(); + }); + + test("When the user clicks on the 'Delete' option, the delete modal opens with its title, confirm and abort deletion CTA", async () => { + await planPage.elements().extraActionsMenu().click(); + await planPage.elements().deletePlanActionItem().click(); + await expect(planPage.elements().deletePlanModal()).toBeVisible(); + await expect(planPage.elements().deletePlanModalTitle()).toBeVisible(); + await expect(planPage.elements().deletePlanModalConfirmCTA()).toBeVisible(); + await expect(planPage.elements().deletePlanModalConfirmCTA()).toBeEnabled(); + await expect(planPage.elements().deletePlanModalCancelCTA()).toBeVisible(); + await expect(planPage.elements().deletePlanModalCancelCTA()).toBeEnabled(); + }); + + test("When the user clicks on the 'Delete permanently' CTA, he is redirected to the home page", async ({ + page, + }) => { + await planPage.elements().extraActionsMenu().click(); + await planPage.elements().deletePlanActionItem().click(); + await planPage.elements().deletePlanModalConfirmCTA().click(); + + await expect(planPage.elements().deletePlanModal()).not.toBeVisible(); + await expect(page).toHaveURL('/'); + }); +}); diff --git a/tests/e2e/video/sentiment.spec.ts b/tests/e2e/video/sentiment.spec.ts new file mode 100644 index 000000000..0204a6d2b --- /dev/null +++ b/tests/e2e/video/sentiment.spec.ts @@ -0,0 +1,62 @@ +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(); + await expect(videopage.elements().paragraphContent().first()).toBeVisible(); + }); + + test('Should print the content of the paragraphs', async () => { + const paragraphCount = await videopage + .elements() + .paragraphContent() + .count(); + expect(paragraphCount).toBe(5); + + const paragraphs = await videopage.elements().paragraphContent().all(); + + const contents = await Promise.all( + paragraphs.map(async (paragraph) => paragraph.innerText()) + ); + + expect(contents).toEqual([ + 'ciao \n', + 'ciao2 \n', + 'ciao3 \n', + 'ciao4 \n', + 'ciao5 \n', + ]); + }); + + test('Should print the sentiment values', async ({ i18n }) => { + const sentimentCount = await videopage + .elements() + .sentimentWrapper() + .count(); + expect(sentimentCount).toBe(5); + + const sentiments = await videopage.elements().sentimentItem().all(); + + const contents = await Promise.all( + sentiments.map(async (paragraph) => paragraph.innerText()) + ); + + expect(contents).toEqual([ + i18n.t('__TRANSCRIPT_SENTIMENT_VALUE_VERY_NEGATIVE'), + i18n.t('__TRANSCRIPT_SENTIMENT_VALUE_NEGATIVE'), + i18n.t('__TRANSCRIPT_SENTIMENT_VALUE_NEUTRAL'), + i18n.t('__TRANSCRIPT_SENTIMENT_VALUE_POSITIVE'), + i18n.t('__TRANSCRIPT_SENTIMENT_VALUE_VERY_POSITIVE'), + ]); + }); +}); diff --git a/tests/fixtures/pages/Archive.ts b/tests/fixtures/pages/Archive.ts new file mode 100644 index 000000000..437d862cd --- /dev/null +++ b/tests/fixtures/pages/Archive.ts @@ -0,0 +1,58 @@ +import { type Page } from '@playwright/test'; +import { UnguessPage } from '../UnguessPage'; +import apiProjectsCampaigns from '../../api/projects/pid/campaigns/_get/200_Example_Project_Campaigns.json'; +import emptyArchive from '../../api/workspaces/wid/projects/pid/_get/200_Archived_Empty_Project.json'; + +export class Archive extends UnguessPage { + readonly page: Page; + + readonly url = 'projects/1'; + + readonly projectCampaigns = apiProjectsCampaigns.items; + + readonly emptyArchive = emptyArchive; + + readonly campaignCount = this.projectCampaigns.length; + + constructor(page: Page) { + super(page); + this.page = page; + } + + elements() { + return { + ...super.elements(), + emptyState1: () => + this.page.getByText(this.i18n.t('__DASHBOARD_EMPTY_ARCHIVE_TITLE')), + emptyState2: () => + this.page.getByText(this.i18n.t('__DASHBOARD_EMPTY_ARCHIVE_TITLE')), + campaignTable: () => + this.page.getByRole('table', { name: 'project-campaigns-table' }), + campaignRow: () => this.elements().campaignTable().getByRole('row'), + }; + } + + async mockEmptyArchive() { + await this.page.route('*/**/api/projects/1', async (route) => { + await route.fulfill({ + path: 'tests/api/workspaces/wid/projects/pid/_get/200_Archived_Empty_Project.json', + }); + }); + } + + async mockArchive() { + await this.page.route('*/**/api/projects/1', async (route) => { + await route.fulfill({ + path: 'tests/api/workspaces/wid/projects/pid/_get/200_Archived_Project.json', + }); + }); + } + + async mockProjectCampaigns() { + await this.page.route('*/**/api/projects/1/campaigns*', async (route) => { + await route.fulfill({ + path: 'tests/api/projects/pid/campaigns/_get/200_Example_Project_Campaigns.json', + }); + }); + } +} diff --git a/tests/fixtures/pages/Plan.ts b/tests/fixtures/pages/Plan.ts index 719505f0d..51c26ece5 100644 --- a/tests/fixtures/pages/Plan.ts +++ b/tests/fixtures/pages/Plan.ts @@ -69,6 +69,28 @@ export class PlanPage extends UnguessPage { this.page.getByTestId('digital-literacy-module'), digitalLiteracyModuleErrorMessage: () => this.page.getByTestId('literacy-error'), + extraActionsMenu: () => this.page.getByTestId('extra-actions-menu'), + deletePlanActionItem: () => this.page.getByTestId('delete-action-item'), + deletePlanModal: () => + this.page.getByRole('dialog', { + name: this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_TITLE'), + }), + deletePlanModalTitle: () => + this.elements() + .deletePlanModal() + .getByText(this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_TITLE')), + deletePlanModalConfirmCTA: () => + this.elements() + .deletePlanModal() + .getByRole('button', { + name: this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_BUTTON_CONFIRM'), + }), + deletePlanModalCancelCTA: () => + this.elements() + .deletePlanModal() + .getByText( + this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_BUTTON_CANCEL') + ), }; } diff --git a/tests/fixtures/pages/Video.ts b/tests/fixtures/pages/Video.ts new file mode 100644 index 000000000..04ed1f23a --- /dev/null +++ b/tests/fixtures/pages/Video.ts @@ -0,0 +1,49 @@ +import { type Page } from '@playwright/test'; +import { UnguessPage } from '../UnguessPage'; + +export class VideoPage extends UnguessPage { + readonly page: Page; + + constructor(page: Page) { + super(page); + this.page = page; + this.url = `campaigns/1/videos/1`; + } + + elements() { + return { + ...super.elements(), + paragraphContent: () => + this.page.getByTestId('transcript-paragraph').locator('.content'), + sentimentWrapper: () => this.page.getByTestId('transcript-sentiment'), + sentimentItem: () => + this.page + .getByTestId('transcript-sentiment') + .locator('[data-garden-id="tags.tag_view"]'), + }; + } + + async mockGetCampaign() { + await this.page.route('*/**/api/campaigns/1', async (route) => { + await route.fulfill({ + path: 'tests/api/campaigns/cid/_get/200_Example_1.json', + }); + }); + } + + async mockGetVideo() { + await this.page.route('*/**/api/videos/1', async (route) => { + await route.fulfill({ + path: 'tests/api/videos/vid/_get/200_consistency_paragraphs_sentiments.json', + }); + }); + } + + async mockGetVideoObservations() { + await this.page.route('*/**/api/videos/1/observations', async (route) => { + await route.fulfill({ + path: 'tests/api/videos/vid/observations/_get/200_Example_1.json', + }); + }); + } +} diff --git a/yarn.lock b/yarn.lock index 0d6d284e6..6bd9a21eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -59,10 +59,10 @@ dependencies: hls.js "^1.4.8" -"@appquality/unguess-design-system@4.0.35": - version "4.0.35" - resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.35.tgz#d4cad0a997bad011367b136b137f477f0beb713e" - integrity sha512-epNYyNjpWTktYZ/TQsgHC6px8Ip1PpFpxk1lD4gux8A/F+Z2yyKoXcHEFhMo6pR8TAyc7igW1TPaDjnlVKuP6Q== +"@appquality/unguess-design-system@4.0.36": + version "4.0.36" + resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.36.tgz#c773cf1777e04850b2ba5209106e7c10ae7cc686" + integrity sha512-BEBK4eO2s2KzEmq6ZaiiF0CWLhNuyA6X0q3Zz6ZzuSc+Q0kTw0DWDHQPlngFE66vBnG26U3unUH9UiagVW1l9Q== dependencies: "@appquality/stream-player" "1.0.6" "@nivo/bar" "^0.87.0"