first commit
This commit is contained in:
57
libs/web/tracker/domain/src/lib/models/enums.ts
Normal file
57
libs/web/tracker/domain/src/lib/models/enums.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
export enum IssueType {
|
||||
BUG = 'BUG',
|
||||
FEATURE = 'FEATURE',
|
||||
UI_CHANGE = 'UI_CHANGE',
|
||||
TASK = 'TASK',
|
||||
QA = 'QA',
|
||||
}
|
||||
|
||||
export enum IssueArea {
|
||||
FRONTEND = 'FRONTEND',
|
||||
BACKEND = 'BACKEND',
|
||||
MOBILE = 'MOBILE',
|
||||
QA = 'QA',
|
||||
DESIGN = 'DESIGN',
|
||||
DEVOPS = 'DEVOPS',
|
||||
}
|
||||
|
||||
export enum IssuePriority {
|
||||
LOWEST = 'LOWEST',
|
||||
LOW = 'LOW',
|
||||
MEDIUM = 'MEDIUM',
|
||||
HIGH = 'HIGH',
|
||||
CRITICAL = 'CRITICAL',
|
||||
}
|
||||
|
||||
export enum IssueStatus {
|
||||
BACKLOG = 'BACKLOG',
|
||||
TODO = 'TODO',
|
||||
IN_PROGRESS = 'IN_PROGRESS',
|
||||
IN_REVIEW = 'IN_REVIEW',
|
||||
READY_FOR_QA = 'READY_FOR_QA',
|
||||
IN_QA = 'IN_QA',
|
||||
QA_FAILED = 'QA_FAILED',
|
||||
DONE = 'DONE',
|
||||
CANCELLED = 'CANCELLED',
|
||||
}
|
||||
|
||||
export enum IssueLinkType {
|
||||
BLOCKS = 'BLOCKS',
|
||||
DEPENDS_ON = 'DEPENDS_ON',
|
||||
TESTS = 'TESTS',
|
||||
DUPLICATES = 'DUPLICATES',
|
||||
RELATES_TO = 'RELATES_TO',
|
||||
}
|
||||
|
||||
export enum ProjectRole {
|
||||
OWNER = 'OWNER',
|
||||
ADMIN = 'ADMIN',
|
||||
MEMBER = 'MEMBER',
|
||||
VIEWER = 'VIEWER',
|
||||
}
|
||||
|
||||
export enum ReleaseStatus {
|
||||
PLANNED = 'PLANNED',
|
||||
RELEASED = 'RELEASED',
|
||||
ARCHIVED = 'ARCHIVED',
|
||||
}
|
||||
65
libs/web/tracker/domain/src/lib/models/inputs.ts
Normal file
65
libs/web/tracker/domain/src/lib/models/inputs.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
IssueArea,
|
||||
IssueLinkType,
|
||||
IssuePriority,
|
||||
IssueStatus,
|
||||
IssueType,
|
||||
ProjectRole,
|
||||
} from './enums';
|
||||
|
||||
export interface CreateProjectInput {
|
||||
key: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateIssueInput {
|
||||
projectId: string;
|
||||
type: IssueType;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
area?: IssueArea | null;
|
||||
priority?: IssuePriority;
|
||||
labels?: string[];
|
||||
figmaUrl?: string | null;
|
||||
assigneeId?: string | null;
|
||||
releaseId?: string | null;
|
||||
}
|
||||
|
||||
export type UpdateIssueInput = Partial<
|
||||
Omit<CreateIssueInput, 'projectId' | 'assigneeId' | 'releaseId'>
|
||||
> & {
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
export interface ChangeStatusInput {
|
||||
issueId: string;
|
||||
status: IssueStatus;
|
||||
reason?: string | null;
|
||||
}
|
||||
|
||||
export interface LinkIssuesInput {
|
||||
issueId: string;
|
||||
type: IssueLinkType;
|
||||
targetIssueId: string;
|
||||
}
|
||||
|
||||
export interface MemberInput {
|
||||
projectId: string;
|
||||
userId: string;
|
||||
role: ProjectRole;
|
||||
}
|
||||
|
||||
export interface CreateReleaseInput {
|
||||
projectId: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
targetDate?: string | null;
|
||||
}
|
||||
|
||||
export interface BoardFilter {
|
||||
projectId: string;
|
||||
assigneeId?: string;
|
||||
releaseId?: string;
|
||||
area?: IssueArea;
|
||||
}
|
||||
131
libs/web/tracker/domain/src/lib/models/models.ts
Normal file
131
libs/web/tracker/domain/src/lib/models/models.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
IssueArea,
|
||||
IssueLinkType,
|
||||
IssuePriority,
|
||||
IssueStatus,
|
||||
IssueType,
|
||||
ProjectRole,
|
||||
ReleaseStatus,
|
||||
} from './enums';
|
||||
|
||||
export interface UserRef {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ProjectMember {
|
||||
user: UserRef;
|
||||
role: ProjectRole;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
owner: UserRef;
|
||||
members: ProjectMember[];
|
||||
updatedAt: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface IssueLink {
|
||||
type: IssueLinkType;
|
||||
/** OUTWARD: this issue → other. INWARD: other → this issue. */
|
||||
direction: 'OUTWARD' | 'INWARD';
|
||||
issueId: string;
|
||||
issueKey: string;
|
||||
}
|
||||
|
||||
/** What boards and lists show. */
|
||||
export interface IssueSummary {
|
||||
id: string;
|
||||
projectId: string;
|
||||
key: string;
|
||||
type: IssueType;
|
||||
area: IssueArea | null;
|
||||
title: string;
|
||||
status: IssueStatus;
|
||||
priority: IssuePriority;
|
||||
assignee: UserRef | null;
|
||||
release: { id: string; name: string } | null;
|
||||
labels: string[];
|
||||
commentCount: number;
|
||||
updatedAt: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface Issue extends IssueSummary {
|
||||
description: string | null;
|
||||
reporter: UserRef;
|
||||
figmaUrl: string | null;
|
||||
storyPoints: number | null;
|
||||
dueDate: string | null;
|
||||
links: IssueLink[];
|
||||
allowedTransitions: IssueStatus[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
id: string;
|
||||
issueId: string;
|
||||
author: UserRef;
|
||||
body: string;
|
||||
createdAt: string;
|
||||
editedAt: string | null;
|
||||
}
|
||||
|
||||
export interface Release {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
targetDate: string | null;
|
||||
status: ReleaseStatus;
|
||||
releasedAt: string | null;
|
||||
progress: { total: number; finished: number; percent: number };
|
||||
}
|
||||
|
||||
export interface Activity {
|
||||
id: string;
|
||||
type: string;
|
||||
issueId: string | null;
|
||||
issueKey: string | null;
|
||||
actor: UserRef;
|
||||
summary: string;
|
||||
occurredAt: string;
|
||||
}
|
||||
|
||||
export interface CountBy<T = string> {
|
||||
key: T;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ProjectStats {
|
||||
total: number;
|
||||
open: number;
|
||||
byStatus: CountBy<IssueStatus>[];
|
||||
byType: CountBy[];
|
||||
byPriority: CountBy[];
|
||||
byArea: CountBy[];
|
||||
byAssignee: { user: UserRef | null; count: number }[];
|
||||
}
|
||||
|
||||
export interface BoardColumn {
|
||||
status: IssueStatus;
|
||||
issues: IssueSummary[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface UserDirectoryEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
/** Ack of a command: the read model catches up asynchronously. */
|
||||
export interface CommandResult {
|
||||
id: string;
|
||||
key?: string | null;
|
||||
version: number;
|
||||
}
|
||||
50
libs/web/tracker/domain/src/lib/models/workflow.ts
Normal file
50
libs/web/tracker/domain/src/lib/models/workflow.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { IssuePriority, IssueStatus, IssueType } from './enums';
|
||||
|
||||
/** Statuses that require an explanation (the API rejects them without one). */
|
||||
export const REASON_REQUIRED: readonly IssueStatus[] = [
|
||||
IssueStatus.QA_FAILED,
|
||||
IssueStatus.CANCELLED,
|
||||
];
|
||||
|
||||
export const BOARD_COLUMNS: readonly IssueStatus[] = [
|
||||
IssueStatus.BACKLOG,
|
||||
IssueStatus.TODO,
|
||||
IssueStatus.IN_PROGRESS,
|
||||
IssueStatus.IN_REVIEW,
|
||||
IssueStatus.READY_FOR_QA,
|
||||
IssueStatus.IN_QA,
|
||||
IssueStatus.QA_FAILED,
|
||||
IssueStatus.DONE,
|
||||
];
|
||||
|
||||
export const STATUS_LABEL: Record<IssueStatus, string> = {
|
||||
[IssueStatus.BACKLOG]: 'Backlog',
|
||||
[IssueStatus.TODO]: 'To do',
|
||||
[IssueStatus.IN_PROGRESS]: 'In progress',
|
||||
[IssueStatus.IN_REVIEW]: 'In review',
|
||||
[IssueStatus.READY_FOR_QA]: 'Ready for QA',
|
||||
[IssueStatus.IN_QA]: 'In QA',
|
||||
[IssueStatus.QA_FAILED]: 'QA failed',
|
||||
[IssueStatus.DONE]: 'Done',
|
||||
[IssueStatus.CANCELLED]: 'Cancelled',
|
||||
};
|
||||
|
||||
export const TYPE_LABEL: Record<IssueType, string> = {
|
||||
[IssueType.BUG]: 'Bug',
|
||||
[IssueType.FEATURE]: 'Feature',
|
||||
[IssueType.UI_CHANGE]: 'UI change',
|
||||
[IssueType.TASK]: 'Task',
|
||||
[IssueType.QA]: 'QA',
|
||||
};
|
||||
|
||||
export const PRIORITY_ORDER: readonly IssuePriority[] = [
|
||||
IssuePriority.CRITICAL,
|
||||
IssuePriority.HIGH,
|
||||
IssuePriority.MEDIUM,
|
||||
IssuePriority.LOW,
|
||||
IssuePriority.LOWEST,
|
||||
];
|
||||
|
||||
export function isFinished(status: IssueStatus): boolean {
|
||||
return status === IssueStatus.DONE || status === IssueStatus.CANCELLED;
|
||||
}
|
||||
108
libs/web/tracker/domain/src/lib/ports/gateways.ts
Normal file
108
libs/web/tracker/domain/src/lib/ports/gateways.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
Activity,
|
||||
BoardColumn,
|
||||
Comment,
|
||||
CommandResult,
|
||||
Issue,
|
||||
IssueSummary,
|
||||
Project,
|
||||
ProjectStats,
|
||||
Release,
|
||||
UserDirectoryEntry,
|
||||
} from '../models/models';
|
||||
import {
|
||||
BoardFilter,
|
||||
ChangeStatusInput,
|
||||
CreateIssueInput,
|
||||
CreateProjectInput,
|
||||
CreateReleaseInput,
|
||||
LinkIssuesInput,
|
||||
MemberInput,
|
||||
UpdateIssueInput,
|
||||
} from '../models/inputs';
|
||||
|
||||
/*
|
||||
* Outbound ports of the tracker. The application layer depends on these
|
||||
* abstract classes; the infrastructure layer implements them over GraphQL.
|
||||
*/
|
||||
|
||||
export abstract class ProjectsGateway {
|
||||
abstract myProjects(): Observable<Project[]>;
|
||||
abstract project(idOrKey: string): Observable<Project>;
|
||||
abstract stats(projectId: string): Observable<ProjectStats>;
|
||||
abstract create(input: CreateProjectInput): Observable<CommandResult>;
|
||||
abstract addMember(input: MemberInput): Observable<CommandResult>;
|
||||
abstract changeMemberRole(input: MemberInput): Observable<CommandResult>;
|
||||
abstract removeMember(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
): Observable<CommandResult>;
|
||||
}
|
||||
|
||||
export abstract class IssuesGateway {
|
||||
abstract board(filter: BoardFilter): Observable<BoardColumn[]>;
|
||||
abstract issue(idOrKey: string): Observable<Issue>;
|
||||
abstract search(projectId: string, text: string): Observable<IssueSummary[]>;
|
||||
abstract create(input: CreateIssueInput): Observable<CommandResult>;
|
||||
abstract update(input: UpdateIssueInput): Observable<CommandResult>;
|
||||
abstract changeStatus(input: ChangeStatusInput): Observable<CommandResult>;
|
||||
abstract assign(
|
||||
issueId: string,
|
||||
assigneeId: string | null,
|
||||
): Observable<CommandResult>;
|
||||
abstract link(input: LinkIssuesInput): Observable<CommandResult>;
|
||||
abstract unlink(input: LinkIssuesInput): Observable<CommandResult>;
|
||||
abstract setRelease(
|
||||
issueId: string,
|
||||
releaseId: string | null,
|
||||
): Observable<CommandResult>;
|
||||
}
|
||||
|
||||
export abstract class CommentsGateway {
|
||||
abstract list(issueId: string): Observable<Comment[]>;
|
||||
abstract add(issueId: string, body: string): Observable<CommandResult>;
|
||||
abstract edit(commentId: string, body: string): Observable<CommandResult>;
|
||||
abstract remove(commentId: string): Observable<CommandResult>;
|
||||
}
|
||||
|
||||
export abstract class ReleasesGateway {
|
||||
abstract list(projectId: string): Observable<Release[]>;
|
||||
abstract create(input: CreateReleaseInput): Observable<CommandResult>;
|
||||
abstract publish(
|
||||
releaseId: string,
|
||||
force: boolean,
|
||||
): Observable<CommandResult>;
|
||||
abstract archive(releaseId: string): Observable<CommandResult>;
|
||||
}
|
||||
|
||||
export abstract class ActivityGateway {
|
||||
abstract list(projectId: string, issueId?: string): Observable<Activity[]>;
|
||||
}
|
||||
|
||||
export abstract class UsersGateway {
|
||||
abstract search(term: string): Observable<UserDirectoryEntry[]>;
|
||||
}
|
||||
|
||||
/** Realtime (GraphQL subscriptions over WebSocket). */
|
||||
export interface IssueChanged {
|
||||
action: string;
|
||||
version: number;
|
||||
actorId: string;
|
||||
issue: IssueSummary;
|
||||
}
|
||||
|
||||
export interface CommentChanged {
|
||||
action: string;
|
||||
commentId: string;
|
||||
comment: Comment | null;
|
||||
}
|
||||
|
||||
export abstract class TrackerRealtimeGateway {
|
||||
abstract issueChanges(projectId: string): Observable<IssueChanged>;
|
||||
abstract commentChanges(issueId: string): Observable<CommentChanged>;
|
||||
abstract activity(projectId: string): Observable<Activity>;
|
||||
abstract releaseChanges(projectId: string): Observable<Release>;
|
||||
abstract projectChanges(projectId: string): Observable<Project>;
|
||||
abstract myProjectsChanges(): Observable<Project>;
|
||||
}
|
||||
Reference in New Issue
Block a user