{errorMessage}
{briefDescription}
: null; + }, + }, + tags: { + formatter: (data, { intl, separator = ', ' } = {}) => { + const messages = defineMessages({ + concepts: { + id: 'detailList.tags.collectionobject.concepts', + description: 'The prefix for content concept tags in the search detail view', + defaultMessage: `{count, plural, + one {CONCEPT TAG: } + other {CONCEPT TAGS: } + }`, + }, + }); + + let count = 0; + let conceptTags; + const contentConcepts = data.get('contentConcepts'); + if (contentConcepts) { + const contentConcept = contentConcepts.get('contentConcept'); + if (Immutable.List.isList(contentConcept)) { + conceptTags = contentConcept.filter((concept) => !!concept) + .map((concept) => formatRefNameWithDefault(concept)) + .join(separator); + count = contentConcept.size; + } else { + conceptTags = formatRefNameWithDefault(contentConcept); + count = 1; + } + } + + const prefix = intl.formatMessage(messages.concepts, { count }); + + return conceptTags ? ( ++ {prefix} + {conceptTags} +
+ ) : undefined; + }, + }, + aside: { + formatter: (data, { intl } = {}) => { + const messages = defineMessages({ + computedLocation: { + id: 'detailList.aside.collectionobject.currentLocation', + description: 'The prefix for current location in the search detail view', + defaultMessage: 'Current Storage Location:', + }, + locationNotFound: { + id: 'detailList.aside.collectionobject.locationNotFound', + description: 'The text when the computedCurrentLocation is null or empty', + defaultMessage: 'Storage Location not assigned', + }, + responsibleDepartment: { + id: 'detailList.aside.collectionobject.responsibleDepartment', + description: 'The prefix for responsible department in the search detail view', + defaultMessage: 'Responsible Department:', + }, + }); + + const locationData = formatRefNameWithDefault(data.get('computedCurrentLocation')); + const responsibleDepartmentData = data.get('responsibleDepartment'); + + const location = locationData ? ( +{locationData}
+{responsibleDepartmentData}
+{text}
: null; + }, + }, + description: { + formatter: (data) => { + const agent = formatAgent(data) || formatFieldCollector(data); + return agent ?{agent}
: null; + }, + }, + tags: { + // unused in core + formatter: () => null, + }, + }; +}; diff --git a/src/plugins/recordTypes/collectionobject/index.js b/src/plugins/recordTypes/collectionobject/index.js index 42b67c280..b81b03358 100644 --- a/src/plugins/recordTypes/collectionobject/index.js +++ b/src/plugins/recordTypes/collectionobject/index.js @@ -1,12 +1,15 @@ import advancedSearch from './advancedSearch'; import columns from './columns'; +import detailList from './detailList'; import fields from './fields'; import forms from './forms'; +import grid from './grid'; import idGenerators from './idGenerators'; import messages from './messages'; import optionLists from './optionLists'; import prepareForSending from './prepareForSending'; import serviceConfig from './serviceConfig'; +import sort from './sort'; import title from './title'; export default () => (configContext) => ({ @@ -17,11 +20,14 @@ export default () => (configContext) => ({ messages, prepareForSending, serviceConfig, + sort, advancedSearch: advancedSearch(configContext), columns: columns(configContext), + detailList: detailList(configContext), defaultForSearch: true, // Is this the default in search dropdowns? fields: fields(configContext), forms: forms(configContext), + grid: grid(configContext), title: title(configContext), }, }, diff --git a/src/plugins/recordTypes/collectionobject/serviceConfig.js b/src/plugins/recordTypes/collectionobject/serviceConfig.js index 9831f5d95..e6c3c2225 100644 --- a/src/plugins/recordTypes/collectionobject/serviceConfig.js +++ b/src/plugins/recordTypes/collectionobject/serviceConfig.js @@ -6,6 +6,10 @@ export default { objectName: 'CollectionObject', documentName: 'collectionobjects', + features: { + updatedSearch: true, + }, + quickAddData: (values) => ({ document: { 'ns2:collectionobjects_common': { diff --git a/src/plugins/recordTypes/collectionobject/sort.js b/src/plugins/recordTypes/collectionobject/sort.js new file mode 100644 index 000000000..3a13178f7 --- /dev/null +++ b/src/plugins/recordTypes/collectionobject/sort.js @@ -0,0 +1,74 @@ +import { defineMessages } from 'react-intl'; + +/** + * We store a key which is the value of the sort option, then use the 'sortBy' to store + * the actual field associated with the key. This is to keep some compatibility with how + * things have previously worked so that we aren't changing too much at once. + */ +export default { + objectNumber: { + messages: defineMessages({ + label: { + id: 'sortBy.collectionobjects.objectNumber', + defaultMessage: 'Identification number', + }, + }), + sortBy: 'collectionobjects_common:objectNumber', + }, + objectName: { + messages: defineMessages({ + label: { + id: 'sortBy.collectionobjects.objectName', + defaultMessage: 'Object name', + }, + }), + sortBy: 'collectionobjects_common:objectNameList/0/objectName', + }, + objectNameControlled: { + messages: defineMessages({ + label: { + id: 'sortBy.collectionobjects.objectNameControlled', + defaultMessage: 'Object name controlled', + }, + }), + sortBy: 'collectionobjects_common:objectNameList/0/objectNameControlled', + }, + title: { + messages: defineMessages({ + label: { + id: 'sortBy.collectionobjects.title', + defaultMessage: 'Title', + }, + }), + sortBy: 'collectionobjects_common:titleGroupList/0/title', + }, + updatedAt: { + messages: defineMessages({ + label: { + id: 'sortBy.collectionobjects.updatedAt', + defaultMessage: 'Updated at', + }, + }), + sortBy: 'collectionspace_core:updatedAt', + }, + createdAt: { + messages: defineMessages({ + label: { + id: 'sortBy.collectionobjects.createdAt', + defaultMessage: 'Created at', + }, + }), + sortBy: 'collectionspace_core:createdAt', + }, + computedCurrentLocation: { + messages: defineMessages({ + label: { + id: 'sortBy.collectionobjects.computedCurrentLocation', + defaultMessage: 'Computed current location', + }, + }), + sortBy: 'collectionobjects_common:computedCurrentLocation', + }, + defaultSortBy: 'updatedAt', + defaultSortDirection: 'desc', +}; diff --git a/src/plugins/recordTypes/dutyofcare/fields.js b/src/plugins/recordTypes/dutyofcare/fields.js index 01029a36d..81c63e7d7 100644 --- a/src/plugins/recordTypes/dutyofcare/fields.js +++ b/src/plugins/recordTypes/dutyofcare/fields.js @@ -254,7 +254,7 @@ export default (configContext) => { messages: defineMessages({ name: { id: 'field.dutiesofcare_common.partiesInvolvedGroup.name', - defaultMessage: 'Party involved', + defaultMessage: 'Parties involved', }, }), repeating: true, @@ -270,7 +270,7 @@ export default (configContext) => { messages: defineMessages({ fullName: { id: 'field.dutiesofcare_common.involvedParty.fullName', - defaultMessage: 'Party involved person', + defaultMessage: 'Parties involved person', }, name: { id: 'field.dutiesofcare_common.involvedParty.name', @@ -290,7 +290,7 @@ export default (configContext) => { messages: defineMessages({ fullName: { id: 'field.dutiesofcare_common.involvedOnBehalfOf.fullName', - defaultMessage: 'Party involved on behalf of', + defaultMessage: 'Parties involved on behalf of', }, name: { id: 'field.dutiesofcare_common.involvedOnBehalfOf.name', @@ -310,7 +310,7 @@ export default (configContext) => { messages: defineMessages({ fullName: { id: 'field.dutiesofcare_common.involvedRole.fullName', - defaultMessage: 'Party involved roles', + defaultMessage: 'Parties involved roles', }, name: { id: 'field.dutiesofcare_common.involvedRole.name', diff --git a/src/plugins/recordTypes/index.js b/src/plugins/recordTypes/index.js index 8bef6b73d..18ba9a3a8 100644 --- a/src/plugins/recordTypes/index.js +++ b/src/plugins/recordTypes/index.js @@ -1,5 +1,6 @@ import account from './account'; import acquisition from './acquisition'; +import advancedsearch from './advancedsearch'; import all from './all'; import audit from './audit'; import authority from './authority'; @@ -54,6 +55,7 @@ import work from './work'; export default [ account, acquisition, + advancedsearch, all, audit, authority, diff --git a/src/plugins/recordTypes/summarydocumentation/fields.js b/src/plugins/recordTypes/summarydocumentation/fields.js index e7b16291b..369435026 100644 --- a/src/plugins/recordTypes/summarydocumentation/fields.js +++ b/src/plugins/recordTypes/summarydocumentation/fields.js @@ -191,7 +191,7 @@ export default (configContext) => { messages: defineMessages({ name: { id: 'field.summarydocumentations_common.partiesInvolvedGroup.name', - defaultMessage: 'Party involved', + defaultMessage: 'Parties involved', }, }), repeating: true, @@ -207,7 +207,7 @@ export default (configContext) => { messages: defineMessages({ fullName: { id: 'field.summarydocumentations_common.involvedParty.fullName', - defaultMessage: 'Party involved person', + defaultMessage: 'Parties involved person', }, name: { id: 'field.summarydocumentations_common.involvedParty.name', @@ -227,7 +227,7 @@ export default (configContext) => { messages: defineMessages({ fullName: { id: 'field.summarydocumentations_common.involvedOnBehalfOf.fullName', - defaultMessage: 'Party involved on behalf of', + defaultMessage: 'Parties involved on behalf of', }, name: { id: 'field.summarydocumentations_common.involvedOnBehalfOf.name', @@ -247,7 +247,7 @@ export default (configContext) => { messages: defineMessages({ fullName: { id: 'field.summarydocumentations_common.involvedRole.fullName', - defaultMessage: 'Party involved role', + defaultMessage: 'Parties involved role', }, name: { id: 'field.summarydocumentations_common.involvedRole.name', diff --git a/src/reducers/index.js b/src/reducers/index.js index 2d530bdb4..b2ccb97a2 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -141,6 +141,14 @@ export const getAdvancedSearchBooleanOp = (state) => ( fromPrefs.getAdvancedSearchBooleanOp(state.prefs) ); +export const getAdvancedSearchNewBooleanOp = (state, searchTermsGroup) => ( + fromPrefs.getAdvancedSearchNewBooleanOp(state.prefs, searchTermsGroup) +); + +export const getSearchNewCondition = (state, recordType, searchTermsGroup) => ( + fromPrefs.getSearchNewCondition(state.prefs, recordType, searchTermsGroup) +); + export const isPanelCollapsed = (state, recordType, name) => ( fromPrefs.isPanelCollapsed(state.prefs, recordType, name) ); @@ -157,6 +165,10 @@ export const getSearchResultPagePageSize = (state) => ( fromPrefs.getSearchResultPagePageSize(state.prefs) ); +export const getSearchResultPageView = (state) => ( + fromPrefs.getSearchResultPageView(state.prefs) +); + export const getSearchToSelectPageSize = (state) => ( fromPrefs.getSearchToSelectPageSize(state.prefs) ); @@ -169,6 +181,10 @@ export const getAdminTab = (state) => fromPrefs.getAdminTab(state.prefs); export const getToolTab = (state) => fromPrefs.getToolTab(state.prefs); +export const getNewSearchShown = (state) => fromPrefs.getNewSearchShown(state.prefs); + +export const getUseNewSearch = (state) => fromPrefs.getUseNewSearch(state.prefs); + export const getOptionList = (state, optionListName) => ( fromOptionList.get(state.optionList, optionListName) ); @@ -186,6 +202,12 @@ export const getIDGenerator = (state, idGeneratorName) => ( ); export const getSearchPageAdvanced = (state) => fromSearchPage.getAdvanced(state.searchPage); +export const getSearchPageAdvancedLimitBy = (state) => ( + fromSearchPage.getAdvancedLimitBy(state.searchPage) +); +export const getSearchPageAdvancedSearchTerms = (state) => ( + fromSearchPage.getAdvancedSearchTerms(state.searchPage) +); export const getSearchPageKeyword = (state) => fromSearchPage.getKeyword(state.searchPage); diff --git a/src/reducers/prefs.js b/src/reducers/prefs.js index 8c14591b7..511e87660 100644 --- a/src/reducers/prefs.js +++ b/src/reducers/prefs.js @@ -14,6 +14,7 @@ import { SET_QUICK_SEARCH_VOCABULARY, SET_SEARCH_PANEL_PAGE_SIZE, SET_SEARCH_RESULT_PAGE_PAGE_SIZE, + SET_SEARCH_RESULT_PAGE_VIEW, SET_SEARCH_TO_SELECT_PAGE_SIZE, SET_FORM, SET_UPLOAD_TYPE, @@ -22,10 +23,14 @@ import { SET_STICKY_FIELDS, SET_SEARCH_PAGE_ADVANCED, SET_SEARCH_TO_SELECT_ADVANCED, + TOGGLE_USE_NEW_SEARCH, + SET_NEW_SEARCH_SHOWN, + SET_SEARCH_PAGE_ADVANCED_SEARCH_TERMS, + SET_SEARCH_PAGE_ADVANCED_LIMIT_BY, } from '../constants/actionCodes'; const handleAdvancedSearchConditionChange = (state, action) => { - const { recordType } = action.meta; + const { recordType, searchTermsGroup } = action.meta; if (!recordType) { return state; @@ -37,11 +42,15 @@ const handleAdvancedSearchConditionChange = (state, action) => { let nextState = state; if (op === OP_AND || op === OP_OR) { - nextState = nextState.set('advancedSearchBooleanOp', op); + nextState = searchTermsGroup + ? nextState.setIn(['advancedSearchNewBooleanOp', searchTermsGroup], op) + : nextState.set('advancedSearchBooleanOp', op); } nextState = nextState.setIn( - ['searchCond', recordType], + searchTermsGroup + ? ['searchNewCond', recordType, searchTermsGroup] + : ['searchCond', recordType], clearAdvancedSearchConditionValues(condition), ); @@ -111,6 +120,8 @@ export default (state = Immutable.Map(), action) => { ); case SET_SEARCH_RESULT_PAGE_PAGE_SIZE: return state.set('searchResultPagePageSize', action.payload); + case SET_SEARCH_RESULT_PAGE_VIEW: + return state.set('searchResultPageView', action.payload); case SET_SEARCH_TO_SELECT_PAGE_SIZE: return state.set('searchToSelectPageSize', action.payload); case SET_FORM: @@ -119,6 +130,8 @@ export default (state = Immutable.Map(), action) => { return state.set('uploadType', action.payload); case SET_SEARCH_PAGE_ADVANCED: case SET_SEARCH_TO_SELECT_ADVANCED: + case SET_SEARCH_PAGE_ADVANCED_SEARCH_TERMS: + case SET_SEARCH_PAGE_ADVANCED_LIMIT_BY: return handleAdvancedSearchConditionChange(state, action); case TOGGLE_RECORD_SIDEBAR: return handleToggleRecordSidebar(state, action); @@ -126,6 +139,11 @@ export default (state = Immutable.Map(), action) => { return handleToggleSearchResultSidebar(state, action); case SET_STICKY_FIELDS: return setStickyFields(state, action); + case TOGGLE_USE_NEW_SEARCH: + return state.set('useNewSearch', typeof state.get('useNewSearch') === 'undefined' ? false + : !state.get('useNewSearch')); + case SET_NEW_SEARCH_SHOWN: + return state.set('newSearchShown', true); default: return state; } @@ -135,6 +153,10 @@ export const getAdvancedSearchBooleanOp = (state) => state.get('advancedSearchBo export const getSearchCondition = (state, recordType) => state.getIn(['searchCond', recordType]); +export const getAdvancedSearchNewBooleanOp = (state, searchTermsGroup) => state.getIn(['advancedSearchNewBooleanOp', searchTermsGroup]); + +export const getSearchNewCondition = (state, recordType, searchTermsGroup) => state.getIn(['searchNewCond', recordType, searchTermsGroup]); + export const getSearchPageRecordType = (state) => state.getIn(['searchPage', 'recordType']); export const getSearchPageVocabulary = (state, recordType) => state.getIn(['searchPage', 'vocabulary', recordType]); @@ -147,6 +169,8 @@ export const getSearchPanelPageSize = (state, recordType, name) => state.getIn([ export const getSearchResultPagePageSize = (state) => state.get('searchResultPagePageSize'); +export const getSearchResultPageView = (state) => state.get('searchResultPageView'); + export const getSearchToSelectPageSize = (state) => state.get('searchToSelectPageSize'); export const isPanelCollapsed = (state, recordType, name) => state.getIn(['panels', recordType, name, 'collapsed']); @@ -161,6 +185,10 @@ export const getAdminTab = (state) => state.get('adminTab'); export const getToolTab = (state) => state.get('toolTab'); +export const getNewSearchShown = (state) => state.get('newSearchShown'); + +export const getUseNewSearch = (state) => state.get('useNewSearch'); + export const isRecordSidebarOpen = (state) => state.get('recordSidebarOpen'); export const isSearchResultSidebarOpen = (state) => state.get('searchResultSidebarOpen'); diff --git a/src/reducers/searchPage.js b/src/reducers/searchPage.js index 8e05a97bb..a907c276d 100644 --- a/src/reducers/searchPage.js +++ b/src/reducers/searchPage.js @@ -3,6 +3,8 @@ import Immutable from 'immutable'; import { CLEAR_SEARCH_PAGE, SET_SEARCH_PAGE_ADVANCED, + SET_SEARCH_PAGE_ADVANCED_LIMIT_BY, + SET_SEARCH_PAGE_ADVANCED_SEARCH_TERMS, SET_SEARCH_PAGE_KEYWORD, SET_SEARCH_PAGE_RECORD_TYPE, } from '../constants/actionCodes'; @@ -11,10 +13,17 @@ export default (state = Immutable.Map(), action) => { switch (action.type) { case SET_SEARCH_PAGE_ADVANCED: return state.set('advanced', Immutable.fromJS(action.payload)); + case SET_SEARCH_PAGE_ADVANCED_LIMIT_BY: + return state.set('advancedLimitBy', Immutable.fromJS(action.payload)); + case SET_SEARCH_PAGE_ADVANCED_SEARCH_TERMS: + return state.set('advancedSearchTerms', Immutable.fromJS(action.payload)); case SET_SEARCH_PAGE_KEYWORD: return state.set('keyword', action.payload); case SET_SEARCH_PAGE_RECORD_TYPE: - return state.delete('advanced'); + return state + .delete('advanced') + .delete('advancedLimitBy') + .delete('advancedSearchTerms'); case CLEAR_SEARCH_PAGE: return state.clear(); default: @@ -23,4 +32,7 @@ export default (state = Immutable.Map(), action) => { }; export const getAdvanced = (state) => state.get('advanced'); +export const getAdvancedLimitBy = (state) => state.get('advancedLimitBy'); +export const getAdvancedSearchTerms = (state) => state.get('advancedSearchTerms'); + export const getKeyword = (state) => state.get('keyword'); diff --git a/styles/colors.css b/styles/colors.css index fa7a04826..eada988d2 100644 --- a/styles/colors.css +++ b/styles/colors.css @@ -4,3 +4,7 @@ @value textLabel: rgb(80, 80, 80); @value inputBg: rgb(255, 255, 255); + +@value buttonBg: #73AA4F; +@value buttonOutlinedBg: rgb(255, 255, 255); +@value buttonOutlinedBgActive: rgb(220, 220, 220); diff --git a/styles/cspace-ui/Footer.css b/styles/cspace-ui/Footer.css index bd4a0123e..eaf73b0e8 100644 --- a/styles/cspace-ui/Footer.css +++ b/styles/cspace-ui/Footer.css @@ -13,8 +13,13 @@ padding: 0 10px; } +.common > ul li { + line-height: 24px; +} + .common, .common a { color: rgb(115, 115, 115); + padding: 4px 0; } .common > ul:last-of-type { diff --git a/styles/cspace-ui/Image.css b/styles/cspace-ui/Image.css index 6d1e6865d..103b782b1 100644 --- a/styles/cspace-ui/Image.css +++ b/styles/cspace-ui/Image.css @@ -18,3 +18,14 @@ composes: common; padding: 10px; } + +.noimage { + display: flex; + width: 100%; + height: 100%; + background-color: rgba(250, 250, 250); + align-items: center; + justify-content: center; + font-size: inherit; + font-weight: 600; +} diff --git a/styles/cspace-ui/Pager.css b/styles/cspace-ui/Pager.css index 5d520d222..955f167f2 100644 --- a/styles/cspace-ui/Pager.css +++ b/styles/cspace-ui/Pager.css @@ -1,4 +1,5 @@ @value inputHeight from '../dimensions.css'; +@value inputBg from '../colors.css'; .common { font-size: 12px; @@ -56,3 +57,10 @@ .common > nav > :global(.cspace-input-MiniButton--common) { padding: 0 10px; } + +.current > :global(.cspace-input-MiniButton--common) { + background-color: inputBg; + font-weight: bold; + border: 1px solid black; + color: black; +} diff --git a/styles/cspace-ui/RootPage.css b/styles/cspace-ui/RootPage.css index 0d6fede17..656c62ff6 100644 --- a/styles/cspace-ui/RootPage.css +++ b/styles/cspace-ui/RootPage.css @@ -1,5 +1,5 @@ @import 'https://fonts.googleapis.com/css?family=Open+Sans:400,600'; -@import 'https://fonts.googleapis.com/icon?family=Material+Icons'; +@import 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@40,400,0,0&icon_names=close,format_list_bulleted,grid_view,vertical_split'; body { margin: 0; @@ -14,7 +14,16 @@ body { } body :global(.material-icons) { - font-size: 12px; + font-family: 'Material Symbols Outlined'; + font-weight: normal; + font-style: normal; + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; } a { diff --git a/styles/cspace-ui/SearchForm.css b/styles/cspace-ui/SearchForm.css index 6461e02b7..10393d689 100644 --- a/styles/cspace-ui/SearchForm.css +++ b/styles/cspace-ui/SearchForm.css @@ -15,3 +15,7 @@ .common > footer > :global(.cspace-ui-ButtonBar--common) { text-align: right; } + +.common .mb12 { + margin-bottom: 12px; +} diff --git a/styles/cspace-ui/SearchGrid.css b/styles/cspace-ui/SearchGrid.css new file mode 100644 index 000000000..2569930a0 --- /dev/null +++ b/styles/cspace-ui/SearchGrid.css @@ -0,0 +1,59 @@ +.grid { + display: grid; + justify-items: center; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; +} + +.grid :global(.cspace-ui-Image--noimage) { + aspect-ratio: 1/1; + margin: 10px; + width: auto; + height: auto; +} + +.card { + display: flex; + flex-direction: column; + background:rgb(255, 255, 255); + width: 100%; + border: 1px solid rgb(220, 220, 220); + box-sizing: border-box; +} + +.card img { + padding: 10px; + max-width: 100%; + object-fit: cover; +} + +.summary { + padding: 5px 10px; + display: flex; +} + +.summary > p { + font-weight: bold; + margin: 0 0 0 5; +} + +.info { + display: flex; + flex-direction: column; + margin-left: 1em; +} + +.info h2 { + margin: 0; + font-size: 1em; + font-weight: bolder; +} + +.info p { + margin-bottom: 0; + margin-top: 0; +} + +.mt10 { + margin-top: 10px; +} diff --git a/styles/cspace-ui/SearchList.css b/styles/cspace-ui/SearchList.css new file mode 100644 index 000000000..69f321400 --- /dev/null +++ b/styles/cspace-ui/SearchList.css @@ -0,0 +1,79 @@ +.detail { + display: flex; + flex-direction: column; + gap: calc(.25rem * 4); +} + +.innerDetail { + display: flex; + flex-direction: row; + border: 1px solid rgb(220, 220, 220); + padding: 30px; +} + +.imageContainer { + width: 20%; + margin-right: 1em; +} + +.imageContainer > a { + text-decoration: none; +} + +.imageContainer > img { + max-width: 100%; +} + +.imageContainer > * > img { + max-width: 100%; +} + +.detailList { + list-style: none; + padding-left: 0; + margin-top: 0; +} + +.description { + margin-left: 1em; + margin-right: 1em; + width: 55%; +} + +.description h2 { + /** + * Remove some of the vertical spacing from the h2 + * to better align it with the other content + */ + margin-top: -7px; + margin-bottom: 0; + font-weight: bold; +} + +.description > h3 { + margin-top: 0; + margin-bottom: 0; +} + +.info { + flex-grow: 1; + text-align: center; +} + +.info > aside { + background-color: aliceblue; + height: 200px; + margin-bottom: 1em; + display: flex; + flex-direction: column; + justify-content: space-evenly; +} + +.info > aside > div > span { + font-weight: bold; +} + +.info > aside > div > p { + margin-top: 0; + margin-bottom: 0; +} diff --git a/styles/cspace-ui/SearchPage.css b/styles/cspace-ui/SearchPage.css index b3dd19f2c..99b84f39d 100644 --- a/styles/cspace-ui/SearchPage.css +++ b/styles/cspace-ui/SearchPage.css @@ -1,3 +1,20 @@ +@value buttonOutlinedBg, buttonOutlinedBgActive, textDark from '../colors.css'; + .common { background-color: rgb(250, 250, 250); } +/* TODO: Move this to shared lib */ +.toggleButton button { + background-color: buttonOutlinedBg; + border: 1px solid; + color: textDark; +} +.toggleButton button:hover:not(:disabled) { + background-color: buttonOutlinedBgActive; +} +.toggleButton button:focus { + box-shadow: 0 0 0 2px buttonOutlinedBgActive; +} +.toggleButton a { + margin-left: 10px; +} diff --git a/styles/cspace-ui/SearchResultSummary.css b/styles/cspace-ui/SearchResultSummary.css index ca32e08c3..b9d639af3 100644 --- a/styles/cspace-ui/SearchResultSummary.css +++ b/styles/cspace-ui/SearchResultSummary.css @@ -47,3 +47,11 @@ .error { composes: common; } + +.flex { + display: flex; +} + +.flexInitial { + flex: 0 auto; +} \ No newline at end of file diff --git a/styles/cspace-ui/SearchResults.css b/styles/cspace-ui/SearchResults.css new file mode 100644 index 000000000..809d05faa --- /dev/null +++ b/styles/cspace-ui/SearchResults.css @@ -0,0 +1,34 @@ +.common { + background: rgb(250, 250, 250); +} + +.body { + display: flex; +} + +.full { + composes: body; +} + +.results { + flex: 1 1 auto; + background-color: white; +} + +.body > .results { + padding: 0 10px 10px 0; + width: 70%; +} + +.full > .results { + padding: 0 0 10px 0; +} + +.detailList { + transform: rotate(180deg); +} + +.toggle { + padding: 6px; + font-size: 18px; +} diff --git a/styles/cspace-ui/SearchTable.css b/styles/cspace-ui/SearchTable.css new file mode 100644 index 000000000..b75ca48a5 --- /dev/null +++ b/styles/cspace-ui/SearchTable.css @@ -0,0 +1,69 @@ +.results > table { + font-family: "Open Sans", Arial, sans-serif; + font-size: 14px; + font-weight: 400; + table-layout: auto; + width: 100%; + border-collapse: collapse; +} + +/* todo: flex col sizing */ +.results > table > thead { + border: 1px solid rgb(220, 220, 220); + border-width: 1px 0; + background: rgb(240, 240, 240); + box-sizing: border-box; + text-transform: none; + font-size: 12px; + font-weight: inherit; +} + +.results > table > thead > tr { + height: 23px; + overflow: hidden; + padding-right: 0px; +} + +.results > table > thead > tr > th { + cursor: pointer; + font-size: 12px; + font-weight: inherit; + text-decoration: underline; +} + +.results > table > thead > tr > th:focus { + outline: 1px dotted black; +} + +/* todo: flex col sizing */ +.results > table > tbody > td { + height: 23px; + left: 0px; + position: absolute; + top: 0px; + width: 1856px; + padding-right: 0px; +} + +.results > table > tbody > tr:hover { + background-color: #ACC9EB; + cursor: pointer; +} + +.results > table > tbody > tr:focus { + outline: 1px dotted black; +} + +.results > table > tbody > tr > a { + width: 100%; + display: inline-block; + text-decoration: none; +} + +.even { + background: white; +} + +.odd { + background: #f0f5fb; +} diff --git a/styles/cspace-ui/SelectBar.css b/styles/cspace-ui/SelectBar.css index 861a96892..63551ddf6 100644 --- a/styles/cspace-ui/SelectBar.css +++ b/styles/cspace-ui/SelectBar.css @@ -2,7 +2,6 @@ .common { display: flex; - align-items: baseline; border-top: 1px solid rgb(220, 220, 220); padding: 4px 10px; background: rgb(240, 240, 240); diff --git a/styles/cspace-ui/SortBy.css b/styles/cspace-ui/SortBy.css new file mode 100644 index 000000000..66270b714 --- /dev/null +++ b/styles/cspace-ui/SortBy.css @@ -0,0 +1,30 @@ +@value buttonBg from '../colors.css'; +@value inputHeight from '../dimensions.css'; + +.flex { + display: flex; + margin-right: inputHeight; +} + +.mt2 { + margin-top: 2px; +} + +.mr5 { + margin-right: 5px; +} + +.sortByButton { + height: inputHeight; + width: inputHeight; + border-radius: 0 3px 3px 0; + background-color: buttonBg !important; +} + +.ascending { + background-image: url(../../images/collapseWhite.svg) !important; +} + +.descending { + background-image: url(../../images/expandWhite.svg) !important; +} diff --git a/test/helpers/utils.js b/test/helpers/utils.js new file mode 100644 index 000000000..5ab9ceebe --- /dev/null +++ b/test/helpers/utils.js @@ -0,0 +1,6 @@ +export default function throwAxeViolationsError(violations) { + const violationMessages = violations.map( + (violation) => `${violation.id}: ${violation.description} (${violation.nodes.length} nodes)`, + ).join('\n'); + throw new Error(`Accessibility violations found:\n${violationMessages}`); +} diff --git a/test/specs/actions/relation.spec.js b/test/specs/actions/relation.spec.js index 177a1ed8b..d3d1eabb1 100644 --- a/test/specs/actions/relation.spec.js +++ b/test/specs/actions/relation.spec.js @@ -28,7 +28,6 @@ import { clearState, find, create, - createBidirectional, batchCreate, batchCreateBidirectional, deleteRelation, @@ -333,72 +332,6 @@ describe('relation action creator', () => { afterEach(() => { worker.resetHandlers(); }); - - it('should dispatch RELATION_SAVE_FULFILLED twice, once in each direction', () => { - const store = mockStore({ - relation: Immutable.Map(), - }); - - worker.use( - rest.post(createUrl, (req, res, ctx) => res( - ctx.status(201), - ctx.set('location', 'some/new/url'), - )), - ); - - return store.dispatch(createBidirectional(subject, object, predicate)) - .then(() => { - const actions = store.getActions(); - - actions.should.have.lengthOf(5); - - actions[0].should.deep.equal({ - type: RELATION_SAVE_STARTED, - meta: { - subject, - object, - predicate, - }, - }); - - actions[1].type.should.equal(RELATION_SAVE_FULFILLED); - actions[1].payload.status.should.equal(201); - actions[1].payload.headers.location.should.equal('some/new/url'); - - actions[1].meta.should.deep.equal({ - subject, - object, - predicate, - }); - - actions[2].should.deep.equal({ - type: RELATION_SAVE_STARTED, - meta: { - subject: object, - object: subject, - predicate, - }, - }); - - actions[3].type.should.equal(RELATION_SAVE_FULFILLED); - actions[3].payload.status.should.equal(201); - actions[3].payload.headers.location.should.equal('some/new/url'); - - actions[3].meta.should.deep.equal({ - subject: object, - object: subject, - predicate, - }); - - actions[4].should.contain({ - type: SUBJECT_RELATIONS_UPDATED, - }); - - actions[4].meta.should.contain({ - subject, - }); - }); - }); }); describe('batchCreate', () => { @@ -682,7 +615,7 @@ describe('relation action creator', () => { worker.resetHandlers(); }); - it('should dispatch RELATION_SAVE_FULFILLED twice for each object', () => { + it('should dispatch RELATION_SAVE_STARTED and SUBJECT_RELATIONS_UPDATED once for all objects', () => { const store = mockStore({ relation: Immutable.Map(), }); @@ -726,99 +659,28 @@ describe('relation action creator', () => { }, ]; - return store.dispatch(batchCreateBidirectional(subject, objects, predicate)) + return store.dispatch(batchCreateBidirectional([subject], objects, predicate)) .then(() => { const actions = store.getActions(); - actions.should.have.lengthOf(10); + actions.should.have.lengthOf(3); actions[0].should.deep.equal({ type: RELATION_SAVE_STARTED, meta: { - subject, - object: objects[0], + subjects: [subject], + objects, predicate, }, }); - actions[1].type.should.equal(RELATION_SAVE_FULFILLED); - actions[1].payload.status.should.equal(201); - actions[1].payload.headers.location.should.equal('some/new/url'); - - actions[1].meta.should.deep.equal({ - subject, - object: objects[0], - predicate, - }); - - actions[2].should.deep.equal({ - type: RELATION_SAVE_STARTED, - meta: { - subject: objects[0], - object: subject, - predicate, - }, - }); - - actions[3].type.should.equal(RELATION_SAVE_FULFILLED); - actions[3].payload.status.should.equal(201); - actions[3].payload.headers.location.should.equal('some/new/url'); - - actions[3].meta.should.deep.equal({ - subject: objects[0], - object: subject, - predicate, - }); - - actions[4].should.deep.equal({ - type: RELATION_SAVE_STARTED, - meta: { - subject, - object: objects[1], - predicate, - }, - }); - - actions[5].type.should.equal(RELATION_SAVE_FULFILLED); - actions[5].payload.status.should.equal(201); - actions[5].payload.headers.location.should.equal('some/new/url'); - - actions[5].meta.should.deep.equal({ - subject, - object: objects[1], - predicate, - }); - - actions[6].should.deep.equal({ - type: RELATION_SAVE_STARTED, - meta: { - subject: objects[1], - object: subject, - predicate, - }, - }); - - actions[7].type.should.equal(RELATION_SAVE_FULFILLED); - actions[7].payload.status.should.equal(201); - actions[7].payload.headers.location.should.equal('some/new/url'); - - actions[7].meta.should.deep.equal({ - subject: objects[1], - object: subject, - predicate, - }); - - actions[8].should.contain({ + actions[1].should.contain({ type: SHOW_NOTIFICATION, }); - actions[9].should.contain({ + actions[2].should.contain({ type: SUBJECT_RELATIONS_UPDATED, }); - - actions[9].meta.should.contain({ - subject, - }); }); }); @@ -863,7 +725,7 @@ describe('relation action creator', () => { }, ]; - return store.dispatch(batchCreateBidirectional(subject, objects, predicate)) + return store.dispatch(batchCreateBidirectional([subject], objects, predicate)) .then(() => { const actions = store.getActions(); @@ -872,8 +734,8 @@ describe('relation action creator', () => { actions[0].should.deep.equal({ type: RELATION_SAVE_STARTED, meta: { - subject, - object: objects[0], + subjects: [subject], + objects, predicate, }, }); @@ -888,7 +750,7 @@ describe('relation action creator', () => { }); }); - it('should dispatch nothing for each object that is already related to the subject', () => { + it('should dispatch RELATION_SAVE_REJECTED for each object that is already related to the subject', () => { const store = mockStore({ relation: Immutable.Map(), }); @@ -927,25 +789,75 @@ describe('relation action creator', () => { }, ]; - return store.dispatch(batchCreateBidirectional(subject, objects, predicate)) + return store.dispatch(batchCreateBidirectional([subject], objects, predicate)) .then(() => { const actions = store.getActions(); - actions.should.have.lengthOf(2); + actions.should.have.lengthOf(3); actions[0].should.contain({ - type: SHOW_NOTIFICATION, + type: RELATION_SAVE_STARTED, }); actions[1].should.contain({ - type: SUBJECT_RELATIONS_UPDATED, + type: RELATION_SAVE_REJECTED, }); - actions[1].meta.should.contain({ - subject, + actions[2].should.contain({ + type: SHOW_NOTIFICATION, }); }); }); + + it('should dispatch one SHOW_NOTIFICATION when relating more than 5 subjects', async () => { + const store = mockStore({ + relation: Immutable.Map(), + }); + + worker.use( + rest.get(checkUrl, (req, res, ctx) => res(ctx.status(200))), + + rest.post(createUrl, (req, res, ctx) => res( + ctx.status(201), + ctx.set('location', 'some/new/url'), + )), + ); + + const subjects = Array.from({ length: 6 }, (_, i) => ({ csid: `subject${i}`, title: `Subject ${i}` })); + const objects = Array.from({ length: 6 }, (_, i) => ({ csid: `object${i}`, title: `Object ${i}` })); + + await store.dispatch(batchCreateBidirectional(subjects, objects, predicate)); + + const actions = store.getActions(); + + actions.should.have.lengthOf(8); + actions.filter((a) => a.type === 'SHOW_NOTIFICATION').should.have.lengthOf(1); + }); + + it('should dispatch n SHOW_NOTIFICATION when relating n subjects, where n less than 6', async () => { + const store = mockStore({ + relation: Immutable.Map(), + }); + + worker.use( + rest.get(checkUrl, (req, res, ctx) => res(ctx.status(200))), + + rest.post(createUrl, (req, res, ctx) => res( + ctx.status(201), + ctx.set('location', 'some/new/url'), + )), + ); + + const subjects = Array.from({ length: 5 }, (_, i) => ({ csid: `subject${i}`, title: `Subject ${i}` })); + const objects = Array.from({ length: 5 }, (_, i) => ({ csid: `object${i}`, title: `Object ${i}` })); + + await store.dispatch(batchCreateBidirectional(subjects, objects, predicate)); + + const actions = store.getActions(); + + actions.should.have.lengthOf(11); + actions.filter((a) => a.type === 'SHOW_NOTIFICATION').should.have.lengthOf(5); + }); }); describe('deleteRelation', () => { diff --git a/test/specs/actions/report.spec.js b/test/specs/actions/report.spec.js index d4b0fae87..14addfbc3 100644 --- a/test/specs/actions/report.spec.js +++ b/test/specs/actions/report.spec.js @@ -6,7 +6,6 @@ import chaiImmutable from 'chai-immutable'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { setupWorker, rest } from 'msw'; -import qs from 'qs'; import { SHOW_NOTIFICATION, @@ -277,7 +276,7 @@ describe('report action creator', () => { return store.dispatch(openReport(config, reportMetadata, invocationDescriptor)) .then(() => { - openedPath.should.equal(`/report/${reportCsid}?mode=single&csid=${recordCsid}&outputMIME=${outputMIME}&recordType=${recordType}`); + openedPath.should.equal(`/report/${reportCsid}?mode=single&outputMIME=${outputMIME}&recordType=${recordType}`); window.open = savedWindowOpen; }); @@ -315,11 +314,7 @@ describe('report action creator', () => { return store.dispatch(openReport(config, reportMetadata, invocationDescriptor)) .then(() => { - const expectedParams = qs.stringify({ - params: JSON.stringify(params), - }); - - openedPath.should.equal(`/report/${reportCsid}?mode=single&csid=${recordCsid}&recordType=${recordType}&${expectedParams}`); + openedPath.should.equal(`/report/${reportCsid}?mode=single&recordType=${recordType}`); window.open = savedWindowOpen; }); diff --git a/test/specs/actions/search.spec.js b/test/specs/actions/search.spec.js index de811c4da..77a3bdc5d 100644 --- a/test/specs/actions/search.spec.js +++ b/test/specs/actions/search.spec.js @@ -98,6 +98,7 @@ describe('search action creator', () => { }, subresources: { terms: { + listType: 'common', serviceConfig: { servicePath: termsServicePath, }, diff --git a/test/specs/actions/searchPage.spec.js b/test/specs/actions/searchPage.spec.js index eb17da770..243898354 100644 --- a/test/specs/actions/searchPage.spec.js +++ b/test/specs/actions/searchPage.spec.js @@ -104,6 +104,7 @@ describe('search page action creator', () => { advanced: advancedSearchCondition, }), prefs: Immutable.fromJS({ + useNewSearch: false, searchPage: { recordType: 'loanin', }, diff --git a/test/specs/components/admin/AccountSearchBar.spec.jsx b/test/specs/components/admin/AccountSearchBar.spec.jsx index ab14bf16b..662d8bec1 100644 --- a/test/specs/components/admin/AccountSearchBar.spec.jsx +++ b/test/specs/components/admin/AccountSearchBar.spec.jsx @@ -1,9 +1,11 @@ import React from 'react'; import { Simulate } from 'react-dom/test-utils'; import { IntlProvider } from 'react-intl'; +import * as axe from 'axe-core'; import createTestContainer from '../../../helpers/createTestContainer'; import { render } from '../../../helpers/renderHelpers'; import AccountSearchBar from '../../../../src/components/admin/AccountSearchBar'; +import throwAxeViolationsError from '../../../helpers/utils'; chai.should(); @@ -12,7 +14,8 @@ describe('AccountSearchBar', () => { this.container = createTestContainer(this); }); - it('should render as a div', function test() { + // TODO: fix a11y violations + it.skip('should render as a div without a11y violations', async function test() { render(