huridocs/uwazi

View on GitHub
app/react/V2/Routes/Settings/Collection/Collection.tsx

Summary

Maintainability
A
1 hr
Test Coverage
/* eslint-disable max-statements */
/* eslint-disable max-lines */
/* eslint-disable camelcase */
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';
import { IncomingHttpHeaders } from 'http';
import { LoaderFunction, useLoaderData, useRevalidator } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { useSetAtom } from 'jotai';
import { isUndefined } from 'lodash';
import { Tooltip } from 'flowbite-react';
import { QuestionMarkCircleIcon } from '@heroicons/react/20/solid';
import * as SettingsAPI from 'V2/api/settings';
import * as TemplatesAPI from 'V2/api/templates';
import { notificationAtom } from 'app/V2/atoms';
import { InputField, Select, MultiSelect, Geolocation } from 'app/V2/Components/Forms';
import { Button, Card } from 'app/V2/Components/UI';
import { settingsAtom } from 'app/V2/atoms/settingsAtom';
import { SettingsContent } from 'app/V2/Components/Layouts/SettingsContent';
import { Translate, t } from 'app/I18N';
import { ClientSettings, Template } from 'app/apiResponseTypes';
import { FetchResponseError } from 'shared/JSONRequest';
import * as tips from './collectionSettingsTips';
import { CollectionOptionToggle } from './CollectionOptionToggle';

const collectionLoader =
  (headers?: IncomingHttpHeaders): LoaderFunction =>
  async () => {
    const settings = await SettingsAPI.get(headers);
    const templates = await TemplatesAPI.get(headers);
    return { settings, templates };
  };

const dateOptions = () => {
  const date = new Date();
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');

  const yearLabel = t('System', 'Year', null, false);
  const monthLabel = t('System', 'Month', null, false);
  const dayLabel = t('System', 'Day', null, false);

  return [
    {
      value: 'yyyy/MM/dd',
      label: `${year}/${month}/${day} (${yearLabel}/${monthLabel}/${dayLabel})`,
    },
    {
      value: 'dd/MM/yyyy',
      label: `${day}/${month}/${year} (${dayLabel}/${monthLabel}/${yearLabel})`,
    },
    {
      value: 'MM/dd/yyyy',
      label: `${month}/${day}/${year} (${monthLabel}/${dayLabel}/${yearLabel})`,
    },
    {
      value: 'yyyy-MM-dd',
      label: `${year}-${month}-${day} (${yearLabel}-${monthLabel}-${dayLabel})`,
    },
    {
      value: 'dd-MM-yyyy',
      label: `${day}-${month}-${year} (${dayLabel}-${monthLabel}-${yearLabel})`,
    },
    {
      value: 'MM-dd-yyyy',
      label: `${month}-${day}-${year} (${monthLabel}-${dayLabel}-${yearLabel})`,
    },
  ];
};

