Skip to content

Commit

Permalink
Story/000150/story complete status (#192)
Browse files Browse the repository at this point in the history
* added atoll scripts etc.

* added backlog item status to UI

* added ability to patch backlogitem via api

* make sture status has default "N" value

* updated data model docs

* added reference for checkbox component

* bump ver

* drop as any

Co-authored-by: Kevin <kevinbe71@gmail.com>
  • Loading branch information
51ngul4r1ty and singularity15 authored Nov 30, 2020
1 parent 05314f1 commit abfc8cd
Show file tree
Hide file tree
Showing 16 changed files with 345 additions and 64 deletions.
59 changes: 28 additions & 31 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
{
"editor.rulers": [
132
],
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"node_modules": true,
".yalc": true
},
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true,
"build": true,
"coverage": true
},
"bookmarks.navigateThroughAllFiles": true,
"bookmarks.backgroundLineColor": "#102030",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"coverage-gutters.showGutterCoverage": true,
"coverage-gutters.coverageFileNames": [
"lcov.info"
],
"coverage-gutters.lcovname": "lcov.info",
"coverage-gutters.coverageReportFileName": "coverage/**/index.html",
"coverage-gutters.showLineCoverage": false
}
"editor.rulers": [132],
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"node_modules": true,
".yalc": true
},
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true,
"build": true,
"coverage": true
},
"bookmarks.navigateThroughAllFiles": true,
"bookmarks.backgroundLineColor": "#102030",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"coverage-gutters.showGutterCoverage": true,
"coverage-gutters.coverageFileNames": ["lcov.info"],
"coverage-gutters.lcovname": "lcov.info",
"coverage-gutters.coverageReportFileName": "coverage/**/index.html",
"coverage-gutters.showLineCoverage": false,
"deno.enable": false
}
14 changes: 9 additions & 5 deletions atoll-core-main.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@
},
{
"path": "../atoll-shared"
},
{
"path": "../atoll-scripts"
}
],
"settings": {
"bookmarks.navigateThroughAllFiles": true,
"bookmarks.backgroundLineColor": "#102030",
"coverage-gutters.showGutterCoverage": true,
"coverage-gutters.coverageFileNames": [
"lcov.info"
],
"coverage-gutters.coverageFileNames": ["lcov.info"],
"coverage-gutters.lcovname": "lcov.info",
"coverage-gutters.coverageReportFileName": "coverage/**/index.html",
"coverage-gutters.showLineCoverage": false,
"typescript.tsdk": "atoll-core\\node_modules\\typescript\\lib"
"typescript.tsdk": "atoll-core\\node_modules\\typescript\\lib",
"deno.import_intellisense_origins": {
"https://deno.land": true
}
}
}
}
6 changes: 6 additions & 0 deletions docs/HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ Atomic Design

A quick overview of Atomic Design terminology:
https://www.youtube.com/watch?v=aMtnGeiWTyU

Miscellaneous Component References
----------------------------------

How to build the checkbox:
https://webdesign.tutsplus.com/tutorials/how-to-make-custom-accessible-checkboxes-and-radio-buttons--cms-32074
5 changes: 3 additions & 2 deletions docs/dataModel/DATA_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ Planned vs Unplanned
Splitting vs Continuing a Story
-------------------------------

