diff --git a/app/components/entry/EntryInput/index.tsx b/app/components/entry/EntryInput/index.tsx index 735af63fc6..48bc774b2b 100644 --- a/app/components/entry/EntryInput/index.tsx +++ b/app/components/entry/EntryInput/index.tsx @@ -81,6 +81,9 @@ interface EntryInputProps { allWidgets: Widget[] | undefined | null; rightComponent?: React.ReactNode; noPaddingInWidgetContainer?: boolean; + + excerptShown?: boolean; + displayHorizontally?: boolean; } function EntryInput(props: EntryInputProps) { @@ -111,6 +114,8 @@ function EntryInput(props: EntryInputProp onApplyToAll, rightComponent, noPaddingInWidgetContainer = false, + excerptShown = false, + displayHorizontally = false, } = props; const error = getErrorObject(riskyError); @@ -194,11 +199,12 @@ function EntryInput(props: EntryInputProp className={_cs( className, compactMode && styles.compact, + displayHorizontally && styles.horizontal, styles.entryInput, )} > - {!compactMode && ( + {(!compactMode || excerptShown) && ( (props: EntryInputProp styles.section, sectionContainerClassName, compactMode && styles.compact, + displayHorizontally && styles.horizontal, )} renderer={CompactSection} keySelector={sectionKeySelector} diff --git a/app/components/entry/EntryInput/styles.css b/app/components/entry/EntryInput/styles.css index 018ba54547..86d0ff405c 100644 --- a/app/components/entry/EntryInput/styles.css +++ b/app/components/entry/EntryInput/styles.css @@ -54,6 +54,14 @@ } } + &.horizontal { + flex-direction: row; + .excerpt { + width: 100%; + max-width: 30rem; + } + } + .section { background-color: var(--dui-color-foreground); width: var(--inner-width); @@ -61,6 +69,9 @@ &.compact { border-bottom: var(--dui-width-separator-thin) solid var(--dui-color-separator); } + &.horizontal { + border-bottom: unset; + } } .secondary-tagging { diff --git a/app/components/general/ExportHistory/index.tsx b/app/components/general/ExportHistory/index.tsx index dc539d9edb..103e29fc72 100644 --- a/app/components/general/ExportHistory/index.tsx +++ b/app/components/general/ExportHistory/index.tsx @@ -112,6 +112,8 @@ const PROJECT_EXPORTS = gql` widgetKey } excelDecoupled + dateFormat + reportCitationStyle reportExportingWidgets reportLevels { id diff --git a/app/components/leadFilters/SourcesAppliedFilters/index.tsx b/app/components/leadFilters/SourcesAppliedFilters/index.tsx index adf98de02d..62b1b946c7 100644 --- a/app/components/leadFilters/SourcesAppliedFilters/index.tsx +++ b/app/components/leadFilters/SourcesAppliedFilters/index.tsx @@ -171,7 +171,7 @@ function SourcesAppliedFilters(props: Props) { keySelector={organizationKeySelector} /> diff --git a/app/components/selections/FrameworkTagSelectInput/index.tsx b/app/components/selections/FrameworkTagSelectInput/index.tsx new file mode 100644 index 0000000000..5159a095ba --- /dev/null +++ b/app/components/selections/FrameworkTagSelectInput/index.tsx @@ -0,0 +1,145 @@ +import React, { useMemo, useCallback } from 'react'; +import { + Button, + BadgeInput, + BadgeInputProps, +} from '@the-deep/deep-ui'; +import { useQuery, gql } from '@apollo/client'; + +import { + FrameworkTagOptionsQuery, + FrameworkTagOptionsQueryVariables, +} from '#generated/types'; + +import styles from './styles.css'; + +const FRAMEWORK_TAGS = gql` + query FrameworkTagOptions( + $page: Int, + $pageSize: Int, + ) { + analysisFrameworkTags( + page: $page, + pageSize: $pageSize, + ) { + page + pageSize + results { + description + id + title + icon { + name + url + } + } + totalCount + } + } +`; + +const PAGE_SIZE = 10; + +type BasicFrameworkTag = NonNullable['analysisFrameworkTags']>['results']>[number]; +const keySelector = (item: BasicFrameworkTag) => item.id; +const labelSelector = (item: BasicFrameworkTag) => item.title; +const titleSelector = (item: BasicFrameworkTag) => item.description; +function iconSelector(item: BasicFrameworkTag) { + if (!item.icon?.url) { + return undefined; + } + return ( + {item.icon.url} + ); +} + +type Props = Omit< + BadgeInputProps, + 'options' | 'keySelector' | 'labelSelector' +>; + +function FrameworkTagSelectInput( + props: Props, +) { + const variables = useMemo(() => ({ + page: 1, + pageSize: PAGE_SIZE, + }), []); + + const { + data, + fetchMore, + } = useQuery( + FRAMEWORK_TAGS, + { + variables, + }, + ); + + const handleShowMoreClick = useCallback(() => { + fetchMore({ + variables: { + ...variables, + page: (data?.analysisFrameworkTags?.page ?? 1) + 1, + }, + updateQuery: (previousResult, { fetchMoreResult }) => { + if (!previousResult.analysisFrameworkTags) { + return previousResult; + } + + const oldFrameworkTags = previousResult.analysisFrameworkTags; + const newFrameworkTags = fetchMoreResult?.analysisFrameworkTags; + + if (!newFrameworkTags) { + return previousResult; + } + + return ({ + ...previousResult, + analysisFrameworkTags: { + ...newFrameworkTags, + results: [ + ...(oldFrameworkTags.results ?? []), + ...(newFrameworkTags.results ?? []), + ], + }, + }); + }, + }); + }, [ + data?.analysisFrameworkTags?.page, + fetchMore, + variables, + ]); + + return ( + <> + + {(data?.analysisFrameworkTags?.totalCount ?? 0) + > (data?.analysisFrameworkTags?.results ?? []).length && ( + + )} + + ); +} + +export default FrameworkTagSelectInput; diff --git a/app/components/selections/FrameworkTagSelectInput/styles.css b/app/components/selections/FrameworkTagSelectInput/styles.css new file mode 100644 index 0000000000..965ca45ff6 --- /dev/null +++ b/app/components/selections/FrameworkTagSelectInput/styles.css @@ -0,0 +1,5 @@ +.icon { + width: auto; + height: 1rem; + object-fit: contain; +} diff --git a/app/types/user.tsx b/app/types/user.tsx index 7978737a5e..79f3002f52 100644 --- a/app/types/user.tsx +++ b/app/types/user.tsx @@ -36,12 +36,12 @@ export interface Assignment { displayName: string; email: string; }; - contentObjectDetails: { + contentObjectDetails?: { id: number; title: string; lead?: string; entry?: string; - }; + } | undefined; isDone: boolean; contentObjectType: 'lead' | 'entryreviewcomment' | 'entrycomment'; } diff --git a/app/views/AnalyticalFramework/AssistedTagging/WidgetTagList/Matrix2dTagInput/index.tsx b/app/views/AnalyticalFramework/AssistedTagging/WidgetTagList/Matrix2dTagInput/index.tsx index 5bc332d7a7..e6a6a372b4 100644 --- a/app/views/AnalyticalFramework/AssistedTagging/WidgetTagList/Matrix2dTagInput/index.tsx +++ b/app/views/AnalyticalFramework/AssistedTagging/WidgetTagList/Matrix2dTagInput/index.tsx @@ -139,7 +139,7 @@ function Matrix2dTagInput(props: Props) { const subColumnMappings = useMemo(() => ( mappings?.filter((mappingItem): mappingItem is SubColumnMappingItem => ( - mappingItem.association.type === 'SUB_ROW' + mappingItem.association.type === 'SUB_COLUMN' )) ), [ mappings, diff --git a/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx b/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx index 007552694b..7aeab3323a 100644 --- a/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx +++ b/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx @@ -4,16 +4,11 @@ import { Kraken, Container, Message, - QuickActionButton, } from '@the-deep/deep-ui'; import { SetValueArg, Error, } from '@togglecorp/toggle-form'; -import { - IoClose, -} from 'react-icons/io5'; -import { FiEdit2 } from 'react-icons/fi'; import EntryInput from '#components/entry/EntryInput'; import { GeoArea } from '#components/GeoMultiSelectInput'; @@ -26,38 +21,42 @@ import { import styles from './styles.css'; -interface Props { +interface Props { className?: string; + entryInputClassName?: string; frameworkDetails: Framework; leadId: string; value: PartialEntryType; - onChange: (val: SetValueArg, name: undefined) => void; + onChange: (val: SetValueArg, name: NAME) => void; error: Error | undefined; - onEntryCreateButtonClick: () => void; + variant?: 'normal' | 'compact' | 'nlp'; // NOTE: Normal entry creation refers to entry created without use of // recommendations - onNormalEntryCreateButtonClick: () => void; - onEntryDiscardButtonClick: () => void; geoAreaOptions: GeoArea[] | undefined | null; onGeoAreaOptionsChange: React.Dispatch>; predictionsLoading?: boolean; hints: WidgetHint[] | undefined; recommendations: PartialAttributeType[] | undefined; predictionsErrored: boolean; + name: NAME; messageText: string | undefined; + excerptShown?: boolean; + displayHorizontally?: boolean; + + footerActions: React.ReactNode; } -function AssistPopup(props: Props) { +function AssistPopup(props: Props) { const { className, + entryInputClassName, + variant = 'nlp', leadId, value, onChange, + name, error, frameworkDetails, - onEntryCreateButtonClick, - onNormalEntryCreateButtonClick, - onEntryDiscardButtonClick, geoAreaOptions, onGeoAreaOptionsChange, predictionsLoading, @@ -65,6 +64,9 @@ function AssistPopup(props: Props) { predictionsErrored, messageText, recommendations, + excerptShown = false, + displayHorizontally = false, + footerActions, } = props; const allWidgets = useMemo(() => { @@ -89,28 +91,7 @@ function AssistPopup(props: Props) { // heading="Assisted Tagging" headingSize="extraSmall" spacing="compact" - footerQuickActions={( - <> - - - - - - - - )} + footerQuickActions={footerActions} contentClassName={styles.body} > {isMessageShown ? ( @@ -136,9 +117,9 @@ function AssistPopup(props: Props) { /> ) : ( )} diff --git a/app/views/EntryEdit/LeftPane/AssistItem/index.tsx b/app/views/EntryEdit/LeftPane/AssistItem/index.tsx index decf7093f2..af2cbfa8b4 100644 --- a/app/views/EntryEdit/LeftPane/AssistItem/index.tsx +++ b/app/views/EntryEdit/LeftPane/AssistItem/index.tsx @@ -18,6 +18,7 @@ import { Container, } from '@the-deep/deep-ui'; import { IoClose } from 'react-icons/io5'; +import { FiEdit2 } from 'react-icons/fi'; import { GeoArea } from '#components/GeoMultiSelectInput'; import brainIcon from '#resources/img/brain.svg'; @@ -152,7 +153,11 @@ interface Props { className?: string; text: string; onAssistedEntryAdd: ( - (newEntry: EntryInput, locations?: GeoArea[]) => void + ( + newEntry: EntryInput, + locations?: GeoArea[], + selectCreatedEntry?: boolean, + ) => void ) | undefined; frameworkDetails?: Framework; leadId: string; @@ -641,6 +646,7 @@ function AssistItem(props: Props) { draftEntry: data?.project?.assistedTagging?.draftEntry?.id, }, geoAreaOptions ?? undefined, + true, ); } }, @@ -741,13 +747,11 @@ function AssistItem(props: Props) { frameworkDetails={frameworkDetails} value={value} onChange={setValue} + name={undefined} error={error} leadId={leadId} hints={allHints} recommendations={allRecommendations} - onEntryDiscardButtonClick={handleDiscardButtonClick} - onEntryCreateButtonClick={handleEntryCreateButtonClick} - onNormalEntryCreateButtonClick={handleNormalEntryCreateButtonClick} geoAreaOptions={geoAreaOptions} onGeoAreaOptionsChange={setGeoAreaOptions} predictionsLoading={ @@ -757,6 +761,34 @@ function AssistItem(props: Props) { } predictionsErrored={!!fetchErrors || !!createErrors || isErrored} messageText={messageText} + footerActions={( + <> + + + + + + + + )} /> )} diff --git a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx new file mode 100644 index 0000000000..4cb73df613 --- /dev/null +++ b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx @@ -0,0 +1,1000 @@ +import React, { + useMemo, + useEffect, + useCallback, + useState, +} from 'react'; +import { + isNotDefined, + isDefined, + randomString, + noOp, + listToMap, +} from '@togglecorp/fujs'; +import { + useForm, + useFormArray, +} from '@togglecorp/toggle-form'; +import { + gql, + useQuery, + useMutation, +} from '@apollo/client'; +import { + Modal, + ListView, + Tab, + Tabs, + TabPanel, + TabList, + useAlert, + Button, +} from '@the-deep/deep-ui'; + +import { mergeLists } from '#utils/common'; +import { type Framework } from '#components/entry/types'; +import { type GeoArea } from '#components/GeoMultiSelectInput'; +import { + mappingsSupportedWidgets, + isCategoricalMappings, + WidgetHint, + filterMatrix1dMappings, + filterMatrix2dMappings, + filterScaleMappings, + filterSelectMappings, + filterMultiSelectMappings, + filterOrganigramMappings, + type MappingsItem, +} from '#types/newAnalyticalFramework'; +import getSchema, { + defaultFormValues, + PartialEntryType, + PartialAttributeType, +} from '#components/entry/schema'; +import { + AutoEntriesForLeadQuery, + AutoEntriesForLeadQueryVariables, + CreateAutoDraftEntriesMutation, + CreateAutoDraftEntriesMutationVariables, + AutoDraftEntriesStatusQuery, + AutoDraftEntriesStatusQueryVariables, + UpdateDraftEntryMutation, + UpdateDraftEntryMutationVariables, +} from '#generated/types'; +import AssistPopup from '../AssistItem/AssistPopup'; +import { createDefaultAttributes } from '../../utils'; +import { + createOrganigramAttr, + createMatrix1dAttr, + createMatrix2dAttr, + createScaleAttr, + createSelectAttr, + createMultiSelectAttr, + createGeoAttr, +} from '../AssistItem/utils'; + +import styles from './styles.css'; + +const GEOLOCATION_DEEPL_MODEL_ID = 'geolocation'; + +const AUTO_ENTRIES_FOR_LEAD = gql` + query AutoEntriesForLead( + $projectId: ID!, + $leadId: ID!, + $isDiscarded: Boolean, + ) { + project(id: $projectId) { + id + assistedTagging { + draftEntryByLeads( + filter: { + draftEntryTypes: AUTO, + leads: $leadId, + isDiscarded: $isDiscarded, + }) { + id + excerpt + predictionReceivedAt + predictionStatus + predictions { + id + draftEntry + tag + dataTypeDisplay + dataType + category + isSelected + modelVersion + modelVersionDeeplModelId + prediction + threshold + value + } + } + } + } + } +`; + +const CREATE_AUTO_DRAFT_ENTRIES = gql` + mutation CreateAutoDraftEntries ( + $projectId: ID!, + $leadId: ID!, + ) { + project(id: $projectId) { + id + assistedTagging { + triggerAutoDraftEntry(data: {lead: $leadId}) { + ok + errors + } + } + } + } +`; + +const AUTO_DRAFT_ENTRIES_STATUS = gql` + query AutoDraftEntriesStatus ( + $projectId: ID!, + $leadId: ID!, + ) { + project(id: $projectId) { + id + assistedTagging { + extractionStatusByLead(leadId: $leadId) { + autoEntryExtractionStatus + } + } + } + } +`; + +const UPDATE_DRAFT_ENTRY = gql` + mutation UpdateDraftEntry( + $projectId: ID!, + $draftEntryId: ID!, + $input: UpdateDraftEntryInputType!, + ){ + project(id: $projectId) { + id + assistedTagging { + updateDraftEntry( + data: $input, + id: $draftEntryId, + ) { + errors + ok + } + } + } + } +`; + +interface EntryAttributes { + predictions: { + tags: string[]; + locations: GeoArea[]; + }; + mappings: MappingsItem[] | null | undefined; + filteredWidgets: NonNullable[number]['widgets'] + | NonNullable; +} + +function handleMappingsFetch(entryAttributes: EntryAttributes) { + const { + predictions, + mappings, + filteredWidgets, + } = entryAttributes; + + if (predictions.tags.length <= 0 && predictions.locations.length <= 0) { + // setMessageText('DEEP could not provide any recommendations for the selected text.'); + return {}; + } + + if (isNotDefined(filteredWidgets)) { + return {}; + } + + const matchedMappings = mappings + ?.filter(isCategoricalMappings) + .filter((m) => m.tag && predictions.tags.includes(m.tag)); + + const supportedGeoWidgets = mappings + ?.filter((mappingItem) => mappingItem.widgetType === 'GEO') + ?.map((mappingItem) => mappingItem.widget); + + const { + tempAttrs: recommendedAttributes, + tempHints: widgetsHints, + } = filteredWidgets.reduce( + ( + acc: { tempAttrs: PartialAttributeType[]; tempHints: WidgetHint[]; }, + widget, + ) => { + const { + tempAttrs: oldTempAttrs, + tempHints: oldTempHints, + } = acc; + + if (widget.widgetId === 'MATRIX1D') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterMatrix1dMappings); + + const attr = createMatrix1dAttr(supportedTags, widget); + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + + if (widget.widgetId === 'MATRIX2D') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterMatrix2dMappings); + + const attr = createMatrix2dAttr(supportedTags, widget); + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + if (widget.widgetId === 'SCALE') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterScaleMappings); + + const { + attr, + hints, + } = createScaleAttr(supportedTags, widget); + + const hintsWithInfo: WidgetHint | undefined = hints ? { + widgetPk: widget.id, + widgetType: 'SCALE', + hints, + } : undefined; + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: hintsWithInfo + ? [...oldTempHints, hintsWithInfo] + : oldTempHints, + }; + } + if (widget.widgetId === 'SELECT') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterSelectMappings); + + const { + attr, + hints, + } = createSelectAttr(supportedTags, widget); + + const hintsWithInfo: WidgetHint | undefined = hints ? { + widgetPk: widget.id, + widgetType: 'SELECT', + hints, + } : undefined; + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: hintsWithInfo + ? [...oldTempHints, hintsWithInfo] + : oldTempHints, + }; + } + if (widget.widgetId === 'MULTISELECT') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterMultiSelectMappings); + + const attr = createMultiSelectAttr( + supportedTags, + widget, + ); + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + if (widget.widgetId === 'ORGANIGRAM') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterOrganigramMappings); + + const attr = createOrganigramAttr( + supportedTags, + widget, + ); + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + if ( + widget.widgetId === 'GEO' + && predictions.locations.length > 0 + && supportedGeoWidgets?.includes(widget.id) + ) { + const attr = createGeoAttr( + predictions.locations, + widget, + ); + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + return acc; + }, + { + tempAttrs: [], + tempHints: [], + }, + ); + + if (recommendedAttributes.length <= 0 && widgetsHints.length <= 0) { + // setMessageText( + // 'The provided recommendations for this text did not fit any tags in this project.', + // ); + return {}; + } + + return { + hints: widgetsHints, + recommendations: recommendedAttributes, + geoAreas: predictions.locations, + }; +} + +const entryKeySelector = (entry: PartialEntryType) => entry.clientId; + +type EntriesTabType = 'extracted' | 'discarded'; + +interface Props { + onModalClose: () => void; + projectId: string; + leadId: string; + frameworkDetails: Framework; + createdEntries: PartialEntryType[] | undefined | null; + onAssistedEntryAdd: ( + (newEntry: PartialEntryType, locations?: GeoArea[]) => void + ) | undefined; +} + +function AutoEntriesModal(props: Props) { + const { + onModalClose, + projectId, + leadId, + onAssistedEntryAdd, + frameworkDetails, + createdEntries, + } = props; + + const alert = useAlert(); + const draftEntriesMap = useMemo(() => ( + listToMap( + createdEntries?.filter((item) => isDefined(item.draftEntry)) ?? [], + (item) => item.draftEntry ?? '', + () => true, + ) + ), [createdEntries]); + + const [ + selectedTab, + setSelectedTab, + ] = useState('extracted'); + + const { + allWidgets, + filteredWidgets, + } = useMemo(() => { + const widgetsFromPrimary = frameworkDetails?.primaryTagging?.flatMap( + (item) => (item.widgets ?? []), + ) ?? []; + const widgetsFromSecondary = frameworkDetails?.secondaryTagging ?? []; + const widgets = [ + ...widgetsFromPrimary, + ...widgetsFromSecondary, + ]; + return { + allWidgets: widgets, + filteredWidgets: widgets.filter((w) => mappingsSupportedWidgets.includes(w.widgetId)), + }; + }, [ + frameworkDetails, + ]); + + const schema = useMemo( + () => { + const widgetsMapping = listToMap( + allWidgets, + (item) => item.id, + (item) => item, + ); + + return getSchema(widgetsMapping); + }, + [allWidgets], + ); + const { + setValue, + value, + setFieldValue, + } = useForm(schema, defaultFormValues); + + const { + setValue: onEntryChange, + } = useFormArray<'entries', PartialEntryType>('entries', setFieldValue); + + const [ + geoAreaOptions, + setGeoAreaOptions, + ] = useState | undefined>(undefined); + + // FIXME: randomId is used to create different query variables after each poll + // so that apollo doesn't create unnecessary cache + const [randomId, setRandomId] = useState(randomString()); + + const autoEntryStatusVariables = useMemo(() => { + if (isNotDefined(projectId)) { + return undefined; + } + return ({ + leadId, + randomId, + projectId, + }); + }, [ + randomId, + leadId, + projectId, + ]); + + const [draftEntriesLoading, setDraftEntriesLoading] = useState(true); + + const { + data: autoEntryExtractionStatus, + refetch: retriggerEntryExtractionStatus, + } = useQuery( + AUTO_DRAFT_ENTRIES_STATUS, + { + skip: isNotDefined(autoEntryStatusVariables), + variables: autoEntryStatusVariables, + onCompleted: (response) => { + const status = response?.project + ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus; + if (status === 'SUCCESS') { + setDraftEntriesLoading(false); + } + }, + }, + ); + + const extractionStatus = autoEntryExtractionStatus?.project + ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus; + + // TODO: This polling calls two queries at a time. Fix this. + useEffect(() => { + const timeout = setTimeout( + () => { + const shouldPoll = extractionStatus === 'PENDING' || extractionStatus === 'STARTED'; + if (shouldPoll) { + setDraftEntriesLoading(true); + setRandomId(randomString()); + retriggerEntryExtractionStatus(); + } else { + setDraftEntriesLoading(false); + } + }, + 2000, + ); + + return () => { + clearTimeout(timeout); + }; + }, [ + extractionStatus, + leadId, + retriggerEntryExtractionStatus, + ]); + + const [ + triggerAutoEntriesCreate, + ] = useMutation( + CREATE_AUTO_DRAFT_ENTRIES, + { + onCompleted: (response) => { + const autoEntriesResponse = response?.project?.assistedTagging + ?.triggerAutoDraftEntry; + if (autoEntriesResponse?.ok) { + retriggerEntryExtractionStatus(); + } else { + alert.show( + 'Failed to extract entries using NLP.', + { + variant: 'error', + }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to extract entries using NLP.', + { + variant: 'error', + }, + ); + }, + }, + ); + + const handleAutoExtractClick = useCallback(() => { + triggerAutoEntriesCreate({ + variables: { + projectId, + leadId, + }, + }); + }, [ + projectId, + leadId, + triggerAutoEntriesCreate, + ]); + + const [ + allRecommendations, + setAllRecommendations, + ] = useState | undefined>(undefined); + + const [allHints, setAllHints] = useState< + Record | undefined + >(undefined); + + const mappings = frameworkDetails?.predictionTagsMapping; + + const autoEntriesVariables = useMemo(() => ({ + projectId, + leadId, + isDiscarded: selectedTab === 'discarded', + }), [ + projectId, + leadId, + selectedTab, + ]); + + const { + data: autoEntries, + loading: autoEntriesLoading, + refetch: retriggerAutoEntriesFetch, + } = useQuery( + AUTO_ENTRIES_FOR_LEAD, + { + skip: isNotDefined(extractionStatus) + || extractionStatus !== 'SUCCESS' + || isNotDefined(autoEntriesVariables), + variables: autoEntriesVariables, + // TODO: This is due to caching issue in apollo. + notifyOnNetworkStatusChange: true, + onCompleted: (response) => { + const entries = response.project?.assistedTagging?.draftEntryByLeads; + const transformedEntries = (entries ?? [])?.map((entry) => { + const validPredictions = entry.predictions?.filter(isDefined); + const categoricalTags = validPredictions?.filter( + (prediction) => ( + prediction.modelVersionDeeplModelId !== GEOLOCATION_DEEPL_MODEL_ID + && prediction.isSelected + ), + ).map( + (prediction) => prediction.tag, + ).filter(isDefined) ?? []; + + const entryAttributeData: EntryAttributes = { + predictions: { + tags: categoricalTags, + locations: [], + }, + mappings, + filteredWidgets, + }; + + const { + hints: entryHints, + recommendations: entryRecommendations, + geoAreas: entryGeoAreas, + } = handleMappingsFetch(entryAttributeData); + + const entryId = randomString(); + const requiredEntry = { + clientId: entryId, + entryType: 'EXCERPT' as const, + lead: leadId, + excerpt: entry.excerpt, + draftEntry: entry.id, + droppedExcerpt: entry.excerpt, + attributes: entryRecommendations?.map((attr) => { + if (attr.widgetType !== 'GEO') { + return attr; + } + // NOTE: Selecting only the 1st recommendation + return ({ + ...attr, + data: { + value: attr?.data?.value.slice(0, 1) ?? [], + }, + }); + }), + }; + + return { + entryId, + geoLocations: entryGeoAreas, + recommendations: entryRecommendations, + hints: entryHints, + entry: requiredEntry, + }; + }); + const requiredDraftEntries = transformedEntries?.map( + (draftEntry) => draftEntry.entry, + ); + const entryRecommendations = listToMap( + transformedEntries, + (item) => item.entryId, + (item) => item.recommendations, + ); + const entryHints = listToMap( + transformedEntries, + (item) => item.entryId, + (item) => item.hints, + ); + const entryGeoAreas = listToMap( + transformedEntries, + (item) => item.entryId, + (item) => item.geoLocations, + ); + setValue({ + entries: requiredDraftEntries, + }); + setAllRecommendations(entryRecommendations); + setAllHints(entryHints); + setGeoAreaOptions(entryGeoAreas); + }, + }, + ); + + const handleEntryCreateButtonClick = useCallback((entryId: string) => { + if (!allRecommendations?.[entryId]) { + return; + } + + const selectedEntry = value?.entries?.find((item) => item.clientId === entryId); + if (onAssistedEntryAdd && selectedEntry) { + const duplicateEntryCheck = createdEntries?.find( + (entry) => entry.droppedExcerpt === selectedEntry.droppedExcerpt, + ); + + if (isDefined(duplicateEntryCheck)) { + alert.show( + 'Similar entry found. Failed to add entry from recommendations.', + { + variant: 'error', + }, + ); + return; + } + + const defaultAttributes = createDefaultAttributes(allWidgets); + + const newAttributes = mergeLists( + defaultAttributes, + selectedEntry?.attributes ?? [], + (attr) => attr.widget, + (defaultAttr, newAttr) => ({ + ...newAttr, + clientId: defaultAttr.clientId, + widget: defaultAttr.widget, + id: defaultAttr.id, + widgetVersion: defaultAttr.widgetVersion, + }), + ); + + onAssistedEntryAdd( + { + ...selectedEntry, + attributes: newAttributes, + }, + geoAreaOptions?.[entryId] ?? undefined, + ); + + alert.show( + 'Successfully added entry from recommendation.', + { + variant: 'success', + }, + ); + } else { + alert.show( + 'Failed to add entry from recommendations.', + { + variant: 'error', + }, + ); + } + }, [ + alert, + value?.entries, + allWidgets, + geoAreaOptions, + allRecommendations, + onAssistedEntryAdd, + createdEntries, + ]); + + const [ + triggerUpdateDraftEntry, + ] = useMutation( + UPDATE_DRAFT_ENTRY, + { + onCompleted: (response) => { + const updateDraftEntryResponse = response?.project?.assistedTagging + ?.updateDraftEntry; + retriggerAutoEntriesFetch(); + if (updateDraftEntryResponse?.ok) { + alert.show( + 'Successfully changed the discard status.', + { + variant: 'success', + }, + ); + } else { + alert.show( + 'Failed to change the discard status.', + { + variant: 'error', + }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to change the discard status.', + { + variant: 'error', + }, + ); + }, + }, + ); + + const handleUpdateDraftEntryClick = useCallback((entryId: string | undefined) => { + triggerUpdateDraftEntry({ + variables: { + projectId, + input: { + lead: leadId, + isDiscarded: true, + }, + // FIXME: Handle this better + draftEntryId: entryId ?? '', + }, + }); + }, [ + triggerUpdateDraftEntry, + leadId, + projectId, + ]); + + const handleUndiscardEntryClick = useCallback((entryId: string | undefined) => { + triggerUpdateDraftEntry({ + variables: { + projectId, + input: { + lead: leadId, + isDiscarded: false, + }, + // FIXME: Handle this better + draftEntryId: entryId ?? '', + }, + }); + }, [ + triggerUpdateDraftEntry, + leadId, + projectId, + ]); + + const filteredEntries = useMemo(() => ( + value?.entries?.filter( + (item) => item.draftEntry && !draftEntriesMap[item.draftEntry], + ) + ), [ + value?.entries, + draftEntriesMap, + ]); + + const rendererParams = useCallback(( + entryId: string, + datum: PartialEntryType, + ) => { + const onEntryCreateButtonClick = () => handleEntryCreateButtonClick(entryId); + const index = value?.entries?.findIndex((item) => item.clientId === entryId); + const footerActions = (selectedTab === 'extracted' ? ( +
+ + +
+ ) : ( + + )); + + return ({ + frameworkDetails, + value: datum, + className: styles.listItem, + entryInputClassName: styles.entryInput, + name: index, + onChange: onEntryChange, + leadId, + hints: allHints?.[entryId], + recommendations: allRecommendations?.[entryId], + geoAreaOptions: geoAreaOptions?.[entryId], + onGeoAreaOptionsChange: noOp, + predictionsLoading: false, + predictionsErrored: false, + messageText: undefined, + variant: 'normal' as const, + error: undefined, + excerptShown: true, + displayHorizontally: true, + footerActions, + }); + }, [ + value?.entries, + handleEntryCreateButtonClick, + onEntryChange, + allHints, + allRecommendations, + frameworkDetails, + leadId, + geoAreaOptions, + handleUpdateDraftEntryClick, + handleUndiscardEntryClick, + selectedTab, + ]); + + const isFiltered = useMemo(() => ( + (filteredEntries?.length ?? 0) < (value?.entries?.length ?? 0) + ), [ + filteredEntries, + value?.entries, + ]); + + /* + const hideList = draftEntriesLoading + || autoEntriesLoading + || extractionStatus === 'NONE'; + */ + + return ( + + + + + All Recommendations + + + Discarded Recommendations + + + + + Recommend entries + + )} + messageShown + messageIconShown + borderBetweenItem + /> + + + + Recommend entries + + )} + messageShown + messageIconShown + borderBetweenItem + /> + + + + ); +} + +export default AutoEntriesModal; diff --git a/app/views/EntryEdit/LeftPane/AutoEntriesModal/styles.css b/app/views/EntryEdit/LeftPane/AutoEntriesModal/styles.css new file mode 100644 index 0000000000..1abbc56c75 --- /dev/null +++ b/app/views/EntryEdit/LeftPane/AutoEntriesModal/styles.css @@ -0,0 +1,12 @@ +.modal-body { + .list-item { + .entry-input { + height: 360px; + } + } + + .footer-buttons { + display: flex; + gap: var(--dui-spacing-medium); + } +} diff --git a/app/views/EntryEdit/LeftPane/SimplifiedTextView/index.tsx b/app/views/EntryEdit/LeftPane/SimplifiedTextView/index.tsx index 1acdfb8796..4c644cd819 100644 --- a/app/views/EntryEdit/LeftPane/SimplifiedTextView/index.tsx +++ b/app/views/EntryEdit/LeftPane/SimplifiedTextView/index.tsx @@ -326,7 +326,6 @@ function SimplifiedTextView(props: Props) { styles.simplifiedTextView, className, disableAddButton && styles.disabled, - assistedTaggingEnabled && styles.assistedEnabled, )} > {children} diff --git a/app/views/EntryEdit/LeftPane/SimplifiedTextView/styles.css b/app/views/EntryEdit/LeftPane/SimplifiedTextView/styles.css index 8d293e7518..5b04b7b114 100644 --- a/app/views/EntryEdit/LeftPane/SimplifiedTextView/styles.css +++ b/app/views/EntryEdit/LeftPane/SimplifiedTextView/styles.css @@ -16,13 +16,6 @@ margin: var(--dui-spacing-medium) 0; } - &.assisted-enabled { - >*::selection { - background: var(--dui-color-nlp); - color: var(--dui-color-text-on-accent); - } - } - .actions-popup { display: flex; position: absolute; diff --git a/app/views/EntryEdit/LeftPane/index.tsx b/app/views/EntryEdit/LeftPane/index.tsx index 7df23619e9..7cee0ecd29 100644 --- a/app/views/EntryEdit/LeftPane/index.tsx +++ b/app/views/EntryEdit/LeftPane/index.tsx @@ -15,6 +15,7 @@ import { TextInput, QuickActionButton, useBooleanState, + useModalState, Button, QuickActionDropdownMenu, QuickActionDropdownMenuProps, @@ -24,7 +25,6 @@ import { QuickActionLink, PendingMessage, Message, - Switch, } from '@the-deep/deep-ui'; import { IoAdd, @@ -37,7 +37,6 @@ import { IoCheckmark, } from 'react-icons/io5'; -import useLocalStorage from '#hooks/useLocalStorage'; import { GeoArea } from '#components/GeoMultiSelectInput'; import LeadPreview from '#components/lead/LeadPreview'; import Screenshot from '#components/Screenshot'; @@ -55,6 +54,7 @@ import CanvasDrawModal from './CanvasDrawModal'; import { Lead, EntryImagesMap } from '../index'; import SimplifiedTextView from './SimplifiedTextView'; import EntryItem, { ExcerptModal } from './EntryItem'; +import AutoEntriesModal from './AutoEntriesModal'; import styles from './styles.css'; const LEAD_PREVIEW = gql` @@ -150,9 +150,10 @@ function LeftPane(props: Props) { ); const [ - assistedTaggingEnabled, - onAssistedTaggingStatusChange, - ] = useLocalStorage('assisted-tagging-enabled', false); + autoEntriesModalShown, + showAutoEntriesModal, + hideAutoEntriesModal, + ] = useModalState(false); useEffect(() => { if (activeTabRef) { @@ -501,8 +502,7 @@ function LeftPane(props: Props) { ); - const assistedTaggingShown = assistedTaggingEnabled - && !project?.isPrivate + const assistedTaggingShown = !project?.isPrivate && isAssistedTaggingAccessible && frameworkDetails?.assistedTaggingEnabled && (frameworkDetails?.predictionTagsMapping?.length ?? 0) > 0; @@ -544,21 +544,17 @@ function LeftPane(props: Props) { activeClassName={styles.simplifiedTab} retainMount="lazy" > - {(leadPreview?.textExtract?.length ?? 0) > 0 ? ( - <> - {(frameworkDetails?.predictionTagsMapping?.length ?? 0) > 0 - && !project?.isPrivate - && frameworkDetails?.assistedTaggingEnabled - && isAssistedTaggingAccessible - && ( - - )} + <> + + + {(leadPreview?.textExtract?.length ?? 0) > 0 ? ( - - ) : ( - - )} - message={( - (extractionStatus === 'PENDING' - || extractionStatus === 'STARTED') - ? 'Simplified text is currently being extracted from this source. Please retry after few minutes.' - : 'Oops! Either the source was empty or we couldn\'t extract its text.' - )} - errored={extractionStatus === 'FAILED'} - erroredEmptyIcon={( - - )} - erroredEmptyMessage="There was an when issue extracting simplified - text for this source." - actions={(extractionStatus === 'PENDING' || extractionStatus === 'STARTED') && ( - - )} - /> - )} + ) : ( + + )} + message={( + (extractionStatus === 'PENDING' + || extractionStatus === 'STARTED') + ? 'Simplified text is currently being extracted from this source. Please retry after few minutes.' + : 'Oops! Either the source was empty or we couldn\'t extract its text.' + )} + errored={extractionStatus === 'FAILED'} + erroredEmptyIcon={( + + )} + erroredEmptyMessage="There was an when issue extracting simplified + text for this source." + actions={(extractionStatus === 'PENDING' || extractionStatus === 'STARTED') && ( + + )} + /> + )} + )} {!hideOriginalPreview && ( @@ -660,6 +656,16 @@ function LeftPane(props: Props) { /> + {autoEntriesModalShown && isDefined(projectId) && frameworkDetails && ( + + )} ); } diff --git a/app/views/EntryEdit/LeftPane/styles.css b/app/views/EntryEdit/LeftPane/styles.css index cefec76d1f..e3b035c498 100644 --- a/app/views/EntryEdit/LeftPane/styles.css +++ b/app/views/EntryEdit/LeftPane/styles.css @@ -31,7 +31,7 @@ background-color: var(--dui-color-background-information); padding: var(--dui-spacing-large); overflow: auto; - gap: var(--dui-spacing-small); + gap: var(--dui-spacing-medium); .switch { flex-shrink: 0; diff --git a/app/views/EntryEdit/index.tsx b/app/views/EntryEdit/index.tsx index 8f4f081c3d..b45e888c7e 100644 --- a/app/views/EntryEdit/index.tsx +++ b/app/views/EntryEdit/index.tsx @@ -907,8 +907,14 @@ function EntryEdit(props: Props) { ); const handleAssistedEntryAdd = useCallback( - (newValue: PartialEntryType, newGeoAreaOptions?: GeoArea[]) => { - createRestorePoint(); + ( + newValue: PartialEntryType, + newGeoAreaOptions?: GeoArea[], + selectCreatedEntry = false, + ) => { + if (selectCreatedEntry) { + createRestorePoint(); + } setFormFieldValue( (prevValue: PartialFormType['entries']) => [ ...(prevValue ?? []), @@ -919,7 +925,9 @@ function EntryEdit(props: Props) { ], 'entries', ); - setSelectedEntry(newValue.clientId); + if (selectCreatedEntry) { + setSelectedEntry(newValue.clientId); + } if (newGeoAreaOptions && newGeoAreaOptions.length > 0) { setGeoAreaOptions((oldAreas) => { const newAreas = unique([ diff --git a/app/views/Home/Assignment/AssignmentItem/index.tsx b/app/views/Home/Assignment/AssignmentItem/index.tsx index 6386d1ff64..97ef179d29 100644 --- a/app/views/Home/Assignment/AssignmentItem/index.tsx +++ b/app/views/Home/Assignment/AssignmentItem/index.tsx @@ -55,7 +55,7 @@ function AssignmentItem(props: AssignmentItemProps) { )); } if (contentObjectType === 'entryreviewcomment' || contentObjectType === 'entrycomment') { - if (!projectDetails?.id && contentObjectDetails?.lead) { + if (!contentObjectDetails || !projectDetails?.id || !contentObjectDetails?.lead) { return ( an entry diff --git a/app/views/NewExport/AdvancedOptionsSelection/index.tsx b/app/views/NewExport/AdvancedOptionsSelection/index.tsx index d0a2150831..6447cfb767 100644 --- a/app/views/NewExport/AdvancedOptionsSelection/index.tsx +++ b/app/views/NewExport/AdvancedOptionsSelection/index.tsx @@ -4,12 +4,18 @@ import { Modal, Checkbox, Tag, + RadioInput, } from '@the-deep/deep-ui'; +import { useQuery, gql } from '@apollo/client'; import TreeSelection from '#components/TreeSelection'; -import { ExportFormatEnum } from '#generated/types'; +import { + ExportFormatEnum, + ExportEnumsQuery, + ExportDateFormatEnum, + ExportReportCitationStyleEnum, +} from '#generated/types'; import _ts from '#ts'; - import EntryPreview from '../EntryPreview'; import { TreeSelectableWidget, @@ -23,6 +29,37 @@ import { import { ExcelColumnNode } from '..'; import styles from './styles.css'; +const EXPORT_ENUMS = gql` + query ExportEnums { + enums { + ExportExtraOptionsSerializerDateFormat { + description + enum + label + } + ExportExtraOptionsSerializerReportCitationStyle { + description + enum + label + } + } + } +`; +interface DateFormatOption { + enum: ExportDateFormatEnum; + label: string; +} +const dateFormatKeySelector = (item: DateFormatOption) => item.enum; +const dateFormatLabelSelector = (item: DateFormatOption) => item.label; + +interface CitationFormatOption { + enum: ExportReportCitationStyleEnum; + label: string; + description?: string | null | undefined; +} +const citationFormatKeySelector = (item: CitationFormatOption) => item.enum; +const citationFormatLabelSelector = (item: CitationFormatOption) => item.description ?? item.label; + function columnsLabelSelector(node: ExcelColumnNode) { const isEntryMetadata = node.key.includes('ENTRY'); @@ -92,6 +129,10 @@ interface Props { onTextWidgetsChange: (value: TreeSelectableWidget[]) => void; widgetColumns: ExcelColumnNode[]; onWidgetColumnChange: (value: ExcelColumnNode[]) => void; + dateFormat: ExportDateFormatEnum | undefined; + citationFormat: ExportReportCitationStyleEnum | undefined; + onDateFormatChange: (newVal: ExportDateFormatEnum | undefined) => void; + onCitationFormatChange: (newVal: ExportReportCitationStyleEnum | undefined) => void; } function AdvancedOptionsSelection(props: Props) { const { @@ -118,8 +159,18 @@ function AdvancedOptionsSelection(props: Props) { onExcelDecoupledChange, widgetColumns, onWidgetColumnChange, + dateFormat, + onDateFormatChange, + citationFormat, + onCitationFormatChange, } = props; + const { + data: exportEnums, + } = useQuery( + EXPORT_ENUMS, + ); + const handleSwapOrderValueChange = useCallback((newValue: boolean) => { if (newValue) { onReportStructureVariantChange(DIMENSION_FIRST); @@ -203,41 +254,72 @@ function AdvancedOptionsSelection(props: Props) { )} - - - - - )} - headingDescription={( -

- Options shown are based on dimensions - available after filtering from the main export page -

- )} - > - -
+
+ + + + + + + + + )} + headingDescription={( +

+ Options shown are based on dimensions + available after filtering from the main export page +

+ )} + > + +
+
@@ -262,6 +346,23 @@ function AdvancedOptionsSelection(props: Props) {

{_ts('export', 'decoupledEntriesTitle2')}

{_ts('export', 'decoupledEntriesTitle')}

+ + + {widgetColumns.length > 0 && ( { if (widget.widgetId === 'SCALE') { const firstItem = widget.properties?.options?.[0]; @@ -115,10 +151,12 @@ function WidgetSample(props: WidgetSampleProps) { return firstItem.label; } if (widget.widgetId === 'DATE_RANGE') { - return sampleDateRange; + const oneYearLaterDate = new Date(sampleDateOne); + oneYearLaterDate.setFullYear(oneYearLaterDate.getFullYear() + 1); + return `${sampleDateOne} - ${sampleDateTwo}`; } if (widget.widgetId === 'DATE') { - return sampleDate; + return sampleDateOne; } if (widget.widgetId === 'TIME') { return sampleTime; @@ -130,7 +168,7 @@ function WidgetSample(props: WidgetSampleProps) { return sampleGeo; } return undefined; - }, [widget]); + }, [widget, sampleDateOne, sampleDateTwo]); if (!content) { return null; @@ -150,6 +188,8 @@ interface Props { showLeadEntryId: boolean; showAssessmentData: boolean; showEntryWidgetData: boolean; + dateFormat: ExportDateFormatEnum | undefined; + citationFormat: ExportReportCitationStyleEnum | undefined; } function EntryPreview(props: Props) { @@ -160,11 +200,35 @@ function EntryPreview(props: Props) { showAssessmentData, showEntryWidgetData, textWidgets, + dateFormat, + citationFormat, } = props; - const selectedExcerpt = useMemo(() => ( - sampleExcerpts[Math.floor(Math.random() * sampleExcerpts.length)] - ), []); + const { + data: exportEnums, + } = useQuery( + EXPORT_ENUMS, + ); + + const selectedFormat = useMemo(() => { + const options = exportEnums?.enums?.ExportExtraOptionsSerializerDateFormat ?? []; + return options.find((item) => item.enum === dateFormat); + }, [exportEnums, dateFormat]); + + const dateInSelectedFormat = useMemo(() => ( + formatDateToString(new Date(), selectedFormat?.label ?? 'dd-MM-yyyy') + ), [ + selectedFormat, + ]); + + const selectedExcerpt = useMemo(() => { + const excerpt = sampleExcerpts[Math.floor(Math.random() * sampleExcerpts.length)]; + if (citationFormat === 'STYLE_1') { + return excerpt.replace(/\.$/, ''); + } + return excerpt; + }, [citationFormat]); + const filteredContextualWidgets = useMemo(() => ( contextualWidgets?.filter((widget) => widget.selected) ), [contextualWidgets]); @@ -175,7 +239,10 @@ function EntryPreview(props: Props) { const widgetSampleRendererParams = useCallback((_: string, widget: Widget) => ({ widget, - }), []); + selectedFormat, + }), [ + selectedFormat, + ]); const textWidgetRendererParams = useCallback((_: string, widget: Widget) => ({ title: widget.title, @@ -201,7 +268,7 @@ function EntryPreview(props: Props) { [ - , 612 Key Informant Interview, Data collection: 08/04/2022 - 20/04/2022] + {`, 612 Key Informant Interview, Data collection: ${dateInSelectedFormat}`} )} {showEntryWidgetData && filteredContextualWidgets?.length > 0 && ( @@ -231,13 +298,24 @@ function EntryPreview(props: Props) { errored={false} /> )} - - ( - - HarperCollins + {citationFormat === 'DEFAULT' && ( + + ( + + HarperCollins + + {`, Moby-Dick, ${dateInSelectedFormat})`} - , Moby-Dick, 17/04/2022) - + )} + {citationFormat === 'STYLE_1' && ( + + ( + + Moby-Dick + + {` ${dateInSelectedFormat}).`} + + )} ); diff --git a/app/views/NewExport/index.tsx b/app/views/NewExport/index.tsx index ddfc131fe9..21d3f52e97 100644 --- a/app/views/NewExport/index.tsx +++ b/app/views/NewExport/index.tsx @@ -48,6 +48,9 @@ import { ProjectSourceStatsForExportQueryVariables, LeadsFilterDataInputType, ExportExcelSelectedStaticColumnEnum, + ExportDateFormatEnum, + ExportReportCitationStyleEnum, + ExportCreateInputType, } from '#generated/types'; import { transformToFormError, ObjectError } from '#base/utils/errorTransform'; import { @@ -322,6 +325,12 @@ function NewExport(props: Props) { excelDecoupled, setExcelDecoupled, ] = useState(locationState?.extraOptions?.excelDecoupled ?? false); + const [dateFormat, setDateFormat] = useState( + locationState?.extraOptions?.dateFormat ?? 'DEFAULT', + ); + const [citationFormat, setCitationFormat] = useState( + locationState?.extraOptions?.reportCitationStyle ?? 'DEFAULT', + ); const [ textWidgets, @@ -620,9 +629,11 @@ function NewExport(props: Props) { setPristine(true); }, [clearSourcesFilterValue, setPristine]); - const getCreateExportData = useCallback((isPreview: boolean) => ({ + const getCreateExportData = useCallback((isPreview: boolean): ExportCreateInputType => ({ extraOptions: { excelDecoupled, + dateFormat, + reportCitationStyle: citationFormat, excelColumns: columns.filter((widget) => widget.selected).map((col) => ( col.isWidget ? { isWidget: col.isWidget, @@ -656,6 +667,8 @@ function NewExport(props: Props) { type: 'ENTRIES' as const, title: queryTitle, }), [ + dateFormat, + citationFormat, columns, exportFileFormat, contextualWidgets, @@ -866,6 +879,10 @@ function NewExport(props: Props) { showMatrix2dOptions={showMatrix2dOptions} contextualWidgets={contextualWidgets} textWidgets={textWidgets} + dateFormat={dateFormat} + citationFormat={citationFormat} + onDateFormatChange={setDateFormat} + onCitationFormatChange={setCitationFormat} onReportStructureChange={setReportStructure} onReportShowLeadEntryIdChange={setReportShowLeadEntryId} onReportShowAssessmentDataChange={setReportShowAssessmentData} diff --git a/app/views/ProjectEdit/Framework/index.tsx b/app/views/ProjectEdit/Framework/index.tsx index 46be87e2e2..6b06d35f49 100644 --- a/app/views/ProjectEdit/Framework/index.tsx +++ b/app/views/ProjectEdit/Framework/index.tsx @@ -8,6 +8,7 @@ import { import { Button, Container, + Checkbox, Kraken, ListView, Message, @@ -29,6 +30,7 @@ import { import SmartButtonLikeLink from '#base/components/SmartButtonLikeLink'; import useDebouncedValue from '#hooks/useDebouncedValue'; import ProjectContext from '#base/context/ProjectContext'; +import FrameworkTagSelectInput from '#components/selections/FrameworkTagSelectInput'; import { isFiltered } from '#utils/common'; import routes from '#base/configs/routes'; import _ts from '#ts'; @@ -49,7 +51,9 @@ const frameworkKeySelector = (d: FrameworkType) => d.id; export const PROJECT_FRAMEWORKS = gql` query ProjectAnalysisFrameworks( $isCurrentUserMember: Boolean, + $tags: [ID!], $search: String, + $recentlyUsed: Boolean, $page: Int, $pageSize: Int, $createdBy: [ID!], @@ -57,8 +61,10 @@ export const PROJECT_FRAMEWORKS = gql` analysisFrameworks( search: $search, isCurrentUserMember: $isCurrentUserMember + recentlyUsed: $recentlyUsed, page: $page, pageSize: $pageSize, + tags: $tags, createdBy: $createdBy, ) { results { @@ -133,7 +139,9 @@ const relatedToMeLabelSelector = (d: Option) => d.label; type FormType = { relatedToMe?: 'true' | 'false'; + recentlyUsed: boolean; search: string; + tag?: string; }; type FormSchema = ObjectSchema>; @@ -142,12 +150,16 @@ type FormSchemaFields = ReturnType const schema: FormSchema = { fields: (): FormSchemaFields => ({ search: [], + tag: [], + recentlyUsed: [], relatedToMe: [requiredCondition], }), }; const defaultFormValue: PartialForm = { relatedToMe: 'true', + recentlyUsed: false, + tag: undefined, search: '', }; @@ -183,12 +195,16 @@ function ProjectFramework(props: Props) { const analysisFrameworkVariables = useMemo(() => ( { isCurrentUserMember: delayedValue.relatedToMe === 'true' ? true : undefined, + tags: delayedValue.tag ? [delayedValue.tag] : undefined, + recentlyUsed: delayedValue.recentlyUsed, search: delayedValue.search, page: 1, pageSize: PAGE_SIZE, } ), [ delayedValue.relatedToMe, + delayedValue.recentlyUsed, + delayedValue.tag, delayedValue.search, ]); @@ -287,6 +303,17 @@ function ProjectFramework(props: Props) { value={value.search} placeholder={_ts('projectEdit', 'searchLabel')} /> + +