UNPKG

cspace-ui

Version:
685 lines (658 loc) 25.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isSavePending = exports.isReadVocabularyItemRefsPending = exports.isReadPending = exports.isModifiedExceptPart = exports.isModified = exports.getValidationErrors = exports.getSubrecordData = exports.getSubrecordCsid = exports.getRelationUpdatedTimestamp = exports.getNewSubrecordCsid = exports.getNewData = exports.getError = exports.getData = exports.default = void 0; var _immutable = _interopRequireDefault(require("immutable")); var _get = _interopRequireDefault(require("lodash/get")); var _actionCodes = require("../constants/actionCodes"); var _recordDataHelpers = require("../helpers/recordDataHelpers"); var _configHelpers = require("../helpers/configHelpers"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const BASE_NEW_RECORD_KEY = ''; const unsavedRecordKey = subrecordName => subrecordName ? `${BASE_NEW_RECORD_KEY}/${subrecordName}` : BASE_NEW_RECORD_KEY; const getCurrentData = (state, csid) => state.getIn([csid, 'data', 'current']); const setCurrentData = (state, csid, data) => state.setIn([csid, 'data', 'current'], data); const getBaselineData = (state, csid) => state.getIn([csid, 'data', 'baseline']); const setBaselineData = (state, csid, data) => state.setIn([csid, 'data', 'baseline'], data); const clear = (state, csid, clearSubrecords = false) => { const recordState = state.get(csid); if (!recordState) { return state; } let nextState = state; const subrecord = recordState.get('subrecord'); if (clearSubrecords && subrecord) { nextState = subrecord.reduce((reducedState, subrecordCsid) => clear(reducedState, subrecordCsid, clearSubrecords), nextState); } return nextState.delete(csid); }; const clearAll = state => state.clear(); const clearFiltered = (state, filter) => { let nextState = state; state.filter(filter).forEach((recordState, csid) => { nextState = clear(nextState, csid); }); return nextState; }; const addFieldInstance = (state, action) => { const { csid, path, position, recordTypeConfig } = action.meta; const data = getCurrentData(state, csid); if (!data) { return state; } const value = (0, _recordDataHelpers.deepGet)(data, path); const list = _immutable.default.List.isList(value) ? value : _immutable.default.List.of(value); const fieldDescriptor = (0, _get.default)(recordTypeConfig, ['fields', ...(0, _configHelpers.dataPathToFieldDescriptorPath)(path)]); const defaultData = (0, _recordDataHelpers.initializeChildren)(fieldDescriptor, (0, _recordDataHelpers.applyDefaults)(fieldDescriptor)); const updatedList = typeof position === 'undefined' || position < 0 || position >= list.size ? list.push(defaultData) : list.insert(position, defaultData); const updatedData = (0, _recordDataHelpers.deepSet)(data, path, updatedList); return setCurrentData(state, csid, updatedData); }; const sortFieldInstances = (state, action) => { const { config, csid, path, byField } = action.meta; const data = getCurrentData(state, csid); if (!data) { return state; } const value = (0, _recordDataHelpers.deepGet)(data, path); const list = _immutable.default.List.isList(value) ? value : _immutable.default.List.of(value); // TODO: Check for a custom sort comparator function in field config. // For now just use the default. const comparator = (str1, str2) => str1.localeCompare(str2, config.locale); const sortedList = byField ? list.sortBy(item => item.get(byField), comparator) : list.sort(comparator); const updatedData = (0, _recordDataHelpers.deepSet)(data, path, sortedList); return setCurrentData(state, csid, updatedData); }; const doCreateNew = (state, config, recordTypeConfig, options = {}) => { const { cloneCsid, subrecordName, stickyFields } = options; let data; if (cloneCsid) { data = (0, _recordDataHelpers.cloneRecordData)(recordTypeConfig, cloneCsid, getCurrentData(state, cloneCsid)); } if (!data) { data = (0, _recordDataHelpers.createRecordData)(recordTypeConfig); if (stickyFields) { // Merge in the user's saved sticky fields for this record type, if any. const fields = stickyFields.get(recordTypeConfig.name); if (fields) { data = data.mergeDeep(fields); } } } const csid = unsavedRecordKey(subrecordName); let nextState = state.delete(csid); const { subrecords } = recordTypeConfig; if (subrecords) { Object.keys(subrecords).forEach(name => { const subrecordConfig = subrecords[name]; const { csidField } = subrecordConfig; let cloneSubrecordCsid; if (csidField) { cloneSubrecordCsid = (0, _recordDataHelpers.deepGet)(data, csidField); } if (!cloneSubrecordCsid) { const subrecordType = subrecordConfig.recordType; const subrecordTypeConfig = (0, _get.default)(config, ['recordTypes', subrecordType]); const subrecordCsid = state.getIn([cloneCsid, 'subrecord', name]); nextState = doCreateNew(nextState, config, subrecordTypeConfig, { cloneCsid: subrecordCsid, subrecordName: name, stickyFields }); cloneSubrecordCsid = `${csid}/${name}`; if (csidField) { data = (0, _recordDataHelpers.deepSet)(data, csidField, cloneSubrecordCsid); } } nextState = nextState.setIn([csid, 'subrecord', name], cloneSubrecordCsid); }); } nextState = setBaselineData(nextState, csid, data); nextState = setCurrentData(nextState, csid, data); return nextState; }; const createNewRecord = (state, action) => { const { config, recordTypeConfig, cloneCsid, stickyFields } = action.meta; return doCreateNew(state, config, recordTypeConfig, { cloneCsid, stickyFields }); }; const deleteFieldValue = (state, action) => { const { csid, path } = action.meta; const data = getCurrentData(state, csid); if (!data) { return state; } const updatedData = (0, _recordDataHelpers.deepDelete)(data, path); return setCurrentData(state, csid, updatedData); }; const moveFieldValue = (state, action) => { const { csid, path, newPosition } = action.meta; const data = getCurrentData(state, csid); if (!data) { return state; } const listPath = path.slice(0, -1); const oldPosition = path[path.length - 1]; let list = (0, _recordDataHelpers.deepGet)(data, listPath); if (!_immutable.default.List.isList(list)) { return state; } const value = list.get(oldPosition); list = list.delete(oldPosition); list = list.insert(newPosition, value); const updatedData = (0, _recordDataHelpers.deepSet)(data, listPath, list); return setCurrentData(state, csid, updatedData); }; const setFieldValue = (state, action) => { const { csid, path } = action.meta; const data = getCurrentData(state, csid); if (!data) { return state; } const newValue = action.payload; const updatedData = (0, _recordDataHelpers.deepSet)(data, path, newValue); const nextState = setCurrentData(state, csid, updatedData); return nextState; }; const handleFieldComputeFulfilled = (state, action) => { const { csid, path } = action.meta; const data = getCurrentData(state, csid); if (!data) { return state; } if (path.length === 0) { // The entire record was computed. const computedData = action.payload; const updatedData = data.mergeDeep(computedData); const nextState = setCurrentData(state, csid, updatedData); return nextState; } // TODO: Handle an individual field being computed. return state; }; const handleRecordReadFulfilled = (state, action) => { const { csid, recordTypeConfig } = action.meta; const data = (0, _recordDataHelpers.normalizeRecordData)(recordTypeConfig, _immutable.default.fromJS(action.payload.data)); let nextState = state.deleteIn([csid, 'isReadPending']).deleteIn([csid, 'error']); nextState = setBaselineData(nextState, csid, data); nextState = setCurrentData(nextState, csid, data); return nextState; }; const handleRecordSaveFulfilled = (state, action) => { const { csid, recordTypeConfig, relatedSubjectCsid, recordPagePrimaryCsid } = action.meta; const data = (0, _recordDataHelpers.normalizeRecordData)(recordTypeConfig, _immutable.default.fromJS(action.payload.data)); let nextState = state; nextState = nextState.deleteIn([csid, 'isSavePending']); nextState = setBaselineData(nextState, csid, data); nextState = setCurrentData(nextState, csid, data); if (relatedSubjectCsid) { nextState = nextState.setIn([relatedSubjectCsid, 'relationUpdatedTime'], (0, _recordDataHelpers.getUpdatedTimestamp)(data)); } // Remove all record state besides the record that was just saved, any of its subrecords, and any // related record. This isn't strictly necessary, but it's a good time to expire record data, // since other records may have fields computed from this record via service layer handlers. let persistCsids = [csid, relatedSubjectCsid, BASE_NEW_RECORD_KEY // Don't clear unsaved record data ]; const subrecord = nextState.getIn([csid, 'subrecord']); if (subrecord) { subrecord.valueSeq().forEach(subrecordCsid => { persistCsids.push(subrecordCsid); }); } // avoid clearing any subrecords for the current page const recordPageSubrecord = nextState.getIn([recordPagePrimaryCsid, 'subrecord']); if (recordPageSubrecord) { recordPageSubrecord.valueSeq().forEach(subrecordCsid => { persistCsids.push(subrecordCsid); }); } persistCsids = new Set(persistCsids.filter(value => value !== null && typeof value !== 'undefined')); nextState = clearFiltered(nextState, (recordState, candidateCsid) => !persistCsids.has(candidateCsid) && !candidateCsid.startsWith(`${BASE_NEW_RECORD_KEY}/`) // Don't clear unsaved subrecord data && !recordState.get('isSavePending') // Don't clear records that are being saved && candidateCsid !== recordPagePrimaryCsid // Don't clear the primary record data ); return nextState; }; const revertRecord = (state, action) => { const { recordTypeConfig, csid } = action.meta; const baselineData = getBaselineData(state, csid); let nextState = setCurrentData(state, csid, baselineData); // Revert subrecords. const subrecords = nextState.getIn([csid, 'subrecord']); if (subrecords) { subrecords.forEach(subrecordCsid => { nextState = revertRecord(nextState, { meta: { csid: subrecordCsid } }); }); } // Revert any cached subrecord csids that originated from fields in the record. const configuredSubrecords = (0, _get.default)(recordTypeConfig, 'subrecords'); if (configuredSubrecords) { Object.entries(configuredSubrecords).forEach(entry => { const [subrecordName, subrecordConfig] = entry; const { csidField } = subrecordConfig; if (csidField) { const revertedSubrecordCsid = (0, _recordDataHelpers.deepGet)(baselineData, csidField); nextState = nextState.setIn([csid, 'subrecord', subrecordName], revertedSubrecordCsid); // Revert the reattached subrecord. nextState = revertRecord(nextState, { meta: { csid: revertedSubrecordCsid } }); } }); } return nextState; }; const handleSubrecordCreated = (state, action) => { const { csid, csidField, subrecordName, subrecordCsid, isDefault } = action.meta; let nextState = state.setIn([csid, 'subrecord', subrecordName], subrecordCsid); if (csidField) { const currentData = getCurrentData(state, csid); const baselineData = getBaselineData(state, csid); if (isDefault && currentData === baselineData) { // This subrecord was created as the default for the container. Set the csid field in both // the baseline and current data, so it won't be considered a modification. const updatedData = (0, _recordDataHelpers.deepSet)(baselineData, csidField, subrecordCsid); nextState = setBaselineData(nextState, csid, updatedData); nextState = setCurrentData(nextState, csid, updatedData); } else { const updatedData = (0, _recordDataHelpers.deepSet)(currentData, csidField, subrecordCsid); nextState = setCurrentData(nextState, csid, updatedData); } } return nextState; }; const createNewSubrecord = (state, action) => { const { config, csid, csidField, subrecordName, subrecordTypeConfig, cloneCsid, isDefault, stickyFields } = action.meta; let nextState = doCreateNew(state, config, subrecordTypeConfig, { cloneCsid, subrecordName, stickyFields }); const subrecordCsid = unsavedRecordKey(subrecordName); nextState = handleSubrecordCreated(nextState, { meta: { csid, csidField, subrecordName, subrecordCsid, isDefault } }); return nextState; }; const handleSubjectRelationsUpdated = (state, action) => { // Currently relations are only ever created (not updated), and we don't bother to retrieve // the relation record after creation. Technically we should retrieve the new relation // record and use its updatedAt value, but that's an extra request. For now the action creator // sets the updated time as a meta field. const { subject, updatedTime } = action.meta; const subjectCsid = subject.csid; if (state.has(subjectCsid)) { return state.setIn([subjectCsid, 'relationUpdatedTime'], updatedTime); } return state; }; const handleCreateIDFulfilled = (state, action) => { const { csid, path, transform } = action.meta; const data = getCurrentData(state, csid); if (!data) { return state; } const value = action.payload.data; const createdID = typeof value === 'number' ? value.toString() : value; const id = transform ? transform(createdID) : createdID; const updatedData = (0, _recordDataHelpers.deepSet)(data, path, id); const nextState = setCurrentData(state, csid, updatedData); return nextState; }; const handleValidationFailed = (state, action) => { const errors = action.payload; const { csid, path } = action.meta; return state.setIn([csid, 'validation', ...path], errors); }; const handleValidationPassed = (state, action) => { const { csid, path } = action.meta; return state.deleteIn([csid, 'validation', ...path]); }; const handleTransitionFulfilled = (state, action) => { const { csid, transitionName, recordPagePrimaryCsid, recordTypeConfig, relatedSubjectCsid, updatedTimestamp } = action.meta; let nextState = state.deleteIn([csid, 'isSavePending']); if (transitionName === 'delete') { nextState = nextState.delete(csid); // Take this opportunity to expire other record data. This isn't strictly necessary, but it's a // good time, since the deletion of a record may affect other records via service layer // handlers. nextState = clearFiltered(nextState, (recordState, candidateCsid) => !recordState.get('isSavePending') // Don't clear records that are being saved && candidateCsid !== recordPagePrimaryCsid // Don't clear the primary record data ); } else { const newData = (0, _get.default)(action, ['payload', 'data']); if (newData) { const data = (0, _recordDataHelpers.normalizeRecordData)(recordTypeConfig, _immutable.default.fromJS(newData)); nextState = setBaselineData(nextState, csid, data); nextState = setCurrentData(nextState, csid, data); } } if (relatedSubjectCsid) { nextState = nextState.setIn([relatedSubjectCsid, 'relationUpdatedTime'], updatedTimestamp); } return nextState; }; const handleDeleteFulfilled = (state, action) => { const { csid, relatedSubjectCsid, updatedTimestamp } = action.meta; let nextState = state.delete(csid); if (relatedSubjectCsid) { nextState = nextState.setIn([relatedSubjectCsid, 'relationUpdatedTime'], updatedTimestamp); } return nextState; }; const detachSubrecord = (state, action) => createNewSubrecord(state, action); const handleLoginFulfilled = (state, action) => { const { prevUsername, username } = action.meta; if (prevUsername !== username) { // The logged in user has changed. Remove all record state, because the new user may not be // permitted to read some records that the previous user could. return clearAll(state); } return state; }; const getRefMap = data => { let items = (0, _get.default)(data, ['ns2:abstract-common-list', 'list-item']) || []; if (!Array.isArray(items)) { items = [items]; } const refMap = {}; items.forEach(({ csid, referenced }) => { refMap[csid] = referenced; }); return refMap; }; const updateItemRefStates = (data, refMap) => { let existingItems = data && data.getIn(['document', 'ns2:abstract-common-list', 'list-item']); if (!existingItems) { return data; } if (!_immutable.default.List.isList(existingItems)) { existingItems = _immutable.default.List.of(existingItems); } const updatedItems = existingItems.map(item => item.set('referenced', refMap[item.get('csid')])); return data.setIn(['document', 'ns2:abstract-common-list', 'list-item'], updatedItems); }; const handleReadVocabularyItemRefsFulfilled = (state, action) => { const { csid } = action.meta; let nextState = state.deleteIn([csid, 'isReadVocabularyItemRefsPending']); const refMap = getRefMap(action.payload.data); const baselineData = getBaselineData(state, csid); const currentData = getCurrentData(state, csid); let nextBaselineData; let nextCurrentData; if (baselineData === currentData) { nextBaselineData = updateItemRefStates(baselineData, refMap); nextCurrentData = nextBaselineData; } else { nextBaselineData = updateItemRefStates(baselineData, refMap); nextCurrentData = updateItemRefStates(currentData, refMap); } nextState = setBaselineData(nextState, csid, nextBaselineData); nextState = setCurrentData(nextState, csid, nextCurrentData); return nextState; }; var _default = (state = _immutable.default.Map(), action) => { switch (action.type) { case _actionCodes.VALIDATION_FAILED: return handleValidationFailed(state, action); case _actionCodes.VALIDATION_PASSED: return handleValidationPassed(state, action); case _actionCodes.ADD_FIELD_INSTANCE: return addFieldInstance(state, action); case _actionCodes.CREATE_NEW_RECORD: return createNewRecord(state, action); case _actionCodes.CREATE_NEW_SUBRECORD: return createNewSubrecord(state, action); case _actionCodes.DELETE_FIELD_VALUE: return deleteFieldValue(state, action); case _actionCodes.MOVE_FIELD_VALUE: return moveFieldValue(state, action); case _actionCodes.SET_FIELD_VALUE: return setFieldValue(state, action); case _actionCodes.FIELD_COMPUTE_FULFILLED: return handleFieldComputeFulfilled(state, action); case _actionCodes.RECORD_CREATED: return state.set(action.meta.newRecordCsid, state.get(action.meta.currentCsid)).delete(action.meta.currentCsid); case _actionCodes.RECORD_READ_STARTED: return state.setIn([action.meta.csid, 'isReadPending'], true); case _actionCodes.RECORD_READ_FULFILLED: return handleRecordReadFulfilled(state, action); case _actionCodes.RECORD_READ_REJECTED: return state.setIn([action.meta.csid, 'error'], _immutable.default.fromJS(action.payload)).deleteIn([action.meta.csid, 'isReadPending']); case _actionCodes.RECORD_SAVE_STARTED: return state.setIn([action.meta.csid, 'isSavePending'], true); case _actionCodes.RECORD_SAVE_FULFILLED: return handleRecordSaveFulfilled(state, action); case _actionCodes.RECORD_SAVE_REJECTED: return state // I don't think there's any reason to store the save error. // .setIn([action.meta.csid, 'error'], Immutable.fromJS(action.payload)) .deleteIn([action.meta.csid, 'isSavePending']); case _actionCodes.RECORD_TRANSITION_STARTED: return state.setIn([action.meta.csid, 'isSavePending'], true); case _actionCodes.RECORD_TRANSITION_FULFILLED: return handleTransitionFulfilled(state, action); case _actionCodes.RECORD_TRANSITION_REJECTED: return state.deleteIn([action.meta.csid, 'isSavePending']); case _actionCodes.RECORD_DELETE_STARTED: return state.setIn([action.meta.csid, 'isSavePending'], true); case _actionCodes.RECORD_DELETE_FULFILLED: return handleDeleteFulfilled(state, action); case _actionCodes.RECORD_DELETE_REJECTED: return state.deleteIn([action.meta.csid, 'isSavePending']); case _actionCodes.SUBRECORD_CREATED: return handleSubrecordCreated(state, action); case _actionCodes.SUBRECORD_READ_FULFILLED: return state.setIn([action.meta.csid, 'subrecord', action.meta.subrecordName], action.meta.subrecordCsid); case _actionCodes.REVERT_RECORD: return revertRecord(state, action); case _actionCodes.SUBJECT_RELATIONS_UPDATED: return handleSubjectRelationsUpdated(state, action); case _actionCodes.CREATE_ID_FULFILLED: return handleCreateIDFulfilled(state, action); case _actionCodes.DETACH_SUBRECORD: return detachSubrecord(state, action); case _actionCodes.CLEAR_RECORD: return clear(state, action.meta.csid, action.meta.clearSubrecords); case _actionCodes.LOGIN_FULFILLED: return handleLoginFulfilled(state, action); case _actionCodes.LOGOUT_FULFILLED: return clearAll(state); case _actionCodes.SORT_FIELD_INSTANCES: return sortFieldInstances(state, action); case _actionCodes.READ_VOCABULARY_ITEM_REFS_STARTED: return state.setIn([action.meta.csid, 'isReadVocabularyItemRefsPending'], true); case _actionCodes.READ_VOCABULARY_ITEM_REFS_FULFILLED: return handleReadVocabularyItemRefsFulfilled(state, action); case _actionCodes.READ_VOCABULARY_ITEM_REFS_REJECTED: return state.setIn([action.meta.csid, 'error'], _immutable.default.fromJS(action.payload)).deleteIn([action.meta.csid, 'isReadVocabularyItemRefsPending']); default: return state; } }; exports.default = _default; const getData = (state, csid) => getCurrentData(state, csid); exports.getData = getData; const getError = (state, csid) => state.getIn([csid, 'error']); exports.getError = getError; const getSubrecordCsid = (state, csid, subrecordName) => state.getIn([csid, 'subrecord', subrecordName]); exports.getSubrecordCsid = getSubrecordCsid; let subrecordDataMemo = null; const getSubrecordData = (state, csid) => { const subrecords = state.getIn([csid, 'subrecord']); let subrecordData = null; if (subrecords) { subrecordData = subrecords.map(subrecordCsid => getData(state, subrecordCsid)); if (_immutable.default.is(subrecordDataMemo, subrecordData)) { return subrecordDataMemo; } } subrecordDataMemo = subrecordData; return subrecordData; }; exports.getSubrecordData = getSubrecordData; const getRelationUpdatedTimestamp = (state, csid) => state.getIn([csid, 'relationUpdatedTime']); exports.getRelationUpdatedTimestamp = getRelationUpdatedTimestamp; const getNewData = state => getData(state, unsavedRecordKey()); exports.getNewData = getNewData; const getNewSubrecordCsid = (state, subrecordName) => getSubrecordCsid(state, unsavedRecordKey(), subrecordName); exports.getNewSubrecordCsid = getNewSubrecordCsid; const getValidationErrors = (state, csid) => state.getIn([csid, 'validation']); exports.getValidationErrors = getValidationErrors; const isReadPending = (state, csid) => state.getIn([csid, 'isReadPending']); exports.isReadPending = isReadPending; const isSavePending = (state, csid) => state.getIn([csid, 'isSavePending']); exports.isSavePending = isSavePending; const isReadVocabularyItemRefsPending = (state, csid) => state.getIn([csid, 'isReadVocabularyItemRefsPending']); exports.isReadVocabularyItemRefsPending = isReadVocabularyItemRefsPending; const isModified = (state, csid) => { // Do a reference equality test between the current and baseline data. This will not detect if // a change is made, then another change is made that undoes the first. But it's more efficient // than a deep value equality test. if (getCurrentData(state, csid) !== getBaselineData(state, csid)) { return true; } // Check subrecords. const subrecords = state.getIn([csid, 'subrecord']); if (subrecords && subrecords.find(subrecordCsid => isModified(state, subrecordCsid))) { return true; } return false; }; exports.isModified = isModified; const isModifiedExceptPart = (state, csid, exceptPart) => { // Check subrecords. const subrecords = state.getIn([csid, 'subrecord']); if (subrecords && subrecords.find(subrecordCsid => isModified(state, subrecordCsid))) { return true; } // Check parts, except for the given exception. const data = state.getIn([csid, 'data']); if (!data) { return false; } const baselineData = data.get('baseline'); const currentData = data.get('current'); if (currentData === baselineData) { return false; } const baselineDocument = baselineData.get('document'); const currentDocument = currentData.get('document'); if (currentDocument === baselineDocument) { return false; } const modifiedPart = currentDocument.keySeq().filter(part => part !== exceptPart && !part.startsWith('@')).find(part => currentDocument.get(part) !== baselineDocument.get(part)); return !!modifiedPart; }; exports.isModifiedExceptPart = isModifiedExceptPart;