Skip to content

sprint fields#48

Merged
textfuel merged 10 commits intomainfrom
sprint-field-discovery
May 3, 2026
Merged

sprint fields#48
textfuel merged 10 commits intomainfrom
sprint-field-discovery

Conversation

@textfuel
Copy link
Copy Markdown
Owner

What

Discover the Sprint custom field id at startup and use it everywhere instead of the hardcoded name sprint. Parse both modern JSON and legacy stringified sprint payloads. Rewrite the sprint key in update requests to the real custom field id

Why

On Jira Cloud Sprint is usually customfield_10020. On older Server and Data Center it is often customfield_10010. Many instances resolve the alias sprint back to the right id but some do not. On those the Sprint field shows None even when the issue has one set. Fixes #33

How to test

  1. Build and run lazyjira against a real Jira where Sprint already works
  2. Confirm Sprint shows the correct value in the Info panel
  3. Run with flag log and see one request to /field at startup before any search
  4. Press e on the Sprint field and change it through the picker. Reopen and confirm it persisted
  5. Press e and clear the sprint. Confirm the field shows None after refresh
  6. Run with flag demo and confirm Sprint still shows on demo issues

@textfuel
Copy link
Copy Markdown
Owner Author

@seflue wanna do a code review on this one?

@seflue
Copy link
Copy Markdown
Contributor

seflue commented Apr 14, 2026

Sure. I will give it a look. 👍

Copy link
Copy Markdown
Contributor

@seflue seflue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for asking me to take a look. A few questions and small readability suggestions below, nothing blocking.

Caveat: I can't verify the Server/DC behavior from my end, so my review is effectively code-only on that side.

One bigger ask up front: would you consider a table-driven test for parseSprintRaw / parseLegacySprint? A handful of cases (modern, legacy, empty, malformed) would double as documentation. I could only vaguely guess from the regex what the legacy input is supposed to look like.

Comment thread pkg/jira/sprint_parse.go
return attributes
}

func assignLegacyAttribute(attributes map[string]string, pair string) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small readability nit: would strings.IndexByte work here? The manual loop is correct but a bit more to parse on first read:

