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
- Name
+ {authors[0].displayNumber && # }
+ Name
Gender
Type
Date of birth
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 && {number} }
{
showCheckboxes ?
@@ -101,6 +103,7 @@ function EditionTable({editions, entity, showAddedAtColumn, showAdd, showCheckbo
+ {editions[0].displayNumber && # }
Name
Format
ISBN
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 && {number} }
{
showCheckboxes ?
@@ -78,6 +80,7 @@ function EditionGroupTable({editionGroups, showAddedAtColumn, showCheckboxes, se
+ {editionGroups[0].displayNumber && # }
Name
Type
{
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 && {number} }
{
showCheckboxes ?
@@ -83,7 +85,8 @@ function PublisherTable({showAddedAtColumn, publishers, showCheckboxes, selected
- Name
+ {publishers[0].displayNumber && # }
+ Name
Area
Type
Date founded
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 && {number} }
{
showCheckboxes ?
@@ -85,6 +87,7 @@ function WorkTable({entity, showAddedAtColumn, works, showAdd, showCheckboxes, s
+ {works[0].displayNumber && # }
Name
Languages
Type
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 &&
+
+
+ Edit
+
+ }
+ {onRemove &&
+
+
+ Remove
+
+ }
+
+
+ }
+
+
+ );
+}
+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 &&
-
-
- Edit
-
- }
- {onRemove &&
-
-
- Remove
-
- }
-
-
- }
-
+ ({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);