const Collection = () => {
  const { settings, templates } = useLoaderData() as {
    settings: ClientSettings;
    templates: Template[];
  };
  const { links, custom, ...formData } = settings;

  const setNotifications = useSetAtom(notificationAtom);
  const setSettings = useSetAtom(settingsAtom);
  const revalidator = useRevalidator();
  formData.private = !formData.private;
  const {
    register,
    setValue,
    watch,
    handleSubmit,
    setError,
    clearErrors,
    formState: { errors },
  } = useForm<ClientSettings>({
    defaultValues: formData,
    mode: 'onSubmit',
  });

  const submit = async (data: ClientSettings) => {
    if (!isUndefined(data.newNameGeneration) && !data.newNameGeneration) {
      delete data.newNameGeneration;
    }
    data.private = !data.private;
    const response = await SettingsAPI.save(data);
    if (response instanceof FetchResponseError) {
      setNotifications({
        type: 'error',
        text: <Translate>An error occurred</Translate>,
        details: response.message || undefined,
      });
    } else {
      setSettings(response);
      setNotifications({
        type: 'success',
        text: <Translate>Settings updated</Translate>,
      });
    }
    revalidator.revalidate();
  };

  const labelWithTip = (label: string, tip: React.ReactNode) => (
    <span className="flex gap-4">
      {label}
      <Tooltip
        // eslint-disable-next-line react/style-prop-object
        style="light"
        content={tip}
        placement="right"
      >
        <QuestionMarkCircleIcon className="w-5 h-5 text-gray-500" />
      </Tooltip>
    </span>
  );

  const templateOptions = templates.map(template => ({
    label: template.name,
    value: template._id,
  }));

  const mapLayersOptions = [
    { label: 'Dark', value: 'Dark' },
    {
      label: 'Streets',
      value: 'Streets',
    },
    {
      label: 'Satellite',
      value: 'Satellite',
    },
    {
      label: 'Hybrid',
      value: 'Hybrid',
    },
  ];

  return (
    <div
      className="tw-content"
      style={{ width: '100%', overflowY: 'auto' }}
      data-testid="settings-collection"
    >
      <SettingsContent>
        <SettingsContent.Header title="Collection" />
        <SettingsContent.Body>
          <form onSubmit={handleSubmit(submit)} id="collection-form">
            <Card className="mb-4" title={<Translate>General</Translate>}>
              <div className="grid gap-4 sm:grid-cols-2 sm:gap-6">
                <div className="sm:col-span-1">
                  <InputField
                    id="collection-name"
                    hasErrors={!!errors.site_name}
                    label={<Translate>Collection Name</Translate>}
                    {...register('site_name', { required: true })}
                  />
                </div>
                <div className="sm:col-span-1">
                  <InputField
                    id="favicon"
                    type="text"
                    label={<Translate>Custom Favicon</Translate>}
                    {...register('favicon')}
                  />
                </div>
                <div className="sm:col-span-1">
                  <Select
                    label={<Translate>Default View</Translate>}
                    id="roles"
                    options={[
                      { label: 'Cards', value: 'cards' },
                      { label: 'Map', value: 'map' },
                      { label: 'Table', value: 'table' },
                    ]}
                    {...register('defaultLibraryView')}
                  />
                </div>
                <div className="sm:col-span-1">
                  <Select
                    label={<Translate>Default date format</Translate>}
                    id="date-format"
                    options={dateOptions()}
                    {...register('dateFormat')}
                  />
                </div>
                <div className="sm:col-span-2">
                  <InputField
                    id="landing-page"
                    preText="eg. https://yourdomain"
                    label={labelWithTip('Custom landing page', tips.landingPageTip)}
                    {...register('home_page')}
                  />
                </div>
                <CollectionOptionToggle
                  valueKey="private"
                  label="Public instance"
                  tip={tips.publicSharing}
                  register={register}
                  defaultChecked={formData.private}
                />
                <CollectionOptionToggle
                  valueKey="cookiepolicy"
                  label="Show cookie policy"
                  tip={tips.cookiePolicy}
                  register={register}
                  defaultChecked={formData.cookiepolicy}
                />
                <CollectionOptionToggle
                  valueKey="allowcustomJS"
                  label="Global JS"
                  tip={tips.globalJS}
                  register={register}
                  defaultChecked={formData.allowcustomJS}
                />
                {!settings.newNameGeneration && (
                  <CollectionOptionToggle
                    valueKey="newNameGeneration"
                    label="Non-latin characters support"
                    tip={tips.characterSupport}
                    register={register}
                    defaultChecked={formData.newNameGeneration}
                  />
                )}
              </div>
            </Card>
            <Card
              className="mb-4"
              title={
                <span className="flex gap-4">
                  <Translate>Analytics</Translate>
                  <Tooltip
                    // eslint-disable-next-line react/style-prop-object
                    style="light"
                    content={tips.analytics}
                    placement="right"
                  >
                    <QuestionMarkCircleIcon className="w-5 h-5 text-gray-500" />
                  </Tooltip>
                </span>
              }
            >
              <div className="grid gap-4 sm:grid-cols-2 sm:gap-6">
                <div className="sm:col-span-1">
                  <InputField
                    id="google-analytics"
                    label={<Translate>Google</Translate>}
                    {...register('analyticsTrackingId')}
                  />
                </div>
                <div className="sm:col-span-1">
                  <InputField
                    id="matomo-analytics"
                    label={<Translate>Matomo</Translate>}
                    {...register('matomoConfig')}
                  />
                </div>
              </div>
            </Card>
            {settings.features?.ocr?.url && (
              <Card className="mb-4" title={<Translate>Services</Translate>}>
                <CollectionOptionToggle
                  valueKey="ocrServiceEnabled"
                  label="Document OCR trigger"
                  tip={tips.ocrTrigger}
                  register={register}
                  defaultChecked={formData.ocrServiceEnabled}
                />
              </Card>
            )}
            <Card className="mb-4" title={<Translate>Forms and email configuration</Translate>}>
              <div className="grid gap-4 sm:grid-cols-2 sm:gap-6">
                <div className="sm:col-span-1">
                  <InputField
                    id="sending-email"
                    label={labelWithTip('Sending email', tips.emails[1])}
                    {...register('senderEmail')}
                  />
                </div>
                <div className="sm:col-span-1">
                  <InputField
                    id="receiving-email"
                    label={labelWithTip('Contact form email', tips.receivingEmail)}
                    {...register('contactEmail')}
                  />
                </div>
                <div className="sm:col-span-2">
                  <InputField
                    id="public-form-destination"
                    label={labelWithTip('Public Form submit URL', tips.publicForm[1])}
                    {...register('publicFormDestination')}
                  />
                </div>
                <CollectionOptionToggle
                  valueKey="openPublicEndpoint"
                  label="Allow captcha bypass"
                  tip={tips.openPublicForm}
                  register={register}
                  defaultChecked={formData.openPublicEndpoint}
                />
                <div className="sm:col-span-2">
                  <MultiSelect
                    label={labelWithTip('Whitelisted templates', tips.publicForm[2])}
                    options={templateOptions}
                    onChange={newValues => {
                      setValue('allowedPublicTemplates', newValues);
                    }}
                    value={settings.allowedPublicTemplates || []}
                  />
                </div>
              </div>
            </Card>
            <Card className="mb-4" title={<Translate>Map</Translate>}>
              <div className="grid gap-4 sm:grid-cols-2 sm:gap-6">
                <div className="sm:col-span-1">
                  <Select
                    label={<Translate>Map Provider</Translate>}
                    id="roles"
                    options={[
                      { label: 'Mapbox', value: 'mapbox' },
                      { label: 'Google', value: 'google' },
                    ]}
                    {...register('tilesProvider')}
                  />
                </div>
                <div className="sm:col-span-1">
                  <InputField
                    id="map-key"
                    label={labelWithTip('Map API key', tips.mapApiKey)}
                    {...register('mapApiKey', { pattern: /^[a-zA-Z0-9._]+$/ })}
                  />
                </div>
                <div className="sm:col-span-2">
                  <MultiSelect
                    label={labelWithTip('Map Layers', tips.mapLayers)}
                    options={mapLayersOptions}
                    hasErrors={!!errors.mapLayers}
                    canBeEmpty={false}
                    value={settings.mapLayers?.length ? settings.mapLayers : ['Streets']}
                    onChange={newValues => {
                      clearErrors('mapLayers');
                      if (!newValues.length) {
                        setError(
                          'mapLayers',
                          { type: 'custom', message: 'Map layers cannot be empty' },
                          { shouldFocus: true }
                        );
                        return;
                      }
                      //@ts-ignore
                      setValue('mapLayers', newValues);
                    }}
                  />
                </div>
                <div className="col-span-2">
                  <Geolocation
                    value={watch('mapStartingPoint')?.[0] ?? {}}
                    onChange={({ lat, lon }: { lat?: number; lon?: number }) => {
                      if (lat && lon) {
                        setValue('mapStartingPoint', [{ lat, lon }]);
                        return;
                      }
                      setValue('mapStartingPoint', []);
                    }}
                    name="mapStartingPoint"
                  />
                </div>
              </div>
            </Card>
          </form>
        </SettingsContent.Body>
        <SettingsContent.Footer>
          <div className="flex gap-2">
            <Button type="submit" form="collection-form">
              <Translate>Save</Translate>
            </Button>
          </div>
        </SettingsContent.Footer>
      </SettingsContent>
    </div>
  );
};

export { Collection, collectionLoader };