Suggested change
func assignLegacyAttribute(attributes map[string]string, pair string) {
func assignLegacyAttribute(attributes map[string]string, pair string) {
eq := strings.IndexByte(pair, '=')
if eq <= 0 {
return
}
attributes[pair[:eq]] = pair[eq+1:]
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

beautiful, gonna use yours!

Comment thread pkg/jira/sprint_parse.go
"strconv"
)

var legacySprintPattern = regexp.MustCompile(`\[([^\[\]]*)\]$`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I could guess that you are interested in the trailing [...]
of a Jira legacy sprint string, it would improve readability to have
an example here or in the function documentation of what such a string
actually looks like.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, gonna add one

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. 👍

Comment thread pkg/jira/sprint_parse.go Outdated
Comment on lines +101 to +112
switch sprint.State {
case "active", "ACTIVE":
return sprint
case "future", "FUTURE":
if future == nil {
future = sprint
}
case "closed", "CLOSED":
if closed == nil {
closed = sprint
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would normalizing the state once up front read a bit cleaner than
listing both casings per case?

Suggested change
switch sprint.State {
case "active", "ACTIVE":
return sprint
case "future", "FUTURE":
if future == nil {
future = sprint
}
case "closed", "CLOSED":
if closed == nil {
closed = sprint
}
}
switch strings.ToLower(sprint.State) {
case "active":
return sprint
case "future":
if future == nil {
future = sprint
}
case "closed":
if closed == nil {
closed = sprint
}
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right. My version is a bit faster, but your suggestion is much more robust and cleaner, gonna use it

Comment thread pkg/jira/sprint_parse.go
}
var modern []Sprint
if err := json.Unmarshal(data, &modern); err == nil {
filtered := modern[:0]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor question: With filtered := modern[:0] you reuse the input slices backing array. It works but surprised me a bit. The allocation saving is tiny (usually a handful of sprints), so I would have expected var filtered []Sprint. No pushback if you prefer the current form, just flagging it as something that made me pause.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread pkg/tui/app.go
cmds = append(cmds, fetchBoards(a.client))
if cmd := a.fetchActiveTab(); cmd != nil {
cmds = append(cmds, cmd)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed fetchActiveTab() moved out of the initial tea.Batch and now fires only after fieldsDiscoveredMsg arrives. That means the first issue fetch waits for fetchFieldDiscovery to complete, where previously the two ran in parallel. Was that intentional? What happens to startup latency if field discovery is slow?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was just a bad decision, nice catch. Gonna put back parallel fetch and just remap after active tab finishes

Comment thread pkg/jira/client.go Outdated
Comment on lines +485 to +498
payload := fields
if value, ok := fields[sprintFieldAlias]; ok {
resolved := c.SprintFieldID()
if resolved != sprintFieldAlias {
payload = make(map[string]any, len(fields))
for key, current := range fields {
if key == sprintFieldAlias {
continue
}
payload[key] = current
}
payload[resolved] = value
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously this method was a thin passthrough, now it does quite a bit. Honestly I got a bit lost here. Would you like to walk me through so I can learn what actually happens? It feels a bit magical.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jira on some versions can't work with just 'sprint' field so we need to identify real sprint field (like customfield_10020) and replace pretty field with real one before sending. Maybe there is more elegant solution, I just brute-forced it though

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're asking about the technical side, we just identify sprint field, if it isn't 'sprint' we replace field map with new one, which copies previous but skips 'sprint' and adds detected sprint field

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great, if we could move the intent a bit closer to the actual code (or the other way around). That you need to replace a hard-coded field by a custom-defined field of a specific format is intent which I prefer to have well encapsulated and documented to avoid surprising the reader of the code (which often is my future me 😉 ).

@textfuel textfuel marked this pull request as draft April 18, 2026 14:29
@textfuel
Copy link
Copy Markdown
Owner Author

@seflue thanks for the review! Really nice catches. I'll try to be more active next week, so I think I'll resolve this one by then

@textfuel textfuel marked this pull request as ready for review April 24, 2026 10:04
@textfuel textfuel requested a review from seflue April 24, 2026 10:04
Comment thread pkg/jira/client.go
Comment on lines +300 to +302
if sprintField == sprintFieldAlias {
fields += ",customfield_10010,customfield_10020"
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a small nit: a one-liner explaining the ids would help. Currently reads like magic constants.

Suggested change
if sprintField == sprintFieldAlias {
fields += ",customfield_10010,customfield_10020"
}
// Default sprint custom-field ids: 10020 (Cloud), 10010 (older Server/DC).
if sprintField == sprintFieldAlias {
fields += ",customfield_10010,customfield_10020"
}

Copy link
Copy Markdown
Contributor

@seflue seflue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for considering my suggestions. Looks good to me. I just had a small additional comment, but no blocker.

@textfuel textfuel merged commit c476e2b into main May 3, 2026
2 checks passed
@textfuel textfuel deleted the sprint-field-discovery branch May 3, 2026 08:22
tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request May 8, 2026
This MR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [textfuel/lazyjira](https://github.com/textfuel/lazyjira) | minor | `v2.9.0` → `v2.13.0` |

MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot).

**Proposed changes to behavior should be submitted there as MRs.**

---

### Release Notes

<details>
<summary>textfuel/lazyjira (textfuel/lazyjira)</summary>

### [`v2.13.0`](https://github.com/textfuel/lazyjira/releases/tag/v2.13.0)

[Compare Source](textfuel/lazyjira@v2.12.0...v2.13.0)

#### Changelog

- [`16a6dd6`](textfuel/lazyjira@16a6dd6) Merge branch 'hotfixes-of-demo-check'
- [`11ba667`](textfuel/lazyjira@11ba667) Reach parents and children from the task list ([#&#8203;68](textfuel/lazyjira#68))
- [`26afc79`](textfuel/lazyjira@26afc79) Show parent-link children in Sub tab on Cloud ([#&#8203;66](textfuel/lazyjira#66))
- [`d5f63df`](textfuel/lazyjira@d5f63df) changelog
- [`34ab861`](textfuel/lazyjira@34ab861) changelog
- [`5dbd650`](textfuel/lazyjira@5dbd650) codowners adjustments
- [`6aa089d`](textfuel/lazyjira@6aa089d) fixes
- [`d22816b`](textfuel/lazyjira@d22816b) release v2.13.0

***

**Full changelog:** [CHANGELOG.md](https://github.com/textfuel/lazyjira/blob/main/CHANGELOG.md)

### [`v2.12.0`](https://github.com/textfuel/lazyjira/releases/tag/v2.12.0)

[Compare Source](textfuel/lazyjira@v2.11.1...v2.12.0)

#### Changelog

- [`6af4d03`](textfuel/lazyjira@6af4d03) Feat/task status icons ([#&#8203;64](textfuel/lazyjira#64))
- [`a19773b`](textfuel/lazyjira@a19773b) changelog
- [`4ca1ee7`](textfuel/lazyjira@4ca1ee7) release v2.12.0
- [`c476e2b`](textfuel/lazyjira@c476e2b) sprint fields ([#&#8203;48](textfuel/lazyjira#48))

***

**Full changelog:** [CHANGELOG.md](https://github.com/textfuel/lazyjira/blob/main/CHANGELOG.md)

### [`v2.11.1`](https://github.com/textfuel/lazyjira/releases/tag/v2.11.1)

[Compare Source](textfuel/lazyjira@v2.11.0...v2.11.1)

#### Changelog

- [`29ca5ae`](textfuel/lazyjira@29ca5ae) Route to tracking only on exact remote match ([#&#8203;62](textfuel/lazyjira#62))
- [`5516244`](textfuel/lazyjira@5516244) release v2.11.1

***

**Full changelog:** [CHANGELOG.md](https://github.com/textfuel/lazyjira/blob/main/CHANGELOG.md)

### [`v2.11.0`](https://github.com/textfuel/lazyjira/releases/tag/v2.11.0)

[Compare Source](textfuel/lazyjira@v2.10.2...v2.11.0)

#### Changelog

- [`382fe06`](textfuel/lazyjira@382fe06) changelog
- [`23b6d49`](textfuel/lazyjira@23b6d49) feat: add catppuccin theme support ([#&#8203;59](textfuel/lazyjira#59))
- [`ced4b10`](textfuel/lazyjira@ced4b10) readme v2 path
- [`9d434fc`](textfuel/lazyjira@9d434fc) release v2.11.0
- [`e570d8b`](textfuel/lazyjira@e570d8b) some dev QOL ([#&#8203;60](textfuel/lazyjira#60))

***

**Full changelog:** [CHANGELOG.md](https://github.com/textfuel/lazyjira/blob/main/CHANGELOG.md)

### [`v2.10.2`](https://github.com/textfuel/lazyjira/releases/tag/v2.10.2)

[Compare Source](textfuel/lazyjira@v2.10.1...v2.10.2)

#### Changelog

- [`94fafd7`](textfuel/lazyjira@94fafd7) fix of v2 version naming ([#&#8203;57](textfuel/lazyjira#57))
- [`86f48fe`](textfuel/lazyjira@86f48fe) release v2.10.2

***

**Full changelog:** [CHANGELOG.md](https://github.com/textfuel/lazyjira/blob/main/CHANGELOG.md)

### [`v2.10.1`](https://github.com/textfuel/lazyjira/releases/tag/v2.10.1)

[Compare Source](textfuel/lazyjira@v2.10.0...v2.10.1)

#### Changelog

- [`fe34157`](textfuel/lazyjira@fe34157) changelog
- [`5728a57`](textfuel/lazyjira@5728a57) pkg/config: accept string shorthand for the projects list ([#&#8203;53](textfuel/lazyjira#53))
- [`cce7280`](textfuel/lazyjira@cce7280) release v2.10.1

***

**Full changelog:** [CHANGELOG.md](https://github.com/textfuel/lazyjira/blob/main/CHANGELOG.md)

### [`v2.10.0`](https://github.com/textfuel/lazyjira/releases/tag/v2.10.0)

[Compare Source](textfuel/lazyjira@v2.9.0...v2.10.0)

#### Changelog

- [`45afec5`](textfuel/lazyjira@45afec5) Add configurable custom commands ([#&#8203;42](textfuel/lazyjira#42))
- [`851bfa1`](textfuel/lazyjira@851bfa1) Context-sensitive preview for Sub/Lnk tabs ([#&#8203;55](textfuel/lazyjira#55))
- [`f546bc7`](textfuel/lazyjira@f546bc7) Make maxResults configurable globally and per tab ([#&#8203;45](textfuel/lazyjira#45))
- [`672fb6d`](textfuel/lazyjira@672fb6d) changelog
- [`67bdf1a`](textfuel/lazyjira@67bdf1a) changelog
- [`d14fb0e`](textfuel/lazyjira@d14fb0e) perl for make release
- [`b04c7a8`](textfuel/lazyjira@b04c7a8) release v2.10.0

***

**Full changelog:** [CHANGELOG.md](https://github.com/textfuel/lazyjira/blob/main/CHANGELOG.md)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjguNSIsInVwZGF0ZWRJblZlciI6IjQzLjE2OC41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiLCJhdXRvbWF0aW9uOmJvdC1hdXRob3JlZCIsImRlcGVuZGVuY3ktdHlwZTo6bWlub3IiXX0=-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot select Sprint

2 participants