From 5e577e2f2ee2a04acfb9608328f8e08107da1f61 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Thu, 17 Mar 2022 00:48:13 +0530 Subject: [PATCH 001/258] [BB-432]:Dont change shape of name-section --- .../name-section/name-section.js | 135 ++++++++++-------- 1 file changed, 78 insertions(+), 57 deletions(-) diff --git a/src/client/entity-editor/name-section/name-section.js b/src/client/entity-editor/name-section/name-section.js index 7c870944cd..3c798a1709 100644 --- a/src/client/entity-editor/name-section/name-section.js +++ b/src/client/entity-editor/name-section/name-section.js @@ -16,7 +16,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import {Alert, Col, ListGroup, Row} from 'react-bootstrap'; +import {Alert, Button, Col, Container, ListGroup, Row} from 'react-bootstrap'; import { checkIfNameExists, debouncedUpdateDisambiguationField, @@ -79,6 +79,10 @@ class NameSection extends React.Component { this.updateNameFieldInputRef = this.updateNameFieldInputRef.bind(this); this.handleNameChange = this.handleNameChange.bind(this); this.searchForMatchindEditionGroups = this.searchForMatchindEditionGroups.bind(this); + this.state = { + open: false, + showButton: false + }; } /* @@ -103,6 +107,10 @@ class NameSection extends React.Component { this.searchForMatchindEditionGroups(nameValue); } + handleToggleCollapse = () => { + this.setState(prevState => ({open: !prevState.open})); + }; + handleNameChange(event) { this.props.onNameChange(event.target.value); this.props.onNameChangeCheckIfExists(event.target.value); @@ -149,12 +157,24 @@ class NameSection extends React.Component { const warnIfExists = !_.isEmpty(exactMatches); + if (!this.state.showButton && searchResults && searchResults.length > 5) { + this.setState({showButton: true}); + } + + if (this.state.showButton && searchResults && searchResults.length <= 5) { + this.setState({showButton: false}); + } + + if (this.state.showButton && searchResults === null) { + this.setState({showButton: false}); + } + return ( -
+

{`What is the ${_.startCase(entityType)} called?`}

- + - - - {isRequiredDisambiguationEmpty( - warnIfExists, - disambiguationDefaultValue - ) ? - - We found the following  - {_.startCase(entityType)}{exactMatches.length > 1 ? 's' : ''} with - exactly the same name or alias: -
Click on a name to open it (Ctrl/Cmd + click to open in a new tab) - - {exactMatches.map((match) => - ( - - {match.defaultAlias.name} {getEntityDisambiguation(match)} - - ))} - - If you are sure your entry is different, please fill the - disambiguation field below to help us differentiate between them. -
: null - } - -
- { - !warnIfExists && - !_.isEmpty(searchResults) && - - - If the {_.startCase(entityType)} you want to add appears in the results - below, click on it to inspect it before adding a possible duplicate.
- Ctrl/Cmd + click to open in a new tab - - -
- } - - - - - - - - - - + + +
+ {isRequiredDisambiguationEmpty( + warnIfExists, + disambiguationDefaultValue + ) ? + + We found the following  + {_.startCase(entityType)}{exactMatches.length > 1 ? 's' : ''} with + exactly the same name or alias: +
Click on a name to open it (Ctrl/Cmd + click to open in a new tab) + + {exactMatches.map((match) => + ( + + {match.defaultAlias.name} {getEntityDisambiguation(match)} + + ))} + + If you are sure your entry is different, please fill the + disambiguation field below to help us differentiate between them. +
: null + } +
+ + { + !warnIfExists && + !_.isEmpty(searchResults) && + + + If the {_.startCase(entityType)} you want to add appears in the results + below, click on it to inspect it before adding a possible duplicate.
+ Ctrl/Cmd + click to open in a new tab + + +
+ } + {this.state.showButton && + + + + + } +
+
-
+ ); } } From 45bee27c2b0836ea275c3bbceb99cd2611881aaf Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Thu, 17 Mar 2022 01:07:14 +0530 Subject: [PATCH 002/258] put margin-right on external div --- src/client/entity-editor/name-section/name-section.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/entity-editor/name-section/name-section.js b/src/client/entity-editor/name-section/name-section.js index 3c798a1709..3516305bb7 100644 --- a/src/client/entity-editor/name-section/name-section.js +++ b/src/client/entity-editor/name-section/name-section.js @@ -16,7 +16,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import {Alert, Button, Col, Container, ListGroup, Row} from 'react-bootstrap'; +import {Alert, Button, Col, ListGroup, Row} from 'react-bootstrap'; import { checkIfNameExists, debouncedUpdateDisambiguationField, @@ -171,7 +171,7 @@ class NameSection extends React.Component { return ( - +

{`What is the ${_.startCase(entityType)} called?`}

@@ -278,7 +278,7 @@ class NameSection extends React.Component { - +
); } } From 76be9ac8a418c24063eebcbe5c23b20079eb0740 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Thu, 24 Mar 2022 19:41:09 +0530 Subject: [PATCH 003/258] optional chaining and md --- src/client/entity-editor/name-section/name-section.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/entity-editor/name-section/name-section.js b/src/client/entity-editor/name-section/name-section.js index 3516305bb7..1e66a4e6e4 100644 --- a/src/client/entity-editor/name-section/name-section.js +++ b/src/client/entity-editor/name-section/name-section.js @@ -157,11 +157,11 @@ class NameSection extends React.Component { const warnIfExists = !_.isEmpty(exactMatches); - if (!this.state.showButton && searchResults && searchResults.length > 5) { + if (!this.state.showButton && searchResults?.length > 5) { this.setState({showButton: true}); } - if (this.state.showButton && searchResults && searchResults.length <= 5) { + if (this.state.showButton && searchResults?.length <= 5) { this.setState({showButton: false}); } @@ -171,10 +171,10 @@ class NameSection extends React.Component { return ( -
+

{`What is the ${_.startCase(entityType)} called?`}

- + - +
{isRequiredDisambiguationEmpty( From acfd44388d2ba3f0bbb9fe5b551420c1489c5310 Mon Sep 17 00:00:00 2001 From: tri10 Date: Wed, 25 May 2022 20:02:19 +0530 Subject: [PATCH 004/258] feat(routes): added uf routes for work, author --- src/common/helpers/search.js | 2 +- src/server/helpers/entityRouteUtils.tsx | 30 ++++ src/server/routes.js | 2 + src/server/routes/entity/author.js | 2 +- src/server/routes/entity/edition-group.js | 2 +- src/server/routes/entity/edition.js | 2 +- src/server/routes/entity/entity.tsx | 188 +++++++++++++++++++++- src/server/routes/entity/publisher.js | 2 +- src/server/routes/entity/series.js | 2 +- src/server/routes/entity/work.js | 2 +- src/server/routes/unifiedform.ts | 8 + 11 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 src/server/routes/unifiedform.ts diff --git a/src/common/helpers/search.js b/src/common/helpers/search.js index e825f14b28..d19cdd847b 100644 --- a/src/common/helpers/search.js +++ b/src/common/helpers/search.js @@ -102,7 +102,7 @@ function _searchForEntities(orm, dslQuery) { .catch(error => log.error(error)); } -async function _bulkIndexEntities(entities) { +export async function _bulkIndexEntities(entities) { if (!entities.length) { return; } diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index 2408cc7408..1c209b4034 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -312,3 +312,33 @@ export function addInitialRelationship(props, relationshipTypeId, relationshipIn return props; } + +export function validateUnifiedForm(body:Record):boolean { + for (const entityKey in body) { + if (Object.prototype.hasOwnProperty.call(body, entityKey)) { + const entityForm = body[entityKey]; + const entityType = _.snakeCase(entityForm.type); + if (!entityType) { + return false; + } + const validator = getValidator(entityType); + if (!validator(entityForm)) { + return false; + } + } + } + return true; +} +export function createEntitesHandler( + req:$Request, + res:$Response +) { + // validating + if (!validateUnifiedForm(req.body)) { + const err = new error.FormSubmissionError(); + error.sendErrorAsJSON(res, err); + } + // transforming + req.body = entityRoutes.transformForm(req.body); + return entityRoutes.handleCreateEntities(req as PassportRequest, res); +} diff --git a/src/server/routes.js b/src/server/routes.js index 56a29a35e3..bfe662b961 100644 --- a/src/server/routes.js +++ b/src/server/routes.js @@ -33,12 +33,14 @@ import revisionsRouter from './routes/revisions'; import searchRouter from './routes/search'; import seriesRouter from './routes/entity/series'; import statisticsRouter from './routes/statistics'; +import unifiedFormRouter from './routes/unifiedform'; import workRouter from './routes/entity/work'; function initRootRoutes(app) { app.use('/', indexRouter); app.use('/', authRouter); + app.use('/', unifiedFormRouter); app.use('/search', searchRouter); app.use('/register', registerRouter); app.use('/revisions', revisionsRouter); diff --git a/src/server/routes/entity/author.js b/src/server/routes/entity/author.js index 330f8b1b26..42f4900deb 100644 --- a/src/server/routes/entity/author.js +++ b/src/server/routes/entity/author.js @@ -46,7 +46,7 @@ const additionalAuthorProps = [ ]; -function transformNewForm(data) { +export function transformNewForm(data) { const aliases = entityRoutes.constructAliases( data.aliasEditor, data.nameSection ); diff --git a/src/server/routes/entity/edition-group.js b/src/server/routes/entity/edition-group.js index fa9d4dc8d2..ad9a0e7b8c 100644 --- a/src/server/routes/entity/edition-group.js +++ b/src/server/routes/entity/edition-group.js @@ -40,7 +40,7 @@ import target from '../../templates/target'; *********** Helpers ************ *******************************/ -function transformNewForm(data) { +export function transformNewForm(data) { const aliases = entityRoutes.constructAliases( data.aliasEditor, data.nameSection ); diff --git a/src/server/routes/entity/edition.js b/src/server/routes/entity/edition.js index e5b49c8b68..451b91e04d 100644 --- a/src/server/routes/entity/edition.js +++ b/src/server/routes/entity/edition.js @@ -48,7 +48,7 @@ const additionalEditionProps = [ 'formatId', 'statusId' ]; -function transformNewForm(data) { +export function transformNewForm(data) { const aliases = entityRoutes.constructAliases( data.aliasEditor, data.nameSection ); diff --git a/src/server/routes/entity/entity.tsx b/src/server/routes/entity/entity.tsx index b949fd3828..50f9ac97ba 100644 --- a/src/server/routes/entity/entity.tsx +++ b/src/server/routes/entity/entity.tsx @@ -26,7 +26,6 @@ import * as propHelpers from '../../../client/helpers/props'; import * as search from '../../../common/helpers/search'; import * as utils from '../../helpers/utils'; - import type {Request as $Request, Response as $Response, NextFunction} from 'express'; import type { EntityTypeString, @@ -48,12 +47,27 @@ 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 {_bulkIndexEntities} from '../../../common/helpers/search'; +import {transformNewForm as authorTransform} from './author'; import {getEntityLabel} from '../../../client/helpers/entity'; import {getOrderedRevisionsForEntityPage} from '../../helpers/revisions'; import log from 'log'; import target from '../../templates/target'; +import {transformNewForm as workTransform} from './work'; + +const transformFunctions = { + author: authorTransform, + work: workTransform +}; +const additionalEntityProps = { + author: [ + 'typeId', 'genderId', 'beginAreaId', 'beginDate', 'endDate', 'ended', + 'endAreaId' + ], + work: 'typeId' +}; type PassportRequest = $Request & {user: any, session: any}; @@ -1312,3 +1326,175 @@ export function displayPreview(req:PassportRequest, res:$Response, next) { title: 'Preview' })); } + +export function transformForm(body:Record):Record { + const modifiedForm = {}; + for (const keyIndex in body) { + if (Object.prototype.hasOwnProperty.call(body, keyIndex)) { + const currentForm = body[keyIndex]; + const transformedForm = transformFunctions[_.lowerFirst(currentForm.type)](currentForm); + modifiedForm[keyIndex] = {type: currentForm.type, ...transformedForm}; + } + } + return modifiedForm; +} + + +export async function handleAddRelationship( + body, + editorJSON, + currentEntity, + entityType:EntityTypeString, + orm, + transacting +) { + const {Revision} = orm; + + const newRevision = await new Revision({ + authorId: editorJSON.id, + isMerge: false + }).save(null, {transacting}); + const relationshipSets = await getNextRelationshipSets( + orm, transacting, currentEntity, body + ); + if (_.isEmpty(relationshipSets)) { + return {}; + } + // 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(); +} + +export function handleCreateEntities( + req: PassportRequest, + res: $Response +) { + const {orm}: {orm?: any} = req.app.locals; + const {Entity, Revision, bookshelf} = orm; + const editorJSON = req.user; + + const {body}: {body: Record} = req; + let 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; + + const entityEditPromise = bookshelf.transaction(async (transacting) => { + /* eslint-disable no-await-in-loop */ + const savedMainEntities = {}; + const bbidMap = {}; + const allRelationships = []; + try { + await Promise.all(Object.keys(body).map(async (entityKey:string) => { + const entityForm = body[entityKey]; + const entityType = _.upperFirst(entityForm.type); + allRelationships.push(entityForm.relationships); + const newEntity = await new Entity({type: entityType}).save(null, {transacting}); + currentEntity = newEntity.toJSON(); + // create new revision for each entity + const newRevision = await new Revision({ + authorId: editorJSON.id, + isMerge: false + }).save(null, {transacting}); + // console.log('here currentEntity', currentEntity); + const changedProps = await getChangedProps( + orm, transacting, true, currentEntity, entityForm, entityType, + newRevision, _.pick(entityForm, additionalEntityProps[_.lowerFirst(entityType)]) + ); + const mainEntity = await fetchOrCreateMainEntity( + orm, transacting, true, currentEntity.bbid, entityType + ); + // console.log('here main entity', mainEntity.toJSON()); + mainEntity.shouldInsert = true; + + // set changed attributes on main entity + _.forOwn(changedProps, (value, key) => mainEntity.set(key, value)); + const savedMainEntity = await saveEntitiesAndFinishRevision( + orm, transacting, true, newRevision, mainEntity, [mainEntity], + editorJSON.id, entityForm.note + ); + + /* We need to load the aliases for search reindexing and refresh it*/ + await savedMainEntity.load('aliasSet.aliases', {transacting}); + + /* New entities will lack some attributes like 'type' required for search indexing */ + await savedMainEntity.refresh({transacting}); + + /* fetch and reindex EditionGroups that may have been created automatically by the ORM and not indexed */ + if (savedMainEntity.get('type') === 'Edition') { + await indexAutoCreatedEditionGroup(orm, savedMainEntity, transacting); + } + bbidMap[entityKey] = savedMainEntity.get('bbid'); + savedMainEntities[entityKey] = savedMainEntity.toJSON(); + })); + + // adding relationship on newly created entites + await Promise.all(allRelationships.map(async (rels, index) => { + if (!_.isEmpty(rels)) { + const relationships = rels.map((rel) => ( + {...rel, sourceBbid: _.get(bbidMap, rel.sourceBbid) ?? rel.sourceBbid, + targetBbid: _.get(bbidMap, rel.targetBbid) ?? rel.targetBbid} + )); + const cEntity = savedMainEntities[index.toString()]; + const {relationshipSetId} = await handleAddRelationship({relationships}, editorJSON, cEntity, cEntity.type, orm, transacting); + cEntity.relationshipSetId = relationshipSetId; + } + })); + return savedMainEntities; + } + catch (err) { + log.error(err); + throw err; + } + }); + const achievementPromise = entityEditPromise.then( + (entitiesJSON:Record) => { + const entitiesAchievementsPromise = []; + for (const entityJSON of Object.values(entitiesJSON)) { + entitiesAchievementsPromise.push(achievement.processEdit( + orm, editorJSON.id, entityJSON.revisionId + ) + .then((unlock) => { + if (unlock.alert) { + entityJSON.alert = unlock.alert; + } + return entityJSON; + })); + } + return Promise.all(entitiesAchievementsPromise).catch(err => { throw err; }); + } + ); + return handler.sendPromiseResult( + res, + achievementPromise, + _bulkIndexEntities + ); +} diff --git a/src/server/routes/entity/publisher.js b/src/server/routes/entity/publisher.js index d0fd67608c..02004d9001 100644 --- a/src/server/routes/entity/publisher.js +++ b/src/server/routes/entity/publisher.js @@ -46,7 +46,7 @@ const additionalPublisherProps = [ 'typeId', 'areaId', 'beginDate', 'endDate', 'ended' ]; -function transformNewForm(data) { +export function transformNewForm(data) { const aliases = entityRoutes.constructAliases( data.aliasEditor, data.nameSection ); diff --git a/src/server/routes/entity/series.js b/src/server/routes/entity/series.js index fb6f1c4c27..810db80e1b 100644 --- a/src/server/routes/entity/series.js +++ b/src/server/routes/entity/series.js @@ -43,7 +43,7 @@ const additionalSeriesProps = [ 'entityType', 'orderingTypeId' ]; -function transformNewForm(data) { +export function transformNewForm(data) { const aliases = entityRoutes.constructAliases( data.aliasEditor, data.nameSection ); diff --git a/src/server/routes/entity/work.js b/src/server/routes/entity/work.js index 99a2a161c4..6818df94a3 100644 --- a/src/server/routes/entity/work.js +++ b/src/server/routes/entity/work.js @@ -43,7 +43,7 @@ import target from '../../templates/target'; *********** Helpers ************ *******************************/ -function transformNewForm(data) { +export function transformNewForm(data) { const aliases = entityRoutes.constructAliases( data.aliasEditor, data.nameSection ); diff --git a/src/server/routes/unifiedform.ts b/src/server/routes/unifiedform.ts new file mode 100644 index 0000000000..4654007147 --- /dev/null +++ b/src/server/routes/unifiedform.ts @@ -0,0 +1,8 @@ +import {createEntitesHandler} from '../helpers/entityRouteUtils'; +import express from 'express'; +import {isAuthenticatedForHandler} from '../helpers/auth'; + + +const router = express.Router(); +router.post('/create/handler', isAuthenticatedForHandler, createEntitesHandler); +export default router; From 83b90201e796f67eff0004d8499ff0c6493ff2a1 Mon Sep 17 00:00:00 2001 From: tri10 Date: Wed, 25 May 2022 22:39:48 +0530 Subject: [PATCH 005/258] fix(route): new transaction for each entity --- src/server/routes/entity/entity.tsx | 114 ++++++++++++++-------------- 1 file changed, 56 insertions(+), 58 deletions(-) diff --git a/src/server/routes/entity/entity.tsx b/src/server/routes/entity/entity.tsx index 50f9ac97ba..e2008b293f 100644 --- a/src/server/routes/entity/entity.tsx +++ b/src/server/routes/entity/entity.tsx @@ -1407,76 +1407,74 @@ export function handleCreateEntities( type: EntityTypeString } | null | undefined; - const entityEditPromise = bookshelf.transaction(async (transacting) => { - /* eslint-disable no-await-in-loop */ - const savedMainEntities = {}; - const bbidMap = {}; - const allRelationships = []; - try { - await Promise.all(Object.keys(body).map(async (entityKey:string) => { - const entityForm = body[entityKey]; - const entityType = _.upperFirst(entityForm.type); - allRelationships.push(entityForm.relationships); - const newEntity = await new Entity({type: entityType}).save(null, {transacting}); - currentEntity = newEntity.toJSON(); - // create new revision for each entity - const newRevision = await new Revision({ - authorId: editorJSON.id, - isMerge: false - }).save(null, {transacting}); - // console.log('here currentEntity', currentEntity); - const changedProps = await getChangedProps( - orm, transacting, true, currentEntity, entityForm, entityType, - newRevision, _.pick(entityForm, additionalEntityProps[_.lowerFirst(entityType)]) - ); - const mainEntity = await fetchOrCreateMainEntity( - orm, transacting, true, currentEntity.bbid, entityType - ); - // console.log('here main entity', mainEntity.toJSON()); - mainEntity.shouldInsert = true; - - // set changed attributes on main entity - _.forOwn(changedProps, (value, key) => mainEntity.set(key, value)); - const savedMainEntity = await saveEntitiesAndFinishRevision( - orm, transacting, true, newRevision, mainEntity, [mainEntity], - editorJSON.id, entityForm.note - ); + const savedMainEntities = {}; + const bbidMap = {}; + const allRelationships = []; + let entityEditPromise; + try { + entityEditPromise = Promise.all(Object.keys(body).map((entityKey:string) => bookshelf.transaction(async (transacting) => { + const entityForm = body[entityKey]; + const entityType = _.upperFirst(entityForm.type); + allRelationships.push(entityForm.relationships); + const newEntity = await new Entity({type: entityType}).save(null, {transacting}); + currentEntity = newEntity.toJSON(); + + // create new revision for each entity + const newRevision = await new Revision({ + authorId: editorJSON.id, + isMerge: false + }).save(null, {transacting}); + const changedProps = await getChangedProps( + orm, transacting, true, currentEntity, entityForm, entityType, + newRevision, _.pick(entityForm, additionalEntityProps[_.lowerFirst(entityType)]) + ); + const mainEntity = await fetchOrCreateMainEntity( + orm, transacting, true, currentEntity.bbid, entityType + ); + mainEntity.shouldInsert = true; - /* We need to load the aliases for search reindexing and refresh it*/ - await savedMainEntity.load('aliasSet.aliases', {transacting}); + // set changed attributes on main entity + _.forOwn(changedProps, (value, key) => mainEntity.set(key, value)); + const savedMainEntity = await saveEntitiesAndFinishRevision( + orm, transacting, true, newRevision, mainEntity, [mainEntity], + editorJSON.id, entityForm.note + ); - /* New entities will lack some attributes like 'type' required for search indexing */ - await savedMainEntity.refresh({transacting}); + /* We need to load the aliases for search reindexing and refresh it*/ + await savedMainEntity.load('aliasSet.aliases', {transacting}); - /* fetch and reindex EditionGroups that may have been created automatically by the ORM and not indexed */ - if (savedMainEntity.get('type') === 'Edition') { - await indexAutoCreatedEditionGroup(orm, savedMainEntity, transacting); - } - bbidMap[entityKey] = savedMainEntity.get('bbid'); - savedMainEntities[entityKey] = savedMainEntity.toJSON(); - })); + /* New entities will lack some attributes like 'type' required for search indexing */ + await savedMainEntity.refresh({transacting}); + + /* fetch and reindex EditionGroups that may have been created automatically by the ORM and not indexed */ + if (savedMainEntity.get('type') === 'Edition') { + await indexAutoCreatedEditionGroup(orm, savedMainEntity, transacting); + } + bbidMap[entityKey] = savedMainEntity.get('bbid'); + savedMainEntities[entityKey] = savedMainEntity.toJSON(); + return savedMainEntities[entityKey]; + }))); + + // adding relationship on newly created entites + } + catch (err) { + log.error(err); + throw err; + } - // adding relationship on newly created entites - await Promise.all(allRelationships.map(async (rels, index) => { + const achievementPromise = entityEditPromise.then( + async (entitiesJSON:Record) => { + await bookshelf.transaction((transacting) => Promise.all(allRelationships.map(async (rels, index) => { if (!_.isEmpty(rels)) { const relationships = rels.map((rel) => ( {...rel, sourceBbid: _.get(bbidMap, rel.sourceBbid) ?? rel.sourceBbid, - targetBbid: _.get(bbidMap, rel.targetBbid) ?? rel.targetBbid} + targetBbid: _.get(bbidMap, rel.targetBbid) ?? rel.targetBbid} )); const cEntity = savedMainEntities[index.toString()]; const {relationshipSetId} = await handleAddRelationship({relationships}, editorJSON, cEntity, cEntity.type, orm, transacting); cEntity.relationshipSetId = relationshipSetId; } - })); - return savedMainEntities; - } - catch (err) { - log.error(err); - throw err; - } - }); - const achievementPromise = entityEditPromise.then( - (entitiesJSON:Record) => { + }))); const entitiesAchievementsPromise = []; for (const entityJSON of Object.values(entitiesJSON)) { entitiesAchievementsPromise.push(achievement.processEdit( From 879909e4b1f01a4b2de9325ac98d10380625588b Mon Sep 17 00:00:00 2001 From: tri10 Date: Thu, 26 May 2022 07:26:35 +0530 Subject: [PATCH 006/258] fix: making transaction call sequentially --- src/server/routes/entity/entity.tsx | 54 ++++++++++++++++------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/src/server/routes/entity/entity.tsx b/src/server/routes/entity/entity.tsx index e2008b293f..dff93a7e0b 100644 --- a/src/server/routes/entity/entity.tsx +++ b/src/server/routes/entity/entity.tsx @@ -1389,6 +1389,7 @@ export async function handleAddRelationship( return savedMainEntity.toJSON(); } + export function handleCreateEntities( req: PassportRequest, res: $Response @@ -1407,15 +1408,15 @@ export function handleCreateEntities( type: EntityTypeString } | null | undefined; - const savedMainEntities = {}; - const bbidMap = {}; - const allRelationships = []; - let entityEditPromise; - try { - entityEditPromise = Promise.all(Object.keys(body).map((entityKey:string) => bookshelf.transaction(async (transacting) => { + const entityEditPromise = bookshelf.transaction(async (transacting) => { + /* eslint-disable no-await-in-loop */ + const savedMainEntities = {}; + const bbidMap = {}; + const allRelationships = {}; + async function processEntity(entityKey:string) { const entityForm = body[entityKey]; const entityType = _.upperFirst(entityForm.type); - allRelationships.push(entityForm.relationships); + allRelationships[entityKey] = entityForm.relationships; const newEntity = await new Entity({type: entityType}).save(null, {transacting}); currentEntity = newEntity.toJSON(); @@ -1452,29 +1453,34 @@ export function handleCreateEntities( } bbidMap[entityKey] = savedMainEntity.get('bbid'); savedMainEntities[entityKey] = savedMainEntity.toJSON(); - return savedMainEntities[entityKey]; - }))); - - // adding relationship on newly created entites - } - catch (err) { - log.error(err); - throw err; - } + } + try { + // bookshelf's transaction issue with Promise.All https://github.com/bookshelf/bookshelf/issues/1498 + await Object.keys(body).reduce((promise, entityKey) => promise.then(() => processEntity(entityKey)), Promise.resolve()); - const achievementPromise = entityEditPromise.then( - async (entitiesJSON:Record) => { - await bookshelf.transaction((transacting) => Promise.all(allRelationships.map(async (rels, index) => { + // adding relationship on newly created entites + await Promise.all(Object.keys(allRelationships).map(async (entityId) => { + const rels = allRelationships[entityId]; if (!_.isEmpty(rels)) { const relationships = rels.map((rel) => ( {...rel, sourceBbid: _.get(bbidMap, rel.sourceBbid) ?? rel.sourceBbid, - targetBbid: _.get(bbidMap, rel.targetBbid) ?? rel.targetBbid} + targetBbid: _.get(bbidMap, rel.targetBbid) ?? rel.targetBbid} )); - const cEntity = savedMainEntities[index.toString()]; - const {relationshipSetId} = await handleAddRelationship({relationships}, editorJSON, cEntity, cEntity.type, orm, transacting); - cEntity.relationshipSetId = relationshipSetId; + const mainEntity = savedMainEntities[entityId]; + const {relationshipSetId} = await handleAddRelationship({relationships}, editorJSON, + mainEntity, mainEntity.type, orm, transacting); + mainEntity.relationshipSetId = relationshipSetId; } - }))); + })); + return savedMainEntities; + } + catch (err) { + log.error(err); + throw err; + } + }); + const achievementPromise = entityEditPromise.then( + (entitiesJSON:Record) => { const entitiesAchievementsPromise = []; for (const entityJSON of Object.values(entitiesJSON)) { entitiesAchievementsPromise.push(achievement.processEdit( From f7b1e4a6cc852dd912df374c36b57e3b573e878a Mon Sep 17 00:00:00 2001 From: tri10 Date: Thu, 26 May 2022 18:37:50 +0530 Subject: [PATCH 007/258] feat(uf-routes): minor improvements --- src/server/helpers/entityRouteUtils.tsx | 19 +++++++++++-- src/server/routes/entity/entity.tsx | 38 ++++++++++++++++++------- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index 1c209b4034..c9b28bc976 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -37,7 +37,7 @@ import {generateProps} from './props'; const {createRootReducer, getEntitySection, getEntitySectionMerge, getValidator} = entityEditorHelpers; - +const validEntityTypes = ['author', 'edition', 'editionGroup', 'publisher', 'series', 'work']; type EntityAction = 'create' | 'edit'; type PassportRequest = $Request & {user: any, session: any}; @@ -313,12 +313,18 @@ export function addInitialRelationship(props, relationshipTypeId, relationshipIn return props; } -export function validateUnifiedForm(body:Record):boolean { +/** + * Validate Unified form + * @param {object} body - request body + * @returns {boolean} + */ + +function validateUnifiedForm(body:Record):boolean { for (const entityKey in body) { if (Object.prototype.hasOwnProperty.call(body, entityKey)) { const entityForm = body[entityKey]; const entityType = _.snakeCase(entityForm.type); - if (!entityType) { + if (!entityType && !validEntityTypes.includes(entityType)) { return false; } const validator = getValidator(entityType); @@ -329,6 +335,13 @@ export function validateUnifiedForm(body:Record):boolean { } return true; } + +/** + * Middleware for handling unified form submission + * @param {object} req - Request object + * @param {object} res - Response object + */ + export function createEntitesHandler( req:$Request, res:$Response diff --git a/src/server/routes/entity/entity.tsx b/src/server/routes/entity/entity.tsx index dff93a7e0b..66b292095c 100644 --- a/src/server/routes/entity/entity.tsx +++ b/src/server/routes/entity/entity.tsx @@ -49,15 +49,23 @@ import WorkPage from '../../../client/components/pages/entities/work'; import _ from 'lodash'; import {_bulkIndexEntities} from '../../../common/helpers/search'; import {transformNewForm as authorTransform} from './author'; +import {transformNewForm as editionGroupTransform} from './edition-group'; +import {transformNewForm as editionTransform} from './edition'; import {getEntityLabel} from '../../../client/helpers/entity'; import {getOrderedRevisionsForEntityPage} from '../../helpers/revisions'; import log from 'log'; +import {transformNewForm as publisherTransform} from './publisher'; +import {transformNewForm as seriesTransform} from './series'; import target from '../../templates/target'; import {transformNewForm as workTransform} from './work'; const transformFunctions = { author: authorTransform, + edition: editionTransform, + editionGroup: editionGroupTransform, + publisher: publisherTransform, + series: seriesTransform, work: workTransform }; const additionalEntityProps = { @@ -65,6 +73,13 @@ const additionalEntityProps = { 'typeId', 'genderId', 'beginAreaId', 'beginDate', 'endDate', 'ended', 'endAreaId' ], + edition: [ + 'editionGroupBbid', 'width', 'height', 'depth', 'weight', 'pages', + 'formatId', 'statusId' + ], + editionGroup: 'typeid', + publisher: ['typeId', 'areaId', 'beginDate', 'endDate', 'ended'], + series: ['entityType', 'orderingTypeId'], work: 'typeId' }; @@ -1341,8 +1356,8 @@ export function transformForm(body:Record):Record { export async function handleAddRelationship( - body, - editorJSON, + body:Record, + editorId:number, currentEntity, entityType:EntityTypeString, orm, @@ -1350,8 +1365,9 @@ export async function handleAddRelationship( ) { const {Revision} = orm; + // new revision for adding relationship const newRevision = await new Revision({ - authorId: editorJSON.id, + authorId: editorId, isMerge: false }).save(null, {transacting}); const relationshipSets = await getNextRelationshipSets( @@ -1364,7 +1380,8 @@ export async function handleAddRelationship( const mainEntity = await fetchOrCreateMainEntity( orm, transacting, false, currentEntity.bbid, entityType ); - // Fetch all entities that definitely exist + + // Fetch all entities that definitely exist const otherEntities = await fetchEntitiesForRelationships( orm, transacting, currentEntity, relationshipSets ); @@ -1384,12 +1401,11 @@ export async function handleAddRelationship( }); const savedMainEntity = await saveEntitiesAndFinishRevision( orm, transacting, false, newRevision, mainEntity, allEntities, - editorJSON.id, body.note + editorId, body.note ); return savedMainEntity.toJSON(); } - export function handleCreateEntities( req: PassportRequest, res: $Response @@ -1409,10 +1425,11 @@ export function handleCreateEntities( } | null | undefined; const entityEditPromise = bookshelf.transaction(async (transacting) => { - /* eslint-disable no-await-in-loop */ const savedMainEntities = {}; + // map dummy id to real bbid const bbidMap = {}; const allRelationships = {}; + // callback for creating entity async function processEntity(entityKey:string) { const entityForm = body[entityKey]; const entityType = _.upperFirst(entityForm.type); @@ -1425,9 +1442,10 @@ export function handleCreateEntities( authorId: editorJSON.id, isMerge: false }).save(null, {transacting}); + const additionalProps = _.pick(entityForm, additionalEntityProps[_.snakeCase(entityType)]); const changedProps = await getChangedProps( orm, transacting, true, currentEntity, entityForm, entityType, - newRevision, _.pick(entityForm, additionalEntityProps[_.lowerFirst(entityType)]) + newRevision, additionalProps ); const mainEntity = await fetchOrCreateMainEntity( orm, transacting, true, currentEntity.bbid, entityType @@ -1455,7 +1473,7 @@ export function handleCreateEntities( savedMainEntities[entityKey] = savedMainEntity.toJSON(); } try { - // bookshelf's transaction issue with Promise.All https://github.com/bookshelf/bookshelf/issues/1498 + // bookshelf's transaction have issue with Promise.All await Object.keys(body).reduce((promise, entityKey) => promise.then(() => processEntity(entityKey)), Promise.resolve()); // adding relationship on newly created entites @@ -1467,7 +1485,7 @@ export function handleCreateEntities( targetBbid: _.get(bbidMap, rel.targetBbid) ?? rel.targetBbid} )); const mainEntity = savedMainEntities[entityId]; - const {relationshipSetId} = await handleAddRelationship({relationships}, editorJSON, + const {relationshipSetId} = await handleAddRelationship({relationships}, editorJSON.id, mainEntity, mainEntity.type, orm, transacting); mainEntity.relationshipSetId = relationshipSetId; } From 8a4402933a111de2d5e0ea3aab038ecf90f067ba Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 28 May 2022 22:12:44 +0530 Subject: [PATCH 008/258] fix(uf-route): adding publishers to a edition --- src/client/entity-editor/validators/edition.ts | 12 ++++++------ src/server/helpers/entityRouteUtils.tsx | 2 +- src/server/routes/entity/entity.tsx | 4 ++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/client/entity-editor/validators/edition.ts b/src/client/entity-editor/validators/edition.ts index 2519b71dfb..2773f2f833 100644 --- a/src/client/entity-editor/validators/edition.ts +++ b/src/client/entity-editor/validators/edition.ts @@ -70,12 +70,12 @@ export function validateEditionSectionEditionGroup(value: any, editionGroupRequi return validateUUID(get(value, 'id', null), editionGroupRequired); } -export function validateEditionSectionPublisher(value: any): boolean { +export function validateEditionSectionPublisher(value: any, isCustom = false): boolean { if (!value) { return true; } - - return validateUUID(get(value, 'id', null), true); + // custom/dummy id is used for unified form + return isCustom ? Boolean(get(value, 'id', null)) : validateUUID(get(value, 'id', null), true); } export function validateEditionSectionReleaseDate(value: any) { @@ -95,7 +95,7 @@ export function validateEditionSectionWidth(value: any): boolean { return validatePositiveInteger(value); } -export function validateEditionSection(data: any): boolean { +export function validateEditionSection(data: any, isCustom = false): boolean { return ( validateEditionSectionDepth(get(data, 'depth', null)) && validateEditionSectionFormat(get(data, 'format', null)) && @@ -106,7 +106,7 @@ export function validateEditionSection(data: any): boolean { get(data, 'editionGroup', null), get(data, 'editionGroupRequired', null) ) && - validateEditionSectionPublisher(get(data, 'publisher', null)) && + validateEditionSectionPublisher(get(data, 'publisher', null), isCustom) && validateEditionSectionReleaseDate(get(data, 'releaseDate', null)).isValid && validateEditionSectionStatus(get(data, 'status', null)) && validateEditionSectionWeight(get(data, 'weight', null)) && @@ -123,7 +123,7 @@ export function validateForm( get(formData, 'identifierEditor', {}), identifierTypes ), validateNameSection(get(formData, 'nameSection', {})), - validateEditionSection(get(formData, 'editionSection', {})), + validateEditionSection(get(formData, 'editionSection', {}), Boolean(formData.type)), validateSubmissionSection(get(formData, 'submissionSection', {})) ]; diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index c9b28bc976..bf3ddb8d89 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -349,7 +349,7 @@ export function createEntitesHandler( // validating if (!validateUnifiedForm(req.body)) { const err = new error.FormSubmissionError(); - error.sendErrorAsJSON(res, err); + return error.sendErrorAsJSON(res, err); } // transforming req.body = entityRoutes.transformForm(req.body); diff --git a/src/server/routes/entity/entity.tsx b/src/server/routes/entity/entity.tsx index 66b292095c..6bab921412 100644 --- a/src/server/routes/entity/entity.tsx +++ b/src/server/routes/entity/entity.tsx @@ -1433,6 +1433,10 @@ export function handleCreateEntities( async function processEntity(entityKey:string) { const entityForm = body[entityKey]; const entityType = _.upperFirst(entityForm.type); + // edition entity should be on the bottom of the list + if (entityType === 'Edition' && !_.isEmpty(entityForm.publishers)) { + entityForm.publishers = entityForm.publishers.map((id) => bbidMap[id] ?? id); + } allRelationships[entityKey] = entityForm.relationships; const newEntity = await new Entity({type: entityType}).save(null, {transacting}); currentEntity = newEntity.toJSON(); From 39ca3d4a93fe2b84dfb8055db4e613e9ecec93b1 Mon Sep 17 00:00:00 2001 From: tri10 Date: Tue, 31 May 2022 16:15:59 +0530 Subject: [PATCH 009/258] feat(uf-routes): add basic tests --- test/src/server/routes/unifiedform.js | 211 ++++++++++++++++++++++++++ test/test-helpers/create-entities.js | 24 ++- 2 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 test/src/server/routes/unifiedform.js diff --git a/test/src/server/routes/unifiedform.js b/test/src/server/routes/unifiedform.js new file mode 100644 index 0000000000..3489a585a7 --- /dev/null +++ b/test/src/server/routes/unifiedform.js @@ -0,0 +1,211 @@ +import {baseState, createEditor, createPublisher, + createWork, getRandomUUID, languageAttribs, truncateEntities} from '../../../test-helpers/create-entities'; +import app from '../../../../src/server/app'; +import chai from 'chai'; +import chaiHttp from 'chai-http'; +import {forOwn} from 'lodash'; +import orm from '../../../bookbrainz-data'; + + +const {Language, RelationshipType} = orm; +const relationshipTypeData = { + description: 'test descryption', + label: 'test label', + linkPhrase: 'test phrase', + reverseLinkPhrase: 'test reverse link phrase', + sourceEntityType: 'Edition', + targetEntityType: 'Work' +}; +chai.use(chaiHttp); +const {expect} = chai; + +describe('Unified form routes', () => { + let agent; + let newLanguage; + let newRelationshipType; + const wBBID = getRandomUUID(); + const pBBID = getRandomUUID(); + + before(async () => { + try { + await createEditor(123456); + await createWork(wBBID); + await createPublisher(pBBID); + newLanguage = await new Language({...languageAttribs}) + .save(null, {method: 'insert'}); + newRelationshipType = await new RelationshipType(relationshipTypeData) + .save(null, {method: 'insert'}); + } + catch (error) { + // console.log(error); + } + // Log in; use agent to use logged in session + agent = await chai.request.agent(app); + await agent.get('/cb'); + }); + after(truncateEntities); + it('should not throw error while creating single entity', async () => { + const postData = {b0: { + ...baseState, + editionSection: {}, + type: 'Edition' + }}; + postData.b0.nameSection.language = newLanguage.id; + const res = await agent.post('/create/handler').send(postData); + expect(res).to.be.ok; + expect(res).to.have.status(200); + }); + + it('should not throw error while creating multiple entities', async () => { + const postData = {b0: { + ...baseState, + editionSection: {}, + type: 'Edition' + }, + b1: { + ...baseState, + type: 'Work', + workSection: { + languages: [], + type: null + } + }}; + forOwn(postData, (value) => { + value.nameSection.language = newLanguage.id; + }); + const res = await agent.post('/create/handler').send(postData); + expect(res).to.be.ok; + expect(res).to.have.status(200); + }); + + it('should not throw error when linking existing works to edition', async () => { + const postData = {b0: { + ...baseState, + editionSection: {}, + relationshipSection: { + relationships: { + n0: { + attributeSetId: null, + attributes: [], + relationshipType: { + id: newRelationshipType.id + }, + rowID: 'n0', + sourceEntity: { + bbid: 'b0' + }, + targetEntity: { + bbid: wBBID + } + } + } + }, + type: 'Edition' + }}; + forOwn(postData, (value) => { + value.nameSection.language = newLanguage.id; + }); + const res = await agent.post('/create/handler').send(postData); + expect(res).to.be.ok; + expect(res).to.have.status(200); + }); + + it('should not throw error when linking newly created works to edition', async () => { + const postData = {b0: { + ...baseState, + editionSection: {}, + relationshipSection: { + relationships: { + n0: { + attributeSetId: null, + attributes: [], + relationshipType: { + id: newRelationshipType.id + }, + rowID: 'n0', + sourceEntity: { + bbid: 'b0' + }, + targetEntity: { + bbid: 'b1' + } + } + } + }, + type: 'Edition' + }, + b1: { + ...baseState, + type: 'Work', + workSection: { + languages: [], + type: null + } + }}; + forOwn(postData, (value) => { + value.nameSection.language = newLanguage.id; + }); + const res = await agent.post('/create/handler').send(postData); + expect(res).to.be.ok; + expect(res).to.have.status(200); + }); + + it('should not throw error while linking existing publisher to edition', async () => { + const postData = {b0: { + ...baseState, + editionSection: { + publisher: { + id: pBBID + } + }, + type: 'Edition' + }}; + postData.b0.nameSection.language = newLanguage.id; + const res = await agent.post('/create/handler').send(postData); + expect(res).to.be.ok; + expect(res).to.have.status(200); + }); + + it('should not throw error while linking newly created publisher to edition', async () => { + const postData = {b0: { + ...baseState, + publisherSection: { + beginDate: { + day: '', + month: '', + year: '' + }, + endDate: { + day: '', + month: '', + year: '' + }, + ended: false, + type: null + }, + type: 'Publisher' + }, + b1: { + ...baseState, + editionSection: { + publisher: { + id: 'b0' + } + }, + type: 'Edition' + }}; + postData.b0.nameSection.language = newLanguage.id; + const res = await agent.post('/create/handler').send(postData); + expect(res).to.be.ok; + expect(res).to.have.status(200); + }); + + it('should throw bad request error while posting invalid form', async () => { + const postData = {b0: { + ...baseState, + editionSection: {} + }}; + const res = await agent.post('/create/handler').send(postData); + expect(res).to.have.status(400); + }); +}); diff --git a/test/test-helpers/create-entities.js b/test/test-helpers/create-entities.js index 3785d9d8e6..0d29572d83 100644 --- a/test/test-helpers/create-entities.js +++ b/test/test-helpers/create-entities.js @@ -45,6 +45,28 @@ export const seedInitialState = { submissionSection: 'note' }; +export const baseState = { + aliasEditor: {}, + annotationSection: { + content: '' + }, + identifierEditor: {}, + nameSection: { + disambiguation: '', + language: 1, + name: 'bob', + sortName: 'bob' + }, + relationshipSection: { + relationships: {} + }, + submissionSection: { + note: 'first entity', + submitError: '', + submitted: false + } +}; + export const editorTypeAttribs = { label: 'test_type' }; @@ -58,7 +80,7 @@ export const editorAttribs = { typeId: 1 }; -const languageAttribs = { +export const languageAttribs = { frequency: 1, isoCode1: 'en', isoCode2b: 'eng', From 82662ab4ccc52ce6eda97b9d58cbdcfb2ff9138f Mon Sep 17 00:00:00 2001 From: tri10 Date: Tue, 31 May 2022 16:16:10 +0530 Subject: [PATCH 010/258] fix linting issues --- src/server/routes/merge.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/routes/merge.ts b/src/server/routes/merge.ts index d7ddcd79a3..7dfbb31b77 100644 --- a/src/server/routes/merge.ts +++ b/src/server/routes/merge.ts @@ -166,7 +166,7 @@ function loadEntityRelationships(entity, orm, transacting): Promise { // Default to empty array, its presence is expected down the line entity.relationships = []; - + if (!entity.relationshipSetId) { return null; } @@ -183,7 +183,7 @@ function loadEntityRelationships(entity, orm, transacting): Promise { ] }) .then((relationshipSet) => { - if(relationshipSet){ + if (relationshipSet) { entity.relationships = relationshipSet.related('relationships').toJSON(); } From 359d667a7d5b04c91cd085071f6a2803e3973c48 Mon Sep 17 00:00:00 2001 From: tri10 Date: Tue, 31 May 2022 16:40:50 +0530 Subject: [PATCH 011/258] minor fixes --- src/client/entity-editor/validators/edition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/entity-editor/validators/edition.ts b/src/client/entity-editor/validators/edition.ts index 2773f2f833..b5509d84b6 100644 --- a/src/client/entity-editor/validators/edition.ts +++ b/src/client/entity-editor/validators/edition.ts @@ -123,7 +123,7 @@ export function validateForm( get(formData, 'identifierEditor', {}), identifierTypes ), validateNameSection(get(formData, 'nameSection', {})), - validateEditionSection(get(formData, 'editionSection', {}), Boolean(formData.type)), + validateEditionSection(get(formData, 'editionSection', {}), Boolean(formData?.type)), validateSubmissionSection(get(formData, 'submissionSection', {})) ]; From 124c42ae01b5c8e21be30ba22c857956267e651e Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 3 Jun 2022 06:43:55 +0530 Subject: [PATCH 012/258] feat(uf) :added basic page for unified form --- src/client/containers/layout.js | 4 ++ src/client/helpers/entity.tsx | 3 +- src/client/unified-form/controller.js | 64 ++++++++++++++++++++++++ src/client/unified-form/cover/cover.tsx | 17 +++++++ src/client/unified-form/helpers.ts | 29 +++++++++++ src/client/unified-form/unified-form.tsx | 17 +++++++ src/server/helpers/entityRouteUtils.tsx | 53 ++++++++++++++++++++ src/server/routes.js | 2 + src/server/routes/unifiedform.ts | 27 ++++++++++ webpack.client.js | 1 + 10 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 src/client/unified-form/controller.js create mode 100644 src/client/unified-form/cover/cover.tsx create mode 100644 src/client/unified-form/helpers.ts create mode 100644 src/client/unified-form/unified-form.tsx create mode 100644 src/server/routes/unifiedform.ts diff --git a/src/client/containers/layout.js b/src/client/containers/layout.js index 172e12f92d..0e6b2ca1cf 100644 --- a/src/client/containers/layout.js +++ b/src/client/containers/layout.js @@ -163,6 +163,10 @@ class Layout extends React.Component { {genEntityIconHTMLElement('Publisher')} Publisher + + {genEntityIconHTMLElement('Book')} + Book + ; +} + +const store = createStore( + rootReducer, + Immutable.fromJS(initialState), + composeEnhancers(applyMiddleware(debouncer, ReduxThunk)) +); + +const markup = ( + + + + {getEntityEditor()} + + + +); + +ReactDOM.hydrate(markup, document.getElementById('target')); + +/* + * As we are not exporting a component, + * we cannot use the react-hot-loader module wrapper, + * but instead directly use webpack Hot Module Replacement API + */ + +if (module.hot) { + module.hot.accept(); +} diff --git a/src/client/unified-form/cover/cover.tsx b/src/client/unified-form/cover/cover.tsx new file mode 100644 index 0000000000..8e1cbfb392 --- /dev/null +++ b/src/client/unified-form/cover/cover.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import {connect} from 'react-redux'; + + +export function Cover(props) { + return
Cover
; +} + +function mapStateToProps(state) { + return {}; +} + +function mapDispatchToProps(state) { + return {}; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Cover); diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts new file mode 100644 index 0000000000..a0bc6e3180 --- /dev/null +++ b/src/client/unified-form/helpers.ts @@ -0,0 +1,29 @@ +import aliasEditorReducer from '../entity-editor/alias-editor/reducer'; +import annotationSectionReducer from '../entity-editor/annotation-section/reducer'; +import buttonBarReducer from '../entity-editor/button-bar/reducer'; +import {combineReducers} from 'redux'; +import editionSectionReducer from '../entity-editor/edition-section/reducer'; +import identifierEditorReducer from '../entity-editor/identifier-editor/reducer'; +import nameSectionReducer from '../entity-editor/name-section/reducer'; +import submissionSectionReducer from '../entity-editor/submission-section/reducer'; + + +type ReduxWindow = typeof window & {__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any}; +export function shouldDevToolsBeInjected(): boolean { + return Boolean( + typeof window === 'object' && + (window as ReduxWindow).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ + ); +} + +export function createRootReducer() { + return combineReducers({ + aliasEditor: aliasEditorReducer, + annotationSection: annotationSectionReducer, + buttonBar: buttonBarReducer, + editionSection: editionSectionReducer, + identifierEditor: identifierEditorReducer, + nameSection: nameSectionReducer, + submissionSection: submissionSectionReducer + }); +} diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx new file mode 100644 index 0000000000..14920473e5 --- /dev/null +++ b/src/client/unified-form/unified-form.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import {connect} from 'react-redux'; + + +export function UnifiedForm(props) { + return
UnifiedForm
; +} + +function mapStateToProps(state, userprops) { + return {}; +} + +function mapDispatchToProps(state) { + return {}; +} + +export default connect(mapStateToProps, mapDispatchToProps)(UnifiedForm); diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index 2408cc7408..71572d82a5 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -34,8 +34,11 @@ import ReactDOMServer from 'react-dom/server'; import _ from 'lodash'; import {createStore} from 'redux'; import {generateProps} from './props'; +import UnifiedForm from '../../client/unified-form/unified-form'; +import * as UnifiedFormHelpers from '../../client/unified-form/helpers'; +const {createRootReducer: ufCreateRootReducer} = UnifiedFormHelpers; const {createRootReducer, getEntitySection, getEntitySectionMerge, getValidator} = entityEditorHelpers; type EntityAction = 'create' | 'edit'; @@ -312,3 +315,53 @@ export function addInitialRelationship(props, relationshipTypeId, relationshipIn return props; } + +/** + * Return markup for the unified form. + * This also modifies the props value with a new initialState! + * @param {object} props - react props + * @returns {object} - Updated props and HTML string with markup + */ + +export function unifiedFormMarkup(props:{initialState:any}) { + const {initialState, ...rest} = props; + const rootReducer = ufCreateRootReducer(); + const store:any = createStore(rootReducer, Immutable.fromJS(initialState)); + const markup = ReactDOMServer.renderToString( + + + + + + ); + return { + markup, + props: Object.assign({}, props, {initialState: store.getState()}) + }; +} + +/** + * Returns a props object with reasonable defaults for unified form. + * @param {request} req - request object + * @param {response} res - response object + * @param {object} additionalProps - additional props + * @param {initialStateCallback} initialStateCallback - callback + * to get the initial state + * @returns {object} - props + */ +export function generateUnifiedProps( + req: PassportRequest, res: $Response, + additionalProps: any, + initialStateCallback: () => any = () => new Object() +): any { + const submissionUrl = '/create/handler'; + const props = Object.assign({ + identifierTypes: res.locals.identifierTypes, + initialStateCallback: initialStateCallback(), + languageOptions: res.locals.languages, + requiresJS: true, + submissionUrl + }, additionalProps); + + return generateProps(req, res, props); +} diff --git a/src/server/routes.js b/src/server/routes.js index 56a29a35e3..9038a29e2f 100644 --- a/src/server/routes.js +++ b/src/server/routes.js @@ -33,12 +33,14 @@ import revisionsRouter from './routes/revisions'; import searchRouter from './routes/search'; import seriesRouter from './routes/entity/series'; import statisticsRouter from './routes/statistics'; +import ufRouter from './routes/unifiedform'; import workRouter from './routes/entity/work'; function initRootRoutes(app) { app.use('/', indexRouter); app.use('/', authRouter); + app.use('/', ufRouter); app.use('/search', searchRouter); app.use('/register', registerRouter); app.use('/revisions', revisionsRouter); diff --git a/src/server/routes/unifiedform.ts b/src/server/routes/unifiedform.ts new file mode 100644 index 0000000000..94bef3d54b --- /dev/null +++ b/src/server/routes/unifiedform.ts @@ -0,0 +1,27 @@ +import * as middleware from '../helpers/middleware'; +import {generateUnifiedProps, unifiedFormMarkup} from '../helpers/entityRouteUtils'; +import {escapeProps} from '../helpers/props'; +import express from 'express'; +import {isAuthenticated} from '../helpers/auth'; +import target from '../templates/target'; + + +type PassportRequest = express.Request & {user: any, session: any}; + +const router = express.Router(); +router.get('/create', isAuthenticated, middleware.loadIdentifierTypes, + middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadGenders, middleware.loadAuthorTypes, + middleware.loadRelationshipTypes, (req:PassportRequest, res:express.Response) => { + const props = generateUnifiedProps(req, res, {}); + const formMarkup = unifiedFormMarkup(props); + const {markup, props: updatedProps} = formMarkup; + return res.send(target({ + markup, + page: 'Unified form', + props: escapeProps(updatedProps), + script: '/js/unified-form.js', + title: 'Unified form' + })); + }); + +export default router; diff --git a/webpack.client.js b/webpack.client.js index 4596dcd838..6979f01993 100644 --- a/webpack.client.js +++ b/webpack.client.js @@ -36,6 +36,7 @@ const clientConfig = { 'editor/editor': ['./controllers/editor/editor.js'], 'entity/entity': ['./controllers/entity/entity.js'], 'entity-editor': ['./entity-editor/controller.js'], + 'unified-form':['./unified-form/controller.js'], 'entity-merge': ['./entity-editor/entity-merge.tsx'], style: './stylesheets/style.scss' }, From 3339cdf3dc8f280a25c2d73290b8ddae7d34ea71 Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 3 Jun 2022 14:22:01 +0530 Subject: [PATCH 013/258] feat(uf): added submission section --- .../submission-section/submission-section.js | 2 +- src/client/unified-form/controller.js | 4 ++-- .../cover.tsx => cover-tab/cover-tab.tsx} | 0 src/client/unified-form/helpers.ts | 13 ++++++++++- src/client/unified-form/interface/type.ts | 8 +++++++ src/client/unified-form/unified-form.tsx | 22 +++++++++++++++++-- src/common/helpers/utils.ts | 8 +++++++ src/server/helpers/entityRouteUtils.tsx | 9 ++++---- src/server/helpers/utils.ts | 11 +--------- 9 files changed, 57 insertions(+), 20 deletions(-) rename src/client/unified-form/{cover/cover.tsx => cover-tab/cover-tab.tsx} (100%) create mode 100644 src/client/unified-form/interface/type.ts diff --git a/src/client/entity-editor/submission-section/submission-section.js b/src/client/entity-editor/submission-section/submission-section.js index 53e8a541e9..b2f097c6e3 100644 --- a/src/client/entity-editor/submission-section/submission-section.js +++ b/src/client/entity-editor/submission-section/submission-section.js @@ -121,7 +121,7 @@ function mapStateToProps(rootState, {validate, identifierTypes}) { const state = rootState.get('submissionSection'); return { errorText: state.get('submitError'), - formValid: validate(rootState, identifierTypes), + formValid: validate && validate(rootState, identifierTypes), note: state.get('note'), submitted: state.get('submitted') }; diff --git a/src/client/unified-form/controller.js b/src/client/unified-form/controller.js index 8091436d6f..09859981e7 100644 --- a/src/client/unified-form/controller.js +++ b/src/client/unified-form/controller.js @@ -16,7 +16,7 @@ import createDebounce from 'redux-debounce'; const { - createRootReducer, shouldDevToolsBeInjected + createRootReducer, shouldDevToolsBeInjected, validatorMap } = helpers; const KEYSTROKE_DEBOUNCE_TIME = 250; @@ -32,7 +32,7 @@ const composeEnhancers = shouldDevToolsBeInjected() ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose; function getEntityEditor() { - return ; + return ; } const store = createStore( diff --git a/src/client/unified-form/cover/cover.tsx b/src/client/unified-form/cover-tab/cover-tab.tsx similarity index 100% rename from src/client/unified-form/cover/cover.tsx rename to src/client/unified-form/cover-tab/cover-tab.tsx diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index a0bc6e3180..f8bd7c60a7 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -1,11 +1,15 @@ import aliasEditorReducer from '../entity-editor/alias-editor/reducer'; import annotationSectionReducer from '../entity-editor/annotation-section/reducer'; import buttonBarReducer from '../entity-editor/button-bar/reducer'; -import {combineReducers} from 'redux'; +import {combineReducers} from 'redux-immutable'; import editionSectionReducer from '../entity-editor/edition-section/reducer'; import identifierEditorReducer from '../entity-editor/identifier-editor/reducer'; import nameSectionReducer from '../entity-editor/name-section/reducer'; import submissionSectionReducer from '../entity-editor/submission-section/reducer'; +import {validateForm as validateAuthorForm} from '../entity-editor/validators/author'; +import {validateForm as validateEditionForm} from '../entity-editor/validators/edition'; +import {validateForm as validatePublisherForm} from '../entity-editor/validators/publisher'; +import {validateForm as validateWorkForm} from '../entity-editor/validators/work'; type ReduxWindow = typeof window & {__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any}; @@ -15,6 +19,13 @@ export function shouldDevToolsBeInjected(): boolean { (window as ReduxWindow).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ); } +export const validatorMap = { + author: validateAuthorForm, + edition: validateEditionForm, + publisher: validatePublisherForm, + work: validateWorkForm +}; + export function createRootReducer() { return combineReducers({ diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts new file mode 100644 index 0000000000..0abeafbf73 --- /dev/null +++ b/src/client/unified-form/interface/type.ts @@ -0,0 +1,8 @@ +type IdentifierType = { + id:number, + entityType:string +}; +export type UnifiedFormProps = { + identifierTypes:IdentifierType[], + validators:Record +}; diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index 14920473e5..bed712fa52 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -1,9 +1,27 @@ +import * as Boostrap from 'react-bootstrap'; +import CoverTab from './cover-tab/cover-tab'; import React from 'react'; +import SubmitSection from '../entity-editor/submission-section/submission-section'; +import {UnifiedFormProps} from './interface/type'; import {connect} from 'react-redux'; +import {filterIdentifierTypesByEntityType} from '../../common/helpers/utils'; -export function UnifiedForm(props) { - return
UnifiedForm
; +const {Tabs, Tab} = Boostrap; +export function UnifiedForm(props:UnifiedFormProps) { + const {identifierTypes, validators} = props; + const [tabKey, setTabKey] = React.useState('cover'); + const editionIdentifierTypes = filterIdentifierTypesByEntityType(identifierTypes, 'edition'); + return ( +
+ + + + + + + +
); } function mapStateToProps(state, userprops) { diff --git a/src/common/helpers/utils.ts b/src/common/helpers/utils.ts index 2b843532f0..5035f5c183 100644 --- a/src/common/helpers/utils.ts +++ b/src/common/helpers/utils.ts @@ -269,3 +269,11 @@ export function isbn13To10(isbn13:string):string | null { return digits.join(''); } +export function filterIdentifierTypesByEntityType( + identifierTypes: Array<{id: number, entityType: string}>, + entityType: string +): Array> { + return identifierTypes.filter( + (type) => type.entityType === entityType + ); +} diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index 71572d82a5..74f4694306 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -19,6 +19,7 @@ import * as Immutable from 'immutable'; import * as React from 'react'; +import * as UnifiedFormHelpers from '../../client/unified-form/helpers'; import * as entityEditorHelpers from '../../client/entity-editor/helpers'; import * as entityRoutes from '../routes/entity/entity'; import * as error from '../../common/helpers/error'; @@ -31,11 +32,11 @@ import EntityMerge from '../../client/entity-editor/entity-merge'; import Layout from '../../client/containers/layout'; import {Provider} from 'react-redux'; import ReactDOMServer from 'react-dom/server'; +import UnifiedForm from '../../client/unified-form/unified-form'; import _ from 'lodash'; import {createStore} from 'redux'; +import {filterIdentifierTypesByEntityType} from '../../common/helpers/utils'; import {generateProps} from './props'; -import UnifiedForm from '../../client/unified-form/unified-form'; -import * as UnifiedFormHelpers from '../../client/unified-form/helpers'; const {createRootReducer: ufCreateRootReducer} = UnifiedFormHelpers; @@ -72,7 +73,7 @@ export function generateEntityProps( const getFilteredIdentifierTypes = isEdit ? _.partialRight(utils.filterIdentifierTypesByEntity, entity) : - _.partialRight(utils.filterIdentifierTypesByEntityType, entityName); + _.partialRight(filterIdentifierTypesByEntityType, entityName); const filteredIdentifierTypes = getFilteredIdentifierTypes( res.locals.identifierTypes ); @@ -357,7 +358,7 @@ export function generateUnifiedProps( const submissionUrl = '/create/handler'; const props = Object.assign({ identifierTypes: res.locals.identifierTypes, - initialStateCallback: initialStateCallback(), + initialState: initialStateCallback(), languageOptions: res.locals.languages, requiresJS: true, submissionUrl diff --git a/src/server/helpers/utils.ts b/src/server/helpers/utils.ts index 0a96cda45c..a8b5eaa6b8 100644 --- a/src/server/helpers/utils.ts +++ b/src/server/helpers/utils.ts @@ -20,8 +20,8 @@ */ import * as search from '../../common/helpers/search'; +import {filterIdentifierTypesByEntityType, unflatten} from '../../common/helpers/utils'; import _ from 'lodash'; -import {unflatten} from '../../common/helpers/utils'; export function getDateBeforeDays(days) { @@ -30,15 +30,6 @@ export function getDateBeforeDays(days) { return date; } -export function filterIdentifierTypesByEntityType( - identifierTypes: Array<{id: number, entityType: string}>, - entityType: string -): Array> { - return identifierTypes.filter( - (type) => type.entityType === entityType - ); -} - export function filterIdentifierTypesByEntity( identifierTypes: any[], entity: any From 43542dbb5124e6e25aab4df1a6c38106392aa1f1 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 4 Jun 2022 17:38:17 +0530 Subject: [PATCH 014/258] feat(uf): added identifier editor --- .../entity-editor/button-bar/button-bar.js | 28 +++++++--- .../button-bar/identifier-button.js | 11 +++- .../entity-editor/common/name-field.tsx | 9 ++- .../name-section/name-section.js | 29 ++++++---- .../submission-section/actions.ts | 37 ++++++++++++ src/client/stylesheets/style.scss | 32 +++++++++++ .../unified-form/content-tab/content-tab.tsx | 17 ++++++ src/client/unified-form/cover-tab/action.ts | 20 +++++++ .../unified-form/cover-tab/cover-tab.tsx | 53 +++++++++++++++--- .../unified-form/cover-tab/isbn-field.tsx | 54 ++++++++++++++++++ src/client/unified-form/cover-tab/reducer.ts | 18 ++++++ .../unified-form/detail-tab/detail-tab.tsx | 17 ++++++ src/client/unified-form/helpers.ts | 2 + src/client/unified-form/interface/type.ts | 56 ++++++++++++++++++- src/client/unified-form/unified-form.tsx | 56 +++++++++++++------ 15 files changed, 388 insertions(+), 51 deletions(-) create mode 100644 src/client/unified-form/content-tab/content-tab.tsx create mode 100644 src/client/unified-form/cover-tab/action.ts create mode 100644 src/client/unified-form/cover-tab/isbn-field.tsx create mode 100644 src/client/unified-form/cover-tab/reducer.ts create mode 100644 src/client/unified-form/detail-tab/detail-tab.tsx diff --git a/src/client/entity-editor/button-bar/button-bar.js b/src/client/entity-editor/button-bar/button-bar.js index 5fbb26e3d4..858e2a47e6 100644 --- a/src/client/entity-editor/button-bar/button-bar.js +++ b/src/client/entity-editor/button-bar/button-bar.js @@ -54,22 +54,28 @@ function ButtonBar({ numAliases, numIdentifiers, onAliasButtonClick, + isUf, onIdentifierButtonClick }) { + const className = isUf ? 'text-right' : 'text-center'; return (
- - - - + { + !isUf && + + + + } + @@ -82,11 +88,15 @@ ButtonBar.displayName = 'ButtonBar'; ButtonBar.propTypes = { aliasesInvalid: PropTypes.bool.isRequired, identifiersInvalid: PropTypes.bool.isRequired, + isUf: PropTypes.bool, numAliases: PropTypes.number.isRequired, numIdentifiers: PropTypes.number.isRequired, onAliasButtonClick: PropTypes.func.isRequired, onIdentifierButtonClick: PropTypes.func.isRequired }; +ButtonBar.defaultProps = { + isUf: false +}; function mapStateToProps(rootState, {identifierTypes}) { return { diff --git a/src/client/entity-editor/button-bar/identifier-button.js b/src/client/entity-editor/button-bar/identifier-button.js index 3df15dc026..5883ef2438 100644 --- a/src/client/entity-editor/button-bar/identifier-button.js +++ b/src/client/entity-editor/button-bar/identifier-button.js @@ -40,6 +40,7 @@ import {faTimes} from '@fortawesome/free-solid-svg-icons'; function IdentifierButton({ identifiersInvalid, numIdentifiers, + isUf, ...props }) { let text = 'Add identifiers (eg. ISBN, Wikidata ID)…'; @@ -49,12 +50,14 @@ function IdentifierButton({ else if (numIdentifiers > 1) { text = `Edit ${numIdentifiers} identifiers (eg. ISBN, Wikidata ID)…`; } - + if (isUf) { + text = 'Add identifiers'; + } const iconElement = identifiersInvalid && ; return ( - @@ -63,7 +66,11 @@ function IdentifierButton({ IdentifierButton.displayName = 'IdentifierButton'; IdentifierButton.propTypes = { identifiersInvalid: PropTypes.bool.isRequired, + isUf: PropTypes.bool, numIdentifiers: PropTypes.number.isRequired }; +IdentifierButton.defaultProps = { + isUf: false +}; export default IdentifierButton; diff --git a/src/client/entity-editor/common/name-field.tsx b/src/client/entity-editor/common/name-field.tsx index a879713e80..0721a6cdc7 100644 --- a/src/client/entity-editor/common/name-field.tsx +++ b/src/client/entity-editor/common/name-field.tsx @@ -27,6 +27,7 @@ import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; type Props = { empty?: boolean, error?: boolean, + label?: string, tooltipText?: string, warn?: boolean }; @@ -47,13 +48,14 @@ type Props = { function NameField({ empty, error, + label, tooltipText, warn, ...rest }: Props) { - const label = ( + const inputLabel = ( - Name + {!label ? 'Name' : label} ); @@ -72,7 +74,7 @@ function NameField({ return ( - {label} + {inputLabel} {helpIconElement} @@ -83,6 +85,7 @@ NameField.displayName = 'NameField'; NameField.defaultProps = { empty: false, error: false, + label: '', tooltipText: null, warn: false }; diff --git a/src/client/entity-editor/name-section/name-section.js b/src/client/entity-editor/name-section/name-section.js index 774050f93c..9e0743d0c2 100644 --- a/src/client/entity-editor/name-section/name-section.js +++ b/src/client/entity-editor/name-section/name-section.js @@ -139,7 +139,8 @@ class NameSection extends React.Component { onLanguageChange, onSortNameChange, onDisambiguationChange, - searchResults + searchResults, + isUf } = this.props; const languageOptionsForDisplay = languageOptions.map((language) => ({ @@ -149,13 +150,17 @@ class NameSection extends React.Component { })); const warnIfExists = !_.isEmpty(exactMatches); - - + const colAttribs = { + lg: {offset: 3, span: 6} + }; + if (isUf) { + colAttribs.lg.offset = 0; + } return (
-

{`What is the ${_.startCase(entityType)} called?`}

+ {!isUf &&

{`What is the ${_.startCase(entityType)} called?`}

} - + - + {isRequiredDisambiguationEmpty( warnIfExists, disambiguationDefaultValue @@ -206,7 +211,7 @@ class NameSection extends React.Component { !warnIfExists && !_.isEmpty(searchResults) && - + If the {_.startCase(entityType)} you want to add appears in the results below, click on it to inspect it before adding a possible duplicate.
Ctrl/Cmd + click to open in a new tab @@ -215,7 +220,7 @@ class NameSection extends React.Component {
} - + - - + - + ): Promise { }); } +function postUFSubmission(url: string, data: Map): Promise { + // transform data + const jsonData = data.toJS(); + if (jsonData.ISBN.type) { + jsonData.identifierEditor.m0 = jsonData.ISBN; + } + + return request.post(url).send(jsonData) + .then((response: Response) => { + if (!response.body) { + window.location.replace('/login'); + } + + const redirectUrl = '/'; + if (response.body.alert) { + const alertParam = `?alert=${response.body.alert}`; + window.location.href = `${redirectUrl}${alertParam}`; + } + else { + window.location.href = redirectUrl; + } + }); +} + type SubmitResult = (arg1: (Action) => unknown, arg2: () => Map) => unknown; export function submit( submissionUrl: string @@ -121,6 +145,19 @@ export function submit( return (dispatch, getState) => { const rootState = getState(); dispatch(setSubmitted(true)); + // TO-DO: not the best best way + if (rootState.get('ISBN')) { + return postUFSubmission(submissionUrl, rootState) + .catch( + (error: {message: string}) => { + const message = + _.get(error, ['response', 'body', 'error'], null) || + error.message; + dispatch(setSubmitted(false)); + return dispatch(setSubmitError(message)); + } + ); + } return postSubmission(submissionUrl, rootState) .catch( (error: {message: string}) => { diff --git a/src/client/stylesheets/style.scss b/src/client/stylesheets/style.scss index 5e93c856a4..79312667ef 100644 --- a/src/client/stylesheets/style.scss +++ b/src/client/stylesheets/style.scss @@ -721,3 +721,35 @@ ul { .input-group > .input-group-append > .btn { padding: 6px 12px; } + +/* Unified form */ +$uf-primary:#EB743B; +.uf-main{ + padding: 0.6rem; +} +.uf-tab{ + border: 1px #dfdfdf solid; + h4{ + padding: 0.8rem; + margin-bottom: 0; + border-bottom: 0.125rem #ed8d60 solid; + background-color: #F7F7F7; + + } + .tab-content{ + padding: 1rem; + } +} +.uf-tab-header{ + padding: 0.6rem 0 0 0.6rem; + margin-bottom: 1rem; + background-color: #F4E9E3; + a{ + color: #754E37; + } + .nav-link.nav-item.active{ + color: $uf-primary; + border-color: $uf-primary; + border-bottom: none; + } +} \ No newline at end of file diff --git a/src/client/unified-form/content-tab/content-tab.tsx b/src/client/unified-form/content-tab/content-tab.tsx new file mode 100644 index 0000000000..9c721bb9d3 --- /dev/null +++ b/src/client/unified-form/content-tab/content-tab.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import {connect} from 'react-redux'; + + +export function ContentTab(props) { + return
ContentTab
; +} + +function mapStateToProps(state) { + return {}; +} + +function mapDispatchToProps(state) { + return {}; +} + +export default connect(mapStateToProps, mapDispatchToProps)(ContentTab); diff --git a/src/client/unified-form/cover-tab/action.ts b/src/client/unified-form/cover-tab/action.ts new file mode 100644 index 0000000000..7350ae24f5 --- /dev/null +++ b/src/client/unified-form/cover-tab/action.ts @@ -0,0 +1,20 @@ +import {Action} from '../interface/type'; + + +export const UPDATE_ISBN_VALUE = 'UPDATE_ISBN_VALUE'; +export const UPDATE_ISBN_TYPE = 'UPDATE_ISBN_TYPE'; + +export function debouncedUpdateISBNValue(newValue: string): Action { + return { + meta: {debounce: 'keystroke'}, + payload: newValue, + type: UPDATE_ISBN_VALUE + }; +} + +export function updateISBNType(typeId:number) { + return { + payload: typeId, + type: UPDATE_ISBN_TYPE + }; +} diff --git a/src/client/unified-form/cover-tab/cover-tab.tsx b/src/client/unified-form/cover-tab/cover-tab.tsx index 8e1cbfb392..37435b593c 100644 --- a/src/client/unified-form/cover-tab/cover-tab.tsx +++ b/src/client/unified-form/cover-tab/cover-tab.tsx @@ -1,17 +1,56 @@ +import {Col, Row} from 'react-bootstrap'; +import ButtonBar from '../../entity-editor/button-bar/button-bar'; +import {CoverProps} from '../interface/type'; +import EntitySearchFieldOption from '../../entity-editor/common/entity-search-field-option'; +import ISBNField from './isbn-field'; +import IdentifierEditor from '../../entity-editor/identifier-editor/identifier-editor'; +import NameSection from '../../entity-editor/name-section/name-section'; import React from 'react'; import {connect} from 'react-redux'; +import {updatePublisher} from '../../entity-editor/edition-section/actions'; -export function Cover(props) { - return
Cover
; +export function CoverTab(props:CoverProps) { + const {publisherValue, onPublisherChange, identifierEditorVisible} = props; + return ( +
+ + + + + + + + + + + + + + + +
+ + ); } -function mapStateToProps(state) { - return {}; +function mapStateToProps(rootState) { + return { + identifierEditorVisible: rootState.getIn(['buttonBar', 'identifierEditorVisible']), + publisherValue: rootState.get('publisher') + }; } -function mapDispatchToProps(state) { - return {}; +function mapDispatchToProps(dispatch) { + return { + onPublisherChange: (value) => dispatch(updatePublisher(value)) + }; } -export default connect(mapStateToProps, mapDispatchToProps)(Cover); +export default connect(mapStateToProps, mapDispatchToProps)(CoverTab); diff --git a/src/client/unified-form/cover-tab/isbn-field.tsx b/src/client/unified-form/cover-tab/isbn-field.tsx new file mode 100644 index 0000000000..71d2a5123f --- /dev/null +++ b/src/client/unified-form/cover-tab/isbn-field.tsx @@ -0,0 +1,54 @@ +import {ISBNDispatchProps, ISBNProps, RInputEvent} from '../interface/type'; +import {debouncedUpdateISBNValue, updateISBNType} from './action'; +import Immutable from 'immutable'; +import NameField from '../../entity-editor/common/name-field'; +import React from 'react'; +import {connect} from 'react-redux'; + + +export function ISBNField(props:ISBNProps) { + const {value, type, onChange} = props; + return ( +
+ +
); +} + +function mapStateToProps(rootState:Immutable.Map):Record { + return { + type: rootState.getIn(['ISBN', 'type']), + value: rootState.getIn(['ISBN', 'value']) + }; +} + +function mapDispatchToProps(dispatch):ISBNDispatchProps { + function onChange(event:RInputEvent) { + const {value} = event.target; + const isbn10rgx = new + RegExp('^(?:ISBN(?:-10)?:?●)?(?=[0-9X]{10}$|(?=(?:[0-9]+[-●]){3})[-●0-9X]{13}$)[0-9]{1,5}[-●]?[0-9]+[-●]?[0-9]+[-●]?[0-9X]$'); + const isbn13rgx = new + RegExp('^(?:ISBN(?:-13)?:?●)?(?=[0-9]{13}$|(?=(?:[0-9]+[-●]){4})[-●0-9]{17}$)97[89][-●]?[0-9]{1,5}[-●]?[0-9]+[-●]?[0-9]+[-●]?[0-9]$'); + if (isbn10rgx.test(value)) { + dispatch(updateISBNType(10)); + } + else if (isbn13rgx.test(value)) { + dispatch(updateISBNType(9)); + } + else { + dispatch(updateISBNType(null)); + } + dispatch(debouncedUpdateISBNValue(value)); + } + return { + onChange + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(ISBNField); diff --git a/src/client/unified-form/cover-tab/reducer.ts b/src/client/unified-form/cover-tab/reducer.ts new file mode 100644 index 0000000000..b2012e4572 --- /dev/null +++ b/src/client/unified-form/cover-tab/reducer.ts @@ -0,0 +1,18 @@ +import {UPDATE_ISBN_TYPE, UPDATE_ISBN_VALUE} from './action'; +import {Map as immutableMap} from 'immutable'; + + +export default function reducer(state = immutableMap({ + type: null, + value: '' +}), action) { + const {payload, type} = action; + switch (type) { + case UPDATE_ISBN_TYPE: + return state.set('type', payload); + case UPDATE_ISBN_VALUE: + return state.set('value', payload); + default: + return state; + } +} diff --git a/src/client/unified-form/detail-tab/detail-tab.tsx b/src/client/unified-form/detail-tab/detail-tab.tsx new file mode 100644 index 0000000000..8cb541c300 --- /dev/null +++ b/src/client/unified-form/detail-tab/detail-tab.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import {connect} from 'react-redux'; + + +export function DetailTab(props) { + return
DetailTab
; +} + +function mapStateToProps(state) { + return {}; +} + +function mapDispatchToProps(state) { + return {}; +} + +export default connect(mapStateToProps, mapDispatchToProps)(DetailTab); diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index f8bd7c60a7..59b1eca9a1 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -1,3 +1,4 @@ +import ISBNReducer from './cover-tab/reducer'; import aliasEditorReducer from '../entity-editor/alias-editor/reducer'; import annotationSectionReducer from '../entity-editor/annotation-section/reducer'; import buttonBarReducer from '../entity-editor/button-bar/reducer'; @@ -29,6 +30,7 @@ export const validatorMap = { export function createRootReducer() { return combineReducers({ + ISBN: ISBNReducer, aliasEditor: aliasEditorReducer, annotationSection: annotationSectionReducer, buttonBar: buttonBarReducer, diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index 0abeafbf73..a16198b1e7 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -1,8 +1,60 @@ -type IdentifierType = { + +export type RInputEvent = React.ChangeEvent; + +export type IdentifierType = { id:number, entityType:string }; +type EditionFormat = { + label: string, + id: number +}; + +type EditionStatus = { + label: string, + id: number +}; +type LanguageOption = { + frequency: number, + name: string, + id: number +}; +export type UnifiedFormDispatchProps = { + onSubmit: (event:React.FormEvent) =>unknown +}; export type UnifiedFormProps = { identifierTypes:IdentifierType[], - validators:Record + validators:Record, +} & UnifiedFormDispatchProps; + +export type CoverOwnProps = { + languageOptions: LanguageOption[], + editionFormats:EditionFormat[], + identifierTypes:IdentifierType[] + editionStatuses: EditionStatus[] +}; +export type CoverStateProps = { + publisherValue:any[], + identifierEditorVisible:boolean +}; +export type CoverDispatchProps = { + onPublisherChange: (arg:any)=>unknown +}; +export type CoverProps = CoverOwnProps & CoverStateProps & CoverDispatchProps; + +export type Action = { + type: string, + payload?: unknown, + meta?: { + debounce?: string + } +}; + +export type ISBNStateProps = { + type:number, + value:string +}; +export type ISBNDispatchProps = { + onChange: (arg:RInputEvent)=>unknown }; +export type ISBNProps = ISBNStateProps & ISBNDispatchProps; diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index bed712fa52..80efb198d5 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -1,35 +1,57 @@ import * as Boostrap from 'react-bootstrap'; +import {IdentifierType, UnifiedFormProps} from './interface/type'; +import ContentTab from './content-tab/content-tab'; import CoverTab from './cover-tab/cover-tab'; +import DetailTab from './detail-tab/detail-tab'; import React from 'react'; import SubmitSection from '../entity-editor/submission-section/submission-section'; -import {UnifiedFormProps} from './interface/type'; import {connect} from 'react-redux'; import {filterIdentifierTypesByEntityType} from '../../common/helpers/utils'; +import {submit} from '../entity-editor/submission-section/actions'; const {Tabs, Tab} = Boostrap; +function getUfValidator(validator) { + return (state, identifierTypes) => { + if (state.get('ISBN') && !state.getIn(['ISBN', 'type']) && state.getIn(['ISBN', 'value'], '').length > 0) { + return false; + } + return validator(state, identifierTypes); + }; +} export function UnifiedForm(props:UnifiedFormProps) { - const {identifierTypes, validators} = props; + const {identifierTypes, validators, onSubmit} = props; const [tabKey, setTabKey] = React.useState('cover'); const editionIdentifierTypes = filterIdentifierTypesByEntityType(identifierTypes, 'edition'); return ( -
- - - - - - - -
); -} +
+
+

Create Book

+ + + + + + + + + + + +
+ -function mapStateToProps(state, userprops) { - return {}; + + ); } -function mapDispatchToProps(state) { - return {}; +function mapDispatchToProps(dispatch, {submissionUrl}) { + return { + onSubmit: (event:React.FormEvent) => { + event.preventDefault(); + dispatch(submit(submissionUrl)); + } + }; } -export default connect(mapStateToProps, mapDispatchToProps)(UnifiedForm); +export default connect(null, mapDispatchToProps)(UnifiedForm); From f390056bf01842ac63b3801575dbf57fbe7049f5 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sun, 5 Jun 2022 11:44:45 +0530 Subject: [PATCH 015/258] feat(uf): basic content tab done --- .../common/entity-search-field-option.js | 11 ++++- .../entity-editor/common/linked-entity.tsx | 7 +-- .../common/search-entity-create-select.tsx | 48 +++++++++++++++++++ src/client/unified-form/content-tab/action.ts | 10 ++++ .../unified-form/content-tab/content-tab.tsx | 47 +++++++++++++++--- .../unified-form/content-tab/reducer.ts | 15 ++++++ src/client/unified-form/helpers.ts | 4 +- src/client/unified-form/interface/type.ts | 13 +++++ 8 files changed, 142 insertions(+), 13 deletions(-) create mode 100644 src/client/unified-form/common/search-entity-create-select.tsx create mode 100644 src/client/unified-form/content-tab/action.ts create mode 100644 src/client/unified-form/content-tab/reducer.ts diff --git a/src/client/entity-editor/common/entity-search-field-option.js b/src/client/entity-editor/common/entity-search-field-option.js index 796cb8586c..2aeaf68806 100644 --- a/src/client/entity-editor/common/entity-search-field-option.js +++ b/src/client/entity-editor/common/entity-search-field-option.js @@ -130,6 +130,10 @@ class EntitySearchFieldOption extends React.Component { return option.text; } + getOptionValue(option) { + return option.id; + } + render() { const labelElement = {this.props.label}; const helpIconElement = this.props.tooltipText && ( @@ -140,14 +144,15 @@ class EntitySearchFieldOption extends React.Component { ); + const SelectWrapper = this.props.SelectWrapper ?? ImmutableAsyncSelect; const wrappedSelect = ( - { render() { const option = this.getSafeOptionValue(this.props.data); - const {disambiguation, text, type, unnamedText, language} = option; + const {disambiguation, text, type, unnamedText, language, __isNew__} = option; const nameComponent = text || {unnamedText}; return (
{ onClick={this.handleParentEvent} {...this.props} > { - type && genEntityIconHTMLElement(type) + !__isNew__ && type && genEntityIconHTMLElement(type) }   {nameComponent} @@ -98,9 +98,10 @@ class LinkedEntity extends React.Component { ({disambiguation}) } {' '} + {!__isNew__ && - + } {language}
); diff --git a/src/client/unified-form/common/search-entity-create-select.tsx b/src/client/unified-form/common/search-entity-create-select.tsx new file mode 100644 index 0000000000..cee9ffe185 --- /dev/null +++ b/src/client/unified-form/common/search-entity-create-select.tsx @@ -0,0 +1,48 @@ +import AsyncCreatable from 'react-select/async-creatable'; +import BaseEntitySearch from '../../entity-editor/common/entity-search-field-option'; +import {CommonProps} from 'react-select'; +import React from 'react'; +import makeImmutable from '../../entity-editor/common/make-immutable'; + + +const ImmutableCreatableAsync = makeImmutable(AsyncCreatable); +const defaultProps = { + bbid: null, + empty: true, + error: false, + filters: [], + languageOptions: [], + tooltipText: null +}; +type SearchEntityCreaateProps = { + bbid?:string, + empty?:boolean, + nextId:string|number, + error?:boolean, + filters?:Array, + label:string, + tooltipText?:string, + languageOptions?:Array, + type:string | Array + +} & typeof defaultProps & CommonProps; + +function SearchEntityCreaate(props:SearchEntityCreaateProps):JSX.Element { + const {type, nextId} = props; + const createLabel = React.useCallback((input) => `Create ${type} "${input}"`, [type]); + const getNewOptionData = React.useCallback((input, label) => ({ + __isNew__: true, + id: nextId, + text: label, + type + }), [type, nextId]); + return (); +} +SearchEntityCreaate.defaultProps = defaultProps; +export default SearchEntityCreaate; + diff --git a/src/client/unified-form/content-tab/action.ts b/src/client/unified-form/content-tab/action.ts new file mode 100644 index 0000000000..16a70d2ba4 --- /dev/null +++ b/src/client/unified-form/content-tab/action.ts @@ -0,0 +1,10 @@ + + +export const UPDATE_WORKS = 'UPDATE_WORKS'; + +export function updateWorks(payload) { + return { + payload, + type: UPDATE_WORKS + }; +} diff --git a/src/client/unified-form/content-tab/content-tab.tsx b/src/client/unified-form/content-tab/content-tab.tsx index 9c721bb9d3..08d34707d9 100644 --- a/src/client/unified-form/content-tab/content-tab.tsx +++ b/src/client/unified-form/content-tab/content-tab.tsx @@ -1,17 +1,50 @@ +import * as Bootstrap from 'react-bootstrap/'; +import {ContentTabDispatchProps, ContentTabProps, ContentTabStateProps} from '../interface/type'; import React from 'react'; +import SearchEntityCreaate from '../common/search-entity-create-select'; import {connect} from 'react-redux'; +import {convertMapToObject} from '../../helpers/utils'; +import {reduce} from 'lodash'; +import {updateWorks} from './action'; -export function ContentTab(props) { - return
ContentTab
; +const {Row, Col} = Bootstrap; +export function ContentTab({value, onChange, nextId}:ContentTabProps) { + return ( + + + + + + ); } -function mapStateToProps(state) { - return {}; +function mapStateToProps(rootState) { + const worksObj = convertMapToObject(rootState.get('works')); + const nextId = reduce(worksObj, (prev, value) => (value.__isNew__ ? prev + 1 : prev), 0); + return { + nextId, + value: Object.values(worksObj) + }; } -function mapDispatchToProps(state) { - return {}; +function mapDispatchToProps(dispatch) { + return { + onChange: (options:any[]) => { + const mappedOptions = Object.fromEntries(options.map((value, index) => { + value.__isNew__ = Boolean(value.__isNew__); + return [index, value]; + })); + return dispatch(updateWorks(mappedOptions)); + } + }; } -export default connect(mapStateToProps, mapDispatchToProps)(ContentTab); +export default connect(mapStateToProps, mapDispatchToProps)(ContentTab); diff --git a/src/client/unified-form/content-tab/reducer.ts b/src/client/unified-form/content-tab/reducer.ts new file mode 100644 index 0000000000..4b39b20eed --- /dev/null +++ b/src/client/unified-form/content-tab/reducer.ts @@ -0,0 +1,15 @@ +import {UPDATE_WORKS} from './action'; +import immutable from 'immutable'; + + +const initialState = {}; + +export default function reducer(state = immutable.Map(initialState), {type, payload}) { + switch (type) { + case UPDATE_WORKS: + return immutable.fromJS(payload); + + default: + return state; + } +} diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index 59b1eca9a1..444008be52 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -11,6 +11,7 @@ import {validateForm as validateAuthorForm} from '../entity-editor/validators/au import {validateForm as validateEditionForm} from '../entity-editor/validators/edition'; import {validateForm as validatePublisherForm} from '../entity-editor/validators/publisher'; import {validateForm as validateWorkForm} from '../entity-editor/validators/work'; +import worksReducer from './content-tab/reducer'; type ReduxWindow = typeof window & {__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any}; @@ -37,6 +38,7 @@ export function createRootReducer() { editionSection: editionSectionReducer, identifierEditor: identifierEditorReducer, nameSection: nameSectionReducer, - submissionSection: submissionSectionReducer + submissionSection: submissionSectionReducer, + works: worksReducer }); } diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index a16198b1e7..e4e38c4253 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -58,3 +58,16 @@ export type ISBNDispatchProps = { onChange: (arg:RInputEvent)=>unknown }; export type ISBNProps = ISBNStateProps & ISBNDispatchProps; + +type EntitySelect = { + text:string, + id:string +}; +export type ContentTabStateProps = { + nextId:string | number, + value:any | any[] +}; +export type ContentTabDispatchProps = { + onChange:(arg:EntitySelect|EntitySelect[])=>unknown +}; +export type ContentTabProps = ContentTabStateProps & ContentTabDispatchProps; From 2dc4dcbc6ba28d4fc2fa967a800189502a906694 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sun, 5 Jun 2022 12:54:27 +0530 Subject: [PATCH 016/258] feat(uf): added navbar to switch tabs --- src/client/stylesheets/style.scss | 18 +++++++++++++++ src/client/unified-form/navbutton.tsx | 28 ++++++++++++++++++++++++ src/client/unified-form/unified-form.tsx | 18 +++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 src/client/unified-form/navbutton.tsx diff --git a/src/client/stylesheets/style.scss b/src/client/stylesheets/style.scss index 79312667ef..b2f77b8032 100644 --- a/src/client/stylesheets/style.scss +++ b/src/client/stylesheets/style.scss @@ -752,4 +752,22 @@ $uf-primary:#EB743B; border-color: $uf-primary; border-bottom: none; } +} +.uf-navbtn-row { + margin: 0; + padding-top: 1rem; + padding-bottom: 1rem; + button{ + width: max-content; + } + .col{ + align-items: stretch; + flex-grow: 0; + margin-right: 0.3rem; + padding: 0; + } + .col:nth-child(1) + { + margin-right: 3rem; + } } \ No newline at end of file diff --git a/src/client/unified-form/navbutton.tsx b/src/client/unified-form/navbutton.tsx new file mode 100644 index 0000000000..c3b755cef5 --- /dev/null +++ b/src/client/unified-form/navbutton.tsx @@ -0,0 +1,28 @@ +import * as Bootstrap from 'react-bootstrap'; +import {faAngleLeft, faAngleRight} from '@fortawesome/free-solid-svg-icons'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import React from 'react'; + + +const {Row, Col, Button} = Bootstrap; +type NavButtonsProps = { + onNext:()=>unknown, + onBack:()=>unknown, + disableBack:boolean, + disableNext:boolean +}; +export default function NavButtons({onNext, onBack, disableBack, disableNext}:NavButtonsProps) { + return ( + + + + + + + + + + + + ); +} diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index 80efb198d5..569c5be2f3 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -3,6 +3,7 @@ import {IdentifierType, UnifiedFormProps} from './interface/type'; import ContentTab from './content-tab/content-tab'; import CoverTab from './cover-tab/cover-tab'; import DetailTab from './detail-tab/detail-tab'; +import NavButtons from './navbutton'; import React from 'react'; import SubmitSection from '../entity-editor/submission-section/submission-section'; import {connect} from 'react-redux'; @@ -23,6 +24,19 @@ export function UnifiedForm(props:UnifiedFormProps) { const {identifierTypes, validators, onSubmit} = props; const [tabKey, setTabKey] = React.useState('cover'); const editionIdentifierTypes = filterIdentifierTypesByEntityType(identifierTypes, 'edition'); + const tabKeys = ['cover', 'content', 'detail']; + const onNextHandler = React.useCallback(() => { + const index = tabKeys.indexOf(tabKey); + if (index >= 0 && index < tabKeys.length - 1) { + setTabKey(tabKeys[index + 1]); + } + }, [tabKey]); + const onBackHandler = React.useCallback(() => { + const index = tabKeys.indexOf(tabKey); + if (index > 0 && index < tabKeys.length) { + setTabKey(tabKeys[index - 1]); + } + }, [tabKey]); return (
@@ -39,6 +53,10 @@ export function UnifiedForm(props:UnifiedFormProps) {
+ From 3338f0ac7e629734a85ea014d003e292686acb5c Mon Sep 17 00:00:00 2001 From: tri10 Date: Sun, 5 Jun 2022 17:56:24 +0530 Subject: [PATCH 017/258] feat(uf): add details tab --- .../annotation-section/annotation-section.js | 15 +++- .../edition-section/edition-section.tsx | 88 +++++++++++-------- .../unified-form/detail-tab/detail-tab.tsx | 31 ++++--- src/client/unified-form/unified-form.tsx | 4 +- src/server/routes/unifiedform.ts | 1 + 5 files changed, 87 insertions(+), 52 deletions(-) diff --git a/src/client/entity-editor/annotation-section/annotation-section.js b/src/client/entity-editor/annotation-section/annotation-section.js index 30c49dde21..b9208009fa 100644 --- a/src/client/entity-editor/annotation-section/annotation-section.js +++ b/src/client/entity-editor/annotation-section/annotation-section.js @@ -40,7 +40,8 @@ import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; */ function AnnotationSection({ annotation, - onAnnotationChange + onAnnotationChange, + isUf }) { const annotationLabel = ( @@ -54,14 +55,17 @@ function AnnotationSection({ Additional freeform data that does not fit in the above form ); - + const colSpan = {offset: 3, span: 6}; + if (isUf) { + colSpan.offset = 0; + } return (

Annotation

- + {annotationLabel} @@ -95,9 +99,12 @@ function AnnotationSection({ AnnotationSection.displayName = 'AnnotationSection'; AnnotationSection.propTypes = { annotation: PropTypes.object.isRequired, + isUf: PropTypes.bool, onAnnotationChange: PropTypes.func.isRequired }; - +AnnotationSection.defaultProps = { + isUf: false +}; function mapStateToProps(rootState) { return { annotation: convertMapToObject(rootState.get('annotationSection')) diff --git a/src/client/entity-editor/edition-section/edition-section.tsx b/src/client/entity-editor/edition-section/edition-section.tsx index e004903e33..a2e1161012 100644 --- a/src/client/entity-editor/edition-section/edition-section.tsx +++ b/src/client/entity-editor/edition-section/edition-section.tsx @@ -94,6 +94,7 @@ type EditionGroup = { type OwnProps = { languageOptions: Array, editionFormats: Array, + isUf:boolean, editionStatuses: Array }; @@ -180,6 +181,7 @@ function EditionSection({ editionGroupValue, editionGroupVisible, matchingNameEditionGroups, + isUf, publisherValue, releaseDateValue, statusValue, @@ -256,37 +258,50 @@ function EditionSection({ Has the work been published, or is it in a draft stage? ); - + const colSpan = { + offset: 3, + span: 6 + }; + const shortColSpan = { + offset: 3, + span: 3 + }; + if (isUf) { + colSpan.offset = 0; + shortColSpan.offset = 0; + } return (
-

+ {!isUf && + <> +

What else do you know about the Edition? -

-

+ +

Edition Group is required — this cannot be blank. You can search for and choose an existing Edition Group, or choose to automatically create one instead. -

- - { - showAutoCreateEditionGroupMessage ? - - -

A new Edition Group with the same name will be created automatically.

-
- -
- : - getEditionGroupSearchSelect() - } - {showMatchingEditionGroups && +

+ + { + showAutoCreateEditionGroupMessage ? + + +

A new Edition Group with the same name will be created automatically.

+
+ +
+ : + getEditionGroupSearchSelect() + } + {showMatchingEditionGroups && {matchingNameEditionGroups.length > 1 ? @@ -310,14 +325,17 @@ function EditionSection({ - } -
+ } +
+ + }

Below fields are optional — leave something blank if you don’t know it

- + {!isUf && + - + } - + - + - + Format @@ -399,7 +417,7 @@ function EditionSection({ - + - + DetailTab
; + return ( +
+ + + Extra info + + + + + + + + + +
); } -function mapStateToProps(state) { - return {}; -} - -function mapDispatchToProps(state) { - return {}; -} - -export default connect(mapStateToProps, mapDispatchToProps)(DetailTab); +export default DetailTab; diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index 569c5be2f3..0fbe465629 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -40,7 +40,7 @@ export function UnifiedForm(props:UnifiedFormProps) { return (
-

Create Book

+

Add Book

@@ -49,7 +49,7 @@ export function UnifiedForm(props:UnifiedFormProps) { - +
diff --git a/src/server/routes/unifiedform.ts b/src/server/routes/unifiedform.ts index 94bef3d54b..b20dadd271 100644 --- a/src/server/routes/unifiedform.ts +++ b/src/server/routes/unifiedform.ts @@ -10,6 +10,7 @@ type PassportRequest = express.Request & {user: any, session: any}; const router = express.Router(); router.get('/create', isAuthenticated, middleware.loadIdentifierTypes, + middleware.loadEditionStatuses, middleware.loadEditionFormats, middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadGenders, middleware.loadAuthorTypes, middleware.loadRelationshipTypes, (req:PassportRequest, res:express.Response) => { const props = generateUnifiedProps(req, res, {}); From 65c57b33bb7793d730496aa25013850b3b2a7dcc Mon Sep 17 00:00:00 2001 From: tri10 Date: Sun, 5 Jun 2022 17:59:40 +0530 Subject: [PATCH 018/258] feat(uf): transform state to require format --- .../submission-section/actions.ts | 37 ++++++++++++++++--- src/client/unified-form/helpers.ts | 2 + src/server/helpers/entityRouteUtils.tsx | 2 +- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/client/entity-editor/submission-section/actions.ts b/src/client/entity-editor/submission-section/actions.ts index 6e35723ce7..6589640db8 100644 --- a/src/client/entity-editor/submission-section/actions.ts +++ b/src/client/entity-editor/submission-section/actions.ts @@ -113,15 +113,42 @@ function postSubmission(url: string, data: Map): Promise { } }); } +function transformFormData(data:Record):Record { + const newData = {}; + const nextId = 0; + // add new publisher + // add new works + + // add edition at last + if (data.ISBN.type) { + data.identifierEditor.m0 = data.ISBN; + } + data.relationshipSection.relationships = _.mapValues(data.works, (work, key) => { + const relationship = { + attributeSetId: null, + attributes: [], + relationshipType: { + id: 10 + }, + rowID: key, + sourceEntity: { + bbid: nextId + }, + targetEntity: { + bbid: work.id + } + }; + return relationship; + }); + newData[`b${nextId}`] = {...data, type: 'Edition'}; + return newData; +} function postUFSubmission(url: string, data: Map): Promise { // transform data const jsonData = data.toJS(); - if (jsonData.ISBN.type) { - jsonData.identifierEditor.m0 = jsonData.ISBN; - } - - return request.post(url).send(jsonData) + const postData = transformFormData(jsonData); + return request.post(url).send(postData) .then((response: Response) => { if (!response.body) { window.location.replace('/login'); diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index 444008be52..a36684b87d 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -6,6 +6,7 @@ import {combineReducers} from 'redux-immutable'; import editionSectionReducer from '../entity-editor/edition-section/reducer'; import identifierEditorReducer from '../entity-editor/identifier-editor/reducer'; import nameSectionReducer from '../entity-editor/name-section/reducer'; +import relationshipSectionReducer from '../entity-editor/relationship-editor/reducer'; import submissionSectionReducer from '../entity-editor/submission-section/reducer'; import {validateForm as validateAuthorForm} from '../entity-editor/validators/author'; import {validateForm as validateEditionForm} from '../entity-editor/validators/edition'; @@ -38,6 +39,7 @@ export function createRootReducer() { editionSection: editionSectionReducer, identifierEditor: identifierEditorReducer, nameSection: nameSectionReducer, + relationshipSection: relationshipSectionReducer, submissionSection: submissionSectionReducer, works: worksReducer }); diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index 74f4694306..e6835a9f5b 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -355,7 +355,7 @@ export function generateUnifiedProps( additionalProps: any, initialStateCallback: () => any = () => new Object() ): any { - const submissionUrl = '/create/handler'; + const submissionUrl = 'localhost:9097/create/handler'; const props = Object.assign({ identifierTypes: res.locals.identifierTypes, initialState: initialStateCallback(), From 5efeef927622258bc2aa6603b70345670c17b65e Mon Sep 17 00:00:00 2001 From: tri10 Date: Mon, 6 Jun 2022 19:33:44 +0530 Subject: [PATCH 019/258] fix(uf): minor fix languagefield --- src/client/entity-editor/name-section/name-section.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/entity-editor/name-section/name-section.js b/src/client/entity-editor/name-section/name-section.js index 9e0743d0c2..8a251d68ec 100644 --- a/src/client/entity-editor/name-section/name-section.js +++ b/src/client/entity-editor/name-section/name-section.js @@ -234,7 +234,7 @@ class NameSection extends React.Component { - Date: Tue, 7 Jun 2022 21:22:34 +0530 Subject: [PATCH 020/258] feat(uf): use search-create for entity inputs --- .../edition-section/edition-section.tsx | 65 ++++++++++--------- .../common/search-entity-create-select.tsx | 8 +-- .../unified-form/content-tab/content-tab.tsx | 4 +- .../unified-form/cover-tab/cover-tab.tsx | 5 +- .../unified-form/cover-tab/isbn-field.tsx | 6 +- 5 files changed, 45 insertions(+), 43 deletions(-) diff --git a/src/client/entity-editor/edition-section/edition-section.tsx b/src/client/entity-editor/edition-section/edition-section.tsx index a2e1161012..5f1cc13c87 100644 --- a/src/client/entity-editor/edition-section/edition-section.tsx +++ b/src/client/entity-editor/edition-section/edition-section.tsx @@ -56,6 +56,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import LanguageField from '../common/language-field'; import LinkedEntity from '../common/linked-entity'; import NumericField from '../common/numeric-field'; +import SearchEntityCreate from '../../unified-form/common/search-entity-create-select'; import Select from 'react-select'; import _ from 'lodash'; import {connect} from 'react-redux'; @@ -214,11 +215,11 @@ function EditionSection({ !editionGroupRequired; const showMatchingEditionGroups = Boolean(hasmatchingNameEditionGroups && !editionGroupValue); - + const EntitySearchField = isUf ? SearchEntityCreate : EntitySearchFieldOption; const getEditionGroupSearchSelect = () => ( - - + {!isUf && - <> +

What else do you know about the Edition?

-

+ } +

Edition Group is required — this cannot be blank. You can search for and choose an existing Edition Group, or choose to automatically create one instead. -

- - { - showAutoCreateEditionGroupMessage ? - - -

A new Edition Group with the same name will be created automatically.

-
- -
- : - getEditionGroupSearchSelect() - } - {showMatchingEditionGroups && +

+ + + { + showAutoCreateEditionGroupMessage ? + + +

A new Edition Group with the same name will be created automatically.

+
+ +
+ : + getEditionGroupSearchSelect() + } + {showMatchingEditionGroups && {matchingNameEditionGroups.length > 1 ? @@ -325,10 +328,10 @@ function EditionSection({ - } -
- - } + } +
+ +

Below fields are optional — leave something blank if you don’t know it diff --git a/src/client/unified-form/common/search-entity-create-select.tsx b/src/client/unified-form/common/search-entity-create-select.tsx index cee9ffe185..a9926b48e1 100644 --- a/src/client/unified-form/common/search-entity-create-select.tsx +++ b/src/client/unified-form/common/search-entity-create-select.tsx @@ -14,7 +14,7 @@ const defaultProps = { languageOptions: [], tooltipText: null }; -type SearchEntityCreaateProps = { +type SearchEntityCreateProps = { bbid?:string, empty?:boolean, nextId:string|number, @@ -27,7 +27,7 @@ type SearchEntityCreaateProps = { } & typeof defaultProps & CommonProps; -function SearchEntityCreaate(props:SearchEntityCreaateProps):JSX.Element { +function SearchEntityCreate(props:SearchEntityCreateProps):JSX.Element { const {type, nextId} = props; const createLabel = React.useCallback((input) => `Create ${type} "${input}"`, [type]); const getNewOptionData = React.useCallback((input, label) => ({ @@ -43,6 +43,6 @@ function SearchEntityCreaate(props:SearchEntityCreaateProps):JSX.Element { {...props} />); } -SearchEntityCreaate.defaultProps = defaultProps; -export default SearchEntityCreaate; +SearchEntityCreate.defaultProps = defaultProps; +export default SearchEntityCreate; diff --git a/src/client/unified-form/content-tab/content-tab.tsx b/src/client/unified-form/content-tab/content-tab.tsx index 08d34707d9..6233baa2aa 100644 --- a/src/client/unified-form/content-tab/content-tab.tsx +++ b/src/client/unified-form/content-tab/content-tab.tsx @@ -1,7 +1,7 @@ import * as Bootstrap from 'react-bootstrap/'; import {ContentTabDispatchProps, ContentTabProps, ContentTabStateProps} from '../interface/type'; import React from 'react'; -import SearchEntityCreaate from '../common/search-entity-create-select'; +import SearchEntityCreate from '../common/search-entity-create-select'; import {connect} from 'react-redux'; import {convertMapToObject} from '../../helpers/utils'; import {reduce} from 'lodash'; @@ -13,7 +13,7 @@ export function ContentTab({value, onChange, nextId}:ContentTabProps) { return ( - - ); } -function mapStateToProps(rootState:Immutable.Map):Record { +function mapStateToProps(rootState:Immutable.Map):ISBNStateProps { return { type: rootState.getIn(['ISBN', 'type']), value: rootState.getIn(['ISBN', 'value']) @@ -51,4 +51,4 @@ function mapDispatchToProps(dispatch):ISBNDispatchProps { }; } -export default connect(mapStateToProps, mapDispatchToProps)(ISBNField); +export default connect(mapStateToProps, mapDispatchToProps)(ISBNField); From bca94f513a7b7a0176f564ddcd54f642eff37ecd Mon Sep 17 00:00:00 2001 From: tri10 Date: Thu, 9 Jun 2022 17:44:43 +0530 Subject: [PATCH 021/258] refactor(uf): improve code readability --- .../entity-editor/button-bar/button-bar.js | 27 +++++++++------- .../entity-editor/common/linked-entity.tsx | 10 +++--- .../edition-section/edition-section.tsx | 8 ++--- .../submission-section/actions.ts | 6 ++-- src/client/unified-form/content-tab/action.ts | 7 ++-- .../unified-form/content-tab/content-tab.tsx | 5 +-- .../unified-form/content-tab/reducer.ts | 6 ++-- .../unified-form/cover-tab/cover-tab.tsx | 4 +-- .../unified-form/detail-tab/detail-tab.tsx | 4 +-- src/client/unified-form/helpers.ts | 6 ++-- src/client/unified-form/interface/type.ts | 32 ++++++++++++++++--- src/client/unified-form/navbutton.tsx | 8 ++--- src/client/unified-form/unified-form.tsx | 8 ++--- src/server/helpers/entityRouteUtils.tsx | 6 ++-- 14 files changed, 82 insertions(+), 55 deletions(-) diff --git a/src/client/entity-editor/button-bar/button-bar.js b/src/client/entity-editor/button-bar/button-bar.js index 858e2a47e6..59d870227c 100644 --- a/src/client/entity-editor/button-bar/button-bar.js +++ b/src/client/entity-editor/button-bar/button-bar.js @@ -58,22 +58,27 @@ function ButtonBar({ onIdentifierButtonClick }) { const className = isUf ? 'text-right' : 'text-center'; + function renderAliasButton() { + if (isUf) { + return null; + } + return ( + + + ); + } + const identifierEditorClass = `btn wrap${!isUf ? '' : ' btn-success'}`; return (

+ {renderAliasButton()} - { - !isUf && - - - - } { return optionToCheck; } + render() { const option = this.getSafeOptionValue(this.props.data); const {disambiguation, text, type, unnamedText, language, __isNew__} = option; const nameComponent = text || {unnamedText}; + const externalLinkComponent = !__isNew__ && + + + ; return (
{ ({disambiguation}) } {' '} - {!__isNew__ && - - - } + {externalLinkComponent} {language}
); diff --git a/src/client/entity-editor/edition-section/edition-section.tsx b/src/client/entity-editor/edition-section/edition-section.tsx index 5f1cc13c87..7c59e2cd39 100644 --- a/src/client/entity-editor/edition-section/edition-section.tsx +++ b/src/client/entity-editor/edition-section/edition-section.tsx @@ -259,6 +259,7 @@ function EditionSection({ Has the work been published, or is it in a draft stage? ); + const headingTag = !isUf &&

What else do you know about the Edition?

; const colSpan = { offset: 3, span: 6 @@ -273,12 +274,7 @@ function EditionSection({ } return (
- {!isUf && - -

- What else do you know about the Edition? -

- } + {headingTag}

Edition Group is required — this cannot be blank. You can search for and choose an existing Edition Group, or choose to automatically create one instead. diff --git a/src/client/entity-editor/submission-section/actions.ts b/src/client/entity-editor/submission-section/actions.ts index 6589640db8..5fabbca654 100644 --- a/src/client/entity-editor/submission-section/actions.ts +++ b/src/client/entity-editor/submission-section/actions.ts @@ -167,13 +167,13 @@ function postUFSubmission(url: string, data: Map): Promise { type SubmitResult = (arg1: (Action) => unknown, arg2: () => Map) => unknown; export function submit( - submissionUrl: string + submissionUrl: string, + isUf = false ): SubmitResult { return (dispatch, getState) => { const rootState = getState(); dispatch(setSubmitted(true)); - // TO-DO: not the best best way - if (rootState.get('ISBN')) { + if (isUf) { return postUFSubmission(submissionUrl, rootState) .catch( (error: {message: string}) => { diff --git a/src/client/unified-form/content-tab/action.ts b/src/client/unified-form/content-tab/action.ts index 16a70d2ba4..1c4474653b 100644 --- a/src/client/unified-form/content-tab/action.ts +++ b/src/client/unified-form/content-tab/action.ts @@ -1,10 +1,11 @@ -export const UPDATE_WORKS = 'UPDATE_WORKS'; +export const ADD_WORK = 'ADD_WORK'; +let nextId = 0; export function updateWorks(payload) { return { - payload, - type: UPDATE_WORKS + payload: {id: nextId++, value: payload}, + type: ADD_WORK }; } diff --git a/src/client/unified-form/content-tab/content-tab.tsx b/src/client/unified-form/content-tab/content-tab.tsx index 6233baa2aa..aa1ffd19cd 100644 --- a/src/client/unified-form/content-tab/content-tab.tsx +++ b/src/client/unified-form/content-tab/content-tab.tsx @@ -9,7 +9,7 @@ import {updateWorks} from './action'; const {Row, Col} = Bootstrap; -export function ContentTab({value, onChange, nextId}:ContentTabProps) { +export function ContentTab({value, onChange, nextId, ...rest}:ContentTabProps) { return ( @@ -20,6 +20,7 @@ export function ContentTab({value, onChange, nextId}:ContentTabProps) { type="work" value={value} onChange={onChange} + {...rest} /> @@ -27,7 +28,7 @@ export function ContentTab({value, onChange, nextId}:ContentTabProps) { } function mapStateToProps(rootState) { - const worksObj = convertMapToObject(rootState.get('works')); + const worksObj = convertMapToObject(rootState.get('Works')); const nextId = reduce(worksObj, (prev, value) => (value.__isNew__ ? prev + 1 : prev), 0); return { nextId, diff --git a/src/client/unified-form/content-tab/reducer.ts b/src/client/unified-form/content-tab/reducer.ts index 4b39b20eed..ed778105fc 100644 --- a/src/client/unified-form/content-tab/reducer.ts +++ b/src/client/unified-form/content-tab/reducer.ts @@ -1,4 +1,4 @@ -import {UPDATE_WORKS} from './action'; +import {ADD_WORK} from './action'; import immutable from 'immutable'; @@ -6,8 +6,8 @@ const initialState = {}; export default function reducer(state = immutable.Map(initialState), {type, payload}) { switch (type) { - case UPDATE_WORKS: - return immutable.fromJS(payload); + case ADD_WORK: + return state.set(payload.id, immutable.fromJS(payload.value)); default: return state; diff --git a/src/client/unified-form/cover-tab/cover-tab.tsx b/src/client/unified-form/cover-tab/cover-tab.tsx index 85205f6ba7..45a36b3b2a 100644 --- a/src/client/unified-form/cover-tab/cover-tab.tsx +++ b/src/client/unified-form/cover-tab/cover-tab.tsx @@ -14,7 +14,7 @@ export function CoverTab(props:CoverProps) { const {publisherValue, onPublisherChange, identifierEditorVisible} = props; return (

- + - +
diff --git a/src/client/unified-form/detail-tab/detail-tab.tsx b/src/client/unified-form/detail-tab/detail-tab.tsx index 39cfb17dae..5c142b80ff 100644 --- a/src/client/unified-form/detail-tab/detail-tab.tsx +++ b/src/client/unified-form/detail-tab/detail-tab.tsx @@ -14,12 +14,12 @@ export function DetailTab(props) { - + - +
); } diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index a36684b87d..3606d37013 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -4,6 +4,7 @@ import annotationSectionReducer from '../entity-editor/annotation-section/reduce import buttonBarReducer from '../entity-editor/button-bar/reducer'; import {combineReducers} from 'redux-immutable'; import editionSectionReducer from '../entity-editor/edition-section/reducer'; +import {createRootReducer as getEntityReducer} from '../entity-editor/helpers'; import identifierEditorReducer from '../entity-editor/identifier-editor/reducer'; import nameSectionReducer from '../entity-editor/name-section/reducer'; import relationshipSectionReducer from '../entity-editor/relationship-editor/reducer'; @@ -33,6 +34,8 @@ export const validatorMap = { export function createRootReducer() { return combineReducers({ ISBN: ISBNReducer, + Work: getEntityReducer('work'), + Works: worksReducer, aliasEditor: aliasEditorReducer, annotationSection: annotationSectionReducer, buttonBar: buttonBarReducer, @@ -40,7 +43,6 @@ export function createRootReducer() { identifierEditor: identifierEditorReducer, nameSection: nameSectionReducer, relationshipSection: relationshipSectionReducer, - submissionSection: submissionSectionReducer, - works: worksReducer + submissionSection: submissionSectionReducer }); } diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index e4e38c4253..4b8928a07f 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -23,15 +23,15 @@ export type UnifiedFormDispatchProps = { onSubmit: (event:React.FormEvent) =>unknown }; export type UnifiedFormProps = { - identifierTypes:IdentifierType[], - validators:Record, + identifierTypes?:IdentifierType[], + validators?:Record, } & UnifiedFormDispatchProps; export type CoverOwnProps = { - languageOptions: LanguageOption[], - editionFormats:EditionFormat[], + languageOptions?: LanguageOption[], + editionFormats?:EditionFormat[], identifierTypes:IdentifierType[] - editionStatuses: EditionStatus[] + editionStatuses?: EditionStatus[] }; export type CoverStateProps = { publisherValue:any[], @@ -71,3 +71,25 @@ export type ContentTabDispatchProps = { onChange:(arg:EntitySelect|EntitySelect[])=>unknown }; export type ContentTabProps = ContentTabStateProps & ContentTabDispatchProps; + +export type NavButtonsProps = { + onNext:()=>unknown, + onBack:()=>unknown, + disableBack:boolean, + disableNext:boolean +}; + +export type SearchEntityCreateProps = { + bbid?:string, + empty?:boolean, + nextId:string|number, + error?:boolean, + filters?:Array, + label:string, + tooltipText?:string, + languageOptions?:Array, + value?:Array | EntitySelect + type:string | Array, + onChange:(arg)=>unknown + +}; diff --git a/src/client/unified-form/navbutton.tsx b/src/client/unified-form/navbutton.tsx index c3b755cef5..531ff2f683 100644 --- a/src/client/unified-form/navbutton.tsx +++ b/src/client/unified-form/navbutton.tsx @@ -1,16 +1,12 @@ import * as Bootstrap from 'react-bootstrap'; import {faAngleLeft, faAngleRight} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {NavButtonsProps} from './interface/type'; import React from 'react'; const {Row, Col, Button} = Bootstrap; -type NavButtonsProps = { - onNext:()=>unknown, - onBack:()=>unknown, - disableBack:boolean, - disableNext:boolean -}; + export default function NavButtons({onNext, onBack, disableBack, disableNext}:NavButtonsProps) { return ( diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index 0fbe465629..ddf9c29924 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -1,5 +1,5 @@ import * as Boostrap from 'react-bootstrap'; -import {IdentifierType, UnifiedFormProps} from './interface/type'; +import {IdentifierType, UnifiedFormDispatchProps, UnifiedFormProps} from './interface/type'; import ContentTab from './content-tab/content-tab'; import CoverTab from './cover-tab/cover-tab'; import DetailTab from './detail-tab/detail-tab'; @@ -46,7 +46,7 @@ export function UnifiedForm(props:UnifiedFormProps) { - + @@ -67,9 +67,9 @@ function mapDispatchToProps(dispatch, {submissionUrl}) { return { onSubmit: (event:React.FormEvent) => { event.preventDefault(); - dispatch(submit(submissionUrl)); + dispatch(submit(submissionUrl), true); } }; } -export default connect(null, mapDispatchToProps)(UnifiedForm); +export default connect(null, mapDispatchToProps)(UnifiedForm); diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index e6835a9f5b..680ba3efb6 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -324,7 +324,7 @@ export function addInitialRelationship(props, relationshipTypeId, relationshipIn * @returns {object} - Updated props and HTML string with markup */ -export function unifiedFormMarkup(props:{initialState:any}) { +export function unifiedFormMarkup(props) { const {initialState, ...rest} = props; const rootReducer = ufCreateRootReducer(); const store:any = createStore(rootReducer, Immutable.fromJS(initialState)); @@ -355,10 +355,12 @@ export function generateUnifiedProps( additionalProps: any, initialStateCallback: () => any = () => new Object() ): any { - const submissionUrl = 'localhost:9097/create/handler'; + const submissionUrl = '/create/handler'; const props = Object.assign({ + entityType: 'edition', identifierTypes: res.locals.identifierTypes, initialState: initialStateCallback(), + isUf: true, languageOptions: res.locals.languages, requiresJS: true, submissionUrl From cf202ecddd0d54d7cc670cebbb526d264d8c49cf Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 10 Jun 2022 19:08:02 +0530 Subject: [PATCH 022/258] refactor: improve code readability --- src/server/helpers/entityRouteUtils.tsx | 7 +- src/server/routes/entity/entity.tsx | 238 +----------------- .../routes/entity/process-unified-form.ts | 236 +++++++++++++++++ src/server/routes/unifiedform.ts | 4 +- 4 files changed, 251 insertions(+), 234 deletions(-) create mode 100644 src/server/routes/entity/process-unified-form.ts diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index bf3ddb8d89..972287a847 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -23,6 +23,7 @@ import * as entityEditorHelpers from '../../client/entity-editor/helpers'; import * as entityRoutes from '../routes/entity/entity'; import * as error from '../../common/helpers/error'; import * as propHelpers from '../../client/helpers/props'; +import * as unifiedRoutes from '../routes/entity/process-unified-form'; import * as utils from './utils'; import type {Request as $Request, Response as $Response} from 'express'; @@ -342,7 +343,7 @@ function validateUnifiedForm(body:Record):boolean { * @param {object} res - Response object */ -export function createEntitesHandler( +export function createEntitiesHandler( req:$Request, res:$Response ) { @@ -352,6 +353,6 @@ export function createEntitesHandler( return error.sendErrorAsJSON(res, err); } // transforming - req.body = entityRoutes.transformForm(req.body); - return entityRoutes.handleCreateEntities(req as PassportRequest, res); + req.body = unifiedRoutes.transformForm(req.body); + return unifiedRoutes.handleCreateMultipleEntities(req as PassportRequest, res); } diff --git a/src/server/routes/entity/entity.tsx b/src/server/routes/entity/entity.tsx index 6bab921412..be9a65f543 100644 --- a/src/server/routes/entity/entity.tsx +++ b/src/server/routes/entity/entity.tsx @@ -47,42 +47,13 @@ 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 {_bulkIndexEntities} from '../../../common/helpers/search'; -import {transformNewForm as authorTransform} from './author'; -import {transformNewForm as editionGroupTransform} from './edition-group'; -import {transformNewForm as editionTransform} from './edition'; import {getEntityLabel} from '../../../client/helpers/entity'; import {getOrderedRevisionsForEntityPage} from '../../helpers/revisions'; import log from 'log'; -import {transformNewForm as publisherTransform} from './publisher'; -import {transformNewForm as seriesTransform} from './series'; +import {processAchievement} from './process-unified-form'; import target from '../../templates/target'; -import {transformNewForm as workTransform} from './work'; -const transformFunctions = { - author: authorTransform, - edition: editionTransform, - editionGroup: editionGroupTransform, - publisher: publisherTransform, - series: seriesTransform, - work: workTransform -}; -const additionalEntityProps = { - author: [ - 'typeId', 'genderId', 'beginAreaId', 'beginDate', 'endDate', 'ended', - 'endAreaId' - ], - edition: [ - 'editionGroupBbid', 'width', 'height', 'depth', 'weight', 'pages', - 'formatId', 'statusId' - ], - editionGroup: 'typeid', - publisher: ['typeId', 'areaId', 'beginDate', 'endDate', 'ended'], - series: ['entityType', 'orderingTypeId'], - work: 'typeId' - -}; type PassportRequest = $Request & {user: any, session: any}; @@ -328,7 +299,7 @@ async function setParentRevisions(transacting, newRevision, parentRevisionIDs) { } -async function saveEntitiesAndFinishRevision( +export async function saveEntitiesAndFinishRevision( orm, transacting, isNew: boolean, newRevision: any, mainEntity: any, updatedEntities: any[], editorID: number, note: string ) { @@ -428,7 +399,7 @@ export async function deleteRelationships(orm: any, transacting: Transaction, ma return otherEntities; } -function fetchOrCreateMainEntity( +export function fetchOrCreateMainEntity( orm, transacting, isNew, bbid, entityType ) { const model = commonUtils.getEntityModelByType(orm, entityType); @@ -858,7 +829,7 @@ async function getNextIdentifierSet(orm, transacting, currentEntity, body) { orm, transacting, oldIdentifierSet, body.identifiers || [] ); } -async function getNextRelationshipAttributeSets(orm, transacting, body) { +export async function getNextRelationshipAttributeSets(orm, transacting, body) { const {RelationshipAttributeSet} = orm; const relationships = await Promise.all(body.relationships.map(async (relationship) => { const id = relationship.attributeSetId; @@ -880,7 +851,7 @@ async function getNextRelationshipAttributeSets(orm, transacting, body) { return relationships; } -async function getNextRelationshipSets( +export async function getNextRelationshipSets( orm, transacting, currentEntity, body ) { const {RelationshipSet} = orm; @@ -930,7 +901,7 @@ async function getNextDisambiguation(orm, transacting, currentEntity, body) { ); } -async function getChangedProps( +export async function getChangedProps( orm, transacting, isNew, currentEntity, body, entityType, newRevision, derivedProps ) { @@ -985,7 +956,7 @@ async function getChangedProps( } -function fetchEntitiesForRelationships( +export function fetchEntitiesForRelationships( orm, transacting, currentEntity, relationshipSets ) { const bbidsToFetch = _.without( @@ -1005,7 +976,7 @@ function fetchEntitiesForRelationships( * @description Edition Groups will be created automatically by the ORM if no EditionGroup BBID is set on a new Edition. * This method fetches and indexes (search) those potential new EditionGroups that may have been created automatically. */ -async function indexAutoCreatedEditionGroup(orm, newEdition, transacting) { +export async function indexAutoCreatedEditionGroup(orm, newEdition, transacting) { const {EditionGroup} = orm; const bbid = newEdition.get('editionGroupBbid'); try { @@ -1158,16 +1129,7 @@ export function handleCreateOrEditEntity( }); const achievementPromise = entityEditPromise.then( - (entityJSON) => achievement.processEdit( - orm, editorJSON.id, entityJSON.revisionId - ) - .then((unlock) => { - if (unlock.alert) { - entityJSON.alert = unlock.alert; - } - return entityJSON; - }) - .catch(err => { throw err; }) + (entityJSON) => processAchievement(orm, editorJSON.id, entityJSON) ); return handler.sendPromiseResult( @@ -1342,185 +1304,3 @@ export function displayPreview(req:PassportRequest, res:$Response, next) { })); } -export function transformForm(body:Record):Record { - const modifiedForm = {}; - for (const keyIndex in body) { - if (Object.prototype.hasOwnProperty.call(body, keyIndex)) { - const currentForm = body[keyIndex]; - const transformedForm = transformFunctions[_.lowerFirst(currentForm.type)](currentForm); - modifiedForm[keyIndex] = {type: currentForm.type, ...transformedForm}; - } - } - return modifiedForm; -} - - -export async function handleAddRelationship( - body:Record, - editorId:number, - currentEntity, - entityType:EntityTypeString, - orm, - transacting -) { - const {Revision} = orm; - - // new revision for adding relationship - const newRevision = await new Revision({ - authorId: editorId, - isMerge: false - }).save(null, {transacting}); - const relationshipSets = await getNextRelationshipSets( - orm, transacting, currentEntity, body - ); - if (_.isEmpty(relationshipSets)) { - return {}; - } - // 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, - editorId, body.note - ); - return savedMainEntity.toJSON(); -} - -export function handleCreateEntities( - req: PassportRequest, - res: $Response -) { - const {orm}: {orm?: any} = req.app.locals; - const {Entity, Revision, bookshelf} = orm; - const editorJSON = req.user; - - const {body}: {body: Record} = req; - let 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; - - const entityEditPromise = bookshelf.transaction(async (transacting) => { - const savedMainEntities = {}; - // map dummy id to real bbid - const bbidMap = {}; - const allRelationships = {}; - // callback for creating entity - async function processEntity(entityKey:string) { - const entityForm = body[entityKey]; - const entityType = _.upperFirst(entityForm.type); - // edition entity should be on the bottom of the list - if (entityType === 'Edition' && !_.isEmpty(entityForm.publishers)) { - entityForm.publishers = entityForm.publishers.map((id) => bbidMap[id] ?? id); - } - allRelationships[entityKey] = entityForm.relationships; - const newEntity = await new Entity({type: entityType}).save(null, {transacting}); - currentEntity = newEntity.toJSON(); - - // create new revision for each entity - const newRevision = await new Revision({ - authorId: editorJSON.id, - isMerge: false - }).save(null, {transacting}); - const additionalProps = _.pick(entityForm, additionalEntityProps[_.snakeCase(entityType)]); - const changedProps = await getChangedProps( - orm, transacting, true, currentEntity, entityForm, entityType, - newRevision, additionalProps - ); - const mainEntity = await fetchOrCreateMainEntity( - orm, transacting, true, currentEntity.bbid, entityType - ); - mainEntity.shouldInsert = true; - - // set changed attributes on main entity - _.forOwn(changedProps, (value, key) => mainEntity.set(key, value)); - const savedMainEntity = await saveEntitiesAndFinishRevision( - orm, transacting, true, newRevision, mainEntity, [mainEntity], - editorJSON.id, entityForm.note - ); - - /* We need to load the aliases for search reindexing and refresh it*/ - await savedMainEntity.load('aliasSet.aliases', {transacting}); - - /* New entities will lack some attributes like 'type' required for search indexing */ - await savedMainEntity.refresh({transacting}); - - /* fetch and reindex EditionGroups that may have been created automatically by the ORM and not indexed */ - if (savedMainEntity.get('type') === 'Edition') { - await indexAutoCreatedEditionGroup(orm, savedMainEntity, transacting); - } - bbidMap[entityKey] = savedMainEntity.get('bbid'); - savedMainEntities[entityKey] = savedMainEntity.toJSON(); - } - try { - // bookshelf's transaction have issue with Promise.All - await Object.keys(body).reduce((promise, entityKey) => promise.then(() => processEntity(entityKey)), Promise.resolve()); - - // adding relationship on newly created entites - await Promise.all(Object.keys(allRelationships).map(async (entityId) => { - const rels = allRelationships[entityId]; - if (!_.isEmpty(rels)) { - const relationships = rels.map((rel) => ( - {...rel, sourceBbid: _.get(bbidMap, rel.sourceBbid) ?? rel.sourceBbid, - targetBbid: _.get(bbidMap, rel.targetBbid) ?? rel.targetBbid} - )); - const mainEntity = savedMainEntities[entityId]; - const {relationshipSetId} = await handleAddRelationship({relationships}, editorJSON.id, - mainEntity, mainEntity.type, orm, transacting); - mainEntity.relationshipSetId = relationshipSetId; - } - })); - return savedMainEntities; - } - catch (err) { - log.error(err); - throw err; - } - }); - const achievementPromise = entityEditPromise.then( - (entitiesJSON:Record) => { - const entitiesAchievementsPromise = []; - for (const entityJSON of Object.values(entitiesJSON)) { - entitiesAchievementsPromise.push(achievement.processEdit( - orm, editorJSON.id, entityJSON.revisionId - ) - .then((unlock) => { - if (unlock.alert) { - entityJSON.alert = unlock.alert; - } - return entityJSON; - })); - } - return Promise.all(entitiesAchievementsPromise).catch(err => { throw err; }); - } - ); - return handler.sendPromiseResult( - res, - achievementPromise, - _bulkIndexEntities - ); -} diff --git a/src/server/routes/entity/process-unified-form.ts b/src/server/routes/entity/process-unified-form.ts new file mode 100644 index 0000000000..d50af7ec11 --- /dev/null +++ b/src/server/routes/entity/process-unified-form.ts @@ -0,0 +1,236 @@ +import * as achievement from '../../helpers/achievement'; +import * as handler from '../../helpers/handler'; +import type {Request as $Request, Response as $Response} from 'express'; +import { + fetchEntitiesForRelationships, fetchOrCreateMainEntity, getChangedProps, + getNextRelationshipSets, indexAutoCreatedEditionGroup, saveEntitiesAndFinishRevision +} from './entity'; +import type { + EntityTypeString +} from 'bookbrainz-data/lib/func/types'; +import _ from 'lodash'; +import {_bulkIndexEntities} from '../../../common/helpers/search'; +import {transformNewForm as authorTransform} from './author'; +import {transformNewForm as editionGroupTransform} from './edition-group'; +import {transformNewForm as editionTransform} from './edition'; +import log from 'log'; +import {transformNewForm as publisherTransform} from './publisher'; +import {transformNewForm as seriesTransform} from './series'; +import {transformNewForm as workTransform} from './work'; + + +type PassportRequest = $Request & {user: any, session: any}; +const transformFunctions = { + author: authorTransform, + edition: editionTransform, + editionGroup: editionGroupTransform, + publisher: publisherTransform, + series: seriesTransform, + work: workTransform +}; +const additionalEntityProps = { + author: [ + 'typeId', 'genderId', 'beginAreaId', 'beginDate', 'endDate', 'ended', + 'endAreaId' + ], + edition: [ + 'editionGroupBbid', 'width', 'height', 'depth', 'weight', 'pages', + 'formatId', 'statusId' + ], + editionGroup: 'typeid', + publisher: ['typeId', 'areaId', 'beginDate', 'endDate', 'ended'], + series: ['entityType', 'orderingTypeId'], + work: 'typeId' + +}; + +export async function processAchievement(orm, editorId, entityJSON) { + const {revisionId} = entityJSON; + try { + const achievementUnlock = await achievement.processEdit(orm, editorId, revisionId); + if (achievementUnlock.alert) { + entityJSON.alert = achievementUnlock.alert; + } + return entityJSON; + } + catch (err) { + return Promise.reject(new Error(err)); + } +} + +export function transformForm(body:Record):Record { + const modifiedForm = {}; + for (const keyIndex in body) { + if (Object.prototype.hasOwnProperty.call(body, keyIndex)) { + const currentForm = body[keyIndex]; + const transformedForm = transformFunctions[_.lowerFirst(currentForm.type)](currentForm); + modifiedForm[keyIndex] = {type: currentForm.type, ...transformedForm}; + } + } + return modifiedForm; +} + + +export async function handleAddRelationship( + body:Record, + editorId:number, + currentEntity, + entityType:EntityTypeString, + orm, + transacting +) { + const {Revision} = orm; + + // new revision for adding relationship + const newRevision = await new Revision({ + authorId: editorId, + isMerge: false + }).save(null, {transacting}); + const relationshipSets = await getNextRelationshipSets( + orm, transacting, currentEntity, body + ); + if (_.isEmpty(relationshipSets)) { + return {}; + } + // 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, + editorId, body.note + ); + return savedMainEntity.toJSON(); +} + +async function processRelationship(rels:Record[], mainEntity, bbidMap:Record, editorId:number, orm, transacting) { + if (!_.isEmpty(rels)) { + const relationships = rels.map((rel) => ( + {...rel, sourceBbid: _.get(bbidMap, rel.sourceBbid) ?? rel.sourceBbid, + targetBbid: _.get(bbidMap, rel.targetBbid) ?? rel.targetBbid} + )); + const {relationshipSetId} = await handleAddRelationship({relationships}, editorId, + mainEntity, mainEntity.type, orm, transacting); + mainEntity.relationshipSetId = relationshipSetId; + } +} + +export function handleCreateMultipleEntities( + req: PassportRequest, + res: $Response +) { + const {orm}: {orm?: any} = req.app.locals; + const {Entity, Revision, bookshelf} = orm; + const editorJSON = req.user; + + const {body}: {body: Record} = req; + let 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; + + const entityEditPromise = bookshelf.transaction(async (transacting) => { + const savedMainEntities = {}; + // map dummy id to real bbid + const bbidMap = {}; + const allRelationships = {}; + // callback for creating entity + async function processEntity(entityKey:string) { + const entityForm = body[entityKey]; + const entityType = _.upperFirst(entityForm.type); + // edition entity should be on the bottom of the list + if (entityType === 'Edition' && !_.isEmpty(entityForm.publishers)) { + entityForm.publishers = entityForm.publishers.map((id) => bbidMap[id] ?? id); + } + allRelationships[entityKey] = entityForm.relationships; + const newEntity = await new Entity({type: entityType}).save(null, {transacting}); + currentEntity = newEntity.toJSON(); + + // create new revision for each entity + const newRevision = await new Revision({ + authorId: editorJSON.id, + isMerge: false + }).save(null, {transacting}); + const additionalProps = _.pick(entityForm, additionalEntityProps[_.snakeCase(entityType)]); + const changedProps = await getChangedProps( + orm, transacting, true, currentEntity, entityForm, entityType, + newRevision, additionalProps + ); + const mainEntity = await fetchOrCreateMainEntity( + orm, transacting, true, currentEntity.bbid, entityType + ); + mainEntity.shouldInsert = true; + + // set changed attributes on main entity + _.forOwn(changedProps, (value, key) => mainEntity.set(key, value)); + const savedMainEntity = await saveEntitiesAndFinishRevision( + orm, transacting, true, newRevision, mainEntity, [mainEntity], + editorJSON.id, entityForm.note + ); + + /* We need to load the aliases for search reindexing and refresh it*/ + await savedMainEntity.load('aliasSet.aliases', {transacting}); + + /* New entities will lack some attributes like 'type' required for search indexing */ + await savedMainEntity.refresh({transacting}); + + /* fetch and reindex EditionGroups that may have been created automatically by the ORM and not indexed */ + if (savedMainEntity.get('type') === 'Edition') { + await indexAutoCreatedEditionGroup(orm, savedMainEntity, transacting); + } + bbidMap[entityKey] = savedMainEntity.get('bbid'); + savedMainEntities[entityKey] = savedMainEntity.toJSON(); + } + try { + // bookshelf's transaction have issue with Promise.All, refer https://github.com/bookshelf/bookshelf/issues/1498 for more details + await Object.keys(body).reduce((promise, entityKey) => promise.then(() => processEntity(entityKey)), Promise.resolve()); + + // adding relationship on newly created entites + await Promise.all(Object.keys(allRelationships).map((entityId) => processRelationship( + allRelationships[entityId], savedMainEntities[entityId], bbidMap, editorJSON.id, orm, transacting + ))); + return savedMainEntities; + } + catch (err) { + log.error(err); + throw err; + } + }); + const achievementPromise = entityEditPromise.then( + (entitiesJSON:Record) => { + const entitiesAchievementsPromise = []; + for (const entityJSON of Object.values(entitiesJSON)) { + entitiesAchievementsPromise.push(processAchievement(orm, editorJSON.id, entityJSON)); + } + return Promise.all(entitiesAchievementsPromise).catch(err => { throw err; }); + } + ); + return handler.sendPromiseResult( + res, + achievementPromise, + _bulkIndexEntities + ); +} diff --git a/src/server/routes/unifiedform.ts b/src/server/routes/unifiedform.ts index 4654007147..49ffb8d42a 100644 --- a/src/server/routes/unifiedform.ts +++ b/src/server/routes/unifiedform.ts @@ -1,8 +1,8 @@ -import {createEntitesHandler} from '../helpers/entityRouteUtils'; +import {createEntitiesHandler} from '../helpers/entityRouteUtils'; import express from 'express'; import {isAuthenticatedForHandler} from '../helpers/auth'; const router = express.Router(); -router.post('/create/handler', isAuthenticatedForHandler, createEntitesHandler); +router.post('/create/handler', isAuthenticatedForHandler, createEntitiesHandler); export default router; From fd3bd19f8351085e4d7bc2f4d0265f5b4b8a4faa Mon Sep 17 00:00:00 2001 From: tri10 Date: Sun, 12 Jun 2022 14:53:31 +0530 Subject: [PATCH 023/258] feat(uf): added very basic entity creation modal --- src/client/unified-form/action.ts | 22 ++++ .../common/create-entity-modal.tsx | 26 ++++ .../unified-form/common/entity-modal-body.tsx | 20 +++ .../common/search-entity-create-select.tsx | 68 ++++++---- src/client/unified-form/content-tab/action.ts | 16 ++- .../unified-form/content-tab/reducer.ts | 11 +- src/client/unified-form/helpers.ts | 123 ++++++++++++++++-- src/client/unified-form/interface/type.ts | 12 +- 8 files changed, 252 insertions(+), 46 deletions(-) create mode 100644 src/client/unified-form/action.ts create mode 100644 src/client/unified-form/common/create-entity-modal.tsx create mode 100644 src/client/unified-form/common/entity-modal-body.tsx diff --git a/src/client/unified-form/action.ts b/src/client/unified-form/action.ts new file mode 100644 index 0000000000..512820b0cd --- /dev/null +++ b/src/client/unified-form/action.ts @@ -0,0 +1,22 @@ +export const DUMP_EDITION = 'DUMP_EDITION'; +export const LOAD_EDITION = 'LOAD_EDITION'; + +const nextEditionId = 0; +export function dumpEdition() { + return { + payload: { + id: nextEditionId, + value: null + }, + type: DUMP_EDITION + }; +} + +export function loadEdition(editionId = 0) { + return { + payload: { + id: editionId + }, + type: LOAD_EDITION + }; +} diff --git a/src/client/unified-form/common/create-entity-modal.tsx b/src/client/unified-form/common/create-entity-modal.tsx new file mode 100644 index 0000000000..0d61a5dc01 --- /dev/null +++ b/src/client/unified-form/common/create-entity-modal.tsx @@ -0,0 +1,26 @@ +import * as Bootstrap from 'react-bootstrap'; +import EntityModalBody from './entity-modal-body'; +import React from 'react'; + + +const {Modal} = Bootstrap; +type CreateEntityModalProps = { + handleClose:() => unknown, + handleSubmit:(e)=> unknown, + type:string, + show:boolean +}; +export default function CreateEntityModal({show, handleClose, handleSubmit, type, ...rest}:CreateEntityModalProps) { + const heading = `Add ${type}`; + return ( + + + {heading} + + + + + + + ); +} diff --git a/src/client/unified-form/common/entity-modal-body.tsx b/src/client/unified-form/common/entity-modal-body.tsx new file mode 100644 index 0000000000..628dd9ea39 --- /dev/null +++ b/src/client/unified-form/common/entity-modal-body.tsx @@ -0,0 +1,20 @@ +// import * as Bootstrap from 'react-bootstrap'; +import {Button} from 'react-bootstrap'; +import NameSection from '../../entity-editor/name-section/name-section'; +import React from 'react'; +import {upperFirst} from 'lodash'; + + +type EntityModalBodyProps = { + onModalSubmit:(e)=>unknown, + type:string +}; + +export default function EntityModalBody({onModalSubmit, type, ...rest}:EntityModalBodyProps) { + return ( + + + + + ); +} diff --git a/src/client/unified-form/common/search-entity-create-select.tsx b/src/client/unified-form/common/search-entity-create-select.tsx index a9926b48e1..7e72b558f4 100644 --- a/src/client/unified-form/common/search-entity-create-select.tsx +++ b/src/client/unified-form/common/search-entity-create-select.tsx @@ -1,7 +1,11 @@ +import {SearchEntityCreateDispatchProps, SearchEntityCreateProps} from '../interface/type'; +import {dumpEdition, loadEdition} from '../action'; import AsyncCreatable from 'react-select/async-creatable'; import BaseEntitySearch from '../../entity-editor/common/entity-search-field-option'; -import {CommonProps} from 'react-select'; +import CreateEntityModal from './create-entity-modal'; import React from 'react'; +import {addWork} from '../content-tab/action'; +import {connect} from 'react-redux'; import makeImmutable from '../../entity-editor/common/make-immutable'; @@ -14,35 +18,55 @@ const defaultProps = { languageOptions: [], tooltipText: null }; -type SearchEntityCreateProps = { - bbid?:string, - empty?:boolean, - nextId:string|number, - error?:boolean, - filters?:Array, - label:string, - tooltipText?:string, - languageOptions?:Array, - type:string | Array - -} & typeof defaultProps & CommonProps; - +const addEntityActions = { + work: addWork +}; function SearchEntityCreate(props:SearchEntityCreateProps):JSX.Element { - const {type, nextId} = props; + const {type, nextId, onModalOpen, onModalClose, onSubmitEntity, ...rest} = props; const createLabel = React.useCallback((input) => `Create ${type} "${input}"`, [type]); + const [showModal, setShowModal] = React.useState(false); const getNewOptionData = React.useCallback((input, label) => ({ __isNew__: true, id: nextId, text: label, type }), [type, nextId]); - return (); + const openModalHandler = React.useCallback(() => { + setShowModal(true); + onModalOpen(); + }, []); + const closeModalHandler = React.useCallback(() => { + setShowModal(false); + onModalClose(); + }, []); + const submitModalHandler = React.useCallback((ev: React.FormEvent) => { + ev.stopPropagation(); + setShowModal(false); + onSubmitEntity(); + onModalClose(); + }, []); + + return ( + <> + + + ); } SearchEntityCreate.defaultProps = defaultProps; -export default SearchEntityCreate; + +function mapDispatchToProps(dispatch, {type}):SearchEntityCreateDispatchProps { + return { + onModalClose: () => dispatch(loadEdition()), + onModalOpen: () => dispatch(dumpEdition()), + onSubmitEntity: () => dispatch(addEntityActions[type]()) + }; +} + +export default connect(null, mapDispatchToProps)(SearchEntityCreate); diff --git a/src/client/unified-form/content-tab/action.ts b/src/client/unified-form/content-tab/action.ts index 1c4474653b..914848b913 100644 --- a/src/client/unified-form/content-tab/action.ts +++ b/src/client/unified-form/content-tab/action.ts @@ -1,11 +1,21 @@ +import {size} from 'lodash'; export const ADD_WORK = 'ADD_WORK'; +export const UPDATE_WORKS = 'UPDATE_WORKS'; -let nextId = 0; -export function updateWorks(payload) { +let nextWorkId = 0; +export function addWork(value = null) { return { - payload: {id: nextId++, value: payload}, + payload: {id: nextWorkId++, value}, type: ADD_WORK }; } + +export function updateWorks(newWorks) { + nextWorkId = size(newWorks); + return { + payload: newWorks, + type: UPDATE_WORKS + }; +} diff --git a/src/client/unified-form/content-tab/reducer.ts b/src/client/unified-form/content-tab/reducer.ts index ed778105fc..c5770649c8 100644 --- a/src/client/unified-form/content-tab/reducer.ts +++ b/src/client/unified-form/content-tab/reducer.ts @@ -1,14 +1,15 @@ -import {ADD_WORK} from './action'; -import immutable from 'immutable'; +import {ADD_WORK, UPDATE_WORKS} from './action'; +import Immutable from 'immutable'; const initialState = {}; -export default function reducer(state = immutable.Map(initialState), {type, payload}) { +export default function reducer(state = Immutable.Map(initialState), {type, payload}) { switch (type) { case ADD_WORK: - return state.set(payload.id, immutable.fromJS(payload.value)); - + return state.set(payload.id, Immutable.fromJS(payload.value)); + case UPDATE_WORKS: + return Immutable.fromJS(payload); default: return state; } diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index 3606d37013..d6653844ed 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -1,10 +1,12 @@ +import {DUMP_EDITION, LOAD_EDITION} from './action'; +import {ADD_WORK} from './content-tab/action'; import ISBNReducer from './cover-tab/reducer'; +import Immutable from 'immutable'; import aliasEditorReducer from '../entity-editor/alias-editor/reducer'; import annotationSectionReducer from '../entity-editor/annotation-section/reducer'; import buttonBarReducer from '../entity-editor/button-bar/reducer'; import {combineReducers} from 'redux-immutable'; import editionSectionReducer from '../entity-editor/edition-section/reducer'; -import {createRootReducer as getEntityReducer} from '../entity-editor/helpers'; import identifierEditorReducer from '../entity-editor/identifier-editor/reducer'; import nameSectionReducer from '../entity-editor/name-section/reducer'; import relationshipSectionReducer from '../entity-editor/relationship-editor/reducer'; @@ -13,6 +15,7 @@ import {validateForm as validateAuthorForm} from '../entity-editor/validators/au import {validateForm as validateEditionForm} from '../entity-editor/validators/edition'; import {validateForm as validatePublisherForm} from '../entity-editor/validators/publisher'; import {validateForm as validateWorkForm} from '../entity-editor/validators/work'; +import workSectionReducer from '../entity-editor/work-section/reducer'; import worksReducer from './content-tab/reducer'; @@ -30,19 +33,111 @@ export const validatorMap = { work: validateWorkForm }; +// dummy reducer +function newEditionReducer(state = Immutable.Map({}), action) { + const {type, payload} = action; + switch (type) { + case DUMP_EDITION: + return state.set(payload.id, Immutable.fromJS(payload.value)); + case LOAD_EDITION: + return Immutable.Map({}); + default: + return state; + } +} +const initialState = Immutable.Map({ + aliasEditor: Immutable.Map({}), + annotationSection: Immutable.Map({content: ''}), + buttonBar: Immutable.Map({ + aliasEditorVisible: false, + identifierEditorVisible: false + }), + editionSection: Immutable.Map({ + format: null, + languages: Immutable.List([]), + matchingNameEditionGroups: [], + physicalEnable: true, + publisher: null, + releaseDate: '', + status: null + }), + identifierEditor: Immutable.OrderedMap(), + nameSection: Immutable.Map({ + disambiguation: '', + exactMatches: [], + language: null, + name: '', + searchResults: [], + sortName: '' + }), + relationshipSection: Immutable.Map({ + canEdit: true, + lastRelationships: null, + relationshipEditorProps: null, + relationshipEditorVisible: false, + relationships: Immutable.OrderedMap() + }), + submissionSection: Immutable.Map({ + note: '', + submitError: '', + submitted: false + }), + workSection: Immutable.Map({ + languages: Immutable.List([]), + type: null + }) +}); export function createRootReducer() { - return combineReducers({ - ISBN: ISBNReducer, - Work: getEntityReducer('work'), - Works: worksReducer, - aliasEditor: aliasEditorReducer, - annotationSection: annotationSectionReducer, - buttonBar: buttonBarReducer, - editionSection: editionSectionReducer, - identifierEditor: identifierEditorReducer, - nameSection: nameSectionReducer, - relationshipSection: relationshipSectionReducer, - submissionSection: submissionSectionReducer - }); + return (state: Immutable.Map, action) => { + const {type} = action; + let intermediateState = state; + const activeEntityState = { + aliasEditor: state.get('aliasEditor'), + annotationSection: state.get('annotationSection'), + buttonBar: state.get('buttonBar'), + identifierEditor: state.get('identifierEditor'), + nameSection: state.get('nameSection'), + relationshipSection: state.get('relationshipSection'), + submissionSection: state.get('submissionSection') + }; + switch (type) { + case DUMP_EDITION: + action.payload.value = { + ...activeEntityState, + editionSection: state.get('editionSection') + }; + intermediateState = intermediateState.merge(initialState); + break; + case ADD_WORK: + action.payload.value = action.payload.value ?? { + ...activeEntityState, + __isNew__: true, + id: action.payload.id, + text: activeEntityState.nameSection.get('name'), + type: 'Work', + workSection: intermediateState.get('workSection') + }; + break; + case LOAD_EDITION: + intermediateState = intermediateState.merge(intermediateState.getIn(['Editions', action.payload.id])); + break; + default: + break; + } + return combineReducers({ + Editions: newEditionReducer, + ISBN: ISBNReducer, + Works: worksReducer, + aliasEditor: aliasEditorReducer, + annotationSection: annotationSectionReducer, + buttonBar: buttonBarReducer, + editionSection: editionSectionReducer, + identifierEditor: identifierEditorReducer, + nameSection: nameSectionReducer, + relationshipSection: relationshipSectionReducer, + submissionSection: submissionSectionReducer, + workSection: workSectionReducer + })(intermediateState, action); + }; } diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index 4b8928a07f..52709b31ef 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -1,3 +1,5 @@ +import {CommonProps} from 'react-select'; + export type RInputEvent = React.ChangeEvent; @@ -78,8 +80,13 @@ export type NavButtonsProps = { disableBack:boolean, disableNext:boolean }; +export type SearchEntityCreateDispatchProps = { + onModalOpen:()=>unknown, + onModalClose:()=>unknown, + onSubmitEntity:()=>unknown +}; -export type SearchEntityCreateProps = { +export type SearchEntityCreateOwnProps = { bbid?:string, empty?:boolean, nextId:string|number, @@ -89,7 +96,8 @@ export type SearchEntityCreateProps = { tooltipText?:string, languageOptions?:Array, value?:Array | EntitySelect - type:string | Array, + type:string, onChange:(arg)=>unknown }; +export type SearchEntityCreateProps = SearchEntityCreateDispatchProps & SearchEntityCreateOwnProps & CommonProps; From d5fa8fcc7ddbf0ae2d94a16307f3ba0a93cafcea Mon Sep 17 00:00:00 2001 From: tri10 Date: Mon, 13 Jun 2022 20:58:07 +0530 Subject: [PATCH 024/258] feat(uf): add all sections for work creation --- .../entity-editor/name-section/actions.ts | 15 +++ src/client/stylesheets/style.scss | 3 + .../common/create-entity-modal.tsx | 7 +- .../unified-form/common/entity-modal-body.tsx | 94 +++++++++++++++++-- .../common/search-entity-create-select.tsx | 12 ++- src/client/unified-form/interface/type.ts | 2 +- src/server/routes/search.js | 3 +- 7 files changed, 121 insertions(+), 15 deletions(-) diff --git a/src/client/entity-editor/name-section/actions.ts b/src/client/entity-editor/name-section/actions.ts index 2551849695..cd74ab4d8a 100644 --- a/src/client/entity-editor/name-section/actions.ts +++ b/src/client/entity-editor/name-section/actions.ts @@ -35,6 +35,20 @@ export type Action = { } }; +/** + * Produces an action indicating that the name for the entity being edited + * should be updated with the provided value. + * + * @param {string} newName - The new value to be used for the name. + * @returns {Action} The resulting UPDATE_NAME_FIELD action. + */ +export function updateNameField(newName: string): Action { + return { + payload: newName, + type: UPDATE_NAME_FIELD + }; +} + /** * Produces an action indicating that the name for the entity being edited * should be updated with the provided value. The action is marked to be @@ -188,6 +202,7 @@ export function searchName( request.get('/search/autocomplete') .query({ q: name, + size: 3, type }) .then(res => { diff --git a/src/client/stylesheets/style.scss b/src/client/stylesheets/style.scss index b2f77b8032..237eb29e2d 100644 --- a/src/client/stylesheets/style.scss +++ b/src/client/stylesheets/style.scss @@ -770,4 +770,7 @@ $uf-primary:#EB743B; { margin-right: 3rem; } +} +.uf-dialog{ + max-width: 700px; } \ No newline at end of file diff --git a/src/client/unified-form/common/create-entity-modal.tsx b/src/client/unified-form/common/create-entity-modal.tsx index 0d61a5dc01..f929a5f6cb 100644 --- a/src/client/unified-form/common/create-entity-modal.tsx +++ b/src/client/unified-form/common/create-entity-modal.tsx @@ -1,4 +1,5 @@ import * as Bootstrap from 'react-bootstrap'; +import {getEntitySection, getValidator} from '../../entity-editor/helpers'; import EntityModalBody from './entity-modal-body'; import React from 'react'; @@ -12,13 +13,17 @@ type CreateEntityModalProps = { }; export default function CreateEntityModal({show, handleClose, handleSubmit, type, ...rest}:CreateEntityModalProps) { const heading = `Add ${type}`; + const EntitySection = getEntitySection(type); + const validate = getValidator(type); return ( {heading} - + + + diff --git a/src/client/unified-form/common/entity-modal-body.tsx b/src/client/unified-form/common/entity-modal-body.tsx index 628dd9ea39..158836eec5 100644 --- a/src/client/unified-form/common/entity-modal-body.tsx +++ b/src/client/unified-form/common/entity-modal-body.tsx @@ -1,20 +1,98 @@ // import * as Bootstrap from 'react-bootstrap'; -import {Button} from 'react-bootstrap'; +import {Accordion, Card} from 'react-bootstrap'; +import AliasEditor from '../../entity-editor/alias-editor/alias-editor'; +import AnnotationSection from '../../entity-editor/annotation-section/annotation-section'; +import ButtonBar from '../../entity-editor/button-bar/button-bar'; +import IdentifierEditor from '../../entity-editor/identifier-editor/identifier-editor'; import NameSection from '../../entity-editor/name-section/name-section'; import React from 'react'; -import {upperFirst} from 'lodash'; +import RelationshipSection from '../../entity-editor/relationship-editor/relationship-section'; +import SubmissionSection from '../../entity-editor/submission-section/submission-section'; +import {connect} from 'react-redux'; -type EntityModalBodyProps = { +type EntityModalBodyStateProps = { + aliasEditorVisible:boolean, + identifierEditorVisible:boolean +}; + +type EntityModalBodyOwnProps = { onModalSubmit:(e)=>unknown, - type:string + type:string, + validate:(arg)=>unknown + children?: React.ReactElement }; +type EntityModalBodyProps = EntityModalBodyOwnProps & EntityModalBodyStateProps; -export default function EntityModalBody({onModalSubmit, type, ...rest}:EntityModalBodyProps) { +function EntityModalBody({onModalSubmit, type, aliasEditorVisible, identifierEditorVisible, children, validate, ...rest}:EntityModalBodyProps) { return (
+ + + + Basic + + + + + + + + + + + + Entity Section + + + + { + React.cloneElement( + React.Children.only(children), + {...rest} + ) + } + + + + + + + Relationships + + + + + + + + + + + + Annotation + + + + + + + + + - - - ); + + ); } +EntityModalBody.defaultProps = { + children: null +}; +function mapStateToProps(rootState): EntityModalBodyStateProps { + const state = rootState.get('buttonBar'); + return { + aliasEditorVisible: state.get('aliasEditorVisible'), + identifierEditorVisible: state.get('identifierEditorVisible') + }; +} + +export default connect(mapStateToProps, null)(EntityModalBody); diff --git a/src/client/unified-form/common/search-entity-create-select.tsx b/src/client/unified-form/common/search-entity-create-select.tsx index 7e72b558f4..c368ebcded 100644 --- a/src/client/unified-form/common/search-entity-create-select.tsx +++ b/src/client/unified-form/common/search-entity-create-select.tsx @@ -7,6 +7,7 @@ import React from 'react'; import {addWork} from '../content-tab/action'; import {connect} from 'react-redux'; import makeImmutable from '../../entity-editor/common/make-immutable'; +import {updateNameField} from '../../entity-editor/name-section/actions'; const ImmutableCreatableAsync = makeImmutable(AsyncCreatable); @@ -31,16 +32,16 @@ function SearchEntityCreate(props:SearchEntityCreateProps):JSX.Element { text: label, type }), [type, nextId]); - const openModalHandler = React.useCallback(() => { + const openModalHandler = React.useCallback((name) => { setShowModal(true); - onModalOpen(); + onModalOpen(name); }, []); const closeModalHandler = React.useCallback(() => { setShowModal(false); onModalClose(); }, []); const submitModalHandler = React.useCallback((ev: React.FormEvent) => { - ev.stopPropagation(); + ev.preventDefault(); setShowModal(false); onSubmitEntity(); onModalClose(); @@ -63,7 +64,10 @@ SearchEntityCreate.defaultProps = defaultProps; function mapDispatchToProps(dispatch, {type}):SearchEntityCreateDispatchProps { return { onModalClose: () => dispatch(loadEdition()), - onModalOpen: () => dispatch(dumpEdition()), + onModalOpen: (name) => { + dispatch(dumpEdition()); + dispatch(updateNameField(name)); + }, onSubmitEntity: () => dispatch(addEntityActions[type]()) }; } diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index 52709b31ef..b152178ebf 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -81,7 +81,7 @@ export type NavButtonsProps = { disableNext:boolean }; export type SearchEntityCreateDispatchProps = { - onModalOpen:()=>unknown, + onModalOpen:(arg:string)=>unknown, onModalClose:()=>unknown, onSubmitEntity:()=>unknown }; diff --git a/src/server/routes/search.js b/src/server/routes/search.js index c5b99f9e6b..0a3a602961 100644 --- a/src/server/routes/search.js +++ b/src/server/routes/search.js @@ -115,8 +115,9 @@ router.get('/autocomplete', (req, res) => { const {orm} = req.app.locals; const query = req.query.q; const type = req.query.type || 'allEntities'; + const size = req.query.size || 42; - const searchPromise = search.autocomplete(orm, query, type); + const searchPromise = search.autocomplete(orm, query, type, size); handler.sendPromiseResult(res, searchPromise); }); From f80e527d1631ab4b8ad2a4160cc92fb1835ebff2 Mon Sep 17 00:00:00 2001 From: tri10 Date: Mon, 13 Jun 2022 23:08:33 +0530 Subject: [PATCH 025/258] feat(uf): add support for creating publishers --- .../common/create-entity-modal.tsx | 9 +- .../unified-form/common/entity-modal-body.tsx | 6 +- .../common/search-entity-create-select.tsx | 7 +- src/client/unified-form/cover-tab/action.ts | 11 +++ .../unified-form/cover-tab/cover-tab.tsx | 1 + src/client/unified-form/cover-tab/reducer.ts | 15 ++- src/client/unified-form/helpers.ts | 91 +++++++++++-------- src/server/routes/unifiedform.ts | 2 +- 8 files changed, 96 insertions(+), 46 deletions(-) diff --git a/src/client/unified-form/common/create-entity-modal.tsx b/src/client/unified-form/common/create-entity-modal.tsx index f929a5f6cb..b8127dad83 100644 --- a/src/client/unified-form/common/create-entity-modal.tsx +++ b/src/client/unified-form/common/create-entity-modal.tsx @@ -2,6 +2,8 @@ import * as Bootstrap from 'react-bootstrap'; import {getEntitySection, getValidator} from '../../entity-editor/helpers'; import EntityModalBody from './entity-modal-body'; import React from 'react'; +import {filterIdentifierTypesByEntityType} from '../../../common/helpers/utils'; +import {upperFirst} from 'lodash'; const {Modal} = Bootstrap; @@ -15,13 +17,18 @@ export default function CreateEntityModal({show, handleClose, handleSubmit, type const heading = `Add ${type}`; const EntitySection = getEntitySection(type); const validate = getValidator(type); + const {identifierTypes} = rest; + const entityIdentifierTypes = filterIdentifierTypesByEntityType(identifierTypes, upperFirst(type)); return ( {heading} - + diff --git a/src/client/unified-form/common/entity-modal-body.tsx b/src/client/unified-form/common/entity-modal-body.tsx index 158836eec5..cf8395698d 100644 --- a/src/client/unified-form/common/entity-modal-body.tsx +++ b/src/client/unified-form/common/entity-modal-body.tsx @@ -18,13 +18,13 @@ type EntityModalBodyStateProps = { type EntityModalBodyOwnProps = { onModalSubmit:(e)=>unknown, - type:string, + entityType:string, validate:(arg)=>unknown children?: React.ReactElement }; type EntityModalBodyProps = EntityModalBodyOwnProps & EntityModalBodyStateProps; -function EntityModalBody({onModalSubmit, type, aliasEditorVisible, identifierEditorVisible, children, validate, ...rest}:EntityModalBodyProps) { +function EntityModalBody({onModalSubmit, aliasEditorVisible, identifierEditorVisible, children, validate, ...rest}:EntityModalBodyProps) { return (
@@ -34,7 +34,7 @@ function EntityModalBody({onModalSubmit, type, aliasEditorVisible, identifierEdi - + diff --git a/src/client/unified-form/common/search-entity-create-select.tsx b/src/client/unified-form/common/search-entity-create-select.tsx index c368ebcded..04cb4a6436 100644 --- a/src/client/unified-form/common/search-entity-create-select.tsx +++ b/src/client/unified-form/common/search-entity-create-select.tsx @@ -4,6 +4,7 @@ import AsyncCreatable from 'react-select/async-creatable'; import BaseEntitySearch from '../../entity-editor/common/entity-search-field-option'; import CreateEntityModal from './create-entity-modal'; import React from 'react'; +import {addPublisher} from '../cover-tab/action'; import {addWork} from '../content-tab/action'; import {connect} from 'react-redux'; import makeImmutable from '../../entity-editor/common/make-immutable'; @@ -19,7 +20,8 @@ const defaultProps = { languageOptions: [], tooltipText: null }; -const addEntityActions = { +const addEntityAction = { + publisher: addPublisher, work: addWork }; function SearchEntityCreate(props:SearchEntityCreateProps):JSX.Element { @@ -42,6 +44,7 @@ function SearchEntityCreate(props:SearchEntityCreateProps):JSX.Element { }, []); const submitModalHandler = React.useCallback((ev: React.FormEvent) => { ev.preventDefault(); + ev.stopPropagation(); setShowModal(false); onSubmitEntity(); onModalClose(); @@ -68,7 +71,7 @@ function mapDispatchToProps(dispatch, {type}):SearchEntityCreateDispatchProps { dispatch(dumpEdition()); dispatch(updateNameField(name)); }, - onSubmitEntity: () => dispatch(addEntityActions[type]()) + onSubmitEntity: () => dispatch(addEntityAction[type]()) }; } diff --git a/src/client/unified-form/cover-tab/action.ts b/src/client/unified-form/cover-tab/action.ts index 7350ae24f5..1fea2040c5 100644 --- a/src/client/unified-form/cover-tab/action.ts +++ b/src/client/unified-form/cover-tab/action.ts @@ -3,6 +3,17 @@ import {Action} from '../interface/type'; export const UPDATE_ISBN_VALUE = 'UPDATE_ISBN_VALUE'; export const UPDATE_ISBN_TYPE = 'UPDATE_ISBN_TYPE'; +export const ADD_PUBLISHER = 'ADD_PUBLISHER'; + + +let nextPublisherId = 0; +export function addPublisher(value = null) { + return { + payload: {id: nextPublisherId++, value}, + type: ADD_PUBLISHER + }; +} + export function debouncedUpdateISBNValue(newValue: string): Action { return { diff --git a/src/client/unified-form/cover-tab/cover-tab.tsx b/src/client/unified-form/cover-tab/cover-tab.tsx index 45a36b3b2a..26705b8a99 100644 --- a/src/client/unified-form/cover-tab/cover-tab.tsx +++ b/src/client/unified-form/cover-tab/cover-tab.tsx @@ -22,6 +22,7 @@ export function CoverTab(props:CoverProps) { type="publisher" value={publisherValue} onChange={onPublisherChange} + {...props} /> diff --git a/src/client/unified-form/cover-tab/reducer.ts b/src/client/unified-form/cover-tab/reducer.ts index b2012e4572..78b370efc4 100644 --- a/src/client/unified-form/cover-tab/reducer.ts +++ b/src/client/unified-form/cover-tab/reducer.ts @@ -1,8 +1,8 @@ -import {UPDATE_ISBN_TYPE, UPDATE_ISBN_VALUE} from './action'; -import {Map as immutableMap} from 'immutable'; +import {ADD_PUBLISHER, UPDATE_ISBN_TYPE, UPDATE_ISBN_VALUE} from './action'; +import Immutable from 'immutable'; -export default function reducer(state = immutableMap({ +export default function reducer(state = Immutable.Map({ type: null, value: '' }), action) { @@ -16,3 +16,12 @@ export default function reducer(state = immutableMap({ return state; } } + +export function publishersReducer(state = Immutable.Map({}), {type, payload}) { + switch (type) { + case ADD_PUBLISHER: + return state.set(payload.id, Immutable.fromJS(payload.value)); + default: + return state; + } +} diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index d6653844ed..a2f4de6e2c 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -1,6 +1,7 @@ import {DUMP_EDITION, LOAD_EDITION} from './action'; +import ISBNReducer, {publishersReducer} from './cover-tab/reducer'; +import {ADD_PUBLISHER} from './cover-tab/action'; import {ADD_WORK} from './content-tab/action'; -import ISBNReducer from './cover-tab/reducer'; import Immutable from 'immutable'; import aliasEditorReducer from '../entity-editor/alias-editor/reducer'; import annotationSectionReducer from '../entity-editor/annotation-section/reducer'; @@ -9,6 +10,7 @@ import {combineReducers} from 'redux-immutable'; import editionSectionReducer from '../entity-editor/edition-section/reducer'; import identifierEditorReducer from '../entity-editor/identifier-editor/reducer'; import nameSectionReducer from '../entity-editor/name-section/reducer'; +import publisherSectionReducer from '../entity-editor/publisher-section/reducer'; import relationshipSectionReducer from '../entity-editor/relationship-editor/reducer'; import submissionSectionReducer from '../entity-editor/submission-section/reducer'; import {validateForm as validateAuthorForm} from '../entity-editor/validators/author'; @@ -88,46 +90,62 @@ const initialState = Immutable.Map({ }) }); +function crossSliceReducer(state, action) { + const {type} = action; + let intermediateState = state; + const activeEntityState = { + aliasEditor: state.get('aliasEditor'), + annotationSection: state.get('annotationSection'), + buttonBar: state.get('buttonBar'), + identifierEditor: state.get('identifierEditor'), + nameSection: state.get('nameSection'), + relationshipSection: state.get('relationshipSection'), + submissionSection: state.get('submissionSection') + }; + switch (type) { + case DUMP_EDITION: + action.payload.value = { + ...activeEntityState, + editionSection: state.get('editionSection') + }; + intermediateState = intermediateState.merge(initialState); + break; + case ADD_WORK: + action.payload.value = action.payload.value ?? { + ...activeEntityState, + __isNew__: true, + id: action.payload.id, + text: activeEntityState.nameSection.get('name'), + type: 'Work', + workSection: intermediateState.get('workSection') + }; + break; + case ADD_PUBLISHER: + action.payload.value = action.payload.value ?? { + ...activeEntityState, + __isNew__: true, + id: action.payload.id, + publisherSection: intermediateState.get('publisherSection'), + text: activeEntityState.nameSection.get('name'), + type: 'Work' + }; + break; + case LOAD_EDITION: + intermediateState = intermediateState.merge(intermediateState.getIn(['Editions', action.payload.id])); + break; + default: + break; + } + return intermediateState; +} + export function createRootReducer() { return (state: Immutable.Map, action) => { - const {type} = action; - let intermediateState = state; - const activeEntityState = { - aliasEditor: state.get('aliasEditor'), - annotationSection: state.get('annotationSection'), - buttonBar: state.get('buttonBar'), - identifierEditor: state.get('identifierEditor'), - nameSection: state.get('nameSection'), - relationshipSection: state.get('relationshipSection'), - submissionSection: state.get('submissionSection') - }; - switch (type) { - case DUMP_EDITION: - action.payload.value = { - ...activeEntityState, - editionSection: state.get('editionSection') - }; - intermediateState = intermediateState.merge(initialState); - break; - case ADD_WORK: - action.payload.value = action.payload.value ?? { - ...activeEntityState, - __isNew__: true, - id: action.payload.id, - text: activeEntityState.nameSection.get('name'), - type: 'Work', - workSection: intermediateState.get('workSection') - }; - break; - case LOAD_EDITION: - intermediateState = intermediateState.merge(intermediateState.getIn(['Editions', action.payload.id])); - break; - default: - break; - } + const intermediateState = crossSliceReducer(state, action); return combineReducers({ Editions: newEditionReducer, ISBN: ISBNReducer, + Publishers: publishersReducer, Works: worksReducer, aliasEditor: aliasEditorReducer, annotationSection: annotationSectionReducer, @@ -135,6 +153,7 @@ export function createRootReducer() { editionSection: editionSectionReducer, identifierEditor: identifierEditorReducer, nameSection: nameSectionReducer, + publisherSection: publisherSectionReducer, relationshipSection: relationshipSectionReducer, submissionSection: submissionSectionReducer, workSection: workSectionReducer diff --git a/src/server/routes/unifiedform.ts b/src/server/routes/unifiedform.ts index b20dadd271..f21c807d0a 100644 --- a/src/server/routes/unifiedform.ts +++ b/src/server/routes/unifiedform.ts @@ -11,7 +11,7 @@ type PassportRequest = express.Request & {user: any, session: any}; const router = express.Router(); router.get('/create', isAuthenticated, middleware.loadIdentifierTypes, middleware.loadEditionStatuses, middleware.loadEditionFormats, - middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadGenders, middleware.loadAuthorTypes, + middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadGenders, middleware.loadPublisherTypes, middleware.loadAuthorTypes, middleware.loadRelationshipTypes, (req:PassportRequest, res:express.Response) => { const props = generateUnifiedProps(req, res, {}); const formMarkup = unifiedFormMarkup(props); From c861681f96b17a0c94a60a948e76c56b236564e3 Mon Sep 17 00:00:00 2001 From: tri10 Date: Tue, 14 Jun 2022 18:25:28 +0530 Subject: [PATCH 026/258] fix(uf): fix minor issues --- src/client/entity-editor/edition-section/edition-section.tsx | 4 +++- src/client/unified-form/action.ts | 4 ++-- src/client/unified-form/content-tab/action.ts | 2 +- src/client/unified-form/cover-tab/action.ts | 2 +- src/client/unified-form/cover-tab/cover-tab.tsx | 4 +++- src/client/unified-form/cover-tab/isbn-field.tsx | 2 +- src/client/unified-form/cover-tab/reducer.ts | 2 +- src/client/unified-form/helpers.ts | 4 ++-- 8 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/client/entity-editor/edition-section/edition-section.tsx b/src/client/entity-editor/edition-section/edition-section.tsx index 7c59e2cd39..03338191e3 100644 --- a/src/client/entity-editor/edition-section/edition-section.tsx +++ b/src/client/entity-editor/edition-section/edition-section.tsx @@ -187,7 +187,8 @@ function EditionSection({ releaseDateValue, statusValue, weightValue, - widthValue + widthValue, + ...rest }: Props) { const languageOptionsForDisplay = languageOptions.map((language) => ({ frequency: language.frequency, @@ -234,6 +235,7 @@ function EditionSection({ type="edition-group" value={editionGroupValue} onChange={onEditionGroupChange} + {...rest} />
); diff --git a/src/client/unified-form/cover-tab/reducer.ts b/src/client/unified-form/cover-tab/reducer.ts index 78b370efc4..ecaf3d624f 100644 --- a/src/client/unified-form/cover-tab/reducer.ts +++ b/src/client/unified-form/cover-tab/reducer.ts @@ -2,7 +2,7 @@ import {ADD_PUBLISHER, UPDATE_ISBN_TYPE, UPDATE_ISBN_VALUE} from './action'; import Immutable from 'immutable'; -export default function reducer(state = Immutable.Map({ +export function ISBNReducer(state = Immutable.Map({ type: null, value: '' }), action) { diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index a2f4de6e2c..1c4702cb25 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -1,5 +1,5 @@ import {DUMP_EDITION, LOAD_EDITION} from './action'; -import ISBNReducer, {publishersReducer} from './cover-tab/reducer'; +import {ISBNReducer, publishersReducer} from './cover-tab/reducer'; import {ADD_PUBLISHER} from './cover-tab/action'; import {ADD_WORK} from './content-tab/action'; import Immutable from 'immutable'; @@ -127,7 +127,7 @@ function crossSliceReducer(state, action) { id: action.payload.id, publisherSection: intermediateState.get('publisherSection'), text: activeEntityState.nameSection.get('name'), - type: 'Work' + type: 'Publisher' }; break; case LOAD_EDITION: From 8faca59a2b0b8b470b20a86ab9cb2245185fa806 Mon Sep 17 00:00:00 2001 From: tri10 Date: Tue, 14 Jun 2022 20:38:17 +0530 Subject: [PATCH 027/258] feat(uf): modal support for edition-group --- .../edition-section/edition-section.tsx | 4 ++-- .../common/search-entity-create-select.tsx | 2 ++ src/client/unified-form/cover-tab/action.ts | 2 +- src/client/unified-form/detail-tab/action.ts | 11 +++++++++++ src/client/unified-form/detail-tab/reducer.ts | 12 ++++++++++++ src/client/unified-form/helpers.ts | 16 ++++++++++++++++ src/server/routes/unifiedform.ts | 2 +- 7 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 src/client/unified-form/detail-tab/action.ts create mode 100644 src/client/unified-form/detail-tab/reducer.ts diff --git a/src/client/entity-editor/edition-section/edition-section.tsx b/src/client/entity-editor/edition-section/edition-section.tsx index 03338191e3..df2031355c 100644 --- a/src/client/entity-editor/edition-section/edition-section.tsx +++ b/src/client/entity-editor/edition-section/edition-section.tsx @@ -195,7 +195,7 @@ function EditionSection({ label: language.name, value: language.id })); - + rest.languageOptions = languageOptions; const editionFormatsForDisplay = editionFormats.map((format) => ({ label: format.label, value: format.id @@ -232,7 +232,7 @@ function EditionSection({
For example paperback, hardcover and e-book editions. } - type="edition-group" + type="editionGroup" value={editionGroupValue} onChange={onEditionGroupChange} {...rest} diff --git a/src/client/unified-form/common/search-entity-create-select.tsx b/src/client/unified-form/common/search-entity-create-select.tsx index 04cb4a6436..55f2d7d8a4 100644 --- a/src/client/unified-form/common/search-entity-create-select.tsx +++ b/src/client/unified-form/common/search-entity-create-select.tsx @@ -4,6 +4,7 @@ import AsyncCreatable from 'react-select/async-creatable'; import BaseEntitySearch from '../../entity-editor/common/entity-search-field-option'; import CreateEntityModal from './create-entity-modal'; import React from 'react'; +import {addEditionGroup} from '../detail-tab/action'; import {addPublisher} from '../cover-tab/action'; import {addWork} from '../content-tab/action'; import {connect} from 'react-redux'; @@ -21,6 +22,7 @@ const defaultProps = { tooltipText: null }; const addEntityAction = { + editionGroup: addEditionGroup, publisher: addPublisher, work: addWork }; diff --git a/src/client/unified-form/cover-tab/action.ts b/src/client/unified-form/cover-tab/action.ts index 50f67572a1..627bf71889 100644 --- a/src/client/unified-form/cover-tab/action.ts +++ b/src/client/unified-form/cover-tab/action.ts @@ -7,7 +7,7 @@ export const ADD_PUBLISHER = 'ADD_PUBLISHER'; let nextPublisherId = 0; -export function addPublisher(value = null) { +export function addPublisher(value = null):Action { return { payload: {id: `n${nextPublisherId++}`, value}, type: ADD_PUBLISHER diff --git a/src/client/unified-form/detail-tab/action.ts b/src/client/unified-form/detail-tab/action.ts new file mode 100644 index 0000000000..4cf9040fee --- /dev/null +++ b/src/client/unified-form/detail-tab/action.ts @@ -0,0 +1,11 @@ +import {Action} from '../interface/type'; + + +export const ADD_EDITION_GROUP = 'ADD_EDITION_GROUP'; +let nextEditionGroupId = 0; +export function addEditionGroup(value = null):Action { + return { + payload: {id: `n${nextEditionGroupId++}`, value}, + type: ADD_EDITION_GROUP + }; +} diff --git a/src/client/unified-form/detail-tab/reducer.ts b/src/client/unified-form/detail-tab/reducer.ts new file mode 100644 index 0000000000..87a6b0a4bc --- /dev/null +++ b/src/client/unified-form/detail-tab/reducer.ts @@ -0,0 +1,12 @@ +import {ADD_EDITION_GROUP} from './action'; +import Immutable from 'immutable'; + + +export default function editionGroupsReducer(state = Immutable.Map({}), {type, payload}) { + switch (type) { + case ADD_EDITION_GROUP: + return state.set(payload.id, Immutable.fromJS(payload.value)); + default: + return state; + } +} diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index 1c4702cb25..220f66dc93 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -1,5 +1,6 @@ import {DUMP_EDITION, LOAD_EDITION} from './action'; import {ISBNReducer, publishersReducer} from './cover-tab/reducer'; +import {ADD_EDITION_GROUP} from './detail-tab/action'; import {ADD_PUBLISHER} from './cover-tab/action'; import {ADD_WORK} from './content-tab/action'; import Immutable from 'immutable'; @@ -7,6 +8,8 @@ import aliasEditorReducer from '../entity-editor/alias-editor/reducer'; import annotationSectionReducer from '../entity-editor/annotation-section/reducer'; import buttonBarReducer from '../entity-editor/button-bar/reducer'; import {combineReducers} from 'redux-immutable'; +import editionGroupSectionReducer from '../entity-editor/edition-group-section/reducer'; +import editionGroupsReducer from './detail-tab/reducer'; import editionSectionReducer from '../entity-editor/edition-section/reducer'; import identifierEditorReducer from '../entity-editor/identifier-editor/reducer'; import nameSectionReducer from '../entity-editor/name-section/reducer'; @@ -55,6 +58,7 @@ const initialState = Immutable.Map({ identifierEditorVisible: false }), editionSection: Immutable.Map({ + editionGroupVisible: true, format: null, languages: Immutable.List([]), matchingNameEditionGroups: [], @@ -110,6 +114,16 @@ function crossSliceReducer(state, action) { }; intermediateState = intermediateState.merge(initialState); break; + case ADD_EDITION_GROUP: + action.payload.value = action.payload.value ?? { + ...activeEntityState, + __isNew__: true, + editionGroupSection: intermediateState.get('editionGroupSection'), + id: action.payload.id, + text: activeEntityState.nameSection.get('name'), + type: 'Work' + }; + break; case ADD_WORK: action.payload.value = action.payload.value ?? { ...activeEntityState, @@ -143,6 +157,7 @@ export function createRootReducer() { return (state: Immutable.Map, action) => { const intermediateState = crossSliceReducer(state, action); return combineReducers({ + EditionGroups: editionGroupsReducer, Editions: newEditionReducer, ISBN: ISBNReducer, Publishers: publishersReducer, @@ -150,6 +165,7 @@ export function createRootReducer() { aliasEditor: aliasEditorReducer, annotationSection: annotationSectionReducer, buttonBar: buttonBarReducer, + editionGroupSection: editionGroupSectionReducer, editionSection: editionSectionReducer, identifierEditor: identifierEditorReducer, nameSection: nameSectionReducer, diff --git a/src/server/routes/unifiedform.ts b/src/server/routes/unifiedform.ts index f21c807d0a..3e7048a235 100644 --- a/src/server/routes/unifiedform.ts +++ b/src/server/routes/unifiedform.ts @@ -10,7 +10,7 @@ type PassportRequest = express.Request & {user: any, session: any}; const router = express.Router(); router.get('/create', isAuthenticated, middleware.loadIdentifierTypes, - middleware.loadEditionStatuses, middleware.loadEditionFormats, + middleware.loadEditionStatuses, middleware.loadEditionFormats, middleware.loadEditionGroupTypes, middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadGenders, middleware.loadPublisherTypes, middleware.loadAuthorTypes, middleware.loadRelationshipTypes, (req:PassportRequest, res:express.Response) => { const props = generateUnifiedProps(req, res, {}); From 2dd85d2747685d5bcdd5809f8665038deb8e2f11 Mon Sep 17 00:00:00 2001 From: tri10 Date: Wed, 15 Jun 2022 18:56:54 +0530 Subject: [PATCH 028/258] feat(uf): correctly transform post data --- .../submission-section/actions.ts | 26 ++++++++++++++++--- src/client/unified-form/action.ts | 4 +-- .../unified-form/common/entity-modal-body.tsx | 4 +-- src/client/unified-form/content-tab/action.ts | 2 +- .../unified-form/content-tab/content-tab.tsx | 2 +- src/client/unified-form/cover-tab/action.ts | 2 +- src/client/unified-form/detail-tab/action.ts | 2 +- src/client/unified-form/helpers.ts | 2 +- src/client/unified-form/unified-form.tsx | 2 +- 9 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/client/entity-editor/submission-section/actions.ts b/src/client/entity-editor/submission-section/actions.ts index 5fabbca654..1893b2ace0 100644 --- a/src/client/entity-editor/submission-section/actions.ts +++ b/src/client/entity-editor/submission-section/actions.ts @@ -117,13 +117,31 @@ function transformFormData(data:Record):Record { const newData = {}; const nextId = 0; // add new publisher + _.forEach(data.Publishers, (publisher, pid) => { + if (publisher.__isNew__) { + newData[pid] = publisher; + } + }); // add new works - + _.forEach(data.Works, (work, wid) => { + if (work.__isNew__) { + newData[wid] = work; + } + }); + // add new ediiton groups + _.forEach(data.EditionGroups, (editionGroup, egid) => { + if (editionGroup.__isNew__) { + newData[egid] = editionGroup; + } + }); // add edition at last if (data.ISBN.type) { data.identifierEditor.m0 = data.ISBN; } - data.relationshipSection.relationships = _.mapValues(data.works, (work, key) => { + // TO-DO will need to modify once we have multiple publisher + data.editionSection.publisher = _.get(data, ['Publishers', 'n0']) ?? data.editionSection.publisher; + data.editionSection.editionGroup = _.get(data, ['EditionGroups', 'n0']) ?? data.editionSection.editionGroup; + data.relationshipSection.relationships = _.mapValues(data.Works, (work, key) => { const relationship = { attributeSetId: null, attributes: [], @@ -132,7 +150,7 @@ function transformFormData(data:Record):Record { }, rowID: key, sourceEntity: { - bbid: nextId + bbid: `e${nextId}` }, targetEntity: { bbid: work.id @@ -140,7 +158,7 @@ function transformFormData(data:Record):Record { }; return relationship; }); - newData[`b${nextId}`] = {...data, type: 'Edition'}; + newData[`e${nextId}`] = {...data, type: 'Edition'}; return newData; } diff --git a/src/client/unified-form/action.ts b/src/client/unified-form/action.ts index bd5b429175..eb0a653963 100644 --- a/src/client/unified-form/action.ts +++ b/src/client/unified-form/action.ts @@ -5,14 +5,14 @@ const nextEditionId = 0; export function dumpEdition() { return { payload: { - id: `n${nextEditionId}`, + id: `e${nextEditionId}`, value: null }, type: DUMP_EDITION }; } -export function loadEdition(editionId = 'n0') { +export function loadEdition(editionId = 'e0') { return { payload: { id: editionId diff --git a/src/client/unified-form/common/entity-modal-body.tsx b/src/client/unified-form/common/entity-modal-body.tsx index cf8395698d..666b5ce470 100644 --- a/src/client/unified-form/common/entity-modal-body.tsx +++ b/src/client/unified-form/common/entity-modal-body.tsx @@ -34,8 +34,8 @@ function EntityModalBody({onModalSubmit, aliasEditorVisible, identifierEditorVis - - + + diff --git a/src/client/unified-form/content-tab/action.ts b/src/client/unified-form/content-tab/action.ts index ec3a546084..1ad112b0ba 100644 --- a/src/client/unified-form/content-tab/action.ts +++ b/src/client/unified-form/content-tab/action.ts @@ -7,7 +7,7 @@ export const UPDATE_WORKS = 'UPDATE_WORKS'; let nextWorkId = 0; export function addWork(value = null) { return { - payload: {id: `n${nextWorkId++}`, value}, + payload: {id: `w${nextWorkId++}`, value}, type: ADD_WORK }; } diff --git a/src/client/unified-form/content-tab/content-tab.tsx b/src/client/unified-form/content-tab/content-tab.tsx index aa1ffd19cd..975fa9a319 100644 --- a/src/client/unified-form/content-tab/content-tab.tsx +++ b/src/client/unified-form/content-tab/content-tab.tsx @@ -41,7 +41,7 @@ function mapDispatchToProps(dispatch) { onChange: (options:any[]) => { const mappedOptions = Object.fromEntries(options.map((value, index) => { value.__isNew__ = Boolean(value.__isNew__); - return [index, value]; + return [`w${index}`, value]; })); return dispatch(updateWorks(mappedOptions)); } diff --git a/src/client/unified-form/cover-tab/action.ts b/src/client/unified-form/cover-tab/action.ts index 627bf71889..6c67eb2f17 100644 --- a/src/client/unified-form/cover-tab/action.ts +++ b/src/client/unified-form/cover-tab/action.ts @@ -9,7 +9,7 @@ export const ADD_PUBLISHER = 'ADD_PUBLISHER'; let nextPublisherId = 0; export function addPublisher(value = null):Action { return { - payload: {id: `n${nextPublisherId++}`, value}, + payload: {id: `p${nextPublisherId++}`, value}, type: ADD_PUBLISHER }; } diff --git a/src/client/unified-form/detail-tab/action.ts b/src/client/unified-form/detail-tab/action.ts index 4cf9040fee..1985438a67 100644 --- a/src/client/unified-form/detail-tab/action.ts +++ b/src/client/unified-form/detail-tab/action.ts @@ -5,7 +5,7 @@ export const ADD_EDITION_GROUP = 'ADD_EDITION_GROUP'; let nextEditionGroupId = 0; export function addEditionGroup(value = null):Action { return { - payload: {id: `n${nextEditionGroupId++}`, value}, + payload: {id: `eg${nextEditionGroupId++}`, value}, type: ADD_EDITION_GROUP }; } diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index 220f66dc93..0a0465a6cd 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -121,7 +121,7 @@ function crossSliceReducer(state, action) { editionGroupSection: intermediateState.get('editionGroupSection'), id: action.payload.id, text: activeEntityState.nameSection.get('name'), - type: 'Work' + type: 'EditionGroup' }; break; case ADD_WORK: diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index ddf9c29924..2d8cf5a338 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -67,7 +67,7 @@ function mapDispatchToProps(dispatch, {submissionUrl}) { return { onSubmit: (event:React.FormEvent) => { event.preventDefault(); - dispatch(submit(submissionUrl), true); + dispatch(submit(submissionUrl, true)); } }; } From 10455158fd961509fdb45d4e041e489eee95efe9 Mon Sep 17 00:00:00 2001 From: tri10 Date: Wed, 15 Jun 2022 19:26:36 +0530 Subject: [PATCH 029/258] fix(uf): minor fixes --- src/client/entity-editor/submission-section/actions.ts | 4 ++-- src/client/unified-form/cover-tab/cover-tab.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/entity-editor/submission-section/actions.ts b/src/client/entity-editor/submission-section/actions.ts index 1893b2ace0..7d8fb95afa 100644 --- a/src/client/entity-editor/submission-section/actions.ts +++ b/src/client/entity-editor/submission-section/actions.ts @@ -139,8 +139,8 @@ function transformFormData(data:Record):Record { data.identifierEditor.m0 = data.ISBN; } // TO-DO will need to modify once we have multiple publisher - data.editionSection.publisher = _.get(data, ['Publishers', 'n0']) ?? data.editionSection.publisher; - data.editionSection.editionGroup = _.get(data, ['EditionGroups', 'n0']) ?? data.editionSection.editionGroup; + data.editionSection.publisher = _.get(data, ['Publishers', 'p0']) ?? data.editionSection.publisher; + data.editionSection.editionGroup = _.get(data, ['EditionGroups', 'eg0']) ?? data.editionSection.editionGroup; data.relationshipSection.relationships = _.mapValues(data.Works, (work, key) => { const relationship = { attributeSetId: null, diff --git a/src/client/unified-form/cover-tab/cover-tab.tsx b/src/client/unified-form/cover-tab/cover-tab.tsx index 8cc96c8759..fc22d4345f 100644 --- a/src/client/unified-form/cover-tab/cover-tab.tsx +++ b/src/client/unified-form/cover-tab/cover-tab.tsx @@ -42,7 +42,7 @@ export function CoverTab(props:CoverProps) { function mapStateToProps(rootState) { // currently it only supports single publisher - const singleNewPublisher = rootState.getIn(['Publishers', 'n0'], null); + const singleNewPublisher = rootState.getIn(['Publishers', 'p0'], null); return { identifierEditorVisible: rootState.getIn(['buttonBar', 'identifierEditorVisible']), publisherValue: singleNewPublisher ?? rootState.getIn(['editionSection', 'publisher']) From e9d9fea1a242af21f14c04c672b3792b285c2236 Mon Sep 17 00:00:00 2001 From: tri10 Date: Wed, 15 Jun 2022 21:33:13 +0530 Subject: [PATCH 030/258] test(uf): add test for linking edition group --- .../entity-editor/validators/edition.ts | 7 +-- src/server/helpers/entityRouteUtils.tsx | 2 +- .../routes/entity/process-unified-form.ts | 9 +++- test/src/server/routes/unifiedform.js | 45 +++++++++++++++++-- 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/client/entity-editor/validators/edition.ts b/src/client/entity-editor/validators/edition.ts index b5509d84b6..fcdeab7015 100644 --- a/src/client/entity-editor/validators/edition.ts +++ b/src/client/entity-editor/validators/edition.ts @@ -66,8 +66,8 @@ export function validateEditionSectionPages(value: any): boolean { return validatePositiveInteger(value); } -export function validateEditionSectionEditionGroup(value: any, editionGroupRequired: boolean | null | undefined): boolean { - return validateUUID(get(value, 'id', null), editionGroupRequired); +export function validateEditionSectionEditionGroup(value: any, editionGroupRequired: boolean | null | undefined, isCustom = false): boolean { + return isCustom ? !editionGroupRequired || Boolean(get(value, 'id', null)) : validateUUID(get(value, 'id', null), editionGroupRequired); } export function validateEditionSectionPublisher(value: any, isCustom = false): boolean { @@ -104,7 +104,8 @@ export function validateEditionSection(data: any, isCustom = false): boolean { validateEditionSectionPages(get(data, 'pages', null)) && validateEditionSectionEditionGroup( get(data, 'editionGroup', null), - get(data, 'editionGroupRequired', null) + get(data, 'editionGroupRequired', null), + isCustom ) && validateEditionSectionPublisher(get(data, 'publisher', null), isCustom) && validateEditionSectionReleaseDate(get(data, 'releaseDate', null)).isValid && diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index 972287a847..8a0f269ecd 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -324,7 +324,7 @@ function validateUnifiedForm(body:Record):boolean { for (const entityKey in body) { if (Object.prototype.hasOwnProperty.call(body, entityKey)) { const entityForm = body[entityKey]; - const entityType = _.snakeCase(entityForm.type); + const entityType = _.camelCase(entityForm.type); if (!entityType && !validEntityTypes.includes(entityType)) { return false; } diff --git a/src/server/routes/entity/process-unified-form.ts b/src/server/routes/entity/process-unified-form.ts index d50af7ec11..4775e82393 100644 --- a/src/server/routes/entity/process-unified-form.ts +++ b/src/server/routes/entity/process-unified-form.ts @@ -162,8 +162,13 @@ export function handleCreateMultipleEntities( const entityForm = body[entityKey]; const entityType = _.upperFirst(entityForm.type); // edition entity should be on the bottom of the list - if (entityType === 'Edition' && !_.isEmpty(entityForm.publishers)) { - entityForm.publishers = entityForm.publishers.map((id) => bbidMap[id] ?? id); + if (entityType === 'Edition') { + if (!_.isEmpty(entityForm.publishers)) { + entityForm.publishers = entityForm.publishers.map((id) => bbidMap[id] ?? id); + } + if (entityForm.editionGroupBbid) { + entityForm.editionGroupBbid = bbidMap[entityForm.editionGroupBbid] ?? entityForm.editionGroupBbid; + } } allRelationships[entityKey] = entityForm.relationships; const newEntity = await new Entity({type: entityType}).save(null, {transacting}); diff --git a/test/src/server/routes/unifiedform.js b/test/src/server/routes/unifiedform.js index 3489a585a7..9904524a4b 100644 --- a/test/src/server/routes/unifiedform.js +++ b/test/src/server/routes/unifiedform.js @@ -1,5 +1,5 @@ -import {baseState, createEditor, createPublisher, - createWork, getRandomUUID, languageAttribs, truncateEntities} from '../../../test-helpers/create-entities'; +import {baseState, createEditionGroup, createEditor, + createPublisher, createWork, getRandomUUID, languageAttribs, truncateEntities} from '../../../test-helpers/create-entities'; import app from '../../../../src/server/app'; import chai from 'chai'; import chaiHttp from 'chai-http'; @@ -25,12 +25,13 @@ describe('Unified form routes', () => { let newRelationshipType; const wBBID = getRandomUUID(); const pBBID = getRandomUUID(); - + const egBBID = getRandomUUID(); before(async () => { try { await createEditor(123456); await createWork(wBBID); await createPublisher(pBBID); + await createEditionGroup(egBBID); newLanguage = await new Language({...languageAttribs}) .save(null, {method: 'insert'}); newRelationshipType = await new RelationshipType(relationshipTypeData) @@ -199,7 +200,43 @@ describe('Unified form routes', () => { expect(res).to.be.ok; expect(res).to.have.status(200); }); - + it('should not throw error while linking existing edition-group to edition', async () => { + const postData = {b0: { + ...baseState, + editionSection: { + editionGroup: { + id: egBBID + } + }, + type: 'Edition' + }}; + postData.b0.nameSection.language = newLanguage.id; + const res = await agent.post('/create/handler').send(postData); + expect(res).to.be.ok; + expect(res).to.have.status(200); + }); + it('should not throw error while linking new edition-group to edition', async () => { + const postData = {b0: { + ...baseState, + editionGroupSection: { + type: null + }, + type: 'EditionGroup' + }, + b1: { + ...baseState, + editionSection: { + editionGroup: { + id: 'b0' + } + }, + type: 'Edition' + }}; + postData.b0.nameSection.language = newLanguage.id; + const res = await agent.post('/create/handler').send(postData); + expect(res).to.be.ok; + expect(res).to.have.status(200); + }); it('should throw bad request error while posting invalid form', async () => { const postData = {b0: { ...baseState, From 639703916ec149ffc8abc1c86ece371c0d600d00 Mon Sep 17 00:00:00 2001 From: tri10 Date: Thu, 16 Jun 2022 17:27:18 +0530 Subject: [PATCH 031/258] feat(uf): auto fill sort name and language field --- src/client/entity-editor/name-section/actions.ts | 14 ++++++++++++++ .../entity-editor/submission-section/actions.ts | 6 +++--- .../common/search-entity-create-select.tsx | 3 ++- src/client/unified-form/helpers.ts | 1 + 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/client/entity-editor/name-section/actions.ts b/src/client/entity-editor/name-section/actions.ts index cd74ab4d8a..bf4923c502 100644 --- a/src/client/entity-editor/name-section/actions.ts +++ b/src/client/entity-editor/name-section/actions.ts @@ -49,6 +49,20 @@ export function updateNameField(newName: string): Action { }; } +/** + * Produces an action indicating that the sort name for the entity being edited + * should be updated with the provided value. + * + * @param {string} newSortName - The new value to be used for the sort name. + * @returns {Action} The resulting UPDATE_SORT_NAME_FIELD action. + */ +export function updateSortNameField(newSortName: string): Action { + return { + payload: newSortName, + type: UPDATE_SORT_NAME_FIELD + }; +} + /** * Produces an action indicating that the name for the entity being edited * should be updated with the provided value. The action is marked to be diff --git a/src/client/entity-editor/submission-section/actions.ts b/src/client/entity-editor/submission-section/actions.ts index 7d8fb95afa..2260d2cfde 100644 --- a/src/client/entity-editor/submission-section/actions.ts +++ b/src/client/entity-editor/submission-section/actions.ts @@ -167,12 +167,12 @@ function postUFSubmission(url: string, data: Map): Promise { const jsonData = data.toJS(); const postData = transformFormData(jsonData); return request.post(url).send(postData) - .then((response: Response) => { + .then((response) => { if (!response.body) { window.location.replace('/login'); } - - const redirectUrl = '/'; + const editionEntity = response.body.find((entity) => entity.type === 'Edition'); + const redirectUrl = `/edition/${editionEntity.bbid}`; if (response.body.alert) { const alertParam = `?alert=${response.body.alert}`; window.location.href = `${redirectUrl}${alertParam}`; diff --git a/src/client/unified-form/common/search-entity-create-select.tsx b/src/client/unified-form/common/search-entity-create-select.tsx index 55f2d7d8a4..8a23ec7b4c 100644 --- a/src/client/unified-form/common/search-entity-create-select.tsx +++ b/src/client/unified-form/common/search-entity-create-select.tsx @@ -1,5 +1,6 @@ import {SearchEntityCreateDispatchProps, SearchEntityCreateProps} from '../interface/type'; import {dumpEdition, loadEdition} from '../action'; +import {updateNameField, updateSortNameField} from '../../entity-editor/name-section/actions'; import AsyncCreatable from 'react-select/async-creatable'; import BaseEntitySearch from '../../entity-editor/common/entity-search-field-option'; import CreateEntityModal from './create-entity-modal'; @@ -9,7 +10,6 @@ import {addPublisher} from '../cover-tab/action'; import {addWork} from '../content-tab/action'; import {connect} from 'react-redux'; import makeImmutable from '../../entity-editor/common/make-immutable'; -import {updateNameField} from '../../entity-editor/name-section/actions'; const ImmutableCreatableAsync = makeImmutable(AsyncCreatable); @@ -72,6 +72,7 @@ function mapDispatchToProps(dispatch, {type}):SearchEntityCreateDispatchProps { onModalOpen: (name) => { dispatch(dumpEdition()); dispatch(updateNameField(name)); + dispatch(updateSortNameField(name)); }, onSubmitEntity: () => dispatch(addEntityAction[type]()) }; diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index 0a0465a6cd..420faa74f5 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -113,6 +113,7 @@ function crossSliceReducer(state, action) { editionSection: state.get('editionSection') }; intermediateState = intermediateState.merge(initialState); + intermediateState = intermediateState.setIn(['nameSection', 'language'], activeEntityState.nameSection.get('language', null)); break; case ADD_EDITION_GROUP: action.payload.value = action.payload.value ?? { From 12a83812411b4d1d729ab159e35de8a437efa983 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 18 Jun 2022 16:37:37 +0530 Subject: [PATCH 032/258] fix(uf): dropdown not going outside of accordion --- .../annotation-section/annotation-section.js | 5 ++--- .../entity-editor/common/language-field.tsx | 1 + .../work-section/work-section.tsx | 19 +++++++++++++------ .../unified-form/common/entity-modal-body.tsx | 9 +++++++-- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/client/entity-editor/annotation-section/annotation-section.js b/src/client/entity-editor/annotation-section/annotation-section.js index b9208009fa..7ad9ac63e7 100644 --- a/src/client/entity-editor/annotation-section/annotation-section.js +++ b/src/client/entity-editor/annotation-section/annotation-section.js @@ -59,11 +59,10 @@ function AnnotationSection({ if (isUf) { colSpan.offset = 0; } + const heading =

Annotation

; return (
-

- Annotation -

+ {!isUf && heading} diff --git a/src/client/entity-editor/common/language-field.tsx b/src/client/entity-editor/common/language-field.tsx index 65b4115bed..2429caad0a 100644 --- a/src/client/entity-editor/common/language-field.tsx +++ b/src/client/entity-editor/common/language-field.tsx @@ -73,6 +73,7 @@ function LanguageField({ newOptions.sort(sortLang); return newOptions; }; + // FIXME: Upgrade to new react-select version return ( diff --git a/src/client/entity-editor/work-section/work-section.tsx b/src/client/entity-editor/work-section/work-section.tsx index 3baef73a63..77742d1646 100644 --- a/src/client/entity-editor/work-section/work-section.tsx +++ b/src/client/entity-editor/work-section/work-section.tsx @@ -55,7 +55,9 @@ type DisplayLanguageOption = { }; type OwnProps = { + isUf?: boolean, languageOptions: Array, + menuPortalTarget?: HTMLElement, workTypes: Array }; @@ -89,6 +91,8 @@ function WorkSection({ languageValues, typeValue, workTypes, + menuPortalTarget, + isUf, onLanguagesChange, onTypeChange }: Props) { @@ -103,18 +107,15 @@ function WorkSection({ value: type.id })); const typeOption = workTypesForDisplay.filter((el) => el.value === typeValue); - const tooltip = ( Literary form or structure of the work ); - + const heading =

What else do you know about the Work?

; return (
-

- What else do you know about the Work? -

+ {!isUf && heading}

All fields optional — leave something blank if you don’t know it @@ -132,9 +133,12 @@ function WorkSection({ - + - + -

+
- + ):Record { if (data.ISBN.type) { data.identifierEditor.m0 = data.ISBN; } - // TO-DO will need to modify once we have multiple publisher + // TODO: will need to modify once we have multiple publisher data.editionSection.publisher = _.get(data, ['Publishers', 'p0']) ?? data.editionSection.publisher; - data.editionSection.editionGroup = _.get(data, ['EditionGroups', 'eg0']) ?? data.editionSection.editionGroup; data.relationshipSection.relationships = _.mapValues(data.Works, (work, key) => { const relationship = { attributeSetId: null, diff --git a/src/client/unified-form/common/search-entity-create-select.tsx b/src/client/unified-form/common/search-entity-create-select.tsx index 8a23ec7b4c..a9e4323de3 100644 --- a/src/client/unified-form/common/search-entity-create-select.tsx +++ b/src/client/unified-form/common/search-entity-create-select.tsx @@ -55,6 +55,7 @@ function SearchEntityCreate(props:SearchEntityCreateProps):JSX.Element { return ( <> Date: Sat, 18 Jun 2022 21:22:35 +0530 Subject: [PATCH 035/258] test(uf-routes): make tests more strict --- src/common/helpers/utils.ts | 30 ++++++++++ test/src/server/routes/unifiedform.js | 83 ++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/common/helpers/utils.ts b/src/common/helpers/utils.ts index 2b843532f0..695239825d 100644 --- a/src/common/helpers/utils.ts +++ b/src/common/helpers/utils.ts @@ -269,3 +269,33 @@ export function isbn13To10(isbn13:string):string | null { return digits.join(''); } + +/** + * + * @param {Object} orm - orm + * @param {string} bbid - bookbrainz id + * @param {Array} otherRelations - entity specific relations to fetch + * @returns {Promise} - Promise resolves to entity data if exist else null + */ +export async function getEntityByBBID(orm, bbid:string, otherRelations:Array = []):Promise | null> { + if (!isValidBBID(bbid)) { + return null; + } + const {Entity} = orm; + const entity = await new Entity({bbid}).fetch({require: false}); + if (!entity) { + return null; + } + const entityType = entity.get('type'); + const baseRelations = [ + 'annotation', + 'disambiguation', + 'defaultAlias', + 'relationshipSet.relationships.type', + 'aliasSet.aliases', + 'identifierSet.identifiers', + ...otherRelations + ]; + const entityData = await orm.func.entity.getEntity(orm, entityType, bbid, baseRelations); + return entityData; +} diff --git a/test/src/server/routes/unifiedform.js b/test/src/server/routes/unifiedform.js index 9904524a4b..e0ad37b1f8 100644 --- a/test/src/server/routes/unifiedform.js +++ b/test/src/server/routes/unifiedform.js @@ -1,9 +1,10 @@ import {baseState, createEditionGroup, createEditor, createPublisher, createWork, getRandomUUID, languageAttribs, truncateEntities} from '../../../test-helpers/create-entities'; +import {every, forOwn, map} from 'lodash'; import app from '../../../../src/server/app'; import chai from 'chai'; import chaiHttp from 'chai-http'; -import {forOwn} from 'lodash'; +import {getEntityByBBID} from '../../../../src/common/helpers/utils'; import orm from '../../../bookbrainz-data'; @@ -19,6 +20,20 @@ const relationshipTypeData = { chai.use(chaiHttp); const {expect} = chai; +function areKeysEqual(fromObj, toObj) { + return every(map(fromObj, (value, key) => toObj[key] === value)); +} +function testDefaultAlias(entity, languageId) { + const expectedDefaultAlias = { + languageId, + name: baseState.nameSection.name, + primary: true, + sortName: baseState.nameSection.sortName + }; + const {defaultAlias: actualDefaultAlias} = entity; + return areKeysEqual(expectedDefaultAlias, actualDefaultAlias); +} + describe('Unified form routes', () => { let agent; let newLanguage; @@ -53,6 +68,12 @@ describe('Unified form routes', () => { }}; postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); + const createdEntities = res.body; + expect(createdEntities.length).equal(1); + const editionEntity = createdEntities[0]; + const fetchedEditionEntity = await getEntityByBBID(orm, editionEntity.bbid); + expect(Boolean(fetchedEditionEntity)).to.be.true; + expect(testDefaultAlias(fetchedEditionEntity, newLanguage.id)).to.be.true; expect(res).to.be.ok; expect(res).to.have.status(200); }); @@ -75,6 +96,13 @@ describe('Unified form routes', () => { value.nameSection.language = newLanguage.id; }); const res = await agent.post('/create/handler').send(postData); + const createdEntities = res.body; + expect(createdEntities.length).equal(2); + const conditions = await map(createdEntities, async (entity) => { + const fetchedEntity = await getEntityByBBID(orm, entity.bbid); + return !fetchedEntity ? false : testDefaultAlias(fetchedEntity, newLanguage.id); + }); + expect(every(conditions)).to.be.true; expect(res).to.be.ok; expect(res).to.have.status(200); }); @@ -107,6 +135,14 @@ describe('Unified form routes', () => { value.nameSection.language = newLanguage.id; }); const res = await agent.post('/create/handler').send(postData); + const createdEntities = res.body; + expect(createdEntities.length).equal(1); + const editionEntity = createdEntities.find((entity) => entity.type === 'Edition'); + const fetchedEditionEntity = await getEntityByBBID(orm, editionEntity.bbid); + expect(Boolean(fetchedEditionEntity)).to.be.true; + const relationship = fetchedEditionEntity.relationshipSet.relationships[0]; + expect(relationship.sourceBbid).equal(fetchedEditionEntity.bbid); + expect(relationship.targetBbid).equal(wBBID); expect(res).to.be.ok; expect(res).to.have.status(200); }); @@ -147,6 +183,17 @@ describe('Unified form routes', () => { value.nameSection.language = newLanguage.id; }); const res = await agent.post('/create/handler').send(postData); + const createdEntities = res.body; + expect(createdEntities.length).equal(2); + const editionEntity = createdEntities.find((entity) => entity.type === 'Edition'); + const workEntity = createdEntities.find((entity) => entity.type === 'Work'); + const fetchedEditionEntity = await getEntityByBBID(orm, editionEntity.bbid); + expect(Boolean(fetchedEditionEntity)).to.be.true; + const fetchedWorkEntity = await getEntityByBBID(orm, workEntity.bbid); + expect(Boolean(fetchedWorkEntity)).to.be.true; + const relationship = fetchedEditionEntity.relationshipSet.relationships[0]; + expect(relationship.sourceBbid).equal(fetchedEditionEntity.bbid); + expect(relationship.targetBbid).equal(fetchedWorkEntity.bbid); expect(res).to.be.ok; expect(res).to.have.status(200); }); @@ -163,6 +210,13 @@ describe('Unified form routes', () => { }}; postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); + const createdEntities = res.body; + expect(createdEntities.length).equal(1); + const editionEntity = createdEntities.find((entity) => entity.type === 'Edition'); + const fetchedEditionEntity = await getEntityByBBID(orm, editionEntity.bbid, ['publisherSet.publishers']); + expect(Boolean(fetchedEditionEntity)).to.be.true; + const publisherId = fetchedEditionEntity.publisherSet.publishers[0].bbid; + expect(publisherId).equal(pBBID); expect(res).to.be.ok; expect(res).to.have.status(200); }); @@ -197,6 +251,16 @@ describe('Unified form routes', () => { }}; postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); + const createdEntities = res.body; + expect(createdEntities.length).equal(2); + const editionEntity = createdEntities.find((entity) => entity.type === 'Edition'); + const publisherEntity = createdEntities.find((entity) => entity.type === 'Publisher'); + const fetchedEditionEntity = await getEntityByBBID(orm, editionEntity.bbid, ['publisherSet.publishers']); + expect(Boolean(fetchedEditionEntity)).to.be.true; + const fetchedPublisherEntity = await getEntityByBBID(orm, publisherEntity.bbid); + expect(Boolean(fetchedPublisherEntity)).to.be.true; + const publisherId = fetchedEditionEntity.publisherSet.publishers[0].bbid; + expect(publisherId).equal(publisherEntity.bbid); expect(res).to.be.ok; expect(res).to.have.status(200); }); @@ -212,6 +276,13 @@ describe('Unified form routes', () => { }}; postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); + const createdEntities = res.body; + expect(createdEntities.length).equal(1); + const editionEntity = createdEntities.find((entity) => entity.type === 'Edition'); + const fetchedEditionEntity = await getEntityByBBID(orm, editionEntity.bbid, ['editionGroup']); + expect(Boolean(fetchedEditionEntity)).to.be.true; + const editionGroupBbid = fetchedEditionEntity.editionGroup.bbid; + expect(editionGroupBbid).equal(egBBID); expect(res).to.be.ok; expect(res).to.have.status(200); }); @@ -234,6 +305,16 @@ describe('Unified form routes', () => { }}; postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); + const createdEntities = res.body; + expect(createdEntities.length).equal(2); + const editionEntity = createdEntities.find((entity) => entity.type === 'Edition'); + const editionGroupEntity = createdEntities.find((entity) => entity.type === 'EditionGroup'); + const fetchedEditionEntity = await getEntityByBBID(orm, editionEntity.bbid, ['editionGroup']); + expect(Boolean(fetchedEditionEntity)).to.be.true; + const fetchedEditionGroupEntity = await getEntityByBBID(orm, editionGroupEntity.bbid); + expect(Boolean(fetchedEditionGroupEntity)).to.be.true; + const linkedEditionGroupBbid = fetchedEditionEntity.editionGroup.bbid; + expect(linkedEditionGroupBbid).equal(fetchedEditionGroupEntity.bbid); expect(res).to.be.ok; expect(res).to.have.status(200); }); From dccade2c71c63277f1e614286d182aa49bcd91ba Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Wed, 22 Jun 2022 15:34:09 +0200 Subject: [PATCH 036/258] fix: Fix missing export after merge conflict --- src/server/routes/entity/edition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/routes/entity/edition.ts b/src/server/routes/entity/edition.ts index fe10b94e7f..32079c451b 100644 --- a/src/server/routes/entity/edition.ts +++ b/src/server/routes/entity/edition.ts @@ -52,7 +52,7 @@ type PassportRequest = express.Request & { user: any, session: any }; -function transformNewForm(data) { +export function transformNewForm(data) { const aliases = entityRoutes.constructAliases( data.aliasEditor, data.nameSection ); From 12b852465b73ba57ce50b0f7bd3555050b2d8900 Mon Sep 17 00:00:00 2001 From: tri10 Date: Thu, 23 Jun 2022 20:52:12 +0530 Subject: [PATCH 037/258] fix(test): include AC, multiple publishers in test --- src/server/routes/entity/process-unified-form.ts | 9 +++++++-- test/src/server/routes/unifiedform.js | 8 ++++++-- test/test-helpers/create-entities.js | 1 + 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/server/routes/entity/process-unified-form.ts b/src/server/routes/entity/process-unified-form.ts index 4775e82393..5ded8395e5 100644 --- a/src/server/routes/entity/process-unified-form.ts +++ b/src/server/routes/entity/process-unified-form.ts @@ -63,7 +63,7 @@ export function transformForm(body:Record):Record { for (const keyIndex in body) { if (Object.prototype.hasOwnProperty.call(body, keyIndex)) { const currentForm = body[keyIndex]; - const transformedForm = transformFunctions[_.lowerFirst(currentForm.type)](currentForm); + const transformedForm = transformFunctions[_.camelCase(currentForm.type)](currentForm); modifiedForm[keyIndex] = {type: currentForm.type, ...transformedForm}; } } @@ -169,6 +169,11 @@ export function handleCreateMultipleEntities( if (entityForm.editionGroupBbid) { entityForm.editionGroupBbid = bbidMap[entityForm.editionGroupBbid] ?? entityForm.editionGroupBbid; } + if (!_.isNil(entityForm.authorCredit)) { + entityForm.authorCredit = entityForm.authorCredit.map( + (credit) => ({...credit, authorBBID: bbidMap[credit.authorBBID] ?? credit.authorBBID}) + ); + } } allRelationships[entityKey] = entityForm.relationships; const newEntity = await new Entity({type: entityType}).save(null, {transacting}); @@ -179,7 +184,7 @@ export function handleCreateMultipleEntities( authorId: editorJSON.id, isMerge: false }).save(null, {transacting}); - const additionalProps = _.pick(entityForm, additionalEntityProps[_.snakeCase(entityType)]); + const additionalProps = _.pick(entityForm, additionalEntityProps[_.camelCase(entityType)]); const changedProps = await getChangedProps( orm, transacting, true, currentEntity, entityForm, entityType, newRevision, additionalProps diff --git a/test/src/server/routes/unifiedform.js b/test/src/server/routes/unifiedform.js index e0ad37b1f8..1b811cb2a4 100644 --- a/test/src/server/routes/unifiedform.js +++ b/test/src/server/routes/unifiedform.js @@ -203,7 +203,9 @@ describe('Unified form routes', () => { ...baseState, editionSection: { publisher: { - id: pBBID + 0: { + id: pBBID + } } }, type: 'Edition' @@ -244,7 +246,9 @@ describe('Unified form routes', () => { ...baseState, editionSection: { publisher: { - id: 'b0' + 0: { + id: 'b0' + } } }, type: 'Edition' diff --git a/test/test-helpers/create-entities.js b/test/test-helpers/create-entities.js index ff8dbce331..4175e50029 100644 --- a/test/test-helpers/create-entities.js +++ b/test/test-helpers/create-entities.js @@ -51,6 +51,7 @@ export const baseState = { annotationSection: { content: '' }, + authorCreditEditor: {}, identifierEditor: {}, nameSection: { disambiguation: '', From b8eab3e46ca76b443e28b25d05a5b61548919221 Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 24 Jun 2022 00:58:42 +0530 Subject: [PATCH 038/258] feat(uf): add multi-publisher support in uf --- .../entity-editor/submission-section/actions.ts | 3 +-- src/client/unified-form/cover-tab/cover-tab.tsx | 14 ++++++++------ src/client/unified-form/helpers.ts | 2 ++ src/client/unified-form/interface/type.ts | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/client/entity-editor/submission-section/actions.ts b/src/client/entity-editor/submission-section/actions.ts index 6e18e450c5..0551cb7ddd 100644 --- a/src/client/entity-editor/submission-section/actions.ts +++ b/src/client/entity-editor/submission-section/actions.ts @@ -138,8 +138,7 @@ function transformFormData(data:Record):Record { if (data.ISBN.type) { data.identifierEditor.m0 = data.ISBN; } - // TODO: will need to modify once we have multiple publisher - data.editionSection.publisher = _.get(data, ['Publishers', 'p0']) ?? data.editionSection.publisher; + data.editionSection.publisher = _.merge(_.get(data, ['Publishers'], {}), _.get(data.editionSection, ['publisher'], {})); data.relationshipSection.relationships = _.mapValues(data.Works, (work, key) => { const relationship = { attributeSetId: null, diff --git a/src/client/unified-form/cover-tab/cover-tab.tsx b/src/client/unified-form/cover-tab/cover-tab.tsx index fc22d4345f..d6537a37f6 100644 --- a/src/client/unified-form/cover-tab/cover-tab.tsx +++ b/src/client/unified-form/cover-tab/cover-tab.tsx @@ -1,23 +1,26 @@ import {Col, Row} from 'react-bootstrap'; +import {CoverProps, EntitySelect} from '../interface/type'; import ButtonBar from '../../entity-editor/button-bar/button-bar'; -import {CoverProps} from '../interface/type'; import ISBNField from './isbn-field'; import IdentifierEditor from '../../entity-editor/identifier-editor/identifier-editor'; import NameSection from '../../entity-editor/name-section/name-section'; import React from 'react'; import SearchEntityCreate from '../common/search-entity-create-select'; import {connect} from 'react-redux'; +import {convertMapToObject} from '../../helpers/utils'; import {updatePublisher} from '../../entity-editor/edition-section/actions'; export function CoverTab(props:CoverProps) { - const {publisherValue, onPublisherChange, identifierEditorVisible} = props; + const {publisherValue: publishers, onPublisherChange, identifierEditorVisible} = props; + const publisherValue:EntitySelect[] = Object.values(convertMapToObject(publishers ?? {})); return (
dispatch(updatePublisher(value)) + onPublisherChange: (value) => dispatch(updatePublisher(Object.fromEntries(value.map((pub, index) => [index, pub])))) }; } diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index 605efe2ff4..e488a69309 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -6,6 +6,7 @@ import {ADD_WORK} from './content-tab/action'; import Immutable from 'immutable'; import aliasEditorReducer from '../entity-editor/alias-editor/reducer'; import annotationSectionReducer from '../entity-editor/annotation-section/reducer'; +import authorCreditEditorReducer from '../entity-editor/author-credit-editor/reducer'; import buttonBarReducer from '../entity-editor/button-bar/reducer'; import {combineReducers} from 'redux-immutable'; import editionGroupSectionReducer from '../entity-editor/edition-group-section/reducer'; @@ -171,6 +172,7 @@ export function createRootReducer() { Works: worksReducer, aliasEditor: aliasEditorReducer, annotationSection: annotationSectionReducer, + authorCreditEditor: authorCreditEditorReducer, buttonBar: buttonBarReducer, editionGroupSection: editionGroupSectionReducer, editionSection: editionSectionReducer, diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index b152178ebf..6ae4b10b94 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -61,7 +61,7 @@ export type ISBNDispatchProps = { }; export type ISBNProps = ISBNStateProps & ISBNDispatchProps; -type EntitySelect = { +export type EntitySelect = { text:string, id:string }; From 355475c98e96029220f8b105723e804f2e08576d Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 24 Jun 2022 17:52:37 +0530 Subject: [PATCH 039/258] feat(uf): add author credits ui --- .../author-credit-section.tsx | 21 +++++++++++--- .../author-section/author-section.tsx | 29 ++++++++++--------- .../unified-form/cover-tab/cover-tab.tsx | 2 ++ 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx index 3d4c531919..a19d23d952 100644 --- a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx +++ b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx @@ -37,6 +37,7 @@ import EntitySearchFieldOption from '../common/entity-search-field-option'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import PropTypes from 'prop-types'; import React from 'react'; +import SearchEntityCreate from '../../unified-form/common/search-entity-create-select'; import ValidationLabel from '../common/validation-label'; import {connect} from 'react-redux'; import {convertMapToObject} from '../../helpers/utils'; @@ -44,6 +45,7 @@ import {validateAuthorCreditSection} from '../validators/common'; type OwnProps = { + isUf?: boolean; }; type StateProps = { @@ -61,7 +63,7 @@ type DispatchProps = { type Props = OwnProps & StateProps & DispatchProps; function AuthorCreditSection({ - authorCreditEditor, onEditAuthorCredit, onEditorClose, showEditor, onAuthorChange, isEditable + authorCreditEditor, onEditAuthorCredit, onEditorClose, showEditor, onAuthorChange, isEditable, isUf, ...rest }: Props) { let editor; if (showEditor) { @@ -75,7 +77,7 @@ function AuthorCreditSection({ const authorCreditPreview = _map(authorCreditEditor, (credit) => `${credit.name}${credit.joinPhrase}`).join(''); const authorCreditRows = _values(authorCreditEditor); - const isValid = validateAuthorCreditSection(authorCreditRows); + const isValid = validateAuthorCreditSection(authorCreditRows.join, isUf); const editButton = ( // eslint-disable-next-line react/jsx-no-bind @@ -102,10 +104,15 @@ function AuthorCreditSection({ Name(s) of the Author(s) as they appear on the book cover ); + let resCol:any = {md: {offset: 3, span: 6}}; + if (isUf) { + resCol = {lg: {offset: 0, span: 6}}; + } + const SelectWrapper = !isUf ? EntitySearchFieldOption : SearchEntityCreate; return ( {editor} - + {label} @@ -118,14 +125,17 @@ function AuthorCreditSection({
-
{editButton} @@ -143,6 +153,9 @@ AuthorCreditSection.propTypes = { showEditor: PropTypes.bool.isRequired }; +AuthorCreditSection.defaultProps = { + isUf: false +}; function mapStateToProps(rootState): StateProps { const firstRowKey = rootState.get('authorCreditEditor').keySeq().first(); const authorCreditRow = rootState.getIn(['authorCreditEditor', firstRowKey]); diff --git a/src/client/entity-editor/author-section/author-section.tsx b/src/client/entity-editor/author-section/author-section.tsx index 24a8703b3f..28f9bd5603 100644 --- a/src/client/entity-editor/author-section/author-section.tsx +++ b/src/client/entity-editor/author-section/author-section.tsx @@ -91,7 +91,8 @@ type DispatchProps = { type OwnProps = { authorTypes: Array, - genderOptions: Array + genderOptions: Array, + isUf?: boolean }; export type Props = StateProps & DispatchProps & OwnProps; @@ -149,6 +150,7 @@ function AuthorSection({ genderShow, genderValue, typeValue, + isUf, onBeginAreaChange, onBeginDateChange, onEndAreaChange, @@ -171,19 +173,20 @@ function AuthorSection({ const {isValid: isValidDob, errorMessage: dobError} = validateAuthorSectionBeginDate(beginDateValue); const {isValid: isValidDod, errorMessage: dodError} = validateAuthorSectionEndDate(beginDateValue, endDateValue, currentAuthorType.label); - - + const heading =

What else do you know about the Author?

; + const lgCol = {offset: 3, span: 6}; + if (isUf) { + lgCol.offset = 0; + } return (
-

- What else do you know about the Author? -

+ {!isUf && heading}

All fields optional — leave something blank if you don’t know it

- + Type - + - + -
+
- + - + + Date: Fri, 24 Jun 2022 17:59:13 +0530 Subject: [PATCH 040/258] feat(uf): add author create modal --- .../edition-section/edition-section.tsx | 2 +- .../submission-section/actions.ts | 6 +++++ .../submission-section/submission-section.js | 4 +-- src/client/entity-editor/validators/common.ts | 13 ++++----- .../entity-editor/validators/edition.ts | 10 +++---- .../common/search-entity-create-select.tsx | 10 ++++--- src/client/unified-form/cover-tab/action.ts | 9 +++++++ src/client/unified-form/cover-tab/reducer.ts | 11 +++++++- src/client/unified-form/helpers.ts | 27 +++++++++++++++++-- src/client/unified-form/interface/type.ts | 3 ++- src/client/unified-form/unified-form.tsx | 6 ++--- src/server/routes/unifiedform.ts | 4 ++- 12 files changed, 78 insertions(+), 27 deletions(-) diff --git a/src/client/entity-editor/edition-section/edition-section.tsx b/src/client/entity-editor/edition-section/edition-section.tsx index dc4aa1757c..d47c9270cb 100644 --- a/src/client/entity-editor/edition-section/edition-section.tsx +++ b/src/client/entity-editor/edition-section/edition-section.tsx @@ -282,7 +282,7 @@ function EditionSection({ return (
{headingTag} - + {!isUf && }

Edition Group is required — this cannot be blank. You can search for and choose an existing Edition Group, or choose to automatically create one instead. diff --git a/src/client/entity-editor/submission-section/actions.ts b/src/client/entity-editor/submission-section/actions.ts index 0551cb7ddd..e561509122 100644 --- a/src/client/entity-editor/submission-section/actions.ts +++ b/src/client/entity-editor/submission-section/actions.ts @@ -122,6 +122,12 @@ function transformFormData(data:Record):Record { newData[pid] = publisher; } }); + // add new authors + _.forEach(data.Authors, (author, aid) => { + if (author.__isNew__) { + newData[aid] = author; + } + }); // add new works _.forEach(data.Works, (work, wid) => { if (work.__isNew__) { diff --git a/src/client/entity-editor/submission-section/submission-section.js b/src/client/entity-editor/submission-section/submission-section.js index 8d9bc618c6..1e86fb4706 100644 --- a/src/client/entity-editor/submission-section/submission-section.js +++ b/src/client/entity-editor/submission-section/submission-section.js @@ -117,11 +117,11 @@ SubmissionSection.propTypes = { submitted: PropTypes.bool.isRequired }; -function mapStateToProps(rootState, {validate, identifierTypes, isMerge}) { +function mapStateToProps(rootState, {validate, identifierTypes, isMerge, isUf}) { const state = rootState.get('submissionSection'); return { errorText: state.get('submitError'), - formValid: validate && validate(rootState, identifierTypes, isMerge), + formValid: validate && validate(rootState, identifierTypes, isMerge, isUf), note: state.get('note'), submitted: state.get('submitted') }; diff --git a/src/client/entity-editor/validators/common.ts b/src/client/entity-editor/validators/common.ts index 85644116f4..99b841e418 100644 --- a/src/client/entity-editor/validators/common.ts +++ b/src/client/entity-editor/validators/common.ts @@ -25,16 +25,17 @@ import { validateUUID } from './base'; +import {AuthorCredit} from '../author-credit-editor/actions'; import {Iterable} from 'immutable'; import _ from 'lodash'; -import {AuthorCredit} from '../author-credit-editor/actions'; export function validateMultiple( values: any[], validationFunction: (value: any, ...rest: any[]) => boolean, additionalArgs?: any, - requiresOneOrMore?: boolean + requiresOneOrMore?: boolean, + isCustom = false ): boolean { if (requiresOneOrMore && _.isEmpty(values)) { return false; @@ -48,7 +49,7 @@ export function validateMultiple( } return every(values, (value) => - validationFunction(value, additionalArgs)); + validationFunction(value, isCustom, additionalArgs)); } export function validateAliasName(value: any): boolean { @@ -184,8 +185,8 @@ export function validateSubmissionSection( ); } -export function validateAuthorCreditRow(row: any): boolean { - return validateUUID(getIn(row, ['author', 'id'], null), true) && +export function validateAuthorCreditRow(row: any, isCustom = false): boolean { + return (isCustom ? Boolean(getIn(row, ['author', 'id'], null)) : validateUUID(getIn(row, ['author', 'id'], null), true)) && validateRequiredString(get(row, 'name', null)) && validateOptionalString(get(row, 'joinPhrase', null)); } @@ -193,7 +194,7 @@ export function validateAuthorCreditRow(row: any): boolean { export const validateAuthorCreditSection = _.partialRight( // Requires at least one Author Credit row validateMultiple, _.partialRight.placeholder, - validateAuthorCreditRow, null, false + validateAuthorCreditRow, null, false, _.partialRight.placeholder ); // In the merge editor we use the authorCredit directly instead of the authorCreditEditor state export function validateAuthorCreditSectionMerge(authorCredit:AuthorCredit) :boolean { diff --git a/src/client/entity-editor/validators/edition.ts b/src/client/entity-editor/validators/edition.ts index 421aaafe3e..60f52cc9d6 100644 --- a/src/client/entity-editor/validators/edition.ts +++ b/src/client/entity-editor/validators/edition.ts @@ -78,17 +78,15 @@ export function validateEditionSectionPublisher(value: any): boolean { return true; } const publishers = convertMapToObject(value); - let flag = false; for (const pubId in publishers) { if (Object.prototype.hasOwnProperty.call(publishers, pubId)) { const publisher = publishers[pubId]; if (!validateUUID(get(publisher, 'id', null), true)) { return false; } - flag = true; } } - return flag; + return true; } export function validateEditionSectionReleaseDate(value: any) { @@ -129,14 +127,15 @@ export function validateEditionSection(data: any): boolean { export function validateForm( formData: any, identifierTypes?: Array<_IdentifierType> | null | undefined, - isMerge?:boolean + isMerge?:boolean, + isCustom?:boolean ): boolean { let validAuthorCredit; if (isMerge) { validAuthorCredit = validateAuthorCreditSectionMerge(get(formData, 'authorCredit', {})); } else { - validAuthorCredit = validateAuthorCreditSection(get(formData, 'authorCreditEditor', {})); + validAuthorCredit = validateAuthorCreditSection(get(formData, 'authorCreditEditor', {}), isCustom); } const conditions = [ validateAliases(get(formData, 'aliasEditor', {})), @@ -148,6 +147,5 @@ export function validateForm( validAuthorCredit, validateSubmissionSection(get(formData, 'submissionSection', {})) ]; - return _.every(conditions); } diff --git a/src/client/unified-form/common/search-entity-create-select.tsx b/src/client/unified-form/common/search-entity-create-select.tsx index a9e4323de3..f51b9d9ef7 100644 --- a/src/client/unified-form/common/search-entity-create-select.tsx +++ b/src/client/unified-form/common/search-entity-create-select.tsx @@ -1,4 +1,5 @@ import {SearchEntityCreateDispatchProps, SearchEntityCreateProps} from '../interface/type'; +import {addAuthor, addPublisher} from '../cover-tab/action'; import {dumpEdition, loadEdition} from '../action'; import {updateNameField, updateSortNameField} from '../../entity-editor/name-section/actions'; import AsyncCreatable from 'react-select/async-creatable'; @@ -6,7 +7,6 @@ import BaseEntitySearch from '../../entity-editor/common/entity-search-field-opt import CreateEntityModal from './create-entity-modal'; import React from 'react'; import {addEditionGroup} from '../detail-tab/action'; -import {addPublisher} from '../cover-tab/action'; import {addWork} from '../content-tab/action'; import {connect} from 'react-redux'; import makeImmutable from '../../entity-editor/common/make-immutable'; @@ -19,15 +19,17 @@ const defaultProps = { error: false, filters: [], languageOptions: [], + rowId: null, tooltipText: null }; const addEntityAction = { + author: addAuthor, editionGroup: addEditionGroup, publisher: addPublisher, work: addWork }; function SearchEntityCreate(props:SearchEntityCreateProps):JSX.Element { - const {type, nextId, onModalOpen, onModalClose, onSubmitEntity, ...rest} = props; + const {type, nextId, onModalOpen, onModalClose, onSubmitEntity, rowId, ...rest} = props; const createLabel = React.useCallback((input) => `Create ${type} "${input}"`, [type]); const [showModal, setShowModal] = React.useState(false); const getNewOptionData = React.useCallback((input, label) => ({ @@ -48,7 +50,7 @@ function SearchEntityCreate(props:SearchEntityCreateProps):JSX.Element { ev.preventDefault(); ev.stopPropagation(); setShowModal(false); - onSubmitEntity(); + onSubmitEntity(rowId); onModalClose(); }, []); @@ -75,7 +77,7 @@ function mapDispatchToProps(dispatch, {type}):SearchEntityCreateDispatchProps { dispatch(updateNameField(name)); dispatch(updateSortNameField(name)); }, - onSubmitEntity: () => dispatch(addEntityAction[type]()) + onSubmitEntity: (arg) => dispatch(addEntityAction[type](arg)) }; } diff --git a/src/client/unified-form/cover-tab/action.ts b/src/client/unified-form/cover-tab/action.ts index 6c67eb2f17..e64d3a6056 100644 --- a/src/client/unified-form/cover-tab/action.ts +++ b/src/client/unified-form/cover-tab/action.ts @@ -2,11 +2,14 @@ import {Action} from '../interface/type'; export const UPDATE_ISBN_VALUE = 'UPDATE_ISBN_VALUE'; +export const ADD_AUTHOR = 'ADD_AUTHOR'; export const UPDATE_ISBN_TYPE = 'UPDATE_ISBN_TYPE'; export const ADD_PUBLISHER = 'ADD_PUBLISHER'; let nextPublisherId = 0; +let nextAuthorId = 0; + export function addPublisher(value = null):Action { return { payload: {id: `p${nextPublisherId++}`, value}, @@ -14,6 +17,12 @@ export function addPublisher(value = null):Action { }; } +export function addAuthor(rowId:string, value = null):Action { + return { + payload: {id: `a${nextAuthorId++}`, rowId, value}, + type: ADD_AUTHOR + }; +} export function debouncedUpdateISBNValue(newValue: string): Action { return { diff --git a/src/client/unified-form/cover-tab/reducer.ts b/src/client/unified-form/cover-tab/reducer.ts index ecaf3d624f..cbe2b7fd9d 100644 --- a/src/client/unified-form/cover-tab/reducer.ts +++ b/src/client/unified-form/cover-tab/reducer.ts @@ -1,4 +1,4 @@ -import {ADD_PUBLISHER, UPDATE_ISBN_TYPE, UPDATE_ISBN_VALUE} from './action'; +import {ADD_AUTHOR, ADD_PUBLISHER, UPDATE_ISBN_TYPE, UPDATE_ISBN_VALUE} from './action'; import Immutable from 'immutable'; @@ -25,3 +25,12 @@ export function publishersReducer(state = Immutable.Map({}), {type, payload}) { return state; } } + +export function authorsReducer(state = Immutable.Map({}), {type, payload}) { + switch (type) { + case ADD_AUTHOR: + return state.set(payload.id, Immutable.fromJS(payload.value)); + default: + return state; + } +} diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index e488a69309..2bfcb3e3fa 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -1,12 +1,13 @@ +import {ADD_AUTHOR, ADD_PUBLISHER} from './cover-tab/action'; import {DUMP_EDITION, LOAD_EDITION} from './action'; -import {ISBNReducer, publishersReducer} from './cover-tab/reducer'; +import {ISBNReducer, authorsReducer, publishersReducer} from './cover-tab/reducer'; import {ADD_EDITION_GROUP} from './detail-tab/action'; -import {ADD_PUBLISHER} from './cover-tab/action'; import {ADD_WORK} from './content-tab/action'; import Immutable from 'immutable'; import aliasEditorReducer from '../entity-editor/alias-editor/reducer'; import annotationSectionReducer from '../entity-editor/annotation-section/reducer'; import authorCreditEditorReducer from '../entity-editor/author-credit-editor/reducer'; +import authorSectionReducer from '../entity-editor/author-section/reducer'; import buttonBarReducer from '../entity-editor/button-bar/reducer'; import {combineReducers} from 'redux-immutable'; import editionGroupSectionReducer from '../entity-editor/edition-group-section/reducer'; @@ -108,9 +109,29 @@ function crossSliceReducer(state, action) { submissionSection: state.get('submissionSection') }; switch (type) { + case ADD_AUTHOR: + intermediateState = intermediateState.setIn(['authorCreditEditor', action.payload.rowId, 'author'], Immutable.Map({ + id: action.payload.id, + rowId: action.payload.rowId, + text: activeEntityState.nameSection.get('name'), + type: 'Author' + })); + intermediateState = intermediateState.setIn( + ['authorCreditEditor', action.payload.rowId, 'name'], activeEntityState.nameSection.get('name') + ); + action.payload.value = action.payload.value ?? { + ...activeEntityState, + __isNew__: true, + authorSection: intermediateState.get('authorSection'), + id: action.payload.id, + text: activeEntityState.nameSection.get('name'), + type: 'Author' + }; + break; case DUMP_EDITION: action.payload.value = { ...activeEntityState, + authorCreditEditor: state.get('authorCreditEditor'), editionSection: state.get('editionSection') }; intermediateState = intermediateState.merge(initialState); @@ -165,6 +186,7 @@ export function createRootReducer() { return (state: Immutable.Map, action) => { const intermediateState = crossSliceReducer(state, action); return combineReducers({ + Authors: authorsReducer, EditionGroups: editionGroupsReducer, Editions: newEditionReducer, ISBN: ISBNReducer, @@ -173,6 +195,7 @@ export function createRootReducer() { aliasEditor: aliasEditorReducer, annotationSection: annotationSectionReducer, authorCreditEditor: authorCreditEditorReducer, + authorSection: authorSectionReducer, buttonBar: buttonBarReducer, editionGroupSection: editionGroupSectionReducer, editionSection: editionSectionReducer, diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index 6ae4b10b94..9f8605b642 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -83,7 +83,7 @@ export type NavButtonsProps = { export type SearchEntityCreateDispatchProps = { onModalOpen:(arg:string)=>unknown, onModalClose:()=>unknown, - onSubmitEntity:()=>unknown + onSubmitEntity:(arg:string)=>unknown }; export type SearchEntityCreateOwnProps = { @@ -97,6 +97,7 @@ export type SearchEntityCreateOwnProps = { languageOptions?:Array, value?:Array | EntitySelect type:string, + rowId?:string, onChange:(arg)=>unknown }; diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index 2d8cf5a338..01e2599ba7 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -13,11 +13,11 @@ import {submit} from '../entity-editor/submission-section/actions'; const {Tabs, Tab} = Boostrap; function getUfValidator(validator) { - return (state, identifierTypes) => { + return (state, identifierTypes, ...args) => { if (state.get('ISBN') && !state.getIn(['ISBN', 'type']) && state.getIn(['ISBN', 'value'], '').length > 0) { return false; } - return validator(state, identifierTypes); + return validator(state, identifierTypes, ...args); }; } export function UnifiedForm(props:UnifiedFormProps) { @@ -57,7 +57,7 @@ export function UnifiedForm(props:UnifiedFormProps) { disableBack={tabKeys.indexOf(tabKey) === 0} disableNext={tabKeys.indexOf(tabKey) === tabKeys.length - 1} onBack={onBackHandler} onNext={onNextHandler} /> - + ); diff --git a/src/server/routes/unifiedform.ts b/src/server/routes/unifiedform.ts index 3e7048a235..baa893868b 100644 --- a/src/server/routes/unifiedform.ts +++ b/src/server/routes/unifiedform.ts @@ -13,7 +13,9 @@ router.get('/create', isAuthenticated, middleware.loadIdentifierTypes, middleware.loadEditionStatuses, middleware.loadEditionFormats, middleware.loadEditionGroupTypes, middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadGenders, middleware.loadPublisherTypes, middleware.loadAuthorTypes, middleware.loadRelationshipTypes, (req:PassportRequest, res:express.Response) => { - const props = generateUnifiedProps(req, res, {}); + const props = generateUnifiedProps(req, res, { + genderOptions: res.locals.genders + }); const formMarkup = unifiedFormMarkup(props); const {markup, props: updatedProps} = formMarkup; return res.send(target({ From b71361608a6e105aa7b7fc3c031d007ec9f027ad Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 24 Jun 2022 21:26:04 +0530 Subject: [PATCH 041/258] feat(uf): create new author using AC editor --- .../author-credit-editor.tsx | 4 +++- .../author-credit-row.tsx | 14 +++++++++++-- .../author-credit-section.tsx | 2 ++ src/client/unified-form/action.ts | 3 ++- .../common/search-entity-create-select.tsx | 2 +- src/client/unified-form/helpers.ts | 21 +++++++++++++++++-- 6 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/client/entity-editor/author-credit-editor/author-credit-editor.tsx b/src/client/entity-editor/author-credit-editor/author-credit-editor.tsx index e90c3fef21..5d52f5cb78 100644 --- a/src/client/entity-editor/author-credit-editor/author-credit-editor.tsx +++ b/src/client/entity-editor/author-credit-editor/author-credit-editor.tsx @@ -50,7 +50,8 @@ const AuthorCreditEditor = ({ authorCredit, onAddAuthorCreditRow, onClose, - showEditor + showEditor, + ...rest }) => ( @@ -79,6 +80,7 @@ const AuthorCreditEditor = ({ index={rowId} // eslint-disable-next-line react/no-array-index-key key={rowId} + {...rest} /> )) } diff --git a/src/client/entity-editor/author-credit-editor/author-credit-row.tsx b/src/client/entity-editor/author-credit-editor/author-credit-row.tsx index 82c59515eb..c53116ee3c 100644 --- a/src/client/entity-editor/author-credit-editor/author-credit-row.tsx +++ b/src/client/entity-editor/author-credit-editor/author-credit-row.tsx @@ -29,6 +29,7 @@ import type {Dispatch} from 'redux'; import EntitySearchFieldOption from '../common/entity-search-field-option'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import React from 'react'; +import SearchEntityCreate from '../../unified-form/common/search-entity-create-select'; import ValidationLabel from '../common/validation-label'; import {connect} from 'react-redux'; import {faTimes} from '@fortawesome/free-solid-svg-icons'; @@ -36,6 +37,7 @@ import {faTimes} from '@fortawesome/free-solid-svg-icons'; type OwnProps = { index: string, + isUf?: boolean }; type StateProps = { @@ -80,22 +82,27 @@ function AuthorCreditRow({ author, joinPhrase, name, + isUf, onAuthorChange, onJoinPhraseChange, onNameChange, - onRemoveButtonClick + onRemoveButtonClick, + ...rest }: Props) { + const SelectWrapper = !isUf ? EntitySearchFieldOption : SearchEntityCreate; return (

- @@ -135,6 +142,9 @@ function AuthorCreditRow({ ); } AuthorCreditRow.displayName = 'AuthorCreditEditor.CreditRow'; +AuthorCreditRow.defaultProps = { + isUf: false +}; function mapStateToProps(rootState, {index}: OwnProps): StateProps { const state = rootState.get('authorCreditEditor'); diff --git a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx index a19d23d952..dfcaedbdf0 100644 --- a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx +++ b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx @@ -69,8 +69,10 @@ function AuthorCreditSection({ if (showEditor) { editor = ( ); } diff --git a/src/client/unified-form/action.ts b/src/client/unified-form/action.ts index eb0a653963..5829708969 100644 --- a/src/client/unified-form/action.ts +++ b/src/client/unified-form/action.ts @@ -2,10 +2,11 @@ export const DUMP_EDITION = 'DUMP_EDITION'; export const LOAD_EDITION = 'LOAD_EDITION'; const nextEditionId = 0; -export function dumpEdition() { +export function dumpEdition(type?:string) { return { payload: { id: `e${nextEditionId}`, + type, value: null }, type: DUMP_EDITION diff --git a/src/client/unified-form/common/search-entity-create-select.tsx b/src/client/unified-form/common/search-entity-create-select.tsx index f51b9d9ef7..695d035746 100644 --- a/src/client/unified-form/common/search-entity-create-select.tsx +++ b/src/client/unified-form/common/search-entity-create-select.tsx @@ -73,7 +73,7 @@ function mapDispatchToProps(dispatch, {type}):SearchEntityCreateDispatchProps { return { onModalClose: () => dispatch(loadEdition()), onModalOpen: (name) => { - dispatch(dumpEdition()); + dispatch(dumpEdition(type)); dispatch(updateNameField(name)); dispatch(updateSortNameField(name)); }, diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index 2bfcb3e3fa..9360d77fbb 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -9,6 +9,7 @@ import annotationSectionReducer from '../entity-editor/annotation-section/reduce import authorCreditEditorReducer from '../entity-editor/author-credit-editor/reducer'; import authorSectionReducer from '../entity-editor/author-section/reducer'; import buttonBarReducer from '../entity-editor/button-bar/reducer'; +import {camelCase} from 'lodash'; import {combineReducers} from 'redux-immutable'; import editionGroupSectionReducer from '../entity-editor/edition-group-section/reducer'; import editionGroupsReducer from './detail-tab/reducer'; @@ -52,6 +53,16 @@ function newEditionReducer(state = Immutable.Map({}), action) { return state; } } +const initialACState = Immutable.fromJS( + { + n0: { + author: null, + automaticJoinPhrase: true, + joinPhrase: '', + name: '' + } + } +); const initialState = Immutable.Map({ aliasEditor: Immutable.Map({}), annotationSection: Immutable.Map({content: ''}), @@ -60,6 +71,7 @@ const initialState = Immutable.Map({ identifierEditorVisible: false }), editionSection: Immutable.Map({ + authorCreditEditorVisible: false, editionGroupVisible: true, format: null, languages: Immutable.List([]), @@ -110,14 +122,14 @@ function crossSliceReducer(state, action) { }; switch (type) { case ADD_AUTHOR: - intermediateState = intermediateState.setIn(['authorCreditEditor', action.payload.rowId, 'author'], Immutable.Map({ + intermediateState = intermediateState.setIn(['Editions', 'e0', 'authorCreditEditor', action.payload.rowId, 'author'], Immutable.Map({ id: action.payload.id, rowId: action.payload.rowId, text: activeEntityState.nameSection.get('name'), type: 'Author' })); intermediateState = intermediateState.setIn( - ['authorCreditEditor', action.payload.rowId, 'name'], activeEntityState.nameSection.get('name') + ['Editions', 'e0', 'authorCreditEditor', action.payload.rowId, 'name'], activeEntityState.nameSection.get('name') ); action.payload.value = action.payload.value ?? { ...activeEntityState, @@ -135,6 +147,11 @@ function crossSliceReducer(state, action) { editionSection: state.get('editionSection') }; intermediateState = intermediateState.merge(initialState); + intermediateState = intermediateState.setIn(['editionSection', 'authorCreditEditorVisible'], + state.getIn(['editionSection', 'authorCreditEditorVisible'])); + if (action.payload.type && camelCase(action.payload.type) === 'editionGroup') { + intermediateState = intermediateState.set('authorCreditEditor', initialACState); + } intermediateState = intermediateState.setIn(['nameSection', 'language'], activeEntityState.nameSection.get('language', null)); break; case ADD_EDITION_GROUP: From fa22e697663b05ce23ebe5caf00dd2aca4d5a40d Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 24 Jun 2022 21:59:37 +0530 Subject: [PATCH 042/258] fix(uf): fix validation issue with eg --- src/client/entity-editor/validators/edition.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/client/entity-editor/validators/edition.ts b/src/client/entity-editor/validators/edition.ts index 60f52cc9d6..a2ef465139 100644 --- a/src/client/entity-editor/validators/edition.ts +++ b/src/client/entity-editor/validators/edition.ts @@ -69,8 +69,8 @@ export function validateEditionSectionPages(value: any): boolean { return validatePositiveInteger(value); } -export function validateEditionSectionEditionGroup(value: any, editionGroupRequired: boolean | null | undefined): boolean { - return validateUUID(get(value, 'id', null), editionGroupRequired); +export function validateEditionSectionEditionGroup(value: any, editionGroupRequired: boolean | null | undefined, isCustom = false): boolean { + return isCustom ? !editionGroupRequired || Boolean(get(value, 'id', null)) : validateUUID(get(value, 'id', null), editionGroupRequired); } export function validateEditionSectionPublisher(value: any): boolean { @@ -106,7 +106,7 @@ export function validateEditionSectionWidth(value: any): boolean { return validatePositiveInteger(value); } -export function validateEditionSection(data: any): boolean { +export function validateEditionSection(data: any, isCustom = false): boolean { return ( validateEditionSectionDepth(get(data, 'depth', null)) && validateEditionSectionFormat(get(data, 'format', null)) && @@ -115,7 +115,8 @@ export function validateEditionSection(data: any): boolean { validateEditionSectionPages(get(data, 'pages', null)) && validateEditionSectionEditionGroup( get(data, 'editionGroup', null), - get(data, 'editionGroupRequired', null) + get(data, 'editionGroupRequired', null), + isCustom ) && validateEditionSectionPublisher(get(data, 'publisher', null)) && validateEditionSectionReleaseDate(get(data, 'releaseDate', null)).isValid && @@ -143,7 +144,7 @@ export function validateForm( get(formData, 'identifierEditor', {}), identifierTypes ), validateNameSection(get(formData, 'nameSection', {})), - validateEditionSection(get(formData, 'editionSection', {})), + validateEditionSection(get(formData, 'editionSection', {}), isCustom), validAuthorCredit, validateSubmissionSection(get(formData, 'submissionSection', {})) ]; From cff0f1584be2aecbf44cb610b510c8736a6f5526 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 25 Jun 2022 06:10:40 +0530 Subject: [PATCH 043/258] test(uf): only allow valid type for publishers --- src/client/entity-editor/validators/edition.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client/entity-editor/validators/edition.ts b/src/client/entity-editor/validators/edition.ts index a2ef465139..bd53077e45 100644 --- a/src/client/entity-editor/validators/edition.ts +++ b/src/client/entity-editor/validators/edition.ts @@ -78,6 +78,9 @@ export function validateEditionSectionPublisher(value: any): boolean { return true; } const publishers = convertMapToObject(value); + if (!_.isPlainObject(publishers)) { + return false; + } for (const pubId in publishers) { if (Object.prototype.hasOwnProperty.call(publishers, pubId)) { const publisher = publishers[pubId]; From 1baa0545cc3166e4085a93ac5931686a0b0bc4f8 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 25 Jun 2022 18:33:51 +0530 Subject: [PATCH 044/258] test(uf): add test for AC --- src/client/entity-editor/validators/common.ts | 13 ++-- .../entity-editor/validators/edition.ts | 8 +- src/server/helpers/entityRouteUtils.tsx | 2 +- test/src/server/routes/unifiedform.js | 73 ++++++++++++++++++- 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/client/entity-editor/validators/common.ts b/src/client/entity-editor/validators/common.ts index 85644116f4..99b841e418 100644 --- a/src/client/entity-editor/validators/common.ts +++ b/src/client/entity-editor/validators/common.ts @@ -25,16 +25,17 @@ import { validateUUID } from './base'; +import {AuthorCredit} from '../author-credit-editor/actions'; import {Iterable} from 'immutable'; import _ from 'lodash'; -import {AuthorCredit} from '../author-credit-editor/actions'; export function validateMultiple( values: any[], validationFunction: (value: any, ...rest: any[]) => boolean, additionalArgs?: any, - requiresOneOrMore?: boolean + requiresOneOrMore?: boolean, + isCustom = false ): boolean { if (requiresOneOrMore && _.isEmpty(values)) { return false; @@ -48,7 +49,7 @@ export function validateMultiple( } return every(values, (value) => - validationFunction(value, additionalArgs)); + validationFunction(value, isCustom, additionalArgs)); } export function validateAliasName(value: any): boolean { @@ -184,8 +185,8 @@ export function validateSubmissionSection( ); } -export function validateAuthorCreditRow(row: any): boolean { - return validateUUID(getIn(row, ['author', 'id'], null), true) && +export function validateAuthorCreditRow(row: any, isCustom = false): boolean { + return (isCustom ? Boolean(getIn(row, ['author', 'id'], null)) : validateUUID(getIn(row, ['author', 'id'], null), true)) && validateRequiredString(get(row, 'name', null)) && validateOptionalString(get(row, 'joinPhrase', null)); } @@ -193,7 +194,7 @@ export function validateAuthorCreditRow(row: any): boolean { export const validateAuthorCreditSection = _.partialRight( // Requires at least one Author Credit row validateMultiple, _.partialRight.placeholder, - validateAuthorCreditRow, null, false + validateAuthorCreditRow, null, false, _.partialRight.placeholder ); // In the merge editor we use the authorCredit directly instead of the authorCreditEditor state export function validateAuthorCreditSectionMerge(authorCredit:AuthorCredit) :boolean { diff --git a/src/client/entity-editor/validators/edition.ts b/src/client/entity-editor/validators/edition.ts index 7b542fa5d6..a22040e81f 100644 --- a/src/client/entity-editor/validators/edition.ts +++ b/src/client/entity-editor/validators/edition.ts @@ -131,14 +131,16 @@ export function validateEditionSection(data: any, isCustom = false): boolean { export function validateForm( formData: any, identifierTypes?: Array<_IdentifierType> | null | undefined, - isMerge?:boolean + isMerge?:boolean, + isUf?:boolean ): boolean { let validAuthorCredit; + const isCustom = isUf || Boolean(formData?.type); if (isMerge) { validAuthorCredit = validateAuthorCreditSectionMerge(get(formData, 'authorCredit', {})); } else { - validAuthorCredit = validateAuthorCreditSection(get(formData, 'authorCreditEditor', {})); + validAuthorCredit = validateAuthorCreditSection(get(formData, 'authorCreditEditor', {}), isCustom); } const conditions = [ validateAliases(get(formData, 'aliasEditor', {})), @@ -146,7 +148,7 @@ export function validateForm( get(formData, 'identifierEditor', {}), identifierTypes ), validateNameSection(get(formData, 'nameSection', {})), - validateEditionSection(get(formData, 'editionSection', {}), Boolean(formData?.type)), + validateEditionSection(get(formData, 'editionSection', {}), isCustom), validAuthorCredit, validateSubmissionSection(get(formData, 'submissionSection', {})) ]; diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index e78997440b..c8427567c2 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -328,7 +328,7 @@ function validateUnifiedForm(body:Record):boolean { if (Object.prototype.hasOwnProperty.call(body, entityKey)) { const entityForm = body[entityKey]; const entityType = _.camelCase(entityForm.type); - if (!entityType && !validEntityTypes.includes(entityType)) { + if (!entityType || !validEntityTypes.includes(entityType)) { return false; } const validator = getValidator(entityType); diff --git a/test/src/server/routes/unifiedform.js b/test/src/server/routes/unifiedform.js index 1b811cb2a4..ef4a959f09 100644 --- a/test/src/server/routes/unifiedform.js +++ b/test/src/server/routes/unifiedform.js @@ -1,4 +1,4 @@ -import {baseState, createEditionGroup, createEditor, +import {baseState, createAuthor, createEditionGroup, createEditor, createPublisher, createWork, getRandomUUID, languageAttribs, truncateEntities} from '../../../test-helpers/create-entities'; import {every, forOwn, map} from 'lodash'; import app from '../../../../src/server/app'; @@ -41,12 +41,14 @@ describe('Unified form routes', () => { const wBBID = getRandomUUID(); const pBBID = getRandomUUID(); const egBBID = getRandomUUID(); + const aBBID = getRandomUUID(); before(async () => { try { await createEditor(123456); await createWork(wBBID); await createPublisher(pBBID); await createEditionGroup(egBBID); + await createAuthor(aBBID); newLanguage = await new Language({...languageAttribs}) .save(null, {method: 'insert'}); newRelationshipType = await new RelationshipType(relationshipTypeData) @@ -253,7 +255,9 @@ describe('Unified form routes', () => { }, type: 'Edition' }}; - postData.b0.nameSection.language = newLanguage.id; + forOwn(postData, (value) => { + value.nameSection.language = newLanguage.id; + }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(2); @@ -307,7 +311,9 @@ describe('Unified form routes', () => { }, type: 'Edition' }}; - postData.b0.nameSection.language = newLanguage.id; + forOwn(postData, (value) => { + value.nameSection.language = newLanguage.id; + }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(2); @@ -322,6 +328,67 @@ describe('Unified form routes', () => { expect(res).to.be.ok; expect(res).to.have.status(200); }); + it('should not throw error while linking existing author to edition using AC', async () => { + const postData = {b0: { + ...baseState, + authorCreditEditor: { + n0: { + author: { + id: aBBID + }, + joinPhrase: '', + name: 'author1' + } + }, + editionSection: {}, + type: 'Edition' + }}; + postData.b0.nameSection.language = newLanguage.id; + const res = await agent.post('/create/handler').send(postData); + const createdEntities = res.body; + expect(createdEntities.length).equal(1); + const editionEntity = createdEntities.find((entity) => entity.type === 'Edition'); + const fetchedEditionEntity = await getEntityByBBID(orm, editionEntity.bbid, ['authorCredit']); + expect(Boolean(fetchedEditionEntity)).to.be.true; + expect(fetchedEditionEntity.authorCredit.authorCount).equal(1); + expect(res).to.be.ok; + expect(res).to.have.status(200); + }); + it('should not throw error while linking new author to edition using AC', async () => { + const postData = { + b0: { + ...baseState, + authorSection: {}, + type: 'Author' + }, + b1: { + ...baseState, + authorCreditEditor: { + n0: { + author: { + id: 'b0' + }, + joinPhrase: '', + name: 'author1' + } + }, + editionSection: {}, + type: 'Edition' + } + }; + forOwn(postData, (value) => { + value.nameSection.language = newLanguage.id; + }); + const res = await agent.post('/create/handler').send(postData); + const createdEntities = res.body; + expect(createdEntities.length).equal(2); + const editionEntity = createdEntities.find((entity) => entity.type === 'Edition'); + const fetchedEditionEntity = await getEntityByBBID(orm, editionEntity.bbid, ['authorCredit']); + expect(Boolean(fetchedEditionEntity)).to.be.true; + expect(fetchedEditionEntity.authorCredit.authorCount).equal(1); + expect(res).to.be.ok; + expect(res).to.have.status(200); + }); it('should throw bad request error while posting invalid form', async () => { const postData = {b0: { ...baseState, From aedf207d7bddb4484cb74dbd723936392272ca2a Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 25 Jun 2022 19:38:18 +0530 Subject: [PATCH 045/258] feat(uf): remove created edition-group on clear --- .../entity-editor/edition-section/edition-section.tsx | 11 +++++++++-- src/client/entity-editor/validators/edition.ts | 3 ++- src/client/unified-form/detail-tab/action.ts | 7 +++++++ src/client/unified-form/detail-tab/reducer.ts | 4 +++- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/client/entity-editor/edition-section/edition-section.tsx b/src/client/entity-editor/edition-section/edition-section.tsx index d47c9270cb..a4e11fea19 100644 --- a/src/client/entity-editor/edition-section/edition-section.tsx +++ b/src/client/entity-editor/edition-section/edition-section.tsx @@ -61,6 +61,7 @@ import NumericField from '../common/numeric-field'; import SearchEntityCreate from '../../unified-form/common/search-entity-create-select'; import Select from 'react-select'; import _ from 'lodash'; +import {clearEditionGroups} from '../../unified-form/detail-tab/action'; import {connect} from 'react-redux'; import {entityToOption} from '../../helpers/entity'; import makeImmutable from '../common/make-immutable'; @@ -90,6 +91,7 @@ type Publisher = { }; type EditionGroup = { + __isNew__: boolean, value: string, id: number }; @@ -129,7 +131,7 @@ type DispatchProps = { onPagesChange: (arg: React.ChangeEvent) => unknown, onPublisherChange: (arg: Publisher[]) => unknown, onToggleShowEditionGroupSection: (showEGSection: boolean) => unknown, - onEditionGroupChange: (arg: EditionGroup) => unknown, + onEditionGroupChange: (arg: EditionGroup, action) => unknown, onReleaseDateChange: (arg: string) => unknown, onStatusChange: (obj: {value: number} | null | undefined) => unknown, onWeightChange: (arg: React.ChangeEvent) => unknown, @@ -510,7 +512,12 @@ function mapDispatchToProps(dispatch: Dispatch): DispatchProps { onDepthChange: (event) => dispatch(debouncedUpdateDepth( event.target.value ? parseInt(event.target.value, 10) : null )), - onEditionGroupChange: (value) => dispatch(updateEditionGroup(value)), + onEditionGroupChange: (value, action) => { + if (action === 'clear' && value.__isNew__) { + dispatch(clearEditionGroups()); + } + dispatch(updateEditionGroup(value)); + }, onFormatChange: (value: {value: number} | null) => { dispatch(updateFormat(value && value.value)); if (value.value === 3) { diff --git a/src/client/entity-editor/validators/edition.ts b/src/client/entity-editor/validators/edition.ts index bd53077e45..d21ba9c769 100644 --- a/src/client/entity-editor/validators/edition.ts +++ b/src/client/entity-editor/validators/edition.ts @@ -132,9 +132,10 @@ export function validateEditionSection(data: any, isCustom = false): boolean { export function validateForm( formData: any, identifierTypes?: Array<_IdentifierType> | null | undefined, isMerge?:boolean, - isCustom?:boolean + isUf?:boolean ): boolean { let validAuthorCredit; + const isCustom = isUf || Boolean(formData?.type); if (isMerge) { validAuthorCredit = validateAuthorCreditSectionMerge(get(formData, 'authorCredit', {})); } diff --git a/src/client/unified-form/detail-tab/action.ts b/src/client/unified-form/detail-tab/action.ts index e0221cd59d..edeb2d8120 100644 --- a/src/client/unified-form/detail-tab/action.ts +++ b/src/client/unified-form/detail-tab/action.ts @@ -2,6 +2,7 @@ import {Action} from '../interface/type'; export const ADD_EDITION_GROUP = 'ADD_EDITION_GROUP'; +export const CLEAR_EDITION_GROUPS = 'CLEAR_EDITION_GROUPS'; const nextEditionGroupId = 0; export function addEditionGroup(value = null):Action { return { @@ -9,3 +10,9 @@ export function addEditionGroup(value = null):Action { type: ADD_EDITION_GROUP }; } + +export function clearEditionGroups():Action { + return { + type: CLEAR_EDITION_GROUPS + }; +} diff --git a/src/client/unified-form/detail-tab/reducer.ts b/src/client/unified-form/detail-tab/reducer.ts index 87a6b0a4bc..37f9eafdca 100644 --- a/src/client/unified-form/detail-tab/reducer.ts +++ b/src/client/unified-form/detail-tab/reducer.ts @@ -1,4 +1,4 @@ -import {ADD_EDITION_GROUP} from './action'; +import {ADD_EDITION_GROUP, CLEAR_EDITION_GROUPS} from './action'; import Immutable from 'immutable'; @@ -6,6 +6,8 @@ export default function editionGroupsReducer(state = Immutable.Map({}), {type, p switch (type) { case ADD_EDITION_GROUP: return state.set(payload.id, Immutable.fromJS(payload.value)); + case CLEAR_EDITION_GROUPS: + return Immutable.Map({}); default: return state; } From fb029478c7c083083d7f631d92602d0919204a37 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 25 Jun 2022 20:51:19 +0530 Subject: [PATCH 046/258] fix onEditionGroupChange handler --- src/client/entity-editor/edition-section/edition-section.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/entity-editor/edition-section/edition-section.tsx b/src/client/entity-editor/edition-section/edition-section.tsx index a4e11fea19..f7a9ec459a 100644 --- a/src/client/entity-editor/edition-section/edition-section.tsx +++ b/src/client/entity-editor/edition-section/edition-section.tsx @@ -513,7 +513,7 @@ function mapDispatchToProps(dispatch: Dispatch): DispatchProps { event.target.value ? parseInt(event.target.value, 10) : null )), onEditionGroupChange: (value, action) => { - if (action === 'clear' && value.__isNew__) { + if (action.action === 'clear') { dispatch(clearEditionGroups()); } dispatch(updateEditionGroup(value)); From bb25e223fa6cc94474bc8ed5ab4b4cd5f30de4fd Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 25 Jun 2022 21:17:59 +0530 Subject: [PATCH 047/258] feat(uf): allow removing created publisher --- .../edition-section/edition-section.tsx | 2 +- src/client/unified-form/cover-tab/action.ts | 8 +++++++- .../unified-form/cover-tab/cover-tab.tsx | 18 ++++++++++++++---- src/client/unified-form/cover-tab/reducer.ts | 4 +++- src/client/unified-form/interface/type.ts | 5 +++-- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/client/entity-editor/edition-section/edition-section.tsx b/src/client/entity-editor/edition-section/edition-section.tsx index f7a9ec459a..7fe86f6d6f 100644 --- a/src/client/entity-editor/edition-section/edition-section.tsx +++ b/src/client/entity-editor/edition-section/edition-section.tsx @@ -513,7 +513,7 @@ function mapDispatchToProps(dispatch: Dispatch): DispatchProps { event.target.value ? parseInt(event.target.value, 10) : null )), onEditionGroupChange: (value, action) => { - if (action.action === 'clear') { + if (action.removedValues[0]?.__isNew__ && action.action === 'clear') { dispatch(clearEditionGroups()); } dispatch(updateEditionGroup(value)); diff --git a/src/client/unified-form/cover-tab/action.ts b/src/client/unified-form/cover-tab/action.ts index e64d3a6056..5ed85a7e97 100644 --- a/src/client/unified-form/cover-tab/action.ts +++ b/src/client/unified-form/cover-tab/action.ts @@ -5,7 +5,7 @@ export const UPDATE_ISBN_VALUE = 'UPDATE_ISBN_VALUE'; export const ADD_AUTHOR = 'ADD_AUTHOR'; export const UPDATE_ISBN_TYPE = 'UPDATE_ISBN_TYPE'; export const ADD_PUBLISHER = 'ADD_PUBLISHER'; - +export const CLEAR_PUBLISHER = 'CLEAR_PUBLISHER'; let nextPublisherId = 0; let nextAuthorId = 0; @@ -17,6 +17,12 @@ export function addPublisher(value = null):Action { }; } +export function clearPublisher(pid:string):Action { + return { + payload: pid, + type: CLEAR_PUBLISHER + }; +} export function addAuthor(rowId:string, value = null):Action { return { payload: {id: `a${nextAuthorId++}`, rowId, value}, diff --git a/src/client/unified-form/cover-tab/cover-tab.tsx b/src/client/unified-form/cover-tab/cover-tab.tsx index 64af634363..52fdf3e030 100644 --- a/src/client/unified-form/cover-tab/cover-tab.tsx +++ b/src/client/unified-form/cover-tab/cover-tab.tsx @@ -1,5 +1,5 @@ import {Col, Row} from 'react-bootstrap'; -import {CoverProps, EntitySelect} from '../interface/type'; +import {CoverDispatchProps, CoverProps, CoverStateProps, EntitySelect} from '../interface/type'; import AuthorCreditSection from '../../entity-editor/author-credit-editor/author-credit-section'; import ButtonBar from '../../entity-editor/button-bar/button-bar'; import ISBNField from './isbn-field'; @@ -7,14 +7,23 @@ import IdentifierEditor from '../../entity-editor/identifier-editor/identifier-e import NameSection from '../../entity-editor/name-section/name-section'; import React from 'react'; import SearchEntityCreate from '../common/search-entity-create-select'; +import {clearPublisher} from './action'; import {connect} from 'react-redux'; import {convertMapToObject} from '../../helpers/utils'; import {updatePublisher} from '../../entity-editor/edition-section/actions'; export function CoverTab(props:CoverProps) { - const {publisherValue: publishers, onPublisherChange, identifierEditorVisible} = props; + const {publisherValue: publishers, onPublisherChange, identifierEditorVisible, onClearPublisher} = props; const publisherValue:EntitySelect[] = Object.values(convertMapToObject(publishers ?? {})); + const onChangeHandler = React.useCallback((value:EntitySelect[], action) => { + if (action.action === 'remove-value' || action.action === 'pop-value') { + if (action.removedValue.__isNew__) { + onClearPublisher(action.removedValue.id); + } + } + onPublisherChange(value); + }, []); return (
@@ -26,7 +35,7 @@ export function CoverTab(props:CoverProps) { label="Publisher" type="publisher" value={publisherValue} - onChange={onPublisherChange} + onChange={onChangeHandler} {...props} /> @@ -55,8 +64,9 @@ function mapStateToProps(rootState) { function mapDispatchToProps(dispatch) { return { + onClearPublisher: (arg) => dispatch(clearPublisher(arg)), onPublisherChange: (value) => dispatch(updatePublisher(Object.fromEntries(value.map((pub, index) => [index, pub])))) }; } -export default connect(mapStateToProps, mapDispatchToProps)(CoverTab); +export default connect(mapStateToProps, mapDispatchToProps)(CoverTab); diff --git a/src/client/unified-form/cover-tab/reducer.ts b/src/client/unified-form/cover-tab/reducer.ts index cbe2b7fd9d..a257065db7 100644 --- a/src/client/unified-form/cover-tab/reducer.ts +++ b/src/client/unified-form/cover-tab/reducer.ts @@ -1,4 +1,4 @@ -import {ADD_AUTHOR, ADD_PUBLISHER, UPDATE_ISBN_TYPE, UPDATE_ISBN_VALUE} from './action'; +import {ADD_AUTHOR, ADD_PUBLISHER, CLEAR_PUBLISHER, UPDATE_ISBN_TYPE, UPDATE_ISBN_VALUE} from './action'; import Immutable from 'immutable'; @@ -21,6 +21,8 @@ export function publishersReducer(state = Immutable.Map({}), {type, payload}) { switch (type) { case ADD_PUBLISHER: return state.set(payload.id, Immutable.fromJS(payload.value)); + case CLEAR_PUBLISHER: + return state.delete(payload); default: return state; } diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index 9f8605b642..06af16c579 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -40,7 +40,8 @@ export type CoverStateProps = { identifierEditorVisible:boolean }; export type CoverDispatchProps = { - onPublisherChange: (arg:any)=>unknown + onPublisherChange: (arg:any)=>unknown, + onClearPublisher: (arg:string)=>unknown, }; export type CoverProps = CoverOwnProps & CoverStateProps & CoverDispatchProps; @@ -98,7 +99,7 @@ export type SearchEntityCreateOwnProps = { value?:Array | EntitySelect type:string, rowId?:string, - onChange:(arg)=>unknown + onChange:(arg, ...optional)=>unknown }; export type SearchEntityCreateProps = SearchEntityCreateDispatchProps & SearchEntityCreateOwnProps & CommonProps; From 6b4667be52bb2b00f9685534798566297ca6ee77 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sun, 26 Jun 2022 07:39:15 +0530 Subject: [PATCH 048/258] feat(uf): properly clear newly created author --- .../author-credit-row.tsx | 25 ++++++++++++++++--- .../author-credit-section.tsx | 21 +++++++++++++--- src/client/unified-form/cover-tab/action.ts | 8 ++++++ src/client/unified-form/cover-tab/reducer.ts | 4 ++- src/client/unified-form/helpers.ts | 1 + 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/client/entity-editor/author-credit-editor/author-credit-row.tsx b/src/client/entity-editor/author-credit-editor/author-credit-row.tsx index c53116ee3c..59fff5e312 100644 --- a/src/client/entity-editor/author-credit-editor/author-credit-row.tsx +++ b/src/client/entity-editor/author-credit-editor/author-credit-row.tsx @@ -18,7 +18,6 @@ import { Action, - Author, removeAuthorCreditRow, updateCreditAuthorValue, updateCreditDisplayValue, @@ -28,9 +27,11 @@ import {Button, Col, Form, Row} from 'react-bootstrap'; import type {Dispatch} from 'redux'; import EntitySearchFieldOption from '../common/entity-search-field-option'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import Immutable from 'immutable'; import React from 'react'; import SearchEntityCreate from '../../unified-form/common/search-entity-create-select'; import ValidationLabel from '../common/validation-label'; +import {clearAuthor} from '../../unified-form/cover-tab/action'; import {connect} from 'react-redux'; import {faTimes} from '@fortawesome/free-solid-svg-icons'; @@ -41,13 +42,14 @@ type OwnProps = { }; type StateProps = { - author: Author, + author: Immutable.Map, joinPhrase: string, name: string }; type DispatchProps = { onAuthorChange: (Author) => unknown, + onClearHandler:(arg) => unknown, onJoinPhraseChange: (string) => unknown, onNameChange: (string) => unknown, onRemoveButtonClick: () => unknown @@ -85,11 +87,25 @@ function AuthorCreditRow({ isUf, onAuthorChange, onJoinPhraseChange, + onClearHandler, onNameChange, onRemoveButtonClick, ...rest }: Props) { const SelectWrapper = !isUf ? EntitySearchFieldOption : SearchEntityCreate; + const onChangeHandler = React.useCallback((value, action) => { + if (['clear', 'pop-value', 'select-option'].includes(action.action) && author && author.get('__isNew__', false)) { + onClearHandler(author.get('id')); + } + onAuthorChange(value); + }, [author]); + const handleButtonClick = React.useCallback(() => { + // don't remove author if it's first row + if (index !== 'n0' && author.get('__isNew__')) { + onClearHandler(author.get('id')); + } + onRemoveButtonClick(); + }, [author, index]); return (
@@ -101,7 +117,7 @@ function AuthorCreditRow({ type="author" validationState={!author ? 'error' : null} value={author} - onChange={onAuthorChange} + onChange={onChangeHandler} {...rest} /> @@ -130,7 +146,7 @@ function AuthorCreditRow({ block className="margin-top-d18" variant="danger" - onClick={onRemoveButtonClick} + onClick={handleButtonClick} >  Remove @@ -161,6 +177,7 @@ function mapDispatchToProps( ): DispatchProps { return { onAuthorChange: (value) => dispatch(updateCreditAuthorValue(index, value)), + onClearHandler: (aid) => dispatch(clearAuthor(aid)), onJoinPhraseChange: (event: React.ChangeEvent) => dispatch(updateCreditJoinPhraseValue(index, event.target.value)), onNameChange: (event: React.ChangeEvent) => dispatch(updateCreditDisplayValue(index, event.target.value)), onRemoveButtonClick: () => dispatch(removeAuthorCreditRow(index)) diff --git a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx index dfcaedbdf0..0d139d973f 100644 --- a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx +++ b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx @@ -28,7 +28,7 @@ import { import {Button, Col, Form, InputGroup, OverlayTrigger, Row, Tooltip} from 'react-bootstrap'; import {SingleValueProps, components} from 'react-select'; -import {map as _map, values as _values} from 'lodash'; +import {get as _get, map as _map, values as _values} from 'lodash'; import {faPencilAlt, faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; import AuthorCreditEditor from './author-credit-editor'; @@ -39,6 +39,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import SearchEntityCreate from '../../unified-form/common/search-entity-create-select'; import ValidationLabel from '../common/validation-label'; +import {clearAuthor} from '../../unified-form/cover-tab/action'; import {connect} from 'react-redux'; import {convertMapToObject} from '../../helpers/utils'; import {validateAuthorCreditSection} from '../validators/common'; @@ -56,6 +57,7 @@ type StateProps = { type DispatchProps = { onAuthorChange: (Author) => unknown, + onClearHandler:(arg) => unknown, onEditAuthorCredit: (rowCount: number) => unknown, onEditorClose: () => unknown, }; @@ -63,7 +65,7 @@ type DispatchProps = { type Props = OwnProps & StateProps & DispatchProps; function AuthorCreditSection({ - authorCreditEditor, onEditAuthorCredit, onEditorClose, showEditor, onAuthorChange, isEditable, isUf, ...rest + authorCreditEditor, onEditAuthorCredit, onEditorClose, showEditor, onAuthorChange, isEditable, onClearHandler, isUf, ...rest }: Props) { let editor; if (showEditor) { @@ -110,6 +112,13 @@ function AuthorCreditSection({ if (isUf) { resCol = {lg: {offset: 0, span: 6}}; } + const onChangeHandler = React.useCallback((value, action) => { + const authorId = _get(authorCreditEditor, 'n0.author.id', null); + if (['clear', 'pop-value', 'select-option'].includes(action.action) && authorId) { + onClearHandler(authorId); + } + onAuthorChange(value); + }, [authorCreditEditor]); const SelectWrapper = !isUf ? EntitySearchFieldOption : SearchEntityCreate; return ( @@ -130,13 +139,14 @@ function AuthorCreditSection({
@@ -172,7 +182,10 @@ function mapStateToProps(rootState): StateProps { function mapDispatchToProps(dispatch: Dispatch): DispatchProps { return { - onAuthorChange: (value) => dispatch(updateCreditAuthorValue(-1, value)), + onAuthorChange: (value) => { + dispatch(updateCreditAuthorValue(-1, value)); + }, + onClearHandler: (aid) => dispatch(clearAuthor(aid)), onEditAuthorCredit: (rowCount:number) => { dispatch(showAuthorCreditEditor()); // Automatically add an empty row if editor is empty diff --git a/src/client/unified-form/cover-tab/action.ts b/src/client/unified-form/cover-tab/action.ts index 5ed85a7e97..e1fc50c3b8 100644 --- a/src/client/unified-form/cover-tab/action.ts +++ b/src/client/unified-form/cover-tab/action.ts @@ -6,6 +6,7 @@ export const ADD_AUTHOR = 'ADD_AUTHOR'; export const UPDATE_ISBN_TYPE = 'UPDATE_ISBN_TYPE'; export const ADD_PUBLISHER = 'ADD_PUBLISHER'; export const CLEAR_PUBLISHER = 'CLEAR_PUBLISHER'; +export const CLEAR_AUTHOR = 'CLEAR_AUTHOR'; let nextPublisherId = 0; let nextAuthorId = 0; @@ -23,6 +24,13 @@ export function clearPublisher(pid:string):Action { type: CLEAR_PUBLISHER }; } + +export function clearAuthor(aid:string):Action { + return { + payload: aid, + type: CLEAR_AUTHOR + }; +} export function addAuthor(rowId:string, value = null):Action { return { payload: {id: `a${nextAuthorId++}`, rowId, value}, diff --git a/src/client/unified-form/cover-tab/reducer.ts b/src/client/unified-form/cover-tab/reducer.ts index a257065db7..f4a76e16e7 100644 --- a/src/client/unified-form/cover-tab/reducer.ts +++ b/src/client/unified-form/cover-tab/reducer.ts @@ -1,4 +1,4 @@ -import {ADD_AUTHOR, ADD_PUBLISHER, CLEAR_PUBLISHER, UPDATE_ISBN_TYPE, UPDATE_ISBN_VALUE} from './action'; +import {ADD_AUTHOR, ADD_PUBLISHER, CLEAR_AUTHOR, CLEAR_PUBLISHER, UPDATE_ISBN_TYPE, UPDATE_ISBN_VALUE} from './action'; import Immutable from 'immutable'; @@ -32,6 +32,8 @@ export function authorsReducer(state = Immutable.Map({}), {type, payload}) { switch (type) { case ADD_AUTHOR: return state.set(payload.id, Immutable.fromJS(payload.value)); + case CLEAR_AUTHOR: + return state.delete(payload); default: return state; } diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index 9360d77fbb..78e4916d7e 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -123,6 +123,7 @@ function crossSliceReducer(state, action) { switch (type) { case ADD_AUTHOR: intermediateState = intermediateState.setIn(['Editions', 'e0', 'authorCreditEditor', action.payload.rowId, 'author'], Immutable.Map({ + __isNew__: true, id: action.payload.id, rowId: action.payload.rowId, text: activeEntityState.nameSection.get('name'), From 6530761c7a34d680b34b074ccb0a3f50e49877ac Mon Sep 17 00:00:00 2001 From: tri10 Date: Sun, 26 Jun 2022 07:57:32 +0530 Subject: [PATCH 049/258] fix(uf): clearing issue on edition-group --- .../entity-editor/edition-section/edition-section.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/entity-editor/edition-section/edition-section.tsx b/src/client/entity-editor/edition-section/edition-section.tsx index 7fe86f6d6f..93c1b2a780 100644 --- a/src/client/entity-editor/edition-section/edition-section.tsx +++ b/src/client/entity-editor/edition-section/edition-section.tsx @@ -199,7 +199,7 @@ function EditionSection({ label: language.name, value: language.id })); - rest.languageOptions = languageOptions; + _.set(rest, 'languageOptions', languageOptions); let publisherValue = publishers ?? {}; publisherValue = Object.values(convertMapToObject(publisherValue)); const editionFormatsForDisplay = editionFormats.map((format) => ({ @@ -227,7 +227,6 @@ function EditionSection({ ): DispatchProps { event.target.value ? parseInt(event.target.value, 10) : null )), onEditionGroupChange: (value, action) => { - if (action.removedValues[0]?.__isNew__ && action.action === 'clear') { + // If the user selected a new edition group, we need to clear the old one + if (['clear', 'pop-value', 'select-option'].includes(action.action)) { dispatch(clearEditionGroups()); } dispatch(updateEditionGroup(value)); From b81590e07454231c53044334633dcc093397c086 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sun, 26 Jun 2022 08:50:48 +0530 Subject: [PATCH 050/258] feat(uf): clear all new publishers at once --- src/client/unified-form/cover-tab/action.ts | 7 +++++++ src/client/unified-form/cover-tab/cover-tab.tsx | 10 +++++++--- src/client/unified-form/cover-tab/reducer.ts | 4 +++- src/client/unified-form/interface/type.ts | 1 + 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/client/unified-form/cover-tab/action.ts b/src/client/unified-form/cover-tab/action.ts index e1fc50c3b8..3d893e9890 100644 --- a/src/client/unified-form/cover-tab/action.ts +++ b/src/client/unified-form/cover-tab/action.ts @@ -6,6 +6,7 @@ export const ADD_AUTHOR = 'ADD_AUTHOR'; export const UPDATE_ISBN_TYPE = 'UPDATE_ISBN_TYPE'; export const ADD_PUBLISHER = 'ADD_PUBLISHER'; export const CLEAR_PUBLISHER = 'CLEAR_PUBLISHER'; +export const CLEAR_PUBLISHERS = 'CLEAR_PUBLISHERS'; export const CLEAR_AUTHOR = 'CLEAR_AUTHOR'; let nextPublisherId = 0; @@ -25,6 +26,12 @@ export function clearPublisher(pid:string):Action { }; } +export function clearPublishers():Action { + return { + type: CLEAR_PUBLISHERS + }; +} + export function clearAuthor(aid:string):Action { return { payload: aid, diff --git a/src/client/unified-form/cover-tab/cover-tab.tsx b/src/client/unified-form/cover-tab/cover-tab.tsx index 52fdf3e030..652fd1469c 100644 --- a/src/client/unified-form/cover-tab/cover-tab.tsx +++ b/src/client/unified-form/cover-tab/cover-tab.tsx @@ -1,5 +1,6 @@ import {Col, Row} from 'react-bootstrap'; import {CoverDispatchProps, CoverProps, CoverStateProps, EntitySelect} from '../interface/type'; +import {clearPublisher, clearPublishers} from './action'; import AuthorCreditSection from '../../entity-editor/author-credit-editor/author-credit-section'; import ButtonBar from '../../entity-editor/button-bar/button-bar'; import ISBNField from './isbn-field'; @@ -7,21 +8,23 @@ import IdentifierEditor from '../../entity-editor/identifier-editor/identifier-e import NameSection from '../../entity-editor/name-section/name-section'; import React from 'react'; import SearchEntityCreate from '../common/search-entity-create-select'; -import {clearPublisher} from './action'; import {connect} from 'react-redux'; import {convertMapToObject} from '../../helpers/utils'; import {updatePublisher} from '../../entity-editor/edition-section/actions'; export function CoverTab(props:CoverProps) { - const {publisherValue: publishers, onPublisherChange, identifierEditorVisible, onClearPublisher} = props; + const {publisherValue: publishers, onPublisherChange, identifierEditorVisible, onClearPublisher, handleClearPublishers} = props; const publisherValue:EntitySelect[] = Object.values(convertMapToObject(publishers ?? {})); const onChangeHandler = React.useCallback((value:EntitySelect[], action) => { - if (action.action === 'remove-value' || action.action === 'pop-value') { + if (['remove-value', 'pop-value'].includes(action.action)) { if (action.removedValue.__isNew__) { onClearPublisher(action.removedValue.id); } } + if (action.action === 'clear') { + handleClearPublishers(); + } onPublisherChange(value); }, []); return ( @@ -64,6 +67,7 @@ function mapStateToProps(rootState) { function mapDispatchToProps(dispatch) { return { + handleClearPublishers: () => dispatch(clearPublishers()), onClearPublisher: (arg) => dispatch(clearPublisher(arg)), onPublisherChange: (value) => dispatch(updatePublisher(Object.fromEntries(value.map((pub, index) => [index, pub])))) }; diff --git a/src/client/unified-form/cover-tab/reducer.ts b/src/client/unified-form/cover-tab/reducer.ts index f4a76e16e7..02592a1b4c 100644 --- a/src/client/unified-form/cover-tab/reducer.ts +++ b/src/client/unified-form/cover-tab/reducer.ts @@ -1,4 +1,4 @@ -import {ADD_AUTHOR, ADD_PUBLISHER, CLEAR_AUTHOR, CLEAR_PUBLISHER, UPDATE_ISBN_TYPE, UPDATE_ISBN_VALUE} from './action'; +import {ADD_AUTHOR, ADD_PUBLISHER, CLEAR_AUTHOR, CLEAR_PUBLISHER, CLEAR_PUBLISHERS, UPDATE_ISBN_TYPE, UPDATE_ISBN_VALUE} from './action'; import Immutable from 'immutable'; @@ -23,6 +23,8 @@ export function publishersReducer(state = Immutable.Map({}), {type, payload}) { return state.set(payload.id, Immutable.fromJS(payload.value)); case CLEAR_PUBLISHER: return state.delete(payload); + case CLEAR_PUBLISHERS: + return Immutable.Map({}); default: return state; } diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index 06af16c579..6087061be8 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -42,6 +42,7 @@ export type CoverStateProps = { export type CoverDispatchProps = { onPublisherChange: (arg:any)=>unknown, onClearPublisher: (arg:string)=>unknown, + handleClearPublishers: ()=>unknown }; export type CoverProps = CoverOwnProps & CoverStateProps & CoverDispatchProps; From 214da2268637e6a5f41c067ef28a312ab42a2071 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sun, 26 Jun 2022 11:06:17 +0530 Subject: [PATCH 051/258] feat(uf): merge new publisher with old publishers --- src/client/unified-form/cover-tab/cover-tab.tsx | 3 +-- src/client/unified-form/helpers.ts | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/client/unified-form/cover-tab/cover-tab.tsx b/src/client/unified-form/cover-tab/cover-tab.tsx index 652fd1469c..d3e61625e0 100644 --- a/src/client/unified-form/cover-tab/cover-tab.tsx +++ b/src/client/unified-form/cover-tab/cover-tab.tsx @@ -58,10 +58,9 @@ export function CoverTab(props:CoverProps) { } function mapStateToProps(rootState) { - const newPublishers = rootState.getIn(['Publishers'], {}); return { identifierEditorVisible: rootState.getIn(['buttonBar', 'identifierEditorVisible']), - publisherValue: newPublishers.merge(rootState.getIn(['editionSection', 'publisher'], {})) + publisherValue: rootState.getIn(['editionSection', 'publisher'], {}) }; } diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index 78e4916d7e..6005d8dcf3 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -77,7 +77,7 @@ const initialState = Immutable.Map({ languages: Immutable.List([]), matchingNameEditionGroups: [], physicalEnable: true, - publisher: null, + publisher: Immutable.Map({}), releaseDate: '', status: null }), @@ -175,16 +175,24 @@ function crossSliceReducer(state, action) { workSection: intermediateState.get('workSection') }; break; - case ADD_PUBLISHER: - action.payload.value = action.payload.value ?? { - ...activeEntityState, + case ADD_PUBLISHER: { + const newPublisher = { __isNew__: true, id: action.payload.id, publisherSection: intermediateState.get('publisherSection'), text: activeEntityState.nameSection.get('name'), type: 'Publisher' }; + action.payload.value = action.payload.value ?? { + ...activeEntityState, + ...newPublisher + }; + intermediateState = intermediateState.setIn( + ['Editions', 'e0', 'editionSection', 'publisher', newPublisher.id] + , Immutable.Map(newPublisher) + ); break; + } case LOAD_EDITION: { intermediateState = intermediateState.merge(intermediateState.getIn(['Editions', action.payload.id])); From bd8330ffd0118071f0d13ad3cadb18f0fadeee77 Mon Sep 17 00:00:00 2001 From: tri10 Date: Mon, 27 Jun 2022 14:52:00 +0530 Subject: [PATCH 052/258] minor improvements --- src/client/entity-editor/submission-section/actions.ts | 6 +++--- src/client/entity-editor/validators/edition.ts | 10 ++++------ src/client/stylesheets/style.scss | 3 +++ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/client/entity-editor/submission-section/actions.ts b/src/client/entity-editor/submission-section/actions.ts index e561509122..d36e475961 100644 --- a/src/client/entity-editor/submission-section/actions.ts +++ b/src/client/entity-editor/submission-section/actions.ts @@ -116,7 +116,7 @@ function postSubmission(url: string, data: Map): Promise { function transformFormData(data:Record):Record { const newData = {}; const nextId = 0; - // add new publisher + // add new publishers _.forEach(data.Publishers, (publisher, pid) => { if (publisher.__isNew__) { newData[pid] = publisher; @@ -134,7 +134,7 @@ function transformFormData(data:Record):Record { newData[wid] = work; } }); - // add new ediiton groups + // add new ediiton group(s) _.forEach(data.EditionGroups, (editionGroup, egid) => { if (editionGroup.__isNew__) { newData[egid] = editionGroup; @@ -144,7 +144,7 @@ function transformFormData(data:Record):Record { if (data.ISBN.type) { data.identifierEditor.m0 = data.ISBN; } - data.editionSection.publisher = _.merge(_.get(data, ['Publishers'], {}), _.get(data.editionSection, ['publisher'], {})); + data.editionSection.publisher = _.get(data.editionSection, ['publisher'], {}); data.relationshipSection.relationships = _.mapValues(data.Works, (work, key) => { const relationship = { attributeSetId: null, diff --git a/src/client/entity-editor/validators/edition.ts b/src/client/entity-editor/validators/edition.ts index d21ba9c769..b96f68682c 100644 --- a/src/client/entity-editor/validators/edition.ts +++ b/src/client/entity-editor/validators/edition.ts @@ -73,18 +73,16 @@ export function validateEditionSectionEditionGroup(value: any, editionGroupRequi return isCustom ? !editionGroupRequired || Boolean(get(value, 'id', null)) : validateUUID(get(value, 'id', null), editionGroupRequired); } -export function validateEditionSectionPublisher(value: any): boolean { +export function validateEditionSectionPublisher(value: any, isCustom = false): boolean { if (!value) { return true; } const publishers = convertMapToObject(value); - if (!_.isPlainObject(publishers)) { - return false; - } for (const pubId in publishers) { if (Object.prototype.hasOwnProperty.call(publishers, pubId)) { const publisher = publishers[pubId]; - if (!validateUUID(get(publisher, 'id', null), true)) { + const isValid = isCustom ? Boolean(get(publisher, 'id', null)) : validateUUID(get(publisher, 'id', null), true); + if (!isValid) { return false; } } @@ -121,7 +119,7 @@ export function validateEditionSection(data: any, isCustom = false): boolean { get(data, 'editionGroupRequired', null), isCustom ) && - validateEditionSectionPublisher(get(data, 'publisher', null)) && + validateEditionSectionPublisher(get(data, 'publisher', null), isCustom) && validateEditionSectionReleaseDate(get(data, 'releaseDate', null)).isValid && validateEditionSectionStatus(get(data, 'status', null)) && validateEditionSectionWeight(get(data, 'weight', null)) && diff --git a/src/client/stylesheets/style.scss b/src/client/stylesheets/style.scss index 73cb23f537..38ab45f511 100644 --- a/src/client/stylesheets/style.scss +++ b/src/client/stylesheets/style.scss @@ -774,6 +774,9 @@ $uf-primary:#EB743B; .uf-dialog{ max-width: 700px; } +.uf-dialog .modal-dialog{ + margin: 1.75rem 2rem; +} .ac-select { flex-grow: 2; } From 53c8bcea8a757ff90a36e8368214fe50e0793ef0 Mon Sep 17 00:00:00 2001 From: tri10 Date: Mon, 27 Jun 2022 19:16:51 +0530 Subject: [PATCH 053/258] minor test fixes --- src/client/entity-editor/validators/edition.ts | 3 +++ src/client/unified-form/helpers.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/src/client/entity-editor/validators/edition.ts b/src/client/entity-editor/validators/edition.ts index b96f68682c..1056fa5697 100644 --- a/src/client/entity-editor/validators/edition.ts +++ b/src/client/entity-editor/validators/edition.ts @@ -78,6 +78,9 @@ export function validateEditionSectionPublisher(value: any, isCustom = false): b return true; } const publishers = convertMapToObject(value); + if (!_.isPlainObject(publishers)) { + return false; + } for (const pubId in publishers) { if (Object.prototype.hasOwnProperty.call(publishers, pubId)) { const publisher = publishers[pubId]; diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index 6005d8dcf3..91f10b40c7 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -159,6 +159,7 @@ function crossSliceReducer(state, action) { action.payload.value = action.payload.value ?? { ...activeEntityState, __isNew__: true, + authorCreditEditor: intermediateState.get('authorCreditEditor'), editionGroupSection: intermediateState.get('editionGroupSection'), id: action.payload.id, text: activeEntityState.nameSection.get('name'), From f086c0c5f8d630a7fc851bfcf5fb248745cbc1b1 Mon Sep 17 00:00:00 2001 From: tri10 Date: Mon, 27 Jun 2022 21:00:35 +0530 Subject: [PATCH 054/258] fix(uf): change book icon for uf --- .../entity-editor/author-credit-editor/author-credit-row.tsx | 1 + src/client/helpers/entity.tsx | 4 ++-- src/client/stylesheets/style.scss | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/client/entity-editor/author-credit-editor/author-credit-row.tsx b/src/client/entity-editor/author-credit-editor/author-credit-row.tsx index 59fff5e312..cdcebef4fd 100644 --- a/src/client/entity-editor/author-credit-editor/author-credit-row.tsx +++ b/src/client/entity-editor/author-credit-editor/author-credit-row.tsx @@ -112,6 +112,7 @@ function AuthorCreditRow({ Date: Mon, 27 Jun 2022 19:27:15 +0000 Subject: [PATCH 055/258] chore(deps): bump @fortawesome/free-brands-svg-icons Bumps [@fortawesome/free-brands-svg-icons](https://github.com/FortAwesome/Font-Awesome) from 5.15.4 to 6.1.1. - [Release notes](https://github.com/FortAwesome/Font-Awesome/releases) - [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md) - [Commits](https://github.com/FortAwesome/Font-Awesome/compare/5.15.4...6.1.1) --- updated-dependencies: - dependency-name: "@fortawesome/free-brands-svg-icons" dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index cbe22922dc..ba98c6ee1c 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@babel/runtime": "^7.17.7", "@elastic/elasticsearch": "^5.6.22", "@fortawesome/fontawesome-svg-core": "^1.2.30", - "@fortawesome/free-brands-svg-icons": "^5.14.0", + "@fortawesome/free-brands-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^5.14.0", "@fortawesome/react-fontawesome": "^0.1.11", "array-move": "^3.0.1", diff --git a/yarn.lock b/yarn.lock index cd4d58c47a..1e6fa2103d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1277,6 +1277,11 @@ resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-7.2.0.tgz#c5fe4c34f3a3664ac64fe1a21bac2004ea5faa22" integrity sha512-dzjQ0LFT+bPLWg0yyV3MpxaLJp/+VW4a0SnjNSWJ4YpJ928LXDOZAN+kB2/JPPisI3Ra0w2BxbD4M9J7o0jcpw== +"@fortawesome/fontawesome-common-types@6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.1.tgz#7dc996042d21fc1ae850e3173b5c67b0549f9105" + integrity sha512-wVn5WJPirFTnzN6tR95abCx+ocH+3IFLXAgyavnf9hUmN0CfWoDjPT/BAWsUVwSlYYVBeCLJxaqi7ZGe4uSjBA== + "@fortawesome/fontawesome-common-types@^0.2.36": version "0.2.36" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz#b44e52db3b6b20523e0c57ef8c42d315532cb903" @@ -1289,12 +1294,12 @@ dependencies: "@fortawesome/fontawesome-common-types" "^0.2.36" -"@fortawesome/free-brands-svg-icons@^5.14.0": - version "5.15.4" - resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.4.tgz#ec8a44dd383bcdd58aa7d1c96f38251e6fec9733" - integrity sha512-f1witbwycL9cTENJegcmcZRYyawAFbm8+c6IirLmwbbpqz46wyjbQYLuxOc7weXFXfB7QR8/Vd2u5R3q6JYD9g== +"@fortawesome/free-brands-svg-icons@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.1.1.tgz#3580961d4f42bd51dc171842402f23a18a5480b1" + integrity sha512-mFbI/czjBZ+paUtw5NPr2IXjun5KAC8eFqh1hnxowjA4mMZxWz4GCIksq6j9ZSa6Uxj9JhjjDVEd77p2LN2Blg== dependencies: - "@fortawesome/fontawesome-common-types" "^0.2.36" + "@fortawesome/fontawesome-common-types" "6.1.1" "@fortawesome/free-solid-svg-icons@^5.14.0": version "5.15.4" From 03683930d3ddd313e27235a9b57ed681407f23fc Mon Sep 17 00:00:00 2001 From: tri10 Date: Thu, 30 Jun 2022 21:39:24 +0530 Subject: [PATCH 056/258] feat(uf): move submit section to tab section --- .../unified-form/submit-tab/summary.tsx | 60 +++++++++++++++++++ src/client/unified-form/unified-form.tsx | 10 +++- 2 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/client/unified-form/submit-tab/summary.tsx diff --git a/src/client/unified-form/submit-tab/summary.tsx b/src/client/unified-form/submit-tab/summary.tsx new file mode 100644 index 0000000000..04071a50ee --- /dev/null +++ b/src/client/unified-form/submit-tab/summary.tsx @@ -0,0 +1,60 @@ +import {Badge, ListGroup} from 'react-bootstrap'; +import Immutable from 'immutable'; +import React from 'react'; +import _ from 'lodash'; +import {connect} from 'react-redux'; +import {convertMapToObject} from '../../helpers/utils'; + + +type SummarySectionProps = { + Authors: Array, + EditionGroups: Array, + Editions: Array, + Publishers: Array, + Works: Array +}; +function SummarySection({Publishers, Works, Authors, EditionGroups, Editions}:SummarySectionProps) { + const createdEntities = {Authors, EditionGroups, Editions, Publishers, Works}; + function renderEntityGroup(entities:Array, entityType:string) { + return ( + + +
+
{entityType}
+ {entities.map( + (entity, index) => + {entity.nameSection.name + (index === entities.length - 1 ? '' : ', ')} + )} +
+ + {entities.length} + +
+ ); + } + return ( +
+

New Entities

+ + {_.map(createdEntities, renderEntityGroup)} + + +
+ ); +} +function getEntitiesArray(state:Immutable.Map) { + return Object.values(convertMapToObject(state)); +} +function mapStateToProps(state) { + return { + Authors: getEntitiesArray(state.get('Authors')), + EditionGroups: getEntitiesArray(state.get('EditionGroups')), + Editions: getEntitiesArray(state.get('Editions')), + Publishers: getEntitiesArray(state.get('Publishers')), + Works: getEntitiesArray(state.get('Works')) + }; +} +export default connect(mapStateToProps)(SummarySection); diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index 01e2599ba7..5f4d22cc0d 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -6,6 +6,7 @@ import DetailTab from './detail-tab/detail-tab'; import NavButtons from './navbutton'; import React from 'react'; import SubmitSection from '../entity-editor/submission-section/submission-section'; +import SummarySection from './submit-tab/summary'; import {connect} from 'react-redux'; import {filterIdentifierTypesByEntityType} from '../../common/helpers/utils'; import {submit} from '../entity-editor/submission-section/actions'; @@ -24,7 +25,7 @@ export function UnifiedForm(props:UnifiedFormProps) { const {identifierTypes, validators, onSubmit} = props; const [tabKey, setTabKey] = React.useState('cover'); const editionIdentifierTypes = filterIdentifierTypesByEntityType(identifierTypes, 'edition'); - const tabKeys = ['cover', 'content', 'detail']; + const tabKeys = ['cover', 'content', 'detail', 'submit']; const onNextHandler = React.useCallback(() => { const index = tabKeys.indexOf(tabKey); if (index >= 0 && index < tabKeys.length - 1) { @@ -51,14 +52,17 @@ export function UnifiedForm(props:UnifiedFormProps) { + + + + +
- - ); } From 3506af428c6538d7c3e4c0560fbc60bf732d6388 Mon Sep 17 00:00:00 2001 From: tri10 Date: Thu, 30 Jun 2022 22:49:42 +0530 Subject: [PATCH 057/258] render alias button inside row component --- src/client/entity-editor/button-bar/button-bar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/entity-editor/button-bar/button-bar.js b/src/client/entity-editor/button-bar/button-bar.js index 59d870227c..64989ce4d2 100644 --- a/src/client/entity-editor/button-bar/button-bar.js +++ b/src/client/entity-editor/button-bar/button-bar.js @@ -74,8 +74,8 @@ function ButtonBar({ const identifierEditorClass = `btn wrap${!isUf ? '' : ' btn-success'}`; return (
- {renderAliasButton()} + {renderAliasButton()} Date: Fri, 1 Jul 2022 18:34:44 +0530 Subject: [PATCH 058/258] feat(uf): exclude existing entities from summary --- .../unified-form/submit-tab/summary.tsx | 63 +++++++++++++------ src/client/unified-form/unified-form.tsx | 2 +- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/client/unified-form/submit-tab/summary.tsx b/src/client/unified-form/submit-tab/summary.tsx index 04071a50ee..6a75ac1ff7 100644 --- a/src/client/unified-form/submit-tab/summary.tsx +++ b/src/client/unified-form/submit-tab/summary.tsx @@ -6,31 +6,49 @@ import {connect} from 'react-redux'; import {convertMapToObject} from '../../helpers/utils'; +type Entity = { + __isNew__: boolean, + text:string, + id:string +}; type SummarySectionProps = { - Authors: Array, - EditionGroups: Array, - Editions: Array, - Publishers: Array, - Works: Array + Authors: Array; + EditionGroups: Array; + Editions: Array; + Publishers: Array; + Works: Array; }; -function SummarySection({Publishers, Works, Authors, EditionGroups, Editions}:SummarySectionProps) { - const createdEntities = {Authors, EditionGroups, Editions, Publishers, Works}; - function renderEntityGroup(entities:Array, entityType:string) { +function SummarySection({ + Publishers, + Works, + Authors, + EditionGroups, + Editions +}: SummarySectionProps) { + const createdEntities = { + Authors, + EditionGroups, + Editions, + Publishers, + Works + }; + function renderEntityGroup(entities: Array, entityType: string) { + const newEntities = entities.filter((entity) => entity.__isNew__); return ( -
{entityType}
- {entities.map( - (entity, index) => - {entity.nameSection.name + (index === entities.length - 1 ? '' : ', ')} - )} + {newEntities.map((entity, index) => ( + + {_.get(entity, 'text') + (index === newEntities.length - 1 ? '' : ', ')} + + ))}
- {entities.length} + {newEntities.length}
); @@ -41,18 +59,27 @@ function SummarySection({Publishers, Works, Authors, EditionGroups, Editions}:Su {_.map(createdEntities, renderEntityGroup)} -
); } -function getEntitiesArray(state:Immutable.Map) { +function getEntitiesArray(state: Immutable.Map): Array { return Object.values(convertMapToObject(state)); } function mapStateToProps(state) { + let EditionGroups = getEntitiesArray(state.get('EditionGroups')); + const Editions:Entity[] = [{ + __isNew__: true, + id: 'e0', + text: state.getIn(['nameSection', 'name']) + }]; + if (EditionGroups.length === 0) { + EditionGroups = Editions; + EditionGroups[0].id = 'eg0'; + } return { Authors: getEntitiesArray(state.get('Authors')), - EditionGroups: getEntitiesArray(state.get('EditionGroups')), - Editions: getEntitiesArray(state.get('Editions')), + EditionGroups, + Editions, Publishers: getEntitiesArray(state.get('Publishers')), Works: getEntitiesArray(state.get('Works')) }; diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index 5f4d22cc0d..dc1999f2e7 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -24,7 +24,7 @@ function getUfValidator(validator) { export function UnifiedForm(props:UnifiedFormProps) { const {identifierTypes, validators, onSubmit} = props; const [tabKey, setTabKey] = React.useState('cover'); - const editionIdentifierTypes = filterIdentifierTypesByEntityType(identifierTypes, 'edition'); + const editionIdentifierTypes = filterIdentifierTypesByEntityType(identifierTypes, 'Edition'); const tabKeys = ['cover', 'content', 'detail', 'submit']; const onNextHandler = React.useCallback(() => { const index = tabKeys.indexOf(tabKey); From 2ce60ee72e958d32558fa9315d83a98aa7e45ea4 Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 1 Jul 2022 19:02:36 +0530 Subject: [PATCH 059/258] fix(uf): left align AC on edition group --- .../author-credit-editor/author-credit-section.tsx | 9 ++++++--- .../edition-group-section/edition-group-section.tsx | 7 +++++-- src/client/unified-form/common/entity-modal-body.tsx | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx index 0d139d973f..b6ac3a546d 100644 --- a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx +++ b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx @@ -46,7 +46,9 @@ import {validateAuthorCreditSection} from '../validators/common'; type OwnProps = { - isUf?: boolean; + isUf?: boolean, + isLeftAlign?: boolean; + }; type StateProps = { @@ -65,7 +67,7 @@ type DispatchProps = { type Props = OwnProps & StateProps & DispatchProps; function AuthorCreditSection({ - authorCreditEditor, onEditAuthorCredit, onEditorClose, showEditor, onAuthorChange, isEditable, onClearHandler, isUf, ...rest + authorCreditEditor, onEditAuthorCredit, onEditorClose, showEditor, onAuthorChange, isEditable, onClearHandler, isUf, isLeftAlign, ...rest }: Props) { let editor; if (showEditor) { @@ -109,7 +111,7 @@ function AuthorCreditSection({ ); let resCol:any = {md: {offset: 3, span: 6}}; - if (isUf) { + if (isUf || isLeftAlign) { resCol = {lg: {offset: 0, span: 6}}; } const onChangeHandler = React.useCallback((value, action) => { @@ -166,6 +168,7 @@ AuthorCreditSection.propTypes = { }; AuthorCreditSection.defaultProps = { + isLeftAlign: false, isUf: false }; function mapStateToProps(rootState): StateProps { diff --git a/src/client/entity-editor/edition-group-section/edition-group-section.tsx b/src/client/entity-editor/edition-group-section/edition-group-section.tsx index 0fa6ffc1b8..218ce8e136 100644 --- a/src/client/entity-editor/edition-group-section/edition-group-section.tsx +++ b/src/client/entity-editor/edition-group-section/edition-group-section.tsx @@ -44,7 +44,8 @@ type DispatchProps = { type OwnProps = { editionGroupTypes: Array, - isUf?: boolean + isUf?: boolean, + isLeftAlign?:boolean }; type Props = StateProps & DispatchProps & OwnProps; @@ -68,6 +69,7 @@ function EditionGroupSection({ editionGroupTypes, typeValue, isUf, + isLeftAlign, onTypeChange }: Props) { const editionGroupTypesForDisplay = editionGroupTypes.map((type) => ({ @@ -84,7 +86,7 @@ function EditionGroupSection({ return (
{!isUf && heading} - +

All fields optional — leave something blank if you don’t know it @@ -116,6 +118,7 @@ function EditionGroupSection({ } EditionGroupSection.displayName = 'EditionGroupSection'; EditionGroupSection.defaultProps = { + isLeftAlign: false, isUf: false }; diff --git a/src/client/unified-form/common/entity-modal-body.tsx b/src/client/unified-form/common/entity-modal-body.tsx index e4440095bd..8683549d96 100644 --- a/src/client/unified-form/common/entity-modal-body.tsx +++ b/src/client/unified-form/common/entity-modal-body.tsx @@ -54,7 +54,7 @@ function EntityModalBody({onModalSubmit, aliasEditorVisible, identifierEditorVis { React.cloneElement( React.Children.only(children), - {...rest, menuPortalTarget} + {...rest, isLeftAlign: true, menuPortalTarget} ) } From 048334608ab9e5c8c2ae48a4f5f62490d4b06504 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 2 Jul 2022 04:03:05 +0530 Subject: [PATCH 060/258] feat(uf): add alias editor to modal --- .../alias-editor/alias-editor.js | 45 +----------- .../alias-editor/alias-modal-body.tsx | 73 +++++++++++++++++++ .../unified-form/common/entity-modal-body.tsx | 18 +++-- 3 files changed, 90 insertions(+), 46 deletions(-) create mode 100644 src/client/entity-editor/alias-editor/alias-modal-body.tsx diff --git a/src/client/entity-editor/alias-editor/alias-editor.js b/src/client/entity-editor/alias-editor/alias-editor.js index 0600212505..d74f49f0d8 100644 --- a/src/client/entity-editor/alias-editor/alias-editor.js +++ b/src/client/entity-editor/alias-editor/alias-editor.js @@ -16,16 +16,14 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import {Button, Col, Modal, OverlayTrigger, Row, Tooltip} from 'react-bootstrap'; +import {Button, Modal, OverlayTrigger, Tooltip} from 'react-bootstrap'; import {addAliasRow, hideAliasEditor, removeEmptyAliases} from './actions'; -import {faPlus, faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; - -import AliasRow from './alias-row'; +import AliasModalBody from './alias-modal-body'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import PropTypes from 'prop-types'; import React from 'react'; -import classNames from 'classnames'; import {connect} from 'react-redux'; +import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; /** @@ -47,21 +45,10 @@ import {connect} from 'react-redux'; * @returns {ReactElement} React element containing the rendered AliasEditor. */ const AliasEditor = ({ - aliases, languageOptions, - onAddAlias, onClose, show }) => { - const languageOptionsForDisplay = languageOptions.map((language) => ({ - frequency: language.frequency, - label: language.name, - value: language.id - })); - - const noAliasesTextClass = - classNames('text-center', {'d-none': aliases.size}); - const helpText = `Variant names for an entity such as alternate spelling, different script, stylistic representation, acronyms, etc. Refer to the help page for more details and examples.`; const helpIconElement = ( @@ -86,29 +73,7 @@ const AliasEditor = ({ -

-

This entity has no aliases

-
-
- { - aliases.map((alias, rowId) => ( - - )).toArray() - } -
- - - - - + @@ -119,9 +84,7 @@ const AliasEditor = ({ }; AliasEditor.displayName = 'AliasEditor'; AliasEditor.propTypes = { - aliases: PropTypes.object.isRequired, languageOptions: PropTypes.array.isRequired, - onAddAlias: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, show: PropTypes.bool }; diff --git a/src/client/entity-editor/alias-editor/alias-modal-body.tsx b/src/client/entity-editor/alias-editor/alias-modal-body.tsx new file mode 100644 index 0000000000..7aef28bf7b --- /dev/null +++ b/src/client/entity-editor/alias-editor/alias-modal-body.tsx @@ -0,0 +1,73 @@ +import {Button, Col, Row} from 'react-bootstrap'; +import AliasRow from './alias-row'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import Immutable from 'immutable'; +import React from 'react'; +import {addAliasRow} from './actions'; +import classNames from 'classnames'; +import {connect} from 'react-redux'; +import {faPlus} from '@fortawesome/free-solid-svg-icons'; + + +type AliasModalBodyStateProps = { + aliases: Immutable.List; +}; +type AliasModalBodyDispatchProps = { + onAddAlias: () => void; +}; + +type AliasModalBodyOwnProps = { + languageOptions:any[], +}; +type AliasModalBodyProps = AliasModalBodyStateProps & AliasModalBodyDispatchProps & AliasModalBodyOwnProps; + +export const AliasModalBody = ({aliases, onAddAlias, languageOptions}:AliasModalBodyProps) => { + const noAliasesTextClass = + classNames('text-center', {'d-none': aliases.size}); + const languageOptionsForDisplay = languageOptions.map((language) => ({ + frequency: language.frequency, + label: language.name, + value: language.id + })); + return ( + <> +
+

This entity has no aliases

+
+
+ { + aliases.map((alias, rowId) => ( + + )).toArray() + } +
+ + + + + + ); +}; + + +function mapDispatchToProps(dispatch) { + return { + onAddAlias: () => dispatch(addAliasRow()) + }; +} + +function mapStateToProps(rootState) { + return { + aliases: rootState.get('aliasEditor') + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(AliasModalBody); diff --git a/src/client/unified-form/common/entity-modal-body.tsx b/src/client/unified-form/common/entity-modal-body.tsx index 8683549d96..a6ba8c9f42 100644 --- a/src/client/unified-form/common/entity-modal-body.tsx +++ b/src/client/unified-form/common/entity-modal-body.tsx @@ -1,6 +1,6 @@ // import * as Bootstrap from 'react-bootstrap'; import {Accordion, Card} from 'react-bootstrap'; -import AliasEditor from '../../entity-editor/alias-editor/alias-editor'; +import AliasModalBody from '../../entity-editor/alias-editor/alias-modal-body'; import AnnotationSection from '../../entity-editor/annotation-section/annotation-section'; import ButtonBar from '../../entity-editor/button-bar/button-bar'; import IdentifierEditor from '../../entity-editor/identifier-editor/identifier-editor'; @@ -12,7 +12,6 @@ import {connect} from 'react-redux'; type EntityModalBodyStateProps = { - aliasEditorVisible:boolean, identifierEditorVisible:boolean }; @@ -24,7 +23,7 @@ type EntityModalBodyOwnProps = { }; type EntityModalBodyProps = EntityModalBodyOwnProps & EntityModalBodyStateProps; -function EntityModalBody({onModalSubmit, aliasEditorVisible, identifierEditorVisible, children, validate, ...rest}:EntityModalBodyProps) { +function EntityModalBody({onModalSubmit, identifierEditorVisible, children, validate, ...rest}:EntityModalBodyProps) { const [menuPortalTarget, setMenuPortalTarget] = React.useState(null); React.useEffect(() => { // FIXME: need better way to scrolling issue in react select menu https://github.com/JedWatson/react-select/issues/4088 @@ -32,7 +31,6 @@ function EntityModalBody({onModalSubmit, aliasEditorVisible, identifierEditorVis }, []); return (
- Basic @@ -45,6 +43,17 @@ function EntityModalBody({onModalSubmit, aliasEditorVisible, identifierEditorVis + + + Aliases + + + + + + + + Details @@ -95,7 +104,6 @@ EntityModalBody.defaultProps = { function mapStateToProps(rootState): EntityModalBodyStateProps { const state = rootState.get('buttonBar'); return { - aliasEditorVisible: state.get('aliasEditorVisible'), identifierEditorVisible: state.get('identifierEditorVisible') }; } From de7b5989c291a3d1f9bd84b749f9e2ce07cbbf8c Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 2 Jul 2022 05:24:06 +0530 Subject: [PATCH 061/258] feat(uf):add serparate identifier section in modal --- .../alias-editor/alias-editor.js | 8 +-- .../identifier-editor/identifier-editor.js | 50 ++------------ .../identifier-modal-body.tsx | 67 +++++++++++++++++++ .../common/create-entity-modal.tsx | 4 +- .../unified-form/common/entity-modal-body.tsx | 34 +++++----- src/client/unified-form/interface/type.ts | 2 +- src/client/unified-form/unified-form.tsx | 4 +- src/server/helpers/entityRouteUtils.tsx | 2 +- 8 files changed, 95 insertions(+), 76 deletions(-) create mode 100644 src/client/entity-editor/identifier-editor/identifier-modal-body.tsx diff --git a/src/client/entity-editor/alias-editor/alias-editor.js b/src/client/entity-editor/alias-editor/alias-editor.js index d74f49f0d8..590f52f705 100644 --- a/src/client/entity-editor/alias-editor/alias-editor.js +++ b/src/client/entity-editor/alias-editor/alias-editor.js @@ -102,10 +102,4 @@ function mapDispatchToProps(dispatch) { }; } -function mapStateToProps(rootState) { - return { - aliases: rootState.get('aliasEditor') - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(AliasEditor); +export default connect(null, mapDispatchToProps)(AliasEditor); diff --git a/src/client/entity-editor/identifier-editor/identifier-editor.js b/src/client/entity-editor/identifier-editor/identifier-editor.js index 1497d9b299..9295e0ae51 100644 --- a/src/client/entity-editor/identifier-editor/identifier-editor.js +++ b/src/client/entity-editor/identifier-editor/identifier-editor.js @@ -16,16 +16,14 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import {Button, Col, Modal, OverlayTrigger, Row, Tooltip} from 'react-bootstrap'; -import {addIdentifierRow, hideIdentifierEditor, removeEmptyIdentifiers} from './actions'; -import {faPlus, faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; - +import {Button, Modal, OverlayTrigger, Tooltip} from 'react-bootstrap'; +import {hideIdentifierEditor, removeEmptyIdentifiers} from './actions'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import IdentifierRow from './identifier-row'; +import IdentifierModalBody from './identifier-modal-body'; import PropTypes from 'prop-types'; import React from 'react'; -import classNames from 'classnames'; import {connect} from 'react-redux'; +import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; /** @@ -48,15 +46,10 @@ import {connect} from 'react-redux'; * IdentifierEditor. */ const IdentifierEditor = ({ - identifiers, identifierTypes, - onAddIdentifier, onClose, show }) => { - const noIdentifiersTextClass = - classNames('text-center', {'d-none': identifiers.size}); - const helpText = `identity of the entity in other databases and services, such as ISBN, barcode, MusicBrainz ID, WikiData ID, OpenLibrary ID, etc. You can enter either the identifier only (Q2517049) or a full link (https://www.wikidata.org/wiki/Q2517049).`; @@ -82,29 +75,7 @@ const IdentifierEditor = ({ -
-

This entity has no identifiers

-
-
- { - identifiers.map((identifier, rowId) => ( - - )).toArray() - } -
- - - - - +
@@ -116,8 +87,6 @@ const IdentifierEditor = ({ IdentifierEditor.displayName = 'IdentifierEditor'; IdentifierEditor.propTypes = { identifierTypes: PropTypes.array.isRequired, - identifiers: PropTypes.object.isRequired, - onAddIdentifier: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, show: PropTypes.bool }; @@ -125,15 +94,8 @@ IdentifierEditor.defaultProps = { show: false }; -function mapStateToProps(state) { - return { - identifiers: state.get('identifierEditor') - }; -} - function mapDispatchToProps(dispatch) { return { - onAddIdentifier: () => dispatch(addIdentifierRow()), onClose: () => { dispatch(hideIdentifierEditor()); dispatch(removeEmptyIdentifiers()); @@ -141,4 +103,4 @@ function mapDispatchToProps(dispatch) { }; } -export default connect(mapStateToProps, mapDispatchToProps)(IdentifierEditor); +export default connect(null, mapDispatchToProps)(IdentifierEditor); diff --git a/src/client/entity-editor/identifier-editor/identifier-modal-body.tsx b/src/client/entity-editor/identifier-editor/identifier-modal-body.tsx new file mode 100644 index 0000000000..88f2ba60f1 --- /dev/null +++ b/src/client/entity-editor/identifier-editor/identifier-modal-body.tsx @@ -0,0 +1,67 @@ +import {Button, Col, Row} from 'react-bootstrap'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import IdentifierRow from './identifier-row'; +import Immutable from 'immutable'; +import React from 'react'; +import {addIdentifierRow} from './actions'; +import classNames from 'classnames'; +import {connect} from 'react-redux'; +import {faPlus} from '@fortawesome/free-solid-svg-icons'; + + +type IdentifierModalBodyStateProps = { + identifiers: Immutable.Map; +}; +type IdentifierModalBodyDispatchProps = { + onAddIdentifier: () => void +}; +type IdentifierModalBodyOwnProps = { + identifierTypes: Array; +}; +type IdentifierModalBodyProps = IdentifierModalBodyOwnProps & IdentifierModalBodyStateProps & IdentifierModalBodyDispatchProps; + + +export const IdentifierModalBody = ({identifiers, onAddIdentifier, identifierTypes}:IdentifierModalBodyProps) => { + const noIdentifiersTextClass = + classNames('text-center', {'d-none': identifiers.size}); + + return ( + <> +
+

This entity has no identifiers

+
+
+ { + identifiers.map((identifier, rowId) => ( + + )).toArray() + } +
+ + + + + + ); +}; + +function mapStateToProps(state) { + return { + identifiers: state.get('identifierEditor') + }; +} + +function mapDispatchToProps(dispatch) { + return { + onAddIdentifier: () => dispatch(addIdentifierRow()) + }; +} +export default connect(mapStateToProps, mapDispatchToProps)(IdentifierModalBody); diff --git a/src/client/unified-form/common/create-entity-modal.tsx b/src/client/unified-form/common/create-entity-modal.tsx index b8127dad83..1727b3c70d 100644 --- a/src/client/unified-form/common/create-entity-modal.tsx +++ b/src/client/unified-form/common/create-entity-modal.tsx @@ -17,8 +17,8 @@ export default function CreateEntityModal({show, handleClose, handleSubmit, type const heading = `Add ${type}`; const EntitySection = getEntitySection(type); const validate = getValidator(type); - const {identifierTypes} = rest; - const entityIdentifierTypes = filterIdentifierTypesByEntityType(identifierTypes, upperFirst(type)); + const {allIdentifierTypes} = rest; + const entityIdentifierTypes = filterIdentifierTypesByEntityType(allIdentifierTypes, upperFirst(type)); return ( diff --git a/src/client/unified-form/common/entity-modal-body.tsx b/src/client/unified-form/common/entity-modal-body.tsx index a6ba8c9f42..bbf7ecfd3f 100644 --- a/src/client/unified-form/common/entity-modal-body.tsx +++ b/src/client/unified-form/common/entity-modal-body.tsx @@ -2,28 +2,22 @@ import {Accordion, Card} from 'react-bootstrap'; import AliasModalBody from '../../entity-editor/alias-editor/alias-modal-body'; import AnnotationSection from '../../entity-editor/annotation-section/annotation-section'; -import ButtonBar from '../../entity-editor/button-bar/button-bar'; -import IdentifierEditor from '../../entity-editor/identifier-editor/identifier-editor'; +import IdentifierModalBody from '../../entity-editor/identifier-editor/identifier-modal-body'; import NameSection from '../../entity-editor/name-section/name-section'; import React from 'react'; import RelationshipSection from '../../entity-editor/relationship-editor/relationship-section'; import SubmissionSection from '../../entity-editor/submission-section/submission-section'; -import {connect} from 'react-redux'; -type EntityModalBodyStateProps = { - identifierEditorVisible:boolean -}; - type EntityModalBodyOwnProps = { onModalSubmit:(e)=>unknown, entityType:string, validate:(arg)=>unknown children?: React.ReactElement }; -type EntityModalBodyProps = EntityModalBodyOwnProps & EntityModalBodyStateProps; +type EntityModalBodyProps = EntityModalBodyOwnProps; -function EntityModalBody({onModalSubmit, identifierEditorVisible, children, validate, ...rest}:EntityModalBodyProps) { +function EntityModalBody({onModalSubmit, children, validate, ...rest}:EntityModalBodyProps) { const [menuPortalTarget, setMenuPortalTarget] = React.useState(null); React.useEffect(() => { // FIXME: need better way to scrolling issue in react select menu https://github.com/JedWatson/react-select/issues/4088 @@ -38,7 +32,6 @@ function EntityModalBody({onModalSubmit, identifierEditorVisible, children, vali -
@@ -54,6 +47,17 @@ function EntityModalBody({onModalSubmit, identifierEditorVisible, children, vali
+ + + Identifiers + + + + + + + + Details @@ -81,7 +85,6 @@ function EntityModalBody({onModalSubmit, identifierEditorVisible, children, vali - Annotation @@ -101,11 +104,4 @@ function EntityModalBody({onModalSubmit, identifierEditorVisible, children, vali EntityModalBody.defaultProps = { children: null }; -function mapStateToProps(rootState): EntityModalBodyStateProps { - const state = rootState.get('buttonBar'); - return { - identifierEditorVisible: state.get('identifierEditorVisible') - }; -} - -export default connect(mapStateToProps, null)(EntityModalBody); +export default EntityModalBody; diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index 6087061be8..5984dcf65e 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -25,7 +25,7 @@ export type UnifiedFormDispatchProps = { onSubmit: (event:React.FormEvent) =>unknown }; export type UnifiedFormProps = { - identifierTypes?:IdentifierType[], + allIdentifierTypes?:IdentifierType[], validators?:Record, } & UnifiedFormDispatchProps; diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index dc1999f2e7..84468d5db2 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -22,9 +22,9 @@ function getUfValidator(validator) { }; } export function UnifiedForm(props:UnifiedFormProps) { - const {identifierTypes, validators, onSubmit} = props; + const {allIdentifierTypes, validators, onSubmit} = props; const [tabKey, setTabKey] = React.useState('cover'); - const editionIdentifierTypes = filterIdentifierTypesByEntityType(identifierTypes, 'Edition'); + const editionIdentifierTypes = filterIdentifierTypesByEntityType(allIdentifierTypes, 'Edition'); const tabKeys = ['cover', 'content', 'detail', 'submit']; const onNextHandler = React.useCallback(() => { const index = tabKeys.indexOf(tabKey); diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index bf309b7ab6..0aada911c7 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -362,7 +362,7 @@ export function generateUnifiedProps( const submissionUrl = '/create/handler'; const props = Object.assign({ entityType: 'edition', - identifierTypes: res.locals.identifierTypes, + allIdentifierTypes: res.locals.identifierTypes, initialState: initialStateCallback(), isUf: true, languageOptions: res.locals.languages, From e08845d4f2c99836edc9cdeea1c68e588bddcfb2 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 2 Jul 2022 05:30:39 +0530 Subject: [PATCH 062/258] feat(uf): move duplicate suggestions to end --- .../name-section/name-section.js | 29 ++++++++++++------- .../unified-form/common/entity-modal-body.tsx | 2 +- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/client/entity-editor/name-section/name-section.js b/src/client/entity-editor/name-section/name-section.js index e537cddc39..45491c4612 100644 --- a/src/client/entity-editor/name-section/name-section.js +++ b/src/client/entity-editor/name-section/name-section.js @@ -145,7 +145,8 @@ class NameSection extends React.Component { onSortNameChange, onDisambiguationChange, searchResults, - isUf + isUf, + isModal } = this.props; const languageOptionsForDisplay = languageOptions.map((language) => ({ @@ -159,6 +160,16 @@ class NameSection extends React.Component { if (isUf) { lgCol.offset = 0; } + const duplicateSuggestions = !warnIfExists && + !_.isEmpty(searchResults) && + + + If the {_.startCase(entityType)} you want to add appears in the results + below, click on it to inspect it before adding a possible duplicate.
+ Ctrl/Cmd + click to open in a new tab + + +
; return (
{!isUf &&

{`What is the ${_.startCase(entityType)} called?`}

} @@ -211,16 +222,7 @@ class NameSection extends React.Component { { - !warnIfExists && - !_.isEmpty(searchResults) && - - - If the {_.startCase(entityType)} you want to add appears in the results - below, click on it to inspect it before adding a possible duplicate.
- Ctrl/Cmd + click to open in a new tab - - -
+ !isModal && duplicateSuggestions } @@ -266,6 +268,9 @@ class NameSection extends React.Component { /> + { + isModal && duplicateSuggestions + }
); } @@ -276,6 +281,7 @@ NameSection.propTypes = { disambiguationDefaultValue: PropTypes.string, entityType: entityTypeProperty.isRequired, exactMatches: PropTypes.array, + isModal: PropTypes.bool, isUf: PropTypes.bool, languageOptions: PropTypes.array.isRequired, languageValue: PropTypes.number, @@ -295,6 +301,7 @@ NameSection.defaultProps = { action: 'create', disambiguationDefaultValue: null, exactMatches: [], + isModal: false, isUf: false, languageValue: null, searchForExistingEditionGroup: true, diff --git a/src/client/unified-form/common/entity-modal-body.tsx b/src/client/unified-form/common/entity-modal-body.tsx index bbf7ecfd3f..b75484ca84 100644 --- a/src/client/unified-form/common/entity-modal-body.tsx +++ b/src/client/unified-form/common/entity-modal-body.tsx @@ -31,7 +31,7 @@ function EntityModalBody({onModalSubmit, children, validate, ...rest}:EntityModa
- +
From d75714509ed4fd69948dd9b88e1b21388f642e0b Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 2 Jul 2022 07:59:01 +0530 Subject: [PATCH 063/258] fix(uf): only send isUf prop when necessary --- .../author-credit-editor/author-credit-section.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx index b6ac3a546d..a34a0cdc8f 100644 --- a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx +++ b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx @@ -73,8 +73,8 @@ function AuthorCreditSection({ if (showEditor) { editor = ( From d6ae32fef617f562d3f2ae0ce2f0c1a0acfd0b32 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 2 Jul 2022 08:32:36 +0530 Subject: [PATCH 064/258] feat(uf): disable submit tab on invalid form --- .../submission-section/submission-section.js | 4 ++-- src/client/unified-form/interface/type.ts | 8 +++++-- src/client/unified-form/unified-form.tsx | 23 ++++++++++++++----- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/client/entity-editor/submission-section/submission-section.js b/src/client/entity-editor/submission-section/submission-section.js index 1e86fb4706..2f34259f4f 100644 --- a/src/client/entity-editor/submission-section/submission-section.js +++ b/src/client/entity-editor/submission-section/submission-section.js @@ -117,11 +117,11 @@ SubmissionSection.propTypes = { submitted: PropTypes.bool.isRequired }; -function mapStateToProps(rootState, {validate, identifierTypes, isMerge, isUf}) { +function mapStateToProps(rootState, {validate, identifierTypes, isMerge, isUf, formValid = false}) { const state = rootState.get('submissionSection'); return { errorText: state.get('submitError'), - formValid: validate && validate(rootState, identifierTypes, isMerge, isUf), + formValid: formValid || (validate && validate(rootState, identifierTypes, isMerge, isUf)), note: state.get('note'), submitted: state.get('submitted') }; diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index 5984dcf65e..3825966878 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -24,10 +24,14 @@ type LanguageOption = { export type UnifiedFormDispatchProps = { onSubmit: (event:React.FormEvent) =>unknown }; -export type UnifiedFormProps = { +export type UnifiedFormStateProps = { + formValid:boolean +}; +export type UnifiedFormOwnProps = { allIdentifierTypes?:IdentifierType[], validators?:Record, -} & UnifiedFormDispatchProps; +}; +export type UnifiedFormProps = UnifiedFormOwnProps & UnifiedFormDispatchProps & UnifiedFormStateProps; export type CoverOwnProps = { languageOptions?: LanguageOption[], diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index 84468d5db2..b94d9448c4 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -1,5 +1,5 @@ import * as Boostrap from 'react-bootstrap'; -import {IdentifierType, UnifiedFormDispatchProps, UnifiedFormProps} from './interface/type'; +import {IdentifierType, UnifiedFormDispatchProps, UnifiedFormOwnProps, UnifiedFormProps, UnifiedFormStateProps} from './interface/type'; import ContentTab from './content-tab/content-tab'; import CoverTab from './cover-tab/cover-tab'; import DetailTab from './detail-tab/detail-tab'; @@ -22,9 +22,10 @@ function getUfValidator(validator) { }; } export function UnifiedForm(props:UnifiedFormProps) { - const {allIdentifierTypes, validators, onSubmit} = props; + const {allIdentifierTypes, validators, onSubmit, formValid} = props; const [tabKey, setTabKey] = React.useState('cover'); const editionIdentifierTypes = filterIdentifierTypesByEntityType(allIdentifierTypes, 'Edition'); + const editionValidator = validators && getUfValidator(validators.edition); const tabKeys = ['cover', 'content', 'detail', 'submit']; const onNextHandler = React.useCallback(() => { const index = tabKeys.indexOf(tabKey); @@ -38,6 +39,9 @@ export function UnifiedForm(props:UnifiedFormProps) { setTabKey(tabKeys[index - 1]); } }, [tabKey]); + const tabIndex = tabKeys.indexOf(tabKey); + const disableNext = tabIndex === tabKeys.length - 1 || ((tabIndex === tabKeys.length - 2) && !formValid); + const disableBack = tabIndex === 0; return (
@@ -52,21 +56,28 @@ export function UnifiedForm(props:UnifiedFormProps) { - + - +
); } +function mapStateToProps(state, {validators, allIdentifierTypes}:UnifiedFormOwnProps) { + const editionValidator = validators && getUfValidator(validators.edition); + const editionIdentifierTypes = filterIdentifierTypesByEntityType(allIdentifierTypes, 'Edition'); + return { + formValid: editionValidator && editionValidator(state, editionIdentifierTypes, false, true) + }; +} function mapDispatchToProps(dispatch, {submissionUrl}) { return { onSubmit: (event:React.FormEvent) => { @@ -76,4 +87,4 @@ function mapDispatchToProps(dispatch, {submissionUrl}) { }; } -export default connect(null, mapDispatchToProps)(UnifiedForm); +export default connect(mapStateToProps, mapDispatchToProps)(UnifiedForm); From 96dfc3f15bcdbc3e2d956170a209e0540ed6c169 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 2 Jul 2022 10:44:18 +0530 Subject: [PATCH 065/258] fix(uf): menu options get clipped by parent --- src/client/entity-editor/work-section/work-section.tsx | 8 +------- src/client/stylesheets/style.scss | 3 +++ src/client/unified-form/common/entity-modal-body.tsx | 7 +------ 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/client/entity-editor/work-section/work-section.tsx b/src/client/entity-editor/work-section/work-section.tsx index 3fb76eb88d..56940513c2 100644 --- a/src/client/entity-editor/work-section/work-section.tsx +++ b/src/client/entity-editor/work-section/work-section.tsx @@ -57,7 +57,6 @@ type DisplayLanguageOption = { type OwnProps = { isUf?: boolean, languageOptions: Array, - menuPortalTarget?: HTMLElement, workTypes: Array }; @@ -91,7 +90,6 @@ function WorkSection({ languageValues, typeValue, workTypes, - menuPortalTarget, isUf, onLanguagesChange, onTypeChange @@ -137,12 +135,9 @@ function WorkSection({ { const index = tabKeys.indexOf(tabKey); if (index >= 0 && index < tabKeys.length - 1) { From 046df6b658568791d18b080a2f1bffd605a93797 Mon Sep 17 00:00:00 2001 From: tri10 Date: Tue, 5 Jul 2022 19:56:21 +0530 Subject: [PATCH 079/258] feat(uf): copy authors from AC to new work --- .../submission-section/actions.ts | 22 ++++++ src/client/stylesheets/style.scss | 5 ++ src/client/unified-form/content-tab/action.ts | 44 +++++++++-- .../unified-form/content-tab/content-tab.tsx | 66 +++++++++-------- .../unified-form/content-tab/reducer.ts | 8 +- .../unified-form/content-tab/work-row.tsx | 74 +++++++++++++++++++ src/client/unified-form/interface/type.ts | 6 +- 7 files changed, 183 insertions(+), 42 deletions(-) create mode 100644 src/client/unified-form/content-tab/work-row.tsx diff --git a/src/client/entity-editor/submission-section/actions.ts b/src/client/entity-editor/submission-section/actions.ts index d36e475961..5cea76d48e 100644 --- a/src/client/entity-editor/submission-section/actions.ts +++ b/src/client/entity-editor/submission-section/actions.ts @@ -130,6 +130,28 @@ function transformFormData(data:Record):Record { }); // add new works _.forEach(data.Works, (work, wid) => { + if (work.checked && work.__isNew__) { + let relationshipCount = 0; + _.forEach(data.authorCreditEditor, (authorCredit) => { + const relationship = { + attributeSetId: null, + attributes: [], + relationshipType: { + id: 8 + }, + rowId: `a${relationshipCount}`, + sourceEntity: { + bbid: authorCredit.author.id + }, + targetEntity: { + bbid: wid + } + }; + work.relationshipSection.relationships[`a${relationshipCount}`] = relationship; + relationshipCount++; + }); + } + if (work.__isNew__) { newData[wid] = work; } diff --git a/src/client/stylesheets/style.scss b/src/client/stylesheets/style.scss index 452e51dfe1..060a71cebe 100644 --- a/src/client/stylesheets/style.scss +++ b/src/client/stylesheets/style.scss @@ -808,4 +808,9 @@ div[class='card-header'] .accordion-arrow { } div[class~='show'] + div[class='card-header'] .accordion-arrow { transform: rotate(90deg); +} + +.work-item{ + border-bottom: 1px solid #EAE7E5; + margin-bottom: 0.5rem; } \ No newline at end of file diff --git a/src/client/unified-form/content-tab/action.ts b/src/client/unified-form/content-tab/action.ts index ae0507070e..7be199213c 100644 --- a/src/client/unified-form/content-tab/action.ts +++ b/src/client/unified-form/content-tab/action.ts @@ -1,9 +1,11 @@ import {Action} from '../interface/type'; -import {size} from 'lodash'; export const ADD_WORK = 'ADD_WORK'; export const UPDATE_WORKS = 'UPDATE_WORKS'; +export const REMOVE_WORK = 'REMOVE_WORK'; +export const UPDATE_WORK = 'UPDATE_WORK'; +export const TOGGLE_CHECK = 'TOGGLE_CHECK'; let nextWorkId = 0; @@ -21,15 +23,41 @@ export function addWork(value = null):Action { } /** - * Produces an action indicating that `Works` state should be updated. + * Produces an action indicating that a Work should be removed from `Works`. * - * @param {Object} works - All Works. - * @returns {Action} The resulting UPDATE_WORKS action. + * @param {string} id - id of the work to be removed + * @returns {Action} The resulting REMOVE_WORK action. */ -export function updateWorks(works:Record):Action { - nextWorkId = size(works); +export function removeWork(id:string):Action { return { - payload: works, - type: UPDATE_WORKS + payload: id, + type: REMOVE_WORK + }; +} + +/** + * Produces an action indicating that a Work should be updated in `Works`. + * + * @param {string} id - id of work to be updated + * @param {Object} value - updated work state. + * @returns {Action} The resulting UPDATE_WORK action. + */ +export function updateWork(id:string, value):Action { + return { + payload: {id, value}, + type: UPDATE_WORK + }; +} + +/** + * Produces an action indicating that a Work's checkbox should be toggled in `Works`. + * + * @param {string} id - id of the work to be toggle + * @returns {Action} The resulting TOGGLE_CHECK action. + */ +export function toggleCheck(id:string):Action { + return { + payload: id, + type: TOGGLE_CHECK }; } diff --git a/src/client/unified-form/content-tab/content-tab.tsx b/src/client/unified-form/content-tab/content-tab.tsx index d043f9973a..59e7b6c1bc 100644 --- a/src/client/unified-form/content-tab/content-tab.tsx +++ b/src/client/unified-form/content-tab/content-tab.tsx @@ -2,50 +2,56 @@ import * as Bootstrap from 'react-bootstrap/'; import {ContentTabDispatchProps, ContentTabProps, ContentTabStateProps, State} from '../interface/type'; import React from 'react'; import SearchEntityCreate from '../common/search-entity-create-select'; +import WorkRow from './work-row'; +import {addWork} from './action'; import {connect} from 'react-redux'; import {convertMapToObject} from '../../helpers/utils'; -import {reduce} from 'lodash'; -import {updateWorks} from './action'; +import {map} from 'lodash'; -const {Row, Col} = Bootstrap; -export function ContentTab({value, onChange, nextId, ...rest}:ContentTabProps) { +const {Row, Col, FormCheck} = Bootstrap; +export function ContentTab({value, onChange, ...rest}:ContentTabProps) { + const [isChecked, setIsChecked] = React.useState(false); + const toggleIsChecked = React.useCallback(() => setIsChecked(!isChecked), [isChecked]); + const onChangeHandler = React.useCallback((work:any) => { + work.checked = isChecked; + onChange(work); + }, [isChecked, onChange]); return ( - - - - - - ); + <> +

Works

+ {map(value, (work, rowId) => )} + + + + + + + + ); } function mapStateToProps(rootState:State) { const worksObj = convertMapToObject(rootState.get('Works')); - // get next id for new work - const nextId = reduce(worksObj, (prev, value) => (value.__isNew__ ? prev + 1 : prev), 0); return { - nextId, - value: Object.values(worksObj) + value: worksObj }; } - function mapDispatchToProps(dispatch) { return { - onChange: (options:any[]) => { - const mappedOptions = Object.fromEntries(options.map((value, index) => { - value.__isNew__ = Boolean(value.__isNew__); - return [`w${index}`, value]; - })); - return dispatch(updateWorks(mappedOptions)); - } + onChange: (value:any) => dispatch(addWork(value)) }; } diff --git a/src/client/unified-form/content-tab/reducer.ts b/src/client/unified-form/content-tab/reducer.ts index 23f816290d..7ee5dc1d38 100644 --- a/src/client/unified-form/content-tab/reducer.ts +++ b/src/client/unified-form/content-tab/reducer.ts @@ -1,4 +1,4 @@ -import {ADD_WORK, UPDATE_WORKS} from './action'; +import {ADD_WORK, REMOVE_WORK, TOGGLE_CHECK, UPDATE_WORK, UPDATE_WORKS} from './action'; import {Action, State} from '../interface/type'; import Immutable from 'immutable'; @@ -11,6 +11,12 @@ export default function reducer(state = Immutable.Map(initialState), {type, payl return state.set(payload.id, Immutable.fromJS(payload.value)); case UPDATE_WORKS: return Immutable.fromJS(payload); + case REMOVE_WORK: + return state.delete(payload); + case UPDATE_WORK: + return state.set(payload.id, Immutable.fromJS(payload.value)); + case TOGGLE_CHECK: + return state.setIn([payload, 'checked'], !state.getIn([payload.id, 'checked'])); default: return state; } diff --git a/src/client/unified-form/content-tab/work-row.tsx b/src/client/unified-form/content-tab/work-row.tsx new file mode 100644 index 0000000000..d96dd01ac1 --- /dev/null +++ b/src/client/unified-form/content-tab/work-row.tsx @@ -0,0 +1,74 @@ +import * as Bootstrap from 'react-bootstrap/'; +import {removeWork, toggleCheck, updateWork} from './action'; +import React from 'react'; +import SearchEntityCreate from '../common/search-entity-create-select'; +import {connect} from 'react-redux'; +import {convertMapToObject} from '../../helpers/utils'; + + +const {Row, Col, Button, FormCheck} = Bootstrap; +type WorkRowStateProps = { + work: any; +}; +type WorkRowDispatchProps = { + onChange: (value:any) => void; + onRemove: () => void; + onToggle: () => void; +}; + +type WorkRowOwnProps = { + rowId: string; +}; + +type WorkRowProps = WorkRowStateProps & WorkRowDispatchProps & WorkRowOwnProps; + +function WorkRow({onChange, work, onRemove, onToggle, ...rest}:WorkRowProps) { + const isChecked = work?.checked; + const onChangeHandler = React.useCallback((value:any) => { + value.checked = isChecked; + onChange(value); + }, [isChecked, onChange]); + // TODO: Add author to exisiting work from AC + return ( +
+ + + + + + + + + {work.__isNew__ && } +
+ ); +} + +function mapStateToProps(state, {rowId}) { + return { + work: convertMapToObject(state.getIn(['Works', rowId])) + }; +} + + +function mapDispatchToProps(dispatch, {rowId}) { + return { + onChange: (value:any) => dispatch(updateWork(rowId, value)), + onRemove: () => dispatch(removeWork(rowId)), + onToggle: () => dispatch(toggleCheck(rowId)) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(WorkRow); diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index b177bc682f..93e4021705 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -74,11 +74,10 @@ export type EntitySelect = { id:string }; export type ContentTabStateProps = { - nextId:string | number, value:any | any[] }; export type ContentTabDispatchProps = { - onChange:(arg:EntitySelect|EntitySelect[])=>unknown + onChange:(value:EntitySelect)=>unknown, }; export type ContentTabProps = ContentTabStateProps & ContentTabDispatchProps; @@ -97,11 +96,12 @@ export type SearchEntityCreateDispatchProps = { export type SearchEntityCreateOwnProps = { bbid?:string, empty?:boolean, + isClearable?:boolean, isMulti?:boolean, nextId?:string|number, error?:boolean, filters?:Array, - label:string, + label?:string, tooltipText?:string, languageOptions?:Array, value?:Array | EntitySelect From 64f2b5bc2089e158fc9c3a545616254fd5a784ed Mon Sep 17 00:00:00 2001 From: tri10 Date: Tue, 5 Jul 2022 21:51:38 +0530 Subject: [PATCH 080/258] remove accordion from details tab --- .../unified-form/detail-tab/detail-tab.tsx | 17 +---------------- src/client/unified-form/submit-tab/summary.tsx | 3 +++ 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/client/unified-form/detail-tab/detail-tab.tsx b/src/client/unified-form/detail-tab/detail-tab.tsx index 0f65922304..5d60359bd1 100644 --- a/src/client/unified-form/detail-tab/detail-tab.tsx +++ b/src/client/unified-form/detail-tab/detail-tab.tsx @@ -1,27 +1,12 @@ -import * as Bootstrap from 'react-bootstrap'; import AnnotationSection from '../../entity-editor/annotation-section/annotation-section'; import EditionSection from '../../entity-editor/edition-section/edition-section'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import React from 'react'; -import {faChevronRight} from '@fortawesome/free-solid-svg-icons'; -const {Card, Accordion} = Bootstrap; export function DetailTab(props) { return (
- - - - - - - - Extra info - - - - +
); } diff --git a/src/client/unified-form/submit-tab/summary.tsx b/src/client/unified-form/submit-tab/summary.tsx index 6a75ac1ff7..9953493832 100644 --- a/src/client/unified-form/submit-tab/summary.tsx +++ b/src/client/unified-form/submit-tab/summary.tsx @@ -33,6 +33,9 @@ function SummarySection({ Works }; function renderEntityGroup(entities: Array, entityType: string) { + if (entities.length === 0) { + return null; + } const newEntities = entities.filter((entity) => entity.__isNew__); return ( Date: Tue, 5 Jul 2022 22:09:47 +0530 Subject: [PATCH 081/258] improve accordion on modal --- src/client/stylesheets/style.scss | 4 ++ .../unified-form/common/entity-modal-body.tsx | 38 +++++++++---------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/client/stylesheets/style.scss b/src/client/stylesheets/style.scss index 060a71cebe..091949e4c0 100644 --- a/src/client/stylesheets/style.scss +++ b/src/client/stylesheets/style.scss @@ -813,4 +813,8 @@ div[class~='show'] + div[class='card-header'] .accordion-arrow { .work-item{ border-bottom: 1px solid #EAE7E5; margin-bottom: 0.5rem; +} + +.uf-modal-body .accordion > .card{ + margin-bottom: 10px; } \ No newline at end of file diff --git a/src/client/unified-form/common/entity-modal-body.tsx b/src/client/unified-form/common/entity-modal-body.tsx index bbf529cb5a..7c5341deb9 100644 --- a/src/client/unified-form/common/entity-modal-body.tsx +++ b/src/client/unified-form/common/entity-modal-body.tsx @@ -16,10 +16,10 @@ import {removeEmptyIdentifiers} from '../../entity-editor/identifier-editor/acti function EntityModalBody({onModalSubmit, children, validate, onAliasClose, onIdentifierClose, ...rest}:EntityModalBodyProps) { const genericProps:any = omit(rest, ['allIdentifierTypes']); return ( -
- + + - Basic + Name @@ -28,45 +28,45 @@ function EntityModalBody({onModalSubmit, children, validate, onAliasClose, onIde - + - Aliases + Details - + { + React.cloneElement( + React.Children.only(children), + {...rest, isLeftAlign: true} + ) + } - + - Identifiers + Aliases - + - + - Details + Identifiers - { - React.cloneElement( - React.Children.only(children), - {...rest, isLeftAlign: true} - ) - } + - + Relationships @@ -77,7 +77,7 @@ function EntityModalBody({onModalSubmit, children, validate, onAliasClose, onIde - + Annotation From 851544034530484f0dc60dc6487228f1bc6e180f Mon Sep 17 00:00:00 2001 From: tri10 Date: Wed, 6 Jul 2022 21:03:17 +0530 Subject: [PATCH 082/258] feat(uf): add wrapper for bootstrap accordion --- src/client/stylesheets/style.scss | 7 +- .../unified-form/common/entity-modal-body.tsx | 96 +++++-------------- .../unified-form/common/single-accordion.tsx | 33 +++++++ 3 files changed, 61 insertions(+), 75 deletions(-) create mode 100644 src/client/unified-form/common/single-accordion.tsx diff --git a/src/client/stylesheets/style.scss b/src/client/stylesheets/style.scss index 091949e4c0..c827b8c844 100644 --- a/src/client/stylesheets/style.scss +++ b/src/client/stylesheets/style.scss @@ -790,6 +790,7 @@ $uf-primary:#EB743B; overflow:visible; } +// Adding icon to accordion header .accordion > .icon-card{ display: flex; overflow: visible; @@ -803,13 +804,13 @@ $uf-primary:#EB743B; justify-content: space-between; } div[class='card-header'] .accordion-arrow { - transition: all 0.5s ease; + transition: all 0.3s linear; transform: rotate(0deg); } -div[class~='show'] + div[class='card-header'] .accordion-arrow { +div[class~='show'] + div[class='card-header'] .accordion-arrow, +div[class~=collapsing]+div[class=card-header] .accordion-arrow { transform: rotate(90deg); } - .work-item{ border-bottom: 1px solid #EAE7E5; margin-bottom: 0.5rem; diff --git a/src/client/unified-form/common/entity-modal-body.tsx b/src/client/unified-form/common/entity-modal-body.tsx index 7c5341deb9..838c2a7864 100644 --- a/src/client/unified-form/common/entity-modal-body.tsx +++ b/src/client/unified-form/common/entity-modal-body.tsx @@ -1,4 +1,3 @@ -import {Accordion, Card} from 'react-bootstrap'; import {EntityModalBodyProps, EntityModalDispatchProps} from '../interface/type'; import AliasModalBody from '../../entity-editor/alias-editor/alias-modal-body'; import AnnotationSection from '../../entity-editor/annotation-section/annotation-section'; @@ -6,6 +5,7 @@ import IdentifierModalBody from '../../entity-editor/identifier-editor/identifie import NameSection from '../../entity-editor/name-section/name-section'; import React from 'react'; import RelationshipSection from '../../entity-editor/relationship-editor/relationship-section'; +import SingleAccordion from './single-accordion'; import SubmissionSection from '../../entity-editor/submission-section/submission-section'; import {connect} from 'react-redux'; import {omit} from 'lodash'; @@ -17,77 +17,29 @@ function EntityModalBody({onModalSubmit, children, validate, onAliasClose, onIde const genericProps:any = omit(rest, ['allIdentifierTypes']); return ( - - - Name - - - - - - - - - - - Details - - - - { - React.cloneElement( - React.Children.only(children), - {...rest, isLeftAlign: true} - ) - } - - - - - - - Aliases - - - - - - - - - - - Identifiers - - - - - - - - - - - Relationships - - - - - - - - - - - Annotation - - - - - - - - + + + + + { + React.cloneElement( + React.Children.only(children), + {...rest, isLeftAlign: true} + ) + } + + + + + + + + + + + + + diff --git a/src/client/unified-form/common/single-accordion.tsx b/src/client/unified-form/common/single-accordion.tsx new file mode 100644 index 0000000000..4c3dd64fdd --- /dev/null +++ b/src/client/unified-form/common/single-accordion.tsx @@ -0,0 +1,33 @@ +import {Accordion, Card} from 'react-bootstrap'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import React from 'react'; +import {faChevronRight} from '@fortawesome/free-solid-svg-icons'; + + +type SingleAccordionProps = { + children: React.ReactNode, + defaultActive?: boolean, + onToggle?: () => void, + heading: string +}; +export default function SingleAccordion({children, defaultActive, heading, onToggle}:SingleAccordionProps) { + return ( + + + + + {children} + + + {heading} + + + + + ); +} + +SingleAccordion.defaultProps = { + defaultActive: false, + onToggle: null +}; From b550d83ad894fe3e675114ffe9bfdbd4cee2c4c6 Mon Sep 17 00:00:00 2001 From: tri10 Date: Wed, 6 Jul 2022 21:21:26 +0530 Subject: [PATCH 083/258] fix(route): process relationships sequentially --- src/server/routes/entity/process-unified-form.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/server/routes/entity/process-unified-form.ts b/src/server/routes/entity/process-unified-form.ts index 5ded8395e5..3f0819d839 100644 --- a/src/server/routes/entity/process-unified-form.ts +++ b/src/server/routes/entity/process-unified-form.ts @@ -219,9 +219,10 @@ export function handleCreateMultipleEntities( await Object.keys(body).reduce((promise, entityKey) => promise.then(() => processEntity(entityKey)), Promise.resolve()); // adding relationship on newly created entites - await Promise.all(Object.keys(allRelationships).map((entityId) => processRelationship( + await Object.keys(allRelationships).reduce((promise, entityId) => promise.then(() => processRelationship( allRelationships[entityId], savedMainEntities[entityId], bbidMap, editorJSON.id, orm, transacting - ))); + )), Promise.resolve()); + return savedMainEntities; } catch (err) { From becff4d6a280485f30007c8df23c55243504518b Mon Sep 17 00:00:00 2001 From: tri10 Date: Thu, 7 Jul 2022 10:03:06 +0530 Subject: [PATCH 084/258] feat(route): edit existing entity through uf-route --- src/server/helpers/entityRouteUtils.tsx | 7 ++ .../routes/entity/process-unified-form.ts | 79 +++++++++++-- test/src/server/routes/unifiedform.js | 108 ++++++++++++------ test/test-helpers/create-entities.js | 15 ++- 4 files changed, 166 insertions(+), 43 deletions(-) diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index c8427567c2..98ad95160e 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -35,6 +35,7 @@ import ReactDOMServer from 'react-dom/server'; import _ from 'lodash'; import {createStore} from 'redux'; import {generateProps} from './props'; +import {isValidBBID} from '../../common/helpers/utils'; const {createRootReducer, getEntitySection, getEntitySectionMerge, getValidator} = entityEditorHelpers; @@ -328,6 +329,12 @@ function validateUnifiedForm(body:Record):boolean { if (Object.prototype.hasOwnProperty.call(body, entityKey)) { const entityForm = body[entityKey]; const entityType = _.camelCase(entityForm.type); + const isNew = _.get(entityForm, '__isNew__', true); + const bbid = _.get(entityForm, 'id', null); + // for existing entity, it must have id attribute set to its bbid + if (!isNew && (!bbid || !isValidBBID(bbid))) { + return false; + } if (!entityType || !validEntityTypes.includes(entityType)) { return false; } diff --git a/src/server/routes/entity/process-unified-form.ts b/src/server/routes/entity/process-unified-form.ts index 3f0819d839..ac2ff0e865 100644 --- a/src/server/routes/entity/process-unified-form.ts +++ b/src/server/routes/entity/process-unified-form.ts @@ -8,6 +8,7 @@ import { import type { EntityTypeString } from 'bookbrainz-data/lib/func/types'; +import {FormSubmissionError} from '../../../common/helpers/error'; import _ from 'lodash'; import {_bulkIndexEntities} from '../../../common/helpers/search'; import {transformNewForm as authorTransform} from './author'; @@ -44,6 +45,47 @@ const additionalEntityProps = { }; +const baseRelations = [ + 'aliasSet.aliases.language', + 'annotation.lastRevision', + 'defaultAlias', + 'disambiguation', + 'identifierSet.identifiers.type', + 'relationshipSet.relationships.type', + 'revision.revision', + 'collections.owner' +]; + +const additionalEntityAttributes = { + author: ['authorType', 'gender', 'beginArea', 'endArea'], + edition: [ + 'authorCredit.names.author.defaultAlias', + 'editionGroup.defaultAlias', + 'languageSet.languages', + 'editionFormat', + 'editionStatus', + 'releaseEventSet.releaseEvents', + 'publisherSet.publishers.defaultAlias' + ], + editionGroup: [ + 'authorCredit.names.author.defaultAlias', + 'editionGroupType', + 'editions.defaultAlias', + 'editions.disambiguation', + 'editions.releaseEventSet.releaseEvents', + 'editions.identifierSet.identifiers.type', + 'editions.editionFormat' + ], + publisher: ['publisherType', 'area'], + series: [ + 'defaultAlias', + 'disambiguation', + 'seriesOrderingType', + 'identifierSet.identifiers.type' + ], + work: ['workType', 'languageSet.languages'] +}; + export async function processAchievement(orm, editorId, entityJSON) { const {revisionId} = entityJSON; try { @@ -64,7 +106,7 @@ export function transformForm(body:Record):Record { if (Object.prototype.hasOwnProperty.call(body, keyIndex)) { const currentForm = body[keyIndex]; const transformedForm = transformFunctions[_.camelCase(currentForm.type)](currentForm); - modifiedForm[keyIndex] = {type: currentForm.type, ...transformedForm}; + modifiedForm[keyIndex] = {__isNew__: currentForm.__isNew__, id: currentForm.id, type: currentForm.type, ...transformedForm}; } } return modifiedForm; @@ -134,6 +176,10 @@ async function processRelationship(rels:Record[], mainEntity, bbidM } } +function getEntityRelations(type:EntityTypeString) { + return [...baseRelations, ...additionalEntityAttributes[_.camelCase(type)]]; +} + export function handleCreateMultipleEntities( req: PassportRequest, res: $Response @@ -144,6 +190,7 @@ export function handleCreateMultipleEntities( const {body}: {body: Record} = req; let currentEntity: { + __isNew__: boolean | undefined, aliasSet: {id: number} | null | undefined, annotation: {id: number} | null | undefined, bbid: string, @@ -176,9 +223,17 @@ export function handleCreateMultipleEntities( } } allRelationships[entityKey] = entityForm.relationships; - const newEntity = await new Entity({type: entityType}).save(null, {transacting}); - currentEntity = newEntity.toJSON(); - + const isNew = _.get(entityForm, '__isNew__', true); + if (isNew) { + const newEntity = await new Entity({type: entityType}).save(null, {transacting}); + currentEntity = newEntity.toJSON(); + } + else { + currentEntity = await orm.func.entity.getEntity(orm, entityType, entityForm.id, getEntityRelations(entityType as EntityTypeString)); + if (!currentEntity) { + throw new FormSubmissionError('Entity with given id not found'); + } + } // create new revision for each entity const newRevision = await new Revision({ authorId: editorJSON.id, @@ -186,18 +241,22 @@ export function handleCreateMultipleEntities( }).save(null, {transacting}); const additionalProps = _.pick(entityForm, additionalEntityProps[_.camelCase(entityType)]); const changedProps = await getChangedProps( - orm, transacting, true, currentEntity, entityForm, entityType, + orm, transacting, isNew, currentEntity, entityForm, entityType, newRevision, additionalProps ); + // If there are no differences, bail + if (_.isEmpty(changedProps)) { + throw new FormSubmissionError('No Updated Field'); + } const mainEntity = await fetchOrCreateMainEntity( - orm, transacting, true, currentEntity.bbid, entityType + orm, transacting, isNew, currentEntity.bbid, entityType ); - mainEntity.shouldInsert = true; + mainEntity.shouldInsert = isNew; // set changed attributes on main entity _.forOwn(changedProps, (value, key) => mainEntity.set(key, value)); const savedMainEntity = await saveEntitiesAndFinishRevision( - orm, transacting, true, newRevision, mainEntity, [mainEntity], + orm, transacting, isNew, newRevision, mainEntity, [mainEntity], editorJSON.id, entityForm.note ); @@ -213,6 +272,10 @@ export function handleCreateMultipleEntities( } bbidMap[entityKey] = savedMainEntity.get('bbid'); savedMainEntities[entityKey] = savedMainEntity.toJSON(); + // getNextRelationshipSet expects relationshipSet.id to exist + if (!isNew) { + _.set(savedMainEntities[entityKey], ['relationshipSet', 'id'], savedMainEntities[entityKey].relationshipSetId); + } } try { // bookshelf's transaction have issue with Promise.All, refer https://github.com/bookshelf/bookshelf/issues/1498 for more details diff --git a/test/src/server/routes/unifiedform.js b/test/src/server/routes/unifiedform.js index ef4a959f09..a6e3410081 100644 --- a/test/src/server/routes/unifiedform.js +++ b/test/src/server/routes/unifiedform.js @@ -1,6 +1,6 @@ -import {baseState, createAuthor, createEditionGroup, createEditor, - createPublisher, createWork, getRandomUUID, languageAttribs, truncateEntities} from '../../../test-helpers/create-entities'; -import {every, forOwn, map} from 'lodash'; +import {authorWorkRelationshipTypeData, baseState, createAuthor, createEditionGroup, + createEditor, createPublisher, createWork, getRandomUUID, truncateEntities} from '../../../test-helpers/create-entities'; +import {every, forOwn, get, map} from 'lodash'; import app from '../../../../src/server/app'; import chai from 'chai'; import chaiHttp from 'chai-http'; @@ -8,7 +8,7 @@ import {getEntityByBBID} from '../../../../src/common/helpers/utils'; import orm from '../../../bookbrainz-data'; -const {Language, RelationshipType} = orm; +const {RelationshipType} = orm; const relationshipTypeData = { description: 'test descryption', label: 'test label', @@ -36,8 +36,9 @@ function testDefaultAlias(entity, languageId) { describe('Unified form routes', () => { let agent; - let newLanguage; + const languageId = 42; let newRelationshipType; + let authorWorkRelationshipType; const wBBID = getRandomUUID(); const pBBID = getRandomUUID(); const egBBID = getRandomUUID(); @@ -49,10 +50,10 @@ describe('Unified form routes', () => { await createPublisher(pBBID); await createEditionGroup(egBBID); await createAuthor(aBBID); - newLanguage = await new Language({...languageAttribs}) - .save(null, {method: 'insert'}); newRelationshipType = await new RelationshipType(relationshipTypeData) .save(null, {method: 'insert'}); + authorWorkRelationshipType = await new RelationshipType(authorWorkRelationshipTypeData) + .save(null, {method: 'insert'}); } catch (error) { // console.log(error); @@ -68,18 +69,82 @@ describe('Unified form routes', () => { editionSection: {}, type: 'Edition' }}; - postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(1); const editionEntity = createdEntities[0]; const fetchedEditionEntity = await getEntityByBBID(orm, editionEntity.bbid); expect(Boolean(fetchedEditionEntity)).to.be.true; - expect(testDefaultAlias(fetchedEditionEntity, newLanguage.id)).to.be.true; + expect(testDefaultAlias(fetchedEditionEntity, languageId)).to.be.true; + expect(res).to.be.ok; + expect(res).to.have.status(200); + }); + it('should not throw error while editing single entity', async () => { + const postData = {b0: { + ...baseState, + __isNew__: false, + id: wBBID, + type: 'Work', + workSection: { + languages: [], + type: null + } + }}; + const newName = 'changedName'; + postData.b0.nameSection.name = newName; + const res = await agent.post('/create/handler').send(postData); + const editEntities = res.body; + expect(editEntities.length).equal(1); + const workEntity = editEntities[0]; + const fetchedworkEntity = await getEntityByBBID(orm, workEntity.bbid); + expect(Boolean(fetchedworkEntity)).to.be.true; + expect(fetchedworkEntity.defaultAlias.name).to.be.equal(newName); + expect(res).to.be.ok; + expect(res).to.have.status(200); + }); + it('should not throw error while adding relationship to single entity', async () => { + // we need to pass extra id and __isNew__ attributes + const postData = {b0: { + ...baseState, + __isNew__: false, + id: wBBID, + relationshipSection: { + relationships: { + a0: { + attributeSetId: null, + attributes: [], + relationshipType: { + id: authorWorkRelationshipType.id + }, + rowId: 'a0', + sourceEntity: { + bbid: aBBID + }, + targetEntity: { + bbid: wBBID + } + } + } + }, + type: 'Work', + workSection: { + languages: [], + type: null + } + }}; + const res = await agent.post('/create/handler').send(postData); + const editEntities = res.body; + expect(editEntities.length).equal(1); + const workEntity = editEntities[0]; + const fetchedworkEntity = await getEntityByBBID(orm, workEntity.bbid); + expect(Boolean(fetchedworkEntity)).to.be.true; + const relationships = get(fetchedworkEntity, ['relationshipSet', 'relationships'], []); + expect(relationships.length).to.be.equal(1); + expect(get(relationships[0], 'targetBbid')).to.be.equal(wBBID); + expect(get(relationships[0], 'sourceBbid')).to.be.equal(aBBID); expect(res).to.be.ok; expect(res).to.have.status(200); }); - it('should not throw error while creating multiple entities', async () => { const postData = {b0: { ...baseState, @@ -94,15 +159,12 @@ describe('Unified form routes', () => { type: null } }}; - forOwn(postData, (value) => { - value.nameSection.language = newLanguage.id; - }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(2); const conditions = await map(createdEntities, async (entity) => { const fetchedEntity = await getEntityByBBID(orm, entity.bbid); - return !fetchedEntity ? false : testDefaultAlias(fetchedEntity, newLanguage.id); + return !fetchedEntity ? false : testDefaultAlias(fetchedEntity, languageId); }); expect(every(conditions)).to.be.true; expect(res).to.be.ok; @@ -133,9 +195,6 @@ describe('Unified form routes', () => { }, type: 'Edition' }}; - forOwn(postData, (value) => { - value.nameSection.language = newLanguage.id; - }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(1); @@ -181,9 +240,6 @@ describe('Unified form routes', () => { type: null } }}; - forOwn(postData, (value) => { - value.nameSection.language = newLanguage.id; - }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(2); @@ -212,7 +268,6 @@ describe('Unified form routes', () => { }, type: 'Edition' }}; - postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(1); @@ -255,9 +310,6 @@ describe('Unified form routes', () => { }, type: 'Edition' }}; - forOwn(postData, (value) => { - value.nameSection.language = newLanguage.id; - }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(2); @@ -282,7 +334,6 @@ describe('Unified form routes', () => { }, type: 'Edition' }}; - postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(1); @@ -311,9 +362,6 @@ describe('Unified form routes', () => { }, type: 'Edition' }}; - forOwn(postData, (value) => { - value.nameSection.language = newLanguage.id; - }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(2); @@ -343,7 +391,6 @@ describe('Unified form routes', () => { editionSection: {}, type: 'Edition' }}; - postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(1); @@ -376,9 +423,6 @@ describe('Unified form routes', () => { type: 'Edition' } }; - forOwn(postData, (value) => { - value.nameSection.language = newLanguage.id; - }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(2); diff --git a/test/test-helpers/create-entities.js b/test/test-helpers/create-entities.js index 4175e50029..a62e1881aa 100644 --- a/test/test-helpers/create-entities.js +++ b/test/test-helpers/create-entities.js @@ -55,9 +55,9 @@ export const baseState = { identifierEditor: {}, nameSection: { disambiguation: '', - language: 1, - name: 'bob', - sortName: 'bob' + language: 42, + name: 'Entity name', + sortName: 'Entity sort name' }, relationshipSection: { relationships: {} @@ -69,6 +69,15 @@ export const baseState = { } }; +export const authorWorkRelationshipTypeData = { + description: 'test descryption', + label: 'test label', + linkPhrase: 'test phrase', + reverseLinkPhrase: 'test reverse link phrase', + sourceEntityType: 'Author', + targetEntityType: 'Work' +}; + export const editorTypeAttribs = { label: 'test_type' }; From 3b681ca74718742f28b893755fb00ec6ec764159 Mon Sep 17 00:00:00 2001 From: tri10 Date: Thu, 7 Jul 2022 12:16:25 +0530 Subject: [PATCH 085/258] fix test issues --- test/src/server/routes/unifiedform.js | 36 +++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/test/src/server/routes/unifiedform.js b/test/src/server/routes/unifiedform.js index a6e3410081..e85de9821c 100644 --- a/test/src/server/routes/unifiedform.js +++ b/test/src/server/routes/unifiedform.js @@ -1,5 +1,5 @@ import {authorWorkRelationshipTypeData, baseState, createAuthor, createEditionGroup, - createEditor, createPublisher, createWork, getRandomUUID, truncateEntities} from '../../../test-helpers/create-entities'; + createEditor, createPublisher, createWork, getRandomUUID, languageAttribs, truncateEntities} from '../../../test-helpers/create-entities'; import {every, forOwn, get, map} from 'lodash'; import app from '../../../../src/server/app'; import chai from 'chai'; @@ -8,7 +8,7 @@ import {getEntityByBBID} from '../../../../src/common/helpers/utils'; import orm from '../../../bookbrainz-data'; -const {RelationshipType} = orm; +const {Language, RelationshipType} = orm; const relationshipTypeData = { description: 'test descryption', label: 'test label', @@ -36,7 +36,7 @@ function testDefaultAlias(entity, languageId) { describe('Unified form routes', () => { let agent; - const languageId = 42; + let newLanguage; let newRelationshipType; let authorWorkRelationshipType; const wBBID = getRandomUUID(); @@ -52,6 +52,8 @@ describe('Unified form routes', () => { await createAuthor(aBBID); newRelationshipType = await new RelationshipType(relationshipTypeData) .save(null, {method: 'insert'}); + newLanguage = await new Language({...languageAttribs}) + .save(null, {method: 'insert'}); authorWorkRelationshipType = await new RelationshipType(authorWorkRelationshipTypeData) .save(null, {method: 'insert'}); } @@ -69,13 +71,14 @@ describe('Unified form routes', () => { editionSection: {}, type: 'Edition' }}; + postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(1); const editionEntity = createdEntities[0]; const fetchedEditionEntity = await getEntityByBBID(orm, editionEntity.bbid); expect(Boolean(fetchedEditionEntity)).to.be.true; - expect(testDefaultAlias(fetchedEditionEntity, languageId)).to.be.true; + expect(testDefaultAlias(fetchedEditionEntity, newLanguage.id)).to.be.true; expect(res).to.be.ok; expect(res).to.have.status(200); }); @@ -91,6 +94,7 @@ describe('Unified form routes', () => { } }}; const newName = 'changedName'; + postData.b0.nameSection.language = newLanguage.id; postData.b0.nameSection.name = newName; const res = await agent.post('/create/handler').send(postData); const editEntities = res.body; @@ -132,6 +136,7 @@ describe('Unified form routes', () => { type: null } }}; + postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); const editEntities = res.body; expect(editEntities.length).equal(1); @@ -159,12 +164,15 @@ describe('Unified form routes', () => { type: null } }}; + forOwn(postData, (value) => { + value.nameSection.language = newLanguage.id; + }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(2); const conditions = await map(createdEntities, async (entity) => { const fetchedEntity = await getEntityByBBID(orm, entity.bbid); - return !fetchedEntity ? false : testDefaultAlias(fetchedEntity, languageId); + return !fetchedEntity ? false : testDefaultAlias(fetchedEntity, newLanguage.id); }); expect(every(conditions)).to.be.true; expect(res).to.be.ok; @@ -195,6 +203,9 @@ describe('Unified form routes', () => { }, type: 'Edition' }}; + forOwn(postData, (value) => { + value.nameSection.language = newLanguage.id; + }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(1); @@ -240,6 +251,9 @@ describe('Unified form routes', () => { type: null } }}; + forOwn(postData, (value) => { + value.nameSection.language = newLanguage.id; + }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(2); @@ -268,6 +282,7 @@ describe('Unified form routes', () => { }, type: 'Edition' }}; + postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(1); @@ -310,6 +325,9 @@ describe('Unified form routes', () => { }, type: 'Edition' }}; + forOwn(postData, (value) => { + value.nameSection.language = newLanguage.id; + }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(2); @@ -334,6 +352,7 @@ describe('Unified form routes', () => { }, type: 'Edition' }}; + postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(1); @@ -362,6 +381,9 @@ describe('Unified form routes', () => { }, type: 'Edition' }}; + forOwn(postData, (value) => { + value.nameSection.language = newLanguage.id; + }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(2); @@ -391,6 +413,7 @@ describe('Unified form routes', () => { editionSection: {}, type: 'Edition' }}; + postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(1); @@ -423,6 +446,9 @@ describe('Unified form routes', () => { type: 'Edition' } }; + forOwn(postData, (value) => { + value.nameSection.language = newLanguage.id; + }); const res = await agent.post('/create/handler').send(postData); const createdEntities = res.body; expect(createdEntities.length).equal(2); From 3ee696919115007d9bc5272068a72de4f3574bd8 Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 8 Jul 2022 00:14:48 +0530 Subject: [PATCH 086/258] feat(routes): build entity state on server --- src/server/helpers/entityRouteUtils.tsx | 8 +- src/server/routes/entity/author.js | 2 +- src/server/routes/entity/edition-group.js | 2 +- src/server/routes/entity/edition.ts | 2 +- .../routes/entity/process-unified-form.ts | 98 +++++++++++++++---- src/server/routes/entity/publisher.js | 2 +- src/server/routes/entity/series.js | 2 +- src/server/routes/entity/work.js | 3 +- test/src/server/routes/unifiedform.js | 35 +++---- 9 files changed, 106 insertions(+), 48 deletions(-) diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index 98ad95160e..7dc050e601 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -339,7 +339,7 @@ function validateUnifiedForm(body:Record):boolean { return false; } const validator = getValidator(entityType); - if (!validator(entityForm)) { + if (isNew && !validator(entityForm)) { return false; } } @@ -353,16 +353,18 @@ function validateUnifiedForm(body:Record):boolean { * @param {object} res - Response object */ -export function createEntitiesHandler( +export async function createEntitiesHandler( req:$Request, res:$Response ) { + const {orm} = req.app.locals; + req.body = await unifiedRoutes.preprocessForm(req.body, orm); // validating if (!validateUnifiedForm(req.body)) { const err = new error.FormSubmissionError(); return error.sendErrorAsJSON(res, err); } - // transforming + // // // transforming req.body = unifiedRoutes.transformForm(req.body); return unifiedRoutes.handleCreateMultipleEntities(req as PassportRequest, res); } diff --git a/src/server/routes/entity/author.js b/src/server/routes/entity/author.js index a706ac02b1..6cd1bd9258 100644 --- a/src/server/routes/entity/author.js +++ b/src/server/routes/entity/author.js @@ -236,7 +236,7 @@ router.get('/:bbid/revisions/revisions', (req, res, next) => { }); -function authorToFormState(author) { +export function authorToFormState(author) { /** The front-end expects a language id rather than the language object. */ const aliases = author.aliasSet ? author.aliasSet.aliases.map(({languageId, ...rest}) => ({ diff --git a/src/server/routes/entity/edition-group.js b/src/server/routes/entity/edition-group.js index 7df9c97f98..6d6d2aa183 100644 --- a/src/server/routes/entity/edition-group.js +++ b/src/server/routes/entity/edition-group.js @@ -226,7 +226,7 @@ router.get('/:bbid/revisions/revisions', (req, res, next) => { }); -function editionGroupToFormState(editionGroup) { +export function editionGroupToFormState(editionGroup) { /** The front-end expects a language id rather than the language object. */ const aliases = editionGroup.aliasSet ? editionGroup.aliasSet.aliases.map(({languageId, ...rest}) => ({ diff --git a/src/server/routes/entity/edition.ts b/src/server/routes/entity/edition.ts index 504479c7c8..abcbd28d32 100644 --- a/src/server/routes/entity/edition.ts +++ b/src/server/routes/entity/edition.ts @@ -368,7 +368,7 @@ router.post( ); -function editionToFormState(edition) { +export function editionToFormState(edition) { /** The front-end expects a language id rather than the language object. */ const aliases = edition.aliasSet ? edition.aliasSet.aliases.map(({languageId, ...rest}) => ({ diff --git a/src/server/routes/entity/process-unified-form.ts b/src/server/routes/entity/process-unified-form.ts index ac2ff0e865..c8fd509b84 100644 --- a/src/server/routes/entity/process-unified-form.ts +++ b/src/server/routes/entity/process-unified-form.ts @@ -1,23 +1,26 @@ import * as achievement from '../../helpers/achievement'; +import * as commonUtils from '../../../common/helpers/utils'; import * as handler from '../../helpers/handler'; +import * as utils from '../../helpers/utils'; import type {Request as $Request, Response as $Response} from 'express'; +import {authorToFormState, transformNewForm as authorTransform} from './author'; +import {editionGroupToFormState, transformNewForm as editionGroupTransform} from './edition-group'; +import {editionToFormState, transformNewForm as editionTransform} from './edition'; import { fetchEntitiesForRelationships, fetchOrCreateMainEntity, getChangedProps, getNextRelationshipSets, indexAutoCreatedEditionGroup, saveEntitiesAndFinishRevision } from './entity'; +import {publisherToFormState, transformNewForm as publisherTransform} from './publisher'; +import {seriesToFormState, transformNewForm as seriesTransform} from './series'; + +import {workToFormState, transformNewForm as workTransform} from './work'; import type { EntityTypeString } from 'bookbrainz-data/lib/func/types'; import {FormSubmissionError} from '../../../common/helpers/error'; import _ from 'lodash'; import {_bulkIndexEntities} from '../../../common/helpers/search'; -import {transformNewForm as authorTransform} from './author'; -import {transformNewForm as editionGroupTransform} from './edition-group'; -import {transformNewForm as editionTransform} from './edition'; import log from 'log'; -import {transformNewForm as publisherTransform} from './publisher'; -import {transformNewForm as seriesTransform} from './series'; -import {transformNewForm as workTransform} from './work'; type PassportRequest = $Request & {user: any, session: any}; @@ -45,6 +48,15 @@ const additionalEntityProps = { }; +const entityToFormStateMap = { + author: authorToFormState, + edition: editionToFormState, + editionGroup: editionGroupToFormState, + publisher: publisherToFormState, + series: seriesToFormState, + work: workToFormState +}; + const baseRelations = [ 'aliasSet.aliases.language', 'annotation.lastRevision', @@ -100,19 +112,65 @@ export async function processAchievement(orm, editorId, entityJSON) { } } +function getEntityRelations(type:EntityTypeString) { + return [...baseRelations, ...additionalEntityAttributes[_.camelCase(type)]]; +} + export function transformForm(body:Record):Record { - const modifiedForm = {}; - for (const keyIndex in body) { - if (Object.prototype.hasOwnProperty.call(body, keyIndex)) { - const currentForm = body[keyIndex]; - const transformedForm = transformFunctions[_.camelCase(currentForm.type)](currentForm); - modifiedForm[keyIndex] = {__isNew__: currentForm.__isNew__, id: currentForm.id, type: currentForm.type, ...transformedForm}; + return _.mapValues(body, (currentForm) => { + const transformedForm = transformFunctions[_.camelCase(currentForm.type)](currentForm); + const __isNew__ = _.get(currentForm, '__isNew__', true); + const {id, type} = currentForm; + return {__isNew__, id, type, ...transformedForm}; + }); +} + +export async function preprocessForm(body:Record, orm):Promise> { + async function processForm(currentForm) { + async function getEntityWithAlias(relEntity) { + const redirectBbid = await orm.func.entity.recursivelyGetRedirectBBID(orm, relEntity.bbid, null); + const model = commonUtils.getEntityModelByType(orm, relEntity.type); + + return model.forge({bbid: redirectBbid}) + .fetch({require: false, withRelated: ['defaultAlias'].concat(utils.getAdditionalRelations(relEntity.type))}); } + const {id, type} = currentForm; + const {RelationshipSet} = orm; + const isNew = _.get(currentForm, '__isNew__', true); + if (!isNew && id) { + const entityType = _.upperFirst(_.camelCase(type)); + const currentEntity = await orm.func.entity.getEntity(orm, entityType, id, getEntityRelations(entityType as EntityTypeString)); + const relationshipSet = await RelationshipSet.forge({id: currentEntity.relationshipSetId}).fetch({ + require: false, + withRelated: [ + 'relationships.source', + 'relationships.target', + 'relationships.type.attributeTypes', + 'relationships.attributeSet.relationshipAttributes.value', + 'relationships.attributeSet.relationshipAttributes.type' + ] + }); + currentEntity.relationships = relationshipSet ? + relationshipSet.related('relationships').toJSON() : []; + + utils.attachAttributes(currentEntity.relationships); + await Promise.all(currentEntity.relationships.map((relationship) => + Promise.all([getEntityWithAlias(relationship.source), getEntityWithAlias(relationship.target)]) + .then(([source, target]) => { + relationship.source = source.toJSON(); + relationship.target = target.toJSON(); + + return relationship; + }))); + const oldFormState = entityToFormStateMap[_.camelCase(type)](currentEntity); + return _.merge(oldFormState, currentForm); + } + return currentForm; } - return modifiedForm; + const allEntities = await Promise.all(_.values(body).map(processForm)); + return Object.fromEntries(Object.keys(body).map((key, index) => [key, allEntities[index]])); } - export async function handleAddRelationship( body:Record, editorId:number, @@ -170,16 +228,12 @@ async function processRelationship(rels:Record[], mainEntity, bbidM {...rel, sourceBbid: _.get(bbidMap, rel.sourceBbid) ?? rel.sourceBbid, targetBbid: _.get(bbidMap, rel.targetBbid) ?? rel.targetBbid} )); - const {relationshipSetId} = await handleAddRelationship({relationships}, editorId, + const relationshipSet = await handleAddRelationship({relationships}, editorId, mainEntity, mainEntity.type, orm, transacting); - mainEntity.relationshipSetId = relationshipSetId; + mainEntity.relationshipSetId = relationshipSet.relationshipSetId; } } -function getEntityRelations(type:EntityTypeString) { - return [...baseRelations, ...additionalEntityAttributes[_.camelCase(type)]]; -} - export function handleCreateMultipleEntities( req: PassportRequest, res: $Response @@ -246,7 +300,9 @@ export function handleCreateMultipleEntities( ); // If there are no differences, bail if (_.isEmpty(changedProps)) { - throw new FormSubmissionError('No Updated Field'); + savedMainEntities[entityKey] = currentEntity; + _.set(savedMainEntities[entityKey], ['relationshipSet', 'id'], savedMainEntities[entityKey].relationshipSetId); + return; } const mainEntity = await fetchOrCreateMainEntity( orm, transacting, isNew, currentEntity.bbid, entityType diff --git a/src/server/routes/entity/publisher.js b/src/server/routes/entity/publisher.js index 3e772c4013..4552937e27 100644 --- a/src/server/routes/entity/publisher.js +++ b/src/server/routes/entity/publisher.js @@ -246,7 +246,7 @@ router.get('/:bbid/revisions/revisions', (req, res, next) => { }); -function publisherToFormState(publisher) { +export function publisherToFormState(publisher) { /** The front-end expects a language id rather than the language object. */ const aliases = publisher.aliasSet ? publisher.aliasSet.aliases.map(({languageId, ...rest}) => ({ diff --git a/src/server/routes/entity/series.js b/src/server/routes/entity/series.js index 5c0e7d9c14..7d53bfb484 100644 --- a/src/server/routes/entity/series.js +++ b/src/server/routes/entity/series.js @@ -233,7 +233,7 @@ router.get('/:bbid/revisions/revisions', (req, res, next) => { entityRoutes.updateDisplayedRevisions(req, res, next, SeriesRevision); }); -function seriesToFormState(series) { +export function seriesToFormState(series) { const aliases = series.aliasSet ? series.aliasSet.aliases.map(({languageId, ...rest}) => ({ ...rest, diff --git a/src/server/routes/entity/work.js b/src/server/routes/entity/work.js index 5d614d1312..076cbc1a07 100644 --- a/src/server/routes/entity/work.js +++ b/src/server/routes/entity/work.js @@ -258,8 +258,7 @@ router.get('/:bbid/revisions/revisions', (req, res, next) => { entityRoutes.updateDisplayedRevisions(req, res, next, WorkRevision); }); - -function workToFormState(work) { +export function workToFormState(work) { /** The front-end expects a language id rather than the language object. */ const aliases = work.aliasSet ? work.aliasSet.aliases.map(({languageId, ...rest}) => ({ diff --git a/test/src/server/routes/unifiedform.js b/test/src/server/routes/unifiedform.js index e85de9821c..a4b0a150d2 100644 --- a/test/src/server/routes/unifiedform.js +++ b/test/src/server/routes/unifiedform.js @@ -84,17 +84,18 @@ describe('Unified form routes', () => { }); it('should not throw error while editing single entity', async () => { const postData = {b0: { - ...baseState, __isNew__: false, id: wBBID, - type: 'Work', - workSection: { - languages: [], - type: null - } + nameSection: {}, + submissionSection: { + note: 'note', + submitError: '', + submitted: false + }, + type: 'Work' }}; const newName = 'changedName'; - postData.b0.nameSection.language = newLanguage.id; + // postData.b0.nameSection.language = newLanguage.id; postData.b0.nameSection.name = newName; const res = await agent.post('/create/handler').send(postData); const editEntities = res.body; @@ -109,7 +110,6 @@ describe('Unified form routes', () => { it('should not throw error while adding relationship to single entity', async () => { // we need to pass extra id and __isNew__ attributes const postData = {b0: { - ...baseState, __isNew__: false, id: wBBID, relationshipSection: { @@ -130,13 +130,13 @@ describe('Unified form routes', () => { } } }, - type: 'Work', - workSection: { - languages: [], - type: null - } + submissionSection: { + note: 'note', + submitError: '', + submitted: false + }, + type: 'Work' }}; - postData.b0.nameSection.language = newLanguage.id; const res = await agent.post('/create/handler').send(postData); const editEntities = res.body; expect(editEntities.length).equal(1); @@ -144,9 +144,10 @@ describe('Unified form routes', () => { const fetchedworkEntity = await getEntityByBBID(orm, workEntity.bbid); expect(Boolean(fetchedworkEntity)).to.be.true; const relationships = get(fetchedworkEntity, ['relationshipSet', 'relationships'], []); - expect(relationships.length).to.be.equal(1); - expect(get(relationships[0], 'targetBbid')).to.be.equal(wBBID); - expect(get(relationships[0], 'sourceBbid')).to.be.equal(aBBID); + // one relationship added on creation + expect(relationships.length).to.be.equal(2); + expect(get(relationships[1], 'targetBbid')).to.be.equal(wBBID); + expect(get(relationships[1], 'sourceBbid')).to.be.equal(aBBID); expect(res).to.be.ok; expect(res).to.have.status(200); }); From db70977699bb59bf4d2ca8b1de375602e2cfebb7 Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 8 Jul 2022 08:53:55 +0530 Subject: [PATCH 087/258] refactor(routes): add comments --- src/server/helpers/entityRouteUtils.tsx | 5 +- src/server/helpers/middleware.ts | 67 +++++++++++-------- .../routes/entity/process-unified-form.ts | 34 +++------- test/src/server/routes/unifiedform.js | 1 - 4 files changed, 52 insertions(+), 55 deletions(-) diff --git a/src/server/helpers/entityRouteUtils.tsx b/src/server/helpers/entityRouteUtils.tsx index 7dc050e601..962edfc821 100644 --- a/src/server/helpers/entityRouteUtils.tsx +++ b/src/server/helpers/entityRouteUtils.tsx @@ -358,13 +358,14 @@ export async function createEntitiesHandler( res:$Response ) { const {orm} = req.app.locals; + // generate the state for current entity req.body = await unifiedRoutes.preprocessForm(req.body, orm); - // validating + // validating the uf state if (!validateUnifiedForm(req.body)) { const err = new error.FormSubmissionError(); return error.sendErrorAsJSON(res, err); } - // // // transforming + // transforming uf state into separate entity state req.body = unifiedRoutes.transformForm(req.body); return unifiedRoutes.handleCreateMultipleEntities(req as PassportRequest, res); } diff --git a/src/server/helpers/middleware.ts b/src/server/helpers/middleware.ts index 0c6e90f786..3e1d1f2100 100644 --- a/src/server/helpers/middleware.ts +++ b/src/server/helpers/middleware.ts @@ -121,6 +121,44 @@ export async function loadWorkTableAuthors(req: $Request, res: $Response, next: return next(); } +/** + * Add the relationships on entity object. + * + * @param {Object} entity - The entity to load the relationships for. + * @param {Object} relationshipSet - The RelationshipSet model. + * @param {Object} orm - The ORM instance. + * @returns + */ + +export function addRelationships(entity, relationshipSet, orm) { + async function getEntityWithAlias(relEntity) { + const redirectBbid = await orm.func.entity.recursivelyGetRedirectBBID(orm, relEntity.bbid, null); + const model = commonUtils.getEntityModelByType(orm, relEntity.type); + + return model.forge({bbid: redirectBbid}) + .fetch({require: false, withRelated: ['defaultAlias'].concat(utils.getAdditionalRelations(relEntity.type))}); + } + entity.relationships = relationshipSet ? + relationshipSet.related('relationships').toJSON() : []; + + utils.attachAttributes(entity.relationships); + + + /** + * Source and target are generic Entity objects, so until we have + * a good way of polymorphically fetching the right specific entity, + * we need to fetch default alias in a somewhat sketchier way. + */ + return Promise.all(entity.relationships.map((relationship) => + Promise.all([getEntityWithAlias(relationship.source), getEntityWithAlias(relationship.target)]) + .then(([source, target]) => { + relationship.source = source.toJSON(); + relationship.target = target.toJSON(); + + return relationship; + }))); +} + export function loadEntityRelationships(req: $Request, res: $Response, next: NextFunction) { const {orm}: any = req.app.locals; const {RelationshipSet} = orm; @@ -146,34 +184,7 @@ export function loadEntityRelationships(req: $Request, res: $Response, next: Nex ] }) ) - .then((relationshipSet) => { - entity.relationships = relationshipSet ? - relationshipSet.related('relationships').toJSON() : []; - - utils.attachAttributes(entity.relationships); - - async function getEntityWithAlias(relEntity) { - const redirectBbid = await orm.func.entity.recursivelyGetRedirectBBID(orm, relEntity.bbid, null); - const model = commonUtils.getEntityModelByType(orm, relEntity.type); - - return model.forge({bbid: redirectBbid}) - .fetch({require: false, withRelated: ['defaultAlias'].concat(utils.getAdditionalRelations(relEntity.type))}); - } - - /** - * Source and target are generic Entity objects, so until we have - * a good way of polymorphically fetching the right specific entity, - * we need to fetch default alias in a somewhat sketchier way. - */ - return Promise.all(entity.relationships.map((relationship) => - Promise.all([getEntityWithAlias(relationship.source), getEntityWithAlias(relationship.target)]) - .then(([source, target]) => { - relationship.source = source.toJSON(); - relationship.target = target.toJSON(); - - return relationship; - }))); - }) + .then((relationshipSet) => addRelationships(entity, relationshipSet, orm)) .then(() => { next(); return null; diff --git a/src/server/routes/entity/process-unified-form.ts b/src/server/routes/entity/process-unified-form.ts index c8fd509b84..5e7fed93fa 100644 --- a/src/server/routes/entity/process-unified-form.ts +++ b/src/server/routes/entity/process-unified-form.ts @@ -1,7 +1,5 @@ import * as achievement from '../../helpers/achievement'; -import * as commonUtils from '../../../common/helpers/utils'; import * as handler from '../../helpers/handler'; -import * as utils from '../../helpers/utils'; import type {Request as $Request, Response as $Response} from 'express'; import {authorToFormState, transformNewForm as authorTransform} from './author'; import {editionGroupToFormState, transformNewForm as editionGroupTransform} from './edition-group'; @@ -20,6 +18,7 @@ import type { import {FormSubmissionError} from '../../../common/helpers/error'; import _ from 'lodash'; import {_bulkIndexEntities} from '../../../common/helpers/search'; +import {addRelationships} from '../../helpers/middleware'; import log from 'log'; @@ -127,19 +126,15 @@ export function transformForm(body:Record):Record { export async function preprocessForm(body:Record, orm):Promise> { async function processForm(currentForm) { - async function getEntityWithAlias(relEntity) { - const redirectBbid = await orm.func.entity.recursivelyGetRedirectBBID(orm, relEntity.bbid, null); - const model = commonUtils.getEntityModelByType(orm, relEntity.type); - - return model.forge({bbid: redirectBbid}) - .fetch({require: false, withRelated: ['defaultAlias'].concat(utils.getAdditionalRelations(relEntity.type))}); - } const {id, type} = currentForm; const {RelationshipSet} = orm; const isNew = _.get(currentForm, '__isNew__', true); + // if new entity, no need to process further if (!isNew && id) { const entityType = _.upperFirst(_.camelCase(type)); + // fetch the entity with all related attributes from the database const currentEntity = await orm.func.entity.getEntity(orm, entityType, id, getEntityRelations(entityType as EntityTypeString)); + // since relationship will not be set by default, we need to add it manually const relationshipSet = await RelationshipSet.forge({id: currentEntity.relationshipSetId}).fetch({ require: false, withRelated: [ @@ -150,19 +145,10 @@ export async function preprocessForm(body:Record, orm):Promise - Promise.all([getEntityWithAlias(relationship.source), getEntityWithAlias(relationship.target)]) - .then(([source, target]) => { - relationship.source = source.toJSON(); - relationship.target = target.toJSON(); - - return relationship; - }))); + await addRelationships(currentEntity, relationshipSet, orm); + // convert this state to normal entity-editor state const oldFormState = entityToFormStateMap[_.camelCase(type)](currentEntity); + // deep merge the old state with new one return _.merge(oldFormState, currentForm); } return currentForm; @@ -228,9 +214,9 @@ async function processRelationship(rels:Record[], mainEntity, bbidM {...rel, sourceBbid: _.get(bbidMap, rel.sourceBbid) ?? rel.sourceBbid, targetBbid: _.get(bbidMap, rel.targetBbid) ?? rel.targetBbid} )); - const relationshipSet = await handleAddRelationship({relationships}, editorId, - mainEntity, mainEntity.type, orm, transacting); - mainEntity.relationshipSetId = relationshipSet.relationshipSetId; + const {relationshipSetId} = await handleAddRelationship({relationships}, editorId, + mainEntity, mainEntity.type, orm, transacting); + mainEntity.relationshipSetId = relationshipSetId; } } diff --git a/test/src/server/routes/unifiedform.js b/test/src/server/routes/unifiedform.js index a4b0a150d2..241f9d8e84 100644 --- a/test/src/server/routes/unifiedform.js +++ b/test/src/server/routes/unifiedform.js @@ -95,7 +95,6 @@ describe('Unified form routes', () => { type: 'Work' }}; const newName = 'changedName'; - // postData.b0.nameSection.language = newLanguage.id; postData.b0.nameSection.name = newName; const res = await agent.post('/create/handler').send(postData); const editEntities = res.body; From e77934c53956fc52386954bb1b8296ac99320b1c Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 8 Jul 2022 09:28:43 +0530 Subject: [PATCH 088/258] feat(uf): add relationships on existing works --- .../entity-editor/submission-section/actions.ts | 15 +++++++++++---- .../unified-form/content-tab/content-tab.tsx | 7 ++++--- src/client/unified-form/content-tab/work-row.tsx | 8 ++++---- src/client/unified-form/submit-tab/summary.tsx | 4 ++-- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/client/entity-editor/submission-section/actions.ts b/src/client/entity-editor/submission-section/actions.ts index 5cea76d48e..c5d3c5de84 100644 --- a/src/client/entity-editor/submission-section/actions.ts +++ b/src/client/entity-editor/submission-section/actions.ts @@ -129,15 +129,16 @@ function transformFormData(data:Record):Record { } }); // add new works + const authorWorkRelationshipTypeId = 8; _.forEach(data.Works, (work, wid) => { - if (work.checked && work.__isNew__) { + if (work.checked) { let relationshipCount = 0; _.forEach(data.authorCreditEditor, (authorCredit) => { const relationship = { attributeSetId: null, attributes: [], relationshipType: { - id: 8 + id: authorWorkRelationshipTypeId }, rowId: `a${relationshipCount}`, sourceEntity: { @@ -147,12 +148,18 @@ function transformFormData(data:Record):Record { bbid: wid } }; - work.relationshipSection.relationships[`a${relationshipCount}`] = relationship; + _.set(work, ['relationshipSection', 'relationships', `a${relationshipCount}`], relationship); relationshipCount++; }); + if (!work.__isNew__) { + work.submissionSection = { + note: _.get(data, ['submissionSection', 'note']) + }; + work.__isNew__ = false; + } } - if (work.__isNew__) { + if (!work.__isNew__ || work.checked) { newData[wid] = work; } }); diff --git a/src/client/unified-form/content-tab/content-tab.tsx b/src/client/unified-form/content-tab/content-tab.tsx index 59e7b6c1bc..0e71af0826 100644 --- a/src/client/unified-form/content-tab/content-tab.tsx +++ b/src/client/unified-form/content-tab/content-tab.tsx @@ -12,7 +12,7 @@ import {map} from 'lodash'; const {Row, Col, FormCheck} = Bootstrap; export function ContentTab({value, onChange, ...rest}:ContentTabProps) { const [isChecked, setIsChecked] = React.useState(false); - const toggleIsChecked = React.useCallback(() => setIsChecked(!isChecked), [isChecked]); + const toggleCheck = React.useCallback(() => setIsChecked(!isChecked), [isChecked]); const onChangeHandler = React.useCallback((work:any) => { work.checked = isChecked; onChange(work); @@ -35,9 +35,10 @@ export function ContentTab({value, onChange, ...rest}:ContentTabProps) { ); diff --git a/src/client/unified-form/content-tab/work-row.tsx b/src/client/unified-form/content-tab/work-row.tsx index d96dd01ac1..4a31edf5f8 100644 --- a/src/client/unified-form/content-tab/work-row.tsx +++ b/src/client/unified-form/content-tab/work-row.tsx @@ -28,7 +28,6 @@ function WorkRow({onChange, work, onRemove, onToggle, ...rest}:WorkRowProps) { value.checked = isChecked; onChange(value); }, [isChecked, onChange]); - // TODO: Add author to exisiting work from AC return (
@@ -45,13 +44,14 @@ function WorkRow({onChange, work, onRemove, onToggle, ...rest}:WorkRowProps) { - {work.__isNew__ && } + />
); } diff --git a/src/client/unified-form/submit-tab/summary.tsx b/src/client/unified-form/submit-tab/summary.tsx index 9953493832..54504cf564 100644 --- a/src/client/unified-form/submit-tab/summary.tsx +++ b/src/client/unified-form/submit-tab/summary.tsx @@ -33,10 +33,10 @@ function SummarySection({ Works }; function renderEntityGroup(entities: Array, entityType: string) { - if (entities.length === 0) { + const newEntities = entities.filter((entity) => entity.__isNew__); + if (newEntities.length === 0) { return null; } - const newEntities = entities.filter((entity) => entity.__isNew__); return ( Date: Fri, 8 Jul 2022 10:34:11 +0530 Subject: [PATCH 089/258] set default annotation for existing entities --- src/server/routes/entity/process-unified-form.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/routes/entity/process-unified-form.ts b/src/server/routes/entity/process-unified-form.ts index 5e7fed93fa..5c0ad566d2 100644 --- a/src/server/routes/entity/process-unified-form.ts +++ b/src/server/routes/entity/process-unified-form.ts @@ -146,6 +146,9 @@ export async function preprocessForm(body:Record, orm):Promise Date: Fri, 8 Jul 2022 10:37:22 +0530 Subject: [PATCH 090/258] fix minor issues with adding relationships --- src/client/entity-editor/submission-section/actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/entity-editor/submission-section/actions.ts b/src/client/entity-editor/submission-section/actions.ts index c5d3c5de84..8bb48e9d1b 100644 --- a/src/client/entity-editor/submission-section/actions.ts +++ b/src/client/entity-editor/submission-section/actions.ts @@ -145,7 +145,7 @@ function transformFormData(data:Record):Record { bbid: authorCredit.author.id }, targetEntity: { - bbid: wid + bbid: work.id } }; _.set(work, ['relationshipSection', 'relationships', `a${relationshipCount}`], relationship); @@ -159,7 +159,7 @@ function transformFormData(data:Record):Record { } } - if (!work.__isNew__ || work.checked) { + if (work.__isNew__ || work.checked) { newData[wid] = work; } }); From d8e410bd8ab3ba845049aafc98c08a97962433a2 Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 8 Jul 2022 16:52:16 +0530 Subject: [PATCH 091/258] fix: don't reset AC state on edition dump --- src/client/unified-form/helpers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index eb6d091b38..3004de480f 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -55,7 +55,6 @@ const initialACState = Immutable.fromJS( const initialState = Immutable.Map({ aliasEditor: Immutable.Map({}), annotationSection: Immutable.Map({content: ''}), - authorCreditEditor: initialACState, buttonBar: Immutable.Map({ aliasEditorVisible: false, identifierEditorVisible: false From 80bc6efcecb80b1d0e5d71d39692949e82e22cb2 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 11 Jul 2022 13:25:24 +0200 Subject: [PATCH 092/258] chore(git): Remove obsolete submodule config Left over after removing the use of submodules in commit 9373aa0ea64f7fbeb84dfcda6b6f1188fab7ad1d Currently making some automated docker image build processes fail: https://github.com/metabrainz/bookbrainz-site/runs/7280994835 --- src/client/stylesheets/lobes | 1 - 1 file changed, 1 deletion(-) delete mode 160000 src/client/stylesheets/lobes diff --git a/src/client/stylesheets/lobes b/src/client/stylesheets/lobes deleted file mode 160000 index ad7d57abdc..0000000000 --- a/src/client/stylesheets/lobes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ad7d57abdc59fdb335e788922a41678a0de1b710 From be7a4b56a4250aef1d880c1beaf87fe2094eef86 Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Tue, 12 Jul 2022 08:47:43 +0000 Subject: [PATCH 093/258] feat: fetch CB reviews for edition group --- src/server/helpers/critiquebrainz.ts | 18 ++++++++++++++++++ src/server/helpers/middleware.ts | 7 ++++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/server/helpers/critiquebrainz.ts diff --git a/src/server/helpers/critiquebrainz.ts b/src/server/helpers/critiquebrainz.ts new file mode 100644 index 0000000000..624e256eca --- /dev/null +++ b/src/server/helpers/critiquebrainz.ts @@ -0,0 +1,18 @@ +import request from "superagent"; + +export const getReviewsFromCB = async (bbid: string, entityType: string) => { + const mapEntityType = { + EditionGroup: "bb_edition_group", + }; + entityType = mapEntityType[entityType]; + console.log(entityType); + const res = await request + .get("https://beta.critiquebrainz.org/ws/1/review") + .query({ + entity_id: bbid, + entity_type: entityType, + limit: 10, + offset: 0, + }); + return res.body; +}; diff --git a/src/server/helpers/middleware.ts b/src/server/helpers/middleware.ts index 0c6e90f786..23ef002c0d 100644 --- a/src/server/helpers/middleware.ts +++ b/src/server/helpers/middleware.ts @@ -20,7 +20,7 @@ import * as commonUtils from '../../common/helpers/utils'; import * as error from '../../common/helpers/error'; import * as utils from '../helpers/utils'; - +import {getReviewsFromCB} from "./critiquebrainz"; import type {Response as $Response, NextFunction, Request} from 'express'; import _ from 'lodash'; @@ -226,6 +226,11 @@ export function makeEntityLoader(modelName: string, additionalRels: Array collection.public === true || parseInt(collection.ownerId, 10) === parseInt(req.user?.id, 10)); } + let reviews = []; + if(entity.type == "EditionGroup"){ + reviews = await getReviewsFromCB(bbid, entity.type); + } + entity.reviews = reviews; res.locals.entity = entity; return next(); } From 19f3d6f0252dc1ca50ddec4f831eec2c50840fb9 Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Tue, 12 Jul 2022 08:48:37 +0000 Subject: [PATCH 094/258] feat: Show CB reviews on EG page --- .../components/pages/entities/cb-review.js | 70 +++++++++++++++++++ .../pages/entities/edition-group.js | 40 +++++++++-- src/client/components/pages/entities/links.js | 13 ++-- .../pages/entities/related-collections.js | 7 +- 4 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 src/client/components/pages/entities/cb-review.js diff --git a/src/client/components/pages/entities/cb-review.js b/src/client/components/pages/entities/cb-review.js new file mode 100644 index 0000000000..f2731d3e4d --- /dev/null +++ b/src/client/components/pages/entities/cb-review.js @@ -0,0 +1,70 @@ +import * as bootstrap from 'react-bootstrap'; +const { Col, Row } = bootstrap; +import React from 'react'; +import { Rating } from "react-simple-star-rating"; + + +function ReviewCard({ reviewData }) { + const publishedDate = new Date(reviewData.published_on).toDateString(); + let reviewText = reviewData.text; + if (reviewText.length > 75) { + reviewText = reviewText.substring(0, 75) + '...'; + } + const reviewLink = "https://critiquebrainz.org/review/" + reviewData.id; + return ( +
+
+ + + Review by: {reviewData.user.display_name} {publishedDate} + +
+
+ {reviewText} + View > +
+
+ ) +} + +function EntityReviews({ entity }) { + const reviews = entity.reviews.reviews; + let reviewContent; + const mapEntityType = { + EditionGroup: "bb_edition_group", + }; + let entityType = mapEntityType[entity.type]; + if (reviews.length) { + reviewContent = ( + + { + reviews.slice(0, 3).map((review) => ( + + )) + } + View all reviews > + + ) + } + return ( +
+

Reviews

+ {reviewContent} +
+ ); +} + +export default EntityReviews; \ No newline at end of file diff --git a/src/client/components/pages/entities/edition-group.js b/src/client/components/pages/entities/edition-group.js index b64162aaec..343663092e 100644 --- a/src/client/components/pages/entities/edition-group.js +++ b/src/client/components/pages/entities/edition-group.js @@ -16,6 +16,8 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +import EntityReviews from './cb-review'; +import { Rating } from "react-simple-star-rating"; import * as bootstrap from 'react-bootstrap'; import * as entityHelper from '../../../helpers/entity'; import AuthorCreditDisplay from '../../author-credit-display'; @@ -54,6 +56,24 @@ function EditionGroupAttributes({editionGroup}) {
{type}
+ +
+
Ratings
+
+ +
+
+
); @@ -105,12 +125,20 @@ function EditionGroupDisplayPage({entity, identifierTypes, user}) { {!entity.deleted && - - + + + + + + + + + + }
- + + - - + + - - +
+ ); } EntityLinks.displayName = 'EntityLinks'; diff --git a/src/client/components/pages/entities/related-collections.js b/src/client/components/pages/entities/related-collections.js index cee825bdc9..bb2170736f 100644 --- a/src/client/components/pages/entities/related-collections.js +++ b/src/client/components/pages/entities/related-collections.js @@ -18,11 +18,12 @@ import PropTypes from 'prop-types'; import React from 'react'; - +import * as bootstrap from 'react-bootstrap'; +const {Row} = bootstrap; function EntityRelatedCollections({collections}) { return ( -
+

Related Collections

{collections &&
    @@ -34,7 +35,7 @@ function EntityRelatedCollections({collections}) { ))}
} -
+ ); } EntityRelatedCollections.displayName = 'EntityRelatedCollections'; From b7fb472a1b3ee55334f1dc8dd9f4a1e29f90f0f9 Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Tue, 12 Jul 2022 08:52:51 +0000 Subject: [PATCH 095/258] feat: Add react star ratings package --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 83a9118fc5..25366cb06e 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "react-redux": "^7.2.6", "react-select": "^4.3.1", "react-select-fast-filter-options": "^0.2.3", + "react-simple-star-rating": "^4.0.5", "react-sortable-hoc": "^2.0.0", "react-sticky": "^6.0.1", "redis": "^3.1.2", From a81853686aa99c4ee5f818904c542b9ed8f2f442 Mon Sep 17 00:00:00 2001 From: tri10 Date: Tue, 12 Jul 2022 21:49:10 +0530 Subject: [PATCH 096/258] fix: remove revision if no changes made to entity --- src/server/routes/entity/process-unified-form.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/routes/entity/process-unified-form.ts b/src/server/routes/entity/process-unified-form.ts index 5c0ad566d2..4a5fb980e7 100644 --- a/src/server/routes/entity/process-unified-form.ts +++ b/src/server/routes/entity/process-unified-form.ts @@ -291,6 +291,8 @@ export function handleCreateMultipleEntities( if (_.isEmpty(changedProps)) { savedMainEntities[entityKey] = currentEntity; _.set(savedMainEntities[entityKey], ['relationshipSet', 'id'], savedMainEntities[entityKey].relationshipSetId); + // remove the revision if no changes made + await newRevision.destroy({transacting}); return; } const mainEntity = await fetchOrCreateMainEntity( From ee3d8c055ec307f294d9ea924e443782a8662ea6 Mon Sep 17 00:00:00 2001 From: tri10 Date: Tue, 12 Jul 2022 22:04:45 +0530 Subject: [PATCH 097/258] feat(uf): add validations to modal sections --- src/client/entity-editor/validators/common.ts | 6 +-- .../unified-form/common/entity-modal-body.tsx | 48 +++++++++++++++---- .../unified-form/common/single-accordion.tsx | 14 +++++- src/client/unified-form/interface/type.ts | 12 ++++- 4 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/client/entity-editor/validators/common.ts b/src/client/entity-editor/validators/common.ts index 99b841e418..f45c2031c4 100644 --- a/src/client/entity-editor/validators/common.ts +++ b/src/client/entity-editor/validators/common.ts @@ -49,7 +49,7 @@ export function validateMultiple( } return every(values, (value) => - validationFunction(value, isCustom, additionalArgs)); + validationFunction(value, additionalArgs, isCustom)); } export function validateAliasName(value: any): boolean { @@ -184,8 +184,8 @@ export function validateSubmissionSection( validateSubmissionSectionAnnotation(get(data, 'annotation.content', null)) ); } - -export function validateAuthorCreditRow(row: any, isCustom = false): boolean { +// Hacky way to achieve polymorphism for both editors +export function validateAuthorCreditRow(row: any, arg:any, isCustom = false): boolean { return (isCustom ? Boolean(getIn(row, ['author', 'id'], null)) : validateUUID(getIn(row, ['author', 'id'], null), true)) && validateRequiredString(get(row, 'name', null)) && validateOptionalString(get(row, 'joinPhrase', null)); diff --git a/src/client/unified-form/common/entity-modal-body.tsx b/src/client/unified-form/common/entity-modal-body.tsx index 838c2a7864..9d07c7384f 100644 --- a/src/client/unified-form/common/entity-modal-body.tsx +++ b/src/client/unified-form/common/entity-modal-body.tsx @@ -1,4 +1,6 @@ -import {EntityModalBodyProps, EntityModalDispatchProps} from '../interface/type'; +import {EntityModalBodyProps, EntityModalDispatchProps, EntityModalStateProps, State} from '../interface/type'; +import {camelCase, omit} from 'lodash'; +import {validateAliases, validateIdentifiers, validateNameSection} from '../../entity-editor/validators/common'; import AliasModalBody from '../../entity-editor/alias-editor/alias-modal-body'; import AnnotationSection from '../../entity-editor/annotation-section/annotation-section'; import IdentifierModalBody from '../../entity-editor/identifier-editor/identifier-modal-body'; @@ -8,19 +10,35 @@ import RelationshipSection from '../../entity-editor/relationship-editor/relatio import SingleAccordion from './single-accordion'; import SubmissionSection from '../../entity-editor/submission-section/submission-section'; import {connect} from 'react-redux'; -import {omit} from 'lodash'; import {removeEmptyAliases} from '../../entity-editor/alias-editor/actions'; import {removeEmptyIdentifiers} from '../../entity-editor/identifier-editor/actions'; +import {validateAuthorSection} from '../../entity-editor/validators/author'; +import {validateEditionGroupSection} from '../../entity-editor/validators/edition-group'; +import {validateEditionSection} from '../../entity-editor/validators/edition'; +import {validatePublisherSection} from '../../entity-editor/validators/publisher'; +import {validateSeriesSection} from '../../entity-editor/validators/series'; +import {validateWorkSection} from '../../entity-editor/validators/work'; -function EntityModalBody({onModalSubmit, children, validate, onAliasClose, onIdentifierClose, ...rest}:EntityModalBodyProps) { +const entitySectionValidators = { + authorSection: validateAuthorSection, + editionGroupSection: validateEditionGroupSection, + editionSection: validateEditionSection, + publisherSection: validatePublisherSection, + seriesSection: validateSeriesSection, + workSection: validateWorkSection +}; +function EntityModalBody({onModalSubmit, children, validate, onAliasClose, onIdentifierClose, isNameSectionValid, isNameSectionEmpty, + isAliasEditorEmpty, isIdentifierEditorEmpty, isEntitySectionValid, + isIdentifierEditorValid, isAliasEditorValid, ...rest} + :EntityModalBodyProps) { const genericProps:any = omit(rest, ['allIdentifierTypes']); return (
- + - + { React.cloneElement( React.Children.only(children), @@ -28,10 +46,10 @@ function EntityModalBody({onModalSubmit, children, validate, onAliasClose, onIde ) } - + - + @@ -48,6 +66,20 @@ function EntityModalBody({onModalSubmit, children, validate, onAliasClose, onIde EntityModalBody.defaultProps = { children: null }; +function mapStateToProps(state:State, {entityType, identifierTypes}) { + const nameSection = state.get('nameSection'); + const entitySection = `${camelCase(entityType)}Section`; + return { + isAliasEditorEmpty: state.get('aliasEditor', {}).size === 0, + isAliasEditorValid: validateAliases(state.get('aliasEditor', {})), + isEntitySectionValid: entitySectionValidators[entitySection](state.get(entitySection)), + isIdentifierEditorEmpty: state.get('identifierEditor', {}).size === 0, + isIdentifierEditorValid: validateIdentifiers(state.get('identifierEditor', {}), identifierTypes), + isNameSectionEmpty: !nameSection.get('name').length && !nameSection.get('sortName').length && !nameSection.get('language'), + isNameSectionValid: validateNameSection(nameSection) && + (!nameSection.get('exactMatches', [])?.length || nameSection.get('disambiguation').length > 0) + }; +} function mapDispatchToProps(dispatch) { return { onAliasClose: () => dispatch(removeEmptyAliases()), @@ -55,4 +87,4 @@ function mapDispatchToProps(dispatch) { }; } -export default connect(null, mapDispatchToProps)(EntityModalBody); +export default connect(mapStateToProps, mapDispatchToProps)(EntityModalBody); diff --git a/src/client/unified-form/common/single-accordion.tsx b/src/client/unified-form/common/single-accordion.tsx index 4c3dd64fdd..4da21997c0 100644 --- a/src/client/unified-form/common/single-accordion.tsx +++ b/src/client/unified-form/common/single-accordion.tsx @@ -1,6 +1,7 @@ import {Accordion, Card} from 'react-bootstrap'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import React from 'react'; +import ValidationLabel from '../../entity-editor/common/validation-label'; import {faChevronRight} from '@fortawesome/free-solid-svg-icons'; @@ -8,9 +9,16 @@ type SingleAccordionProps = { children: React.ReactNode, defaultActive?: boolean, onToggle?: () => void, + isEmpty?: boolean, + isValid?: boolean, heading: string }; -export default function SingleAccordion({children, defaultActive, heading, onToggle}:SingleAccordionProps) { +export default function SingleAccordion({children, defaultActive, heading, onToggle, isEmpty, isValid}:SingleAccordionProps) { + const inputLabel = ( + + {heading} + + ); return ( @@ -19,7 +27,7 @@ export default function SingleAccordion({children, defaultActive, heading, onTog {children} - {heading} + {inputLabel} @@ -29,5 +37,7 @@ export default function SingleAccordion({children, defaultActive, heading, onTog SingleAccordion.defaultProps = { defaultActive: false, + isEmpty: true, + isValid: true, onToggle: null }; diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index 93e4021705..2c4f33e205 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -112,6 +112,16 @@ export type SearchEntityCreateOwnProps = { }; export type SearchEntityCreateProps = SearchEntityCreateDispatchProps & SearchEntityCreateOwnProps; +export type EntityModalStateProps = { + isNameSectionValid:boolean, + isNameSectionEmpty:boolean, + isAliasEditorValid:boolean, + isIdentifierEditorValid:boolean, + isEntitySectionValid:boolean, + isAliasEditorEmpty:boolean, + isIdentifierEditorEmpty:boolean +}; + export type EntityModalBodyOwnProps = { onModalSubmit:(e)=>unknown, entityType:string, @@ -124,7 +134,7 @@ export type EntityModalDispatchProps = { onIdentifierClose: () => unknown }; -export type EntityModalBodyProps = EntityModalDispatchProps & EntityModalBodyOwnProps; +export type EntityModalBodyProps = EntityModalDispatchProps & EntityModalBodyOwnProps & EntityModalStateProps; export type CreateEntityModalOwnProps = { handleClose:() => unknown, From 4ac1ffa4111d977a6793665b596619cb943c21e7 Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Wed, 13 Jul 2022 15:38:51 +0530 Subject: [PATCH 098/258] chore: install react-simple-star-rating --- yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/yarn.lock b/yarn.lock index ba5b1aa2fb..b99c8b26cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6891,6 +6891,11 @@ react-select@^4.3.1: react-input-autosize "^3.0.0" react-transition-group "^4.3.0" +react-simple-star-rating@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/react-simple-star-rating/-/react-simple-star-rating-4.0.5.tgz#030e576015c9fca881677c9eeb55218f42278cd8" + integrity sha512-995YpXtLNNLim/K59lhRqFnvpRXJHsiJAnYAu2iHEjfCn4u8hP9Eam53hi+ubc6stU25FzvBPyXKzjhu7wl+hA== + react-sortable-hoc@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz#f6780d8aa4b922a21f3e754af542f032677078b7" From 527b75fe0f06179bf77bf31f654c3459541f9f8a Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Wed, 13 Jul 2022 06:57:36 +0000 Subject: [PATCH 099/258] refactor: lint changes --- .../components/pages/entities/cb-review.js | 122 +++++++++--------- .../pages/entities/edition-group.js | 14 +- .../pages/entities/related-collections.js | 2 + src/server/helpers/critiquebrainz.ts | 23 ++-- src/server/helpers/middleware.ts | 10 +- 5 files changed, 88 insertions(+), 83 deletions(-) diff --git a/src/client/components/pages/entities/cb-review.js b/src/client/components/pages/entities/cb-review.js index f2731d3e4d..9928e0b538 100644 --- a/src/client/components/pages/entities/cb-review.js +++ b/src/client/components/pages/entities/cb-review.js @@ -1,70 +1,72 @@ import * as bootstrap from 'react-bootstrap'; -const { Col, Row } = bootstrap; import React from 'react'; -import { Rating } from "react-simple-star-rating"; +import {Rating} from 'react-simple-star-rating'; -function ReviewCard({ reviewData }) { - const publishedDate = new Date(reviewData.published_on).toDateString(); - let reviewText = reviewData.text; - if (reviewText.length > 75) { - reviewText = reviewText.substring(0, 75) + '...'; - } - const reviewLink = "https://critiquebrainz.org/review/" + reviewData.id; - return ( -
-
- - +const {Col, Row} = bootstrap; + + +function ReviewCard({reviewData}) { + const publishedDate = new Date(reviewData.published_on).toDateString(); + let reviewText = reviewData.text; + if (reviewText.length > 75) { + reviewText = `${reviewText.substring(0, 75)}...`; + } + const reviewLink = `https://critiquebrainz.org/review/${reviewData.id}`; + return ( +
+
+ + Review by: {reviewData.user.display_name} {publishedDate} - -
-
- {reviewText} - View > -
-
- ) +
+
+
+ {reviewText} + View > +
+
+ ); } -function EntityReviews({ entity }) { - const reviews = entity.reviews.reviews; - let reviewContent; - const mapEntityType = { - EditionGroup: "bb_edition_group", +function EntityReviews({entity}) { + const {reviews} = entity.reviews; + let reviewContent; + const mapEntityType = { + EditionGroup: 'bb_edition_group' }; - let entityType = mapEntityType[entity.type]; - if (reviews.length) { - reviewContent = ( - - { - reviews.slice(0, 3).map((review) => ( - - )) - } - View all reviews > - - ) - } - return ( -
-

Reviews

- {reviewContent} -
- ); + const entityType = mapEntityType[entity.type]; + if (reviews.length) { + reviewContent = ( + + { + reviews.slice(0, 3).map((review) => ( + + )) + } + View all reviews > + + ); + } + return ( +
+

Reviews

+ {reviewContent} +
+ ); } -export default EntityReviews; \ No newline at end of file +export default EntityReviews; diff --git a/src/client/components/pages/entities/edition-group.js b/src/client/components/pages/entities/edition-group.js index 343663092e..c8ddc1b326 100644 --- a/src/client/components/pages/entities/edition-group.js +++ b/src/client/components/pages/entities/edition-group.js @@ -17,7 +17,7 @@ */ import EntityReviews from './cb-review'; -import { Rating } from "react-simple-star-rating"; +import {Rating} from 'react-simple-star-rating'; import * as bootstrap from 'react-bootstrap'; import * as entityHelper from '../../../helpers/entity'; import AuthorCreditDisplay from '../../author-credit-display'; @@ -61,15 +61,15 @@ function EditionGroupAttributes({editionGroup}) {
Ratings
@@ -136,7 +136,7 @@ function EditionGroupDisplayPage({entity, identifierTypes, user}) { - + } diff --git a/src/client/components/pages/entities/related-collections.js b/src/client/components/pages/entities/related-collections.js index bb2170736f..303fe15ea4 100644 --- a/src/client/components/pages/entities/related-collections.js +++ b/src/client/components/pages/entities/related-collections.js @@ -19,6 +19,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import * as bootstrap from 'react-bootstrap'; + + const {Row} = bootstrap; function EntityRelatedCollections({collections}) { diff --git a/src/server/helpers/critiquebrainz.ts b/src/server/helpers/critiquebrainz.ts index 624e256eca..699e7d2980 100644 --- a/src/server/helpers/critiquebrainz.ts +++ b/src/server/helpers/critiquebrainz.ts @@ -1,18 +1,19 @@ -import request from "superagent"; +import request from 'superagent'; + export const getReviewsFromCB = async (bbid: string, entityType: string) => { const mapEntityType = { - EditionGroup: "bb_edition_group", + EditionGroup: 'bb_edition_group' }; entityType = mapEntityType[entityType]; - console.log(entityType); + console.log(entityType); const res = await request - .get("https://beta.critiquebrainz.org/ws/1/review") - .query({ - entity_id: bbid, - entity_type: entityType, - limit: 10, - offset: 0, - }); - return res.body; + .get('https://beta.critiquebrainz.org/ws/1/review') + .query({ + entity_id: bbid, + entity_type: entityType, + limit: 10, + offset: 0 + }); + return res.body; }; diff --git a/src/server/helpers/middleware.ts b/src/server/helpers/middleware.ts index 23ef002c0d..e02efc9b78 100644 --- a/src/server/helpers/middleware.ts +++ b/src/server/helpers/middleware.ts @@ -20,7 +20,7 @@ import * as commonUtils from '../../common/helpers/utils'; import * as error from '../../common/helpers/error'; import * as utils from '../helpers/utils'; -import {getReviewsFromCB} from "./critiquebrainz"; +import {getReviewsFromCB} from './critiquebrainz'; import type {Response as $Response, NextFunction, Request} from 'express'; import _ from 'lodash'; @@ -226,10 +226,10 @@ export function makeEntityLoader(modelName: string, additionalRels: Array collection.public === true || parseInt(collection.ownerId, 10) === parseInt(req.user?.id, 10)); } - let reviews = []; - if(entity.type == "EditionGroup"){ - reviews = await getReviewsFromCB(bbid, entity.type); - } + let reviews = []; + if (entity.type == 'EditionGroup') { + reviews = await getReviewsFromCB(bbid, entity.type); + } entity.reviews = reviews; res.locals.entity = entity; return next(); From 94a960cca4ce56054c60dae1bbcdf4cfdc811ad2 Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Wed, 13 Jul 2022 09:36:56 +0000 Subject: [PATCH 100/258] feat: add review button and minor refactoring --- .../components/pages/entities/cb-review.js | 39 ++++++++++++++----- .../pages/entities/edition-group.js | 2 +- src/client/components/pages/entities/links.js | 1 - 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/client/components/pages/entities/cb-review.js b/src/client/components/pages/entities/cb-review.js index 9928e0b538..9456c724c9 100644 --- a/src/client/components/pages/entities/cb-review.js +++ b/src/client/components/pages/entities/cb-review.js @@ -1,16 +1,21 @@ import * as bootstrap from 'react-bootstrap'; import React from 'react'; import {Rating} from 'react-simple-star-rating'; +import {getEntityLink} from '../../../../common/helpers/utils'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faPlus} from '@fortawesome/free-solid-svg-icons'; -const {Col, Row} = bootstrap; +const {Button, Col, Row} = bootstrap; +const REVIEW_CONTENT_PREVIEW_LENGTH = 75; + function ReviewCard({reviewData}) { const publishedDate = new Date(reviewData.published_on).toDateString(); let reviewText = reviewData.text; - if (reviewText.length > 75) { - reviewText = `${reviewText.substring(0, 75)}...`; + if (reviewText.length > REVIEW_CONTENT_PREVIEW_LENGTH) { + reviewText = `${reviewText.substring(0, REVIEW_CONTENT_PREVIEW_LENGTH)}...`; } const reviewLink = `https://critiquebrainz.org/review/${reviewData.id}`; return ( @@ -39,14 +44,15 @@ function ReviewCard({reviewData}) { ); } -function EntityReviews({entity}) { - const {reviews} = entity.reviews; +function EntityReviews({entityReviews, entityType, entityBBID}) { let reviewContent; const mapEntityType = { - EditionGroup: 'bb_edition_group' + EditionGroup: 'edition-group' }; - const entityType = mapEntityType[entity.type]; - if (reviews.length) { + entityType = mapEntityType[entityType]; + const entityLink = `https://critiquebrainz.org/${entityType}/${entityBBID}`; + if (entityReviews?.length) { + const {reviews} = entityReviews; reviewContent = ( { @@ -57,10 +63,25 @@ function EntityReviews({entity}) { /> )) } - View all reviews > + View all reviews > ); } + else { + reviewContent = ( +
+

No reviews yet.

+ +
+ ); + } return (

Reviews

diff --git a/src/client/components/pages/entities/edition-group.js b/src/client/components/pages/entities/edition-group.js index c8ddc1b326..54bf6d9c43 100644 --- a/src/client/components/pages/entities/edition-group.js +++ b/src/client/components/pages/entities/edition-group.js @@ -136,7 +136,7 @@ function EditionGroupDisplayPage({entity, identifierTypes, user}) { - + } diff --git a/src/client/components/pages/entities/links.js b/src/client/components/pages/entities/links.js index ea858d7b03..a0640f086c 100644 --- a/src/client/components/pages/entities/links.js +++ b/src/client/components/pages/entities/links.js @@ -23,7 +23,6 @@ import EntityIdentifiers from './identifiers'; import EntityRelationships from './relationships'; import PropTypes from 'prop-types'; import React from 'react'; -import EntityReviews from './cb-review'; const {filterOutRelationshipTypeById} = entityHelper; From b5904e51b2dd6afbb8f7aa2cb5f8f1c555644a6d Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Wed, 13 Jul 2022 09:46:23 +0000 Subject: [PATCH 101/258] feat: return empty list if entity not supported --- src/server/helpers/critiquebrainz.ts | 11 ++++++++--- src/server/helpers/middleware.ts | 5 +---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/server/helpers/critiquebrainz.ts b/src/server/helpers/critiquebrainz.ts index 699e7d2980..86a73af934 100644 --- a/src/server/helpers/critiquebrainz.ts +++ b/src/server/helpers/critiquebrainz.ts @@ -1,14 +1,19 @@ import request from 'superagent'; -export const getReviewsFromCB = async (bbid: string, entityType: string) => { +export const getReviewsFromCB = async ( + bbid: string, + entityType: string +): Promise => { const mapEntityType = { EditionGroup: 'bb_edition_group' }; entityType = mapEntityType[entityType]; - console.log(entityType); + if (!entityType) { + return []; + } const res = await request - .get('https://beta.critiquebrainz.org/ws/1/review') + .get('https://critiquebrainz.org/ws/1/review') .query({ entity_id: bbid, entity_type: entityType, diff --git a/src/server/helpers/middleware.ts b/src/server/helpers/middleware.ts index e02efc9b78..1696f78367 100644 --- a/src/server/helpers/middleware.ts +++ b/src/server/helpers/middleware.ts @@ -226,10 +226,7 @@ export function makeEntityLoader(modelName: string, additionalRels: Array collection.public === true || parseInt(collection.ownerId, 10) === parseInt(req.user?.id, 10)); } - let reviews = []; - if (entity.type == 'EditionGroup') { - reviews = await getReviewsFromCB(bbid, entity.type); - } + const reviews = await getReviewsFromCB(bbid, entity.type); entity.reviews = reviews; res.locals.entity = entity; return next(); From 79dfe874bf9a1755989c6f7c08c0fede336c5ac6 Mon Sep 17 00:00:00 2001 From: tri10 Date: Wed, 13 Jul 2022 21:07:58 +0530 Subject: [PATCH 102/258] disable validations on entity creation modal --- .../unified-form/common/entity-modal-body.tsx | 77 +++++++++---------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/src/client/unified-form/common/entity-modal-body.tsx b/src/client/unified-form/common/entity-modal-body.tsx index 9d07c7384f..767d883d8e 100644 --- a/src/client/unified-form/common/entity-modal-body.tsx +++ b/src/client/unified-form/common/entity-modal-body.tsx @@ -1,7 +1,6 @@ -import {EntityModalBodyProps, EntityModalDispatchProps, EntityModalStateProps, State} from '../interface/type'; -import {camelCase, omit} from 'lodash'; -import {validateAliases, validateIdentifiers, validateNameSection} from '../../entity-editor/validators/common'; +import {EntityModalBodyProps, EntityModalDispatchProps} from '../interface/type'; import AliasModalBody from '../../entity-editor/alias-editor/alias-modal-body'; +// import {validateAliases, validateIdentifiers, validateNameSection} from '../../entity-editor/validators/common'; import AnnotationSection from '../../entity-editor/annotation-section/annotation-section'; import IdentifierModalBody from '../../entity-editor/identifier-editor/identifier-modal-body'; import NameSection from '../../entity-editor/name-section/name-section'; @@ -10,35 +9,35 @@ import RelationshipSection from '../../entity-editor/relationship-editor/relatio import SingleAccordion from './single-accordion'; import SubmissionSection from '../../entity-editor/submission-section/submission-section'; import {connect} from 'react-redux'; +import {omit} from 'lodash'; import {removeEmptyAliases} from '../../entity-editor/alias-editor/actions'; import {removeEmptyIdentifiers} from '../../entity-editor/identifier-editor/actions'; -import {validateAuthorSection} from '../../entity-editor/validators/author'; -import {validateEditionGroupSection} from '../../entity-editor/validators/edition-group'; -import {validateEditionSection} from '../../entity-editor/validators/edition'; -import {validatePublisherSection} from '../../entity-editor/validators/publisher'; -import {validateSeriesSection} from '../../entity-editor/validators/series'; -import {validateWorkSection} from '../../entity-editor/validators/work'; +// import {validateAuthorSection} from '../../entity-editor/validators/author'; +// import {validateEditionGroupSection} from '../../entity-editor/validators/edition-group'; +// import {validateEditionSection} from '../../entity-editor/validators/edition'; +// import {validatePublisherSection} from '../../entity-editor/validators/publisher'; +// import {validateSeriesSection} from '../../entity-editor/validators/series'; +// import {validateWorkSection} from '../../entity-editor/validators/work'; +/* currently disabling validations on modal due to slow performance issues of the form. */ -const entitySectionValidators = { - authorSection: validateAuthorSection, - editionGroupSection: validateEditionGroupSection, - editionSection: validateEditionSection, - publisherSection: validatePublisherSection, - seriesSection: validateSeriesSection, - workSection: validateWorkSection -}; -function EntityModalBody({onModalSubmit, children, validate, onAliasClose, onIdentifierClose, isNameSectionValid, isNameSectionEmpty, - isAliasEditorEmpty, isIdentifierEditorEmpty, isEntitySectionValid, - isIdentifierEditorValid, isAliasEditorValid, ...rest} +// const entitySectionValidators = { +// authorSection: validateAuthorSection, +// editionGroupSection: validateEditionGroupSection, +// editionSection: validateEditionSection, +// publisherSection: validatePublisherSection, +// seriesSection: validateSeriesSection, +// workSection: validateWorkSection +// }; +function EntityModalBody({onModalSubmit, children, validate, onAliasClose, onIdentifierClose, ...rest} :EntityModalBodyProps) { const genericProps:any = omit(rest, ['allIdentifierTypes']); return ( - + - + { React.cloneElement( React.Children.only(children), @@ -46,10 +45,10 @@ function EntityModalBody({onModalSubmit, children, validate, onAliasClose, onIde ) } - + - + @@ -66,20 +65,20 @@ function EntityModalBody({onModalSubmit, children, validate, onAliasClose, onIde EntityModalBody.defaultProps = { children: null }; -function mapStateToProps(state:State, {entityType, identifierTypes}) { - const nameSection = state.get('nameSection'); - const entitySection = `${camelCase(entityType)}Section`; - return { - isAliasEditorEmpty: state.get('aliasEditor', {}).size === 0, - isAliasEditorValid: validateAliases(state.get('aliasEditor', {})), - isEntitySectionValid: entitySectionValidators[entitySection](state.get(entitySection)), - isIdentifierEditorEmpty: state.get('identifierEditor', {}).size === 0, - isIdentifierEditorValid: validateIdentifiers(state.get('identifierEditor', {}), identifierTypes), - isNameSectionEmpty: !nameSection.get('name').length && !nameSection.get('sortName').length && !nameSection.get('language'), - isNameSectionValid: validateNameSection(nameSection) && - (!nameSection.get('exactMatches', [])?.length || nameSection.get('disambiguation').length > 0) - }; -} +// function mapStateToProps(state:State, {entityType, identifierTypes}) { +// const nameSection = state.get('nameSection'); +// const entitySection = `${camelCase(entityType)}Section`; +// return { +// isAliasEditorEmpty: state.get('aliasEditor', {}).size === 0, +// isAliasEditorValid: validateAliases(state.get('aliasEditor', {})), +// isEntitySectionValid: entitySectionValidators[entitySection](state.get(entitySection)), +// isIdentifierEditorEmpty: state.get('identifierEditor', {}).size === 0, +// isIdentifierEditorValid: validateIdentifiers(state.get('identifierEditor', {}), identifierTypes), +// isNameSectionEmpty: !nameSection.get('name').length && !nameSection.get('sortName').length && !nameSection.get('language'), +// isNameSectionValid: validateNameSection(nameSection) && +// (!nameSection.get('exactMatches', [])?.length || nameSection.get('disambiguation').length > 0) +// }; +// } function mapDispatchToProps(dispatch) { return { onAliasClose: () => dispatch(removeEmptyAliases()), @@ -87,4 +86,4 @@ function mapDispatchToProps(dispatch) { }; } -export default connect(mapStateToProps, mapDispatchToProps)(EntityModalBody); +export default connect(null, mapDispatchToProps)(EntityModalBody); From 20d12952606f6551636bb41d2d20c2a7ffeabff5 Mon Sep 17 00:00:00 2001 From: tri10 Date: Wed, 13 Jul 2022 22:55:37 +0530 Subject: [PATCH 103/258] feat(uf): major improvements in form performance --- .../author-credit-editor/author-credit-section.tsx | 6 ++++-- src/client/entity-editor/common/language-field.tsx | 4 +--- src/client/entity-editor/name-section/name-section.js | 11 ++++++----- src/client/unified-form/unified-form.tsx | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx index a34a0cdc8f..7cd0ab5c34 100644 --- a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx +++ b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx @@ -67,8 +67,10 @@ type DispatchProps = { type Props = OwnProps & StateProps & DispatchProps; function AuthorCreditSection({ - authorCreditEditor, onEditAuthorCredit, onEditorClose, showEditor, onAuthorChange, isEditable, onClearHandler, isUf, isLeftAlign, ...rest + authorCreditEditor: immutableAuthorCreditEditor, onEditAuthorCredit, onEditorClose, + showEditor, onAuthorChange, isEditable, onClearHandler, isUf, isLeftAlign, ...rest }: Props) { + const authorCreditEditor = convertMapToObject(immutableAuthorCreditEditor); let editor; if (showEditor) { editor = ( @@ -177,7 +179,7 @@ function mapStateToProps(rootState): StateProps { const isEditable = !(rootState.get('authorCreditEditor').size > 1) && authorCreditRow.get('name') === authorCreditRow.getIn(['author', 'text'], ''); return { - authorCreditEditor: convertMapToObject(rootState.get('authorCreditEditor')), + authorCreditEditor: rootState.get('authorCreditEditor'), isEditable, showEditor: rootState.getIn(['editionSection', 'authorCreditEditorVisible']) }; diff --git a/src/client/entity-editor/common/language-field.tsx b/src/client/entity-editor/common/language-field.tsx index 1d2524c850..7d757ec7b9 100644 --- a/src/client/entity-editor/common/language-field.tsx +++ b/src/client/entity-editor/common/language-field.tsx @@ -69,9 +69,7 @@ function LanguageField({ const tooltip = {tooltipText}; rest.options = convertMapToObject(rest.options); const {value, options} = rest; - const filterOptions = createFilterOptions({ - options - }); + const filterOptions = React.useMemo(() => createFilterOptions({options}), []); const sortFilterOptions = (opts, input, selectOptions) => { const newOptions = filterOptions(opts, input, selectOptions).slice(0, MAX_DROPDOWN_OPTIONS); const sortLang = (a, b) => { diff --git a/src/client/entity-editor/name-section/name-section.js b/src/client/entity-editor/name-section/name-section.js index a17baf1fd9..1ca8b2ff76 100644 --- a/src/client/entity-editor/name-section/name-section.js +++ b/src/client/entity-editor/name-section/name-section.js @@ -170,7 +170,7 @@ class NameSection extends React.Component { const { disambiguationDefaultValue, entityType, - exactMatches, + exactMatches: immutableExactMatches, languageOptions, languageValue, nameValue, @@ -178,11 +178,12 @@ class NameSection extends React.Component { onLanguageChange, onSortNameChange, onDisambiguationChange, - searchResults, + searchResults: immutableSearchResults, isUf, isModal } = this.props; - + const exactMatches = convertMapToObject(immutableExactMatches); + const searchResults = convertMapToObject(immutableSearchResults); const languageOptionsForDisplay = languageOptions.map((language) => ({ frequency: language.frequency, label: language.name, @@ -329,11 +330,11 @@ function mapStateToProps(rootState) { ); return { disambiguationDefaultValue: state.get('disambiguation'), - exactMatches: convertMapToObject(state.get('exactMatches')), + exactMatches: state.get('exactMatches'), languageValue: state.get('language'), nameValue: state.get('name'), searchForExistingEditionGroup, - searchResults: convertMapToObject(state.get('searchResults')), + searchResults: state.get('searchResults'), sortNameValue: state.get('sortName') }; } diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index a024968c67..70deb79a46 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -24,7 +24,7 @@ function getUfValidator(validator) { export function UnifiedForm(props:UnifiedFormProps) { const {allIdentifierTypes, validator, onSubmit, formValid} = props; const [tabKey, setTabKey] = React.useState('cover'); - const editionIdentifierTypes = filterIdentifierTypesByEntityType(allIdentifierTypes, 'Edition'); + const editionIdentifierTypes = React.useMemo(() => filterIdentifierTypesByEntityType(allIdentifierTypes, 'Edition'), []); const editionValidator = validator && getUfValidator(validator); const tabKeys = ['cover', 'detail', 'content', 'submit']; const onNextHandler = React.useCallback(() => { From 0904eff631c115cbb885d144c4e2bc4e957e6283 Mon Sep 17 00:00:00 2001 From: tri10 Date: Wed, 13 Jul 2022 23:53:12 +0530 Subject: [PATCH 104/258] feat(uf): performance improvements in modal --- .../annotation-section/annotation-section.js | 5 ++-- .../name-section/name-section.js | 14 ++++++++++- src/client/unified-form/action.ts | 24 +++++++++++++++++++ .../common/search-entity-create-select.tsx | 5 ++-- .../unified-form/cover-tab/cover-tab.tsx | 9 +++---- src/client/unified-form/helpers.ts | 14 ++++++++++- src/client/unified-form/interface/type.ts | 1 + 7 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/client/entity-editor/annotation-section/annotation-section.js b/src/client/entity-editor/annotation-section/annotation-section.js index 0dbd6365c7..0a662fe29f 100644 --- a/src/client/entity-editor/annotation-section/annotation-section.js +++ b/src/client/entity-editor/annotation-section/annotation-section.js @@ -39,10 +39,11 @@ import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; * AnnotationSection. */ function AnnotationSection({ - annotation, + annotation: immutableAnnotation, onAnnotationChange, isUf }) { + const annotation = convertMapToObject(immutableAnnotation); const annotationLabel = ( Annotation @@ -106,7 +107,7 @@ AnnotationSection.defaultProps = { }; function mapStateToProps(rootState) { return { - annotation: convertMapToObject(rootState.get('annotationSection')) + annotation: rootState.get('annotationSection') }; } diff --git a/src/client/entity-editor/name-section/name-section.js b/src/client/entity-editor/name-section/name-section.js index 1ca8b2ff76..b9bf599c8f 100644 --- a/src/client/entity-editor/name-section/name-section.js +++ b/src/client/entity-editor/name-section/name-section.js @@ -34,6 +34,7 @@ import { } from '../validators/common'; import DisambiguationField from './disambiguation-field'; +import Immutable from 'immutable'; import LanguageField from '../common/language-field'; import NameField from '../common/name-field'; import PropTypes from 'prop-types'; @@ -320,7 +321,7 @@ NameSection.defaultProps = { }; -function mapStateToProps(rootState) { +function mapStateToProps(rootState, {isUf, setDefault}) { const state = rootState.get('nameSection'); const editionSectionState = rootState.get('editionSection'); const searchForExistingEditionGroup = Boolean(editionSectionState) && @@ -328,6 +329,17 @@ function mapStateToProps(rootState) { !editionSectionState.get('editionGroup') || editionSectionState.get('editionGroupRequired') ); + // to prevent double double state updates on action caused by modal + if (isUf && setDefault) { + return { + disambiguationDefaultValue: '', + exactMatches: Immutable.Map([]), + languageValue: null, + nameValue: '', + searchResults: Immutable.Map([]), + sortNameValue: '' + }; + } return { disambiguationDefaultValue: state.get('disambiguation'), exactMatches: state.get('exactMatches'), diff --git a/src/client/unified-form/action.ts b/src/client/unified-form/action.ts index ac2590b666..ec136a6903 100644 --- a/src/client/unified-form/action.ts +++ b/src/client/unified-form/action.ts @@ -3,6 +3,8 @@ import {Action} from './interface/type'; export const DUMP_EDITION = 'DUMP_EDITION'; export const LOAD_EDITION = 'LOAD_EDITION'; +export const OPEN_ENTITY_MODAL = 'OPEN_ENTITY_MODAL'; +export const CLOSE_ENTITY_MODAL = 'CLOSE_ENTITY_MODAL'; const nextEditionId = 0; @@ -38,3 +40,25 @@ export function loadEdition(editionId = 'e0'):Action { type: LOAD_EDITION }; } + +/** + * Set entity modal state to open + * + * @returns {Action} The resulting OPEN_ENTITY_MODAL action. + */ +export function openEntityModal():Action { + return { + type: OPEN_ENTITY_MODAL + }; +} + +/** + * Set entity modal state to close + * + * @returns {Action} The resulting CLOSE_ENTITY_MODAL action. + */ +export function closeEntityModal():Action { + return { + type: CLOSE_ENTITY_MODAL + }; +} diff --git a/src/client/unified-form/common/search-entity-create-select.tsx b/src/client/unified-form/common/search-entity-create-select.tsx index 90266f8607..2acab2a2c3 100644 --- a/src/client/unified-form/common/search-entity-create-select.tsx +++ b/src/client/unified-form/common/search-entity-create-select.tsx @@ -1,7 +1,7 @@ import {SearchEntityCreateDispatchProps, SearchEntityCreateProps} from '../interface/type'; import {addAuthor, addPublisher} from '../cover-tab/action'; import {checkIfNameExists, searchName, updateNameField, updateSortNameField} from '../../entity-editor/name-section/actions'; -import {dumpEdition, loadEdition} from '../action'; +import {closeEntityModal, dumpEdition, loadEdition, openEntityModal} from '../action'; import AsyncCreatable from 'react-select/async-creatable'; import BaseEntitySearch from '../../entity-editor/common/entity-search-field-option'; import CreateEntityModal from './create-entity-modal'; @@ -72,13 +72,14 @@ SearchEntityCreate.defaultProps = defaultProps; function mapDispatchToProps(dispatch, {type}):SearchEntityCreateDispatchProps { return { - onModalClose: () => dispatch(loadEdition()), + onModalClose: () => dispatch(loadEdition()) && dispatch(closeEntityModal()), onModalOpen: (name) => { dispatch(dumpEdition(type)); dispatch(updateNameField(name)); dispatch(updateSortNameField(name)); dispatch(checkIfNameExists(name, null, type, null)); dispatch(searchName(name, null, type)); + dispatch(openEntityModal()); }, onSubmitEntity: (arg) => dispatch(addEntityAction[type](arg)) }; diff --git a/src/client/unified-form/cover-tab/cover-tab.tsx b/src/client/unified-form/cover-tab/cover-tab.tsx index d3e61625e0..bb942710ba 100644 --- a/src/client/unified-form/cover-tab/cover-tab.tsx +++ b/src/client/unified-form/cover-tab/cover-tab.tsx @@ -14,7 +14,7 @@ import {updatePublisher} from '../../entity-editor/edition-section/actions'; export function CoverTab(props:CoverProps) { - const {publisherValue: publishers, onPublisherChange, identifierEditorVisible, onClearPublisher, handleClearPublishers} = props; + const {publisherValue: publishers, onPublisherChange, identifierEditorVisible, onClearPublisher, handleClearPublishers, modalIsOpen} = props; const publisherValue:EntitySelect[] = Object.values(convertMapToObject(publishers ?? {})); const onChangeHandler = React.useCallback((value:EntitySelect[], action) => { if (['remove-value', 'pop-value'].includes(action.action)) { @@ -29,7 +29,7 @@ export function CoverTab(props:CoverProps) { }, []); return (
- + @@ -57,14 +57,15 @@ export function CoverTab(props:CoverProps) { ); } -function mapStateToProps(rootState) { +function mapStateToProps(rootState):CoverStateProps { return { identifierEditorVisible: rootState.getIn(['buttonBar', 'identifierEditorVisible']), + modalIsOpen: rootState.get('entityModalIsOpen', false), publisherValue: rootState.getIn(['editionSection', 'publisher'], {}) }; } -function mapDispatchToProps(dispatch) { +function mapDispatchToProps(dispatch):CoverDispatchProps { return { handleClearPublishers: () => dispatch(clearPublishers()), onClearPublisher: (arg) => dispatch(clearPublisher(arg)), diff --git a/src/client/unified-form/helpers.ts b/src/client/unified-form/helpers.ts index 3004de480f..5f1dd22cab 100644 --- a/src/client/unified-form/helpers.ts +++ b/src/client/unified-form/helpers.ts @@ -1,5 +1,5 @@ import {ADD_AUTHOR, ADD_PUBLISHER} from './cover-tab/action'; -import {DUMP_EDITION, LOAD_EDITION} from './action'; +import {CLOSE_ENTITY_MODAL, DUMP_EDITION, LOAD_EDITION, OPEN_ENTITY_MODAL} from './action'; import {ISBNReducer, authorsReducer, publishersReducer} from './cover-tab/reducer'; import {ADD_EDITION_GROUP} from './detail-tab/action'; import {ADD_WORK} from './content-tab/action'; @@ -42,6 +42,17 @@ function newEditionReducer(state = Immutable.Map({}), action) { return state; } } +function entityModalIsOpenReducer(state = false, action) { + const {type} = action; + switch (type) { + case OPEN_ENTITY_MODAL: + return true; + case CLOSE_ENTITY_MODAL: + return false; + default: + return state; + } +} const initialACState = Immutable.fromJS( { n0: { @@ -228,6 +239,7 @@ export function createRootReducer() { buttonBar: buttonBarReducer, editionGroupSection: editionGroupSectionReducer, editionSection: editionSectionReducer, + entityModalIsOpen: entityModalIsOpenReducer, identifierEditor: identifierEditorReducer, nameSection: nameSectionReducer, publisherSection: publisherSectionReducer, diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index 2c4f33e205..88e3e543ce 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -51,6 +51,7 @@ export type CoverOwnProps = { }; export type CoverStateProps = { publisherValue:any[], + modalIsOpen:boolean, identifierEditorVisible:boolean }; export type CoverDispatchProps = { From 848aec981db6b8894bd8ac7085b8c14982e53d95 Mon Sep 17 00:00:00 2001 From: tri10 Date: Thu, 14 Jul 2022 13:00:26 +0530 Subject: [PATCH 105/258] feat: improve modal rendering time --- src/client/entity-editor/common/language-field.tsx | 3 ++- .../name-section/disambiguation-field.tsx | 2 +- src/client/unified-form/common/freezed-objects.ts | 4 ++++ src/client/unified-form/interface/type.ts | 1 + src/client/unified-form/unified-form.tsx | 13 ++++++++++++- 5 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 src/client/unified-form/common/freezed-objects.ts diff --git a/src/client/entity-editor/common/language-field.tsx b/src/client/entity-editor/common/language-field.tsx index 7d757ec7b9..4bdd3632ec 100644 --- a/src/client/entity-editor/common/language-field.tsx +++ b/src/client/entity-editor/common/language-field.tsx @@ -26,6 +26,7 @@ import ValidationLabel from './validation-label'; import {convertMapToObject} from '../../helpers/utils'; import createFilterOptions from 'react-select-fast-filter-options'; import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; +import {freezeObjects} from '../../unified-form/common/freezed-objects'; import {isNumber} from 'lodash'; @@ -69,7 +70,7 @@ function LanguageField({ const tooltip = {tooltipText}; rest.options = convertMapToObject(rest.options); const {value, options} = rest; - const filterOptions = React.useMemo(() => createFilterOptions({options}), []); + const filterOptions = freezeObjects.filterOptions ?? React.useMemo(() => createFilterOptions({options}), []); const sortFilterOptions = (opts, input, selectOptions) => { const newOptions = filterOptions(opts, input, selectOptions).slice(0, MAX_DROPDOWN_OPTIONS); const sortLang = (a, b) => { diff --git a/src/client/entity-editor/name-section/disambiguation-field.tsx b/src/client/entity-editor/name-section/disambiguation-field.tsx index 0ebad8a41c..9f77ca736f 100644 --- a/src/client/entity-editor/name-section/disambiguation-field.tsx +++ b/src/client/entity-editor/name-section/disambiguation-field.tsx @@ -86,4 +86,4 @@ DisambiguationField.defaultProps = { required: false }; -export default DisambiguationField; +export default React.memo(DisambiguationField); diff --git a/src/client/unified-form/common/freezed-objects.ts b/src/client/unified-form/common/freezed-objects.ts new file mode 100644 index 0000000000..8bdc3e733e --- /dev/null +++ b/src/client/unified-form/common/freezed-objects.ts @@ -0,0 +1,4 @@ +// Singleton Pattern: this will ensure that expensive calculations is done only once +export const freezeObjects = { + filterOptions: null +}; diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index 88e3e543ce..15f6443f09 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -39,6 +39,7 @@ export type UnifiedFormStateProps = { }; export type UnifiedFormOwnProps = { allIdentifierTypes?:IdentifierType[], + languageOptions?:LanguageOption[], validator?:(state:Immutable.Map, ...args) => boolean, }; export type UnifiedFormProps = UnifiedFormOwnProps & UnifiedFormDispatchProps & UnifiedFormStateProps; diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index 70deb79a46..43fe9b9692 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -8,7 +8,9 @@ import React from 'react'; import SubmitSection from '../entity-editor/submission-section/submission-section'; import SummarySection from './submit-tab/summary'; import {connect} from 'react-redux'; +import createFilterOptions from 'react-select-fast-filter-options'; import {filterIdentifierTypesByEntityType} from '../../common/helpers/utils'; +import {freezeObjects} from './common/freezed-objects'; import {submit} from '../entity-editor/submission-section/actions'; @@ -22,7 +24,16 @@ function getUfValidator(validator) { }; } export function UnifiedForm(props:UnifiedFormProps) { - const {allIdentifierTypes, validator, onSubmit, formValid} = props; + const {allIdentifierTypes, validator, onSubmit, formValid, languageOptions} = props; + React.useMemo(() => { + const options = languageOptions.map((language) => ({ + frequency: language.frequency, + label: language.name, + value: language.id + })); + freezeObjects.filterOptions = createFilterOptions({options}); + Object.freeze(freezeObjects); + }, []); const [tabKey, setTabKey] = React.useState('cover'); const editionIdentifierTypes = React.useMemo(() => filterIdentifierTypesByEntityType(allIdentifierTypes, 'Edition'), []); const editionValidator = validator && getUfValidator(validator); From 800737347730bee0e650111b3a00ecee36368621 Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Thu, 14 Jul 2022 07:42:36 +0000 Subject: [PATCH 106/258] lint again --- .../components/pages/entities/cb-review.js | 62 ++++++++++++++++--- .../pages/entities/edition-group.js | 5 +- src/client/components/pages/entities/links.js | 3 +- .../pages/entities/related-collections.js | 3 +- src/server/helpers/critiquebrainz.ts | 32 +++++++--- src/server/helpers/middleware.ts | 3 +- 6 files changed, 87 insertions(+), 21 deletions(-) diff --git a/src/client/components/pages/entities/cb-review.js b/src/client/components/pages/entities/cb-review.js index 9456c724c9..e3df6f1db8 100644 --- a/src/client/components/pages/entities/cb-review.js +++ b/src/client/components/pages/entities/cb-review.js @@ -1,17 +1,35 @@ +/* + * Copyright (C) 2022 Ansh Goyal + * + * 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 React from 'react'; -import {Rating} from 'react-simple-star-rating'; -import {getEntityLink} from '../../../../common/helpers/utils'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import PropTypes from 'prop-types'; +import {Rating} from 'react-simple-star-rating'; +import React from 'react'; import {faPlus} from '@fortawesome/free-solid-svg-icons'; -const {Button, Col, Row} = bootstrap; +const {Button} = bootstrap; const REVIEW_CONTENT_PREVIEW_LENGTH = 75; -function ReviewCard({reviewData}) { +function ReviewCard(reviewData) { const publishedDate = new Date(reviewData.published_on).toDateString(); let reviewText = reviewData.text; if (reviewText.length > REVIEW_CONTENT_PREVIEW_LENGTH) { @@ -33,7 +51,7 @@ function ReviewCard({reviewData}) { stars={5} /> - Review by: {reviewData.user.display_name} {publishedDate} + Review by: {reviewData.user.display_name} {publishedDate}
@@ -44,13 +62,14 @@ function ReviewCard({reviewData}) { ); } -function EntityReviews({entityReviews, entityType, entityBBID}) { +function EntityReviews(props) { + const {entityReviews, entityType, entityBBID} = props; let reviewContent; const mapEntityType = { EditionGroup: 'edition-group' }; - entityType = mapEntityType[entityType]; - const entityLink = `https://critiquebrainz.org/${entityType}/${entityBBID}`; + const cbEntityType = mapEntityType[entityType]; + const entityLink = `https://critiquebrainz.org/${cbEntityType}/${entityBBID}`; if (entityReviews?.length) { const {reviews} = entityReviews; reviewContent = ( @@ -90,4 +109,29 @@ function EntityReviews({entityReviews, entityType, entityBBID}) { ); } + +ReviewCard.displayName = 'ReviewCard'; +ReviewCard.propTypes = { + reviewData: PropTypes.shape({ + id: PropTypes.string.isRequired, + // eslint-disable-next-line camelcase + published_on: PropTypes.string.isRequired, + rating: PropTypes.number, + text: PropTypes.string.isRequired, + user: PropTypes.shape({ + // eslint-disable-next-line camelcase + display_name: PropTypes.string.isRequired + }).isRequired + }).isRequired +}; + + +EntityReviews.displayName = 'EntityReviews'; +EntityReviews.propTypes = { + entityBBID: PropTypes.string.isRequired, + entityReviews: PropTypes.array.isRequired, + entityType: PropTypes.string.isRequired +}; + + export default EntityReviews; diff --git a/src/client/components/pages/entities/edition-group.js b/src/client/components/pages/entities/edition-group.js index 54bf6d9c43..90b1a366ea 100644 --- a/src/client/components/pages/entities/edition-group.js +++ b/src/client/components/pages/entities/edition-group.js @@ -1,5 +1,6 @@ /* * Copyright (C) 2017 Ben Ockmore + * 2022 Ansh Goyal * * 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 @@ -16,8 +17,6 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import EntityReviews from './cb-review'; -import {Rating} from 'react-simple-star-rating'; import * as bootstrap from 'react-bootstrap'; import * as entityHelper from '../../../helpers/entity'; import AuthorCreditDisplay from '../../author-credit-display'; @@ -27,8 +26,10 @@ import EntityFooter from './footer'; import EntityImage from './image'; import EntityLinks from './links'; import EntityRelatedCollections from './related-collections'; +import EntityReviews from './cb-review'; import EntityTitle from './title'; import PropTypes from 'prop-types'; +import {Rating} from 'react-simple-star-rating'; import React from 'react'; diff --git a/src/client/components/pages/entities/links.js b/src/client/components/pages/entities/links.js index a0640f086c..8e3d8c299c 100644 --- a/src/client/components/pages/entities/links.js +++ b/src/client/components/pages/entities/links.js @@ -1,5 +1,6 @@ /* * Copyright (C) 2017 Eshan Singh + * 2022 Ansh Goyal * * 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 @@ -26,7 +27,7 @@ import React from 'react'; const {filterOutRelationshipTypeById} = entityHelper; -const {Col, Row} = bootstrap; +const {Row} = bootstrap; function EntityLinks({entity, identifierTypes, urlPrefix}) { // relationshipTypeId = 10 refers the relation ( is contained by ) diff --git a/src/client/components/pages/entities/related-collections.js b/src/client/components/pages/entities/related-collections.js index 303fe15ea4..4da09782af 100644 --- a/src/client/components/pages/entities/related-collections.js +++ b/src/client/components/pages/entities/related-collections.js @@ -1,5 +1,6 @@ /* * Copyright (C) 2021 Akash Gupta + * 2022 Ansh Goyal * * 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 @@ -16,9 +17,9 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +import * as bootstrap from 'react-bootstrap'; import PropTypes from 'prop-types'; import React from 'react'; -import * as bootstrap from 'react-bootstrap'; const {Row} = bootstrap; diff --git a/src/server/helpers/critiquebrainz.ts b/src/server/helpers/critiquebrainz.ts index 86a73af934..76db77a1ef 100644 --- a/src/server/helpers/critiquebrainz.ts +++ b/src/server/helpers/critiquebrainz.ts @@ -1,24 +1,42 @@ +/* + * Copyright (C) 2022 Ansh Goyal + * + * 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 request from 'superagent'; -export const getReviewsFromCB = async ( - bbid: string, - entityType: string -): Promise => { +export async function getReviewsFromCB(bbid: string, + entityType: string): Promise { const mapEntityType = { EditionGroup: 'bb_edition_group' }; - entityType = mapEntityType[entityType]; + const cbEntityType = mapEntityType[entityType]; if (!entityType) { return []; } const res = await request .get('https://critiquebrainz.org/ws/1/review') .query({ + // eslint-disable-next-line camelcase entity_id: bbid, - entity_type: entityType, + // eslint-disable-next-line camelcase + entity_type: cbEntityType, limit: 10, offset: 0 }); return res.body; -}; +} diff --git a/src/server/helpers/middleware.ts b/src/server/helpers/middleware.ts index 1696f78367..ad28345c95 100644 --- a/src/server/helpers/middleware.ts +++ b/src/server/helpers/middleware.ts @@ -2,6 +2,7 @@ * Copyright (C) 2015 Ben Ockmore * 2015-2016 Sean Burke * 2021 Akash Gupta + * 2022 Ansh Goyal * 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 @@ -20,11 +21,11 @@ import * as commonUtils from '../../common/helpers/utils'; import * as error from '../../common/helpers/error'; import * as utils from '../helpers/utils'; -import {getReviewsFromCB} from './critiquebrainz'; import type {Response as $Response, NextFunction, Request} from 'express'; import _ from 'lodash'; import {getRelationshipTargetBBIDByTypeId} from '../../client/helpers/entity'; +import {getReviewsFromCB} from './critiquebrainz'; interface $Request extends Request { From ab7dea3f46933b1810532b4ddbb57c8b4f3ca075 Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Thu, 14 Jul 2022 17:55:36 +0000 Subject: [PATCH 107/258] feat: show reload button if error occurs while fetching reviews --- .../components/pages/entities/cb-review.js | 111 +++++++++++------- src/server/helpers/critiquebrainz.ts | 31 +++-- 2 files changed, 88 insertions(+), 54 deletions(-) diff --git a/src/client/components/pages/entities/cb-review.js b/src/client/components/pages/entities/cb-review.js index e3df6f1db8..b979d42a42 100644 --- a/src/client/components/pages/entities/cb-review.js +++ b/src/client/components/pages/entities/cb-review.js @@ -17,11 +17,11 @@ */ import * as bootstrap from 'react-bootstrap'; +import {faPlus, faRotate} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import PropTypes from 'prop-types'; import {Rating} from 'react-simple-star-rating'; import React from 'react'; -import {faPlus} from '@fortawesome/free-solid-svg-icons'; const {Button} = bootstrap; @@ -62,51 +62,78 @@ function ReviewCard(reviewData) { ); } -function EntityReviews(props) { - const {entityReviews, entityType, entityBBID} = props; - let reviewContent; - const mapEntityType = { - EditionGroup: 'edition-group' - }; - const cbEntityType = mapEntityType[entityType]; - const entityLink = `https://critiquebrainz.org/${cbEntityType}/${entityBBID}`; - if (entityReviews?.length) { - const {reviews} = entityReviews; - reviewContent = ( - - { - reviews.slice(0, 3).map((review) => ( - - )) - } - View all reviews > - - ); +class EntityReviews extends React.Component { + constructor(props) { + super(props); + this.handleRefresh = this.handleRefresh.bind(this); + } + + handleRefresh() { + window.location.reload(false); } - else { - reviewContent = ( + + render() { + const {entityReviews, entityType, entityBBID} = this.props; + const {reviews, successfullyFetched} = entityReviews; + let reviewContent; + const mapEntityType = { + EditionGroup: 'edition-group' + }; + const cbEntityType = mapEntityType[entityType]; + const entityLink = `https://critiquebrainz.org/${cbEntityType}/${entityBBID}`; + + if (reviews?.length) { + const {reviewData} = entityReviews; + reviewContent = ( + + { + reviewData.slice(0, 3).map((review) => ( + + )) + } + View all reviews > + + ); + } + else if (successfullyFetched) { + reviewContent = ( +
+

No reviews yet.

+ +
+ ); + } + else { + reviewContent = ( +
+

Could not fetch reviews.

+ +
+ ); + } + return (
-

No reviews yet.

- +

Reviews

+ {reviewContent}
); } - return ( -
-

Reviews

- {reviewContent} -
- ); } @@ -129,7 +156,7 @@ ReviewCard.propTypes = { EntityReviews.displayName = 'EntityReviews'; EntityReviews.propTypes = { entityBBID: PropTypes.string.isRequired, - entityReviews: PropTypes.array.isRequired, + entityReviews: PropTypes.object.isRequired, entityType: PropTypes.string.isRequired }; diff --git a/src/server/helpers/critiquebrainz.ts b/src/server/helpers/critiquebrainz.ts index 76db77a1ef..f73727e6ea 100644 --- a/src/server/helpers/critiquebrainz.ts +++ b/src/server/helpers/critiquebrainz.ts @@ -26,17 +26,24 @@ export async function getReviewsFromCB(bbid: string, }; const cbEntityType = mapEntityType[entityType]; if (!entityType) { - return []; + return {reviews: [], successfullyFetched: true}; + } + try { + const res = await request + .get('https://critiquebrainz.org/ws/1/review') + .query({ + // eslint-disable-next-line camelcase + entity_id: bbid, + // eslint-disable-next-line camelcase + entity_type: cbEntityType, + limit: 10, + offset: 0 + }); + return {reviews: res.body, successfullyFetched: true}; + } + catch (err) { + // eslint-disable-next-line no-console + console.error(err); + return {reviews: [], successfullyFetched: false}; } - const res = await request - .get('https://critiquebrainz.org/ws/1/review') - .query({ - // eslint-disable-next-line camelcase - entity_id: bbid, - // eslint-disable-next-line camelcase - entity_type: cbEntityType, - limit: 10, - offset: 0 - }); - return res.body; } From a61e53aad3202ca356169fa18c7537d815aa489c Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Fri, 15 Jul 2022 06:28:30 +0000 Subject: [PATCH 108/258] feat: add external service route --- src/server/routes.js | 2 ++ src/server/routes/externalService.js | 34 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 src/server/routes/externalService.js diff --git a/src/server/routes.js b/src/server/routes.js index 56a29a35e3..d4528406e7 100644 --- a/src/server/routes.js +++ b/src/server/routes.js @@ -24,6 +24,7 @@ import collectionsRouter from './routes/collections'; import editionGroupRouter from './routes/entity/edition-group'; import editionRouter from './routes/entity/edition'; import editorRouter from './routes/editor'; +import externalService from './routes/externalService'; import indexRouter from './routes/index'; import mergeRouter from './routes/merge'; import publisherRouter from './routes/entity/publisher'; @@ -44,6 +45,7 @@ function initRootRoutes(app) { app.use('/revisions', revisionsRouter); app.use('/collections', collectionsRouter); app.use('/statistics', statisticsRouter); + app.use('/external-service', externalService); } function initEditionGroupRoutes(app) { diff --git a/src/server/routes/externalService.js b/src/server/routes/externalService.js new file mode 100644 index 0000000000..67f62dfec0 --- /dev/null +++ b/src/server/routes/externalService.js @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 Ansh Goyal + * + * 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 cbHelper from '../helpers/critiquebrainz'; + + +import express from 'express'; + + +const router = express.Router(); + +router.get('/critiquebrainz/reviews', async (req, res) => { + const {entityBBID, entityType} = req.query; + const reviews = await cbHelper.getReviewsFromCB(entityBBID, entityType); + res.json(reviews); +}); + +export default router; From eb92dfb55ef4bc7de28c78fbae339583f6fc1582 Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Fri, 15 Jul 2022 06:54:16 +0000 Subject: [PATCH 109/258] feat: use state to store the entity reviews --- .../components/pages/entities/cb-review.js | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/client/components/pages/entities/cb-review.js b/src/client/components/pages/entities/cb-review.js index b979d42a42..4d399a2703 100644 --- a/src/client/components/pages/entities/cb-review.js +++ b/src/client/components/pages/entities/cb-review.js @@ -22,6 +22,8 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import PropTypes from 'prop-types'; import {Rating} from 'react-simple-star-rating'; import React from 'react'; +import _ from 'lodash'; +import request from 'superagent'; const {Button} = bootstrap; @@ -29,7 +31,7 @@ const {Button} = bootstrap; const REVIEW_CONTENT_PREVIEW_LENGTH = 75; -function ReviewCard(reviewData) { +function ReviewCard({reviewData}) { const publishedDate = new Date(reviewData.published_on).toDateString(); let reviewText = reviewData.text; if (reviewText.length > REVIEW_CONTENT_PREVIEW_LENGTH) { @@ -65,29 +67,41 @@ function ReviewCard(reviewData) { class EntityReviews extends React.Component { constructor(props) { super(props); - this.handleRefresh = this.handleRefresh.bind(this); + this.handleClick = this.handleClick.bind(this); + this.state = { + reviews: props.entityReviews.reviews, + successfullyFetched: props.entityReviews.successfullyFetched + }; + this.entityType = props.entityType; + this.entityBBID = props.entityBBID; } - handleRefresh() { - window.location.reload(false); + async handleClick() { + const data = await request.get('/external-service/critiquebrainz/reviews') + .query({ + entityBBID: this.entityBBID, + entityType: this.entityType + }); + + this.setState({ + reviews: data.body.reviews, + successfullyFetched: data.body.successfullyFetched + }); } render() { - const {entityReviews, entityType, entityBBID} = this.props; - const {reviews, successfullyFetched} = entityReviews; let reviewContent; const mapEntityType = { EditionGroup: 'edition-group' }; - const cbEntityType = mapEntityType[entityType]; - const entityLink = `https://critiquebrainz.org/${cbEntityType}/${entityBBID}`; - - if (reviews?.length) { - const {reviewData} = entityReviews; + const cbEntityType = mapEntityType[this.entityType]; + const entityLink = `https://critiquebrainz.org/${cbEntityType}/${this.entityBBID}`; + if (this.state.reviews && !_.isEmpty(this.state.reviews)) { + const {reviews: reviewsData} = this.state.reviews; reviewContent = ( { - reviewData.slice(0, 3).map((review) => ( + reviewsData.slice(0, 3).map((review) => ( ); } - else if (successfullyFetched) { + else if (this.state.successfullyFetched) { reviewContent = (

No reviews yet.

@@ -119,7 +133,7 @@ class EntityReviews extends React.Component {

Could not fetch reviews.

Select...
"`; diff --git a/test/src/client/unified-form/content-tab/__snapshots__/work-row.jsx.snap b/test/src/client/unified-form/content-tab/__snapshots__/work-row.jsx.snap new file mode 100644 index 0000000000..84f2a74b91 --- /dev/null +++ b/test/src/client/unified-form/content-tab/__snapshots__/work-row.jsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WorkRow should render correctly 1`] = `"
"`; diff --git a/test/src/client/unified-form/content-tab/content-tab.jsx b/test/src/client/unified-form/content-tab/content-tab.jsx new file mode 100644 index 0000000000..78bb710137 --- /dev/null +++ b/test/src/client/unified-form/content-tab/content-tab.jsx @@ -0,0 +1,71 @@ +// eslint-disable-next-line import/no-unassigned-import +import '../../../../../enzyme.config'; +import * as React from 'react'; +import {expect, use} from 'chai'; +import {BASE_PROPS} from '../helpers'; +import ContentTab from '../../../../../src/client/unified-form/content-tab/content-tab'; +import Immutable from 'immutable'; +import {Provider} from 'react-redux'; +import configureStore from 'redux-mock-store'; +import {jestSnapshotPlugin} from 'mocha-chai-jest-snapshot'; +import {mount} from 'enzyme'; +import {spy} from 'sinon'; + + +use(jestSnapshotPlugin()); + +const initialState = Immutable.fromJS({ + Works: { + 0: {}, + 1: {} + } +}); +const props = { + ...BASE_PROPS +}; + +describe('ContentTab', () => { + let store; + let wrapper; + let dispatchSpy; + const newWork = { + name: 'Dummy' + }; + beforeEach(() => { + store = configureStore()(initialState); + dispatchSpy = spy(); + store.dispatch = dispatchSpy; + wrapper = mount( + + + + ); + }); + + it('should render correctly', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('should have correct number of work rows by default', () => { + const rows = wrapper.find('WorkRow'); + expect(rows.length).to.equal(2); + }); + + it('should trigger add work action', () => { + const select = wrapper.find('Select').at(2); + select.props().onChange(newWork, {}); + expect(dispatchSpy.callCount).equal(1); + const {payload} = dispatchSpy.args[0][0]; + expect(payload.value).equal(newWork); + }); + + it('should correctly set check property', () => { + const select = wrapper.find('Select').at(2); + const checkbox = wrapper.find('input.form-check-input').at(2); + checkbox.simulate('change'); + select.props().onChange(newWork, {}); + expect(dispatchSpy.callCount).equal(1); + const {payload} = dispatchSpy.args[0][0]; + expect(payload?.value?.checked).equal(true); + }); +}); diff --git a/test/src/client/unified-form/content-tab/work-row.jsx b/test/src/client/unified-form/content-tab/work-row.jsx new file mode 100644 index 0000000000..726a490475 --- /dev/null +++ b/test/src/client/unified-form/content-tab/work-row.jsx @@ -0,0 +1,74 @@ +// eslint-disable-next-line import/no-unassigned-import +import '../../../../../enzyme.config'; +import * as React from 'react'; +import {expect, use} from 'chai'; +import {BASE_PROPS} from '../helpers'; +import Immutable from 'immutable'; +import {Provider} from 'react-redux'; +import WorkRow from '../../../../../src/client/unified-form/content-tab/work-row'; +import configureStore from 'redux-mock-store'; +import {jestSnapshotPlugin} from 'mocha-chai-jest-snapshot'; +import {mount} from 'enzyme'; +import {spy} from 'sinon'; + + +use(jestSnapshotPlugin()); + +const initialState = Immutable.fromJS({ + Works: { + n0: { + id: 0 + } + } +}); +const props = { + ...BASE_PROPS +}; + +describe('WorkRow', () => { + let store; + let wrapper; + let dispatchSpy; + beforeEach(() => { + store = configureStore()(initialState); + dispatchSpy = spy(); + store.dispatch = dispatchSpy; + wrapper = mount( + + + + ); + }); + + it('should render correctly', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('should trigger delete work action', () => { + const deleteButton = wrapper.find('button.btn-danger').at(0); + deleteButton.simulate('click'); + const payload = dispatchSpy.args[0][0]; + expect(dispatchSpy.callCount).equal(1); + expect(payload.type).equal('REMOVE_WORK'); + expect(payload.payload).equal('n0'); + }); + + it('should update the work value', () => { + const select = wrapper.find('Select').at(0); + select.props().onChange({name: 'Dummy'}, {}); + const payload = dispatchSpy.args[0][0]; + expect(dispatchSpy.callCount).equal(1); + expect(payload.type).equal('UPDATE_WORK'); + expect(payload.payload.id).equal('n0'); + expect(payload?.payload?.value?.name).equal('Dummy'); + }); + + it('should update checkbox on toggle', () => { + const checkbox = wrapper.find('input.form-check-input').at(0); + checkbox.simulate('change'); + const payload = dispatchSpy.args[0][0]; + expect(dispatchSpy.callCount).equal(1); + expect(payload.type).equal('TOGGLE_CHECK'); + expect(payload.payload).equal('n0'); + }); +}); diff --git a/test/src/client/unified-form/helpers.ts b/test/src/client/unified-form/helpers.ts new file mode 100644 index 0000000000..a845a30e9a --- /dev/null +++ b/test/src/client/unified-form/helpers.ts @@ -0,0 +1,4 @@ +export const BASE_PROPS = { + allIdentifierTypes: [], + entityType: 'Author' +}; From 066bfd1e6fbcd6c8a0273d2a8a55f62c7e75dbf8 Mon Sep 17 00:00:00 2001 From: tri10 Date: Thu, 21 Jul 2022 11:46:37 +0530 Subject: [PATCH 126/258] test: add test for common components --- .../search-entity-create-select.jsx.snap | 3 ++ .../__snapshots__/single-accordion.jsx.snap | 2 +- .../common/search-entity-create-select.jsx | 38 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 test/src/client/unified-form/common/__snapshots__/search-entity-create-select.jsx.snap create mode 100644 test/src/client/unified-form/common/search-entity-create-select.jsx diff --git a/test/src/client/unified-form/common/__snapshots__/search-entity-create-select.jsx.snap b/test/src/client/unified-form/common/__snapshots__/search-entity-create-select.jsx.snap new file mode 100644 index 0000000000..86872d8903 --- /dev/null +++ b/test/src/client/unified-form/common/__snapshots__/search-entity-create-select.jsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchEntityCreaateSelect renders correctly 1`] = `"
Select...
"`; diff --git a/test/src/client/unified-form/common/__snapshots__/single-accordion.jsx.snap b/test/src/client/unified-form/common/__snapshots__/single-accordion.jsx.snap index 9e7d3245e2..5d16fa8e06 100644 --- a/test/src/client/unified-form/common/__snapshots__/single-accordion.jsx.snap +++ b/test/src/client/unified-form/common/__snapshots__/single-accordion.jsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`SingleAccordion renders 1`] = `"

HELLOWORLD

BIGHEADING
"`; +exports[`SingleAccordion renders correctly 1`] = `"

HELLOWORLD

BIGHEADING
"`; diff --git a/test/src/client/unified-form/common/search-entity-create-select.jsx b/test/src/client/unified-form/common/search-entity-create-select.jsx new file mode 100644 index 0000000000..0ba3c1224e --- /dev/null +++ b/test/src/client/unified-form/common/search-entity-create-select.jsx @@ -0,0 +1,38 @@ +// eslint-disable-next-line import/no-unassigned-import +import '../../../../../enzyme.config'; +import * as React from 'react'; +import {expect, use} from 'chai'; +import {Provider} from 'react-redux'; +import SearchEntityCreate from '../../../../../src/client/unified-form/common/search-entity-create-select'; +import configureStore from 'redux-mock-store'; +import {jestSnapshotPlugin} from 'mocha-chai-jest-snapshot'; +import {mount} from 'enzyme'; +import {stub} from 'sinon'; + + +use(jestSnapshotPlugin()); + +const props = { + allIdentifierTypes: [], + nextId: 0, + type: 'Work' +}; + +const mockStore = configureStore(); +describe('SearchEntityCreaateSelect', () => { + let wrapper; + let store; + const formatCreateLabelSpy = stub(); + before(() => { + store = mockStore(); + wrapper = mount( + + + + ); + }); + it('renders correctly', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); +}); + From 240935d344153f3c354f4e5b61ae1c40140f7ec7 Mon Sep 17 00:00:00 2001 From: tri10 Date: Thu, 21 Jul 2022 12:15:15 +0530 Subject: [PATCH 127/258] test: add test for submit tab --- .../unified-form/submit-tab/summary.tsx | 2 +- .../submit-tab/__snapshots__/summary.jsx.snap | 3 ++ .../unified-form/submit-tab/summary.jsx | 53 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 test/src/client/unified-form/submit-tab/__snapshots__/summary.jsx.snap create mode 100644 test/src/client/unified-form/submit-tab/summary.jsx diff --git a/src/client/unified-form/submit-tab/summary.tsx b/src/client/unified-form/submit-tab/summary.tsx index f6e723841c..3ce3129aa5 100644 --- a/src/client/unified-form/submit-tab/summary.tsx +++ b/src/client/unified-form/submit-tab/summary.tsx @@ -46,7 +46,7 @@ function SummarySection({
{entityType}
{newEntities.map((entity, index) => ( - + {_.get(entity, 'text') + (index === newEntities.length - 1 ? '' : ', ')} ))} diff --git a/test/src/client/unified-form/submit-tab/__snapshots__/summary.jsx.snap b/test/src/client/unified-form/submit-tab/__snapshots__/summary.jsx.snap new file mode 100644 index 0000000000..0d41f689fc --- /dev/null +++ b/test/src/client/unified-form/submit-tab/__snapshots__/summary.jsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SummarySection should render correctly 1`] = `"

New Entities

  1. EditionGroups
    undefined
    1
  2. Editions
    undefined
    1
  3. Works
    Dummy
    1
"`; diff --git a/test/src/client/unified-form/submit-tab/summary.jsx b/test/src/client/unified-form/submit-tab/summary.jsx new file mode 100644 index 0000000000..ae5c26c4e8 --- /dev/null +++ b/test/src/client/unified-form/submit-tab/summary.jsx @@ -0,0 +1,53 @@ +// eslint-disable-next-line import/no-unassigned-import +import '../../../../../enzyme.config'; +import * as React from 'react'; +import {expect, use} from 'chai'; +import {BASE_PROPS} from '../helpers'; +import Immutable from 'immutable'; +import {Provider} from 'react-redux'; +import SummarySection from '../../../../../src/client/unified-form/submit-tab/summary'; +import configureStore from 'redux-mock-store'; +import {jestSnapshotPlugin} from 'mocha-chai-jest-snapshot'; +import {mount} from 'enzyme'; +import {spy} from 'sinon'; + + +use(jestSnapshotPlugin()); + +const initialState = Immutable.fromJS({ + Authors: {}, + EditionGroups: {}, + Publishers: {}, + Works: { + n0: { + __isNew__: true, + text: 'Dummy' + } + } +}); + +describe('SummarySection', () => { + let store; + let wrapper; + let dispatchSpy; + beforeEach(() => { + store = configureStore()(initialState); + dispatchSpy = spy(); + store.dispatch = dispatchSpy; + wrapper = mount( + + + + ); + }); + + it('should render correctly', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + it('should show proper summary', () => { + const summaries = wrapper.find('li'); + expect(summaries.length).to.equal(3); + const worksPreview = summaries.last().find('span.entities-preview'); + expect(worksPreview.text()).to.equal('Dummy'); + }); +}); From f9a6ae7c55a65dccc23bf4c3bffa1b4ab3032b67 Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 22 Jul 2022 20:49:37 +0530 Subject: [PATCH 128/258] feat: add validation to tabs --- .../entity-editor/common/validation-label.tsx | 5 +- src/client/unified-form/interface/type.ts | 5 ++ src/client/unified-form/unified-form.tsx | 48 +++++++++++------- src/client/unified-form/validators/base.ts | 18 +++++++ .../unified-form/validators/cover-tab.ts | 50 +++++++++++++++++++ .../unified-form/validators/detail-tab.ts | 36 +++++++++++++ 6 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 src/client/unified-form/validators/base.ts create mode 100644 src/client/unified-form/validators/cover-tab.ts create mode 100644 src/client/unified-form/validators/detail-tab.ts diff --git a/src/client/entity-editor/common/validation-label.tsx b/src/client/entity-editor/common/validation-label.tsx index 1a797e7112..3fe2e8fd64 100644 --- a/src/client/entity-editor/common/validation-label.tsx +++ b/src/client/entity-editor/common/validation-label.tsx @@ -66,6 +66,7 @@ type Props = { empty?: boolean, error?: boolean, errorMessage?: '', + isUf?: boolean, warn?: boolean, warnMessage?: '' }; @@ -88,6 +89,7 @@ function ValidationLabel({ children, empty, error, + isUf, errorMessage, warn, warnMessage @@ -96,7 +98,7 @@ function ValidationLabel({ {warnMessage} ; const errorElement = errorMessage && {errorMessage} ; - const iconElement = icon(empty, error, warn) && + const iconElement = !isUf && icon(empty, error, warn) && ; return ( @@ -114,6 +116,7 @@ ValidationLabel.defaultProps = { empty: false, error: false, errorMessage: '', + isUf: false, warn: false, warnMessage: '' }; diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index 15f6443f09..cc3e481bcd 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -35,6 +35,11 @@ export type UnifiedFormDispatchProps = { onSubmit: (event:React.FormEvent) =>unknown }; export type UnifiedFormStateProps = { + contentTabEmpty: boolean, + detailTabValid: boolean, + detailTabEmpty: boolean, + coverTabValid: boolean, + coverTabEmpty: boolean, formValid:boolean }; export type UnifiedFormOwnProps = { diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index ea464bbacb..e263fac2ad 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -1,5 +1,7 @@ import * as Boostrap from 'react-bootstrap'; -import {IdentifierType, UnifiedFormDispatchProps, UnifiedFormOwnProps, UnifiedFormProps, UnifiedFormStateProps} from './interface/type'; +import {IdentifierType, State, UnifiedFormDispatchProps, UnifiedFormOwnProps, UnifiedFormProps, UnifiedFormStateProps} from './interface/type'; +import {isCoverTabEmpty, validateCoverTab} from './validators/cover-tab'; +import {isDetailTabEmpty, validateDetailTab} from './validators/detail-tab'; import ContentTab from './content-tab/content-tab'; import CoverTab from './cover-tab/cover-tab'; import DetailTab from './detail-tab/detail-tab'; @@ -7,24 +9,24 @@ import NavButtons from './navbutton'; import React from 'react'; import SubmitSection from '../entity-editor/submission-section/submission-section'; import SummarySection from './submit-tab/summary'; +import ValidationLabel from '../entity-editor/common/validation-label'; import {connect} from 'react-redux'; +import {convertMapToObject} from '../helpers/utils'; import createFilterOptions from 'react-select-fast-filter-options'; import {filterIdentifierTypesByEntityType} from '../../common/helpers/utils'; import {freezeObjects} from './common/freezed-objects'; +import {getUfValidator} from './validators/base'; +import {omit} from 'lodash'; import {submit} from '../entity-editor/submission-section/actions'; const {Tabs, Tab} = Boostrap; -function getUfValidator(validator) { - return (state, identifierTypes, ...args) => { - if (state.get('ISBN') && !state.getIn(['ISBN', 'type']) && state.getIn(['ISBN', 'value'], '').length > 0) { - return false; - } - return validator(state, identifierTypes, ...args); - }; -} + + export function UnifiedForm(props:UnifiedFormProps) { - const {allIdentifierTypes, validator, onSubmit, formValid, languageOptions} = props; + const {allIdentifierTypes, validator, onSubmit, formValid, + languageOptions, contentTabEmpty, coverTabValid, coverTabEmpty, detailTabValid, detailTabEmpty} = props; + const rest = omit(props, ['contentTabEmpty', 'coverTabValid', 'coverTabEmpty', 'detailTabValid', 'formValid', 'detailTabEmpty']); React.useMemo(() => { // without this check, it would cause undefined behaviour if (!freezeObjects.filterOptions) { @@ -61,14 +63,14 @@ export function UnifiedForm(props:UnifiedFormProps) {

Add Book

- - + Cover}> + - - + Details}> + - - + Contents}> + @@ -85,11 +87,19 @@ export function UnifiedForm(props:UnifiedFormProps) { ); } -function mapStateToProps(state, {validator, allIdentifierTypes}:UnifiedFormOwnProps) { - const editionValidator = validator && getUfValidator(validator); +function mapStateToProps(state:State, {allIdentifierTypes}:UnifiedFormOwnProps) { + const jsonState = convertMapToObject(state); const editionIdentifierTypes = filterIdentifierTypesByEntityType(allIdentifierTypes, 'Edition'); + const coverTabEmpty = isCoverTabEmpty(jsonState); + const coverTabValid = !coverTabEmpty && validateCoverTab(jsonState, editionIdentifierTypes); + const detailTabValid = validateDetailTab(jsonState); return { - formValid: editionValidator && editionValidator(state, editionIdentifierTypes, false, true) + contentTabEmpty: state.get('Works').size === 0, + coverTabEmpty, + coverTabValid, + detailTabEmpty: isDetailTabEmpty(jsonState), + detailTabValid, + formValid: coverTabValid && detailTabValid }; } function mapDispatchToProps(dispatch, {submissionUrl}) { diff --git a/src/client/unified-form/validators/base.ts b/src/client/unified-form/validators/base.ts new file mode 100644 index 0000000000..d20ee107b4 --- /dev/null +++ b/src/client/unified-form/validators/base.ts @@ -0,0 +1,18 @@ +import {State} from '../interface/type'; +import {convertMapToObject} from '../../helpers/utils'; +import {get} from 'lodash'; +import {validateISBN} from './cover-tab'; + +/** + * Validate the unified form state + * + * @param {Function} validator - validator function + * @returns {(state,...args) => boolean} - uf validator function + */ + +export function getUfValidator(validator:any): {(state: State, ...args: any[]): boolean} { + return (state, identifierTypes, ...args) => { + const jsonState = convertMapToObject(state); + return validateISBN(get(jsonState, 'ISBN')) && validator(jsonState, identifierTypes, ...args); + }; +} diff --git a/src/client/unified-form/validators/cover-tab.ts b/src/client/unified-form/validators/cover-tab.ts new file mode 100644 index 0000000000..046b512f8a --- /dev/null +++ b/src/client/unified-form/validators/cover-tab.ts @@ -0,0 +1,50 @@ +import {get, size} from 'lodash'; +import {validateAuthorCreditSection, validateIdentifiers, validateNameSection} from '../../entity-editor/validators/common'; + +/** + * Validates the ISBN Field. + * + * @param {object} isbn - ISBN state object containing `type` and `value` + * @returns {boolean} - true if valid, false if invalid + */ +export function validateISBN(isbn) { + return !( + Boolean(isbn) && + !get(isbn, 'type', null) && + get(isbn, 'value', '').length > 0 + ); +} + +/** + * Validates the Cover Tab state. + * + * @param {object} data - the form state object + * @param {Array} identifierTypes - the list of identifier types + * @returns {boolean} - true if form state valid, false if invalid + */ +export function validateCoverTab(data:any, identifierTypes:any[]) { + return validateNameSection(get(data, 'nameSection')) && + validateIdentifiers(get(data, 'identifierEditor', {}), identifierTypes) && + validateAuthorCreditSection(get(data, 'authorCreditEditor')) && + validateISBN(get(data, 'ISBN')); +} + +/** + * Check whether Cover Tab is modified or not. + * + * @param {object} data - the form state object + * @returns {boolean} - true if cover tab state empty + */ +export function isCoverTabEmpty(data:any) { + const nameSection = get(data, 'nameSection', {}); + const authorCreditEditor = get(data, 'authorCreditEditor', {}); + const ISBN = get(data, 'ISBN', {}); + const identifierEditor = get(data, 'identifierEditor', {}); + return nameSection.name?.length === 0 && + nameSection.sortName?.length === 0 && + !nameSection.language && + nameSection.disambiguation?.length === 0 && + !authorCreditEditor.n0?.author && + size(identifierEditor) === 0 && + !ISBN.type; +} diff --git a/src/client/unified-form/validators/detail-tab.ts b/src/client/unified-form/validators/detail-tab.ts new file mode 100644 index 0000000000..e79c7ef01d --- /dev/null +++ b/src/client/unified-form/validators/detail-tab.ts @@ -0,0 +1,36 @@ +import {get} from 'lodash'; +import {validateEditionSection} from '../../entity-editor/validators/edition'; + + +const initialEditionSection = JSON.stringify({ + authorCreditEditorVisible: false, + format: null, + languages: [], + matchingNameEditionGroups: [], + physicalEnable: true, + publisher: {}, + releaseDate: '', + status: null +}); + +/** + * Validates the Detail Tab state. + * + * @param {object} data - the form state object + * @returns {boolean} - true if detail tab state is valid + */ +export function validateDetailTab(data: any): boolean { + return validateEditionSection(get(data, 'editionSection')); +} + +/** + * Check whether Detail Tab is modified or not. + * + * @param {object} data - the form state object + * @returns {boolean} - true if detail tab state is empty + */ +export function isDetailTabEmpty(data:any): boolean { + const editionSection = get(data, 'editionSection', {}); + const annotationContent = get(data, ['annotationSection', 'content'], ''); + return JSON.stringify(editionSection) === initialEditionSection && annotationContent.length === 0; +} From 0c114d3a43809dbe0965ea38ebf1fd91f631d330 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sat, 23 Jul 2022 20:32:24 +0530 Subject: [PATCH 129/258] feat: add identifier count --- src/client/entity-editor/button-bar/identifier-button.js | 2 +- src/client/unified-form/validators/cover-tab.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/entity-editor/button-bar/identifier-button.js b/src/client/entity-editor/button-bar/identifier-button.js index 5883ef2438..c0ac7669b3 100644 --- a/src/client/entity-editor/button-bar/identifier-button.js +++ b/src/client/entity-editor/button-bar/identifier-button.js @@ -51,7 +51,7 @@ function IdentifierButton({ text = `Edit ${numIdentifiers} identifiers (eg. ISBN, Wikidata ID)…`; } if (isUf) { - text = 'Add identifiers'; + text = `Add identifiers ${numIdentifiers}`; } const iconElement = identifiersInvalid && ; diff --git a/src/client/unified-form/validators/cover-tab.ts b/src/client/unified-form/validators/cover-tab.ts index 046b512f8a..f5e631cda6 100644 --- a/src/client/unified-form/validators/cover-tab.ts +++ b/src/client/unified-form/validators/cover-tab.ts @@ -25,7 +25,7 @@ export function validateISBN(isbn) { export function validateCoverTab(data:any, identifierTypes:any[]) { return validateNameSection(get(data, 'nameSection')) && validateIdentifiers(get(data, 'identifierEditor', {}), identifierTypes) && - validateAuthorCreditSection(get(data, 'authorCreditEditor')) && + validateAuthorCreditSection(get(data, 'authorCreditEditor'), true) && validateISBN(get(data, 'ISBN')); } From 355d01e7e58ea11c4a10153f19c83ee7317e998a Mon Sep 17 00:00:00 2001 From: tri10 Date: Sun, 24 Jul 2022 07:26:10 +0530 Subject: [PATCH 130/258] test: add test for tab validators --- .../unified-form/validators/cover-tab.ts | 2 + .../unified-form/validators/detail-tab.ts | 7 ++-- test/src/client/unified-form/helpers.ts | 18 ++++++++ .../unified-form/validators/test-cover-tab.js | 42 +++++++++++++++++++ .../validators/test-detail-tab.js | 24 +++++++++++ 5 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 test/src/client/unified-form/validators/test-cover-tab.js create mode 100644 test/src/client/unified-form/validators/test-detail-tab.js diff --git a/src/client/unified-form/validators/cover-tab.ts b/src/client/unified-form/validators/cover-tab.ts index f5e631cda6..006cdc3516 100644 --- a/src/client/unified-form/validators/cover-tab.ts +++ b/src/client/unified-form/validators/cover-tab.ts @@ -8,6 +8,7 @@ import {validateAuthorCreditSection, validateIdentifiers, validateNameSection} f * @returns {boolean} - true if valid, false if invalid */ export function validateISBN(isbn) { + // since type will already be defined for valid ISBNs return !( Boolean(isbn) && !get(isbn, 'type', null) && @@ -44,6 +45,7 @@ export function isCoverTabEmpty(data:any) { nameSection.sortName?.length === 0 && !nameSection.language && nameSection.disambiguation?.length === 0 && + size(authorCreditEditor) === 1 && !authorCreditEditor.n0?.author && size(identifierEditor) === 0 && !ISBN.type; diff --git a/src/client/unified-form/validators/detail-tab.ts b/src/client/unified-form/validators/detail-tab.ts index e79c7ef01d..cdbada8566 100644 --- a/src/client/unified-form/validators/detail-tab.ts +++ b/src/client/unified-form/validators/detail-tab.ts @@ -2,7 +2,7 @@ import {get} from 'lodash'; import {validateEditionSection} from '../../entity-editor/validators/edition'; -const initialEditionSection = JSON.stringify({ +export const initialEditionSection = { authorCreditEditorVisible: false, format: null, languages: [], @@ -11,7 +11,8 @@ const initialEditionSection = JSON.stringify({ publisher: {}, releaseDate: '', status: null -}); +}; +const stringifiedState = JSON.stringify(initialEditionSection); /** * Validates the Detail Tab state. @@ -32,5 +33,5 @@ export function validateDetailTab(data: any): boolean { export function isDetailTabEmpty(data:any): boolean { const editionSection = get(data, 'editionSection', {}); const annotationContent = get(data, ['annotationSection', 'content'], ''); - return JSON.stringify(editionSection) === initialEditionSection && annotationContent.length === 0; + return JSON.stringify(editionSection) === stringifiedState && annotationContent.length === 0; } diff --git a/test/src/client/unified-form/helpers.ts b/test/src/client/unified-form/helpers.ts index a845a30e9a..9d1f1804d3 100644 --- a/test/src/client/unified-form/helpers.ts +++ b/test/src/client/unified-form/helpers.ts @@ -2,3 +2,21 @@ export const BASE_PROPS = { allIdentifierTypes: [], entityType: 'Author' }; +export const emptyCoverTabState = { + ISBN: { + type: null, + value: '' + }, + authorCreditEditor: { + n0: { + author: null + } + }, + identifierEditor: {}, + nameSection: { + disambiguation: '', + language: null, + name: '', + sortName: '' + } +}; diff --git a/test/src/client/unified-form/validators/test-cover-tab.js b/test/src/client/unified-form/validators/test-cover-tab.js new file mode 100644 index 0000000000..309b1636df --- /dev/null +++ b/test/src/client/unified-form/validators/test-cover-tab.js @@ -0,0 +1,42 @@ +import {isCoverTabEmpty, validateISBN} from '../../../../../src/client/unified-form/validators/cover-tab'; +import {emptyCoverTabState} from '../helpers'; +import {expect} from 'chai'; + + +function describeISBNState() { + it('should be false for invalid isbn type', () => { + const isbn = { + type: null, + value: 'someisbn' + }; + const isValid = validateISBN(isbn); + expect(isValid).to.be.not.true; + }); + it('should be true for valid isbn type', () => { + const isbn = { + type: 1, + value: 'someisbn' + }; + const isValid = validateISBN(isbn); + expect(isValid).to.be.true; + }); +} +function describeCoverTabValidators() { + it('should be false for modified cover tab state', () => { + const coverTabState = {...emptyCoverTabState, ISBN: {type: 1, value: 'someisbn'}}; + const isEmpty = isCoverTabEmpty(coverTabState); + expect(isEmpty).to.be.not.true; + }); + it('should be true for unmodified cover tab state', () => { + const coverTabState = {...emptyCoverTabState}; + const isEmpty = isCoverTabEmpty(coverTabState); + expect(isEmpty).to.be.true; + }); +} + +function tests() { + describe('validateISBNState', describeISBNState); + describe('validateCoverTabState', describeCoverTabValidators); +} + +describe('CoverTabValidators', tests); diff --git a/test/src/client/unified-form/validators/test-detail-tab.js b/test/src/client/unified-form/validators/test-detail-tab.js new file mode 100644 index 0000000000..819bd584e8 --- /dev/null +++ b/test/src/client/unified-form/validators/test-detail-tab.js @@ -0,0 +1,24 @@ +import {initialEditionSection, isDetailTabEmpty} from '../../../../../src/client/unified-form/validators/detail-tab'; +import {expect} from 'chai'; + + +const emptyDetailTabState = { + annotationSection: { + content: '' + }, + editionSection: initialEditionSection +}; + +describe('DetailTabValidators', () => { + it('should be false for modified detail-tab state', () => { + const detailTabState = {...emptyDetailTabState, annotationSection: { + content: 'some annotation' + }}; + const isEmpty = isDetailTabEmpty(detailTabState); + expect(isEmpty).to.be.not.true; + }); + it('should be true for unmodified detail-tab state', () => { + const isEmpty = isDetailTabEmpty(emptyDetailTabState); + expect(isEmpty).to.be.true; + }); +}); From ef6dc47afa14aaeed20ce92484a2691e19577167 Mon Sep 17 00:00:00 2001 From: tri10 Date: Sun, 24 Jul 2022 10:07:32 +0530 Subject: [PATCH 131/258] feat(uf): show warning on duplicate new entities. --- .../name-section/name-section.js | 30 +++++++++++++++++-- .../unified-form/validators/detail-tab.ts | 2 +- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/client/entity-editor/name-section/name-section.js b/src/client/entity-editor/name-section/name-section.js index 15006d6d71..37310d4236 100644 --- a/src/client/entity-editor/name-section/name-section.js +++ b/src/client/entity-editor/name-section/name-section.js @@ -321,7 +321,7 @@ NameSection.defaultProps = { }; -function mapStateToProps(rootState, {isUf, setDefault}) { +function mapStateToProps(rootState, {isUf, setDefault, entityType}) { const state = rootState.get('nameSection'); const editionSectionState = rootState.get('editionSection'); const searchForExistingEditionGroup = Boolean(editionSectionState) && @@ -329,6 +329,30 @@ function mapStateToProps(rootState, {isUf, setDefault}) { !editionSectionState.get('editionGroup') || editionSectionState.get('editionGroupRequired') ); + let exactMatches = state.get('exactMatches'); + const nameValue = state.get('name'); + // search for duplicates with same name in new entities + if (isUf && entityType && nameValue.length > 0 && _.size(exactMatches) === 0) { + const stateSelector = `${_.upperFirst(_.snakeCase(entityType))}s`; + const entities = rootState.get(stateSelector, {}); + const entitiesJSON = convertMapToObject(entities); + const matchEntities = []; + _.map(entitiesJSON, (value) => { + if (value.__isNew__ && nameValue === _.get(value, ['nameSection', 'name'], '')) { + const disambiguation = _.get(value, ['nameSection', 'disambiguation'], ''); + matchEntities.push({ + bbid: '#', + defaultAlias: { + name: nameValue + }, + disambiguation: disambiguation.length > 0 && { + comment: disambiguation + } + }); + } + }); + exactMatches = matchEntities; + } // to prevent double double state updates on action caused by modal if (isUf && setDefault) { return { @@ -342,9 +366,9 @@ function mapStateToProps(rootState, {isUf, setDefault}) { } return { disambiguationDefaultValue: state.get('disambiguation'), - exactMatches: state.get('exactMatches'), + exactMatches, languageValue: state.get('language'), - nameValue: state.get('name'), + nameValue, searchForExistingEditionGroup, searchResults: state.get('searchResults'), sortNameValue: state.get('sortName') diff --git a/src/client/unified-form/validators/detail-tab.ts b/src/client/unified-form/validators/detail-tab.ts index cdbada8566..e46f76c2ac 100644 --- a/src/client/unified-form/validators/detail-tab.ts +++ b/src/client/unified-form/validators/detail-tab.ts @@ -21,7 +21,7 @@ const stringifiedState = JSON.stringify(initialEditionSection); * @returns {boolean} - true if detail tab state is valid */ export function validateDetailTab(data: any): boolean { - return validateEditionSection(get(data, 'editionSection')); + return validateEditionSection(get(data, 'editionSection'), true); } /** From c3e95484c158aafff0bef86f5da5334bb99dbf6f Mon Sep 17 00:00:00 2001 From: tri10 Date: Tue, 26 Jul 2022 12:19:03 +0530 Subject: [PATCH 132/258] fix: author-credit section in edition-group --- .../author-credit-editor/author-credit-section.tsx | 9 +++++---- .../edition-group-section/edition-group-section.tsx | 7 +++---- .../entity-editor/edition-group-section/reducer.ts | 6 ++++++ .../entity-editor/edition-section/edition-section.tsx | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx index 983637a0db..f89663c4c6 100644 --- a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx +++ b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx @@ -28,7 +28,7 @@ import { import {Button, Col, Form, InputGroup, OverlayTrigger, Row, Tooltip} from 'react-bootstrap'; import {SingleValueProps, components} from 'react-select'; -import {map as _map, values as _values} from 'lodash'; +import {map as _map, values as _values, camelCase} from 'lodash'; import {faPencilAlt, faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; import AuthorCreditEditor from './author-credit-editor'; @@ -143,15 +143,16 @@ AuthorCreditSection.propTypes = { showEditor: PropTypes.bool.isRequired }; -function mapStateToProps(rootState): StateProps { +function mapStateToProps(rootState, {type}): StateProps { const firstRowKey = rootState.get('authorCreditEditor').keySeq().first(); const authorCreditRow = rootState.getIn(['authorCreditEditor', firstRowKey]); const isEditable = !(rootState.get('authorCreditEditor').size > 1) && - authorCreditRow.get('name') === authorCreditRow.getIn(['author', 'text'], ''); + authorCreditRow.get('name') === authorCreditRow.getIn(['author', 'text'], ''); + const entitySection = `${camelCase(type)}Section`; return { authorCreditEditor: convertMapToObject(rootState.get('authorCreditEditor')), isEditable, - showEditor: rootState.getIn(['editionSection', 'authorCreditEditorVisible']) + showEditor: rootState.getIn([entitySection, 'authorCreditEditorVisible']) }; } diff --git a/src/client/entity-editor/edition-group-section/edition-group-section.tsx b/src/client/entity-editor/edition-group-section/edition-group-section.tsx index 25bda10acf..ceecb86173 100644 --- a/src/client/entity-editor/edition-group-section/edition-group-section.tsx +++ b/src/client/entity-editor/edition-group-section/edition-group-section.tsx @@ -17,13 +17,12 @@ */ import * as React from 'react'; -import AuthorCreditSection from '../author-credit-editor/author-credit-section'; import {Action, updateType} from './actions'; import {Col, Form, OverlayTrigger, Row, Tooltip} from 'react-bootstrap'; +import AuthorCreditSection from '../author-credit-editor/author-credit-section'; import type {Dispatch} from 'redux'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import type {Map} from 'immutable'; import Select from 'react-select'; import {connect} from 'react-redux'; import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; @@ -35,7 +34,7 @@ type EditionGroupType = { }; type StateProps = { - typeValue: Map + typeValue: number }; type DispatchProps = { @@ -80,7 +79,7 @@ function EditionGroupSection({

What else do you know about the Edition Group?

- +

All fields optional — leave something blank if you don’t know it diff --git a/src/client/entity-editor/edition-group-section/reducer.ts b/src/client/entity-editor/edition-group-section/reducer.ts index ef6ecf4ae2..55e6604b0c 100644 --- a/src/client/entity-editor/edition-group-section/reducer.ts +++ b/src/client/entity-editor/edition-group-section/reducer.ts @@ -21,6 +21,7 @@ import * as Immutable from 'immutable'; import { Action, UPDATE_TYPE } from './actions'; +import {HIDE_AUTHOR_CREDIT_EDITOR, SHOW_AUTHOR_CREDIT_EDITOR} from '../author-credit-editor/actions'; type State = Immutable.Map; @@ -35,6 +36,11 @@ function reducer( switch (type) { case UPDATE_TYPE: return state.set('type', payload); + case SHOW_AUTHOR_CREDIT_EDITOR: + return state.set('authorCreditEditorVisible', true); + case HIDE_AUTHOR_CREDIT_EDITOR: + return state.set('authorCreditEditorVisible', false); + // no default } return state; diff --git a/src/client/entity-editor/edition-section/edition-section.tsx b/src/client/entity-editor/edition-section/edition-section.tsx index cc443f1206..bdbdc2efb4 100644 --- a/src/client/entity-editor/edition-section/edition-section.tsx +++ b/src/client/entity-editor/edition-section/edition-section.tsx @@ -265,7 +265,7 @@ function EditionSection({

What else do you know about the Edition?

- +

Edition Group is required — this cannot be blank. You can search for and choose an existing Edition Group, or choose to automatically create one instead. From f892bca0b2961c5283d4febcf12dcc31130336af Mon Sep 17 00:00:00 2001 From: tri10 Date: Tue, 26 Jul 2022 17:35:14 +0530 Subject: [PATCH 133/258] feat: Show dialogue box in summary section --- src/client/stylesheets/style.scss | 8 ++ src/client/unified-form/interface/type.ts | 19 ++++ .../submit-tab/single-entity-modal.tsx | 100 ++++++++++++++++++ .../unified-form/submit-tab/single-entity.tsx | 26 +++++ .../unified-form/submit-tab/summary.tsx | 33 +++--- src/client/unified-form/unified-form.tsx | 2 +- 6 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 src/client/unified-form/submit-tab/single-entity-modal.tsx create mode 100644 src/client/unified-form/submit-tab/single-entity.tsx diff --git a/src/client/stylesheets/style.scss b/src/client/stylesheets/style.scss index dd08886f2e..4a46d40d80 100644 --- a/src/client/stylesheets/style.scss +++ b/src/client/stylesheets/style.scss @@ -815,4 +815,12 @@ div[class~=collapsing]+div[class=card-header] .accordion-arrow { .uf-modal-body .accordion > .card{ margin-bottom: 10px; +} +.entities-preview{ + cursor: pointer; + color: $uf-primary; + transition: all 0.2s linear; +} +.entities-preview:hover{ + color: #754E37; } \ No newline at end of file diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index cc3e481bcd..93530ab05b 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -11,6 +11,12 @@ export type Action = { } }; +export type Entity = { + __isNew__: boolean, + text:string, + type:string, + id:string +}; export type State = Immutable.Map; export type IdentifierType = { @@ -151,3 +157,16 @@ export type CreateEntityModalOwnProps = { show:boolean }; export type CreateEntityModalProps = CreateEntityModalOwnProps; + +export type SummarySectionStateProps = { + Authors: Array; + EditionGroups: Array; + Editions: Array; + Publishers: Array; + Works: Array; +}; +export type SummarySectionOwnProps = { + languageOptions: any[] +}; +export type SummarySectionProps = SummarySectionOwnProps & SummarySectionStateProps; + diff --git a/src/client/unified-form/submit-tab/single-entity-modal.tsx b/src/client/unified-form/submit-tab/single-entity-modal.tsx new file mode 100644 index 0000000000..5b2b6f36a4 --- /dev/null +++ b/src/client/unified-form/submit-tab/single-entity-modal.tsx @@ -0,0 +1,100 @@ +import {Modal} from 'react-bootstrap'; +import React from 'react'; +import _ from 'lodash'; +import {dateObjectToISOString} from '../../helpers/utils'; + + +type Props = { + entity:any, + show:boolean, + handleClose:()=>void, + languageOptions:any[] +}; +/* eslint-disable sort-keys */ +const BASE_ENTITY = { + Name: 'nameSection.name', + Language: 'nameSection.language', + 'Sort-Name': 'nameSection.sortName', + Disambiguation: 'nameSection.disambiguation', + Annotation: 'annotationSection.content', + 'Edit-Note': 'submissionSection.note' + +}; +const ENTITY_FIELDS = { + edition: { + ...BASE_ENTITY, + format: 'editionSection.format', + 'Release-date': 'editionSection.releaseDate', + status: 'editionSection.status', + 'Edition-languages': 'editionSection.languages', + pages: 'editionSection.pages', + width: 'editionSection.width', + height: 'editionSection.height', + weight: 'editionSection.weight', + depth: 'editionSection.depth' + }, + editionGroup: { + ...BASE_ENTITY, + Type: 'editionGroupSection.type' + }, + author: { + ...BASE_ENTITY, + Gender: 'authorSection.gender', + Type: 'authorSection.type', + 'Begin-date': 'authorSection.beginDate', + 'Begin-area': 'authorSection.beginArea.text', + 'Dead?': 'authorSection.ended', + 'End-date': 'authorSection.endDate', + 'End-area': 'authorSection.endArea.text' + }, + publisher: { + ...BASE_ENTITY, + Type: 'publisherSection.type', + 'Begin-date': 'publisherSection.beginDate', + 'Dissolved?': 'publisherSection.ended', + 'End-date': 'publisherSection.endDate' + + }, + work: { + ...BASE_ENTITY, + type: 'workSection.type', + 'Work-languages': 'workSection.languages' + } +}; +export default function SingleEntityModal({entity, show, handleClose, languageOptions}:Props) { + const id2LanguageMap = React.useMemo(() => Object.fromEntries(_.map(languageOptions, (option) => [option.id, option.name])), []); + function renderField(path, key) { + let fieldVal = _.get(entity, path, ''); + if (!fieldVal || (fieldVal.length === 0)) { + return; + } + if (key === 'Language') { + fieldVal = id2LanguageMap[fieldVal]; + } + if (key.includes('languages')) { + fieldVal = _.reduce(fieldVal, (acc, next) => `${acc}${acc.length !== 0 ? ',' : ''} ${next.label}`, ''); + } + if (key.includes('date')) { + if (typeof fieldVal !== 'string') { + if (!fieldVal.day && !fieldVal.month && !fieldVal.year) { + return; + } + fieldVal = dateObjectToISOString(fieldVal); + } + } + // eslint-disable-next-line consistent-return + return {key}: {typeof fieldVal === 'string' ? fieldVal : JSON.stringify(fieldVal)}; + } + const entityFields = ENTITY_FIELDS[_.camelCase(entity.type)] ?? {}; + return ( + + + {entity.type} + + + {_.map(entityFields, renderField)} + + + + ); +} diff --git a/src/client/unified-form/submit-tab/single-entity.tsx b/src/client/unified-form/submit-tab/single-entity.tsx new file mode 100644 index 0000000000..e34354219d --- /dev/null +++ b/src/client/unified-form/submit-tab/single-entity.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import SingleEntityModal from './single-entity-modal'; +import {get} from 'lodash'; + + +type SingleEntityProps = { + entity: any; + isLast: boolean; + languageOptions:any[] +}; +export default function SingleEntity({entity, isLast, languageOptions}:SingleEntityProps) { + const [showModal, setShowModal] = React.useState(false); + const handleClose = React.useCallback(() => { + setShowModal(false); + }, []); + const handleShow = React.useCallback(() => { + setShowModal(true); + }, []); + return ( + <> + + + {get(entity, 'text') + (isLast ? '' : ', ')} + + ); +} diff --git a/src/client/unified-form/submit-tab/summary.tsx b/src/client/unified-form/submit-tab/summary.tsx index 3ce3129aa5..5149bedcdf 100644 --- a/src/client/unified-form/submit-tab/summary.tsx +++ b/src/client/unified-form/submit-tab/summary.tsx @@ -1,28 +1,19 @@ import {Badge, ListGroup} from 'react-bootstrap'; +import {Entity, SummarySectionProps, SummarySectionStateProps} from '../interface/type'; import Immutable from 'immutable'; import React from 'react'; +import SingleEntity from './single-entity'; import _ from 'lodash'; import {connect} from 'react-redux'; import {convertMapToObject} from '../../helpers/utils'; -type Entity = { - __isNew__: boolean, - text:string, - id:string -}; -type SummarySectionProps = { - Authors: Array; - EditionGroups: Array; - Editions: Array; - Publishers: Array; - Works: Array; -}; function SummarySection({ Publishers, Works, Authors, EditionGroups, + languageOptions, Editions }: SummarySectionProps) { const createdEntities = { @@ -45,11 +36,10 @@ function SummarySection({ >

{entityType}
- {newEntities.map((entity, index) => ( - - {_.get(entity, 'text') + (index === newEntities.length - 1 ? '' : ', ')} - - ))} + {newEntities.map((entity, index) => ())}
{newEntities.length} @@ -74,10 +64,13 @@ function mapStateToProps(state) { const Editions:Entity[] = [{ __isNew__: true, id: 'e0', - text: state.getIn(['nameSection', 'name']) + text: state.getIn(['nameSection', 'name']), + type: 'Edition', + ...convertMapToObject(state) }]; if (EditionGroups.length === 0) { - EditionGroups = Editions; + EditionGroups = [{...Editions[0]}]; + EditionGroups[0].type = 'EditionGroup'; EditionGroups[0].id = 'eg0'; } return { @@ -88,4 +81,4 @@ function mapStateToProps(state) { Works: getEntitiesArray(state.get('Works')) }; } -export default connect(mapStateToProps)(SummarySection); +export default connect(mapStateToProps)(SummarySection); diff --git a/src/client/unified-form/unified-form.tsx b/src/client/unified-form/unified-form.tsx index e263fac2ad..20848f288b 100644 --- a/src/client/unified-form/unified-form.tsx +++ b/src/client/unified-form/unified-form.tsx @@ -73,7 +73,7 @@ export function UnifiedForm(props:UnifiedFormProps) {
- + From e9a5bc5fc6963dc7df24326a5f52a1ca1bc90d86 Mon Sep 17 00:00:00 2001 From: tri10 Date: Tue, 26 Jul 2022 23:11:29 +0530 Subject: [PATCH 134/258] feat: add ability to copy work. --- src/client/entity-editor/helpers.ts | 5 +-- src/client/unified-form/content-tab/action.ts | 14 ++++++++ .../unified-form/content-tab/content-tab.tsx | 34 ++++++++++++++++--- .../unified-form/content-tab/work-row.tsx | 13 +++++-- src/client/unified-form/helpers.ts | 11 ++++-- src/client/unified-form/interface/type.ts | 2 ++ 6 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/client/entity-editor/helpers.ts b/src/client/entity-editor/helpers.ts index 205e0bb736..882fe30146 100644 --- a/src/client/entity-editor/helpers.ts +++ b/src/client/entity-editor/helpers.ts @@ -36,6 +36,7 @@ import authorCreditEditorReducer from './author-credit-editor/reducer'; import authorCreditMergeReducer from './author-credit-editor/merge-reducer'; import authorSectionReducer from './author-section/reducer'; import buttonBarReducer from './button-bar/reducer'; +import {camelCase} from 'lodash'; import {combineReducers} from 'redux-immutable'; import editionGroupSectionReducer from './edition-group-section/reducer'; import editionSectionReducer from './edition-section/reducer'; @@ -79,7 +80,7 @@ export function getEntitySection(entityType: string) { work: WorkSection }; - return SECTION_MAP[entityType]; + return SECTION_MAP[camelCase(entityType)]; } export function getEntitySectionMerge(entityType: string) { @@ -118,7 +119,7 @@ export function getValidator(entityType: string) { work: validateWorkForm }; - return VALIDATOR_MAP[entityType]; + return VALIDATOR_MAP[camelCase(entityType)]; } function getEntitySectionReducerName(entityType: string): string { diff --git a/src/client/unified-form/content-tab/action.ts b/src/client/unified-form/content-tab/action.ts index 7be199213c..40e5c8ddda 100644 --- a/src/client/unified-form/content-tab/action.ts +++ b/src/client/unified-form/content-tab/action.ts @@ -6,6 +6,7 @@ export const UPDATE_WORKS = 'UPDATE_WORKS'; export const REMOVE_WORK = 'REMOVE_WORK'; export const UPDATE_WORK = 'UPDATE_WORK'; export const TOGGLE_CHECK = 'TOGGLE_CHECK'; +export const COPY_WORK = 'COPY_WORK'; let nextWorkId = 0; @@ -61,3 +62,16 @@ export function toggleCheck(id:string):Action { type: TOGGLE_CHECK }; } + +/** + * Produces an action indicating that a Work need to be copied. + * + * @param {string} id - id of the work to be copied + * @returns {Action} The resulting COPY_WORK action. + */ +export function copyWork(id:string):Action { + return { + payload: id, + type: COPY_WORK + }; +} diff --git a/src/client/unified-form/content-tab/content-tab.tsx b/src/client/unified-form/content-tab/content-tab.tsx index 0e71af0826..29c5712386 100644 --- a/src/client/unified-form/content-tab/content-tab.tsx +++ b/src/client/unified-form/content-tab/content-tab.tsx @@ -1,18 +1,36 @@ import * as Bootstrap from 'react-bootstrap/'; import {ContentTabDispatchProps, ContentTabProps, ContentTabStateProps, State} from '../interface/type'; +import {addWork, copyWork} from './action'; +import {closeEntityModal, dumpEdition, loadEdition, openEntityModal} from '../action'; +import CreateEntityModal from '../common/create-entity-modal'; import React from 'react'; import SearchEntityCreate from '../common/search-entity-create-select'; import WorkRow from './work-row'; -import {addWork} from './action'; import {connect} from 'react-redux'; import {convertMapToObject} from '../../helpers/utils'; import {map} from 'lodash'; const {Row, Col, FormCheck} = Bootstrap; -export function ContentTab({value, onChange, ...rest}:ContentTabProps) { +export function ContentTab({value, onChange, onModalClose, onModalOpen, ...rest}:ContentTabProps) { const [isChecked, setIsChecked] = React.useState(false); const toggleCheck = React.useCallback(() => setIsChecked(!isChecked), [isChecked]); + const [showModal, setShowModal] = React.useState(false); + const openModalHandler = React.useCallback((id) => { + setShowModal(true); + onModalOpen(id); + }, []); + const closeModalHandler = React.useCallback(() => { + setShowModal(false); + onModalClose(); + }, []); + const submitModalHandler = React.useCallback((ev: React.FormEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + setShowModal(false); + onChange(null); + onModalClose(); + }, []); const onChangeHandler = React.useCallback((work:any) => { work.checked = isChecked; onChange(work); @@ -20,7 +38,8 @@ export function ContentTab({value, onChange, ...rest}:ContentTabProps) { return ( <>

Works

- {map(value, (work, rowId) => )} + {map(value, (work, rowId) => )} + dispatch(addWork(value)) + onChange: (value:any) => dispatch(addWork(value)), + onModalClose: () => dispatch(loadEdition()) && dispatch(closeEntityModal()), + onModalOpen: (id) => { + dispatch(dumpEdition(type)); + dispatch(copyWork(id)); + dispatch(openEntityModal()); + } }; } diff --git a/src/client/unified-form/content-tab/work-row.tsx b/src/client/unified-form/content-tab/work-row.tsx index 4a31edf5f8..41428c7a57 100644 --- a/src/client/unified-form/content-tab/work-row.tsx +++ b/src/client/unified-form/content-tab/work-row.tsx @@ -6,7 +6,7 @@ import {connect} from 'react-redux'; import {convertMapToObject} from '../../helpers/utils'; -const {Row, Col, Button, FormCheck} = Bootstrap; +const {Row, Col, Button, FormCheck, ButtonGroup} = Bootstrap; type WorkRowStateProps = { work: any; }; @@ -17,13 +17,15 @@ type WorkRowDispatchProps = { }; type WorkRowOwnProps = { + onCopyHandler:(arg)=>unknown, rowId: string; }; type WorkRowProps = WorkRowStateProps & WorkRowDispatchProps & WorkRowOwnProps; -function WorkRow({onChange, work, onRemove, onToggle, ...rest}:WorkRowProps) { +function WorkRow({onChange, work, onRemove, onToggle, onCopyHandler, ...rest}:WorkRowProps) { const isChecked = work?.checked; + const handleCopy = React.useMemo(() => onCopyHandler(work.id), [onCopyHandler, work]); const onChangeHandler = React.useCallback((value:any) => { value.checked = isChecked; onChange(value); @@ -41,7 +43,12 @@ function WorkRow({onChange, work, onRemove, onToggle, ...rest}:WorkRowProps) { /> - + + { + work.__isNew__ && + } + + istate.delete(key), intermediateState); + break; + } case LOAD_EDITION: { // load old edition state from `Editions` diff --git a/src/client/unified-form/interface/type.ts b/src/client/unified-form/interface/type.ts index 93530ab05b..35a85445d8 100644 --- a/src/client/unified-form/interface/type.ts +++ b/src/client/unified-form/interface/type.ts @@ -91,6 +91,8 @@ export type ContentTabStateProps = { }; export type ContentTabDispatchProps = { onChange:(value:EntitySelect)=>unknown, + onModalOpen:(arg)=>unknown, + onModalClose:()=>unknown, }; export type ContentTabProps = ContentTabStateProps & ContentTabDispatchProps; From 6ba7956fe4289044c205fc3d7939e458aa8a2ec6 Mon Sep 17 00:00:00 2001 From: tri10 Date: Wed, 27 Jul 2022 07:07:37 +0530 Subject: [PATCH 135/258] fix: typos in uf components --- .../author-credit-editor/author-credit-section.tsx | 2 +- src/client/unified-form/content-tab/work-row.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx index 8ef311ccbd..86be13f852 100644 --- a/src/client/entity-editor/author-credit-editor/author-credit-section.tsx +++ b/src/client/entity-editor/author-credit-editor/author-credit-section.tsx @@ -85,7 +85,7 @@ function AuthorCreditSection({ const authorCreditPreview = _map(authorCreditEditor, (credit) => `${credit.name}${credit.joinPhrase}`).join(''); const authorCreditRows = _values(authorCreditEditor); - const isValid = validateAuthorCreditSection(authorCreditRows.join, isUf); + const isValid = validateAuthorCreditSection(authorCreditRows, isUf); const editButton = ( // eslint-disable-next-line react/jsx-no-bind diff --git a/src/client/unified-form/content-tab/work-row.tsx b/src/client/unified-form/content-tab/work-row.tsx index 41428c7a57..7fbd4afc7f 100644 --- a/src/client/unified-form/content-tab/work-row.tsx +++ b/src/client/unified-form/content-tab/work-row.tsx @@ -25,7 +25,7 @@ type WorkRowProps = WorkRowStateProps & WorkRowDispatchProps & WorkRowOwnProps; function WorkRow({onChange, work, onRemove, onToggle, onCopyHandler, ...rest}:WorkRowProps) { const isChecked = work?.checked; - const handleCopy = React.useMemo(() => onCopyHandler(work.id), [onCopyHandler, work]); + const handleCopy = React.useCallback(() => onCopyHandler(work.id), [onCopyHandler, work]); const onChangeHandler = React.useCallback((value:any) => { value.checked = isChecked; onChange(value); From 4ecaa132a81fdf9ef7b138ec15b94ef3895620f3 Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Thu, 28 Jul 2022 06:54:17 +0000 Subject: [PATCH 136/258] chore: Add react-tooltip and i18n-iso-languages --- package.json | 2 ++ yarn.lock | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/package.json b/package.json index 73709d56bf..4c13b0d536 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "browserslist": "> 0.25%, not dead", "dependencies": { "@babel/runtime": "^7.17.7", + "@cospired/i18n-iso-languages": "^4.0.0", "@elastic/elasticsearch": "^5.6.22", "@fortawesome/fontawesome-svg-core": "^1.2.30", "@fortawesome/free-brands-svg-icons": "^6.1.1", @@ -75,6 +76,7 @@ "react-simple-star-rating": "^4.0.5", "react-sortable-hoc": "^2.0.0", "react-sticky": "^6.0.1", + "react-tooltip": "^4.2.21", "redis": "^3.1.2", "redux": "^3.7.2", "redux-debounce": "^1.0.1", diff --git a/yarn.lock b/yarn.lock index ebfc9e5208..52ccb29d0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1156,6 +1156,11 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@cospired/i18n-iso-languages@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@cospired/i18n-iso-languages/-/i18n-iso-languages-4.0.0.tgz#fbd54bd046e28295a2ab8f3806c77b0d92852b29" + integrity sha512-8dKE8TJIhb6/JpwpshTV96Q/fT8kAohnAD2KnOuKkVemhDcWDjurPmzj8NnA25YW4gcKfvJYq/iDbXyB0gZ2jg== + "@discoveryjs/json-ext@^0.5.0": version "0.5.6" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f" @@ -6918,6 +6923,14 @@ react-sticky@^6.0.1: prop-types "^15.5.8" raf "^3.3.0" +react-tooltip@^4.2.21: + version "4.2.21" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.21.tgz#840123ed86cf33d50ddde8ec8813b2960bfded7f" + integrity sha512-zSLprMymBDowknr0KVDiJ05IjZn9mQhhg4PRsqln0OZtURAJ1snt1xi5daZfagsh6vfsziZrc9pErPTDY1ACig== + dependencies: + prop-types "^15.7.2" + uuid "^7.0.3" + react-transition-group@^4.3.0, react-transition-group@^4.4.1: version "4.4.2" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" @@ -8292,6 +8305,11 @@ utils-merge@1.0.1, utils-merge@1.x.x: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +uuid@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" + integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" From 41ae4efd0a4bb477bc74dbef49f4c30f8e48a90a Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Thu, 28 Jul 2022 06:56:18 +0000 Subject: [PATCH 137/258] feat: Add function to count words for reviews --- src/client/helpers/utils.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/client/helpers/utils.tsx b/src/client/helpers/utils.tsx index 1ffa923955..28f3f2ef08 100644 --- a/src/client/helpers/utils.tsx +++ b/src/client/helpers/utils.tsx @@ -219,3 +219,12 @@ export function getEntityKey(entityType:string) { }; return keys[entityType]; } + +export function countWords(text: string) : number { + // Credit goes to iamwhitebox https://stackoverflow.com/a/39125279/14911205 + const words = text.match(/\w+/g); + if (words === null) { + return 0; + } + return words.length; +} From 22381f551da0233d168dd171ff63fa91fc3e77f0 Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Thu, 28 Jul 2022 08:46:57 +0000 Subject: [PATCH 138/258] fix: use post route for refreshing token --- src/server/routes/externalService.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/server/routes/externalService.js b/src/server/routes/externalService.js index 1389896c44..b273764528 100644 --- a/src/server/routes/externalService.js +++ b/src/server/routes/externalService.js @@ -18,7 +18,7 @@ import * as auth from '../helpers/auth'; -import * as cbHelper from '../helpers/critiquebrainz.ts'; +import * as cbHelper from '../helpers/critiquebrainz'; import * as propHelpers from '../../client/helpers/props'; import {BadRequestError, NotFoundError} from '../../common/helpers/error'; import {escapeProps, generateProps} from '../helpers/props'; @@ -42,15 +42,18 @@ router.get('/', async (req, res) => { const {alertType, alertDetails} = req.query; const {orm} = req.app.locals; const editorId = req.user.id; + const cbUser = await orm.func.externalServiceOauth.getOauthToken( editorId, 'critiquebrainz', orm ); + let cbPermission = 'disable'; - if (cbUser?.length) { + if (cbUser) { cbPermission = 'review'; } + const props = generateProps(req, res, { alertDetails, alertType, @@ -104,7 +107,7 @@ router.get('/critiquebrainz/callback', auth.isAuthenticated, async (req, res, ne }); -router.get('/critiquebrainz/refresh', auth.isAuthenticated, async (req, res, next) => { +router.post('/critiquebrainz/refresh', auth.isAuthenticated, async (req, res, next) => { const editorId = req.user.id; const {orm} = req.app.locals; let token = await orm.func.externalServiceOauth.getOauthToken( @@ -112,10 +115,10 @@ router.get('/critiquebrainz/refresh', auth.isAuthenticated, async (req, res, nex 'critiquebrainz', orm ); - if (!token?.length) { + + if (!token) { return next(new NotFoundError('User has not authenticated to CritiqueBrainz')); } - token = token[0]; const tokenExpired = new Date(token.token_expires).getTime() <= new Date(new Date().getTime() + marginTime).getTime(); if (tokenExpired) { try { From bc1b28eb1599244f33953d5e10291c0ba115a4e6 Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Thu, 28 Jul 2022 08:50:25 +0000 Subject: [PATCH 139/258] feat: Add routes and functions to post CB Reviews --- src/server/helpers/critiquebrainz.ts | 49 +++++++++++++++++++++++++++- src/server/routes/reviews.js | 41 +++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/server/helpers/critiquebrainz.ts b/src/server/helpers/critiquebrainz.ts index 29989177fe..e0d0444a24 100644 --- a/src/server/helpers/critiquebrainz.ts +++ b/src/server/helpers/critiquebrainz.ts @@ -25,6 +25,7 @@ import request from 'superagent'; const OAUTH_AUTHORIZE_URL = 'https://critiquebrainz.org/oauth/authorize'; const OAUTH_TOKEN_URL = 'https://critiquebrainz.org/ws/1/oauth/token'; +const REVIEW_URL = 'https://critiquebrainz.org/ws/1/review/'; const critiquebrainzScopes = ['review']; const cbConfig = config.critiquebrainz; @@ -145,7 +146,7 @@ export async function getReviewsFromCB(bbid: string, } try { const res = await request - .get('https://critiquebrainz.org/ws/1/review') + .get(REVIEW_URL) .query({ // eslint-disable-next-line camelcase entity_id: bbid, @@ -161,3 +162,49 @@ export async function getReviewsFromCB(bbid: string, return {reviews: [], successfullyFetched: false}; } } + +export async function submitReviewToCB( + accessToken: string, + review: Record +): Promise { + const mapEntityType = { + EditionGroup: 'bb_edition_group' + }; + const cbEntityType = mapEntityType[review.entityType]; + + if (!cbEntityType) { + return { + message: 'Entity type not supported', + reviewID: null, + successfullySubmitted: false + }; + } + + try { + const res = await request + .post(REVIEW_URL) + .set('Content-Type', 'application/json') + .auth(accessToken, {type: 'bearer'}) + .send({ + entity_id: review.entityBBID, + entity_type: cbEntityType, + lang: review.language, + license_choice: 'CC BY-SA 3.0', + rating: review.rating, + text: review.textContent + }); + return { + message: res.body.message, + reviewID: res.body.id, + successfullySubmitted: true + }; + } + + catch (error) { + return { + message: error.response?.body?.description, + reviewID: null, + successfullySubmitted: false + }; + } +} diff --git a/src/server/routes/reviews.js b/src/server/routes/reviews.js index e47aa643c8..b294fbb1bf 100644 --- a/src/server/routes/reviews.js +++ b/src/server/routes/reviews.js @@ -17,6 +17,7 @@ */ +import * as auth from '../helpers/auth'; import * as cbHelper from '../helpers/critiquebrainz'; import express from 'express'; @@ -29,4 +30,44 @@ router.get('/:entityType/:bbid/reviews', async (req, res) => { res.json(reviews); }); +router.post('/:entityType/:bbid/reviews', auth.isAuthenticated, async (req, res) => { + const editorId = req.user.id; + const {orm} = req.app.locals; + + const {accessToken} = req.body; + const {review} = req.body; + + let response = await cbHelper.submitReviewToCB( + accessToken, + review + ); + + // If the token has expired, we try to refresh it and then submit the review again. + if (response.message === 'The provided authorization token is invalid, expired, revoked, or was issued to another client.') { + try { + const token = await orm.func.externalServiceOauth.getOauthToken( + editorId, + 'critiquebrainz', + orm + ); + + const newAccessToken = await cbHelper.refreshAccessToken( + editorId, + token.refresh_token, + orm + ); + + response = await cbHelper.submitReviewToCB( + newAccessToken.access_token, + review + ); + } + catch (error) { + return res.json({error: error.message}); + } + } + + return res.json(response); +}); + export default router; From 672825692d6c5c825eea75481988baa5fbcc355f Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Thu, 28 Jul 2022 10:42:51 +0000 Subject: [PATCH 140/258] feat: Changed Error type --- src/server/routes/externalService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/routes/externalService.js b/src/server/routes/externalService.js index b273764528..11ce512821 100644 --- a/src/server/routes/externalService.js +++ b/src/server/routes/externalService.js @@ -20,7 +20,7 @@ import * as auth from '../helpers/auth'; import * as cbHelper from '../helpers/critiquebrainz'; import * as propHelpers from '../../client/helpers/props'; -import {BadRequestError, NotFoundError} from '../../common/helpers/error'; +import {AuthenticationFailedError, BadRequestError} from '../../common/helpers/error'; import {escapeProps, generateProps} from '../helpers/props'; import ExternalServices from '../../client/components/pages/externalService'; import Layout from '../../client/containers/layout'; @@ -117,7 +117,7 @@ router.post('/critiquebrainz/refresh', auth.isAuthenticated, async (req, res, ne ); if (!token) { - return next(new NotFoundError('User has not authenticated to CritiqueBrainz')); + return next(new AuthenticationFailedError('User has not authenticated to CritiqueBrainz')); } const tokenExpired = new Date(token.token_expires).getTime() <= new Date(new Date().getTime() + marginTime).getTime(); if (tokenExpired) { From c123dcb86f8dd075ddc83d683a7731527aaab724 Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Thu, 28 Jul 2022 10:44:35 +0000 Subject: [PATCH 141/258] feat: Add CB Review Modal --- .../components/pages/entities/cb-review.js | 6 +- .../pages/entities/cbReviewModal.tsx | 549 ++++++++++++++++++ .../pages/entities/edition-group.js | 28 +- src/client/components/pages/entities/title.js | 21 +- 4 files changed, 595 insertions(+), 9 deletions(-) create mode 100644 src/client/components/pages/entities/cbReviewModal.tsx diff --git a/src/client/components/pages/entities/cb-review.js b/src/client/components/pages/entities/cb-review.js index 70dbe85cdf..0b50f60286 100644 --- a/src/client/components/pages/entities/cb-review.js +++ b/src/client/components/pages/entities/cb-review.js @@ -78,6 +78,7 @@ class EntityReviews extends React.Component { }; this.entityType = props.entityType; this.entityBBID = props.entityBBID; + this.handleModalToggle = props.handleModalToggle; } async handleClick() { @@ -118,8 +119,8 @@ class EntityReviews extends React.Component {

No reviews yet.