Skip to content

poc: Multiple Categories - #24620

Draft
thykel wants to merge 14 commits into
observed_versions_PoCfrom
categories_PoC
Draft

poc: Multiple Categories #24620
thykel wants to merge 14 commits into
observed_versions_PoCfrom
categories_PoC

Conversation

@thykel

@thykel thykel commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

AI-generated prototype.
Previous PR:

Ticket

What are you trying to accomplish?

Screenshots

What approach did you choose and why?

Merge checklist

  • Added/updated tests
  • Added/updated documentation in Lookbook (patterns, previews, etc)
  • Tested major browsers (Chrome, Firefox, Edge, ...)

thykel and others added 14 commits August 5, 2026 20:28
Introduces the experimental work_package_multiple_categories feature flag
together with the user-facing setting of the same name, and
Setting::WorkPackageMultipleCategories as the single predicate call sites
should ask.

Mirrors Setting::WorkPackageMultipleVersions so the phased rollout (adding
the admin switch, then dropping the flag) only ever touches that method.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds work_package_categories as the future home of a work package's
categories, backfilled from the deprecated work_packages.category_id column.

Unlike work_package_versions the table needs no kind discriminator: a work
package holds exactly one set of categories. Both foreign keys cascade,
because categories are removed with their project through a
dependent: :delete_all that skips callbacks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WorkPackage::Categories mirrors WorkPackage::Versions: a name-ordered
categories association, a category_ids_replacements accessor the
contracts/services flow can validate before anything is written, an
effective_categories reader that accounts for a pending override, and an
after_save hook that keeps the deprecated category_id column in sync with the
first category. Ordering by name means #category and #categories.first can
never disagree.

Destroying a category now moves or prunes its join rows and recomputes the
deprecated column for work packages that keep another category, which neither
dependent: :nullify nor the bulk reassignment could get right on their own.

#default_assign and .by_category read the set rather than the single column,
so a work package with several categories takes its default assignee from the
primary one and is counted under each of its categories.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The set-attributes service routes a category_ids attribute through the
replacements accessor rather than the association writer generated by
has_many, which would write to the database before the contract can validate.
A project change reassigns the whole set to the equally named categories of
the new project instead of just the single one.

The contract validates assignability and enforces the two constraints of the
transition: categories holds at most one entry unless the multiple categories
feature is enabled, and a category_id the user writes has to be part of the
set they write.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a categories link collection and schema attribute alongside the now
deprecated single-valued category, following targetVersions: the attribute
always reads and writes a collection, and while the multiple categories
feature is inactive it keeps the "Category" label and announces the
single-value restriction via options.multiple.

The categories enter the work package cache checksum as an aggregated
correlated subquery, since a has_many cannot be expressed by the
left_joins/pluck design the checksum is built on. The neighbouring versions
subquery gains the same CONCAT_WS separator, so that no two distinct tuples
can concatenate to the same string and collide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Only one of the two category attributes is offered at a time, matching the
query columns and the versions handling: categories with the multiple
categories feature enabled, the deprecated single category without it. A saved
configuration holding either name renders as whichever one the current feature
state exposes, and the new gate joins the form attributes cache key.

The stored-to-offered substitution now covers both attribute pairs through one
map instead of a version-specific pair of locals.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Snapshots a work package's categories into work_package_category_journals,
diffs the whole set as a single value and renders it as one line in the
activity. Existing journals are backfilled from work_package_journals'
category_id, so no work package journals a spurious "categories set" change on
its next save.

While the deprecated category_id column mirrors the set, a category change
diffs under both keys; only the set representation is exposed, and the journal
entry keeps the "Category" label until the multiple categories feature is
enabled.

JoinedVersions is renamed to JoinedAssociation: nothing in it was
version-specific, and the categories formatter needs exactly the same
rendering.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Introduces the categories column, offered instead of the deprecated single
category once the multiple categories feature is enabled. Both sort and group
through the join table, so the category set is the group key and a work package
holding several categories forms a group of its own.

The category filter matches against the join table as well, so it finds work
packages by any of their categories. Unlike versions there is no second filter
to add: categories form a single set, and the API name derived from the
existing key is the one the new attribute would want anyway. Only the label
follows the feature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Formats the categories column as the joined category names in CSV, XLS and PDF
exports, and the deprecated category column from the same data. The
workPackageValue macro maps the deprecated attribute to the set it is replaced
by, alongside the equivalent version mapping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bulk edit form writes through category_ids, with the feature only toggling
whether the select accepts a single value or several. Like target_version_ids,
the array parameter is normalized separately from the generic scalar
"none"/blank transforms.

TargetVersionNormalization becomes MultiValueAttributeNormalization, since
nothing in it was version-specific and both array parameters need the same
magic-value handling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a Categories keyword that assigns the whole comma-separated set. Both it
and the deprecated Category keyword are always read so neither leaks into the
description, and the plural one wins when both are given. The single-value
limit stays with the contract, so the full list is parsed regardless of the
feature state.

Unlike versions the category keyword is not overridable by default, so the
categories keyword is overridable exactly when the deprecated one is. The
default overridable attributes move into a constant, keeping assign_options
below the complexity threshold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Duplicating a work package carries its whole category set over. A project copy
points the copied work packages at the categories copied by
Projects::Copy::CategoriesDependentService rather than relying on the
reassign-by-name fallback, which cannot tell equally named categories apart.

The deprecated category_id is written alongside so it does not contradict the
set, as for the versions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Notification mails, the work package summary report and the automatic subject
tokens all read the category set instead of the deprecated column, listing
every category once the multiple categories feature is enabled and keeping the
single "Category" label and value until then.

The summary heading stays singular either way, since it names a single category
per section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The categories attribute always reads and writes a collection, but the schema
restricts it to a single value while the multiple categories feature is
inactive. In that mode the field mimics the single select it stands in for: an
explicit "-" option, no save/cancel controls, saving right on selection.

That behaviour is extracted from the versions field into a shared
SingleOrMultiSelectEditFieldComponent, since it belongs to any attribute
replacing a deprecated single-valued one. Categories add nothing on top: unlike
versions they are never shared across projects and cannot be created from
within the field.

Displaying the attribute follows []Version throughout: the resources field in
tables, one line per category in the single view, and a comma-separated list
under the singleline layout the macro component maps the deprecated attribute
to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant