Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement implied work issue management #59

Merged
merged 6 commits into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/handlers/work/issues/_lib/add-lib.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { claimIssues, verifyIssuesAvailable } from '@liquid-labs/github-toolkit'
import { httpSmartResponse } from '@liquid-labs/http-smart-response'
import { CredentialsDB, purposes } from '@liquid-labs/liq-credentials-db'
import { Octocache } from '@liquid-labs/octocache'

import { commonAssignParameters } from '../../_lib/common-assign-parameters'
import { WorkDB } from '../../_lib/work-db'

const doAddIssues = async({ app, cache, reporter, req, res, workKey }) => {
reporter = reporter.isolate()

let { assignee, comment, issues, noAutoAssign } = req.vars

const credDB = new CredentialsDB({ app, cache, reporter })
const authToken = credDB.getToken(purposes.GITHUB_API)
const workDB = new WorkDB({ app, authToken, reporter })

// normalize the issue spec; add default project to issues
const workData = workDB.requireData(workKey)
// normalize the issue references
const primaryProject = workData.projects[0].name
issues = issues.map((i) => i.match(/^\d+$/) ? primaryProject + '/' + i : i)

await verifyIssuesAvailable({ authToken, issues, noAutoAssign, notClosed : true, reporter })
await claimIssues({ assignee, authToken, comment, issues, reporter })

const updatedWorkData = await workDB.addIssues({ issues, workKey })

httpSmartResponse({
data : updatedWorkData,
msg : `Added '<em>${issues.join("<rst>', '<em>")}<rst>' to unit of work '<em>${workKey}<rst>'.`,
req,
res
})
}

const getIssuesAddEndpointParameters = ({ workDesc }) => {
const help = {
name : 'Work issues add',
summary : `Add issues to the ${workDesc} unit of work.`,
description : `Adds one or more issues to the ${workDesc} unit of work.`
}

const method = 'put'

const parameters = [...commonAssignParameters()]

parameters.find((p) => p.name === 'issues').optionsFunc = issueOptionsFunc
Object.freeze(parameters)

return { help, method, parameters }
}

const issueOptionsFunc = async({ app, cache, workKey }) => {
const credDB = new CredentialsDB({ app, cache })
const authToken = credDB.getToken(purposes.GITHUB_API)
const octocache = new Octocache({ authToken })

const workDB = new WorkDB({ app })

const projects = workDB.getData(workKey).projects?.map((p) => p.name)
if (projects === undefined) return []

const options = []
for (const project of projects) {
const issues = await octocache.paginate(`GET /repos/${project}/issues`, { state : 'open' })
// TODO: use constant for 'assigned'
options.push(...issues
.filter((i) => !i.labels.some((l) => l.name === 'assigned'))
// eslint-disable-next-line prefer-regex-literals
.map((i) => i.url.replace(new RegExp('.+/repos/([^/]+)/([^/]+)/issues/(\\d+).*'), '$1/$2/$3'))
)
}

return options
}

export { doAddIssues, getIssuesAddEndpointParameters }
67 changes: 67 additions & 0 deletions src/handlers/work/issues/_lib/list-lib.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { commonOutputParams, formatOutput } from '@liquid-labs/liq-handlers-lib'
import { tryExec } from '@liquid-labs/shell-toolkit'

import { WorkDB } from '../../_lib/work-db'

const getIssuesListEndpointParameters = ({ workDesc }) => {
const parameters = [
{
name : 'browseEach',
isBoolean : true,
description : 'Will attempt to open a browser window for each issues in the list.'
},
...commonOutputParams()
]
Object.freeze(parameters)

return {
help : {
name : 'Work projects list',
summary : `List the ${workDesc} work issues.`,
description : `Lists the issues associated with the ${workDesc} unit of work.`
},
method : 'get',
parameters
}
}

const allFields = ['name', 'private']
const defaultFields = allFields

const mdFormatter = (issues, title) => `# ${title}\n\n${issues.map((i) => `- __${i.name}__:\n - private: ${i.private}`).join('\n')}\n`

const terminalFormatter = (issues) => issues.map((i) => `<em>${i.name}<rst>:\n - private: <code>${i.private}<rst>`).join('\n')

const textFormatter = (issues) => issues.map((i) => `${i.name}:\n - private: ${i.private}`).join('\n')

