cspace-ui
Version:
CollectionSpace user interface for browsers
701 lines (601 loc) • 18.7 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';
const propTypes = {
config: PropTypes.object,
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),
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,
} = this.props;
const {
recordType: prevRecordType,
vocabulary: prevVocabulary,
csid: prevCsid,
cloneCsid: prevCloneCsid,
} = prevProps;
if (
recordType !== prevRecordType ||
vocabulary !== prevVocabulary ||
csid !== prevCsid ||
cloneCsid !== prevCloneCsid
) {
this.initRecord();
}
}
componentWillUnmount() {
const {
removeNotification,
removeValidationNotification,
} = this.props;
if (removeValidationNotification) {
removeValidationNotification();
}
if (removeNotification) {
removeNotification(HierarchyReparentNotifier.notificationID);
}
}
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(LockRecordModal.modalName);
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);
}
});
}
}
}
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(ConfirmRecordDeleteModal.modalName);
}
}
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);
}
}
renderConfirmNavigationModal() {
const {
isModified,
isSavePending,
openModalName,
validationErrors,
} = this.props;
return (
<ConfirmRecordNavigationModal
isOpen={openModalName === ConfirmRecordNavigationModal.modalName}
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 === ConfirmRecordDeleteModal.modalName}
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 === LockRecordModal.modalName}
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,
validationErrors,
vocabulary,
vocabularyWorkflowState,
checkDeletable,
onRunButtonClick,
} = this.props;
const recordTypeConfig = config.recordTypes[recordType];
if (!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}
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={ConfirmRecordNavigationModal.modalName}
/>
{this.renderConfirmNavigationModal()}
{this.renderConfirmRecordDeleteModal()}
{this.renderLockRecordModal()}
</form>
);
}
}
RecordEditor.propTypes = propTypes;
RecordEditor.defaultProps = defaultProps;