# GSOC 2022: Unified Creation Form - BookBrainz

**URL:** https://community.metabrainz.org/t/gsoc-2022-unified-creation-form-bookbrainz/578635
**Category:** GSoC applications
**Tags:** gsoc-2022, bookbrainz
**Created:** [March 29, 2022, 5:22am UTC](https://community.metabrainz.org/t/gsoc-2022-unified-creation-form-bookbrainz/578635 "2022-03-29T05:22:26Z")
**Posts on this page:** 4
**Page:** 1

<div class="post-metadata">

### Author: ![Shubh](https://community.metabrainz.org/user_avatar/community.metabrainz.org/shubh/32/50961_2.png) [@Shubh](https://community.metabrainz.org/u/Shubh)
#### Post date: [March 29, 2022, 5:22am UTC](https://community.metabrainz.org/t/gsoc-2022-unified-creation-form-bookbrainz/578635/1 "2022-03-29T05:22:26Z")

</div>

# Personal Information

Name: Shubham Gupta

IRC Nick: Shubh

Github: [tr1ten](https://github.com/tr1ten)

Portfolio: [imshubh.ml](http://imshubh.ml/)

# Proposal

## Project Overview

Currently, Users have to face complex workflow for simple use cases (such as “adding a book”) with lots of repetition of data.

This project aims to design and implement a simple yet powerful editor for such cases which can abstract away bookbrainz specifics from a user by introducing a new **book** template which will wrap over the existing edition entity with useful defaults and pre-configured relationships.

This will also allow experienced users to create multiple entities and link them in the same workflow.

## Features

- A user will be easily able to add a book through **book template** , the user can add its author(credits) & publisher(s) either through search or create if not already exist.
- User can add contents(works) to a book by just specifying type (i.e. work type).
- All created entities can easily be modified by selecting the required entity in the **Created Entities** section.
- Of course this editor is not only limited to book creation, a user will be able to add multiple entities and link them on the same page.

## UI Prototype

_note: these designs will likely be modified according to suggestions received by community members during the initial phase of the project._

New item **Book** to access **Unified Form Editor**

 ![navbar](https://community.metabrainz.org/uploads/default/original/3X/b/0/b067a46ff4967c17faedee00de465d71524369f5.png)

**Unified form** will have a **Book** already created in the **Created Entities** section.

**Book** template will have three tabs as described below:

- **Basic** : This will include information that users will most likely find on the book’s cover like the publisher’s name.
- **Contents** : Works like forewords contained in the book will be added here.
- **Details** : Specific details like pages, format, etc will be added here with annotation

This will hide the unnecessary complexities like relationship editor from a user, but that is to say, a user can modify other entities by following the usual workflow as described below.

Other **entities** like the publisher, author, etc, can also be created individually by selecting the required entity in the **Add** section.

It will follow a similar workflow to the current entity-editor but with tabs and minor UX improvements.

It will have four tabs namely

- **Basic** : Name Section + Entity Section
- **Identifiers**
- **Relationships**
- **Annotation**

This will allow a user to create multiple entities with the familiar workflow of entity-editor.

_Example filling of unified form for book creation_

From Sidebar, using **Created Entities** section user can select newly created entity and modify it if needed, or add one using **Add** section.  
After hitting submit, user will be presented with a modal to confirm the changes.

 ![create](https://community.metabrainz.org/uploads/default/original/3X/b/8/b8e996bc8c9c32dbdde4583af6e500c6f114a45a.png)

**Contents** Tab

User can search select work (other fields will be disabled in that case) or create if not exist.

 ![bookwrapper-form-content](https://community.metabrainz.org/uploads/default/original/3X/c/0/c0d82f2dd37c8bcb179251038aca3cfe75039664.png)

**Details** Tab

 ![bookwrapper-form-details](https://community.metabrainz.org/uploads/default/original/3X/e/b/eb1bf46d0b194af19aa10e2aa553fde34f726f76.png)

## Server Changes

**/create** & / **create/handler** will be added to express routes /index with the required controller and helper functions.

```auto
import {createEntitesHandler, generateUnifiedProps, unifiedFormMarkup} from '../helpers/entityRouteUtils';

 router.get('/create', isAuthenticated, middleware.loadIdentifierTypes,
 	middleware.loadLanguages, middleware.loadGenders, middleware.loadEntityTypes, 
	middleware.loadEditionStatuses, middleware.loadEditionFormats
 	middleware.loadRelationshipTypes, (req, res) => {
 		const props = generateUnifiedProps(req, res, {});
 		const formMarkup = unifiedFormMarkup(props);
 		const {markup, props: updatedProps} = formMarkup;
 		return res.send(target({
 			markup,
 			props: escapeProps(updatedProps),
 			script: '/js/unified-form.js',
 			title: 'Unified form'
 		}));
 	});

  router.post('/create/handler', isAuthenticatedForHandler, createEntitesHandler);

```

**createEntitesHandler** in server helpers/entityRouteUtils will be added with required validator and transformation.

```auto
export function createEntitesHandler(req: PassportRequest, res: $Response) {
	const validate = getValidator("unifiedForm");
	if (!validate(req.body)) {
		const err = new error.FormSubmissionError();
		error.sendErrorAsJSON(res, err);
	}
	req.body = entityRoutes.transformForm(req.body);
	return entityRoutes.handleCreateEntities(req, res);
}

```

**handleCreateEntities** ( in routes/entity/entity) to create multiple entities each given a new revision, also new revision for adding relationships on them.

```auto
export async function handleAddRelationships(
	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<string, any>} = 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 = {};
		const bbidMap = {};
		const allRelationships = [];
		try {
			for (const [entityKey, entityForm] of Object.entries(body)) {
				const entityType = _.upperFirst(entityForm.type);
				allRelationships.push(entityForm.relationships);
				const newEntity = await new Entity({type: entityType}).save(null, {transacting});
				currentEntity = newEntity.toJSON();
				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;
				_.forOwn(changedProps, (value, key) => mainEntity.set(key, value));
				const savedMainEntity = await saveEntitiesAndFinishRevision(
					orm, transacting, true, newRevision, mainEntity, [mainEntity],
					editorJSON.id, entityForm.note
				);
				await savedMainEntity.load('aliasSet.aliases', {transacting});
				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();
			}
			for (const [index, rels] of allRelationships.entries()) {
				if (!_.isEmpty(rels)) {
					const relationships = rels.map((rel) => (
						{...rel, sourceBbid: _.get(bbidMap, rel.sourceBbid.toString()) ?? rel.sourceBbid,
							 targetBbid: _.get(bbidMap, rel.targetBbid.toString()) ?? 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<string, any>) => {
			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,
		search._bulkIndexEntities
	);
}

```

## Frontend Changes

We will need a new redux store for handling form state, redux state should look something like this

```auto
{
	entitySection:{
		activeEntity:{
			nameSection,
			….,
			type,
			id,
			isActive,
			}
		entities:{0:{}}
		}
	submissionSection:{...}
}

```

**Redux Reducer** in client/unified-form/helper  
Single entity will be using the same entity-editor state with some extra attributes

```auto
function entitySectionReducer(state, action) {
	const intermediateState = combineReducers({
		activeEntity: combineReducers(
			{
				active: activeReducer,
				aliasEditor: aliasEditorReducer,
				annotationSection: annotationSectionReducer,
				authorSection: authorSectionReducer,
				id: idReducer,
				identifierEditor: identifierEditorReducer,
				nameSection: nameSectionReducer,
				relationshipSection: relationshipSectionReducer,
				type: typeReducer,
				workSection: workSectionReducer
			}
		),
		entities: entitiesReducer
	})(state, action);
	const finalState = crossSliceReducer(intermediateState, action);
	return finalState;
}
export function createRootReducer() {
	return combineReducers({
		entitySection: entitySectionReducer,
		submissionSection: submissionSectionReducer
	});
}

```

**crossSliceReducer** for facilitating interaction between different states

```auto
function crossSliceReducer(state, action) {
	const {type, payload} = action;
	switch (type) {
		case SELECT_ENTITY: {
			const newState = crossSliceReducer(state.setIn(['activeEntity', 'active'], false), {type: SAVE_ENTITY});
			return newState.setIn(['entities', payload, 'active'], true).set('activeEntity', newState.getIn(['entities', payload]));
		}
		case SAVE_ENTITY:
			return state.setIn(['entities', state.getIn(['activeEntity', 'id'])], state.get('activeEntity'));
		case REMOVE_ENTITY:
		{
			const activeEntityId = state.getIn(['activeEntity', 'id']);
			const newState = state.deleteIn(['entities', payload]);
			if (payload === activeEntityId) {
				return newState.set('activeEntity', newState.get('entities').first());
			}
			return newState;
		}
		case REMOVEALL_ENTITY:
			return state.set('activeEntity', new Map()).set('entities', new Map());
		default:
			return state;
	}
}

```

entity-section will manage namely these actions

- **REMOVE\_ALL** : Remove all entity from “activeEntity” as well as from “entities”
- **REMOVE\_ENTITY** : Remove specific entity with id
- **SAVE\_ENTITY** : Save the active entity state in entities
- **SELECT\_ENTITY** : Select active entity from entities

# TIMELINE

### Community Bonding Period

I believe this project is similar to the present entity-editor in bookbrainz so I will try to understand it more sincerely.

But my priority would be to design an awesome unified form with the suggestions of community members.

Also, I will be taking on some small tickets in the meantime.

### Week 1-2: Designing Phase

Together with my mentor, we will be finalizing the designs for a unified form.  
This will need a thorough discussion with mentors and community members for perfecting it!

### Week 3: Implementation Phase

I will be working on the routes for submitting multiple entities, also writing thorough unit tests for the same.

### Week 4-5-6-7

I will then start with front-end work, I will first work on accommodating the existing entities in the editor.

I will add a unified-form page and create or modify its required components.

I will also be adding the redux reducers and their respective actions.

### Week 8

Taking reviews from the mentor, fixing bugs, UX issues, and improving the overall code quality.

### Week 9-10-11

I will then start to implement the book template, also I will be adding the default entity-entity relationships so the user doesn’t need to manually link them.

### Week 12

Testing unified form thoroughly for bugs, UX issues.

### Stretch Goal

- Adding graph to visualize entities before submission
- Adding useful validation messages

## Detailed information about yourself

My name is Shubham Gupta (aka Shubh), I am a first-year undergrad student pursuing a bachelor’s degree in Information Technology from the National Institute of Technology, Kurushetra. I love working with computers, building stuff to solve real-life problems is what drives me forwards apart from that I’ve always been a big fan of fiction series.

I joined the **metabrainz** community at the end of November’21

**My PRs** : [Check out](https://github.com/metabrainz/bookbrainz-site/pulls?q=is%3Apr+sort%3Aupdated-desc+author%3Atr1ten)

**Tell us about the computer(s) you have available for working on your SoC project!**

I have a dell Inspiron with 12 gigs of memory and an i3 6th gen processor

**When did you first start programming?**

In middle school, I wrote my first hello world program in C language.

**What type of music do you listen to?**

I’m a big fan of artist [Alec Benjamin](https://musicbrainz.org/artist/21a14ee3-cede-420a-9d6d-a33517d2f952), some of my favorites are [outrunning karma](https://musicbrainz.org/release/efe29403-43c5-445f-ae0d-5cacb4e37dde) and [demons](https://musicbrainz.org/release/828d19c0-bef8-4123-ab58-66362b95c691).

**If applying for a BookBrainz project: what type of books do you read?**

I love reading fiction, my favorites include [Classroom of elite](https://bookbrainz.org/series/71c35cfc-3fa2-4398-8be6-af6338468391), [Sherlock Holmes](https://bookbrainz.org/series/99d911ad-e5da-40fd-9e32-281b3d2df345).

**What aspects of the project you’re applying for the interest you the most?**

Designing the intuitive unified form excites me the most

**Have you contributed to other Open Source projects?**

No, Bookbrainz is my first Open Source project

**How much time do you have available, and how would you plan to use it?**

I would be able to give approx 45 hours every week to the project.

**Do you plan to have a job or study during the summer in conjunction with Summer of Code?**

No, I would be able to devote all my time to the project

---

<div class="post-metadata">

### Author: ![Shubh](https://community.metabrainz.org/user_avatar/community.metabrainz.org/shubh/32/50961_2.png) [@Shubh](https://community.metabrainz.org/u/Shubh)
#### Post date: [April 4, 2022, 7:36am UTC](https://community.metabrainz.org/t/gsoc-2022-unified-creation-form-bookbrainz/578635/2 "2022-04-04T07:36:24Z")

</div>

@mr_monkey When you get time please have a look, would love to hear your suggestions to improve the proposal.  
also looking forward to community member’s thoughts on the new form design 😄

---

<div class="post-metadata">

### Author: ![mr\_monkey](https://community.metabrainz.org/letter_avatar_proxy/v4/letter/m/e95f7d/32.png) [@mr\_monkey](https://community.metabrainz.org/u/mr_monkey)
#### Post date: [April 17, 2022, 6:28pm UTC](https://community.metabrainz.org/t/gsoc-2022-unified-creation-form-bookbrainz/578635/3 "2022-04-17T18:28:44Z")

</div>

Hi!  
Thanks for your proposal, and sorry to answer only this late, life got in the way.

In general the proposal is looking great!

I like that the Basic tab is about information you would find on a cover, that’s a smart approach. Why not call the tab **Cover** instead?

Here are some details that we would need to figure out in the early design process:

The Contents tab should include a way to search for an existing Work, which I don’t see there.  
We would also probably find a good way to add some other common relationships when creating a new work such as translator, illustrator…

I’m not sure what purpose the “created entities” section on the right currently has that isn’t covered by a recap screen at the end to confirm data before submission, as we previously talked about (and which I don’t see mentioned in the proposal; it would be good to add it).  
The mockup doesn’t make it clear to me how i would use it to add entities, which is what I expect the “+ add work” button in the Contents tab would be for.  
I think we can find a way to add entities inline without having that side bar.

One thing that is going to be important is to write tests as the project goes along.  
So when you would implement the back-end routes, you would write the tests for it in the continuation before moving to the next task.  
It’s easier to write tests with the implementation fresh in mind than at the end, possibly running out of time.

Thanks for your submission !

---

<div class="post-metadata">

### Author: ![Shubh](https://community.metabrainz.org/user_avatar/community.metabrainz.org/shubh/32/50961_2.png) [@Shubh](https://community.metabrainz.org/u/Shubh)
#### Post date: [April 18, 2022, 7:18am UTC](https://community.metabrainz.org/t/gsoc-2022-unified-creation-form-bookbrainz/578635/4 "2022-04-18T07:18:07Z")

</div>

> [@mr\_monkey](#):
>
> The Contents tab should include a way to search for an existing Work, which I don’t see there.

Yeah, my bad we should definitely have a search (or create if not exist) field for contents tab.

> [@mr\_monkey](#):
>
> I’m not sure what purpose the “created entities” section

I suppose user might want to modify entity that automatically created like publisher, although we can get rid of that for simplicity but i’m afraid it might make this form more restrictive for existing users.

> [@mr\_monkey](#):
>
> recap screen at the end to confirm data before submission,

I am planning to show a confirmation modal to list all the entities that will be created after submission.

> [@mr\_monkey](#):
>
> The mockup doesn’t make it clear to me how i would use it to add entities

We will be using search (create if not exist) dropdown to create entity with search query as a primary alias (like publisher shown in the example).

> [@mr\_monkey](#):
>
> I think we can find a way to add entities inline without having that side bar.

like i said i’m worried if it might make this form restrictive in its usage ☹

> [@mr\_monkey](#):
>
> It’s easier to write tests with the implementation fresh in mind than at the end

Yep!

Thanks for all the suggestions, i will be adding these details (with mockups) in the proposal.
