cspace-ui
Version: 
CollectionSpace user interface for browsers
724 lines (623 loc) • 19.3 kB
JSX
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Prompt } from 'react-router';
import Immutable from 'immutable';
import get from 'lodash/get';
import Dock from '../sections/Dock';
import RecordButtonBar from './RecordButtonBar';
import RecordHeader from './RecordHeader';
import ConfirmRecordNavigationModal from './ConfirmRecordNavigationModal';
import ConfirmRecordDeleteModal from './ConfirmRecordDeleteModal';
import LockRecordModal from './LockRecordModal';
import HierarchyReparentNotifier from './HierarchyReparentNotifier';
import RecordFormContainer from '../../containers/record/RecordFormContainer';
import {
  canCreate, canDelete, canUpdate, canSoftDelete,
} from '../../helpers/permissionHelpers';
import { isRecordDeprecated, isRecordImmutable } from '../../helpers/recordDataHelpers';
import { isLocked } from '../../helpers/workflowStateHelpers';
import styles from '../../../styles/cspace-ui/RecordEditor.css';
import {
  MODAL_CONFIRM_RECORD_DELETE,
  MODAL_CONFIRM_RECORD_NAVIGATION,
  MODAL_LOCK_RECORD,
} from '../../constants/modalNames';
const propTypes = {
  config: PropTypes.shape({
    recordTypes: PropTypes.object,
    termDeprecationEnabled: PropTypes.bool,
  }),
  recordType: PropTypes.string.isRequired,
  vocabulary: PropTypes.string,
  csid: PropTypes.string,
  cloneCsid: PropTypes.string,
  data: PropTypes.instanceOf(Immutable.Map),
  dockTop: PropTypes.number,
  formName: PropTypes.string,
  perms: PropTypes.instanceOf(Immutable.Map),
  roleNames: PropTypes.instanceOf(Immutable.List),
  subrecordData: PropTypes.instanceOf(Immutable.Map),
  validationErrors: PropTypes.instanceOf(Immutable.Map),
  vocabularyWorkflowState: PropTypes.string,
  isModified: PropTypes.bool,
  isReadPending: PropTypes.bool,
  isSavePending: PropTypes.bool,
  isSidebarOpen: PropTypes.bool,
  isHardDelete: PropTypes.bool,
  // The workflow state of the related subject (aka primary) record when we're in a secondary tab.
  relatedSubjectWorkflowState: PropTypes.string,
  openModalName: PropTypes.string,
  checkDeletable: PropTypes.func,
  checkForRelations: PropTypes.func,
  checkForUses: PropTypes.func,
  checkForRoleUses: PropTypes.func,
  createNewRecord: PropTypes.func,
  readRecord: PropTypes.func,
  onRecordCreated: PropTypes.func,
  onRecordSaved: PropTypes.func,
  onSaveCancelled: PropTypes.func,
  closeModal: PropTypes.func,
  openModal: PropTypes.func,
  deleteRecord: PropTypes.func,
  save: PropTypes.func,
  saveWithTransition: PropTypes.func,
  revert: PropTypes.func,
  clone: PropTypes.func,
  transitionRecord: PropTypes.func,
  removeNotification: PropTypes.func,
  removeValidationNotification: PropTypes.func,
  setForm: PropTypes.func,
  validateRecordData: PropTypes.func,
  onRecordReadComplete: PropTypes.func,
  onRecordDeleted: PropTypes.func,
  onRecordTransitioned: PropTypes.func,
  onRunButtonClick: PropTypes.func,
};
const defaultProps = {
  data: Immutable.Map(),
  isSidebarOpen: true,
};
export default class RecordEditor extends Component {
  constructor() {
    super();
    // Confirm delete modal button handlers.
    this.handleConfirmDeleteButtonClick = this.handleConfirmDeleteButtonClick.bind(this);
    // Confirm navigation modal button handlers.
    this.handleConfirmNavigationSaveButtonClick = (
      this.handleConfirmNavigationSaveButtonClick.bind(this)
    );
    this.handleConfirmNavigationRevertButtonClick = (
      this.handleConfirmNavigationRevertButtonClick.bind(this)
    );
    // Lock modal button handlers.
    this.handleSaveOnlyButtonClick = this.handleSaveOnlyButtonClick.bind(this);
    this.handleSaveLockButtonClick = this.handleSaveLockButtonClick.bind(this);
    // Shared modal handlers.
    this.handleModalCancelButtonClick = this.handleModalCancelButtonClick.bind(this);
    // Button bar handlers.
    this.handleDeprecateButtonClick = this.handleDeprecateButtonClick.bind(this);
    this.handleSaveButtonClick = this.handleSaveButtonClick.bind(this);
    this.handleSaveButtonErrorBadgeClick = this.handleSaveButtonErrorBadgeClick.bind(this);
    this.handleRevertButtonClick = this.handleRevertButtonClick.bind(this);
    this.handleCloneButtonClick = this.handleCloneButtonClick.bind(this);
    this.handleDeleteButtonClick = this.handleDeleteButtonClick.bind(this);
    this.handleRecordFormSelectorCommit = this.handleRecordFormSelectorCommit.bind(this);
    this.handleUndeprecateButtonClick = this.handleUndeprecateButtonClick.bind(this);
  }
  componentDidMount() {
    this.initRecord();
  }
  componentDidUpdate(prevProps) {
    const {
      recordType,
      vocabulary,
      csid,
      cloneCsid,
      data,
    } = this.props;
    const {
      recordType: prevRecordType,
      vocabulary: prevVocabulary,
      csid: prevCsid,
      cloneCsid: prevCloneCsid,
      data: prevData,
    } = prevProps;
    if (
      recordType !== prevRecordType
      || vocabulary !== prevVocabulary
      || csid !== prevCsid
      || cloneCsid !== prevCloneCsid
      // DRYD-859: Re-init when data is reset but other props stay the same.
      || (prevData.size > 0 && data.size === 0)
    ) {
      this.initRecord();
    }
  }
  componentWillUnmount() {
    const {
      removeNotification,
      removeValidationNotification,
    } = this.props;
    if (removeValidationNotification) {
      removeValidationNotification();
    }
    if (removeNotification) {
      removeNotification(HierarchyReparentNotifier.notificationID);
    }
  }
  handleModalCancelButtonClick(event) {
    const {
      closeModal,
      onSaveCancelled,
    } = this.props;
    if (closeModal) {
      event.stopPropagation();
      closeModal(false);
    }
    if (onSaveCancelled) {
      onSaveCancelled();
    }
  }
  handleUndeprecateButtonClick() {
    const {
      transitionRecord,
      onRecordTransitioned,
    } = this.props;
    const transitionName = 'undeprecate';
    if (transitionRecord) {
      transitionRecord(transitionName)
        .then(() => {
          if (onRecordTransitioned) {
            onRecordTransitioned(transitionName);
          }
        });
    }
  }
  handleDeprecateButtonClick() {
    const {
      transitionRecord,
      onRecordTransitioned,
    } = this.props;
    const transitionName = 'deprecate';
    if (transitionRecord) {
      transitionRecord(transitionName)
        .then(() => {
          if (onRecordTransitioned) {
            onRecordTransitioned(transitionName);
          }
        });
    }
  }
  handleConfirmDeleteButtonClick() {
    this.delete();
  }
  handleConfirmNavigationSaveButtonClick() {
    const {
      closeModal,
      onRecordCreated,
    } = this.props;
    // Wrap the onRecordCreated callback in a function that sets isNavigating to true. This lets
    // the callback know that we're already navigating away, so it should not do any navigation
    // of its own.
    const callback = onRecordCreated
      ? (newRecordCsid) => { onRecordCreated(newRecordCsid, true); }
      : undefined;
    const saveCalled = this.save(callback);
    if (saveCalled && closeModal) {
      closeModal(true);
    }
  }
  handleConfirmNavigationRevertButtonClick() {
    const {
      closeModal,
      revert,
    } = this.props;
    if (revert) {
      revert();
    }
    if (closeModal) {
      closeModal(true);
    }
  }
  handleCloneButtonClick() {
    const {
      clone,
      csid,
    } = this.props;
    if (clone) {
      clone(csid);
    }
  }
  handleDeleteButtonClick() {
    const {
      openModal,
    } = this.props;
    if (openModal) {
      openModal(MODAL_CONFIRM_RECORD_DELETE);
    }
  }
  handleRevertButtonClick() {
    const {
      revert,
    } = this.props;
    if (revert) {
      revert();
    }
  }
  handleSaveButtonClick() {
    const {
      onRecordCreated,
    } = this.props;
    this.save(onRecordCreated);
  }
  handleSaveButtonErrorBadgeClick() {
    const {
      validateRecordData,
    } = this.props;
    if (validateRecordData) {
      validateRecordData();
    }
  }
  handleSaveOnlyButtonClick() {
    const {
      save,
      closeModal,
      onRecordCreated,
    } = this.props;
    if (save) {
      save(onRecordCreated)
        .then(() => {
          if (closeModal) {
            closeModal(true);
          }
        });
    }
  }
  handleSaveLockButtonClick() {
    const {
      saveWithTransition,
      closeModal,
      onRecordCreated,
    } = this.props;
    if (saveWithTransition) {
      saveWithTransition('lock', onRecordCreated)
        .then(() => {
          if (closeModal) {
            closeModal(true);
          }
        });
    }
  }
  handleRecordFormSelectorCommit(path, value) {
    const {
      setForm,
    } = this.props;
    if (setForm) {
      setForm(value);
    }
  }
  initRecord() {
    const {
      csid,
      cloneCsid,
      createNewRecord,
      readRecord,
      removeValidationNotification,
      onRecordReadComplete,
    } = this.props;
    if (removeValidationNotification) {
      removeValidationNotification();
    }
    if (csid) {
      if (readRecord) {
        readRecord().then(() => {
          if (onRecordReadComplete) {
            onRecordReadComplete();
          }
        });
      }
    } else if (createNewRecord) {
      createNewRecord(cloneCsid);
    }
  }
  save(onRecordCreated) {
    const {
      config,
      data,
      recordType,
      openModal,
      save,
      saveWithTransition,
      onRecordSaved,
    } = this.props;
    const recordTypeConfig = config.recordTypes[recordType];
    const { lockOnSave } = recordTypeConfig;
    if (lockOnSave === 'prompt' && openModal) {
      openModal(MODAL_LOCK_RECORD);
      return false;
    }
    let savePromise;
    if (lockOnSave === true && saveWithTransition) {
      savePromise = saveWithTransition('lock', onRecordCreated);
    } else if (save) {
      savePromise = save(onRecordCreated);
    }
    if (savePromise) {
      savePromise.then(() => {
        if (recordTypeConfig.onRecordSaved) {
          // TODO: Pass in the post-save data (which could have been modified due to services layer
          // event handlers) instead of the pre-save data. The save action would need to resolve
          // with the data instead of a csid.
          recordTypeConfig.onRecordSaved({ data, recordEditor: this });
        }
        if (onRecordSaved) {
          onRecordSaved();
        }
      });
    }
    return true;
  }
  delete() {
    const {
      closeModal,
      isHardDelete,
      deleteRecord,
      onRecordDeleted,
      transitionRecord,
      onRecordTransitioned,
    } = this.props;
    if (isHardDelete) {
      if (deleteRecord) {
        deleteRecord()
          .then(() => {
            if (closeModal) {
              closeModal(true);
            }
            if (onRecordDeleted) {
              onRecordDeleted();
            }
          });
      }
    } else {
      const transitionName = 'delete';
      if (transitionRecord) {
        transitionRecord(transitionName)
          .then(() => {
            if (closeModal) {
              closeModal(true);
            }
            if (onRecordTransitioned) {
              onRecordTransitioned(transitionName);
            }
          });
      }
    }
  }
  renderConfirmNavigationModal() {
    const {
      isModified,
      isSavePending,
      openModalName,
      validationErrors,
    } = this.props;
    return (
      <ConfirmRecordNavigationModal
        isOpen={openModalName === MODAL_CONFIRM_RECORD_NAVIGATION}
        isModified={isModified}
        isSavePending={isSavePending}
        validationErrors={validationErrors}
        onCancelButtonClick={this.handleModalCancelButtonClick}
        onCloseButtonClick={this.handleModalCancelButtonClick}
        onSaveButtonClick={this.handleConfirmNavigationSaveButtonClick}
        onSaveButtonErrorBadgeClick={this.handleSaveButtonErrorBadgeClick}
        onRevertButtonClick={this.handleConfirmNavigationRevertButtonClick}
      />
    );
  }
  renderConfirmRecordDeleteModal() {
    const {
      config,
      csid,
      data,
      isSavePending,
      openModalName,
      recordType,
      vocabulary,
      checkForRelations,
      checkForUses,
      checkForRoleUses,
    } = this.props;
    return (
      <ConfirmRecordDeleteModal
        config={config}
        csid={csid}
        data={data}
        isOpen={openModalName === MODAL_CONFIRM_RECORD_DELETE}
        isSavePending={isSavePending}
        recordType={recordType}
        vocabulary={vocabulary}
        checkForRelations={checkForRelations}
        checkForUses={checkForUses}
        checkForRoleUses={checkForRoleUses}
        onCancelButtonClick={this.handleModalCancelButtonClick}
        onCloseButtonClick={this.handleModalCancelButtonClick}
        onDeleteButtonClick={this.handleConfirmDeleteButtonClick}
      />
    );
  }
  renderLockRecordModal() {
    const {
      config,
      csid,
      isSavePending,
      openModalName,
      recordType,
    } = this.props;
    const recordTypeConfig = config.recordTypes[recordType];
    const { lockOnSave } = recordTypeConfig;
    if (lockOnSave !== 'prompt') {
      return null;
    }
    return (
      <LockRecordModal
        csid={csid}
        isOpen={openModalName === MODAL_LOCK_RECORD}
        isSavePending={isSavePending}
        onCancelButtonClick={this.handleModalCancelButtonClick}
        onCloseButtonClick={this.handleModalCancelButtonClick}
        onSaveOnlyButtonClick={this.handleSaveOnlyButtonClick}
        onSaveLockButtonClick={this.handleSaveLockButtonClick}
      />
    );
  }
  render() {
    const {
      config,
      csid,
      data,
      dockTop,
      formName,
      isModified,
      isReadPending,
      isSavePending,
      isSidebarOpen,
      perms,
      recordType,
      relatedSubjectWorkflowState,
      roleNames,
      subrecordData,
      validationErrors,
      vocabulary,
      vocabularyWorkflowState,
      checkDeletable,
      onRunButtonClick,
    } = this.props;
    const recordTypeConfig = config.recordTypes[recordType];
    if (!data || !recordTypeConfig) {
      return null;
    }
    const serviceType = get(recordTypeConfig, ['serviceConfig', 'serviceType']);
    const selectedFormName = formName || recordTypeConfig.defaultForm || 'default';
    const relatedSubjectLocked = isLocked(relatedSubjectWorkflowState);
    const vocabularyLocked = isLocked(vocabularyWorkflowState);
    const immutable = isRecordImmutable(data);
    const readOnly = (
      isReadPending
      || immutable
      || !(csid ? canUpdate(recordType, perms) : canCreate(recordType, perms))
    );
    const isRunnable = !!onRunButtonClick;
    const isCloneable = (
      // The record must be saved.
      !!csid
      && !vocabularyLocked
      // If we're editing an object record in a secondary tab, and the primary record is locked,
      // a new cloned record would not be able to be related to the primary, so the clone
      // button should not appear.
      && !relatedSubjectLocked
      // We must have permission to create a new record of the type.
      && canCreate(recordType, perms)
      // FIXME: Prevent cloning seeded authroles, since they may contain permissions that are not
      // normally creatable via the UI. Instead of hardcoding this, should be able to configure a
      // normalizeCloneData function that will clean up cloned data for a record type, and/or
      // a cloneable function that will determine if a record is cloneable based on its data.
      && !(recordType === 'authrole' && data.getIn(['ns2:role', 'permsProtection']) === 'immutable')
    );
    const isDeletable = (
      (checkDeletable ? checkDeletable(data) : true)
      && !!csid
      && !immutable
      && !vocabularyLocked
      // Security resources don't have soft-delete, so also need to check hard delete.
      && (canSoftDelete(recordType, perms) || canDelete(recordType, perms))
    );
    const isDeprecatable = (
      !!csid
      && config.termDeprecationEnabled
      && serviceType === 'authority'
      && !isRecordDeprecated(data)
    );
    const isUndeprecatable = (
      !!csid
      && config.termDeprecationEnabled
      && serviceType === 'authority'
      && isRecordDeprecated(data)
    );
    const className = isSidebarOpen ? styles.normal : styles.full;
    return (
      <form className={className} autoComplete="off">
        <Dock
          dockTop={dockTop}
          isSidebarOpen={isSidebarOpen}
        >
          <RecordHeader
            config={config}
            data={data}
            formName={selectedFormName}
            isRunnable={isRunnable}
            isCloneable={isCloneable}
            isDeletable={isDeletable}
            isDeprecatable={isDeprecatable}
            isUndeprecatable={isUndeprecatable}
            isModified={isModified}
            isReadPending={isReadPending}
            isSavePending={isSavePending}
            readOnly={readOnly}
            recordType={recordType}
            validationErrors={validationErrors}
            onCloneButtonClick={this.handleCloneButtonClick}
            onCommit={this.handleRecordFormSelectorCommit}
            onDeprecateButtonClick={this.handleDeprecateButtonClick}
            onDeleteButtonClick={this.handleDeleteButtonClick}
            onSaveButtonClick={this.handleSaveButtonClick}
            onSaveButtonErrorBadgeClick={this.handleSaveButtonErrorBadgeClick}
            onRevertButtonClick={this.handleRevertButtonClick}
            onUndeprecateButtonClick={this.handleUndeprecateButtonClick}
            onRunButtonClick={onRunButtonClick}
          />
        </Dock>
        <RecordFormContainer
          config={config}
          csid={csid}
          data={data}
          formName={selectedFormName}
          readOnly={readOnly}
          recordType={recordType}
          recordTypeConfig={recordTypeConfig}
          roleNames={roleNames}
          subrecordData={subrecordData}
          vocabulary={vocabulary}
        />
        <footer>
          <RecordButtonBar
            isRunnable={isRunnable}
            isCloneable={isCloneable}
            isDeletable={isDeletable}
            isDeprecatable={isDeprecatable}
            isUndeprecatable={isUndeprecatable}
            isModified={isModified}
            isReadPending={isReadPending}
            isSavePending={isSavePending}
            readOnly={readOnly}
            validationErrors={validationErrors}
            onSaveButtonClick={this.handleSaveButtonClick}
            onSaveButtonErrorBadgeClick={this.handleSaveButtonErrorBadgeClick}
            onRevertButtonClick={this.handleRevertButtonClick}
            onCloneButtonClick={this.handleCloneButtonClick}
            onDeleteButtonClick={this.handleDeleteButtonClick}
            onRunButtonClick={onRunButtonClick}
          />
        </footer>
        <Prompt
          when={isModified && !isSavePending}
          message={MODAL_CONFIRM_RECORD_NAVIGATION}
        />
        {this.renderConfirmNavigationModal()}
        {this.renderConfirmRecordDeleteModal()}
        {this.renderLockRecordModal()}
      </form>
    );
  }
}
RecordEditor.propTypes = propTypes;
RecordEditor.defaultProps = defaultProps;