diff --git a/src/actions/login.js b/src/actions/login.js index 0dc48221..de627620 100644 --- a/src/actions/login.js +++ b/src/actions/login.js @@ -6,6 +6,7 @@ import { readAuthVocabs } from './authority'; import { createSession, setSession } from './cspace'; import { loadPrefs, savePrefs } from './prefs'; import { readAccountRoles } from './account'; +import readServiceTags from './tags'; import { getUserUsername } from '../reducers'; import { @@ -207,6 +208,7 @@ export const login = (config, authCode, authCodeRequestData = {}) => (dispatch, return Promise.resolve(); }) .then(() => dispatch(loadPrefs(config, username))) + .then(() => dispatch(readServiceTags())) .then(() => dispatch(readAuthVocabs(config))) .then(() => dispatch({ type: LOGIN_FULFILLED, diff --git a/src/actions/tags.js b/src/actions/tags.js new file mode 100644 index 00000000..72d2aa4d --- /dev/null +++ b/src/actions/tags.js @@ -0,0 +1,80 @@ +import { get } from 'lodash'; +import getSession from '../helpers/session'; + +import { + SERVICE_TAGS_READ_STARTED, + SERVICE_TAGS_READ_FULFILLED, + SERVICE_TAGS_READ_REJECTED, + PROCEDURE_BY_TAG_READ_STARTED, + PROCEDURE_BY_TAG_READ_FULFILLED, + PROCEDURE_BY_TAG_READ_REJECTED, +} from '../constants/actionCodes'; + +const doRead = (tag, dispatch) => { + dispatch({ + type: PROCEDURE_BY_TAG_READ_STARTED, + meta: { + tag: tag.name, + }, + }); + + const session = getSession(); + const requestConfig = { + params: { + servicetag: tag.name, + }, + }; + + return session.read('servicegroups/procedure', requestConfig) + .then((response) => dispatch({ + type: PROCEDURE_BY_TAG_READ_FULFILLED, + payload: response, + meta: { + tag: tag.name, + }, + })) + .catch((error) => { + dispatch({ + type: PROCEDURE_BY_TAG_READ_REJECTED, + meta: { + tag: tag.name, + }, + }); + return Promise.reject(error); + }); +}; + +const readProcedures = (response, dispatch) => { + let tags = get(response, ['data', 'ns2:abstract-common-list', 'list-item']); + if (!tags) { + return Promise.resolve(); + } + + if (!Array.isArray(tags)) { + tags = [tags]; + } + + const promises = tags.map((tag) => doRead(tag, dispatch)); + return Promise.all(promises) + .catch((error) => Promise.reject(error)); +}; + +export default () => (dispatch) => { + dispatch({ type: SERVICE_TAGS_READ_STARTED }); + + const session = getSession(); + + return session.read('servicegroups/procedure/tags') + .then((response) => { + dispatch({ type: SERVICE_TAGS_READ_FULFILLED }); + return readProcedures(response, dispatch); + }) + .catch((error) => { + dispatch({ + type: SERVICE_TAGS_READ_REJECTED, + payload: error, + }); + + return Promise.reject(error); + }); +}; diff --git a/src/components/pages/CreatePage.jsx b/src/components/pages/CreatePage.jsx index fd463c65..7aec89bb 100644 --- a/src/components/pages/CreatePage.jsx +++ b/src/components/pages/CreatePage.jsx @@ -30,6 +30,17 @@ const messages = defineMessages({ }, }); +const tagMessages = defineMessages({ + nagpra: { + id: 'createPage.tag.nagpra', + defaultMessage: 'NAGPRA', + }, + legacy: { + id: 'createPage.tag.legacy', + defaultMessage: 'Legacy', + }, +}); + const getRecordTypesByServiceType = (recordTypes, perms, intl) => { const recordTypesByServiceType = {}; @@ -135,6 +146,144 @@ const getVocabularies = (recordTypeConfig, intl, getAuthorityVocabWorkflowState) return vocabularyNames; }; +const renderListItem = (recordType, config) => { + const recordConfig = config[recordType]; + const recordDisplayName = ; + const recordLink = {recordDisplayName}; + return ( +
  • + {recordLink} +
  • + ); +}; + +/** + * Render the panel for a group of Procedures + * + * @param {String} serviceType the name of the service type (object, procedure, authority) + * @param {Array} items the array of list items to display for the service + * @returns + */ +const renderPanel = (serviceType, items) => ( + items && items.length > 0 ? ( +
    +

    + +
    + ) : null +); + +/** + * Render the div for Object records + * + * @param {Array} recordTypes the object records + * @param {Object} config the cspace config + * @returns the div + */ +const renderObjects = (recordTypes = [], config) => { + const serviceType = 'object'; + const items = recordTypes.map((recordType) => renderListItem(recordType, config)); + return renderPanel(serviceType, items); +}; + +/** + * Render the div for procedure records. The procedures are grouped together by their service tags + * in order to display procedures in a workflow together. Each tag has its own header in order to + * act as a delimiter within the div. Procedures without a tag do not have a header and are part + * of a default group. + * + * @param {Array} recordTypes the procedure record types + * @param {Object} config the cspace config + * @param {Function} getTagsForRecord function to query the service tag of a record + * @param {Object} tagConfig the configuration for the service tags containing their sortOrder + * @returns + */ +const renderProcedures = (recordTypes = [], config, getTagsForRecord, tagConfig) => { + const serviceType = 'procedure'; + + const grouped = Object.groupBy(recordTypes, (recordType) => getTagsForRecord(recordType) || 'defaultGroup'); + const { + defaultGroup: defaultRecordTypes = [], + ...taggedRecordTypes + } = grouped; + + const defaultItems = defaultRecordTypes.map((recordType) => renderListItem(recordType, config)); + + const taggedItems = Object.keys(taggedRecordTypes).sort((lhs, rhs) => { + const lhsConfig = tagConfig[lhs] || {}; + const rhsConfig = tagConfig[rhs] || {}; + + const { + sortOrder: lhsOrder = Number.MAX_SAFE_INTEGER, + } = lhsConfig; + + const { + sortOrder: rhsOrder = Number.MAX_SAFE_INTEGER, + } = rhsConfig; + + return lhsOrder > rhsOrder ? 1 : -1; + }).map((tag) => { + const tagRecordTypes = taggedRecordTypes[tag]; + const items = tagRecordTypes.map((recordType) => renderListItem(recordType, config)); + + return ( +
  • +

    + +
  • + ); + }); + + return renderPanel(serviceType, defaultItems.concat(taggedItems)); +}; + +/** + * Render the div for creating authority items. Each authority is a header and its vocabulary items + * are represented as a sub-list. + * + * @param {Array} recordTypes the authority records + * @param {Object} config the cspace config + * @param {intlShape} intl the intl object + * @param {Function} getAuthorityVocabWorkflowState function to get workflow states + */ +const renderAuthorities = (recordTypes = [], config, intl, getAuthorityVocabWorkflowState) => { + const authorityItems = recordTypes.map((recordType) => { + const recordConfig = config[recordType]; + const vocabularies = getVocabularies( + recordConfig, intl, getAuthorityVocabWorkflowState, + ); + + if (!vocabularies || vocabularies.length === 0) { + return null; + } + + const vocabularyItems = vocabularies.map((vocabulary) => ( +
  • + + + +
  • + )); + const vocabularyList = ; + + const recordDisplayName = ; + const recordLink =

    {recordDisplayName}

    ; + + return ( +
  • + {recordLink} + {vocabularyList} +
  • + ); + }); + + return renderPanel('authority', authorityItems); +}; + const contextTypes = { config: PropTypes.shape({ recordTypes: PropTypes.object, @@ -145,6 +294,7 @@ const propTypes = { intl: intlShape, perms: PropTypes.instanceOf(Immutable.Map), getAuthorityVocabWorkflowState: PropTypes.func, + getTagsForRecord: PropTypes.func, }; const defaultProps = { @@ -156,6 +306,7 @@ export default function CreatePage(props, context) { intl, perms, getAuthorityVocabWorkflowState, + getTagsForRecord, } = props; const { @@ -163,77 +314,28 @@ export default function CreatePage(props, context) { } = context; const { + tags: tagConfig, recordTypes, } = config; - const itemsByServiceType = {}; - const lists = []; + let objectPanel; + let procedurePanel; + let authorityPanel; if (recordTypes) { const recordTypesByServiceType = getRecordTypesByServiceType(recordTypes, perms, intl); - serviceTypes.forEach((serviceType) => { - itemsByServiceType[serviceType] = recordTypesByServiceType[serviceType].map((recordType) => { - const recordTypeConfig = recordTypes[recordType]; - - const vocabularies = getVocabularies( - recordTypeConfig, intl, getAuthorityVocabWorkflowState, - ); - - let vocabularyList; - - if (vocabularies && vocabularies.length > 0) { - const vocabularyItems = vocabularies.map((vocabulary) => ( -
  • - - - -
  • - )); - - vocabularyList = ; - } - - if (recordTypeConfig.vocabularies && !vocabularyList) { - // The record type is an authority, but no vocabularies are enabled. Don't render - // anything. + objectPanel = renderObjects(recordTypesByServiceType.object, recordTypes); - return null; - } - - const recordDisplayName = ; - - let recordLink; - - if (vocabularyList) { - recordLink =

    {recordDisplayName}

    ; - } else { - recordLink = {recordDisplayName}; - } + procedurePanel = renderProcedures(recordTypesByServiceType.procedure, + recordTypes, + getTagsForRecord, + tagConfig); - return ( -
  • - {recordLink} - {vocabularyList} -
  • - ); - }); - }); - - serviceTypes.forEach((serviceType) => { - const items = itemsByServiceType[serviceType].filter((item) => !!item); - - if (items && items.length > 0) { - lists.push( -
    -

    -
      - {items} -
    -
    , - ); - } - }); + authorityPanel = renderAuthorities(recordTypesByServiceType.authority, + recordTypes, + intl, + getAuthorityVocabWorkflowState); } const title = ; @@ -243,7 +345,9 @@ export default function CreatePage(props, context) {
    - {lists} + {objectPanel} + {procedurePanel} + {authorityPanel}
    ); diff --git a/src/constants/actionCodes.js b/src/constants/actionCodes.js index 2d346d79..58820643 100644 --- a/src/constants/actionCodes.js +++ b/src/constants/actionCodes.js @@ -148,6 +148,16 @@ export const SET_RELATED_RECORD_BROWSER_RELATED_CSID = 'SET_RELATED_RECORD_BROWS export const SET_RECORD_PAGE_PRIMARY_CSID = 'SET_RECORD_PAGE_PRIMARY_CSID'; +// record service tags + +export const SERVICE_TAGS_READ_STARTED = 'SERVICE_TAGS_READ_STARTED'; +export const SERVICE_TAGS_READ_FULFILLED = 'SERVICE_TAGS_READ_FULFILLED'; +export const SERVICE_TAGS_READ_REJECTED = 'SERVICE_TAGS_READ_REJECTED'; + +export const PROCEDURE_BY_TAG_READ_STARTED = 'PROCEDURE_BY_TAG_READ_STARTED'; +export const PROCEDURE_BY_TAG_READ_FULFILLED = 'PROCEDURE_BY_TAG_READ_FULFILLED'; +export const PROCEDURE_BY_TAG_READ_REJECTED = 'PROCEDURE_BY_TAG_READ_REJECTED'; + // relation export const CLEAR_RELATION_STATE = 'CLEAR_RELATION_STATE'; diff --git a/src/containers/pages/CreatePageContainer.js b/src/containers/pages/CreatePageContainer.js index ca3747cb..9f368081 100644 --- a/src/containers/pages/CreatePageContainer.js +++ b/src/containers/pages/CreatePageContainer.js @@ -4,6 +4,7 @@ import CreatePage from '../../components/pages/CreatePage'; import { getAuthorityVocabWorkflowState, getUserPerms, + getTagsForRecord, } from '../../reducers'; const mapStateToProps = (state) => ({ @@ -11,6 +12,7 @@ const mapStateToProps = (state) => ({ getAuthorityVocabWorkflowState: (recordType, vocabulary) => getAuthorityVocabWorkflowState( state, recordType, vocabulary, ), + getTagsForRecord: (recordType) => getTagsForRecord(state, recordType), }); export default connect( diff --git a/src/index.jsx b/src/index.jsx index 87a75280..8c882f15 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -55,6 +55,17 @@ const defaultConfig = mergeConfig({ showTermListStateIcon: false, structDateOptionListNames: ['dateQualifiers'], structDateVocabNames: ['dateera', 'datecertainty', 'datequalifier'], + tags: { + defaultGroup: { + sortOrder: 0, + }, + nagpra: { + sortOrder: 1, + }, + legacy: { + sortOrder: 3, + }, + }, tenantId: '1', termDeprecationEnabled: false, }, { diff --git a/src/reducers/index.js b/src/reducers/index.js index 4e78a7d7..2d530bdb 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -17,6 +17,7 @@ import recordPage, * as fromRecordPage from './recordPage'; import searchToSelect, * as fromSearchToSelect from './searchToSelect'; import relation, * as fromRelation from './relation'; import search, * as fromSearch from './search'; +import tags, * as fromTags from './tags'; import user, * as fromUser from './user'; import vocabulary, * as fromVocabulary from './vocabulary'; @@ -39,6 +40,7 @@ export default combineReducers({ searchToSelect, relation, search, + tags, user, vocabulary, }); @@ -266,3 +268,5 @@ export const getNotifications = (state) => fromNotification.getNotifications(sta export const getOpenModalName = (state) => fromNotification.getModal(state.notification); export const getCSpaceSystemInfo = (state) => fromCspace.getSystemInfo(state.cspace); + +export const getTagsForRecord = (state, recordType) => fromTags.getTags(state.tags, recordType); diff --git a/src/reducers/tags.js b/src/reducers/tags.js new file mode 100644 index 00000000..ba1a8f15 --- /dev/null +++ b/src/reducers/tags.js @@ -0,0 +1,43 @@ +import Immutable from 'immutable'; +import get from 'lodash/get'; +import { PROCEDURE_BY_TAG_READ_FULFILLED } from '../constants/actionCodes'; + +import { + NS_PREFIX, + DOCUMENT_PROPERTY_NAME, +} from '../constants/xmlNames'; + +const handleProcedureByTagFulfilled = (state, action) => { + const response = action.payload; + + const hasDocTypes = get(response, ['data', + DOCUMENT_PROPERTY_NAME, + `${NS_PREFIX}:servicegroups_common`, + 'hasDocTypes', + 'hasDocType']); + + if (!hasDocTypes) { + return state; + } + + // currently this makes an assumption that doctypes will only have one service tag + // it should be updated to merge state properly + let nextState = state; + const docTypes = Array.isArray(hasDocTypes) ? hasDocTypes : [hasDocTypes]; + docTypes.forEach((docType) => { + nextState = nextState.setIn([docType.toLowerCase()], action.meta.tag); + }); + + return nextState; +}; + +export default (state = Immutable.Map(), action) => { + switch (action.type) { + case PROCEDURE_BY_TAG_READ_FULFILLED: + return handleProcedureByTagFulfilled(state, action); + default: + return state; + } +}; + +export const getTags = (state, recordType) => state.get(recordType); diff --git a/styles/cspace-ui/CreatePagePanel.css b/styles/cspace-ui/CreatePagePanel.css index 41b6cd80..4d72a491 100644 --- a/styles/cspace-ui/CreatePagePanel.css +++ b/styles/cspace-ui/CreatePagePanel.css @@ -27,18 +27,16 @@ list-style: none; } -.common > ul > li > ul > li { +.authority > ul > li > ul > li { display: inline-block; } -.common > ul > li > ul > li + li::before { +.authority > ul > li > ul > li + li::before { content: ' | '; } -.audit { - composes: common; - background-color: #F0F5FB; - color: #305A8D; +.tag > ul > li + li { + margin-top: 4px; } .object { diff --git a/test/specs/actions/login.spec.js b/test/specs/actions/login.spec.js index 183ae359..2051af5c 100644 --- a/test/specs/actions/login.spec.js +++ b/test/specs/actions/login.spec.js @@ -16,6 +16,8 @@ import { ACCOUNT_PERMS_READ_FULFILLED, ACCOUNT_PERMS_READ_REJECTED, ACCOUNT_ROLES_READ_FULFILLED, + SERVICE_TAGS_READ_STARTED, + SERVICE_TAGS_READ_FULFILLED, } from '../../../src/constants/actionCodes'; import { @@ -65,6 +67,7 @@ describe('login action creator', () => { const tokenUrl = '/cspace-services/oauth2/token'; const accountPermsUrl = '/cspace-services/accounts/0/accountperms'; const accountRolesUrl = `/cspace-services/accounts/${accountId}/accountroles`; + const procedureTagsUrl = '/cspace-services/servicegroups/procedure/tags'; const config = {}; const store = mockStore({ @@ -109,13 +112,19 @@ describe('login action creator', () => { }))), rest.get(accountRolesUrl, (req, res, ctx) => res(ctx.json({}))), + + rest.get(procedureTagsUrl, (req, res, ctx) => res(ctx.json({ + 'ns2:abstract-common-list': { + 'list-item': [], + }, + }))), ); return store.dispatch(login(config, authCode, authCodeRequestData)) .then(() => { const actions = store.getActions(); - actions.should.have.lengthOf(6); + actions.should.have.lengthOf(8); actions[0].should.deep.equal({ type: LOGIN_STARTED, @@ -145,7 +154,10 @@ describe('login action creator', () => { actions[4].should.have.property('type', PREFS_LOADED); - actions[5].should.deep.equal({ + actions[5].should.have.property('type', SERVICE_TAGS_READ_STARTED); + actions[6].should.have.property('type', SERVICE_TAGS_READ_FULFILLED); + + actions[7].should.deep.equal({ type: LOGIN_FULFILLED, meta: { landingPath: authCodeRequestData.landingPath, diff --git a/test/specs/actions/tags.spec.js b/test/specs/actions/tags.spec.js new file mode 100644 index 00000000..2b424ab8 --- /dev/null +++ b/test/specs/actions/tags.spec.js @@ -0,0 +1,196 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import chaiAsPromised from 'chai-as-promised'; +import { setupWorker, rest } from 'msw'; + +import { + configureCSpace, +} from '../../../src/actions/cspace'; + +import readServiceTags from '../../../src/actions/tags'; +import { + PROCEDURE_BY_TAG_READ_FULFILLED, + PROCEDURE_BY_TAG_READ_REJECTED, + PROCEDURE_BY_TAG_READ_STARTED, + SERVICE_TAGS_READ_FULFILLED, + SERVICE_TAGS_READ_REJECTED, + SERVICE_TAGS_READ_STARTED, +} from '../../../src/constants/actionCodes'; + +chai.use(chaiAsPromised); +chai.should(); + +const mockStore = configureMockStore([thunk]); +const successTag = 'test-tag'; +const failureTag = 'failure-tag'; + +describe('tags action creator', () => { + const worker = setupWorker(); + const procedureUrl = '/cspace-services/servicegroups/procedure'; + const procedureTagsUrl = '/cspace-services/servicegroups/procedure/tags'; + + before(async () => { + await worker.start({ quiet: true }); + }); + + after(() => { + worker.stop(); + }); + + describe('success', () => { + const store = mockStore(); + + before(() => store.dispatch(configureCSpace()) + .then(() => store.clearActions())); + + afterEach(() => { + store.clearActions(); + worker.resetHandlers(); + }); + + it('reads tags and procedures when successful', () => { + worker.use( + rest.get(procedureTagsUrl, (req, res, ctx) => res(ctx.json({ + 'ns2:abstract-common-list': { + 'list-item': [{ name: successTag }], + }, + }))), + + rest.get(procedureUrl, (req, res, ctx) => res(ctx.json({ + }))), + ); + + return store.dispatch(readServiceTags()) + .then(() => { + const actions = store.getActions(); + + const tagReadIdx = actions.findIndex((el) => el.type === SERVICE_TAGS_READ_STARTED); + const tagFulfilledIdx = actions.findIndex( + (el) => el.type === SERVICE_TAGS_READ_FULFILLED, + ); + const procedureReadIdx = actions.findIndex( + (el) => el.type === PROCEDURE_BY_TAG_READ_STARTED, + ); + const procedureFulfilledIdx = actions.findIndex( + (el) => el.type === PROCEDURE_BY_TAG_READ_FULFILLED, + ); + + [tagReadIdx, tagFulfilledIdx, procedureReadIdx, procedureFulfilledIdx] + .should.not.include(-1); + + const procedureRead = actions[procedureReadIdx]; + const procedureFulfilled = actions[procedureFulfilledIdx]; + + procedureRead.meta.should.contain({ tag: successTag }); + procedureFulfilled.meta.should.contain({ tag: successTag }); + procedureFulfilled.payload.status.should.equal(200); + }); + }); + + it('resolves early when no tags are found', () => { + worker.use( + rest.get(procedureTagsUrl, (req, res, ctx) => res(ctx.json({ + 'ns2:abstract-common-list': { + }, + }))), + ); + + return store.dispatch(readServiceTags()) + .then(() => { + const actions = store.getActions(); + + const tagReadIdx = actions.findIndex((el) => el.type === SERVICE_TAGS_READ_STARTED); + const tagFulfilledIdx = actions.findIndex( + (el) => el.type === SERVICE_TAGS_READ_FULFILLED, + ); + const procedureReadIdx = actions.findIndex( + (el) => el.type === PROCEDURE_BY_TAG_READ_STARTED, + ); + + [tagReadIdx, tagFulfilledIdx] + .should.not.include(-1); + + procedureReadIdx.should.equal(-1); + }); + }); + }); + + describe('failure', () => { + const store = mockStore(); + + before(() => store.dispatch(configureCSpace()) + .then(() => store.clearActions())); + + afterEach(() => { + store.clearActions(); + worker.resetHandlers(); + }); + + it('should not query procedures when the service tag read fails', () => { + worker.use( + rest.get(procedureTagsUrl, (req, res, ctx) => res( + ctx.status(403), + ctx.json({ message: 'forbidden' }), + )), + ); + + return store.dispatch(readServiceTags()).should.eventually.be.rejected + .then(() => { + const actions = store.getActions(); + + const tagReadIdx = actions.findIndex((el) => el.type === SERVICE_TAGS_READ_STARTED); + const tagRejectedIdx = actions.findIndex( + (el) => el.type === SERVICE_TAGS_READ_REJECTED, + ); + const tagFulfilledIdx = actions.findIndex( + (el) => el.type === SERVICE_TAGS_READ_FULFILLED, + ); + + [tagReadIdx, tagRejectedIdx].should.not.include(-1, 'Expected service reads to exist in actions'); + tagFulfilledIdx.should.equal(-1, 'Did not expect fulfilled to exist'); + }); + }); + + it('should reject if one procedure read fails', () => { + worker.use( + rest.get(procedureTagsUrl, (req, res, ctx) => res(ctx.json({ + 'ns2:abstract-common-list': { + 'list-item': [{ name: successTag }, { name: failureTag }], + }, + }))), + + rest.get(procedureUrl, (req, res, ctx) => { + const tag = req.url.searchParams.get('servicetag'); + + if (tag === failureTag) { + return res(ctx.status(400)); + } + + return res(ctx.json({})); + }), + ); + + return store.dispatch(readServiceTags()).should.eventually.be.rejected + .then(() => { + const actions = store.getActions(); + + const tagReadIdx = actions.findIndex((el) => el.type === SERVICE_TAGS_READ_STARTED); + const tagFulfilledIdx = actions.findIndex( + (el) => el.type === SERVICE_TAGS_READ_FULFILLED, + ); + const procedureReadIdx = actions.findIndex( + (el) => el.type === PROCEDURE_BY_TAG_READ_STARTED, + ); + const procedureRejectedIdx = actions.findIndex( + (el) => el.type === PROCEDURE_BY_TAG_READ_REJECTED, + ); + + [tagReadIdx, tagFulfilledIdx, procedureReadIdx, procedureRejectedIdx] + .should.not.include(-1); + + const rejected = actions[procedureRejectedIdx]; + rejected.meta.should.contain({ tag: failureTag }); + }); + }); + }); +}); diff --git a/test/specs/components/pages/CreatePage.spec.jsx b/test/specs/components/pages/CreatePage.spec.jsx index 09d4a105..2f117ff2 100644 --- a/test/specs/components/pages/CreatePage.spec.jsx +++ b/test/specs/components/pages/CreatePage.spec.jsx @@ -15,6 +15,7 @@ const { expect } = chai; chai.should(); const config = { + tags: {}, recordTypes: { collectionobject: { messages: { @@ -190,6 +191,7 @@ const perms = Immutable.fromJS({ describe('CreatePage', () => { const getAuthorityVocabWorkflowState = () => 'project'; + const getTagsForRecord = () => undefined; beforeEach(function before() { this.container = createTestContainer(this); @@ -202,6 +204,7 @@ describe('CreatePage', () => { @@ -219,6 +222,7 @@ describe('CreatePage', () => { @@ -253,6 +257,7 @@ describe('CreatePage', () => { @@ -274,6 +279,7 @@ describe('CreatePage', () => { @@ -297,6 +303,7 @@ describe('CreatePage', () => { @@ -320,6 +327,7 @@ describe('CreatePage', () => { @@ -341,6 +349,7 @@ describe('CreatePage', () => { @@ -383,6 +392,7 @@ describe('CreatePage', () => { @@ -434,6 +444,7 @@ describe('CreatePage', () => { diff --git a/test/specs/reducers/index.spec.js b/test/specs/reducers/index.spec.js index 3af9b536..4075b160 100644 --- a/test/specs/reducers/index.spec.js +++ b/test/specs/reducers/index.spec.js @@ -61,6 +61,7 @@ import reducer, { getSearchToSelectVocabulary, getNotifications, getOpenModalName, + getTagsForRecord, } from '../../../src/reducers'; import { searchKey } from '../../../src/reducers/search'; @@ -91,6 +92,7 @@ describe('reducer', () => { 'relation', 'search', 'searchToSelect', + 'tags', 'user', 'vocabulary', ]); @@ -1026,4 +1028,17 @@ describe('reducer', () => { }).should.equal(modalName); }); }); + + describe('getTagsForProcedure selector', () => { + it('should select from the tags key', () => { + const recordType = 'collectionobject'; + const recordTags = Immutable.Map(); + + getTagsForRecord({ + tags: Immutable.Map({ + [recordType]: recordTags, + }), + }, recordType).should.deep.equal(recordTags); + }); + }); }); diff --git a/test/specs/reducers/tags.spec.js b/test/specs/reducers/tags.spec.js new file mode 100644 index 00000000..2feee4cd --- /dev/null +++ b/test/specs/reducers/tags.spec.js @@ -0,0 +1,77 @@ +import Immutable from 'immutable'; +import chaiImmutable from 'chai-immutable'; + +import { + PROCEDURE_BY_TAG_READ_FULFILLED, +} from '../../../src/constants/actionCodes'; + +import reducer, { + getTags, +} from '../../../src/reducers/tags'; + +const { expect } = chai; + +chai.use(chaiImmutable); +chai.should(); + +describe('tags reducer', () => { + it('should have an empty immutable initial state', () => { + reducer(undefined, {}).should.deep.equal(Immutable.Map({})); + }); + + it('should do nothing if no record types are present', () => { + const recordType = 'collectionobject'; + const serviceTag = 'test-tag'; + const tagData = { + document: { + 'ns2:servicegroups_common': { + }, + }, + }; + + const state = reducer(undefined, { + type: PROCEDURE_BY_TAG_READ_FULFILLED, + payload: { + data: tagData, + }, + meta: { + tag: serviceTag, + }, + }); + + state.should.deep.equal(Immutable.Map({})); + expect(getTags(state, recordType)).to.equal(undefined); + }); + + it('should associate record types with a tag', () => { + const recordType = 'CollectionObject'; + const recordTypeLower = recordType.toLowerCase(); + const serviceTag = 'test-tag'; + const tagData = { + document: { + 'ns2:servicegroups_common': { + hasDocTypes: { + hasDocType: [ + recordType, + ], + }, + }, + }, + }; + + const state = reducer(undefined, { + type: PROCEDURE_BY_TAG_READ_FULFILLED, + payload: { + data: tagData, + }, + meta: { + tag: serviceTag, + }, + }); + + state.should.deep.equal(Immutable.Map({ + [recordTypeLower]: serviceTag, + })); + expect(getTags(state, recordTypeLower)).to.equal(serviceTag); + }); +});