huridocs/uwazi

View on GitHub
app/react/V2/Routes/Settings/Pages/PageEditor.tsx

Summary

Maintainability
C
1 day
Test Coverage
/* eslint-disable max-lines */
/* eslint-disable max-statements */
/* eslint-disable react/jsx-props-no-spreading */
import React, { useEffect, useMemo, useState } from 'react';
import { IncomingHttpHeaders } from 'http';
import {
  Link,
  LoaderFunction,
  useBlocker,
  useLoaderData,
  useNavigate,
  useRevalidator,
} from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { useSetAtom } from 'jotai';
import { debounce } from 'lodash';
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid';
import { Translate, t } from 'app/I18N';
import * as pagesAPI from 'V2/api/pages';
import { Page } from 'V2/shared/types';
import { SettingsContent } from 'V2/Components/Layouts/SettingsContent';
import { Button, CopyValueInput, Tabs } from 'V2/Components/UI';
import { CodeEditor } from 'V2/Components/CodeEditor';
import { ConfirmNavigationModal, EnableButtonCheckbox, InputField } from 'app/V2/Components/Forms';
import { notificationAtom } from 'V2/atoms';
import { FetchResponseError } from 'shared/JSONRequest';
import { getPageUrl } from './components/PageListTable';
import { HTMLNotification, JSNotification } from './components/PageEditorComponents';

const pageEditorLoader =
  (headers?: IncomingHttpHeaders): LoaderFunction =>
  async ({ params }) => {
    if (params.sharedId) {
      const page = await pagesAPI.getBySharedId(params.sharedId, headers);

      return page;
    }

    return {};
  };

