import { CommentAddedEvent, CommentDeletedEvent, CommentEditedEvent, IssueAssignedEvent, IssueAttachmentAddedEvent, IssueAttachmentRemovedEvent, IssueChecklistItemAddedEvent, IssueChecklistItemRemovedEvent, IssueChecklistItemUpdatedEvent, IssueParentChangedEvent, IssueReviewAnsweredEvent, IssueReviewCancelledEvent, IssueReviewRequestedEvent, ReviewVerdict, IssueDevLinkRecordedEvent, IssueTrashedEvent, IssueRestoredEvent, IssueCreatedEvent, IssueLinkedEvent, IssueLinkType, IssueReleaseChangedEvent, IssueStatusChangedEvent, IssueUnlinkedEvent, IssueUpdatedEvent, ProjectCreatedEvent, ProjectMemberAddedEvent, ProjectMemberRemovedEvent, ProjectMemberRoleChangedEvent, ProjectRole, ProjectSettingsChangedEvent, ProjectUpdatedEvent, ProjectArchivedEvent, ProjectUnarchivedEvent, ProjectWorkflow, ReleaseArchivedEvent, ReleaseNotesWrittenEvent, ReleaseCreatedEvent, ReleaseShippedEvent, ReleaseUpdatedEvent, IssueSprintChangedEvent, SprintClosedEvent, SprintCreatedEvent, SprintDeletedEvent, SprintStartedEvent, SprintUpdatedEvent, TrackerEvent, } from '@task-stream/api/tracker/domain'; export interface SummaryContext { actor: string; user: (id: string | null) => string; issueKey: string; projectKey: string; releaseName: string; /** '' when unknown (e.g. the sprint was deleted). */ sprintName: string; /** The project's workflow: names of statuses and types. */ workflow: ProjectWorkflow; } /* The activity feed is read by people: Spanish, with the same labels as the web app. */ /** Default types keep their lowercase wording in sentences ("creó WEB-3 (tarea)"). */ const DEFAULT_TYPE_WORDING: Record = { BUG: 'bug', FEATURE: 'feature', UI_CHANGE: 'cambio de UI', TASK: 'tarea', QA: 'QA', }; const LINK: Record = { [IssueLinkType.BLOCKS]: 'bloquea a', [IssueLinkType.DEPENDS_ON]: 'depende de', [IssueLinkType.TESTS]: 'prueba', [IssueLinkType.DUPLICATES]: 'duplica a', [IssueLinkType.RELATES_TO]: 'está relacionado con', }; const ROLE: Record = { [ProjectRole.OWNER]: 'propietario', [ProjectRole.ADMIN]: 'admin', [ProjectRole.MEMBER]: 'miembro', [ProjectRole.VIEWER]: 'lector', [ProjectRole.GUEST]: 'invitado', }; const FIELD: Record = { title: 'el título', description: 'la descripción', type: 'el tipo', area: 'el área', team: 'el equipo', priority: 'la prioridad', labels: 'las etiquetas', figmaUrl: 'el enlace de Figma', storyPoints: 'los puntos', startDate: 'la fecha de inicio', dueDate: 'la fecha límite', customFields: 'los campos personalizados', }; const list = (items: string[]) => items.length <= 1 ? items.join('') : `${items.slice(0, -1).join(', ')} y ${items[items.length - 1]}`; /** One human-readable line per event, for the project activity feed. */ export function describeEvent( event: TrackerEvent, ctx: SummaryContext, ): string { const { actor, issueKey } = ctx; if (event instanceof ProjectCreatedEvent) return `${actor} creó el proyecto ${event.project.key}`; if (event instanceof ProjectUpdatedEvent) return `${actor} actualizó el proyecto ${ctx.projectKey}`; if (event instanceof ProjectArchivedEvent) return `${actor} archivó el proyecto ${ctx.projectKey}`; if (event instanceof ProjectUnarchivedEvent) return `${actor} desarchivó el proyecto ${ctx.projectKey}`; if (event instanceof ProjectSettingsChangedEvent) return `${actor} cambió los estados, tipos o áreas del proyecto ${ctx.projectKey}`; if (event instanceof ProjectMemberAddedEvent) { return `${actor} añadió a ${ctx.user(event.userId)} como ${ROLE[event.role]}`; } if (event instanceof ProjectMemberRemovedEvent) { return event.userId === event.actorId ? `${actor} salió del proyecto` : `${actor} quitó a ${ctx.user(event.userId)} del proyecto`; } if (event instanceof ProjectMemberRoleChangedEvent) { return `${actor} cambió el rol de ${ctx.user(event.userId)} a ${ROLE[event.role]}`; } if (event instanceof IssueCreatedEvent) { const type = DEFAULT_TYPE_WORDING[event.issue.type] ?? ctx.workflow.typeName(event.issue.type); return `${actor} creó ${event.issue.key} (${type}): ${event.issue.title}`; } if (event instanceof IssueUpdatedEvent) { return `${actor} cambió ${list(Object.keys(event.changes).map((f) => FIELD[f] ?? f))} de ${issueKey}`; } if (event instanceof IssueStatusChangedEvent) { // Names as they were then (old events: as configured now). const from = event.meta?.fromName ?? ctx.workflow.statusName(event.from); const to = event.meta?.toName ?? ctx.workflow.statusName(event.to); return `${actor} movió ${issueKey} de ${from} a ${to}${event.reason ? `: ${event.reason}` : ''}`; } if (event instanceof IssueAssignedEvent) { return event.assigneeId ? `${actor} asignó ${issueKey} a ${ctx.user(event.assigneeId)}` : `${actor} dejó ${issueKey} sin asignar`; } if (event instanceof IssueLinkedEvent) { return `${actor} indicó que ${issueKey} ${LINK[event.link.type]} ${event.link.targetKey}`; } if (event instanceof IssueUnlinkedEvent) { return `${actor} quitó la relación entre ${issueKey} y ${event.link.targetKey}`; } if (event instanceof IssueReleaseChangedEvent) { return event.releaseId ? `${actor} metió ${issueKey} en la release ${ctx.releaseName}` : `${actor} sacó ${issueKey} de su release`; } if (event instanceof IssueAttachmentAddedEvent) return `${actor} adjuntó ${event.attachment.fileName} a ${issueKey}`; if (event instanceof IssueAttachmentRemovedEvent) return `${actor} quitó el adjunto ${event.fileName} de ${issueKey}`; if (event instanceof IssueSprintChangedEvent) { const sprint = ctx.sprintName ? `el sprint ${ctx.sprintName}` : 'un sprint'; return event.sprintId ? `${actor} metió ${issueKey} en ${sprint}` : `${actor} sacó ${issueKey} de ${sprint}`; } if (event instanceof IssueTrashedEvent) return `${actor} movió ${issueKey} a la papelera`; if (event instanceof IssueRestoredEvent) return `${actor} recuperó ${issueKey} de la papelera`; if (event instanceof IssueDevLinkRecordedEvent) { const { kind, ref, state, author } = event.link; const who = author ? ` (${author})` : ''; if (kind === 'COMMIT') return `Commit ${ref} sobre ${issueKey}${who}`; if (kind === 'BRANCH') return `Rama ${ref} para ${issueKey}${who}`; const verb = state === 'MERGED' ? 'fusionada' : state === 'CLOSED' ? 'cerrada' : 'abierta'; return `Pull request ${ref} ${verb} para ${issueKey}${who}`; } if (event instanceof IssueReviewRequestedEvent) return `${actor} pidió a ${ctx.user(event.reviewerId)} que revise ${issueKey}`; if (event instanceof IssueReviewAnsweredEvent) return event.verdict === ReviewVerdict.APPROVED ? `${actor} dio por bueno ${issueKey} al revisarlo` : `${actor} pidió cambios en ${issueKey} al revisarlo`; if (event instanceof IssueReviewCancelledEvent) return `${actor} retiró una petición de revisión de ${issueKey}`; if (event instanceof IssueParentChangedEvent) { return event.parentKey ? `${actor} colocó ${issueKey} bajo ${event.parentKey}` : `${actor} sacó ${issueKey} de su ticket padre`; } if (event instanceof IssueChecklistItemAddedEvent) return `${actor} añadió «${event.item.text}» a la lista de ${issueKey}`; if (event instanceof IssueChecklistItemUpdatedEvent) { if (event.changes.done === true) return `${actor} completó un paso de la lista de ${issueKey}`; if (event.changes.done === false) return `${actor} reabrió un paso de la lista de ${issueKey}`; return `${actor} cambió un paso de la lista de ${issueKey}`; } if (event instanceof IssueChecklistItemRemovedEvent) return `${actor} quitó «${event.text}» de la lista de ${issueKey}`; if (event instanceof CommentAddedEvent) return event.comment.replyTo ? `${actor} respondió a un comentario en ${issueKey}` : `${actor} comentó en ${issueKey}`; if (event instanceof CommentEditedEvent) return `${actor} editó un comentario en ${issueKey}`; if (event instanceof CommentDeletedEvent) return `${actor} borró un comentario en ${issueKey}`; if (event instanceof ReleaseCreatedEvent) return `${actor} creó la release ${event.release.name}`; if (event instanceof ReleaseUpdatedEvent) return `${actor} actualizó la release ${ctx.releaseName}`; if (event instanceof ReleaseShippedEvent) return `${actor} publicó la release ${ctx.releaseName} 🚀`; if (event instanceof ReleaseNotesWrittenEvent) return event.notes ? `${actor} escribió las notas de la release ${ctx.releaseName}` : `${actor} quitó las notas de la release ${ctx.releaseName}`; if (event instanceof ReleaseArchivedEvent) return `${actor} archivó la release ${ctx.releaseName}`; const sprint = ctx.sprintName || 'un sprint'; if (event instanceof SprintCreatedEvent) return `${actor} planificó el sprint ${event.sprint.name}`; if (event instanceof SprintUpdatedEvent) return `${actor} actualizó el sprint ${sprint}`; if (event instanceof SprintStartedEvent) return `${actor} empezó el sprint ${sprint} 🏁`; if (event instanceof SprintClosedEvent) { const left = event.unfinishedIssues ? `; ${event.unfinishedIssues} ticket(s) sin terminar pasan ${event.carryOverTo ? 'al siguiente sprint' : 'al backlog'}` : ''; return `${actor} cerró el sprint ${sprint}${left}`; } if (event instanceof SprintDeletedEvent) return `${actor} borró el sprint ${sprint}`; return `${actor}: ${event.constructor.name}`; }