diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index f7e7d3b991..c64b5bc8af 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -75,6 +75,7 @@ }, "composed": { "add": "Add {{what}}", + "new": "New {{what}}", "applicationAssessment": "Application assessment", "applicationInventory": "Application inventory", "businessCriticality": "Business criticality (1=low, 10=high)", @@ -170,12 +171,14 @@ "noDataAvailableTitle": "No data available", "noResultsFoundBody": "No results match the filter criteria. Remove all filters or clear all filters to show results.", "noResultsFoundTitle": "No results found", - "overrideAssessmentDescription": "The archetype for {{what}} already has an assessment.", - "overrideAssessmentConfirmation": "Do you want to create a dedicated assessment for this application?", + "overrideAssessmentDescription": "The application {{name}} already is associated with archetypes: {{what}}.", + "overrideAssessmentConfirmation": "Do you want to create a dedicated assessment for this application and override the inherited archetype assessment(s)?", "overrideReviewConfirmation": "This application has already been reviewed. Do you want to continue?", "reasonForError": "The reported reason for the error:", "reviewInstructions": "Use this section to provide your assessment of the possible migration/modernization plan and effort estimation.", "savingSelection": "Saving selection", + "selectedBecauseArchetypeTags": "Selected because the archetype's tags include {{tags}}", + "selectedBecauseAppOrArchetypeTags": "Selected because the application or archetype's tags include {{tags}}", "selectOwnerFromStakeholdersList": "Select owner from list of stakeholders", "suggestedAdoptionPlanHelpText": "The suggested approach to migration based on effort, priority, and dependencies.", "taskInProgressForTags": "A new analysis is in-progress. Tags may be updated upon completion.", diff --git a/client/src/app/Paths.ts b/client/src/app/Paths.ts index d293facac0..b2e55c63fa 100644 --- a/client/src/app/Paths.ts +++ b/client/src/app/Paths.ts @@ -11,10 +11,12 @@ export enum Paths { archetypesAssessment = "/archetypes/assessment/:assessmentId", applicationsAssessment = "/applications/assessment/:assessmentId", applicationAssessmentActions = "/applications/assessment-actions/:applicationId", + viewArchetypes = "/applications/view-archetypes/:applicationId/archetypes/:archetypeId", archetypeAssessmentActions = "/archetypes/assessment-actions/:archetypeId", applicationAssessmentSummary = "/applications/assessment-summary/:assessmentId", archetypeAssessmentSummary = "/archetypes/assessment-summary/:assessmentId", applicationsReview = "/applications/:applicationId/review", + archetypeReview = "/archetypes/:archetypeId/review", applicationsAnalysis = "/applications/analysis", archetypes = "/archetypes", controls = "/controls", @@ -57,6 +59,11 @@ export interface AssessmentActionsRoute { archetypeId?: string; } +export interface ViewArchetypesRoute { + applicationId?: string; + archetypeId?: string; +} + export interface ReviewRoute { applicationId?: string; archetypeId?: string; diff --git a/client/src/app/Routes.tsx b/client/src/app/Routes.tsx index 25a700720c..1575586ea6 100644 --- a/client/src/app/Routes.tsx +++ b/client/src/app/Routes.tsx @@ -44,6 +44,10 @@ const Questionnaire = lazy( () => import("./pages/assessment-management/questionnaire/questionnaire-page") ); +const ViewArchetypes = lazy( + () => + import("./pages/assessment/components/view-archetypes/view-archetypes-page") +); const AssessmentActions = lazy( () => import( @@ -86,6 +90,11 @@ export const devRoutes: IRoute[] = [ comp: Assessment, exact: false, }, + { + path: Paths.archetypeReview, + comp: Review, + exact: false, + }, { path: Paths.applicationsReview, comp: Review, @@ -101,6 +110,11 @@ export const devRoutes: IRoute[] = [ comp: AssessmentActions, exact: false, }, + { + path: Paths.viewArchetypes, + comp: ViewArchetypes, + exact: false, + }, { path: Paths.applicationAssessmentSummary, comp: AssessmentSummary, diff --git a/client/src/app/api/models.ts b/client/src/app/api/models.ts index 78983090a6..d4ad356db9 100644 --- a/client/src/app/api/models.ts +++ b/client/src/app/api/models.ts @@ -139,6 +139,7 @@ export interface Review { workPriority: number; comments?: string; application?: Ref; + archetype?: Ref; } export interface ApplicationDependency { @@ -682,6 +683,7 @@ export interface Answer { mitigation?: string; applyTags?: CategorizedTag[]; autoAnswerFor?: CategorizedTag[]; + autoAnswered?: boolean; selected?: boolean; } export interface Thresholds { @@ -752,4 +754,5 @@ export interface Archetype { stakeholderGroups?: Ref[]; applications?: Ref[]; assessments?: Ref[]; + review?: Ref; } diff --git a/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx b/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx index 09c3dedb33..5d82ba8841 100644 --- a/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx +++ b/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx @@ -65,7 +65,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useLocalTableControls } from "@app/hooks/table-controls"; // Queries -import { Application, Assessment, Task } from "@app/api/models"; +import { Application, Assessment, Ref, Task } from "@app/api/models"; import { ApplicationsQueryKey, useBulkDeleteApplicationMutation, @@ -73,7 +73,7 @@ import { } from "@app/queries/applications"; import { useFetchTasks } from "@app/queries/tasks"; import { useDeleteAssessmentMutation } from "@app/queries/assessments"; -import { useDeleteReviewMutation, useFetchReviews } from "@app/queries/reviews"; +import { useDeleteReviewMutation } from "@app/queries/reviews"; import { useFetchIdentities } from "@app/queries/identities"; import { useFetchTagCategories } from "@app/queries/tags"; @@ -86,6 +86,7 @@ import { ImportApplicationsForm } from "../components/import-applications-form"; import { ConditionalRender } from "@app/components/ConditionalRender"; import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; import { ConditionalTooltip } from "@app/components/ConditionalTooltip"; +import { getAssessmentsByItemId } from "@app/api/rest"; export const ApplicationsTable: React.FC = () => { const { t } = useTranslation(); @@ -106,28 +107,17 @@ export const ApplicationsTable: React.FC = () => { const createUpdateApplications = saveApplicationModalState !== "create" ? saveApplicationModalState : null; - const [applicationToCopyAssessmentFrom, setApplicationToCopyAssessmentFrom] = - React.useState(null); - - const isCopyAssessmentModalOpen = applicationToCopyAssessmentFrom !== null; - - const [isAssessModalOpen, setAssessModalOpen] = React.useState(false); + const [archetypeRefsToOverride, setArchetypeRefsToOverride] = React.useState< + Ref[] | null + >(null); - const [ - applicationToCopyAssessmentAndReviewFrom, - setCopyAssessmentAndReviewModalState, - ] = React.useState(null); - - const isCopyAssessmentAndReviewModalOpen = - applicationToCopyAssessmentAndReviewFrom !== null; + const [applicationToAssess, setApplicationToAssess] = + React.useState(null); const [applicationDependenciesToManage, setApplicationDependenciesToManage] = React.useState(null); const isDependenciesModalOpen = applicationDependenciesToManage !== null; - const [applicationToAssess, setApplicationToAssess] = - React.useState(null); - const [assessmentToEdit, setAssessmentToEdit] = React.useState(null); @@ -156,9 +146,6 @@ export const ApplicationsTable: React.FC = () => { refetch: fetchApplications, } = useFetchApplications(); - //TODO: check if any archetypes match this application here - const matchingArchetypes = []; - const onDeleteApplicationSuccess = (appIDCount: number) => { pushNotification({ title: t("toastr.success.applicationDeleted", { @@ -401,12 +388,6 @@ export const ApplicationsTable: React.FC = () => { selectionState: { selectedItems: selectedRows }, } = tableControls; - const { - reviews, - isFetching: isFetchingReviews, - fetchError: fetchErrorReviews, - } = useFetchReviews(); - const [isApplicationImportModalOpen, setIsApplicationImportModalOpen] = React.useState(false); @@ -460,19 +441,56 @@ export const ApplicationsTable: React.FC = () => { : []; const dropdownItems = [...importDropdownItems, ...applicationDeleteDropdown]; - const assessSelectedApp = (application: Application) => { - // if application/archetype has an assessment, ask if user wants to override it - if (matchingArchetypes.length) { - setAssessModalOpen(true); - setApplicationToAssess(application); + const handleNavToAssessment = (application: Application) => { + application?.id && + history.push( + formatPath(Paths.applicationAssessmentActions, { + applicationId: application?.id, + }) + ); + }; + + const handleNavToViewArchetypes = (application: Application) => { + application?.id && + archetypeRefsToOverride?.length && + history.push( + formatPath(Paths.viewArchetypes, { + applicationId: application?.id, + archetypeId: archetypeRefsToOverride[0].id, + }) + ); + }; + + const assessSelectedApp = async (application: Application) => { + setApplicationToAssess(application); + + if (application?.archetypes?.length) { + for (const archetypeRef of application.archetypes) { + try { + const assessments = await getAssessmentsByItemId( + true, + archetypeRef.id + ); + + if (assessments && assessments.length > 0) { + setArchetypeRefsToOverride(application.archetypes); + break; + } else { + handleNavToAssessment(application); + } + } catch (error) { + console.error( + `Error fetching archetype with ID ${archetypeRef.id}:`, + error + ); + pushNotification({ + title: t("terms.error"), + variant: "danger", + }); + } + } } else { - application?.id && - history.push( - formatPath(Paths.applicationAssessmentActions, { - applicationId: application?.id, - }) - ); - setApplicationToAssess(null); + handleNavToAssessment(application); } }; const reviewSelectedApp = (application: Application) => { @@ -858,23 +876,36 @@ export const ApplicationsTable: React.FC = () => { }} /> archetypeRef.name) + .join(", ") || "Archetype name", + })} + message={t("message.overrideAssessmentConfirmation")} titleIconVariant={"warning"} - isOpen={isAssessModalOpen} - message={t("message.overrideArchetypeConfirmation")} + isOpen={archetypeRefsToOverride !== null} confirmBtnVariant={ButtonVariant.primary} - confirmBtnLabel={t("actions.accept")} + confirmBtnLabel={t("actions.override")} cancelBtnLabel={t("actions.cancel")} - onCancel={() => setApplicationToAssess(null)} - onClose={() => setApplicationToAssess(null)} - onConfirm={() => { + customActionLabel={t("actions.viewArchetypes")} + onCancel={() => setArchetypeRefsToOverride(null)} + onClose={() => setArchetypeRefsToOverride(null)} + onCustomAction={() => { applicationToAssess && - history.push( - formatPath(Paths.applicationAssessmentActions, { - applicationId: applicationToAssess?.id, - }) - ); - setApplicationToAssess(null); + handleNavToViewArchetypes(applicationToAssess); + }} + onConfirm={() => { + history.push( + formatPath(Paths.applicationAssessmentActions, { + applicationId: activeRowItem?.id, + }) + ); + setArchetypeRefsToOverride(null); + applicationToAssess && handleNavToAssessment(applicationToAssess); }} /> diff --git a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer-assessment.tsx b/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer-assessment.tsx index 9889a331fa..6df452469b 100644 --- a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer-assessment.tsx +++ b/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer-assessment.tsx @@ -20,7 +20,7 @@ import { ApplicationDetailDrawer, IApplicationDetailDrawerProps, } from "./application-detail-drawer"; -import { useGetReviewByAppId } from "@app/queries/reviews"; +import { useFetchReviewById } from "@app/queries/reviews"; export interface IApplicationDetailDrawerAssessmentProps extends Pick { @@ -32,7 +32,7 @@ export const ApplicationDetailDrawerAssessment: React.FC< > = ({ application, onCloseClick, task }) => { const { t } = useTranslation(); - const { review: appReview } = useGetReviewByAppId(application?.id || ""); + const { review: appReview } = useFetchReviewById(application?.review?.id); const notYetReviewed = ( ); diff --git a/client/src/app/pages/archetypes/archetypes-page.tsx b/client/src/app/pages/archetypes/archetypes-page.tsx index 586003ede1..60fad8847d 100644 --- a/client/src/app/pages/archetypes/archetypes-page.tsx +++ b/client/src/app/pages/archetypes/archetypes-page.tsx @@ -185,19 +185,6 @@ const Archetypes: React.FC = () => { setArchetypeToAssess(null); } }; - const reviewSelectedArchetype = (archetype: Archetype) => { - //TODO: Review archetype - // if (application.review) { - // setReviewToEdit(application.id); - // } else { - // history.push( - // formatPath(Paths.applicationsReview, { - // applicationId: application.id, - // }) - // ); - // } - }; - return ( <> @@ -309,6 +296,15 @@ const Archetypes: React.FC = () => { onClick: () => assessSelectedArchetype(archetype), }, + { + title: t("actions.review"), + onClick: () => + history.push( + formatPath(Paths.archetypeReview, { + archetypeId: archetype.id, + }) + ), + }, { title: t("actions.edit"), onClick: () => setArchetypeToEdit(archetype), diff --git a/client/src/app/pages/assessment-management/import-questionnaire-form/import-questionnaire-form.tsx b/client/src/app/pages/assessment-management/import-questionnaire-form/import-questionnaire-form.tsx index 9ef57af15b..abb3b72634 100644 --- a/client/src/app/pages/assessment-management/import-questionnaire-form/import-questionnaire-form.tsx +++ b/client/src/app/pages/assessment-management/import-questionnaire-form/import-questionnaire-form.tsx @@ -133,14 +133,14 @@ export const ImportQuestionnaireForm: React.FC< name="yamlFile" label={t("terms.uploadYamlFile")} fieldId="yamlFile" - helperText={t("dialog.uploadYamlFile")} + helperText={t("dialog.message.uploadYamlFile")} renderInput={({ field: { onChange, name }, fieldState: { error } }) => ( 1000000) { methods.setError(name, { type: "custom", - message: t("dialog.maxFileSize"), + message: t("dialog.message.maxFileSize"), }); } setIsFileRejected(true); diff --git a/client/src/app/pages/assessment/components/assessment-actions/components/dynamic-assessment-actions-row.tsx b/client/src/app/pages/assessment/components/assessment-actions/components/dynamic-assessment-actions-row.tsx index e718baf9c4..69df98b501 100644 --- a/client/src/app/pages/assessment/components/assessment-actions/components/dynamic-assessment-actions-row.tsx +++ b/client/src/app/pages/assessment/components/assessment-actions/components/dynamic-assessment-actions-row.tsx @@ -5,14 +5,13 @@ import { Assessment, InitialAssessment, Questionnaire, - Ref, } from "@app/api/models"; import { assessmentsByItemIdQueryKey, useCreateAssessmentMutation, useDeleteAssessmentMutation, } from "@app/queries/assessments"; -import { Button, ButtonVariant } from "@patternfly/react-core"; +import { Button } from "@patternfly/react-core"; import React, { FunctionComponent } from "react"; import { useHistory } from "react-router-dom"; import "./dynamic-assessment-actions-row.css"; @@ -23,8 +22,6 @@ import { NotificationsContext } from "@app/components/NotificationsContext"; import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; import { TrashIcon } from "@patternfly/react-icons"; -import { ConfirmDialog } from "@app/components/ConfirmDialog"; -import { getAssessmentsByItemId } from "@app/api/rest"; import useIsArchetype from "@app/hooks/useIsArchetype"; enum AssessmentAction { @@ -38,11 +35,12 @@ interface DynamicAssessmentActionsRowProps { application?: Application; archetype?: Archetype; assessment?: Assessment; + isReadonly?: boolean; } const DynamicAssessmentActionsRow: FunctionComponent< DynamicAssessmentActionsRowProps -> = ({ questionnaire, application, archetype, assessment }) => { +> = ({ questionnaire, application, archetype, assessment, isReadonly }) => { const isArchetype = useIsArchetype(); const history = useHistory(); const { t } = useTranslation(); @@ -50,9 +48,6 @@ const DynamicAssessmentActionsRow: FunctionComponent< const { pushNotification } = React.useContext(NotificationsContext); - const [archetypeRefToOverride, setArchetypeRefToOverride] = - React.useState(null); - const onSuccessHandler = () => {}; const onErrorHandler = () => {}; @@ -148,17 +143,7 @@ const DynamicAssessmentActionsRow: FunctionComponent< if (!isArchetype && application?.archetypes?.length) { for (const archetypeRef of application.archetypes) { try { - const assessments = await getAssessmentsByItemId( - true, - archetypeRef.id - ); - - if (assessments && assessments.length > 0) { - setArchetypeRefToOverride(archetypeRef); - break; - } else { - createAssessment(); - } + createAssessment(); } catch (error) { console.error( `Error fetching archetype with ID ${archetypeRef.id}:`, @@ -227,16 +212,18 @@ const DynamicAssessmentActionsRow: FunctionComponent< <>
- + {!isReadonly ? ( + + ) : null} {assessment?.status === "complete" ? ( + ) : isReadonly ? ( + ) : null}
@@ -275,37 +266,6 @@ const DynamicAssessmentActionsRow: FunctionComponent< ) : null} - setArchetypeRefToOverride(null)} - onClose={() => setArchetypeRefToOverride(null)} - //TODO - // onCustomAction={() => { - // //nav to view archetypes - // console.log("nav to view archetypes"); - // }} - onConfirm={() => { - history.push( - formatPath(Paths.applicationsAssessment, { - assessmentId: archetypeRefToOverride?.id, - }) - ); - setArchetypeRefToOverride(null); - createAssessment(); - }} - /> ); }; diff --git a/client/src/app/pages/assessment/components/assessment-actions/components/questionnaires-table.tsx b/client/src/app/pages/assessment/components/assessment-actions/components/questionnaires-table.tsx index 77acd47bd3..f5848175a2 100644 --- a/client/src/app/pages/assessment/components/assessment-actions/components/questionnaires-table.tsx +++ b/client/src/app/pages/assessment/components/assessment-actions/components/questionnaires-table.tsx @@ -19,6 +19,7 @@ import DynamicAssessmentActionsRow from "./dynamic-assessment-actions-row"; interface QuestionnairesTableProps { tableName: string; isFetching: boolean; + isReadonly?: boolean; application?: Application; archetype?: Archetype; assessments?: Assessment[]; @@ -28,7 +29,7 @@ interface QuestionnairesTableProps { const QuestionnairesTable: React.FC = ({ assessments, questionnaires, - isFetching, + isReadonly, application, archetype, tableName, @@ -109,6 +110,7 @@ const QuestionnairesTable: React.FC = ({ questionnaire={questionnaire} application={application} archetype={archetype} + isReadonly={isReadonly} /> ) : null} diff --git a/client/src/app/pages/assessment/components/questionnaire-form/multi-input-selection/multi-input-selection.tsx b/client/src/app/pages/assessment/components/questionnaire-form/multi-input-selection/multi-input-selection.tsx index f237960c37..5a74d2d966 100644 --- a/client/src/app/pages/assessment/components/questionnaire-form/multi-input-selection/multi-input-selection.tsx +++ b/client/src/app/pages/assessment/components/questionnaire-form/multi-input-selection/multi-input-selection.tsx @@ -1,11 +1,14 @@ import React, { useMemo } from "react"; -import { Radio, Stack, StackItem } from "@patternfly/react-core"; +import { Icon, Radio, Stack, StackItem, Tooltip } from "@patternfly/react-core"; +import InfoCircleIcon from "@patternfly/react-icons/dist/esm/icons/info-circle-icon"; import { Question } from "@app/api/models"; import { HookFormPFGroupController } from "@app/components/HookFormPFFields"; import { useFormContext } from "react-hook-form"; import { getQuestionFieldName } from "../../../form-utils"; import { AssessmentWizardValues } from "@app/pages/assessment/components/assessment-wizard/assessment-wizard"; +import useIsArchetype from "@app/hooks/useIsArchetype"; +import { useTranslation } from "react-i18next"; export interface MultiInputSelectionProps { question: Question; @@ -21,6 +24,10 @@ export const MultiInputSelection: React.FC = ({ }, [question]); const questionFieldName = getQuestionFieldName(question, true); + + const isArchetype = useIsArchetype(); + const { t } = useTranslation(); + return ( {sortedOptions.map((option, i) => ( @@ -37,7 +44,30 @@ export const MultiInputSelection: React.FC = ({ onChange={(checked, e) => { onChange(option.text); }} - label={option.text} + aria-label={option.text} + label={ + <> + {option.autoAnswered && option.autoAnswerFor?.length ? ( + `"${t.tag}"`) + .join(", "), + } + )} + > + + + + + ) : null}{" "} + {option.text} + + } value={option.text} /> )} diff --git a/client/src/app/pages/assessment/components/view-archetypes/components/view-archetypes-table.tsx b/client/src/app/pages/assessment/components/view-archetypes/components/view-archetypes-table.tsx new file mode 100644 index 0000000000..c6b741f9e5 --- /dev/null +++ b/client/src/app/pages/assessment/components/view-archetypes/components/view-archetypes-table.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { Ref } from "@app/api/models"; +import { useFetchQuestionnaires } from "@app/queries/questionnaires"; +import { useFetchAssessmentsByItemId } from "@app/queries/assessments"; +import { useFetchArchetypeById } from "@app/queries/archetypes"; +import QuestionnairesTable from "../../assessment-actions/components/questionnaires-table"; + +export interface ViewArchetypesTableProps { + archetypeRef?: Ref | null; +} + +const ViewArchetypesTable: React.FC = ({ + archetypeRef, +}) => { + const { questionnaires, isFetching: isFetchingQuestionnaires } = + useFetchQuestionnaires(); + const { archetype } = useFetchArchetypeById(archetypeRef?.id); + const { assessments, isFetching: isFetchingAssessmentsById } = + useFetchAssessmentsByItemId(true, archetypeRef?.id); + + const requiredQuestionnaires = questionnaires.filter( + (questionnaire) => questionnaire.required + ); + const archivedQuestionnaires = questionnaires.filter( + (questionnaire) => !questionnaire.required + ); + return ( + <> + + + + + ); +}; + +export default ViewArchetypesTable; diff --git a/client/src/app/pages/assessment/components/view-archetypes/view-archetypes-page.tsx b/client/src/app/pages/assessment/components/view-archetypes/view-archetypes-page.tsx new file mode 100644 index 0000000000..a3f6e29132 --- /dev/null +++ b/client/src/app/pages/assessment/components/view-archetypes/view-archetypes-page.tsx @@ -0,0 +1,106 @@ +import React, { useEffect } from "react"; +import { + Text, + TextContent, + PageSection, + PageSectionVariants, + Breadcrumb, + BreadcrumbItem, +} from "@patternfly/react-core"; +import { Link, useParams } from "react-router-dom"; +import { ViewArchetypesRoute, Paths } from "@app/Paths"; +import { ConditionalRender } from "@app/components/ConditionalRender"; +import { AppPlaceholder } from "@app/components/AppPlaceholder"; +import ViewArchetypesTable from "./components/view-archetypes-table"; +import { useFetchArchetypeById } from "@app/queries/archetypes"; +import { useFetchApplicationById } from "@app/queries/applications"; +import { OptionWithValue, SimpleSelect } from "@app/components/SimpleSelect"; +import { Ref } from "@app/api/models"; +import { formatPath } from "@app/utils/utils"; + +const ViewArchetypes: React.FC = () => { + const { applicationId, archetypeId } = useParams(); + const { archetype } = useFetchArchetypeById(archetypeId); + const { application } = useFetchApplicationById(applicationId); + + const [activeArchetype, setActiveArchetype] = React.useState( + null + ); + useEffect(() => { + if (archetypeId && archetype) { + setActiveArchetype({ + id: parseInt(archetypeId, 10), + name: archetype.name, + }); + } + }, [archetypeId, archetype]); + + function mapRefToOption(ref: Ref | null): OptionWithValue { + if (ref) { + return { + value: ref, + toString: () => ref.name, + }; + } else { + return { + value: null, + toString: () => "All", + }; + } + } + const options: OptionWithValue[] = [ + ...(application?.archetypes?.map(mapRefToOption) || []), + ]; + + return ( + <> + + + View Archetypes + + + + Applications + + + {application?.name} + + + Archetypes + + + {archetype?.name} + + + + + }> + {application?.archetypes && application?.archetypes?.length > 1 && ( + { + const selectedArchetype = + selection as OptionWithValue; + setActiveArchetype(selectedArchetype.value); + }} + options={options} + /> + )} + + {} + + + + + ); +}; + +export default ViewArchetypes; diff --git a/client/src/app/pages/reports/components/adoption-candidate-graph/adoption-candidate-graph.tsx b/client/src/app/pages/reports/components/adoption-candidate-graph/adoption-candidate-graph.tsx index a2af7bf25c..05a8d49ec9 100644 --- a/client/src/app/pages/reports/components/adoption-candidate-graph/adoption-candidate-graph.tsx +++ b/client/src/app/pages/reports/components/adoption-candidate-graph/adoption-candidate-graph.tsx @@ -36,8 +36,8 @@ import { import { ApplicationSelectionContext } from "../../application-selection-context"; import { CartesianSquare } from "./cartesian-square"; import { Arrow } from "./arrow"; -import { useGetReviewByAppId } from "@app/queries/reviews"; import useFetchApplicationDependencies from "@app/hooks/useFetchApplicationDependencies/useFetchApplicationDependencies"; +import { useFetchReviewById } from "@app/queries/reviews"; interface Line { from: LinePoint; @@ -165,7 +165,7 @@ export const AdoptionCandidateGraph: React.FC = () => { const appConfidence = confidences.find( (elem) => elem.applicationId === current.id ); - const { review: appReview } = useGetReviewByAppId(current?.id || ""); + const { review: appReview } = useFetchReviewById(current?.review?.id); if (appConfidence && appReview) { const key = appReview.proposedAction; diff --git a/client/src/app/pages/reports/components/adoption-candidate-table/adoption-candidate-table.tsx b/client/src/app/pages/reports/components/adoption-candidate-table/adoption-candidate-table.tsx index b5169ef6cf..3db5cddda8 100644 --- a/client/src/app/pages/reports/components/adoption-candidate-table/adoption-candidate-table.tsx +++ b/client/src/app/pages/reports/components/adoption-candidate-table/adoption-candidate-table.tsx @@ -22,7 +22,7 @@ import { } from "@app/api/models"; import { ApplicationSelectionContext } from "../../application-selection-context"; -import { useGetReviewByAppId } from "@app/queries/reviews"; +import { useFetchReviewById } from "@app/queries/reviews"; import { useQuery } from "@tanstack/react-query"; import { useFetchRisks } from "@app/queries/risks"; import { AppTableWithControls } from "@app/components/AppTableWithControls"; @@ -110,7 +110,7 @@ export const AdoptionCandidateTable: React.FC = () => { const confidenceData = confidence?.find( (e) => e.applicationId === app.id ); - const { review: reviewData } = useGetReviewByAppId(app?.id || ""); + const { review: reviewData } = useFetchReviewById(app?.review?.id); const riskData = assessmentRisks?.find((e) => e.applicationId === app.id); diff --git a/client/src/app/pages/review/components/application-details/application-details.tsx b/client/src/app/pages/review/components/application-details/application-details.tsx index 13973a8871..bab21fcfbf 100644 --- a/client/src/app/pages/review/components/application-details/application-details.tsx +++ b/client/src/app/pages/review/components/application-details/application-details.tsx @@ -13,7 +13,7 @@ import { Application, Assessment } from "@app/api/models"; import { useFetchQuestionnaires } from "@app/queries/questionnaires"; export interface IApplicationDetailsProps { - application: Application; + application?: Application; assessment?: Assessment; } @@ -27,7 +27,7 @@ export const ApplicationDetails: React.FC = ({ (questionnaire) => questionnaire.id === assessment?.questionnaire?.id ); const { t } = useTranslation(); - if (!matchingQuestionnaire) { + if (!matchingQuestionnaire || !application) { return null; } diff --git a/client/src/app/pages/review/components/review-form/review-form.tsx b/client/src/app/pages/review/components/review-form/review-form.tsx index abb25131bb..d7cb37c527 100644 --- a/client/src/app/pages/review/components/review-form/review-form.tsx +++ b/client/src/app/pages/review/components/review-form/review-form.tsx @@ -15,6 +15,7 @@ import { PROPOSED_ACTION_LIST, EFFORT_ESTIMATE_LIST } from "@app/Constants"; import { number } from "yup"; import { Application, + Archetype, EffortEstimate, New, ProposedAction, @@ -27,14 +28,13 @@ import { } from "@app/components/HookFormPFFields"; import { OptionWithValue, SimpleSelect } from "@app/components/SimpleSelect"; import { - reviewsByItemIdQueryKey, useCreateReviewMutation, useUpdateReviewMutation, } from "@app/queries/reviews"; -import { useQueryClient } from "@tanstack/react-query"; import { useHistory } from "react-router-dom"; import { Paths } from "@app/Paths"; import { NotificationsContext } from "@app/components/NotificationsContext"; +import useIsArchetype from "@app/hooks/useIsArchetype"; export interface FormValues { action: ProposedAction; @@ -45,17 +45,21 @@ export interface FormValues { } export interface IReviewFormProps { - application: Application; + application?: Application; + archetype?: Archetype; review?: Review | null; } export const ReviewForm: React.FC = ({ + archetype, application, review, }) => { + console.log("existing review", review); const { t } = useTranslation(); const history = useHistory(); const { pushNotification } = React.useContext(NotificationsContext); + const isArchetype = useIsArchetype(); const actionOptions: OptionWithValue[] = useMemo(() => { return Object.entries(PROPOSED_ACTION_LIST).map(([key, value]) => ({ @@ -107,45 +111,8 @@ export const ReviewForm: React.FC = ({ console.log("Invalid form", errors); }; - // const onSubmit = (formValues: FormValues) => { - // const payload: Review = { - // ...review, - // proposedAction: formValues.action, - // effortEstimate: formValues.effort, - // businessCriticality: formValues.criticality || 0, - // workPriority: formValues.priority || 0, - // comments: formValues.comments.trim(), - // // application: { ...application, review: undefined }, - // application: { id: application.id, name: application.name }, - // }; - - // let promise: Promise; - // if (review) { - // promise = updateReview({ - // ...review, - // ...payload, - // }); - // } else { - // promise = createReview(payload); - // } - - // promise - // .then((response) => { - // onSaved(response); - // }) - // .catch((error) => {}); - // }; - const queryClient = useQueryClient(); - const onHandleUpdateReviewSuccess = () => { - queryClient.invalidateQueries([ - reviewsByItemIdQueryKey, - application.review?.id, - ]); - }; const createReviewMutation = useCreateReviewMutation(); - const updateReviewMutation = useUpdateReviewMutation( - onHandleUpdateReviewSuccess - ); + const updateReviewMutation = useUpdateReviewMutation(); const onSubmit = async (formValues: FormValues) => { const payload: New = { @@ -155,7 +122,15 @@ export const ReviewForm: React.FC = ({ businessCriticality: formValues.criticality || 0, workPriority: formValues.priority || 0, comments: formValues.comments.trim(), - application: { id: application.id, name: application.name }, + ...(isArchetype && archetype + ? { + archetype: { id: archetype.id, name: archetype.name }, + } + : application + ? { + application: { id: application.id, name: application.name }, + } + : undefined), }; try { @@ -178,7 +153,7 @@ export const ReviewForm: React.FC = ({ }); } - history.push(Paths.applications); + history.push(isArchetype ? Paths.archetypes : Paths.applications); } catch (error) { console.error("Error:", error); pushNotification({ @@ -266,7 +241,7 @@ export const ReviewForm: React.FC = ({ label={t("composed.workPriority")} fieldId="priority" isRequired - renderInput={({ field: { value, name, onChange } }) => ( + renderInput={({ field: { value, onChange } }) => ( = ({ aria-label="cancel" variant={ButtonVariant.link} onClick={() => { - history.push(Paths.applications); + history.push(isArchetype ? Paths.archetypes : Paths.applications); }} > {t("actions.cancel")} diff --git a/client/src/app/pages/review/review-page.tsx b/client/src/app/pages/review/review-page.tsx index 6e1053b9a7..5928336b07 100644 --- a/client/src/app/pages/review/review-page.tsx +++ b/client/src/app/pages/review/review-page.tsx @@ -22,22 +22,46 @@ import QuestionnaireSummary, { SummaryType, } from "@app/components/questionnaire-summary/questionnaire-summary"; import { PageHeader } from "@app/components/PageHeader"; -import { useGetReviewByAppId } from "@app/queries/reviews"; +import { useFetchReviewById } from "@app/queries/reviews"; +import useIsArchetype from "@app/hooks/useIsArchetype"; +import { useFetchApplicationById } from "@app/queries/applications"; +import { useFetchArchetypeById } from "@app/queries/archetypes"; const ReviewPage: React.FC = () => { const { t } = useTranslation(); const { applicationId, archetypeId } = useParams(); + const isArchetype = useIsArchetype(); - const { application, review, fetchError, isFetching } = useGetReviewByAppId( - applicationId || "" - ); - - //TODO: Review archetypes? - // const { archetype } = useFetchArchetypeById(archetypeId || ""); + const { archetype } = useFetchArchetypeById(archetypeId); + const { application } = useFetchApplicationById(applicationId); - //TODO: Add a dropdown with multiple assessments to choose from + const { review, fetchError, isFetching } = useFetchReviewById( + isArchetype ? archetype?.review?.id : application?.review?.id + ); + console.log("archetype.review", archetype?.review?.id); + console.log("reviewl", review); + console.log("isFetchign", isFetching); const assessment = undefined; + const breadcrumbs = [ + ...(isArchetype + ? [ + { + title: t("terms.archetypes"), + path: Paths.archetypes, + }, + ] + : [ + { + title: t("terms.applications"), + path: Paths.applications, + }, + ]), + // { + // title: t("terms.review"), + // path: Paths.applicationsReview, + // }, + ]; if (fetchError) { return ( @@ -48,16 +72,7 @@ const ReviewPage: React.FC = () => { description={ {t("message.reviewInstructions")} } - breadcrumbs={[ - { - title: t("terms.applications"), - path: Paths.applications, - }, - { - title: t("terms.review"), - path: Paths.applicationsReview, - }, - ]} + breadcrumbs={breadcrumbs} menuActions={[]} />
@@ -81,37 +96,30 @@ const ReviewPage: React.FC = () => { description={ {t("message.reviewInstructions")} } - breadcrumbs={[ - { - title: t("terms.applications"), - path: Paths.applications, - }, - { - title: t("terms.review"), - path: Paths.applicationsReview, - }, - ]} + breadcrumbs={breadcrumbs} menuActions={[]} /> }> - {application && ( - -
- - - - - - -
-
- )} + +
+ + + + + + +
+
{assessment && ( diff --git a/client/src/app/queries/reviews.ts b/client/src/app/queries/reviews.ts index b49541ddf8..c841da5880 100644 --- a/client/src/app/queries/reviews.ts +++ b/client/src/app/queries/reviews.ts @@ -9,7 +9,6 @@ import { } from "@app/api/rest"; import { New, Review } from "@app/api/models"; import { AxiosError } from "axios"; -import { useFetchApplicationById } from "./applications"; export const reviewQueryKey = "review"; export const reviewsByItemIdQueryKey = "reviewsByItemId"; @@ -40,6 +39,7 @@ export const useCreateReviewMutation = ( queryClient.invalidateQueries([ reviewsByItemIdQueryKey, res?.application?.id, + res?.archetype?.id, ]); onSuccess && onSuccess(res?.application?.name || ""); }, @@ -61,6 +61,7 @@ export const useUpdateReviewMutation = ( queryClient.invalidateQueries([ reviewsByItemIdQueryKey, _?.application?.id, + _.archetype?.id, ]); onSuccess && onSuccess(args?.application?.name || ""); }, @@ -89,39 +90,18 @@ export const useDeleteReviewMutation = ( }); }; -export function useGetReviewByAppId(applicationId: string | number) { - const { - application, - isFetching: isApplicationFetching, - fetchError: applicationError, - } = useFetchApplicationById(applicationId); - - let review = null; - let isLoading = false; - let isError = false; - - const appReviewId = application?.review?.id; - - const reviewQuery = useQuery( - ["review", application?.review?.id], - () => (appReviewId ? getReviewById(appReviewId) : null), - { - enabled: !!appReviewId, - } - ); - - if (appReviewId) { - review = reviewQuery.data; - isLoading = reviewQuery.isLoading; - isError = reviewQuery.isError; - } +export const useFetchReviewById = (id?: number | string) => { + const { data, isLoading, error, isFetching } = useQuery({ + queryKey: [reviewQueryKey, id], + queryFn: () => + id === undefined ? Promise.resolve(null) : getReviewById(id), + onError: (error: AxiosError) => console.log("error, ", error), + enabled: id !== undefined, + }); return { - application, - review, - isLoading, - isError, - isFetching: isApplicationFetching || isLoading, - fetchError: applicationError || (review ? reviewQuery.error : null), + review: data, + isFetching: isFetching, + fetchError: error, }; -} +};