zulip-plane/format.js

225 lines
6.9 KiB
JavaScript

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<unknown>,
"assignees": Array<string>,
"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<unknown, unknown, unknown, unknown, unknown>}
* 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<string, import('./plane.js').State>}*/
const stateMap = {}
states.forEach(s => {
stateMap[s.id] = s
})
/** @type {(s: import('./plane.js').StateGroup) => Array<import('./plane.js').Issue>} */
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<string, string>} */
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```',
}
}