const doListIssues = async({ app, cache, reporter, req, res, workKey }) => {
reporter = reporter.isolate()

const { browseEach = false } = req.vars

const workDB = new WorkDB({ app })
const workData = await workDB.getData(workKey)

if (browseEach === true) {
for (const issue of workData.projects) {
const projectFQN = issue.name
tryExec(`open 'https://github.com/${projectFQN}'`)
}
}

formatOutput({
basicTitle : `${workKey} Projects`,
data : workData.projects,
allFields,
defaultFields,
mdFormatter,
terminalFormatter,
textFormatter,
reporter,
req,
res,
...req.vars
})
}

export { doListIssues, getIssuesListEndpointParameters }
77 changes: 77 additions & 0 deletions src/handlers/work/issues/_lib/remove-lib.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import createError from 'http-errors'

import { releaseIssues } from '@liquid-labs/github-toolkit'
import { httpSmartResponse } from '@liquid-labs/http-smart-response'
import { CredentialsDB, purposes } from '@liquid-labs/liq-credentials-db'

import { commonIssuesParameters } from '../../_lib/common-issues-parameters'
import { WorkDB } from '../../_lib/work-db'

const doRemoveIssues = async({ app, cache, reporter, req, res, workKey }) => {
reporter = reporter.isolate()

let { comment, issues, noUnassign, noUnlabel } = req.vars

const workDB = new WorkDB({ app, reporter })
const workData = workDB.getData(workKey)
if (workData === undefined) {
throw createError.NotFound(`No such active unit of work '${workKey}'.`)
}

// normalize the issue spec; add default project to issues
const primaryProject = workData.projects[0].name
issues = issues.map((i) => i.match(/^\d+$/) ? primaryProject + '/' + i : i)

const credDB = new CredentialsDB({ app, cache, reporter })
const authToken = credDB.getToken(purposes.GITHUB_API)

await releaseIssues({ authToken, comment, issues, noUnassign, noUnlabel, reporter })

const updatedWorkData = workDB.removeIssues({ workKey, issues })

httpSmartResponse({
data : updatedWorkData,
msg : `Removed issues '<em>${issues.join("<rst>', '<em>")}<rst>' from unit of work '<em>${workKey}<rst>'.`,
req,
res
})
}

const getIssuesRemoveEndpointParameters = ({ workDesc }) => {
const parameters = [
{
name : 'comment',
description : 'Comment to add to the issues as they are removed. A default comment will be generated if none is provided. Pass an empty string to suppress leaving a comment.'
},
{
name : 'noUnassign',
isBoolean : true,
description : 'Setting `noUnassign` to true maintains the issue assignments rather than the default behavior of unassigning the issue.'
},
{
name : 'noUnlabel',
isBoolean : true,
description : "Setting `noUnlabel` to true keeps the 'claim label' on the issue rather than the default behavior of removing it."
},
...commonIssuesParameters
]
parameters.find((p) => p.name === 'issues').optionsFunc = issueOptionsFunc
Object.freeze(parameters)

return {
help : {
name : 'Work issues remove',
summary : `Remove issues from the ${workDesc} unit of work.`,
description : `Removes issues from the ${workDesc} unit of work.`
},
method : 'delete',
parameters
}
}

const issueOptionsFunc = ({ app, workKey }) => {
const workDB = new WorkDB({ app })
return workDB.getIssueKeys(workKey)
}

export { doRemoveIssues, getIssuesRemoveEndpointParameters }
22 changes: 22 additions & 0 deletions src/handlers/work/issues/add-implied.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import createError from 'http-errors'

import { determineCurrentBranch } from '@liquid-labs/git-toolkit'

import { doAddIssues, getIssuesAddEndpointParameters } from './_lib/add-lib'

const { help, method, parameters } = getIssuesAddEndpointParameters({ workDesc : 'current' })

const path = ['work', 'issues', 'add']

const func = ({ app, cache, model, reporter }) => async(req, res) => {
const currDir = req.get('X-CWD')
if (currDir === undefined) {
throw createError.BadRequest('Called \'work issues add\' with implied work, but \'X-CWD\' header not found.')
}

const workKey = await determineCurrentBranch({ projectPath : currDir, reporter })

await doAddIssues({ app, cache, reporter, req, res, workKey })
}

export { func, help, method, parameters, path }
76 changes: 5 additions & 71 deletions src/handlers/work/issues/add.js
Original file line number Diff line number Diff line change
@@ -1,79 +1,13 @@
import createError from 'http-errors'
import { doAddIssues, getIssuesAddEndpointParameters } from './_lib/add-lib'

