function FiltersConfigModal()

in superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx [149:769]


function FiltersConfigModal({
  isOpen,
  initialFilterId,
  createNewOnOpen,
  onSave,
  onCancel,
}: FiltersConfigModalProps) {
  const dispatch = useDispatch();
  const theme = useTheme();

  const [form] = AntdForm.useForm<NativeFiltersForm>();

  const configFormRef = useRef<any>();

  // the filter config from redux state, this does not change until modal is closed.
  const filterConfig = useFilterConfiguration();
  const filterConfigMap = useFilterConfigMap();

  // this state contains the changes that we'll be sent through the PATCH endpoint
  const [filterChanges, setFilterChanges] = useState<FilterChangesType>(
    DEFAULT_FILTER_CHANGES,
  );

  const resetFilterChanges = () => {
    setFilterChanges(DEFAULT_FILTER_CHANGES);
  };

  const handleModifyFilter = useCallback(
    (filterId: string) => {
      if (!filterChanges.modified.includes(filterId)) {
        setFilterChanges(prev => ({
          ...prev,
          modified: [...prev.modified, filterId],
        }));
      }
    },
    [filterChanges.modified],
  );

  // new filter ids belong to filters have been added during
  // this configuration session, and only exist in the form state until we submit.
  const [newFilterIds, setNewFilterIds] = useState<string[]>(
    DEFAULT_EMPTY_FILTERS,
  );

  // store ids of filters that have been removed with the time they were removed
  // so that we can disappear them after a few secs.
  // filters are still kept in state until form is submitted.
  const [removedFilters, setRemovedFilters] = useState<
    Record<string, FilterRemoval>
  >(DEFAULT_REMOVED_FILTERS);

  const [saveAlertVisible, setSaveAlertVisible] = useState<boolean>(false);

  // The full ordered set of ((original + new) - completely removed) filter ids
  // Use this as the canonical list of what filters are being configured!
  // This includes filter ids that are pending removal, so check for that.
  const filterIds = useMemo(
    () =>
      uniq([...getFilterIds(filterConfig), ...newFilterIds]).filter(
        id => !removedFilters[id] || removedFilters[id]?.isPending,
      ),
    [filterConfig, newFilterIds, removedFilters],
  );

  // open the first filter in the list to start
  const initialCurrentFilterId = initialFilterId ?? filterIds[0];
  const [currentFilterId, setCurrentFilterId] = useState(
    initialCurrentFilterId,
  );
  const [erroredFilters, setErroredFilters] = useState<string[]>(
    DEFAULT_EMPTY_FILTERS,
  );

  // the form values are managed by the antd form, but we copy them to here
  // so that we can display them (e.g. filter titles in the tab headers)
  const [formValues, setFormValues] =
    useState<NativeFiltersForm>(DEFAULT_FORM_VALUES);

  const unsavedFiltersIds = newFilterIds.filter(id => !removedFilters[id]);
  // brings back a filter that was previously removed ("Undo")
  const restoreFilter = useCallback(
    (id: string) => {
      const removal = removedFilters[id];
      // Clear the removal timeout if the filter is pending deletion
      if (removal?.isPending) clearTimeout(removal.timerId);

      setRemovedFilters(current => ({ ...current, [id]: null }));

      setFilterChanges(prev => ({
        ...prev,
        deleted: prev.deleted.filter(deletedId => deletedId !== id),
      }));
    },
    [removedFilters, setRemovedFilters],
  );
  const initialFilterOrder = useMemo(
    () => Object.keys(filterConfigMap),
    [filterConfigMap],
  );

  // State for tracking the re-ordering of filters
  const [orderedFilters, setOrderedFilters] =
    useState<string[]>(initialFilterOrder);

  // State for rendered filter to improve performance
  const [renderedFilters, setRenderedFilters] = useState<string[]>([
    initialCurrentFilterId,
  ]);

  const getActiveFilterPanelKey = (filterId: string) => [
    `${filterId}-${FilterPanels.configuration.key}`,
    `${filterId}-${FilterPanels.settings.key}`,
  ];

  const [activeFilterPanelKey, setActiveFilterPanelKey] = useState<
    string | string[]
  >(getActiveFilterPanelKey(initialCurrentFilterId));

  const handleTabChange = (filterId: string) => {
    setCurrentFilterId(filterId);
    setActiveFilterPanelKey(getActiveFilterPanelKey(filterId));
  };

  // generates a new filter id and appends it to the newFilterIds
  const addFilter = useCallback(
    (type: NativeFilterType) => {
      const newFilterId = generateFilterId(type);
      setNewFilterIds([...newFilterIds, newFilterId]);
      handleModifyFilter(newFilterId);
      setCurrentFilterId(newFilterId);
      setSaveAlertVisible(false);
      setOrderedFilters([...orderedFilters, newFilterId]);
      setActiveFilterPanelKey(getActiveFilterPanelKey(newFilterId));
    },
    [newFilterIds, handleModifyFilter, orderedFilters],
  );

  useOpenModal(isOpen, addFilter, createNewOnOpen);

  useRemoveCurrentFilter(
    removedFilters,
    currentFilterId,
    orderedFilters,
    setCurrentFilterId,
  );

  const handleRemoveItem = createHandleRemoveItem(
    setRemovedFilters,
    setOrderedFilters,
    setSaveAlertVisible,
    filterId => {
      setFilterChanges(prev => ({
        ...prev,
        deleted: [...prev.deleted, filterId],
      }));
    },
  );

  // After this, it should be as if the modal was just opened fresh.
  // Called when the modal is closed.
  const resetForm = (isSaving = false) => {
    setNewFilterIds(DEFAULT_EMPTY_FILTERS);
    setCurrentFilterId(initialCurrentFilterId);
    setRemovedFilters(DEFAULT_REMOVED_FILTERS);
    setSaveAlertVisible(false);
    setFormValues(DEFAULT_FORM_VALUES);
    resetFilterChanges();
    setErroredFilters(DEFAULT_EMPTY_FILTERS);
    if (filterIds.length > 0) {
      setActiveFilterPanelKey(getActiveFilterPanelKey(filterIds[0]));
    }
    if (!isSaving) {
      setOrderedFilters(initialFilterOrder);
    }
    setRenderedFilters([initialCurrentFilterId]);
    form.resetFields(['filters']);
    form.setFieldsValue({ changed: false });
  };

  const getFilterTitle = useCallback(
    (id: string) => {
      const formValue = formValues.filters[id];
      const config = filterConfigMap[id];
      return (
        (formValue && 'name' in formValue && formValue.name) ||
        (formValue && 'title' in formValue && formValue.title) ||
        (config && 'name' in config && config.name) ||
        (config && 'title' in config && config.title) ||
        t('[untitled]')
      );
    },
    [filterConfigMap, formValues.filters],
  );

  const canBeUsedAsDependency = useCallback(
    (filterId: string) => {
      if (removedFilters[filterId]) {
        return false;
      }
      const component =
        form.getFieldValue('filters')?.[filterId] || filterConfigMap[filterId];
      return (
        component &&
        'filterType' in component &&
        ALLOW_DEPENDENCIES.includes(component.filterType)
      );
    },
    [filterConfigMap, form, removedFilters],
  );

  const getAvailableFilters = useCallback(
    (filterId: string) =>
      filterIds
        .filter(id => id !== filterId)
        .filter(id => canBeUsedAsDependency(id))
        .map(id => ({
          label: getFilterTitle(id),
          value: id,
          type: filterConfigMap[id]?.filterType,
        })),
    [canBeUsedAsDependency, filterConfigMap, filterIds, getFilterTitle],
  );

  /**
   * Manages dependencies of filters associated with a deleted filter.
   *
   * @param values the native filters form
   * @returns the updated filterConfigMap
   */
  const cleanDeletedParents = (values: NativeFiltersForm | null) => {
    const modifiedParentFilters = new Set<string>();
    const updatedFilterConfigMap = Object.keys(filterConfigMap).reduce(
      (acc, key) => {
        const filter = filterConfigMap[key];
        const cascadeParentIds = filter.cascadeParentIds?.filter(id =>
          canBeUsedAsDependency(id),
        );

        if (
          cascadeParentIds &&
          !isEqual(cascadeParentIds, filter.cascadeParentIds)
        ) {
          dispatch(updateCascadeParentIds(key, cascadeParentIds));
          modifiedParentFilters.add(key);
        }

        return {
          ...acc,
          [key]: {
            ...filter,
            cascadeParentIds,
          },
        };
      },
      {},
    );

    const filters = values?.filters;
    if (filters) {
      Object.keys(filters).forEach(key => {
        const filter = filters[key];

        if (!('dependencies' in filter)) {
          return;
        }

        const originalDependencies = filter.dependencies || [];
        const cleanedDependencies = originalDependencies.filter(id =>
          canBeUsedAsDependency(id),
        );

        if (!isEqual(cleanedDependencies, originalDependencies)) {
          filter.dependencies = cleanedDependencies;
          modifiedParentFilters.add(key);
        }
      });
    }

    return [updatedFilterConfigMap, modifiedParentFilters];
  };

  const handleErroredFilters = useCallback(() => {
    // managing left pane errored filters indicators
    const formValidationFields = form.getFieldsError();
    const erroredFiltersIds: string[] = [];

    formValidationFields.forEach(field => {
      const filterId = field.name[1] as string;
      if (field.errors.length > 0 && !erroredFiltersIds.includes(filterId)) {
        erroredFiltersIds.push(filterId);
      }
    });

    // no form validation issues found, resets errored filters
    if (!erroredFiltersIds.length && erroredFilters.length > 0) {
      setErroredFilters(DEFAULT_EMPTY_FILTERS);
      return;
    }
    // form validation issues found, sets errored filters
    if (
      erroredFiltersIds.length > 0 &&
      !isEqual(sortBy(erroredFilters), sortBy(erroredFiltersIds))
    ) {
      setErroredFilters(erroredFiltersIds);
    }
  }, [form, erroredFilters]);

  const handleSave = async () => {
    const values: NativeFiltersForm | null = await validateForm(
      form,
      currentFilterId,
      setCurrentFilterId,
    );

    handleErroredFilters();

    if (values) {
      const [updatedFilterConfigMap, modifiedParentFilters] =
        cleanDeletedParents(values);

      const allModified = [
        ...new Set([
          ...(modifiedParentFilters as Set<string>),
          ...filterChanges.modified,
        ]),
      ];

      const actualChanges = {
        ...filterChanges,
        modified:
          allModified.length && filterChanges.deleted.length
            ? allModified.filter(id => !filterChanges.deleted.includes(id))
            : allModified,
        reordered:
          filterChanges.reordered.length &&
          !isEqual(filterChanges.reordered, initialFilterOrder)
            ? filterChanges.reordered
            : [],
      };

      createHandleSave(onSave, actualChanges, values, updatedFilterConfigMap)();
      resetForm(true);
      resetFilterChanges();
    } else {
      configFormRef.current?.changeTab?.('configuration');
    }
  };

  const handleConfirmCancel = () => {
    resetForm();
    onCancel();
  };

  const handleCancel = () => {
    const changed = form.getFieldValue('changed');
    const didChangeOrder =
      orderedFilters.length !== initialFilterOrder.length ||
      orderedFilters.some((val, index) => val !== initialFilterOrder[index]);
    if (
      unsavedFiltersIds.length > 0 ||
      form.isFieldsTouched() ||
      changed ||
      didChangeOrder ||
      Object.values(removedFilters).some(f => f?.isPending)
    ) {
      setSaveAlertVisible(true);
    } else {
      handleConfirmCancel();
    }
  };
  const handleRearrange = (dragIndex: number, targetIndex: number) => {
    const newOrderedFilter = [...orderedFilters];
    const removed = newOrderedFilter.splice(dragIndex, 1)[0];
    newOrderedFilter.splice(targetIndex, 0, removed);
    setOrderedFilters(newOrderedFilter);
    setFilterChanges(prev => ({
      ...prev,
      reordered: newOrderedFilter,
    }));
  };

  const buildDependencyMap = useCallback(() => {
    const dependencyMap = new Map<string, string[]>();
    const filters = form.getFieldValue('filters');
    if (filters) {
      Object.keys(filters).forEach(key => {
        const formItem = filters[key];
        const configItem = filterConfigMap[key];
        let array: string[] = [];
        if (formItem && 'dependencies' in formItem) {
          array = [...formItem.dependencies];
        } else if (configItem?.cascadeParentIds) {
          array = [...configItem.cascadeParentIds];
        }
        dependencyMap.set(key, array);
      });
    }
    return dependencyMap;
  }, [filterConfigMap, form]);

  const validateDependencies = useCallback(() => {
    const dependencyMap = buildDependencyMap();
    filterIds
      .filter(id => !removedFilters[id])
      .forEach(filterId => {
        const result = hasCircularDependency(dependencyMap, filterId);
        const field = {
          name: ['filters', filterId, 'dependencies'],
          errors: result ? [t('Cyclic dependency detected')] : [],
        };
        form.setFields([field]);
      });
    handleErroredFilters();
  }, [
    buildDependencyMap,
    filterIds,
    form,
    handleErroredFilters,
    removedFilters,
  ]);

  const getDependencySuggestion = useCallback(
    (filterId: string) => {
      const dependencyMap = buildDependencyMap();
      const possibleDependencies = orderedFilters.filter(
        key => key !== filterId && canBeUsedAsDependency(key),
      );
      const found = possibleDependencies.find(filter => {
        const dependencies = dependencyMap.get(filterId) || [];
        dependencies.push(filter);
        if (hasCircularDependency(dependencyMap, filterId)) {
          dependencies.pop();
          return false;
        }
        return true;
      });
      return found || possibleDependencies[0];
    },
    [buildDependencyMap, canBeUsedAsDependency, orderedFilters],
  );

  const [expanded, setExpanded] = useState(false);
  const toggleExpand = useEffectEvent(() => {
    setExpanded(!expanded);
  });
  const ToggleIcon = expanded
    ? Icons.FullscreenExitOutlined
    : Icons.FullscreenOutlined;

  const handleValuesChange = useMemo(
    () =>
      debounce((changes: any, values: NativeFiltersForm) => {
        const didChangeFilterName =
          changes.filters &&
          Object.values(changes.filters).some(
            (filter: any) => filter.name && filter.name !== null,
          );
        const didChangeSectionTitle =
          changes.filters &&
          Object.values(changes.filters).some(
            (filter: any) => filter.title && filter.title !== null,
          );
        if (didChangeFilterName || didChangeSectionTitle) {
          // we only need to set this if a name/title changed
          setFormValues(values);
        }
        setSaveAlertVisible(false);
        handleErroredFilters();
      }, SLOW_DEBOUNCE),
    [handleErroredFilters],
  );

  useEffect(() => {
    if (!isEmpty(removedFilters)) {
      setErroredFilters(prevErroredFilters =>
        prevErroredFilters.filter(f => !removedFilters[f]),
      );
    }
  }, [removedFilters]);

  useEffect(() => {
    if (!renderedFilters.includes(currentFilterId)) {
      setRenderedFilters([...renderedFilters, currentFilterId]);
    }
  }, [currentFilterId]);

  const handleActiveFilterPanelChange = useCallback(
    key => setActiveFilterPanelKey(key),
    [setActiveFilterPanelKey],
  );

  const formList = useMemo(
    () =>
      orderedFilters.map(id => {
        if (!renderedFilters.includes(id)) return null;
        const isDivider = id.startsWith(NATIVE_FILTER_DIVIDER_PREFIX);
        const isActive = currentFilterId === id;
        return (
          <div
            key={id}
            style={{
              height: '100%',
              overflowY: 'auto',
              display: isActive ? '' : 'none',
            }}
          >
            {isDivider ? (
              <DividerConfigForm
                componentId={id}
                divider={filterConfigMap[id] as Divider}
              />
            ) : (
              <FiltersConfigForm
                expanded={expanded}
                ref={configFormRef}
                form={form}
                filterId={id}
                filterToEdit={filterConfigMap[id] as Filter}
                removedFilters={removedFilters}
                restoreFilter={restoreFilter}
                getAvailableFilters={getAvailableFilters}
                key={id}
                activeFilterPanelKeys={activeFilterPanelKey}
                handleActiveFilterPanelChange={handleActiveFilterPanelChange}
                isActive={isActive}
                setErroredFilters={setErroredFilters}
                validateDependencies={validateDependencies}
                getDependencySuggestion={getDependencySuggestion}
                onModifyFilter={handleModifyFilter}
              />
            )}
          </div>
        );
      }),
    [
      orderedFilters,
      renderedFilters,
      currentFilterId,
      filterConfigMap,
      expanded,
      form,
      removedFilters,
      restoreFilter,
      getAvailableFilters,
      activeFilterPanelKey,
      handleActiveFilterPanelChange,
      validateDependencies,
      getDependencySuggestion,
      handleModifyFilter,
    ],
  );

  useEffect(() => {
    resetFilterChanges();
  }, []);

  return (
    <StyledModalWrapper
      open={isOpen}
      maskClosable={false}
      title={t('Add and edit filters')}
      expanded={expanded}
      destroyOnClose
      onCancel={handleCancel}
      onOk={handleSave}
      centered
      data-test="filter-modal"
      footer={
        <div
          css={css`
            display: flex;
            justify-content: flex-end;
            align-items: flex-end;
          `}
        >
          <Footer
            onDismiss={() => setSaveAlertVisible(false)}
            onCancel={handleCancel}
            handleSave={handleSave}
            canSave={!erroredFilters.length}
            saveAlertVisible={saveAlertVisible}
            onConfirmCancel={handleConfirmCancel}
          />
          <StyledExpandButtonWrapper>
            <ToggleIcon
              iconSize="l"
              iconColor={theme.colors.grayscale.dark2}
              onClick={toggleExpand}
            />
          </StyledExpandButtonWrapper>
        </div>
      }
    >
      <ErrorBoundary>
        <StyledModalBody expanded={expanded}>
          <StyledForm
            form={form}
            onValuesChange={handleValuesChange}
            layout="vertical"
          >
            <FilterConfigurePane
              erroredFilters={erroredFilters}
              onRemove={handleRemoveItem}
              onAdd={addFilter}
              onChange={handleTabChange}
              getFilterTitle={getFilterTitle}
              currentFilterId={currentFilterId}
              removedFilters={removedFilters}
              restoreFilter={restoreFilter}
              onRearrange={handleRearrange}
              filters={orderedFilters}
            >
              {formList}
            </FilterConfigurePane>
          </StyledForm>
        </StyledModalBody>
      </ErrorBoundary>
    </StyledModalWrapper>
  );
}