[OP-19606] Preserve move form values on refresh#24062
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses the bulk move/copy work package form losing already-entered values (assignee, dates, priority, custom fields, etc.) when the form refreshes after changing target project/type. It does so by round-tripping the currently selected form values back into the move form component (render-only), while centralizing and hardening the permitted parameter handling used by both refresh and execution.
Changes:
- Preserve user-entered move/copy form values across Turbo Stream refreshes by passing
selected_valuesintoWorkPackages::Moves::FormComponent. - Centralize move form value permitting via
PermittedParams#move_work_package_form_valuesand guard against malformed custom field params. - Move form option derivation (types/versions/statuses) and “type unavailable in target project” warning logic into the component, and expand test coverage across request/component/feature specs.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| spec/requests/work_packages/moves_spec.rb | Adds request coverage for preserving form selections and handling malformed custom field params during refresh. |
| spec/models/permitted_params_spec.rb | Adds specs for move_work_package normalization and new move_work_package_form_values permitting/guarding. |
| spec/features/work_packages/bulk/move_work_package_spec.rb | Adds JS feature spec ensuring form values persist after a project-change-triggered refresh. |
| spec/components/work_packages/moves/form_component_spec.rb | Adds component specs for selected values preservation and warning rendering behavior. |
| app/views/work_packages/moves/new.html.erb | Switches to rendering a pre-built controller-provided component instance. |
| app/models/permitted_params.rb | Introduces move_work_package_form_values and tightens custom field value handling against malformed shapes. |
| app/helpers/custom_fields_helper.rb | Extends bulk-edit custom field rendering helper to accept a preselected value and apply it to inputs. |
| app/controllers/work_packages/moves_controller.rb | Builds and reuses the move form component with selected_values, turbo_stream_url, and current_user. |
| app/components/work_packages/moves/form_component.rb | Computes types/versions/statuses internally and introduces warning logic for unavailable types in target project. |
| app/components/work_packages/moves/form_component.html.erb | Applies selected values to selects/date pickers/custom fields and uses the new warning predicate. |
Keeps the submitted move-form field values when the Turbo Stream refresh rebuilds the form after project or type changes. Passes the refresh request values into the streamable form component for rendering only, while leaving the persisted move/copy attributes on the existing permitted params path. https://community.openproject.org/wp/OP-19606
Derives the type list, selected target type, versions, statuses, and unavailable-type warning inside FormComponent from its inputs instead of precomputing render-only state in the controller. Passes `current_user` explicitly so status availability stays a real dependency, and avoids recomputing form-only collections on create paths where the form is never rendered. Use an indexed `find_by(id:)` lookup for the selected target project instead of loading the whole `allowed_target_projects_on_move` relation only to detect one project. Scopes required custom-field options to the target project while rendering the refreshed move form.
Add `PermittedParams#move_work_package_form_values` for form refresh rendering so the move controller does not maintain the duplicate `MOVE_FORM_VALUE_PARAMS` allow-list. Reuse it from `#move_work_package`, then merge the submit path keys normalized from `new_project_id`, `new_type_id`, and `notes` for persistence. Cover both move submit params and refresh-form params, including custom field value filtering.
Treat array-shaped `custom_field_values` as absent before the shared custom field helper filters permitted values. This avoids `Array#each_value` failures in both move submit params and move form refresh params while keeping hash-shaped `custom_field_values` unchanged.
The unavailable-type check compared the selected new type against the target project's types, but that type is always chosen from them, so the branch was dead. It now checks the moved work packages' current types, matching the warning copy. Also fixes `ancestor_types_missing_in_target?`, which called `work_packages.select(:id)` as a query subquery; that only works when `work_packages` is an `ActiveRecord::Relation`. The predicate now runs this branch unconditionally, so specs that pass a plain array (this file's convention) surfaced the crash. Switched to `.map(&:id)` to match the sibling method and work with either.
`selected_value` fell back between symbol and string keys to bridge test hashes and controller `Parameters`. Converting `@selected_values` to indifferent access once lets the lookup use a single key.
2221d90 to
b0359da
Compare
480c637 to
b731557
Compare
The unavailable-type check looked at the ancestor side of each hierarchy row, i.e. the moved work packages' own types, so any child carried along with its parent never triggered the warning.
The notes value only reaches the refresh request via the CKEditor flush, then must survive re-rendering into a fresh editor instance; no end-to-end spec exercised that path.
|
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. |
EinLama
left a comment
There was a problem hiding this comment.
Found only a handful of smaller issues. The rest looks good to me! 😄 I agree that it's worth updating the translation to make the error message clearer.
dombesz
left a comment
There was a problem hiding this comment.
Looks good, just left a few minor comments on code cleanup.
Clarifies that the method returns the target project's types, not the source project's, and aligns the name with the sibling readers `available_versions` and `available_statuses`.
Normalizes `new_type_id`/`new_project_id` inside `move_work_package_form_values` so the controller no longer passes the type separately and the component reads it from `selected_values`.
The refresh endpoint is a constant collection route, so the component derives it via `url_helpers` instead of receiving it from the controller.
The status options were derived from the user's roles in the source project, although the chosen status is applied in the target project. Drive-by fix of pre-existing behavior, surfaced during review.
The warning also fires when only a descendant's type is missing in the target project, so the copy now covers both cases and points at enabling the missing types.
EinLama
left a comment
There was a problem hiding this comment.
Looks great, thanks for addressing my points 👏 ![]()
dombesz
left a comment
There was a problem hiding this comment.
Looks good, thanks Alex!
Note
This PR is stacked on #23938 and #24061. Please review/merge those PRs first.
Ticket
https://community.openproject.org/wp/OP-19606
What are you trying to accomplish?
On the move/copy work package form, changing the target project or type triggers a form refresh that wiped every previously filled field (dates, assignee, priority, custom fields, …) — even when those fields were still applicable in the target project.
Screen recording
Screen.Recording.2026-07-02.at.12.27.12.mov
What approach did you choose and why?
WorkPackages::Moves::FormComponentasselected_values(rendering only), so the refreshed form keeps the user's current selections; the persisted move/copy attributes stay on the existing permitted-params path.PermittedParams#move_work_package_form_values, shared by the refresh and the move itself; malformed custom-field params are guarded.current_userinjected) and warns when a moved work package's current type does not exist in the target project.Review follow-ups
types→available_typesand let the component compute its own refresh URL.new_type_id/new_project_idare now normalized insidePermittedParams#move_work_package_form_valuesinstead of being passed separately.Important
Note for QA: the move/copy form's status dropdown now lists statuses from the user's workflows in the target project (previously: source project). No visible change for admins; non-admin users with differing roles between source and target will see a different status list after picking a target project.
Stacked PRs (split per work package)
Stacks on #23938 ([SC-272] streamable form component + POST refresh), which is based on #24061 ([OP-19677] — preserving the Budget field relies on
budget_idbeing permitted). Merge order: #24061 → #23938 → this PR.Merge checklist