import Plane from './plane.js' /** * @template Event * @template Action * @template EventData * @template Field * @template FieldData * @typedef * {{event: Event, action: Action, data: EventData, activity: {field: Field} & FieldData}} * GenericEvent */ /** * @typedef * {{ "id": string, "cycle": { "id": string, "name": string, "start_date": string, "end_date": string, "created_by": string, "updated_by": string, "project": string, "workspace": string, "owned_by": string }, "labels": Array, "assignees": Array, "state": { "id": string, "name": string, "color": string, "group": import('./plane.js').StateGroup }, "estimate_point": number, "name": string, "sequence_id": number, "description": unknown, "description_html": string, "description_stripped": string, "priority": string, "start_date": unknown, "target_date": unknown, "created_by": string, "updated_by": string, "project": string, "workspace": string, "parent": string | null }} * IssueEventData */ /** @typedef {{ "id": string, "first_name": string, "last_name": string, "email": string, "avatar": string, "display_name": string }} EventActor */ /** @typedef {GenericEvent<"issue", "created", IssueEventData, null, {actor: EventActor}>} EventIssueCreated */ /** @template Field @typedef {GenericEvent<"issue", "created", IssueEventData, Field, {actor: EventActor}>} EventIssueUpdated */ /** * @typedef * {EventIssueCreated | EventIssueUpdated<"description"> | GenericEvent} * Event */ /** @type {(e: Event) => e is GenericEvent<"issue", unknown, IssueEventData, unknown, {actor: EventActor}>} */ export const isIssue = e => e.event === 'issue' /** @type {(e: Event) => e is EventIssueCreated} */ export const isIssueCreated = e => e.event === 'issue' && e.action === 'created' && e.activity.field === null /** @type {(e: Event) => e is EventIssueUpdated<"description">} */ export const isIssueDescriptionUpdated = e => e.event === 'issue' && typeof e.action === 'string' && ['created', 'updated'].includes(e.action) && e.activity.field === 'description' /** @type {(e: Event) => e is EventIssueUpdated<"estimate_point">} */ export const isIssueEstimateUpdated = e => e.event === 'issue' && typeof e.action === 'string' && ['created', 'updated'].includes(e.action) && e.activity.field === 'estimate_point' /** @type {(e: Event) => e is EventIssueUpdated<"state">} */ export const isIssueStateUpdated = e => e.event === 'issue' && e.action === 'updated' && e.activity.field === 'state' /** @type {(e: Event) => e is EventIssueUpdated<"name">} */ export const isIssueTitleUpdated = e => e.event === 'issue' && e.action === 'updated' && e.activity.field === 'name' /** @type {(c: import('./plane.js').Config, p: string, i: string) => string} */ export const issueURL = (c, project, issue) => c.baseURI + '/' + c.workspace + '/projects/' + project + '/issues/' + issue /** @type {(plane: import('./plane.js').Config, e: Event) => Promise<{content: string, topic: string}>} */ export default async (plane, ev) => { if (isIssue(ev)) { const user = `\`${ev.activity.actor.display_name}\`` const description = ev.data.description_stripped.trim() ? `> ${ev.data.description_stripped.trim()}` : '' const parent = await (async () => { if (!ev.data.parent) { return null } return Plane.Issue.get(plane, ev.data.project, ev.data.parent) })() const subIssues = await (async () => { const id = (parent || ev.data).id const allIssues = await Plane.Issue.getAll(plane, ev.data.project) return allIssues.filter(i => i.parent === id) })() /** @type {(_: {sequence_id: number, name: string, project: string, id: string}) => string} */ const link = ({ sequence_id, name, project, id }) => `**[CORE-${sequence_id}](${issueURL(plane, project, id)}) ${name}**` const thisLink = link(ev.data) const title = parent ? link(parent) + '\n⤷ ' + link(ev.data) : link(ev.data) const topic = parent ? 'CORE-' + parent.sequence_id : 'CORE-' + ev.data.sequence_id /** @type {(g: import('./plane.js').StateGroup) => string} */ const stateEmoji = s => s === 'completed' ? ':check:' : s === 'started' ? ':yellow_large_square:' : ':white_large_square:' const state = `${stateEmoji(ev.data.state.group)} ${ev.data.state.name}` const subIssueSummary = await (async () => { const states = await Plane.State.getAll(plane, ev.data.project) /** @type {Record}*/ const stateMap = {} states.forEach(s => { stateMap[s.id] = s }) /** @type {(s: import('./plane.js').StateGroup) => Array} */ const by_state = state => subIssues.flatMap(i => { const s = stateMap[i.state] if (!s) return [] if (s.group !== state) return [] if (i.id === ev.data.id) return [{ ...ev.data, state: ev.data.state.id }] return [i] }) return ( '**Sub-issues**\n' + by_state('completed') .concat(by_state('started')) .concat(by_state('unstarted')) .concat(by_state('backlog')) .map(i => ' - ' + stateEmoji(stateMap[i.state].group) + ' ' + link(i)) .join('\n') ) })() /** @type {Record} */ const estimateMap = { 0: '**XS** (Extra Small)', 1: '**S** (Small)', 2: '**M** (Medium)', 3: '**L** (Large)', 4: '**XL** (Extra Large)', } if (isIssueCreated(ev) && !!parent) { return { topic, content: `${title}\n${user} created sub-issue ${thisLink}\n${subIssueSummary}`, } } else if (isIssueCreated(ev)) { return { topic, content: `${title}\n${user} created issue` } } else if (isIssueEstimateUpdated(ev)) { return { topic, content: `${title}\n${user} set estimate to ${estimateMap[ev.data.estimate_point.toString()]}`, } } else if (isIssueTitleUpdated(ev)) { return { topic, content: `${title}\n${user} updated title to ${ev.data.name}`, } } else if (isIssueDescriptionUpdated(ev)) { return { topic, content: `${title}\n${user} updated description\n${description}`, } } else if (isIssueStateUpdated(ev) && !!parent) { return { topic, content: `${title}\n${user} changed ${thisLink}'s state to ${state}\n${subIssueSummary}`, } } else if (isIssueStateUpdated(ev)) { return { topic, content: `${title}\n${user} changed state to ${state}` } } } return { topic: 'unknown', content: '```json\n' + JSON.stringify(ev, null, 2) + '\n```', } }