diff --git a/package-lock.json b/package-lock.json index b06acf0fe4..01a0f1175f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4359,6 +4359,11 @@ "is-string": "^1.0.5" } }, + "array-move": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-move/-/array-move-3.0.1.tgz", + "integrity": "sha512-H3Of6NIn2nNU1gsVDqDnYKY/LCdWvCMMOWifNGhKcVQgiZ6nOek39aESOvro6zmueP07exSl93YLvkN4fZOkSg==" + }, "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", @@ -4937,11 +4942,10 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "bookbrainz-data": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/bookbrainz-data/-/bookbrainz-data-2.10.0.tgz", - "integrity": "sha512-xSHHfjw4/XvFh+UgTMTTfTQcV1fL8T4pWKMSE431YwouComOknb0twhoFhY82witVu0mBdKfBHoLkRgsi1/X9g==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bookbrainz-data/-/bookbrainz-data-2.11.0.tgz", + "integrity": "sha512-a6LQiE2u9PbuDHB3JEJ5YMdajb+T3krdD7PqpxZpNLS6+GxuR1Dm4rEQJ4UBEN1BXzEYWjUAgpiwif2gDEyFbA==", "requires": { - "bluebird": "^3.7.2", "bookshelf": "^1.2.0", "bookshelf-virtuals-plugin": "^0.1.1", "deep-diff": "^1.0.2", @@ -9194,9 +9198,9 @@ "dev": true }, "inflection": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", - "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.1.tgz", + "integrity": "sha512-dldYtl2WlN0QDkIDtg8+xFwOS2Tbmp12t1cHa5/YClU6ZQjTFm7B66UcVbh9NQB+HvT5BAd2t5+yKsBkw5pcqA==" }, "inflight": { "version": "1.0.6", @@ -9939,14 +9943,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "requires": { - "homedir-polyfill": "^1.0.1" - } } } }, @@ -13045,6 +13041,16 @@ "react-input-autosize": "^2.1.2" } }, + "react-sortable-hoc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz", + "integrity": "sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==", + "requires": { + "@babel/runtime": "^7.2.0", + "invariant": "^2.2.4", + "prop-types": "^15.5.7" + } + }, "react-sticky": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/react-sticky/-/react-sticky-6.0.3.tgz", @@ -15565,23 +15571,10 @@ "ms": "^2.1.1" } }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, @@ -17501,7 +17494,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "dev": true, "requires": { "homedir-polyfill": "^1.0.1" } diff --git a/package.json b/package.json index 62f39bd128..e53aef6f0e 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "@fortawesome/free-brands-svg-icons": "^5.14.0", "@fortawesome/free-solid-svg-icons": "^5.14.0", "@fortawesome/react-fontawesome": "^0.1.11", - "bookbrainz-data": "^2.10.0", + "array-move": "^3.0.1", + "bookbrainz-data": "^2.11.0", "chart.js": "^2.9.4", "chartjs-adapter-date-fns": "^1.0.0", "classnames": "^2.2.5", @@ -69,6 +70,7 @@ "react-hot-loader": "^4.13.0", "react-redux": "^5.1.2", "react-select": "^1.1.0", + "react-sortable-hoc": "^2.0.0", "react-sticky": "^6.0.1", "react-virtualized-select": "^3.0.1", "redux": "^3.7.2", diff --git a/src/client/components/pages/collection.js b/src/client/components/pages/collection.js index f42a4d3e67..2cf2193c88 100644 --- a/src/client/components/pages/collection.js +++ b/src/client/components/pages/collection.js @@ -18,48 +18,21 @@ import * as bootstrap from 'react-bootstrap'; import {faPencilAlt, faPlus, faTimesCircle, faTrashAlt} from '@fortawesome/free-solid-svg-icons'; +import {formatDate, getEntityKey, getEntityTable} from '../../helpers/utils'; import AddEntityToCollectionModal from './parts/add-entity-to-collection-modal'; -import AuthorTable from './entities/author-table'; import DeleteOrRemoveCollaborationModal from './parts/delete-or-remove-collaboration-modal'; import {ENTITY_TYPE_ICONS} from '../../helpers/entity'; -import EditionGroupTable from './entities/editionGroup-table'; -import EditionTable from './entities/edition-table'; import EntityImage from './entities/image'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import PagerElement from './parts/pager'; import PropTypes from 'prop-types'; -import PublisherTable from './entities/publisher-table'; import React from 'react'; -import WorkTable from './entities/work-table'; import _ from 'lodash'; -import {formatDate} from '../../helpers/utils'; import request from 'superagent'; const {Alert, Badge, Button, Col, Row} = bootstrap; -function getEntityTable(entityType) { - const tables = { - Author: AuthorTable, - Edition: EditionTable, - EditionGroup: EditionGroupTable, - Publisher: PublisherTable, - Work: WorkTable - }; - return tables[entityType]; -} - -function getEntityKey(entityType) { - const keys = { - Author: 'authors', - Edition: 'editions', - EditionGroup: 'editionGroups', - Publisher: 'publishers', - Work: 'works' - }; - return keys[entityType]; -} - function CollectionAttributes({collection}) { return (
diff --git a/src/client/components/pages/entities/author-table.js b/src/client/components/pages/entities/author-table.js index 2239cf0deb..e584ab13f1 100644 --- a/src/client/components/pages/entities/author-table.js +++ b/src/client/components/pages/entities/author-table.js @@ -29,6 +29,7 @@ const {transformISODateForDisplay, extractAttribute, getEntityDisambiguation, ge function AuthorTableRow({author, showAddedAtColumn, showCheckboxes, selectedEntities, onToggleRow}) { const name = getEntityLabel(author); const disambiguation = getEntityDisambiguation(author); + const number = author.number || '?'; const authorType = author.authorType ? author.authorType.label : '?'; const gender = author.gender ? author.gender.name : '?'; const beginDate = transformISODateForDisplay(extractAttribute(author.beginDate)); @@ -37,6 +38,7 @@ function AuthorTableRow({author, showAddedAtColumn, showCheckboxes, selectedEnti /* eslint-disable react/jsx-no-bind */ return ( + {author.displayNumber && {number}} { showCheckboxes ? @@ -83,7 +85,8 @@ function AuthorTable({authors, showAddedAtColumn, showCheckboxes, selectedEntiti - + {authors[0].displayNumber && } + diff --git a/src/client/components/pages/entities/edition-table.js b/src/client/components/pages/entities/edition-table.js index 9055c14393..c29e1a3a4c 100644 --- a/src/client/components/pages/entities/edition-table.js +++ b/src/client/components/pages/entities/edition-table.js @@ -37,6 +37,7 @@ const {Button, Table} = bootstrap; function EditionTableRow({edition, showAddedAtColumn, showCheckboxes, selectedEntities, onToggleRow}) { const name = getEntityLabel(edition); const disambiguation = getEntityDisambiguation(edition); + const number = edition.number || '?'; const releaseDate = getEditionReleaseDate(edition); const isbn = getISBNOfEdition(edition); const editionFormat = getEditionFormat(edition); @@ -45,6 +46,7 @@ function EditionTableRow({edition, showAddedAtColumn, showCheckboxes, selectedEn /* eslint-disable react/jsx-no-bind */ return ( + {edition.displayNumber && }
Name#Name Gender Type Date of birth
{number} { showCheckboxes ? @@ -101,6 +103,7 @@ function EditionTable({editions, entity, showAddedAtColumn, showAdd, showCheckbo + {editions[0].displayNumber && } diff --git a/src/client/components/pages/entities/editionGroup-table.js b/src/client/components/pages/entities/editionGroup-table.js index de5302d628..2d3dc38df2 100644 --- a/src/client/components/pages/entities/editionGroup-table.js +++ b/src/client/components/pages/entities/editionGroup-table.js @@ -30,6 +30,7 @@ const {getEntityDisambiguation, getEntityLabel} = entityHelper; function EditionGroupTableRow({editionGroup, showAddedAtColumn, showCheckboxes, selectedEntities, onToggleRow}) { const name = getEntityLabel(editionGroup); + const number = editionGroup.number || '?'; const disambiguation = getEntityDisambiguation(editionGroup); const editionGroupType = editionGroup.editionGroupType ? editionGroup.editionGroupType.label : '?'; const addedAt = showAddedAtColumn ? utilHelper.formatDate(new Date(editionGroup.addedAt), true) : null; @@ -37,6 +38,7 @@ function EditionGroupTableRow({editionGroup, showAddedAtColumn, showCheckboxes, /* eslint-disable react/jsx-no-bind */ return ( + {editionGroup.displayNumber && }
#Name Format ISBN
{number} { showCheckboxes ? @@ -78,6 +80,7 @@ function EditionGroupTable({editionGroups, showAddedAtColumn, showCheckboxes, se + {editionGroups[0].displayNumber && } { diff --git a/src/client/components/pages/entities/publisher-table.js b/src/client/components/pages/entities/publisher-table.js index b5f54dfbbc..424c3ddb95 100644 --- a/src/client/components/pages/entities/publisher-table.js +++ b/src/client/components/pages/entities/publisher-table.js @@ -29,6 +29,7 @@ const {transformISODateForDisplay, extractAttribute, getEntityDisambiguation, ge function PublisherTableRow({showAddedAtColumn, publisher, showCheckboxes, selectedEntities, onToggleRow}) { const name = getEntityLabel(publisher); + const number = publisher.number || '?'; const disambiguation = getEntityDisambiguation(publisher); const publisherType = publisher.publisherType ? publisher.publisherType.label : '?'; const area = publisher.area ? publisher.area.name : '?'; @@ -39,6 +40,7 @@ function PublisherTableRow({showAddedAtColumn, publisher, showCheckboxes, select /* eslint-disable react/jsx-no-bind */ return ( + {publisher.displayNumber && }
#Name Type
{number} { showCheckboxes ? @@ -83,7 +85,8 @@ function PublisherTable({showAddedAtColumn, publishers, showCheckboxes, selected - + {publishers[0].displayNumber && } + diff --git a/src/client/components/pages/entities/series.js b/src/client/components/pages/entities/series.js new file mode 100644 index 0000000000..6e7eff5cd4 --- /dev/null +++ b/src/client/components/pages/entities/series.js @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2021 Akash Gupta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as bootstrap from 'react-bootstrap'; +import * as entityHelper from '../../../helpers/entity'; +import {getEntityKey, getEntityTable} from '../../../helpers/utils'; +import EntityAnnotation from './annotation'; +import EntityFooter from './footer'; +import EntityImage from './image'; +import EntityLinks from './links'; +import EntityRelatedCollections from './related-collections'; +import EntityTitle from './title'; +import PropTypes from 'prop-types'; +import React from 'react'; + + +const {deletedEntityMessage, getEntityUrl, ENTITY_TYPE_ICONS, getSortNameOfDefaultAlias} = entityHelper; +const {Col, Row} = bootstrap; + +function SeriesAttributes({series}) { + if (series.deleted) { + return deletedEntityMessage; + } + const sortNameOfDefaultAlias = getSortNameOfDefaultAlias(series); + return ( +
+ +
+
+
Sort Name
+
{sortNameOfDefaultAlias}
+
+ + +
+
Series Type
+
{series.entityType}
+
+ + +
+
Ordering Type
+
{series.seriesOrderingType.label}
+
+ + +
+
Total Items
+
{series.seriesItems.length}
+
+ + + + ); +} +SeriesAttributes.displayName = 'SeriesAttributes'; +SeriesAttributes.propTypes = { + series: PropTypes.object.isRequired +}; + + +function SeriesDisplayPage({entity, identifierTypes, user}) { + const urlPrefix = getEntityUrl(entity); + const EntityTable = getEntityTable(entity.entityType); + const entityKey = getEntityKey(entity.entityType); + const propsForTable = { + [entityKey]: entity.seriesItems, + showAdd: false, + showAddedAtColumn: false, + showCheckboxes: false + }; + return ( +
+ +
+ + + + + + + + + + {!entity.deleted && + + + + + } +
+ + + ); +} +SeriesDisplayPage.displayName = 'SeriesDisplayPage'; +SeriesDisplayPage.propTypes = { + entity: PropTypes.object.isRequired, + identifierTypes: PropTypes.array, + user: PropTypes.object.isRequired +}; +SeriesDisplayPage.defaultProps = { + identifierTypes: [] +}; + +export default SeriesDisplayPage; diff --git a/src/client/components/pages/entities/work-table.js b/src/client/components/pages/entities/work-table.js index 1e1aa2b680..9d23040a43 100644 --- a/src/client/components/pages/entities/work-table.js +++ b/src/client/components/pages/entities/work-table.js @@ -35,6 +35,7 @@ const {getEntityDisambiguation, getLanguageAttribute, getEntityLabel} = entityHe function WorkTableRow({showAddedAtColumn, work, showCheckboxes, selectedEntities, onToggleRow}) { const name = getEntityLabel(work); + const number = work.number || '?'; const disambiguation = getEntityDisambiguation(work); const workType = work.workType ? work.workType.label : '?'; const languages = getLanguageAttribute(work).data; @@ -43,6 +44,7 @@ function WorkTableRow({showAddedAtColumn, work, showCheckboxes, selectedEntities /* eslint-disable react/jsx-no-bind */ return (
+ {work.displayNumber && }
Name#Name Area Type Date founded
{number} { showCheckboxes ? @@ -85,6 +87,7 @@ function WorkTable({entity, showAddedAtColumn, works, showAdd, showCheckboxes, s + {works[0].displayNumber && } diff --git a/src/client/containers/layout.js b/src/client/containers/layout.js index 528971a8f9..02bb69d079 100644 --- a/src/client/containers/layout.js +++ b/src/client/containers/layout.js @@ -142,6 +142,10 @@ class Layout extends React.Component { {genEntityIconHTMLElement('EditionGroup')} Edition Group + + {genEntityIconHTMLElement('Series')} + Series + {genEntityIconHTMLElement('Author')} diff --git a/src/client/controllers/entity/entity.js b/src/client/controllers/entity/entity.js index 8f3e41901b..cb5a18baf4 100644 --- a/src/client/controllers/entity/entity.js +++ b/src/client/controllers/entity/entity.js @@ -31,6 +31,7 @@ import Layout from '../../containers/layout'; import PublisherPage from '../../components/pages/entities/publisher'; import React from 'react'; import ReactDOM from 'react-dom'; +import SeriesPage from '../../components/pages/entities/series'; import WorkPage from '../../components/pages/entities/work'; @@ -39,6 +40,7 @@ const entityComponents = { edition: EditionPage, editionGroup: EditionGroupPage, publisher: PublisherPage, + series: SeriesPage, work: WorkPage }; const propsTarget = document.getElementById('props'); diff --git a/src/client/entity-editor/entity-editor.tsx b/src/client/entity-editor/entity-editor.tsx index 9a9a5555c2..542b9f9d5f 100644 --- a/src/client/entity-editor/entity-editor.tsx +++ b/src/client/entity-editor/entity-editor.tsx @@ -81,13 +81,13 @@ const EntityEditor = (props: Props) => { - { React.cloneElement( React.Children.only(children), {...props} ) } + diff --git a/src/client/entity-editor/helpers.ts b/src/client/entity-editor/helpers.ts index ed1c3c679f..39b64f3a50 100644 --- a/src/client/entity-editor/helpers.ts +++ b/src/client/entity-editor/helpers.ts @@ -25,6 +25,7 @@ import EditionSection from './edition-section/edition-section'; import EditionSectionMerge from './edition-section/edition-section-merge'; import PublisherSection from './publisher-section/publisher-section'; import PublisherSectionMerge from './publisher-section/publisher-section-merge'; +import SeriesSection from './series-section/series-section'; import WorkSection from './work-section/work-section'; import WorkSectionMerge from './work-section/work-section-merge'; import aliasEditorReducer from './alias-editor/reducer'; @@ -38,6 +39,7 @@ import identifierEditorReducer from './identifier-editor/reducer'; import nameSectionReducer from './name-section/reducer'; import publisherSectionReducer from './publisher-section/reducer'; import relationshipSectionReducer from './relationship-editor/reducer'; +import seriesSectionReducer from './series-section/reducer'; import submissionSectionReducer from './submission-section/reducer'; import {validateForm as validateAuthorForm} from './validators/author'; import {validateForm as validateEditionForm} from './validators/edition'; @@ -45,6 +47,7 @@ import { validateForm as validateEditionGroupForm } from './validators/edition-group'; import {validateForm as validatePublisherForm} from './validators/publisher'; +import {validateForm as validateSeriesForm} from './validators/series'; import {validateForm as validateWorkForm} from './validators/work'; import workSectionReducer from './work-section/reducer'; @@ -67,6 +70,7 @@ export function getEntitySection(entityType: string) { edition: EditionSection, editionGroup: EditionGroupSection, publisher: PublisherSection, + series: SeriesSection, work: WorkSection }; @@ -91,6 +95,7 @@ function getEntitySectionReducer(entityType: string) { edition: editionSectionReducer, editionGroup: editionGroupSectionReducer, publisher: publisherSectionReducer, + series: seriesSectionReducer, work: workSectionReducer }; @@ -103,6 +108,7 @@ export function getValidator(entityType: string) { edition: validateEditionForm, editionGroup: validateEditionGroupForm, publisher: validatePublisherForm, + series: validateSeriesForm, work: validateWorkForm }; diff --git a/src/client/entity-editor/relationship-editor/actions.ts b/src/client/entity-editor/relationship-editor/actions.ts index 73b73840e6..0614fe0dc7 100644 --- a/src/client/entity-editor/relationship-editor/actions.ts +++ b/src/client/entity-editor/relationship-editor/actions.ts @@ -1,6 +1,6 @@ /* * Copyright (C) 2018 Ben Ockmore - * + * 2021 Akash Gupta * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or @@ -15,8 +15,11 @@ * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +/* eslint-disable no-inline-comments */ -import type {Relationship} from './types'; +import type {Relationship, Attribute as _Attribute} from './types'; +import arrayMove from 'array-move'; +import {sortRelationshipOrdinal} from '../../../common/helpers/utils'; export const SHOW_RELATIONSHIP_EDITOR = 'SHOW_RELATIONSHIP_EDITOR'; @@ -25,6 +28,7 @@ export const ADD_RELATIONSHIP = 'ADD_RELATIONSHIP'; export const EDIT_RELATIONSHIP = 'EDIT_RELATIONSHIP'; export const REMOVE_RELATIONSHIP = 'REMOVE_RELATIONSHIP'; export const UNDO_LAST_SAVE = 'UNDO_LAST_SAVE'; +export const SORT_RELATIONSHIPS = 'SORT_RELATIONSHIPS'; export type Action = { type: string, @@ -51,6 +55,53 @@ export function addRelationship(data: Relationship): Action { }; } +/** + * This action creator first sorts the relationship object and then pass the + * sorted object in the payload while dispatching the action. + * + * @param {number} oldIndex - Old Position of the relationship. + * @param {number} newIndex - New Position of the relationship. + * @returns {void} + */ +export function sortRelationships(oldIndex, newIndex):any { + return (dispatch, getState) => { + const state = getState(); + const relationships = state.get('relationshipSection').get('relationships'); + const orderTypeValue = state.get('seriesSection').get('orderType'); + const relObject = relationships.toJS(); + const relArray = Object.entries(relObject); + const automaticSort = []; + let automaticSortedArr: [string, Relationship][]; // stores the sorted array of relationships(sorting performed on number) + + if (orderTypeValue === 1) { // OrderType 1 for Automatic Ordering + relArray.forEach((relationship:[string, Relationship]) => { + relationship[1].attributes.forEach((attribute:_Attribute) => { + if (attribute.attributeType === 2) { // Attribute Type 2 for number + automaticSort.push({number: attribute.value.textValue, relationship}); + } + }); + }); + automaticSort.sort(sortRelationshipOrdinal('number')); // sorts the array of relationships on number attribute + automaticSortedArr = automaticSort.map(item => item.relationship); + } + + // eslint-disable-next-line max-len + const sortedRelationships = orderTypeValue === 1 ? arrayMove(automaticSortedArr, oldIndex, newIndex) : arrayMove(relArray, oldIndex, newIndex); + + sortedRelationships.forEach((relationship: [string, Relationship], index: number) => { + relationship[1].attributes.forEach((attribute: _Attribute) => { + if (attribute.attributeType === 1) { // Attribute type 1 for position + attribute.value.textValue = `${index}`; // assigns the position value to the sorted relationship array + } + }); + }); + + const sortedRelationshipObject = Object.fromEntries(new Map([...sortedRelationships])); + const payload = sortedRelationshipObject; + dispatch({payload, type: SORT_RELATIONSHIPS}); + }; +} + export function editRelationship(rowID: number): Action { return { payload: rowID, diff --git a/src/client/entity-editor/relationship-editor/attributes.js b/src/client/entity-editor/relationship-editor/attributes.js new file mode 100644 index 0000000000..bcc39369ad --- /dev/null +++ b/src/client/entity-editor/relationship-editor/attributes.js @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2021 Akash Gupta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {ControlLabel} from 'react-bootstrap'; +import PropTypes from 'prop-types'; +import React from 'react'; + + +export function NumberAttribute({ + onHandleChange, value +}) { + return ( + <> + Number + + + ); +} + + +NumberAttribute.propTypes = { + onHandleChange: PropTypes.func.isRequired, + value: PropTypes.string +}; + +NumberAttribute.defaultProps = { + value: '' +}; diff --git a/src/client/entity-editor/relationship-editor/helper.js b/src/client/entity-editor/relationship-editor/helper.js new file mode 100644 index 0000000000..eb2bdb676d --- /dev/null +++ b/src/client/entity-editor/relationship-editor/helper.js @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2021 Akash Gupta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + + +/** + * A function to extract individual attribute object corresponding to a attribute id + * from attributes array. + * + * @param {array} attributes - Array of attributes. + * @param {number} id - Attribute id. + * @returns {void} + */ +export const getInitAttribute = (attributes, id) => { + const relAttribute = attributes.filter(attribute => attribute.attributeType === id); + if (relAttribute.length === 0) { + return []; + } + + return relAttribute[0]; +}; + + +/** + * A function to insert all the individual attribute object(number, position etc) to + * a array. + * + * @param {object} state - Relationship state. + * @param {array} attributeTypes - All the attribute types associated with a relationship type. + * @returns {array} Array of attributes. + */ +export const setAttribute = (state, attributeTypes) => { + const attributes = attributeTypes.map(attribute => attribute.name); + const result = []; + if (attributes.includes('number')) { + result.push(state.attributeNumber); + } + if (attributes.includes('position')) { + result.push(state.attributePosition); + } + return result; +}; + +/** + * This function returns the attribute name corresponding + * to a attribute ID. + * + * @param {number} id - Attribute ID. + * @returns {string} returns the attribute name. + */ +export const getAttributeName = (id) => { + switch (id) { + case 1: + return 'position'; + case 2: + return 'number'; + default: + return 'unnamed'; + } +}; diff --git a/src/client/entity-editor/relationship-editor/reducer.js b/src/client/entity-editor/relationship-editor/reducer.js index cb166cbb57..de568e9711 100644 --- a/src/client/entity-editor/relationship-editor/reducer.js +++ b/src/client/entity-editor/relationship-editor/reducer.js @@ -24,6 +24,7 @@ import { HIDE_RELATIONSHIP_EDITOR, REMOVE_RELATIONSHIP, SHOW_RELATIONSHIP_EDITOR, + SORT_RELATIONSHIPS, UNDO_LAST_SAVE } from './actions'; @@ -42,6 +43,8 @@ function reducer( case SHOW_RELATIONSHIP_EDITOR: return state.set('relationshipEditorVisible', true) .set('relationshipEditorProps', null); + case SORT_RELATIONSHIPS: + return state.set('relationships', Immutable.fromJS(action.payload)); case HIDE_RELATIONSHIP_EDITOR: return state.set('relationshipEditorVisible', false); case ADD_RELATIONSHIP: { diff --git a/src/client/entity-editor/relationship-editor/relationship-attribute.tsx b/src/client/entity-editor/relationship-editor/relationship-attribute.tsx new file mode 100644 index 0000000000..ccb69856d8 --- /dev/null +++ b/src/client/entity-editor/relationship-editor/relationship-attribute.tsx @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2021 Akash Gupta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as React from 'react'; +import type {Attribute} from './types'; +import PropTypes from 'prop-types'; +import {getAttributeName} from './helper'; + + +type RelationshipAttributeProps = { + attributes: Array, + showAttributes: boolean +}; + +function getAttributeForDisplay(attribute, key) { + const attributeName = getAttributeName(attribute.attributeType); + const attributeValue = attribute.value.textValue; + if (!attributeValue) { + return null; + } + switch (attributeName) { + case 'number': + return ({attributeName}: {attributeValue}); + default: + return null; + } +} + +function RelationshipAttribute({attributes, showAttributes}: RelationshipAttributeProps) { + if (showAttributes) { + return ( + <>{attributes.map((attribute, index) => getAttributeForDisplay(attribute, index))} + ); + } + return null; +} +RelationshipAttribute.displayName = 'RelationshipAttribute'; +RelationshipAttribute.propTypes = { + attributes: PropTypes.array, + showAttributes: PropTypes.bool +}; +RelationshipAttribute.defaultProps = { + attributes: [], + showAttributes: false +}; +export default RelationshipAttribute; diff --git a/src/client/entity-editor/relationship-editor/relationship-editor.tsx b/src/client/entity-editor/relationship-editor/relationship-editor.tsx index c77e07b743..4f5c9c23ef 100644 --- a/src/client/entity-editor/relationship-editor/relationship-editor.tsx +++ b/src/client/entity-editor/relationship-editor/relationship-editor.tsx @@ -1,6 +1,6 @@ /* * Copyright (C) 2018 Ben Ockmore - * + * 2021 Akash Gupta * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or @@ -34,12 +34,16 @@ import type { EntityType, RelationshipType, RelationshipWithLabel, - Relationship as _Relationship + Attribute as _Attribute, + Relationship as _Relationship, + setPosition as _setPosition } from './types'; import {faExternalLinkAlt, faPlus, faTimes} from '@fortawesome/free-solid-svg-icons'; +import {getInitAttribute, setAttribute} from './helper'; import EntitySearchFieldOption from '../common/entity-search-field-option'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {NumberAttribute} from './attributes'; import ReactSelect from 'react-select'; import Relationship from './relationship'; import _ from 'lodash'; @@ -144,17 +148,24 @@ type EntitySearchResult = { type RelationshipModalProps = { relationshipTypes: Array, baseEntity: Entity, + seriesType: string, initRelationship: _Relationship | null | undefined, languageOptions: Array<{label: string, value: number}>, onCancel?: () => unknown, onClose?: () => unknown, - onAdd?: (_Relationship) => unknown + onAdd?: (_Relationship) => unknown, + setPosition?: (_setPosition) => unknown }; + type RelationshipModalState = { + attributeSetId: number | null, relationshipType?: RelationshipType | null | undefined, relationship?: _Relationship | null | undefined, - targetEntity?: EntitySearchResult | null | undefined + targetEntity?: EntitySearchResult | null | undefined, + attributes?: _Attribute[], + attributePosition?: _Attribute, + attributeNumber?: _Attribute }; function getInitState( @@ -162,6 +173,10 @@ function getInitState( ): RelationshipModalState { if (_.isNull(initRelationship)) { return { + attributeNumber: {attributeType: 2, value: {textValue: null}}, + attributePosition: {attributeType: 1, value: {textValue: null}}, + attributeSetId: null, + attributes: [], relationship: null, relationshipType: null, targetEntity: null @@ -187,6 +202,9 @@ function getInitState( _.set(thisEntity, defaultAliasPath, baseEntityName); } } + const attributes = _.get(initRelationship, ['attributes']); + const attributePosition = getInitAttribute(attributes, 1); + const attributeNumber = getInitAttribute(attributes, 2); const searchFormatOtherEntity = otherEntity && { id: _.get(otherEntity, ['bbid']), @@ -198,6 +216,10 @@ function getInitState( }; return { + attributeNumber, + attributePosition, + attributeSetId: _.get(initRelationship, ['attributeSetId']), + attributes, relationship: initRelationship, relationshipType: _.get(initRelationship, ['relationshipType']), targetEntity: searchFormatOtherEntity @@ -258,11 +280,33 @@ class RelationshipModal }); }; + handleNumberAttributeChange = ({target}) => { + const value = target.value === '' ? null : target.value; + const attributeNumber = { + attributeType: 2, + value: {textValue: value} + }; + const attributePosition = { + attributeType: 1, + value: {textValue: null} + }; + this.setState({ + attributeNumber, + attributePosition + }); + }; + + handleAdd = () => { - const {onAdd} = this.props; + const {onAdd, setPosition, baseEntity} = this.props; if (onAdd) { if (this.state.relationship) { - onAdd(this.state.relationship); + const {relationship} = this.state; + relationship.attributes = setAttribute(this.state, this.state.relationshipType.attributeTypes); + onAdd(relationship); + if (baseEntity.type === 'Series') { + setPosition({newIndex: null, oldIndex: null}); + } } } }; @@ -289,9 +333,15 @@ class RelationshipModal } renderEntitySelect() { - const {baseEntity, relationshipTypes} = this.props; + const {baseEntity, relationshipTypes, seriesType} = this.props; const {targetEntity} = this.state; - const types = getValidOtherEntityTypes(relationshipTypes, baseEntity); + let types; + if (baseEntity.type === 'Series') { + types = [seriesType]; + } + else { + types = getValidOtherEntityTypes(relationshipTypes, baseEntity); + } if (!types.length) { return null; } @@ -347,6 +397,11 @@ class RelationshipModal relationshipTypes, baseEntity, otherEntity ); + const attributeTypes = this.state.relationshipType ? this.state.relationshipType.attributeTypes : null; + let attributes = []; + if (attributeTypes) { + attributes = attributeTypes.map(attribute => attribute.name); + } return ( Relationship @@ -363,6 +418,7 @@ class RelationshipModal {this.state.relationshipType && {this.state.relationshipType.description} } + {attributes.includes('number') ? : null} ); } diff --git a/src/client/entity-editor/relationship-editor/relationship-section.tsx b/src/client/entity-editor/relationship-editor/relationship-section.tsx index b981daecf1..9dfef70f29 100644 --- a/src/client/entity-editor/relationship-editor/relationship-section.tsx +++ b/src/client/entity-editor/relationship-editor/relationship-section.tsx @@ -1,6 +1,6 @@ /* * Copyright (C) 2018 Ben Ockmore - * + * 2021 Akash Gupta * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or @@ -27,6 +27,7 @@ import { hideRelationshipEditor, removeRelationship, showRelationshipEditor, + sortRelationships, undoLastSave } from './actions'; import {Button, ButtonGroup, Col, Row} from 'react-bootstrap'; @@ -36,8 +37,10 @@ import type { LanguageOption, RelationshipForDisplay, RelationshipType, - Relationship as _Relationship + Relationship as _Relationship, + setPosition as _setPosition } from './types'; +import {SortableContainer, SortableElement} from 'react-sortable-hoc'; import {faPencilAlt, faPlus, faTimes, faUndo} from '@fortawesome/free-solid-svg-icons'; import type {Dispatch} from 'redux'; // eslint-disable-line import/named import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; @@ -48,6 +51,73 @@ import _ from 'lodash'; import {connect} from 'react-redux'; +export function RelationshipListItem({contextEntity, onEdit, onRemove, attributes, relationshipType, sourceEntity, targetEntity, rowID, dragHandler}) { + /* eslint-disable react/jsx-no-bind */ + return ( + + + + + {(onEdit || onRemove) && + + + {onEdit && + + } + {onRemove && + + } + + + } + + + ); +} +const SortableItem = SortableElement(({value, onEdit, onRemove, contextEntity}) => { + const {relationshipType, sourceEntity, targetEntity, attributes, rowID} = value; + return ( + + ); +}); + +const SortableList = SortableContainer(({children}) =>
{children}
); + type RelationshipListProps = { contextEntity: Entity, relationships: Array, @@ -63,54 +133,24 @@ type RelationshipListProps = { export function RelationshipList( {contextEntity, relationships, onEdit, onRemove}: RelationshipListProps ) { - /* eslint-disable react/jsx-no-bind */ const renderedRelationships = _.map( relationships, - ({relationshipType, sourceEntity, targetEntity}, rowID) => ( - -
- - - {(onEdit || onRemove) && - - - {onEdit && - - } - {onRemove && - - } - - - } - + ({relationshipType, sourceEntity, targetEntity, attributes, rowID}) => ( + ) ); - /* eslint-enable react/jsx-no-bind */ - return
{renderedRelationships}
; } @@ -123,6 +163,8 @@ type OwnProps = { type StateProps = { canEdit: boolean, + seriesTypeValue: string, + orderTypeValue: number, entityName: string, relationships: Immutable.List, relationshipEditorProps: Immutable.Map, @@ -133,6 +175,7 @@ type StateProps = { type DispatchProps = { onAddRelationship: () => unknown, onEditorClose: () => unknown, + onSortRelationships: (_setPosition) => unknown, onEditorAdd: (_Relationship) => unknown, onEdit: (number) => unknown, onRemove: (number) => unknown, @@ -144,8 +187,8 @@ type Props = OwnProps & StateProps & DispatchProps; function RelationshipSection({ canEdit, entity, entityType, entityName, languageOptions, showEditor, relationships, - relationshipEditorProps, relationshipTypes, onAddRelationship, - onEditorClose, onEditorAdd, onEdit, onRemove, onUndo, undoPossible + relationshipEditorProps, relationshipTypes, orderTypeValue, seriesTypeValue, onAddRelationship, + onEditorClose, onEditorAdd, onSortRelationships, onEdit, onRemove, onUndo, undoPossible }: Props) { const baseEntity = { bbid: _.get(entity, 'bbid'), @@ -156,6 +199,7 @@ function RelationshipSection({ type: _.upperFirst(entityType) }; const relationshipsObject = relationships.toJS(); + const relationshipsArray: Array = Object.values(relationshipsObject); /* If one of the relationships is to a new entity (in creation), update that new entity's name to replace "New Entity" */ @@ -185,6 +229,8 @@ function RelationshipSection({ } languageOptions={languageOptionsForDisplay} relationshipTypes={relationshipTypes} + seriesType={seriesTypeValue} + setPosition={onSortRelationships} onAdd={onEditorAdd} onCancel={onEditorClose} onClose={onEditorClose} @@ -197,12 +243,25 @@ function RelationshipSection({

How are other entities related to this {_.startCase(entityType)}?

- + {orderTypeValue === 2 ? + + {relationshipsArray.map((value, index) => ( + + ))} + : + } {canEdit && @@ -246,14 +305,32 @@ RelationshipSection.displayName = 'RelationshipSection'; RelationshipSection.propTypes = { languageOptions: PropTypes.array.isRequired }; +RelationshipListItem.propTypes = { + attributes: PropTypes.array, + contextEntity: PropTypes.object.isRequired, + dragHandler: PropTypes.bool, + onEdit: PropTypes.func.isRequired, + onRemove: PropTypes.func.isRequired, + relationshipType: PropTypes.object.isRequired, + rowID: PropTypes.string.isRequired, + sourceEntity: PropTypes.object.isRequired, + targetEntity: PropTypes.object.isRequired +}; +RelationshipListItem.defaultProps = { + attributes: [], + dragHandler: false +}; + function mapStateToProps(rootState): StateProps { const state = rootState.get('relationshipSection'); return { canEdit: state.get('canEdit'), entityName: rootState.getIn(['nameSection', 'name']), + orderTypeValue: rootState.getIn(['seriesSection', 'orderType']), relationshipEditorProps: state.get('relationshipEditorProps'), relationships: state.get('relationships'), + seriesTypeValue: rootState.getIn(['seriesSection', 'seriesType']), showEditor: state.get('relationshipEditorVisible'), undoPossible: state.get('lastRelationships') !== null }; @@ -266,6 +343,7 @@ function mapDispatchToProps(dispatch: Dispatch): DispatchProps { onEditorAdd: (data) => dispatch(addRelationship(data)), onEditorClose: () => dispatch(hideRelationshipEditor()), onRemove: (rowID) => dispatch(removeRelationship(rowID)), + onSortRelationships: ({oldIndex, newIndex}) => dispatch(sortRelationships(oldIndex, newIndex)), onUndo: () => dispatch(undoLastSave()) }; } diff --git a/src/client/entity-editor/relationship-editor/relationship.tsx b/src/client/entity-editor/relationship-editor/relationship.tsx index c0f8ef88d0..c1e2bb553a 100644 --- a/src/client/entity-editor/relationship-editor/relationship.tsx +++ b/src/client/entity-editor/relationship-editor/relationship.tsx @@ -18,10 +18,13 @@ import * as React from 'react'; +import type {Attribute, RelationshipType, Entity as _Entity} from './types'; import {OverlayTrigger, Tooltip} from 'react-bootstrap'; -import type {RelationshipType, Entity as _Entity} from './types'; import Entity from '../common/entity'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import RelationshipAttribute from './relationship-attribute'; import _ from 'lodash'; +import {faBars} from '@fortawesome/free-solid-svg-icons'; import {getEntityLink} from '../../../server/helpers/utils'; @@ -46,11 +49,14 @@ type RelationshipProps = { contextEntity: _Entity | null | undefined, // eslint-disable-line react/require-default-props sourceEntity: _Entity, targetEntity: _Entity, + attributes?: Array, + showAttributes?: boolean, + dragHandler: boolean, relationshipType: RelationshipType }; function Relationship({ - contextEntity, link, relationshipType, sourceEntity, targetEntity + contextEntity, link, relationshipType, sourceEntity, attributes, showAttributes, dragHandler, targetEntity }: RelationshipProps) { const {depth, description, id, linkPhrase, reverseLinkPhrase} = relationshipType; @@ -79,17 +85,22 @@ function Relationship({ placement="bottom" >
+ {dragHandler ? <>    : null} {` ${usedLinkPhrase} `} + {' '} +
); } Relationship.displayName = 'Relationship'; Relationship.defaultProps = { + attributes: [], contextEntity: null, // eslint-disable-line react/default-props-match-prop-types, max-len - link: false // eslint-disable-line react/default-props-match-prop-types + link: false, // eslint-disable-line react/default-props-match-prop-types + showAttributes: false }; export default Relationship; diff --git a/src/client/entity-editor/relationship-editor/types.ts b/src/client/entity-editor/relationship-editor/types.ts index c102698639..f1a2d1f28e 100644 --- a/src/client/entity-editor/relationship-editor/types.ts +++ b/src/client/entity-editor/relationship-editor/types.ts @@ -26,8 +26,18 @@ export type Entity = { type: EntityType }; +export type AttributeTypes = { + id: number, + parent: number | null, + root: number, + childOrder: number | null, + name: string, + description: string | null, +}; + export type RelationshipType = { id: number, + attributeTypes?: Array, childOrder: number, deprecated: boolean, depth?: number, @@ -39,11 +49,19 @@ export type RelationshipType = { sourceEntityType: EntityType, targetEntityType: EntityType }; +export type Attribute = { + attributeType: number, + value: { + textValue: string | null + }, + type?: AttributeTypes +}; export type Relationship = { relationshipType: RelationshipType, sourceEntity: Entity, - targetEntity: Entity + targetEntity: Entity, + attributes?: Array }; export type RelationshipWithLabel = { @@ -54,10 +72,11 @@ export type RelationshipWithLabel = { }; export type RelationshipForDisplay = { + attributes: Array, relationshipType: RelationshipType, sourceEntity: Entity, targetEntity: Entity, - rowID: number + rowID: string }; export type LanguageOption = { @@ -65,6 +84,11 @@ export type LanguageOption = { id: number }; +export type setPosition = { + oldIndex: number | null, + newIndex: number | null +}; + export enum RelationshipTypes { AuthorWorkedOnWork = 1, AuthorIllustratedEdition = 2, diff --git a/src/client/entity-editor/series-section/actions.ts b/src/client/entity-editor/series-section/actions.ts new file mode 100644 index 0000000000..ca6df653d3 --- /dev/null +++ b/src/client/entity-editor/series-section/actions.ts @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2021 Akash Gupta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + + +export const UPDATE_ORDER_TYPE = 'UPDATE_ORDER_TYPE'; +export const UPDATE_SERIES_TYPE = 'UPDATE_SERIES_TYPE'; + +export type Action = { + type: string, + payload?: unknown +}; + + +/** + * Produces an action indicating that the series type for the series being + * edited should be updated with the provided value. + * + * @param {number} seriesType - The new value to be used for the series type ID. + * @returns {Action} The resulting UPDATE_SERIES_TYPE action. + */ + +export function updateSeriesType(seriesType: string): Action { + return { + payload: seriesType, + type: UPDATE_SERIES_TYPE + }; +} + +/** + * Produces an action indicating that the ordering type for the series being + * edited should be updated with the provided value. + * + * @param {number} newType - The new value to be used for the series type ID. + * @returns {Action} The resulting UPDATE_ORDER_TYPE action. + */ +export function updateOrderType(newType: number | null | undefined): Action { + return { + payload: newType, + type: UPDATE_ORDER_TYPE + }; +} diff --git a/src/client/entity-editor/series-section/reducer.ts b/src/client/entity-editor/series-section/reducer.ts new file mode 100644 index 0000000000..ba645c8a09 --- /dev/null +++ b/src/client/entity-editor/series-section/reducer.ts @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2021 Akash Gupta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + + +import * as Immutable from 'immutable'; +import { + Action, UPDATE_ORDER_TYPE, UPDATE_SERIES_TYPE +} from './actions'; + + +type State = Immutable.Map; + +function reducer( + state: State = Immutable.Map({ + orderType: 1, + seriesType: 'Author' + }), + action: Action +): State { + const {type, payload} = action; + switch (type) { + case UPDATE_ORDER_TYPE: + return state.set('orderType', payload); + case UPDATE_SERIES_TYPE: + return state.set('seriesType', payload); + // no default + } + return state; +} + +export default reducer; diff --git a/src/client/entity-editor/series-section/series-section.tsx b/src/client/entity-editor/series-section/series-section.tsx new file mode 100644 index 0000000000..da54291c6d --- /dev/null +++ b/src/client/entity-editor/series-section/series-section.tsx @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2021 Akash Gupta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + + +import * as Immutable from 'immutable'; +import * as React from 'react'; +import {Action, updateOrderType, updateSeriesType} from './actions'; +import {Col, Row} from 'react-bootstrap'; + +import CustomInput from '../../input'; +import type {Dispatch} from 'redux'; +import Select from 'react-select'; +import {connect} from 'react-redux'; +import {sortRelationships} from '../relationship-editor/actions'; + + +type SeriesOrderingType = { + label: string, + id: number +}; + +type StateProps = { + orderTypeValue: number, + seriesTypeValue: string, + relationships: Immutable.List[] +}; + + +type DispatchProps = { + onOrderTypeChange: (obj: {value: number}) => unknown, + onSeriesTypeChange: (obj: {value: string}) => unknown +}; + +type OwnProps = { + seriesOrderingTypes: Array +}; + +type Props = StateProps & DispatchProps & OwnProps; + +/** + * Container component. The SeriesSection component contains input fields + * specific to the series entity. The intention is that this component is + * rendered as a modular section within the entity editor. + * + * @param {Object} props - The properties passed to the component. + * @param {Array} props.seriesOrderingTypes - The list of possible ordering + * types for a series. + * @param {number} props.orderTypeValue - The ID of the ordering type currently selected for + * the series. + * @param {string} props.seriesTypeValue - The value of the entity type currently selected for + * the series. + * @param {Immutable.List[]} props.relationships - The list of relationships conatined by + * the series. + * @param {Function} props.onOrderTypeChange - A function to be called when + * a different ordering type is selected. + * @param {Function} props.onSeriesTypeChange - A function to be called when + * a different series type is selected. + * @returns {ReactElement} React element containing the rendered + * SeriesSection. + */ +function SeriesSection({ + seriesOrderingTypes, + orderTypeValue, + seriesTypeValue, + relationships, + onOrderTypeChange, + onSeriesTypeChange +}: Props) { + const seriesOrderingTypesForDisplay = seriesOrderingTypes.map((type) => ({ + label: type.label, + value: type.id + })); + const seriesTypesForDisplay = ['Author', 'Work', 'Edition', 'EditionGroup', 'Publisher'].map((entity) => ({ + label: entity, + value: entity + })); + return ( +
+

+ What else do you know about the Series? +

+

+ All fields are mandatory — select the option from dropdown +

+ +
+ + + + + + + ); +} +SeriesSection.displayName = 'SeriesSection'; + +function mapStateToProps(rootState): StateProps { + const state = rootState.get('seriesSection'); + + return { + orderTypeValue: state.get('orderType'), + relationships: rootState.getIn(['relationshipSection', 'relationships']).toArray(), + seriesTypeValue: state.get('seriesType') + }; +} + +function mapDispatchToProps(dispatch: Dispatch): DispatchProps { + return { + onOrderTypeChange: (value) => { + dispatch(updateOrderType(value && value.value)); + if (value && value.value === 1) { + dispatch(sortRelationships(null, null)); + } + }, + onSeriesTypeChange: (value) => dispatch(updateSeriesType(value && value.value)) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(SeriesSection); diff --git a/src/client/entity-editor/validators/series.ts b/src/client/entity-editor/validators/series.ts new file mode 100644 index 0000000000..996952eb55 --- /dev/null +++ b/src/client/entity-editor/validators/series.ts @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2021 Akash Gupta + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + + +import {get, validatePositiveInteger} from './base'; +import { + validateAliases, + validateIdentifiers, + validateNameSection, + validateSubmissionSection +} from './common'; + +import _ from 'lodash'; +import type {_IdentifierType} from '../../../types'; + + +export function validateSeriesSectionOrderingType(value: any): boolean { + return validatePositiveInteger(value, true); +} + +export function validateSeriesSectionEntityType(value: any): boolean { + const entity = ['Author', 'Work', 'Edition', 'EditionGroup', 'Publisher']; + return entity.includes(value); +} + +export function validateSeriesSection(data: any): boolean { + return ( + validateSeriesSectionOrderingType(get(data, 'orderType', null)) && + validateSeriesSectionEntityType(get(data, 'seriesType', null)) + + ); +} + +export function validateForm( + formData: any, identifierTypes?: Array<_IdentifierType> | null | undefined +): boolean { + const conditions = [ + validateAliases(get(formData, 'aliasEditor', {})), + validateIdentifiers( + get(formData, 'identifierEditor', {}), identifierTypes + ), + validateNameSection(get(formData, 'nameSection', {})), + validateSeriesSection(get(formData, 'seriesSection', {})), + validateSubmissionSection(get(formData, 'submissionSection', {})) + ]; + + return _.every(conditions); +} diff --git a/src/client/helpers/entity.tsx b/src/client/helpers/entity.tsx index 6a539fc36d..927c5b51ce 100644 --- a/src/client/helpers/entity.tsx +++ b/src/client/helpers/entity.tsx @@ -23,7 +23,7 @@ import * as React from 'react'; import {FontAwesomeIconProps as FAProps, FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {get as _get, isNil as _isNil, kebabCase as _kebabCase, upperFirst} from 'lodash'; import { - faBook, faGlobe, faGripVertical, faPenNib, faUniversity, faUser, faUserCircle, faWindowRestore + faBook, faGlobe, faGripVertical, faLayerGroup, faPenNib, faUniversity, faUser, faUserCircle, faWindowRestore } from '@fortawesome/free-solid-svg-icons'; import {format, isValid, parseISO} from 'date-fns'; import {dateObjectToISOString} from './utils'; @@ -249,6 +249,7 @@ export const ENTITY_TYPE_ICONS = { EditionGroup: faWindowRestore, Editor: faUserCircle, Publisher: faUniversity, + Series: faLayerGroup, Work: faPenNib }; diff --git a/src/client/helpers/react-validators.js b/src/client/helpers/react-validators.js index c1635be2fb..cda8d0c4b1 100644 --- a/src/client/helpers/react-validators.js +++ b/src/client/helpers/react-validators.js @@ -6,6 +6,7 @@ export const entityTypeProperty = PropTypes.oneOf([ 'edition', 'editionGroup', 'publisher', + 'series', 'work' ]); diff --git a/src/client/helpers/utils.tsx b/src/client/helpers/utils.tsx index 39425a616c..0263525997 100644 --- a/src/client/helpers/utils.tsx +++ b/src/client/helpers/utils.tsx @@ -1,6 +1,6 @@ /* * Copyright (C) 2016 Daniel Hsing - * + * 2021 Akash Gupta * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or @@ -17,8 +17,13 @@ */ /* eslint-disable no-useless-escape */ +import AuthorTable from '../components/pages/entities/author-table'; import DOMPurify from 'isomorphic-dompurify'; +import EditionGroupTable from '../components/pages/entities/editionGroup-table'; +import EditionTable from '../components/pages/entities/edition-table'; +import PublisherTable from '../components/pages/entities/publisher-table'; import React from 'react'; +import WorkTable from '../components/pages/entities/work-table'; import _ from 'lodash'; import {format} from 'date-fns'; import {isIterable} from '../../types'; @@ -177,3 +182,26 @@ export function stringToHTMLWithLinks(string: string) { // eslint-disable-next-line react/no-danger return ; } + + +export function getEntityTable(entityType: string) { + const tables = { + Author: AuthorTable, + Edition: EditionTable, + EditionGroup: EditionGroupTable, + Publisher: PublisherTable, + Work: WorkTable + }; + return tables[entityType]; +} + +export function getEntityKey(entityType:string) { + const keys = { + Author: 'authors', + Edition: 'editions', + EditionGroup: 'editionGroups', + Publisher: 'publishers', + Work: 'works' + }; + return keys[entityType]; +} diff --git a/src/common/helpers/search.js b/src/common/helpers/search.js index 020d1b5773..7aaab48f3e 100644 --- a/src/common/helpers/search.js +++ b/src/common/helpers/search.js @@ -246,7 +246,7 @@ export function refreshIndex() { /* eslint camelcase: 0, no-magic-numbers: 1 */ export async function generateIndex(orm) { - const {Area, Author, Edition, EditionGroup, Editor, Publisher, UserCollection, Work} = orm; + const {Area, Author, Edition, EditionGroup, Editor, Publisher, Series, UserCollection, Work} = orm; const indexMappings = { mappings: { _default_: { @@ -356,6 +356,7 @@ export async function generateIndex(orm) { }, {model: EditionGroup, relations: ['editionGroupType']}, {model: Publisher, relations: ['publisherType', 'area']}, + {model: Series, relations: ['seriesOrderingType']}, {model: Work, relations: ['workType']} ]; @@ -496,7 +497,7 @@ export function searchByName(orm, name, type, size, from) { let modifiedType; if (type === 'all_entities') { - modifiedType = ['author', 'edition', 'edition_group', 'work', 'publisher']; + modifiedType = ['author', 'edition', 'edition_group', 'series', 'work', 'publisher']; } else { modifiedType = type; diff --git a/src/common/helpers/utils.ts b/src/common/helpers/utils.ts index af031dff37..9ba111ae12 100644 --- a/src/common/helpers/utils.ts +++ b/src/common/helpers/utils.ts @@ -26,12 +26,13 @@ export function isValidBBID(bbid: string): boolean { * @returns {object} - Object mapping model name to the entity model */ export function getEntityModels(orm: any) { - const {Author, Edition, EditionGroup, Publisher, Work} = orm; + const {Author, Edition, EditionGroup, Publisher, Series, Work} = orm; return { Author, Edition, EditionGroup, Publisher, + Series, Work }; } @@ -77,3 +78,17 @@ export function makePromiseFromObject(obj: Unresolved): Promise { return res as T; }); } + +/** + * This function sorts the relationship array + * @param {string} sortByProperty - name of property which will be used for sorting + * @returns {array} - sorted relationship array + */ +/* eslint-disable no-param-reassign */ +export function sortRelationshipOrdinal(sortByProperty: string) { + return (a:string, b:string) => { + a = a[sortByProperty] || ''; + b = b[sortByProperty] || ''; + return a.localeCompare(b); + }; +} diff --git a/src/server/helpers/middleware.ts b/src/server/helpers/middleware.ts index e4685be0a1..124e649638 100644 --- a/src/server/helpers/middleware.ts +++ b/src/server/helpers/middleware.ts @@ -1,7 +1,7 @@ /* * Copyright (C) 2015 Ben Ockmore * 2015-2016 Sean Burke - * + * 2021 Akash Gupta * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or @@ -29,11 +29,11 @@ interface $Request extends Request { user: any } -function makeLoader(modelName, propName, sortFunc?) { +function makeLoader(modelName, propName, sortFunc?, relations = []) { return function loaderFunc(req: $Request, res: $Response, next: NextFunction) { const {orm}: any = req.app.locals; const model = orm[modelName]; - return model.fetchAll() + return model.fetchAll({withRelated: [...relations]}) .then((results) => { const resultsSerial = results.toJSON(); @@ -58,8 +58,10 @@ export const loadEditionGroupTypes = makeLoader('EditionGroupType', 'editionGroupTypes'); export const loadPublisherTypes = makeLoader('PublisherType', 'publisherTypes'); export const loadWorkTypes = makeLoader('WorkType', 'workTypes'); +export const loadSeriesOrderingTypes = + makeLoader('SeriesOrderingType', 'seriesOrderingTypes'); export const loadRelationshipTypes = - makeLoader('RelationshipType', 'relationshipTypes'); + makeLoader('RelationshipType', 'relationshipTypes', null, ['attributeTypes']); export const loadGenders = makeLoader('Gender', 'genders', (a, b) => a.id > b.id); @@ -72,6 +74,35 @@ export const loadLanguages = makeLoader('Language', 'languages', (a, b) => { return a.name.localeCompare(b.name); }); +export function loadSeriesItems(req: $Request, res: $Response, next: NextFunction) { + try { + const {entity} = res.locals; + if (entity.dataId) { + const {relationships} = entity; + + if (entity.seriesOrderingType.label === 'Manual') { + relationships.sort(commonUtils.sortRelationshipOrdinal('position')); + } + else { + relationships.sort(commonUtils.sortRelationshipOrdinal('number')); + } + const seriesItems = relationships.map((rel) => ( + {...rel.source, displayNumber: true, + number: rel.number, + position: rel.position} + )); + res.locals.entity.seriesItems = seriesItems; + } + else { + res.locals.entity.seriesItems = []; + } + return next(); + } + catch (err) { + return next(err); + } +} + export function loadEntityRelationships(req: $Request, res: $Response, next: NextFunction) { const {orm}: any = req.app.locals; const {RelationshipSet} = orm; @@ -91,7 +122,9 @@ export function loadEntityRelationships(req: $Request, res: $Response, next: Nex withRelated: [ 'relationships.source', 'relationships.target', - 'relationships.type' + 'relationships.type.attributeTypes', + 'relationships.attributeSet.relationshipAttributes.value', + 'relationships.attributeSet.relationshipAttributes.type' ] }) ) @@ -99,6 +132,15 @@ export function loadEntityRelationships(req: $Request, res: $Response, next: Nex entity.relationships = relationshipSet ? relationshipSet.related('relationships').toJSON() : []; + // Attach attributes to relationship object + entity.relationships.forEach((relationship) => { + if (relationship.attributeSet?.relationshipAttributes) { + relationship.attributeSet.relationshipAttributes.forEach(attribute => { + relationship[`${attribute.type.name}`] = attribute.value.textValue; + }); + } + }); + async function getEntityWithAlias(relEntity) { const redirectBbid = await orm.func.entity.recursivelyGetRedirectBBID(orm, relEntity.bbid, null); const model = commonUtils.getEntityModelByType(orm, relEntity.type); diff --git a/src/server/routes.js b/src/server/routes.js index fd4d580f9b..56a29a35e3 100644 --- a/src/server/routes.js +++ b/src/server/routes.js @@ -31,6 +31,7 @@ import registerRouter from './routes/register'; import revisionRouter from './routes/revision'; import revisionsRouter from './routes/revisions'; import searchRouter from './routes/search'; +import seriesRouter from './routes/entity/series'; import statisticsRouter from './routes/statistics'; import workRouter from './routes/entity/work'; @@ -63,6 +64,10 @@ function initMergeRoutes(app) { app.use('/merge', mergeRouter); } +function initSeriesRoutes(app) { + app.use('/series', seriesRouter); +} + function initWorkRoutes(app) { app.use('/work', workRouter); } @@ -90,6 +95,7 @@ function initRoutes(app) { initCollectionRoutes(app); initEditionRoutes(app); initMergeRoutes(app); + initSeriesRoutes(app); initWorkRoutes(app); initPublisherRoutes(app); initRevisionRoutes(app); diff --git a/src/server/routes/entity/author.js b/src/server/routes/entity/author.js index d07437a4a4..069f0b760b 100644 --- a/src/server/routes/entity/author.js +++ b/src/server/routes/entity/author.js @@ -227,9 +227,11 @@ function authorToFormState(author) { }; author.relationships.forEach((relationship) => ( - relationshipSection.relationships[relationship.id] = { + relationshipSection.relationships[`n${relationship.id}`] = { + attributeSetId: relationship.attributeSetId, + attributes: relationship.attributeSet ? relationship.attributeSet.relationshipAttributes : [], relationshipType: relationship.type, - rowID: relationship.id, + rowID: `n${relationship.id}`, sourceEntity: relationship.source, targetEntity: relationship.target } diff --git a/src/server/routes/entity/edition-group.js b/src/server/routes/entity/edition-group.js index 46458ec5fc..5f6bcf3b01 100644 --- a/src/server/routes/entity/edition-group.js +++ b/src/server/routes/entity/edition-group.js @@ -213,9 +213,11 @@ function editionGroupToFormState(editionGroup) { }; editionGroup.relationships.forEach((relationship) => ( - relationshipSection.relationships[relationship.id] = { + relationshipSection.relationships[`n${relationship.id}`] = { + attributeSetId: relationship.attributeSetId, + attributes: relationship.attributeSet ? relationship.attributeSet.relationshipAttributes : [], relationshipType: relationship.type, - rowID: relationship.id, + rowID: `n${relationship.id}`, sourceEntity: relationship.source, targetEntity: relationship.target } diff --git a/src/server/routes/entity/edition.js b/src/server/routes/entity/edition.js index 611671ecff..7eb7dbaafb 100644 --- a/src/server/routes/entity/edition.js +++ b/src/server/routes/entity/edition.js @@ -354,9 +354,11 @@ function editionToFormState(edition) { }; edition.relationships.forEach((relationship) => ( - relationshipSection.relationships[relationship.id] = { + relationshipSection.relationships[`n${relationship.id}`] = { + attributeSetId: relationship.attributeSetId, + attributes: relationship.attributeSet ? relationship.attributeSet.relationshipAttributes : [], relationshipType: relationship.type, - rowID: relationship.id, + rowID: `n${relationship.id}`, sourceEntity: relationship.source, targetEntity: relationship.target } diff --git a/src/server/routes/entity/entity.tsx b/src/server/routes/entity/entity.tsx index 095035f644..1766c5755a 100644 --- a/src/server/routes/entity/entity.tsx +++ b/src/server/routes/entity/entity.tsx @@ -1,7 +1,7 @@ /* * Copyright (C) 2016 Ben Ockmore * 2016 Sean Burke - * + * 2021 Akash Gupta * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or @@ -44,6 +44,7 @@ import EntityRevisions from '../../../client/components/pages/entity-revisions'; import Layout from '../../../client/containers/layout'; import PublisherPage from '../../../client/components/pages/entities/publisher'; import ReactDOMServer from 'react-dom/server'; +import SeriesPage from '../../../client/components/pages/entities/series'; import WorkPage from '../../../client/components/pages/entities/work'; import _ from 'lodash'; import {getEntityLabel} from '../../../client/helpers/entity'; @@ -60,6 +61,7 @@ const entityComponents = { edition: EditionPage, editionGroup: EditionGroupPage, publisher: PublisherPage, + series: SeriesPage, work: WorkPage }; @@ -825,12 +827,33 @@ async function getNextIdentifierSet(orm, transacting, currentEntity, body) { orm, transacting, oldIdentifierSet, body.identifiers || [] ); } +async function getNextRelationshipAttributeSets(orm, transacting, body) { + const {RelationshipAttributeSet} = orm; + const relationships = await Promise.all(body.relationships.map(async (relationship) => { + const id = relationship.attributeSetId; + const oldRelationshipAttributeSet = await ( + id && + new RelationshipAttributeSet({id}).fetch({ + require: false, + transacting, withRelated: ['relationshipAttributes.value'] + }) + ); + const attributeSet = await orm.func.relationshipAttributes.updateRelationshipAttributeSet( + orm, transacting, oldRelationshipAttributeSet, relationship.attributes || [] + ); + const attributeSetId = attributeSet && attributeSet.get('id'); + relationship.attributeSetId = attributeSetId; + delete relationship.attributes; + return relationship; + })); + return relationships; +} async function getNextRelationshipSets( orm, transacting, currentEntity, body ) { const {RelationshipSet} = orm; - + const relationships = await getNextRelationshipAttributeSets(orm, transacting, body); const id = _.get(currentEntity, ['relationshipSet', 'id']); const oldRelationshipSet = await ( @@ -842,7 +865,7 @@ async function getNextRelationshipSets( ); return orm.func.relationship.updateRelationshipSets( - orm, transacting, oldRelationshipSet, body.relationships || [] + orm, transacting, oldRelationshipSet, relationships || [] ); } @@ -1174,7 +1197,9 @@ export function constructIdentifiers( export function constructRelationships(relationshipSection) { return _.map( relationshipSection.relationships, - ({rowID, relationshipType, sourceEntity, targetEntity}) => ({ + ({attributeSetId, rowID, relationshipType, sourceEntity, targetEntity, attributes}) => ({ + attributeSetId, + attributes, id: rowID, sourceBbid: _.get(sourceEntity, 'bbid'), targetBbid: _.get(targetEntity, 'bbid'), diff --git a/src/server/routes/entity/publisher.js b/src/server/routes/entity/publisher.js index 8ca35764d2..77f2bcda5c 100644 --- a/src/server/routes/entity/publisher.js +++ b/src/server/routes/entity/publisher.js @@ -237,9 +237,11 @@ function publisherToFormState(publisher) { }; publisher.relationships.forEach((relationship) => ( - relationshipSection.relationships[relationship.id] = { + relationshipSection.relationships[`n${relationship.id}`] = { + attributeSetId: relationship.attributeSetId, + attributes: relationship.attributeSet ? relationship.attributeSet.relationshipAttributes : [], relationshipType: relationship.type, - rowID: relationship.id, + rowID: `n${relationship.id}`, sourceEntity: relationship.source, targetEntity: relationship.target } diff --git a/src/server/routes/entity/series.js b/src/server/routes/entity/series.js new file mode 100644 index 0000000000..cb63b2a94a --- /dev/null +++ b/src/server/routes/entity/series.js @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2021 Akash Gupta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + + +import * as auth from '../../helpers/auth'; +import * as entityRoutes from './entity'; +import * as middleware from '../../helpers/middleware'; +import * as utils from '../../helpers/utils'; + +import { + entityEditorMarkup, + generateEntityProps, + makeEntityCreateOrEditHandler +} from '../../helpers/entityRouteUtils'; + +import _ from 'lodash'; +import {escapeProps} from '../../helpers/props'; +import express from 'express'; +import {sortRelationshipOrdinal} from '../../../common/helpers/utils'; +import target from '../../templates/target'; + +/** **************************** +*********** Helpers ************ +*******************************/ +const additionalSeriesProps = [ + 'entityType', 'orderingTypeId' +]; + +function transformNewForm(data) { + const aliases = entityRoutes.constructAliases( + data.aliasEditor, data.nameSection + ); + + const identifiers = entityRoutes.constructIdentifiers( + data.identifierEditor + ); + const relationships = entityRoutes.constructRelationships( + data.relationshipSection + ); + + return { + aliases, + annotation: data.annotationSection.content, + disambiguation: data.nameSection.disambiguation, + entityType: data.seriesSection.seriesType, + identifiers, + note: data.submissionSection.note, + orderingTypeId: data.seriesSection.orderType, + relationships + }; +} + +const createOrEditHandler = makeEntityCreateOrEditHandler( + 'series', transformNewForm, additionalSeriesProps +); + +/** **************************** +*********** Routes ************ +*******************************/ + +const router = express.Router(); + +// Creation +router.get( + '/create', auth.isAuthenticated, middleware.loadIdentifierTypes, + middleware.loadLanguages, + middleware.loadRelationshipTypes, middleware.loadSeriesOrderingTypes, (req, res) => { + const {markup, props} = entityEditorMarkup(generateEntityProps( + 'series', req, res, {} + )); + + return res.send(target({ + markup, + props: escapeProps(props), + script: '/js/entity-editor.js', + title: props.heading + })); + } +); + + +router.post('/create/handler', auth.isAuthenticatedForHandler, + createOrEditHandler); + + +/* If the route specifies a BBID, make sure it does not redirect to another bbid then load the corresponding entity */ +router.param( + 'bbid', + middleware.redirectedBbid +); +router.param( + 'bbid', + middleware.makeEntityLoader( + 'Series', + [ + 'defaultAlias', + 'disambiguation', + 'seriesOrderingType', + 'identifierSet.identifiers.type' + ], + 'Series not found' + ) +); + +function _setSeriesTitle(res) { + res.locals.title = utils.createEntityPageTitle( + res.locals.entity, + 'Series', + utils.template`Series “${'name'}”` + ); +} + +router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadSeriesItems, (req, res) => { + _setSeriesTitle(res); + entityRoutes.displayEntity(req, res); +}); + + +function seriesToFormState(series) { + const aliases = series.aliasSet ? + series.aliasSet.aliases.map(({languageId, ...rest}) => ({ + ...rest, + language: languageId + })) : []; + + const defaultAliasIndex = entityRoutes.getDefaultAliasIndex(series.aliasSet); + const defaultAliasList = aliases.splice(defaultAliasIndex, 1); + + const aliasEditor = {}; + aliases.forEach((alias) => { aliasEditor[alias.id] = alias; }); + + const buttonBar = { + aliasEditorVisible: false, + identifierEditorVisible: false + }; + + const nameSection = _.isEmpty(defaultAliasList) ? { + language: null, + name: '', + sortName: '' + } : defaultAliasList[0]; + nameSection.disambiguation = + series.disambiguation && series.disambiguation.comment; + + const identifiers = series.identifierSet ? + series.identifierSet.identifiers.map(({type, ...rest}) => ({ + type: type.id, + ...rest + })) : []; + + const identifierEditor = {}; + identifiers.forEach( + (identifier) => { identifierEditor[identifier.id] = identifier; } + ); + const seriesSection = { + orderType: series.seriesOrderingType && series.seriesOrderingType.id, + seriesType: series.entityType + }; + + const relationshipSection = { + canEdit: true, + lastRelationships: null, + relationshipEditorProps: null, + relationshipEditorVisible: false, + relationships: {} + }; + series.relationships.forEach((relationship) => { + relationship.attributeSet.relationshipAttributes.forEach(attribute => { + relationship[`${attribute.type.name}`] = attribute.value.textValue; + }); + }); + + if (series.seriesOrderingType.label === 'Manual') { + series.relationships.sort(sortRelationshipOrdinal('position')); + } + else { + series.relationships.sort(sortRelationshipOrdinal('number')); + } + series.relationships.forEach((relationship) => ( + relationshipSection.relationships[`n${relationship.id}`] = { + attributeSetId: relationship.attributeSetId, + attributes: relationship.attributeSet ? relationship.attributeSet.relationshipAttributes : [], + relationshipType: relationship.type, + rowID: `n${relationship.id}`, + sourceEntity: relationship.source, + targetEntity: relationship.target + } + )); + + const optionalSections = {}; + if (series.annotation) { + optionalSections.annotationSection = series.annotation; + } + + return { + aliasEditor, + buttonBar, + identifierEditor, + nameSection, + relationshipSection, + seriesSection, + ...optionalSections + }; +} + +router.get( + '/:bbid/edit', auth.isAuthenticated, middleware.loadIdentifierTypes, + middleware.loadSeriesOrderingTypes, middleware.loadLanguages, + middleware.loadEntityRelationships, middleware.loadRelationshipTypes, + (req, res) => { + const {markup, props} = entityEditorMarkup(generateEntityProps( + 'series', req, res, {}, seriesToFormState + )); + + return res.send(target({ + markup, + props: escapeProps(props), + script: '/js/entity-editor.js', + title: props.heading + })); + } +); + +router.post('/:bbid/edit/handler', auth.isAuthenticatedForHandler, + createOrEditHandler); + +export default router; diff --git a/src/server/routes/entity/work.js b/src/server/routes/entity/work.js index fcbc8af27b..5e68d3bf91 100644 --- a/src/server/routes/entity/work.js +++ b/src/server/routes/entity/work.js @@ -255,9 +255,11 @@ function workToFormState(work) { }; work.relationships.forEach((relationship) => ( - relationshipSection.relationships[relationship.id] = { + relationshipSection.relationships[`n${relationship.id}`] = { + attributeSetId: relationship.attributeSetId, + attributes: relationship.attributeSet ? relationship.attributeSet.relationshipAttributes : [], relationshipType: relationship.type, - rowID: relationship.id, + rowID: `n${relationship.id}`, sourceEntity: relationship.source, targetEntity: relationship.target } diff --git a/test/src/client/entity-editor/validators/test-series.js b/test/src/client/entity-editor/validators/test-series.js new file mode 100644 index 0000000000..37deda1407 --- /dev/null +++ b/test/src/client/entity-editor/validators/test-series.js @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2021 Akash Gupta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as Immutable from 'immutable'; + +import { + EMPTY_SUBMISSION_SECTION, + IDENTIFIER_TYPES, + INVALID_ALIASES, + INVALID_IDENTIFIERS, + INVALID_NAME_SECTION, + VALID_ALIASES, + VALID_IDENTIFIERS, + VALID_NAME_SECTION, + VALID_SUBMISSION_SECTION +} from './data'; +import { + validateForm, + validateSeriesSection, + validateSeriesSectionEntityType, + validateSeriesSectionOrderingType +} from '../../../../../src/client/entity-editor/validators/series'; + +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import {testValidatePositiveIntegerFunc} from './helpers'; + + +chai.use(chaiAsPromised); +const {expect} = chai; + + +const VALID_SERIES_TYPE = 'Author'; +const INVALID_SERIES_TYPE = 'XYZ'; + +function describeValidateSeriesSectionOrderingType() { + testValidatePositiveIntegerFunc(validateSeriesSectionOrderingType, true); +} + +function describeValidateSeriesSectionEntityType() { + it('should return true if passed a valid series type', () => { + const result = validateSeriesSectionEntityType(VALID_SERIES_TYPE); + expect(result).to.be.true; + }); + it('should return false if passed a invalid series type', () => { + const result = validateSeriesSectionEntityType(INVALID_SERIES_TYPE); + expect(result).to.be.false; + }); +} + +const VALID_SERIES_SECTION = { + orderType: 1, + seriesType: VALID_SERIES_TYPE +}; +const INVALID_SERIES_SECTION = {...VALID_SERIES_SECTION, seriesType: INVALID_SERIES_TYPE}; + +function describeValidateSeriesSection() { + it('should pass a valid Object', () => { + const result = validateSeriesSection(VALID_SERIES_SECTION); + expect(result).to.be.true; + }); + + it('should pass a valid Immutable.Map', () => { + const result = validateSeriesSection( + Immutable.fromJS(VALID_SERIES_SECTION) + ); + expect(result).to.be.true; + }); + + it('should reject an Object with an invalid ordering type', () => { + const result = validateSeriesSection({ + ...VALID_SERIES_SECTION, + orderType: {} + }); + expect(result).to.be.false; + }); + + it('should reject an Object with an invalid series type', () => { + const result = validateSeriesSection({ + ...VALID_SERIES_SECTION, + seriesType: INVALID_SERIES_TYPE + }); + expect(result).to.be.false; + }); + + it('should reject an invalid Immutable.Map', () => { + const result = validateSeriesSection( + Immutable.fromJS(INVALID_SERIES_SECTION) + ); + expect(result).to.be.false; + }); + + it('should reject any other non-null data type', () => { + const result = validateSeriesSection(1); + expect(result).to.be.false; + }); + + it('should reject a null value', () => { + const result = validateSeriesSection(null); + expect(result).to.be.false; + }); +} + +function describeValidateForm() { + const validForm = { + aliasEditor: VALID_ALIASES, + identifierEditor: VALID_IDENTIFIERS, + nameSection: VALID_NAME_SECTION, + seriesSection: VALID_SERIES_SECTION, + submissionSection: VALID_SUBMISSION_SECTION + }; + + it('should pass a valid Object', () => { + const result = validateForm(validForm, IDENTIFIER_TYPES); + expect(result).to.be.true; + }); + + it('should pass a valid Immutable.Map', () => { + const result = validateForm( + Immutable.fromJS(validForm), + IDENTIFIER_TYPES + ); + expect(result).to.be.true; + }); + + it('should reject an Object with an invalid alias editor', () => { + const result = validateForm( + { + ...validForm, + aliasEditor: INVALID_ALIASES + }, + IDENTIFIER_TYPES + ); + expect(result).to.be.false; + }); + + it('should reject an Object with an invalid identifier editor', () => { + const result = validateForm( + { + ...validForm, + identifierEditor: INVALID_IDENTIFIERS + }, IDENTIFIER_TYPES + ); + expect(result).to.be.false; + }); + + it('should reject an Object with an invalid name section', () => { + const result = validateForm( + { + ...validForm, + nameSection: INVALID_NAME_SECTION + }, + IDENTIFIER_TYPES + ); + expect(result).to.be.false; + }); + + it('should reject an Object with an invalid series section', () => { + const result = validateForm( + { + ...validForm, + seriesSection: INVALID_SERIES_SECTION + }, + IDENTIFIER_TYPES + ); + expect(result).to.be.false; + }); + + it('should pass an Object with an empty submission section', () => { + const result = validateForm( + { + ...validForm, + submissionSection: EMPTY_SUBMISSION_SECTION + }, + IDENTIFIER_TYPES + ); + expect(result).to.be.true; + }); + + const invalidForm = { + ...validForm, + nameSection: INVALID_NAME_SECTION + }; + + it('should reject an invalid Immutable.Map', () => { + const result = validateForm( + Immutable.fromJS(invalidForm), + IDENTIFIER_TYPES + ); + expect(result).to.be.false; + }); + + it('should reject any other non-null data type', () => { + const result = validateForm(1, IDENTIFIER_TYPES); + expect(result).to.be.false; + }); + + it('should reject a null value', () => { + const result = validateForm(null, IDENTIFIER_TYPES); + expect(result).to.be.false; + }); +} + + +function tests() { + describe( + 'validateSeriesSectionOrderingType', + describeValidateSeriesSectionOrderingType + ); + describe( + 'validateSeriesSectionEntityType', + describeValidateSeriesSectionEntityType + ); + describe( + 'validateSeriesSection', + describeValidateSeriesSection + ); + describe( + 'validateForm', + describeValidateForm + ); +} + +describe('validateSeriesSection* functions', tests);
#Name Languages Type