"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.FlatTermSelector = FlatTermSelector;
exports.default = void 0;
var _i18n = require("@wordpress/i18n");
var _element = require("@wordpress/element");
var _components = require("@wordpress/components");
var _data = require("@wordpress/data");
var _deprecated = _interopRequireDefault(require("@wordpress/deprecated"));
var _coreData = require("@wordpress/core-data");
var _compose = require("@wordpress/compose");
var _a11y = require("@wordpress/a11y");
var _notices = require("@wordpress/notices");
var _store = require("../../store");
var _terms = require("../../utils/terms");
var _mostUsedTerms = _interopRequireDefault(require("./most-used-terms"));
var _jsxRuntime = require("react/jsx-runtime");
/**
 * WordPress dependencies
 */

/**
 * Internal dependencies
 */

/**
 * Shared reference to an empty array for cases where it is important to avoid
 * returning a new array reference on every invocation.
 *
 * @type {Array<any>}
 */const EMPTY_ARRAY = [];

/**
 * How the max suggestions limit was chosen:
 *  - Matches the `per_page` range set by the REST API.
 *  - Can't use "unbound" query. The `FormTokenField` needs a fixed number.
 *  - Matches default for `FormTokenField`.
 */
const MAX_TERMS_SUGGESTIONS = 100;
const DEFAULT_QUERY = {
  per_page: MAX_TERMS_SUGGESTIONS,
  _fields: 'id,name',
  context: 'view'
};
const isSameTermName = (termA, termB) => (0, _terms.unescapeString)(termA).toLowerCase() === (0, _terms.unescapeString)(termB).toLowerCase();
const termNamesToIds = (names, terms) => {
  return names.map(termName => terms.find(term => isSameTermName(term.name, termName))?.id).filter(id => id !== undefined);
};
const Wrapper = ({
  children,
  __nextHasNoMarginBottom
}) => __nextHasNoMarginBottom ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalVStack, {
  spacing: 4,
  children: children
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_element.Fragment, {
  children: children
});

/**
 * Renders a flat term selector component.
 *
 * @param {Object}  props                         The component props.
 * @param {string}  props.slug                    The slug of the taxonomy.
 * @param {boolean} props.__nextHasNoMarginBottom Start opting into the new margin-free styles that will become the default in a future version, currently scheduled to be WordPress 7.0. (The prop can be safely removed once this happens.)
 *
 * @return {React.ReactNode} The rendered flat term selector component.
 */
