From e2fe8c2293ba0c57f4646f025f8b8bb9b15089d2 Mon Sep 17 00:00:00 2001 From: Adam Malantonio Date: Wed, 1 Mar 2017 14:11:05 -0500 Subject: [PATCH 1/2] convert tabs to 2-spaces --- env.config.js | 6 +- karma.conf.js | 202 +-- lib/__tests__/facet-helpers-test.js | 198 +-- lib/__tests__/search-history-test.js | 134 +- lib/__tests__/sort-facets-test.js | 86 +- lib/api/request.js | 70 +- lib/api/search.js | 8 +- lib/api/terms.js | 42 +- lib/api/vocabulary.js | 28 +- lib/api/work.js | 18 +- lib/camel-case.js | 24 +- lib/colors.js | 8 +- lib/create-new-term.js | 24 +- lib/create-range-facet.js | 20 +- lib/facet-helpers.js | 98 +- lib/format-search-querystring.js | 96 +- lib/get-work-title.js | 18 +- lib/is-fresh.js | 18 +- lib/is-work-updated.js | 42 +- lib/pref-label.js | 20 +- lib/scroll-to-top.js | 8 +- lib/search-history.js | 42 +- lib/search-result-settings.js | 36 +- lib/sort-by-hits.js | 12 +- lib/sort-facets.js | 42 +- lib/work-fields.js | 130 +- src/App.jsx | 30 +- src/actions/__tests__/autocomplete-test.js | 128 +- .../__tests__/data/vocabularies-with-terms.js | 192 +- src/actions/__tests__/notifications-test.js | 38 +- src/actions/__tests__/search-test.js | 354 ++-- src/actions/__tests__/terms-test.js | 704 ++++---- src/actions/__tests__/vocabulary-test.js | 328 ++-- src/actions/__tests__/work-test.js | 256 +-- src/actions/autocomplete.js | 44 +- src/actions/notifications.js | 12 +- src/actions/search.js | 220 +-- src/actions/terms.js | 292 +-- src/actions/vocabulary.js | 218 +-- src/actions/work.js | 98 +- src/components/Button.jsx | 94 +- src/components/ModalWithHeader.jsx | 106 +- src/components/NavToSearchResults.jsx | 22 +- src/components/Navbar.jsx | 46 +- src/components/NotificationCenter.jsx | 140 +- src/components/Toggle.jsx | 178 +- src/components/catalog/Facet.jsx | 394 ++--- src/components/catalog/FacetGroup.jsx | 218 +-- src/components/catalog/FacetList.jsx | 134 +- .../catalog/FacetListSelectedItem.jsx | 98 +- .../catalog/FacetListWithViewMore.jsx | 292 +-- .../catalog/FacetRangeLimitDate.jsx | 172 +- src/components/catalog/RangeSliderDate.jsx | 332 ++-- src/components/catalog/ResultsContainer.jsx | 70 +- src/components/catalog/ResultsGallery.jsx | 30 +- src/components/catalog/ResultsGalleryItem.jsx | 76 +- src/components/catalog/ResultsPager.jsx | 322 ++-- src/components/catalog/ResultsTable.jsx | 386 ++-- .../catalog/ResultsTableFieldSelect.jsx | 174 +- src/components/catalog/SearchBreadcrumb.jsx | 188 +- .../catalog/SearchBreadcrumbTrail.jsx | 90 +- src/components/catalog/SearchFacetSidebar.jsx | 182 +- .../catalog/SearchResultsHeader.jsx | 128 +- .../catalog/SearchResultsPagerHeader.jsx | 128 +- .../catalog/__tests__/Facet-test.js | 68 +- .../catalog/__tests__/FacetList-test.jsx | 96 +- .../catalog/__tests__/FacetListItem-test.jsx | 76 +- .../__tests__/FacetListWithViewMore-test.jsx | 92 +- .../__tests__/FacetRangeLimitDate-test.jsx | 146 +- .../__tests__/RangeSliderDate-test.jsx | 322 ++-- .../catalog/__tests__/ResultsGallery-test.jsx | 10 +- .../__tests__/ResultsGalleryItem-test.jsx | 64 +- .../catalog/__tests__/ResultsPager-test.jsx | 180 +- .../catalog/__tests__/data/docs.json | 1562 ++++++++--------- .../catalog/__tests__/data/pages.json | 20 +- .../common/__tests__/calculate-range-test.js | 128 +- .../__tests__/format-date-value-test.js | 24 +- .../__tests__/round-date-to-interval-test.js | 82 +- .../catalog/common/calculate-range.js | 44 +- .../catalog/common/date-intervals.js | 6 +- .../catalog/common/format-date-value.js | 40 +- .../catalog/common/parse-input-date-value.js | 26 +- .../catalog/common/round-date-to-interval.js | 56 +- src/components/media/OpenSeadragonViewer.jsx | 170 +- src/components/media/PDFViewer.jsx | 26 +- src/components/media/PDFViewerTemplate.jsx | 618 +++---- src/components/media/ThumbnailPreview.jsx | 74 +- .../__tests__/OpenSeadragonViewer-test.jsx | 30 +- .../metadata/ControlledVocabularyInput.jsx | 320 ++-- src/components/metadata/DateInput.jsx | 106 +- src/components/metadata/FormField.jsx | 366 ++-- src/components/metadata/MetadataForm.jsx | 134 +- src/components/metadata/Select.jsx | 150 +- src/components/metadata/StringInput.jsx | 96 +- src/components/metadata/TextInput.jsx | 68 +- .../metadata/__tests__/DateInput-test.jsx | 158 +- .../metadata/__tests__/FormField-test.jsx | 188 +- .../metadata/__tests__/MetadataForm-test.jsx | 92 +- .../metadata/__tests__/StringInput-test.jsx | 144 +- .../metadata/__tests__/TextInput-test.jsx | 132 +- .../recent-items/RecentItemsList.jsx | 42 +- src/components/schema/GenericTerm.jsx | 142 +- src/components/schema/GenericVocabulary.jsx | 84 +- src/components/schema/GenericWork.jsx | 254 +-- .../__tests__/GenericVocabulary-test.jsx | 190 +- src/components/tags/Tag.jsx | 122 +- src/components/tags/TagList.jsx | 96 +- src/components/tags/__tests__/Tag-test.jsx | 110 +- .../tags/__tests__/TagList-test.jsx | 104 +- .../vocabulary/BulkTermsEditModal.jsx | 98 +- src/components/vocabulary/BulkTermsEditor.jsx | 430 ++--- .../vocabulary/CreateVocabularyModal.jsx | 100 +- .../vocabulary/EditVocabularyModal.jsx | 138 +- src/components/vocabulary/TermEditModal.jsx | 174 +- src/components/vocabulary/TermsManager.jsx | 276 +-- src/components/vocabulary/VocabularyList.jsx | 470 ++--- .../__tests__/BulkTermsEditor-test.jsx | 206 +-- .../__tests__/VocabularyList-test.jsx | 530 +++--- .../__tests__/data/multiple-vocabularies.js | 188 +- src/components/work/Edit.jsx | 176 +- src/components/work/Header.jsx | 20 +- src/index.js | 24 +- src/pages/Home.jsx | 48 +- src/pages/Main.jsx | 30 +- src/pages/SearchLanding.jsx | 136 +- src/pages/SearchResults.jsx | 628 +++---- src/pages/VocabularyManager.jsx | 580 +++--- src/pages/Work.jsx | 426 ++--- src/pages/WorkNotFound.jsx | 38 +- .../__tests__/active-vocabulary-terms-test.js | 390 ++-- .../__tests__/autocomplete-terms-test.js | 54 +- src/reducers/__tests__/data/vocabularies.json | 4 +- src/reducers/__tests__/notifications-test.js | 36 +- src/reducers/__tests__/search-test.js | 172 +- src/reducers/__tests__/vocabulary-test.js | 310 ++-- src/reducers/__tests__/work-test.js | 234 +-- src/reducers/active-vocabulary-terms.js | 124 +- src/reducers/autocomplete-terms.js | 26 +- src/reducers/index.js | 14 +- src/reducers/notifications.js | 216 +-- src/reducers/search.js | 124 +- src/reducers/vocabularies.js | 182 +- src/reducers/work.js | 90 +- src/scss/_badges.scss | 12 +- src/scss/_buttons.scss | 96 +- src/scss/_error-page.scss | 20 +- src/scss/_form-element.scss | 72 +- src/scss/_metadata-form.scss | 2 +- src/scss/_navbar.scss | 66 +- src/scss/_openseadragon.scss | 86 +- src/scss/_pdf.scss | 52 +- src/scss/_reset.scss | 10 +- src/scss/_thumbnail-preview.scss | 14 +- src/scss/_vocabulary-management.scss | 200 +-- src/scss/_work-header.scss | 38 +- src/scss/_work.scss | 118 +- src/scss/catalog/_facet-range-limit-date.scss | 12 +- src/scss/catalog/_results-gallery.scss | 44 +- src/scss/catalog/_results-table.scss | 132 +- src/scss/reset/_label.scss | 2 +- src/scss/reset/_typography.scss | 6 +- src/store/index.js | 38 +- webpack.config.js | 104 +- 163 files changed, 11609 insertions(+), 11609 deletions(-) diff --git a/env.config.js b/env.config.js index 56a411d..98b2e95 100644 --- a/env.config.js +++ b/env.config.js @@ -1,5 +1,5 @@ module.exports = { - 'API_BASE_URL': JSON.stringify('https://concerns.stage.lafayette.edu'), - 'AUTH_BASE_URL': JSON.stringify('http://authority.lafayette.edu/ns'), - 'SEARCH_BASE_URL': JSON.stringify('https://concerns.stage.lafayette.edu/catalog.json') + 'API_BASE_URL': JSON.stringify('https://concerns.stage.lafayette.edu'), + 'AUTH_BASE_URL': JSON.stringify('http://authority.lafayette.edu/ns'), + 'SEARCH_BASE_URL': JSON.stringify('https://concerns.stage.lafayette.edu/catalog.json') } diff --git a/karma.conf.js b/karma.conf.js index 0a725d0..9d2848d 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,105 +1,105 @@ const webpack = require('webpack') module.exports = function(config) { - config.set({ - - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', - - - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'], - - - // list of files / patterns to load in the browser - files: [ - 'test.webpack.js' - ], - - - // list of files to exclude - exclude: [ - ], - - // preprocess matching files before serving them to the browser - // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - preprocessors: { - 'test.webpack.js': [ 'webpack', 'sourcemap' ], - }, - - webpack: { - module: { - loaders: [ - { - test: /\.jsx?$/, - loader: 'babel-loader', - query: { - presets: ['es2015', 'react'] - }, - }, - { - test: /\.json$/, - loader: 'json-loader', - }, - { - test: /\.txt$/, - loader: 'raw-loader', - }, - { - test: /\.css$/, - loader: 'css-loader', - }, - ], - }, - externals: { - 'react/addons': true, - 'react/lib/ExecutionEnvironment': true, - 'react/lib/ReactContext': true, - }, - plugins: [ - new webpack.DefinePlugin({ - 'process.env': { - API_BASE_URL: JSON.stringify('http://example.org'), - AUTH_BASE_URL: JSON.stringify('http://auth.example.org/ns'), - SEARCH_BASE_URL: JSON.stringify('/catalog.json'), - }, - }) - ], - devtool: '#inline-source-map', - node: { - fs: 'empty', - } - }, - webpackServer: { - noInfo: true, - }, - - // test results reporter to use - // possible values: 'dots', 'progress' - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['mocha'], - - // web server port - port: 9876, - - - // enable / disable colors in the output (reporters and logs) - colors: true, - - - // level of logging - // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - logLevel: config.LOG_INFO, - - // start these browsers - // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: [ - 'PhantomJS', - ], - - // Concurrency level - // how many browser should be started simultaneous - concurrency: Infinity - }) + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'], + + + // list of files / patterns to load in the browser + files: [ + 'test.webpack.js' + ], + + + // list of files to exclude + exclude: [ + ], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'test.webpack.js': [ 'webpack', 'sourcemap' ], + }, + + webpack: { + module: { + loaders: [ + { + test: /\.jsx?$/, + loader: 'babel-loader', + query: { + presets: ['es2015', 'react'] + }, + }, + { + test: /\.json$/, + loader: 'json-loader', + }, + { + test: /\.txt$/, + loader: 'raw-loader', + }, + { + test: /\.css$/, + loader: 'css-loader', + }, + ], + }, + externals: { + 'react/addons': true, + 'react/lib/ExecutionEnvironment': true, + 'react/lib/ReactContext': true, + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + API_BASE_URL: JSON.stringify('http://example.org'), + AUTH_BASE_URL: JSON.stringify('http://auth.example.org/ns'), + SEARCH_BASE_URL: JSON.stringify('/catalog.json'), + }, + }) + ], + devtool: '#inline-source-map', + node: { + fs: 'empty', + } + }, + webpackServer: { + noInfo: true, + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['mocha'], + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: [ + 'PhantomJS', + ], + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity + }) } diff --git a/lib/__tests__/facet-helpers-test.js b/lib/__tests__/facet-helpers-test.js index 63ce99a..9691047 100644 --- a/lib/__tests__/facet-helpers-test.js +++ b/lib/__tests__/facet-helpers-test.js @@ -2,112 +2,112 @@ import { expect } from 'chai' import * as f from '../facet-helpers' describe('lib/facet-helpers', function () { - describe('#createFacetNameMap', function () { - it('works with facets, using `value` as default field', function () { - const facets = [ - {value: 'facet_val_1', label: 'Facet 1', hits: 100}, - {value: 'facet_val_3', label: 'Facet 3', hits: 12}, - {value: 'facet_val_2', label: 'Facet 2', hits: 77}, - ] + describe('#createFacetNameMap', function () { + it('works with facets, using `value` as default field', function () { + const facets = [ + {value: 'facet_val_1', label: 'Facet 1', hits: 100}, + {value: 'facet_val_3', label: 'Facet 3', hits: 12}, + {value: 'facet_val_2', label: 'Facet 2', hits: 77}, + ] - const out = { - 'facet_val_1': 'Facet 1', - 'facet_val_3': 'Facet 3', - 'facet_val_2': 'Facet 2', - } + const out = { + 'facet_val_1': 'Facet 1', + 'facet_val_3': 'Facet 3', + 'facet_val_2': 'Facet 2', + } - expect(f.createFacetNameMap(facets)).to.deep.equal(out) - }) + expect(f.createFacetNameMap(facets)).to.deep.equal(out) + }) - it('works with variable `field` parameter', function () { - const groups = [ - {name: 'facet-one', label: 'Facet One'}, - {name: 'facet-two', label: 'Facet Two'}, - {name: 'facet-three', label: 'Facet Three'}, - ] + it('works with variable `field` parameter', function () { + const groups = [ + {name: 'facet-one', label: 'Facet One'}, + {name: 'facet-two', label: 'Facet Two'}, + {name: 'facet-three', label: 'Facet Three'}, + ] - const out = { - 'facet-one': 'Facet One', - 'facet-two': 'Facet Two', - 'facet-three': 'Facet Three', - } + const out = { + 'facet-one': 'Facet One', + 'facet-two': 'Facet Two', + 'facet-three': 'Facet Three', + } - expect(f.createFacetNameMap(groups, 'name')).to.deep.equal(out) - }) - }) + expect(f.createFacetNameMap(groups, 'name')).to.deep.equal(out) + }) + }) - describe('#getBreadcrumbList', function () { - it('converts a map of selected facets into an array of breadcrumb objects', function () { - const pool = [ - { - name: 'facet_list_1', - label: 'Facet List 1', - }, - { - name: 'facet_list_2', - label: 'Facet List 2', - }, - { - name: 'facet_list_3', - label: 'Facet List 3', - } - ] + describe('#getBreadcrumbList', function () { + it('converts a map of selected facets into an array of breadcrumb objects', function () { + const pool = [ + { + name: 'facet_list_1', + label: 'Facet List 1', + }, + { + name: 'facet_list_2', + label: 'Facet List 2', + }, + { + name: 'facet_list_3', + label: 'Facet List 3', + } + ] - const selected = { - facet_list_2: [ - {label: 'Item 1', value: 'item_1'}, - {label: 'Item 3', value: 'item_3'}, - ], - facet_list_1: [ - {label: 'Item 4', value: 'item_4'}, - {label: 'Item 2', value: 'item_2'}, - ] - } + const selected = { + facet_list_2: [ + {label: 'Item 1', value: 'item_1'}, + {label: 'Item 3', value: 'item_3'}, + ], + facet_list_1: [ + {label: 'Item 4', value: 'item_4'}, + {label: 'Item 2', value: 'item_2'}, + ] + } - const out = [ - { - group: { - name: 'facet_list_1', - label: 'Facet List 1', - }, - facet: { - value: 'item_4', - label: 'Item 4', - }, - }, - { - group: { - name: 'facet_list_1', - label: 'Facet List 1', - }, - facet: { - value: 'item_2', - label: 'Item 2', - } - }, - { - group: { - name: 'facet_list_2', - label: 'Facet List 2', - }, - facet: { - value: 'item_1', - label: 'Item 1', - } - }, - { - group: { - name: 'facet_list_2', - label: 'Facet List 2', - }, - facet: { - value: 'item_3', - label: 'Item 3', - } - }, - ] + const out = [ + { + group: { + name: 'facet_list_1', + label: 'Facet List 1', + }, + facet: { + value: 'item_4', + label: 'Item 4', + }, + }, + { + group: { + name: 'facet_list_1', + label: 'Facet List 1', + }, + facet: { + value: 'item_2', + label: 'Item 2', + } + }, + { + group: { + name: 'facet_list_2', + label: 'Facet List 2', + }, + facet: { + value: 'item_1', + label: 'Item 1', + } + }, + { + group: { + name: 'facet_list_2', + label: 'Facet List 2', + }, + facet: { + value: 'item_3', + label: 'Item 3', + } + }, + ] - expect(f.getBreadcrumbList(pool, selected)).to.deep.equal(out) - }) - }) + expect(f.getBreadcrumbList(pool, selected)).to.deep.equal(out) + }) + }) }) diff --git a/lib/__tests__/search-history-test.js b/lib/__tests__/search-history-test.js index 6c444aa..59d928b 100644 --- a/lib/__tests__/search-history-test.js +++ b/lib/__tests__/search-history-test.js @@ -1,85 +1,85 @@ import { expect } from 'chai' import { - addSearch, - getSearches, - getPreviousQueries, - STORED_SEARCH_KEY, + addSearch, + getSearches, + getPreviousQueries, + STORED_SEARCH_KEY, } from '../search-history' describe('lib/search-history', function () { - // need to run this _before_ each test so as to clear out previous searches - // saved from `search` action tests - beforeEach(function () { - localStorage.clear() - }) + // need to run this _before_ each test so as to clear out previous searches + // saved from `search` action tests + beforeEach(function () { + localStorage.clear() + }) - describe('#addSearch/#getSearches', function () { - it('writes a search object to localStorage', function () { - const search = { - query: 'cats', - facets: {}, - options: {}, - } + describe('#addSearch/#getSearches', function () { + it('writes a search object to localStorage', function () { + const search = { + query: 'cats', + facets: {}, + options: {}, + } - addSearch(search) - expect(getSearches()).to.deep.equal([search]) - }) + addSearch(search) + expect(getSearches()).to.deep.equal([search]) + }) - it('puts most recent search at beginning of array', function () { - const first = { - query: 'cats 1', - facets: {}, - options: {}, - } + it('puts most recent search at beginning of array', function () { + const first = { + query: 'cats 1', + facets: {}, + options: {}, + } - const second = { - query: 'cats 2', - facets: {}, - options: {}, - } + const second = { + query: 'cats 2', + facets: {}, + options: {}, + } - addSearch(first) - addSearch(second) + addSearch(first) + addSearch(second) - expect(getSearches()).to.deep.equal([second, first]) - }) - }) + expect(getSearches()).to.deep.equal([second, first]) + }) + }) - describe('#getSearches', function () { - it('returns an empty array if search key is not found in local storage', function () { - expect(localStorage.getItem(STORED_SEARCH_KEY)).to.be.null - expect(getSearches()).to.deep.equal([]) - }) - }) + describe('#getSearches', function () { + it('returns an empty array if search key is not found in local storage', function () { + expect(localStorage.getItem(STORED_SEARCH_KEY)).to.be.null + expect(getSearches()).to.deep.equal([]) + }) + }) - describe('#getPreviousQueries', function () { - it('returns an empty array if search key is not found in local storage', function () { - expect(localStorage.getItem(STORED_SEARCH_KEY)).to.be.null - expect(getPreviousQueries()).to.deep.equal([]) - }) + describe('#getPreviousQueries', function () { + it('returns an empty array if search key is not found in local storage', function () { + expect(localStorage.getItem(STORED_SEARCH_KEY)).to.be.null + expect(getPreviousQueries()).to.deep.equal([]) + }) - it('returns only unique previous queries', function () { - const history = [ - { query: 'animals', facets: {}, options: {page: 2} }, - { query: 'animals', facets: {}, options: {page: 1} }, - { query: 'cats', facets: {author: ['Cleary, Beverly']}, options: {} }, - { query: 'cool things', facets: {}, options: {} }, - { query: 'dumb things', facets: {}, options: {} }, - { query: 'cats', facets: {}, options: {} }, - { query: 'dogs', facets: {}, options: {} }, - ] + it('returns only unique previous queries', function () { + const history = [ + { query: 'animals', facets: {}, options: {page: 2} }, + { query: 'animals', facets: {}, options: {page: 1} }, + { query: 'cats', facets: {author: ['Cleary, Beverly']}, options: {} }, + { query: 'cool things', facets: {}, options: {} }, + { query: 'dumb things', facets: {}, options: {} }, + { query: 'cats', facets: {}, options: {} }, + { query: 'dogs', facets: {}, options: {} }, + ] - const expected = [ - 'animals', - 'cats', - 'cool things', - 'dumb things', - 'dogs', - ] + const expected = [ + 'animals', + 'cats', + 'cool things', + 'dumb things', + 'dogs', + ] - localStorage.setItem(STORED_SEARCH_KEY, JSON.stringify(history)) + localStorage.setItem(STORED_SEARCH_KEY, JSON.stringify(history)) - expect(getPreviousQueries()).to.deep.equal(expected) - }) - }) + expect(getPreviousQueries()).to.deep.equal(expected) + }) + }) }) diff --git a/lib/__tests__/sort-facets-test.js b/lib/__tests__/sort-facets-test.js index c754483..9bd64c9 100644 --- a/lib/__tests__/sort-facets-test.js +++ b/lib/__tests__/sort-facets-test.js @@ -2,21 +2,21 @@ import { expect } from 'chai' import createSortFacets from '../sort-facets' const facetSet = [ - { - 'value': 'At the Library', - 'hits': 19523, - 'label': 'At the Library' - }, - { - 'value': 'On order', - 'hits': 48, - 'label': 'On order' - }, - { - 'value': 'Online', - 'hits': 8735, - 'label': 'Online' - }, + { + 'value': 'At the Library', + 'hits': 19523, + 'label': 'At the Library' + }, + { + 'value': 'On order', + 'hits': 48, + 'label': 'On order' + }, + { + 'value': 'Online', + 'hits': 8735, + 'label': 'Online' + }, ] const sortedHitsAsc = [48, 8735, 19523] @@ -25,41 +25,41 @@ const sortedLabelAsc = ['At the Library', 'On order', 'Online'] const sortedLabelDesc = ['Online', 'On order', 'At the Library'] describe('lib/sort-factes', function () { - it('returns a search function', function () { - const res = createSortFacets('desc', 'hits') - expect(res).to.be.a('function') - }) + it('returns a search function', function () { + const res = createSortFacets('desc', 'hits') + expect(res).to.be.a('function') + }) - it('sorts by number for `hits` field', function () { - const copyD = [].concat(facetSet) - const copyA = [].concat(facetSet) + it('sorts by number for `hits` field', function () { + const copyD = [].concat(facetSet) + const copyA = [].concat(facetSet) - copyD.sort(createSortFacets('desc', 'hits')) - copyA.sort(createSortFacets('asc', 'hits')) + copyD.sort(createSortFacets('desc', 'hits')) + copyA.sort(createSortFacets('asc', 'hits')) - expect(copyD.map(f => f.hits)).to.deep.equal(sortedHitsDesc) - expect(copyA.map(f => f.hits)).to.deep.equal(sortedHitsAsc) - }) + expect(copyD.map(f => f.hits)).to.deep.equal(sortedHitsDesc) + expect(copyA.map(f => f.hits)).to.deep.equal(sortedHitsAsc) + }) - it('sorts by string for `label` field', function () { - const copyD = [].concat(facetSet) - const copyA = [].concat(facetSet) + it('sorts by string for `label` field', function () { + const copyD = [].concat(facetSet) + const copyA = [].concat(facetSet) - copyD.sort(createSortFacets('desc', 'label')) - copyA.sort(createSortFacets('asc', 'label')) + copyD.sort(createSortFacets('desc', 'label')) + copyA.sort(createSortFacets('asc', 'label')) - expect(copyD.map(f => f.label)).to.deep.equal(sortedLabelDesc) - expect(copyA.map(f => f.label)).to.deep.equal(sortedLabelAsc) - }) + expect(copyD.map(f => f.label)).to.deep.equal(sortedLabelDesc) + expect(copyA.map(f => f.label)).to.deep.equal(sortedLabelAsc) + }) - it('sorts by string for `value` field', function () { - const copyD = [].concat(facetSet) - const copyA = [].concat(facetSet) + it('sorts by string for `value` field', function () { + const copyD = [].concat(facetSet) + const copyA = [].concat(facetSet) - copyD.sort(createSortFacets('desc', 'value')) - copyA.sort(createSortFacets('asc', 'value')) + copyD.sort(createSortFacets('desc', 'value')) + copyA.sort(createSortFacets('asc', 'value')) - expect(copyD.map(f => f.value)).to.deep.equal(sortedLabelDesc) - expect(copyA.map(f => f.value)).to.deep.equal(sortedLabelAsc) - }) + expect(copyD.map(f => f.value)).to.deep.equal(sortedLabelDesc) + expect(copyA.map(f => f.value)).to.deep.equal(sortedLabelAsc) + }) }) diff --git a/lib/api/request.js b/lib/api/request.js index d52ee80..793bbd6 100644 --- a/lib/api/request.js +++ b/lib/api/request.js @@ -7,69 +7,69 @@ import isAbsoluteUrl from 'is-absolute-url' const API_BASE_URL = process.env.API_BASE_URL function buildUrl (path) { - if (isAbsoluteUrl(path)) - return path + if (isAbsoluteUrl(path)) + return path - const p = path.substring(0,1) === '/' ? path : `/${path}` + const p = path.substring(0,1) === '/' ? path : `/${path}` - return API_BASE_URL + p + return API_BASE_URL + p } function apiRequest (method, path, data, options) { - const uri = buildUrl(path) - - const defaults = { - method, - headers: { - 'Accept': 'application/json', - } - } - - if (data) { - defaults.headers['Content-Type'] = 'application/json' - defaults.body = JSON.stringify(data) - } - - const opts = assign({}, defaults, options) - - return fetch(uri, opts) - .then(handleResponseStatus) - .then(parseJSON) - .catch(err => { throw err }) + const uri = buildUrl(path) + + const defaults = { + method, + headers: { + 'Accept': 'application/json', + } + } + + if (data) { + defaults.headers['Content-Type'] = 'application/json' + defaults.body = JSON.stringify(data) + } + + const opts = assign({}, defaults, options) + + return fetch(uri, opts) + .then(handleResponseStatus) + .then(parseJSON) + .catch(err => { throw err }) } // github example: // https://github.com/github/fetch/blob/master/README.md#handling-http-error-statuses function handleResponseStatus (response) { - if (response.status >= 200 && response.status < 300) - return response + if (response.status >= 200 && response.status < 300) + return response - const err = new Error(response.statusText) - err.status = response.status + const err = new Error(response.statusText) + err.status = response.status - throw err + throw err } function parseJSON (response) { - return response.json() + return response.json() } export function del () { - return apiRequest.bind(null, 'DELETE').apply(null, arguments) + return apiRequest.bind(null, 'DELETE').apply(null, arguments) } export function get () { - return apiRequest.bind(null, 'GET').apply(null, arguments) + return apiRequest.bind(null, 'GET').apply(null, arguments) } export function patch () { - return apiRequest.bind(null, 'PATCH').apply(null, arguments) + return apiRequest.bind(null, 'PATCH').apply(null, arguments) } export function post () { - return apiRequest.bind(null, 'POST').apply(null, arguments) + return apiRequest.bind(null, 'POST').apply(null, arguments) } export function put () { - return apiRequest.bind(null, 'PUT').apply(null, arguments) + return apiRequest.bind(null, 'PUT').apply(null, arguments) } diff --git a/lib/api/search.js b/lib/api/search.js index c5a97fc..11f6f53 100644 --- a/lib/api/search.js +++ b/lib/api/search.js @@ -4,8 +4,8 @@ const searchBase = process.env.SEARCH_BASE_URL || '' export function search (querystring) { - // prevent any double question-mark shenanigans from occurring - // when passing the qs from the browser - const url = searchBase + '/?' + querystring.replace(/^\?/, '') - return get(url) + // prevent any double question-mark shenanigans from occurring + // when passing the qs from the browser + const url = searchBase + '/?' + querystring.replace(/^\?/, '') + return get(url) } diff --git a/lib/api/terms.js b/lib/api/terms.js index cc34044..fa2636d 100644 --- a/lib/api/terms.js +++ b/lib/api/terms.js @@ -2,34 +2,34 @@ import { patch, put } from './request' // expects export function addTermToVocabulary (vocab, term) { - const url = vocab.absolute_path - const payload = { - vocabulary: { - terms: [term] - } - } + const url = vocab.absolute_path + const payload = { + vocabulary: { + terms: [term] + } + } - return patch(url, payload) + return patch(url, payload) } export function patchTerm (vocab, term) { - const url = vocab.absolute_path - const data = { - vocabulary: { - terms: [term] - } - } + const url = vocab.absolute_path + const data = { + vocabulary: { + terms: [term] + } + } - return patch(url, data) + return patch(url, data) } export function putTerms (vocab, terms) { - const url = vocab.absolute_path - const data = { - vocabulary: { - terms, - } - } + const url = vocab.absolute_path + const data = { + vocabulary: { + terms, + } + } - return put(url, data) + return put(url, data) } diff --git a/lib/api/vocabulary.js b/lib/api/vocabulary.js index 8739e17..01177f9 100644 --- a/lib/api/vocabulary.js +++ b/lib/api/vocabulary.js @@ -3,33 +3,33 @@ import { del, get, post, patch } from './request' import { VOCABULARY_PATH } from './constants' export function createVocabulary (data) { - const url = `/${VOCABULARY_PATH}.json` + const url = `/${VOCABULARY_PATH}.json` - return post(url, data) + return post(url, data) } export function deleteVocabulary (data) { - const url = data.absolute_path - return del(url) + const url = data.absolute_path + return del(url) } export function getVocabulary (path) { - // TODO: correct this to take a vocabulary object as its parameter - if (typeof path === 'object') - path = path.absolute_path + // TODO: correct this to take a vocabulary object as its parameter + if (typeof path === 'object') + path = path.absolute_path - return get(path) + return get(path) } export function getVocabularies () { - return get(`/${VOCABULARY_PATH}.json`) + return get(`/${VOCABULARY_PATH}.json`) } export function updateVocabulary (vocab) { - const url = vocab.absolute_path - const data = { - vocabulary: vocab, - } + const url = vocab.absolute_path + const data = { + vocabulary: vocab, + } - return patch(url, data) + return patch(url, data) } diff --git a/lib/api/work.js b/lib/api/work.js index bdae5a8..93ba19c 100644 --- a/lib/api/work.js +++ b/lib/api/work.js @@ -1,24 +1,24 @@ import { - get, patch, + get, patch, } from './request' import { - JSON_EXTENSION, - WORK_PATH, + JSON_EXTENSION, + WORK_PATH, } from './constants' function buildWorkPath (id) { - return `/${WORK_PATH}/${id}${JSON_EXTENSION}` + return `/${WORK_PATH}/${id}${JSON_EXTENSION}` } export function getWork (id) { - const path = buildWorkPath(id) - return get(path) + const path = buildWorkPath(id) + return get(path) } export function updateWork (id, updates) { - const path = buildWorkPath(id) - const update = {generic_work: updates} + const path = buildWorkPath(id) + const update = {generic_work: updates} - return patch(path, update) + return patch(path, update) } diff --git a/lib/camel-case.js b/lib/camel-case.js index 72b4b12..9d6c5a2 100644 --- a/lib/camel-case.js +++ b/lib/camel-case.js @@ -1,16 +1,16 @@ export default function camelCase (str) { - if (!str) - return '' + if (!str) + return '' - if (typeof str !== 'string') - str = '' + str + if (typeof str !== 'string') + str = '' + str - return str.replace(/[^0-9a-zA-Z\s]/g, '') - .replace(/\s+/, ' ') - .toLowerCase() - .split(' ') - .map((w, i) => { - if (i === 0) return w - return w.substr(0,1).toUpperCase() + w.substr(1) - }).join('') + return str.replace(/[^0-9a-zA-Z\s]/g, '') + .replace(/\s+/, ' ') + .toLowerCase() + .split(' ') + .map((w, i) => { + if (i === 0) return w + return w.substr(0,1).toUpperCase() + w.substr(1) + }).join('') } diff --git a/lib/colors.js b/lib/colors.js index 1688320..f49fe3c 100644 --- a/lib/colors.js +++ b/lib/colors.js @@ -1,11 +1,11 @@ import colorShade from 'color-shade' export function lighten (start, amount) { - const percentage = amount / 100 - return colorShade(percentage, start, '#ffffff') + const percentage = amount / 100 + return colorShade(percentage, start, '#ffffff') } export function darken (start, amount) { - const percentage = amount / 100 - return colorShade(percentage, start, '#000000') + const percentage = amount / 100 + return colorShade(percentage, start, '#000000') } diff --git a/lib/create-new-term.js b/lib/create-new-term.js index fde97b6..50e7c09 100644 --- a/lib/create-new-term.js +++ b/lib/create-new-term.js @@ -1,19 +1,19 @@ import camelCase from './camel-case' export default function createNewTerm (value, vocabulary) { - if (!value) return + if (!value) return - const cameled = camelCase(value) - let uri = '' + const cameled = camelCase(value) + let uri = '' - if (vocabulary.uri) - uri = `${vocabulary.uri}/${cameled}` + if (vocabulary.uri) + uri = `${vocabulary.uri}/${cameled}` - return { - uri, - label: [value], - pref_label: [value], - alt_label: [], - hidden_label: [], - } + return { + uri, + label: [value], + pref_label: [value], + alt_label: [], + hidden_label: [], + } } diff --git a/lib/create-range-facet.js b/lib/create-range-facet.js index 47b1478..2957d22 100644 --- a/lib/create-range-facet.js +++ b/lib/create-range-facet.js @@ -1,13 +1,13 @@ export default function createRangeFacet (name, min, max) { - const label = min === max ? `${min}` : `${min} - ${max}` + const label = min === max ? `${min}` : `${min} - ${max}` - return { - label, - name, - value: { - begin: min, - end: max, - }, - type: 'range' - } + return { + label, + name, + value: { + begin: min, + end: max, + }, + type: 'range' + } } diff --git a/lib/facet-helpers.js b/lib/facet-helpers.js index b5ba21a..9cb7dbd 100644 --- a/lib/facet-helpers.js +++ b/lib/facet-helpers.js @@ -1,68 +1,68 @@ export function createFacetNameMap (arr, field) { - if (!field) - field = 'value' + if (!field) + field = 'value' - const out = {} + const out = {} - for (let i = 0; i < arr.length; i++) - if (!out[arr[i][field]]) - out[arr[i][field]] = arr[i].label + for (let i = 0; i < arr.length; i++) + if (!out[arr[i][field]]) + out[arr[i][field]] = arr[i].label - return out + return out } export function getBreadcrumbList (pool, selected) { - const hasOwnProperty = Object.prototype.hasOwnProperty - let out = [] + const hasOwnProperty = Object.prototype.hasOwnProperty + let out = [] - if (!selected) - return out + if (!selected) + return out - const selKeys = Object.keys(selected) + const selKeys = Object.keys(selected) - if (!selKeys.length) - return out + if (!selKeys.length) + return out - // loop through the pool of potential facets - // (these are the responses returned w/ a search query) - for (let p = 0; p < pool.length; p++) { - const group = pool[p] + // loop through the pool of potential facets + // (these are the responses returned w/ a search query) + for (let p = 0; p < pool.length; p++) { + const group = pool[p] - // loop through the keys of the selected facets - // and find our matching group (which should exist - // because it's part of our search) - for (let s = 0; s < selKeys.length; s++) { - const name = selKeys[s] + // loop through the keys of the selected facets + // and find our matching group (which should exist + // because it's part of our search) + for (let s = 0; s < selKeys.length; s++) { + const name = selKeys[s] - if (group.name === name) { + if (group.name === name) { - // we're going to append our collection (`out`) with data - // for each of the selected facets. this will allow us to - // display a breadcrumb like 'Facet Group > Facet Value' - // using label values (for cleaner values) + retain the - // values used by the back-end - out = out.concat(selected[name].map(sk => { - const selVal = hasOwnProperty.call(sk, 'value') ? sk.value : sk - const selLabel = hasOwnProperty.call(sk, 'label') ? sk.label : sk - return { - group: { - name: group.name, - label: group.label - }, - facet: { - value: selVal, - label: selLabel, - } - } - })) + // we're going to append our collection (`out`) with data + // for each of the selected facets. this will allow us to + // display a breadcrumb like 'Facet Group > Facet Value' + // using label values (for cleaner values) + retain the + // values used by the back-end + out = out.concat(selected[name].map(sk => { + const selVal = hasOwnProperty.call(sk, 'value') ? sk.value : sk + const selLabel = hasOwnProperty.call(sk, 'label') ? sk.label : sk + return { + group: { + name: group.name, + label: group.label + }, + facet: { + value: selVal, + label: selLabel, + } + } + })) - // selKeys.splice(s, 1) - // break - } - } - } + // selKeys.splice(s, 1) + // break + } + } + } - return [].concat.apply([], out) + return [].concat.apply([], out) } diff --git a/lib/format-search-querystring.js b/lib/format-search-querystring.js index 6aff1c0..ef15117 100644 --- a/lib/format-search-querystring.js +++ b/lib/format-search-querystring.js @@ -2,53 +2,53 @@ import { stringify } from 'blacklight-querystring' import assign from 'object-assign' export default function formatSearchQuerystring (query, facets, options) { - if (!facets) - facets = {} - - // instead of passing the query, facets, and options directly - // to `blacklight-querystring`, we'll need to do a little cleanup: - // - // 1) blqs#stringify expects a flat array of facet values per group, - // whereas we're storing the full objects in state (which contain - // the value, name, and label). so we'll extract the value. - // 2) the `range` facet is handled completely differently from - // other facets, so we'll look for values w/ a `type` attribute, - // assume a shallow value + assign it to an object (whose key - // matches the `type` attribute) within the `options` object. - - const facetKeys = Object.keys(facets) - const mappedFacets = {} - const opts = options ? assign({}, options) : {} - - facetKeys.forEach(function (k) { - let i = 0 - let current, key - - for (; i < facets[k].length; i++) { - current = facets[k][i] - key = current.type - - if (typeof key !== 'undefined') { - if (!opts[key]) - opts[key] = {} - - opts[key][k] = current.value - } else { - if (!mappedFacets[k]) - mappedFacets[k] = [] - - mappedFacets[k].push(current.value) - } - } - }) - - const toStringify = { - options: opts, - facets: mappedFacets, - query, - } - - const strung = stringify(toStringify) - return strung + if (!facets) + facets = {} + + // instead of passing the query, facets, and options directly + // to `blacklight-querystring`, we'll need to do a little cleanup: + // + // 1) blqs#stringify expects a flat array of facet values per group, + // whereas we're storing the full objects in state (which contain + // the value, name, and label). so we'll extract the value. + // 2) the `range` facet is handled completely differently from + // other facets, so we'll look for values w/ a `type` attribute, + // assume a shallow value + assign it to an object (whose key + // matches the `type` attribute) within the `options` object. + + const facetKeys = Object.keys(facets) + const mappedFacets = {} + const opts = options ? assign({}, options) : {} + + facetKeys.forEach(function (k) { + let i = 0 + let current, key + + for (; i < facets[k].length; i++) { + current = facets[k][i] + key = current.type + + if (typeof key !== 'undefined') { + if (!opts[key]) + opts[key] = {} + + opts[key][k] = current.value + } else { + if (!mappedFacets[k]) + mappedFacets[k] = [] + + mappedFacets[k].push(current.value) + } + } + }) + + const toStringify = { + options: opts, + facets: mappedFacets, + query, + } + + const strung = stringify(toStringify) + return strung } diff --git a/lib/get-work-title.js b/lib/get-work-title.js index 85aa018..3740f40 100644 --- a/lib/get-work-title.js +++ b/lib/get-work-title.js @@ -1,15 +1,15 @@ export default function getWorkTitle (work) { - if (!work || !work.title) - return null + if (!work || !work.title) + return null - const title = work.title + const title = work.title - if (Array.isArray(title)) - return title[0] + if (Array.isArray(title)) + return title[0] - if (typeof title === 'string') - return title + if (typeof title === 'string') + return title - // edge cases we haven't encountered - return null + // edge cases we haven't encountered + return null } diff --git a/lib/is-fresh.js b/lib/is-fresh.js index 931efbb..b1a0937 100644 --- a/lib/is-fresh.js +++ b/lib/is-fresh.js @@ -1,15 +1,15 @@ export default function isFresh (data, stalePeriod) { - if (!data) - return false + if (!data) + return false - if (!data.fetchedAt) - return false + if (!data.fetchedAt) + return false - if (!stalePeriod) - stalePeriod = (5 * 60 * 1000) + if (!stalePeriod) + stalePeriod = (5 * 60 * 1000) - if ((Date.now() - data.fetchedAt) > stalePeriod) - return false + if ((Date.now() - data.fetchedAt) > stalePeriod) + return false - return true + return true } diff --git a/lib/is-work-updated.js b/lib/is-work-updated.js index cc63c7e..fb63f01 100644 --- a/lib/is-work-updated.js +++ b/lib/is-work-updated.js @@ -1,31 +1,31 @@ export default function isWorkUpdated (prev, next) { - if (!next) - return false + if (!next) + return false - const keys = Object.keys(next) + const keys = Object.keys(next) - if (!keys.length) - return false + if (!keys.length) + return false - for (let i = 0; i < keys.length; i++) { - const key = keys[i] + for (let i = 0; i < keys.length; i++) { + const key = keys[i] - if (typeof prev[key] === 'string' && typeof next[key] === 'string') { - return prev[key] === next[key] - } + if (typeof prev[key] === 'string' && typeof next[key] === 'string') { + return prev[key] === next[key] + } - const nextFiltered = next[key].filter(Boolean) + const nextFiltered = next[key].filter(Boolean) - if (nextFiltered.length !== prev[key].length) { - return true - } + if (nextFiltered.length !== prev[key].length) { + return true + } - for (let j = 0; j < nextFiltered.length; j++) { - if (next[key][j] !== prev[key][j]) { - return true - } - } - } + for (let j = 0; j < nextFiltered.length; j++) { + if (next[key][j] !== prev[key][j]) { + return true + } + } + } - return false + return false } diff --git a/lib/pref-label.js b/lib/pref-label.js index 3de9145..dd117df 100644 --- a/lib/pref-label.js +++ b/lib/pref-label.js @@ -1,17 +1,17 @@ export function getPrefLabel (obj) { - const pref = obj.pref_label + const pref = obj.pref_label - if (!pref) - return null + if (!pref) + return null - return Array.isArray(pref) ? pref[0] : pref + return Array.isArray(pref) ? pref[0] : pref } export function setPrefLabel (obj, val) { - if (obj.pref_label) { - if (Array.isArray(obj.pref_label)) - obj.pref_label = [val] - else - obj.pref_label = val - } + if (obj.pref_label) { + if (Array.isArray(obj.pref_label)) + obj.pref_label = [val] + else + obj.pref_label = val + } } diff --git a/lib/scroll-to-top.js b/lib/scroll-to-top.js index 63c3e63..7f59d57 100644 --- a/lib/scroll-to-top.js +++ b/lib/scroll-to-top.js @@ -1,6 +1,6 @@ export default function scrollToTop () { - const interval = setInterval(() => { - if (window.pageYOffset <= 0) return clearInterval(interval) - window.scrollTo(0, window.pageYOffset - 75) - }, 1) + const interval = setInterval(() => { + if (window.pageYOffset <= 0) return clearInterval(interval) + window.scrollTo(0, window.pageYOffset - 75) + }, 1) } diff --git a/lib/search-history.js b/lib/search-history.js index b03bdf6..08a6af3 100644 --- a/lib/search-history.js +++ b/lib/search-history.js @@ -5,37 +5,37 @@ const STORED_SEARCH_LIMIT = 20 export const STORED_SEARCH_KEY = 'search-history' export function addSearch (search, limit) { - limit = limit || STORED_SEARCH_LIMIT + limit = limit || STORED_SEARCH_LIMIT - const history = getSearches() - const update = [].concat(search, history.slice(0, limit - 1)) + const history = getSearches() + const update = [].concat(search, history.slice(0, limit - 1)) - localStorage.setItem(STORED_SEARCH_KEY, JSON.stringify(update)) + localStorage.setItem(STORED_SEARCH_KEY, JSON.stringify(update)) } export function clearSearches () { - localStorage.setItem(STORED_SEARCH_KEY, '[]') + localStorage.setItem(STORED_SEARCH_KEY, '[]') } export function getSearches () { - const history = localStorage.getItem(STORED_SEARCH_KEY) || '[]' + const history = localStorage.getItem(STORED_SEARCH_KEY) || '[]' - try { - return JSON.parse(history) - } catch(e) { - return [] - } + try { + return JSON.parse(history) + } catch(e) { + return [] + } } export function getPreviousQueries () { - const history = localStorage.getItem(STORED_SEARCH_KEY) || '[]' - - try { - const parsed = JSON.parse(history) - return parsed - .map(search => search.query) - .filter((query, idx, arr) => query && (arr.indexOf(query) === idx)) - } catch (e) { - return [] - } + const history = localStorage.getItem(STORED_SEARCH_KEY) || '[]' + + try { + const parsed = JSON.parse(history) + return parsed + .map(search => search.query) + .filter((query, idx, arr) => query && (arr.indexOf(query) === idx)) + } catch (e) { + return [] + } } diff --git a/lib/search-result-settings.js b/lib/search-result-settings.js index 0d0a9ae..e360066 100644 --- a/lib/search-result-settings.js +++ b/lib/search-result-settings.js @@ -1,36 +1,36 @@ function getFactory (key, def) { - return function () { - const stored = localStorage.getItem(key) + return function () { + const stored = localStorage.getItem(key) - if (!stored) { - return def - } + if (!stored) { + return def + } - try { - return JSON.parse(stored) - } catch (e) { - return def - } - } + try { + return JSON.parse(stored) + } catch (e) { + return def + } + } } function setFactory (key) { - return function (value) { - localStorage.setItem(key, JSON.stringify(value)) - } + return function (value) { + localStorage.setItem(key, JSON.stringify(value)) + } } const FIELD_KEY = 'search-result--fields' const DISPLAY_KEY = 'search-result--display' const fields = { - get: getFactory(FIELD_KEY, []), - set: setFactory(FIELD_KEY), + get: getFactory(FIELD_KEY, []), + set: setFactory(FIELD_KEY), } const display = { - get: getFactory(DISPLAY_KEY), - set: setFactory(DISPLAY_KEY), + get: getFactory(DISPLAY_KEY), + set: setFactory(DISPLAY_KEY), } export default { fields, display } diff --git a/lib/sort-by-hits.js b/lib/sort-by-hits.js index 23fe07e..d29d552 100644 --- a/lib/sort-by-hits.js +++ b/lib/sort-by-hits.js @@ -1,11 +1,11 @@ export default function sortByHits (items, opts) { - const sortAsc = (a, b) => a.hits - b.hits - const sortDesc = (a, b) => b.hits - a.hits + const sortAsc = (a, b) => a.hits - b.hits + const sortDesc = (a, b) => b.hits - a.hits - if (!opts) - opts = {} + if (!opts) + opts = {} - const sortFn = opts.sortFn || (opts.sortAsc ? sortAsc : sortDesc) + const sortFn = opts.sortFn || (opts.sortAsc ? sortAsc : sortDesc) - return items.sort(sortFn) + return items.sort(sortFn) } diff --git a/lib/sort-facets.js b/lib/sort-facets.js index 8efc777..3394c02 100644 --- a/lib/sort-facets.js +++ b/lib/sort-facets.js @@ -1,37 +1,37 @@ function sortNum (direction, field) { - return function (a, b) { - if (direction === 'desc') - return b[field] - a[field] + return function (a, b) { + if (direction === 'desc') + return b[field] - a[field] - return a[field] - b[field] - } + return a[field] - b[field] + } } function sortStr (direction, field) { - return function (a, b) { - const aa = a[field].toLowerCase() - const bb = b[field].toLowerCase() + return function (a, b) { + const aa = a[field].toLowerCase() + const bb = b[field].toLowerCase() - if (aa === bb) return 0 + if (aa === bb) return 0 - if (direction === 'desc') - return aa < bb ? 1 : -1 + if (direction === 'desc') + return aa < bb ? 1 : -1 - return aa < bb ? -1 : 1 - } + return aa < bb ? -1 : 1 + } } export default function createFacetsSortFunction (direction, field) { - const fields = ['hits', 'label', 'value'] + const fields = ['hits', 'label', 'value'] - if (!direction || (direction !== 'asc' && direction !== 'desc')) - direction = 'desc' + if (!direction || (direction !== 'asc' && direction !== 'desc')) + direction = 'desc' - if (!field || fields.indexOf(field) === -1) - field = 'hits' + if (!field || fields.indexOf(field) === -1) + field = 'hits' - if (field === 'hits') - return sortNum(direction, field) + if (field === 'hits') + return sortNum(direction, field) - return sortStr(direction, field) + return sortStr(direction, field) } diff --git a/lib/work-fields.js b/lib/work-fields.js index 043c5da..95f18e8 100644 --- a/lib/work-fields.js +++ b/lib/work-fields.js @@ -1,70 +1,70 @@ const fields = { - 'id': 'ID', - 'title': 'Title', - 'creator': 'Creator', - 'creator_photographer': 'Creator - Photographer', - 'format_medium': 'Format - Medium', - 'format_size': 'Format - Size', - 'date_approximate': 'Date (Approximate)', - 'date_range': 'Date Range', - 'creator_maker': 'Creator - Maker', - 'date_original_display': 'Date - Original (Display)', - 'description_size': 'Size', - 'description_note': 'Note', - 'subject_lcsh': 'Subject (LCSH)', - 'publisher_original': 'Publisher - Original', - 'date_original': 'Date - Original', - 'format_extent': 'Extent', - 'description_condition': 'Condition', - 'description_provenance': 'Provenance', - 'description_series': 'Series', - 'identifier_itemnumber': 'Item Number', - 'publisher_digital': 'Publisher - Digital', - 'format_digital': 'Format - Digital', - 'rights_digital': 'Rights - Digital', - 'subject_ocm': 'Subject (OCM)', - 'description_critical': 'Description - Critial', - 'description_indicia': 'Indicia', - 'description_text': 'Description - Text', - 'description_inscription': 'Inscription', - 'description_ethnicity': 'Ethnicity', - 'description_citation': 'Citation', - 'coverage_location_country': 'Location - Country', - 'coverage_location': 'Location', - 'creator_company': 'Company', - 'relation_seealso': 'See Also', - 'date_image_upper': 'Date - Image (Upper)', - 'date_image_lower': 'Date - Image (Lower)', - 'title_name': 'Title - Name', - 'description_class': 'Class', - 'date_birth_display': 'Date - Birth (Display)', - 'coverage_place_birth': 'Place - Birth', - 'description_military_branch': 'Military Branch', - 'description_military_rank': 'Military Rank', - 'description_military_unit': 'Military Unit', - 'date_death_display': 'Date - Death (Display)', - 'coverage_place_death': 'Place - Death', - 'description_cause_death': 'Cause of Death', - 'description_honors': 'Honors', - 'type': 'Type', - 'contributor': 'Contributor(s)', - 'description': 'Description', - 'keyword': 'Keyword', - 'rights': 'Rights', - 'publisher': 'Publisher', - 'date_created': 'Date - Created', - 'subject': 'Subject', - 'language': 'Language', - 'identifier': 'Identifier', - 'based_near': 'Based Near', - 'related_url': 'Related URL', - 'bibliographic_citation': 'Bibliographic Citation', - 'source': 'Source', - 'date_artifact_upper': 'Date - Artifact (Upper)', - 'date_artifact_lower': 'Date - Artifact (Lower)', + 'id': 'ID', + 'title': 'Title', + 'creator': 'Creator', + 'creator_photographer': 'Creator - Photographer', + 'format_medium': 'Format - Medium', + 'format_size': 'Format - Size', + 'date_approximate': 'Date (Approximate)', + 'date_range': 'Date Range', + 'creator_maker': 'Creator - Maker', + 'date_original_display': 'Date - Original (Display)', + 'description_size': 'Size', + 'description_note': 'Note', + 'subject_lcsh': 'Subject (LCSH)', + 'publisher_original': 'Publisher - Original', + 'date_original': 'Date - Original', + 'format_extent': 'Extent', + 'description_condition': 'Condition', + 'description_provenance': 'Provenance', + 'description_series': 'Series', + 'identifier_itemnumber': 'Item Number', + 'publisher_digital': 'Publisher - Digital', + 'format_digital': 'Format - Digital', + 'rights_digital': 'Rights - Digital', + 'subject_ocm': 'Subject (OCM)', + 'description_critical': 'Description - Critial', + 'description_indicia': 'Indicia', + 'description_text': 'Description - Text', + 'description_inscription': 'Inscription', + 'description_ethnicity': 'Ethnicity', + 'description_citation': 'Citation', + 'coverage_location_country': 'Location - Country', + 'coverage_location': 'Location', + 'creator_company': 'Company', + 'relation_seealso': 'See Also', + 'date_image_upper': 'Date - Image (Upper)', + 'date_image_lower': 'Date - Image (Lower)', + 'title_name': 'Title - Name', + 'description_class': 'Class', + 'date_birth_display': 'Date - Birth (Display)', + 'coverage_place_birth': 'Place - Birth', + 'description_military_branch': 'Military Branch', + 'description_military_rank': 'Military Rank', + 'description_military_unit': 'Military Unit', + 'date_death_display': 'Date - Death (Display)', + 'coverage_place_death': 'Place - Death', + 'description_cause_death': 'Cause of Death', + 'description_honors': 'Honors', + 'type': 'Type', + 'contributor': 'Contributor(s)', + 'description': 'Description', + 'keyword': 'Keyword', + 'rights': 'Rights', + 'publisher': 'Publisher', + 'date_created': 'Date - Created', + 'subject': 'Subject', + 'language': 'Language', + 'identifier': 'Identifier', + 'based_near': 'Based Near', + 'related_url': 'Related URL', + 'bibliographic_citation': 'Bibliographic Citation', + 'source': 'Source', + 'date_artifact_upper': 'Date - Artifact (Upper)', + 'date_artifact_lower': 'Date - Artifact (Lower)', - //'thumbnail_path': 'Thumbnail Path', - //'uses_vocabulary': 'Uses Vocabulary', + //'thumbnail_path': 'Thumbnail Path', + //'uses_vocabulary': 'Uses Vocabulary', } export default fields diff --git a/src/App.jsx b/src/App.jsx index f5ebcbb..f7ed258 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,30 +7,30 @@ import * as actionCreators from './actions/' import Main from './pages/Main.jsx' function mapStateToProps (state) { - return { - // collection: state.collection, - // collections: state.collections, + return { + // collection: state.collection, + // collections: state.collections, - // error: state.error, + // error: state.error, - // schema: state.collection.schema, + // schema: state.collection.schema, - // activeVocabulary, - activeVocabularyTerms: state.activeVocabularyTerms, - autocompleteTerms: state.autocompleteTerms, - - notifications: state.notifications, + // activeVocabulary, + activeVocabularyTerms: state.activeVocabularyTerms, + autocompleteTerms: state.autocompleteTerms, + + notifications: state.notifications, - search: state.search, + search: state.search, - vocabularies: state.vocabularies, + vocabularies: state.vocabularies, - work: state.work, - } + work: state.work, + } } function mapDispatchToProps (dispatch) { - return bindActionCreators(actionCreators, dispatch) + return bindActionCreators(actionCreators, dispatch) } const App = connect(mapStateToProps, mapDispatchToProps)(Main) diff --git a/src/actions/__tests__/autocomplete-test.js b/src/actions/__tests__/autocomplete-test.js index da4c1d1..897d9a3 100644 --- a/src/actions/__tests__/autocomplete-test.js +++ b/src/actions/__tests__/autocomplete-test.js @@ -5,88 +5,88 @@ import thunk from 'redux-thunk' import fetchMock from 'fetch-mock' import { - RECEIVE_AUTOCOMPLETE_TERMS, + RECEIVE_AUTOCOMPLETE_TERMS, } from '../../constants' import { - testVocabulary as VOCAB_ONE, - anotherTestVocabulary as VOCAB_TWO, + testVocabulary as VOCAB_ONE, + anotherTestVocabulary as VOCAB_TWO, } from './data/vocabularies-with-terms' const API_BASE = process.env.API_BASE_URL const mockStore = configureMockStore([thunk]) describe('Autocomplete actionCreators', function () { - beforeEach(function () { - if (!API_BASE) { - this.skip() - return - } - }) + beforeEach(function () { + if (!API_BASE) { + this.skip() + return + } + }) - describe('#getAutocompleteTerms', function () { - beforeEach(function () { - fetchMock.get(VOCAB_ONE.absolute_path, VOCAB_ONE) - fetchMock.get(VOCAB_TWO.absolute_path, VOCAB_TWO) - fetchMock.get('*', 404) - }) + describe('#getAutocompleteTerms', function () { + beforeEach(function () { + fetchMock.get(VOCAB_ONE.absolute_path, VOCAB_ONE) + fetchMock.get(VOCAB_TWO.absolute_path, VOCAB_TWO) + fetchMock.get('*', 404) + }) - afterEach(fetchMock.restore) + afterEach(fetchMock.restore) - it('fetches only the terms from a vocabulary', function () { - const store = mockStore({autocompleteTerms: {}}) - const expectAction = [ - { - type: RECEIVE_AUTOCOMPLETE_TERMS, - terms: VOCAB_ONE.terms.map(t => t.pref_label[0]), - vocabulary: VOCAB_ONE, - } - ] + it('fetches only the terms from a vocabulary', function () { + const store = mockStore({autocompleteTerms: {}}) + const expectAction = [ + { + type: RECEIVE_AUTOCOMPLETE_TERMS, + terms: VOCAB_ONE.terms.map(t => t.pref_label[0]), + vocabulary: VOCAB_ONE, + } + ] - return store.dispatch(actions.fetchAutocompleteTerms(VOCAB_ONE)) - .then(() => { - const actions = store.getActions() - expect(actions).to.deep.equal(expectAction) - }) - }) + return store.dispatch(actions.fetchAutocompleteTerms(VOCAB_ONE)) + .then(() => { + const actions = store.getActions() + expect(actions).to.deep.equal(expectAction) + }) + }) - it('dispatches RECEIVE_AUTOCOMPLETE_TERMS for each vocabulary', function () { - const store = mockStore({autocompleteTerms: {}}) + it('dispatches RECEIVE_AUTOCOMPLETE_TERMS for each vocabulary', function () { + const store = mockStore({autocompleteTerms: {}}) - return store.dispatch(actions.fetchAutocompleteTerms(VOCAB_ONE)) - .then(store.dispatch(actions.fetchAutocompleteTerms(VOCAB_TWO))) - .then(() => { - const actions = store.getActions() - expect(actions).to.have.length(2) + return store.dispatch(actions.fetchAutocompleteTerms(VOCAB_ONE)) + .then(store.dispatch(actions.fetchAutocompleteTerms(VOCAB_TWO))) + .then(() => { + const actions = store.getActions() + expect(actions).to.have.length(2) - actions.forEach(act => { - expect(act.type).to.equal(RECEIVE_AUTOCOMPLETE_TERMS) - }) - }) - }) + actions.forEach(act => { + expect(act.type).to.equal(RECEIVE_AUTOCOMPLETE_TERMS) + }) + }) + }) - it('ignores duplicate vocab requests', function () { - const store = mockStore({autocompleteTerms: {}}) + it('ignores duplicate vocab requests', function () { + const store = mockStore({autocompleteTerms: {}}) - return store.dispatch(actions.fetchAutocompleteTerms(VOCAB_ONE)) - .then(store.dispatch(actions.fetchAutocompleteTerms(VOCAB_ONE))) - .then(store.dispatch(actions.fetchAutocompleteTerms(VOCAB_TWO))) - .then(store.dispatch(actions.fetchAutocompleteTerms(VOCAB_ONE))) - .then(() => { - expect(store.getActions()).to.have.length(2) - expect(fetchMock.calls().matched).to.have.length(2) - }) - }) + return store.dispatch(actions.fetchAutocompleteTerms(VOCAB_ONE)) + .then(store.dispatch(actions.fetchAutocompleteTerms(VOCAB_ONE))) + .then(store.dispatch(actions.fetchAutocompleteTerms(VOCAB_TWO))) + .then(store.dispatch(actions.fetchAutocompleteTerms(VOCAB_ONE))) + .then(() => { + expect(store.getActions()).to.have.length(2) + expect(fetchMock.calls().matched).to.have.length(2) + }) + }) - it('ignores previously fetched vocabularies in state', function () { - const store = mockStore({autocompleteTerms: { - [VOCAB_ONE.uri]: VOCAB_ONE.terms.map(t => t.pref_label[0]) - }}) + it('ignores previously fetched vocabularies in state', function () { + const store = mockStore({autocompleteTerms: { + [VOCAB_ONE.uri]: VOCAB_ONE.terms.map(t => t.pref_label[0]) + }}) - return store.dispatch(actions.fetchAutocompleteTerms(VOCAB_ONE)) - .then(() => { - expect(fetchMock.calls().matched).to.have.length(0) - }) - }) - }) + return store.dispatch(actions.fetchAutocompleteTerms(VOCAB_ONE)) + .then(() => { + expect(fetchMock.calls().matched).to.have.length(0) + }) + }) + }) }) diff --git a/src/actions/__tests__/data/vocabularies-with-terms.js b/src/actions/__tests__/data/vocabularies-with-terms.js index aad0932..0306c83 100644 --- a/src/actions/__tests__/data/vocabularies-with-terms.js +++ b/src/actions/__tests__/data/vocabularies-with-terms.js @@ -2,106 +2,106 @@ const API_BASE = process.env.API_BASE_URL const AUTH_BASE = process.env.AUTH_BASE_URL export const testVocabulary = { - uri: `${AUTH_BASE}/testVocabulary`, - label: ['Test Vocabulary'], - pref_label: ['Test Vocabulary'], - alt_label: ['Test Description'], - hidden_label: [], - absolute_path: `${API_BASE}/vocabularies/testVocabulary.json`, - terms: [ - { - uri: `${AUTH_BASE}/testVocabulary/firstTerm`, - label: ['First Term'], - pref_label: ['First Term'], - alt_label: ['First Term Alt Label'], - hidden_label: ['First Term Hidden Label'], - absolute_path: `${API_BASE}/vocabularies/testVocabulary/firstTerm.json` - }, - { - uri: `${AUTH_BASE}/testVocabulary/secondTerm`, - label: ['Second Term'], - pref_label: ['Second Term'], - alt_label: ['Second Term Alt Label'], - hidden_label: ['Second Term Hidden Label'], - absolute_path: `${API_BASE}/vocabularies/testVocabulary/secondTerm.json` - }, - { - uri: `${AUTH_BASE}/testVocabulary/thirdTerm`, - label: ['Third Term'], - pref_label: ['Third Term'], - alt_label: ['Third Term Alt Label'], - hidden_label: ['Third Term Hidden Label'], - absolute_path: `${API_BASE}/vocabularies/testVocabulary/thirdTerm.json` - }, - { - uri: `${AUTH_BASE}/testVocabulary/fourthTerm`, - label: ['4th Term'], - pref_label: ['Fourth Term'], - alt_label: ['Fourth Term Alt Label'], - hidden_label: ['Fourth Term Hidden Label'], - absolute_path: `${API_BASE}/vocabularies/testVocabulary/fourthTerm.json` - }, - { - uri: `${AUTH_BASE}/testVocabulary/fifthTerm`, - label: ['Fifth Term'], - pref_label: ['Fifth Term'], - alt_label: ['Fifth Term Alt Label'], - hidden_label: ['Fifth Term Hidden Label'], - absolute_path: `${API_BASE}/vocabularies/testVocabulary/fifthTerm.json` - } - ] + uri: `${AUTH_BASE}/testVocabulary`, + label: ['Test Vocabulary'], + pref_label: ['Test Vocabulary'], + alt_label: ['Test Description'], + hidden_label: [], + absolute_path: `${API_BASE}/vocabularies/testVocabulary.json`, + terms: [ + { + uri: `${AUTH_BASE}/testVocabulary/firstTerm`, + label: ['First Term'], + pref_label: ['First Term'], + alt_label: ['First Term Alt Label'], + hidden_label: ['First Term Hidden Label'], + absolute_path: `${API_BASE}/vocabularies/testVocabulary/firstTerm.json` + }, + { + uri: `${AUTH_BASE}/testVocabulary/secondTerm`, + label: ['Second Term'], + pref_label: ['Second Term'], + alt_label: ['Second Term Alt Label'], + hidden_label: ['Second Term Hidden Label'], + absolute_path: `${API_BASE}/vocabularies/testVocabulary/secondTerm.json` + }, + { + uri: `${AUTH_BASE}/testVocabulary/thirdTerm`, + label: ['Third Term'], + pref_label: ['Third Term'], + alt_label: ['Third Term Alt Label'], + hidden_label: ['Third Term Hidden Label'], + absolute_path: `${API_BASE}/vocabularies/testVocabulary/thirdTerm.json` + }, + { + uri: `${AUTH_BASE}/testVocabulary/fourthTerm`, + label: ['4th Term'], + pref_label: ['Fourth Term'], + alt_label: ['Fourth Term Alt Label'], + hidden_label: ['Fourth Term Hidden Label'], + absolute_path: `${API_BASE}/vocabularies/testVocabulary/fourthTerm.json` + }, + { + uri: `${AUTH_BASE}/testVocabulary/fifthTerm`, + label: ['Fifth Term'], + pref_label: ['Fifth Term'], + alt_label: ['Fifth Term Alt Label'], + hidden_label: ['Fifth Term Hidden Label'], + absolute_path: `${API_BASE}/vocabularies/testVocabulary/fifthTerm.json` + } + ] } export const anotherTestVocabulary = { - uri: `${AUTH_BASE}/anotherTestVocabulary`, - label: ['Another Test Vocabulary'], - pref_label: ['Another Test Vocabulary'], - alt_label: ['Another Test Description'], - hidden_label: [], - absolute_path: `${API_BASE}/vocabularies/anotherTestVocabulary.json`, - terms: [ - { - uri: `${AUTH_BASE}/anotherTestVocabulary/firstTerm`, - label: ['First Term'], - pref_label: ['First Term'], - alt_label: [], - hidden_label: [], - absolute_path: `${API_BASE}/vocabularies/anotherTestVocabulary/firstTerm.json` - }, - { - uri: `${AUTH_BASE}/anotherTestVocabulary/secondTerm`, - label: ['Second Term'], - pref_label: ['Second Term'], - alt_label: [], - hidden_label: [], - absolute_path: `${API_BASE}/vocabularies/anotherTestVocabulary/secondTerm.json` - }, - { - uri: `${AUTH_BASE}/anotherTestVocabulary/thirdTerm`, - label: ['Third Term'], - pref_label: ['Third Term'], - alt_label: [], - hidden_label: [], - absolute_path: `${API_BASE}/vocabularies/anotherTestVocabulary/thirdTerm.json` - }, - { - uri: `${AUTH_BASE}/anotherTestVocabulary/fourthTerm`, - label: ['4th Term'], - pref_label: ['Fourth Term'], - alt_label: ['Fourth Term'], - hidden_label: [], - absolute_path: `${API_BASE}/vocabularies/anotherTestVocabulary/fourthTerm.json` - }, - { - uri: `${AUTH_BASE}/anotherTestVocabulary/fifthTerm`, - label: ['Fifth Term'], - pref_label: ['Fifth Term'], - alt_label: [], - hidden_label: [], - absolute_path: `${API_BASE}/vocabularies/anotherTestVocabulary/fifthTerm.json` - } - ] + uri: `${AUTH_BASE}/anotherTestVocabulary`, + label: ['Another Test Vocabulary'], + pref_label: ['Another Test Vocabulary'], + alt_label: ['Another Test Description'], + hidden_label: [], + absolute_path: `${API_BASE}/vocabularies/anotherTestVocabulary.json`, + terms: [ + { + uri: `${AUTH_BASE}/anotherTestVocabulary/firstTerm`, + label: ['First Term'], + pref_label: ['First Term'], + alt_label: [], + hidden_label: [], + absolute_path: `${API_BASE}/vocabularies/anotherTestVocabulary/firstTerm.json` + }, + { + uri: `${AUTH_BASE}/anotherTestVocabulary/secondTerm`, + label: ['Second Term'], + pref_label: ['Second Term'], + alt_label: [], + hidden_label: [], + absolute_path: `${API_BASE}/vocabularies/anotherTestVocabulary/secondTerm.json` + }, + { + uri: `${AUTH_BASE}/anotherTestVocabulary/thirdTerm`, + label: ['Third Term'], + pref_label: ['Third Term'], + alt_label: [], + hidden_label: [], + absolute_path: `${API_BASE}/vocabularies/anotherTestVocabulary/thirdTerm.json` + }, + { + uri: `${AUTH_BASE}/anotherTestVocabulary/fourthTerm`, + label: ['4th Term'], + pref_label: ['Fourth Term'], + alt_label: ['Fourth Term'], + hidden_label: [], + absolute_path: `${API_BASE}/vocabularies/anotherTestVocabulary/fourthTerm.json` + }, + { + uri: `${AUTH_BASE}/anotherTestVocabulary/fifthTerm`, + label: ['Fifth Term'], + pref_label: ['Fifth Term'], + alt_label: [], + hidden_label: [], + absolute_path: `${API_BASE}/vocabularies/anotherTestVocabulary/fifthTerm.json` + } + ] } export default testVocabulary diff --git a/src/actions/__tests__/notifications-test.js b/src/actions/__tests__/notifications-test.js index c874311..3357841 100644 --- a/src/actions/__tests__/notifications-test.js +++ b/src/actions/__tests__/notifications-test.js @@ -9,26 +9,26 @@ const mockStore = configureMockStore([thunk]) const store = mockStore([]) describe('Notifications actionCreator', function () { - afterEach(function () { - store.clearActions() - }) + afterEach(function () { + store.clearActions() + }) - describe('#clearNotification', function () { - it('dispatches CLEAR_NOTIFICATION type + index', function () { - const idx = 0 - store.dispatch(actionCreators.clearNotification(idx)) - const actions = store.getActions() + describe('#clearNotification', function () { + it('dispatches CLEAR_NOTIFICATION type + index', function () { + const idx = 0 + store.dispatch(actionCreators.clearNotification(idx)) + const actions = store.getActions() - expect(actions).to.have.length(1) - expect(actions[0].type).to.equal(CLEAR_NOTIFICATION) - expect(actions[0]).to.have.property('index') - expect(actions[0].index).to.equal(idx) - }) + expect(actions).to.have.length(1) + expect(actions[0].type).to.equal(CLEAR_NOTIFICATION) + expect(actions[0]).to.have.property('index') + expect(actions[0].index).to.equal(idx) + }) - it('does not dispatch if no index is passed', function () { - store.dispatch(actionCreators.clearNotification()) - const actions = store.getActions() - expect(actions).to.be.empty - }) - }) + it('does not dispatch if no index is passed', function () { + store.dispatch(actionCreators.clearNotification()) + const actions = store.getActions() + expect(actions).to.be.empty + }) + }) }) diff --git a/src/actions/__tests__/search-test.js b/src/actions/__tests__/search-test.js index be5475d..55ad8f0 100644 --- a/src/actions/__tests__/search-test.js +++ b/src/actions/__tests__/search-test.js @@ -4,195 +4,195 @@ import thunk from 'redux-thunk' import fetchMock from 'fetch-mock' import { - searchCatalog, - setSearchOption, - toggleSearchFacet, + searchCatalog, + setSearchOption, + toggleSearchFacet, } from '../search' import { - RECEIVE_SEARCH_RESULTS, - SEARCHING, + RECEIVE_SEARCH_RESULTS, + SEARCHING, } from '../../constants' const mockStore = configureMockStore([thunk]) const SEARCH_BASE = process.env.SEARCH_BASE_URL const state = { - query: 'cats AND dogs', - facets: { - collection: [ - 'fake collection' - ], - format: [ - 'book', 'dvd', - ] - }, - options: {}, + query: 'cats AND dogs', + facets: { + collection: [ + 'fake collection' + ], + format: [ + 'book', 'dvd', + ] + }, + options: {}, } const store = mockStore({search: state}) describe('Search actionCreator', function () { - beforeEach(function () { - if (!SEARCH_BASE) { - this.skip() - return - } - - const escaped = SEARCH_BASE.replace(/\./g, '\\.') - const reg = new RegExp(escaped + '\?.*') - - fetchMock.get(reg, {status: 200, body: {response: {}}}) - }) - - afterEach(function () { - fetchMock.restore() - store.clearActions() - }) - - describe('#searchCatalog', function () { - it('dispatches `SEARCHING` and `RECEIVE_SEARCH_RESULTS`', function () { - return store.dispatch(searchCatalog('some query')) - .then(() => { - const actions = store.getActions() - expect(actions).to.have.length(2) - expect(actions[0].type).to.equal(SEARCHING) - expect(actions[1].type).to.equal(RECEIVE_SEARCH_RESULTS) - }) - }) - - it('dispatches `SEARCHING` w/ query details', function () { - const query = 'cats AND dogs' - const facets = { one: ['a', 'b']} - const options = { per_page: 5 } - - return store.dispatch(searchCatalog(query, facets, options)) - .then(() => { - const actions = store.getActions() - expect(actions).to.have.length(2) - expect(actions[0].type).to.equal(SEARCHING) - expect(actions[0].query).to.equal(query) - expect(actions[0].facets).to.deep.equal(facets) - expect(actions[0].options.per_page).to.equal(options.per_page) - }) - }) - }) - - describe('#setSearchOption', function () { - it('adds key/val to `search.options`', function () { - const key = 'key' - const val = 'val' - - return store.dispatch(setSearchOption(key, val)).then(() => { - const actions = store.getActions() - expect(actions[0].type).to.equal(SEARCHING) - expect(actions[0].options).to.have.property(key) - expect(actions[0].options[key]).to.equal(val) - }) - }) - - it('will make an api call even if the key/val already exists', function () { - const key = 'key' - const val = 'val' - - const options = { - [key]: val - } - - const store = mockStore({search: {options}}) - - return store.dispatch(setSearchOption(key, val)) - .then(() => { - const actions = store.getActions() - expect(actions).to.not.be.empty - }) - }) - }) - - describe('#toggleSearchFacet', function () { - it('sets a facet + makes an API call', function () { - const field = 'facet_field' - const value = 'value' - - return store.dispatch(toggleSearchFacet(field, value, true)) - .then(() => { - const actions = store.getActions() - - // it calls 2 actions (SEARCHING + RECEIVE_SEARCH_{RESULTS,ERROR}) - expect(actions).to.have.length(2) - - // it sends the updated props w/ SEARCHING - const action = actions[0] - - // the facet prop now has the new property - expect(action.facets).to.have.property(field) - expect(action.facets[field]).to.have.length(1) - expect(action.facets[field].indexOf(value)).to.be.greaterThan(-1) - - // finally, make sure the API was actually called - const calls = fetchMock.calls() - expect(calls.matched).to.not.be.empty - expect(calls.matched).to.have.length(1) - }) - }) - - it('does not send duplicate search requests (for string values)', function () { - const field = 'facet_field' - const value = 'value' - const facets = {} - facets[field] = [value] - - const store = mockStore({search: {facets}}) - - return store.dispatch(toggleSearchFacet(field, value, true)) - .then(() => { - expect(fetchMock.calls().matched).to.have.length(0) - }) - }) - - it('does not send duplicate search requests (for non-string values)', function () { - const field = 'facet_field' - const value = {name: 'aye', value: 'a'} - const facets = {} - facets[field] = [value] - - const store = mockStore({search: {facets}}) - - return store.dispatch(toggleSearchFacet(field, value, true)) - .then(() => { - expect(fetchMock.calls().matched).to.have.length(0) - }) - }) - - it('removes a facet + makes an API call', function () { - const search = { - query: 'search query', - facets: { - 'facet_field': [ - {value: 'one', name: 'one', hits: 12}, - {value: 'two', name: 'two', hits: 123}, - {value: 'three', name: 'three', hits: 1234}, - ] - } - } - - const store = mockStore({search}) - const field = 'facet_field' - const value = search.facets[field][1] - - return store.dispatch(toggleSearchFacet(field, value, false)) - .then(() => { - const actions = store.getActions() - - expect(actions).to.have.length(2) - - const action = actions[0] - expect(action.facets[field]).to.have.length(search.facets[field].length - 1) - - const calls = fetchMock.calls() - expect(calls.matched).to.not.be.empty - expect(calls.matched).to.have.length(1) - }) - }) - }) + beforeEach(function () { + if (!SEARCH_BASE) { + this.skip() + return + } + + const escaped = SEARCH_BASE.replace(/\./g, '\\.') + const reg = new RegExp(escaped + '\?.*') + + fetchMock.get(reg, {status: 200, body: {response: {}}}) + }) + + afterEach(function () { + fetchMock.restore() + store.clearActions() + }) + + describe('#searchCatalog', function () { + it('dispatches `SEARCHING` and `RECEIVE_SEARCH_RESULTS`', function () { + return store.dispatch(searchCatalog('some query')) + .then(() => { + const actions = store.getActions() + expect(actions).to.have.length(2) + expect(actions[0].type).to.equal(SEARCHING) + expect(actions[1].type).to.equal(RECEIVE_SEARCH_RESULTS) + }) + }) + + it('dispatches `SEARCHING` w/ query details', function () { + const query = 'cats AND dogs' + const facets = { one: ['a', 'b']} + const options = { per_page: 5 } + + return store.dispatch(searchCatalog(query, facets, options)) + .then(() => { + const actions = store.getActions() + expect(actions).to.have.length(2) + expect(actions[0].type).to.equal(SEARCHING) + expect(actions[0].query).to.equal(query) + expect(actions[0].facets).to.deep.equal(facets) + expect(actions[0].options.per_page).to.equal(options.per_page) + }) + }) + }) + + describe('#setSearchOption', function () { + it('adds key/val to `search.options`', function () { + const key = 'key' + const val = 'val' + + return store.dispatch(setSearchOption(key, val)).then(() => { + const actions = store.getActions() + expect(actions[0].type).to.equal(SEARCHING) + expect(actions[0].options).to.have.property(key) + expect(actions[0].options[key]).to.equal(val) + }) + }) + + it('will make an api call even if the key/val already exists', function () { + const key = 'key' + const val = 'val' + + const options = { + [key]: val + } + + const store = mockStore({search: {options}}) + + return store.dispatch(setSearchOption(key, val)) + .then(() => { + const actions = store.getActions() + expect(actions).to.not.be.empty + }) + }) + }) + + describe('#toggleSearchFacet', function () { + it('sets a facet + makes an API call', function () { + const field = 'facet_field' + const value = 'value' + + return store.dispatch(toggleSearchFacet(field, value, true)) + .then(() => { + const actions = store.getActions() + + // it calls 2 actions (SEARCHING + RECEIVE_SEARCH_{RESULTS,ERROR}) + expect(actions).to.have.length(2) + + // it sends the updated props w/ SEARCHING + const action = actions[0] + + // the facet prop now has the new property + expect(action.facets).to.have.property(field) + expect(action.facets[field]).to.have.length(1) + expect(action.facets[field].indexOf(value)).to.be.greaterThan(-1) + + // finally, make sure the API was actually called + const calls = fetchMock.calls() + expect(calls.matched).to.not.be.empty + expect(calls.matched).to.have.length(1) + }) + }) + + it('does not send duplicate search requests (for string values)', function () { + const field = 'facet_field' + const value = 'value' + const facets = {} + facets[field] = [value] + + const store = mockStore({search: {facets}}) + + return store.dispatch(toggleSearchFacet(field, value, true)) + .then(() => { + expect(fetchMock.calls().matched).to.have.length(0) + }) + }) + + it('does not send duplicate search requests (for non-string values)', function () { + const field = 'facet_field' + const value = {name: 'aye', value: 'a'} + const facets = {} + facets[field] = [value] + + const store = mockStore({search: {facets}}) + + return store.dispatch(toggleSearchFacet(field, value, true)) + .then(() => { + expect(fetchMock.calls().matched).to.have.length(0) + }) + }) + + it('removes a facet + makes an API call', function () { + const search = { + query: 'search query', + facets: { + 'facet_field': [ + {value: 'one', name: 'one', hits: 12}, + {value: 'two', name: 'two', hits: 123}, + {value: 'three', name: 'three', hits: 1234}, + ] + } + } + + const store = mockStore({search}) + const field = 'facet_field' + const value = search.facets[field][1] + + return store.dispatch(toggleSearchFacet(field, value, false)) + .then(() => { + const actions = store.getActions() + + expect(actions).to.have.length(2) + + const action = actions[0] + expect(action.facets[field]).to.have.length(search.facets[field].length - 1) + + const calls = fetchMock.calls() + expect(calls.matched).to.not.be.empty + expect(calls.matched).to.have.length(1) + }) + }) + }) }) diff --git a/src/actions/__tests__/terms-test.js b/src/actions/__tests__/terms-test.js index 30e1be8..d80f503 100644 --- a/src/actions/__tests__/terms-test.js +++ b/src/actions/__tests__/terms-test.js @@ -6,371 +6,371 @@ import assign from 'object-assign' import randomIndex from 'random-array-index' import { - addTermToVocabulary, - bulkEditTermsInVocabulary, - fetchTermsFromVocabulary, - removeTermFromVocabulary, - updateTermInVocabulary, + addTermToVocabulary, + bulkEditTermsInVocabulary, + fetchTermsFromVocabulary, + removeTermFromVocabulary, + updateTermInVocabulary, } from '../terms' import { - testVocabulary as VOCAB_ONE, + testVocabulary as VOCAB_ONE, } from './data/vocabularies-with-terms' import { - ADD_TERM_TO_VOCABULARY, - BULK_EDIT_TERMS, - FETCHING_VOCABULARY_TERMS, - RECEIVE_VOCABULARY_TERMS, - RECEIVE_VOCABULARY_TERMS_ERR, - REMOVE_TERM_FROM_VOCABULARY, - UPDATE_TERM_REQUEST, - UPDATE_TERM_RESPONSE_OK, - UPDATE_TERM_RESPONSE_ERR, + ADD_TERM_TO_VOCABULARY, + BULK_EDIT_TERMS, + FETCHING_VOCABULARY_TERMS, + RECEIVE_VOCABULARY_TERMS, + RECEIVE_VOCABULARY_TERMS_ERR, + REMOVE_TERM_FROM_VOCABULARY, + UPDATE_TERM_REQUEST, + UPDATE_TERM_RESPONSE_OK, + UPDATE_TERM_RESPONSE_ERR, } from '../../constants' const mockStore = configureMockStore([thunk]) const API_BASE = process.env.API_BASE_URL const expectTerm = term => { - expect(term).to.be.an('object') - expect(term).to.have.property('uri') - expect(term).to.have.property('label') - expect(term).to.have.property('alt_label') - expect(term).to.have.property('pref_label') - expect(term).to.have.property('hidden_label') - - expect(term.uri).to.be.a('string') - - expect(term.label).to.be.an('array') - expect(term.alt_label).to.be.an('array') - expect(term.pref_label).to.be.an('array') - expect(term.hidden_label).to.be.an('array') + expect(term).to.be.an('object') + expect(term).to.have.property('uri') + expect(term).to.have.property('label') + expect(term).to.have.property('alt_label') + expect(term).to.have.property('pref_label') + expect(term).to.have.property('hidden_label') + + expect(term.uri).to.be.a('string') + + expect(term.label).to.be.an('array') + expect(term.alt_label).to.be.an('array') + expect(term.pref_label).to.be.an('array') + expect(term.hidden_label).to.be.an('array') } describe('Terms actionCreators', function () { - beforeEach(function () { - if (!API_BASE) - this.skip() - }) - - describe('#addTermToVocabulary', function () { - const vocab = VOCAB_ONE - const testTerm = 'test-term' - const store = mockStore({}) - - beforeEach(function () { - fetchMock.mock( - vocab.absolute_path, - {status: 200, body: {status: 'ok'}}, - {method: 'PATCH'} - ) - }) - - afterEach(function () { - fetchMock.restore() - store.clearActions() - }) - - const boundAction = addTermToVocabulary.bind(null, vocab, testTerm) - - it('dispatches ADD_TERM_TO_VOCABULARY type', function () { - return store.dispatch(boundAction()).then(() => { - const actions = store.getActions() - expect(actions).to.have.length(1) - expect(actions[0].type).to.equal(ADD_TERM_TO_VOCABULARY) - }) - }) - - describe('the `action.data` object returned', function () { - it('is indeed an object', function () { - return store.dispatch(boundAction()).then(() => { - expect(store.getActions()[0].data).to.be.an('object') - }) - }) - - it('creates `label` using the term string', function () { - return store.dispatch(boundAction()).then(() => { - const actions = store.getActions() - const action = actions[0] - - expect(action.data.label).to.be.an('array') - expect(action.data.label).to.have.length(1) - expect(action.data.label[0]).to.equal(testTerm) - }) - }) - - it('sets `pref_label` to the same value as `label`', function () { - return store.dispatch(boundAction()).then(() => { - const actions = store.getActions() - const action = actions[0] - - expect(action.data.pref_label).to.be.an('array') - expect(action.data.pref_label).to.have.length(1) - expect(action.data.pref_label).to.deep.equal(action.data.label) - }) - }) - }) - }) - - describe('#bulkEditTermsInVocabulary', function () { - const vocab = VOCAB_ONE - const activeTerms = [].concat(vocab.terms) - const state = {activeVocabularyTerms: { data: activeTerms }} - - const prevTerms = activeTerms.map(t => t.pref_label[0]) - const newTerms = [ - 'Picrodon', - 'Mononychus', - 'Bothriospondylus', - 'Velociraptor', - 'Eolosaurus', - 'Compsosuchus', - ] - - const store = mockStore(state) - const boundAction = bulkEditTermsInVocabulary.bind(null, vocab) - - beforeEach(function () { - fetchMock.put(vocab.absolute_path, {status: 200, body: {status: 'ok'}}) - }) - - afterEach(function () { - fetchMock.restore() - store.clearActions() - }) - - it('dispatches BULK_EDIT_TERMS type', function () { - return store.dispatch(boundAction(prevTerms)) - .then(() => { - const actions = store.getActions() - const action = actions[0] - - expect(actions).to.have.length(1) - expect(action.type).to.equal(BULK_EDIT_TERMS) - }) - }) - - it('converts terms array of strings into term objects', function () { - const isString = t => t && typeof t === 'string' - expect(newTerms.every(isString)).to.be.true - - return store.dispatch(boundAction(newTerms)) - .then(() => store.getActions()[0].terms.forEach(expectTerm)) - }) - - it('retains terms previously found in state', function () { - return store.dispatch(boundAction(prevTerms)) - .then(() => { - const terms = store.getActions()[0].terms - expect(terms).to.deep.equal(activeTerms) - }) - }) - - }) - - describe('#fetchTermsFromVocabulary', function () { - const vocab = VOCAB_ONE - const store = mockStore({}) - const boundAction = fetchTermsFromVocabulary.bind(null, vocab) - - beforeEach(function () { - fetchMock.get(vocab.absolute_path, {status: 200, body: vocab}) - fetchMock.get('*', 404) - }) - - afterEach(function () { - fetchMock.restore() - store.clearActions() - }) - - it('dispatches {FETCHING,RECEIVE}_VOCABULARY_TERMS types', function () { - return store.dispatch(boundAction()) - .then(() => { - const actions = store.getActions() - expect(actions).to.have.length(2) - expect(actions[0].type).to.equal(FETCHING_VOCABULARY_TERMS) - expect(actions[1].type).to.equal(RECEIVE_VOCABULARY_TERMS) - }) - }) - - it('dispatches RECEIVE_VOCABULARY_TERMS_ERR when 404\'d', function () { - const nope = { - absolute_path: `${API_BASE}/vocabularies/nope.json`, - } - - return store.dispatch(fetchTermsFromVocabulary(nope)) - .then(() => { - const actions = store.getActions() - expect(actions).to.have.length(2) - expect(actions[0].type).to.equal(FETCHING_VOCABULARY_TERMS) - expect(actions[1].type).to.equal(RECEIVE_VOCABULARY_TERMS_ERR) - }) - }) - - it('dispatches the terms extracted from vocabulary response', function () { - return store.dispatch(boundAction()) - .then(() => { - const actions = store.getActions() - const terms = actions[1].data - - expect(terms).to.be.an('array') - terms.forEach(expectTerm) - }) - }) - }) - - describe('#removeTermFromVocabulary', function () { - const vocab = VOCAB_ONE - const terms = [].concat(vocab.terms) - - const removeRandomTerm = () => { - const index = randomIndex(terms) - const term = terms[index].pref_label[0] - - return removeTermFromVocabulary(vocab, term, index) - } - - const state = { - activeVocabularyTerms: { - data: terms, - } - } - - const store = mockStore(state) - - beforeEach(function () { - fetchMock.put(vocab.absolute_path, {status: 200, body: {status: 'ok'}}) - }) - - afterEach(function () { - fetchMock.restore() - store.clearActions() - }) - - it('dispatches REMOVE_TERM_FROM_VOCABULARY type', function () { - return store.dispatch(removeRandomTerm()) - .then(() => { - const actions = store.getActions() - expect(actions).to.have.length(1) - expect(actions[0].type).to.equal(REMOVE_TERM_FROM_VOCABULARY) - }) - }) - - it('sends index, term, and vocabulary data with action', function () { - return store.dispatch(removeRandomTerm()) - .then(() => { - const action = store.getActions()[0] - expect(action).to.have.property('index') - expect(action).to.have.property('term') - expect(action).to.have.property('vocabulary') - }) - }) - - it('slices the term out of the array', function () { - return store.dispatch(removeRandomTerm()) - .then(() => { - let body = fetchMock.lastOptions().body - if (typeof body === 'string') - body = JSON.parse(body) - - const stateTerms = store.getState().activeVocabularyTerms.data - const sentTerms = body.vocabulary.terms - - expect(stateTerms.length).to.be.greaterThan(sentTerms.length) - expect(stateTerms.length - sentTerms.length).to.equal(1) - - const term = store.getActions()[0].term - - expect(sentTerms.some(t => t.pref_label.indexOf(term) > -1)).to.be.false - }) - }) - }) - - describe('#updateTermInVocabulary', function () { - const vocab = VOCAB_ONE - const terms = [].concat(vocab.terms) - const store = mockStore({}) - - const getRandomTerm = () => { - const index = randomIndex(terms) - return assign({}, terms[index]) - } - - const updateRandomTerm = () => { - const term = getRandomTerm() - - // see https://github.com/dariusk/corpora/blob/master/data/games/wrestling_moves.json - const pool = [ - 'Double inverted DDT', 'Elevated jawbreaker', 'Elevated splash', - 'Enzuigiri', 'European uppercut', 'Eye poke', 'Go 2 Sleep', - ] - - const update = pool[randomIndex(pool)] - term.alt_label.push(update) - - return term - } - - beforeEach(function () { - fetchMock.mock( - vocab.absolute_path, - {status: 200, body: {status: 'ok'}}, - {method: 'PATCH'} - ) - fetchMock.mock('*', 404, {method: 'PATCH'}) - }) - - afterEach(function () { - fetchMock.restore() - store.clearActions() - }) - - it('dispatches UPDATE_TERM_{REQUEST,RESPONSE_OK} types', function () { - const term = updateRandomTerm() - const termStr = term.pref_label[0] - return store.dispatch(updateTermInVocabulary(vocab, termStr, term)) - .then(() => { - const actions = store.getActions() - expect(actions).to.have.length(2) - expect(actions[0].type).to.equal(UPDATE_TERM_REQUEST) - expect(actions[1].type).to.equal(UPDATE_TERM_RESPONSE_OK) - }) - }) - - it('dispatches UPDATE_TERM_{REQUEST,RESPONSE_ERR} when 404\'d', function () { - const nope = { - absolute_path: 'http://nope.org' - } - - const term = updateRandomTerm() - const termStr = term.pref_label[0] - - return store.dispatch(updateTermInVocabulary(nope, termStr, term)) - .then(() => { - const actions = store.getActions() - expect(actions).to.have.length(2) - expect(actions[0].type).to.equal(UPDATE_TERM_REQUEST) - expect(actions[1].type).to.equal(UPDATE_TERM_RESPONSE_ERR) - }) - }) - - it('sends the previous pref_label with action', function () { - const term = getRandomTerm() - const termStr = term.pref_label[0] - - term.pref_label = ['=^_^= ~~~ new Pref Label'] - - return store.dispatch(updateTermInVocabulary(vocab, termStr, term)) - .then(() => { - const actions = store.getActions() - - expect(actions).to.have.length(2) - - const update = actions[1] - expect(update.previousPrefLabel).to.not.equal(update.data.pref_label[0]) - expect(update.previousPrefLabel).to.equal(termStr) - }) - }) - }) + beforeEach(function () { + if (!API_BASE) + this.skip() + }) + + describe('#addTermToVocabulary', function () { + const vocab = VOCAB_ONE + const testTerm = 'test-term' + const store = mockStore({}) + + beforeEach(function () { + fetchMock.mock( + vocab.absolute_path, + {status: 200, body: {status: 'ok'}}, + {method: 'PATCH'} + ) + }) + + afterEach(function () { + fetchMock.restore() + store.clearActions() + }) + + const boundAction = addTermToVocabulary.bind(null, vocab, testTerm) + + it('dispatches ADD_TERM_TO_VOCABULARY type', function () { + return store.dispatch(boundAction()).then(() => { + const actions = store.getActions() + expect(actions).to.have.length(1) + expect(actions[0].type).to.equal(ADD_TERM_TO_VOCABULARY) + }) + }) + + describe('the `action.data` object returned', function () { + it('is indeed an object', function () { + return store.dispatch(boundAction()).then(() => { + expect(store.getActions()[0].data).to.be.an('object') + }) + }) + + it('creates `label` using the term string', function () { + return store.dispatch(boundAction()).then(() => { + const actions = store.getActions() + const action = actions[0] + + expect(action.data.label).to.be.an('array') + expect(action.data.label).to.have.length(1) + expect(action.data.label[0]).to.equal(testTerm) + }) + }) + + it('sets `pref_label` to the same value as `label`', function () { + return store.dispatch(boundAction()).then(() => { + const actions = store.getActions() + const action = actions[0] + + expect(action.data.pref_label).to.be.an('array') + expect(action.data.pref_label).to.have.length(1) + expect(action.data.pref_label).to.deep.equal(action.data.label) + }) + }) + }) + }) + + describe('#bulkEditTermsInVocabulary', function () { + const vocab = VOCAB_ONE + const activeTerms = [].concat(vocab.terms) + const state = {activeVocabularyTerms: { data: activeTerms }} + + const prevTerms = activeTerms.map(t => t.pref_label[0]) + const newTerms = [ + 'Picrodon', + 'Mononychus', + 'Bothriospondylus', + 'Velociraptor', + 'Eolosaurus', + 'Compsosuchus', + ] + + const store = mockStore(state) + const boundAction = bulkEditTermsInVocabulary.bind(null, vocab) + + beforeEach(function () { + fetchMock.put(vocab.absolute_path, {status: 200, body: {status: 'ok'}}) + }) + + afterEach(function () { + fetchMock.restore() + store.clearActions() + }) + + it('dispatches BULK_EDIT_TERMS type', function () { + return store.dispatch(boundAction(prevTerms)) + .then(() => { + const actions = store.getActions() + const action = actions[0] + + expect(actions).to.have.length(1) + expect(action.type).to.equal(BULK_EDIT_TERMS) + }) + }) + + it('converts terms array of strings into term objects', function () { + const isString = t => t && typeof t === 'string' + expect(newTerms.every(isString)).to.be.true + + return store.dispatch(boundAction(newTerms)) + .then(() => store.getActions()[0].terms.forEach(expectTerm)) + }) + + it('retains terms previously found in state', function () { + return store.dispatch(boundAction(prevTerms)) + .then(() => { + const terms = store.getActions()[0].terms + expect(terms).to.deep.equal(activeTerms) + }) + }) + + }) + + describe('#fetchTermsFromVocabulary', function () { + const vocab = VOCAB_ONE + const store = mockStore({}) + const boundAction = fetchTermsFromVocabulary.bind(null, vocab) + + beforeEach(function () { + fetchMock.get(vocab.absolute_path, {status: 200, body: vocab}) + fetchMock.get('*', 404) + }) + + afterEach(function () { + fetchMock.restore() + store.clearActions() + }) + + it('dispatches {FETCHING,RECEIVE}_VOCABULARY_TERMS types', function () { + return store.dispatch(boundAction()) + .then(() => { + const actions = store.getActions() + expect(actions).to.have.length(2) + expect(actions[0].type).to.equal(FETCHING_VOCABULARY_TERMS) + expect(actions[1].type).to.equal(RECEIVE_VOCABULARY_TERMS) + }) + }) + + it('dispatches RECEIVE_VOCABULARY_TERMS_ERR when 404\'d', function () { + const nope = { + absolute_path: `${API_BASE}/vocabularies/nope.json`, + } + + return store.dispatch(fetchTermsFromVocabulary(nope)) + .then(() => { + const actions = store.getActions() + expect(actions).to.have.length(2) + expect(actions[0].type).to.equal(FETCHING_VOCABULARY_TERMS) + expect(actions[1].type).to.equal(RECEIVE_VOCABULARY_TERMS_ERR) + }) + }) + + it('dispatches the terms extracted from vocabulary response', function () { + return store.dispatch(boundAction()) + .then(() => { + const actions = store.getActions() + const terms = actions[1].data + + expect(terms).to.be.an('array') + terms.forEach(expectTerm) + }) + }) + }) + + describe('#removeTermFromVocabulary', function () { + const vocab = VOCAB_ONE + const terms = [].concat(vocab.terms) + + const removeRandomTerm = () => { + const index = randomIndex(terms) + const term = terms[index].pref_label[0] + + return removeTermFromVocabulary(vocab, term, index) + } + + const state = { + activeVocabularyTerms: { + data: terms, + } + } + + const store = mockStore(state) + + beforeEach(function () { + fetchMock.put(vocab.absolute_path, {status: 200, body: {status: 'ok'}}) + }) + + afterEach(function () { + fetchMock.restore() + store.clearActions() + }) + + it('dispatches REMOVE_TERM_FROM_VOCABULARY type', function () { + return store.dispatch(removeRandomTerm()) + .then(() => { + const actions = store.getActions() + expect(actions).to.have.length(1) + expect(actions[0].type).to.equal(REMOVE_TERM_FROM_VOCABULARY) + }) + }) + + it('sends index, term, and vocabulary data with action', function () { + return store.dispatch(removeRandomTerm()) + .then(() => { + const action = store.getActions()[0] + expect(action).to.have.property('index') + expect(action).to.have.property('term') + expect(action).to.have.property('vocabulary') + }) + }) + + it('slices the term out of the array', function () { + return store.dispatch(removeRandomTerm()) + .then(() => { + let body = fetchMock.lastOptions().body + if (typeof body === 'string') + body = JSON.parse(body) + + const stateTerms = store.getState().activeVocabularyTerms.data + const sentTerms = body.vocabulary.terms + + expect(stateTerms.length).to.be.greaterThan(sentTerms.length) + expect(stateTerms.length - sentTerms.length).to.equal(1) + + const term = store.getActions()[0].term + + expect(sentTerms.some(t => t.pref_label.indexOf(term) > -1)).to.be.false + }) + }) + }) + + describe('#updateTermInVocabulary', function () { + const vocab = VOCAB_ONE + const terms = [].concat(vocab.terms) + const store = mockStore({}) + + const getRandomTerm = () => { + const index = randomIndex(terms) + return assign({}, terms[index]) + } + + const updateRandomTerm = () => { + const term = getRandomTerm() + + // see https://github.com/dariusk/corpora/blob/master/data/games/wrestling_moves.json + const pool = [ + 'Double inverted DDT', 'Elevated jawbreaker', 'Elevated splash', + 'Enzuigiri', 'European uppercut', 'Eye poke', 'Go 2 Sleep', + ] + + const update = pool[randomIndex(pool)] + term.alt_label.push(update) + + return term + } + + beforeEach(function () { + fetchMock.mock( + vocab.absolute_path, + {status: 200, body: {status: 'ok'}}, + {method: 'PATCH'} + ) + fetchMock.mock('*', 404, {method: 'PATCH'}) + }) + + afterEach(function () { + fetchMock.restore() + store.clearActions() + }) + + it('dispatches UPDATE_TERM_{REQUEST,RESPONSE_OK} types', function () { + const term = updateRandomTerm() + const termStr = term.pref_label[0] + return store.dispatch(updateTermInVocabulary(vocab, termStr, term)) + .then(() => { + const actions = store.getActions() + expect(actions).to.have.length(2) + expect(actions[0].type).to.equal(UPDATE_TERM_REQUEST) + expect(actions[1].type).to.equal(UPDATE_TERM_RESPONSE_OK) + }) + }) + + it('dispatches UPDATE_TERM_{REQUEST,RESPONSE_ERR} when 404\'d', function () { + const nope = { + absolute_path: 'http://nope.org' + } + + const term = updateRandomTerm() + const termStr = term.pref_label[0] + + return store.dispatch(updateTermInVocabulary(nope, termStr, term)) + .then(() => { + const actions = store.getActions() + expect(actions).to.have.length(2) + expect(actions[0].type).to.equal(UPDATE_TERM_REQUEST) + expect(actions[1].type).to.equal(UPDATE_TERM_RESPONSE_ERR) + }) + }) + + it('sends the previous pref_label with action', function () { + const term = getRandomTerm() + const termStr = term.pref_label[0] + + term.pref_label = ['=^_^= ~~~ new Pref Label'] + + return store.dispatch(updateTermInVocabulary(vocab, termStr, term)) + .then(() => { + const actions = store.getActions() + + expect(actions).to.have.length(2) + + const update = actions[1] + expect(update.previousPrefLabel).to.not.equal(update.data.pref_label[0]) + expect(update.previousPrefLabel).to.equal(termStr) + }) + }) + }) }) diff --git a/src/actions/__tests__/vocabulary-test.js b/src/actions/__tests__/vocabulary-test.js index d931841..b007939 100644 --- a/src/actions/__tests__/vocabulary-test.js +++ b/src/actions/__tests__/vocabulary-test.js @@ -7,18 +7,18 @@ import thunk from 'redux-thunk' import fetchMock from 'fetch-mock' import { - CREATE_VOCABULARY_REQUEST, - CREATE_VOCABULARY_RESPONSE_OK, + CREATE_VOCABULARY_REQUEST, + CREATE_VOCABULARY_RESPONSE_OK, - DELETE_VOCABULARY_REQUEST, - DELETE_VOCABULARY_RESPONSE_OK, + DELETE_VOCABULARY_REQUEST, + DELETE_VOCABULARY_RESPONSE_OK, - FETCHING_ALL_VOCABULARIES, - RECEIVE_ALL_VOCABULARIES, + FETCHING_ALL_VOCABULARIES, + RECEIVE_ALL_VOCABULARIES, - UPDATING_VOCABULARY, - UPDATE_VOCABULARY_OK, - UPDATE_VOCABULARY_ERR, + UPDATING_VOCABULARY, + UPDATE_VOCABULARY_OK, + UPDATE_VOCABULARY_ERR, } from '../../constants' import { testVocabulary as VOCAB_DATA } from './data/vocabularies-with-terms' @@ -27,159 +27,159 @@ const mockStore = configureMockStore([thunk]) const API_BASE = process.env.API_BASE_URL describe('Vocabulary actionCreator', function () { - beforeEach(function () { - if (!API_BASE) - this.skip() - }) - describe('#createVocabulary', function () { - beforeEach(function () { - fetchMock.post( - `${API_BASE}/vocabularies.json`, - { status: 200, body: {status: 'ok'} } - ) - }) - - afterEach(fetchMock.restore) - - const data = { - label: ['Test Vocabulary'], - alt_label: ['Test Description'], - pref_label: [], - hidden_label: [], - } - - const store = mockStore({vocabularies: []}) - - // the data we get back is the same as the full Vocab output - // (minus the `terms` array) - const expectData = assign({}, VOCAB_DATA) - delete expectData.terms - - const expectActions = [ - { - type: CREATE_VOCABULARY_REQUEST - }, - { - type: CREATE_VOCABULARY_RESPONSE_OK, - data: expectData, - } - ] - - it('dispatches CREATE_VOCABULARY_REQUEST{,_OK}', function () { - return store.dispatch(actions.createVocabulary(data)) - .then(() => { - expect(store.getActions()).to.deep.equal(expectActions) - }) - }) - }) - - describe('#deleteVocabulary', function () { - before(function () { - fetchMock.delete( - VOCAB_DATA.absolute_path, - {status: 200, body: {status: 'ok'}} - ) - }) - - after(fetchMock.restore) - - const store = mockStore({vocabularies: [VOCAB_DATA]}) - const expectActions = [ - { - type: DELETE_VOCABULARY_REQUEST, - }, - { - type: DELETE_VOCABULARY_RESPONSE_OK, - data: VOCAB_DATA, - } - ] - - it('dispatches DELETE_VOCABULARY_{REQUEST,RESPONSE_OK}', function () { - return store.dispatch(actions.deleteVocabulary(VOCAB_DATA)) - .then(() => { - expect(store.getActions()).to.deep.equal(expectActions) - }) - }) - - it('does not send data as part of the DELETE request', function () { - const opts = fetchMock.lastOptions(VOCAB_DATA.absolute_path) - expect(opts.body).to.be.empty - }) - }) - - describe('#fetchAllVocabularies', function () { - beforeEach(function () { - fetchMock.get( - `${API_BASE}/vocabularies.json`, - { - vocabularies: [assign({}, VOCAB_DATA)] - } - ) - }) - - afterEach(fetchMock.restore) - - const store = mockStore({vocabularies: []}) - const expectActions = [ - { - type: FETCHING_ALL_VOCABULARIES - }, - { - type: RECEIVE_ALL_VOCABULARIES, - data: [assign({}, VOCAB_DATA)] - } - ] - - it('dispatches FETCHING_ALL_VOCABULARIES and RECEIVE_ALL_VOCABULARIES', function () { - return store.dispatch(actions.fetchAllVocabularies()) - .then(() => { - expect(store.getActions()).to.deep.equal(expectActions) - }) - }) - }) - - describe('#updateVocabularyMetadata', function () { - const DATA = assign({}, VOCAB_DATA) - delete DATA.terms - - const url = DATA.absolute_path - - beforeEach(function () { - fetchMock.patch(url, {status: 200, body: {status: 'ok'}}) - fetchMock.patch('*', 404) - }) - - afterEach(fetchMock.restore) - - it('patches an update the API', function () { - const store = mockStore({vocabularies: [DATA]}) - const data = assign({}, DATA) - data.hidden_label = [`A new hidden label - ${Date.now()}`] - - return store.dispatch(actions.updateVocabularyMetadata(data)) - .then(() => { - const actions = store.getActions() - expect(actions).to.have.length(2) - expect(actions[0].type).to.equal(UPDATING_VOCABULARY) - expect(actions[1].type).to.equal(UPDATE_VOCABULARY_OK) - }) - }) - - it('dispatches UPDATE_VOCABULARY_ERR when there is a problem', function () { - const store = mockStore({vocabularies: [DATA]}) - const data = assign({}, DATA) - data.absolute_path = 'http://this.isnt.real.biz' - - return store.dispatch(actions.updateVocabularyMetadata(data)) - .then(() => { - throw Error('this should have thrown an error') - }) - .catch(() => { - const actions = store.getActions() - expect(actions).to.have.length(2) - expect(actions[0].type).to.equal(UPDATING_VOCABULARY) - expect(actions[1].type).to.equal(UPDATE_VOCABULARY_ERR) - }) - }) - }) + beforeEach(function () { + if (!API_BASE) + this.skip() + }) + describe('#createVocabulary', function () { + beforeEach(function () { + fetchMock.post( + `${API_BASE}/vocabularies.json`, + { status: 200, body: {status: 'ok'} } + ) + }) + + afterEach(fetchMock.restore) + + const data = { + label: ['Test Vocabulary'], + alt_label: ['Test Description'], + pref_label: [], + hidden_label: [], + } + + const store = mockStore({vocabularies: []}) + + // the data we get back is the same as the full Vocab output + // (minus the `terms` array) + const expectData = assign({}, VOCAB_DATA) + delete expectData.terms + + const expectActions = [ + { + type: CREATE_VOCABULARY_REQUEST + }, + { + type: CREATE_VOCABULARY_RESPONSE_OK, + data: expectData, + } + ] + + it('dispatches CREATE_VOCABULARY_REQUEST{,_OK}', function () { + return store.dispatch(actions.createVocabulary(data)) + .then(() => { + expect(store.getActions()).to.deep.equal(expectActions) + }) + }) + }) + + describe('#deleteVocabulary', function () { + before(function () { + fetchMock.delete( + VOCAB_DATA.absolute_path, + {status: 200, body: {status: 'ok'}} + ) + }) + + after(fetchMock.restore) + + const store = mockStore({vocabularies: [VOCAB_DATA]}) + const expectActions = [ + { + type: DELETE_VOCABULARY_REQUEST, + }, + { + type: DELETE_VOCABULARY_RESPONSE_OK, + data: VOCAB_DATA, + } + ] + + it('dispatches DELETE_VOCABULARY_{REQUEST,RESPONSE_OK}', function () { + return store.dispatch(actions.deleteVocabulary(VOCAB_DATA)) + .then(() => { + expect(store.getActions()).to.deep.equal(expectActions) + }) + }) + + it('does not send data as part of the DELETE request', function () { + const opts = fetchMock.lastOptions(VOCAB_DATA.absolute_path) + expect(opts.body).to.be.empty + }) + }) + + describe('#fetchAllVocabularies', function () { + beforeEach(function () { + fetchMock.get( + `${API_BASE}/vocabularies.json`, + { + vocabularies: [assign({}, VOCAB_DATA)] + } + ) + }) + + afterEach(fetchMock.restore) + + const store = mockStore({vocabularies: []}) + const expectActions = [ + { + type: FETCHING_ALL_VOCABULARIES + }, + { + type: RECEIVE_ALL_VOCABULARIES, + data: [assign({}, VOCAB_DATA)] + } + ] + + it('dispatches FETCHING_ALL_VOCABULARIES and RECEIVE_ALL_VOCABULARIES', function () { + return store.dispatch(actions.fetchAllVocabularies()) + .then(() => { + expect(store.getActions()).to.deep.equal(expectActions) + }) + }) + }) + + describe('#updateVocabularyMetadata', function () { + const DATA = assign({}, VOCAB_DATA) + delete DATA.terms + + const url = DATA.absolute_path + + beforeEach(function () { + fetchMock.patch(url, {status: 200, body: {status: 'ok'}}) + fetchMock.patch('*', 404) + }) + + afterEach(fetchMock.restore) + + it('patches an update the API', function () { + const store = mockStore({vocabularies: [DATA]}) + const data = assign({}, DATA) + data.hidden_label = [`A new hidden label - ${Date.now()}`] + + return store.dispatch(actions.updateVocabularyMetadata(data)) + .then(() => { + const actions = store.getActions() + expect(actions).to.have.length(2) + expect(actions[0].type).to.equal(UPDATING_VOCABULARY) + expect(actions[1].type).to.equal(UPDATE_VOCABULARY_OK) + }) + }) + + it('dispatches UPDATE_VOCABULARY_ERR when there is a problem', function () { + const store = mockStore({vocabularies: [DATA]}) + const data = assign({}, DATA) + data.absolute_path = 'http://this.isnt.real.biz' + + return store.dispatch(actions.updateVocabularyMetadata(data)) + .then(() => { + throw Error('this should have thrown an error') + }) + .catch(() => { + const actions = store.getActions() + expect(actions).to.have.length(2) + expect(actions[0].type).to.equal(UPDATING_VOCABULARY) + expect(actions[1].type).to.equal(UPDATE_VOCABULARY_ERR) + }) + }) + }) }) diff --git a/src/actions/__tests__/work-test.js b/src/actions/__tests__/work-test.js index b862bdb..37af8a3 100644 --- a/src/actions/__tests__/work-test.js +++ b/src/actions/__tests__/work-test.js @@ -6,140 +6,140 @@ import thunk from 'redux-thunk' import fetchMock from 'fetch-mock' import { - FETCHING_WORK, - FETCHING_WORK_ERR, - RECEIVE_WORK, - SAVING_WORK, - SAVED_WORK, - WORK_NOT_FOUND_ERR, + FETCHING_WORK, + FETCHING_WORK_ERR, + RECEIVE_WORK, + SAVING_WORK, + SAVED_WORK, + WORK_NOT_FOUND_ERR, } from '../../constants' const mockStore = configureMockStore([thunk]) const apiWorkUrl = id => ( - `${process.env.API_BASE_URL}/concern/generic_works/${id}.json` + `${process.env.API_BASE_URL}/concern/generic_works/${id}.json` ) describe('Work actionCreator', function () { - describe('#fetchWork', function () { - const id = 'example-id' - - before(function () { - if (!process.env.API_BASE_URL) { - this.skip() - return - } - - fetchMock - .get(apiWorkUrl(id), { title: ['HULLO!'] }) - .get(apiWorkUrl('fake-id'), 404) - .get('*', 500) - }) - - after(fetchMock.restore) - - - it('sends FETCHING_WORK and RECEIVE_WORK actions', function () { - const expectedActions = [ - {type: FETCHING_WORK, id}, - {type: RECEIVE_WORK, data: {title: ['HULLO!']}} - ] - - const store = mockStore({work: {}}) - - return store.dispatch(actions.fetchWork(id)) - .then(() => { - expect(store.getActions()).to.deep.equal(expectedActions) - }) - }) - - it('sends FETCHING_WORK and WORK_NOT_FOUND if not found', function () { - const id = 'fake-id' - const store = mockStore({work: {}}) - - return store.dispatch(actions.fetchWork(id)) - .then(() => { - const actions = store.getActions() - expect(actions[0].type).to.equal(FETCHING_WORK) - expect(actions[0].id).to.equal(id) - expect(actions[1].type).to.equal(WORK_NOT_FOUND_ERR) - expect(actions[1].error.status).to.equal(404) - }) - }) - - it('sends FETCHING_WORK and FETCHING_WORK_ERR if encountering an error', function () { - const id = 'whatever' - const store = mockStore({work: {}}) - - return store.dispatch(actions.fetchWork(id)) - .then(() => { - const actions = store.getActions() - expect(actions[0].type).to.equal(FETCHING_WORK) - expect(actions[1].id).to.equal(id) - expect(actions[1].type).to.equal(FETCHING_WORK_ERR) - }) - }) - }) - - describe('#saveWork', function () { - const id = 'example-work' - - before(function () { - if (!process.env.API_BASE_URL) { - this.skip() - return - } - - fetchMock.mock({ - matcher: apiWorkUrl(id), - method: 'PATCH', - response: { - status: 200, - body: { - status: 'ok' - } - } - }) - }) - - after(fetchMock.restore) - - it('dispatches SAVING_WORK and SAVED_WORK when successful', function () { - const expectedActions = [ - { - type: SAVING_WORK, - id, - }, - { - type: SAVED_WORK, - } - ] - - const state = { - work: { - data: { - title: ['Old Title'], - } - } - } - - const updates = { - title: ['New Title'] - } - - const store = mockStore(state) - - return store.dispatch(actions.saveWork(id, updates)) - .then(() => { - const actions = store.getActions() - - expect(actions).to.have.length(expectedActions.length) - expectedActions.forEach((action, idx) => { - expect(action.type).to.equal(actions[idx].type) - }) - - expect(actions[0].id).to.equal(expectedActions[0].id) - }) - }) - }) + describe('#fetchWork', function () { + const id = 'example-id' + + before(function () { + if (!process.env.API_BASE_URL) { + this.skip() + return + } + + fetchMock + .get(apiWorkUrl(id), { title: ['HULLO!'] }) + .get(apiWorkUrl('fake-id'), 404) + .get('*', 500) + }) + + after(fetchMock.restore) + + + it('sends FETCHING_WORK and RECEIVE_WORK actions', function () { + const expectedActions = [ + {type: FETCHING_WORK, id}, + {type: RECEIVE_WORK, data: {title: ['HULLO!']}} + ] + + const store = mockStore({work: {}}) + + return store.dispatch(actions.fetchWork(id)) + .then(() => { + expect(store.getActions()).to.deep.equal(expectedActions) + }) + }) + + it('sends FETCHING_WORK and WORK_NOT_FOUND if not found', function () { + const id = 'fake-id' + const store = mockStore({work: {}}) + + return store.dispatch(actions.fetchWork(id)) + .then(() => { + const actions = store.getActions() + expect(actions[0].type).to.equal(FETCHING_WORK) + expect(actions[0].id).to.equal(id) + expect(actions[1].type).to.equal(WORK_NOT_FOUND_ERR) + expect(actions[1].error.status).to.equal(404) + }) + }) + + it('sends FETCHING_WORK and FETCHING_WORK_ERR if encountering an error', function () { + const id = 'whatever' + const store = mockStore({work: {}}) + + return store.dispatch(actions.fetchWork(id)) + .then(() => { + const actions = store.getActions() + expect(actions[0].type).to.equal(FETCHING_WORK) + expect(actions[1].id).to.equal(id) + expect(actions[1].type).to.equal(FETCHING_WORK_ERR) + }) + }) + }) + + describe('#saveWork', function () { + const id = 'example-work' + + before(function () { + if (!process.env.API_BASE_URL) { + this.skip() + return + } + + fetchMock.mock({ + matcher: apiWorkUrl(id), + method: 'PATCH', + response: { + status: 200, + body: { + status: 'ok' + } + } + }) + }) + + after(fetchMock.restore) + + it('dispatches SAVING_WORK and SAVED_WORK when successful', function () { + const expectedActions = [ + { + type: SAVING_WORK, + id, + }, + { + type: SAVED_WORK, + } + ] + + const state = { + work: { + data: { + title: ['Old Title'], + } + } + } + + const updates = { + title: ['New Title'] + } + + const store = mockStore(state) + + return store.dispatch(actions.saveWork(id, updates)) + .then(() => { + const actions = store.getActions() + + expect(actions).to.have.length(expectedActions.length) + expectedActions.forEach((action, idx) => { + expect(action.type).to.equal(actions[idx].type) + }) + + expect(actions[0].id).to.equal(expectedActions[0].id) + }) + }) + }) }) diff --git a/src/actions/autocomplete.js b/src/actions/autocomplete.js index d4d6e8a..f3fc66b 100644 --- a/src/actions/autocomplete.js +++ b/src/actions/autocomplete.js @@ -9,35 +9,35 @@ const queue = new Queue let fetchedVocabularies = [] export const fetchAutocompleteTerms = vocabulary => (dispatch, getState) => { - return queue.add(genGetVocab.bind(null, vocabulary)).then((data) => { - if (!data) - return + return queue.add(genGetVocab.bind(null, vocabulary)).then((data) => { + if (!data) + return - const terms = data.terms.map(t => t.pref_label[0]) + const terms = data.terms.map(t => t.pref_label[0]) - dispatch({ - type: RECEIVE_AUTOCOMPLETE_TERMS, - terms, - vocabulary, - }) + dispatch({ + type: RECEIVE_AUTOCOMPLETE_TERMS, + terms, + vocabulary, + }) - // reset when we reach zero - if (queue.getPendingLength() === 0) - fetchedVocabularies = [] + // reset when we reach zero + if (queue.getPendingLength() === 0) + fetchedVocabularies = [] - return - }) + return + }) - function genGetVocab (vocab) { - if (fetchedVocabularies.indexOf(vocab.uri) > -1) - return + function genGetVocab (vocab) { + if (fetchedVocabularies.indexOf(vocab.uri) > -1) + return - if (Object.keys(getState().autocompleteTerms).indexOf(vocab.uri) > -1) - return + if (Object.keys(getState().autocompleteTerms).indexOf(vocab.uri) > -1) + return - fetchedVocabularies.push(vocab.uri) - return getVocabulary(vocab) - } + fetchedVocabularies.push(vocab.uri) + return getVocabulary(vocab) + } } diff --git a/src/actions/notifications.js b/src/actions/notifications.js index cd63d78..b119038 100644 --- a/src/actions/notifications.js +++ b/src/actions/notifications.js @@ -1,11 +1,11 @@ import { CLEAR_NOTIFICATION } from '../constants' export const clearNotification = index => dispatch => { - if (typeof index === 'undefined') - return + if (typeof index === 'undefined') + return - return dispatch({ - type: CLEAR_NOTIFICATION, - index, - }) + return dispatch({ + type: CLEAR_NOTIFICATION, + index, + }) } diff --git a/src/actions/search.js b/src/actions/search.js index f9856b2..31390c5 100644 --- a/src/actions/search.js +++ b/src/actions/search.js @@ -10,54 +10,54 @@ import { parse as parseQuerystring } from 'blacklight-querystring' import createRangeFacet from '../../lib/create-range-facet' import { - RECEIVE_SEARCH_ERR, - RECEIVE_SEARCH_RESULTS, - SEARCHING, + RECEIVE_SEARCH_ERR, + RECEIVE_SEARCH_RESULTS, + SEARCHING, } from '../constants' const hasOwnProperty = Object.prototype.hasOwnProperty function conductSearch (dispatch, query, facets, options, queryString) { - dispatch({ - type: SEARCHING, - query, - facets, - options, - queryString, - }) - - addSearchToHistory({query, facets, options}) - - return search(queryString + '&format=json') - .then(results => { - dispatch({ - type: RECEIVE_SEARCH_RESULTS, - results, - }) - - return results - }) - .catch(error => { - dispatch({ - type: RECEIVE_SEARCH_ERR, - error, - }) - - throw error - }) + dispatch({ + type: SEARCHING, + query, + facets, + options, + queryString, + }) + + addSearchToHistory({query, facets, options}) + + return search(queryString + '&format=json') + .then(results => { + dispatch({ + type: RECEIVE_SEARCH_RESULTS, + results, + }) + + return results + }) + .catch(error => { + dispatch({ + type: RECEIVE_SEARCH_ERR, + error, + }) + + throw error + }) } export const searchCatalog = (query, facets, opts) => dispatch => { - // save ourselves the hassle of keeping track of these defaults - const options = assign({}, opts) + // save ourselves the hassle of keeping track of these defaults + const options = assign({}, opts) - if (!facets) - facets = {} + if (!facets) + facets = {} - const queryString = formatSearchQuerystring(query, facets, options) - browserHistory.push('/search?' + queryString) + const queryString = formatSearchQuerystring(query, facets, options) + browserHistory.push('/search?' + queryString) - return conductSearch(dispatch, query, facets, options, queryString) + return conductSearch(dispatch, query, facets, options, queryString) } // this function is used when arriving on a Search page w/ a pre-populated @@ -65,91 +65,91 @@ export const searchCatalog = (query, facets, opts) => dispatch => { // `parseSearchQuerystring` is used to extract the query, facets, and // options + passed to `conductSearch` export const searchCatalogByQueryString = queryString => dispatch => { - const {query, facets, options} = parseQuerystring(queryString) + const {query, facets, options} = parseQuerystring(queryString) - if (hasOwnProperty.call(options, 'range')) { - const range = options.range - delete options.range + if (hasOwnProperty.call(options, 'range')) { + const range = options.range + delete options.range - for (let r in range) { - if (!facets[r]) - facets[r] = [] + for (let r in range) { + if (!facets[r]) + facets[r] = [] - facets[r].push(createRangeFacet(r, range[r].begin, range[r].end)) - } - } + facets[r].push(createRangeFacet(r, range[r].begin, range[r].end)) + } + } - return conductSearch(dispatch, query, facets, options, queryString) + return conductSearch(dispatch, query, facets, options, queryString) } // used to toggle options such as `per_page` and `page` export const setSearchOption = (field, value) => (dispatch, getState) => { - const search = getState().search || {} + const search = getState().search || {} - const query = search.query || '' - const facets = assign({}, search.facets) - const options = assign({}, search.options) + const query = search.query || '' + const facets = assign({}, search.facets) + const options = assign({}, search.options) - // we'll pass null to remove the option - if (value === null) { - delete options[field] - } else { - options[field] = value - } + // we'll pass null to remove the option + if (value === null) { + delete options[field] + } else { + options[field] = value + } - // since searchCatalog's a thunk, we'll use the passed `dispatch` + call it again - return searchCatalog(query, facets, options)(dispatch) + // since searchCatalog's a thunk, we'll use the passed `dispatch` + call it again + return searchCatalog(query, facets, options)(dispatch) } export const toggleSearchFacet = (field, facet, checked) => (dispatch, getState) => { - const search = getState().search || {} - - // recycling the previous search info - const query = search.query || '' - const options = assign({}, search.options) - const facets = assign({}, search.facets) - - let dirty = false - let idx = -1 - - if (facets[field]) { - idx = findIndex(facets[field], f => { - if (hasOwnProperty.call(f, 'value') && hasOwnProperty.call(facet, 'value')) - return isEqual(f.value, facet.value) - else - return isEqual(f, facet) - }) - } - - // add to selected-facets - if (checked) { - if (idx === -1) { - facets[field] = [].concat(facets[field], facet).filter(Boolean) - dirty = true - } - } - - // remove from selected-facets - else { - if (idx !== -1) { - facets[field] = [].concat( - facets[field].slice(0, idx), - facets[field].slice(idx + 1) - ) - - if (!facets[field].length) - delete facets[field] - - dirty = true - } - } - - if (!dirty) - return Promise.resolve() - - // reset the page count (if it's already set) - if (options.page) - delete options.page - - return searchCatalog(query, facets, options)(dispatch) + const search = getState().search || {} + + // recycling the previous search info + const query = search.query || '' + const options = assign({}, search.options) + const facets = assign({}, search.facets) + + let dirty = false + let idx = -1 + + if (facets[field]) { + idx = findIndex(facets[field], f => { + if (hasOwnProperty.call(f, 'value') && hasOwnProperty.call(facet, 'value')) + return isEqual(f.value, facet.value) + else + return isEqual(f, facet) + }) + } + + // add to selected-facets + if (checked) { + if (idx === -1) { + facets[field] = [].concat(facets[field], facet).filter(Boolean) + dirty = true + } + } + + // remove from selected-facets + else { + if (idx !== -1) { + facets[field] = [].concat( + facets[field].slice(0, idx), + facets[field].slice(idx + 1) + ) + + if (!facets[field].length) + delete facets[field] + + dirty = true + } + } + + if (!dirty) + return Promise.resolve() + + // reset the page count (if it's already set) + if (options.page) + delete options.page + + return searchCatalog(query, facets, options)(dispatch) } diff --git a/src/actions/terms.js b/src/actions/terms.js index e777c94..9b50644 100644 --- a/src/actions/terms.js +++ b/src/actions/terms.js @@ -1,137 +1,137 @@ import { - ADD_TERM_TO_VOCABULARY, - ADD_TERM_TO_VOCABULARY_ERR, + ADD_TERM_TO_VOCABULARY, + ADD_TERM_TO_VOCABULARY_ERR, - BULK_EDIT_TERMS, + BULK_EDIT_TERMS, - FETCHING_VOCABULARY_TERMS, + FETCHING_VOCABULARY_TERMS, - RECEIVE_VOCABULARY_TERMS, - RECEIVE_VOCABULARY_TERMS_ERR, + RECEIVE_VOCABULARY_TERMS, + RECEIVE_VOCABULARY_TERMS_ERR, - REMOVE_TERM_FROM_VOCABULARY, + REMOVE_TERM_FROM_VOCABULARY, - UPDATE_TERM_REQUEST, - UPDATE_TERM_RESPONSE_ERR, - UPDATE_TERM_RESPONSE_OK, + UPDATE_TERM_REQUEST, + UPDATE_TERM_RESPONSE_ERR, + UPDATE_TERM_RESPONSE_OK, } from '../constants' import { - addTermToVocabulary as addTerm, - getVocabulary, - patchTerm, - putTerms, + addTermToVocabulary as addTerm, + getVocabulary, + patchTerm, + putTerms, } from '../../lib/api' import createNewTerm from '../../lib/create-new-term' function mintUri (vocab, term) { - const authBaseUrl = vocab.uri - const cameledTerm = term - .replace(/[^0-9a-zA-Z\s]/g, '') - .replace(/\s+/, ' ') - .toLowerCase() - .split(' ') - .map((w, i) => { - if (i === 0) return w - return w.substr(0,1).toUpperCase() + w.substr(1) - }).join('') - - return `${authBaseUrl}/${cameledTerm}` + const authBaseUrl = vocab.uri + const cameledTerm = term + .replace(/[^0-9a-zA-Z\s]/g, '') + .replace(/\s+/, ' ') + .toLowerCase() + .split(' ') + .map((w, i) => { + if (i === 0) return w + return w.substr(0,1).toUpperCase() + w.substr(1) + }).join('') + + return `${authBaseUrl}/${cameledTerm}` } // 1. creates the term object // 2. makes an API request to add new term // 3. dispatches: -// 1. ADD_TERM_TO_VOCABULARY -// 2. UPDATE_VOCABULARY_TERM_COUNT +// 1. ADD_TERM_TO_VOCABULARY +// 2. UPDATE_VOCABULARY_TERM_COUNT export const addTermToVocabulary = function (vocabulary, term) { - return dispatch => { - const newTerm = createNewTerm(term, vocabulary) - const uri = vocabulary.uri - - // Eventually I expect this will be handled on the server, but until then, - // a URI needs to be passed for the term to be accepted, so we'll create a - // mock one. - newTerm.uri = mintUri(vocabulary, term) - - return addTerm(vocabulary, newTerm) - .then(() => { - dispatch({ - type: ADD_TERM_TO_VOCABULARY, - data: newTerm, - uri, - vocabulary, - }) - }) - .catch(error => { - dispatch({ - type: ADD_TERM_TO_VOCABULARY_ERR, - error, - term, - }) - }) - } + return dispatch => { + const newTerm = createNewTerm(term, vocabulary) + const uri = vocabulary.uri + + // Eventually I expect this will be handled on the server, but until then, + // a URI needs to be passed for the term to be accepted, so we'll create a + // mock one. + newTerm.uri = mintUri(vocabulary, term) + + return addTerm(vocabulary, newTerm) + .then(() => { + dispatch({ + type: ADD_TERM_TO_VOCABULARY, + data: newTerm, + uri, + vocabulary, + }) + }) + .catch(error => { + dispatch({ + type: ADD_TERM_TO_VOCABULARY_ERR, + error, + term, + }) + }) + } } export const bulkEditTermsInVocabulary = (vocabulary, terms) => { - return (dispatch, getState) => { - - const prevTerms = getState().activeVocabularyTerms.data - - // cut down on the # of array traversals by building an index - // `{ termUri: indexInPrevTerms }` - const indexed = {} - prevTerms.forEach((term, index) => ( - indexed[term.pref_label[0]] = index - )) - - const updates = terms.map(term => { - const idx = indexed[term] - - if (typeof idx !== 'undefined') - return prevTerms[idx] - - return createNewTerm(term, vocabulary) - }) - - return putTerms(vocabulary, updates) - .then(() => { - dispatch({ - type: BULK_EDIT_TERMS, - terms: updates, - vocabulary, - }) - }) - //.catch(function (err) {}) - } + return (dispatch, getState) => { + + const prevTerms = getState().activeVocabularyTerms.data + + // cut down on the # of array traversals by building an index + // `{ termUri: indexInPrevTerms }` + const indexed = {} + prevTerms.forEach((term, index) => ( + indexed[term.pref_label[0]] = index + )) + + const updates = terms.map(term => { + const idx = indexed[term] + + if (typeof idx !== 'undefined') + return prevTerms[idx] + + return createNewTerm(term, vocabulary) + }) + + return putTerms(vocabulary, updates) + .then(() => { + dispatch({ + type: BULK_EDIT_TERMS, + terms: updates, + vocabulary, + }) + }) + //.catch(function (err) {}) + } } export const fetchTermsFromVocabulary = vocabulary => { - return dispatch => { - dispatch({ - type: FETCHING_VOCABULARY_TERMS, - vocabulary, - }) - - return getVocabulary(vocabulary) - .then(results => results.terms) - .then(terms => { - dispatch({ - type: RECEIVE_VOCABULARY_TERMS, - data: terms, - vocabulary, - }) - }) - .catch(error => { - dispatch({ - type: RECEIVE_VOCABULARY_TERMS_ERR, - error, - vocabulary, - }) - }) - } + return dispatch => { + dispatch({ + type: FETCHING_VOCABULARY_TERMS, + vocabulary, + }) + + return getVocabulary(vocabulary) + .then(results => results.terms) + .then(terms => { + dispatch({ + type: RECEIVE_VOCABULARY_TERMS, + data: terms, + vocabulary, + }) + }) + .catch(error => { + dispatch({ + type: RECEIVE_VOCABULARY_TERMS_ERR, + error, + vocabulary, + }) + }) + } } // currently, the way we can remove a term is by omitting it @@ -139,46 +139,46 @@ export const fetchTermsFromVocabulary = vocabulary => { // might open up so that only a DELETE request to an absolute_path // would be needed to remove a term export const removeTermFromVocabulary = (vocabulary, termData, index) => { - return (dispatch, getState) => { - const termsList = getState().activeVocabularyTerms.data - const terms = [].concat( - termsList.slice(0, index), - termsList.slice(index + 1) - ) - - return putTerms(vocabulary, terms) - .then(() => { - dispatch({ - type: REMOVE_TERM_FROM_VOCABULARY, - index, - term: termData, - vocabulary, - }) - }) - //.catch(err => {}) - } + return (dispatch, getState) => { + const termsList = getState().activeVocabularyTerms.data + const terms = [].concat( + termsList.slice(0, index), + termsList.slice(index + 1) + ) + + return putTerms(vocabulary, terms) + .then(() => { + dispatch({ + type: REMOVE_TERM_FROM_VOCABULARY, + index, + term: termData, + vocabulary, + }) + }) + //.catch(err => {}) + } } export const updateTermInVocabulary = function (vocabulary, term, data) { - return dispatch => { - dispatch({type: UPDATE_TERM_REQUEST}) - - return patchTerm(vocabulary, data) - .then(() => { - dispatch({ - type: UPDATE_TERM_RESPONSE_OK, - previousPrefLabel: term, - data, - vocabulary, - }) - }) - .catch(error => { - dispatch({ - type: UPDATE_TERM_RESPONSE_ERR, - error, - term, - vocabulary, - }) - }) - } + return dispatch => { + dispatch({type: UPDATE_TERM_REQUEST}) + + return patchTerm(vocabulary, data) + .then(() => { + dispatch({ + type: UPDATE_TERM_RESPONSE_OK, + previousPrefLabel: term, + data, + vocabulary, + }) + }) + .catch(error => { + dispatch({ + type: UPDATE_TERM_RESPONSE_ERR, + error, + term, + vocabulary, + }) + }) + } } diff --git a/src/actions/vocabulary.js b/src/actions/vocabulary.js index 16360bd..fc46f70 100644 --- a/src/actions/vocabulary.js +++ b/src/actions/vocabulary.js @@ -1,29 +1,29 @@ import { - CREATE_VOCABULARY_REQUEST, - CREATE_VOCABULARY_RESPONSE_ERR, - CREATE_VOCABULARY_RESPONSE_OK, + CREATE_VOCABULARY_REQUEST, + CREATE_VOCABULARY_RESPONSE_ERR, + CREATE_VOCABULARY_RESPONSE_OK, - DELETE_VOCABULARY_REQUEST, - DELETE_VOCABULARY_RESPONSE_ERR, - DELETE_VOCABULARY_RESPONSE_OK, + DELETE_VOCABULARY_REQUEST, + DELETE_VOCABULARY_RESPONSE_ERR, + DELETE_VOCABULARY_RESPONSE_OK, - FETCHING_ALL_VOCABULARIES, - FETCHING_ALL_VOCABULARIES_ERR, + FETCHING_ALL_VOCABULARIES, + FETCHING_ALL_VOCABULARIES_ERR, - RECEIVE_ALL_VOCABULARIES, - // RECEIVE_VOCABULARY_ERR, + RECEIVE_ALL_VOCABULARIES, + // RECEIVE_VOCABULARY_ERR, - UPDATING_VOCABULARY, - UPDATE_VOCABULARY_ERR, - UPDATE_VOCABULARY_OK, + UPDATING_VOCABULARY, + UPDATE_VOCABULARY_ERR, + UPDATE_VOCABULARY_OK, } from '../constants' // import { get } from '../../lib/api/request' import { - createVocabulary as create, - deleteVocabulary as deleteVocab, - getVocabularies, - updateVocabulary, + createVocabulary as create, + deleteVocabulary as deleteVocab, + getVocabularies, + updateVocabulary, } from '../../lib/api' // import isFresh from '../../lib/is-fresh' import camelCase from '../../lib/camel-case' @@ -32,108 +32,108 @@ import assign from 'object-assign' // const STALE_TIME = 60 * 1000 const mockMintAuthUri = val => ( - `${process.env.AUTH_BASE_URL}/${camelCase(val)}` + `${process.env.AUTH_BASE_URL}/${camelCase(val)}` ) export const createVocabulary = data => dispatch => { - dispatch({ - type: CREATE_VOCABULARY_REQUEST, - }) - - const payload = assign({}, data) - const name = payload.label[0] - - payload.uri = mockMintAuthUri(name) - payload.pref_label = payload.label - - return create(payload) - .then(() => { - - // append `absolute_path` to the payload - payload.absolute_path = ( - `${process.env.API_BASE_URL}/vocabularies/${camelCase(name)}.json` - ) - - dispatch({ - type: CREATE_VOCABULARY_RESPONSE_OK, - data: payload, - }) - - return payload - }) - .catch(error => { - dispatch({ - type: CREATE_VOCABULARY_RESPONSE_ERR, - error, - }) - - throw error - }) + dispatch({ + type: CREATE_VOCABULARY_REQUEST, + }) + + const payload = assign({}, data) + const name = payload.label[0] + + payload.uri = mockMintAuthUri(name) + payload.pref_label = payload.label + + return create(payload) + .then(() => { + + // append `absolute_path` to the payload + payload.absolute_path = ( + `${process.env.API_BASE_URL}/vocabularies/${camelCase(name)}.json` + ) + + dispatch({ + type: CREATE_VOCABULARY_RESPONSE_OK, + data: payload, + }) + + return payload + }) + .catch(error => { + dispatch({ + type: CREATE_VOCABULARY_RESPONSE_ERR, + error, + }) + + throw error + }) } export const deleteVocabulary = data => dispatch => { - dispatch({type: DELETE_VOCABULARY_REQUEST}) - - return deleteVocab(data) - .then(() => { - dispatch({ - type: DELETE_VOCABULARY_RESPONSE_OK, - data, - }) - }) - .catch(error => { - dispatch({ - type: DELETE_VOCABULARY_RESPONSE_ERR, - data, - error, - }) - - throw error - }) + dispatch({type: DELETE_VOCABULARY_REQUEST}) + + return deleteVocab(data) + .then(() => { + dispatch({ + type: DELETE_VOCABULARY_RESPONSE_OK, + data, + }) + }) + .catch(error => { + dispatch({ + type: DELETE_VOCABULARY_RESPONSE_ERR, + data, + error, + }) + + throw error + }) } export const fetchAllVocabularies = () => dispatch => { - dispatch({ - type: FETCHING_ALL_VOCABULARIES, - }) - - return getVocabularies() - .then(response => { - dispatch({ - type: RECEIVE_ALL_VOCABULARIES, - data: response.vocabularies, - }) - }) - .catch(error => { - dispatch({ - type: FETCHING_ALL_VOCABULARIES_ERR, - error, - }) - - throw error - }) + dispatch({ + type: FETCHING_ALL_VOCABULARIES, + }) + + return getVocabularies() + .then(response => { + dispatch({ + type: RECEIVE_ALL_VOCABULARIES, + data: response.vocabularies, + }) + }) + .catch(error => { + dispatch({ + type: FETCHING_ALL_VOCABULARIES_ERR, + error, + }) + + throw error + }) } export const updateVocabularyMetadata = data => dispatch => { - dispatch({ - type: UPDATING_VOCABULARY, - vocabulary: data, - }) - - return updateVocabulary(data) - .then(() => { - dispatch({ - type: UPDATE_VOCABULARY_OK, - vocabulary: data, - }) - }) - .catch(error => { - dispatch({ - type: UPDATE_VOCABULARY_ERR, - error, - vocabulary: data, - }) - - throw error - }) + dispatch({ + type: UPDATING_VOCABULARY, + vocabulary: data, + }) + + return updateVocabulary(data) + .then(() => { + dispatch({ + type: UPDATE_VOCABULARY_OK, + vocabulary: data, + }) + }) + .catch(error => { + dispatch({ + type: UPDATE_VOCABULARY_ERR, + error, + vocabulary: data, + }) + + throw error + }) } diff --git a/src/actions/work.js b/src/actions/work.js index 6768335..937097f 100644 --- a/src/actions/work.js +++ b/src/actions/work.js @@ -1,66 +1,66 @@ import { - FETCHING_WORK, - FETCHING_WORK_ERR, - RECEIVE_WORK, - SAVED_WORK, - SAVING_WORK, - WORK_NOT_FOUND_ERR, + FETCHING_WORK, + FETCHING_WORK_ERR, + RECEIVE_WORK, + SAVED_WORK, + SAVING_WORK, + WORK_NOT_FOUND_ERR, } from '../constants' import { getWork, updateWork } from '../../lib/api' // import isFresh from '../../lib/is-fresh' export const fetchWork = id => (dispatch/*, getState*/) => { - // const work = getState().work + // const work = getState().work - dispatch({ - type: FETCHING_WORK, - id, - }) + dispatch({ + type: FETCHING_WORK, + id, + }) - return getWork(id) - .then( - data => { - dispatch({ - type: RECEIVE_WORK, - data, - }) - }, - error => { - if (error.status === 404) { - dispatch({ - type: WORK_NOT_FOUND_ERR, - error, - id, - }) + return getWork(id) + .then( + data => { + dispatch({ + type: RECEIVE_WORK, + data, + }) + }, + error => { + if (error.status === 404) { + dispatch({ + type: WORK_NOT_FOUND_ERR, + error, + id, + }) - return - } + return + } - dispatch({ - type: FETCHING_WORK_ERR, - error, - id, - }) - } - ) + dispatch({ + type: FETCHING_WORK_ERR, + error, + id, + }) + } + ) } export const saveWork = (id, updates) => dispatch => { - // TODO - // bail on save if already saving so to prevent duplicates + // TODO + // bail on save if already saving so to prevent duplicates - dispatch({ - type: SAVING_WORK, - id, - }) + dispatch({ + type: SAVING_WORK, + id, + }) - return updateWork(id, updates) - .then(() => { - dispatch({ - type: SAVED_WORK, - updates, - }) - }) - //.catch(err => {}) + return updateWork(id, updates) + .then(() => { + dispatch({ + type: SAVED_WORK, + updates, + }) + }) + //.catch(err => {}) } diff --git a/src/components/Button.jsx b/src/components/Button.jsx index 550ac02..e5d2656 100644 --- a/src/components/Button.jsx +++ b/src/components/Button.jsx @@ -4,53 +4,53 @@ import assign from 'object-assign' const T = React.PropTypes const Button = React.createClass({ - propTypes: { - onClick: T.func, - size: T.string, - style: T.object, - type: T.string, - }, - - getSizeClassName: function () { - switch (this.props.size) { - case 'small': - return 'btn-size-small' - case 'large': - return 'btn-size-large' - } - }, - - getTypeClassName: function () { - switch (this.props.type) { - case 'danger': - return 'btn-type-danger' - case 'info': - return 'btn-type-info' - case 'success': - return 'btn-type-success' - case 'text': - return 'btn-type-text' - case 'warning': - return 'btn-type-warning' - } - }, - - render: function () { - const className = [ - this.getSizeClassName(), - this.getTypeClassName(), - this.props.className, - ].join(' ') - - const uProps = assign({}, this.props) - delete uProps.size - delete uProps.type - delete uProps.className - - const props = assign({}, uProps, {className}) - - return React.createElement('button', props) - } + propTypes: { + onClick: T.func, + size: T.string, + style: T.object, + type: T.string, + }, + + getSizeClassName: function () { + switch (this.props.size) { + case 'small': + return 'btn-size-small' + case 'large': + return 'btn-size-large' + } + }, + + getTypeClassName: function () { + switch (this.props.type) { + case 'danger': + return 'btn-type-danger' + case 'info': + return 'btn-type-info' + case 'success': + return 'btn-type-success' + case 'text': + return 'btn-type-text' + case 'warning': + return 'btn-type-warning' + } + }, + + render: function () { + const className = [ + this.getSizeClassName(), + this.getTypeClassName(), + this.props.className, + ].join(' ') + + const uProps = assign({}, this.props) + delete uProps.size + delete uProps.type + delete uProps.className + + const props = assign({}, uProps, {className}) + + return React.createElement('button', props) + } }) export default Button diff --git a/src/components/ModalWithHeader.jsx b/src/components/ModalWithHeader.jsx index 0a74217..2c8e698 100644 --- a/src/components/ModalWithHeader.jsx +++ b/src/components/ModalWithHeader.jsx @@ -6,70 +6,70 @@ const T = React.PropTypes const PADDING_VAL = 20 const ModalWithHeader = React.createClass({ - propTypes: { - header: T.oneOfType([T.string, T.func]), - headerProps: T.object, - allowHTML: T.bool, - }, + propTypes: { + header: T.oneOfType([T.string, T.func]), + headerProps: T.object, + allowHTML: T.bool, + }, - getHeaderProps: function () { - const defaultProps = { - key: '__dss-modal-w-header', - style: { - backgroundColor: '#1d5f83', - color: '#fff', - fontSize: '1.125em', - margin: '-' + PADDING_VAL + 'px', - marginBottom: PADDING_VAL + 'px', - padding: Math.round(PADDING_VAL / 2) + 'px', - textAlign: 'center', - } - } + getHeaderProps: function () { + const defaultProps = { + key: '__dss-modal-w-header', + style: { + backgroundColor: '#1d5f83', + color: '#fff', + fontSize: '1.125em', + margin: '-' + PADDING_VAL + 'px', + marginBottom: PADDING_VAL + 'px', + padding: Math.round(PADDING_VAL / 2) + 'px', + textAlign: 'center', + } + } - return assign(defaultProps, this.props.headerProps) - }, + return assign(defaultProps, this.props.headerProps) + }, - renderHeader: function () { - // allow a user-provided function to return - if (typeof this.props.header === 'function') - return this.props.header.call(null, this) + renderHeader: function () { + // allow a user-provided function to return + if (typeof this.props.header === 'function') + return this.props.header.call(null, this) - const props = this.getHeaderProps() + const props = this.getHeaderProps() - if (this.props.allowHTML) - props.dangerouslySetInnerHTML = { __html: this.props.header } - else - props.children = this.props.header + if (this.props.allowHTML) + props.dangerouslySetInnerHTML = { __html: this.props.header } + else + props.children = this.props.header - return React.createElement('header', props) - }, + return React.createElement('header', props) + }, - render: function () { - const headerStyle = (this.getHeaderProps().style || {}) - const defaultContentStyle = { - borderStyle: 'solid', - borderWidth: '1px', - bottom: '10%', - left: '10%', - padding: PADDING_VAL + 'px', - top: '10%', - right: '10%', - } + render: function () { + const headerStyle = (this.getHeaderProps().style || {}) + const defaultContentStyle = { + borderStyle: 'solid', + borderWidth: '1px', + bottom: '10%', + left: '10%', + padding: PADDING_VAL + 'px', + top: '10%', + right: '10%', + } - if (headerStyle.backgroundColor) - defaultContentStyle.borderColor = headerStyle.backgroundColor + if (headerStyle.backgroundColor) + defaultContentStyle.borderColor = headerStyle.backgroundColor - const modalContentStyle = assign({}, defaultContentStyle, - (this.props.style ? this.props.style.content : undefined) - ) + const modalContentStyle = assign({}, defaultContentStyle, + (this.props.style ? this.props.style.content : undefined) + ) - const props = assign({style: {content: modalContentStyle}}, this.props) + const props = assign({style: {content: modalContentStyle}}, this.props) - return React.createElement(Modal, props, [ - this.renderHeader(), - this.props.children - ]) - } + return React.createElement(Modal, props, [ + this.renderHeader(), + this.props.children + ]) + } }) export default ModalWithHeader diff --git a/src/components/NavToSearchResults.jsx b/src/components/NavToSearchResults.jsx index d3e24a7..a1e6bd5 100644 --- a/src/components/NavToSearchResults.jsx +++ b/src/components/NavToSearchResults.jsx @@ -3,15 +3,15 @@ import Button from './Button.jsx' import browserHistory from 'react-router/lib/browserHistory' export default function NavToSearchResults (props) { - return ( - - ) + return ( + + ) } diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index a3fd94f..5e2048f 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -4,31 +4,31 @@ import Link from 'react-router/lib/Link' const T = React.PropTypes const Navbar = React.createClass({ - render: function () { - return ( -
- - - + render: function () { + return ( +
+ + + -
- ) - } +
  • + + Vocabulary Manager + +
  • + + +
    + ) + } }) export default Navbar diff --git a/src/components/NotificationCenter.jsx b/src/components/NotificationCenter.jsx index 27f7bbc..d0f64ee 100644 --- a/src/components/NotificationCenter.jsx +++ b/src/components/NotificationCenter.jsx @@ -3,81 +3,81 @@ import { NotificationStack } from 'react-notification' import assign from 'object-assign' import { - NOTIFICATION_ERR as ERROR, - NOTIFICAITON_SUCCESS as SUCCESS, + NOTIFICATION_ERR as ERROR, + NOTIFICAITON_SUCCESS as SUCCESS, } from '../constants' const T = React.PropTypes const NotificationCenter = React.createClass({ - propTypes: { - notifications: T.array.isRequired, - onClearNotification: T.func.isRequired, - }, - - getStyles: function (type) { - const defaults = { - } - - switch (type) { - case ERROR: - return assign({}, defaults, { - barStyle: { - backgroundColor: '#910029', - color: '#fff', - }, - - actionStyle: { - color: '#fff', - fontWeight: 'bold', - } - }) - - default: - return defaults - } - }, - - styleFactory: function (index, style) { - return assign({}, style, { - // need to null out `bottom`, as it's set by react-notification's - // defaultStyles. this will effectively remove it from the style object. - bottom: null, - top: `${3 + (index * 4)}rem`, - }) - }, - - render: function () { - const notifications = this.props.notifications.map((notification, index) => { - const isError = notification.type === ERROR - - return { - action: 'X', - onClick: this.props.onClearNotification.bind(null, index), - - dismissAfter: isError ? false : (1000 * 10), - isActive: true, - key: 'notification-'+index, - message: notification.message, - - ...this.getStyles(notification.type), - - // - __index: index, - } - }) - - const props = { - activeBarStyleFactory: this.styleFactory, - barStyleFactory: this.styleFactory, - notifications, - onDismiss: notification => { - this.props.onClearNotification(notification.__index) - }, - } - - return - } + propTypes: { + notifications: T.array.isRequired, + onClearNotification: T.func.isRequired, + }, + + getStyles: function (type) { + const defaults = { + } + + switch (type) { + case ERROR: + return assign({}, defaults, { + barStyle: { + backgroundColor: '#910029', + color: '#fff', + }, + + actionStyle: { + color: '#fff', + fontWeight: 'bold', + } + }) + + default: + return defaults + } + }, + + styleFactory: function (index, style) { + return assign({}, style, { + // need to null out `bottom`, as it's set by react-notification's + // defaultStyles. this will effectively remove it from the style object. + bottom: null, + top: `${3 + (index * 4)}rem`, + }) + }, + + render: function () { + const notifications = this.props.notifications.map((notification, index) => { + const isError = notification.type === ERROR + + return { + action: 'X', + onClick: this.props.onClearNotification.bind(null, index), + + dismissAfter: isError ? false : (1000 * 10), + isActive: true, + key: 'notification-'+index, + message: notification.message, + + ...this.getStyles(notification.type), + + // + __index: index, + } + }) + + const props = { + activeBarStyleFactory: this.styleFactory, + barStyleFactory: this.styleFactory, + notifications, + onDismiss: notification => { + this.props.onClearNotification(notification.__index) + }, + } + + return + } }) export default NotificationCenter diff --git a/src/components/Toggle.jsx b/src/components/Toggle.jsx index 6f77602..f43c83a 100644 --- a/src/components/Toggle.jsx +++ b/src/components/Toggle.jsx @@ -5,95 +5,95 @@ const T = React.PropTypes const BORDER_RADIUS = '2px' const Toggle = React.createClass({ - propTypes: { - values: T.array.isRequired, - - styles: T.shape({ - container: T.object, - toggle: T.object, - selected: T.object, - }), - - value: T.oneOfType([T.string, T.number]) - }, - - getDefaultProps: function () { - return { - styles: {}, - } - }, - - handleToggleChange: function (val) { - if (val === this.props.value) - return - - this.props.onChange(val) - }, - - renderToggleOption: function (_value, index) { - let label, value - - if (typeof _value === 'object') { - label = _value.label - value = _value.value - } else { - label = _value - value = _value - } - - const selected = value === this.props.value - - const labelStyle = assign({}, { - backgroundColor: (selected ? '#b0b0b0' : '#efefef'), - cursor: (selected ? 'auto' : 'pointer'), - display: 'inline-block', - fontWeight: 'normal', - minWidth: '5em', - padding: '5px', - textAlign: 'center', - }, this.props.styles.toggle, - (selected ? this.props.styles.selected : undefined) - ) - - if (index === 0) { - labelStyle.borderBottomLeftRadius = '1px' - labelStyle.borderTopLeftRadius = '1px' - } - - if (index === (this.props.values.length - 1)) { - labelStyle.borderBottomRightRadius = '1px' - labelStyle.borderTopRightRadius = '1px' - } - - return ( - - ) - }, - - render: function () { - const containerStyles = assign({}, { - borderColor: '#b0b0b0', - borderRadius: BORDER_RADIUS, - borderStyle: 'solid', - borderWidth: '1px', - fontSize: '.9em', - }, this.props.styles.container) - - return ( -
    - {this.props.values.map(this.renderToggleOption)} -
    - ) - } + propTypes: { + values: T.array.isRequired, + + styles: T.shape({ + container: T.object, + toggle: T.object, + selected: T.object, + }), + + value: T.oneOfType([T.string, T.number]) + }, + + getDefaultProps: function () { + return { + styles: {}, + } + }, + + handleToggleChange: function (val) { + if (val === this.props.value) + return + + this.props.onChange(val) + }, + + renderToggleOption: function (_value, index) { + let label, value + + if (typeof _value === 'object') { + label = _value.label + value = _value.value + } else { + label = _value + value = _value + } + + const selected = value === this.props.value + + const labelStyle = assign({}, { + backgroundColor: (selected ? '#b0b0b0' : '#efefef'), + cursor: (selected ? 'auto' : 'pointer'), + display: 'inline-block', + fontWeight: 'normal', + minWidth: '5em', + padding: '5px', + textAlign: 'center', + }, this.props.styles.toggle, + (selected ? this.props.styles.selected : undefined) + ) + + if (index === 0) { + labelStyle.borderBottomLeftRadius = '1px' + labelStyle.borderTopLeftRadius = '1px' + } + + if (index === (this.props.values.length - 1)) { + labelStyle.borderBottomRightRadius = '1px' + labelStyle.borderTopRightRadius = '1px' + } + + return ( + + ) + }, + + render: function () { + const containerStyles = assign({}, { + borderColor: '#b0b0b0', + borderRadius: BORDER_RADIUS, + borderStyle: 'solid', + borderWidth: '1px', + fontSize: '.9em', + }, this.props.styles.container) + + return ( +
    + {this.props.values.map(this.renderToggleOption)} +
    + ) + } }) export default Toggle diff --git a/src/components/catalog/Facet.jsx b/src/components/catalog/Facet.jsx index 8adf5b8..2786c55 100644 --- a/src/components/catalog/Facet.jsx +++ b/src/components/catalog/Facet.jsx @@ -10,203 +10,203 @@ import assign from 'object-assign' const T = React.PropTypes const Facet = React.createClass({ - propTypes: { - // an array of facet items provided from Blacklight. - items: T.arrayOf(T.shape({ - name: T.string, - hits: T.number, - value: T.any, - })), //.isRequired, - - // the name of the facet that relates to the key within the `facets` object - // provided by Blacklight. - name: T.string, //.isRequired, - - // triggered when a `selectedFacet`s `X` button is clicked. - // (passed to panel-body `SelectedFacetsList` component) - // @param object the facet being removed - onRemoveSelectedFacet: T.func, //.isRequired, - - // triggered when a facet is selected (passed to panel-body) - // @param object the facet being selected - onSelectFacet: T.func, //.isRequired, - - // React Component used to render the facet body when opened. Not _technically_ - // required, but will bail early if no component is provided. - // Props passed to are passed downstream to `bodyComponent`. - bodyComponent: T.func, - - // The display label to use on the facet header - // (defaults to the `name` property which is required) - label: T.string, - - // whether or not the panel-body is visible. this is handled in state - // but this allows us to have a panel open initially - // (default: `false`) - open: T.bool, - - // array of facet options selected for the group - // (default: `[]`) - selectedFacets: T.array, - - // whether or not to display an angled line ('arrow' w/o a stem) as visual - // feedback on the facet-header - // (default: `true`) - showHeaderArrow: T.bool, - - // styles used for the facet wrapper - styles: T.shape({ - default: T.shape({ - panel: T.object, - header: T.object, - }), - hasSelectedFacets: T.shape({ - panel: T.object, - header: T.object, - }), - }), - }, - - getDefaultProps: function () { - const panelColor = '#dddddd' - const selectedColor = '#d8ecd8' - const textColor = '#1e1e1e' - - return { - bodyComponent: null, - open: false, - selectedFacets: [], - showHeaderArrow: true, - sort: 'desc', - - styles: { - default: { - panel: { - backgroundColor: '#fff', - borderColor: panelColor, - borderRadius: '2px', - borderStyle: 'solid', - borderWidth: '1px', - color: textColor, - margin: '5px 0', - }, - - header: { - backgroundColor: panelColor, - cursor: 'pointer', - padding: '5px', - }, - }, - hasSelectedFacets: { - panel: { - borderColor: selectedColor, - color: textColor, - }, - - header: { - backgroundColor: selectedColor, - color: textColor, - } - }, - } - } - }, - - getInitialState: function () { - return { - open: this.props.open || this.props.selectedFacets.length > 0, - } - }, - - maybeRenderHeaderArrow: function () { - if (!this.props.showHeaderArrow) - return - - const hasSel = this.props.selectedFacets.length > 0 - - let stroke = hasSel - ? this.props.styles.hasSelectedFacets.color - : this.props.styles.default.color - - if (!stroke) - stroke = '#1e1e1e' - - // remember to convert the hash for hex colors! (%23) - stroke = stroke.replace(/^#/, '%23') - - const transformDeg = this.state.open ? 90 : 0 - const arrowSvg = [ - '', - '', - ''].join('') - - const props = { - key: 'dss-fp-header-arrow', - style: { - backgroundImage: "url('data:image/svg+xml;utf8," + arrowSvg + "')", - backgroundRepeat: 'no-repeat', - backgroundPosition: 'center center', - display: 'inline-block', - float: 'right', - height: '15px', - marginLeft: '15px', - marginTop: '2px', - transform: 'rotate(' + transformDeg + 'deg)', - verticalAlign: 'middle', - width: '20px', - } - } - - return React.createElement('span', props) - }, - - renderFacetBody: function () { - if (!this.state.open || !this.props.bodyComponent) - return - - return React.createElement( - 'div', - { - className: 'facet-panel--body', - style: {padding: '5px'} - }, - React.createElement(this.props.bodyComponent, this.props) - ) - }, - - render: function () { - // skip if there's nothing to display - if (!this.props.items || !this.props.items.length) - return null - - const defaultPanel = this.props.styles.default.panel - const defaultHeader = this.props.styles.default.header - const selPanel = this.props.styles.hasSelectedFacets.panel - const selHeader = this.props.styles.hasSelectedFacets.header - const hasSel = this.props.selectedFacets.length > 0 - - const panelStyles = assign({}, defaultPanel, (hasSel ? selPanel : null)) - const headerStyles = assign({}, defaultHeader, (hasSel ? selHeader : null)) - - const headerLabel = { - margin: '0', - padding: '0', - verticalAlign: 'middle', - } - - return ( -
    -
    this.setState({open: !this.state.open})} style={headerStyles}> -

    - {this.props.label} - {this.maybeRenderHeaderArrow()} -

    -
    - - {this.renderFacetBody()} -
    - ) - } + propTypes: { + // an array of facet items provided from Blacklight. + items: T.arrayOf(T.shape({ + name: T.string, + hits: T.number, + value: T.any, + })), //.isRequired, + + // the name of the facet that relates to the key within the `facets` object + // provided by Blacklight. + name: T.string, //.isRequired, + + // triggered when a `selectedFacet`s `X` button is clicked. + // (passed to panel-body `SelectedFacetsList` component) + // @param object the facet being removed + onRemoveSelectedFacet: T.func, //.isRequired, + + // triggered when a facet is selected (passed to panel-body) + // @param object the facet being selected + onSelectFacet: T.func, //.isRequired, + + // React Component used to render the facet body when opened. Not _technically_ + // required, but will bail early if no component is provided. + // Props passed to are passed downstream to `bodyComponent`. + bodyComponent: T.func, + + // The display label to use on the facet header + // (defaults to the `name` property which is required) + label: T.string, + + // whether or not the panel-body is visible. this is handled in state + // but this allows us to have a panel open initially + // (default: `false`) + open: T.bool, + + // array of facet options selected for the group + // (default: `[]`) + selectedFacets: T.array, + + // whether or not to display an angled line ('arrow' w/o a stem) as visual + // feedback on the facet-header + // (default: `true`) + showHeaderArrow: T.bool, + + // styles used for the facet wrapper + styles: T.shape({ + default: T.shape({ + panel: T.object, + header: T.object, + }), + hasSelectedFacets: T.shape({ + panel: T.object, + header: T.object, + }), + }), + }, + + getDefaultProps: function () { + const panelColor = '#dddddd' + const selectedColor = '#d8ecd8' + const textColor = '#1e1e1e' + + return { + bodyComponent: null, + open: false, + selectedFacets: [], + showHeaderArrow: true, + sort: 'desc', + + styles: { + default: { + panel: { + backgroundColor: '#fff', + borderColor: panelColor, + borderRadius: '2px', + borderStyle: 'solid', + borderWidth: '1px', + color: textColor, + margin: '5px 0', + }, + + header: { + backgroundColor: panelColor, + cursor: 'pointer', + padding: '5px', + }, + }, + hasSelectedFacets: { + panel: { + borderColor: selectedColor, + color: textColor, + }, + + header: { + backgroundColor: selectedColor, + color: textColor, + } + }, + } + } + }, + + getInitialState: function () { + return { + open: this.props.open || this.props.selectedFacets.length > 0, + } + }, + + maybeRenderHeaderArrow: function () { + if (!this.props.showHeaderArrow) + return + + const hasSel = this.props.selectedFacets.length > 0 + + let stroke = hasSel + ? this.props.styles.hasSelectedFacets.color + : this.props.styles.default.color + + if (!stroke) + stroke = '#1e1e1e' + + // remember to convert the hash for hex colors! (%23) + stroke = stroke.replace(/^#/, '%23') + + const transformDeg = this.state.open ? 90 : 0 + const arrowSvg = [ + '', + '', + ''].join('') + + const props = { + key: 'dss-fp-header-arrow', + style: { + backgroundImage: "url('data:image/svg+xml;utf8," + arrowSvg + "')", + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center center', + display: 'inline-block', + float: 'right', + height: '15px', + marginLeft: '15px', + marginTop: '2px', + transform: 'rotate(' + transformDeg + 'deg)', + verticalAlign: 'middle', + width: '20px', + } + } + + return React.createElement('span', props) + }, + + renderFacetBody: function () { + if (!this.state.open || !this.props.bodyComponent) + return + + return React.createElement( + 'div', + { + className: 'facet-panel--body', + style: {padding: '5px'} + }, + React.createElement(this.props.bodyComponent, this.props) + ) + }, + + render: function () { + // skip if there's nothing to display + if (!this.props.items || !this.props.items.length) + return null + + const defaultPanel = this.props.styles.default.panel + const defaultHeader = this.props.styles.default.header + const selPanel = this.props.styles.hasSelectedFacets.panel + const selHeader = this.props.styles.hasSelectedFacets.header + const hasSel = this.props.selectedFacets.length > 0 + + const panelStyles = assign({}, defaultPanel, (hasSel ? selPanel : null)) + const headerStyles = assign({}, defaultHeader, (hasSel ? selHeader : null)) + + const headerLabel = { + margin: '0', + padding: '0', + verticalAlign: 'middle', + } + + return ( +
    +
    this.setState({open: !this.state.open})} style={headerStyles}> +

    + {this.props.label} + {this.maybeRenderHeaderArrow()} +

    +
    + + {this.renderFacetBody()} +
    + ) + } }) export default Facet diff --git a/src/components/catalog/FacetGroup.jsx b/src/components/catalog/FacetGroup.jsx index f002b49..cd73cae 100644 --- a/src/components/catalog/FacetGroup.jsx +++ b/src/components/catalog/FacetGroup.jsx @@ -4,115 +4,115 @@ import assign from 'object-assign' const T = React.PropTypes const FacetGroup = React.createClass({ - propTypes: { - // `facets` is an array of facet-objects from blacklight. - // ---------- - // "facets": [ - // { - // "label": "Database Topic", - // "name": "db_az_subject", - // "items": [ - // { - // "value": "American History", - // "label": "American History", - // "hits": 100, - // } - // ] - // } - // ] - data: T.arrayOf(T.shape({ - name: T.string, - label: T.string, - items: T.arrayOf(T.shape({ - hits: T.number, - label: T.string, - value: T.any, - })) - })).isRequired, - - onRemoveSelectedFacet: T.func.isRequired, - onSelectFacet: T.func.isRequired, - - defaultBodyComponent: T.func, - - // `selectedFacets` is a map of facet objects - // ----------- - // { - // `example_name`: [ - // { value: 'Facet 1', name: 'Facet 1', hits: 200 }, - // ] - // } - selectedFacets: T.object, - }, - - getDefaultProps: function () { - return { - selectedFacets: {}, - } - }, - - buildFacetDictionary: function () { - var dict = {} - - this.props.data.forEach((facet, index) => { - dict[facet.name] = index - }) - - return dict - }, - - getSelectedFacets: function (name) { - return this.props.selectedFacets[name] || [] - }, - - renderChildren: function () { - var dict = this.buildFacetDictionary() - - return React.Children.map(this.props.children, (child, index) => { - const name = child.props.name - const idx = dict[name] - - // skip facets w/o a name property - if (typeof idx === 'undefined') - return - - const data = this.props.data[idx] - const items = child.props.data || data.items - - // skip empty facets - if (!items.length) - return - - const bodyComponent = child.props.bodyComponent - ? child.props.bodyComponent - : this.props.defaultBodyComponent - - const props = { - bodyComponent, - items, - key: name + index, - label: child.props.label || data.label, - onRemoveSelectedFacet: this.props.onRemoveSelectedFacet.bind(null, name), - onSelectFacet: this.props.onSelectFacet.bind(null, name), - selectedFacets: this.getSelectedFacets(name) - } - - return React.cloneElement(child, props) - }) - }, - - render: function () { - const wrapperStyles = { - display: 'inline-block', - width: '100%', - } - - return ( -
    - {this.renderChildren()} -
    - ) - } + propTypes: { + // `facets` is an array of facet-objects from blacklight. + // ---------- + // "facets": [ + // { + // "label": "Database Topic", + // "name": "db_az_subject", + // "items": [ + // { + // "value": "American History", + // "label": "American History", + // "hits": 100, + // } + // ] + // } + // ] + data: T.arrayOf(T.shape({ + name: T.string, + label: T.string, + items: T.arrayOf(T.shape({ + hits: T.number, + label: T.string, + value: T.any, + })) + })).isRequired, + + onRemoveSelectedFacet: T.func.isRequired, + onSelectFacet: T.func.isRequired, + + defaultBodyComponent: T.func, + + // `selectedFacets` is a map of facet objects + // ----------- + // { + // `example_name`: [ + // { value: 'Facet 1', name: 'Facet 1', hits: 200 }, + // ] + // } + selectedFacets: T.object, + }, + + getDefaultProps: function () { + return { + selectedFacets: {}, + } + }, + + buildFacetDictionary: function () { + var dict = {} + + this.props.data.forEach((facet, index) => { + dict[facet.name] = index + }) + + return dict + }, + + getSelectedFacets: function (name) { + return this.props.selectedFacets[name] || [] + }, + + renderChildren: function () { + var dict = this.buildFacetDictionary() + + return React.Children.map(this.props.children, (child, index) => { + const name = child.props.name + const idx = dict[name] + + // skip facets w/o a name property + if (typeof idx === 'undefined') + return + + const data = this.props.data[idx] + const items = child.props.data || data.items + + // skip empty facets + if (!items.length) + return + + const bodyComponent = child.props.bodyComponent + ? child.props.bodyComponent + : this.props.defaultBodyComponent + + const props = { + bodyComponent, + items, + key: name + index, + label: child.props.label || data.label, + onRemoveSelectedFacet: this.props.onRemoveSelectedFacet.bind(null, name), + onSelectFacet: this.props.onSelectFacet.bind(null, name), + selectedFacets: this.getSelectedFacets(name) + } + + return React.cloneElement(child, props) + }) + }, + + render: function () { + const wrapperStyles = { + display: 'inline-block', + width: '100%', + } + + return ( +
    + {this.renderChildren()} +
    + ) + } }) export default FacetGroup diff --git a/src/components/catalog/FacetList.jsx b/src/components/catalog/FacetList.jsx index 8e4cb11..4969393 100644 --- a/src/components/catalog/FacetList.jsx +++ b/src/components/catalog/FacetList.jsx @@ -6,85 +6,85 @@ import FacetListItem from './FacetListItem.jsx' const T = React.PropTypes const FacetList = React.createClass({ - propTypes: { - // data related to the facet (as returned from the Blacklight JSON api) - label: T.string.isRequired, - name: T.string.isRequired, - items: T.array.isRequired, + propTypes: { + // data related to the facet (as returned from the Blacklight JSON api) + label: T.string.isRequired, + name: T.string.isRequired, + items: T.array.isRequired, - onRemoveSelectedFacet: T.func.isRequired, - onSelectFacet: T.func.isRequired, - selectedFacets: T.array.isRequired, - }, + onRemoveSelectedFacet: T.func.isRequired, + onSelectFacet: T.func.isRequired, + selectedFacets: T.array.isRequired, + }, - componentWillUpdate: function () { - // need to clear out internal list when props change, otherwise - // previously selected values will linger around in the array - // and not appear as unselected values when the render arrives - this._selectedFacetValues = null - }, + componentWillUpdate: function () { + // need to clear out internal list when props change, otherwise + // previously selected values will linger around in the array + // and not appear as unselected values when the render arrives + this._selectedFacetValues = null + }, - renderFacetList: function () { - const { items, name, onSelectFacet } = this.props + renderFacetList: function () { + const { items, name, onSelectFacet } = this.props - return items.map((item, index) => { - if (this._selectedFacetValues && this._selectedFacetValues.indexOf(item.value) > -1) - return null + return items.map((item, index) => { + if (this._selectedFacetValues && this._selectedFacetValues.indexOf(item.value) > -1) + return null - const props = { - data: item, - onClick: this.props.onSelectFacet, - } + const props = { + data: item, + onClick: this.props.onSelectFacet, + } - return React.createElement( - 'li', - { key: 'unsel' + name + index }, - React.createElement(FacetListItem, props) - ) - }) - }, + return React.createElement( + 'li', + { key: 'unsel' + name + index }, + React.createElement(FacetListItem, props) + ) + }) + }, - renderSelectedFacetList: function () { - const selected = this.props.selectedFacets - if (!selected.length) - return + renderSelectedFacetList: function () { + const selected = this.props.selectedFacets + if (!selected.length) + return - return selected.map((item, index) => { - const props = { - data: item, - // onRemove: this.removeFacetFromInternalValueList, - onRemove: this.props.onRemoveSelectedFacet, - } + return selected.map((item, index) => { + const props = { + data: item, + // onRemove: this.removeFacetFromInternalValueList, + onRemove: this.props.onRemoveSelectedFacet, + } - if (this._selectedFacetValues && this._selectedFacetValues.push) - this._selectedFacetValues.push(item.value) - else - this._selectedFacetValues = [item.value] + if (this._selectedFacetValues && this._selectedFacetValues.push) + this._selectedFacetValues.push(item.value) + else + this._selectedFacetValues = [item.value] - return React.createElement( - 'li', - { key: 'sel-' + index + '-' + item.value }, - React.createElement(FacetListSelectedItem, props) - ) - }) - }, + return React.createElement( + 'li', + { key: 'sel-' + index + '-' + item.value }, + React.createElement(FacetListSelectedItem, props) + ) + }) + }, - render: function () { - const styles = { - list: { - listStyleType: 'none', - margin: '0', - padding: '0', - } - } + render: function () { + const styles = { + list: { + listStyleType: 'none', + margin: '0', + padding: '0', + } + } - return ( -
      - {this.renderSelectedFacetList()} - {this.renderFacetList()} -
    - ) - } + return ( +
      + {this.renderSelectedFacetList()} + {this.renderFacetList()} +
    + ) + } }) export default FacetList diff --git a/src/components/catalog/FacetListSelectedItem.jsx b/src/components/catalog/FacetListSelectedItem.jsx index 6ff584e..5412d43 100644 --- a/src/components/catalog/FacetListSelectedItem.jsx +++ b/src/components/catalog/FacetListSelectedItem.jsx @@ -4,55 +4,55 @@ import Button from '../Button.jsx' const T = React.PropTypes const FacetListSelectedItem = React.createClass({ - propTypes: { - data: T.object.isRequired, - onRemove: T.func.isRequired, - - color: T.string, - }, - - getDefaultProps: function () { - return { - color: '#a4b9a4' - } - }, - - maybeRenderHits: function (props) { - const hits = this.props.data.hits - - if (this.props.hideCount || (typeof hits !== 'number')) - return - - return React.createElement('span', props, hits) - }, - - render: function () { - const styles = { - item: { - color: this.props.color, - margin: '5px 0', - position: 'relative', - }, - - count: { - right: '0', - position: 'absolute', - } - } - - return ( -
    - {this.props.data.label} -
    - ) - } + propTypes: { + data: T.object.isRequired, + onRemove: T.func.isRequired, + + color: T.string, + }, + + getDefaultProps: function () { + return { + color: '#a4b9a4' + } + }, + + maybeRenderHits: function (props) { + const hits = this.props.data.hits + + if (this.props.hideCount || (typeof hits !== 'number')) + return + + return React.createElement('span', props, hits) + }, + + render: function () { + const styles = { + item: { + color: this.props.color, + margin: '5px 0', + position: 'relative', + }, + + count: { + right: '0', + position: 'absolute', + } + } + + return ( +
    + {this.props.data.label} +
    + ) + } }) export default FacetListSelectedItem diff --git a/src/components/catalog/FacetListWithViewMore.jsx b/src/components/catalog/FacetListWithViewMore.jsx index 0573203..83ad645 100644 --- a/src/components/catalog/FacetListWithViewMore.jsx +++ b/src/components/catalog/FacetListWithViewMore.jsx @@ -6,152 +6,152 @@ import FacetList from './FacetList.jsx' const T = React.PropTypes const FacetListWithViewMore = React.createClass({ - propTypes: { - label: T.string.isRequired, - name: T.string.isRequired, - items: T.array.isRequired, - - onRemoveSelectedFacet: T.func.isRequired, - onSelectFacet: T.func.isRequired, - selectedFacets: T.array.isRequired, - - limit: T.number, - - // determines whether or not the View More modal is open on mount. - // this is mostly as a means to test the modal w/o having to trigger - // it on each test. - // (default: `false`) - modalOpen: T.bool, - - // text used for `View more...` link/span that will trigger modal open. - viewMoreText: T.string, - }, - - getDefaultProps: function () { - return { - limit: 5, - modalOpen: false, - viewMoreText: 'View more...', - } - }, - - getInitialState: function () { - return { - modalOpen: this.props.modalOpen, - } - }, - - getFacetListProps: function (xtend) { - return assign({}, { - className: 'facet-list', - items: this.props.items, - label: this.props.label, - name: this.props.name, - onRemoveSelectedFacet: this.handleRemoveSelectedFacet, - onSelectFacet: this.handleSelectFacet, - selectedFacets: this.props.selectedFacets, - }, xtend) - }, - - handleRemoveSelectedFacet: function () { - if (this.state.modalOpen) - this.setState({modalOpen: false}) - - this.props.onRemoveSelectedFacet.apply(null, arguments) - }, - - handleSelectFacet: function () { - if (this.state.modalOpen) - this.setState({modalOpen: false}) - - this.props.onSelectFacet.apply(null, arguments) - }, - - maybeRenderModal: function () { - if (!this.state.modalOpen) - return - - const label = this.props.label - const props = { - contentLabel: `Viewing all facets for ${label}`, - isOpen: this.state.modalOpen, - header: function (modal) { - return ( -
    - Viewing all for {label} -
    - ) - }, - key: 'dss-flwvm-modal', - onRequestClose: this.toggleModal, - style: { - content: { - bottom: '33%', - left: '33%', - right: '33%', - top: '10%', - } - } - } - - return ( - - - - ) - }, - - renderLimitedFacetList: function () { - const els = [] - - const limit = this.props.limit - const flProps = this.getFacetListProps({ - items: this.props.items.slice(0, limit), - key: 'dss-fpwvm-limited-list', - }) - - const LimitedList = React.createElement(FacetList, flProps) - - // no need to add a `view more` link if there aren't more to view - if (this.props.items.length <= limit) - return LimitedList - - const ViewMoreLink = React.createElement('span', { - className: 'view-more', - key: 'dss-fpwvm-view-more', - onClick: this.toggleModal, - style: { - borderBottom: '1px dotted #aaa', - cursor: 'pointer', - display: 'inline-block', - fontWeight: 'bold', - marginTop: '10px', - } - }, this.props.viewMoreText) - - return [LimitedList, ViewMoreLink] - }, - - toggleModal: function () { - this.setState({modalOpen: !this.state.modalOpen}) - }, - - render: function () { - const styles = { - list: { - listStyleType: 'none', - margin: '0', - padding: '0', - } - } - - return ( -
    - {this.renderLimitedFacetList()} - {this.maybeRenderModal()} -
    - ) - } + propTypes: { + label: T.string.isRequired, + name: T.string.isRequired, + items: T.array.isRequired, + + onRemoveSelectedFacet: T.func.isRequired, + onSelectFacet: T.func.isRequired, + selectedFacets: T.array.isRequired, + + limit: T.number, + + // determines whether or not the View More modal is open on mount. + // this is mostly as a means to test the modal w/o having to trigger + // it on each test. + // (default: `false`) + modalOpen: T.bool, + + // text used for `View more...` link/span that will trigger modal open. + viewMoreText: T.string, + }, + + getDefaultProps: function () { + return { + limit: 5, + modalOpen: false, + viewMoreText: 'View more...', + } + }, + + getInitialState: function () { + return { + modalOpen: this.props.modalOpen, + } + }, + + getFacetListProps: function (xtend) { + return assign({}, { + className: 'facet-list', + items: this.props.items, + label: this.props.label, + name: this.props.name, + onRemoveSelectedFacet: this.handleRemoveSelectedFacet, + onSelectFacet: this.handleSelectFacet, + selectedFacets: this.props.selectedFacets, + }, xtend) + }, + + handleRemoveSelectedFacet: function () { + if (this.state.modalOpen) + this.setState({modalOpen: false}) + + this.props.onRemoveSelectedFacet.apply(null, arguments) + }, + + handleSelectFacet: function () { + if (this.state.modalOpen) + this.setState({modalOpen: false}) + + this.props.onSelectFacet.apply(null, arguments) + }, + + maybeRenderModal: function () { + if (!this.state.modalOpen) + return + + const label = this.props.label + const props = { + contentLabel: `Viewing all facets for ${label}`, + isOpen: this.state.modalOpen, + header: function (modal) { + return ( +
    + Viewing all for {label} +
    + ) + }, + key: 'dss-flwvm-modal', + onRequestClose: this.toggleModal, + style: { + content: { + bottom: '33%', + left: '33%', + right: '33%', + top: '10%', + } + } + } + + return ( + + + + ) + }, + + renderLimitedFacetList: function () { + const els = [] + + const limit = this.props.limit + const flProps = this.getFacetListProps({ + items: this.props.items.slice(0, limit), + key: 'dss-fpwvm-limited-list', + }) + + const LimitedList = React.createElement(FacetList, flProps) + + // no need to add a `view more` link if there aren't more to view + if (this.props.items.length <= limit) + return LimitedList + + const ViewMoreLink = React.createElement('span', { + className: 'view-more', + key: 'dss-fpwvm-view-more', + onClick: this.toggleModal, + style: { + borderBottom: '1px dotted #aaa', + cursor: 'pointer', + display: 'inline-block', + fontWeight: 'bold', + marginTop: '10px', + } + }, this.props.viewMoreText) + + return [LimitedList, ViewMoreLink] + }, + + toggleModal: function () { + this.setState({modalOpen: !this.state.modalOpen}) + }, + + render: function () { + const styles = { + list: { + listStyleType: 'none', + margin: '0', + padding: '0', + } + } + + return ( +
    + {this.renderLimitedFacetList()} + {this.maybeRenderModal()} +
    + ) + } }) export default FacetListWithViewMore diff --git a/src/components/catalog/FacetRangeLimitDate.jsx b/src/components/catalog/FacetRangeLimitDate.jsx index 71e2931..3b1beb3 100644 --- a/src/components/catalog/FacetRangeLimitDate.jsx +++ b/src/components/catalog/FacetRangeLimitDate.jsx @@ -8,8 +8,8 @@ import FacetListSelectedItem from './FacetListSelectedItem.jsx' import createRangeFacet from '../../../lib/create-range-facet' import { - INTERVALS, - VALUES as INTERVAL_VALUES, + INTERVALS, + VALUES as INTERVAL_VALUES, } from './common/date-intervals' import calculateRange from './common/calculate-range' @@ -19,90 +19,90 @@ import roundDate from './common/round-date-to-interval' const T = React.PropTypes const FacetRangeLimitDate = React.createClass({ - propTypes: { - name: T.string, - label: T.string, - items: T.array, - - onSelectFacet: T.func.isRequired, - onRemoveSelectedFacet: T.func.isRequired, - selectedFacets: T.array.isRequired, - - interval: T.oneOf(INTERVAL_VALUES) - }, - - getInitialState: function () { - return { - hits: 0, - items: this.props.items, - max: 0, - min: 0, - } - }, - - componentWillMount: function () { - // TODO: don't assume correctly formatted ISO timestamps - const range = calculateRange(this.props.items, v => { - const parsed = Date.parse(v) - return parsed - }) - - range.min = roundDate(this.props.interval, range.min) - range.max = roundDate(this.props.interval, range.max) - - this.setState(range) - }, - - getFormattedDateValue: function (raw) { - return formatDateValue(this.props.interval, raw) - }, - - handleApplyRange: function (range) { - const [rawMin, rawMax] = range - - const min = this.getFormattedDateValue(rawMin) - const max = this.getFormattedDateValue(rawMax) - - const facet = createRangeFacet(this.props.name, min, max) - - this.props.onSelectFacet(facet) - }, - - maybeRenderSelectedFacets: function () { - if (!this.props.selectedFacets.length) - return - - return this.props.selectedFacets.map((facet, index) => { - const key = `sel-${facet.name}-${facet.value.begin}-${facet.value.end}` - const props = { - data: facet, - onRemove: this.props.onRemoveSelectedFacet, - key, - } - - return React.createElement(FacetListSelectedItem, props) - }) - }, - - render: function () { - const containerProps = { - style: { - padding: '10px', - } - } - - return ( -
    - {this.maybeRenderSelectedFacets()} - -
    - ) - } + propTypes: { + name: T.string, + label: T.string, + items: T.array, + + onSelectFacet: T.func.isRequired, + onRemoveSelectedFacet: T.func.isRequired, + selectedFacets: T.array.isRequired, + + interval: T.oneOf(INTERVAL_VALUES) + }, + + getInitialState: function () { + return { + hits: 0, + items: this.props.items, + max: 0, + min: 0, + } + }, + + componentWillMount: function () { + // TODO: don't assume correctly formatted ISO timestamps + const range = calculateRange(this.props.items, v => { + const parsed = Date.parse(v) + return parsed + }) + + range.min = roundDate(this.props.interval, range.min) + range.max = roundDate(this.props.interval, range.max) + + this.setState(range) + }, + + getFormattedDateValue: function (raw) { + return formatDateValue(this.props.interval, raw) + }, + + handleApplyRange: function (range) { + const [rawMin, rawMax] = range + + const min = this.getFormattedDateValue(rawMin) + const max = this.getFormattedDateValue(rawMax) + + const facet = createRangeFacet(this.props.name, min, max) + + this.props.onSelectFacet(facet) + }, + + maybeRenderSelectedFacets: function () { + if (!this.props.selectedFacets.length) + return + + return this.props.selectedFacets.map((facet, index) => { + const key = `sel-${facet.name}-${facet.value.begin}-${facet.value.end}` + const props = { + data: facet, + onRemove: this.props.onRemoveSelectedFacet, + key, + } + + return React.createElement(FacetListSelectedItem, props) + }) + }, + + render: function () { + const containerProps = { + style: { + padding: '10px', + } + } + + return ( +
    + {this.maybeRenderSelectedFacets()} + +
    + ) + } }) export default FacetRangeLimitDate diff --git a/src/components/catalog/RangeSliderDate.jsx b/src/components/catalog/RangeSliderDate.jsx index 8d71f86..dd5df4d 100644 --- a/src/components/catalog/RangeSliderDate.jsx +++ b/src/components/catalog/RangeSliderDate.jsx @@ -7,8 +7,8 @@ import roundDateValue from './common/round-date-to-interval' import parseInputValue from './common/parse-input-date-value' import { - INTERVALS, - VALUES as INTERVAL_VALUES, + INTERVALS, + VALUES as INTERVAL_VALUES, } from './common/date-intervals' // TODO: move this styling to SCSS area @@ -17,188 +17,188 @@ import 'rc-slider/assets/index.css' const { YEAR, MONTH, DAY } = INTERVALS const propTypes = { - // min/max are UTC timestamp values (similar to Date.now()) - min: React.PropTypes.number.isRequired, - max: React.PropTypes.number.isRequired, + // min/max are UTC timestamp values (similar to Date.now()) + min: React.PropTypes.number.isRequired, + max: React.PropTypes.number.isRequired, - onApplyRange: React.PropTypes.func.isRequired, + onApplyRange: React.PropTypes.func.isRequired, - interval: React.PropTypes.oneOf(INTERVAL_VALUES), + interval: React.PropTypes.oneOf(INTERVAL_VALUES), } const defaultProps = { - interval: DAY, + interval: DAY, } // helper fns function getInputType (interval) { - switch (interval) { - case MONTH: return 'month' - case YEAR: return 'number' - case DAY: - default: return 'date' - } + switch (interval) { + case MONTH: return 'month' + case YEAR: return 'number' + case DAY: + default: return 'date' + } } function getStepValue (min, max, interval) { - const day = 1000 * 60 * 60 * 24 + const day = 1000 * 60 * 60 * 24 - let divisor + let divisor - switch (interval) { - case YEAR: divisor = day * 365.25 - case MONTH: divisor = (day * 365.25) / 12 - case DAY: - default: divisor = day - } + switch (interval) { + case YEAR: divisor = day * 365.25 + case MONTH: divisor = (day * 365.25) / 12 + case DAY: + default: divisor = day + } - return Math.round((max - min) / divisor) + return Math.round((max - min) / divisor) } class RangeSliderDate extends React.Component { - constructor (props) { - super(props) - - this.state = { - min: props.min, - max: props.max, - } - - this._formatted = { - min: formatDateValue(props.interval, props.min), - max: formatDateValue(props.interval, props.max) - } - - this._rounded = { - min: roundDateValue(props.interval, props.min), - max: roundDateValue(props.interval, props.max), - } - - this.getStepValue = this.getStepValue.bind(this) - this.handleApplyRange = this.handleApplyRange.bind(this) - // this.handleInputChange is bound within this.renderInput - this.renderInput = this.renderInput.bind(this) - this.renderSlider = this.renderSlider.bind(this) - } - - componentWillUpdate (nextProps) { - if (nextProps.interval !== this.props.interval) - this.step = null - } - - getStepValue () { - if (!this._step && this._step !== 0) { - const { min, max } = this._rounded ? this._rounded : this.props - const { interval } = this.props - this._step = getStepValue(min, max, interval) - } - - return this._step - } - - handleApplyRange () { - const value = [ - this.state.min, - this.state.max, - ].map(v => formatDateValue(this.props.interval, v)) - - this.props.onApplyRange(value) - } - - handleInputChange (which, event) { - const target = (event || {}).target || {} - const value = target.value - const parsed = parseInputValue(value) - - const update = {} - // (parsed !== parsed) === Number.isNaN(parsed) - update[which] = parsed !== parsed ? null : parsed - - this.setState(update) - } - - renderInput (which) { - const tsValue = this.state[which] - const value = tsValue ? formatDateValue(this.props.interval, tsValue) : '' - const type = getInputType(this.props.interval) - - const props = { - className: 'range-slider-date--input', - key: `input-${which}`, - min: this._formatted.min, - max: this._formatted.max, - onChange: this.handleInputChange.bind(this, which), - type, - value, - } - - return ( - - ) - } - - renderSlider () { - const hasSingleValue = this.props.min === this.props.max - - const value = hasSingleValue ? (this.state.min || 0) : [ - this.state.min || this.props.min, - this.state.max || this.props.max, - ] - - const onChange = value => { - this.setState({ - min: value[0], - max: value[1], - }) - } - - const props = { - allowCross: true, - min: this._rounded.min, - max: this._rounded.max, - onChange, - pushable: false, - range: !hasSingleValue, - tipFormatter: formatDateValue.bind(null, this.props.interval), - value, - } - - if (!hasSingleValue) { - props.step = this.getStepValue() - } - - return - } - - render () { - const hasOneValue = this.props.min === this.props.max - const applyRangeProps = { - onClick: this.handleApplyRange - } - - // disable the ability to set a range when either - // the min or max are empty - if (!this.state.min || !this.state.max) { - applyRangeProps.disabled = true - } - - return ( -
    -
    - {this.renderInput('min')} - {this.renderInput('max')} - -
    - - {this.renderSlider()} -
    - ) - } + constructor (props) { + super(props) + + this.state = { + min: props.min, + max: props.max, + } + + this._formatted = { + min: formatDateValue(props.interval, props.min), + max: formatDateValue(props.interval, props.max) + } + + this._rounded = { + min: roundDateValue(props.interval, props.min), + max: roundDateValue(props.interval, props.max), + } + + this.getStepValue = this.getStepValue.bind(this) + this.handleApplyRange = this.handleApplyRange.bind(this) + // this.handleInputChange is bound within this.renderInput + this.renderInput = this.renderInput.bind(this) + this.renderSlider = this.renderSlider.bind(this) + } + + componentWillUpdate (nextProps) { + if (nextProps.interval !== this.props.interval) + this.step = null + } + + getStepValue () { + if (!this._step && this._step !== 0) { + const { min, max } = this._rounded ? this._rounded : this.props + const { interval } = this.props + this._step = getStepValue(min, max, interval) + } + + return this._step + } + + handleApplyRange () { + const value = [ + this.state.min, + this.state.max, + ].map(v => formatDateValue(this.props.interval, v)) + + this.props.onApplyRange(value) + } + + handleInputChange (which, event) { + const target = (event || {}).target || {} + const value = target.value + const parsed = parseInputValue(value) + + const update = {} + // (parsed !== parsed) === Number.isNaN(parsed) + update[which] = parsed !== parsed ? null : parsed + + this.setState(update) + } + + renderInput (which) { + const tsValue = this.state[which] + const value = tsValue ? formatDateValue(this.props.interval, tsValue) : '' + const type = getInputType(this.props.interval) + + const props = { + className: 'range-slider-date--input', + key: `input-${which}`, + min: this._formatted.min, + max: this._formatted.max, + onChange: this.handleInputChange.bind(this, which), + type, + value, + } + + return ( + + ) + } + + renderSlider () { + const hasSingleValue = this.props.min === this.props.max + + const value = hasSingleValue ? (this.state.min || 0) : [ + this.state.min || this.props.min, + this.state.max || this.props.max, + ] + + const onChange = value => { + this.setState({ + min: value[0], + max: value[1], + }) + } + + const props = { + allowCross: true, + min: this._rounded.min, + max: this._rounded.max, + onChange, + pushable: false, + range: !hasSingleValue, + tipFormatter: formatDateValue.bind(null, this.props.interval), + value, + } + + if (!hasSingleValue) { + props.step = this.getStepValue() + } + + return + } + + render () { + const hasOneValue = this.props.min === this.props.max + const applyRangeProps = { + onClick: this.handleApplyRange + } + + // disable the ability to set a range when either + // the min or max are empty + if (!this.state.min || !this.state.max) { + applyRangeProps.disabled = true + } + + return ( +
    +
    + {this.renderInput('min')} + {this.renderInput('max')} + +
    + + {this.renderSlider()} +
    + ) + } } RangeSliderDate.propTypes = propTypes diff --git a/src/components/catalog/ResultsContainer.jsx b/src/components/catalog/ResultsContainer.jsx index bd034ee..e407a19 100644 --- a/src/components/catalog/ResultsContainer.jsx +++ b/src/components/catalog/ResultsContainer.jsx @@ -3,41 +3,41 @@ import assign from 'object-assign' const T = React.PropTypes const ResultsContainer = React.createClass({ - propTypes: { - data: T.array.isRequired, - displayComponent: T.func.isRequired, - - containerProps: T.object, - itemProps: T.object, - offset: T.oneOfType([T.number, T.string]), - }, - - renderResults: function () { - const Component = this.props.displayComponent - - if (!Component) - return - - return this.props.data.map((item, index) => { - const props = assign({ - item, - position: (this.props.offset || 0) + 1 + index, - key: 'result-' + (this.props.offset + index) + '-' + item.id, - }, this.props.itemProps) - - return React.createElement(Component, props) - }) - }, - - render: function () { - const containerProps = assign({ - style: { - marginTop: '10px', - } - }, this.props.containerProps) - - return React.createElement('div', containerProps, this.renderResults()) - } + propTypes: { + data: T.array.isRequired, + displayComponent: T.func.isRequired, + + containerProps: T.object, + itemProps: T.object, + offset: T.oneOfType([T.number, T.string]), + }, + + renderResults: function () { + const Component = this.props.displayComponent + + if (!Component) + return + + return this.props.data.map((item, index) => { + const props = assign({ + item, + position: (this.props.offset || 0) + 1 + index, + key: 'result-' + (this.props.offset + index) + '-' + item.id, + }, this.props.itemProps) + + return React.createElement(Component, props) + }) + }, + + render: function () { + const containerProps = assign({ + style: { + marginTop: '10px', + } + }, this.props.containerProps) + + return React.createElement('div', containerProps, this.renderResults()) + } }) export default ResultsContainer diff --git a/src/components/catalog/ResultsGallery.jsx b/src/components/catalog/ResultsGallery.jsx index 11e645d..382adba 100644 --- a/src/components/catalog/ResultsGallery.jsx +++ b/src/components/catalog/ResultsGallery.jsx @@ -2,29 +2,29 @@ import React from 'react' import ResultsGalleryItem from './ResultsGalleryItem.jsx' const propTypes = { - data: React.PropTypes.array, + data: React.PropTypes.array, } const defaultProps = { - data: [], + data: [], } class ResultsGallery extends React.Component { - constructor (props) { - super(props) - } + constructor (props) { + super(props) + } - renderGalleryItem (item, index) { - return - } + renderGalleryItem (item, index) { + return + } - render () { - return ( -
    - {this.props.data.map(this.renderGalleryItem)} -
    - ) - } + render () { + return ( +
    + {this.props.data.map(this.renderGalleryItem)} +
    + ) + } } ResultsGallery.propTypes = propTypes diff --git a/src/components/catalog/ResultsGalleryItem.jsx b/src/components/catalog/ResultsGalleryItem.jsx index 4dea8cc..463a15b 100644 --- a/src/components/catalog/ResultsGalleryItem.jsx +++ b/src/components/catalog/ResultsGalleryItem.jsx @@ -2,50 +2,50 @@ import React from 'react' import Link from 'react-router/lib/Link' const propTypes = { - data: React.PropTypes.object, + data: React.PropTypes.object, } const defaultProps = { - data: {} + data: {} } class ResultsGalleryItem extends React.PureComponent { - getTitle () { - const { title } = this.props.data - - if (!title || !title.length) - return null - - return title[0] - } - - renderThumbnail () { - const { thumbnail_path } = this.props.data - - if (!thumbnail_path) - return null - - const src = `${process.env.API_BASE_URL}${thumbnail_path}` - const props = { - className: 'search-results-gallery--thumbnail', - src, - } - return - } - - render () { - const src = process.env.API_BASE_URL + this.props.data.thumbnail_path - return ( -
    - - {this.renderThumbnail()} -
    - {this.getTitle()} -
    - -
    - ) - } + getTitle () { + const { title } = this.props.data + + if (!title || !title.length) + return null + + return title[0] + } + + renderThumbnail () { + const { thumbnail_path } = this.props.data + + if (!thumbnail_path) + return null + + const src = `${process.env.API_BASE_URL}${thumbnail_path}` + const props = { + className: 'search-results-gallery--thumbnail', + src, + } + return + } + + render () { + const src = process.env.API_BASE_URL + this.props.data.thumbnail_path + return ( +
    + + {this.renderThumbnail()} +
    + {this.getTitle()} +
    + +
    + ) + } } ResultsGalleryItem.propTypes = propTypes diff --git a/src/components/catalog/ResultsPager.jsx b/src/components/catalog/ResultsPager.jsx index c5c5c7c..c6020c8 100644 --- a/src/components/catalog/ResultsPager.jsx +++ b/src/components/catalog/ResultsPager.jsx @@ -18,167 +18,167 @@ import commafy from 'number-with-commas' const T = React.PropTypes const ResultsPager = React.createClass({ - propTypes: { - onNextClick: T.func.isRequired, - onPreviousClick: T.func.isRequired, - - // there are more values passed from Blacklight, but these - // are the only ones we really need to populate the - // `(start) - (end) of (total)` message - limit_value: T.number.isRequired, - next_page: T.number, - offset_value: T.number.isRequired, - prev_page: T.number, - total_count: T.number.isRequired, - - // message generated between prev/next buttons - // displaying current page info + total. - // - // if a string, it is passed to sprintf with the (en-US) commafied values: - // start, end, total count - // - // if a function, these three values are passed (un-(en-US)commafied) to - // the function, which is expected to return a string - // - // presently: html values are allowed (the string is passed - // into `dangerouslySetInnerHtml`) - // - // (default: '%s – %s of %s') - message: T.oneOfType([T.string, T.func]), - - // text used for next/prev buttons - - // (default: 'Next »') - nextText: T.string, - - // (default: '« Previous') - previousText: T.string, - - style: T.object, - }, - - getDefaultProps: function () { - return { - next_page: null, - prev_page: null, - message: '%s – %s of %s', - nextText: 'Next »', - previousText: '« Previous', - } - }, - - atFirstPage: function () { - return this.props.prev_page === null - }, - - atLastPage: function () { - return this.props.next_page === null - }, - - handleNextClick: function (ev) { - ev.preventDefault && ev.preventDefault() - this.props.onNextClick() - }, - - handlePreviousClick: function (ev) { - ev.preventDefault && ev.preventDefault() - this.props.onPreviousClick() - }, - - pagerText: function () { - const total = this.props.total_count - const offset = this.props.offset_value - const lower = offset + 1 - - // prevent the upper-bounds from exceeding the total value - const upper = Math.min(offset + this.props.limit_value, total) - let msg - - if (typeof this.props.message === 'function') { - msg = this.props.message.call(null, lower, upper, total) - } else { - msg = sprintf(this.props.message, - commafy(lower), - commafy(upper), - commafy(total) - ) - } - - // the template currently uses HTML (for a tag), so we'll need - // to `dangerouslySetInnerHTML` - return React.createElement('span', { - key: 'pager-message', - dangerouslySetInnerHTML: {__html: msg} - }) - }, - - positionButtonProps: function (which, positionCheck, onClick, style) { - const atLimit = positionCheck() - const text = this.props[which + 'Text'] - - const props = { - children: text, - key: 'dir-' + which, - ref: e => this[which + 'Button'] = e, - style: assign({}, { - backgroundColor: 'transparent', - border: 'none', - fontSize: '1em', - outline: 'none', - cursor: (atLimit ? 'default' : 'pointer'), - }, style), - } - - if (!atLimit) - props.onClick = onClick - else - props.disabled = true - - return props - }, - - positionButtonStyles: function (direction) { - const dir = direction === 'prev' ? 'prev' : 'next' - const styleDir = dir === 'prev' ? 'Right' : 'Left' - - const style = { - padding: '0 5px', - } - - style['border' + styleDir] = '1px solid #000' - style['margin' + styleDir] = '5px' - - return style - }, - - - render: function () { - const style = assign({}, { - display: 'inline-block', - }, this.props.style) - - const prevProps = this.positionButtonProps( - 'previous', - this.atFirstPage, - this.handlePreviousClick, - this.positionButtonStyles('prev') - ) - - const nextProps = this.positionButtonProps( - 'next', - this.atLastPage, - this.handleNextClick, - this.positionButtonStyles('next') - ) - - return ( -
    -
    - ) - } + propTypes: { + onNextClick: T.func.isRequired, + onPreviousClick: T.func.isRequired, + + // there are more values passed from Blacklight, but these + // are the only ones we really need to populate the + // `(start) - (end) of (total)` message + limit_value: T.number.isRequired, + next_page: T.number, + offset_value: T.number.isRequired, + prev_page: T.number, + total_count: T.number.isRequired, + + // message generated between prev/next buttons + // displaying current page info + total. + // + // if a string, it is passed to sprintf with the (en-US) commafied values: + // start, end, total count + // + // if a function, these three values are passed (un-(en-US)commafied) to + // the function, which is expected to return a string + // + // presently: html values are allowed (the string is passed + // into `dangerouslySetInnerHtml`) + // + // (default: '%s – %s of %s') + message: T.oneOfType([T.string, T.func]), + + // text used for next/prev buttons + + // (default: 'Next »') + nextText: T.string, + + // (default: '« Previous') + previousText: T.string, + + style: T.object, + }, + + getDefaultProps: function () { + return { + next_page: null, + prev_page: null, + message: '%s – %s of %s', + nextText: 'Next »', + previousText: '« Previous', + } + }, + + atFirstPage: function () { + return this.props.prev_page === null + }, + + atLastPage: function () { + return this.props.next_page === null + }, + + handleNextClick: function (ev) { + ev.preventDefault && ev.preventDefault() + this.props.onNextClick() + }, + + handlePreviousClick: function (ev) { + ev.preventDefault && ev.preventDefault() + this.props.onPreviousClick() + }, + + pagerText: function () { + const total = this.props.total_count + const offset = this.props.offset_value + const lower = offset + 1 + + // prevent the upper-bounds from exceeding the total value + const upper = Math.min(offset + this.props.limit_value, total) + let msg + + if (typeof this.props.message === 'function') { + msg = this.props.message.call(null, lower, upper, total) + } else { + msg = sprintf(this.props.message, + commafy(lower), + commafy(upper), + commafy(total) + ) + } + + // the template currently uses HTML (for a tag), so we'll need + // to `dangerouslySetInnerHTML` + return React.createElement('span', { + key: 'pager-message', + dangerouslySetInnerHTML: {__html: msg} + }) + }, + + positionButtonProps: function (which, positionCheck, onClick, style) { + const atLimit = positionCheck() + const text = this.props[which + 'Text'] + + const props = { + children: text, + key: 'dir-' + which, + ref: e => this[which + 'Button'] = e, + style: assign({}, { + backgroundColor: 'transparent', + border: 'none', + fontSize: '1em', + outline: 'none', + cursor: (atLimit ? 'default' : 'pointer'), + }, style), + } + + if (!atLimit) + props.onClick = onClick + else + props.disabled = true + + return props + }, + + positionButtonStyles: function (direction) { + const dir = direction === 'prev' ? 'prev' : 'next' + const styleDir = dir === 'prev' ? 'Right' : 'Left' + + const style = { + padding: '0 5px', + } + + style['border' + styleDir] = '1px solid #000' + style['margin' + styleDir] = '5px' + + return style + }, + + + render: function () { + const style = assign({}, { + display: 'inline-block', + }, this.props.style) + + const prevProps = this.positionButtonProps( + 'previous', + this.atFirstPage, + this.handlePreviousClick, + this.positionButtonStyles('prev') + ) + + const nextProps = this.positionButtonProps( + 'next', + this.atLastPage, + this.handleNextClick, + this.positionButtonStyles('next') + ) + + return ( +
    +
    + ) + } }) export default ResultsPager diff --git a/src/components/catalog/ResultsTable.jsx b/src/components/catalog/ResultsTable.jsx index 41b6762..d781cba 100644 --- a/src/components/catalog/ResultsTable.jsx +++ b/src/components/catalog/ResultsTable.jsx @@ -19,14 +19,14 @@ import ResultsTableFieldSelect from './ResultsTableFieldSelect.jsx' import { fields as searchResultFields } from '../../../lib/search-result-settings' const propTypes = { - data: React.PropTypes.array, - getSelectedFields: React.PropTypes.func, - setSelectedFields: React.PropTypes.func, + data: React.PropTypes.array, + getSelectedFields: React.PropTypes.func, + setSelectedFields: React.PropTypes.func, } const defaultProps = { - data: [], - defaultFields: ['title', 'creator'], + data: [], + defaultFields: ['title', 'creator'], } // create a dictionary to use for sorting the fields based on position. @@ -34,202 +34,202 @@ const defaultProps = { // `ResultsTableFieldSelect` component const workFieldKeys = Object.keys(workFields) const workFieldSortDict = workFieldKeys.reduce((dict, key, index) => { - dict[key] = index + 1 - return dict + dict[key] = index + 1 + return dict }, {}) const sortByField = (a, b) => (workFieldSortDict[a] - workFieldSortDict[b]) const ResultsTable = React.createClass({ - propTypes: propTypes, - - getDefaultProps: function () { - return defaultProps - }, - - getInitialState: function () { - let fields = searchResultFields.get() - - if (fields.length === 0) { - fields = this.props.defaultFields - } - - return { - fields, - fieldSelectOpen: false, - } - }, - - getColumns: function () { - // set default column first (thumbnail) - const columns = [ - { - className: 'thumbnail-preview', - header: this.getFieldToggleHeader(), - id: 'thumbnail_path', - - // TODO: a11y updates - renderer: (path, _, rowData) => ( - - - - ), - - thClassName: 'field-select', - - // disable sorting by setting type to None - type: DataType.None, - } - ] - - // TODO: should fields w/ multiple values be more than - // semi-colon delimited? - const renderer = data => { - if (Array.isArray(data)) - return data.join('; ') - - return data - } - - const fields = this.state.fields.map(id => ({ - id, - header: workFields[id], - renderer, - type: DataType.None, - })) - - return columns.concat(fields) - }, - - getFieldToggleHeader: function () { - const open = this.state.fieldSelectOpen - - // simple wrapper for svg lines - const rect = (x, y) => { - const props = { - width: '250', - height: '40', - fill: '#1e1e1e', - x, - y, - } - - return - } - - // our button will just have an svg of three stacked lines (the ol' - // hamburger button), + we'll do this w/ jsx - const contents = ( - + propTypes: propTypes, + + getDefaultProps: function () { + return defaultProps + }, + + getInitialState: function () { + let fields = searchResultFields.get() + + if (fields.length === 0) { + fields = this.props.defaultFields + } + + return { + fields, + fieldSelectOpen: false, + } + }, + + getColumns: function () { + // set default column first (thumbnail) + const columns = [ + { + className: 'thumbnail-preview', + header: this.getFieldToggleHeader(), + id: 'thumbnail_path', + + // TODO: a11y updates + renderer: (path, _, rowData) => ( + + + + ), + + thClassName: 'field-select', + + // disable sorting by setting type to None + type: DataType.None, + } + ] + + // TODO: should fields w/ multiple values be more than + // semi-colon delimited? + const renderer = data => { + if (Array.isArray(data)) + return data.join('; ') + + return data + } + + const fields = this.state.fields.map(id => ({ + id, + header: workFields[id], + renderer, + type: DataType.None, + })) + + return columns.concat(fields) + }, + + getFieldToggleHeader: function () { + const open = this.state.fieldSelectOpen + + // simple wrapper for svg lines + const rect = (x, y) => { + const props = { + width: '250', + height: '40', + fill: '#1e1e1e', + x, + y, + } + + return + } + + // our button will just have an svg of three stacked lines (the ol' + // hamburger button), + we'll do this w/ jsx + const contents = ( + { rect('25', '80') } { rect('25', '170') } { rect('25', '260') } - - ) - - const buttonProps = { - children: contents, - className: cn('btn-size-small', 'toggle', {open}), - key: 'toggle-btn', - onClick: ev => { - ev.preventDefault && ev.preventDefault() - this.setState({ - fieldSelectOpen: !this.state.fieldSelectOpen, - }) - }, - } - - return [ - - ) - - return ( -
    -
    - this.setState({query: e.target.value})} - name="query" - style={styles.input} - type="search" - /> -
    - {button} -
    - ) - }, - - render: function () { - const containerProps = { - style: { - // TODO: maybe get rid of this border - borderRight: '1px solid #ccc', - paddingRight: '10px', - } - } - - return ( -
    - {this.renderSearchHeader()} - - - {this.props.children} - -
    - ) - } + propTypes: { + data: T.array.isRequired, + + onClearSelectedFacets: T.func.isRequired, + + onRemoveSelectedFacet: T.func.isRequired, + onSelectFacet: T.func.isRequired, + onSubmitSearchQuery: T.func.isRequired, + + selectedFacets: T.object.isRequired, + + query: T.string, + }, + + componentWillReceiveProps: function (nextProps) { + this.setState({query: nextProps.query}) + }, + + getInitialState: function () { + return { + query: this.props.query, + } + }, + + handleClearFacets: function (ev) { + ev.preventDefault && ev.preventDefault() + this.props.onClearSelectedFacets() + }, + + handleSearchSubmit: function (ev) { + ev.preventDefault && ev.preventDefault() + + const query = this.state.query + this.props.onSubmitSearchQuery(query) + }, + + renderSearchHeader: function () { + const styles = { + container: { + marginBottom: '10px', + }, + input: { + width: '100%', + } + } + + // TODO: this button isn't rendering correctly + const button = !Object.keys(this.props.selectedFacets).length ? null : ( + + ) + + return ( +
    +
    + this.setState({query: e.target.value})} + name="query" + style={styles.input} + type="search" + /> +
    + {button} +
    + ) + }, + + render: function () { + const containerProps = { + style: { + // TODO: maybe get rid of this border + borderRight: '1px solid #ccc', + paddingRight: '10px', + } + } + + return ( +
    + {this.renderSearchHeader()} + + + {this.props.children} + +
    + ) + } }) export default SearchFacetSidebar diff --git a/src/components/catalog/SearchResultsHeader.jsx b/src/components/catalog/SearchResultsHeader.jsx index c60d8d0..0c646c8 100644 --- a/src/components/catalog/SearchResultsHeader.jsx +++ b/src/components/catalog/SearchResultsHeader.jsx @@ -7,80 +7,80 @@ const T = React.PropTypes const BORDER_RADIUS = '2px' const SearchResultsHeader = React.createClass({ - propTypes: { - onNextPage: T.func.isRequired, - onOpenToolModal: T.func.isRequired, - onPreviousPage: T.func.isRequired, - onPerPageChange: T.func.isRequired, - onViewChange: T.func.isRequired, + propTypes: { + onNextPage: T.func.isRequired, + onOpenToolModal: T.func.isRequired, + onPreviousPage: T.func.isRequired, + onPerPageChange: T.func.isRequired, + onViewChange: T.func.isRequired, - pageData: T.object.isRequired, - view: T.string, - viewOptions: T.array, + pageData: T.object.isRequired, + view: T.string, + viewOptions: T.array, - perPage: T.oneOfType([T.number, T.string]), - perPageOptions: T.array, - }, + perPage: T.oneOfType([T.number, T.string]), + perPageOptions: T.array, + }, - viewToggle: function () { - return ( - - ) - }, + viewToggle: function () { + return ( + + ) + }, - render: function () { - const styles = { - buttonContainer: { - display: 'inline-block', - }, + render: function () { + const styles = { + buttonContainer: { + display: 'inline-block', + }, - divider: { - border: 'none', - borderBottom: '1px solid #ccc', - margin: 'auto', - marginBottom: '10px', - marginTop: '10px', - width: '98%', - }, + divider: { + border: 'none', + borderBottom: '1px solid #ccc', + margin: 'auto', + marginBottom: '10px', + marginTop: '10px', + width: '98%', + }, - toggleContainer: { - float: 'right', - display: 'inline-block', - }, - } + toggleContainer: { + float: 'right', + display: 'inline-block', + }, + } - return ( -
    -
    -
    - -
    + return ( +
    +
    +
    + +
    -
    - {this.viewToggle()} -
    -
    +
    + {this.viewToggle()} +
    +
    -
    +
    - -
    - ) - } + +
    + ) + } }) export default SearchResultsHeader diff --git a/src/components/catalog/SearchResultsPagerHeader.jsx b/src/components/catalog/SearchResultsPagerHeader.jsx index a04055b..26a19fe 100644 --- a/src/components/catalog/SearchResultsPagerHeader.jsx +++ b/src/components/catalog/SearchResultsPagerHeader.jsx @@ -6,80 +6,80 @@ import { RESULTS_PAGER as RESULTS_PAGER_MESSAGE} from '../../messages/results' const T = React.PropTypes const SearchResultsPagerHeader = React.createClass({ - propTypes: { - data: T.object.isRequired, - onNextPage: T.func.isRequired, - onPreviousPage: T.func.isRequired, - onPerPageChange: T.func.isRequired, + propTypes: { + data: T.object.isRequired, + onNextPage: T.func.isRequired, + onPreviousPage: T.func.isRequired, + onPerPageChange: T.func.isRequired, - perPage: T.oneOfType([T.number, T.string]), - perPageOptions: T.array, - }, + perPage: T.oneOfType([T.number, T.string]), + perPageOptions: T.array, + }, - getDefaultProps: function () { - return { - perPage: 10, - perPageOptions: [10, 25, 50, 100, 250], - } - }, + getDefaultProps: function () { + return { + perPage: 10, + perPageOptions: [10, 25, 50, 100, 250], + } + }, - handlePerPageChange: function (val) { - this.props.onPerPageChange(val) - }, + handlePerPageChange: function (val) { + this.props.onPerPageChange(val) + }, - perPageOption: function (amount, index) { - return React.createElement('option', { - children: amount, - key: 'per-page-' + index + amount, - value: amount, - }) - }, + perPageOption: function (amount, index) { + return React.createElement('option', { + children: amount, + key: 'per-page-' + index + amount, + value: amount, + }) + }, - renderPerPage: function () { - const containerProps = { - style: { - float: 'right', - textAlign: 'right', - } - } + renderPerPage: function () { + const containerProps = { + style: { + float: 'right', + textAlign: 'right', + } + } - const selectProps = { - children: this.props.perPageOptions.map(this.perPageOption), - key: 'per-page-select', - onChange: this.handlePerPageChange, - value: this.props.perPage, - style: { - borderColor: '#ccc', - outlineColor: '#1d5f83', - marginLeft: '10px', - } - } + const selectProps = { + children: this.props.perPageOptions.map(this.perPageOption), + key: 'per-page-select', + onChange: this.handlePerPageChange, + value: this.props.perPage, + style: { + borderColor: '#ccc', + outlineColor: '#1d5f83', + marginLeft: '10px', + } + } - return ( -
    - per page - +
    + ) + }, - render: function () { - const pagerProps = { - ...this.props.data, - message: RESULTS_PAGER_MESSAGE, - onNextClick: this.props.onNextPage, - onPreviousClick: this.props.onPreviousPage, - } + render: function () { + const pagerProps = { + ...this.props.data, + message: RESULTS_PAGER_MESSAGE, + onNextClick: this.props.onNextPage, + onPreviousClick: this.props.onPreviousPage, + } - return ( -
    - + return ( +
    + - {this.renderPerPage()} -
    -
    - ) - } + {this.renderPerPage()} +
    +
    + ) + } }) export default SearchResultsPagerHeader diff --git a/src/components/catalog/__tests__/Facet-test.js b/src/components/catalog/__tests__/Facet-test.js index 6dea21d..617be81 100644 --- a/src/components/catalog/__tests__/Facet-test.js +++ b/src/components/catalog/__tests__/Facet-test.js @@ -9,46 +9,46 @@ import data from './data/facet.json' const noop = () => {} const defaultProps = { - ...data, - onRemoveSelectedFacet: noop, - onSelectFacet: noop, - open: true, + ...data, + onRemoveSelectedFacet: noop, + onSelectFacet: noop, + open: true, } const wrap = (xtend, renderer) => { - const props = assign({}, defaultProps, xtend) - return renderer(React.createElement(Facet, props)) + const props = assign({}, defaultProps, xtend) + return renderer(React.createElement(Facet, props)) } const shallowEl = xtend => wrap(xtend, shallow) describe('', function () { - it('uses the `bodyComponent` prop to render the body', function () { - const SomeFacetBody = () => (

    hello!

    ) - - const $el = shallowEl({bodyComponent: SomeFacetBody}) - expect($el.find('SomeFacetBody')).to.have.length(1) - }) - - it('hides `.facet-panel--body` when `open` prop is false', function () { - const $el = shallowEl({open: false}) - expect($el.find('.facet-panel--body')).to.have.length(0) - }) - - it('toggles `.facet-panel--body` when the header is clicked', function () { - const SomeFacetBody = () => (

    hey

    ) - const $el = shallowEl({bodyComponent: SomeFacetBody, open: false}) - expect($el.state('open')).to.be.false - expect($el.find('.facet-panel--body')).to.have.length(0) - - $el.find('header').simulate('click') - - expect($el.find('.facet-panel--body')).to.have.length(1) - expect($el.state('open')).to.be.true - }) - - it('does not render if no items are passed', function () { - const $el = shallowEl({items: []}) - expect($el.find('Facet')).to.have.length(0) - }) + it('uses the `bodyComponent` prop to render the body', function () { + const SomeFacetBody = () => (

    hello!

    ) + + const $el = shallowEl({bodyComponent: SomeFacetBody}) + expect($el.find('SomeFacetBody')).to.have.length(1) + }) + + it('hides `.facet-panel--body` when `open` prop is false', function () { + const $el = shallowEl({open: false}) + expect($el.find('.facet-panel--body')).to.have.length(0) + }) + + it('toggles `.facet-panel--body` when the header is clicked', function () { + const SomeFacetBody = () => (

    hey

    ) + const $el = shallowEl({bodyComponent: SomeFacetBody, open: false}) + expect($el.state('open')).to.be.false + expect($el.find('.facet-panel--body')).to.have.length(0) + + $el.find('header').simulate('click') + + expect($el.find('.facet-panel--body')).to.have.length(1) + expect($el.state('open')).to.be.true + }) + + it('does not render if no items are passed', function () { + const $el = shallowEl({items: []}) + expect($el.find('Facet')).to.have.length(0) + }) }) diff --git a/src/components/catalog/__tests__/FacetList-test.jsx b/src/components/catalog/__tests__/FacetList-test.jsx index 8dcd815..806c9a7 100644 --- a/src/components/catalog/__tests__/FacetList-test.jsx +++ b/src/components/catalog/__tests__/FacetList-test.jsx @@ -8,70 +8,70 @@ import FacetList from '../FacetList.jsx' import data from './data/facet.json' const defaultProps = { - ...data, - onRemoveSelectedFacet: () => {}, - onSelectFacet: () => {}, - selectedFacets: [], + ...data, + onRemoveSelectedFacet: () => {}, + onSelectFacet: () => {}, + selectedFacets: [], } const renderEl = (xtend, renderer) => { - const props = assign({}, defaultProps, xtend) - return renderer(React.createElement(FacetList, props)) + const props = assign({}, defaultProps, xtend) + return renderer(React.createElement(FacetList, props)) } const mountEl = xtend => renderEl(xtend, mount) const shallowEl = xtend => renderEl(xtend, shallow) describe('', function () { - it('renders `FacetListItem`s for each item passed', function () { - const $el = shallowEl() - expect($el.find('FacetListItem')).to.have.length(defaultProps.items.length) - }) + it('renders `FacetListItem`s for each item passed', function () { + const $el = shallowEl() + expect($el.find('FacetListItem')).to.have.length(defaultProps.items.length) + }) - it('retains the order of items passed', function () { - const $el = shallowEl() - const $listItems = $el.find('FacetListItem') + it('retains the order of items passed', function () { + const $el = shallowEl() + const $listItems = $el.find('FacetListItem') - $listItems.forEach(($li, index) => { - const item = defaultProps.items[index] - expect($li.prop('data').hits).to.equal(item.hits) - }) - }) + $listItems.forEach(($li, index) => { + const item = defaultProps.items[index] + expect($li.prop('data').hits).to.equal(item.hits) + }) + }) - it('calls `onSelectFacet` when an Item is clicked, passing the facet object', function (done) { - const idx = randomIndex(defaultProps.items) - const item = defaultProps.items[idx] + it('calls `onSelectFacet` when an Item is clicked, passing the facet object', function (done) { + const idx = randomIndex(defaultProps.items) + const item = defaultProps.items[idx] - const onSelectFacet = facet => { - expect(facet).to.deep.equal(item) - done() - } + const onSelectFacet = facet => { + expect(facet).to.deep.equal(item) + done() + } - const $el = mountEl({onSelectFacet}) - const $item = $el.find('FacetListItem').at(idx) - $item.find('.facet-label').simulate('click') - }) + const $el = mountEl({onSelectFacet}) + const $item = $el.find('FacetListItem').at(idx) + $item.find('.facet-label').simulate('click') + }) - // skipping for now because of using FacetListSelectedItem instead - describe('when provided `selectedFacets`', function () { - const items = [].concat(defaultProps.items) - const idx = randomIndex(items) - const selectedFacets = items.splice(idx, 1) + // skipping for now because of using FacetListSelectedItem instead + describe('when provided `selectedFacets`', function () { + const items = [].concat(defaultProps.items) + const idx = randomIndex(items) + const selectedFacets = items.splice(idx, 1) - it('renders a component for each item', function () { - const $el = mountEl({items, selectedFacets}) - expect($el.find('FacetListSelectedItem')).to.have.length(1) - }) + it('renders a component for each item', function () { + const $el = mountEl({items, selectedFacets}) + expect($el.find('FacetListSelectedItem')).to.have.length(1) + }) - it('triggers `onRemoveSelectedFacet` when selectedFacet X button is clicked', function (done) { - const onRemoveSelectedFacet = facet => { - expect(facet).to.deep.equal(selectedFacets[0]) - done() - } + it('triggers `onRemoveSelectedFacet` when selectedFacet X button is clicked', function (done) { + const onRemoveSelectedFacet = facet => { + expect(facet).to.deep.equal(selectedFacets[0]) + done() + } - const $el = mountEl({items, selectedFacets, onRemoveSelectedFacet}) - const $button = $el.find('FacetListSelectedItem').find('Button') - $button.simulate('click') - }) - }) + const $el = mountEl({items, selectedFacets, onRemoveSelectedFacet}) + const $button = $el.find('FacetListSelectedItem').find('Button') + $button.simulate('click') + }) + }) }) diff --git a/src/components/catalog/__tests__/FacetListItem-test.jsx b/src/components/catalog/__tests__/FacetListItem-test.jsx index 5bd24c1..05dd142 100644 --- a/src/components/catalog/__tests__/FacetListItem-test.jsx +++ b/src/components/catalog/__tests__/FacetListItem-test.jsx @@ -6,54 +6,54 @@ import randomBool from 'random-bool' import FacetListItem from '../FacetListItem.jsx' const defaultProps = { - data: { - hits: 10, - label: 'test label', - value: 'test_value', - }, - onClick: () => {}, + data: { + hits: 10, + label: 'test label', + value: 'test_value', + }, + onClick: () => {}, } const mountEl = (xtend) => { - const props = assign({}, defaultProps, xtend) - return mount(React.createElement(FacetListItem, props)) + const props = assign({}, defaultProps, xtend) + return mount(React.createElement(FacetListItem, props)) } const LABEL_SEL = '.facet-label' const COUNT_SEL = '.facet-count' describe('', function () { - it('renders a `span.facet-count` of the `hits` by default', function () { - const hits = Math.floor(Math.random() * 1000) - const data = assign({}, defaultProps.data, {hits}) - const $el = mountEl({data}) - const $span = $el.find(COUNT_SEL) - expect($span).to.have.length(1) - expect(parseInt($span.text(), 10)).to.equal(hits) - }) + it('renders a `span.facet-count` of the `hits` by default', function () { + const hits = Math.floor(Math.random() * 1000) + const data = assign({}, defaultProps.data, {hits}) + const $el = mountEl({data}) + const $span = $el.find(COUNT_SEL) + expect($span).to.have.length(1) + expect(parseInt($span.text(), 10)).to.equal(hits) + }) - it('hides the `hits` count when `hideCount` is true', function () { - const $el = mountEl({hideCount: true}) - expect($el.find('span.facet-count')).to.have.length(0) - }) + it('hides the `hits` count when `hideCount` is true', function () { + const $el = mountEl({hideCount: true}) + expect($el.find('span.facet-count')).to.have.length(0) + }) - it('calls `onClick` w/ the facet data when clicked ', function (done) { - const props = { - data: { - label: 'new test label', - value: 'hullo I am the value!', - hits: 1234, - }, - onClick: (facet) => { - const data = props.data - expect(facet.value).to.equal(data.value) - expect(facet.label).to.equal(data.label) - expect(facet.hits).to.equal(data.hits) - done() - }, - } + it('calls `onClick` w/ the facet data when clicked ', function (done) { + const props = { + data: { + label: 'new test label', + value: 'hullo I am the value!', + hits: 1234, + }, + onClick: (facet) => { + const data = props.data + expect(facet.value).to.equal(data.value) + expect(facet.label).to.equal(data.label) + expect(facet.hits).to.equal(data.hits) + done() + }, + } - const $el = mountEl(props) - $el.find(LABEL_SEL).simulate('click') - }) + const $el = mountEl(props) + $el.find(LABEL_SEL).simulate('click') + }) }) diff --git a/src/components/catalog/__tests__/FacetListWithViewMore-test.jsx b/src/components/catalog/__tests__/FacetListWithViewMore-test.jsx index 58e2767..ff40b8b 100644 --- a/src/components/catalog/__tests__/FacetListWithViewMore-test.jsx +++ b/src/components/catalog/__tests__/FacetListWithViewMore-test.jsx @@ -9,18 +9,18 @@ import data from './data/facet.json' const noop = () => {} const defaultProps = { - ...data, - onRemoveSelectedFacet: noop, - onSelectFacet: noop, - selectedFacets: [], + ...data, + onRemoveSelectedFacet: noop, + onSelectFacet: noop, + selectedFacets: [], - limit: 5, - viewMoreText: 'View more...', + limit: 5, + viewMoreText: 'View more...', } const wrap = (xtend, renderer) => { - const props = assign({}, defaultProps, xtend) - return renderer(React.createElement(FacetListWithViewMore, props)) + const props = assign({}, defaultProps, xtend) + return renderer(React.createElement(FacetListWithViewMore, props)) } const mountEl = xtend => wrap(xtend, mount) @@ -28,51 +28,51 @@ const shallowEl = xtend => wrap(xtend, shallow) const renderEl = xtend => wrap(xtend, render) describe('', function () { - it('renders the number of items established with `limit`', function () { - const limit = Math.floor(Math.random() * defaultProps.items.length) - const $el = mountEl({limit}) - expect($el.find('FacetListItem')).to.have.length(limit) - }) + it('renders the number of items established with `limit`', function () { + const limit = Math.floor(Math.random() * defaultProps.items.length) + const $el = mountEl({limit}) + expect($el.find('FacetListItem')).to.have.length(limit) + }) - describe('the `View More` span', function () { - const SEL = 'span.view-more' - it('renders if # items exceeds limit', function () { - const $el = shallowEl() - expect($el.find(SEL)).to.have.length(1) - }) + describe('the `View More` span', function () { + const SEL = 'span.view-more' + it('renders if # items exceeds limit', function () { + const $el = shallowEl() + expect($el.find(SEL)).to.have.length(1) + }) - it('does not render if # items is below or equal to limit', function () { - const $first = shallowEl({limit: defaultProps.items.length}) - expect($first.find(SEL)).to.have.length(0) + it('does not render if # items is below or equal to limit', function () { + const $first = shallowEl({limit: defaultProps.items.length}) + expect($first.find(SEL)).to.have.length(0) - const $second = shallowEl({limit: Infinity}) - expect($second.find(SEL)).to.have.length(0) - }) + const $second = shallowEl({limit: Infinity}) + expect($second.find(SEL)).to.have.length(0) + }) - it('uses text provided with `viewMoreText` prop', function () { - const viewMoreText = 'I am not View More =^_^=' - const $el = mountEl({viewMoreText}) - expect($el.find(SEL).text()).to.equal(viewMoreText) - }) + it('uses text provided with `viewMoreText` prop', function () { + const viewMoreText = 'I am not View More =^_^=' + const $el = mountEl({viewMoreText}) + expect($el.find(SEL).text()).to.equal(viewMoreText) + }) - it('opens the View More modal (w/ header) when clicked', function () { - const $el = shallowEl() + it('opens the View More modal (w/ header) when clicked', function () { + const $el = shallowEl() - expect($el.state('modalOpen')).to.be.false - expect($el.find('ModalWithHeader')).to.have.length(0) + expect($el.state('modalOpen')).to.be.false + expect($el.find('ModalWithHeader')).to.have.length(0) - $el.find(SEL).simulate('click') + $el.find(SEL).simulate('click') - expect($el.state('modalOpen')).to.be.true - expect($el.find('ModalWithHeader')).to.have.length(1) - }) - }) + expect($el.state('modalOpen')).to.be.true + expect($el.find('ModalWithHeader')).to.have.length(1) + }) + }) - // TODO: lots of headaches in trying to put the Modal component - // into an `enzyme` wrapper. - // - // see: https://github.com/reactjs/react-modal#testing - describe('the `View More` modal', function () { - xit('displays all of the facet options', function () {}) - }) + // TODO: lots of headaches in trying to put the Modal component + // into an `enzyme` wrapper. + // + // see: https://github.com/reactjs/react-modal#testing + describe('the `View More` modal', function () { + xit('displays all of the facet options', function () {}) + }) }) diff --git a/src/components/catalog/__tests__/FacetRangeLimitDate-test.jsx b/src/components/catalog/__tests__/FacetRangeLimitDate-test.jsx index 5870bf2..299fdeb 100644 --- a/src/components/catalog/__tests__/FacetRangeLimitDate-test.jsx +++ b/src/components/catalog/__tests__/FacetRangeLimitDate-test.jsx @@ -10,86 +10,86 @@ import isoDateRange from '../common/__tests__/data/iso-date-range.json' const noop = () => {} const defaultProps = { - ...isoDateRange, - onSelectFacet: noop, - onRemoveSelectedFacet: noop, - selectedFacets: [], + ...isoDateRange, + onSelectFacet: noop, + onRemoveSelectedFacet: noop, + selectedFacets: [], } const wrapper = (xtend, renderer) => { - const props = assign({}, defaultProps, xtend) - return renderer(React.createElement(FacetRangeLimitDate, props)) + const props = assign({}, defaultProps, xtend) + return renderer(React.createElement(FacetRangeLimitDate, props)) } const shallowEl = xtend => wrapper(xtend, shallow) const mountEl = xtend => wrapper(xtend, mount) describe('', function () { - it('calculates and stores min/max and hits in state', function () { - const props = {...isoDateRange} - const $el = shallowEl(props) - const {min, max, hits, items} = calculateRange(props.items, val => Date.parse(val)) - - expect($el.state('hits')).to.equal(hits) - expect($el.state('items')).to.deep.equal(items) - - expect($el.state('min')).to.not.be.undefined - expect($el.state('max')).to.not.be.undefined - }) - - it('does not render FacetListSelectedItems if not selected items', function () { - const $el = shallowEl({selectedFacets: []}) - expect($el.find('FacetListSelectedItem')).to.have.length(0) - }) - - it('will render FacetListSelectedItems if selected items are passed', function () { - const selectedFacets = [ - { - name: 'Facet name', - value: { - begin: Date.parse('1986-02-11T00:00:00Z'), - end: Date.now(), - }, - } - ] - - const $el = shallowEl({selectedFacets}) - expect($el.find('FacetListSelectedItem')).to.have.length(selectedFacets.length) - }) - - describe('the `onSelectFacet` callback', function () { - it('is called when triggered via `RangeSliderDate`', function (done) { - const onSelectFacet = () => { done() } - const $el = mountEl({onSelectFacet}) - const $btn = $el.find('Button') - $btn.simulate('click') - }) - - it('is sent a facet object when called', function (done) { - const onSelectFacet = facet => { - expect(facet).to.have.property('name') - expect(facet.name).to.be.a.string - expect(facet.name).to.equal(defaultProps.name) - - expect(facet).to.have.property('label') - expect(facet.label).to.be.a.string - - expect(facet).to.have.property('value') - expect(facet.value).to.be.an.object - expect(facet.value).to.have.property('begin') - expect(facet.value.begin).to.be.a.number - expect(facet.value).to.have.property('end') - expect(facet.value.end).to.be.a.number - - expect(facet).to.have.property('type') - expect(facet.type).to.equal('range') - - done() - } - - const $el = mountEl({onSelectFacet}) - const $btn = $el.find('Button') - $btn.simulate('click') - }) - }) + it('calculates and stores min/max and hits in state', function () { + const props = {...isoDateRange} + const $el = shallowEl(props) + const {min, max, hits, items} = calculateRange(props.items, val => Date.parse(val)) + + expect($el.state('hits')).to.equal(hits) + expect($el.state('items')).to.deep.equal(items) + + expect($el.state('min')).to.not.be.undefined + expect($el.state('max')).to.not.be.undefined + }) + + it('does not render FacetListSelectedItems if not selected items', function () { + const $el = shallowEl({selectedFacets: []}) + expect($el.find('FacetListSelectedItem')).to.have.length(0) + }) + + it('will render FacetListSelectedItems if selected items are passed', function () { + const selectedFacets = [ + { + name: 'Facet name', + value: { + begin: Date.parse('1986-02-11T00:00:00Z'), + end: Date.now(), + }, + } + ] + + const $el = shallowEl({selectedFacets}) + expect($el.find('FacetListSelectedItem')).to.have.length(selectedFacets.length) + }) + + describe('the `onSelectFacet` callback', function () { + it('is called when triggered via `RangeSliderDate`', function (done) { + const onSelectFacet = () => { done() } + const $el = mountEl({onSelectFacet}) + const $btn = $el.find('Button') + $btn.simulate('click') + }) + + it('is sent a facet object when called', function (done) { + const onSelectFacet = facet => { + expect(facet).to.have.property('name') + expect(facet.name).to.be.a.string + expect(facet.name).to.equal(defaultProps.name) + + expect(facet).to.have.property('label') + expect(facet.label).to.be.a.string + + expect(facet).to.have.property('value') + expect(facet.value).to.be.an.object + expect(facet.value).to.have.property('begin') + expect(facet.value.begin).to.be.a.number + expect(facet.value).to.have.property('end') + expect(facet.value.end).to.be.a.number + + expect(facet).to.have.property('type') + expect(facet.type).to.equal('range') + + done() + } + + const $el = mountEl({onSelectFacet}) + const $btn = $el.find('Button') + $btn.simulate('click') + }) + }) }) diff --git a/src/components/catalog/__tests__/RangeSliderDate-test.jsx b/src/components/catalog/__tests__/RangeSliderDate-test.jsx index 60486a0..e8bc14a 100644 --- a/src/components/catalog/__tests__/RangeSliderDate-test.jsx +++ b/src/components/catalog/__tests__/RangeSliderDate-test.jsx @@ -6,181 +6,181 @@ import RangeSliderDate from '../RangeSliderDate.jsx' import { INTERVALS } from '../common/date-intervals' const wrapper = (xtend, renderer) => { - const props = assign({}, { - min: Date.parse('1986-02-11T00:00:00Z'), - max: Date.parse('2016-11-06T00:00:00Z'), - onApplyRange: () => {}, - }, xtend) + const props = assign({}, { + min: Date.parse('1986-02-11T00:00:00Z'), + max: Date.parse('2016-11-06T00:00:00Z'), + onApplyRange: () => {}, + }, xtend) - return renderer(React.createElement(RangeSliderDate, props)) + return renderer(React.createElement(RangeSliderDate, props)) } const shallowEl = xtend => wrapper(xtend, shallow) describe('', function () { - describe('when interval="month"', function () { - const sel = 'input[type="month"]' - let $el - - beforeEach(function () { - $el = shallowEl({interval: INTERVALS.MONTH }) - }) - - it('renders inputs with `type="month"', function () { - const $els = $el.find(sel) - - expect($els).to.have.length(2) - }) - - it('parses value to YYYY-MM', function () { - const $els = $el.find(sel) - - $els.forEach($$el => { - expect($$el.prop('value')).to.match(/^\d{4}-\d{2}$/) - }) - }) - }) - - describe('when interval="day"', function () { - const sel = 'input[type="date"]' - let $el - - beforeEach(function () { - $el = shallowEl({interval: INTERVALS.DAY}) - }) - - it('renders inputs with `type="date"`', function () { - const $els = $el.find(sel) - expect($els).to.have.length(2) - }) - - it('parses value to YYYY-MM-DD', function () { - const $els = $el.find(sel) - $els.forEach($$el => { - expect($$el.prop('value')).to.match(/^\d{4}-\d{2}-\d{2}$/) - }) - }) - }) - - describe('when interval="year"', function () { - const sel = 'input[type="number"]' - let $el - - beforeEach(function () { - $el = shallowEl({interval: INTERVALS.YEAR}) - }) - - it('renders inputs with `type="number"`', function () { - const $els = $el.find(sel) - expect($els).to.have.length(2) - }) - - it('parses value to YYYY', function () { - const $els = $el.find(sel) - $els.forEach($$el => { - expect($$el.prop('value')).to.match(/^\d{4}$/) - }) - }) - }) - - describe('the `onApplyRange` handler', function () { - it('is called with an array of 2 numbers', function (done) { - const onApplyRange = vals => { - expect(Array.isArray(vals)).to.be.true - expect(vals).to.have.length(2) - - vals.map(v => expect(v).to.be.a.number) - - done() - } - - const $el = shallowEl({onApplyRange}) - const $button = $el.find('Button') - - $button.simulate('click') - }) - - it('is called with rounded date values', function (done) { - const min = Date.parse('1986-02-11T11:11:00Z') - const max = Date.parse('2016-11-06T15:49:00Z') - - const dates = [ - new Date(min), - new Date(max), - ] - - const onApplyRange = vals => { - const fns = [ - 'getUTCFullYear', 'getUTCMonth', 'getUTCDate' - ] - - let i = 0 - let current, comp - - // throw if there's a problem - expect(vals).to.have.length(dates.length) - - for (; i < vals.length; i++) { - current = new Date(vals[i]) - comp = dates[i] - - expect(current.toISOString()).to.not.equal(comp.toISOString()) + describe('when interval="month"', function () { + const sel = 'input[type="month"]' + let $el + + beforeEach(function () { + $el = shallowEl({interval: INTERVALS.MONTH }) + }) + + it('renders inputs with `type="month"', function () { + const $els = $el.find(sel) + + expect($els).to.have.length(2) + }) + + it('parses value to YYYY-MM', function () { + const $els = $el.find(sel) + + $els.forEach($$el => { + expect($$el.prop('value')).to.match(/^\d{4}-\d{2}$/) + }) + }) + }) + + describe('when interval="day"', function () { + const sel = 'input[type="date"]' + let $el + + beforeEach(function () { + $el = shallowEl({interval: INTERVALS.DAY}) + }) + + it('renders inputs with `type="date"`', function () { + const $els = $el.find(sel) + expect($els).to.have.length(2) + }) + + it('parses value to YYYY-MM-DD', function () { + const $els = $el.find(sel) + $els.forEach($$el => { + expect($$el.prop('value')).to.match(/^\d{4}-\d{2}-\d{2}$/) + }) + }) + }) + + describe('when interval="year"', function () { + const sel = 'input[type="number"]' + let $el + + beforeEach(function () { + $el = shallowEl({interval: INTERVALS.YEAR}) + }) + + it('renders inputs with `type="number"`', function () { + const $els = $el.find(sel) + expect($els).to.have.length(2) + }) + + it('parses value to YYYY', function () { + const $els = $el.find(sel) + $els.forEach($$el => { + expect($$el.prop('value')).to.match(/^\d{4}$/) + }) + }) + }) + + describe('the `onApplyRange` handler', function () { + it('is called with an array of 2 numbers', function (done) { + const onApplyRange = vals => { + expect(Array.isArray(vals)).to.be.true + expect(vals).to.have.length(2) + + vals.map(v => expect(v).to.be.a.number) + + done() + } + + const $el = shallowEl({onApplyRange}) + const $button = $el.find('Button') + + $button.simulate('click') + }) + + it('is called with rounded date values', function (done) { + const min = Date.parse('1986-02-11T11:11:00Z') + const max = Date.parse('2016-11-06T15:49:00Z') + + const dates = [ + new Date(min), + new Date(max), + ] + + const onApplyRange = vals => { + const fns = [ + 'getUTCFullYear', 'getUTCMonth', 'getUTCDate' + ] + + let i = 0 + let current, comp + + // throw if there's a problem + expect(vals).to.have.length(dates.length) + + for (; i < vals.length; i++) { + current = new Date(vals[i]) + comp = dates[i] + + expect(current.toISOString()).to.not.equal(comp.toISOString()) - fns.forEach(fn => { - expect(current[fn]()).to.equal(comp[fn]()) - }) - } - - done() - } - - const $el = shallowEl({min, max, onApplyRange}) - const $button = $el.find('Button') - $button.simulate('click') - }) - }) - - describe('the `min` input', function () { - it('changes the value of `state.min`', function () { - const minValue = '1999-12-31' - const split = minValue.split('-').map(Number) - split[1] = split[1] - 1 - - const minTs = Date.UTC.apply(Date, split) - const $el = shallowEl({interval: INTERVALS.DAY}) + fns.forEach(fn => { + expect(current[fn]()).to.equal(comp[fn]()) + }) + } + + done() + } + + const $el = shallowEl({min, max, onApplyRange}) + const $button = $el.find('Button') + $button.simulate('click') + }) + }) + + describe('the `min` input', function () { + it('changes the value of `state.min`', function () { + const minValue = '1999-12-31' + const split = minValue.split('-').map(Number) + split[1] = split[1] - 1 + + const minTs = Date.UTC.apply(Date, split) + const $el = shallowEl({interval: INTERVALS.DAY}) - const $min = $el.find('input[type="date"]').filterWhere(el => ( - el.key() === 'input-min' - )) + const $min = $el.find('input[type="date"]').filterWhere(el => ( + el.key() === 'input-min' + )) - expect($min).to.have.length(1) - - $min.simulate('change', {target: {value: minValue}}) + expect($min).to.have.length(1) + + $min.simulate('change', {target: {value: minValue}}) - const minState = $el.state('min') - expect(minState).to.equal(minTs) - }) - }) + const minState = $el.state('min') + expect(minState).to.equal(minTs) + }) + }) - describe('the `max` input', function () { - it('changes the value of `state.max`', function () { - const maxValue = '2017-01-01' - const split = maxValue.split('-').map(Number) - split[1] = split[1] - 1 + describe('the `max` input', function () { + it('changes the value of `state.max`', function () { + const maxValue = '2017-01-01' + const split = maxValue.split('-').map(Number) + split[1] = split[1] - 1 - const maxTs = Date.UTC.apply(Date, split) - const $el = shallowEl({interval: INTERVALS.DAY}) + const maxTs = Date.UTC.apply(Date, split) + const $el = shallowEl({interval: INTERVALS.DAY}) - const $max = $el.find('input[type="date"]').filterWhere(el => ( - el.key() === 'input-max' - )) + const $max = $el.find('input[type="date"]').filterWhere(el => ( + el.key() === 'input-max' + )) - expect($max).to.have.length(1) + expect($max).to.have.length(1) - $max.simulate('change', {target: {value: maxValue}}) + $max.simulate('change', {target: {value: maxValue}}) - const maxState = $el.state('max') - expect(maxState).to.equal(maxTs) - }) - }) + const maxState = $el.state('max') + expect(maxState).to.equal(maxTs) + }) + }) }) diff --git a/src/components/catalog/__tests__/ResultsGallery-test.jsx b/src/components/catalog/__tests__/ResultsGallery-test.jsx index 07f595e..a41916a 100644 --- a/src/components/catalog/__tests__/ResultsGallery-test.jsx +++ b/src/components/catalog/__tests__/ResultsGallery-test.jsx @@ -6,10 +6,10 @@ import ResultsGallery from '../ResultsGallery.jsx' import docs from './data/docs.json' describe('', function () { - it('renders a ResultsGalleryItem for each result item', function () { - const $el = shallow() - const $items = $el.find('ResultsGalleryItem') + it('renders a ResultsGalleryItem for each result item', function () { + const $el = shallow() + const $items = $el.find('ResultsGalleryItem') - expect($items).to.have.length(docs.length) - }) + expect($items).to.have.length(docs.length) + }) }) diff --git a/src/components/catalog/__tests__/ResultsGalleryItem-test.jsx b/src/components/catalog/__tests__/ResultsGalleryItem-test.jsx index d00b2fa..24746a3 100644 --- a/src/components/catalog/__tests__/ResultsGalleryItem-test.jsx +++ b/src/components/catalog/__tests__/ResultsGalleryItem-test.jsx @@ -8,39 +8,39 @@ import ResultsGalleryItem from '../ResultsGalleryItem.jsx' import docs from './data/docs.json' const wrap = props => { - return shallow(React.createElement(ResultsGalleryItem, props)) + return shallow(React.createElement(ResultsGalleryItem, props)) } describe('ResultsGalleryItem', function () { - it('uses classNames for each piece', function () { - const idx = randomIndex(docs) - const data = docs[idx] - const $el = wrap({data}) - - const classNames = [ - 'search-results-gallery--item', - 'search-results-gallery--thumbnail', - 'search-results-gallery--caption', - ] - - classNames.forEach(function (cn) { - expect($el.find(`.${cn}`)).to.have.length(1) - }) - }) - - describe('the link to the work', function () { - const idx = randomIndex(docs) - const data = docs[idx] - - const $el = wrap({data}) - const $link = $el.find('Link') - - it('has been generated', function () { - expect($link).to.have.length(1) - }) - - it('uses the `id` prop for a location', function () { - expect($link.prop('to')).to.contain(data.id) - }) - }) + it('uses classNames for each piece', function () { + const idx = randomIndex(docs) + const data = docs[idx] + const $el = wrap({data}) + + const classNames = [ + 'search-results-gallery--item', + 'search-results-gallery--thumbnail', + 'search-results-gallery--caption', + ] + + classNames.forEach(function (cn) { + expect($el.find(`.${cn}`)).to.have.length(1) + }) + }) + + describe('the link to the work', function () { + const idx = randomIndex(docs) + const data = docs[idx] + + const $el = wrap({data}) + const $link = $el.find('Link') + + it('has been generated', function () { + expect($link).to.have.length(1) + }) + + it('uses the `id` prop for a location', function () { + expect($link.prop('to')).to.contain(data.id) + }) + }) }) diff --git a/src/components/catalog/__tests__/ResultsPager-test.jsx b/src/components/catalog/__tests__/ResultsPager-test.jsx index 2a2d1c4..258483b 100644 --- a/src/components/catalog/__tests__/ResultsPager-test.jsx +++ b/src/components/catalog/__tests__/ResultsPager-test.jsx @@ -10,103 +10,103 @@ import data from './data/pages.json' const noop = () => {} const defaultProps = { - ...data, - onNextClick: noop, - onPreviousClick: noop, + ...data, + onNextClick: noop, + onPreviousClick: noop, } const wrap = (xtend, renderer) => { - const props = assign({}, defaultProps, xtend) - return renderer(React.createElement(ResultsPager, props)) + const props = assign({}, defaultProps, xtend) + return renderer(React.createElement(ResultsPager, props)) } const shallowEl = xtend => wrap(xtend, shallow) const mountEl = xtend => wrap(xtend, mount) describe('', function () { - it('renders next button as disabled when `next_page` is null', function () { - const $el = shallowEl({ - next_page: null, - prev_page: 1, - }) - - expect($el.find('button').last().prop('disabled')).to.be.true - }) - - it('renders previous button as disabled when `prev_page` is null', function () { - const $el = shallowEl({ - next_page: 2, - prev_page: null, - }) - - expect($el.find('button').first().prop('disabled')).to.be.true - }) - - it('triggers `onNextClick` when next button is clicked', function (done) { - const onNextClick = done - const $el = shallowEl({ - onNextClick, - next_page: 3, - prev_page: 1, - }) - - const $next = $el.find('button').last() - $next.simulate('click', {preventDefault: noop}) - }) - - it('triggers `onPreviousClick` when the previous button is clicked', function (done) { - const onPreviousClick = done - const $el = shallowEl({ - onPreviousClick, - next_page: 3, - prev_page: 1, - }) - - const $prev = $el.find('button').first() - $prev.simulate('click', {preventDefault: noop}) - }) - - it('uses text passed to `nextText` in next button', function () { - const nextText = 'HAI NEXT PLZ ^_^' - - const $el = shallowEl({nextText}) - - expect($el.find('button').last().text()).to.equal(nextText) - }) - - it('uses text passed to `previousText` in previous button', function () { - const previousText = 'Previously... on Search Results' - const $el = shallowEl({previousText}) - - expect($el.find('button').first().text()).to.equal(previousText) - }) - - describe('props.message', function () { - it('allows a string template to be passed', function () { - const { limit_value, offset_value, total_count } = defaultProps - const first = commafy(offset_value + 1) - const last = commafy(offset_value + limit_value) - const total = commafy(total_count) - - const message = 'first: %s, last: %s, total: %s' - const res = `first: ${first}, last: ${last}, total: ${total}` - - const $el = shallowEl({message}) - - expect($el.find('span').html()).to.contain(res) - }) - - it('allows a function to generate a message', function () { - const { limit_value, offset_value, total_count } = defaultProps - const message = function (first, last, total) { - expect(arguments).to.have.length(3) - return `start: ${first}, end: ${last}, total: ${total}` - } - - const res = message(offset_value + 1, offset_value + limit_value, total_count) - const $el = shallowEl({message}) - - expect($el.find('span').html()).to.contain(res) - }) - }) + it('renders next button as disabled when `next_page` is null', function () { + const $el = shallowEl({ + next_page: null, + prev_page: 1, + }) + + expect($el.find('button').last().prop('disabled')).to.be.true + }) + + it('renders previous button as disabled when `prev_page` is null', function () { + const $el = shallowEl({ + next_page: 2, + prev_page: null, + }) + + expect($el.find('button').first().prop('disabled')).to.be.true + }) + + it('triggers `onNextClick` when next button is clicked', function (done) { + const onNextClick = done + const $el = shallowEl({ + onNextClick, + next_page: 3, + prev_page: 1, + }) + + const $next = $el.find('button').last() + $next.simulate('click', {preventDefault: noop}) + }) + + it('triggers `onPreviousClick` when the previous button is clicked', function (done) { + const onPreviousClick = done + const $el = shallowEl({ + onPreviousClick, + next_page: 3, + prev_page: 1, + }) + + const $prev = $el.find('button').first() + $prev.simulate('click', {preventDefault: noop}) + }) + + it('uses text passed to `nextText` in next button', function () { + const nextText = 'HAI NEXT PLZ ^_^' + + const $el = shallowEl({nextText}) + + expect($el.find('button').last().text()).to.equal(nextText) + }) + + it('uses text passed to `previousText` in previous button', function () { + const previousText = 'Previously... on Search Results' + const $el = shallowEl({previousText}) + + expect($el.find('button').first().text()).to.equal(previousText) + }) + + describe('props.message', function () { + it('allows a string template to be passed', function () { + const { limit_value, offset_value, total_count } = defaultProps + const first = commafy(offset_value + 1) + const last = commafy(offset_value + limit_value) + const total = commafy(total_count) + + const message = 'first: %s, last: %s, total: %s' + const res = `first: ${first}, last: ${last}, total: ${total}` + + const $el = shallowEl({message}) + + expect($el.find('span').html()).to.contain(res) + }) + + it('allows a function to generate a message', function () { + const { limit_value, offset_value, total_count } = defaultProps + const message = function (first, last, total) { + expect(arguments).to.have.length(3) + return `start: ${first}, end: ${last}, total: ${total}` + } + + const res = message(offset_value + 1, offset_value + limit_value, total_count) + const $el = shallowEl({message}) + + expect($el.find('span').html()).to.contain(res) + }) + }) }) diff --git a/src/components/catalog/__tests__/data/docs.json b/src/components/catalog/__tests__/data/docs.json index fc93a81..5172e81 100644 --- a/src/components/catalog/__tests__/data/docs.json +++ b/src/components/catalog/__tests__/data/docs.json @@ -1,803 +1,803 @@ [ { - "id": "ng451h49f", - "title": [ - "[ip0010] Shumpozan-Hakubunji Temple, Keijo." - ], - "creator": [], - "creator_photographer": null, - "format_medium": null, - "format_size": null, - "date_approximate": null, - "date_range": null, - "creator_maker": null, - "date_original_display": null, - "description_size": null, - "description_note": null, - "subject_lcsh": null, - "publisher_original": null, - "date_original": null, - "format_extent": null, - "description_condition": null, - "description_provenance": null, - "description_series": null, - "identifier_itemnumber": null, - "publisher_digital": null, - "format_digital": null, - "rights_digital": null, - "subject_ocm": null, - "description_critical": null, - "description_indicia": null, - "description_text": null, - "description_inscription": null, - "description_ethnicity": null, - "description_citation": null, - "coverage_location_country": null, - "coverage_location": null, - "creator_company": null, - "relation_seealso": null, - "date_image_upper": null, - "date_image_lower": null, - "title_name": null, - "description_class": null, - "date_birth_display": null, - "coverage_place_birth": null, - "description_military_branch": null, - "description_military_rank": null, - "description_military_unit": null, - "date_death_display": null, - "coverage_place_death": null, - "description_cause_death": null, - "description_honors": null, - "type": "Work", - "contributor": [ - "Ikegami Naoko", - "Seo-Hyun Park" - ], - "description": [], - "keyword": [], - "rights": [ - "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." - ], - "publisher": [ - "Special Collections & College Archives, Skillman Library, Lafayette College" - ], - "date_created": [], - "subject": [], - "language": [], - "identifier": null, - "based_near": null, - "related_url": null, - "bibliographic_citation": null, - "source": [], - "thumbnail_path": "/downloads/bc386j21m?file=thumbnail", - "date_artifact_upper": "1945-08-15T00:00:00Z", - "date_artifact_lower": "1933-02-15T00:00:00Z", - "uses_vocabulary": [], - "form": [], - "score": 1 + "id": "ng451h49f", + "title": [ + "[ip0010] Shumpozan-Hakubunji Temple, Keijo." + ], + "creator": [], + "creator_photographer": null, + "format_medium": null, + "format_size": null, + "date_approximate": null, + "date_range": null, + "creator_maker": null, + "date_original_display": null, + "description_size": null, + "description_note": null, + "subject_lcsh": null, + "publisher_original": null, + "date_original": null, + "format_extent": null, + "description_condition": null, + "description_provenance": null, + "description_series": null, + "identifier_itemnumber": null, + "publisher_digital": null, + "format_digital": null, + "rights_digital": null, + "subject_ocm": null, + "description_critical": null, + "description_indicia": null, + "description_text": null, + "description_inscription": null, + "description_ethnicity": null, + "description_citation": null, + "coverage_location_country": null, + "coverage_location": null, + "creator_company": null, + "relation_seealso": null, + "date_image_upper": null, + "date_image_lower": null, + "title_name": null, + "description_class": null, + "date_birth_display": null, + "coverage_place_birth": null, + "description_military_branch": null, + "description_military_rank": null, + "description_military_unit": null, + "date_death_display": null, + "coverage_place_death": null, + "description_cause_death": null, + "description_honors": null, + "type": "Work", + "contributor": [ + "Ikegami Naoko", + "Seo-Hyun Park" + ], + "description": [], + "keyword": [], + "rights": [ + "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." + ], + "publisher": [ + "Special Collections & College Archives, Skillman Library, Lafayette College" + ], + "date_created": [], + "subject": [], + "language": [], + "identifier": null, + "based_near": null, + "related_url": null, + "bibliographic_citation": null, + "source": [], + "thumbnail_path": "/downloads/bc386j21m?file=thumbnail", + "date_artifact_upper": "1945-08-15T00:00:00Z", + "date_artifact_lower": "1933-02-15T00:00:00Z", + "uses_vocabulary": [], + "form": [], + "score": 1 }, { - "id": "rb68xb87r", - "title": [ - "[ip0009] A Strong Porter Carrying Heavy Pot" - ], - "creator": [], - "creator_photographer": null, - "format_medium": null, - "format_size": null, - "date_approximate": null, - "date_range": null, - "creator_maker": null, - "date_original_display": null, - "description_size": null, - "description_note": null, - "subject_lcsh": null, - "publisher_original": null, - "date_original": null, - "format_extent": null, - "description_condition": null, - "description_provenance": null, - "description_series": null, - "identifier_itemnumber": null, - "publisher_digital": null, - "format_digital": null, - "rights_digital": null, - "subject_ocm": null, - "description_critical": null, - "description_indicia": null, - "description_text": null, - "description_inscription": null, - "description_ethnicity": null, - "description_citation": null, - "coverage_location_country": null, - "coverage_location": null, - "creator_company": null, - "relation_seealso": null, - "date_image_upper": null, - "date_image_lower": null, - "title_name": null, - "description_class": null, - "date_birth_display": null, - "coverage_place_birth": null, - "description_military_branch": null, - "description_military_rank": null, - "description_military_unit": null, - "date_death_display": null, - "coverage_place_death": null, - "description_cause_death": null, - "description_honors": null, - "type": "Work", - "contributor": [ - "Seo-Hyun Park", - "Ikegami Naoko" - ], - "description": [ - "Description of the postcard" - ], - "keyword": [], - "rights": [ - "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." - ], - "publisher": [ - "Special Collections & College Archives, Skillman Library, Lafayette College" - ], - "date_created": [], - "subject": [], - "language": [], - "identifier": null, - "based_near": null, - "related_url": null, - "bibliographic_citation": null, - "source": [], - "thumbnail_path": "/downloads/rb68xb86g?file=thumbnail", - "date_artifact_upper": "1933-02-14T00:00:00Z", - "date_artifact_lower": "1918-03-01T00:00:00Z", - "uses_vocabulary": [], - "form": [], - "score": 1 + "id": "rb68xb87r", + "title": [ + "[ip0009] A Strong Porter Carrying Heavy Pot" + ], + "creator": [], + "creator_photographer": null, + "format_medium": null, + "format_size": null, + "date_approximate": null, + "date_range": null, + "creator_maker": null, + "date_original_display": null, + "description_size": null, + "description_note": null, + "subject_lcsh": null, + "publisher_original": null, + "date_original": null, + "format_extent": null, + "description_condition": null, + "description_provenance": null, + "description_series": null, + "identifier_itemnumber": null, + "publisher_digital": null, + "format_digital": null, + "rights_digital": null, + "subject_ocm": null, + "description_critical": null, + "description_indicia": null, + "description_text": null, + "description_inscription": null, + "description_ethnicity": null, + "description_citation": null, + "coverage_location_country": null, + "coverage_location": null, + "creator_company": null, + "relation_seealso": null, + "date_image_upper": null, + "date_image_lower": null, + "title_name": null, + "description_class": null, + "date_birth_display": null, + "coverage_place_birth": null, + "description_military_branch": null, + "description_military_rank": null, + "description_military_unit": null, + "date_death_display": null, + "coverage_place_death": null, + "description_cause_death": null, + "description_honors": null, + "type": "Work", + "contributor": [ + "Seo-Hyun Park", + "Ikegami Naoko" + ], + "description": [ + "Description of the postcard" + ], + "keyword": [], + "rights": [ + "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." + ], + "publisher": [ + "Special Collections & College Archives, Skillman Library, Lafayette College" + ], + "date_created": [], + "subject": [], + "language": [], + "identifier": null, + "based_near": null, + "related_url": null, + "bibliographic_citation": null, + "source": [], + "thumbnail_path": "/downloads/rb68xb86g?file=thumbnail", + "date_artifact_upper": "1933-02-14T00:00:00Z", + "date_artifact_lower": "1918-03-01T00:00:00Z", + "uses_vocabulary": [], + "form": [], + "score": 1 }, { - "id": "1g05fb61q", - "title": [ - "[ip0008] An Old Potter" - ], - "creator": [], - "creator_photographer": null, - "format_medium": null, - "format_size": null, - "date_approximate": null, - "date_range": null, - "creator_maker": null, - "date_original_display": null, - "description_size": null, - "description_note": null, - "subject_lcsh": null, - "publisher_original": null, - "date_original": null, - "format_extent": null, - "description_condition": null, - "description_provenance": null, - "description_series": null, - "identifier_itemnumber": null, - "publisher_digital": null, - "format_digital": null, - "rights_digital": null, - "subject_ocm": null, - "description_critical": null, - "description_indicia": null, - "description_text": null, - "description_inscription": null, - "description_ethnicity": null, - "description_citation": null, - "coverage_location_country": null, - "coverage_location": null, - "creator_company": null, - "relation_seealso": null, - "date_image_upper": null, - "date_image_lower": null, - "title_name": null, - "description_class": null, - "date_birth_display": null, - "coverage_place_birth": null, - "description_military_branch": null, - "description_military_rank": null, - "description_military_unit": null, - "date_death_display": null, - "coverage_place_death": null, - "description_cause_death": null, - "description_honors": null, - "type": "Work", - "contributor": [ - "Ikegami Naoko", - "Seo-Hyun Park" - ], - "description": [ - "馬の尾で作られる高価な冠を被り、清楚な白衣を着て働くこの老陶工に見る朝鮮の昔ながらの風俗は、上品ではあるが余りにも静的でないか。" - ], - "keyword": [], - "rights": [ - "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." - ], - "publisher": [ - "Special Collections & College Archives, Skillman Library, Lafayette College" - ], - "date_created": [], - "subject": [], - "language": [], - "identifier": null, - "based_near": null, - "related_url": null, - "bibliographic_citation": null, - "source": [], - "thumbnail_path": "/downloads/3n203z084?file=thumbnail", - "date_artifact_upper": "1933-02-14T00:00:00Z", - "date_artifact_lower": "1918-03-01T00:00:00Z", - "uses_vocabulary": [], - "form": [], - "score": 1 + "id": "1g05fb61q", + "title": [ + "[ip0008] An Old Potter" + ], + "creator": [], + "creator_photographer": null, + "format_medium": null, + "format_size": null, + "date_approximate": null, + "date_range": null, + "creator_maker": null, + "date_original_display": null, + "description_size": null, + "description_note": null, + "subject_lcsh": null, + "publisher_original": null, + "date_original": null, + "format_extent": null, + "description_condition": null, + "description_provenance": null, + "description_series": null, + "identifier_itemnumber": null, + "publisher_digital": null, + "format_digital": null, + "rights_digital": null, + "subject_ocm": null, + "description_critical": null, + "description_indicia": null, + "description_text": null, + "description_inscription": null, + "description_ethnicity": null, + "description_citation": null, + "coverage_location_country": null, + "coverage_location": null, + "creator_company": null, + "relation_seealso": null, + "date_image_upper": null, + "date_image_lower": null, + "title_name": null, + "description_class": null, + "date_birth_display": null, + "coverage_place_birth": null, + "description_military_branch": null, + "description_military_rank": null, + "description_military_unit": null, + "date_death_display": null, + "coverage_place_death": null, + "description_cause_death": null, + "description_honors": null, + "type": "Work", + "contributor": [ + "Ikegami Naoko", + "Seo-Hyun Park" + ], + "description": [ + "馬の尾で作られる高価な冠を被り、清楚な白衣を着て働くこの老陶工に見る朝鮮の昔ながらの風俗は、上品ではあるが余りにも静的でないか。" + ], + "keyword": [], + "rights": [ + "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." + ], + "publisher": [ + "Special Collections & College Archives, Skillman Library, Lafayette College" + ], + "date_created": [], + "subject": [], + "language": [], + "identifier": null, + "based_near": null, + "related_url": null, + "bibliographic_citation": null, + "source": [], + "thumbnail_path": "/downloads/3n203z084?file=thumbnail", + "date_artifact_upper": "1933-02-14T00:00:00Z", + "date_artifact_lower": "1918-03-01T00:00:00Z", + "uses_vocabulary": [], + "form": [], + "score": 1 }, { - "id": "q524jn76v", - "title": [ - "[ip0007] [Making Hats that all Married Men Must Wear]" - ], - "creator": [], - "creator_photographer": null, - "format_medium": null, - "format_size": null, - "date_approximate": null, - "date_range": null, - "creator_maker": null, - "date_original_display": null, - "description_size": null, - "description_note": null, - "subject_lcsh": null, - "publisher_original": null, - "date_original": null, - "format_extent": null, - "description_condition": null, - "description_provenance": null, - "description_series": null, - "identifier_itemnumber": null, - "publisher_digital": null, - "format_digital": null, - "rights_digital": null, - "subject_ocm": null, - "description_critical": null, - "description_indicia": null, - "description_text": null, - "description_inscription": null, - "description_ethnicity": null, - "description_citation": null, - "coverage_location_country": null, - "coverage_location": null, - "creator_company": null, - "relation_seealso": null, - "date_image_upper": null, - "date_image_lower": null, - "title_name": null, - "description_class": null, - "date_birth_display": null, - "coverage_place_birth": null, - "description_military_branch": null, - "description_military_rank": null, - "description_military_unit": null, - "date_death_display": null, - "coverage_place_death": null, - "description_cause_death": null, - "description_honors": null, - "type": "Work", - "contributor": [ - "Seo-Hyun Park", - "Ikegami Naoko" - ], - "description": [ - "既婚男子に無くてはならぬ冠帽の製作" - ], - "keyword": [], - "rights": [ - "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." - ], - "publisher": [ - "Special Collections & College Archives, Skillman Library, Lafayette College" - ], - "date_created": [], - "subject": [], - "language": [], - "identifier": null, - "based_near": null, - "related_url": null, - "bibliographic_citation": null, - "source": [], - "thumbnail_path": "/downloads/f4752g73w?file=thumbnail", - "date_artifact_upper": "1933-02-14T00:00:00Z", - "date_artifact_lower": "1918-03-01T00:00:00Z", - "uses_vocabulary": [], - "form": [], - "score": 1 + "id": "q524jn76v", + "title": [ + "[ip0007] [Making Hats that all Married Men Must Wear]" + ], + "creator": [], + "creator_photographer": null, + "format_medium": null, + "format_size": null, + "date_approximate": null, + "date_range": null, + "creator_maker": null, + "date_original_display": null, + "description_size": null, + "description_note": null, + "subject_lcsh": null, + "publisher_original": null, + "date_original": null, + "format_extent": null, + "description_condition": null, + "description_provenance": null, + "description_series": null, + "identifier_itemnumber": null, + "publisher_digital": null, + "format_digital": null, + "rights_digital": null, + "subject_ocm": null, + "description_critical": null, + "description_indicia": null, + "description_text": null, + "description_inscription": null, + "description_ethnicity": null, + "description_citation": null, + "coverage_location_country": null, + "coverage_location": null, + "creator_company": null, + "relation_seealso": null, + "date_image_upper": null, + "date_image_lower": null, + "title_name": null, + "description_class": null, + "date_birth_display": null, + "coverage_place_birth": null, + "description_military_branch": null, + "description_military_rank": null, + "description_military_unit": null, + "date_death_display": null, + "coverage_place_death": null, + "description_cause_death": null, + "description_honors": null, + "type": "Work", + "contributor": [ + "Seo-Hyun Park", + "Ikegami Naoko" + ], + "description": [ + "既婚男子に無くてはならぬ冠帽の製作" + ], + "keyword": [], + "rights": [ + "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." + ], + "publisher": [ + "Special Collections & College Archives, Skillman Library, Lafayette College" + ], + "date_created": [], + "subject": [], + "language": [], + "identifier": null, + "based_near": null, + "related_url": null, + "bibliographic_citation": null, + "source": [], + "thumbnail_path": "/downloads/f4752g73w?file=thumbnail", + "date_artifact_upper": "1933-02-14T00:00:00Z", + "date_artifact_lower": "1918-03-01T00:00:00Z", + "uses_vocabulary": [], + "form": [], + "score": 1 }, { - "id": "9s1616164", - "title": [ - "[ip0006] [Glamorous and Bold Sword-Dancing Kisaeng]" - ], - "creator": [], - "creator_photographer": null, - "format_medium": null, - "format_size": null, - "date_approximate": null, - "date_range": null, - "creator_maker": null, - "date_original_display": null, - "description_size": null, - "description_note": null, - "subject_lcsh": null, - "publisher_original": null, - "date_original": null, - "format_extent": null, - "description_condition": null, - "description_provenance": null, - "description_series": null, - "identifier_itemnumber": null, - "publisher_digital": null, - "format_digital": null, - "rights_digital": null, - "subject_ocm": null, - "description_critical": null, - "description_indicia": null, - "description_text": null, - "description_inscription": null, - "description_ethnicity": null, - "description_citation": null, - "coverage_location_country": null, - "coverage_location": null, - "creator_company": null, - "relation_seealso": null, - "date_image_upper": null, - "date_image_lower": null, - "title_name": null, - "description_class": null, - "date_birth_display": null, - "coverage_place_birth": null, - "description_military_branch": null, - "description_military_rank": null, - "description_military_unit": null, - "date_death_display": null, - "coverage_place_death": null, - "description_cause_death": null, - "description_honors": null, - "type": "Work", - "contributor": [ - "Seo-Hyun Park", - "Ikegami Naoko", - "Michaela Kelly" - ], - "description": [ - "あだやかに勇ましき剣舞を舞はんとする妓生達" - ], - "keyword": [], - "rights": [ - "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." - ], - "publisher": [ - "Special Collections & College Archives, Skillman Library, Lafayette College" - ], - "date_created": [], - "subject": [], - "language": [], - "identifier": null, - "based_near": null, - "related_url": null, - "bibliographic_citation": null, - "source": [], - "thumbnail_path": "/downloads/qj72p713s?file=thumbnail", - "date_artifact_upper": "1933-02-14T00:00:00Z", - "date_artifact_lower": "1918-03-01T00:00:00Z", - "uses_vocabulary": [], - "form": [], - "score": 1 + "id": "9s1616164", + "title": [ + "[ip0006] [Glamorous and Bold Sword-Dancing Kisaeng]" + ], + "creator": [], + "creator_photographer": null, + "format_medium": null, + "format_size": null, + "date_approximate": null, + "date_range": null, + "creator_maker": null, + "date_original_display": null, + "description_size": null, + "description_note": null, + "subject_lcsh": null, + "publisher_original": null, + "date_original": null, + "format_extent": null, + "description_condition": null, + "description_provenance": null, + "description_series": null, + "identifier_itemnumber": null, + "publisher_digital": null, + "format_digital": null, + "rights_digital": null, + "subject_ocm": null, + "description_critical": null, + "description_indicia": null, + "description_text": null, + "description_inscription": null, + "description_ethnicity": null, + "description_citation": null, + "coverage_location_country": null, + "coverage_location": null, + "creator_company": null, + "relation_seealso": null, + "date_image_upper": null, + "date_image_lower": null, + "title_name": null, + "description_class": null, + "date_birth_display": null, + "coverage_place_birth": null, + "description_military_branch": null, + "description_military_rank": null, + "description_military_unit": null, + "date_death_display": null, + "coverage_place_death": null, + "description_cause_death": null, + "description_honors": null, + "type": "Work", + "contributor": [ + "Seo-Hyun Park", + "Ikegami Naoko", + "Michaela Kelly" + ], + "description": [ + "あだやかに勇ましき剣舞を舞はんとする妓生達" + ], + "keyword": [], + "rights": [ + "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." + ], + "publisher": [ + "Special Collections & College Archives, Skillman Library, Lafayette College" + ], + "date_created": [], + "subject": [], + "language": [], + "identifier": null, + "based_near": null, + "related_url": null, + "bibliographic_citation": null, + "source": [], + "thumbnail_path": "/downloads/qj72p713s?file=thumbnail", + "date_artifact_upper": "1933-02-14T00:00:00Z", + "date_artifact_lower": "1918-03-01T00:00:00Z", + "uses_vocabulary": [], + "form": [], + "score": 1 }, { - "id": "wd375w296", - "title": [ - "[ip0005] [Kisaeng Garments Rear-view]" - ], - "creator": [], - "creator_photographer": null, - "format_medium": null, - "format_size": null, - "date_approximate": null, - "date_range": null, - "creator_maker": null, - "date_original_display": null, - "description_size": null, - "description_note": null, - "subject_lcsh": null, - "publisher_original": null, - "date_original": null, - "format_extent": null, - "description_condition": null, - "description_provenance": null, - "description_series": null, - "identifier_itemnumber": null, - "publisher_digital": null, - "format_digital": null, - "rights_digital": null, - "subject_ocm": null, - "description_critical": null, - "description_indicia": null, - "description_text": null, - "description_inscription": null, - "description_ethnicity": null, - "description_citation": null, - "coverage_location_country": null, - "coverage_location": null, - "creator_company": null, - "relation_seealso": null, - "date_image_upper": null, - "date_image_lower": null, - "title_name": null, - "description_class": null, - "date_birth_display": null, - "coverage_place_birth": null, - "description_military_branch": null, - "description_military_rank": null, - "description_military_unit": null, - "date_death_display": null, - "coverage_place_death": null, - "description_cause_death": null, - "description_honors": null, - "type": "Work", - "contributor": [ - "Seo-Hyun Park", - "Michaela Kelly", - "Ikegami Naoko" - ], - "description": [ - "盛装を_らしたる妓生の背面" - ], - "keyword": [], - "rights": [ - "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." - ], - "publisher": [ - "Special Collections & College Archives, Skillman Library, Lafayette College" - ], - "date_created": [], - "subject": [], - "language": [], - "identifier": null, - "based_near": null, - "related_url": null, - "bibliographic_citation": null, - "source": [], - "thumbnail_path": "/downloads/cn69m416c?file=thumbnail", - "date_artifact_upper": "1933-02-14T00:00:00Z", - "date_artifact_lower": "1918-03-01T00:00:00Z", - "uses_vocabulary": [], - "form": [], - "score": 1 + "id": "wd375w296", + "title": [ + "[ip0005] [Kisaeng Garments Rear-view]" + ], + "creator": [], + "creator_photographer": null, + "format_medium": null, + "format_size": null, + "date_approximate": null, + "date_range": null, + "creator_maker": null, + "date_original_display": null, + "description_size": null, + "description_note": null, + "subject_lcsh": null, + "publisher_original": null, + "date_original": null, + "format_extent": null, + "description_condition": null, + "description_provenance": null, + "description_series": null, + "identifier_itemnumber": null, + "publisher_digital": null, + "format_digital": null, + "rights_digital": null, + "subject_ocm": null, + "description_critical": null, + "description_indicia": null, + "description_text": null, + "description_inscription": null, + "description_ethnicity": null, + "description_citation": null, + "coverage_location_country": null, + "coverage_location": null, + "creator_company": null, + "relation_seealso": null, + "date_image_upper": null, + "date_image_lower": null, + "title_name": null, + "description_class": null, + "date_birth_display": null, + "coverage_place_birth": null, + "description_military_branch": null, + "description_military_rank": null, + "description_military_unit": null, + "date_death_display": null, + "coverage_place_death": null, + "description_cause_death": null, + "description_honors": null, + "type": "Work", + "contributor": [ + "Seo-Hyun Park", + "Michaela Kelly", + "Ikegami Naoko" + ], + "description": [ + "盛装を_らしたる妓生の背面" + ], + "keyword": [], + "rights": [ + "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." + ], + "publisher": [ + "Special Collections & College Archives, Skillman Library, Lafayette College" + ], + "date_created": [], + "subject": [], + "language": [], + "identifier": null, + "based_near": null, + "related_url": null, + "bibliographic_citation": null, + "source": [], + "thumbnail_path": "/downloads/cn69m416c?file=thumbnail", + "date_artifact_upper": "1933-02-14T00:00:00Z", + "date_artifact_lower": "1918-03-01T00:00:00Z", + "uses_vocabulary": [], + "form": [], + "score": 1 }, { - "id": "br86b356q", - "title": [ - "[ip0004] [Modern Sericulture]" - ], - "creator": [], - "creator_photographer": null, - "format_medium": null, - "format_size": null, - "date_approximate": null, - "date_range": null, - "creator_maker": null, - "date_original_display": null, - "description_size": null, - "description_note": null, - "subject_lcsh": null, - "publisher_original": null, - "date_original": null, - "format_extent": null, - "description_condition": null, - "description_provenance": null, - "description_series": null, - "identifier_itemnumber": null, - "publisher_digital": null, - "format_digital": null, - "rights_digital": null, - "subject_ocm": null, - "description_critical": null, - "description_indicia": null, - "description_text": null, - "description_inscription": null, - "description_ethnicity": null, - "description_citation": null, - "coverage_location_country": null, - "coverage_location": null, - "creator_company": null, - "relation_seealso": null, - "date_image_upper": null, - "date_image_lower": null, - "title_name": null, - "description_class": null, - "date_birth_display": null, - "coverage_place_birth": null, - "description_military_branch": null, - "description_military_rank": null, - "description_military_unit": null, - "date_death_display": null, - "coverage_place_death": null, - "description_cause_death": null, - "description_honors": null, - "type": "Work", - "contributor": [ - "Michaela Kelly", - "Ikegami Naoko", - "Seo-Hyun Park" - ], - "description": [ - "近時益々普及発達を見るに至りたる婦人の養蚕", - "[trans. of Japanese text]: \"These days one sees a rapid diffusion and development of female sericulture\"" - ], - "keyword": [], - "rights": [ - "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." - ], - "publisher": [ - "Special Collections & College Archives, Skillman Library, Lafayette College" - ], - "date_created": [], - "subject": [], - "language": [], - "identifier": null, - "based_near": null, - "related_url": null, - "bibliographic_citation": null, - "source": [], - "thumbnail_path": "/downloads/7d278t01v?file=thumbnail", - "date_artifact_upper": "1933-02-14T00:00:00Z", - "date_artifact_lower": "1918-03-01T00:00:00Z", - "uses_vocabulary": [], - "form": [], - "score": 1 + "id": "br86b356q", + "title": [ + "[ip0004] [Modern Sericulture]" + ], + "creator": [], + "creator_photographer": null, + "format_medium": null, + "format_size": null, + "date_approximate": null, + "date_range": null, + "creator_maker": null, + "date_original_display": null, + "description_size": null, + "description_note": null, + "subject_lcsh": null, + "publisher_original": null, + "date_original": null, + "format_extent": null, + "description_condition": null, + "description_provenance": null, + "description_series": null, + "identifier_itemnumber": null, + "publisher_digital": null, + "format_digital": null, + "rights_digital": null, + "subject_ocm": null, + "description_critical": null, + "description_indicia": null, + "description_text": null, + "description_inscription": null, + "description_ethnicity": null, + "description_citation": null, + "coverage_location_country": null, + "coverage_location": null, + "creator_company": null, + "relation_seealso": null, + "date_image_upper": null, + "date_image_lower": null, + "title_name": null, + "description_class": null, + "date_birth_display": null, + "coverage_place_birth": null, + "description_military_branch": null, + "description_military_rank": null, + "description_military_unit": null, + "date_death_display": null, + "coverage_place_death": null, + "description_cause_death": null, + "description_honors": null, + "type": "Work", + "contributor": [ + "Michaela Kelly", + "Ikegami Naoko", + "Seo-Hyun Park" + ], + "description": [ + "近時益々普及発達を見るに至りたる婦人の養蚕", + "[trans. of Japanese text]: \"These days one sees a rapid diffusion and development of female sericulture\"" + ], + "keyword": [], + "rights": [ + "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." + ], + "publisher": [ + "Special Collections & College Archives, Skillman Library, Lafayette College" + ], + "date_created": [], + "subject": [], + "language": [], + "identifier": null, + "based_near": null, + "related_url": null, + "bibliographic_citation": null, + "source": [], + "thumbnail_path": "/downloads/7d278t01v?file=thumbnail", + "date_artifact_upper": "1933-02-14T00:00:00Z", + "date_artifact_lower": "1918-03-01T00:00:00Z", + "uses_vocabulary": [], + "form": [], + "score": 1 }, { - "id": "p5547r367", - "title": [ - "[ip0003] [Bustling Market on a Village Road]" - ], - "creator": [], - "creator_photographer": null, - "format_medium": null, - "format_size": null, - "date_approximate": null, - "date_range": null, - "creator_maker": null, - "date_original_display": null, - "description_size": null, - "description_note": null, - "subject_lcsh": null, - "publisher_original": null, - "date_original": null, - "format_extent": null, - "description_condition": null, - "description_provenance": null, - "description_series": null, - "identifier_itemnumber": null, - "publisher_digital": null, - "format_digital": null, - "rights_digital": null, - "subject_ocm": null, - "description_critical": null, - "description_indicia": null, - "description_text": null, - "description_inscription": null, - "description_ethnicity": null, - "description_citation": null, - "coverage_location_country": null, - "coverage_location": null, - "creator_company": null, - "relation_seealso": null, - "date_image_upper": null, - "date_image_lower": null, - "title_name": null, - "description_class": null, - "date_birth_display": null, - "coverage_place_birth": null, - "description_military_branch": null, - "description_military_rank": null, - "description_military_unit": null, - "date_death_display": null, - "coverage_place_death": null, - "description_cause_death": null, - "description_honors": null, - "type": "Work", - "contributor": [ - "Ikegami Naoko", - "Michaela Kelly", - "Seo-Hyun Park" - ], - "description": [ - "白衣の中に紅緑を交へて雑閙する商埠" - ], - "keyword": [], - "rights": [ - "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." - ], - "publisher": [ - "Special Collections & College Archives, Skillman Library, Lafayette College" - ], - "date_created": [], - "subject": [], - "language": [], - "identifier": null, - "based_near": null, - "related_url": null, - "bibliographic_citation": null, - "source": [], - "thumbnail_path": "/downloads/v405s9379?file=thumbnail", - "date_artifact_upper": "1933-02-14T00:00:00Z", - "date_artifact_lower": "1918-03-01T00:00:00Z", - "uses_vocabulary": [], - "form": [], - "score": 1 + "id": "p5547r367", + "title": [ + "[ip0003] [Bustling Market on a Village Road]" + ], + "creator": [], + "creator_photographer": null, + "format_medium": null, + "format_size": null, + "date_approximate": null, + "date_range": null, + "creator_maker": null, + "date_original_display": null, + "description_size": null, + "description_note": null, + "subject_lcsh": null, + "publisher_original": null, + "date_original": null, + "format_extent": null, + "description_condition": null, + "description_provenance": null, + "description_series": null, + "identifier_itemnumber": null, + "publisher_digital": null, + "format_digital": null, + "rights_digital": null, + "subject_ocm": null, + "description_critical": null, + "description_indicia": null, + "description_text": null, + "description_inscription": null, + "description_ethnicity": null, + "description_citation": null, + "coverage_location_country": null, + "coverage_location": null, + "creator_company": null, + "relation_seealso": null, + "date_image_upper": null, + "date_image_lower": null, + "title_name": null, + "description_class": null, + "date_birth_display": null, + "coverage_place_birth": null, + "description_military_branch": null, + "description_military_rank": null, + "description_military_unit": null, + "date_death_display": null, + "coverage_place_death": null, + "description_cause_death": null, + "description_honors": null, + "type": "Work", + "contributor": [ + "Ikegami Naoko", + "Michaela Kelly", + "Seo-Hyun Park" + ], + "description": [ + "白衣の中に紅緑を交へて雑閙する商埠" + ], + "keyword": [], + "rights": [ + "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." + ], + "publisher": [ + "Special Collections & College Archives, Skillman Library, Lafayette College" + ], + "date_created": [], + "subject": [], + "language": [], + "identifier": null, + "based_near": null, + "related_url": null, + "bibliographic_citation": null, + "source": [], + "thumbnail_path": "/downloads/v405s9379?file=thumbnail", + "date_artifact_upper": "1933-02-14T00:00:00Z", + "date_artifact_lower": "1918-03-01T00:00:00Z", + "uses_vocabulary": [], + "form": [], + "score": 1 }, { - "id": "g158bh28p", - "title": [ - "[ip0002] [Kisaeng Beauty]" - ], - "creator": [], - "creator_photographer": null, - "format_medium": null, - "format_size": null, - "date_approximate": null, - "date_range": null, - "creator_maker": null, - "date_original_display": null, - "description_size": null, - "description_note": null, - "subject_lcsh": null, - "publisher_original": null, - "date_original": null, - "format_extent": null, - "description_condition": null, - "description_provenance": null, - "description_series": null, - "identifier_itemnumber": null, - "publisher_digital": null, - "format_digital": null, - "rights_digital": null, - "subject_ocm": null, - "description_critical": null, - "description_indicia": null, - "description_text": null, - "description_inscription": null, - "description_ethnicity": null, - "description_citation": null, - "coverage_location_country": null, - "coverage_location": null, - "creator_company": null, - "relation_seealso": null, - "date_image_upper": null, - "date_image_lower": null, - "title_name": null, - "description_class": null, - "date_birth_display": null, - "coverage_place_birth": null, - "description_military_branch": null, - "description_military_rank": null, - "description_military_unit": null, - "date_death_display": null, - "coverage_place_death": null, - "description_cause_death": null, - "description_honors": null, - "type": "Work", - "contributor": [ - "Seo-Hyun Park", - "Ikegami Naoko" - ], - "description": [], - "keyword": [], - "rights": [ - "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." - ], - "publisher": [ - "Special Collections & College Archives, Skillman Library, Lafayette College" - ], - "date_created": [], - "subject": [], - "language": [], - "identifier": null, - "based_near": null, - "related_url": null, - "bibliographic_citation": null, - "source": [], - "thumbnail_path": "/downloads/k643b116n?file=thumbnail", - "date_artifact_upper": "1933-02-14T00:00:00Z", - "date_artifact_lower": "1918-03-01T00:00:00Z", - "uses_vocabulary": [], - "form": [], - "score": 1 + "id": "g158bh28p", + "title": [ + "[ip0002] [Kisaeng Beauty]" + ], + "creator": [], + "creator_photographer": null, + "format_medium": null, + "format_size": null, + "date_approximate": null, + "date_range": null, + "creator_maker": null, + "date_original_display": null, + "description_size": null, + "description_note": null, + "subject_lcsh": null, + "publisher_original": null, + "date_original": null, + "format_extent": null, + "description_condition": null, + "description_provenance": null, + "description_series": null, + "identifier_itemnumber": null, + "publisher_digital": null, + "format_digital": null, + "rights_digital": null, + "subject_ocm": null, + "description_critical": null, + "description_indicia": null, + "description_text": null, + "description_inscription": null, + "description_ethnicity": null, + "description_citation": null, + "coverage_location_country": null, + "coverage_location": null, + "creator_company": null, + "relation_seealso": null, + "date_image_upper": null, + "date_image_lower": null, + "title_name": null, + "description_class": null, + "date_birth_display": null, + "coverage_place_birth": null, + "description_military_branch": null, + "description_military_rank": null, + "description_military_unit": null, + "date_death_display": null, + "coverage_place_death": null, + "description_cause_death": null, + "description_honors": null, + "type": "Work", + "contributor": [ + "Seo-Hyun Park", + "Ikegami Naoko" + ], + "description": [], + "keyword": [], + "rights": [ + "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." + ], + "publisher": [ + "Special Collections & College Archives, Skillman Library, Lafayette College" + ], + "date_created": [], + "subject": [], + "language": [], + "identifier": null, + "based_near": null, + "related_url": null, + "bibliographic_citation": null, + "source": [], + "thumbnail_path": "/downloads/k643b116n?file=thumbnail", + "date_artifact_upper": "1933-02-14T00:00:00Z", + "date_artifact_lower": "1918-03-01T00:00:00Z", + "uses_vocabulary": [], + "form": [], + "score": 1 }, { - "id": "2j62s484w", - "title": [ - "[ip0001] Chosen Customs, Set 3" - ], - "creator": [], - "creator_photographer": null, - "format_medium": null, - "format_size": null, - "date_approximate": null, - "date_range": null, - "creator_maker": null, - "date_original_display": null, - "description_size": null, - "description_note": null, - "subject_lcsh": null, - "publisher_original": null, - "date_original": null, - "format_extent": null, - "description_condition": null, - "description_provenance": null, - "description_series": null, - "identifier_itemnumber": null, - "publisher_digital": null, - "format_digital": null, - "rights_digital": null, - "subject_ocm": null, - "description_critical": null, - "description_indicia": null, - "description_text": null, - "description_inscription": null, - "description_ethnicity": null, - "description_citation": null, - "coverage_location_country": null, - "coverage_location": null, - "creator_company": null, - "relation_seealso": null, - "date_image_upper": null, - "date_image_lower": null, - "title_name": null, - "description_class": null, - "date_birth_display": null, - "coverage_place_birth": null, - "description_military_branch": null, - "description_military_rank": null, - "description_military_unit": null, - "date_death_display": null, - "coverage_place_death": null, - "description_cause_death": null, - "description_honors": null, - "type": "Work", - "contributor": [ - "Li Guo", - "Michaela Kelly", - "Ikegami Naoko", - "Seo-Hyun Park" - ], - "description": [], - "keyword": [], - "rights": [ - "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." - ], - "publisher": [ - "Special Collections & College Archives, Skillman Library, Lafayette College" - ], - "date_created": [], - "subject": [], - "language": [], - "identifier": null, - "based_near": null, - "related_url": null, - "bibliographic_citation": null, - "source": [], - "thumbnail_path": "/downloads/cn69m4153?file=thumbnail", - "date_artifact_upper": "1933-02-14T00:00:00Z", - "date_artifact_lower": "1918-03-01T00:00:00Z", - "uses_vocabulary": [], - "form": [], - "score": 1 + "id": "2j62s484w", + "title": [ + "[ip0001] Chosen Customs, Set 3" + ], + "creator": [], + "creator_photographer": null, + "format_medium": null, + "format_size": null, + "date_approximate": null, + "date_range": null, + "creator_maker": null, + "date_original_display": null, + "description_size": null, + "description_note": null, + "subject_lcsh": null, + "publisher_original": null, + "date_original": null, + "format_extent": null, + "description_condition": null, + "description_provenance": null, + "description_series": null, + "identifier_itemnumber": null, + "publisher_digital": null, + "format_digital": null, + "rights_digital": null, + "subject_ocm": null, + "description_critical": null, + "description_indicia": null, + "description_text": null, + "description_inscription": null, + "description_ethnicity": null, + "description_citation": null, + "coverage_location_country": null, + "coverage_location": null, + "creator_company": null, + "relation_seealso": null, + "date_image_upper": null, + "date_image_lower": null, + "title_name": null, + "description_class": null, + "date_birth_display": null, + "coverage_place_birth": null, + "description_military_branch": null, + "description_military_rank": null, + "description_military_unit": null, + "date_death_display": null, + "coverage_place_death": null, + "description_cause_death": null, + "description_honors": null, + "type": "Work", + "contributor": [ + "Li Guo", + "Michaela Kelly", + "Ikegami Naoko", + "Seo-Hyun Park" + ], + "description": [], + "keyword": [], + "rights": [ + "This image is posted publicly for non-profit educational use, excluding print publication. For additional information, please see http://digital.lafayette.edu/copyright for our Reproduction, Use, and Copyright Guidelines." + ], + "publisher": [ + "Special Collections & College Archives, Skillman Library, Lafayette College" + ], + "date_created": [], + "subject": [], + "language": [], + "identifier": null, + "based_near": null, + "related_url": null, + "bibliographic_citation": null, + "source": [], + "thumbnail_path": "/downloads/cn69m4153?file=thumbnail", + "date_artifact_upper": "1933-02-14T00:00:00Z", + "date_artifact_lower": "1918-03-01T00:00:00Z", + "uses_vocabulary": [], + "form": [], + "score": 1 } ] diff --git a/src/components/catalog/__tests__/data/pages.json b/src/components/catalog/__tests__/data/pages.json index a5953ad..39d129d 100644 --- a/src/components/catalog/__tests__/data/pages.json +++ b/src/components/catalog/__tests__/data/pages.json @@ -1,11 +1,11 @@ -{ - "current_page": 2, - "next_page": 3, - "prev_page": 1, - "total_pages": 924, - "limit_value": 25, - "offset_value": 25, - "total_count": 23088, - "first_page?": false, - "last_page?": false +{ + "current_page": 2, + "next_page": 3, + "prev_page": 1, + "total_pages": 924, + "limit_value": 25, + "offset_value": 25, + "total_count": 23088, + "first_page?": false, + "last_page?": false } diff --git a/src/components/catalog/common/__tests__/calculate-range-test.js b/src/components/catalog/common/__tests__/calculate-range-test.js index 08a6bdf..99f23b2 100644 --- a/src/components/catalog/common/__tests__/calculate-range-test.js +++ b/src/components/catalog/common/__tests__/calculate-range-test.js @@ -7,80 +7,80 @@ const yearModifier = value => Number(value) const dateModifier = value => Date.parse(value) describe('catalog/common/calculate-range', function () { - it('uses a pass-thru function if no valueModifier provided', function () { - var items = [ - { - value: 'a', - hits: 0, - }, - { - value: 'z', - hits: 0, - }, - { - value: 'm', - hits: 0, - } - ] + it('uses a pass-thru function if no valueModifier provided', function () { + var items = [ + { + value: 'a', + hits: 0, + }, + { + value: 'z', + hits: 0, + }, + { + value: 'm', + hits: 0, + } + ] - const range = calculateRange(items) - expect(range.min).to.equal(Infinity) - expect(range.max).to.equal(-Infinity) - expect(range.hits).to.equal(0) - }) + const range = calculateRange(items) + expect(range.min).to.equal(Infinity) + expect(range.max).to.equal(-Infinity) + expect(range.hits).to.equal(0) + }) - it('calculates the total number of hits', function () { - const items = yearRange.items - const totalHits = items.reduce(function (total, item) { - return total + item.hits - }, 0) + it('calculates the total number of hits', function () { + const items = yearRange.items + const totalHits = items.reduce(function (total, item) { + return total + item.hits + }, 0) - const range = calculateRange(yearRange.items, yearModifier) + const range = calculateRange(yearRange.items, yearModifier) - expect(range.hits).to.equal(totalHits) - }) + expect(range.hits).to.equal(totalHits) + }) - describe('with string numeric + Number modifier', function () { - const range = calculateRange(yearRange.items, yearModifier) - - it('calculates the min value', function () { - const min = yearRange.items.reduce(function (min, item) { - const val = yearModifier(item.value) - return val < min ? val : min - }, Infinity) + describe('with string numeric + Number modifier', function () { + const range = calculateRange(yearRange.items, yearModifier) + + it('calculates the min value', function () { + const min = yearRange.items.reduce(function (min, item) { + const val = yearModifier(item.value) + return val < min ? val : min + }, Infinity) - expect(range.min).to.equal(min) - }) + expect(range.min).to.equal(min) + }) - it('calculates the max value', function () { - const max = yearRange.items.reduce(function (max, item) { - const val = yearModifier(item.value) - return val > max ? val : max - }, -Infinity) + it('calculates the max value', function () { + const max = yearRange.items.reduce(function (max, item) { + const val = yearModifier(item.value) + return val > max ? val : max + }, -Infinity) - expect(range.max).to.equal(max) - }) - }) + expect(range.max).to.equal(max) + }) + }) - describe('with ISO timestamp + Date modifier', function () { - const range = calculateRange(isoDateRange.items, dateModifier) - - it('calculates the min date value', function () { - const min = isoDateRange.items.reduce(function (current, item) { - const val = dateModifier(item.value) - return val < current ? val : current - }, Infinity) + describe('with ISO timestamp + Date modifier', function () { + const range = calculateRange(isoDateRange.items, dateModifier) + + it('calculates the min date value', function () { + const min = isoDateRange.items.reduce(function (current, item) { + const val = dateModifier(item.value) + return val < current ? val : current + }, Infinity) - expect(range.min).to.equal(min) - }) + expect(range.min).to.equal(min) + }) - it('calculates the max date value', function () { - const max = isoDateRange.items.reduce(function (current, item) { - const val = dateModifier(item.value) - return val > current ? val : current - }, -Infinity) + it('calculates the max date value', function () { + const max = isoDateRange.items.reduce(function (current, item) { + const val = dateModifier(item.value) + return val > current ? val : current + }, -Infinity) - expect(range.max).to.equal(max) - }) - }) + expect(range.max).to.equal(max) + }) + }) }) diff --git a/src/components/catalog/common/__tests__/format-date-value-test.js b/src/components/catalog/common/__tests__/format-date-value-test.js index 5c52446..8c702cd 100644 --- a/src/components/catalog/common/__tests__/format-date-value-test.js +++ b/src/components/catalog/common/__tests__/format-date-value-test.js @@ -3,18 +3,18 @@ import formatDateValue from '../format-date-value' import { INTERVALS } from '../date-intervals' describe('catalog/common/format-date-value', function () { - it('formats "day" as YYYY-MM-DD', function () { - const res = formatDateValue(INTERVALS.DAY, Date.now()) - expect(res).to.match(/^\d{4}-\d{2}-\d{2}$/) - }) + it('formats "day" as YYYY-MM-DD', function () { + const res = formatDateValue(INTERVALS.DAY, Date.now()) + expect(res).to.match(/^\d{4}-\d{2}-\d{2}$/) + }) - it('formats "month" as YYYY-MM', function () { - const res = formatDateValue(INTERVALS.MONTH, Date.now()) - expect(res).to.match(/^\d{4}-\d{2}$/) - }) + it('formats "month" as YYYY-MM', function () { + const res = formatDateValue(INTERVALS.MONTH, Date.now()) + expect(res).to.match(/^\d{4}-\d{2}$/) + }) - it('formats "year" as YYYY', function () { - const res = formatDateValue(INTERVALS.YEAR, Date.now()) - expect(res).to.match(/^\d{4}$/) - }) + it('formats "year" as YYYY', function () { + const res = formatDateValue(INTERVALS.YEAR, Date.now()) + expect(res).to.match(/^\d{4}$/) + }) }) diff --git a/src/components/catalog/common/__tests__/round-date-to-interval-test.js b/src/components/catalog/common/__tests__/round-date-to-interval-test.js index 421701b..6a9bf1a 100644 --- a/src/components/catalog/common/__tests__/round-date-to-interval-test.js +++ b/src/components/catalog/common/__tests__/round-date-to-interval-test.js @@ -3,60 +3,60 @@ import { INTERVALS } from '../date-intervals' import { expect } from 'chai' describe('catalog/common/round-date-to-interval', function () { - // 2016-11-29T11:18:00Z - const TIMESTAMP = Date.UTC(2016, 10, 29, 11, 18, 0) + // 2016-11-29T11:18:00Z + const TIMESTAMP = Date.UTC(2016, 10, 29, 11, 18, 0) - it('rounds `day` to the day', function () { - const rounded = roundDate(INTERVALS.DAY, TIMESTAMP) - const d = new Date(TIMESTAMP) - const r = new Date(rounded) + it('rounds `day` to the day', function () { + const rounded = roundDate(INTERVALS.DAY, TIMESTAMP) + const d = new Date(TIMESTAMP) + const r = new Date(rounded) - expect(r.getUTCFullYear()).to.equal(d.getUTCFullYear()) - expect(r.getUTCMonth()).to.equal(d.getUTCMonth()) - expect(r.getUTCDate()).to.equal(d.getUTCDate()) + expect(r.getUTCFullYear()).to.equal(d.getUTCFullYear()) + expect(r.getUTCMonth()).to.equal(d.getUTCMonth()) + expect(r.getUTCDate()).to.equal(d.getUTCDate()) - expect(r.getUTCHours()).to.not.equal(d.getUTCHours()) - expect(r.getUTCHours()).to.equal(0) + expect(r.getUTCHours()).to.not.equal(d.getUTCHours()) + expect(r.getUTCHours()).to.equal(0) - expect(r.getUTCMinutes()).to.not.equal(d.getUTCMinutes()) - expect(r.getUTCMinutes()).to.equal(0) - }) + expect(r.getUTCMinutes()).to.not.equal(d.getUTCMinutes()) + expect(r.getUTCMinutes()).to.equal(0) + }) - it('rounds `month` to the month', function () { - const rounded = roundDate(INTERVALS.MONTH, TIMESTAMP) - const d = new Date(TIMESTAMP) - const r = new Date(rounded) + it('rounds `month` to the month', function () { + const rounded = roundDate(INTERVALS.MONTH, TIMESTAMP) + const d = new Date(TIMESTAMP) + const r = new Date(rounded) - expect(r.getUTCFullYear()).to.equal(d.getUTCFullYear()) - expect(r.getUTCMonth()).to.equal(d.getUTCMonth()) + expect(r.getUTCFullYear()).to.equal(d.getUTCFullYear()) + expect(r.getUTCMonth()).to.equal(d.getUTCMonth()) - expect(r.getUTCDate()).to.not.equal(d.getUTCDate()) - expect(r.getUTCDate()).to.equal(1) + expect(r.getUTCDate()).to.not.equal(d.getUTCDate()) + expect(r.getUTCDate()).to.equal(1) - expect(r.getUTCHours()).to.not.equal(d.getUTCHours()) - expect(r.getUTCHours()).to.equal(0) + expect(r.getUTCHours()).to.not.equal(d.getUTCHours()) + expect(r.getUTCHours()).to.equal(0) - expect(r.getUTCMinutes()).to.not.equal(d.getUTCMinutes()) - expect(r.getUTCMinutes()).to.equal(0) - }) + expect(r.getUTCMinutes()).to.not.equal(d.getUTCMinutes()) + expect(r.getUTCMinutes()).to.equal(0) + }) - it('rounds `year` to the year', function () { - const rounded = roundDate(INTERVALS.YEAR, TIMESTAMP) - const d = new Date(TIMESTAMP) - const r = new Date(rounded) + it('rounds `year` to the year', function () { + const rounded = roundDate(INTERVALS.YEAR, TIMESTAMP) + const d = new Date(TIMESTAMP) + const r = new Date(rounded) - expect(r.getUTCFullYear()).to.equal(d.getUTCFullYear()) + expect(r.getUTCFullYear()).to.equal(d.getUTCFullYear()) - expect(r.getUTCMonth()).to.not.equal(d.getUTCMonth()) - expect(r.getUTCMonth()).to.equal(0) + expect(r.getUTCMonth()).to.not.equal(d.getUTCMonth()) + expect(r.getUTCMonth()).to.equal(0) - expect(r.getUTCDate()).to.not.equal(d.getUTCDate()) - expect(r.getUTCDate()).to.equal(1) + expect(r.getUTCDate()).to.not.equal(d.getUTCDate()) + expect(r.getUTCDate()).to.equal(1) - expect(r.getUTCHours()).to.not.equal(d.getUTCHours()) - expect(r.getUTCHours()).to.equal(0) + expect(r.getUTCHours()).to.not.equal(d.getUTCHours()) + expect(r.getUTCHours()).to.equal(0) - expect(r.getUTCMinutes()).to.not.equal(d.getUTCMinutes()) - expect(r.getUTCMinutes()).to.equal(0) - }) + expect(r.getUTCMinutes()).to.not.equal(d.getUTCMinutes()) + expect(r.getUTCMinutes()).to.equal(0) + }) }) diff --git a/src/components/catalog/common/calculate-range.js b/src/components/catalog/common/calculate-range.js index 668c984..8eab1ba 100644 --- a/src/components/catalog/common/calculate-range.js +++ b/src/components/catalog/common/calculate-range.js @@ -1,33 +1,33 @@ import assign from 'object-assign' export default function calculateRange (items, valueModifier) { - if (!valueModifier) - valueModifier = v => v + if (!valueModifier) + valueModifier = v => v - let max = -Infinity - let min = Infinity - let totalHits = 0 + let max = -Infinity + let min = Infinity + let totalHits = 0 - const cleaned = items.map(_item => { - // prevent overwriting the original data - const item = assign({}, _item) - let value = item.value = valueModifier(item.value) + const cleaned = items.map(_item => { + // prevent overwriting the original data + const item = assign({}, _item) + let value = item.value = valueModifier(item.value) - if (value < min) - min = value + if (value < min) + min = value - if (value > max) - max = value + if (value > max) + max = value - totalHits += item.hits + totalHits += item.hits - return item - }) + return item + }) - return { - items: cleaned, - hits: totalHits, - max, - min, - } + return { + items: cleaned, + hits: totalHits, + max, + min, + } } diff --git a/src/components/catalog/common/date-intervals.js b/src/components/catalog/common/date-intervals.js index 38752d2..7f6fee2 100644 --- a/src/components/catalog/common/date-intervals.js +++ b/src/components/catalog/common/date-intervals.js @@ -1,7 +1,7 @@ export const INTERVALS = { - DAY: 'day', - MONTH: 'month', - YEAR: 'year', + DAY: 'day', + MONTH: 'month', + YEAR: 'year', } export const VALUES = Object.keys(INTERVALS).map(k => INTERVALS[k]) diff --git a/src/components/catalog/common/format-date-value.js b/src/components/catalog/common/format-date-value.js index 3bf9e2f..0fe3401 100644 --- a/src/components/catalog/common/format-date-value.js +++ b/src/components/catalog/common/format-date-value.js @@ -1,42 +1,42 @@ import { INTERVALS } from './date-intervals' export default function formatDateValue (interval, timestamp) { - const d = new Date(timestamp) + const d = new Date(timestamp) - switch (interval) { - case INTERVALS.MONTH: - return formatMonth(d) + switch (interval) { + case INTERVALS.MONTH: + return formatMonth(d) - case INTERVALS.DAY: - return formatDay(d) + case INTERVALS.DAY: + return formatDay(d) - default: - return formatYear(d) - } + default: + return formatYear(d) + } } export function formatDay (date) { - const yr = date.getUTCFullYear() - const mo = padNumber(date.getUTCMonth() + 1) - const day = padNumber(date.getUTCDate()) + const yr = date.getUTCFullYear() + const mo = padNumber(date.getUTCMonth() + 1) + const day = padNumber(date.getUTCDate()) - return `${yr}-${mo}-${day}` + return `${yr}-${mo}-${day}` } export function formatMonth (date) { - const yr = date.getUTCFullYear() - const mo = padNumber(date.getUTCMonth() + 1) + const yr = date.getUTCFullYear() + const mo = padNumber(date.getUTCMonth() + 1) - return `${yr}-${mo}` + return `${yr}-${mo}` } export function formatYear (date) { - return `${date.getUTCFullYear()}` + return `${date.getUTCFullYear()}` } function padNumber (val) { - if (val < 10) - return '0' + val + if (val < 10) + return '0' + val - return '' + val + return '' + val } diff --git a/src/components/catalog/common/parse-input-date-value.js b/src/components/catalog/common/parse-input-date-value.js index c2165b2..2d43760 100644 --- a/src/components/catalog/common/parse-input-date-value.js +++ b/src/components/catalog/common/parse-input-date-value.js @@ -1,22 +1,22 @@ // converts YYYY(-MM(-DD)) to Date.UTC timestamp export default function parseUtcTimestamp (value) { - if (!value) { - return NaN - } + if (!value) { + return NaN + } - const args = value.split('-') + const args = value.split('-') - if (args.length === 0) - return '' + if (args.length === 0) + return '' - if (args.length > 1) - args[1] = args[1] - 1 + if (args.length > 1) + args[1] = args[1] - 1 - if (args.length === 1) - return Date.UTC.apply(Date, [args[0], 0, 1]) + if (args.length === 1) + return Date.UTC.apply(Date, [args[0], 0, 1]) - if (args.length === 2) - return Date.UTC.apply(Date, args.concat(1)) + if (args.length === 2) + return Date.UTC.apply(Date, args.concat(1)) - return Date.UTC.apply(Date, args) + return Date.UTC.apply(Date, args) } diff --git a/src/components/catalog/common/round-date-to-interval.js b/src/components/catalog/common/round-date-to-interval.js index e97c6cd..f498cea 100644 --- a/src/components/catalog/common/round-date-to-interval.js +++ b/src/components/catalog/common/round-date-to-interval.js @@ -1,46 +1,46 @@ import { INTERVALS } from './date-intervals' export default function roundDateToInterval (interval, timestamp) { - const date = new Date(timestamp) + const date = new Date(timestamp) - // isNaN - if (date !== date) - return timestamp + // isNaN + if (date !== date) + return timestamp - switch (interval) { - case INTERVALS.DAY: - return roundDay(date) + switch (interval) { + case INTERVALS.DAY: + return roundDay(date) - case INTERVALS.MONTH: - return roundMonth(date) + case INTERVALS.MONTH: + return roundMonth(date) - case INTERVALS.YEAR: - return roundYear(date) - } + case INTERVALS.YEAR: + return roundYear(date) + } - return timestamp + return timestamp } function roundDay (d) { - return Date.UTC( - d.getUTCFullYear(), - d.getUTCMonth(), - d.getUTCDate() - ) + return Date.UTC( + d.getUTCFullYear(), + d.getUTCMonth(), + d.getUTCDate() + ) } function roundMonth (d) { - return Date.UTC( - d.getUTCFullYear(), - d.getUTCMonth(), - 1 - ) + return Date.UTC( + d.getUTCFullYear(), + d.getUTCMonth(), + 1 + ) } function roundYear (d) { - return Date.UTC( - d.getUTCFullYear(), - 0, - 1 - ) + return Date.UTC( + d.getUTCFullYear(), + 0, + 1 + ) } diff --git a/src/components/media/OpenSeadragonViewer.jsx b/src/components/media/OpenSeadragonViewer.jsx index 0e23a9d..10f9e45 100644 --- a/src/components/media/OpenSeadragonViewer.jsx +++ b/src/components/media/OpenSeadragonViewer.jsx @@ -6,91 +6,91 @@ import Button from '../Button.jsx' const T = React.PropTypes const OpenSeadragonViewer = React.createClass({ - propTypes: { - prefixUrl: T.string, - tileSources: T.oneOfType([T.array, T.string]), - sequenceMode: T.bool, - showReferenceStrip: T.bool, - referenceStripScroll: T.string, - showNavigator: T.bool, - onClose: T.func - }, - - getInitialState: function () { - return { - height: this.getHeight(), - } - }, - - componentWillMount: function () { - window.addEventListener('resize', this.resizeContainer) - }, - - componentWillUnmount: function () { - window.removeEventListener('resize', this.resizeContainer) - }, - - componentDidMount: function () { - this.initOpenSeadragon() - }, - - shouldComponentUpdate: function (nextProps, nextState) { - return nextState.height !== this.state.height - }, - - getHeight: function () { - return Math.floor(window.innerHeight * 0.5) - }, - - initOpenSeadragon: function () { - const props = assign({}, this.props) - delete props.onClose - - this.viewer = OpenSeadragon({ - element: this._element, - - ...props, - }) - - const icon = 'http://icons.iconarchive.com/icons/custom-icon-design/mono-general-1/32/close-icon.png' - const additionalControls = []; - const closeButton = new OpenSeadragon.Button({ - tooltip: 'Close Viewer', - onClick: this.props.onClose, - srcRest: icon, - srcGroup: icon, - srcHover: icon, - srcDown: icon - }); - additionalControls.push(closeButton); - const URButtonGroup = new OpenSeadragon.ButtonGroup({buttons: additionalControls}); - - this.viewer.addControl(URButtonGroup.element, { - anchor: OpenSeadragon.ControlAnchor.TOP_RIGHT - }); - }, - - resizeContainer: function () { - let timeout - - if (!timeout) { - timeout = setTimeout(() => { - timeout = null - this.setState({height: this.getHeight()}) - }, 66) - } - }, - - render: function () { - const { height } = this.state - return ( -
    this._element = el} - style={{height}} - /> - ) - } + propTypes: { + prefixUrl: T.string, + tileSources: T.oneOfType([T.array, T.string]), + sequenceMode: T.bool, + showReferenceStrip: T.bool, + referenceStripScroll: T.string, + showNavigator: T.bool, + onClose: T.func + }, + + getInitialState: function () { + return { + height: this.getHeight(), + } + }, + + componentWillMount: function () { + window.addEventListener('resize', this.resizeContainer) + }, + + componentWillUnmount: function () { + window.removeEventListener('resize', this.resizeContainer) + }, + + componentDidMount: function () { + this.initOpenSeadragon() + }, + + shouldComponentUpdate: function (nextProps, nextState) { + return nextState.height !== this.state.height + }, + + getHeight: function () { + return Math.floor(window.innerHeight * 0.5) + }, + + initOpenSeadragon: function () { + const props = assign({}, this.props) + delete props.onClose + + this.viewer = OpenSeadragon({ + element: this._element, + + ...props, + }) + + const icon = 'http://icons.iconarchive.com/icons/custom-icon-design/mono-general-1/32/close-icon.png' + const additionalControls = []; + const closeButton = new OpenSeadragon.Button({ + tooltip: 'Close Viewer', + onClick: this.props.onClose, + srcRest: icon, + srcGroup: icon, + srcHover: icon, + srcDown: icon + }); + additionalControls.push(closeButton); + const URButtonGroup = new OpenSeadragon.ButtonGroup({buttons: additionalControls}); + + this.viewer.addControl(URButtonGroup.element, { + anchor: OpenSeadragon.ControlAnchor.TOP_RIGHT + }); + }, + + resizeContainer: function () { + let timeout + + if (!timeout) { + timeout = setTimeout(() => { + timeout = null + this.setState({height: this.getHeight()}) + }, 66) + } + }, + + render: function () { + const { height } = this.state + return ( +
    this._element = el} + style={{height}} + /> + ) + } }) export default OpenSeadragonViewer diff --git a/src/components/media/PDFViewer.jsx b/src/components/media/PDFViewer.jsx index 1f9c6f1..8b982b2 100644 --- a/src/components/media/PDFViewer.jsx +++ b/src/components/media/PDFViewer.jsx @@ -4,21 +4,21 @@ import PDFViewerTemplate from './PDFViewerTemplate.jsx' const T = React.PropTypes const PDFViewer = React.createClass({ - propTypes: { - src: T.string - }, + propTypes: { + src: T.string + }, - componentDidMount() { - webViewerLoad(this.props.src) - }, + componentDidMount() { + webViewerLoad(this.props.src) + }, - render: function() { - return ( -
    - -
    - ) - } + render: function() { + return ( +
    + +
    + ) + } }) export default PDFViewer diff --git a/src/components/media/PDFViewerTemplate.jsx b/src/components/media/PDFViewerTemplate.jsx index 44457cc..e8c45d0 100644 --- a/src/components/media/PDFViewerTemplate.jsx +++ b/src/components/media/PDFViewerTemplate.jsx @@ -8,315 +8,315 @@ import React from 'react' const PDFViewerTemplate = React.createClass({ - render: function() { - - return( - -
    - -
    -
    -
    - - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - - -
    - -
    - -
    - - - - - -
    - -
    -
    - - - - - - - - - - Current View - - -
    - - - - -
    - - - - -
    - - - -
    - - -
    -
    - -
    -
    -
    -
    - -
    - -
    - -
    - -
    - - - -
    -
    - - - - - - - - - Current View - - -
    - - -
    -
    -
    -
    - -
    - -
    - - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - - - -
    -
    -
    - - -
    - -
    -
    -
    -
    -

    Enter the password to open this PDF file:

    -
    -
    - -
    -
    - - -
    -
    -
    -
    -
    -
    - File name:

    -

    -
    -
    - File size:

    -

    -
    -
    -
    - Title:

    -

    -
    -
    - Author:

    -

    -
    -
    - Subject:

    -

    -
    -
    - Keywords:

    -

    -
    -
    - Creation Date:

    -

    -
    -
    - Modification Date:

    -

    -
    -
    - Creator:

    -

    -
    -
    -
    - PDF Producer:

    -

    -
    -
    - PDF Version:

    -

    -
    -
    - Page Count:

    -

    -
    -
    - -
    -
    -
    -
    -
    - -
    - -
    - - )} + render: function() { + + return( + +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + +
    + +
    + +
    + + + + + +
    + +
    +
    + + + + + + + + + + Current View + + +
    + + + + +
    + + + + +
    + + + +
    + + +
    +
    + +
    +
    +
    +
    + +
    + +
    + +
    + +
    + + + +
    +
    + + + + + + + + + Current View + + +
    + + +
    +
    +
    +
    + +
    + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + + + + + +
    +
    +
    + + +
    + +
    +
    +
    +
    +

    Enter the password to open this PDF file:

    +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    +
    + File name:

    -

    +
    +
    + File size:

    -

    +
    +
    +
    + Title:

    -

    +
    +
    + Author:

    -

    +
    +
    + Subject:

    -

    +
    +
    + Keywords:

    -

    +
    +
    + Creation Date:

    -

    +
    +
    + Modification Date:

    -

    +
    +
    + Creator:

    -

    +
    +
    +
    + PDF Producer:

    -

    +
    +
    + PDF Version:

    -

    +
    +
    + Page Count:

    -

    +
    +
    + +
    +
    +
    +
    +
    + +
    + +
    + + )} }) diff --git a/src/components/media/ThumbnailPreview.jsx b/src/components/media/ThumbnailPreview.jsx index 70866ff..76d64fe 100644 --- a/src/components/media/ThumbnailPreview.jsx +++ b/src/components/media/ThumbnailPreview.jsx @@ -4,43 +4,43 @@ import Button from '../Button.jsx' const T = React.PropTypes const ThumbnailPreview = React.createClass({ - propTypes: { - src: T.string.isRequired, - - caption: T.string, - onClick: T.func, - }, - - handleOnClick: function () { - if (!this.props.onClick) - return - - return this.props.onClick() - }, - - maybeRenderCaption: function () { - if (!this.props.caption) - return - - return ( -
    - {this.props.caption} -
    - ) - }, - - render: function () { - return( -
    -
    - - - {this.maybeRenderCaption()} -
    - -
    - ) - } + propTypes: { + src: T.string.isRequired, + + caption: T.string, + onClick: T.func, + }, + + handleOnClick: function () { + if (!this.props.onClick) + return + + return this.props.onClick() + }, + + maybeRenderCaption: function () { + if (!this.props.caption) + return + + return ( +
    + {this.props.caption} +
    + ) + }, + + render: function () { + return( +
    +
    + + + {this.maybeRenderCaption()} +
    + +
    + ) + } }) export default ThumbnailPreview diff --git a/src/components/media/__tests__/OpenSeadragonViewer-test.jsx b/src/components/media/__tests__/OpenSeadragonViewer-test.jsx index 5dce822..5bc22ac 100644 --- a/src/components/media/__tests__/OpenSeadragonViewer-test.jsx +++ b/src/components/media/__tests__/OpenSeadragonViewer-test.jsx @@ -6,30 +6,30 @@ import assign from 'object-assign' import OpenSeadragonViewer from '../OpenSeadragonViewer.jsx' const defaultProps = { - prefixUrl: '//localhost.localdomain/images/', - tileSources: ['//localhost.localdomain/iiif/image.jp2/info.json'], - sequenceMode: true, - showReferenceStrip: true, - referenceStripScroll: 'vertical', - showNavigator: true + prefixUrl: '//localhost.localdomain/images/', + tileSources: ['//localhost.localdomain/iiif/image.jp2/info.json'], + sequenceMode: true, + showReferenceStrip: true, + referenceStripScroll: 'vertical', + showNavigator: true } const noop = () => {} const wrapEl = (xtend, renderer) => { - const props = assign({}, { - defaultProps, - onClose: noop - }, xtend) + const props = assign({}, { + defaultProps, + onClose: noop + }, xtend) - return renderer(React.createElement(OpenSeadragonViewer, props)) + return renderer(React.createElement(OpenSeadragonViewer, props)) } const shallowEl = xtend => wrapEl(xtend, shallow) describe('', () => { - it('renders an OpenSeadragonViewer element', () => { - const $el = shallowEl() - expect($el.find(OpenSeadragonViewer)).to.be.a('Object'); - }) + it('renders an OpenSeadragonViewer element', () => { + const $el = shallowEl() + expect($el.find(OpenSeadragonViewer)).to.be.a('Object'); + }) }) diff --git a/src/components/metadata/ControlledVocabularyInput.jsx b/src/components/metadata/ControlledVocabularyInput.jsx index e206f1f..9ad0954 100644 --- a/src/components/metadata/ControlledVocabularyInput.jsx +++ b/src/components/metadata/ControlledVocabularyInput.jsx @@ -5,166 +5,166 @@ import assign from 'object-assign' const T = React.PropTypes const ControlledVocabularyInput = React.createClass({ - propTypes: { - // function used to retrieve terms from a remote source - // (we're using an intermediary function rather than requesting - // from within the component to allow some WorkEdit level caching - // of responses) - fetchTerms: T.func, - - // whether or not to highlight the matching area of the text - highlightMatch: T.bool, - - onChange: T.func, - - // option to pass terms directly to the component if you so wish! - terms: T.array, - value: T.string, - }, - - componentDidMount: function () { - if (this.props.fetchTerms) { - this.props.fetchTerms() - .then(terms => this.setState({terms})) - - // TODO: do something if there's a problem - // (maybe disable the input?) - .catch(console.warn) - } - }, - - // if we're passing terms through props (+ bypassing `fetchTerms`), - // we need to update the state if/when updated terms are passed - componentWillReceiveProps: function (nextProps) { - if (nextProps.terms && (nextProps.terms.length !== this.state.terms.length)) { - this.setState({terms: nextProps.terms}) - } - }, - - getInitialState: function () { - return { - inputValue: this.props.value || '', - terms: this.props.terms || [], - } - }, - - getItemValue: function (item) { - return item.pref_label[0] - }, - - handleChange: function (ev, value) { - this.setState({inputValue: value}) - - const menu = this._autocomplete.refs.menu - - if (menu) - menu.scrollTop = 0 - }, - - handleSelect: function (value, item) { - this.setState({inputValue: value}) - this.props.onChange && this.props.onChange.call(null, value) - }, - - renderItem: function (item, isHighlighted, style) { - const itemStyle = { - backgroundColor: isHighlighted ? '#4ea8dd' : 'inherit', - cursor: 'pointer', - padding: '.25em', - } - - const label = item.pref_label[0] - const value = this.state.inputValue.trim().toLowerCase() - - if (value === '' || !this.props.highlightMatch) - return
    {label}
    - - const idx = label.toLowerCase().indexOf(value) - const pre = label.slice(0, idx) - const sel = label.slice(idx, idx + value.length) - const post = label.slice(idx + value.length) - - const highlightStyle = { - backgroundColor: '#6fcaff', - } - - return ( -
    - {pre} - {sel} - {post} -
    - ) - }, - - renderMenu: function (items, value, style) { - const menuStyle = { - borderRadius: '2px', - boxShadow: '0 2px 12px #aaa', - background: '#fff', - fontSize: '90%', - height: '10em', - left: '0', - overflowY: 'scroll', - padding: '4px', - position: 'absolute', - top: '2.5em', - width: '100%', - zIndex: '10', - } - - return ( -
    - ) - }, - - shouldItemRender: function (item) { - const val = this.state.inputValue.trim().toLowerCase() - - if (val === '') - return true - - const label = item.pref_label[0] - - if (!label) - return false - - return label.toLowerCase().indexOf(val) > -1 - }, - - render: function () { - const wrapperStyle = { - clear: 'both', - float: 'left', - position: 'relative', - verticalAlign: 'middle', - width: '90%', - } - - const inputProps = { - style: { - width: '100%', - }, - - type: 'text' - } - - return ( - this._autocomplete = el} - renderItem={this.renderItem} - renderMenu={this.renderMenu} - shouldItemRender={this.shouldItemRender} - value={this.state.inputValue} - wrapperStyle={wrapperStyle} - /> - ) - } + propTypes: { + // function used to retrieve terms from a remote source + // (we're using an intermediary function rather than requesting + // from within the component to allow some WorkEdit level caching + // of responses) + fetchTerms: T.func, + + // whether or not to highlight the matching area of the text + highlightMatch: T.bool, + + onChange: T.func, + + // option to pass terms directly to the component if you so wish! + terms: T.array, + value: T.string, + }, + + componentDidMount: function () { + if (this.props.fetchTerms) { + this.props.fetchTerms() + .then(terms => this.setState({terms})) + + // TODO: do something if there's a problem + // (maybe disable the input?) + .catch(console.warn) + } + }, + + // if we're passing terms through props (+ bypassing `fetchTerms`), + // we need to update the state if/when updated terms are passed + componentWillReceiveProps: function (nextProps) { + if (nextProps.terms && (nextProps.terms.length !== this.state.terms.length)) { + this.setState({terms: nextProps.terms}) + } + }, + + getInitialState: function () { + return { + inputValue: this.props.value || '', + terms: this.props.terms || [], + } + }, + + getItemValue: function (item) { + return item.pref_label[0] + }, + + handleChange: function (ev, value) { + this.setState({inputValue: value}) + + const menu = this._autocomplete.refs.menu + + if (menu) + menu.scrollTop = 0 + }, + + handleSelect: function (value, item) { + this.setState({inputValue: value}) + this.props.onChange && this.props.onChange.call(null, value) + }, + + renderItem: function (item, isHighlighted, style) { + const itemStyle = { + backgroundColor: isHighlighted ? '#4ea8dd' : 'inherit', + cursor: 'pointer', + padding: '.25em', + } + + const label = item.pref_label[0] + const value = this.state.inputValue.trim().toLowerCase() + + if (value === '' || !this.props.highlightMatch) + return
    {label}
    + + const idx = label.toLowerCase().indexOf(value) + const pre = label.slice(0, idx) + const sel = label.slice(idx, idx + value.length) + const post = label.slice(idx + value.length) + + const highlightStyle = { + backgroundColor: '#6fcaff', + } + + return ( +
    + {pre} + {sel} + {post} +
    + ) + }, + + renderMenu: function (items, value, style) { + const menuStyle = { + borderRadius: '2px', + boxShadow: '0 2px 12px #aaa', + background: '#fff', + fontSize: '90%', + height: '10em', + left: '0', + overflowY: 'scroll', + padding: '4px', + position: 'absolute', + top: '2.5em', + width: '100%', + zIndex: '10', + } + + return ( +
    + ) + }, + + shouldItemRender: function (item) { + const val = this.state.inputValue.trim().toLowerCase() + + if (val === '') + return true + + const label = item.pref_label[0] + + if (!label) + return false + + return label.toLowerCase().indexOf(val) > -1 + }, + + render: function () { + const wrapperStyle = { + clear: 'both', + float: 'left', + position: 'relative', + verticalAlign: 'middle', + width: '90%', + } + + const inputProps = { + style: { + width: '100%', + }, + + type: 'text' + } + + return ( + this._autocomplete = el} + renderItem={this.renderItem} + renderMenu={this.renderMenu} + shouldItemRender={this.shouldItemRender} + value={this.state.inputValue} + wrapperStyle={wrapperStyle} + /> + ) + } }) export default ControlledVocabularyInput diff --git a/src/components/metadata/DateInput.jsx b/src/components/metadata/DateInput.jsx index a2c1a7f..64a7c38 100644 --- a/src/components/metadata/DateInput.jsx +++ b/src/components/metadata/DateInput.jsx @@ -4,78 +4,78 @@ import assign from 'object-assign' const T = React.PropTypes const DateInput = React.createClass({ - propTypes: { - onChange: T.func, + propTypes: { + onChange: T.func, - style: T.object, + style: T.object, - type: T.oneOf(['day', 'month', 'year']), + type: T.oneOf(['day', 'month', 'year']), - // expect ISO timestamp - value: T.string, - }, + // expect ISO timestamp + value: T.string, + }, - getInitialState: function () { - const value = this.props.value + getInitialState: function () { + const value = this.props.value - if (!value) - return { date: '' } + if (!value) + return { date: '' } - // `time` will either be `HH:MM:SS.000Z` - // or undefined (if short date (YYYY-MM-DD) is passed) - let [ date ] = value.split('T') + // `time` will either be `HH:MM:SS.000Z` + // or undefined (if short date (YYYY-MM-DD) is passed) + let [ date ] = value.split('T') - if (this.props.type === 'month') - date = date.replace(/\-\d{2}$/, '') + if (this.props.type === 'month') + date = date.replace(/\-\d{2}$/, '') - return { date } - }, + return { date } + }, - getDateValue: function () { - if (!this.state.date) - return null + getDateValue: function () { + if (!this.state.date) + return null - const values = this.state.date.split('-').map(Number) + const values = this.state.date.split('-').map(Number) - // months are 0-indexed - if (values[1] > 0) - values[1] = values[1] - 1 + // months are 0-indexed + if (values[1] > 0) + values[1] = values[1] - 1 - return new Date(Date.UTC.apply(Date, values)) - }, + return new Date(Date.UTC.apply(Date, values)) + }, - getInputType: function () { - switch (this.props.type) { - case 'month': return 'month' - default: - return 'date' - } - }, + getInputType: function () { + switch (this.props.type) { + case 'month': return 'month' + default: + return 'date' + } + }, - handleBlur: function () { - if (!this.props.onChange) - return + handleBlur: function () { + if (!this.props.onChange) + return - this.props.onChange && this.props.onChange.call(null, - this.getDateValue(), this.state.date - ) - }, + this.props.onChange && this.props.onChange.call(null, + this.getDateValue(), this.state.date + ) + }, - render: function () { - const props = { - onBlur: this.handleBlur, - onChange: (ev) => this.setState({date: ev.target.value}), + render: function () { + const props = { + onBlur: this.handleBlur, + onChange: (ev) => this.setState({date: ev.target.value}), - style: assign({ - display: 'inline-block', - }, this.props.style), + style: assign({ + display: 'inline-block', + }, this.props.style), - type: this.getInputType(), - value: this.state.date, - } + type: this.getInputType(), + value: this.state.date, + } - return React.createElement('input', props) - } + return React.createElement('input', props) + } }) export default DateInput diff --git a/src/components/metadata/FormField.jsx b/src/components/metadata/FormField.jsx index beb72c4..36e0c7b 100644 --- a/src/components/metadata/FormField.jsx +++ b/src/components/metadata/FormField.jsx @@ -10,189 +10,189 @@ import Button from '../Button.jsx' const T = React.PropTypes const FormField = React.createClass({ - propTypes: { - - /** - * implied-required fields - * --------- - * these are fields needed to actually accomplish anything, but will - * be provided by a form container (*cough* WorkMetadataForm *cough*). - * however, if we mark them as `.isRequired`, propTypes will throw - * bc they're not declared explicitly (but passed via the container's - * React.Children.map call) - */ - - onChange: T.func, - renderer: T.func, - value: T.any, - - /** - * optional fields - */ - - // whether or not a