Personal Information
Name: Shubham Gupta
IRC Nick: Shubh
Github: tr1ten
Portfolio: 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
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.
Contents Tab
User can search select work (other fields will be disabled in that case) or create if not exist.
Details Tab
Server Changes
/create & /create/handler will be added to express routes /index with the required controller and helper functions.
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.
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.
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
{
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
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
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
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, some of my favorites are outrunning karma and demons.
If applying for a BookBrainz project: what type of books do you read?
I love reading fiction, my favorites include Classroom of elite, Sherlock Holmes.
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