UNPKG

@sanity/form-builder

Version:
371 lines (365 loc) • 22.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ObjectInput = void 0; var _findLast2 = _interopRequireDefault(require("lodash/findLast")); var _find3 = _interopRequireDefault(require("lodash/find")); var _castArray2 = _interopRequireDefault(require("lodash/castArray")); var _react = _interopRequireWildcard(require("react")); var _components = require("@sanity/base/components"); var _ui = require("@sanity/ui"); var _autoId = require("@reach/auto-id"); var _hooks = require("@sanity/base/hooks"); var _paths = require("@sanity/util/paths"); var _PatchEvent = _interopRequireWildcard(require("../../PatchEvent")); var _applyPatch = require("../../patch/applyPatch"); var _empty = require("../../utils/empty"); var _conditionalReadOnly = require("../common/conditionalReadOnly"); var _contexts = require("../../sanity/contexts"); var _common = require("../../utils/common"); var _ObjectInputField = require("./ObjectInputField"); var _UnknownFields = require("./UnknownFields"); var _ObjectFieldSet = require("./ObjectFieldSet"); var _utils = require("./utils"); var _fieldGroups = require("./fieldGroups"); var _ObjectInput = require("./ObjectInput.styled"); var _excluded = ["hidden"]; function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _iterableToArrayLimit(arr, i) { var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"]; if (null != _i) { var _s, _e, _x, _r, _arr = [], _n = !0, _d = !1; try { if (_x = (_i = _i.call(arr)).next, 0 === i) { if (Object(_i) !== _i) return; _n = !1; } else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0); } catch (err) { _d = !0, _e = err; } finally { try { if (!_n && null != _i.return && (_r = _i.return(), Object(_r) !== _r)) return; } finally { if (_d) throw _e; } } return _arr; } } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } var EMPTY_MARKERS = _empty.EMPTY_ARRAY; var EMPTY_PRESENCE = _empty.EMPTY_ARRAY; var EMPTY_PATH = _empty.EMPTY_ARRAY; var DEFAULT_FIELD_GROUP_NAME = 'all-fields'; function isSingleFieldset(fieldset) { return Boolean(fieldset.single); } function isRecord(obj) { return typeof obj === 'object' && obj !== null && !Array.isArray(obj); } var DEFAULT_FILTER_FIELD = () => true; /** * Please read this about collapsible fields * To support deep linking, the received focusPath must always takes precedence over internal collapsed/expanded state. * If a field has been expanded (either manually by the user, or because the focus path has caused it to expand) it * should then stay open and *not* collapse when the field loses focus (e.g. no autocollapse!) * If a field has been actively collapsed by the user, it must still expand again if it receives focus on a path within later on. */ // disable eslint false positive // eslint-disable-next-line react/display-name var ObjectInput = /*#__PURE__*/(0, _react.memo)( /*#__PURE__*/(0, _react.forwardRef)(function ObjectInput(fieldProps, forwardedRef) { var type = fieldProps.type, _fieldProps$presence = fieldProps.presence, presence = _fieldProps$presence === void 0 ? EMPTY_PRESENCE : _fieldProps$presence, _fieldProps$markers = fieldProps.markers, markers = _fieldProps$markers === void 0 ? EMPTY_MARKERS : _fieldProps$markers, onChange = fieldProps.onChange, readOnly = fieldProps.readOnly, _fieldProps$level = fieldProps.level, level = _fieldProps$level === void 0 ? 0 : _fieldProps$level, _fieldProps$focusPath = fieldProps.focusPath, focusPath = _fieldProps$focusPath === void 0 ? EMPTY_PATH : _fieldProps$focusPath, _fieldProps$isRoot = fieldProps.isRoot, isRoot = _fieldProps$isRoot === void 0 ? false : _fieldProps$isRoot, value = fieldProps.value, onFocus = fieldProps.onFocus, onBlur = fieldProps.onBlur, compareValue = fieldProps.compareValue, _fieldProps$filterFie = fieldProps.filterField, filterField = _fieldProps$filterFie === void 0 ? DEFAULT_FILTER_FIELD : _fieldProps$filterFie; var _useCurrentUser = (0, _hooks.useCurrentUser)(), currentUser = _useCurrentUser.value; var _useReviewChanges = (0, _contexts.useReviewChanges)(), changesOpen = _useReviewChanges.changesOpen; var inputId = (0, _autoId.useId)(); var filterGroups = _react.default.useMemo(() => { if (!type.groups || type.groups.length === 0) { return []; } var groups = [{ name: DEFAULT_FIELD_GROUP_NAME, title: 'All fields', fields: type.fields }, ...(type.groups || [])].map(group => { var hidden = group.hidden, rest = _objectWithoutProperties(group, _excluded); return _objectSpread({ hidden: (0, _common.isTrueIsh)(hidden, 'hidden', { currentUser: (0, _common.omitDeprecatedRole)(currentUser), value: group, parent: type.groups }) }, rest); }).filter(group => { var hidden = group.hidden; return !hidden; }); return groups; }, [type.groups, type.fields, currentUser]); var defaultFieldGroupName = _react.default.useMemo(() => { var _ref; if (filterGroups.length === 0) { return DEFAULT_FIELD_GROUP_NAME; } return ((_ref = (0, _findLast2.default)(filterGroups, fieldGroup => fieldGroup.default && !fieldGroup.hidden) || (filterGroups === null || filterGroups === void 0 ? void 0 : filterGroups[0])) === null || _ref === void 0 ? void 0 : _ref.name) || DEFAULT_FIELD_GROUP_NAME; }, [filterGroups]); var _useState = (0, _react.useState)(defaultFieldGroupName), _useState2 = _slicedToArray(_useState, 2), selectedFieldGroupName = _useState2[0], setSelectedFieldGroupName = _useState2[1]; var fieldGroupRootFocusPaths = _react.default.useMemo(() => { var _find2; if (filterGroups.length === 0) { return type.fields.map(field => field.name); } return (((_find2 = (0, _find3.default)(filterGroups, fieldGroup => fieldGroup.name === selectedFieldGroupName)) === null || _find2 === void 0 ? void 0 : _find2.fields) || []).map(field => field.name); }, [filterGroups, selectedFieldGroupName, type.fields]); var handleSelectTab = (0, _react.useCallback)(tabName => { setSelectedFieldGroupName(tabName); }, []); var hasGroups = filterGroups.length > 1; (0, _react.useEffect)(() => { setSelectedFieldGroupName(defaultFieldGroupName); }, [type.fields]); (0, _react.useEffect)(() => { if (changesOpen && selectedFieldGroupName !== DEFAULT_FIELD_GROUP_NAME) { setSelectedFieldGroupName(DEFAULT_FIELD_GROUP_NAME); } }, [changesOpen, selectedFieldGroupName]); var handleFieldChange = _react.default.useCallback((fieldEvent, field) => { var event = fieldEvent.prefixAll(field.name); var patchesIncludesUnset = event.patches.some(patch => patch.type === 'unset'); // check if the result of the incoming patches will result in an empty object. // first apply all the patches to the current value to get the result. var result = // check if the patch includes an unset and bail out early if not patchesIncludesUnset && (0, _applyPatch.applyAll)(value || {}, // unset the `_type` when computing the result since its auto-populated. // see note below for `_key` event.append((0, _PatchEvent.unset)(['_type'])).patches); if (!isRoot) { // if the result has no keys left in it. // note: for arrays we retain empty objects so `_key` is not considered if (isRecord(result) && Object.keys(result).length === 0) { // then unset the whole object onChange === null || onChange === void 0 ? void 0 : onChange(_PatchEvent.default.from((0, _PatchEvent.unset)())); return; } event = event.prepend((0, _PatchEvent.setIfMissing)(type.name === 'object' ? {} : { _type: type.name })); if (value) { var valueTypeName = value && value._type; var schemaTypeName = type.name; if (valueTypeName && schemaTypeName === 'object') { // The value has a _type key, but the type name from schema is 'object', // but _type: 'object' is implicit so we should fix it by removing it event = event.prepend((0, _PatchEvent.unset)(['_type'])); } else if (schemaTypeName !== 'object' && valueTypeName !== schemaTypeName) { // There's a mismatch between schema type and the value _type // fix it by setting _type to type name defined in schema event = event.prepend((0, _PatchEvent.set)(schemaTypeName, ['_type'])); } } } onChange === null || onChange === void 0 ? void 0 : onChange(event); }, [isRoot, onChange, type.name, value]); var renderField = _react.default.useCallback((field, fieldLevel, index, isFieldsetReadOnly, fieldSetValues) => { var fieldValue = value === null || value === void 0 ? void 0 : value[field.name]; if (!filterField(type, field)) { return null; } return /*#__PURE__*/_react.default.createElement(_conditionalReadOnly.ConditionalReadOnlyField, { value: fieldSetValues, parent: value, readOnly: readOnly || isFieldsetReadOnly, key: "field-".concat(field.name) }, /*#__PURE__*/_react.default.createElement(_ObjectInputField.ObjectInputField, { parent: (0, _utils.hasConditionalFields)(field.type) ? value : undefined, field: field, value: fieldValue, onChange: handleFieldChange, onFocus: onFocus, onBlur: onBlur, compareValue: compareValue, markers: markers, focusPath: focusPath, level: fieldLevel, presence: presence, filterField: filterField, ref: !hasGroups && index === 0 ? forwardedRef : null })); }, [compareValue, filterField, focusPath, forwardedRef, handleFieldChange, hasGroups, markers, onBlur, onFocus, presence, readOnly, type, value]); var fieldGroupPredicate = (0, _react.useCallback)(fieldToCheck => !hasGroups || selectedFieldGroupName === DEFAULT_FIELD_GROUP_NAME || fieldToCheck.group && (0, _castArray2.default)(fieldToCheck.group).includes(selectedFieldGroupName), [selectedFieldGroupName, hasGroups]); var fieldsForTypeAndGroup = _react.default.useMemo(() => { return type.fields.filter(field => fieldGroupPredicate(field)); }, [fieldGroupPredicate, type.fields]); var fieldSetsForGroup = _react.default.useMemo(() => { return type.fieldsets.filter(fieldset => { if (selectedFieldGroupName === DEFAULT_FIELD_GROUP_NAME) { return true; } var hasFieldsetGroups = fieldGroupPredicate(fieldset); if (fieldset.single === true) { var fieldBelongsToGroup = fieldGroupPredicate(fieldset.field); return hasFieldsetGroups || fieldBelongsToGroup; } var hasGroupFields = hasFieldsetGroups || // eslint-disable-next-line max-nested-callbacks !fieldset.single && fieldset.fields.some(field => fieldGroupPredicate(field)); return hasGroupFields; }); }, [fieldGroupPredicate, selectedFieldGroupName, type.fieldsets]); var renderFields = (0, _react.useCallback)(() => { if (!type.fieldsets) { // this is a fallback for schema types that are not parsed to be objects, but still has jsonType == 'object' return (fieldsForTypeAndGroup || []).map((field, index) => renderField(field, level + 1, index)); } return fieldSetsForGroup.map((fieldset, fieldsetIndex) => { if (isSingleFieldset(fieldset)) { return renderField(fieldset.field, level + 1, fieldsetIndex, fieldset.readOnly); } var fieldSetValuesObject = {}; // eslint-disable-next-line max-nested-callbacks fieldset.fields.forEach(field => { if (value) { fieldSetValuesObject[field.name] = value[field.name]; } }); // @todo Maybe have a set? // eslint-disable-next-line max-nested-callbacks var fieldsetFields = fieldset.fields.filter(field => fieldGroupPredicate(field)); return /*#__PURE__*/_react.default.createElement(_ObjectFieldSet.ObjectFieldSet, { key: "fieldset-".concat(fieldset.name), "data-testid": "fieldset-".concat(fieldset.name), fieldset: _objectSpread(_objectSpread({}, fieldset), {}, { fields: fieldsetFields }), focusPath: focusPath, onFocus: onFocus, level: level + 1, presence: presence, markers: markers, fieldSetParent: value, fieldValues: fieldSetValuesObject }, () => // lazy render children fieldsetFields // eslint-disable-next-line max-nested-callbacks .map((field, fieldIndex) => renderField(field, level + 2, fieldsetIndex + fieldIndex, fieldset.readOnly, fieldSetValuesObject))); }); }, [type.fieldsets, fieldSetsForGroup, fieldsForTypeAndGroup, renderField, level, focusPath, onFocus, presence, markers, value, fieldGroupPredicate]); var renderUnknownFields = (0, _react.useCallback)(() => { if (!type.fields) { return null; } var knownFieldNames = type.fields.map(field => field.name); var unknownFields = Object.keys(value || {}).filter(key => !key.startsWith('_') && !knownFieldNames.includes(key)); if (unknownFields.length === 0) { return null; } return /*#__PURE__*/_react.default.createElement(_UnknownFields.UnknownFields, { fieldNames: unknownFields, value: value, onChange: onChange }); }, [onChange, type.fields, value]); var renderAllFields = (0, _react.useCallback)(() => { return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, renderFields(), renderUnknownFields()); }, [renderFields, renderUnknownFields]); var collapsibleOpts = (0, _utils.getCollapsedWithDefaults)(type.options, level); var _React$useState = _react.default.useState(collapsibleOpts.collapsed), _React$useState2 = _slicedToArray(_React$useState, 2), isCollapsed = _React$useState2[0], setCollapsed = _React$useState2[1]; var handleToggleFieldset = _react.default.useCallback(nextCollapsed => { if (nextCollapsed) { setCollapsed(true); // We can now put focus on the object value itself since it's collapsed onFocus([]); } else { onFocus([type.fields[0].name]); } }, [onFocus, type.fields]); _react.default.useEffect(() => { var hasFocusWithin = focusPath.length > 0; if (hasFocusWithin) { setCollapsed(false); // Restore to default field group if focus path changes and the root focus path is not // available in this field group if (hasGroups && selectedFieldGroupName !== DEFAULT_FIELD_GROUP_NAME && typeof (focusPath === null || focusPath === void 0 ? void 0 : focusPath[0]) === 'string' && (focusPath === null || focusPath === void 0 ? void 0 : focusPath[0]) !== _paths.FOCUS_TERMINATOR && !fieldGroupRootFocusPaths.includes(focusPath === null || focusPath === void 0 ? void 0 : focusPath[0])) { setSelectedFieldGroupName(DEFAULT_FIELD_GROUP_NAME); } } }, [focusPath]); _react.default.useEffect(() => { if (defaultFieldGroupName !== DEFAULT_FIELD_GROUP_NAME && selectedFieldGroupName !== defaultFieldGroupName) { setSelectedFieldGroupName(defaultFieldGroupName); } }, [defaultFieldGroupName]); _react.default.useEffect(() => { if (selectedFieldGroupName !== DEFAULT_FIELD_GROUP_NAME && !hasGroups) { setSelectedFieldGroupName(DEFAULT_FIELD_GROUP_NAME); } }, [hasGroups, selectedFieldGroupName]); var columns = type.options && type.options.columns; var renderFieldGroups = (0, _react.useCallback)(() => { if (!hasGroups) { return level === 0 ? /*#__PURE__*/_react.default.createElement(_ui.Grid, { columns: columns, gapX: 4, gapY: 5 }, renderAllFields()) : /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, renderAllFields()); } return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_ObjectInput.FieldGroupTabsWrapper, { $level: level, "data-testid": "field-groups" }, /*#__PURE__*/_react.default.createElement(_fieldGroups.FieldGroupTabs, { disabled: changesOpen, inputId: inputId, onClick: handleSelectTab, selectedName: selectedFieldGroupName, groups: filterGroups, ref: forwardedRef, shouldAutoFocus: level === 0 && (focusPath.length === 0 || focusPath[0] === _paths.FOCUS_TERMINATOR) })), level === 0 ? /*#__PURE__*/_react.default.createElement(_ui.Grid, { columns: columns, gapX: 4, gapY: 5, id: "".concat(inputId, "-field-group-fields") }, renderAllFields()) : /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, renderAllFields())); }, [type.groups, type.fields, columns, changesOpen, renderAllFields, level, selectedFieldGroupName, handleSelectTab]); if (level === 0) { // We don't want to render the fields wrapped in a fieldset if nesting level is 0 // (e.g. when the object input is used as the root element in a form or a dialog) return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, renderFieldGroups()); } return /*#__PURE__*/_react.default.createElement(_components.FormFieldSet, { ref: isCollapsed ? forwardedRef : null, level: level, title: type.title, description: type.description, columns: columns, collapsible: collapsibleOpts.collapsible, collapsed: isCollapsed, onToggle: handleToggleFieldset, __unstable_presence: isCollapsed ? presence : _empty.EMPTY_ARRAY, __unstable_markers: isCollapsed ? markers : _empty.EMPTY_ARRAY, __unstable_changeIndicator: false }, renderFieldGroups()); })); exports.ObjectInput = ObjectInput;