function FlatTermSelector({
  slug,
  __nextHasNoMarginBottom
}) {
  var _taxonomy$labels$add_, _taxonomy$labels$sing2;
  const [values, setValues] = (0, _element.useState)([]);
  const [search, setSearch] = (0, _element.useState)('');
  const debouncedSearch = (0, _compose.useDebounce)(setSearch, 500);
  if (!__nextHasNoMarginBottom) {
    (0, _deprecated.default)('Bottom margin styles for wp.editor.PostTaxonomiesFlatTermSelector', {
      since: '6.7',
      version: '7.0',
      hint: 'Set the `__nextHasNoMarginBottom` prop to true to start opting into the new styles, which will become the default in a future version.'
    });
  }
  const {
    terms,
    termIds,
    taxonomy,
    hasAssignAction,
    hasCreateAction,
    hasResolvedTerms
  } = (0, _data.useSelect)(select => {
    var _post$_links, _post$_links2;
    const {
      getCurrentPost,
      getEditedPostAttribute
    } = select(_store.store);
    const {
      getEntityRecords,
      getEntityRecord,
      hasFinishedResolution
    } = select(_coreData.store);
    const post = getCurrentPost();
    const _taxonomy = getEntityRecord('root', 'taxonomy', slug);
    const _termIds = _taxonomy ? getEditedPostAttribute(_taxonomy.rest_base) : EMPTY_ARRAY;
    const query = {
      ...DEFAULT_QUERY,
      include: _termIds?.join(','),
      per_page: -1
    };
    return {
      hasCreateAction: _taxonomy ? (_post$_links = post._links?.['wp:action-create-' + _taxonomy.rest_base]) !== null && _post$_links !== void 0 ? _post$_links : false : false,
      hasAssignAction: _taxonomy ? (_post$_links2 = post._links?.['wp:action-assign-' + _taxonomy.rest_base]) !== null && _post$_links2 !== void 0 ? _post$_links2 : false : false,
      taxonomy: _taxonomy,
      termIds: _termIds,
      terms: _termIds?.length ? getEntityRecords('taxonomy', slug, query) : EMPTY_ARRAY,
      hasResolvedTerms: hasFinishedResolution('getEntityRecords', ['taxonomy', slug, query])
    };
  }, [slug]);
  const {
    searchResults
  } = (0, _data.useSelect)(select => {
    const {
      getEntityRecords
    } = select(_coreData.store);
    return {
      searchResults: !!search ? getEntityRecords('taxonomy', slug, {
        ...DEFAULT_QUERY,
        search
      }) : EMPTY_ARRAY
    };
  }, [search, slug]);

  // Update terms state only after the selectors are resolved.
  // We're using this to avoid terms temporarily disappearing on slow networks
  // while core data makes REST API requests.
  (0, _element.useEffect)(() => {
    if (hasResolvedTerms) {
      const newValues = (terms !== null && terms !== void 0 ? terms : []).map(term => (0, _terms.unescapeString)(term.name));
      setValues(newValues);
    }
  }, [terms, hasResolvedTerms]);
  const suggestions = (0, _element.useMemo)(() => {
    return (searchResults !== null && searchResults !== void 0 ? searchResults : []).map(term => (0, _terms.unescapeString)(term.name));
  }, [searchResults]);
  const {
    editPost
  } = (0, _data.useDispatch)(_store.store);
  const {
    saveEntityRecord
  } = (0, _data.useDispatch)(_coreData.store);
  const {
    createErrorNotice
  } = (0, _data.useDispatch)(_notices.store);
  if (!hasAssignAction) {
    return null;
  }
  async function findOrCreateTerm(term) {
    try {
      const newTerm = await saveEntityRecord('taxonomy', slug, term, {
        throwOnError: true
      });
      return (0, _terms.unescapeTerm)(newTerm);
    } catch (error) {
      if (error.code !== 'term_exists') {
        throw error;
      }
      return {
        id: error.data.term_id,
        name: term.name
      };
    }
  }
  function onUpdateTerms(newTermIds) {
    editPost({
      [taxonomy.rest_base]: newTermIds
    });
  }
  function onChange(termNames) {
    const availableTerms = [...(terms !== null && terms !== void 0 ? terms : []), ...(searchResults !== null && searchResults !== void 0 ? searchResults : [])];
    const uniqueTerms = termNames.reduce((acc, name) => {
      if (!acc.some(n => n.toLowerCase() === name.toLowerCase())) {
        acc.push(name);
      }
      return acc;
    }, []);
    const newTermNames = uniqueTerms.filter(termName => !availableTerms.find(term => isSameTermName(term.name, termName)));

    // Optimistically update term values.
    // The selector will always re-fetch terms later.
    setValues(uniqueTerms);
    if (newTermNames.length === 0) {
      onUpdateTerms(termNamesToIds(uniqueTerms, availableTerms));
      return;
    }
    if (!hasCreateAction) {
      return;
    }
    Promise.all(newTermNames.map(termName => findOrCreateTerm({
      name: termName
    }))).then(newTerms => {
      const newAvailableTerms = availableTerms.concat(newTerms);
      onUpdateTerms(termNamesToIds(uniqueTerms, newAvailableTerms));
    }).catch(error => {
      createErrorNotice(error.message, {
        type: 'snackbar'
      });
      // In case of a failure, try assigning available terms.
      // This will invalidate the optimistic update.
      onUpdateTerms(termNamesToIds(uniqueTerms, availableTerms));
    });
  }
  function appendTerm(newTerm) {
    var _taxonomy$labels$sing;
    if (termIds.includes(newTerm.id)) {
      return;
    }
    const newTermIds = [...termIds, newTerm.id];
    const defaultName = slug === 'post_tag' ? (0, _i18n.__)('Tag') : (0, _i18n.__)('Term');
    const termAddedMessage = (0, _i18n.sprintf)(/* translators: %s: term name. */
    (0, _i18n._x)('%s added', 'term'), (_taxonomy$labels$sing = taxonomy?.labels?.singular_name) !== null && _taxonomy$labels$sing !== void 0 ? _taxonomy$labels$sing : defaultName);
    (0, _a11y.speak)(termAddedMessage, 'assertive');
    onUpdateTerms(newTermIds);
  }
  const newTermLabel = (_taxonomy$labels$add_ = taxonomy?.labels?.add_new_item) !== null && _taxonomy$labels$add_ !== void 0 ? _taxonomy$labels$add_ : slug === 'post_tag' ? (0, _i18n.__)('Add Tag') : (0, _i18n.__)('Add Term');
  const singularName = (_taxonomy$labels$sing2 = taxonomy?.labels?.singular_name) !== null && _taxonomy$labels$sing2 !== void 0 ? _taxonomy$labels$sing2 : slug === 'post_tag' ? (0, _i18n.__)('Tag') : (0, _i18n.__)('Term');
  const termAddedLabel = (0, _i18n.sprintf)(/* translators: %s: term name. */
  (0, _i18n._x)('%s added', 'term'), singularName);
  const termRemovedLabel = (0, _i18n.sprintf)(/* translators: %s: term name. */
  (0, _i18n._x)('%s removed', 'term'), singularName);
  const removeTermLabel = (0, _i18n.sprintf)(/* translators: %s: term name. */
  (0, _i18n._x)('Remove %s', 'term'), singularName);
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(Wrapper, {
    __nextHasNoMarginBottom: __nextHasNoMarginBottom,
    children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.FormTokenField, {
      __next40pxDefaultSize: true,
      value: values,
      suggestions: suggestions,
      onChange: onChange,
      onInputChange: debouncedSearch,
      maxSuggestions: MAX_TERMS_SUGGESTIONS,
      label: newTermLabel,
      messages: {
        added: termAddedLabel,
        removed: termRemovedLabel,
        remove: removeTermLabel
      },
      __nextHasNoMarginBottom: __nextHasNoMarginBottom
    }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_mostUsedTerms.default, {
      taxonomy: taxonomy,
      onSelect: appendTerm
    })]
  });
}
var _default = exports.default = (0, _components.withFilters)('editor.PostTaxonomyType')(FlatTermSelector);
//# sourceMappingURL=flat-term-selector.js.map