import { claimIssues, verifyIssuesAvailable } from '@liquid-labs/github-toolkit'
import { httpSmartResponse } from '@liquid-labs/http-smart-response'
import { CredentialsDB, purposes } from '@liquid-labs/liq-credentials-db'
import { Octocache } from '@liquid-labs/octocache'
const { help, method, parameters } = getIssuesAddEndpointParameters({ workDesc : 'named' })

import { commonAssignParameters } from '../_lib/common-assign-parameters'
import { WorkDB } from '../_lib/work-db'

const help = {
name : 'Work issues add',
summary : 'Add issues to a unit of work.',
description : 'Adds one or more issues to a unit of work.'
}

const method = 'put'
const path = ['work', ':workKey', 'issues', 'add']

const parameters = [
...commonAssignParameters()
]
const issueOptionsFunc = async({ app, cache, workKey }) => {
const credDB = new CredentialsDB({ app, cache })
const authToken = credDB.getToken(purposes.GITHUB_API)
const octocache = new Octocache({ authToken })

const workDB = new WorkDB({ app })

const projects = workDB.getData(workKey).projects?.map((p) => p.name)
if (projects === undefined) return []

const options = []
for (const project of projects) {
const issues = await octocache.paginate(`GET /repos/${project}/issues`, { state : 'open' })
// TODO: use constant for 'assigned'
options.push(...issues
.filter((i) => !i.labels.some((l) => l.name === 'assigned'))
// eslint-disable-next-line prefer-regex-literals
.map((i) => i.url.replace(new RegExp('.+/repos/([^/]+)/([^/]+)/issues/(\\d+).*'), '$1/$2/$3'))
)
}

return options
}
parameters.find((p) => p.name === 'issues').optionsFunc = issueOptionsFunc
Object.freeze(parameters)

const func = ({ app, cache, model, reporter }) => async(req, res) => {
reporter = reporter.isolate()

let { assignee, comment, issues, noAutoAssign, workKey } = req.vars

const credDB = new CredentialsDB({ app, cache, reporter })
const authToken = credDB.getToken(purposes.GITHUB_API)
const workDB = new WorkDB({ app, authToken, reporter })

// normalize the issue spec; add default project to issues
const workData = workDB.getData(workKey)
if (workData === undefined) {
throw createError.NotFound(`No such active unit of work '${workKey}'.`)
}
const primaryProject = workData.projects[0].name
issues = issues.map((i) => i.match(/^\d+$/) ? primaryProject + '/' + i : i)

await verifyIssuesAvailable({ authToken, issues, noAutoAssign, notClosed : true, reporter })
await claimIssues({ assignee, authToken, comment, issues, reporter })

const updatedWorkData = await workDB.addIssues({ issues, workKey })
const { workKey } = req.vars

httpSmartResponse({
data : updatedWorkData,
msg : `Added '<em>${issues.join("<rst>', '<em>")}<rst>' to unit of work '<em>${workKey}<rst>'.`,
req,
res
})
await doAddIssues({ app, cache, reporter, req, res, workKey })
}

export { func, help, parameters, path, method }
export { func, help, method, parameters, path }
5 changes: 4 additions & 1 deletion src/handlers/work/issues/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import * as addHandler from './add'
import * as addImpliedHandler from './add-implied'
import * as listHandler from './list'
import * as listImpliedHandler from './list-implied'
import * as removeHandler from './remove'
import * as removeImpliedHandler from './remove-implied'

const handlers = [addHandler, listHandler, removeHandler]
const handlers = [addHandler, addImpliedHandler, listHandler, listImpliedHandler, removeHandler, removeImpliedHandler]

export { handlers }
22 changes: 22 additions & 0 deletions src/handlers/work/issues/list-implied.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import createError from 'http-errors'

import { determineCurrentBranch } from '@liquid-labs/git-toolkit'

import { doListIssues, getIssuesListEndpointParameters } from './_lib/list-lib'

const { help, method, parameters } = getIssuesListEndpointParameters({ workDesc : 'current' })

const path = ['work', 'issues', 'list']

const func = ({ app, cache, model, reporter }) => async(req, res) => {
const currDir = req.get('X-CWD')
if (currDir === undefined) {
throw createError.BadRequest('Called \'work issues list\' with implied work, but \'X-CWD\' header not found.')
}

const workKey = await determineCurrentBranch({ projectPath : currDir, reporter })

await doListIssues({ app, cache, reporter, req, res, workKey })
}

export { func, help, method, parameters, path }
Loading