const PageEditor = () => {
  const page = useLoaderData() as Page;
  const revalidator = useRevalidator();
  const navigate = useNavigate();
  const [showConfirmationModal, setShowConfirmationModal] = useState(false);
  const setNotifications = useSetAtom(notificationAtom);

  const debouncedChangeHandler = useMemo(() => (handler: () => void) => debounce(handler, 500), []);

  const {
    register,
    formState: { errors, dirtyFields, isSubmitting },
    watch,
    getValues,
    setValue,
    handleSubmit,
  } = useForm({
    defaultValues: { title: t('System', 'New page', null, false) },
    values: page,
  });

  const isDirty = !!Object.keys(dirtyFields).length;
  const blocker = useBlocker(isDirty && !isSubmitting);

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

  const notify = (response: Page | FetchResponseError) => {
    const hasErrors = response instanceof FetchResponseError;

    setNotifications({
      type: !hasErrors ? 'success' : 'error',
      text: !hasErrors ? (
        <Translate>Saved successfully.</Translate>
      ) : (
        <Translate>An error occurred</Translate>
      ),
      ...(hasErrors && { details: response.message }),
    });
  };

  const handleRevalidate = (response: Page) => {
    if (!page.sharedId) {
      navigate(`/${response.language}/settings/pages/page/${response.sharedId}`, {
        replace: true,
      });
    } else {
      revalidator.revalidate();
    }
  };

  const save = async (data: Page) => {
    const response = await pagesAPI.save(data);

    return response;
  };

  const handleSave = async (data: Page) => {
    const response = await save(data);
    const hasErrors = response instanceof FetchResponseError;

    notify(response);

    if (!hasErrors) {
      handleRevalidate(response);
    }
  };

  const handleSaveAndPreview = async (data: Page) => {
    const response = await save(data);
    const hasErrors = response instanceof FetchResponseError;

    notify(response);

    if (!hasErrors) {
      const pageUrl = getPageUrl(response.sharedId!, response.title);
      window.open(`${window.location.origin}/${pageUrl}`);
      handleRevalidate(response);
    }
  };

  return (
    <div className="tw-content" style={{ width: '100%', height: '100%', overflowY: 'auto' }}>
      <SettingsContent>
        <SettingsContent.Header
          path={new Map([['Pages', '/settings/pages']])}
          title={watch('title')}
        />

        <SettingsContent.Body>
          <Tabs unmountTabs={false}>
            <Tabs.Tab id="Basic" label={<Translate>Basic</Translate>}>
              <form>
                <input className="hidden" {...register('sharedId')} />
                <div className="flex flex-col max-w-2xl gap-4">
                  <div className="flex items-center gap-4">
                    <Translate className="font-bold">
                      Enable this page to be used as an entity view page:
                    </Translate>
                    <EnableButtonCheckbox
                      {...register('entityView')}
                      defaultChecked={page.entityView}
                    />
                  </div>

                  <InputField
                    id="title"
                    label={<Translate>Title</Translate>}
                    {...register('title', { required: true })}
                    hasErrors={errors.title !== undefined}
                    errorMessage={errors.title && <Translate>This field is required</Translate>}
                  />

                  <CopyValueInput
                    value={
                      !getValues('entityView') && getValues('sharedId')
                        ? `/${getPageUrl(getValues('sharedId')!, getValues('title'))}`
                        : ''
                    }
                    label={<Translate>URL</Translate>}
                    className="w-full mb-4"
                    id="page-url"
                  />

                  {getValues('sharedId') && !getValues('entityView') && (
                    <Link
                      target="_blank"
                      to={`/${getPageUrl(getValues('sharedId')!, getValues('title'))}`}
                    >
                      <div className="flex gap-2 hover:font-bold hover:cursor-pointer">
                        <ArrowTopRightOnSquareIcon className="w-4" />
                        <Translate className="underline hover:text-primary-700">
                          View page
                        </Translate>
                      </div>
                    </Link>
                  )}
                </div>
              </form>
            </Tabs.Tab>

            <Tabs.Tab id="Code" key="html" label={<Translate>Markdown</Translate>}>
              <div className="flex flex-col h-full gap-2">
                <HTMLNotification />
                <div className="h-full pt-2">
                  <CodeEditor
                    language="html"
                    intialValue={page.metadata?.content}
                    onMount={editor => {
                      editor.getModel()?.onDidChangeContent(
                        debouncedChangeHandler(() => {
                          setValue('metadata.content', editor.getValue(), { shouldDirty: true });
                        })
                      );
                    }}
                    fallbackElement={
                      <textarea {...register('metadata.content')} className="w-full h-full" />
                    }
                  />
                </div>
              </div>
            </Tabs.Tab>

            <Tabs.Tab id="Advanced" label={<Translate>Javascript</Translate>}>
              <div className="flex flex-col h-full gap-2">
                <JSNotification />
                <div className="h-full pt-2">
                  <CodeEditor
                    language="javascript"
                    intialValue={page.metadata?.script}
                    onMount={editor => {
                      editor.getModel()?.onDidChangeContent(
                        debouncedChangeHandler(() => {
                          setValue('metadata.script', editor.getValue(), { shouldDirty: true });
                        })
                      );
                    }}
                    fallbackElement={
                      <textarea {...register('metadata.script')} className="w-full h-full" />
                    }
                  />
                </div>
              </div>
            </Tabs.Tab>
          </Tabs>
        </SettingsContent.Body>

        <SettingsContent.Footer>
          <div className="flex justify-end gap-2">
            <Link to="/settings/pages">
              <Button styling="light" disabled={isSubmitting}>
                <Translate>Cancel</Translate>
              </Button>
            </Link>

            <Button
              styling="solid"
              color="primary"
              onClick={handleSubmit(handleSaveAndPreview)}
              disabled={getValues('entityView') || isSubmitting}
            >
              <Translate>Save & Preview</Translate>
            </Button>

            <Button
              styling="solid"
              color="success"
              onClick={handleSubmit(handleSave)}
              disabled={isSubmitting}
            >
              <Translate>Save</Translate>
            </Button>
          </div>
        </SettingsContent.Footer>
      </SettingsContent>

      {showConfirmationModal && (
        <ConfirmNavigationModal
          setShowModal={setShowConfirmationModal}
          onConfirm={blocker.proceed}
        />
      )}
    </div>
  );
};

export { PageEditor, pageEditorLoader };