From a3af782543da0f29e250dd771cc283b3b5760a90 Mon Sep 17 00:00:00 2001 From: Subina Date: Wed, 5 Jun 2024 16:55:36 +0545 Subject: [PATCH] Contextual data for auto cluster and summarization --- app/components/TagInput/index.tsx | 212 +++++++++++++++ app/components/TagInput/styles.css | 45 ++++ .../SummaryTagsModal/index.tsx | 50 ++++ .../StoryAnalysisModal/index.tsx | 24 +- .../StoryAnalysisModal/styles.css | 12 + .../AnalyticalStatementInput/index.tsx | 160 ++++++++++- .../AutoClusteringTagsModal/index.tsx | 52 ++++ .../PillarAnalysis/AutoClustering/index.tsx | 62 +++-- app/views/PillarAnalysis/index.tsx | 255 +++++++++++++----- 9 files changed, 789 insertions(+), 83 deletions(-) create mode 100644 app/components/TagInput/index.tsx create mode 100644 app/components/TagInput/styles.css create mode 100644 app/views/PillarAnalysis/AnalyticalStatementInput/StoryAnalysisModal/SummaryTagsModal/index.tsx create mode 100644 app/views/PillarAnalysis/AutoClustering/AutoClusteringTagsModal/index.tsx diff --git a/app/components/TagInput/index.tsx b/app/components/TagInput/index.tsx new file mode 100644 index 0000000000..c7be321633 --- /dev/null +++ b/app/components/TagInput/index.tsx @@ -0,0 +1,212 @@ +import React, { useState, useCallback } from 'react'; +import { _cs } from '@togglecorp/fujs'; +import { + IoClose, + IoCheckmarkSharp, + IoAdd, +} from 'react-icons/io5'; +import { + Button, + RawInput, + List, + Tag, + TagProps as PropsFromTag, + useBooleanState, +} from '@the-deep/deep-ui'; + +import styles from './styles.css'; + +type TagVariant = PropsFromTag['variant']; + +interface TagProps extends PropsFromTag { + label: string; + onRemove: (key: string) => void; + variant?: TagVariant; + disabled?: boolean; + readOnly?: boolean; + className?: string; +} + +function ModifiedTag(props: TagProps) { + const { + className, + onRemove, + label, + variant, + disabled, + readOnly, + ...otherProps + } = props; + + return ( + + )} + /> + )} + variant={variant} + {...otherProps} + > + {label} + + ); +} + +const keySelector = (d: string) => d; +const emptyValue: string[] = []; + +interface Props extends PropsFromTag{ + className?: string; + tagClassName?: string; + value?: string[]; + label: string; + name: N; + variant?: TagVariant; + onChange?: (newVal: string[], name: N) => void; + disabled?: boolean; + readOnly?: boolean; +} + +function TagInput(props: Props) { + const { + className, + value = emptyValue, + onChange, + name, + label, + variant, + disabled, + readOnly, + tagClassName, + ...otherProps + } = props; + + const [newTagValue, setNewTagValue] = useState(); + const [newTagAddShown, showNewTagAdd, hideNewTagAdd] = useBooleanState(false); + + const handleTagAdd = useCallback(() => { + if (!newTagValue) { + setNewTagValue(undefined); + hideNewTagAdd(); + return; + } + + const indexToRemove = value.indexOf(newTagValue); + if (indexToRemove === -1) { + const newValues = [...value]; + newValues.push(newTagValue); + if (onChange) { + onChange(newValues, name); + } + } + setNewTagValue(undefined); + hideNewTagAdd(); + }, [onChange, value, name, hideNewTagAdd, newTagValue]); + + const handleTagRemove = useCallback((tagToRemove: string) => { + const indexToRemove = value.indexOf(tagToRemove); + if (indexToRemove !== -1) { + const newValues = [...value]; + newValues.splice(indexToRemove, 1); + if (onChange) { + onChange(newValues, name); + } + } + }, [onChange, value, name]); + + const tagRendererParams = useCallback( + (d: string) => ({ + label: d, + onRemove: handleTagRemove, + variant, + disabled, + readOnly, + className: tagClassName, + ...otherProps, + }), + [handleTagRemove, variant, readOnly, disabled, tagClassName, otherProps], + ); + + const handleNewTagAddCancel = useCallback(() => { + setNewTagValue(undefined); + hideNewTagAdd(); + }, [hideNewTagAdd]); + + return ( +
+
+ {label} +
+
+ + {!readOnly && ( + +
+
+ ); +} + +export default TagInput; diff --git a/app/components/TagInput/styles.css b/app/components/TagInput/styles.css new file mode 100644 index 0000000000..db27cc9cc7 --- /dev/null +++ b/app/components/TagInput/styles.css @@ -0,0 +1,45 @@ +.tag-input { + .tag-button { + --padding: var(--dui-spacing-extra-small); + border: none; + border-radius: calc(1em + var(--padding) / 2); + background-color: transparent; + padding: var(--padding); + width: calc(1em + 2 * var(--padding)); + height: calc(1em + 2 * var(--padding)); + color: inherit; + + .children { + padding: 0; + } + + &.check-button { + color: var(--dui-color-success); + } + + &.cancel-button { + color: var(--dui-color-danger); + } + } + + .label { + padding: var(--dui-spacing-small) var(--dui-spacing-medium); + color: var(--dui-color-text-label); + font-size: var(--dui-font-size-small); + font-weight: var(--dui-font-weight-bold); + } + + .tags { + display: flex; + flex-wrap: wrap; + + .tag { + margin: var(--dui-spacing-extra-small); + } + + .tag-actions { + display: flex; + align-items: center; + } + } +} diff --git a/app/views/PillarAnalysis/AnalyticalStatementInput/StoryAnalysisModal/SummaryTagsModal/index.tsx b/app/views/PillarAnalysis/AnalyticalStatementInput/StoryAnalysisModal/SummaryTagsModal/index.tsx new file mode 100644 index 0000000000..a69f54f5fb --- /dev/null +++ b/app/views/PillarAnalysis/AnalyticalStatementInput/StoryAnalysisModal/SummaryTagsModal/index.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +import { + Modal, + Button, +} from '@the-deep/deep-ui'; + +import TagInput from '#components/TagInput'; + +interface Props { + widgetTags: string[]; + setWidgetTags: React.Dispatch>; + handleSubmitButtonClick: () => void; + onCloseButtonClick: () => void; +} + +function SummaryTagsModal(props: Props) { + const { + widgetTags, + setWidgetTags, + handleSubmitButtonClick, + onCloseButtonClick, + } = props; + + return ( + + Submit tags + + )} + > + Please select/add relevant tags to get better summary. + + + + ); +} + +export default SummaryTagsModal; diff --git a/app/views/PillarAnalysis/AnalyticalStatementInput/StoryAnalysisModal/index.tsx b/app/views/PillarAnalysis/AnalyticalStatementInput/StoryAnalysisModal/index.tsx index 5347fafffe..a99dfffa05 100644 --- a/app/views/PillarAnalysis/AnalyticalStatementInput/StoryAnalysisModal/index.tsx +++ b/app/views/PillarAnalysis/AnalyticalStatementInput/StoryAnalysisModal/index.tsx @@ -6,23 +6,31 @@ import React, { } from 'react'; import { Button, + CollapsibleContainer, Container, ListView, - useConfirmation, Modal, QuickActionButton, - CollapsibleContainer, SegmentInput, Tab, TabList, TabPanel, Tabs, TextInput, + Tooltip, + useConfirmation, } from '@the-deep/deep-ui'; -import { isDefined, encodeDate, _cs, unique, listToGroupList } from '@togglecorp/fujs'; +import { + isDefined, + encodeDate, + _cs, + unique, + listToGroupList, +} from '@togglecorp/fujs'; import { IoChevronForward, IoChevronBackOutline, + IoInformation, } from 'react-icons/io5'; import { VscServerProcess } from 'react-icons/vsc'; @@ -385,6 +393,16 @@ function StoryAnalysisModal(props: Props) { > Automatic Summary + {project?.isPrivate && ( +
+ + + Auto summary is not available + for private projects to + maintain document privacy. + +
+ )} )} contentClassName={styles.tabPanelContainer} diff --git a/app/views/PillarAnalysis/AnalyticalStatementInput/StoryAnalysisModal/styles.css b/app/views/PillarAnalysis/AnalyticalStatementInput/StoryAnalysisModal/styles.css index da93d93ee1..cd5fcd0e3f 100644 --- a/app/views/PillarAnalysis/AnalyticalStatementInput/StoryAnalysisModal/styles.css +++ b/app/views/PillarAnalysis/AnalyticalStatementInput/StoryAnalysisModal/styles.css @@ -45,6 +45,18 @@ font-size: var(--dui-font-size-large); font-weight: var(--dui-font-weight-regular); } + .info { + display: flex; + align-self: center; + border: var(--dui-width-separator-thin) solid var(--dui-color-separator); + border-radius: 50%; + padding: var(--dui-spacing-extra-small); + font-size: var(--dui-font-size-medium); + } + + .brain-icon { + align-self: center; + } .tab-panel-container { display: flex; diff --git a/app/views/PillarAnalysis/AnalyticalStatementInput/index.tsx b/app/views/PillarAnalysis/AnalyticalStatementInput/index.tsx index 95bc282b97..758ba47b0d 100644 --- a/app/views/PillarAnalysis/AnalyticalStatementInput/index.tsx +++ b/app/views/PillarAnalysis/AnalyticalStatementInput/index.tsx @@ -15,6 +15,8 @@ import { isDefined, isNotDefined, randomString, + unique, + compareString, } from '@togglecorp/fujs'; import { Tabs, @@ -46,6 +48,8 @@ import { AnalysisGeoLocationMutationVariables, AnalysisAutomaticNgramMutation, AnalysisAutomaticNgramMutationVariables, + PillarAnalysisDetailsQuery, + AttributeType as WidgetAttributeRaw, } from '#generated/types'; import MarkdownEditor from '#components/MarkdownEditor'; @@ -54,6 +58,8 @@ import NonFieldError from '#components/NonFieldError'; import { GeoArea } from '#components/GeoMultiSelectInput'; import { Attributes, Listeners } from '#components/SortableList'; import { reorder, genericMemo } from '#utils/common'; +import { DeepReplace } from '#utils/types'; +import { WidgetAttribute as WidgetAttributeFromEntry } from '#types/newEntry'; import { AnalyticalStatementType, @@ -63,6 +69,7 @@ import { import { Framework } from '..'; import AnalyticalEntryInput from './AnalyticalEntryInput'; +import SummaryTagsModal from './StoryAnalysisModal/SummaryTagsModal'; import StoryAnalysisModal from './StoryAnalysisModal'; import styles from './styles.css'; @@ -122,9 +129,18 @@ const ANALYSIS_GEO_LOCATION = gql` `; const ANALYSIS_AUTOMATIC_SUMMARY = gql` - mutation AnalysisAutomaticSummary($projectId: ID!, $entriesId: [ID!]) { + mutation AnalysisAutomaticSummary( + $projectId: ID!, + $entriesId: [ID!], + $widgetTags: [String!], + ) { project(id: $projectId) { - triggerAnalysisAutomaticSummary(data: {entriesId: $entriesId}) { + triggerAnalysisAutomaticSummary( + data: { + entriesId: $entriesId, + widgetTags: $widgetTags, + } + ) { errors ok result { @@ -152,9 +168,13 @@ export interface DroppedValue { statementClientId?: string; } +type EntryRaw = DeepReplace, WidgetAttributeFromEntry>; +export type EntryDetailType = NonNullable['analysisPillar']>['statements']>[number]>['entries']>[number]; + export interface AnalyticalStatementInputProps { className?: string; value: PartialAnalyticalStatementType; + entriesDetail: EntryDetailType[], error: Error | undefined; onChange: (value: SetValueArg, index: number) => void; onRemove: (index: number) => void; @@ -166,6 +186,7 @@ export interface AnalyticalStatementInputProps { listeners?: Listeners; onSelectedNgramChange: (item: string | undefined) => void; framework: Framework; + frameworkTagLabels: Record; geoAreaOptions: GeoArea[] | undefined | null; setGeoAreaOptions: React.Dispatch>; onEntryDataChange: () => void; @@ -187,11 +208,88 @@ function AnalyticalStatementInput(props: AnalyticalStatementInputProps) { geoAreaOptions, setGeoAreaOptions, onEntryDataChange, + entriesDetail, + frameworkTagLabels, } = props; const { project, } = useContext(ProjectContext); + const [prevSummaryEntryIds, setPrevSummaryEntryIds] = useState(); + + const [ + automaticSummaryTagsModalShown, + showAutomaticSummaryTagsModal, + hideAutomaticSummaryTagsModal, + ] = useModalState(false); + + const [widgetTags, setWidgetTags] = useState(); + + const widgetsFromAttributes = useMemo(() => ( + entriesDetail?.flatMap((entry) => entry?.entry?.attributes) + ?.filter(isDefined) + ), [entriesDetail]); + + const tags = useMemo(() => ( + unique(widgetsFromAttributes?.map( + (attribute) => { + if (!attribute) { + return undefined; + } + if (attribute.widgetType === 'SELECT') { + return attribute.data?.value; + } + if (attribute.widgetType === 'MULTISELECT') { + return attribute.data?.value; + } + if (attribute.widgetType === 'ORGANIGRAM') { + return attribute.data?.value; + } + if (attribute.widgetType === 'MATRIX1D') { + const pillars = attribute.data?.value; + const pillarKeys = Object.keys(pillars ?? []) + ?.map((pillarKey) => Object.keys(pillars?.[pillarKey] ?? {})) + ?.flat(); + + return ([ + ...pillarKeys, + ...(pillars ? Object.keys(pillars) : []), + ]); + } + if (attribute.widgetType === 'MATRIX2D') { + const dims = attribute.data?.value; + + const subPillarList = Object.values(dims ?? {}) + ?.flatMap((subPillar) => Object.values(subPillar ?? {})); + + const pillars = Object.keys(dims ?? {}); + const subPillars = pillars.map((key) => Object.keys(dims?.[key] ?? {})).flat(); + const sectors = unique(subPillarList + .flatMap((sector) => Object.keys(sector ?? {}))); + const subSectors = unique(subPillarList + .flatMap((sector) => Object.values(sector ?? {})) + .flat()); + const widgetKeys = [ + ...pillars, + ...subPillars, + ...sectors, + ...subSectors, + ]; + return widgetKeys; + } + return undefined; + }, + ).filter(isDefined).flat()) + ), [widgetsFromAttributes]); + + const allWidgetTagNames = useMemo(() => ( + tags?.map((key) => key && frameworkTagLabels?.[key]).filter(isDefined) + ), [ + frameworkTagLabels, + tags, + ]); + + console.log('all tags', allWidgetTagNames); const [selectedField, setSelectedField] = useState('statement'); const [selectedContent, setSelectedContent] = useState('entries'); @@ -234,6 +332,7 @@ function AnalyticalStatementInput(props: AnalyticalStatementInputProps) { { variant: 'error' }, ); } + setPrevSummaryEntryIds(value.entries?.map((ae) => ae.entry).filter(isDefined)); }, onError: () => { alert.show( @@ -308,12 +407,13 @@ function AnalyticalStatementInput(props: AnalyticalStatementInputProps) { }, ); - const handleStoryAnalysisModalOpen = useCallback(() => { + const triggerAutomaticStoryAnalysis = useCallback(() => { if (!project?.isPrivate) { createAnalysisAutomaticSummary({ variables: { projectId, entriesId: value.entries?.map((ae) => ae.entry).filter(isDefined), + widgetTags: widgetTags ?? [], }, }); } @@ -332,6 +432,7 @@ function AnalyticalStatementInput(props: AnalyticalStatementInputProps) { showStoryAnalysisModal(); }, [ + widgetTags, value.entries, project, showStoryAnalysisModal, @@ -341,6 +442,50 @@ function AnalyticalStatementInput(props: AnalyticalStatementInputProps) { createAnalysisAutomaticNgram, ]); + const handleStoryAnalysisModalOpen = useCallback(() => { + if (project?.isPrivate) { + showStoryAnalysisModal(); + return; + } + const prevEntryIds = prevSummaryEntryIds?.sort((a, b) => compareString(a, b)); + const prevEntryIdsStringified = JSON.stringify(prevEntryIds); + const currentEntryIds = value.entries + ?.map((ae) => ae.entry).filter(isDefined).sort((a, b) => compareString(a, b)); + const currentEntryIdsStringified = JSON.stringify(currentEntryIds); + + if (prevEntryIdsStringified === currentEntryIdsStringified) { + showStoryAnalysisModal(); + } else { + setWidgetTags(allWidgetTagNames); + showAutomaticSummaryTagsModal(); + } + }, [ + project?.isPrivate, + value.entries, + showStoryAnalysisModal, + allWidgetTagNames, + prevSummaryEntryIds, + showAutomaticSummaryTagsModal, + ]); + + const handleSubmitTagsButtonClick = useCallback(() => { + triggerAutomaticStoryAnalysis(); + hideAutomaticSummaryTagsModal(); + showStoryAnalysisModal(); + }, [ + hideAutomaticSummaryTagsModal, + showStoryAnalysisModal, + triggerAutomaticStoryAnalysis, + ]); + + const handleSummaryTagsModalClose = useCallback(() => { + hideAutomaticSummaryTagsModal(); + // hideStoryAnalysisModal(); + }, [ + // hideStoryAnalysisModal, + hideAutomaticSummaryTagsModal, + ]); + const { // setValue: onAnalyticalEntryChange, removeValue: onAnalyticalEntryRemove, @@ -737,6 +882,15 @@ function AnalyticalStatementInput(props: AnalyticalStatementInputProps) { /> )} + {automaticSummaryTagsModalShown && ( + + )} ); } diff --git a/app/views/PillarAnalysis/AutoClustering/AutoClusteringTagsModal/index.tsx b/app/views/PillarAnalysis/AutoClustering/AutoClusteringTagsModal/index.tsx new file mode 100644 index 0000000000..a79d80afd9 --- /dev/null +++ b/app/views/PillarAnalysis/AutoClustering/AutoClusteringTagsModal/index.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { + Modal, + Button, +} from '@the-deep/deep-ui'; + +import TagInput from '#components/TagInput'; + +interface Props { + onClose: () => void; + widgetTags: string[]; + setWidgetTags: React.Dispatch>; + handleAutoClusteringTriggerClick: () => void; +} + +function AutoClusteringTagsModal(props: Props) { + const { + onClose, + widgetTags, + setWidgetTags, + handleAutoClusteringTriggerClick, + } = props; + + return ( + + Auto Cluster + + )} + heading="Auto Clustering Confirmation" + onCloseButtonClick={onClose} + > + + Are you sure you want to trigger auto clustering of entries + into new stories? This will replace current analytical statements + with suggested groupings using NLP. + Entries from confidential sources are filtered out to maintain document privacy. + + + ); +} + +export default AutoClusteringTagsModal; diff --git a/app/views/PillarAnalysis/AutoClustering/index.tsx b/app/views/PillarAnalysis/AutoClustering/index.tsx index a9fd8c2b4f..4c8e2c0cac 100644 --- a/app/views/PillarAnalysis/AutoClustering/index.tsx +++ b/app/views/PillarAnalysis/AutoClustering/index.tsx @@ -16,7 +16,6 @@ import { Message, Button, useBooleanState, - ConfirmButton, useAlert, } from '@the-deep/deep-ui'; @@ -35,6 +34,7 @@ import { import { PartialAnalyticalStatementType, } from '../schema'; +import AutoClusteringTagsModal from './AutoClusteringTagsModal'; import styles from './styles.css'; @@ -98,12 +98,14 @@ const PILLAR_AUTO_CLUSTERING = gql` $pillarId: ID!, $projectId: ID!, $filterData: EntriesFilterDataInputType, + $widgetTags: [String!], ) { project(id: $projectId) { triggerAnalysisTopicModel( data: { analysisPillar: $pillarId, additionalFilters: $filterData, + widgetTags: $widgetTags, }, ) { ok @@ -125,6 +127,7 @@ interface Props { isPrivateProject?: boolean; onEntriesMappingChange: React.Dispatch>>; onStatementsFromClustersSet: (newStatements: PartialAnalyticalStatementType[]) => void; + widgetTagLabels: string[] | undefined; } function AutoClustering(props: Props) { @@ -136,12 +139,20 @@ function AutoClustering(props: Props) { isPrivateProject, onEntriesMappingChange, onStatementsFromClustersSet, + widgetTagLabels: widgetTagLabelsFromProps, } = props; + const [widgetTags, setWidgetTags] = useState(widgetTagLabelsFromProps); const alert = useAlert(); const [activeTopicModellingId, setActiveTopicModellingId] = useState(); + const [ + widgetTagsModalShown, + showWidgetTagsModal, + hideWidgetTagsModal, + ] = useBooleanState(false); + const [ modalShown, showModal, @@ -235,9 +246,11 @@ function AutoClustering(props: Props) { projectId, pillarId, filterData: entriesFilter, + widgetTags, }, }); }, [ + widgetTags, triggerAutoClustering, entriesFilter, pillarId, @@ -296,6 +309,14 @@ function AutoClustering(props: Props) { buttonTitle = 'DEEP is processing entries'; } + const handleAutoClusterButtonClick = useCallback(() => { + setWidgetTags(widgetTagLabelsFromProps); + showWidgetTagsModal(); + }, [ + showWidgetTagsModal, + widgetTagLabelsFromProps, + ]); + if (activeTopicModellingId) { return ( <> @@ -349,20 +370,31 @@ function AutoClustering(props: Props) { } return ( - - Auto Cluster - + <> + + {widgetTagsModalShown && ( + + )} + ); } diff --git a/app/views/PillarAnalysis/index.tsx b/app/views/PillarAnalysis/index.tsx index b8093e5f44..ba5ed0965f 100644 --- a/app/views/PillarAnalysis/index.tsx +++ b/app/views/PillarAnalysis/index.tsx @@ -104,17 +104,7 @@ import _ts from '#ts'; import { AnalysisPillars } from '#types'; import { WidgetAttribute as WidgetAttributeFromEntry } from '#types/newEntry'; import { FrameworkFilterType, Widget } from '#types/newAnalyticalFramework'; - -/* -import { - projectIdFromRouteSelector, - analysisIdFromRouteSelector, - pillarAnalysisIdFromRouteSelector, - activeProjectFromStateSelector, - editPillarAnalysisPillarAnalysisSelector, - setPillarAnalysisDataAction, -} from '#redux'; -*/ +import { type EntryDetailType } from './AnalyticalStatementInput'; import DiscardedEntries from './DiscardedEntries'; import SourceEntryItem, { Props as SourceEntryItemProps } from './SourceEntryItem'; @@ -458,30 +448,28 @@ const maxItemsPerPage = 25; const entryKeySelector = (d: Entry) => d.id; -/* -const mapStateToProps = (state: AppState, props: unknown) => ({ - // FIXME: get this from url directly - pillarId: pillarAnalysisIdFromRouteSelector(state), - analysisId: analysisIdFromRouteSelector(state), - projectId: projectIdFromRouteSelector(state), - - // FIXME: get this from request - activeProject: activeProjectFromStateSelector(state), - - // FIXME: the inferred typing is wrong in this case - pillarAnalysis: editPillarAnalysisPillarAnalysisSelector(state, props), -}); +type FormType = typeof defaultFormValues; -interface PropsFromDispatch { - setPillarAnalysisData: typeof setPillarAnalysisDataAction; +interface OrganigramDatum { + key: string; + label: string; + children?: OrganigramDatum[]; } -const mapDispatchToProps = (dispatch: Dispatch): PropsFromDispatch => ({ - setPillarAnalysisData: params => dispatch(setPillarAnalysisDataAction(params)), -}); -*/ +function flatten(data: OrganigramDatum) { + let base = { + [data.key]: data.label, + }; -type FormType = typeof defaultFormValues; + data.children?.forEach((child) => { + base = { + ...base, + ...flatten(child), + }; + }); + + return base; +} const statementKeySelector = (d: PartialAnalyticalStatementType) => d.clientId ?? ''; @@ -801,6 +789,47 @@ function PillarAnalysis() { const [entryOrdering, setEntryOrdering] = useState('-created_at'); + /* Contextual data for NLP */ + const widgetTagLabels = useMemo(() => { + const selectedFilters = listToMap( + sourcesFilterValue?.entriesFilterData?.filterableData, + (d) => d.filterKey, + (d) => d.valueList, + ); + + const selectedFilterKeys = Object.keys(selectedFilters ?? {}); + + const widgetTagMap = frameworkFilters + ?.map((widget) => { + if ( + widget.widgetType === 'SELECT' + || widget.widgetType === 'MULTISELECT' + || widget.widgetType === 'ORGANIGRAM' + || widget.widgetType === 'MATRIX1D' + || widget.widgetType === 'MATRIX2D' + ) { + return widget; + } + return undefined; + }).filter(isDefined).filter( + (ff) => selectedFilterKeys.includes(ff.key), + )?.map((ff) => ({ + key: ff.key, + options: ff.properties?.options, + })); + + return widgetTagMap?.flatMap( + (option) => option?.options?.filter( + (widget) => selectedFilters?.[option.key]?.includes(widget.key), + )?.map( + (item: {key: string, label: string}) => item?.label, + ), + ).filter(isDefined); + }, [ + frameworkFilters, + sourcesFilterValue?.entriesFilterData?.filterableData, + ]); + const variables = useMemo( (): ProjectEntriesForAnalysisQueryVariables | undefined => ( (projectId) ? { @@ -836,36 +865,6 @@ function PillarAnalysis() { }, ); - /* - const { - pending: pendingPillarAnalysisSave, - trigger: updateAnalysisPillars, - } = useLazyRequest({ - url: `server://projects/${projectId}/analysis/${analysisId}/pillars/${pillarId}/`, - body: (ctx) => ctx, - method: 'PATCH', - onSuccess: (response) => { - setValue((): FormType => ({ - mainStatement: response.mainStatement, - informationGap: response.informationGap, - statements: response.statements, - })); - alert.show( - _ts('pillarAnalysis', 'pillarAnalysisUpdateSuccess'), - { - variant: 'success', - }, - ); - }, - onFailure: (response, ctx) => { - if (response.value.errors) { - setError(transformErrorToToggleFormError(schema, ctx, response.value.errors)); - } - }, - failureMessage: 'Failed to update pillar analysis.', - }); - */ - const [ updateAnalysisPillars, { @@ -1151,15 +1150,144 @@ function PillarAnalysis() { })); }, [setSourcesFilterValue]); + const entriesForStatements = useMemo(() => listToMap( + analysisPillarDetails?.statements, + (statement) => statement.id, + (statement) => statement.entries?.filter(isDefined), + ), [ + analysisPillarDetails?.statements, + ]); + + const frameworkTagLabels: Record = useMemo(() => { + const widgetTagsFromPrimaryTagging = frameworkDetails?.primaryTagging + ?.flatMap((section) => section.widgets) + ?.map((widget) => { + if (widget?.widgetId === 'MATRIX1D') { + const rows = widget?.properties?.rows; + const cells = rows + ?.flatMap((row) => row.cells); + const rowsWithCells = [ + ...(cells ?? []), + ...(rows ?? []), + ]; + + const keyValueMap = rowsWithCells.reduce( + (obj, item) => Object.assign(obj, { [item.key]: item.label }), {}, + ); + + return keyValueMap; + } + if (widget?.widgetId === 'MATRIX2D') { + const rows = widget?.properties?.rows; + const subRows = rows?.flatMap((row) => row.subRows); + const columns = widget?.properties?.columns; + const subColumns = columns?.flatMap((col) => col.subColumns); + const rowsWithColumns = [ + ...(rows ?? []), + ...(subRows ?? []), + ...(columns ?? []), + ...(subColumns ?? []), + ]; + const keyValueMap = rowsWithColumns.reduce( + (obj, item) => Object.assign(obj, { [item.key]: item.label }), {}, + ); + return keyValueMap; + } + if (widget?.widgetId === 'MULTISELECT') { + const options = widget?.properties?.options; + const keyValueMap = options?.reduce( + (obj, item) => Object.assign(obj, { [item.key]: item.label }), {}, + ); + return keyValueMap; + } + if (widget?.widgetId === 'SELECT') { + const options = widget?.properties?.options; + const keyValueMap = options?.reduce( + (obj, item) => Object.assign(obj, { [item.key]: item.label }), {}, + ); + return keyValueMap; + } + // TODO: add organigram + return undefined; + }).filter(isDefined); + const widgetTagsFromSecondaryTagging = frameworkDetails?.secondaryTagging + ?.map((widget) => { + if (widget?.widgetId === 'MATRIX1D') { + const rows = widget?.properties?.rows; + const cells = rows + ?.flatMap((row) => row.cells); + const rowsWithCells = [ + ...(cells ?? []), + ...(rows ?? []), + ]; + + const keyValueMap = rowsWithCells.reduce( + (obj, item) => Object.assign(obj, { [item.key]: item.label }), {}, + ); + + return keyValueMap; + } + if (widget?.widgetId === 'MATRIX2D') { + const rows = widget?.properties?.rows; + const subRows = rows?.flatMap((row) => row.subRows); + const columns = widget?.properties?.columns; + const subColumns = columns?.flatMap((col) => col.subColumns); + const rowsWithColumns = [ + ...(rows ?? []), + ...(subRows ?? []), + ...(columns ?? []), + ...(subColumns ?? []), + ]; + const keyValueMap = rowsWithColumns.reduce( + (obj, item) => Object.assign(obj, { [item.key]: item.label }), {}, + ); + return keyValueMap; + } + if (widget?.widgetId === 'MULTISELECT') { + const options = widget?.properties?.options; + const keyValueMap = options?.reduce( + (obj, item) => Object.assign(obj, { [item.key]: item.label }), {}, + ); + return keyValueMap; + } + if (widget?.widgetId === 'SELECT') { + const options = widget?.properties?.options; + const keyValueMap = options?.reduce( + (obj, item) => Object.assign(obj, { [item.key]: item.label }), {}, + ); + return keyValueMap; + } + if (widget?.widgetId === 'ORGANIGRAM') { + const options = widget?.properties?.options; + const keyValueMap = isDefined(options) ? flatten(options) : undefined; + return keyValueMap; + } + // TODO: add organigram + return undefined; + }).filter(isDefined); + + return Object.assign( + {}, + ...(widgetTagsFromPrimaryTagging ?? []), + ...(widgetTagsFromSecondaryTagging ?? []), + ); + }, [ + frameworkDetails?.primaryTagging, + frameworkDetails?.secondaryTagging, + ]); + const analyticalStatementRendererParams = useCallback(( - _: string, + id: string, statement: PartialAnalyticalStatementType, index: number, ): AnalyticalStatementInputProps => ({ className: styles.analyticalStatement, index, value: statement, + // TODO: Fix this issue + entriesDetail: entriesForStatements?.[id] as EntryDetailType[], framework: frameworkDetails, + frameworkTagLabels, onChange: onAnalyticalStatementChange, onRemove: onAnalyticalStatementRemove, geoAreaOptions, @@ -1176,9 +1304,11 @@ function PillarAnalysis() { getAnalysisDetails, handleEntryMove, handleEntryDrop, + frameworkTagLabels, frameworkDetails, geoAreaOptions, arrayError, + entriesForStatements, ]); const onOrderChange = useCallback(( @@ -1397,6 +1527,7 @@ function PillarAnalysis() { onStatementsFromClustersSet={ handleStatementsFromClustersSet } + widgetTagLabels={widgetTagLabels} /> {project?.isPrivate && (