feat(web): priority, estimate and assignee change right on the board card
The card's title is now the link (stretched over the card), and the three chips carry an invisible native select each, so they work by keyboard, on a phone and with a screen reader without opening the ticket. Dragging still works from anywhere on the card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,16 @@ import {
|
||||
CommandResult,
|
||||
CreateIssueInput,
|
||||
IssueChanged,
|
||||
IssuePriority,
|
||||
SavedFilter,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
|
||||
/** What a card can change without opening the ticket. */
|
||||
export type CardChange =
|
||||
| { priority: IssuePriority }
|
||||
| { storyPoints: number | null }
|
||||
| { assigneeId: string | null };
|
||||
|
||||
export const boardPageEvents = eventGroup({
|
||||
source: 'Board Page',
|
||||
events: {
|
||||
@@ -21,6 +28,8 @@ export const boardPageEvents = eventGroup({
|
||||
reason?: string | null;
|
||||
}>(),
|
||||
issueCreateRequested: type<CreateIssueInput>(),
|
||||
/** From the card: priority, estimate or assignee (the live update repaints it). */
|
||||
issueEditRequested: type<{ issueId: string; change: CardChange }>(),
|
||||
/** Save the current filter under a name. */
|
||||
viewSaveRequested: type<{ name: string; shared: boolean }>(),
|
||||
viewDeleteRequested: type<{ id: string }>(),
|
||||
@@ -33,6 +42,7 @@ export const boardApiEvents = eventGroup({
|
||||
loaded: type<BoardColumn[]>(),
|
||||
loadFailed: type<string>(),
|
||||
issueMoveFailed: type<string>(),
|
||||
issueEditFailed: type<string>(),
|
||||
issueCreated: type<CommandResult>(),
|
||||
issueCreateFailed: type<string>(),
|
||||
viewsLoaded: type<SavedFilter[]>(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
CreateIssueInput,
|
||||
sameCriteria,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
import { boardPageEvents } from '../events/board.events';
|
||||
import { boardPageEvents, CardChange } from '../events/board.events';
|
||||
import { BoardStore } from '../stores/board.store';
|
||||
|
||||
/** Kanban board of a project, live. */
|
||||
@@ -46,6 +46,11 @@ export class BoardFacade {
|
||||
this.dispatch.issueMoved({ issueId, to, reason });
|
||||
}
|
||||
|
||||
/** A change from the card itself, without opening the ticket. */
|
||||
editIssue(issueId: string, change: CardChange): void {
|
||||
this.dispatch.issueEditRequested({ issueId, change });
|
||||
}
|
||||
|
||||
createIssue(input: CreateIssueInput): void {
|
||||
this.dispatch.issueCreateRequested(input);
|
||||
}
|
||||
@@ -65,3 +70,6 @@ export class BoardFacade {
|
||||
export function provideBoardFacade(): Provider[] {
|
||||
return [BoardStore, BoardFacade];
|
||||
}
|
||||
|
||||
/** What a card can change by itself (see BoardFacade.editIssue). */
|
||||
export type { CardChange } from '../events/board.events';
|
||||
|
||||
@@ -186,6 +186,23 @@ export const BoardStore = signalStore(
|
||||
),
|
||||
),
|
||||
),
|
||||
edit$: events.on(boardPageEvents.issueEditRequested).pipe(
|
||||
mergeMap(({ payload: { issueId, change } }) =>
|
||||
('assigneeId' in change
|
||||
? issues.assign(issueId, change.assigneeId)
|
||||
: issues.update({ issueId, ...change })
|
||||
).pipe(
|
||||
mapResponse({
|
||||
next: () => null,
|
||||
error: (error) =>
|
||||
boardApiEvents.issueEditFailed(describeError(error)),
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
editFailed$: events
|
||||
.on(boardApiEvents.issueEditFailed)
|
||||
.pipe(tap(({ payload }) => notifications.error(payload))),
|
||||
/** The API refused the move: tell the user and put the card back. */
|
||||
rollback$: events.on(boardApiEvents.issueMoveFailed).pipe(
|
||||
tap(({ payload }) => notifications.error(payload)),
|
||||
|
||||
Reference in New Issue
Block a user