Skip to content

Commit

Permalink
Dependency implies dependencies on all sub-issues
Browse files Browse the repository at this point in the history
With this commit, dependencies on parent issues are no longer (de facto)
ignored. (“De factor” because parent issues often have no remaining
effort of their own.) Parent issues now have implicit dependencies on
their sub-issues. Hence, any issue depending on a parent issue P can
only be scheduled once all sub-issues of P are completed.
  • Loading branch information
fschopp committed Aug 3, 2019
1 parent 5f2d7ab commit 128f528
Show file tree
Hide file tree
Showing 8 changed files with 372 additions and 86 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"node": ">=10.0.0"
},
"dependencies": {
"@fschopp/project-planning-js": "^1.1.1"
"@fschopp/project-planning-js": "^1.1.2"
},
"devDependencies": {
"@babel/core": "^7.5.5",
Expand All @@ -57,7 +57,8 @@
"jest": "^24.8.0",
"jsdom": "^15.1.1",
"parcel-bundler": "^1.12.3",
"rollup": "^1.17.0",
"regenerator-runtime": "^0.13.3",
"rollup": "^1.18.0",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-sourcemaps": "^0.4.2",
"rollup-plugin-terser": "^5.1.1",
Expand Down
3 changes: 1 addition & 2 deletions src/demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,5 @@ <h1 class="mt-5">Demo: Project Planning for YouTrack</h1>

<!-- Required because our browserslist settings includes older browsers without async/await support. Hence, the
transpilation results in code for which a runtime component is necessary. -->
<script src="https://cdn.jsdelivr.net/npm/@babel/polyfill@7.4.4/dist/polyfill.min.js"
integrity="sha256-lu1gm0Fb5u5n6tuNLefOZNE96ckovOjhNzvsl+Iz50w=" crossorigin="anonymous"></script>
<script src="../../node_modules/regenerator-runtime/runtime.js"></script>
<script src="demo.ts"></script>
56 changes: 50 additions & 6 deletions src/main/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,48 @@ export interface IssueActivity {
isWaiting: boolean;
}

/**
* Node in an issue tree (or forest).
*
* In an issue tree (or forest), there is a one-to-one correspondence between {@link SchedulableIssue} objects and
* {@link IssueNode} objects. Parent-child and dependency relationships are “lifted” to {@link IssueNode}. That is, the
* dependencies of an {@link IssueNode} `a` are just those nodes that correspond to the issues referenced by
* `a.issue.dependencies`.
*
* See also {@link makeForest}().
*/
export interface IssueNode<T extends SchedulableIssue> {
/**
* Index of {@link issue} in the underlying (flat) array that was used to create this tree.
*/
index: number;

/**
* The issue corresponding to the current node.
*/
issue: T;

/**
* The parent of the the current issue node, or `undefined` if this node is a root node.
*/
parent?: IssueNode<T>;

/**
* Children of the current issue node.
*/
children: IssueNode<T>[];

/**
* Dependencies of the current issue node.
*/
dependencies: IssueNode<T>[];

/**
* Dependents of the current issue node.
*/
dependents: IssueNode<T>[];
}

/**
* An issue activity with one or more assignees.
*
Expand Down Expand Up @@ -167,7 +209,7 @@ export interface RetrieveProjectPlanOptions {
}

/**
* An issue with remaining effort or wait time.
* An issue that can be scheduled.
*
* This interface contains all issue information relevant to its (future) scheduling.
*/
Expand Down Expand Up @@ -203,6 +245,13 @@ export interface SchedulableIssue {
*/
remainingWaitTimeMs?: number;

/**
* Issue identifier (see {@link id}) of the parent issue.
*
* By default, the issue has no parent; that is, this property is the empty string.
*/
parent?: string;

/**
* Whether this issue can be split across more than one person.
*
Expand Down Expand Up @@ -495,11 +544,6 @@ export interface YouTrackIssue extends Required<SchedulableIssue> {
*/
state: string;

/**
* Issue identifier (see {@link id}) of the parent issue, if any, or empty string if the issue has no parent.
*/
parent: string;

/**
* Dictionary of custom field values.
*
Expand Down
1 change: 1 addition & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './api-types';
export * from './issue-forest';
export * from './scheduling';
export * from './you-track-http';
export * from './you-track-project-planning';
Expand Down
89 changes: 89 additions & 0 deletions src/main/issue-forest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { IssueNode, SchedulableIssue } from './api-types';

/**
* Creates an issue tree (or forest) representing the given issues, and returns an iterable over all root nodes.
*
* @typeparam T the issue type
* @param issues Array of issues. The array is expected to be “closed” in the sense that a parent or dependency
* referenced by any of the issues is guaranteed to be contained in `issues`, too.
* @return An iterable over all root nodes. The iterable will return the root nodes in the order they appeared in the
* array. Likewise, the children of each node (at any level) will be stored in input order.
*/
export function makeForest<T extends SchedulableIssue>(issues: T[]): Iterable<IssueNode<T>> {
const idToNode: Map<string, IssueNode<T>> = issues
.reduce((map, issue, index) => map.set(issue.id, {
index,
issue,
children: [],
dependencies: [],
dependents: [],
}), new Map<string, IssueNode<T>>());
// Creating array, so we later close over a simple data structure instead of a map.
const nodes: IssueNode<T>[] = Array.from(idToNode.values());
for (const node of nodes) {
const issue: T = node.issue;
const parentKey: string | undefined = issue.parent;
if (parentKey !== undefined && parentKey.length > 0) {
node.parent = idToNode.get(parentKey)!;
node.parent.children.push(node);
}

if (issue.dependencies !== undefined) {
for (const dependency of issue.dependencies) {
const dependencyNode: IssueNode<T> = idToNode.get(dependency)!;
dependencyNode.dependents.push(node);
node.dependencies.push(dependencyNode);
}
}
}
return {
* [Symbol.iterator]() {
for (const node of nodes[Symbol.iterator]()) {
if (node.parent === undefined) {
yield node;
}
}
},
};
}

/**
* Traverses each of the given issue trees and invokes the given visitor functions.
*
* @typeparam T the issue type
* @param rootNodes The root nodes of the trees making up the forest.
* @param enterNode Visitor function that will be called on entering a node (that is, before any of its children have
* been visited).
* @param enterNode.node The node that is currently being visited.
* @param leaveNode Visitor function that will be called on leaving a node (that is, after all of its children have been
* visited).
* @param leaveNode.node The node that is currently being visited.
*/
export function traverseIssueForest<T extends SchedulableIssue>(
rootNodes: Iterable<IssueNode<T>>,
enterNode: (node: IssueNode<T>) => void,
leaveNode: (node: IssueNode<T>) => void = () => { /* no-op */ }
): void {
let currentIterator: Iterator<IssueNode<T>> = rootNodes[Symbol.iterator]();
const stack: [IssueNode<T>, Iterator<IssueNode<T>>][] = [];
while (true) {
const iteratorResult = currentIterator.next();
if (iteratorResult.done) {
if (stack.length === 0) {
break;
}
let node: IssueNode<T>;
[node, currentIterator] = stack.pop()!;
leaveNode(node);
} else {
const node: IssueNode<T> = iteratorResult.value;
enterNode(node);
if (node.children.length > 0) {
stack.push([node, currentIterator]);
currentIterator = node.children[Symbol.iterator]();
} else {
leaveNode(node);
}
}
}
}
Loading

0 comments on commit 128f528

Please sign in to comment.