A story may not be completed in a sprint so it can be continued. Also, if the team recognizes that a story can be done in multiple
parts then it can be split. These are not the same thing and should be treated differently:
It is possible that a story is not be completed in a sprint. In that case it will need to span multiple sprints so that it can be
continued. Also, if the team recognizes that a story can be done in multiple parts then it can be split. These are not the same
thing and should be treated differently:
- any part of a story (i.e. a task) can be allocated to a sprint individually (2+ sprints containing same story)
- multiple stories can relate to an originating story (there's an inherent hierachy)

Expand Down
54 changes: 54 additions & 0 deletions docs/dataModel/DATA_MODEL_BACKLOG_ITEMS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Backlog Items
=============

Statuses
--------

1. Not Started
2. In Progress
3. Completed

Sub-statuses
------------

Within each of the above statuses we have smaller "steps" that are defined below. These
steps could actually be done in parallel and indepently and depend on a team's process and
maturity with CI/CD etc. For example, someone performing manual testing could be doing it
using a branch that the developer has provided for code review before the code review is
complete. To support this, these sub-statuses are treated as independent checks on a
checklist - once all have been completed the issue/story can "exit" its current status and
"enter" the next one.

**1. Not Started**
1.1. Not Started - Idea
1.2. Not Started - Defined

**2. In Progress**
2.1. In Progress - In Development
2.2. In Progress - Code Review
2.3. In Progress - Testing
2.4. In Progress - Acceptance

**3. Ready to Release**
3.1. Ready to Release

**4. Released**
4.1. Released

Feature Toggles and "Release" Status
------------------------------------

A story could be implemented behind a feature toggle. The code itself could then be
released to production and toggled on later. For the purpose of Atoll's data model this
toggled-on state is the true "Released" status.


Blocked "Status"
----------------

Unlike other statuses, "blocked" simply means that a work item has been paused because something is preventing it from being
worked on. The workflow in and out of the blocked state can follow a few paths:

1. Not Started --> Blocked --> In Progress
2. Not Started --> Blocked --> Not Started
3. In Progress --> Blocked --> In Progress
File renamed without changes.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "atoll",
"version": "0.22.5",
"version": "0.23.0",
"author": {
"name": "Kevin Berry",
"email": "41717340+51ngul4r1ty@users.noreply.github.com"
Expand Down Expand Up @@ -57,7 +57,7 @@
"setup": "ts-node ./scripts/setup.ts"
},
"dependencies": {
"@atoll/shared": "0.22.5",
"@atoll/shared": "0.23.0",
"@flopflip/memory-adapter": "1.6.0",
"@flopflip/react-broadcast": "10.1.11",
"axios": "0.19.2",
Expand Down
2 changes: 2 additions & 0 deletions src/database/model/upgrade.sql
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ alter table sprintbacklogitem add column "status" char(1);
update sprintbacklogitem set "status" = 'D';

alter table backlogitem alter column estimate type decimal(10, 2);

alter table backlogitem add column status char(1);
44 changes: 40 additions & 4 deletions src/server/api/handlers/backlogItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {
} from "../utils/responder";
import { getParamsFromRequest } from "../utils/filterHelper";
import { backlogItemFetcher } from "./fetchers/backlogItemFetcher";
import { addIdToBody } from "../utils/uuidHelper";
import { getInvalidPatchMessage, getPatchedItem } from "../utils/patcher";
import { backlogItemRankFirstItemInserter } from "./inserters/backlogItemRankInserter";

// data access
import {
Expand All @@ -30,10 +33,6 @@ import {
} from "../../dataaccess";
import { sequelize } from "../../dataaccess/connection";

// interfaces/types
import { addIdToBody } from "../utils/uuidHelper";
import { backlogItemRankFirstItemInserter } from "./inserters/backlogItemRankInserter";

export const backlogItemsGetHandler = async (req: Request, res: Response) => {
const params = getParamsFromRequest(req);
const result = await backlogItemFetcher(params.projectId);
Expand Down Expand Up @@ -325,6 +324,43 @@ export const backlogItemPutHandler = async (req: Request, res: Response) => {
}
};

export const backlogItemPatchHandler = async (req: Request, res: Response) => {
const queryParamItemId = req.params.itemId;
if (!queryParamItemId) {
respondWithFailedValidation(res, "Item ID is required in URI path for this operation");
return;
}
const bodyItemId = req.body.id;
if (bodyItemId) {
respondWithFailedValidation(
res,
`Item ID should only be provided in URI path - Item ID was found in payload: ${bodyItemId}`
);
return;
}
try {
const backlogItem = await BacklogItemModel.findOne({
where: { id: queryParamItemId }
});
if (!backlogItem) {
respondWithNotFound(res, `Unable to find backlogitem to patch with ID ${queryParamItemId}`);
} else {
const originalBacklogItem = mapToBacklogItem(backlogItem);
const invalidPatchMessage = getInvalidPatchMessage(originalBacklogItem, req.body);
if (invalidPatchMessage) {
respondWithFailedValidation(res, `Unable to patch: ${invalidPatchMessage}`);
} else {
const newItem = getPatchedItem(originalBacklogItem, req.body);

await backlogItem.update(newItem);
respondWithItem(res, backlogItem, originalBacklogItem);
}
}
} catch (err) {
respondWithError(res, err);
}
};

export const backlogItemsReorderPostHandler = async (req: Request, res: Response) => {
const sourceItemId = req.body.sourceItemId;
const targetItemId = req.body.targetItemId;
Expand Down
4 changes: 3 additions & 1 deletion src/server/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
backlogItemsPostHandler,
backlogItemsReorderPostHandler,
backlogItemGetHandler,
backlogItemPutHandler
backlogItemPutHandler,
backlogItemPatchHandler
} from "./handlers/backlogItems";
import { sprintPostHandler, sprintsGetHandler, sprintDeleteHandler } from "./handlers/sprints";
import { backlogItemRanksGetHandler, backlogItemRankGetHandler } from "./handlers/backlogItemRanks";
Expand Down Expand Up @@ -75,6 +76,7 @@ setupRoutes(router, `/${BACKLOG_ITEM_RESOURCE_NAME}`, { get: backlogItemsGetHand
setupRoutes(router, `/${BACKLOG_ITEM_RESOURCE_NAME}/:itemId`, {
get: backlogItemGetHandler,
put: backlogItemPutHandler,
patch: backlogItemPatchHandler,
delete: backlogItemsDeleteHandler
});

Expand Down
105 changes: 105 additions & 0 deletions src/server/api/utils/__tests__/patcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// test related
import "jest";

// code under test
import { getValidationFailureMessage, validateBaseKeys, validatePatchObjects } from "../patcher";

describe("Patcher", () => {
describe("validateBaseKeys", () => {
it("should handle empty objects correctly", () => {
const actual = validateBaseKeys({}, {});
expect(actual.valid).toBeTruthy();
});
it("should treat null and empty objects the same", () => {
const actual = validateBaseKeys(null, {});
expect(actual.valid).toBeTruthy();
});
it("should treat empty and null objects the same", () => {
const actual = validateBaseKeys({}, null);
expect(actual.valid).toBeTruthy();
});
it("should handle flat object structure correctly", () => {
const actual = validateBaseKeys({ a: 1, b: 2, c: 3 }, { a: 10, b: 20, c: 30 });
expect(actual.valid).toBeTruthy();
});
it("should return invalid when extra fields found", () => {
const actual = validateBaseKeys({ a: 1, b: 2 }, { a: 10, b: 20, c: 30 });
expect(actual.valid).toBeFalsy();
expect(actual.extraFields).toStrictEqual(["c"]);
});
it("should ignore complex object fields in target node", () => {
const actual = validateBaseKeys({ a: 1, complex: { x: 5, y: 9 } }, { a: 10 });
expect(actual.valid).toBeTruthy();
});
it("should treat extra complex object fields in source node as invalid", () => {
const actual = validateBaseKeys({ a: 1 }, { a: 10, complex: { x: 5, y: 9 } });
expect(actual.valid).toBeFalsy();
expect(actual.extraFields).toStrictEqual(["complex"]);
});
it("should ignore complex object extra fields in source node", () => {
const actual = validateBaseKeys({ a: 1, complex: { x: 5, y: 9 } }, { a: 10, complex: { x: 5, y: 9, z: 7 } });
expect(actual.valid).toBeTruthy();
});
});
describe("validatePatchObjects", () => {
it("should handle unpatchable nested objects correctly", () => {
const actual = validatePatchObjects(
{
a: 1,
b: {
ba: 2,
c: {
ca: 3
}
}
},
{
a: 1,
b: {
ba: 2,
c: {
ca: 3,
cb: "invalid"
}
}
}
);
expect(actual.valid).toBeFalsy();
expect(actual.extraFields).toStrictEqual(["b.c.cb"]);
});
it("should handle patchable nested objects correctly", () => {
const actual = validatePatchObjects(
{
a: 1,
b: {
ba: 2,
c: {
ca: 3,
cb: "valid"
}
}
},
{
a: 1,
b: {
ba: 2,
c: {
ca: 3
}
}
}
);
expect(actual.valid).toBeTruthy();
});
});
describe("getValidationFailureMessage", () => {
it("should handle a single invalid nested field", () => {
const actual = getValidationFailureMessage({ valid: false, extraFields: ["b.c.cb"] });
expect(actual).toEqual("extra fields found in new object: b.c.cb");
});
it("should handle valid scenario", () => {
const actual = getValidationFailureMessage({ valid: true, extraFields: [] });
expect(actual).toEqual("patch object is valid");
});
});
});
Loading

0 comments on commit abfc8cd

Please sign in to comment.