diff --git a/app/react/App/styles/globals.css b/app/react/App/styles/globals.css index 503dc0a90c..960970b326 100644 --- a/app/react/App/styles/globals.css +++ b/app/react/App/styles/globals.css @@ -1824,6 +1824,10 @@ input[type="range"]::-ms-fill-lower { margin-top: 1rem; } +.mt-5 { + margin-top: 1.25rem; +} + .mt-6 { margin-top: 1.5rem; } @@ -1981,6 +1985,14 @@ input[type="range"]::-ms-fill-lower { max-height: 100svh; } +.min-h-\[300px\] { + min-height: 300px; +} + +.min-h-\[327px\] { + min-height: 327px; +} + .min-h-fit { min-height: -moz-fit-content; min-height: fit-content; diff --git a/app/react/V2/Components/Forms/MultiselectList.tsx b/app/react/V2/Components/Forms/MultiselectList.tsx index 2fa881d010..d444924cc7 100644 --- a/app/react/V2/Components/Forms/MultiselectList.tsx +++ b/app/react/V2/Components/Forms/MultiselectList.tsx @@ -5,6 +5,7 @@ import React, { useEffect, useState, useRef } from 'react'; import { Translate } from 'app/I18N'; import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'; +import { isString } from 'lodash'; import { InputField, RadioSelect } from '.'; import { Pill } from '../UI/Pill'; import { Label } from './Label'; @@ -31,8 +32,12 @@ interface MultiselectListProps { startOnSelected?: boolean; search?: string; suggestions?: boolean; + blankState?: string | React.ReactNode; } +const renderChild = (child: string | React.ReactNode) => + isString(child) ? {child} : child; + const MultiselectList = ({ items, onChange, @@ -47,6 +52,7 @@ const MultiselectList = ({ startOnSelected = false, search = '', suggestions = false, + blankState = No items available, }: MultiselectListProps) => { const [selectedItems, setSelectedItems] = useState(value || []); const [showAll, setShowAll] = useState(!(startOnSelected && selectedItems.length)); @@ -345,6 +351,11 @@ const MultiselectList = ({ + {items.length === 0 && ( +
+ {renderChild(blankState)} +
+ )} diff --git a/app/react/V2/Components/Forms/specs/MultiselectList.cy.tsx b/app/react/V2/Components/Forms/specs/MultiselectList.cy.tsx index 30473fa02f..05071b64d7 100644 --- a/app/react/V2/Components/Forms/specs/MultiselectList.cy.tsx +++ b/app/react/V2/Components/Forms/specs/MultiselectList.cy.tsx @@ -223,4 +223,47 @@ describe('MultiselectList.cy.tsx', () => { cy.contains('Pepperoni').should('be.visible'); }); }); + + // add blank state test + describe('blank state property', () => { + it('should show blank state property if there is no items passed to the component', () => { + cy.viewport(450, 650); + mount( + +
+ {}} items={[]} /> +
+
+ ); + cy.contains('No items available').should('be.visible'); + }); + + it('should accept a blank state string', () => { + cy.viewport(450, 650); + mount( + +
+ {}} items={[]} blankState="nada" /> +
+
+ ); + cy.contains('nada').should('be.visible'); + }); + + it('should accept a blank state component', () => { + cy.viewport(450, 650); + mount( + +
+ {}} + items={[]} + blankState={
no items string
} + /> +
+
+ ); + cy.contains('no items string').should('be.visible'); + }); + }); }); diff --git a/app/react/V2/Routes/Settings/ParagraphExtraction/ParagraphExtraction.tsx b/app/react/V2/Routes/Settings/ParagraphExtraction/ParagraphExtraction.tsx index b7a2b434ed..053d6302a0 100644 --- a/app/react/V2/Routes/Settings/ParagraphExtraction/ParagraphExtraction.tsx +++ b/app/react/V2/Routes/Settings/ParagraphExtraction/ParagraphExtraction.tsx @@ -11,8 +11,9 @@ import { Translate } from 'app/I18N'; import { useSetAtom } from 'jotai'; import { notificationAtom } from 'V2/atoms'; import { extractorsTableColumns } from './components/TableElements'; -import { TableExtractor, Extractor } from './types'; +import { TableParagraphExtractor, ParagraphExtractorApiResponse } from './types'; import { List } from './components/List'; +import { ExtractorModal } from './components/ExtractorModal'; const getTemplateName = (templates: ClientTemplateSchema[], targetId: string) => { const foundTemplate = templates.find(template => template._id === targetId); @@ -20,27 +21,34 @@ const getTemplateName = (templates: ClientTemplateSchema[], targetId: string) => }; const formatExtractors = ( - extractors: Extractor[], + extractors: ParagraphExtractorApiResponse[], templates: ClientTemplateSchema[] -): TableExtractor[] => - (extractors || []).map(extractor => { +): TableParagraphExtractor[] => + extractors.map(extractor => { const targetTemplateName = getTemplateName(templates, extractor.templateTo); - const originTemplateNames = extractor.templateFrom.map(templateFrom => + const originTemplateNames = (extractor.templatesFrom || []).map(templateFrom => getTemplateName(templates, templateFrom) ); - return { ...extractor, rowId: extractor._id, originTemplateNames, targetTemplateName }; + return { + ...extractor, + rowId: extractor._id || '', + originTemplateNames, + targetTemplateName, + }; }); const ParagraphExtractorDashboard = () => { const { extractors = [], templates } = useLoaderData() as { - extractors: TableExtractor[]; + extractors: ParagraphExtractorApiResponse[]; templates: ClientTemplateSchema[]; }; - const [isSaving, setIsSaving] = useState(false); + const revalidator = useRevalidator(); - const [selected, setSelected] = useState([]); + const [isSaving, setIsSaving] = useState(false); + const [selected, setSelected] = useState([]); const [confirmModal, setConfirmModal] = useState(false); + const [extractorModal, setExtractorModal] = useState(false); const setNotifications = useSetAtom(notificationAtom); const deleteExtractors = async () => { @@ -64,7 +72,15 @@ const ParagraphExtractorDashboard = () => { setIsSaving(false); } }; - // const handleSave = async (extractor: IXExtractorInfo) => {}; + + const handleSave = async () => { + revalidator.revalidate(); + setNotifications({ + type: 'success', + text: Paragraph Extractor added, + }); + }; + const paragraphExtractorData = useMemo( () => formatExtractors(extractors, templates), [extractors, templates] @@ -73,14 +89,13 @@ const ParagraphExtractorDashboard = () => { return (
- {/* should create a component for empty data? */} { {selected?.length === 1 ? ( - ) : undefined} @@ -115,7 +130,7 @@ const ParagraphExtractorDashboard = () => { Delete ) : ( - )} @@ -136,6 +151,16 @@ const ParagraphExtractorDashboard = () => { dangerStyle /> )} + + {extractorModal && ( + setExtractorModal(false)} + onAccept={handleSave} + templates={templates} + extractor={selected?.length ? selected[0] : undefined} + /> + )} ); }; diff --git a/app/react/V2/Routes/Settings/ParagraphExtraction/components/ExtractorModal.tsx b/app/react/V2/Routes/Settings/ParagraphExtraction/components/ExtractorModal.tsx new file mode 100644 index 0000000000..d52d8bd24d --- /dev/null +++ b/app/react/V2/Routes/Settings/ParagraphExtraction/components/ExtractorModal.tsx @@ -0,0 +1,165 @@ +/* eslint-disable max-lines */ +/* eslint-disable max-statements */ +import React, { useEffect, useState } from 'react'; +import * as extractorsAPI from 'app/V2/api/paragraphExtractor/extractors'; +import { ArrowRightIcon } from '@heroicons/react/20/solid'; +import { Modal, Button, MultiselectList } from 'V2/Components/UI'; +import { Translate } from 'app/I18N'; +import { ClientTemplateSchema } from 'app/istore'; +import { ParagraphExtractorApiPayload } from '../types'; +import { NoQualifiedTemplatesMessage } from './NoQualifiedTemplate'; + +interface ExtractorModalProps { + setShowModal: React.Dispatch>; + onClose: () => void; + onAccept: () => void; + templates: ClientTemplateSchema[]; + extractor?: ParagraphExtractorApiPayload; +} + +const formatOptions = (templates: ClientTemplateSchema[]) => + templates.map(template => { + const option = { + label: template.name, + id: template._id, + searchLabel: template.name, + value: template._id, + properties: template.properties, + }; + return option; + }); + +const templatesWithParagraph = (template: ClientTemplateSchema) => + template.properties.some(({ name }) => name === 'rich_text'); + +const isActiveStepClassName = (isActive: boolean) => (isActive ? 'bg-indigo-700' : 'bg-gray-200'); + +const ExtractorModal = ({ + setShowModal, + onClose, + onAccept, + templates, + extractor, +}: ExtractorModalProps) => { + const [step, setStep] = useState(1); + const [templatesFrom, setTemplatesFrom] = useState(extractor?.templatesFrom || []); + const [templateTo, setTemplateTo] = useState(extractor?.templateTo ?? ''); + + const [templateToOptions] = useState(formatOptions(templates.filter(templatesWithParagraph))); + const [templateFromOptions, setTemplateFromOptions] = useState( + formatOptions(templates.filter(template => template._id !== templateTo)) + ); + + useEffect(() => { + setTemplateFromOptions( + formatOptions(templates.filter(template => template._id !== templateTo)) + ); + }, [templateTo, templates]); + + const handleClose = () => { + onClose(); + }; + + const handleSubmit = async () => { + try { + const values = { + ...extractor, + templatesFrom, + templateTo, + }; + await extractorsAPI.save(values); + handleClose(); + onAccept(); + } catch (e) { + console.error('Error saving extractor:', e); + } + }; + + return ( + + +

+ {extractor ? ( + Edit Extractor + ) : ( + (step === 1 && Target template) || + (step === 2 && Paragraph extractor) + )} +

+ setShowModal(false)} /> +
+ + +
+ { + setTemplateTo(selected[0]); + }} + singleSelect + startOnSelected={!!templateTo} + className="min-h-[327px]" + blankState={} + /> +
+
+
+ 0} + startOnSelected={templatesFrom.length > 0} + className="min-h-[327px]" + /> +
+
+ +
+
+ {/* duplicate structure, can be a function */} +
+
+
+ {templateToOptions.length !== 0 && step === 1 && ( + + Templates meeting required criteria + + )} +
+ + + +
+
+ {step === 1 ? ( + <> + + + + ) : ( + <> + + + + )} +
+
+
+ + ); +}; + +export { ExtractorModal, formatOptions }; diff --git a/app/react/V2/Routes/Settings/ParagraphExtraction/components/List.tsx b/app/react/V2/Routes/Settings/ParagraphExtraction/components/List.tsx index 550e16ed69..f81526ba0b 100644 --- a/app/react/V2/Routes/Settings/ParagraphExtraction/components/List.tsx +++ b/app/react/V2/Routes/Settings/ParagraphExtraction/components/List.tsx @@ -1,10 +1,9 @@ import React from 'react'; import { Translate } from 'app/I18N'; -import { TableExtractor } from '../types'; +import { TableParagraphExtractor } from '../types'; -const List = ({ items }: { items: TableExtractor[] }) => ( +const List = ({ items }: { items: TableParagraphExtractor[] }) => (
    - {/* what should be displayed on the confirm modal? */} {items.map(item => (
  • Templates: diff --git a/app/react/V2/Routes/Settings/ParagraphExtraction/components/NoQualifiedTemplate.tsx b/app/react/V2/Routes/Settings/ParagraphExtraction/components/NoQualifiedTemplate.tsx new file mode 100644 index 0000000000..35f1d927c8 --- /dev/null +++ b/app/react/V2/Routes/Settings/ParagraphExtraction/components/NoQualifiedTemplate.tsx @@ -0,0 +1,15 @@ +import { Translate } from 'app/I18N'; +import React from 'react'; + +const NoQualifiedTemplatesMessage = () => ( +
    +

    + No valid target template available +

    +

    + Qualified templates should have Rich Text property +

    +
    +); + +export { NoQualifiedTemplatesMessage }; diff --git a/app/react/V2/Routes/Settings/ParagraphExtraction/components/TableElements.tsx b/app/react/V2/Routes/Settings/ParagraphExtraction/components/TableElements.tsx index 0014e25255..1a1df4d762 100644 --- a/app/react/V2/Routes/Settings/ParagraphExtraction/components/TableElements.tsx +++ b/app/react/V2/Routes/Settings/ParagraphExtraction/components/TableElements.tsx @@ -5,9 +5,9 @@ import { CellContext, createColumnHelper } from '@tanstack/react-table'; import { Link } from 'react-router-dom'; import { Translate } from 'app/I18N'; import { Button, Pill } from 'V2/Components/UI'; -import { TableExtractor } from '../types'; +import { TableParagraphExtractor } from '../types'; -const extractorColumnHelper = createColumnHelper(); +const extractorColumnHelper = createColumnHelper(); const TemplateFromHeader = () => Template; const TemplateToHeader = () => Target Template; @@ -18,11 +18,13 @@ const ActionHeader = () => Action; const NumericCell = ({ cell, }: CellContext< - TableExtractor, - TableExtractor['documents'] | TableExtractor['generatedEntities'] + TableParagraphExtractor, + TableParagraphExtractor['documents'] | TableParagraphExtractor['generatedEntities'] >) => {cell.getValue()}; -const TemplatesCell = ({ cell }: CellContext) => ( +const TemplatesCell = ({ + cell, +}: CellContext) => (
    {cell.getValue()} @@ -32,7 +34,7 @@ const TemplatesCell = ({ cell }: CellContext) => ( +}: CellContext) => (
    {cell.getValue().map(value => (
    @@ -42,7 +44,9 @@ const TemplateFromCell = ({
    ); -const LinkButton = ({ cell }: CellContext) => ( +const LinkButton = ({ + cell, +}: CellContext) => (