229 lines
6.9 KiB
JavaScript
229 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 {(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 }) =>
|
||
|
`**[ATH-${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
|
||
|
? 'ATH-' + parent.sequence_id
|
||
|
: 'ATH-' + 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')
|
||
|
.map(i => stateEmoji(stateMap[i.state].group) + ' ' + link(i))
|
||
|
.concat(
|
||
|
by_state('started').map(
|
||
|
i => stateEmoji(stateMap[i.state].group) + ' ' + link(i),
|
||
|
),
|
||
|
)
|
||
|
.concat(
|
||
|
by_state('unstarted').map(
|
||
|
i => stateEmoji(stateMap[i.state].group) + ' ' + link(i),
|
||
|
),
|
||
|
)
|
||
|
.concat(
|
||
|
by_state('backlog').map(
|
||
|
i => stateEmoji(stateMap[i.state].group) + ' ' + link(i),
|
||
|
),
|
||
|
)
|
||
|
.map(l => ' - ' + l)
|
||
|
.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 (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```',
|
||
|
}
|
||
|
}
|