feat(web): the roadmap shows dependencies on other projects
Tickets of other projects that block a dated epic, or that it depends on, now show under «De otros proyectos» with a dashed bar, their arrows, and the same warning when the dates clash («EPI-1 empieza el 25 oct, pero BACK-1 (otro proyecto), que la bloquea, acaba el 5 nov»). Both halves of a relation are read, since one recorded from the other project only leaves its inward half here. The ones without dates, or out of reach, are named under the chart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { IssueLinkType } from './enums';
|
||||
import {
|
||||
dependencyClash,
|
||||
externalDependencies,
|
||||
roadmapBar,
|
||||
roadmapDependencies,
|
||||
roadmapMonths,
|
||||
@@ -74,3 +75,43 @@ describe('roadmap', () => {
|
||||
expect(dependencyClash('2026-11-10', '2026-11-03')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dependencies on other projects', () => {
|
||||
const link = (
|
||||
type: IssueLinkType,
|
||||
direction: 'OUTWARD' | 'INWARD',
|
||||
issueId: string,
|
||||
) => ({ type, direction, issueId, issueKey: issueId.toUpperCase() });
|
||||
|
||||
it('reads both halves of a relation with a ticket outside, once each', () => {
|
||||
expect(
|
||||
externalDependencies([
|
||||
{
|
||||
issue: { id: 'web' },
|
||||
links: [
|
||||
// API-1 blocks us (recorded on its side): our inward half.
|
||||
link(IssueLinkType.BLOCKS, 'INWARD', 'api-1'),
|
||||
// We depend on API-2: it goes first.
|
||||
link(IssueLinkType.DEPENDS_ON, 'OUTWARD', 'api-2'),
|
||||
// We block MOB-1.
|
||||
link(IssueLinkType.BLOCKS, 'OUTWARD', 'mob-1'),
|
||||
// MOB-2 depends on us (recorded on its side).
|
||||
link(IssueLinkType.DEPENDS_ON, 'INWARD', 'mob-2'),
|
||||
link(IssueLinkType.RELATES_TO, 'OUTWARD', 'doc-1'),
|
||||
link(IssueLinkType.BLOCKS, 'OUTWARD', 'mob-1'),
|
||||
],
|
||||
},
|
||||
// Among the epics shown: not external.
|
||||
{
|
||||
issue: { id: 'web-2' },
|
||||
links: [link(IssueLinkType.BLOCKS, 'OUTWARD', 'web')],
|
||||
},
|
||||
]).map((d) => `${d.before}>${d.after} (${d.externalKey})`),
|
||||
).toEqual([
|
||||
'api-1>web (API-1)',
|
||||
'api-2>web (API-2)',
|
||||
'web>mob-1 (MOB-1)',
|
||||
'web>mob-2 (MOB-2)',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,6 +111,51 @@ export function roadmapDependencies(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A dependency on a ticket outside the epics shown (another project). */
|
||||
export interface ExternalDependency extends RoadmapDependency {
|
||||
/** Id and key of the ticket outside. */
|
||||
externalId: string;
|
||||
externalKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* «Bloquea» / «depende de» between these epics and tickets that are not
|
||||
* among them (another project's): both directions, since the relation was
|
||||
* maybe recorded from the other side and only its inward half is here.
|
||||
*/
|
||||
export function externalDependencies(
|
||||
epics: { issue: { id: string }; links: IssueLink[] }[],
|
||||
): ExternalDependency[] {
|
||||
const ids = new Set(epics.map((e) => e.issue.id));
|
||||
const seen = new Set<string>();
|
||||
const out: ExternalDependency[] = [];
|
||||
for (const epic of epics)
|
||||
for (const link of epic.links) {
|
||||
if (ids.has(link.issueId)) continue;
|
||||
const outward = link.direction === 'OUTWARD';
|
||||
// Who goes first: the one that blocks, or the one depended on.
|
||||
const otherFirst =
|
||||
link.type === IssueLinkType.BLOCKS
|
||||
? !outward
|
||||
: link.type === IssueLinkType.DEPENDS_ON
|
||||
? outward
|
||||
: null;
|
||||
if (otherFirst === null) continue;
|
||||
const pair = otherFirst
|
||||
? { before: link.issueId, after: epic.issue.id }
|
||||
: { before: epic.issue.id, after: link.issueId };
|
||||
const key = `${pair.before}>${pair.after}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push({
|
||||
...pair,
|
||||
externalId: link.issueId,
|
||||
externalKey: link.issueKey,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The epic that waits starts on or before the day the other one ends. */
|
||||
export function dependencyClash(
|
||||
beforeEnds: string,
|
||||
|
||||
Reference in New Issue
Block a user