Compare commits
8 Commits
8b0c751344
...
4.30.1
| Author | SHA1 | Date | |
|---|---|---|---|
| de07665a8b | |||
| 5dd7a15bf4 | |||
| 6ac068fe75 | |||
| 759d00e2f8 | |||
| bc37640f2e | |||
| 3ee5bdf8fd | |||
| 85d561c8a5 | |||
| 1064de3e1b |
@@ -1,2 +1,2 @@
|
|||||||
[tools]
|
[tools]
|
||||||
nim = "2.2.0"
|
nim = "2.2.6"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "4.29.0"
|
version = "4.30.1"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "Personal issue tracker."
|
description = "Personal issue tracker."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -13,14 +13,13 @@ bin = @["pit"]
|
|||||||
requires @[
|
requires @[
|
||||||
"nim >= 1.4.0",
|
"nim >= 1.4.0",
|
||||||
"docopt >= 0.7.1",
|
"docopt >= 0.7.1",
|
||||||
"jester >= 0.6.0",
|
|
||||||
"uuids >= 0.1.10",
|
"uuids >= 0.1.10",
|
||||||
"zero_functional"
|
"zero_functional"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Dependencies from git.jdb-software.com/jdb/nim-packages
|
# Dependencies from git.jdb-software.com/jdb/nim-packages
|
||||||
requires @[
|
requires @[
|
||||||
"cliutils >= 0.9.1",
|
"cliutils >= 0.10.2",
|
||||||
"langutils >= 0.4.0",
|
"langutils >= 0.4.0",
|
||||||
"timeutils >= 0.5.4",
|
"timeutils >= 0.5.4",
|
||||||
"data_uri > 1.0.0",
|
"data_uri > 1.0.0",
|
||||||
|
|||||||
150
src/pit.nim
150
src/pit.nim
@@ -6,7 +6,7 @@ import data_uri, docopt, json, timeutils, uuids
|
|||||||
|
|
||||||
from nre import re
|
from nre import re
|
||||||
import strutils except alignLeft, capitalize, strip, toUpper, toLower
|
import strutils except alignLeft, capitalize, strip, toUpper, toLower
|
||||||
import pit/[cliconstants, formatting, libpit, sync_pbm_vsb]
|
import pit/[cliconstants, formatting, libpit, projects, sync_pbm_vsb]
|
||||||
|
|
||||||
export formatting, libpit
|
export formatting, libpit
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ proc parseExclPropertiesOption(propsOpt: string): TableRef[string, seq[string]]
|
|||||||
if result.hasKey(pair[0]): result[pair[0]].add(val)
|
if result.hasKey(pair[0]): result[pair[0]].add(val)
|
||||||
else: result[pair[0]] = @[val]
|
else: result[pair[0]] = @[val]
|
||||||
|
|
||||||
|
|
||||||
proc reorder(ctx: CliContext, state: IssueState) =
|
proc reorder(ctx: CliContext, state: IssueState) =
|
||||||
|
|
||||||
# load the issues to make sure the order file contains all issues in the state.
|
# load the issues to make sure the order file contains all issues in the state.
|
||||||
@@ -87,6 +88,10 @@ when isMainModule:
|
|||||||
var tagsOption = none(seq[string])
|
var tagsOption = none(seq[string])
|
||||||
var exclTagsOption = none(seq[string])
|
var exclTagsOption = none(seq[string])
|
||||||
|
|
||||||
|
let filter = initFilter()
|
||||||
|
var filterOption = none(IssueFilter)
|
||||||
|
|
||||||
|
|
||||||
if args["--properties"] or args["--context"]:
|
if args["--properties"] or args["--context"]:
|
||||||
|
|
||||||
var props =
|
var props =
|
||||||
@@ -116,6 +121,54 @@ when isMainModule:
|
|||||||
if args["--excl-tags"]: exclTagsOption =
|
if args["--excl-tags"]: exclTagsOption =
|
||||||
some(($args["--excl-tags"]).split(",").mapIt(it.strip))
|
some(($args["--excl-tags"]).split(",").mapIt(it.strip))
|
||||||
|
|
||||||
|
# Initialize filter with properties (if given)
|
||||||
|
if propertiesOption.isSome:
|
||||||
|
filter.properties = propertiesOption.get
|
||||||
|
filterOption = some(filter)
|
||||||
|
|
||||||
|
# Add property exclusions (if given)
|
||||||
|
if exclPropsOption.isSome:
|
||||||
|
filter.exclProperties = exclPropsOption.get
|
||||||
|
filterOption = some(filter)
|
||||||
|
|
||||||
|
# If they supplied text matches, add that to the filter.
|
||||||
|
if args["--match"]:
|
||||||
|
filter.summaryMatch = some(re("(?i)" & $args["--match"]))
|
||||||
|
filterOption = some(filter)
|
||||||
|
|
||||||
|
if args["--match-all"]:
|
||||||
|
filter.fullMatch = some(re("(?i)" & $args["--match-all"]))
|
||||||
|
filterOption = some(filter)
|
||||||
|
|
||||||
|
# If no "context" property is given, use the default (if we have one)
|
||||||
|
if ctx.defaultContext.isSome and not filter.properties.hasKey("context"):
|
||||||
|
stderr.writeLine("Limiting to default context: " & ctx.defaultContext.get)
|
||||||
|
filter.properties["context"] = ctx.defaultContext.get
|
||||||
|
filterOption = some(filter)
|
||||||
|
|
||||||
|
if tagsOption.isSome:
|
||||||
|
filter.hasTags = tagsOption.get
|
||||||
|
filterOption = some(filter)
|
||||||
|
|
||||||
|
if exclTagsOption.isSome:
|
||||||
|
filter.exclTags = exclTagsOption.get
|
||||||
|
filterOption = some(filter)
|
||||||
|
|
||||||
|
if args["--today"]:
|
||||||
|
filter.inclStates.add(@[Current, TodoToday, Pending])
|
||||||
|
filterOption = some(filter)
|
||||||
|
|
||||||
|
if args["--future"]:
|
||||||
|
filter.inclStates.add(@[Pending, Todo])
|
||||||
|
filterOption = some(filter)
|
||||||
|
|
||||||
|
# Finally, if the "context" is "all", don't filter on context
|
||||||
|
if filter.properties.hasKey("context") and
|
||||||
|
filter.properties["context"] == "all":
|
||||||
|
|
||||||
|
filter.properties.del("context")
|
||||||
|
filter.exclProperties.del("context")
|
||||||
|
|
||||||
## Actual command runners
|
## Actual command runners
|
||||||
if args["new"] or args["add"]:
|
if args["new"] or args["add"]:
|
||||||
let state =
|
let state =
|
||||||
@@ -224,7 +277,6 @@ when isMainModule:
|
|||||||
updatedIssues.add(nextIssue)
|
updatedIssues.add(nextIssue)
|
||||||
stdout.writeLine formatIssue(nextIssue)
|
stdout.writeLine formatIssue(nextIssue)
|
||||||
|
|
||||||
|
|
||||||
issue.changeState(ctx.cfg.tasksDir, targetState)
|
issue.changeState(ctx.cfg.tasksDir, targetState)
|
||||||
updatedIssues.add(issue)
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
@@ -276,55 +328,16 @@ when isMainModule:
|
|||||||
|
|
||||||
elif args["list"]:
|
elif args["list"]:
|
||||||
|
|
||||||
let filter = initFilter()
|
|
||||||
var filterOption = none(IssueFilter)
|
|
||||||
|
|
||||||
# Initialize filter with properties (if given)
|
|
||||||
if propertiesOption.isSome:
|
|
||||||
filter.properties = propertiesOption.get
|
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
# Add property exclusions (if given)
|
|
||||||
if exclPropsOption.isSome:
|
|
||||||
filter.exclProperties = exclPropsOption.get
|
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
# If they supplied text matches, add that to the filter.
|
|
||||||
if args["--match"]:
|
|
||||||
filter.summaryMatch = some(re("(?i)" & $args["--match"]))
|
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
if args["--match-all"]:
|
|
||||||
filter.fullMatch = some(re("(?i)" & $args["--match-all"]))
|
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
# If no "context" property is given, use the default (if we have one)
|
|
||||||
if ctx.defaultContext.isSome and not filter.properties.hasKey("context"):
|
|
||||||
stderr.writeLine("Limiting to default context: " & ctx.defaultContext.get)
|
|
||||||
filter.properties["context"] = ctx.defaultContext.get
|
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
if tagsOption.isSome:
|
|
||||||
filter.hasTags = tagsOption.get
|
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
if exclTagsOption.isSome:
|
|
||||||
filter.exclTags = exclTagsOption.get
|
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
# Finally, if the "context" is "all", don't filter on context
|
|
||||||
if filter.properties.hasKey("context") and
|
|
||||||
filter.properties["context"] == "all":
|
|
||||||
|
|
||||||
filter.properties.del("context")
|
|
||||||
filter.exclProperties.del("context")
|
|
||||||
|
|
||||||
var listContexts = false
|
var listContexts = false
|
||||||
var listTags = false
|
var listTags = false
|
||||||
|
var listProjects = false
|
||||||
|
var listMilestones = false
|
||||||
var statesOption = none(seq[IssueState])
|
var statesOption = none(seq[IssueState])
|
||||||
var issueIdsOption = none(seq[string])
|
var issueIdsOption = none(seq[string])
|
||||||
|
|
||||||
if args["contexts"]: listContexts = true
|
if args["contexts"]: listContexts = true
|
||||||
|
elif args["projects"]: listProjects = true
|
||||||
|
elif args["milestones"]: listMilestones = true
|
||||||
elif args["tags"]: listTags = true
|
elif args["tags"]: listTags = true
|
||||||
elif args["<stateOrId>"]:
|
elif args["<stateOrId>"]:
|
||||||
try:
|
try:
|
||||||
@@ -372,6 +385,12 @@ when isMainModule:
|
|||||||
let issue = ctx.cfg.tasksDir.loadIssueById(issueId)
|
let issue = ctx.cfg.tasksDir.loadIssueById(issueId)
|
||||||
stdout.writeLine formatIssue(issue)
|
stdout.writeLine formatIssue(issue)
|
||||||
|
|
||||||
|
# List projects
|
||||||
|
elif listProjects: ctx.listProjects(filterOption)
|
||||||
|
|
||||||
|
# List milestones
|
||||||
|
elif listMilestones: ctx.listMilestones(filterOption)
|
||||||
|
|
||||||
# List all issues
|
# List all issues
|
||||||
else:
|
else:
|
||||||
trace "listing all issues"
|
trace "listing all issues"
|
||||||
@@ -384,6 +403,32 @@ when isMainModule:
|
|||||||
showHidden = args["--show-hidden"],
|
showHidden = args["--show-hidden"],
|
||||||
verbose = ctx.verbose)
|
verbose = ctx.verbose)
|
||||||
|
|
||||||
|
elif args["show"]:
|
||||||
|
|
||||||
|
if args["project-board"]:
|
||||||
|
ctx.showProjectBoard(filterOption)
|
||||||
|
discard
|
||||||
|
|
||||||
|
elif args["dupes"]:
|
||||||
|
ctx.loadAllIssues()
|
||||||
|
|
||||||
|
var idsToPaths = newTable[string, var seq[string]]()
|
||||||
|
for (state, issues) in pairs(ctx.issues):
|
||||||
|
for issue in issues:
|
||||||
|
let issueId = $issue.id
|
||||||
|
|
||||||
|
if idsToPaths.hasKey(issueId): idsToPaths[issueId].add(issue.filepath)
|
||||||
|
else: idsToPaths[issueId] = @[issue.filepath]
|
||||||
|
|
||||||
|
for (issueId, issuePaths) in pairs(idsToPaths):
|
||||||
|
if issuePaths.len < 2: continue
|
||||||
|
stdout.writeLine(issueId & ":\p " & issuePaths.join("\p ") & "\p\p")
|
||||||
|
|
||||||
|
else: # list specific Issues
|
||||||
|
for issueId in args["<id>"].mapIt($it):
|
||||||
|
let issue = ctx.cfg.tasksDir.loadIssueById(issueId)
|
||||||
|
stdout.writeLine formatIssue(issue)
|
||||||
|
|
||||||
elif args["add-binary-property"]:
|
elif args["add-binary-property"]:
|
||||||
let issue = ctx.cfg.tasksDir.loadIssueById($(args["<id>"]))
|
let issue = ctx.cfg.tasksDir.loadIssueById($(args["<id>"]))
|
||||||
|
|
||||||
@@ -412,21 +457,6 @@ when isMainModule:
|
|||||||
try: write(propOut, decodeDataUri(issue[$(args["<propName>"])]))
|
try: write(propOut, decodeDataUri(issue[$(args["<propName>"])]))
|
||||||
finally: close(propOut)
|
finally: close(propOut)
|
||||||
|
|
||||||
elif args["show-dupes"]:
|
|
||||||
ctx.loadAllIssues()
|
|
||||||
|
|
||||||
var idsToPaths = newTable[string, var seq[string]]()
|
|
||||||
for (state, issues) in pairs(ctx.issues):
|
|
||||||
for issue in issues:
|
|
||||||
let issueId = $issue.id
|
|
||||||
|
|
||||||
if idsToPaths.hasKey(issueId): idsToPaths[issueId].add(issue.filepath)
|
|
||||||
else: idsToPaths[issueId] = @[issue.filepath]
|
|
||||||
|
|
||||||
for (issueId, issuePaths) in pairs(idsToPaths):
|
|
||||||
if issuePaths.len < 2: continue
|
|
||||||
stdout.writeLine(issueId & ":\p " & issuePaths.join("\p ") & "\p\p")
|
|
||||||
|
|
||||||
elif args["sync"]:
|
elif args["sync"]:
|
||||||
if ctx.cfg.syncTargets.len == 0:
|
if ctx.cfg.syncTargets.len == 0:
|
||||||
info "No sync targets configured"
|
info "No sync targets configured"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
const PIT_VERSION* = "4.29.0"
|
const PIT_VERSION* = "4.30.1"
|
||||||
|
|
||||||
const USAGE* = """Usage:
|
const USAGE* = """Usage:
|
||||||
pit ( new | add) <summary> [<state>] [options]
|
pit ( new | add) <summary> [<state>] [options]
|
||||||
pit list contexts [options]
|
pit list contexts [options]
|
||||||
|
pit list projects [options]
|
||||||
|
pit list milestones [options]
|
||||||
pit list tags [options]
|
pit list tags [options]
|
||||||
pit list [<stateOrId>...] [options]
|
pit list [<stateOrId>...] [options]
|
||||||
pit ( start | done | pending | todo-today | todo | suspend ) <id>... [options]
|
pit ( start | done | pending | todo-today | todo | suspend ) <id>... [options]
|
||||||
@@ -15,10 +17,13 @@ const USAGE* = """Usage:
|
|||||||
pit ( delete | rm ) <id>... [options]
|
pit ( delete | rm ) <id>... [options]
|
||||||
pit add-binary-property <id> <propName> <propSource> [options]
|
pit add-binary-property <id> <propName> <propSource> [options]
|
||||||
pit get-binary-property <id> <propName> <propDest> [options]
|
pit get-binary-property <id> <propName> <propDest> [options]
|
||||||
pit show-dupes
|
pit show dupes [options]
|
||||||
|
pit show project-board [options]
|
||||||
|
pit show <id> [options]
|
||||||
pit sync [<syncTarget>...] [options]
|
pit sync [<syncTarget>...] [options]
|
||||||
pit help [options]
|
pit help [options]
|
||||||
|
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-h, --help Print this usage and help information.
|
-h, --help Print this usage and help information.
|
||||||
@@ -144,6 +149,38 @@ Issue Properties:
|
|||||||
|
|
||||||
hide-until: 2024-01-01T13:45:00-05:00
|
hide-until: 2024-01-01T13:45:00-05:00
|
||||||
|
|
||||||
|
priority
|
||||||
|
|
||||||
|
Allows setting a priority/priority level. This is used in the project
|
||||||
|
management view to automatically order issues being displayed. Valid
|
||||||
|
values, in order from most to least important, are:
|
||||||
|
|
||||||
|
- essential Intended for issues that must be done. Failure to
|
||||||
|
complete these issues would result in failure of the
|
||||||
|
project.
|
||||||
|
|
||||||
|
- vital Intended for issues that are vital to the success of the
|
||||||
|
project, but not absolutely essential. Failure to complete
|
||||||
|
these issues may not result in failure of the project but
|
||||||
|
would seriously impact it's value or viability.
|
||||||
|
|
||||||
|
- important Intended for issues that are important to the project,
|
||||||
|
but not vital. These should be completed, but delay is
|
||||||
|
acceptable.
|
||||||
|
|
||||||
|
- optional Intended for issues that are worth doing but can be deferred
|
||||||
|
or skipped if necessary.
|
||||||
|
|
||||||
|
priority: essential
|
||||||
|
|
||||||
|
milestone
|
||||||
|
|
||||||
|
Allows grouping issues according to a milestone name. Milestones are
|
||||||
|
available as subsets of projects. The 'list milestones' command will show
|
||||||
|
all values of 'milestone' set in existing issues for a given project.
|
||||||
|
|
||||||
|
milestone: Phase 1
|
||||||
|
|
||||||
pending
|
pending
|
||||||
|
|
||||||
When an issue is blocked by a third party, this property can be used to
|
When an issue is blocked by a third party, this property can be used to
|
||||||
@@ -152,6 +189,13 @@ Issue Properties:
|
|||||||
|
|
||||||
pending: Results of WCAG analysis.
|
pending: Results of WCAG analysis.
|
||||||
|
|
||||||
|
project
|
||||||
|
|
||||||
|
Allows grouping issues according to a project name. The 'list projects'
|
||||||
|
command will show all values of 'project' set in existing issues.
|
||||||
|
|
||||||
|
project: Website Redesign
|
||||||
|
|
||||||
recurrence
|
recurrence
|
||||||
|
|
||||||
When an issue is moved to the "done" state, if the issue has a valid
|
When an issue is moved to the "done" state, if the issue has a valid
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import std/[options, sequtils, wordwrap, tables, terminal, times, unicode, wordwrap]
|
import std/[options, sequtils, tables, terminal, times, unicode, wordwrap]
|
||||||
import cliutils, uuids
|
import cliutils, uuids
|
||||||
import std/strutils except alignLeft, capitalize, strip, toLower, toUpper
|
import std/strutils except alignLeft, capitalize, strip, toLower, toUpper
|
||||||
import ./libpit
|
import ./libpit
|
||||||
|
|
||||||
proc adjustedTerminalWidth(): int = min(terminalWidth(), 80)
|
proc adjustedTerminalWidth(): int = min(terminalWidth(), 80)
|
||||||
|
|
||||||
proc getIssueContextDisplayName*(ctx: CliContext, context: string): string =
|
|
||||||
if not ctx.contexts.hasKey(context):
|
|
||||||
if context.isEmptyOrWhitespace: return "<default>"
|
|
||||||
else: return context.capitalize()
|
|
||||||
return ctx.contexts[context]
|
|
||||||
|
|
||||||
|
|
||||||
proc formatIssue*(issue: Issue): string =
|
proc formatIssue*(issue: Issue): string =
|
||||||
result = ($issue.id).withColor(fgBlack, true) & "\n"&
|
result = ($issue.id).withColor(fgBlack, true) & "\n"&
|
||||||
issue.summary.withColor(fgWhite) & "\n"
|
issue.summary.withColor(fgWhite) & "\n"
|
||||||
@@ -21,9 +14,22 @@ proc formatIssue*(issue: Issue): string =
|
|||||||
issue.tags.join(",").withColor(fgGreen, true) & "\n"
|
issue.tags.join(",").withColor(fgGreen, true) & "\n"
|
||||||
|
|
||||||
if issue.properties.len > 0:
|
if issue.properties.len > 0:
|
||||||
result &= termColor(fgMagenta)
|
for k, v in issue.properties:
|
||||||
for k, v in issue.properties: result &= k & ": " & v & "\n"
|
|
||||||
|
|
||||||
|
if k == "project":
|
||||||
|
result &= "project: ".withColor(fgMagenta) &
|
||||||
|
v.withColor(fgBlue, bright = true) & "\n"
|
||||||
|
|
||||||
|
elif k == "milestone":
|
||||||
|
result &= "milestone: ".withColor(fgMagenta) &
|
||||||
|
v.withColor(fgBlue, bright = true) & "\n"
|
||||||
|
|
||||||
|
elif k == "priority":
|
||||||
|
result &= "priority: ".withColor(fgMagenta) &
|
||||||
|
v.withColor(fgRed, bright = true) & "\n"
|
||||||
|
|
||||||
|
else:
|
||||||
|
result &= termColor(fgMagenta) & k & ": " & v & "\n"
|
||||||
|
|
||||||
result &= "--------".withColor(fgBlack, true) & "\n"
|
result &= "--------".withColor(fgBlack, true) & "\n"
|
||||||
if not issue.details.isEmptyOrWhitespace:
|
if not issue.details.isEmptyOrWhitespace:
|
||||||
@@ -55,7 +61,8 @@ proc formatSectionIssue*(
|
|||||||
issue: Issue,
|
issue: Issue,
|
||||||
width: int = 80,
|
width: int = 80,
|
||||||
indent = "",
|
indent = "",
|
||||||
verbose = false): string =
|
verbose = false,
|
||||||
|
bold = false): string =
|
||||||
|
|
||||||
result = (indent & ($issue.id)[0..<6]).withColor(fgBlack, true) & " "
|
result = (indent & ($issue.id)[0..<6]).withColor(fgBlack, true) & " "
|
||||||
|
|
||||||
@@ -68,7 +75,7 @@ proc formatSectionIssue*(
|
|||||||
.wrapWords(summaryWidth)
|
.wrapWords(summaryWidth)
|
||||||
.splitLines
|
.splitLines
|
||||||
|
|
||||||
result &= summaryLines[0].withColor(fgWhite)
|
result &= summaryLines[0].termFmt(fgWhite, bold=bold, underline=bold)
|
||||||
|
|
||||||
for line in summaryLines[1..^1]:
|
for line in summaryLines[1..^1]:
|
||||||
result &= "\p" & line.indent(summaryIndentLen)
|
result &= "\p" & line.indent(summaryIndentLen)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import std/[json, logging, options, os, strformat, strutils, tables, times]
|
import std/[json, logging, options, os, strformat, strutils, tables, times,
|
||||||
|
unicode]
|
||||||
import cliutils, docopt, langutils, uuids, zero_functional
|
import cliutils, docopt, langutils, uuids, zero_functional
|
||||||
|
|
||||||
import nre except toSeq
|
import nre except toSeq
|
||||||
@@ -18,18 +19,22 @@ type
|
|||||||
Current = "current",
|
Current = "current",
|
||||||
TodoToday = "todo-today",
|
TodoToday = "todo-today",
|
||||||
Pending = "pending",
|
Pending = "pending",
|
||||||
Done = "done",
|
|
||||||
Todo = "todo"
|
Todo = "todo"
|
||||||
Dormant = "dormant"
|
Dormant = "dormant"
|
||||||
|
Done = "done",
|
||||||
|
|
||||||
IssueFilter* = ref object
|
IssueFilter* = ref object
|
||||||
completedRange*: Option[tuple[b, e: DateTime]]
|
completedRange*: Option[tuple[b, e: DateTime]]
|
||||||
fullMatch*, summaryMatch*: Option[Regex]
|
fullMatch*, summaryMatch*: Option[Regex]
|
||||||
|
inclStates*: seq[IssueState]
|
||||||
|
exclStates*: seq[IssueState]
|
||||||
hasTags*: seq[string]
|
hasTags*: seq[string]
|
||||||
exclTags*: seq[string]
|
exclTags*: seq[string]
|
||||||
properties*: TableRef[string, string]
|
properties*: TableRef[string, string]
|
||||||
exclProperties*: TableRef[string, seq[string]]
|
exclProperties*: TableRef[string, seq[string]]
|
||||||
|
|
||||||
|
IssuePriority* {.pure.} = enum essential, vital, important, optional
|
||||||
|
|
||||||
PitConfig* = ref object
|
PitConfig* = ref object
|
||||||
tasksDir*: string
|
tasksDir*: string
|
||||||
contexts*: TableRef[string, string]
|
contexts*: TableRef[string, string]
|
||||||
@@ -129,6 +134,13 @@ proc getRecurrence*(issue: Issue): Option[Recurrence] =
|
|||||||
else: weeks(1),
|
else: weeks(1),
|
||||||
cloneId: c[6]))
|
cloneId: c[6]))
|
||||||
|
|
||||||
|
proc setPriority*(issue: Issue, priority: IssuePriority) =
|
||||||
|
issue["priority"] = $priority
|
||||||
|
|
||||||
|
proc getPriority*(issue: Issue): IssuePriority =
|
||||||
|
try: result = parseEnum[IssuePriority](issue["priority"].toLowerAscii())
|
||||||
|
except CatchableError: result = IssuePriority.optional
|
||||||
|
|
||||||
|
|
||||||
## Issue filtering
|
## Issue filtering
|
||||||
proc initFilter*(): IssueFilter =
|
proc initFilter*(): IssueFilter =
|
||||||
@@ -136,6 +148,8 @@ proc initFilter*(): IssueFilter =
|
|||||||
completedRange: none(tuple[b, e: DateTime]),
|
completedRange: none(tuple[b, e: DateTime]),
|
||||||
fullMatch: none(Regex),
|
fullMatch: none(Regex),
|
||||||
summaryMatch: none(Regex),
|
summaryMatch: none(Regex),
|
||||||
|
inclStates: @[],
|
||||||
|
exclStates: @[],
|
||||||
hasTags: @[],
|
hasTags: @[],
|
||||||
exclTags: @[],
|
exclTags: @[],
|
||||||
properties: newTable[string, string](),
|
properties: newTable[string, string](),
|
||||||
@@ -165,6 +179,10 @@ proc hasTagsFilter*(tags: seq[string]): IssueFilter =
|
|||||||
result = initFilter()
|
result = initFilter()
|
||||||
result.hasTags = tags
|
result.hasTags = tags
|
||||||
|
|
||||||
|
proc stateFilter*(states: seq[IssueState]): IssueFilter =
|
||||||
|
result = initFilter()
|
||||||
|
result.inclStates = states
|
||||||
|
|
||||||
proc groupBy*(issues: seq[Issue], propertyKey: string): TableRef[string, seq[Issue]] =
|
proc groupBy*(issues: seq[Issue], propertyKey: string): TableRef[string, seq[Issue]] =
|
||||||
result = newTable[string, seq[Issue]]()
|
result = newTable[string, seq[Issue]]()
|
||||||
for i in issues:
|
for i in issues:
|
||||||
@@ -426,6 +444,12 @@ proc filter*(issues: seq[Issue], filter: IssueFilter): seq[Issue] =
|
|||||||
let exclTag = exclTagLent
|
let exclTag = exclTagLent
|
||||||
f = f --> filter(it.tags.find(exclTag) < 0)
|
f = f --> filter(it.tags.find(exclTag) < 0)
|
||||||
|
|
||||||
|
if filter.inclStates.len > 0:
|
||||||
|
f = f --> filter(filter.inclStates.contains(it.state))
|
||||||
|
|
||||||
|
if filter.exclStates.len > 0:
|
||||||
|
f = f --> filter(not filter.exclStates.contains(it.state))
|
||||||
|
|
||||||
return f # not using result because zero_functional doesn't play nice with it
|
return f # not using result because zero_functional doesn't play nice with it
|
||||||
|
|
||||||
proc find*(
|
proc find*(
|
||||||
@@ -513,3 +537,9 @@ proc loadAllIssues*(ctx: CliContext) =
|
|||||||
proc filterIssues*(ctx: CliContext, filter: IssueFilter) =
|
proc filterIssues*(ctx: CliContext, filter: IssueFilter) =
|
||||||
for state, issueList in ctx.issues:
|
for state, issueList in ctx.issues:
|
||||||
ctx.issues[state] = issueList.filter(filter)
|
ctx.issues[state] = issueList.filter(filter)
|
||||||
|
|
||||||
|
proc getIssueContextDisplayName*(ctx: CliContext, context: string): string =
|
||||||
|
if not ctx.contexts.hasKey(context):
|
||||||
|
if context.isEmptyOrWhitespace: return "<default>"
|
||||||
|
else: return context.capitalize()
|
||||||
|
return ctx.contexts[context]
|
||||||
|
|||||||
368
src/pit/projects.nim
Normal file
368
src/pit/projects.nim
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
import std/[algorithm, json, jsonutils, options, os, sets, strutils, tables,
|
||||||
|
terminal, times, unicode, wordwrap]
|
||||||
|
from std/sequtils import repeat, toSeq
|
||||||
|
import cliutils, uuids, zero_functional
|
||||||
|
import ./[formatting, libpit]
|
||||||
|
|
||||||
|
type
|
||||||
|
ProjectCfg* = ref object of RootObj
|
||||||
|
name: string
|
||||||
|
milestoneOrder*: seq[string]
|
||||||
|
|
||||||
|
Project* = ref object of ProjectCfg
|
||||||
|
milestones*: TableRef[string, seq[Issue]]
|
||||||
|
|
||||||
|
ProjectsConfiguration* = TableRef[string, seq[ProjectCfg]]
|
||||||
|
## ProjectCfgs by context
|
||||||
|
|
||||||
|
ProjectsDatabase* = TableRef[string, seq[Project]]
|
||||||
|
## Projects by context
|
||||||
|
|
||||||
|
|
||||||
|
converter extractConfig(pdb: ProjectsDatabase): ProjectsConfiguration =
|
||||||
|
result = newTable[string, seq[ProjectCfg]]()
|
||||||
|
for (context, projects) in pairs(pdb):
|
||||||
|
result[context] = @[]
|
||||||
|
for project in projects:
|
||||||
|
result[context].add(ProjectCfg(
|
||||||
|
name: project.name,
|
||||||
|
milestoneOrder: project.milestoneOrder))
|
||||||
|
|
||||||
|
|
||||||
|
proc loadProjectsConfiguration*(ctx: CliContext): ProjectsConfiguration =
|
||||||
|
|
||||||
|
let projectsCfgFile = ctx.cfg.tasksDir & "/projects.json"
|
||||||
|
|
||||||
|
if not fileExists(projectsCfgFile):
|
||||||
|
return newTable[string, seq[ProjectCfg]]()
|
||||||
|
else:
|
||||||
|
fromJson[ProjectsConfiguration](result, parseFile(projectsCfgFile))
|
||||||
|
|
||||||
|
|
||||||
|
proc saveProjectsConfiguration*(ctx: CliContext, cfg: ProjectsConfiguration) =
|
||||||
|
let projectsCfgFile = ctx.cfg.tasksDir / "projects.json"
|
||||||
|
writeFile(projectsCfgFile, toJson(cfg).pretty)
|
||||||
|
|
||||||
|
|
||||||
|
proc buildDb*(ctx: CliContext, cfg: ProjectsConfiguration): ProjectsDatabase =
|
||||||
|
result = newTable[string, seq[Project]]()
|
||||||
|
|
||||||
|
# Expand the configuration into the database structure
|
||||||
|
for (context, projectCfgs) in pairs(cfg):
|
||||||
|
result[context] = @[]
|
||||||
|
for projectCfg in projectCfgs:
|
||||||
|
let pcfg = projectCfg
|
||||||
|
let project = Project(
|
||||||
|
name: projectCfg.name,
|
||||||
|
milestoneOrder: projectCfg.milestoneOrder,
|
||||||
|
milestones: newTable[string, seq[Issue]](
|
||||||
|
pcfg.milestoneOrder --> map((it, newSeq[Issue]()))))
|
||||||
|
result[context].add(project)
|
||||||
|
|
||||||
|
# Now populate the database with issues
|
||||||
|
for (state, issues) in pairs(ctx.issues):
|
||||||
|
for issue in issues:
|
||||||
|
if not issue.hasProp("project") or
|
||||||
|
not issue.hasProp("milestone"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
let projectName = issue["project"]
|
||||||
|
let milestone = issue["milestone"]
|
||||||
|
let context =
|
||||||
|
if issue.hasProp("context"): issue["context"]
|
||||||
|
else: "<no-context>"
|
||||||
|
|
||||||
|
# Make sure we have entries for this context and project
|
||||||
|
if not result.hasKey(context): result[context] = @[]
|
||||||
|
|
||||||
|
var projectsInContext = result[context]
|
||||||
|
|
||||||
|
if not (projectsInContext --> exists(it.name == projectName)):
|
||||||
|
projectsInContext.add(Project(
|
||||||
|
name: projectName,
|
||||||
|
milestoneOrder: @[],
|
||||||
|
milestones: newTable[string, seq[Issue]]()))
|
||||||
|
|
||||||
|
let projectIdx = projectsInContext --> index(it.name == projectName)
|
||||||
|
var project = projectsInContext[projectIdx]
|
||||||
|
|
||||||
|
# Make sure we have entries for this milestone
|
||||||
|
if not project.milestones.hasKey(milestone):
|
||||||
|
project.milestones[milestone] = @[]
|
||||||
|
|
||||||
|
if not project.milestoneOrder.contains(milestone):
|
||||||
|
project.milestoneOrder.add(milestone)
|
||||||
|
|
||||||
|
project.milestones[milestone].add(issue)
|
||||||
|
|
||||||
|
result[context] = projectsInContext
|
||||||
|
|
||||||
|
ctx.saveProjectsConfiguration(result)
|
||||||
|
|
||||||
|
proc cmp*(a, b: Issue): int =
|
||||||
|
if a.state != b.state:
|
||||||
|
return cmp(ord(a.state), ord(b.state))
|
||||||
|
|
||||||
|
if a.hasProp("priority") or b.hasProp("priority"):
|
||||||
|
if a.getPriority != b.getPriority:
|
||||||
|
return cmp(a.getPriority, b.getPriority) # higher priority first
|
||||||
|
|
||||||
|
if a.hasProp("last-updated") or b.hasProp("last-updated"):
|
||||||
|
var aUpdated = a.getDateTime("last-updated", local(fromUnix(0)))
|
||||||
|
var bUpdated = b.getDateTime("last-updated", local(fromUnix(0)))
|
||||||
|
|
||||||
|
if aUpdated != bUpdated:
|
||||||
|
return cmp(bUpdated, aUpdated) # newer first
|
||||||
|
|
||||||
|
return cmp(a.summary, b.summary)
|
||||||
|
|
||||||
|
|
||||||
|
proc listProjects*(ctx: CliContext, filter = none[IssueFilter]()) =
|
||||||
|
ctx.loadAllIssues()
|
||||||
|
let projectsCfg = ctx.loadProjectsConfiguration()
|
||||||
|
var projectsCfgChanged = false
|
||||||
|
|
||||||
|
if filter.isSome: ctx.filterIssues(filter.get)
|
||||||
|
|
||||||
|
let projectsByContext = newTable[string, CountTableRef[string]]()
|
||||||
|
|
||||||
|
for (state, issues) in pairs(ctx.issues):
|
||||||
|
for issue in issues:
|
||||||
|
if issue.hasProp("project"):
|
||||||
|
let context =
|
||||||
|
if issue.hasProp("context"): issue["context"]
|
||||||
|
else: "<no-context>"
|
||||||
|
|
||||||
|
if not projectsByContext.hasKey(context):
|
||||||
|
projectsByContext[context] = newCountTable[string]()
|
||||||
|
|
||||||
|
projectsByContext[context].inc(issue["project"])
|
||||||
|
|
||||||
|
for (context, projects) in pairs(projectsByContext):
|
||||||
|
|
||||||
|
stdout.writeLine(withColor(
|
||||||
|
ctx.getIssueContextDisplayName(context) & ":",
|
||||||
|
fgYellow) & termReset)
|
||||||
|
stdout.writeLine("")
|
||||||
|
|
||||||
|
var toList = toHashSet(toSeq(keys(projects)))
|
||||||
|
|
||||||
|
# Loop through the projects in the configured order first
|
||||||
|
if not projectsCfg.hasKey(context):
|
||||||
|
projectsCfg[context] = @[]
|
||||||
|
|
||||||
|
for project in projectsCfg[context]:
|
||||||
|
if project.name in toList:
|
||||||
|
toList.excl(project.name)
|
||||||
|
stdout.writeLine(" " & project.name &
|
||||||
|
" (" & $projects[project.name] & " issues)")
|
||||||
|
|
||||||
|
# Then list any remaining projects not in the configuration, and add them
|
||||||
|
# to the configuration
|
||||||
|
for (projectName, count) in pairs(projects):
|
||||||
|
if projectName in toList:
|
||||||
|
stdout.writeLine(" " & projectName & " (" & $count & " issues)")
|
||||||
|
projectsCfg[context].add(ProjectCfg(name: projectName, milestoneOrder: @[]))
|
||||||
|
projectsCfgChanged = true
|
||||||
|
|
||||||
|
stdout.writeLine("")
|
||||||
|
|
||||||
|
if projectsCfgChanged: ctx.saveProjectsConfiguration(projectsCfg)
|
||||||
|
|
||||||
|
proc listMilestones*(ctx: CliContext, filter = none[IssueFilter]()) =
|
||||||
|
ctx.loadAllIssues()
|
||||||
|
if filter.isSome: ctx.filterIssues(filter.get)
|
||||||
|
|
||||||
|
let projectsCfg = ctx.loadProjectsConfiguration()
|
||||||
|
let projectsDb = ctx.buildDb(projectsCfg)
|
||||||
|
|
||||||
|
var milestones = newCountTable[string]()
|
||||||
|
|
||||||
|
for (context, projects) in pairs(projectsDb):
|
||||||
|
for p in projects:
|
||||||
|
let project = p
|
||||||
|
if values(project.milestones) --> all(it.len == 0):
|
||||||
|
continue
|
||||||
|
|
||||||
|
stdout.writeLine(withColor(project.name, fgBlue, bold = true, bright=true))
|
||||||
|
stdout.writeLine(withColor(
|
||||||
|
"─".repeat(runeLen(stripAnsi(project.name))),
|
||||||
|
fgBlue, bold = true))
|
||||||
|
|
||||||
|
for milestone in project.milestoneOrder:
|
||||||
|
if project.milestones.hasKey(milestone) and
|
||||||
|
project.milestones[milestone].len > 0:
|
||||||
|
let issueCount = project.milestones[milestone].len
|
||||||
|
stdout.writeLine(" " & milestone & " (" & $issueCount & " issues)")
|
||||||
|
|
||||||
|
stdout.writeLine("")
|
||||||
|
|
||||||
|
proc formatProjectIssue(
|
||||||
|
ctx: CliContext,
|
||||||
|
issue: Issue,
|
||||||
|
width: int): seq[string] =
|
||||||
|
|
||||||
|
var firstLine = ""
|
||||||
|
if issue.state == IssueState.Done:
|
||||||
|
firstLine &= withColor(" ✔ ", fgBlack, bold=true, bright=true)
|
||||||
|
else:
|
||||||
|
case issue.getPriority
|
||||||
|
of IssuePriority.essential:
|
||||||
|
firstLine &= withColor("❶ ", fgRed, bold=true, bright=true)
|
||||||
|
of IssuePriority.vital:
|
||||||
|
firstLine &= withColor("❷ ", fgYellow, bold=true, bright=true)
|
||||||
|
of IssuePriority.important:
|
||||||
|
firstLine &= withColor("❸ ", fgBlue, bold=true, bright=true)
|
||||||
|
of IssuePriority.optional:
|
||||||
|
firstLine &= withColor("❹ ", fgBlack, bold=false, bright=true)
|
||||||
|
|
||||||
|
let summaryText = formatSectionIssue(issue, width - 3,
|
||||||
|
bold = [Current, TodoToday].contains(issue.state)).splitLines
|
||||||
|
firstLine &= summaryText[0]
|
||||||
|
|
||||||
|
if issue.state == IssueState.Done:
|
||||||
|
firstLine = withColor(stripAnsi(firstLine), fgBlack, bright=true)
|
||||||
|
|
||||||
|
result.add(firstLine)
|
||||||
|
result.add(summaryText[1 .. ^1] --> map(" " & it))
|
||||||
|
|
||||||
|
if issue.state == IssueState.Done:
|
||||||
|
let origLines = result
|
||||||
|
result = origLines --> map(withColor(stripAnsi(it), fgBlack, bright=true))
|
||||||
|
|
||||||
|
proc formatParentIssue*(
|
||||||
|
ctx: CliContext,
|
||||||
|
parentIssue: Issue,
|
||||||
|
children: seq[Issue],
|
||||||
|
width: int): seq[string] =
|
||||||
|
|
||||||
|
result.add(ctx.formatProjectIssue(parentIssue, width))
|
||||||
|
|
||||||
|
for child in sorted(children, cmp):
|
||||||
|
let childLines = ctx.formatProjectIssue(child, width - 3)
|
||||||
|
result.add(childLines --> map(withColor(" │ ", fgBlack, bright=true) & it))
|
||||||
|
|
||||||
|
result.add("")
|
||||||
|
|
||||||
|
|
||||||
|
proc formatMilestone*(
|
||||||
|
ctx: CliContext,
|
||||||
|
milestone: string,
|
||||||
|
issues: seq[Issue],
|
||||||
|
availWidth: int): seq[string] =
|
||||||
|
|
||||||
|
result = @[""]
|
||||||
|
result.add(withColor(milestone, fgWhite, bold=true))
|
||||||
|
result.add(withColor("─".repeat(availWidth), fgWhite))
|
||||||
|
|
||||||
|
var parentsToChildren = issues -->
|
||||||
|
filter(it.hasProp("parent")).group(it["parent"])
|
||||||
|
|
||||||
|
var issuesToFormat = sorted(issues, cmp) -->
|
||||||
|
filter(not it.hasProp("parent"))
|
||||||
|
|
||||||
|
for issue in issuesToFormat:
|
||||||
|
if parentsToChildren.hasKey($issue.id):
|
||||||
|
result.add(
|
||||||
|
ctx.formatParentIssue(issue, parentsToChildren[$issue.id], availWidth))
|
||||||
|
else:
|
||||||
|
result.add(ctx.formatProjectIssue(issue, availWidth))
|
||||||
|
|
||||||
|
proc findShortestColumn(columns: seq[seq[string]]): int =
|
||||||
|
var shortestIdx = 0
|
||||||
|
var shortestLen = columns[0].len
|
||||||
|
|
||||||
|
for i in 1 ..< columns.len:
|
||||||
|
if columns[i].len < shortestLen:
|
||||||
|
shortestLen = columns[i].len
|
||||||
|
shortestIdx = i
|
||||||
|
|
||||||
|
return shortestIdx
|
||||||
|
|
||||||
|
|
||||||
|
proc joinColumns(columns: seq[seq[string]], columnWidth: int): seq[string] =
|
||||||
|
let maxLines = columns --> map(it.len).max()
|
||||||
|
|
||||||
|
for lineNo in 0 ..< maxLines:
|
||||||
|
var newLine = ""
|
||||||
|
for col in columns:
|
||||||
|
if lineNo < col.len:
|
||||||
|
let lineLen = runeLen(stripAnsi(col[lineNo]))
|
||||||
|
newLine &= col[lineNo] & " ".repeat(max(0, columnWidth - lineLen) + 2)
|
||||||
|
else:
|
||||||
|
newLine &= " ".repeat(columnWidth + 2)
|
||||||
|
|
||||||
|
result.add(newLine)
|
||||||
|
|
||||||
|
proc showProject*(ctx: CliContext, project: Project) =
|
||||||
|
|
||||||
|
let fullWidth = terminalWidth() - 1
|
||||||
|
let columnWidth = 80
|
||||||
|
let numColumns = (fullWidth - 4) div (columnWidth + 2)
|
||||||
|
|
||||||
|
stdout.writeLine("")
|
||||||
|
stdout.writeLine(withColor(
|
||||||
|
"┌" & "─".repeat(project.name.len + 2) &
|
||||||
|
"┬" & "─".repeat(fullWidth - project.name.len - 4) & "┐",
|
||||||
|
fgBlue, bold=true))
|
||||||
|
stdout.writeLine(
|
||||||
|
withColor("│ ", fgBlue, bold=true) &
|
||||||
|
withColor(project.name, fgBlue, bold=true, bright=true) &
|
||||||
|
withColor(" │" & " ".repeat(fullWidth - project.name.len - 4) & "│", fgBlue, bold=true))
|
||||||
|
stdout.writeLine(withColor(
|
||||||
|
"├" & "─".repeat(project.name.len + 2) &
|
||||||
|
"┘" & " ".repeat(fullWidth - project.name.len - 4) & "│",
|
||||||
|
fgBlue, bold=true))
|
||||||
|
|
||||||
|
let milestoneTexts: seq[seq[string]] = project.milestoneOrder -->
|
||||||
|
filter(project.milestones.hasKey(it) and project.milestones[it].len > 0).
|
||||||
|
map(ctx.formatMilestone(it, project.milestones[it], columnWidth))
|
||||||
|
|
||||||
|
var columns: seq[seq[string]] = repeat(newSeq[string](), numColumns)
|
||||||
|
|
||||||
|
for milestoneText in milestoneTexts:
|
||||||
|
let shortestColumnIdx = findShortestColumn(columns)
|
||||||
|
columns[shortestColumnIdx].add(milestoneText)
|
||||||
|
|
||||||
|
let joinedLines = joinColumns(columns, columnWidth)
|
||||||
|
|
||||||
|
for line in joinedLines:
|
||||||
|
let padLen = fullWidth - runeLen(stripAnsi(line)) - 3
|
||||||
|
stdout.writeLine(
|
||||||
|
withColor("│ ", fgBlue) &
|
||||||
|
line &
|
||||||
|
" ".repeat(padLen) &
|
||||||
|
withColor(" │", fgBlue))
|
||||||
|
|
||||||
|
stdout.writeLine(withColor(
|
||||||
|
"└" & "─".repeat(terminalWidth() - 2) & "┘",
|
||||||
|
fgBlue, bold=true))
|
||||||
|
|
||||||
|
|
||||||
|
proc showProjectBoard*(ctx: CliContext, filter = none[IssueFilter]()) =
|
||||||
|
ctx.loadAllIssues()
|
||||||
|
if filter.isSome: ctx.filterIssues(filter.get)
|
||||||
|
|
||||||
|
let projectsCfg = ctx.loadProjectsConfiguration()
|
||||||
|
let projectsDb = ctx.buildDb(projectsCfg)
|
||||||
|
|
||||||
|
var contextsAndProjects: seq[(string, seq[Project])] = @[]
|
||||||
|
|
||||||
|
for (context, pjs) in pairs(projectsDb):
|
||||||
|
let projects = pjs
|
||||||
|
let issues: seq[Issue] = projects --> map(toSeq(values(it.milestones))).flatten().flatten()
|
||||||
|
if issues.len > 0:
|
||||||
|
contextsAndProjects.add((context, projects))
|
||||||
|
|
||||||
|
for (context, projects) in contextsAndProjects:
|
||||||
|
if contextsAndProjects.len > 1:
|
||||||
|
stdout.writeLine("")
|
||||||
|
stdout.writeLine(withColor(
|
||||||
|
ctx.getIssueContextDisplayName(context) & ":",
|
||||||
|
fgYellow, bold=true))
|
||||||
|
stdout.writeLine("")
|
||||||
|
|
||||||
|
for p in projects:
|
||||||
|
let project = p
|
||||||
|
if (values(project.milestones) --> exists(it.len > 0)):
|
||||||
|
ctx.showProject(project)
|
||||||
@@ -19,8 +19,11 @@ type
|
|||||||
lastUpdatedAt*: DateTime
|
lastUpdatedAt*: DateTime
|
||||||
archivedAt*: Option[DateTime]
|
archivedAt*: Option[DateTime]
|
||||||
tags*: seq[string]
|
tags*: seq[string]
|
||||||
|
context*: Option[string]
|
||||||
|
parent*: Option[string]
|
||||||
priority*: Option[string]
|
priority*: Option[string]
|
||||||
project*: Option[string]
|
project*: Option[string]
|
||||||
|
properties*: JsonNode
|
||||||
milestone*: Option[string]
|
milestone*: Option[string]
|
||||||
hideUntil*: Option[DateTime]
|
hideUntil*: Option[DateTime]
|
||||||
|
|
||||||
@@ -32,7 +35,7 @@ proc fromJsonHook(dt: var DateTime, n: JsonNode): void =
|
|||||||
|
|
||||||
|
|
||||||
#func `%`*(p: Project): JsonNode = toJson(p)
|
#func `%`*(p: Project): JsonNode = toJson(p)
|
||||||
func `%`*(t: ServerTask): JsonNode = toJson(t)
|
proc `%`*(t: ServerTask): JsonNode = toJson(t)
|
||||||
|
|
||||||
|
|
||||||
proc toServerTask(i: Issue): ServerTask =
|
proc toServerTask(i: Issue): ServerTask =
|
||||||
@@ -42,9 +45,18 @@ proc toServerTask(i: Issue): ServerTask =
|
|||||||
details: i.details,
|
details: i.details,
|
||||||
state: $i.state,
|
state: $i.state,
|
||||||
tags: i.tags,
|
tags: i.tags,
|
||||||
|
properties: %i.properties,
|
||||||
|
|
||||||
|
context:
|
||||||
|
if i.hasProp("context"): some(i["context"])
|
||||||
|
else: none[string](),
|
||||||
|
|
||||||
lastUpdatedAt:
|
lastUpdatedAt:
|
||||||
if i.hasProp("last-updated"): parseIso8601(i["last-updated"])
|
if i.hasProp("last-updated"): parseIso8601(i["last-updated"])
|
||||||
else: now(),
|
else: now(),
|
||||||
|
parent:
|
||||||
|
if i.hasProp("parent"): some(i["parent"])
|
||||||
|
else: none[string](),
|
||||||
priority:
|
priority:
|
||||||
if i.hasProp("priority"): some(i["priority"])
|
if i.hasProp("priority"): some(i["priority"])
|
||||||
else: none[string](),
|
else: none[string](),
|
||||||
@@ -76,7 +88,7 @@ proc initSyncContext*(pit: PitConfig, syncConfig: JsonNode): PbmVsbSyncContext =
|
|||||||
|
|
||||||
proc fetchServerTasks*(ctx: PbmVsbSyncContext): seq[ServerTask] =
|
proc fetchServerTasks*(ctx: PbmVsbSyncContext): seq[ServerTask] =
|
||||||
result = newSeq[ServerTask]()
|
result = newSeq[ServerTask]()
|
||||||
let url = ctx.apiBaseUrl & "/tasks"
|
let url = ctx.apiBaseUrl & "/tasks?context=" & ctx.issueContext
|
||||||
|
|
||||||
debug "Fetching tasks from server:\n\t" & url
|
debug "Fetching tasks from server:\n\t" & url
|
||||||
let resp = ctx.http.get(url)
|
let resp = ctx.http.get(url)
|
||||||
|
|||||||
Reference in New Issue
Block a user