Repository navigation
[COMMS-1071] Find or create labels when the work package is saved - #26270
Conversation
A work package can carry label names next to label ids. The names are resolved or created inside the save transaction, so an abandoned form leaves no label behind and the contract rejects names the user may not create before anything is written.
A `_links.labels` entry with a null href and a title is a label to create on save; the form endpoints echo it back without persisting.
The autocompleter keeps a new label as a local pending item and the work package payload sends it as a titled link without href.
The form echo and the contract looked a label up per name; both now match all names at once with the case folding done in Postgres.
Deploying openproject with ⚡ PullPreview
|
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
The create service ignored the result of its unvalidated save, so an aborted label creation needed a raise to avoid a silent success and ended in a 500. Honouring the save result turns it into the same 422 the update path gives.
Names that already exist are filtered in one query, so uniqueness and the permission lookup no longer run once per pending name on every form refresh.
A pending label and a persisted label with the same name are the same label, since the backend identifies labels by their lower-cased name.
Without a labels link on the reference the base changeset writes raw resources into the payload and the pending titles were lost.
Running the model's own validation keeps the contract in step with Label; the cap bounds how many labels one save can create.
Every item carries a name, so the href fallback could never run.
POST /api/v3/labels is hidden behind the flag, so saving a work package must not create labels or change labelings either.
Creating another aggregate's records as the acting user is service work; the model only refuses to save while names are unresolved. Names are created in a stable order so concurrent saves cannot deadlock.
API clients need the permission, length and per-request limits to handle the 422 responses.
A shared work package grants edit rights without the project-wide right to create labels, so updates and their form must reject new names.
Copying only the error symbols dropped the length limit, so rendering a too-long label name raised instead of answering with a 422.
FindOrCreateLabels says that typed names become existing or new labels; pending and resolve left the reader guessing.
Labels are now created when the work package saves, so a bad name must come back as a field error in split view, the table and the create form.
No permission is called create labels; the check is edit rights in any project, the same as POST /api/v3/labels.
Pending said nothing about what the names are for; the checks validate label names and pick out the ones that would create labels.
The label ids and names are what the next save assigns, which pending did not say.
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
Mirrors Attachments::ValidateReplacements so the work package contract does not carry label rules inline.
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
Labels are resolved on their own line, like attachments, so the save no longer reads as depending on them; the post-save steps move out to keep the method within the complexity limit.
The after_validate hook is the slot for work that needs a passed contract but no save yet, so persist stays the plain save.
Matches the server, which treats a title-only label link as a new record.
brunopagno
left a comment
There was a problem hiding this comment.
To me this looks pretty good. I think this is the better approach. I really appreciate the effort of making it 🙇
| module Labelable | ||
| extend ActiveSupport::Concern | ||
|
|
||
| MAX_LABEL_NAMES_PER_SAVE = 50 |
There was a problem hiding this comment.
This feels like a check we would add after something broke 😅
Maybe it's good that we're preemptive. I don't disagree with validating the amount.
Any special reason for 50?
There was a problem hiding this comment.
Arbitrary limit tbh, not sure it's practical to allow 1000 labels? 😄 Happy to drop it though 👍🏾
There was a problem hiding this comment.
The UI does not really allow someone to add 1000 labels. But maybe the API would. I guess it's a problem that is a bit too abstract, I would not expect users to "abuse" this system too much. But I could be wrong
In any case, I think it's fine the way it is. My initial gut feeling was to make the limit stricter, not more lenient 😅
50 is fine 👍
|
Verbal Ok from Henriette on the new pattern, merging 👍🏾 |
Ticket
https://community.openproject.org/wp/81938
What are you trying to accomplish?
Make labels typed into the form now stay local until the work package is saved. The autocompleter keeps them as pending items, the payload sends each one as a
_links.labelsentry with a nullhrefand the name astitle, and the label is found or created in the same transaction as the work package.{ "_links": { "labels": [ { "href": "/api/v3/labels/130" }, { "href": "/api/v3/labels/131" }, { "href": "/api/v3/labels/132" }, { "href": null, "title": "Needs triage" } // new label, created when the work package saves ] }, "_meta": { "validateCustomFields": true }, "lockVersion": 1 }Screen.Recording.2026-10-27.at.6.31.34.PM.mov
What approach did you choose and why?
Creating labels is service work, so the work package create and update services find or create the named labels as the acting user inside the transaction they already open; a failed save leaves no label behind. Names are created in a stable order so that two saves introducing the same names cannot deadlock on the unique name index, and a label created concurrently under the same name is reused instead of failing the save. The model itself only refuses to save while names are still unresolved, so nothing can drop them silently.
AI involvement
Collaborative - AI generated a substantial part of the code; I reviewed and understand every line.
Merge checklist
Closes #26187