Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 76 additions & 7 deletions packages/software-factory/realm/issue-tracker.gts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import CheckboxIcon from '@cardstack/boxel-icons/checkbox';
import CircleAlert from '@cardstack/boxel-icons/circle-alert';
import Folder from '@cardstack/boxel-icons/folder';
import LayoutSidebarRightCollapse from '@cardstack/boxel-icons/layout-sidebar-right-collapse';
import Link2 from '@cardstack/boxel-icons/link-2';
import LayoutSidebarRightExpand from '@cardstack/boxel-icons/layout-sidebar-right-expand';
import MessageSquare from '@cardstack/boxel-icons/message-square';
import Settings from '@cardstack/boxel-icons/settings';
Expand Down Expand Up @@ -163,7 +164,7 @@ class IssueIsolated extends Component<typeof Issue> {
@tracked showSidebar = true;
@tracked descriptionOpen = true;
@tracked acceptanceCriteriaOpen = true;
@tracked commentsOpen = false;
@tracked commentsOpen = true;

get statusColor(): string | undefined {
return getIssueStatusColor(this.args.model, this.args.model?.status);
Expand Down Expand Up @@ -345,7 +346,12 @@ class IssueIsolated extends Component<typeof Issue> {

{{#if @model.blockedBy.length}}
<div class='sidebar-section'>
<h3 class='sidebar-section-title'>Blocked By</h3>
<h3
class='sidebar-section-title'
data-test-dependencies-heading
>
Depends On
</h3>
<div class='related-list'>
<@fields.blockedBy />
</div>
Expand Down Expand Up @@ -660,7 +666,7 @@ class IssueEdit extends Component<typeof Issue> {
@tracked showSidebar = true;
@tracked descriptionOpen = true;
@tracked acceptanceCriteriaOpen = true;
@tracked commentsOpen = false;
@tracked commentsOpen = true;

get statusColor(): string | undefined {
return getIssueStatusColor(this.args.model, this.args.model?.status);
Expand Down Expand Up @@ -835,7 +841,9 @@ class IssueEdit extends Component<typeof Issue> {
</FieldContainer>
</div>
<div class='sidebar-section'>
<h3 class='sidebar-section-title'>Blocked By</h3>
<h3 class='sidebar-section-title' data-test-dependencies-heading>
Depends On
</h3>
<FieldContainer @label='' @vertical={{true}}>
<@fields.blockedBy />
</FieldContainer>
Expand Down Expand Up @@ -1097,6 +1105,35 @@ export class Issue extends CardDef {
});

static fitted = class Fitted extends Component<typeof Issue> {
// Dependencies normally sequence work. Use the blocked warning state only
// when a direct dependency is itself in the blocked status.
get blockedDependencies() {
return (this.args.model?.blockedBy ?? []).filter(
(dependency) => dependency?.status === 'blocked',
);
}

get hasBlockedDependency(): boolean {
return this.blockedDependencies.length > 0;
}

get dependencyLabel(): string {
let dependencies = this.args.model?.blockedBy ?? [];
let issueIds = (list: typeof dependencies): string =>
list
.map((dependency) => dependency?.issueId)
.filter(Boolean)
.join(', ');

if (this.hasBlockedDependency) {
return `Blocked by ${
issueIds(this.blockedDependencies) || this.blockedDependencies.length
}`;
}

return `Depends on ${issueIds(dependencies) || dependencies.length}`;
}

get statusColor(): string | undefined {
return getIssueStatusColor(this.args.model, this.args.model?.status);
}
Expand Down Expand Up @@ -1144,9 +1181,29 @@ export class Issue extends CardDef {
</div>
{{/if}}
{{#if @model.blockedBy.length}}
<div class='meta-link-item meta-blocked-by'>
<CircleAlert class='meta-link-icon' />
<span>Blocked by {{@model.blockedBy.length}}</span>
<div
class='meta-link-item meta-depends-on
{{if this.hasBlockedDependency "has-blocked-dependency"}}'
title={{if
this.hasBlockedDependency
'A dependency is blocked — this issue cannot proceed until it unblocks'
}}
>
{{#if this.hasBlockedDependency}}
<CircleAlert class='meta-link-icon' />
{{else}}
<Link2 class='meta-link-icon' />
{{/if}}
<span data-test-dependency-label={{@model.issueId}}>
{{this.dependencyLabel}}
</span>
{{#if this.hasBlockedDependency}}
<span
class='blocked-dependency-dot'
aria-hidden='true'
data-test-blocked-dependency-dot={{@model.issueId}}
></span>
{{/if}}
</div>
{{/if}}
{{#if @model.relatedKnowledge.length}}
Expand Down Expand Up @@ -1309,6 +1366,18 @@ export class Issue extends CardDef {
color: var(--muted-foreground, var(--boxel-500));
overflow: hidden;
}
.meta-depends-on.has-blocked-dependency {
color: var(--destructive, var(--boxel-danger));
}
.blocked-dependency-dot {
display: inline-block;
width: 0.375rem;
height: 0.375rem;
border-radius: 50%;
background: var(--destructive, var(--boxel-danger));
margin-left: 0.125rem;
flex-shrink: 0;
}
.meta-link-icon {
width: 0.75rem;
height: 0.75rem;
Expand Down
129 changes: 127 additions & 2 deletions packages/software-factory/realm/issue-tracker.test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,19 @@ async function selectGroupBy(key: string) {

function makeIssueWithFields(
issueId: string,
attrs: Record<string, string | null | undefined>,
attrs: Record<string, unknown>,
filename: string,
extraRelationships: Record<string, unknown> = {},
): Record<string, Record<string, unknown>> {
return {
[filename]: {
data: {
type: 'card',
attributes: { issueId, summary: `${issueId} issue`, ...attrs },
relationships: { project: { links: { self: projectId } } },
relationships: {
project: { links: { self: projectId } },
...extraRelationships,
},
meta: { adoptsFrom: { module: issueTrackerModule, name: 'Issue' } },
},
},
Expand Down Expand Up @@ -130,6 +134,127 @@ function makeBoard(
}

export function runTests() {
module('Issue Tracker | issue formats', function (hooks) {
setupApplicationTest(hooks);
setupLocalIndexing(hooks);
setupOnSave(hooks);

let mockMatrixUtils = setupMockMatrix(hooks, {
loggedInAs: '@testuser:localhost',
activeRealms: [testRealmURL],
});

hooks.beforeEach(async function () {
await setupAcceptanceTestRealm({
realmURL: testRealmURL,
mockMatrixUtils,
contents: {
...SYSTEM_CARD_FIXTURE_CONTENTS,
...makeProject(),
...makeIssueWithFields(
'IT-1',
{ status: 'done' },
'Issues/done-dependency.json',
),
...makeIssueWithFields(
'IT-2',
{
status: 'in_progress',
comments: [
{
author: 'Test User',
body: 'A visible comment by default.',
},
],
},
'Issues/sequenced-issue.json',
{
'blockedBy.0': {
links: { self: `${testRealmURL}Issues/done-dependency` },
},
},
),
...makeIssueWithFields(
'IT-3',
{ status: 'blocked' },
'Issues/blocked-dependency.json',
),
...makeIssueWithFields(
'IT-4',
{ status: 'backlog' },
'Issues/blocked-consumer.json',
{
'blockedBy.0': {
links: { self: `${testRealmURL}Issues/blocked-dependency` },
},
},
),
...makeBoard(),
},
});
});

test('comments and dependency details are expanded and labeled clearly by default', async function (assert) {
let issueId = `${testRealmURL}Issues/sequenced-issue`;

await visitOperatorMode({
stacks: [[{ id: issueId, format: 'isolated' }]],
});
await waitFor('#comments');

assert
.dom('#comments')
.hasAttribute('aria-expanded', 'true', 'isolated comments are open');
assert
.dom('#section-comments')
.hasAttribute('aria-hidden', 'false', 'isolated comments are visible');
assert
.dom('[data-test-dependencies-heading]')
.hasText('Depends On', 'isolated view describes normal dependencies');

await visitOperatorMode({
stacks: [[{ id: issueId, format: 'edit' }]],
});
await waitFor('[data-test-issue-edit]');

assert
.dom('#comments')
.hasAttribute('aria-expanded', 'true', 'edit comments are open');
assert
.dom('#section-comments')
.hasAttribute('aria-hidden', 'false', 'edit comments are visible');
assert
.dom('[data-test-dependencies-heading]')
.hasText('Depends On', 'edit view describes normal dependencies');
});

test('fitted cards distinguish dependencies from blocked dependencies', async function (assert) {
await visitOperatorMode({
stacks: [[{ id: boardId, format: 'isolated' }]],
});
await waitFor('[data-test-dependency-label="IT-4"]');

assert
.dom('[data-test-dependency-label="IT-2"]')
.hasText(
'Depends on IT-1',
'completed dependency is normal sequencing',
);
assert
.dom('[data-test-blocked-dependency-dot="IT-2"]')
.doesNotExist('normal dependency has no blocked warning');
assert
.dom('[data-test-dependency-label="IT-4"]')
.hasText(
'Blocked by IT-3',
'a directly blocked dependency prevents the issue from proceeding',
);
assert
.dom('[data-test-blocked-dependency-dot="IT-4"]')
.exists('blocked dependency has a warning indicator');
});
});

module('Issue Tracker | board interactions', function (hooks) {
setupApplicationTest(hooks);
setupLocalIndexing(hooks);
Expand Down