diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index e165ef415..1695b1e6b 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -65,6 +65,7 @@ export interface IExtraArgsForURLParamHooks< export interface ITableControlDataDependentArgs { isLoading?: boolean; idProperty: KeyWithValueType; + forceNumRenderedColumns?: number; } // Derived state option args @@ -112,6 +113,5 @@ export interface IUseTableControlPropsArgs< IExpansionDerivedStateArgs, IActiveRowDerivedStateArgs { currentPageItems: TItem[]; - forceNumRenderedColumns?: number; selectionState: ReturnType>; // TODO make this optional? fold it in? } diff --git a/client/src/app/pages/assessment-management/questionnaire/components/questionnaire-section-tab-title.tsx b/client/src/app/pages/assessment-management/questionnaire/components/questionnaire-section-tab-title.tsx new file mode 100644 index 000000000..571a4db32 --- /dev/null +++ b/client/src/app/pages/assessment-management/questionnaire/components/questionnaire-section-tab-title.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { TabTitleText, Badge } from "@patternfly/react-core"; +import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; +import { CustomYamlAssessmentQuestion } from "@app/api/models"; + +const QuestionnaireSectionTabTitle: React.FC<{ + isSearching: boolean; + sectionName: string; + unfilteredQuestions: CustomYamlAssessmentQuestion[]; + filteredQuestions: CustomYamlAssessmentQuestion[]; +}> = ({ isSearching, sectionName, unfilteredQuestions, filteredQuestions }) => ( + + {sectionName} +
+ + {unfilteredQuestions.length} questions + {isSearching ? ( + + {`${filteredQuestions.length} match${ + filteredQuestions.length === 1 ? "" : "es" + }`} + + ) : null} + +
+); + +export default QuestionnaireSectionTabTitle; diff --git a/client/src/app/pages/assessment-management/questionnaire/components/questions-table.tsx b/client/src/app/pages/assessment-management/questionnaire/components/questions-table.tsx index f367da0d0..361ff9e2c 100644 --- a/client/src/app/pages/assessment-management/questionnaire/components/questions-table.tsx +++ b/client/src/app/pages/assessment-management/questionnaire/components/questions-table.tsx @@ -9,29 +9,40 @@ import { ExpandableRowContent, } from "@patternfly/react-table"; import { + ConditionalTableBody, TableHeaderContentWithControls, TableRowContentWithControls, } from "@app/components/TableControls"; import { useTranslation } from "react-i18next"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; -import { CustomYamlAssessmentQuestion } from "@app/api/models"; +import { CustomYamlAssessmentQuestion, YamlAssessment } from "@app/api/models"; import { useLocalTableControls } from "@app/hooks/table-controls"; import { Label } from "@patternfly/react-core"; import AnswerTable from "./answer-table"; +import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; const QuestionsTable: React.FC<{ - fetchError: boolean; + fetchError?: Error; questions?: CustomYamlAssessmentQuestion[]; -}> = ({ fetchError = false, questions }) => { + isSearching?: boolean; + assessmentData?: YamlAssessment | null; + isAllQuestionsTab?: boolean; +}> = ({ + fetchError, + questions, + isSearching = false, + assessmentData, + isAllQuestionsTab = false, +}) => { const tableControls = useLocalTableControls({ idProperty: "formulation", items: questions || [], columnNames: { formulation: "Name", + section: "Section", }, - hasActionsColumn: true, - isSelectable: false, expandableVariant: "single", + forceNumRenderedColumns: isAllQuestionsTab ? 3 : 2, // columns+1 for expand control }); const { @@ -54,44 +65,78 @@ const QuestionsTable: React.FC<{ + {isAllQuestionsTab ? ( + + ) : null} - - {currentPageItems?.map((question, rowIndex) => ( - <> - - - - {(!!question?.include_if_tags_present?.length || - !!question?.skip_if_tags_present?.length) && ( - - )} - {question.formulation} - - - - {isCellExpanded(question) ? ( - - - - - {question.explanation} - - - - - ) : null} - - ))} - + + } + > + + {currentPageItems?.map((question, rowIndex) => { + const sectionName = + assessmentData?.sections.find((section) => + section.questions.includes(question) + )?.name || ""; + return ( + <> + + + + {(!!question?.include_if_tags_present?.length || + !!question?.skip_if_tags_present?.length) && ( + + )} + {question.formulation} + + {isAllQuestionsTab ? ( + + {sectionName} + + ) : null} + + + {isCellExpanded(question) ? ( + + + + + {question.explanation} + + + + + ) : null} + + ); + })} + + ); }; diff --git a/client/src/app/pages/assessment-management/questionnaire/questionnaire-page.css b/client/src/app/pages/assessment-management/questionnaire/questionnaire-page.css index ea51579a9..52420e3fe 100644 --- a/client/src/app/pages/assessment-management/questionnaire/questionnaire-page.css +++ b/client/src/app/pages/assessment-management/questionnaire/questionnaire-page.css @@ -1,6 +1,11 @@ .tabs-vertical-container { - width: 20em; + display: flex; } -.tab-content-container { - width: 80em; + +.tabs-vertical-container .pf-v5-c-tabs { + width: 20%; +} + +.tabs-vertical-container .pf-v5-c-tab-content { + width: 80%; } diff --git a/client/src/app/pages/assessment-management/questionnaire/questionnaire-page.tsx b/client/src/app/pages/assessment-management/questionnaire/questionnaire-page.tsx index 9de663589..4b550f627 100644 --- a/client/src/app/pages/assessment-management/questionnaire/questionnaire-page.tsx +++ b/client/src/app/pages/assessment-management/questionnaire/questionnaire-page.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useMemo } from "react"; import yaml from "js-yaml"; import { Text, @@ -7,40 +7,47 @@ import { PageSectionVariants, Breadcrumb, BreadcrumbItem, - Tab, - TabTitleText, + Button, Tabs, - TabContent, + Toolbar, + ToolbarItem, + SearchInput, + ToolbarContent, + Tab, } from "@patternfly/react-core"; +import AngleLeftIcon from "@patternfly/react-icons/dist/esm/icons/angle-left-icon"; import { YamlAssessment } from "@app/api/models"; import { Link } from "react-router-dom"; import { Paths } from "@app/Paths"; import { ConditionalRender } from "@app/components/ConditionalRender"; import { AppPlaceholder } from "@app/components/AppPlaceholder"; import { useTranslation } from "react-i18next"; -import "./questionnaire-page.css"; +import QuestionnaireSectionTabTitle from "./components/questionnaire-section-tab-title"; import QuestionsTable from "./components/questions-table"; +import "./questionnaire-page.css"; const Questionnaire: React.FC = () => { const { t } = useTranslation(); - const [activeTabKey, setActiveTabKey] = React.useState(0); + const [activeSectionIndex, setActiveSectionIndex] = React.useState< + "all" | number + >("all"); const handleTabClick = ( - event: React.MouseEvent | React.KeyboardEvent | MouseEvent, - tabIndex: string | number + _event: React.MouseEvent | React.KeyboardEvent | MouseEvent, + tabKey: string | number ) => { - setActiveTabKey(tabIndex as number); + setActiveSectionIndex(tabKey as "all" | number); }; const [assessmentData, setAssessmentData] = useState( null ); - const activeSection = assessmentData?.sections[activeTabKey]; // ------------------------!! // TODO: replace this with the real data from the API - const fetchError = false; + const fetchError = undefined; + const isLoading = false; useEffect(() => { fetch("/questionnaire-data.yaml") // adjust this path @@ -52,6 +59,27 @@ const Questionnaire: React.FC = () => { }, []); // ------------------------!! + const [searchValue, setSearchValue] = React.useState(""); + const filteredAssessmentData = useMemo(() => { + if (!assessmentData) return null; + return { + ...assessmentData, + sections: assessmentData?.sections.map((section) => ({ + ...section, + questions: section.questions.filter(({ formulation, explanation }) => + [formulation, explanation].some((text) => + text?.toLowerCase().includes(searchValue.toLowerCase()) + ) + ), + })), + }; + }, [assessmentData, searchValue]); + const allQuestions = + assessmentData?.sections.flatMap((section) => section.questions) || []; + const allMatchingQuestions = + filteredAssessmentData?.sections.flatMap((section) => section.questions) || + []; + return ( <> @@ -71,48 +99,89 @@ const Questionnaire: React.FC = () => { } >
- - {assessmentData?.sections.map((section, index) => { - return ( + + + + setSearchValue(value)} + onClear={() => setSearchValue("")} + resultsCount={ + (searchValue && allMatchingQuestions.length) || undefined + } + /> + + + + + + +
+ + {[ - {section.name} - + } > - - - - - ); - })} - + + , + ...(assessmentData?.sections.map((section, index) => { + const filteredQuestions = + filteredAssessmentData?.sections[index]?.questions || []; + return ( + + } + > + + + ); + }) || []), + ]} + +