huridocs/uwazi

View on GitHub
app/react/V2/Routes/Settings/ParagraphExtraction/components/ExtractorModal.tsx

Summary

Maintainability
A
1 hr
Test Coverage
/* 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 { Template } from 'app/apiResponseTypes';
import { ParagraphExtractorApiPayload } from '../types';
import { NoQualifiedTemplatesMessage } from './NoQualifiedTemplate';
import { Link } from 'react-router-dom';

interface ExtractorModalProps {
  setShowModal: React.Dispatch<React.SetStateAction<boolean>>;
  onClose: () => void;
  onAccept: () => void;
  templates: Template[];
  extractor?: ParagraphExtractorApiPayload;
}

const formatOptions = (templates: Template[]) =>
  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: Template) =>
  template.properties?.some(({ name }) => name === 'rich_text');

const isActiveStepClassName = (isActive: boolean) => (isActive ? 'bg-indigo-700' : 'bg-gray-200');

const linkPXTemplateCriteria =
  'https://uwazi.readthedocs.io/en/latest/admin-docs/paragraph-extraction.html';

const ExtractorModal = ({
  setShowModal,
  onClose,
  onAccept,
  templates,
  extractor,
}: ExtractorModalProps) => {
  const [step, setStep] = useState(1);
  const [templatesFrom, setTemplatesFrom] = useState<string[]>(extractor?.templatesFrom || []);
  const [templateTo, setTemplateTo] = useState<string>(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 (
    <Modal size="xxl">
      <Modal.Header>
        <h1 className="text-xl font-medium text-gray-900">
          {extractor ? (
            <Translate>Edit Extractor</Translate>
          ) : (
            (step === 1 && <Translate>Target template</Translate>) ||
            (step === 2 && <Translate>Paragraph extractor</Translate>)
          )}
        </h1>
        <Modal.CloseButton onClick={() => setShowModal(false)} />
      </Modal.Header>

      <Modal.Body className="pt-0">
        <div className={`${step !== 1 && 'hidden'}`}>
          <MultiselectList
            value={[templateTo]}
            items={templateToOptions}
            onChange={selected => {
              setTemplateTo(selected[0]);
            }}
            singleSelect
            startOnSelected={!!templateTo}
            className="min-h-[327px]"
            blankState={<NoQualifiedTemplatesMessage />}
          />
        </div>
        <div className={`${step !== 2 && 'hidden'}`}>
          <div>
            <MultiselectList
              value={templatesFrom || []}
              items={templateFromOptions}
              onChange={setTemplatesFrom}
              allowSelelectAll={templateFromOptions.length > 0}
              startOnSelected={templatesFrom.length > 0}
              className="min-h-[327px]"
            />
          </div>
        </div>

        <div className="flex flex-col">
          <div className="flex justify-center w-full gap-2">
            <div className={`w-2 h-2 rounded-full ${isActiveStepClassName(step === 1)}`} />
            <div className={`w-2 h-2 rounded-full ${isActiveStepClassName(step === 2)}`} />
          </div>
          {templateToOptions.length !== 0 && step === 1 && (
            <span className="mt-5 text-gray-500 font-light text-sm">
              <Translate>Templates meeting required criteria</Translate>.{' '}
              <Link to={linkPXTemplateCriteria} target="_blank">
                <Translate className="underline">Read Documentation</Translate>
              </Link>
            </span>
          )}
        </div>
      </Modal.Body>

      <Modal.Footer>
        <div className="flex flex-col w-full">
          <div className="flex gap-2">
            {step === 1 ? (
              <>
                <Button styling="light" onClick={() => setShowModal(false)} className="grow">
                  <Translate>Cancel</Translate>
                </Button>
                <Button className="grow" onClick={() => setStep(2)} disabled={!templateTo}>
                  <span className="flex items-center justify-center gap-2 flex-nowrap">
                    <Translate>Next</Translate>
                    <ArrowRightIcon className="w-5" />
                  </span>
                </Button>
              </>
            ) : (
              <>
                <Button styling="light" onClick={() => setStep(1)} className="grow">
                  <Translate>Back</Translate>
                </Button>
                <Button className="grow" onClick={async () => handleSubmit()} color="success">
                  {extractor ? <Translate>Update</Translate> : <Translate>Add</Translate>}
                </Button>
              </>
            )}
          </div>
        </div>
      </Modal.Footer>
    </Modal>
  );
};

export { ExtractorModal, formatOptions };