diff --git a/src/client/components/pages/entities/edition-table.js b/src/client/components/pages/entities/edition-table.js index 59d6227d36..ffa364c5d1 100644 --- a/src/client/components/pages/entities/edition-table.js +++ b/src/client/components/pages/entities/edition-table.js @@ -20,11 +20,12 @@ import * as bootstrap from 'react-bootstrap'; import * as entityHelper from '../../../helpers/entity'; import * as utilHelper from '../../../helpers/utils'; +import React, {useState} from 'react'; import {faBook, faPlus} from '@fortawesome/free-solid-svg-icons'; +import AddEntityRelationModal from '../parts/add-entity-relation-modal'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import PropTypes from 'prop-types'; -import React from 'react'; import {kebabCase as _kebabCase} from 'lodash'; @@ -32,7 +33,7 @@ const { getEditionReleaseDate, getEntityLabel, getEntityDisambiguation, getISBNOfEdition, getEditionFormat } = entityHelper; -const {Button, Table} = bootstrap; +const {Alert, Button, Table} = bootstrap; function EditionTableRow({edition, showAddedAtColumn, showCheckboxes, selectedEntities, onToggleRow}) { const name = getEntityLabel(edition); @@ -95,8 +96,28 @@ EditionTableRow.defaultProps = { showCheckboxes: false }; -function EditionTable({editions, entity, showAddedAtColumn, showAdd, showCheckboxes, selectedEntities, onToggleRow}) { +function EditionTable({editions, entity, showAddedAtColumn, showAdd, showCheckboxes, selectedEntities, onToggleRow, relationshipTypeId}) { let tableContent; + const [show, setShow] = useState(false); + const [message, setMessage] = useState({}); + const haveRelationshipId = Boolean(relationshipTypeId); + function onCloseModalHandler() { + setShow(false); + } + function handleOpenModal() { + setShow(true); + } + function closeModalAndShowMessage(msg) { + setMessage(msg); + onCloseModalHandler(); + } + function handleAlertDismiss() { + setMessage({}); + } + const submitUrl = `/${entity.type.toLowerCase()}/${ + entity.bbid + }/relationships`; + const buttonProps = !haveRelationshipId ? {href: `/edition/create?${_kebabCase(entity.type)}=${entity.bbid}`} : {onClick: handleOpenModal}; if (editions.length) { tableContent = ( @@ -128,15 +149,17 @@ function EditionTable({editions, entity, showAddedAtColumn, showAdd, showCheckbo } - {showAdd && + {showAdd && ( + ) } ); @@ -146,8 +169,8 @@ function EditionTable({editions, entity, showAddedAtColumn, showAdd, showCheckbo @@ -136,7 +161,7 @@ function WorkTable({entity, showAddedAtColumn, works, showAdd, showCheckboxes, s + ); + return ( +
+ this.handleChangeEntity(newEntity, index)} + /> +
+ ); + }) + } + + + + ); + + return ( + + + + Add {upperFirst(this.props.targetEntityType)}s + + + + {addEntityToCollectionForm} + {errorComponent} + + + + + + + + + + ); + } +} + +AddEntityRelationModal.displayName = 'AddEntityRelationModal'; +AddEntityRelationModal.propTypes = { + closeModalAndShowMessage: PropTypes.func.isRequired, + entity: PropTypes.object.isRequired, + isSource: PropTypes.bool.isRequired, + onCloseModal: PropTypes.func.isRequired, + relationshipTypeId: PropTypes.number.isRequired, + show: PropTypes.bool.isRequired, + submitUrl: PropTypes.string.isRequired, + targetEntityType: PropTypes.string.isRequired +}; +export default AddEntityRelationModal; diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index 2408cc7408..e9bd8b97bb 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -28,6 +28,7 @@ import * as utils from './utils'; import type {Request as $Request, Response as $Response} from 'express'; import EntityEditor from '../../client/entity-editor/entity-editor'; import EntityMerge from '../../client/entity-editor/entity-merge'; +import {EntityTypeString} from 'bookbrainz-data/lib/func/types'; import Layout from '../../client/containers/layout'; import {Provider} from 'react-redux'; import ReactDOMServer from 'react-dom/server'; @@ -312,3 +313,20 @@ export function addInitialRelationship(props, relationshipTypeId, relationshipIn return props; } + +/** + * Makes a middleware handler for adding relationships. + * @param {string} entityType - entity type + * @returns {addRelationshipHandler} addRelationshipHandler - middleware handler + */ + +export function makeAddRelationshipHandler(entityType:string) { + const entityName = _.upperFirst(entityType); + return function addRelationsipHandler(req:PassportRequest, res:$Response) { + if (!req.body.relationships) { + throw new error.BadRequestError('Relationships field does not exist on request body!'); + } + req.body.relationships = entityRoutes.constructRelationships(req.body); + return entityRoutes.handleAddRelationship(req, res, entityName as EntityTypeString); + }; +} diff --git a/src/server/routes/entity/author.js b/src/server/routes/entity/author.js index 37c6227606..2f397b9a71 100644 --- a/src/server/routes/entity/author.js +++ b/src/server/routes/entity/author.js @@ -25,6 +25,7 @@ import * as utils from '../../helpers/utils'; import { entityEditorMarkup, generateEntityProps, + makeAddRelationshipHandler, makeEntityCreateOrEditHandler } from '../../helpers/entityRouteUtils'; @@ -77,6 +78,8 @@ function transformNewForm(data) { }; } +const addRelationshipHandler = makeAddRelationshipHandler('author'); + const createOrEditHandler = makeEntityCreateOrEditHandler( 'author', transformNewForm, additionalAuthorProps ); @@ -159,6 +162,8 @@ router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { return entityRoutes.displayDeleteEntity(req, res); }); +router.post('/:bbid/relationships', auth.isAuthenticatedForHandler, addRelationshipHandler); + router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, (req, res) => { diff --git a/src/server/routes/entity/edition-group.js b/src/server/routes/entity/edition-group.js index 7e1e62435a..45d52bc60c 100644 --- a/src/server/routes/entity/edition-group.js +++ b/src/server/routes/entity/edition-group.js @@ -25,6 +25,7 @@ import * as utils from '../../helpers/utils'; import { entityEditorMarkup, generateEntityProps, + makeAddRelationshipHandler, makeEntityCreateOrEditHandler } from '../../helpers/entityRouteUtils'; @@ -62,6 +63,8 @@ function transformNewForm(data) { }; } +const addRelationshipHandler = makeAddRelationshipHandler('editionGroup'); + const createOrEditHandler = makeEntityCreateOrEditHandler( 'editionGroup', transformNewForm, 'typeId' ); @@ -143,6 +146,8 @@ router.get('/:bbid', middleware.loadEntityRelationships, (req, res) => { entityRoutes.displayEntity(req, res); }); +router.post('/:bbid/relationships', auth.isAuthenticatedForHandler, addRelationshipHandler); + router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); diff --git a/src/server/routes/entity/edition.js b/src/server/routes/entity/edition.js index ef046c1c73..2895a268cd 100644 --- a/src/server/routes/entity/edition.js +++ b/src/server/routes/entity/edition.js @@ -26,6 +26,7 @@ import { addInitialRelationship, entityEditorMarkup, generateEntityProps, + makeAddRelationshipHandler, makeEntityCreateOrEditHandler } from '../../helpers/entityRouteUtils'; @@ -110,6 +111,8 @@ function getInitialNameSection(entity) { }; } +const addRelationshipHandler = makeAddRelationshipHandler('edition'); + const createOrEditHandler = makeEntityCreateOrEditHandler( 'edition', transformNewForm, additionalEditionProps ); @@ -269,6 +272,8 @@ router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { return entityRoutes.displayDeleteEntity(req, res); }); +router.post('/:bbid/relationships', auth.isAuthenticatedForHandler, addRelationshipHandler); + router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, (req, res) => { diff --git a/src/server/routes/entity/entity.tsx b/src/server/routes/entity/entity.tsx index eb5727994d..f6f0860a19 100644 --- a/src/server/routes/entity/entity.tsx +++ b/src/server/routes/entity/entity.tsx @@ -992,6 +992,67 @@ async function indexAutoCreatedEditionGroup(orm, newEdition, transacting) { } } +export function handleAddRelationship( + req:PassportRequest, + res:$Response, + entityType:EntityTypeString +) { + const {body} = req; + const {orm} = req.app.locals; + const editorJSON = req.user; + const {Revision, bookshelf} = orm; + const currentEntity: { + aliasSet: {id: number} | null | undefined, + annotation: {id: number} | null | undefined, + bbid: string, + disambiguation: {id: number} | null | undefined, + identifierSet: {id: number} | null | undefined, + type: EntityTypeString + } | null | undefined = res.locals.entity; + const entityEditPromise = bookshelf.transaction(async (transacting) => { + const newRevision = await new Revision({ + authorId: editorJSON.id, + isMerge: false + }).save(null, {transacting}); + const relationshipSets = await getNextRelationshipSets( + orm, transacting, currentEntity, body + ); + if (_.isEmpty(relationshipSets)) { + throw new error.FormSubmissionError('No Updated Relationship Field'); + } + // Fetch main entity + const mainEntity = await fetchOrCreateMainEntity( + orm, transacting, false, currentEntity.bbid, entityType + ); + // Fetch all entities that definitely exist + const otherEntities = await fetchEntitiesForRelationships( + orm, transacting, currentEntity, relationshipSets + ); + otherEntities.forEach(entity => { entity.shouldInsert = false; }); + mainEntity.shouldInsert = false; + const allEntities = [...otherEntities, mainEntity] + .filter(entity => entity.get('dataId') !== null); + _.forEach(allEntities, (entityModel) => { + const bbid: string = entityModel.get('bbid'); + if (_.has(relationshipSets, bbid)) { + entityModel.set( + 'relationshipSetId', + // Set to relationshipSet id or null if empty set + relationshipSets[bbid] && relationshipSets[bbid].get('id') + ); + } + }); + const savedMainEntity = await saveEntitiesAndFinishRevision( + orm, transacting, false, newRevision, mainEntity, allEntities, + editorJSON.id, body.note + ); + return savedMainEntity.toJSON(); + }); + return handler.sendPromiseResult( + res, + entityEditPromise + ); +} export function handleCreateOrEditEntity( req: PassportRequest, res: $Response, diff --git a/src/server/routes/entity/publisher.js b/src/server/routes/entity/publisher.js index 2c4730b4cc..8b991915a1 100644 --- a/src/server/routes/entity/publisher.js +++ b/src/server/routes/entity/publisher.js @@ -25,6 +25,7 @@ import * as utils from '../../helpers/utils'; import { entityEditorMarkup, generateEntityProps, + makeAddRelationshipHandler, makeEntityCreateOrEditHandler } from '../../helpers/entityRouteUtils'; @@ -72,6 +73,8 @@ function transformNewForm(data) { }; } +const addRelationshipHandler = makeAddRelationshipHandler('publisher'); + const createOrEditHandler = makeEntityCreateOrEditHandler( 'publisher', transformNewForm, additionalPublisherProps ); @@ -166,6 +169,8 @@ router.get('/:bbid', middleware.loadEntityRelationships, (req, res, next) => { .catch(next); }); +router.post('/:bbid/relationships', auth.isAuthenticatedForHandler, addRelationshipHandler); + router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); diff --git a/src/server/routes/entity/series.js b/src/server/routes/entity/series.js index 5dcb43f3ad..24c445411f 100644 --- a/src/server/routes/entity/series.js +++ b/src/server/routes/entity/series.js @@ -25,6 +25,7 @@ import * as utils from '../../helpers/utils'; import { entityEditorMarkup, generateEntityProps, + makeAddRelationshipHandler, makeEntityCreateOrEditHandler } from '../../helpers/entityRouteUtils'; @@ -69,6 +70,8 @@ function transformNewForm(data) { }; } +const addRelationshipHandler = makeAddRelationshipHandler('series'); + const createOrEditHandler = makeEntityCreateOrEditHandler( 'series', transformNewForm, additionalSeriesProps ); @@ -157,6 +160,8 @@ router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { return entityRoutes.displayDeleteEntity(req, res); }); +router.post('/:bbid/relationships', auth.isAuthenticatedForHandler, addRelationshipHandler); + router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, (req, res) => { diff --git a/src/server/routes/entity/work.js b/src/server/routes/entity/work.js index d75ea3467a..9746a2b0c9 100644 --- a/src/server/routes/entity/work.js +++ b/src/server/routes/entity/work.js @@ -26,6 +26,7 @@ import { addInitialRelationship, entityEditorMarkup, generateEntityProps, + makeAddRelationshipHandler, makeEntityCreateOrEditHandler } from '../../helpers/entityRouteUtils'; @@ -70,6 +71,8 @@ function transformNewForm(data) { }; } +const addRelationshipHandler = makeAddRelationshipHandler('work'); + const createOrEditHandler = makeEntityCreateOrEditHandler( 'work', transformNewForm, 'typeId' ); @@ -199,6 +202,8 @@ router.post( } ); +router.post('/:bbid/relationships', auth.isAuthenticatedForHandler, addRelationshipHandler); + router.get('/:bbid/revisions', (req, res, next) => { const {WorkRevision} = req.app.locals.orm; _setWorkTitle(res);