metadata-based-explorer1
Version:
Box UI Elements
661 lines (564 loc) • 24.6 kB
JavaScript
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (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 = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import isEqual from 'lodash/isEqual';
import cloneDeep from 'lodash/cloneDeep';
import Collapsible from '../../components/collapsible/Collapsible';
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, TEMPLATE_CUSTOM_PROPERTIES, JSON_PATCH_OP_REMOVE, JSON_PATCH_OP_ADD, JSON_PATCH_OP_REPLACE, JSON_PATCH_OP_TEST } from './constants';
import { isValidValue } from './fields/validateField';
import isHidden from './metadataUtil';
import { RESIN_TAG_TARGET } from '../../common/variables';
import './Instance.scss';
var createFieldKeyToTypeMap = function createFieldKeyToTypeMap() {
var fields = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
return fields.reduce(function (prev, _ref) {
var key = _ref.key,
type = _ref.type;
prev[key] = type;
return prev;
}, {});
};
var getValue = function getValue(data, key, type) {
var value = data[key];
switch (type) {
case FIELD_TYPE_FLOAT:
return parseFloat(value);
case FIELD_TYPE_INTEGER:
return parseInt(value, 10);
default:
return value;
}
};
var Instance =
/*#__PURE__*/
function (_React$PureComponent) {
_inherits(Instance, _React$PureComponent);
function Instance(props) {
var _this;
_classCallCheck(this, Instance);
_this = _possibleConstructorReturn(this, _getPrototypeOf(Instance).call(this, props));
_defineProperty(_assertThisInitialized(_this), "onCancel", function () {
var _this$props = _this.props,
id = _this$props.id,
onModification = _this$props.onModification;
_this.setState(_this.getState(_this.props)); // Callback to parent to tell that something is dirty
if (onModification) {
onModification(id, false);
}
});
_defineProperty(_assertThisInitialized(_this), "onConfirmRemove", function () {
_this.setState({
shouldConfirmRemove: true
});
});
_defineProperty(_assertThisInitialized(_this), "onConfirmCancel", function () {
_this.setState({
shouldConfirmRemove: false
});
});
_defineProperty(_assertThisInitialized(_this), "onRemove", function () {
if (!_this.isEditing()) {
return;
}
var _this$props2 = _this.props,
id = _this$props2.id,
onRemove = _this$props2.onRemove;
if (onRemove) {
onRemove(id);
_this.setState({
isBusy: true
});
}
});
_defineProperty(_assertThisInitialized(_this), "onSave", function () {
var _this$props3 = _this.props,
cascadePolicy = _this$props3.cascadePolicy,
originalData = _this$props3.data,
id = _this$props3.id,
isDirty = _this$props3.isDirty,
isCascadingPolicyApplicable = _this$props3.isCascadingPolicyApplicable,
onSave = _this$props3.onSave;
var _this$state = _this.state,
currentData = _this$state.data,
errors = _this$state.errors,
isCascadingEnabled = _this$state.isCascadingEnabled,
isCascadingOverwritten = _this$state.isCascadingOverwritten;
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));
});
_defineProperty(_assertThisInitialized(_this), "onFieldChange", function (key, value, type) {
var _this$state2 = _this.state,
data = _this$state2.data,
errors = _this$state2.errors; // Don't do anything if data is the same or not in edit mode
if (!_this.isEditing() || isEqual(data[key], value)) {
return;
}
var isValid = isValidValue(type, value);
var finalErrors = _objectSpread({}, errors);
var finalData = cloneDeep(data);
finalData[key] = value;
if (isValid) {
delete finalErrors[key];
} else {
finalErrors[key] = React.createElement(FormattedMessage, messages.invalidInput);
}
_this.setState({
data: finalData,
errors: finalErrors
}, function () {
_this.setDirty(type);
});
});
_defineProperty(_assertThisInitialized(_this), "onFieldRemove", function (key) {
if (!_this.isEditing()) {
return;
}
var _this$state3 = _this.state,
data = _this$state3.data,
errors = _this$state3.errors;
var finalData = cloneDeep(data);
var finalErrors = _objectSpread({}, errors);
delete finalData[key];
delete finalErrors[key];
_this.setState({
data: finalData,
errors: finalErrors
}, _this.setDirty);
});
_defineProperty(_assertThisInitialized(_this), "onCascadeToggle", function (value) {
var isCascadingPolicyApplicable = _this.props.isCascadingPolicyApplicable;
if (!isCascadingPolicyApplicable) {
return;
}
_this.setState({
isCascadingEnabled: value,
shouldShowCascadeOptions: value
}, _this.setDirty);
});
_defineProperty(_assertThisInitialized(_this), "onCascadeModeChange", function (value) {
var isCascadingPolicyApplicable = _this.props.isCascadingPolicyApplicable;
if (!isCascadingPolicyApplicable) {
return;
}
_this.setState({
isCascadingOverwritten: value
}, _this.setDirty);
});
_defineProperty(_assertThisInitialized(_this), "renderDeleteMessage", function (isFile, template) {
var message;
var isProperties = template.templateKey === TEMPLATE_CUSTOM_PROPERTIES;
if (isProperties) {
message = isFile ? 'fileMetadataRemoveCustomTemplateConfirm' : 'folderMetadataRemoveCustomTemplateConfirm';
} else {
message = isFile ? 'fileMetadataRemoveTemplateConfirm' : 'folderMetadataRemoveTemplateConfirm';
}
return React.createElement(FormattedMessage, _extends({}, messages[message], {
values: {
metadataName: template.displayName
}
}));
});
_defineProperty(_assertThisInitialized(_this), "setDirty", function (type) {
var _this$props4 = _this.props,
id = _this$props4.id,
isCascadingPolicyApplicable = _this$props4.isCascadingPolicyApplicable,
onModification = _this$props4.onModification;
var _this$state4 = _this.state,
data = _this$state4.data,
isCascadingEnabled = _this$state4.isCascadingEnabled,
isCascadingOverwritten = _this$state4.isCascadingOverwritten;
var hasDataChanged = !isEqual(data, _this.props.data);
var 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(_assertThisInitialized(_this), "collapsibleRef", React.createRef());
_defineProperty(_assertThisInitialized(_this), "toggleIsEditing", function () {
_this.setState(function (prevState) {
return {
isEditing: !prevState.isEditing
};
});
});
_defineProperty(_assertThisInitialized(_this), "renderEditButton", function () {
var isDirty = _this.props.isDirty;
var isBusy = _this.state.isBusy;
var canEdit = _this.canEdit();
var isEditing = _this.isEditing();
var editClassName = classNames('metadata-instance-editor-instance-edit', {
'metadata-instance-editor-instance-is-editing': isEditing
});
if (canEdit && !isDirty && !isBusy) {
return React.createElement(Tooltip, {
position: "top-left",
text: React.createElement(FormattedMessage, messages.metadataEditTooltip)
}, React.createElement(PlainButton, {
className: editClassName,
"data-resin-target": "metadata-instanceedit",
onClick: _this.toggleIsEditing,
type: "button"
}, React.createElement(IconEdit, null)));
}
return null;
});
_this.state = _this.getState(props);
_this.fieldKeyToTypeMap = createFieldKeyToTypeMap(props.template.fields);
return _this;
}
_createClass(Instance, [{
key: "componentWillReceiveProps",
value: function componentWillReceiveProps(nextProps) {
var hasError = nextProps.hasError,
isDirty = nextProps.isDirty;
var isEditing = this.state.isEditing; // This only handles cases when an error occurred
// or when the dirty state of the instance has changed.
// The dirty state can change when either
// the metadata was saved OR when the metadata manually
// reverted to its original state.
if (hasError) {
// 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 (this.props.isDirty && !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
});
}
}
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate() {
var element = this.collapsibleRef.current;
if (element && this.state.shouldConfirmRemove) {
scrollIntoView(element, {
block: 'start',
behavior: 'smooth'
});
}
}
/**
* Undo any changes made
*
* @return {void}
*/
}, {
key: "getState",
/**
* Returns the state from props
*
* @return {Object} - react state
*/
value: function 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
*/
}, {
key: "getTitle",
value: function getTitle() {
var _this$props5 = this.props,
_this$props5$cascadeP = _this$props5.cascadePolicy,
cascadePolicy = _this$props5$cascadeP === void 0 ? {} : _this$props5$cascadeP,
hasError = _this$props5.hasError,
isCascadingPolicyApplicable = _this$props5.isCascadingPolicyApplicable,
template = _this$props5.template;
var isProperties = template.templateKey === TEMPLATE_CUSTOM_PROPERTIES;
var type = isCascadingPolicyApplicable && cascadePolicy.id ? 'cascade' : 'default';
return React.createElement("span", {
className: "metadata-instance-editor-instance-title"
}, React.createElement(IconMetadataColored, {
type: type
}), React.createElement("span", {
className: classNames('metadata-instance-editor-instance-title-text', {
'metadata-instance-editor-instance-has-error': hasError
})
}, isProperties ? React.createElement(FormattedMessage, messages.customTitle) : template.displayName), hasError && React.createElement(IconAlertCircle, {
color: bdlWatermelonRed
}));
}
/**
* Render the correct delete message to show based on custom metadata and file/folder metadata
*/
}, {
key: "getConfirmationMessage",
/**
* Get the delete confirmation message base on the template key
*/
value: function getConfirmationMessage() {
var _this$props6 = this.props,
template = _this$props6.template,
isCascadingPolicyApplicable = _this$props6.isCascadingPolicyApplicable;
var isFile = !isCascadingPolicyApplicable;
return this.renderDeleteMessage(isFile, template);
}
/**
* Evaluates if the metadata was changed or cascading policy
* altered or enabled.
*
* @return {void}
*/
}, {
key: "isCascadingEnabled",
/**
* 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
*/
value: function isCascadingEnabled(props) {
if (props.cascadePolicy) {
return !!props.cascadePolicy.id;
}
return false;
}
/**
* Toggles the edit mode
*
* @private
* @return {void}
*/
}, {
key: "createJSONPatch",
/**
* 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
*/
value: function createJSONPatch(currentData, originalData) {
var _this2 = this;
var ops = [];
var 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(function (key) {
var type = _this2.fieldKeyToTypeMap[key];
var originalValue = getValue(originalData, key, type);
var path = "/".concat(key);
if (Object.prototype.hasOwnProperty.call(data, key)) {
var 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: path,
value: originalValue
});
ops.push({
op: JSON_PATCH_OP_REPLACE,
path: path,
value: value
});
}
} else {
// Key was removed
// Add a test OP for removes
ops.push({
op: JSON_PATCH_OP_TEST,
path: path,
value: originalValue
});
ops.push({
op: JSON_PATCH_OP_REMOVE,
path: path
});
}
delete data[key];
}); // Iterate over the remaining keys that are new.
Object.keys(data).forEach(function (key) {
var type = _this2.fieldKeyToTypeMap[key];
var value = getValue(data, key, type);
ops.push({
op: JSON_PATCH_OP_ADD,
path: "/".concat(key),
value: value
});
});
return ops;
}
/**
* Utility function to determine if instance is editable
*
* @return {boolean} true if editable
*/
}, {
key: "canEdit",
value: function canEdit() {
var _this$props7 = this.props,
canEdit = _this$props7.canEdit,
onModification = _this$props7.onModification,
onRemove = _this$props7.onRemove,
onSave = _this$props7.onSave;
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
*/
}, {
key: "isEditing",
value: function isEditing() {
var isEditing = this.state.isEditing;
return this.canEdit() && isEditing;
}
}, {
key: "render",
value: function render() {
var _this$props8 = this.props,
_this$props8$cascadeP = _this$props8.cascadePolicy,
cascadePolicy = _this$props8$cascadeP === void 0 ? {} : _this$props8$cascadeP,
isDirty = _this$props8.isDirty,
isCascadingPolicyApplicable = _this$props8.isCascadingPolicyApplicable,
isOpen = _this$props8.isOpen,
template = _this$props8.template;
var _template$fields = template.fields,
fields = _template$fields === void 0 ? [] : _template$fields;
var _this$state5 = this.state,
data = _this$state5.data,
errors = _this$state5.errors,
isBusy = _this$state5.isBusy,
isCascadingEnabled = _this$state5.isCascadingEnabled,
shouldConfirmRemove = _this$state5.shouldConfirmRemove,
shouldShowCascadeOptions = _this$state5.shouldShowCascadeOptions,
isCascadingOverwritten = _this$state5.isCascadingOverwritten;
var isProperties = template.templateKey === TEMPLATE_CUSTOM_PROPERTIES;
var isEditing = this.isEditing();
if (!template || isHidden(template)) {
return null;
} // Animate short and tall cards at consistent speeds.
var animationDuration = (fields.length + 1) * 50;
return React.createElement("div", {
ref: this.collapsibleRef
}, React.createElement(Collapsible, {
animationDuration: animationDuration,
buttonProps: _defineProperty({}, RESIN_TAG_TARGET, 'metadata-card'),
hasStickyHeader: true,
headerActionItems: this.renderEditButton(),
isBordered: true,
isOpen: isOpen,
title: this.getTitle()
}, shouldConfirmRemove && React.createElement(LoadingIndicatorWrapper, {
isLoading: isBusy
}, React.createElement(MetadataInstanceConfirmDialog, {
confirmationMessage: this.getConfirmationMessage(),
onCancel: this.onConfirmCancel,
onConfirm: this.onRemove
})), !shouldConfirmRemove && React.createElement(LoadingIndicatorWrapper, {
isLoading: isBusy
}, React.createElement("div", {
className: "metadata-instance-editor-instance"
}, isCascadingPolicyApplicable && React.createElement(CascadePolicy, {
canEdit: isEditing && !!cascadePolicy.canEdit,
isCascadingEnabled: isCascadingEnabled,
isCascadingOverwritten: isCascadingOverwritten,
isCustomMetadata: isProperties,
onCascadeModeChange: this.onCascadeModeChange,
onCascadeToggle: this.onCascadeToggle,
shouldShowCascadeOptions: shouldShowCascadeOptions
}), isProperties ? React.createElement(CustomInstance, {
canEdit: isEditing,
data: data,
onFieldChange: this.onFieldChange,
onFieldRemove: this.onFieldRemove
}) : React.createElement(TemplatedInstance, {
canEdit: isEditing,
data: data,
errors: errors,
onFieldChange: this.onFieldChange,
onFieldRemove: this.onFieldRemove,
template: template
})), isEditing && React.createElement(Footer, {
onCancel: this.onCancel,
onRemove: this.onConfirmRemove,
onSave: isDirty ? this.onSave : undefined
}))));
}
}]);
return Instance;
}(React.PureComponent);
_defineProperty(Instance, "defaultProps", {
data: {},
isDirty: false
});
export default Instance;