Skip to content

feat: per-group label pulldowns and named label chips - #1

Merged
thestumonkey merged 1 commit into
planeforkfrom
feat/label-groups-in-work-item-form
Aug 23, 2026
Merged

thestumonkey merged 1 commit into
planeforkfrom
feat/label-groups-in-work-item-form

Conversation

@thestumonkey

Copy link
Copy Markdown
Member

Description

Two changes to how labels are picked and shown.

1. One pulldown per label group when creating a work item

Plane already models a "label group" as a top-level label that has children (parent points at the group). Previously the work item form had a single Labels dropdown that listed every label with group headers inside it.

Now IssueLabelSelect renders:

  • one dropdown per group, named after the group, listing only that group's labels
  • one final "Labels" dropdown for labels that have no group

Implementation notes:

  • Each dropdown owns only its own label ids. onChange strips just those ids from the value and appends the new ones, so a selection in one group can never clobber another group's picks (or a label that isn't in the project list).
  • WorkItemLabelSelectBase gained a flat prop (render options as one plain list, no group headers) and a placeholder prop (button text when empty). The option JSX was extracted into a LabelOption component so both modes share it.
  • Creating a label from inside a group dropdown sets parent to that group.
  • Labels are now fetched on mount rather than only on first dropdown open — the group pulldowns can't be rendered until the labels are known.
  • Projects with no groups get exactly one dropdown, i.e. the previous behaviour.

2. Displays show label names, not "N Labels"

Several places collapsed to a 2 Labels / 3 Labels pill once there were more labels than maxRender. They now render the actual label chips (colour dot + name, truncated) followed by a +N overflow chip. The full comma-separated list is still in the tooltip.

Files: ui/labels-list.tsx, issues/label.tsx, web issue-layouts/properties/labels.tsx, space issue-layouts/properties/labels.tsx.

Also added two file-level oxlint-disable comments in select/base.tsx for pre-existing jsx_a11y warnings on the Combobox wrapper and the "add label" <p onClick> — these already existed in the file and the pre-commit hook runs with --deny-warnings.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Feature (non-breaking change which adds functionality)
  • Improvement (change that would cause existing functionality to not work as expected)
  • Code refactoring
  • Performance improvements
  • Documentation update

Screenshots and Media (if applicable)

Test Scenarios

Verified: tsc --noEmit and oxlint --deny-warnings pass on all touched files. Not yet exercised in a running app — the scenarios below still need a manual pass.

  1. In a project with label groups, open New work item and confirm one dropdown appears per group plus a trailing "Labels" dropdown.
  2. Pick labels in two different groups; confirm both selections survive and both appear on the created work item.
  3. Deselect a label in one group; confirm the other group's labels are untouched.
  4. As a project admin, type a new label name inside a group dropdown and press Enter; confirm the label is created under that group (check Project Settings → Labels).
  5. In a project with no label groups, confirm the form shows a single "Labels" dropdown behaving as before.
  6. Open New work item from a workspace-level view (where project labels may not be preloaded) and confirm the group dropdowns appear without having to open a dropdown first.
  7. Put 4+ labels on a work item and check the spreadsheet, list, kanban and calendar layouts show label names plus a +N chip rather than "4 Labels"; hover to confirm the tooltip lists all of them.
  8. Check the same work item on a published (space) view.

References

No linked work item.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T68HuqHfZwiS1BuA4injJN

Work item creation now renders one dropdown per label group (a top-level
label that has children), plus a final "Labels" dropdown for ungrouped
labels. Each dropdown owns only its own label ids, so a selection in one
group never clobbers another. Labels created from inside a group dropdown
are created as children of that group.

Displays that previously collapsed to "N Labels" now render the actual
label chips (name + colour dot) with a "+N" overflow chip; the full list
stays in the tooltip.

- select/base.tsx: add `placeholder` and `flat` props, extract LabelOption
- select/dropdown.tsx: build group/ungrouped scopes, fetch labels on mount
- ui/labels-list.tsx, issues/label.tsx, issue-layouts/properties/labels.tsx,
  space properties/labels.tsx: render names instead of a count

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T68HuqHfZwiS1BuA4injJN
@github-actions

Copy link
Copy Markdown

React Doctor found 5 new issues in 2 files · 5 warnings · score 91 / 100 (Great) · 3 fixed · vs planefork

5 warnings

core/components/issues/issue-layouts/properties/labels.tsx

  • ⚠️ L98 Array lookup inside a loop js-set-map-lookups
  • ⚠️ L245 Chained array iterations js-combine-iterations

core/components/issues/select/dropdown.tsx

  • ⚠️ L63 Chained array iterations js-combine-iterations
  • ⚠️ L67 Chained array iterations js-combine-iterations
  • ⚠️ L70 Chained array iterations js-combine-iterations

Reviewed by React Doctor for commit e653988. See inline comments for fixes.

}: LabelSummaryProps) {
const { t } = useTranslation();
// labels the work item actually has, in project order
const selectedLabels = projectLabels?.filter((l) => value.includes(l?.id)) ?? [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/js-set-map-lookups (warning)

This scales poorly because array.includes() inside a loop scans the whole list every time. Use a Set for constant-time lookups.

Fix → Use a Set or Map when you check for the same items over and over. Array.includes/find scans the whole list each time

Docs

// A "group" is a label that has children. Its children are the selectable options.
// Everything else with no parent is a plain, ungrouped label.
const labels = (projectLabelIds ?? []).map((id) => getLabelById(id)).filter((l): l is IIssueLabel => !!l);
const groups = labels

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/js-combine-iterations (warning)

This loops over your list twice because .filter().map() makes two passes, so do it in one pass with .reduce() or a for...of loop

Fix → Combine .map().filter() style chains into one pass with .reduce() or a for...of loop, so you only loop over the list once

Docs

.filter((l) => !l.parent && labels.some((child) => child.parent === l.id))
.map((group) => ({
group,
childIds: labels.filter((child) => child.parent === group.id).map((child) => child.id),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/js-combine-iterations (warning)

This loops over your list twice because .filter().map() makes two passes, so do it in one pass with .reduce() or a for...of loop

Fix → Combine .map().filter() style chains into one pass with .reduce() or a for...of loop, so you only loop over the list once

Docs

childIds: labels.filter((child) => child.parent === group.id).map((child) => child.id),
}));
const groupIds = new Set(groups.map((g) => g.group.id));
const ungroupedLabelIds = labels.filter((l) => !l.parent && !groupIds.has(l.id)).map((l) => l.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/js-combine-iterations (warning)

This loops over your list twice because .filter().map() makes two passes, so do it in one pass with .reduce() or a for...of loop

Fix → Combine .map().filter() style chains into one pass with .reduce() or a for...of loop, so you only loop over the list once

Docs

@thestumonkey
thestumonkey merged commit 628c800 into planefork Aug 23, 2026
9 of 10 checks passed
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.

1 participant