diff --git a/cypress/e2e/1-dx/2-datasets.cy.ts b/cypress/e2e/1-dx/2-datasets.cy.ts index 0ff9e8284..2ac91085b 100644 --- a/cypress/e2e/1-dx/2-datasets.cy.ts +++ b/cypress/e2e/1-dx/2-datasets.cy.ts @@ -24,11 +24,16 @@ describe("Testing connecting data on DX", () => { cy.restoreLocalStorageCache(); // cy.setGoogleAccessToken(); + cy.intercept("GET", `${apiUrl}/users/plan-data`).as("planData"); + cy.visit("/"); + cy.wait("@planData"); + cy.get('[data-cy="cookie-btn"]').click(); cy.intercept(`${apiUrl}/external-sources/search?q=*`).as("getDefaultData"); cy.get('[data-cy="home-connect-dataset-button"]').click(); + cy.wait("@planData"); }); it("Can filter results by source in the federated search", () => { @@ -212,7 +217,11 @@ describe("Edit, Delete and Duplicate Dataset", () => { beforeEach(() => { cy.restoreLocalStorageCache(); + cy.intercept("GET", `${apiUrl}/users/plan-data`).as("planData"); + cy.visit("/"); + + cy.wait("@planData"); cy.get('[data-cy="cookie-btn"]').click(); cy.intercept("GET", `${apiUrl}/datasets?filter=*`).as("fetchDatasets"); diff --git a/cypress/e2e/1-dx/3-charts.cy.ts b/cypress/e2e/1-dx/3-charts.cy.ts index 723aecddc..f2128047c 100644 --- a/cypress/e2e/1-dx/3-charts.cy.ts +++ b/cypress/e2e/1-dx/3-charts.cy.ts @@ -21,8 +21,12 @@ describe("Testing create chart on DX", () => { beforeEach(() => { cy.restoreLocalStorageCache(); + cy.intercept("GET", `${apiUrl}/users/plan-data`).as("planData"); + cy.visit("/"); + cy.wait("@planData"); + cy.get('[data-cy="cookie-btn"]').click(); cy.intercept("GET", `${apiUrl}/datasets?**`).as("getDatasets"); @@ -186,8 +190,12 @@ describe("Testing Ai chart creation", () => { beforeEach(() => { cy.restoreLocalStorageCache(); + cy.intercept("GET", `${apiUrl}/users/plan-data`).as("planData"); + cy.visit("/"); + cy.wait("@planData"); + cy.get('[data-cy="cookie-btn"]').click(); cy.intercept("GET", `${apiUrl}/datasets?**`).as("getDatasets"); cy.get('[data-cy="home-create-chart-button"]').click(); @@ -271,8 +279,12 @@ describe("Edit, duplicate and delete chart", () => { const apiUrl = Cypress.env("api_url"); beforeEach(() => { cy.restoreLocalStorageCache(); + cy.intercept("GET", `${apiUrl}/users/plan-data`).as("planData"); + cy.visit("/"); + cy.wait("@planData"); + cy.get('[data-cy="cookie-btn"]').click(); cy.intercept("GET", `${apiUrl}/charts*`).as("fetchCharts"); diff --git a/cypress/e2e/1-dx/4-reports.cy.ts b/cypress/e2e/1-dx/4-reports.cy.ts index ffb23c361..220766498 100644 --- a/cypress/e2e/1-dx/4-reports.cy.ts +++ b/cypress/e2e/1-dx/4-reports.cy.ts @@ -21,8 +21,12 @@ describe("Testing reports on DX", () => { cy.restoreLocalStorageCache(); // Navigating to dx home page + cy.intercept("GET", `${apiUrl}/users/plan-data`).as("planData"); + cy.visit("/"); + cy.wait("@planData"); + cy.get('[data-cy="cookie-btn"]').click(); }); @@ -43,11 +47,9 @@ describe("Testing reports on DX", () => { cy.get('[data-cy="report-sub-header-title-input"]').type(reportTestName); cy.get('[data-cy="report-header-block"]').within(() => { - cy.get('[data-cy="report-header-block-title-input"]').type( - reportTestName - ); - cy.get('[data-cy="rich-text-editor-container"]').click(); - cy.get('[data-testid="rich-text-editor"]').type( + cy.get('[data-testid="heading-rich-text-editor"]').type(reportTestName); + cy.get('[data-cy="description-rich-text-editor-container"]').click(); + cy.get('[data-testid="description-rich-text-editor"]').type( "This is a report on football players" ); }); @@ -75,7 +77,7 @@ describe("Testing reports on DX", () => { cy.get('[data-cy="row-frame-item-drop-zone-0-0"]').drop(); cy.get("[data-cy=row-frame-0]").within(() => { - cy.get('[data-testid="rich-text-editor"]') + cy.get('[data-testid="report-rich-text-editor"]') .first() .type( "This is a report on football players who played in a match last year" @@ -176,8 +178,12 @@ describe("Edit, duplicate and delete report", () => { beforeEach(() => { cy.restoreLocalStorageCache(); + cy.intercept("GET", `${apiUrl}/users/plan-data`).as("planData"); + cy.visit("/"); + cy.wait("@planData"); + cy.get('[data-cy="cookie-btn"]').click(); cy.intercept(`${apiUrl}/reports?filter=*`).as("fetchReports"); @@ -213,9 +219,9 @@ describe("Edit, duplicate and delete report", () => { cy.get('[data-cy="report-sub-header-title-input"]').type(" - Edited"); cy.get('[data-cy="report-header-block"]').within(() => { - cy.get('[data-cy="report-header-block-title-input"]').type(" - Edited"); - cy.get('[data-cy="rich-text-editor-container"]').click(); - cy.get('[data-testid="rich-text-editor"]').type(" - Edited"); + cy.get('[data-testid="heading-rich-text-editor"]').type(" - Edited"); + cy.get('[data-cy="description-rich-text-editor-container"]').click(); + cy.get('[data-testid="description-rich-text-editor"]').type(" - Edited"); }); cy.get('[data-cy="save-report-button"]').click(); diff --git a/src/app/components/Dialogs/EmbedChartDialog/index.tsx b/src/app/components/Dialogs/EmbedChartDialog/index.tsx index 5119ac1f0..724166138 100644 --- a/src/app/components/Dialogs/EmbedChartDialog/index.tsx +++ b/src/app/components/Dialogs/EmbedChartDialog/index.tsx @@ -7,10 +7,9 @@ import IconButton from "@material-ui/core/IconButton"; import Snackbar from "@material-ui/core/Snackbar"; import get from "lodash/get"; import { useLoadDatasetDetails } from "app/modules/report-module/components/chart-wrapper/useLoadDatasetDetailsAPI"; -import { useAuth0 } from "@auth0/auth0-react"; import ChartContainer from "./chartContainer"; import { copyToClipboard } from "app/utils/copyToClipboard"; -import { useStoreState } from "app/state/store/hooks"; +import { useStoreActions, useStoreState } from "app/state/store/hooks"; import LinkOptions from "./linkOptions"; import BasicSwitch from "app/components/Switch/BasicSwitch"; import EmbedOptions from "./embedOptions"; @@ -24,8 +23,13 @@ export default function EmbedChartDialog(props: { }) { const containerRef = React.useRef(null); const token = useStoreState((state) => state.AuthToken.value); + const fetchUserProfile = useStoreActions( + (state) => state.user.UserProfile.fetch + ); + const userProfile = useStoreState( + (state) => state.user.UserProfile.crudData + ) as { username: string }; const classes = useStyles(); - const { user } = useAuth0(); const { datasetDetails } = useLoadDatasetDetails( props.datasetId!, token ?? undefined @@ -38,7 +42,6 @@ export default function EmbedChartDialog(props: { React.useState("embed-code"); const { - loading, notFound, chartErrorMessage, dataError, @@ -49,6 +52,11 @@ export default function EmbedChartDialog(props: { setNotFound, } = useRenderChartFromAPI(token, props.chartId); + React.useEffect(() => { + if (datasetDetails.owner) { + fetchUserProfile({ getId: datasetDetails.owner }); + } + }, [datasetDetails]); let newVisualOptions = visualOptions; const displayModes = [ @@ -226,7 +234,7 @@ export default function EmbedChartDialog(props: { >

Author: - {user?.given_name || "NOT SPECIFIED"} + {userProfile?.username}

; loading: boolean; } - +type IdatasStats = { + name: string; + type: "bar" | "percentage" | "unique"; + data: { name: string; value: number }[]; +}; export default function PreviewTable(props: PreviewTableProps) { const [toolboxDisplay, setToolboxDisplay] = React.useState(false); + let columns: string[] = []; + let dataStats: IdatasStats[] = []; + if (props.columns.length > 0 && props.dataStats.length > 0) { + if (props.columns.length < 5) { + columns = [...props.columns, ...Array(5).fill("")]; + dataStats = [...props.dataStats, ...Array(5).fill("")]; + } else { + columns = [...props.columns, ...Array(2).fill("")]; + dataStats = [...props.dataStats, ...Array(2).fill("")]; + } + } return ( <> @@ -88,7 +103,7 @@ export default function PreviewTable(props: PreviewTableProps) { padding: 0rem 0.4rem; `} > - {props.columns.map((val, index) => { + {columns.map((val, index) => { return (
{props.dataTypes?.[val] === "string" ? "Aa" : "#"} @@ -127,25 +143,25 @@ export default function PreviewTable(props: PreviewTableProps) { > {val}

- - - + {val && ( + + + + )}
); })} - {props.dataStats?.map((val) => ( + {dataStats?.map((val) => ( {val.name !== "ID" && (
- {props.columns.map((val, cellIndex) => ( + {columns.map((val, cellIndex) => (

void; isSaveEnabled?: boolean; rawViz?: any; - setHasSubHeaderTitleFocused?: (value: boolean) => void; - setHasSubHeaderTitleBlurred?: (value: boolean) => void; + setHasReportNameFocused?: (value: boolean) => void; + setHasReportNameBlurred?: (value: boolean) => void; plugins: ToolbarPluginsType; headerDetails: IHeaderDetails; framesArray: IFramesArray[]; diff --git a/src/app/modules/chart-module/components/chartSubheaderToolbar/index.tsx b/src/app/modules/chart-module/components/chartSubheaderToolbar/index.tsx index f6345be92..8b578b244 100644 --- a/src/app/modules/chart-module/components/chartSubheaderToolbar/index.tsx +++ b/src/app/modules/chart-module/components/chartSubheaderToolbar/index.tsx @@ -26,7 +26,7 @@ import { styles } from "app/modules/chart-module/components/chartSubheaderToolba import { useStoreActions, useStoreState } from "app/state/store/hooks"; import DeleteChartDialog from "app/components/Dialogs/deleteChartDialog"; import { ChartAPIModel, emptyChartAPI } from "app/modules/chart-module/data"; -import { SubheaderToolbarProps } from "app/modules/chart-module/components/chartSubheaderToolbar/data"; +import { ChartSubheaderToolbarProps } from "app/modules/chart-module/components/chartSubheaderToolbar/data"; import { ExportChartButton } from "app/modules/chart-module/components/chartSubheaderToolbar/exportButton"; import { ISnackbarState } from "app/modules/dataset-module/routes/upload-module/upload-steps/previewFragment"; import { chartFromReportAtom, planDialogAtom } from "app/state/recoil/atoms"; @@ -41,7 +41,9 @@ import useMediaQuery from "@material-ui/core/useMediaQuery"; import DuplicateMessage from "app/modules/common/mobile-duplicate-message"; import { InfoSnackbar } from "app/modules/report-module/components/reportSubHeaderToolbar/infosnackbar"; -export function ChartSubheaderToolbar(props: Readonly) { +export function ChartSubheaderToolbar( + props: Readonly +) { const classes = useStyles(); const history = useHistory(); const isMobile = useMediaQuery("(max-width: 599px)"); diff --git a/src/app/modules/chart-module/components/toolbox/index.tsx b/src/app/modules/chart-module/components/toolbox/index.tsx index 24d29e121..0e4a673d0 100644 --- a/src/app/modules/chart-module/components/toolbox/index.tsx +++ b/src/app/modules/chart-module/components/toolbox/index.tsx @@ -1,6 +1,5 @@ /* third-party */ import React from "react"; -import { useRecoilState } from "recoil"; import { useAuth0 } from "@auth0/auth0-react"; import { useHistory, useParams } from "react-router-dom"; import { useStoreActions, useStoreState } from "app/state/store/hooks"; @@ -15,12 +14,17 @@ import { } from "app/modules/chart-module/components/toolbox/data"; import { ChartToolBoxSteps } from "app/modules/chart-module/components/toolbox/steps"; import { TriangleXSIcon } from "app/assets/icons/TriangleXS"; -import { emptyChartAPI, ChartAPIModel } from "app/modules/chart-module/data"; +import { + emptyChartAPI, + ChartAPIModel, + chartViews, +} from "app/modules/chart-module/data"; import ToolboxNav from "app/modules/chart-module/components/toolbox/steps/navbar"; import { InfoSnackbar } from "../chartSubheaderToolbar/infoSnackbar"; export function ChartModuleToolBox(props: Readonly) { const { page, view } = useParams<{ page: string; view?: string }>(); + const isValidView = Object.values(chartViews).find((v) => v === view); const history = useHistory(); const { isAuthenticated, user } = useAuth0(); const isMobile = useMediaQuery("(max-width: 767px)"); @@ -114,7 +118,11 @@ export function ChartModuleToolBox(props: Readonly) { }; React.useEffect(() => { - if (location.pathname === `/chart/${page}` || view == "preview") { + if ( + location.pathname === `/chart/${page}` || + view == "preview" || + !isValidView + ) { setDisplayToolbar("none"); props.setToolboxOpen(false); } else { diff --git a/src/app/modules/chart-module/data.ts b/src/app/modules/chart-module/data.ts index 479475e85..d8e71f186 100644 --- a/src/app/modules/chart-module/data.ts +++ b/src/app/modules/chart-module/data.ts @@ -490,3 +490,23 @@ export const emptyChartAPI: ChartAPIModel = { isMappingValid: false, isAIAssisted: false, }; +export const chartViews = { + customize: "customize", + preview: "preview", + previewData: "preview-data", + filters: "filters", + data: "data", + mapping: "mapping", + chartType: "chart-type", +}; + +export const chartPaths = { + detail: "/chart/:page", + customize: `/chart/:page/${chartViews.customize}`, + preview: `/chart/:page/${chartViews.preview}`, + previewData: `/chart/:page/${chartViews.previewData}`, + filters: `/chart/:page/${chartViews.filters}`, + data: `/chart/:page/${chartViews.data}`, + mapping: `/chart/:page/${chartViews.mapping}`, + chartType: `/chart/:page/${chartViews.chartType}`, +}; diff --git a/src/app/modules/chart-module/index.tsx b/src/app/modules/chart-module/index.tsx index c59de515e..bd1f2464a 100644 --- a/src/app/modules/chart-module/index.tsx +++ b/src/app/modules/chart-module/index.tsx @@ -23,7 +23,7 @@ import { import { PageLoader } from "app/modules/common/page-loader"; import { useChartsRawData } from "app/hooks/useChartsRawData"; import { NoMatchPage } from "app/modules/common/no-match-page"; -import ChartModuleDataView from "app/modules/chart-module/routes/data"; +import ChartModuleDataView from "app/modules/chart-module/routes/select-data"; import { ChartSubheaderToolbar } from "./components/chartSubheaderToolbar"; import ChartBuilderMapping from "app/modules/chart-module/routes/mapping"; import ChartBuilderFilters from "app/modules/chart-module/routes/filters"; @@ -38,6 +38,8 @@ import { routeToConfig, ChartRenderedItem, defaultChartOptions, + chartViews, + chartPaths, } from "app/modules/chart-module/data"; import { NotAuthorizedMessageModule } from "app/modules/common/not-authorized-message"; import { isEmpty } from "lodash"; @@ -64,6 +66,7 @@ export default function ChartModule() { const token = useStoreState((state) => state.AuthToken.value); const history = useHistory(); const { page, view } = useParams<{ page: string; view?: string }>(); + const isValidView = Object.values(chartViews).find((v) => v === view); const [chartFromAPI, setChartFromAPI] = React.useState(null); const [visualOptions, setVisualOptions] = useSessionStorage( @@ -74,7 +77,7 @@ export default function ChartModule() { const setPlanDialog = useSetRecoilState(planDialogAtom); const [rawViz, setRawViz] = React.useState(null); - const [toolboxOpen, setToolboxOpen] = React.useState(Boolean(view)); + const [toolboxOpen, setToolboxOpen] = React.useState(Boolean(isValidView)); const [savedChanges, setSavedChanges] = React.useState(false); const [chartName, setChartName] = React.useState("Untitled Chart"); @@ -600,7 +603,7 @@ export default function ChartModule() { ref={ref} > - + - + - + - + - + - + - + - + >; placeholder: string; + onBlur?: () => void; + onFocus?: () => void; + testId?: string; }): ReactElement => { const editor = useRef(null); @@ -46,7 +49,7 @@ export const RichEditor = (props: { }; React.useEffect(() => { - if (props.focusOnMount) { + if (props.focusOnMount && props.editMode) { focus(); } }, []); @@ -135,7 +138,8 @@ export const RichEditor = (props: { font-family: "GothamNarrow-Book", "Helvetica Neue", sans-serif; } `} - data-cy="rich-text-editor-container" + data-cy={`${props.testId}-container`} + data-testid={`${props.testId}-container`} > { + props.onBlur?.(); if (props.textContent.getCurrentContent().getPlainText().length === 0) props.setPlaceholderState(props.placeholder); }} onFocus={() => { + props.onFocus?.(); props.setPlugins?.(plugins); props.setPlaceholderState(""); }} @@ -165,8 +171,7 @@ export const RichEditor = (props: { ref={(element) => { editor.current = element; }} - webDriverTestID="rich-text-editor" - data-cy="rich-text-editor" + webDriverTestID={props.testId} />

); diff --git a/src/app/modules/common/no-match-page/asset/404.svg b/src/app/modules/common/no-match-page/asset/404.svg new file mode 100644 index 000000000..b84a1b45a --- /dev/null +++ b/src/app/modules/common/no-match-page/asset/404.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/modules/common/no-match-page/asset/bg-ellipse.svg b/src/app/modules/common/no-match-page/asset/bg-ellipse.svg new file mode 100644 index 000000000..94166dd6b --- /dev/null +++ b/src/app/modules/common/no-match-page/asset/bg-ellipse.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/modules/common/no-match-page/index.tsx b/src/app/modules/common/no-match-page/index.tsx index 2937bcb04..ddbbf763d 100644 --- a/src/app/modules/common/no-match-page/index.tsx +++ b/src/app/modules/common/no-match-page/index.tsx @@ -1,100 +1,142 @@ import React from "react"; import get from "lodash/get"; -import { Link } from "react-router-dom"; +import { Link, useHistory } from "react-router-dom"; +import { ReactComponent as NotFoundIcon } from "app/modules/common/no-match-page/asset/404.svg"; +import { ReactComponent as BgImg } from "app/modules/common/no-match-page/asset/bg-ellipse.svg"; + +import SmallFooter from "app/modules/home-module/components/Footer/smallFooter"; // cc:refactor this component, inline css need to be moved to proper styled components export const NoMatchPage = () => { + const history = useHistory(); return ( -
-
-
Oops! Page not found
-
+
-
404
-
-
-
We are sorry, but the page you requested was not found
-
- + +
+ +
+
+
+

Oops! This page could not be found

+

+ Sorry but the page you are looking for does not exist, have been + removed, have changed or is temporarily unavailable. +

+
- -
Back to Home Page
-
+ + Back to Home Page + + +
- +
+
); }; diff --git a/src/app/modules/dataset-module/index.tsx b/src/app/modules/dataset-module/index.tsx index 01e994e2f..d63e24a32 100644 --- a/src/app/modules/dataset-module/index.tsx +++ b/src/app/modules/dataset-module/index.tsx @@ -4,6 +4,7 @@ import DatasetUploadSteps from "app/modules/dataset-module/routes/upload-module/ import { Route, Switch } from "react-router-dom"; import DatasetDetail from "app/modules/dataset-module/routes/datasetDetail"; import EditMetaData from "app/modules/dataset-module/routes/edit"; +import { NoMatchPage } from "app/modules/common/no-match-page"; export default function DatasetDetailModule() { useTitle("Dataxplorer - Datasets"); @@ -11,15 +12,18 @@ export default function DatasetDetailModule() { return ( - + - + - + + + + ); } diff --git a/src/app/modules/home-module/__test__/reportCard.test.tsx b/src/app/modules/home-module/__test__/reportCard.test.tsx index a750f641c..4eee3d48e 100644 --- a/src/app/modules/home-module/__test__/reportCard.test.tsx +++ b/src/app/modules/home-module/__test__/reportCard.test.tsx @@ -1,25 +1,26 @@ import { Auth0Provider } from "@auth0/auth0-react"; -import { render } from "@testing-library/react"; import userEvent, { PointerEventsCheckLevel, } from "@testing-library/user-event"; -import { screen } from "@testing-library/react"; +import { screen, render } from "@testing-library/react"; import { mockUseAuth0 } from "app/utils/mockAuth0"; import { Router } from "react-router-dom"; import { createMemoryHistory } from "history"; import GridItem from "app/modules/home-module/components/AssetCollection/Reports/gridItem"; +import { ContentState, EditorState } from "draft-js"; interface MockProps { date: Date; id?: string; title: string; - descr: string; + name: string; color: string; viz: JSX.Element; handleDelete?: jest.Mock; handleDuplicate?: jest.Mock; showMenuButton: boolean; owner: string; + heading: EditorState; } let mockLoginStatus = true; @@ -41,14 +42,17 @@ const defaultProps = (newProps: Partial = {}): MockProps => { return { date: "2021-08-13", id: "report-id", - title: "report-title", - descr: "report-description", + heading: EditorState.createWithContent( + ContentState.createFromText("report-title") + ), + name: "report-description", color: "#ffffff", viz:
report
, handleDelete: jest.fn(), handleDuplicate: jest.fn(), showMenuButton: true, owner: "auth0|123", + ...newProps, } as MockProps; }; diff --git a/src/app/modules/home-module/components/AssetCollection/All/assetsGrid.tsx b/src/app/modules/home-module/components/AssetCollection/All/assetsGrid.tsx index d67a3e0a9..eed5d3f01 100644 --- a/src/app/modules/home-module/components/AssetCollection/All/assetsGrid.tsx +++ b/src/app/modules/home-module/components/AssetCollection/All/assetsGrid.tsx @@ -19,10 +19,12 @@ import ReportGridItem from "app/modules/home-module/components/AssetCollection/R import ColoredReportIcon from "app/assets/icons/ColoredReportIcon"; import DeleteDatasetDialog from "app/components/Dialogs/deleteDatasetDialog"; import DeleteReportDialog from "app/components/Dialogs/deleteReportDialog"; -import { HomepageTable } from "../../Table"; +import { EditorState, convertFromRaw } from "draft-js"; +import { DatasetListItemAPIModel } from "app/modules/dataset-module/data"; +import { HomepageTable } from "app/modules/home-module/components/Table/"; import { planDialogAtom } from "app/state/recoil/atoms"; import { useSetRecoilState } from "recoil"; -import { getColumns } from "./data"; +import { getColumns } from "app/modules/home-module/components/AssetCollection/All/data"; interface Props { sortBy: string; @@ -283,11 +285,12 @@ export default function AssetsGrid(props: Props) { type: data.assetType, }; } - return { id: data.id, name: data.name, - description: data.title, + heading: data.heading + ? EditorState.createWithContent(convertFromRaw(data.heading)) + : EditorState.createEmpty(), createdDate: data.createdDate, type: data.assetType, }; @@ -342,7 +345,7 @@ export default function AssetsGrid(props: Props) { } color={d.backgroundColor} @@ -353,7 +356,13 @@ export default function AssetsGrid(props: Props) { handleDuplicate={() => handleDuplicate(d.id, d.assetType as assetType) } - title={d.title || d.name} + heading={ + d.heading + ? EditorState.createWithContent( + convertFromRaw(d.heading) + ) + : EditorState.createEmpty() + } owner={d.owner} /> ), diff --git a/src/app/modules/home-module/components/AssetCollection/Charts/chartsGrid.tsx b/src/app/modules/home-module/components/AssetCollection/Charts/chartsGrid.tsx index 653b75381..224d3be19 100644 --- a/src/app/modules/home-module/components/AssetCollection/Charts/chartsGrid.tsx +++ b/src/app/modules/home-module/components/AssetCollection/Charts/chartsGrid.tsx @@ -27,6 +27,16 @@ interface Props { addCard?: boolean; } +export interface IChartAsset { + id: string; + name: string; + createdDate: Date; + vizType: string; + isMappingValid: boolean; + owner: string; + isAIAssisted: boolean; +} + export default function ChartsGrid(props: Props) { const observerTarget = React.useRef(null); const [chartId, setChartId] = React.useState(""); @@ -43,7 +53,7 @@ export default function ChartsGrid(props: Props) { const setPlanDialog = useSetRecoilState(planDialogAtom); const charts = useStoreState( - (state) => (state.charts.ChartGetList.crudData ?? []) as any[] + (state) => (state.charts.ChartGetList.crudData ?? []) as IChartAsset[] ); const loadChartsCount = useStoreActions( (actions) => actions.charts.ChartsCount.fetch diff --git a/src/app/modules/home-module/components/AssetCollection/Reports/gridItem.tsx b/src/app/modules/home-module/components/AssetCollection/Reports/gridItem.tsx index eea01a0a2..8484790e7 100644 --- a/src/app/modules/home-module/components/AssetCollection/Reports/gridItem.tsx +++ b/src/app/modules/home-module/components/AssetCollection/Reports/gridItem.tsx @@ -9,13 +9,14 @@ import { ReactComponent as EditIcon } from "app/modules/home-module/assets/edit. import { ReactComponent as DeleteIcon } from "app/modules/home-module/assets/delete.svg"; import { ReactComponent as ClockIcon } from "app/modules/home-module/assets/clock-icon.svg"; import { ReactComponent as DuplicateIcon } from "app/modules/home-module/assets/duplicate.svg"; +import { EditorState } from "draft-js"; import { useMediaQuery } from "@material-ui/core"; interface Props { date: Date; id?: string; - title: string; - descr: string; + heading: EditorState; + name: string; color: string; viz: JSX.Element; handleDelete?: (id: string) => void; @@ -24,7 +25,7 @@ interface Props { owner: string; } -export default function gridItem(props: Props) { +export default function GridItem(props: Readonly) { const { user, isAuthenticated } = useAuth0(); const [menuOptionsDisplay, setMenuOptionsDisplay] = React.useState(false); const isMobile = useMediaQuery("(max-width: 767px)"); @@ -86,7 +87,7 @@ export default function gridItem(props: Props) { `} >

- {props.title} + {props.heading.getCurrentContent().getPlainText()}

- {props.descr} + {props.name}

) { const observerTarget = React.useRef(null); const [cardId, setCardId] = React.useState(""); const [modalDisplay, setModalDisplay] = React.useState(false); @@ -220,14 +221,20 @@ export default function ReportsGrid(props: Props) { } color={data.backgroundColor} showMenuButton={props.showMenuButton} handleDelete={() => handleModal(data.id)} handleDuplicate={() => handleDuplicate(data.id)} - title={data.title || data.name} + heading={ + data.heading + ? EditorState.createWithContent( + convertFromRaw(data.heading) + ) + : EditorState.createEmpty() + } owner={data.owner} /> @@ -245,6 +252,11 @@ export default function ReportsGrid(props: Props) { ], data: loadedReports.map((data) => ({ ...data, + description: data.heading + ? EditorState.createWithContent(convertFromRaw(data.heading)) + .getCurrentContent() + .getPlainText() + : "", type: "report", })), }} diff --git a/src/app/modules/home-module/components/Table/index.tsx b/src/app/modules/home-module/components/Table/index.tsx index db9060ab3..84a63578d 100644 --- a/src/app/modules/home-module/components/Table/index.tsx +++ b/src/app/modules/home-module/components/Table/index.tsx @@ -12,19 +12,21 @@ import { isValidDate } from "app/utils/isValidDate"; interface IData { id: string; name: string; - description: string; + description?: string; createdDate: Date; type: string; } -export function HomepageTable(props: { - inChartBuilder?: boolean; - onItemClick?: (v: string) => void; - all?: boolean; - tableData: { - columns: { key: string; label: string; icon?: React.ReactNode }[]; - data: any[]; - }; -}) { +export function HomepageTable( + props: Readonly<{ + inChartBuilder?: boolean; + onItemClick?: (v: string) => void; + all?: boolean; + tableData: { + columns: { key: string; label: string; icon?: React.ReactNode }[]; + data: any[]; + }; + }> +) { const history = useHistory(); const getDestinationPath = (data: IData) => { diff --git a/src/app/modules/report-module/__test__/headerBlock.test.tsx b/src/app/modules/report-module/__test__/headerBlock.test.tsx index 12a4387ff..10380eb7c 100644 --- a/src/app/modules/report-module/__test__/headerBlock.test.tsx +++ b/src/app/modules/report-module/__test__/headerBlock.test.tsx @@ -1,13 +1,7 @@ -import { - act, - fireEvent, - render, - screen, - waitFor, -} from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import HeaderBlock from "app/modules/report-module/sub-module/components/headerBlock/"; -import { EditorState } from "draft-js"; +import HeaderBlock from "app/modules/report-module/components/headerBlock"; +import { ContentState, EditorState } from "draft-js"; import { ToolbarPluginsType } from "app/modules/report-module/components/reportSubHeaderToolbar/staticToolbar"; import Router from "react-router-dom"; import { MutableSnapshot, RecoilRoot } from "recoil"; @@ -18,6 +12,7 @@ import { DndProvider, useDrag } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; interface MockProps { + isToolboxOpen: boolean; previewMode: boolean; hasSubHeaderTitleFocused?: boolean; setHasSubHeaderTitleFocused?: React.Dispatch>; @@ -29,6 +24,7 @@ interface MockProps { title: string; showHeader: boolean; description: EditorState; + heading: EditorState; createdDate: Date; backgroundColor: string; titleColor: string; @@ -40,6 +36,7 @@ interface MockProps { const headerDetailsResult = { headerDetails: { title: "", + heading: EditorState.createEmpty(), description: EditorState.createEmpty(), backgroundColor: "", titleColor: "", @@ -64,6 +61,7 @@ jest.mock("react-router-dom", () => ({ const defaultProps = (props: Partial): MockProps => { return { + isToolboxOpen: false, previewMode: false, hasSubHeaderTitleFocused: false, setHasSubHeaderTitleFocused: jest.fn(), @@ -73,6 +71,9 @@ const defaultProps = (props: Partial): MockProps => { headerDetails: { title: "Test Title", showHeader: true, + heading: EditorState.createWithContent( + ContentState.createFromText("heading") + ), description: EditorState.createEmpty(), createdDate: new Date(), backgroundColor: "#fff", @@ -144,6 +145,7 @@ const appSetup = (newProps: Partial = {}) => { }; test("title input should be visible and editable", async () => { + const user = userEvent.setup(); jest .spyOn(Router, "useParams") .mockReturnValue({ page: "12345", view: "edit" }); @@ -155,14 +157,11 @@ test("title input should be visible and editable", async () => { jest.spyOn(window, "scrollTo").mockImplementation(() => {}); const { app, props } = appSetup(); render(app); - expect(screen.getByPlaceholderText("Add a header title")).toBeEnabled(); - fireEvent.change(screen.getByPlaceholderText("Add a header title"), { - target: { value: "Test Tite" }, - }); - expect(props.setHeaderDetails).toHaveBeenCalledWith( - expect.objectContaining({ title: "Test Tite" }) - ); - expect(headerDetailsResult.headerDetails.title).toBe("Test Tite"); + expect(screen.getByText("heading")).toBeEnabled(); + await user.type(screen.getByText("heading"), "Test Tite"); + expect( + headerDetailsResult.headerDetails.heading.getCurrentContent().getPlainText() + ).toBe("headingTest Tite"); }); test("focusing on description input should clear placeholder", async () => { diff --git a/src/app/modules/report-module/__test__/reportSubheaderToolbar.test.tsx b/src/app/modules/report-module/__test__/reportSubheaderToolbar.test.tsx index f32d02382..ab1733f65 100644 --- a/src/app/modules/report-module/__test__/reportSubheaderToolbar.test.tsx +++ b/src/app/modules/report-module/__test__/reportSubheaderToolbar.test.tsx @@ -42,7 +42,7 @@ interface MockProps { isSaveEnabled?: boolean; rawViz?: any; setHasSubHeaderTitleFocused?: (value: boolean) => void; - setHasSubHeaderTitleBlurred?: (value: boolean) => void; + setHasReportNameFocused?: (value: boolean) => void; plugins: ToolbarPluginsType; isEditorFocused: boolean; headerDetails: IHeaderDetails; @@ -94,7 +94,7 @@ const defaultProps = (props: Partial = {}): MockProps => { isSaveEnabled: false, rawViz: {}, setHasSubHeaderTitleFocused: jest.fn(), - setHasSubHeaderTitleBlurred: jest.fn(), + setHasReportNameFocused: jest.fn(), plugins: {} as ToolbarPluginsType, isEditorFocused: false, headerDetails: {} as IHeaderDetails, @@ -170,17 +170,17 @@ describe("Tests for tablet and desktop view", () => { beforeEach(() => { setMediaQueryForTest(768); }); - test("focusing on input should call setHasSubHeaderTitleFocused", async () => { + test("focusing on input should call setHasReportNameFocused", async () => { jest .spyOn(Router, "useParams") .mockReturnValue({ page: "65dcb26aaf4c8500693f1ab7", view: "edit" }); const { app, props } = appSetup({ mockActions: false }); render(app); screen.getByRole("textbox").focus(); - expect(props.setHasSubHeaderTitleFocused).toHaveBeenCalledWith(true); + expect(props.setHasReportNameFocused).toHaveBeenCalledWith(true); }); - test("blurring on input should call setHasSubHeaderTitleBlurred", async () => { + test("blurring on input should call setHasReportNameFocused", async () => { jest .spyOn(Router, "useParams") .mockReturnValue({ page: "65dcb26aaf4c8500693f1ab7", view: "edit" }); @@ -188,7 +188,7 @@ describe("Tests for tablet and desktop view", () => { render(app); screen.getByRole("textbox").focus(); screen.getByRole("textbox").blur(); - expect(props.setHasSubHeaderTitleBlurred).toHaveBeenCalledWith(true); + expect(props.setHasReportNameFocused).toHaveBeenCalledWith(true); }); test("clicking on input when value is Untitled report should clear the input", async () => { diff --git a/src/app/modules/report-module/sub-module/components/headerBlock/index.tsx b/src/app/modules/report-module/components/headerBlock/index.tsx similarity index 53% rename from src/app/modules/report-module/sub-module/components/headerBlock/index.tsx rename to src/app/modules/report-module/components/headerBlock/index.tsx index 77872cda0..e870a375c 100644 --- a/src/app/modules/report-module/sub-module/components/headerBlock/index.tsx +++ b/src/app/modules/report-module/components/headerBlock/index.tsx @@ -1,7 +1,7 @@ import React from "react"; import get from "lodash/get"; import { useDrop } from "react-dnd"; -import { EditorState } from "draft-js"; +import { ContentState, EditorState, convertToRaw } from "draft-js"; import { useRecoilState } from "recoil"; import Box from "@material-ui/core/Box"; import Container from "@material-ui/core/Container"; @@ -11,56 +11,45 @@ import { reportRightPanelViewAtom } from "app/state/recoil/atoms"; import { RichEditor } from "app/modules/common/RichEditor"; import { ReactComponent as EditIcon } from "app/modules/report-module/asset/editIcon.svg"; import { ReactComponent as DeleteIcon } from "app/modules/report-module/asset/deleteIcon.svg"; -import { headerBlockcss } from "app/modules/report-module/sub-module/components/headerBlock/style"; +import { headerBlockcss } from "app/modules/report-module/components/headerBlock/style"; import { ReactComponent as HeaderHandlesvg } from "app/modules/report-module/asset/header-handle.svg"; import { Tooltip } from "@material-ui/core"; import useDebounce from "react-use/lib/useDebounce"; import { ToolbarPluginsType } from "app/modules/report-module/components/reportSubHeaderToolbar/staticToolbar"; +import { IHeaderDetails } from "app/modules/report-module/components/right-panel/data"; interface Props { + isToolboxOpen: boolean; previewMode: boolean; - hasSubHeaderTitleFocused?: boolean; - setHasSubHeaderTitleFocused?: React.Dispatch>; + hasReportNameFocused?: boolean; + isReportHeadingModified?: boolean; + sethasReportNameFocused?: React.Dispatch>; setReportName?: React.Dispatch>; reportName?: string; handleRightPanelOpen: () => void; setPlugins: React.Dispatch>; - headerDetails: { - title: string; - showHeader: boolean; - description: EditorState; - backgroundColor: string; - titleColor: string; - descriptionColor: string; - dateColor: string; - }; - setHeaderDetails: React.Dispatch< - React.SetStateAction<{ - title: string; - showHeader: boolean; - description: EditorState; - backgroundColor: string; - titleColor: string; - descriptionColor: string; - dateColor: string; - }> - >; + headerDetails: IHeaderDetails; + setHeaderDetails: React.Dispatch>; } export default function HeaderBlock(props: Props) { const location = useLocation(); const { page } = useParams<{ page: string }>(); - const inputRef = React.useRef(null); const [currentView, setCurrentView] = useRecoilState( reportRightPanelViewAtom ); const [handleDisplay, setHandleDisplay] = React.useState(false); - const placeholder = "Add a header description"; + const descriptionPlaceholder = "Add a header description"; + const headingPlaceholder = "Add a header title"; + const [headingPlaceholderState, setHeadingPlaceholderState] = + React.useState(headingPlaceholder); + const [charCount, setCharCount] = React.useState(null); + const [maxCharCount, setMaxCharCount] = React.useState(50); + const [isHeadingFocused, setIsHeadingFocused] = React.useState(true); + const [isDescriptionFocused, setIsDescriptionFocused] = React.useState(false); + const [updateCharCount, setUpdateCharCount] = React.useState(false); const [descriptionPlaceholderState, setDescriptionPlaceholderState] = - React.useState(placeholder); - - const [isReportTitleModified, setIsReportTitleModified] = - React.useState(false); + React.useState(descriptionPlaceholder); const viewOnlyMode = page !== "new" && get(location.pathname.split("/"), "[3]", "") !== "edit"; @@ -72,19 +61,26 @@ export default function HeaderBlock(props: Props) { }; React.useEffect(() => { - inputRef.current?.focus(); - }, []); + const plainText = getPlainTextFromEditorState(props.headerDetails.heading); + if (charCount === null && props.headerDetails.isUpdated) { + setCharCount(plainText.length); + setUpdateCharCount(true); + setMaxCharCount(50); + } + }, [props.headerDetails.isUpdated]); //handles report name state const [,] = useDebounce( () => { - // checks when headerDetails.title is empty and report title has not been focused - if (!props.hasSubHeaderTitleFocused && isReportTitleModified) { - props.setReportName?.(props.headerDetails.title); + // checks when headerDetails.heading is empty and report heading has not been focused + if (!props.hasReportNameFocused && props.isReportHeadingModified) { + props.setReportName?.( + props.headerDetails.heading.getCurrentContent().getPlainText() + ); } }, 500, - [props.headerDetails.title] + [props.headerDetails.heading] ); const [{ isOver }, drop] = useDrop(() => ({ @@ -101,24 +97,47 @@ export default function HeaderBlock(props: Props) { }); }, })); - - const setDescriptionContent = (text: EditorState) => { - props.setHeaderDetails({ - ...props.headerDetails, - description: text, - }); + const getPlainTextFromEditorState = (text: EditorState) => { + return text.getCurrentContent().getPlainText(); }; - const handleChange = ( - event: React.ChangeEvent + const setTextContent = ( + text: EditorState, + propsState: EditorState, + type: "heading" | "description" ) => { - const { name, value } = event.target; - props.setHeaderDetails({ - ...props.headerDetails, - [name]: value, - }); - if (name == "title") { - setIsReportTitleModified(true); + let max = type === "heading" ? 50 : 250; + setMaxCharCount(max); + const plainText = getPlainTextFromEditorState(text); + const plainDescr = getPlainTextFromEditorState(propsState); + if (updateCharCount) { + setCharCount(plainText.length); + } + if (plainText.length <= max) { + if (type === "heading") { + props.setHeaderDetails({ + ...props.headerDetails, + heading: text, + }); + } else { + props.setHeaderDetails({ + ...props.headerDetails, + description: text, + }); + } + } else { + if (type === "heading") { + props.setHeaderDetails({ + ...props.headerDetails, + heading: EditorState.moveFocusToEnd(propsState), + }); + } else { + props.setHeaderDetails({ + ...props.headerDetails, + description: EditorState.moveFocusToEnd(propsState), + }); + } + setCharCount(plainDescr.length); } }; @@ -152,11 +171,28 @@ export default function HeaderBlock(props: Props) { return (
+
+ {charCount} / {maxCharCount} +
{(handleDisplay || currentView === "editHeader") && (
-
- div { + padding: 0; + .public-DraftEditorPlaceholder-inner { + position: absolute; + color: ${props.headerDetails.titleColor}; + opacity: 0.5; + font-size: 29px; !important; + font-family: "GothamNarrow-Bold", sans-serif; + } + span { + font-family: "GothamNarrow-Bold", sans-serif; + } + > div { + > div { + > div { + font-family: "GothamNarrow-Bold", sans-serif; + + min-height: 32px !important; + } + } + } + } + `} + > + + setTextContent(text, props.headerDetails.heading, "heading") + } + placeholder={headingPlaceholder} + placeholderState={headingPlaceholderState} + setPlaceholderState={setHeadingPlaceholderState} + textContent={props.headerDetails.heading} + setPlugins={props.setPlugins} + focusOnMount + onBlur={() => { + setIsHeadingFocused(false); + if (!props.isReportHeadingModified && props.reportName === "") { + props.setReportName?.("Untitled Report"); + } + }} + onFocus={() => { + setIsHeadingFocused(true); + }} + testId="heading-rich-text-editor" />
@@ -298,12 +386,25 @@ export default function HeaderBlock(props: Props) { + setTextContent( + text, + props.headerDetails.description, + "description" + ) + } + placeholder={descriptionPlaceholder} placeholderState={descriptionPlaceholderState} setPlaceholderState={setDescriptionPlaceholderState} textContent={props.headerDetails.description} setPlugins={props.setPlugins} + onBlur={() => { + setIsDescriptionFocused(false); + }} + onFocus={() => { + setIsDescriptionFocused(true); + }} + testId="description-rich-text-editor" />
diff --git a/src/app/modules/report-module/sub-module/components/headerBlock/style.ts b/src/app/modules/report-module/components/headerBlock/style.ts similarity index 87% rename from src/app/modules/report-module/sub-module/components/headerBlock/style.ts rename to src/app/modules/report-module/components/headerBlock/style.ts index ca70ea4ea..9f6836085 100644 --- a/src/app/modules/report-module/sub-module/components/headerBlock/style.ts +++ b/src/app/modules/report-module/components/headerBlock/style.ts @@ -1,8 +1,9 @@ import { css } from "styled-components/macro"; export const headerBlockcss = { - container: (backgroundColor: string) => css` + container: (backgroundColor: string, istoolboxOpen: boolean) => css` width: 100%; + transition: width 225ms cubic-bezier(0, 0, 0.2, 1) 0ms; height: 215px; padding: 35px 0; position: relative; diff --git a/src/app/modules/report-module/components/placeholder/index.tsx b/src/app/modules/report-module/components/placeholder/index.tsx new file mode 100644 index 000000000..0ec0e5671 --- /dev/null +++ b/src/app/modules/report-module/components/placeholder/index.tsx @@ -0,0 +1,134 @@ +import React from "react"; +import { PlaceholderProps } from "app/modules/report-module/views/create/data"; +import { ReportElementsType } from "app/modules/report-module/components/right-panel-create-view"; +import { useDrop } from "react-dnd"; +import { isDividerOrRowFrameDraggingAtom } from "app/state/recoil/atoms"; +import { useRecoilValue } from "recoil"; +import { v4 } from "uuid"; + +const PlaceHolder = (props: PlaceholderProps) => { + const moveCard = React.useCallback((itemId: string) => { + props.updateFramesArray((draft) => { + const dragIndex = draft.findIndex((frame) => frame.id === itemId); + + const dropIndex = + props.index ?? draft.findIndex((frame) => frame.id === props.rowId) + 1; + + const fakeId = v4(); + const tempItem = draft[dragIndex]; + draft[dragIndex].id = fakeId; + + draft.splice(dropIndex, 0, tempItem); + const fakeIndex = draft.findIndex((frame) => frame.id === fakeId); + draft.splice(fakeIndex, 1); + }); + }, []); + const [{ isOver, handlerId, item: dragItem }, drop] = useDrop(() => ({ + // The type (or types) to accept - strings or symbols + accept: [ + ReportElementsType.DIVIDER, + ReportElementsType.ROWFRAME, + ReportElementsType.ROW, + ], + // Props to collect + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + item: monitor.getItem(), + handlerId: monitor.getHandlerId(), + }), + drop: (item: any, monitor) => { + if (item.type === ReportElementsType.ROW) { + moveCard(item.id); + } else { + props.updateFramesArray((draft) => { + const tempIndex = + props.index ?? + draft.findIndex((frame) => frame.id === props.rowId) + 1; + + const id = v4(); + draft.splice(tempIndex, 0, { + id, + frame: { + rowId: id, + rowIndex: tempIndex, + + type: item.type, + }, + content: + item.type === ReportElementsType.ROWFRAME ? [] : ["divider"], + contentWidths: [], + contentHeights: [], + contentTypes: + item.type === ReportElementsType.ROWFRAME ? [] : ["divider"], + structure: null, + }); + }); + } + }, + })); + + const isItemDragging = useRecoilValue(isDividerOrRowFrameDraggingAtom); + + const itemDragIndex = props.framesArray.findIndex( + (frame) => frame.id === isItemDragging.rowId + ); + + const placeholderIndex = + props.index ?? + props.framesArray.findIndex((frame) => frame.id === props.rowId) + 1; + + const dragIndex = props.framesArray.findIndex( + (frame) => frame.id === (dragItem as any)?.id + ); + + const placeholderActive = () => { + if (isOver) { + if (dragIndex === -1) { + return true; + } + if (placeholderIndex === dragIndex) { + return false; + } + if (placeholderIndex - 1 === dragIndex) { + return false; + } + return true; + } + return false; + }; + + const isDroppable = () => { + if (isItemDragging.state) { + if (itemDragIndex === -1) { + return true; + } + if (placeholderIndex === itemDragIndex) { + return false; + } + if (placeholderIndex - 1 === itemDragIndex) { + return false; + } + return true; + } + return false; + }; + + return ( +
+ ); +}; + +export default PlaceHolder; diff --git a/src/app/modules/report-module/components/reportSubHeaderToolbar/index.tsx b/src/app/modules/report-module/components/reportSubHeaderToolbar/index.tsx index 3d05ce826..76acdbd84 100644 --- a/src/app/modules/report-module/components/reportSubHeaderToolbar/index.tsx +++ b/src/app/modules/report-module/components/reportSubHeaderToolbar/index.tsx @@ -298,11 +298,11 @@ export function ReportSubheaderToolbar( }} onBlur={() => { setInputSpanVisibility(true); - props.setHasSubHeaderTitleBlurred?.(true); + props.setHasReportNameBlurred?.(true); }} onFocus={() => { - props.setHasSubHeaderTitleFocused?.(true); - props.setHasSubHeaderTitleBlurred?.(false); + props.setHasReportNameFocused?.(true); + props.setHasReportNameBlurred?.(false); setInputSpanVisibility(false); }} disabled={props.isPreviewView} diff --git a/src/app/modules/report-module/components/right-panel-create-view/index.tsx b/src/app/modules/report-module/components/right-panel-create-view/index.tsx index dc9a1b39c..7c459eef2 100644 --- a/src/app/modules/report-module/components/right-panel-create-view/index.tsx +++ b/src/app/modules/report-module/components/right-panel-create-view/index.tsx @@ -59,17 +59,9 @@ import { useSearchMediaSources } from "app/hooks/useSearchMediaSources"; import { useDebounce } from "react-use"; import Skeleton from "@material-ui/lab/Skeleton"; import { useInfinityScroll } from "app/hooks/useInfinityScroll"; +import { IHeaderDetails } from "app/modules/report-module/components/right-panel/data"; import { useCheckUserPlan } from "app/hooks/useCheckUserPlan"; -interface IHeaderDetails { - title: string; - showHeader: boolean; - description: EditorState; - backgroundColor: string; - titleColor: string; - descriptionColor: string; - dateColor: string; -} interface Props { showHeaderItem: boolean; headerDetails: IHeaderDetails; diff --git a/src/app/modules/report-module/components/right-panel/data.ts b/src/app/modules/report-module/components/right-panel/data.ts index 197cfbe25..bbb35a3f6 100644 --- a/src/app/modules/report-module/components/right-panel/data.ts +++ b/src/app/modules/report-module/components/right-panel/data.ts @@ -3,12 +3,14 @@ import { IFramesArray } from "../../views/create/data"; export interface IHeaderDetails { title: string; + heading: EditorState; showHeader: boolean; description: EditorState; backgroundColor: string; titleColor: string; descriptionColor: string; dateColor: string; + isUpdated?: boolean; } export interface ReportRightPanelProps { open: boolean; diff --git a/src/app/modules/report-module/sub-module/rowStructure/addRowFrameButton.tsx b/src/app/modules/report-module/components/rowStructure/addRowFrameButton.tsx similarity index 100% rename from src/app/modules/report-module/sub-module/rowStructure/addRowFrameButton.tsx rename to src/app/modules/report-module/components/rowStructure/addRowFrameButton.tsx diff --git a/src/app/modules/report-module/sub-module/rowStructure/index.tsx b/src/app/modules/report-module/components/rowStructure/index.tsx similarity index 99% rename from src/app/modules/report-module/sub-module/rowStructure/index.tsx rename to src/app/modules/report-module/components/rowStructure/index.tsx index ce2ac197e..6dddbeda5 100644 --- a/src/app/modules/report-module/sub-module/rowStructure/index.tsx +++ b/src/app/modules/report-module/components/rowStructure/index.tsx @@ -5,7 +5,7 @@ import { useUpdateEffect } from "react-use"; import IconButton from "@material-ui/core/IconButton"; import { useHistory, useLocation, useParams } from "react-router-dom"; import { itemSpacing, containerGap } from "app/modules/report-module/data"; -import RowstructureDisplay from "app/modules/report-module/sub-module/rowStructure/rowStructureDisplay"; +import RowstructureDisplay from "app/modules/report-module/components/rowStructure/rowStructureDisplay"; import { ReactComponent as CloseIcon } from "app/modules/report-module/asset/closeIcon.svg"; import { ReactComponent as DeleteIcon } from "app/modules/report-module/asset/deleteIcon.svg"; import { @@ -15,7 +15,7 @@ import { import { blockcss, containercss, -} from "app/modules/report-module/sub-module/rowStructure/style"; +} from "app/modules/report-module/components/rowStructure/style"; import { IFramesArray } from "app/modules/report-module/views/create/data"; import { useOnClickOutside } from "usehooks-ts"; import { ToolbarPluginsType } from "app/modules/report-module/components/reportSubHeaderToolbar/staticToolbar"; diff --git a/src/app/modules/report-module/sub-module/rowStructure/rowStructureDisplay.tsx b/src/app/modules/report-module/components/rowStructure/rowStructureDisplay.tsx similarity index 99% rename from src/app/modules/report-module/sub-module/rowStructure/rowStructureDisplay.tsx rename to src/app/modules/report-module/components/rowStructure/rowStructureDisplay.tsx index 8b9d9cae4..39fe30fe7 100644 --- a/src/app/modules/report-module/sub-module/rowStructure/rowStructureDisplay.tsx +++ b/src/app/modules/report-module/components/rowStructure/rowStructureDisplay.tsx @@ -604,6 +604,7 @@ const Box = (props: { placeholder={placeholder} setPlaceholderState={setTextPlaceholderState} placeholderState={textPlaceholderState} + testId="report-rich-text-editor" />
diff --git a/src/app/modules/report-module/sub-module/rowStructure/style.ts b/src/app/modules/report-module/components/rowStructure/style.ts similarity index 100% rename from src/app/modules/report-module/sub-module/rowStructure/style.ts rename to src/app/modules/report-module/components/rowStructure/style.ts diff --git a/src/app/modules/report-module/data.ts b/src/app/modules/report-module/data.ts index 141b87c6d..b57642220 100644 --- a/src/app/modules/report-module/data.ts +++ b/src/app/modules/report-module/data.ts @@ -6,7 +6,8 @@ export interface ReportModel { title: string; public: boolean; showHeader: boolean; - subTitle: RawDraftContentState; + description: RawDraftContentState; + heading: RawDraftContentState; rows: { structure: | null @@ -32,6 +33,7 @@ export interface ReportModel { descriptionColor: string; owner: string; dateColor: string; + isUpdated: boolean; } export const emptyReport: ReportModel = { @@ -39,7 +41,8 @@ export const emptyReport: ReportModel = { name: "Untitled report", title: "", public: false, - subTitle: convertToRaw(EditorState.createEmpty().getCurrentContent()), + description: convertToRaw(EditorState.createEmpty().getCurrentContent()), + heading: convertToRaw(EditorState.createEmpty().getCurrentContent()), showHeader: true, rows: [], createdDate: new Date(), @@ -48,6 +51,7 @@ export const emptyReport: ReportModel = { descriptionColor: "#ffffff", owner: "", dateColor: "#ffffff", + isUpdated: false, }; export const itemSpacing = "30px"; diff --git a/src/app/modules/report-module/index.tsx b/src/app/modules/report-module/index.tsx index 1e5bbfc63..1619f55f4 100644 --- a/src/app/modules/report-module/index.tsx +++ b/src/app/modules/report-module/index.tsx @@ -81,10 +81,8 @@ export default function ReportModule() { const [rightPanelOpen, setRightPanelOpen] = React.useState(true); const [reportName, setReportName] = React.useState("Untitled report"); - const [hasSubHeaderTitleFocused, setHasSubHeaderTitleFocused] = - React.useState(false); - const [hasSubHeaderTitleBlurred, setHasSubHeaderTitleBlurred] = - React.useState(false); + const [hasReportNameFocused, setHasReportNameFocused] = React.useState(false); + const [hasReportNameBlurred, setHasReportNameBlurred] = React.useState(false); const [reportType, setReportType] = React.useState< "basic" | "advanced" | "ai" | null @@ -167,6 +165,7 @@ export default function ReportModule() { const [headerDetails, setHeaderDetails] = React.useState({ title: "", description: EditorState.createEmpty(), + heading: EditorState.createEmpty(), showHeader: true, backgroundColor: "#252c34", titleColor: "#ffffff", @@ -179,18 +178,17 @@ export default function ReportModule() { React.useEffect(() => { //set report name back to untitled report if it is empty and user is not focused on subheader title - if (reportName === "" && hasSubHeaderTitleBlurred) { + if (reportName === "" && hasReportNameBlurred) { setReportName("Untitled report"); } return () => { - setHasSubHeaderTitleBlurred(false); + setHasReportNameBlurred(false); }; - }, [hasSubHeaderTitleBlurred]); + }, [hasReportNameBlurred]); const deleteFrame = (id: string) => { updateFramesArray((draft) => { const frameId = draft.findIndex((frame) => frame.id === id); - draft.splice(frameId, 1); }); }; @@ -306,6 +304,9 @@ export default function ReportModule() { reportName: "Untitled report", headerDetails: { title: "", + heading: JSON.stringify( + convertToRaw(EditorState.createEmpty().getCurrentContent()) + ), description: JSON.stringify( convertToRaw(EditorState.createEmpty().getCurrentContent()) ), @@ -321,6 +322,7 @@ export default function ReportModule() { setHeaderDetails({ title: "", + heading: EditorState.createEmpty(), description: EditorState.createEmpty(), showHeader: true, backgroundColor: "#252c34", @@ -345,7 +347,12 @@ export default function ReportModule() { authId: user?.sub, showHeader: headerDetails.showHeader, title: headerDetails.showHeader ? headerDetails.title : undefined, - subTitle: convertToRaw( + heading: convertToRaw( + headerDetails.showHeader + ? headerDetails.heading.getCurrentContent() + : EditorState.createEmpty().getCurrentContent() + ), + description: convertToRaw( headerDetails.showHeader ? headerDetails.description.getCurrentContent() : EditorState.createEmpty().getCurrentContent() @@ -426,20 +433,18 @@ export default function ReportModule() { }, [user, isAuthenticated, reportGetData]); const showReportHeader = view === "edit" ? canEditDeleteReport : true; - return ( {!reportError401 && showReportHeader && - view !== "ai-template" && - view !== "initial" && ( + (view === "edit" || view === undefined) && ( )} - {view && - !reportError401 && - view !== "preview" && - canEditDeleteReport && - view !== "initial" && - view !== "ai-template" && ( - setRightPanelOpen(true)} - onClose={() => setRightPanelOpen(false)} - showHeaderItem={!headerDetails.showHeader} - framesArray={framesArray} - reportName={reportName} - onSave={onSave} - /> - )} -
+ {view && !reportError401 && view === "edit" && canEditDeleteReport && ( + setRightPanelOpen(true)} + onClose={() => setRightPanelOpen(false)} + showHeaderItem={!headerDetails.showHeader} + framesArray={framesArray} + reportName={reportName} + onSave={onSave} + /> + )} + - + +
- + - - setRightPanelOpen(true)} - view={view} - setReportName={setReportName} - reportName={reportName} - deleteFrame={deleteFrame} - hasSubHeaderTitleFocused={hasSubHeaderTitleFocused} - reportType={reportType} - framesArray={framesArray} - headerDetails={headerDetails} - updateFramesArray={updateFramesArray} - setHeaderDetails={setHeaderDetails} - setPlugins={setPlugins} - onSave={onSave} + +
- - setRightPanelOpen(true)} @@ -523,27 +508,26 @@ export default function ReportModule() { stopInitializeFramesWidth={stopInitializeFramesWidth} setStopInitializeFramesWidth={setStopInitializeFramesWidth} view={view} - hasSubHeaderTitleFocused={hasSubHeaderTitleFocused} - setHasSubHeaderTitleFocused={setHasSubHeaderTitleFocused} + hasReportNameFocused={hasReportNameFocused} + setHasReportNameFocused={setHasReportNameFocused} setPlugins={setPlugins} setAutoSave={setAutoSave} isSaveEnabled={isSaveEnabled} onSave={onSave} /> - - +
- - - + diff --git a/src/app/modules/report-module/views/create/data.ts b/src/app/modules/report-module/views/create/data.ts index 7bd3fc0c1..17d5afaf1 100644 --- a/src/app/modules/report-module/views/create/data.ts +++ b/src/app/modules/report-module/views/create/data.ts @@ -1,5 +1,5 @@ -import { EditorState } from "draft-js"; import { ToolbarPluginsType } from "app/modules/report-module/components/reportSubHeaderToolbar/staticToolbar"; +import { IHeaderDetails } from "../../components/right-panel/data"; import { Updater } from "use-immer"; interface IRowFrame { @@ -35,28 +35,10 @@ export interface ReportCreateViewProps { updateFramesArray: Updater; deleteFrame: (id: string) => void; framesArray: IFramesArray[]; + hasReportNameFocused: boolean; + headerDetails: IHeaderDetails; + setHeaderDetails: React.Dispatch>; onSave: (type: "create" | "edit") => Promise; - hasSubHeaderTitleFocused: boolean; - headerDetails: { - title: string; - showHeader: boolean; - description: EditorState; - backgroundColor: string; - titleColor: string; - descriptionColor: string; - dateColor: string; - }; - setHeaderDetails: React.Dispatch< - React.SetStateAction<{ - title: string; - showHeader: boolean; - description: EditorState; - backgroundColor: string; - titleColor: string; - descriptionColor: string; - dateColor: string; - }> - >; setPlugins: React.Dispatch>; } diff --git a/src/app/modules/report-module/views/create/index.tsx b/src/app/modules/report-module/views/create/index.tsx index 983e403cf..11688a9a6 100644 --- a/src/app/modules/report-module/views/create/index.tsx +++ b/src/app/modules/report-module/views/create/index.tsx @@ -1,27 +1,22 @@ import React from "react"; import { v4 } from "uuid"; -import { useDrop } from "react-dnd"; import Box from "@material-ui/core/Box"; import Container from "@material-ui/core/Container"; import useResizeObserver from "use-resize-observer"; -import { useRecoilState, useRecoilValue } from "recoil"; +import { useRecoilState } from "recoil"; import { GridColumns } from "app/modules/report-module/components/grid-columns"; -import HeaderBlock from "app/modules/report-module/sub-module/components/headerBlock"; +import HeaderBlock from "app/modules/report-module/components/headerBlock"; import { ItemComponent } from "app/modules/report-module/components/order-container"; -import { ReportElementsType } from "app/modules/report-module/components/right-panel-create-view"; -import AddRowFrameButton from "app/modules/report-module/sub-module/rowStructure/addRowFrameButton"; -import RowFrame from "app/modules/report-module/sub-module/rowStructure"; -import { - ReportCreateViewProps, - PlaceholderProps, -} from "app/modules/report-module/views/create/data"; +import AddRowFrameButton from "app/modules/report-module/components/rowStructure/addRowFrameButton"; +import RowFrame from "app/modules/report-module/components/rowStructure"; +import { ReportCreateViewProps } from "app/modules/report-module/views/create/data"; import { IRowFrameStructure, reportContentContainerWidth, - isDividerOrRowFrameDraggingAtom, } from "app/state/recoil/atoms"; import TourGuide from "app/components/Dialogs/TourGuide"; import { useTitle } from "react-use"; +import PlaceHolder from "app/modules/report-module/components/placeholder"; function ReportCreateView(props: Readonly) { useTitle("DX Dataxplorer - Create Report"); @@ -106,12 +101,13 @@ function ReportCreateView(props: Readonly) { `} /> @@ -196,127 +192,3 @@ function ReportCreateView(props: Readonly) { } export default ReportCreateView; - -export const PlaceHolder = (props: PlaceholderProps) => { - const moveCard = React.useCallback((itemId: string) => { - props.updateFramesArray((draft) => { - const dragIndex = draft.findIndex((frame) => frame.id === itemId); - - const dropIndex = - props.index ?? draft.findIndex((frame) => frame.id === props.rowId) + 1; - - const fakeId = v4(); - const tempItem = draft[dragIndex]; - draft[dragIndex].id = fakeId; - - draft.splice(dropIndex, 0, tempItem); - const fakeIndex = draft.findIndex((frame) => frame.id === fakeId); - draft.splice(fakeIndex, 1); - }); - }, []); - const [{ isOver, handlerId, item: dragItem }, drop] = useDrop(() => ({ - // The type (or types) to accept - strings or symbols - accept: [ - ReportElementsType.DIVIDER, - ReportElementsType.ROWFRAME, - ReportElementsType.ROW, - ], - // Props to collect - collect: (monitor) => ({ - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), - item: monitor.getItem(), - handlerId: monitor.getHandlerId(), - }), - drop: (item: any, monitor) => { - if (item.type === ReportElementsType.ROW) { - moveCard(item.id); - } else { - props.updateFramesArray((draft) => { - const tempIndex = - props.index ?? - draft.findIndex((frame) => frame.id === props.rowId) + 1; - - const id = v4(); - draft.splice(tempIndex, 0, { - id, - frame: { - rowId: id, - rowIndex: tempIndex, - - type: item.type, - }, - content: - item.type === ReportElementsType.ROWFRAME ? [] : ["divider"], - contentWidths: [], - contentHeights: [], - contentTypes: - item.type === ReportElementsType.ROWFRAME ? [] : ["divider"], - structure: null, - }); - }); - } - }, - })); - - const isItemDragging = useRecoilValue(isDividerOrRowFrameDraggingAtom); - - const itemDragIndex = props.framesArray.findIndex( - (frame) => frame.id === isItemDragging.rowId - ); - - const placeholderIndex = - props.index ?? - props.framesArray.findIndex((frame) => frame.id === props.rowId) + 1; - - const dragIndex = props.framesArray.findIndex( - (frame) => frame.id === (dragItem as any)?.id - ); - - const placeholderActive = () => { - if (isOver) { - if (dragIndex === -1) { - return true; - } - if (placeholderIndex === dragIndex) { - return false; - } - if (placeholderIndex - 1 === dragIndex) { - return false; - } - return true; - } - return false; - }; - - const isDroppable = () => { - if (isItemDragging.state) { - if (itemDragIndex === -1) { - return true; - } - if (placeholderIndex === itemDragIndex) { - return false; - } - if (placeholderIndex - 1 === itemDragIndex) { - return false; - } - return true; - } - return false; - }; - - return ( -
- ); -}; diff --git a/src/app/modules/report-module/views/edit/compareStates.ts b/src/app/modules/report-module/views/edit/compareStates.ts index 4a2105b80..f4a3753f0 100644 --- a/src/app/modules/report-module/views/edit/compareStates.ts +++ b/src/app/modules/report-module/views/edit/compareStates.ts @@ -24,7 +24,7 @@ export const compareHeaderDetailsState = ( // Check if all values are the same for (const key of propsKeys) { - if (key === "description") { + if (key === "description" || key === "heading") { if ( headerDetailsProps[key].getCurrentContent().getPlainText() !== headerDetailsState[key].getCurrentContent().getPlainText() diff --git a/src/app/modules/report-module/views/edit/data.ts b/src/app/modules/report-module/views/edit/data.ts index 6d35f339d..5efcd2178 100644 --- a/src/app/modules/report-module/views/edit/data.ts +++ b/src/app/modules/report-module/views/edit/data.ts @@ -1,6 +1,6 @@ -import { EditorState } from "draft-js"; import { IFramesArray } from "app/modules/report-module/views/create/data"; import { ToolbarPluginsType } from "app/modules/report-module/components/reportSubHeaderToolbar/staticToolbar"; +import { IHeaderDetails } from "app/modules/report-module/components/right-panel/data"; import { Updater } from "use-immer"; export interface ReportEditViewProps { @@ -9,8 +9,8 @@ export interface ReportEditViewProps { reportType: "basic" | "advanced" | "ai" | null; isSaveEnabled: boolean; view: "initial" | "edit" | "create" | "preview" | "ai-template"; - hasSubHeaderTitleFocused: boolean; - setHasSubHeaderTitleFocused: React.Dispatch>; + hasReportNameFocused: boolean; + setHasReportNameFocused: React.Dispatch>; updateFramesArray: Updater; framesArray: IFramesArray[]; localPickedCharts: string[]; @@ -22,27 +22,9 @@ export interface ReportEditViewProps { }> >; reportName: string; + headerDetails: IHeaderDetails; + setHeaderDetails: React.Dispatch>; setHasChangesBeenMade: React.Dispatch>; - headerDetails: { - title: string; - showHeader: boolean; - description: EditorState; - backgroundColor: string; - titleColor: string; - descriptionColor: string; - dateColor: string; - }; - setHeaderDetails: React.Dispatch< - React.SetStateAction<{ - title: string; - showHeader: boolean; - description: EditorState; - backgroundColor: string; - titleColor: string; - descriptionColor: string; - dateColor: string; - }> - >; stopInitializeFramesWidth: boolean; setStopInitializeFramesWidth: React.Dispatch>; onSave: (type: "create" | "edit") => Promise; diff --git a/src/app/modules/report-module/views/edit/index.tsx b/src/app/modules/report-module/views/edit/index.tsx index 3a2025140..677353f7b 100644 --- a/src/app/modules/report-module/views/edit/index.tsx +++ b/src/app/modules/report-module/views/edit/index.tsx @@ -6,17 +6,16 @@ import { useParams } from "react-router-dom"; import useResizeObserver from "use-resize-observer"; import Container from "@material-ui/core/Container"; import { EditorState, RawDraftContentState, convertFromRaw } from "draft-js"; -import { useTitle, useUpdateEffect } from "react-use"; +import { useTitle } from "react-use"; import { useAuth0 } from "@auth0/auth0-react"; -import { PlaceHolder } from "app/modules/report-module/views/create"; import { useStoreActions, useStoreState } from "app/state/store/hooks"; import { ReportModel, emptyReport } from "app/modules/report-module/data"; import { ReportEditViewProps } from "app/modules/report-module/views/edit/data"; -import HeaderBlock from "app/modules/report-module/sub-module/components/headerBlock"; +import HeaderBlock from "app/modules/report-module/components/headerBlock"; import { NotAuthorizedMessageModule } from "app/modules/common/not-authorized-message"; import { ItemComponent } from "app/modules/report-module/components/order-container"; import { ReportElementsType } from "app/modules/report-module/components/right-panel-create-view"; -import AddRowFrameButton from "app/modules/report-module/sub-module/rowStructure/addRowFrameButton"; +import AddRowFrameButton from "app/modules/report-module/components/rowStructure/addRowFrameButton"; import { GridColumns } from "app/modules/report-module/components/grid-columns"; import { @@ -25,35 +24,35 @@ import { reportContentContainerWidth, } from "app/state/recoil/atoms"; import { IFramesArray } from "app/modules/report-module/views/create/data"; -import RowFrame from "app/modules/report-module/sub-module/rowStructure"; +import RowFrame from "app/modules/report-module/components/rowStructure"; import TourGuide from "app/components/Dialogs/TourGuide"; import useCookie from "@devhammed/use-cookie"; -import { get } from "lodash"; +import isEqual from "lodash/isEqual"; +import get from "lodash/get"; import { PageLoader } from "app/modules/common/page-loader"; import { handleDragOverScroll } from "app/utils/handleAutoScroll"; import { compareFramesArrayState, compareHeaderDetailsState, } from "app/modules/report-module/views/edit/compareStates"; +import PlaceHolder from "app/modules/report-module/components/placeholder"; -function ReportEditView(props: ReportEditViewProps) { +function ReportEditView(props: Readonly) { useTitle("DX Dataxplorer - Edit Report"); const { page } = useParams<{ page: string }>(); const token = useStoreState((state) => state.AuthToken.value); const { isAuthenticated, user } = useAuth0(); - const { ref, width } = useResizeObserver(); - const [tourCookie, setTourCookie] = useCookie("tourGuide", "true"); const [openTour, setOpenTour] = React.useState( tourCookie && !props.isSaveEnabled ); - const [containerWidth, setContainerWidth] = useRecoilState( reportContentContainerWidth ); - + const [isReportHeadingModified, setIsReportHeadingModified] = + React.useState(false); const [persistedReportState] = useRecoilState(persistedReportStateAtom); const [rowStructureType, setRowStructuretype] = React.useState({ @@ -196,15 +195,23 @@ function ReportEditView(props: ReportEditViewProps) { return { title: reportData.title, showHeader: reportData.showHeader, - description: reportData?.subTitle + heading: reportData?.heading + ? EditorState.moveFocusToEnd( + EditorState.createWithContent( + convertFromRaw(reportData?.heading as RawDraftContentState) + ) + ) + : EditorState.moveFocusToEnd(EditorState.createEmpty()), + description: reportData?.description ? EditorState.createWithContent( - convertFromRaw(reportData?.subTitle as RawDraftContentState) + convertFromRaw(reportData?.description as RawDraftContentState) ) : EditorState.createEmpty(), backgroundColor: reportData.backgroundColor, titleColor: reportData.titleColor, descriptionColor: reportData.descriptionColor, dateColor: reportData.dateColor, + isUpdated: true, }; }; @@ -228,12 +235,21 @@ function ReportEditView(props: ReportEditViewProps) { ) { props.setHasChangesBeenMade(true); } + if ( + !isEqual( + props.headerDetails.heading.getCurrentContent().getPlainText(), + headerDetailsFromReportData().heading.getCurrentContent().getPlainText() + ) + ) { + setIsReportHeadingModified(true); + } }; React.useEffect(() => { hasChangesBeenMadeCheck(); return () => { props.setHasChangesBeenMade(false); + setIsReportHeadingModified(false); }; }, [ props.framesArray, @@ -246,7 +262,7 @@ function ReportEditView(props: ReportEditViewProps) { if (reportData.id !== page) { return; } - props.setHasSubHeaderTitleFocused(reportData.name !== "Untitled report"); + props.setHasReportNameFocused(reportData.name !== "Untitled report"); props.setReportName(reportData.name); props.setHeaderDetails(headerDetailsFromReportData()); props.updateFramesArray(framesArrayFromReportData()); @@ -315,11 +331,13 @@ function ReportEditView(props: ReportEditViewProps) { }} reportName={reportData.name} setReportName={props.setReportName} - hasSubHeaderTitleFocused={props.hasSubHeaderTitleFocused} - setHasSubHeaderTitleFocused={props.setHasSubHeaderTitleFocused} + hasReportNameFocused={props.hasReportNameFocused} + sethasReportNameFocused={props.setHasReportNameFocused} setHeaderDetails={props.setHeaderDetails} setPlugins={props.setPlugins} + isToolboxOpen={props.rightPanelOpen} handleRightPanelOpen={props.handleRightPanelOpen} + isReportHeadingModified={isReportHeadingModified} />
- +
({ + matches: mediaQuery.match(query, { + width, + }), + addListener: () => {}, + removeListener: () => {}, + media: "", + onchange: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + }); +} +export function setMediaQueryForTest(width: number) { + window.matchMedia = createMatchMedia(width); +}