huridocs/uwazi

View on GitHub
app/react/V2/Routes/Settings/Thesauri/ThesaurusForm.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
/* eslint-disable max-statements */
/* eslint-disable max-lines */
import React, { useEffect, useMemo, useState } from 'react';
import { Link, LoaderFunction, useBlocker, useLoaderData, useNavigate } from 'react-router-dom';
import { IncomingHttpHeaders } from 'http';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useSetAtom } from 'jotai';
import { Row } from '@tanstack/react-table';
import { Translate } from 'app/I18N';
import { SettingsContent } from 'V2/Components/Layouts/SettingsContent';
import { Button, Table } from 'V2/Components/UI';
import { ConfirmNavigationModal, InputField } from 'V2/Components/Forms';
import { ClientThesaurusValue, ClientThesaurus } from 'app/apiResponseTypes';
import { notificationAtom } from 'V2/atoms/notificationAtom';
import ThesauriAPI from 'V2/api/thesauri';
import uniqueID from 'shared/uniqueID';
import { ThesaurusSchema } from 'shared/types/thesaurusType';
import {
  ThesauriValueFormSidepanel,
  FormThesauriValue,
} from './components/ThesauriValueFormSidepanel';
import { ThesauriGroupFormSidepanel } from './components/ThesauriGroupFormSidepanel';
import { sanitizeThesaurusValues } from './helpers';
import { ImportButton } from './components/ImportButton';
import { columns, TableThesaurusValue } from './components/TableComponents';

const editTheasaurusLoader =
  (headers?: IncomingHttpHeaders): LoaderFunction =>
  async ({ params: { _id } }) => {
    const response = await ThesauriAPI.getThesauri({ _id }, headers);
    return response[0];
  };

