dashpresshq/dashpress

View on GitHub
src/frontend/views/entity/PersistentQuery/Form.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
A
100%
import type { MessageDescriptor } from "@lingui/core";
import { msg } from "@lingui/macro";
import arrayMutators from "final-form-arrays";
import { Fragment } from "react";
import { Field, Form, useField } from "react-final-form";
import { FieldArray } from "react-final-form-arrays";

import { DELETE_BUTTON_PROPS } from "@/components/app/button/constants";
import { FormButton } from "@/components/app/button/form";
import { SoftButton } from "@/components/app/button/soft";
import { FormSelect } from "@/components/app/form/input/select";
import { FormSelectButton } from "@/components/app/form/input/select-button";
import { FormInput } from "@/components/app/form/input/text";
import { FormGrid } from "@/components/app/form/schema/form-grid";
import { FILTER_OPERATOR_CONFIG } from "@/components/app/table/filters/constants";
import type { ITableColumn } from "@/components/app/table/types";
import { Card, CardContent } from "@/components/ui/card";
import { useAppConfigurationDomainMessages } from "@/frontend/hooks/configuration/configuration.constant";
import type { IFormProps } from "@/frontend/lib/form/types";
import { composeValidators, required } from "@/frontend/lib/validations";
import { ACTIONS_ACCESSOR } from "@/frontend/views/data/Table/useTableColumns";
import { typescriptSafeObjectDotEntries } from "@/shared/lib/objects";
import type { QueryFilterSchema } from "@/shared/types/data";

const OPERATOR_SELECTORS = [
  {
    value: "and",
    label: msg`AND`,
  },
  {
    value: "or",
    label: msg`OR`,
  },
];

const filterOperatorSelections = typescriptSafeObjectDotEntries(
  FILTER_OPERATOR_CONFIG
)
  .filter(([, value]) => !value.disabled)
  .map(([key, value]) => ({ value: key, label: value.label }));

function FilterRow({
  queryFilter,
  columns,
}: {
  queryFilter: string;
  columns: ITableColumn[];
}) {
  const operatorValue = useField(`${queryFilter}.value.operator`).input.value;

  const noValue = FILTER_OPERATOR_CONFIG[operatorValue]?.numberOfInput === 0;

  return (
    <div className="w-full">
      <FormGrid.Root>
        <Field
          name={`${queryFilter}.id`}
          validate={composeValidators(required)}
          validateFields={[]}
        >
          {({ meta, input }) => (
            <FormGrid.Item span="3">
              <FormSelect
                label={msg`Field`}
                required
                selectData={columns.map((column) => ({
                  value: column.accessor,
                  label: column.Header as MessageDescriptor,
                }))}
                meta={meta}
                input={input}
              />
            </FormGrid.Item>
          )}
        </Field>

        <Field
          name={`${queryFilter}.value.operator`}
          validate={composeValidators(required)}
          validateFields={[]}
        >
          {({ meta, input }) => (
            <FormGrid.Item span={noValue ? "9" : "3"}>
              <FormSelect
                label={msg`Operator`}
                required
                selectData={filterOperatorSelections}
                meta={meta}
                input={input}
              />
            </FormGrid.Item>
          )}
        </Field>
        {noValue ? null : (
          <Field
            name={`${queryFilter}.value.value`}
            validate={composeValidators(required)}
            validateFields={[]}
          >
            {({ meta, input }) => (
              <FormGrid.Item span="6">
                <FormInput
                  label={msg`Value`}
                  required
                  meta={meta}
                  input={input}
                />
              </FormGrid.Item>
            )}
          </Field>
        )}
      </FormGrid.Root>
    </div>
  );
}

export function EntityPersistentQueryForm({
  onSubmit,
  initialValues,
  tableColumns,
}: IFormProps<QueryFilterSchema> & { tableColumns: ITableColumn[] }) {
  const columns = tableColumns.filter(
    ({ accessor, filter }) => ACTIONS_ACCESSOR !== accessor && filter
  );
  const domainMessages = useAppConfigurationDomainMessages("persistent_query");

  return (
    <Form
      onSubmit={onSubmit}
      mutators={{
        ...arrayMutators,
      }}
      initialValues={initialValues}
      render={({ handleSubmit, pristine, submitting }) => {
        return (
          <>
            <FieldArray name="children">
              {({ fields: queryFields }) => (
                <div>
                  {queryFields.map((name, queryFieldIndex) => (
                    <Fragment key={name}>
                      {queryFields.length > 1 && queryFieldIndex !== 0 && (
                        <Field name="operator" validateFields={[]}>
                          {({ input, meta }) => (
                            <FormSelectButton
                              required
                              size="sm"
                              selectData={OPERATOR_SELECTORS}
                              meta={meta}
                              input={input}
                            />
                          )}
                        </Field>
                      )}
                      <Card className="mb-4">
                        <CardContent>
                          <FieldArray name={`${name}.children`}>
                            {({ fields: queryFilters }) => (
                              <div>
                                {queryFilters.map(
                                  (queryFilter, queryFilterIndex) => (
                                    <div key={queryFilter}>
                                      {queryFilters.length > 1 &&
                                        queryFilterIndex !== 0 && (
                                          <Field
                                            name={`${name}.operator`}
                                            validateFields={[]}
                                          >
                                            {({ input, meta }) => (
                                              <FormSelectButton
                                                size="sm"
                                                required
                                                selectData={OPERATOR_SELECTORS}
                                                meta={meta}
                                                input={input}
                                              />
                                            )}
                                          </Field>
                                        )}
                                      <div className="flex items-center gap-2">
                                        <FilterRow
                                          queryFilter={queryFilter}
                                          columns={columns}
                                        />
                                        <SoftButton
                                          size="icon"
                                          {...DELETE_BUTTON_PROPS({
                                            action: () => {
                                              if (queryFilters.length === 1) {
                                                queryFields.remove(
                                                  queryFieldIndex
                                                );
                                                return;
                                              }
                                              queryFilters.remove(
                                                queryFilterIndex
                                              );
                                            },
                                            label: msg`Remove Nested Filter`,
                                            isMakingRequest: false,
                                            shouldConfirmAlert: undefined,
                                          })}
                                        />
                                      </div>
                                    </div>
                                  )
                                )}
                                <SoftButton
                                  systemIcon="Plus"
                                  label={msg`Add Nested Filter`}
                                  action={() =>
                                    queryFilters.push({
                                      id: "",
                                      value: {
                                        operator: undefined,
                                        value: "",
                                      },
                                    })
                                  }
                                />
                              </div>
                            )}
                          </FieldArray>
                        </CardContent>
                      </Card>
                    </Fragment>
                  ))}
                  <div className="mt-2 flex justify-end">
                    <SoftButton
                      systemIcon="Plus"
                      label={msg`Add Filter`}
                      action={() =>
                        queryFields.push({
                          operator: "and",
                          children: [
                            {
                              id: "",
                              value: {
                                operator: undefined,
                                value: "",
                              },
                            },
                          ],
                        })
                      }
                    />
                  </div>
                </div>
              )}
            </FieldArray>
            <FormButton
              className="mt-3"
              isMakingRequest={submitting}
              onClick={handleSubmit}
              text={domainMessages.FORM_LANG.UPSERT}
              disabled={pristine}
              systemIcon="Save"
            />
          </>
        );
      }}
    />
  );
}