Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1d196ed
Allow only numbers for logged units
HDinger Mar 2, 2026
1f931e3
Catch a case where the service is loaded before the turbo event has f…
HDinger Mar 30, 2026
fa2a07a
send a non default string if sprints should be open by default
ulferts Apr 1, 2026
7149d3a
update label on checkbox
ulferts Apr 1, 2026
1f78f5a
use preference service inside user update service - for backlogs as well
ulferts Apr 2, 2026
0e2de3f
remove hook whose call was removed in eb888b7d2e8
ulferts Apr 2, 2026
8cdb38c
allow empty task color in schema
ulferts Apr 2, 2026
c245ea5
Merge remote-tracking branch 'origin/release/17.3' into bug/73464-use…
ulferts Apr 2, 2026
8f91efd
fix field name
ulferts Apr 2, 2026
4b33d55
Merge remote-tracking branch 'origin/release/17.3' into bug/73464-use…
ulferts Apr 2, 2026
f96603e
stabilize flickering spec
ulferts Apr 2, 2026
56b4c9e
Fix participants ordering
mrmir Apr 2, 2026
3205dd5
Make participants dialog banner more clear
mrmir Apr 2, 2026
8e2e79c
Remove invited members from participant selector list
mrmir Apr 2, 2026
898fb0e
Remove dead sorting code
mrmir Apr 2, 2026
741d8d2
Merge pull request #22145 from opf/bug/72685-user-can-input-anything-…
HDinger Apr 7, 2026
ad73509
Merge pull request #22592 from opf/bug/70388-sidebar-menu-button-unre…
HDinger Apr 7, 2026
39d38f8
Merge pull request #22653 from opf/bug/73196-meeting-participants-sor…
mrmir Apr 7, 2026
72748bf
[#73464] Merge branch 'release/17.3' into bug/73464-user-cannot-save-…
EinLama Apr 7, 2026
af7abae
Merge pull request #22641 from opf/bug/73464-user-cannot-save-their-n…
EinLama Apr 7, 2026
fb3d8f9
[#73467] Rename "Sprint planning" to "Backlogs and Sprints"
EinLama Mar 30, 2026
e5cccb9
[#73467] Rename "Backlog and Sprints" to "Backlog"
EinLama Apr 2, 2026
948aa28
[#73467] Use "Backlog and Sprints" for some translations/labels
EinLama Apr 2, 2026
be68bd9
[#73467] "Backlog and Sprints" -> "Backlog and sprints"
EinLama Apr 7, 2026
388c947
[#73467] Use renamed spec helper page in new files
EinLama Apr 7, 2026
138224e
Prevent loading the default value of all custom options when loaded a…
oliverguenther Apr 7, 2026
2da809e
Merge branch 'release/17.2' into release/17.3
openprojectci Apr 7, 2026
647788d
[#73446] Sum of work packages and their story points in sprint header…
dombesz Mar 30, 2026
992444b
Merge pull request #22633 from opf/bug/73446-sum-of-work-packages-and…
dombesz Apr 7, 2026
ac7be25
update locales from crowdin [ci skip]
openprojectci Apr 8, 2026
1d4580f
Merge branch 'dev' into merge-release/17.3-20260408042316
myabc Apr 8, 2026
eb9f8f8
Reapply changes from #22641
EinLama Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/controllers/my_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,10 @@ def handle_email_changes
end

def user_params
permitted_params.my_account_settings.to_h
# The Users::UpdateService updates the user's pref using the UserPreferences::UpdateService
# which has a contract/schema applied to the values which is why it is ok
# to blindly allow all scalar values in pref.
permitted_params.user.to_h.merge(params.permit(pref: {}))
end

def update_global_notification_setting(update_params)
Expand Down
10 changes: 8 additions & 2 deletions app/models/custom_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,15 @@ def check_searchability
true
end

def default_value
def default_value # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity
if list?
ids = custom_options.where(default_value: true).pluck(:id).map(&:to_s)
# Use loaded association data when available to avoid N+1 queries.
# .where().pluck() always hits the database, bypassing eager-loaded data.
ids = if custom_options.loaded?
custom_options.select(&:default_value).map { |o| o.id.to_s }
else
custom_options.where(default_value: true).pluck(:id).map(&:to_s)
end

if multi_value?
ids
Expand Down
4 changes: 0 additions & 4 deletions app/models/permitted_params.rb
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,6 @@ def placeholder_user
params.require(:placeholder_user).permit(*self.class.permitted_attributes[:placeholder_user])
end

def my_account_settings
user.merge(pref:)
end

def user_register_via_omniauth
permitted_params = params
.require(:user)
Expand Down
5 changes: 3 additions & 2 deletions app/services/users/update_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ def before_perform(_service_result)
def persist(_service_result)
service_result = super

if service_result.success?
service_result.success = model.pref.save
if service_result.success? && params[:pref].present?
preference_service = UserPreferences::UpdateService.new(user:, model: model.pref)
service_result.add_dependent!(preference_service.call(params[:pref]))
end

service_result
Expand Down
18 changes: 13 additions & 5 deletions frontend/src/app/core/main-menu/main-menu-toggle.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ export class MainMenuToggleService {

private htmlNode = document.getElementsByTagName('html')[0];

private mainMenu = document.querySelector<HTMLElement>('#main-menu')!; // main menu, containing sidebar and resizer
private get mainMenu():HTMLElement|null {
return document.querySelector<HTMLElement>('#main-menu');
}

// Notes all changes of the menu size (currently needed in wp-resizer.component.ts)
private changeData = new BehaviorSubject<number|undefined>(undefined);
Expand All @@ -69,7 +71,8 @@ export class MainMenuToggleService {
}

public initializeMenu():void {
if (!this.mainMenu) {
const mainMenu = this.mainMenu;
if (!mainMenu) {
return;
}

Expand All @@ -80,7 +83,7 @@ export class MainMenuToggleService {
this.wasCollapsedByUser = menuCollapsed;

if (!this.elementWidth) {
this.saveWidth(this.mainMenu.offsetWidth);
this.saveWidth(mainMenu.offsetWidth);
} else if (menuCollapsed) {
this.closeMenu();
} else {
Expand Down Expand Up @@ -129,7 +132,9 @@ export class MainMenuToggleService {
// This needs to be called after AngularJS has rendered the menu, which happens some when after(!) we leave this
// method here. So we need to set the focus after a timeout.
setTimeout(() => {
const firstVisibleMenuItem = queryVisible('[class*="-menu-item"]', this.mainMenu)[0];
const mainMenu = this.mainMenu;
if (!mainMenu) return;
const firstVisibleMenuItem = queryVisible('[class*="-menu-item"]', mainMenu)[0];
firstVisibleMenuItem?.focus();
}, 500);
}
Expand All @@ -151,8 +156,11 @@ export class MainMenuToggleService {
this.elementWidth = width;
}

const mainMenu = this.mainMenu;
if (!mainMenu) return;

// Apply the width directly to the main menu
this.mainMenu.style.width = `${this.elementWidth}px`;
mainMenu.style.width = `${this.elementWidth}px`;

// Apply to root CSS variable for any related layout adjustments
this.htmlNode.style.setProperty('--main-menu-width', `${this.elementWidth}px`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ import { Controller } from '@hotwired/stimulus';
import { MainMenuToggleService } from 'core-app/core/main-menu/main-menu-toggle.service';

export default class MainToggleController extends Controller {
mainMenuService:MainMenuToggleService;
mainMenuService:MainMenuToggleService|undefined;

async connect() {
await window.OpenProject.getPluginContext()
connect() {
window.OpenProject.getPluginContext()
.then((pluginContext) => pluginContext.injector.get(MainMenuToggleService))
.then((service) => {
if (!this.element.isConnected) return;
this.mainMenuService = service;
this.mainMenuService.initializeMenu();
});
})
.catch(() => { /* Do nothing */ });
}

disconnect() {
this.mainMenuService = undefined;
}

toggleNavigation(e:Event) {
this.mainMenuService.toggleNavigation(e);
this.mainMenuService?.toggleNavigation(e);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ See COPYRIGHT and LICENSE files for more details.
block: true,
align_content: :start,
underline: true,
href: sprint_planning_backlogs_project_backlogs_path(project, all: 1)
href: backlog_backlogs_project_backlogs_path(project, all: 1)
)
) { t(".show_more", count: middle_count) }
%>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= component_wrapper(tag: :section) do %>
<%= render(Primer::Beta::BorderBox.new(**@system_arguments)) do |border_box| %>
<% border_box.with_header(id: dom_target(sprint, :header)) do %>
<%= render(Backlogs::SprintHeaderComponent.new(sprint:, project:, folded: folded?, active_sprint_ids:)) %>
<%= render(Backlogs::SprintHeaderComponent.new(sprint:, project:, stories:, folded: folded?, active_sprint_ids:)) %>
<% end %>
<% if stories.empty? %>
<% border_box.with_row(data: { empty_list_item: true }) do %>
Expand Down
10 changes: 4 additions & 6 deletions modules/backlogs/app/components/backlogs/sprint_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,17 @@ class SprintComponent < ApplicationComponent
include OpTurbo::Streamable
include RbCommonHelper

attr_reader :sprint, :project, :current_user, :active_sprint_ids
attr_reader :sprint, :project, :stories, :current_user, :active_sprint_ids

def initialize(sprint:, project:, current_user: User.current, active_sprint_ids: nil, **system_arguments)
def initialize(sprint:, project:, stories: nil, current_user: User.current,
active_sprint_ids: nil, **system_arguments)
super()

@sprint = sprint
@project = project
@current_user = current_user
@active_sprint_ids = active_sprint_ids
@stories = stories || sprint.work_packages_for(project)

@system_arguments = system_arguments
@system_arguments[:id] = dom_id(sprint)
Expand All @@ -55,10 +57,6 @@ def initialize(sprint:, project:, current_user: User.current, active_sprint_ids:
)
end

def stories
sprint.work_packages.where(project:).order(:position)
end

def wrapper_uniq_by
sprint.id
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ class SprintHeaderComponent < ApplicationComponent
include Redmine::I18n
include RbCommonHelper

attr_reader :sprint, :project, :collapsed, :current_user, :active_sprint_ids
attr_reader :sprint, :project, :stories, :collapsed, :current_user, :active_sprint_ids

delegate :name, to: :sprint, prefix: :sprint

def initialize(
sprint:,
project:,
stories: nil,
folded: false,
current_user: User.current,
active_sprint_ids: nil
Expand All @@ -51,6 +52,7 @@ def initialize(

@sprint = sprint
@project = project
@stories = stories || sprint.work_packages_for(project)
@collapsed = folded
@current_user = current_user
@active_sprint_ids = active_sprint_ids
Expand All @@ -60,10 +62,6 @@ def wrapper_uniq_by
sprint.id
end

def stories
@sprint.work_packages
end

private

def show_start_sprint_action?
Expand Down
24 changes: 14 additions & 10 deletions modules/backlogs/app/controllers/rb_master_backlogs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,23 @@ class RbMasterBacklogsController < RbApplicationController
menu_item :backlogs_legacy

# With the feature flag, we have a proper menu, select the correct sub entry
current_menu_item [:sprint_planning] do
:sprint_planning
current_menu_item [:backlog] do
:backlog
end

before_action :not_authorized_on_feature_flag_inactive, only: :sprint_planning
before_action :load_backlogs, only: %i[index sprint_planning]
before_action :not_authorized_on_feature_flag_inactive, only: :backlog
before_action :load_backlogs, only: %i[index backlog]

def sprint_planning
def backlog
if turbo_frame_request?
render partial: "sprint_planning_list", layout: false
render partial: "backlog_list", layout: false
else
render :sprint_planning
render :backlog
end
end

def index
return redirect_to action: :sprint_planning if OpenProject::FeatureDecisions.scrum_projects_active?
return redirect_to action: :backlog if OpenProject::FeatureDecisions.scrum_projects_active?

if turbo_frame_request?
render partial: "list", layout: false
Expand All @@ -67,7 +67,7 @@ def details
load_backlogs

if OpenProject::FeatureDecisions.scrum_projects_active?
render :sprint_planning
render :backlog
else
render :index
end
Expand All @@ -76,7 +76,7 @@ def details

def split_view_base_route
if OpenProject::FeatureDecisions.scrum_projects_active?
sprint_planning_backlogs_project_backlogs_path(request.query_parameters)
backlog_backlogs_project_backlogs_path(request.query_parameters)
else
backlogs_project_backlogs_path(request.query_parameters)
end
Expand All @@ -89,6 +89,10 @@ def load_backlogs

if OpenProject::FeatureDecisions.scrum_projects_active?
@sprints = Agile::Sprint.for_project(@project).not_completed.order_by_date
@stories_by_sprint_id = WorkPackage
.where(sprint: @sprints, project: @project)
.order_by_position
.group_by(&:sprint_id)
@active_sprint_ids = @sprints.select(&:active?).map(&:id)
@inbox_work_packages = Backlog.inbox_for(project: @project)
else
Expand Down
4 changes: 2 additions & 2 deletions modules/backlogs/app/controllers/rb_sprints_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def create # rubocop:disable Metrics/AbcSize

if call.success?
flash[:notice] = I18n.t(:notice_successful_create)
render turbo_stream: turbo_stream.redirect_to(sprint_planning_backlogs_project_backlogs_path(@project))
render turbo_stream: turbo_stream.redirect_to(backlog_backlogs_project_backlogs_path(@project))
else
update_new_sprint_form_component_via_turbo_stream(sprint: call.result, base_errors: call.errors[:base])
respond_with_turbo_streams
Expand Down Expand Up @@ -127,7 +127,7 @@ def finish

if result.success?
flash[:notice] = I18n.t(:notice_successful_finish)
render turbo_stream: turbo_stream.redirect_to(sprint_planning_backlogs_project_backlogs_path(@project))
render turbo_stream: turbo_stream.redirect_to(backlog_backlogs_project_backlogs_path(@project))
elsif result.includes_error?(:base, :unfinished_work_packages)
show_finish_sprint_dialog
else
Expand Down
24 changes: 6 additions & 18 deletions modules/backlogs/app/forms/my/backlogs_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,33 +32,21 @@ class My::BacklogsForm < ApplicationForm
form do |f|
Comment thread
EinLama marked this conversation as resolved.
f.fieldset_group(title: helpers.t("backlogs.user_preference.header_backlogs"), mt: 4) do |fg|
unless OpenProject::FeatureDecisions.scrum_projects_active?
fg.text_field name: :task_color,
fg.text_field name: :backlogs_task_color,
label: helpers.t("backlogs.task_color"),
value: @color,
input_width: :xsmall
end

fg.check_box name: :versions_default_fold_state,
fg.check_box name: :backlogs_versions_default_fold_state,
value: DEFAULT_FOLD_STATE,
checked: default_fold_state_checked?,
label: helpers.t("backlogs.label_versions_default_fold_state"),
caption: helpers.t("backlogs.caption_versions_default_fold_state")
unchecked_value: DEFAULT_EXPAND_STATE,
label: I18n.t("activerecord.attributes.user_preference.backlogs_versions_default_fold_state"),
caption: I18n.t("backlogs.caption_sprints_default_fold_state")

fg.submit(name: :submit, label: helpers.t("backlogs.user_preference.button_update_backlogs"), scheme: :default)
end
end

DEFAULT_FOLD_STATE = "closed"

def initialize(color:, versions_default_fold_state:)
super()
@color = color
@versions_default_fold_state = versions_default_fold_state
end

private

def default_fold_state_checked?
@versions_default_fold_state == DEFAULT_FOLD_STATE
end
DEFAULT_EXPAND_STATE = "open"
end
6 changes: 5 additions & 1 deletion modules/backlogs/app/models/agile/sprint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class Sprint < ApplicationRecord
include ::Scopes::Scoped

belongs_to :project
has_many :work_packages, dependent: :nullify
has_many :work_packages, inverse_of: :sprint, dependent: :nullify
has_many :task_boards,
as: :linked,
class_name: "Boards::Grid",
Expand Down Expand Up @@ -89,6 +89,10 @@ def task_board_for(project)
task_boards.find_by(project:)
end

def work_packages_for(project)
work_packages.where(project:).order_by_position
end

def owned_by?(project)
project_id == project.id
end
Expand Down
Loading
Loading