UNPKG

@orfeas126/box-ui-elements

Version:
624 lines (614 loc) 20.6 kB
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } import * as React from 'react'; import { injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import isEqual from 'lodash/isEqual'; import cloneDeep from 'lodash/cloneDeep'; import noop from 'lodash/noop'; import Collapsible from '../../components/collapsible/Collapsible'; import Form from '../../components/form-elements/form/Form'; import LoadingIndicatorWrapper from '../../components/loading-indicator/LoadingIndicatorWrapper'; import PlainButton from '../../components/plain-button/PlainButton'; import Tooltip from '../../components/tooltip'; import IconMetadataColored from '../../icons/general/IconMetadataColored'; import IconAlertCircle from '../../icons/general/IconAlertCircle'; import IconEdit from '../../icons/general/IconEdit'; import { bdlWatermelonRed } from '../../styles/variables'; import { scrollIntoView } from '../../utils/dom'; import CascadePolicy from './CascadePolicy'; import TemplatedInstance from './TemplatedInstance'; import CustomInstance from './CustomInstance'; import MetadataInstanceConfirmDialog from './MetadataInstanceConfirmDialog'; import Footer from './Footer'; import messages from './messages'; import { FIELD_TYPE_FLOAT, FIELD_TYPE_INTEGER } from '../metadata-instance-fields/constants'; import TEMPLATE_CUSTOM_PROPERTIES from './constants'; import { JSON_PATCH_OP_REMOVE, JSON_PATCH_OP_ADD, JSON_PATCH_OP_REPLACE, JSON_PATCH_OP_TEST } from '../../common/constants'; import { isValidValue } from '../metadata-instance-fields/validateMetadataField'; import { isHidden } from './metadataUtil'; import { RESIN_TAG_TARGET } from '../../common/variables'; import './Instance.scss'; const createFieldKeyToTypeMap = (fields = []) => fields.reduce((prev, { key, type }) => { prev[key] = type; return prev; }, {}); const getValue = (data, key, type) => { const value = data[key]; switch (type) { case FIELD_TYPE_FLOAT: return parseFloat(value); case FIELD_TYPE_INTEGER: return parseInt(value, 10); default: return value; } }; class Instance extends React.PureComponent { constructor(props) { super(props); /** * Undo any changes made * * @return {void} */ _defineProperty(this, "onCancel", () => { const { id, onModification } = this.props; this.setState(this.getState(this.props)); // Callback to parent to tell that something is dirty if (onModification) { onModification(id, false); } }); /** * Allows a user to confirm metadata instance removal * * @return {void} */ _defineProperty(this, "onConfirmRemove", () => { this.setState({ shouldConfirmRemove: true }); }); /** * Cancel the remove instance attempt * * @return {void} */ _defineProperty(this, "onConfirmCancel", () => { this.setState({ shouldConfirmRemove: false }); }); /** * Removes an instance * * @return {void} */ _defineProperty(this, "onRemove", () => { if (!this.isEditing()) { return; } const { id, onRemove } = this.props; if (onRemove) { onRemove(id); this.setState({ isBusy: true }); } }); /** * Saves instance data * * @return {void} */ _defineProperty(this, "onSave", () => { const { cascadePolicy, data: originalData, id, isDirty, isCascadingPolicyApplicable, onSave } = this.props; const { data: currentData, errors, isCascadingEnabled, isCascadingOverwritten } = this.state; if (!this.isEditing() || !isDirty || !onSave || Object.keys(errors).length) { return; } this.setState({ isBusy: true, isEditing: false, shouldShowCascadeOptions: false }); onSave(id, this.createJSONPatch(currentData, originalData), isCascadingPolicyApplicable ? { canEdit: cascadePolicy ? cascadePolicy.canEdit : false, id: cascadePolicy ? cascadePolicy.id : undefined, isEnabled: isCascadingEnabled, overwrite: isCascadingOverwritten } : undefined, cloneDeep(currentData)); }); /** * Updates a key value in the instance data * * @param {string} key - key to update * @param {FieldValue} value - value to update * @param {string} type - type of field * @return {void} */ _defineProperty(this, "onFieldChange", (key, value, type) => { const { data, errors } = this.state; // Don't do anything if data is the same or not in edit mode if (!this.isEditing() || isEqual(data[key], value)) { return; } const isValid = isValidValue(type, value); const finalErrors = _objectSpread({}, errors); const finalData = cloneDeep(data); finalData[key] = value; if (isValid) { delete finalErrors[key]; } else { finalErrors[key] = /*#__PURE__*/React.createElement(FormattedMessage, messages.invalidInput); } this.setState({ data: finalData, errors: finalErrors }, () => { this.setDirty(type); }); }); /** * Removes a key from instance data * * @param {string} key - key to remove * @return {void} */ _defineProperty(this, "onFieldRemove", key => { if (!this.isEditing()) { return; } const { data, errors } = this.state; const finalData = cloneDeep(data); const finalErrors = _objectSpread({}, errors); delete finalData[key]; delete finalErrors[key]; this.setState({ data: finalData, errors: finalErrors }, this.setDirty); }); /** * Toggle cascading policy * * @param {boolean} value - true when turned on * @return {void} */ _defineProperty(this, "onCascadeToggle", value => { const { isCascadingPolicyApplicable } = this.props; if (!isCascadingPolicyApplicable) { return; } this.setState({ isCascadingEnabled: value, shouldShowCascadeOptions: value }, this.setDirty); }); /** * Changes the cascade mode. * isCascadingOverwritten is slways false to start off. * * @param {boolean} value - true when overwrite policy is chosen * @return {void} */ _defineProperty(this, "onCascadeModeChange", value => { const { isCascadingPolicyApplicable } = this.props; if (!isCascadingPolicyApplicable) { return; } this.setState({ isCascadingOverwritten: value }, this.setDirty); }); /** * Render the correct delete message to show based on custom metadata and file/folder metadata */ _defineProperty(this, "renderDeleteMessage", (isFile, template) => { let message; const isProperties = template.templateKey === TEMPLATE_CUSTOM_PROPERTIES; if (isProperties) { message = isFile ? 'fileMetadataRemoveCustomTemplateConfirm' : 'folderMetadataRemoveCustomTemplateConfirm'; } else { message = isFile ? 'fileMetadataRemoveTemplateConfirm' : 'folderMetadataRemoveTemplateConfirm'; } return /*#__PURE__*/React.createElement(FormattedMessage, _extends({}, messages[message], { values: { metadataName: template.displayName } })); }); /** * Evaluates if the metadata was changed or cascading policy * altered or enabled. * * @return {void} */ _defineProperty(this, "setDirty", type => { const { id, isCascadingPolicyApplicable, onModification } = this.props; const { data, isCascadingEnabled, isCascadingOverwritten } = this.state; const hasDataChanged = !isEqual(data, this.props.data); let hasCascadingChanged = false; if (isCascadingPolicyApplicable) { // isCascadingOverwritten always starts out as false, so true signifies a change hasCascadingChanged = isCascadingOverwritten || isCascadingEnabled !== this.isCascadingEnabled(this.props); } // Callback to parent to tell that something is dirty if (onModification) { onModification(id, hasDataChanged || hasCascadingChanged, type); } }); _defineProperty(this, "collapsibleRef", /*#__PURE__*/React.createRef()); /** * Toggles the edit mode * * @private * @return {void} */ _defineProperty(this, "toggleIsEditing", () => { this.setState(prevState => ({ isEditing: !prevState.isEditing })); }); _defineProperty(this, "renderEditButton", () => { const { intl, isDirty } = this.props; const { isBusy } = this.state; const canEdit = this.canEdit(); const isEditing = this.isEditing(); const editClassName = classNames('metadata-instance-editor-instance-edit', { 'metadata-instance-editor-instance-is-editing': isEditing }); if (canEdit && !isDirty && !isBusy) { const metadataLabelEditText = intl.formatMessage(messages.metadataEditTooltip); return /*#__PURE__*/React.createElement(Tooltip, { position: "top-left", text: metadataLabelEditText }, /*#__PURE__*/React.createElement(PlainButton, { "aria-label": metadataLabelEditText, "aria-pressed": isEditing, className: editClassName, "data-resin-target": "metadata-instanceedit", onClick: this.toggleIsEditing, type: "button" }, /*#__PURE__*/React.createElement(IconEdit, null))); } return null; }); this.state = this.getState(props); this.fieldKeyToTypeMap = createFieldKeyToTypeMap(props.template.fields); } componentDidUpdate({ hasError: prevHasError, isDirty: prevIsDirty }, prevState) { const currentElement = this.collapsibleRef.current; const { hasError, isDirty } = this.props; const { isEditing } = prevState; if (currentElement && this.state.shouldConfirmRemove) { scrollIntoView(currentElement, { block: 'start', behavior: 'smooth' }); } if (hasError && hasError !== prevHasError) { // If hasError is true, which means an error occurred while // doing a network operation and hence hide the busy indicator // Saving also disables isEditing, so need to enable that back. // isDirty remains as it was before. this.setState({ isBusy: false, isEditing: true }); } else if (prevIsDirty && !isDirty) { // If the form was dirty and now its not dirty // we know a successful save may have happened. // We don't modify isEditing here because we maintain the // prior state for that. If we came here from a save // success then save already disabled isEditing. if (isEditing) { // We are still editing so don't reset it this.setState({ isBusy: false }); } else { // For a successfull save we reset cascading overwrite radio this.setState({ isBusy: false, isCascadingOverwritten: false }); } } } /** * Returns the state from props * * @return {Object} - react state */ getState(props) { return { data: cloneDeep(props.data), errors: {}, isBusy: false, isCascadingEnabled: this.isCascadingEnabled(props), isCascadingOverwritten: false, isEditing: false, shouldConfirmRemove: false, shouldShowCascadeOptions: false }; } /** * Returns the card title with possible error mark * * @return {Object} - react title element */ getTitle() { const { cascadePolicy = {}, hasError, isCascadingPolicyApplicable, template } = this.props; const isProperties = template.templateKey === TEMPLATE_CUSTOM_PROPERTIES; const type = isCascadingPolicyApplicable && cascadePolicy.id ? 'cascade' : 'default'; return /*#__PURE__*/React.createElement("span", { className: "metadata-instance-editor-instance-title" }, /*#__PURE__*/React.createElement(IconMetadataColored, { type: type }), /*#__PURE__*/React.createElement("span", { className: classNames('metadata-instance-editor-instance-title-text', { 'metadata-instance-editor-instance-has-error': hasError }) }, isProperties ? /*#__PURE__*/React.createElement(FormattedMessage, messages.customTitle) : template.displayName), hasError && /*#__PURE__*/React.createElement(IconAlertCircle, { color: bdlWatermelonRed })); } /** * Get the delete confirmation message base on the template key */ getConfirmationMessage() { const { template, isCascadingPolicyApplicable } = this.props; const isFile = !isCascadingPolicyApplicable; return this.renderDeleteMessage(isFile, template); } /** * Determines if cascading policy is enabled based on * whether it has an id or not. * * @param {Object} props - component props * @return {boolean} true if cascading policy is enabled */ isCascadingEnabled(props) { if (props.cascadePolicy) { return !!props.cascadePolicy.id; } return false; } /** * Creates JSON Patch operations from the passed in * data while comparing it to the original data from props. * * Only diffs at the root level and primitives. * * @param {*} currentData - the latest changes by the user * @param {*} originalData - the original values * @return {Array} - JSON patch operations */ createJSONPatch(currentData, originalData) { const ops = []; const data = cloneDeep(currentData); // clone the data for mutation // Iterate over the original data and find keys that have changed. // Also remove them from the data object to only leave new keys. Object.keys(originalData).forEach(key => { const type = this.fieldKeyToTypeMap[key]; const originalValue = getValue(originalData, key, type); const path = `/${key}`; if (Object.prototype.hasOwnProperty.call(data, key)) { const value = getValue(data, key, type); // Only register changed data if (!isEqual(value, originalValue)) { // Add a test OP for each replaces ops.push({ op: JSON_PATCH_OP_TEST, path, value: originalValue }); ops.push({ op: JSON_PATCH_OP_REPLACE, path, value }); } } else { // Key was removed // Add a test OP for removes ops.push({ op: JSON_PATCH_OP_TEST, path, value: originalValue }); ops.push({ op: JSON_PATCH_OP_REMOVE, path }); } delete data[key]; }); // Iterate over the remaining keys that are new. Object.keys(data).forEach(key => { const type = this.fieldKeyToTypeMap[key]; const value = getValue(data, key, type); ops.push({ op: JSON_PATCH_OP_ADD, path: `/${key}`, value }); }); return ops; } /** * Utility function to determine if instance is editable * * @return {boolean} true if editable */ canEdit() { const { canEdit, onModification, onRemove, onSave } = this.props; return canEdit && typeof onRemove === 'function' && typeof onSave === 'function' && typeof onModification === 'function'; } /** * Utility function to determine if instance is in edit mode * * @return {boolean} true if editing */ isEditing() { const { isEditing } = this.state; return this.canEdit() && isEditing; } render() { const { cascadePolicy = {}, isDirty, isCascadingPolicyApplicable, isOpen, template } = this.props; const { fields = [] } = template; const { data, errors, isBusy, isCascadingEnabled, shouldConfirmRemove, shouldShowCascadeOptions, isCascadingOverwritten } = this.state; const isProperties = template.templateKey === TEMPLATE_CUSTOM_PROPERTIES; const isEditing = this.isEditing(); if (!template || isHidden(template)) { return null; } // Animate short and tall cards at consistent speeds. const animationDuration = (fields.length + 1) * 50; return /*#__PURE__*/React.createElement("div", { ref: this.collapsibleRef }, /*#__PURE__*/React.createElement(Collapsible, { animationDuration: animationDuration, buttonProps: { [RESIN_TAG_TARGET]: 'metadata-card' }, hasStickyHeader: true, headerActionItems: this.renderEditButton(), isBordered: true, isOpen: isOpen, title: this.getTitle() }, shouldConfirmRemove && /*#__PURE__*/React.createElement(LoadingIndicatorWrapper, { isLoading: isBusy }, /*#__PURE__*/React.createElement(MetadataInstanceConfirmDialog, { confirmationMessage: this.getConfirmationMessage(), onCancel: this.onConfirmCancel, onConfirm: this.onRemove })), !shouldConfirmRemove && /*#__PURE__*/React.createElement(LoadingIndicatorWrapper, { isLoading: isBusy }, /*#__PURE__*/React.createElement(Form, { onValidSubmit: isDirty ? this.onSave : noop }, /*#__PURE__*/React.createElement("div", { className: "metadata-instance-editor-instance" }, isCascadingPolicyApplicable && /*#__PURE__*/React.createElement(CascadePolicy, { canEdit: isEditing && !!cascadePolicy.canEdit, isCascadingEnabled: isCascadingEnabled, isCascadingOverwritten: isCascadingOverwritten, isCustomMetadata: isProperties, onCascadeModeChange: this.onCascadeModeChange, onCascadeToggle: this.onCascadeToggle, shouldShowCascadeOptions: shouldShowCascadeOptions }), isProperties ? /*#__PURE__*/React.createElement(CustomInstance, { canEdit: isEditing, data: data, onFieldChange: this.onFieldChange, onFieldRemove: this.onFieldRemove }) : /*#__PURE__*/React.createElement(TemplatedInstance, { canEdit: isEditing, data: data, errors: errors, onFieldChange: this.onFieldChange, onFieldRemove: this.onFieldRemove, template: template })), isEditing && /*#__PURE__*/React.createElement(Footer, { onCancel: this.onCancel, onRemove: this.onConfirmRemove, showSave: isDirty }))))); } } _defineProperty(Instance, "defaultProps", { data: {}, isDirty: false }); export { Instance as InstanceBase }; export default injectIntl(Instance); //# sourceMappingURL=Instance.js.map