const ThesaurusForm = () => {
  const navigate = useNavigate();
  const thesaurus = useLoaderData() as ClientThesaurus;
  const [showNavigationModal, setShowNavigationModal] = useState(false);
  const [groupFormValues, setGroupFormValues] = useState<FormThesauriValue>();
  const [itemFormValues, setItemFormValues] = useState<FormThesauriValue[]>([]);
  const [showThesauriValueFormSidepanel, setShowThesauriValueFormSidepanel] = useState(false);
  const [showThesauriGroupFormSidepanel, setShowThesauriGroupFormSidepanel] = useState(false);
  const [selectedThesaurusValue, setSelectedThesaurusValue] = useState<Row<TableThesaurusValue>[]>(
    []
  );
  const [thesaurusValues, setThesaurusValues] = useState<TableThesaurusValue[]>([]);
  const setNotifications = useSetAtom(notificationAtom);

  useEffect(() => {
    if (thesaurus) {
      const values = thesaurus.values || [];
      const valuesWithIds = values.map((value: ClientThesaurusValue) => ({
        ...value,
        _id: `temp_${uniqueID()}`,
        values: value.values?.map(val => ({
          ...val,
          _id: `temp_${uniqueID()}`,
        })),
      }));
      setThesaurusValues(valuesWithIds);
    }
  }, [thesaurus]);

  const {
    watch,
    register,
    getValues,
    handleSubmit,
    formState: { errors, isDirty },
  } = useForm<ClientThesaurus>({
    defaultValues: thesaurus,
    mode: 'onSubmit',
  });

  const blocker = useBlocker(({ nextLocation }) => {
    const nameHasChanged = isDirty;
    const hasSaved = nextLocation.pathname.includes('edit');
    if (hasSaved) return false;
    if (thesaurus) {
      // We are editing
      const valuesHaveChanged = thesaurusValues.length !== thesaurus.values.length;
      return valuesHaveChanged || thesaurus.name !== getValues().name;
    }
    const newValues = Boolean(thesaurusValues.length);
    return newValues || nameHasChanged;
  });

  useMemo(() => {
    if (blocker.state === 'blocked') {
      setShowNavigationModal(true);
    }
  }, [blocker, setShowNavigationModal]);

  const addItem = () => {
    setItemFormValues([]);
    setShowThesauriValueFormSidepanel(true);
  };

  const addGroup = () => {
    setGroupFormValues({
      _id: `temp_${uniqueID()}`,
      label: '',
      values: [{ label: '', _id: `temp_${uniqueID()}` }],
    });
    setShowThesauriGroupFormSidepanel(true);
  };

  const editGroup = (row: Row<TableThesaurusValue>) => {
    setGroupFormValues(row.original);
    setShowThesauriGroupFormSidepanel(true);
  };

  const editValue = (row: Row<TableThesaurusValue>) => {
    setItemFormValues([{ ...row.original, groupId: row.getParentRow()?.original._id }]);
    setShowThesauriValueFormSidepanel(true);
  };

  const edit = (row: Row<TableThesaurusValue>) => {
    if (row.original.values) {
      editGroup(row);
    } else {
      editValue(row);
    }
  };

  const deleteSelected = () => {
    const parentsDeleted = thesaurusValues.filter(
      currentValue =>
        !selectedThesaurusValue.find(selected => selected.original._id === currentValue._id)
    );

    const childrenDeleted = parentsDeleted.map(singleThesaurus => {
      if (singleThesaurus.values) {
        const newValues = singleThesaurus.values?.filter(
          currentGroupItem =>
            !selectedThesaurusValue.find(selected => selected.original._id === currentGroupItem._id)
        );
        singleThesaurus.values = newValues;
      }
      return singleThesaurus;
    });

    setThesaurusValues(childrenDeleted);
  };

  const sortValues = () => {
    const valuesCopy = [...thesaurusValues];
    valuesCopy.sort((first, second) => (first.label > second.label ? 1 : -1));
    setThesaurusValues(valuesCopy);
  };

  const addItemSubmit = (items: FormThesauriValue[]) => {
    const addingNewItems = !items[0]._id;
    const thesaurusValuesCopy = [...thesaurusValues];
    if (addingNewItems) {
      items.forEach(item => {
        const itemWithId = { ...item, _id: `temp_${uniqueID()}` };
        if (!itemWithId.groupId) {
          thesaurusValuesCopy.push(itemWithId);
          return;
        }

        const group = thesaurusValuesCopy.find(groupItem => groupItem._id === itemWithId.groupId);
        if (group) {
          group.values = group.values || [];
          group.values.push(itemWithId);
        }
      });

      setThesaurusValues(thesaurusValuesCopy);
      return;
    }

    items.forEach(item => {
      if (!item.groupId) {
        const index = thesaurusValuesCopy.findIndex(tv => tv._id === item._id);
        thesaurusValuesCopy[index] = item;
        return;
      }

      const group = thesaurusValuesCopy.find(groupItem => groupItem._id === item.groupId);
      if (group) {
        const index = group.values?.findIndex(tv => tv._id === item._id);
        if (index !== undefined && index !== -1) {
          group.values = group.values || [];
          group.values[index] = item;
        }
      }
    });
    setThesaurusValues(thesaurusValuesCopy);
  };

  const addGroupSubmit = (group: FormThesauriValue) => {
    const thesaurusValuesCopy = thesaurusValues.filter(item => item._id !== group._id);
    setThesaurusValues([...thesaurusValuesCopy, group]);
  };

  const formSubmit: SubmitHandler<ClientThesaurus> = async data => {
    const sanitizedThesaurus = sanitizeThesaurusValues(data, thesaurusValues);
    try {
      const savedThesaurus = await ThesauriAPI.save(sanitizedThesaurus);
      setNotifications({
        type: 'success',
        text: thesaurus ? (
          <Translate>Thesauri updated.</Translate>
        ) : (
          <Translate>Thesauri added.</Translate>
        ),
      });
      navigate(`/settings/thesauri/edit/${savedThesaurus._id}`);
    } catch (e) {
      setNotifications({
        type: 'error',
        text: <Translate>Error updating thesauri.</Translate>,
      });
    }
  };

  return (
    <div
      className="tw-content"
      style={{ width: '100%', overflowY: 'auto' }}
      data-testid="settings-thesauri"
    >
      <SettingsContent className="flex flex-col h-full">
        <SettingsContent.Header
          path={new Map([['Thesauri', '/settings/thesauri']])}
          title={watch('name')}
        />
        <SettingsContent.Body>
          <form onSubmit={handleSubmit(formSubmit)} id="edit-thesaurus">
            <div data-testid="thesauri" className="rounded-md border border-gray-50 shadow-sm">
              <div className="p-4">
                <InputField
                  clearFieldAction={() => {}}
                  id="thesauri-name"
                  placeholder="Thesauri name"
                  className="mb-2"
                  hasErrors={!!errors.name}
                  {...register('name', { required: true })}
                />
              </div>
              <Table<TableThesaurusValue>
                draggableRows
                enableSelection
                subRowsKey="values"
                onChange={setThesaurusValues}
                columns={columns({ edit })}
                data={thesaurusValues}
                initialState={{ sorting: [{ id: 'label', desc: false }] }}
                onSelection={setSelectedThesaurusValue}
                allowEditGroupsWithDnD={false}
              />
            </div>
          </form>
        </SettingsContent.Body>
        <SettingsContent.Footer className="bottom-0 bg-indigo-50">
          {selectedThesaurusValue.length ? (
            <div className="flex gap-2 items-center">
              <Button
                type="button"
                onClick={deleteSelected}
                color="error"
                data-testid="thesauri-remove-button"
              >
                <Translate>Remove</Translate>
              </Button>
              <Translate>Selected</Translate> {selectedThesaurusValue.length}{' '}
              <Translate>of</Translate> {thesaurusValues.length}
            </div>
          ) : (
            <div className="flex justify-between w-full">
              <div className="flex gap-2">
                <Button onClick={addItem}>
                  <Translate>Add item</Translate>
                </Button>
                <Button styling="outline" onClick={addGroup}>
                  <Translate>Add group</Translate>
                </Button>
                <Button styling="outline" onClick={sortValues}>
                  <Translate>Sort</Translate>
                </Button>
                <ImportButton
                  thesaurus={{ ...getValues(), values: thesaurusValues }}
                  onSuccess={(savedThesaurus: ThesaurusSchema) => {
                    setNotifications({
                      type: 'success',
                      text: <Translate>Thesauri updated.</Translate>,
                    });
                    navigate(`/settings/thesauri/edit/${savedThesaurus._id}`);
                  }}
                  onFailure={() => {
                    setNotifications({
                      type: 'error',
                      text: <Translate>Error adding thesauri.</Translate>,
                    });
                  }}
                />
              </div>
              <div className="flex gap-2">
                <Link to="/settings/thesauri">
                  <Button styling="light" type="button">
                    <Translate>Cancel</Translate>
                  </Button>
                </Link>
                <Button styling="solid" color="success" type="submit" form="edit-thesaurus">
                  <Translate>Save</Translate>
                </Button>
              </div>
            </div>
          )}
        </SettingsContent.Footer>
      </SettingsContent>
      <ThesauriValueFormSidepanel
        showSidepanel={showThesauriValueFormSidepanel}
        submit={addItemSubmit}
        value={itemFormValues}
        closePanel={() => {
          setShowThesauriValueFormSidepanel(false);
        }}
        groups={thesaurusValues.filter((value: ClientThesaurusValue) =>
          Array.isArray(value.values)
        )}
      />
      <ThesauriGroupFormSidepanel
        showSidepanel={showThesauriGroupFormSidepanel}
        submit={addGroupSubmit}
        value={groupFormValues}
        closePanel={() => {
          setShowThesauriGroupFormSidepanel(false);
        }}
      />
      {showNavigationModal && (
        <ConfirmNavigationModal setShowModal={setShowNavigationModal} onConfirm={blocker.proceed} />
      )}
    </div>
  );
};

export { ThesaurusForm, editTheasaurusLoader };