diff --git a/app/components/work_packages/bulk_delete_dialog_component.html.erb b/app/components/work_packages/bulk_delete_dialog_component.html.erb
new file mode 100644
index 00000000000..148e8dc3f64
--- /dev/null
+++ b/app/components/work_packages/bulk_delete_dialog_component.html.erb
@@ -0,0 +1,81 @@
+<%#
+ -- copyright
+ OpenProject is an open source project management software.
+ Copyright (C) the OpenProject GmbH
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License version 3.
+
+ OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+ Copyright (C) 2006-2013 Jean-Philippe Lang
+ Copyright (C) 2010-2013 the ChiliProject Team
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version 2
+ of the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ See COPYRIGHT and LICENSE files for more details.
+
+ ++#
+%>
+
+<%=
+ render(
+ Primer::OpenProject::DangerDialog.new(
+ id:,
+ title:,
+ form_arguments: {
+ action: form_action,
+ method: :delete,
+ data: { turbo: false }
+ },
+ size: :medium_portrait
+ )
+ ) do |dialog|
+%>
+ <% dialog.with_confirmation_message do |message|
+ message.with_heading(tag: :h2) { heading }
+ message.with_description_content(description)
+ end %>
+
+ <% dialog.with_additional_details(display: :block) do %>
+ <% if multiple_projects? %>
+ <%= render(Primer::Alpha::Banner.new(mb: 2, scheme: :warning, icon: :alert)) do %>
+ <%= I18n.t("work_packages.bulk_delete_dialog.cross_project_warning", projects: project_names) %>
+ <% end %>
+ <% end %>
+ <%= render(OpPrimer::InsetBoxComponent.new) do %>
+ <% work_packages.each do |wp| %>
+ <%= render WorkPackages::InfoLineComponent.new(work_package: wp,
+ show_subject: true,
+ show_status: false,
+ show_project: multiple_projects?) %>
+ <% if descendants_for(wp).any? %>
+ <%= render(Primer::Box.new(pl: 2, my: 1)) do %>
+ <%= render(Primer::Beta::Text.new(mt: 1, font_size: :small, font_weight: :bold, display: :block, mb: 1)) do %>
+ <%= I18n.t("work_packages.bulk_delete_dialog.children_label") %>
+ <% end %>
+ <% descendants_for(wp).each do |descendant| %>
+ <%= render WorkPackages::InfoLineComponent.new(work_package: descendant,
+ show_subject: true,
+ show_status: false,
+ show_project: descendant.project != wp.project) %>
+ <% end %>
+ <% end %>
+ <% end %>
+ <% end %>
+ <% end %>
+ <% end %>
+
+ <% dialog.with_confirmation_check_box_content(confirmation_checkbox_text) %>
+<% end %>
diff --git a/app/components/work_packages/bulk_delete_dialog_component.rb b/app/components/work_packages/bulk_delete_dialog_component.rb
new file mode 100644
index 00000000000..a3e75df4cbf
--- /dev/null
+++ b/app/components/work_packages/bulk_delete_dialog_component.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module WorkPackages
+ class BulkDeleteDialogComponent < ApplicationComponent
+ include OpTurbo::Streamable
+
+ attr_reader :work_packages
+
+ def initialize(work_packages:, back_url: nil)
+ super
+ @work_packages = work_packages
+ @back_url = back_url
+ end
+
+ private
+
+ def id = "wp-delete-dialog"
+
+ def title
+ I18n.t("work_packages.bulk_delete_dialog.title", count: total_count)
+ end
+
+ def heading
+ I18n.t("work_packages.bulk_delete_dialog.heading", count: total_count)
+ end
+
+ def description
+ if has_descendants?
+ I18n.t("work_packages.bulk_delete_dialog.description_with_children")
+ else
+ I18n.t("work_packages.bulk_delete_dialog.description")
+ end
+ end
+
+ def confirmation_checkbox_text
+ if has_descendants?
+ I18n.t("work_packages.bulk_delete_dialog.confirm_children_deletion")
+ else
+ I18n.t("text_permanent_delete_confirmation_checkbox_label")
+ end
+ end
+
+ def total_count
+ @total_count ||= work_packages.count + descendants_by_work_package.values.sum(&:size)
+ end
+
+ def multiple_projects?
+ projects.size > 1
+ end
+
+ def project_names
+ projects.map(&:name).join(", ")
+ end
+
+ def descendants_for(work_package)
+ (descendants_by_work_package[work_package.id] || [])
+ .reject { |child| work_packages.include?(child) }
+ end
+
+ def has_descendants?
+ work_packages.any? { |wp| descendants_for(wp).any? }
+ end
+
+ def form_action
+ helpers.work_packages_bulk_path(ids: work_packages.map(&:id), back_url: @back_url)
+ end
+
+ def projects
+ @projects ||= work_packages.filter_map(&:project).uniq
+ end
+
+ def descendants_by_work_package
+ @descendants_by_work_package ||= begin
+ hierarchies = WorkPackageHierarchy
+ .where(ancestor_id: work_packages.map(&:id))
+ .where("generations > 0")
+ .order(:generations, :descendant_id)
+
+ descendant_records = WorkPackage
+ .where(id: hierarchies.pluck(:descendant_id))
+ .includes(:project, :type, :status)
+ .index_by(&:id)
+
+ hierarchies
+ .group_by(&:ancestor_id)
+ .transform_values { |rows| rows.filter_map { |r| descendant_records[r.descendant_id] } }
+ end
+ end
+ end
+end
diff --git a/app/components/work_packages/delete_dialog_component.html.erb b/app/components/work_packages/delete_dialog_component.html.erb
new file mode 100644
index 00000000000..ac66a997eab
--- /dev/null
+++ b/app/components/work_packages/delete_dialog_component.html.erb
@@ -0,0 +1,75 @@
+<%#
+ -- copyright
+ OpenProject is an open source project management software.
+ Copyright (C) the OpenProject GmbH
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License version 3.
+
+ OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+ Copyright (C) 2006-2013 Jean-Philippe Lang
+ Copyright (C) 2010-2013 the ChiliProject Team
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version 2
+ of the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ See COPYRIGHT and LICENSE files for more details.
+
+ ++#
+%>
+
+<%=
+ render(
+ Primer::OpenProject::DangerDialog.new(
+ id:,
+ title:,
+ form_arguments: {
+ action: form_action,
+ method: :delete,
+ data: { turbo: false }
+ },
+ size: :medium_portrait
+ )
+ ) do |dialog|
+%>
+ <% dialog.with_confirmation_message do |message|
+ message.with_heading(tag: :h2) { heading }
+ message.with_description_content(description)
+ end %>
+
+ <% if has_descendants? %>
+ <% dialog.with_additional_details(display: :block) do %>
+ <% if cross_project_descendants? %>
+ <%= render(Primer::Alpha::Banner.new(mb: 2, scheme: :warning, icon: :alert)) do %>
+ <%= I18n.t("work_packages.delete_dialog.cross_project_warning", projects: all_project_names) %>
+ <% end %>
+ <% end %>
+ <%= render(OpPrimer::InsetBoxComponent.new) do %>
+ <%= render(Primer::Beta::Text.new(font_size: :small, font_weight: :bold, display: :block, mb: 2)) do %>
+ <%= I18n.t("work_packages.bulk_delete_dialog.children_label") %>
+ <% end %>
+ <% descendants.each do |descendant| %>
+ <%= render WorkPackages::InfoLineComponent.new(pl:2,
+ my: 1,
+ work_package: descendant,
+ show_subject: true,
+ show_status: false,
+ show_project: descendant.project != work_package.project) %>
+ <% end %>
+ <% end %>
+ <% end %>
+ <% end %>
+
+ <% dialog.with_confirmation_check_box_content(confirmation_checkbox_text) %>
+<% end %>
diff --git a/app/components/work_packages/delete_dialog_component.rb b/app/components/work_packages/delete_dialog_component.rb
new file mode 100644
index 00000000000..d2daf8ec27a
--- /dev/null
+++ b/app/components/work_packages/delete_dialog_component.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module WorkPackages
+ class DeleteDialogComponent < ApplicationComponent
+ include OpTurbo::Streamable
+
+ attr_reader :work_package
+
+ def initialize(work_package:, back_url: nil)
+ super
+ @work_package = work_package
+ @back_url = back_url
+ end
+
+ private
+
+ def id = "wp-delete-dialog"
+
+ def title
+ I18n.t("work_packages.delete_dialog.title")
+ end
+
+ def heading
+ I18n.t("work_packages.delete_dialog.heading")
+ end
+
+ def description
+ I18n.t("work_packages.delete_dialog.description", name: work_package.to_s)
+ end
+
+ def confirmation_checkbox_text
+ if has_descendants?
+ I18n.t("work_packages.delete_dialog.confirm_descendants_deletion")
+ else
+ I18n.t("text_permanent_delete_confirmation_checkbox_label")
+ end
+ end
+
+ def descendants
+ @descendants ||= WorkPackage
+ .joins("INNER JOIN work_package_hierarchies ON work_package_hierarchies.descendant_id = work_packages.id")
+ .where(work_package_hierarchies: { ancestor_id: work_package.id })
+ .where("work_package_hierarchies.generations > 0")
+ .includes(:project, :type, :status)
+ .order("work_package_hierarchies.generations ASC, work_packages.id ASC")
+ end
+
+ def has_descendants?
+ descendants.any?
+ end
+
+ def cross_project_descendants?
+ descendants.any? { |d| d.project != work_package.project }
+ end
+
+ def all_project_names
+ names = descendants
+ .filter_map(&:project)
+ .uniq
+ .reject { |p| p == work_package.project }
+ .map(&:name)
+
+ names
+ .unshift(work_package.project.name)
+ .join(", ")
+ end
+
+ def form_action
+ helpers.work_packages_bulk_path(ids: [work_package.id], back_url: @back_url)
+ end
+ end
+end
diff --git a/app/components/work_packages/info_line_component.html.erb b/app/components/work_packages/info_line_component.html.erb
index e124a5c3582..1f590ec6550 100644
--- a/app/components/work_packages/info_line_component.html.erb
+++ b/app/components/work_packages/info_line_component.html.erb
@@ -1,9 +1,14 @@
<%=
- flex_layout(flex_wrap: :wrap) do |flex|
+ flex_layout(flex_wrap: :wrap, **@system_arguments) do |flex|
+ if @show_project && !@show_subject
+ flex.with_column(mr: 2) do
+ render(Primer::Beta::Text.new(font_size: @font_size)) { "#{@work_package.project.name}: " }
+ end
+ end
flex.with_column(mr: 2) do
render(WorkPackages::HighlightedTypeComponent.new(work_package: @work_package, font_size: :small))
end
- flex.with_column(mr: 2) do
+ flex.with_column do
render(
Primer::Beta::Link.new(
href: url_for(controller: "/work_packages", action: "show", id: @work_package),
@@ -13,8 +18,23 @@
)
) { "##{@work_package.id}" }
end
- flex.with_column do
- render WorkPackages::StatusBadgeComponent.new(status: @work_package.status)
+
+ if @show_status
+ flex.with_column(ml: 2) do
+ render WorkPackages::StatusBadgeComponent.new(status: @work_package.status)
+ end
+ end
+
+ if @show_subject
+ flex.with_column(classes: "ellipsis", ml: 1) do
+ render(Primer::Beta::Text.new(font_size: @font_size)) do
+ if @show_project
+ "#{@work_package.project.name}: #{@work_package.subject}"
+ else
+ @work_package.subject
+ end
+ end
+ end
end
end
%>
diff --git a/app/components/work_packages/info_line_component.rb b/app/components/work_packages/info_line_component.rb
index 293d79eedc5..9be082a5acc 100644
--- a/app/components/work_packages/info_line_component.rb
+++ b/app/components/work_packages/info_line_component.rb
@@ -31,10 +31,20 @@
class WorkPackages::InfoLineComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
- def initialize(work_package:, font_size: :small)
+ def initialize(work_package:,
+ show_project: false,
+ show_subject: false,
+ show_status: true,
+ font_size: :small,
+ **system_arguments)
super
@work_package = work_package
@font_size = font_size
+ @show_project = show_project
+ @show_subject = show_subject
+ @show_status = show_status
+
+ @system_arguments = system_arguments
end
end
diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb
index e969080bf0d..fef8f2a0476 100644
--- a/app/controllers/members_controller.rb
+++ b/app/controllers/members_controller.rb
@@ -166,7 +166,7 @@ class MembersController < ApplicationController
end
def members_filter_options(roles)
- groups = Group.all.sort
+ groups = Group.visible.sort
shares = WorkPackageRole.all
status = Members::UserFilterComponent.status_param(params)
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 856d15dcef6..b54d615a889 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -81,7 +81,7 @@ class UsersController < ApplicationController
include PaginationHelper
def index
- @groups = Group.all.sort
+ @groups = Group.visible.sort
@status = Users::UserFilterComponent.status_param params
@users = Users::UserFilterComponent.filter params
end
diff --git a/app/controllers/work_packages/bulk_controller.rb b/app/controllers/work_packages/bulk_controller.rb
index e281c2b4c10..2393cff6737 100644
--- a/app/controllers/work_packages/bulk_controller.rb
+++ b/app/controllers/work_packages/bulk_controller.rb
@@ -38,6 +38,18 @@ class WorkPackages::BulkController < ApplicationController
include QueriesHelper
include WorkPackages::BulkErrorMessage
+ include OpTurbo::ComponentStream
+
+ def delete_dialog
+ component =
+ if @work_packages.one?
+ WorkPackages::DeleteDialogComponent.new(work_package: @work_packages.first, back_url: params[:back_url])
+ else
+ WorkPackages::BulkDeleteDialogComponent.new(work_packages: @work_packages, back_url: params[:back_url])
+ end
+
+ respond_with_dialog component
+ end
def edit
setup_edit
@@ -70,20 +82,21 @@ class WorkPackages::BulkController < ApplicationController
end
end
- def destroy
+ def destroy # rubocop:disable Metrics/AbcSize
if WorkPackage.cleanup_associated_before_destructing_if_required(@work_packages, current_user, params[:to_do])
destroy_work_packages(@work_packages)
respond_to do |format|
format.html do
- redirect_to (project_work_packages_path(@work_packages.first.project))
+ redirect_back_or_default(project_work_packages_path(@work_packages.first.project),
+ status: :see_other)
end
format.json do
head :ok
end
end
else
- redirect_to(action: :reassign, ids: @work_packages.map(&:id))
+ redirect_to(action: :reassign, ids: @work_packages.map(&:id), back_url: params[:back_url])
end
end
diff --git a/app/models/queries/work_packages/filter/group_filter.rb b/app/models/queries/work_packages/filter/group_filter.rb
index 853fc9ed559..46a1578b1d2 100644
--- a/app/models/queries/work_packages/filter/group_filter.rb
+++ b/app/models/queries/work_packages/filter/group_filter.rb
@@ -93,6 +93,6 @@ class Queries::WorkPackages::Filter::GroupFilter < Queries::WorkPackages::Filter
end
def all_groups
- @all_groups ||= ::Group.all
+ @all_groups ||= ::Group.visible
end
end
diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb
index d50df0fb4d6..67b18ebc2fa 100644
--- a/config/initializers/permissions.rb
+++ b/config/initializers/permissions.rb
@@ -435,7 +435,7 @@ Rails.application.reloader.to_prepare do
wpt.permission :delete_work_packages,
{
work_packages: :destroy,
- "work_packages/bulk": %i[destroy reassign]
+ "work_packages/bulk": %i[delete_dialog destroy reassign]
},
permissible_on: :project,
require: :member,
diff --git a/config/locales/crowdin/fr.yml b/config/locales/crowdin/fr.yml
index 676cc4f8cf5..74fccf5e2e4 100644
--- a/config/locales/crowdin/fr.yml
+++ b/config/locales/crowdin/fr.yml
@@ -154,7 +154,7 @@ fr:
connection_timeout: 'La connexion au serveur Jira a expiré : %{message}'
parse_error: 'Échec de l''analyse de la réponse de l''API Jira : %{message}'
api_error: L'API Jira a renvoyé le statut d'erreur %{status}
- 401_error: Jira API returned a 401 error. Your authentication token may have expired or lack the required permissions. Please ensure the token belongs to a Jira administrator.
+ 401_error: L'API Jira a renvoyé une erreur 401. Votre jeton d'authentification peut avoir expiré ou ne pas avoir les autorisations requises. Veuillez vous assurer que le jeton appartient à un administrateur Jira.
columns:
projects: Projets
last_change: Dernière modification
@@ -2923,7 +2923,7 @@ fr:
title: Module Enterprise
plan_title: Add-on Enterprise %{plan}
plan_name: Abonnement Enterprise %{plan}
- trial_text: This feature is included in your active Enterprise trial.
+ trial_text: Cette fonctionnalité est incluse dans votre essai en cours d'entreprise.
plan_text_html: Disponible à partir de l'abonnement %{plan_name}.
unlimited: Illimité
already_have_token: 'Vous avez déjà un jeton ? Ajoutez-le en utilisant le bouton ci-dessous pour passer à l''abonnement Enterprise réservé.
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 0920a4006d1..83f6c910a85 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1417,6 +1417,20 @@ en:
no_results_title_text: There are currently no workflows.
work_packages:
+ delete_dialog:
+ title: "Delete work package"
+ heading: "Permanently delete this work package?"
+ description: 'Are you sure you want to delete the work package "%{name}"?'
+ confirm_descendants_deletion: "I acknowledge that ALL descendants of this work package will be recursively removed."
+ cross_project_warning: "Work packages from the following projects will be deleted: %{projects}"
+ bulk_delete_dialog:
+ title: "Delete %{count} work packages"
+ heading: "Permanently delete these %{count} work packages?"
+ description: "The following work packages, including children and all associated data, will permanently be deleted:"
+ description_with_children: "The following work packages, including child work packages, and all associated data will be permanently deleted:"
+ confirm_children_deletion: "I acknowledge that all selected work packages and their children will be permanently deleted."
+ cross_project_warning: "These work packages span multiple projects: %{projects}"
+ children_label: "The following children will also be deleted:"
datepicker_modal:
banner:
description:
diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml
index 5aeaa76a179..50545ecb125 100644
--- a/config/locales/js-en.yml
+++ b/config/locales/js-en.yml
@@ -1040,13 +1040,6 @@ en:
form_submit:
title: "Confirm to continue"
text: "Are you sure you want to perform this action?"
- destroy_work_package:
- title: "Confirm deletion of %{label}"
- single_text: "Are you sure you want to delete the work package"
- bulk_text: "Are you sure you want to delete the following %{label}?"
- has_children: "The work package has %{childUnits}:"
- confirm_deletion_children: "I acknowledge that ALL descendants of the listed work packages will be recursively removed."
- deletes_children: "All child work packages and their descendants will also be recursively deleted."
destroy_time_entry:
title: "Confirm deletion of time entry"
text: "Are you sure you want to delete the following time entry?"
diff --git a/config/routes.rb b/config/routes.rb
index 889ba506625..2153a519612 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -821,6 +821,7 @@ Rails.application.routes.draw do
resource :bulk, controller: "bulk", only: %i[edit update destroy] do
collection do
match :reassign, via: %i[get delete]
+ get :delete_dialog
end
end
end
diff --git a/frontend/src/app/core/path-helper/path-helper.service.ts b/frontend/src/app/core/path-helper/path-helper.service.ts
index dcc9a2a571a..5a5f305c589 100644
--- a/frontend/src/app/core/path-helper/path-helper.service.ts
+++ b/frontend/src/app/core/path-helper/path-helper.service.ts
@@ -411,6 +411,12 @@ export class PathHelperService {
return `${this.workPackagesPath(null)}/bulk`;
}
+ public workPackagesBulkDeleteDialogPath(ids:string[], backUrl?:string) {
+ const params = ids.map((id) => `ids[]=${encodeURIComponent(id)}`).join('&');
+ const backParam = backUrl ? `&back_url=${encodeURIComponent(backUrl)}` : '';
+ return `${this.workPackagesPath(null)}/bulk/delete_dialog?${params}${backParam}`;
+ }
+
public workPackagesBulkReassignmentPath() {
return `${this.workPackagesPath(null)}/bulk/reassign`;
}
diff --git a/frontend/src/app/features/work-packages/openproject-work-packages.module.ts b/frontend/src/app/features/work-packages/openproject-work-packages.module.ts
index 021d9d994fd..490ca2e61c0 100644
--- a/frontend/src/app/features/work-packages/openproject-work-packages.module.ts
+++ b/frontend/src/app/features/work-packages/openproject-work-packages.module.ts
@@ -272,7 +272,6 @@ import {
import { QuerySharingModalComponent } from 'core-app/shared/components/modals/share-modal/query-sharing.modal';
import { SaveQueryModalComponent } from 'core-app/shared/components/modals/save-modal/save-query.modal';
import { QuerySharingFormComponent } from 'core-app/shared/components/modals/share-modal/query-sharing-form.component';
-import { WpDestroyModalComponent } from 'core-app/shared/components/modals/wp-destroy-modal/wp-destroy.modal';
import {
WorkPackageTypeStatusComponent,
} from 'core-app/features/work-packages/components/wp-type-status/wp-type-status.component';
@@ -617,7 +616,6 @@ import { WorkPackageFullViewEntryComponent } from 'core-app/features/work-packag
QuerySharingFormComponent,
QuerySharingModalComponent,
SaveQueryModalComponent,
- WpDestroyModalComponent,
WorkPackageShareModalComponent,
WorkPackageReminderModalComponent,
diff --git a/frontend/src/app/shared/components/modals/wp-destroy-modal/wp-destroy.modal.html b/frontend/src/app/shared/components/modals/wp-destroy-modal/wp-destroy.modal.html
deleted file mode 100644
index e8494a84166..00000000000
--- a/frontend/src/app/shared/components/modals/wp-destroy-modal/wp-destroy.modal.html
+++ /dev/null
@@ -1,89 +0,0 @@
-
diff --git a/frontend/src/app/shared/components/modals/wp-destroy-modal/wp-destroy.modal.ts b/frontend/src/app/shared/components/modals/wp-destroy-modal/wp-destroy.modal.ts
deleted file mode 100644
index fd272e81cb5..00000000000
--- a/frontend/src/app/shared/components/modals/wp-destroy-modal/wp-destroy.modal.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-//-- copyright
-// OpenProject is an open source project management software.
-// Copyright (C) the OpenProject GmbH
-//
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License version 3.
-//
-// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-// Copyright (C) 2006-2013 Jean-Philippe Lang
-// Copyright (C) 2010-2013 the ChiliProject Team
-//
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License
-// as published by the Free Software Foundation; either version 2
-// of the License, or (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-//
-// See COPYRIGHT and LICENSE files for more details.
-//++
-
-import { WorkPackagesListService } from 'core-app/features/work-packages/components/wp-list/wp-list.service';
-import { States } from 'core-app/core/states/states.service';
-import {
- ChangeDetectionStrategy,
- ChangeDetectorRef,
- Component,
- ElementRef,
- Inject,
- OnInit,
-} from '@angular/core';
-import { OpModalComponent } from 'core-app/shared/components/modal/modal.component';
-import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service';
-import { OpModalLocalsMap } from 'core-app/shared/components/modal/modal.types';
-import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
-import { WorkPackageViewFocusService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-focus.service';
-import { StateService } from '@uirouter/core';
-import { I18nService } from 'core-app/core/i18n/i18n.service';
-import { WorkPackageNotificationService } from 'core-app/features/work-packages/services/notifications/work-package-notification.service';
-import { WorkPackageService } from 'core-app/features/work-packages/services/work-package.service';
-import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
-import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
-import { BackRoutingService } from 'core-app/features/work-packages/components/back-routing/back-routing.service';
-
-@Component({
- templateUrl: './wp-destroy.modal.html',
- standalone: false,
- // TODO: This component has been partially migrated to be zoneless-compatible.
- // After testing, this should be updated to ChangeDetectionStrategy.OnPush.
- // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
- changeDetection: ChangeDetectionStrategy.Default,
-})
-export class WpDestroyModalComponent extends OpModalComponent implements OnInit {
- // When deleting multiple
- public workPackages:WorkPackageResource[];
-
- public workPackageLabel:string;
-
- // Single work package
- public singleWorkPackage:WorkPackageResource;
-
- public singleWorkPackageChildren:WorkPackageResource[];
-
- public busy = false;
-
- // Need to confirm deletion when children are involved
- public childrenDeletionConfirmed = false;
-
- public text = {
- label_visibility_settings: this.I18n.t('js.label_visibility_settings'),
- button_save: this.I18n.t('js.modals.button_save'),
- confirm: this.I18n.t('js.modals.button_delete'),
- warning: this.I18n.t('js.label_warning'),
- cancel: this.I18n.t('js.button_cancel'),
- close: this.I18n.t('js.close_popup_title'),
- label_confirm_children_deletion: this.I18n.t('js.modals.destroy_work_package.confirm_deletion_children'),
- title: '',
- bulk_text: '',
- single_text: this.I18n.t('js.modals.destroy_work_package.single_text'),
- childCount: (_wp:WorkPackageResource):string => '',
- hasChildren: (_wp:WorkPackageResource):string => '',
- deletesChildren: '',
- };
-
- constructor(
- readonly elementRef:ElementRef,
- readonly workPackageService:WorkPackageService,
- @Inject(OpModalLocalsToken) public locals:OpModalLocalsMap,
- readonly I18n:I18nService,
- readonly cdRef:ChangeDetectorRef,
- readonly $state:StateService,
- readonly states:States,
- readonly wpTableFocus:WorkPackageViewFocusService,
- readonly wpListService:WorkPackagesListService,
- readonly notificationService:WorkPackageNotificationService,
- readonly currentProject:CurrentProjectService,
- readonly pathHelper:PathHelperService,
- readonly backRoutingService:BackRoutingService,
- ) {
- super(locals, cdRef, elementRef);
- }
-
- ngOnInit():void {
- super.ngOnInit();
-
- this.workPackages = this.locals.workPackages;
- this.workPackageLabel = this.I18n.t('js.units.workPackage', { count: this.workPackages.length });
-
- // Ugly way to provide the same view bindings as the ng-init in the previous template.
- if (this.workPackages.length === 1) {
- this.singleWorkPackage = this.workPackages[0];
- this.singleWorkPackageChildren = this.singleWorkPackage.children;
- }
-
- this.text.title = this.I18n.t('js.modals.destroy_work_package.title', { label: this.workPackageLabel });
- this.text.bulk_text = this.I18n.t('js.modals.destroy_work_package.bulk_text', {
- label: this.workPackageLabel,
- count: this.workPackages.length,
- });
-
- this.text.childCount = (wp:WorkPackageResource) => {
- const count = this.children(wp).length;
- return this.I18n.t('js.units.child_work_packages', { count });
- };
-
- this.text.hasChildren = (wp:WorkPackageResource) => {
- const childUnits = this.text.childCount(wp);
- return this.I18n.t('js.modals.destroy_work_package.has_children', { childUnits });
- };
- this.text.deletesChildren = this.I18n.t('js.modals.destroy_work_package.deletes_children');
- }
-
- public get blockedDueToUnconfirmedChildren():boolean {
- return this.mustConfirmChildren && !this.childrenDeletionConfirmed;
- }
-
- public get mustConfirmChildren():boolean {
- let result = false;
-
- if (this.singleWorkPackage && this.singleWorkPackageChildren) {
- result = this.singleWorkPackageChildren.length > 0;
- }
-
- return result || !!_.find(this.workPackages, (wp) => wp.children && wp.children.length > 0);
- }
-
- public confirmDeletion($event:Event):boolean {
- if (this.busy || this.blockedDueToUnconfirmedChildren) {
- return false;
- }
-
- this.busy = true;
- this.cdRef.markForCheck();
- const ids = this.workPackages
- .map((el) => el.id)
- .filter((id) => id !== null);
- this.workPackageService.performBulkDelete(ids, true)
- .then(() => {
- this.busy = false;
- this.cdRef.markForCheck();
- this.closeMe($event);
- this.wpTableFocus.clear('Clearing after destroying work packages');
- if (this.$state.current.data?.baseRoute) {
- this.backRoutingService.goBack(true);
- } else {
- const projectIdentifier = this.currentProject.identifier;
- window.location.href = this.pathHelper.workPackagesPath(projectIdentifier) + window.location.search;
- }
- })
- .catch(() => {
- this.busy = false;
- this.cdRef.markForCheck();
- });
-
- return false;
- }
-
- public children(workPackage:WorkPackageResource) {
- if (workPackage.hasOwnProperty('children')) {
- return workPackage.children;
- }
- return [];
- }
-}
diff --git a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts
index 04b2972400d..f9124b4486e 100644
--- a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts
+++ b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts
@@ -12,18 +12,17 @@ import { OpContextMenuItem } from 'core-app/shared/components/op-context-menu/op
import {
PERMITTED_CONTEXT_MENU_ACTIONS,
} from 'core-app/shared/components/op-context-menu/wp-context-menu/wp-static-context-menu-actions';
-import { OpModalService } from 'core-app/shared/components/modal/modal.service';
import { CopyToClipboardService } from 'core-app/shared/components/copy-to-clipboard/copy-to-clipboard.service';
import {
WorkPackageAction,
} from 'core-app/features/work-packages/components/wp-table/context-menu-helper/wp-context-menu-helper.service';
-import { WpDestroyModalComponent } from 'core-app/shared/components/modals/wp-destroy-modal/wp-destroy.modal';
import { WorkPackageAuthorization } from 'core-app/features/work-packages/services/work-package-authorization.service';
import { TurboRequestsService } from 'core-app/core/turbo/turbo-requests.service';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { TimeEntryTimerService } from 'core-app/shared/components/time_entries/services/time-entry-timer.service';
import { TimeEntryResource } from 'core-app/features/hal/resources/time-entry-resource';
import { DeviceService } from 'core-app/core/browser/device.service';
+import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
@@ -41,10 +40,10 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger
readonly injector = inject(Injector);
readonly PathHelper = inject(PathHelperService);
readonly elementRef = inject(ElementRef);
- readonly opModalService = inject(OpModalService);
readonly turboRequests = inject(TurboRequestsService);
readonly apiV3Service = inject(ApiV3Service);
readonly authorisationService = inject(AuthorisationService);
+ readonly currentProject = inject(CurrentProjectService);
readonly timeEntryService = inject(TimeEntryTimerService);
protected copyToClipboardService = inject(CopyToClipboardService);
protected deviceService = inject(DeviceService);
@@ -96,9 +95,18 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger
window.location.href = `${this.PathHelper.workPackageCopyPath(this.workPackage.project.identifier, this.workPackage.id)}`;
}
break;
- case 'delete':
- this.opModalService.show(WpDestroyModalComponent, this.injector, { workPackages: [this.workPackage] });
+ case 'delete': {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ const currentBaseRoute = this.$state.current.data?.baseRoute as string | undefined;
+ const backUrl = currentBaseRoute
+ ? this.$state.href(currentBaseRoute)
+ : this.PathHelper.workPackagesPath(this.currentProject.identifier ?? null);
+ void this.turboRequests.request(
+ this.PathHelper.workPackagesBulkDeleteDialogPath([this.workPackage.id!], backUrl),
+ { method: 'GET' },
+ );
break;
+ }
case 'log_time':
void this.turboRequests.request(this.PathHelper.timeEntryWorkPackageDialog(this.workPackage.id!), { method: 'GET' });
break;
diff --git a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts
index f4e6953a8fe..e3cf08e3a50 100644
--- a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts
+++ b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts
@@ -20,12 +20,10 @@ import {
import {
PERMITTED_CONTEXT_MENU_ACTIONS,
} from 'core-app/shared/components/op-context-menu/wp-context-menu/wp-static-context-menu-actions';
-import { OpModalService } from 'core-app/shared/components/modal/modal.service';
import { StateService } from '@uirouter/core';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { CopyToClipboardService } from 'core-app/shared/components/copy-to-clipboard/copy-to-clipboard.service';
import { splitViewRoute } from 'core-app/features/work-packages/routing/split-view-routes.helper';
-import { WpDestroyModalComponent } from 'core-app/shared/components/modals/wp-destroy-modal/wp-destroy.modal';
import isNewResource from 'core-app/features/hal/helpers/is-new-resource';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { TurboRequestsService } from 'core-app/core/turbo/turbo-requests.service';
@@ -40,8 +38,6 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler {
@InjectField() protected wpRelationsHierarchyService:WorkPackageRelationsHierarchyService;
- @InjectField() protected opModalService:OpModalService;
-
@InjectField() protected $state!:StateService;
@InjectField() protected wpTableSelection:WorkPackageViewSelectionService;
@@ -152,7 +148,9 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler {
private deleteSelectedWorkPackages() {
const selected = this.getSelectedWorkPackages();
- this.opModalService.show(WpDestroyModalComponent, this.injector, { workPackages: selected });
+ const ids = selected.map((wp) => wp.id).filter((id) => id !== null);
+ const backUrl = this.$state.href(this.baseRoute as string) || this.pathHelper.workPackagesPath(this.currentProject.identifier ?? null);
+ void this.turboRequests.request(this.pathHelper.workPackagesBulkDeleteDialogPath(ids, backUrl), { method: 'GET' });
}
private editSelectedWorkPackages(link:any) {
diff --git a/lookbook/previews/open_project/work_packages/info_line_component_preview.rb b/lookbook/previews/open_project/work_packages/info_line_component_preview.rb
index 21812980029..8331d308dcc 100644
--- a/lookbook/previews/open_project/work_packages/info_line_component_preview.rb
+++ b/lookbook/previews/open_project/work_packages/info_line_component_preview.rb
@@ -32,8 +32,16 @@ module OpenProject::WorkPackages
# @logical_path OpenProject/WorkPackages
class InfoLineComponentPreview < ViewComponent::Preview
# See the [component documentation](/lookbook/pages/components/work_package_info_line) for more details.
- def playground
- render(WorkPackages::InfoLineComponent.new(work_package: WorkPackage.visible.first))
+ # @param show_project [Boolean]
+ # @param show_subject [Boolean]
+ # @param show_status [Boolean]
+ # @param font_size [Symbol] select [small, normal]
+ def playground(show_project: false, show_subject: false, show_status: true, font_size: :small)
+ render(WorkPackages::InfoLineComponent.new(work_package: WorkPackage.visible.first,
+ show_project:,
+ show_subject:,
+ show_status:,
+ font_size:))
end
end
end
diff --git a/modules/meeting/config/locales/crowdin/fr.yml b/modules/meeting/config/locales/crowdin/fr.yml
index 2b4609a2e1b..3bb47669d04 100644
--- a/modules/meeting/config/locales/crowdin/fr.yml
+++ b/modules/meeting/config/locales/crowdin/fr.yml
@@ -42,9 +42,9 @@ fr:
start_date: Date
start_time: Heure de début
start_time_hour: Heure de début
- end_time: End time
- template: Template
- notify: Send notifications
+ end_time: Heure de fin
+ template: Modèle
+ notify: Envoyer les notifications
sharing: Partage
meeting_agenda_item:
title: Titre
@@ -54,13 +54,13 @@ fr:
presenter: Responsable
item_type: Type
position: Position
- lock_version: Lock version
+ lock_version: Verrouiller la version
notes: Notes
meeting_section:
title: Titre
position: Position
recurring_meeting:
- title: Title
+ title: Titre
frequency: Fréquence
interval: Intervalle
start_date: Commence le
@@ -69,10 +69,10 @@ fr:
end_after: La série de réunions se termine
end_date: Échéance
iterations: Occurrences
- time_zone: Time zone
- location: Location
- duration: Duration
- notify: Send notifications
+ time_zone: Fuseau horaire
+ location: Emplacement
+ duration: Durée
+ notify: Envoyer les notifications
recurring_meeting_interim_response:
start_time: Heure de début
meeting_participant:
@@ -435,9 +435,9 @@ fr:
meeting_not_found: Réunion non trouvée pour l'UID donné.
widgets:
blankslate:
- heading: No upcoming meetings
- description: Upcoming meetings where you are the organizer or a participant will appear here.
- view_details: View all meetings
+ heading: Aucune réunion à venir
+ description: Les prochaines réunions où vous êtes l'organisateur ou un participant apparaîtront ici.
+ view_details: Voir toutes les réunions
meeting_section:
untitled_title: Section sans titre
delete_confirmation: Supprimer la section supprimera également tous les éléments de l'ordre du jour qui s'y rapportent. Voulez-vous vraiment continuer ?
diff --git a/modules/meeting/config/locales/en.yml b/modules/meeting/config/locales/en.yml
index 138fe93349a..2089fe0156f 100644
--- a/modules/meeting/config/locales/en.yml
+++ b/modules/meeting/config/locales/en.yml
@@ -720,9 +720,9 @@ en:
text_exit_draft_mode_dialog_template_subtitle: "You cannot return to draft mode after this."
label_meeting_template_sharing: "Sharing"
- label_meeting_template_sharing_none: "Only this project"
- label_meeting_template_sharing_descendants: "Subprojects"
- label_meeting_template_sharing_system: "All projects"
+ label_meeting_template_sharing_none: "Only with this project"
+ label_meeting_template_sharing_descendants: "With subprojects"
+ label_meeting_template_sharing_system: "With all projects"
text_meeting_template_sharing_description: "This template can be shared with subprojects or other projects in this instance. Only the agenda items and attachments will be copied."
text_meeting_not_editable_anymore: "This meeting is not editable anymore."
diff --git a/modules/meeting/spec/support/pages/meetings/show.rb b/modules/meeting/spec/support/pages/meetings/show.rb
index 2000899ded6..ac215201e3d 100644
--- a/modules/meeting/spec/support/pages/meetings/show.rb
+++ b/modules/meeting/spec/support/pages/meetings/show.rb
@@ -478,7 +478,7 @@ module Pages::Meetings
end
def edit_agenda_item(item, save: true, wait_for_reference_update: false, &)
- select_action item, "Edit"
+ wait_for_turbo_stream { select_action item, "Edit" }
expect_item_edit_form(item)
reference_value = meeting_reference_value
page.within("#meeting-agenda-items-form-component-#{item.id}") do
diff --git a/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb b/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb
index 9c873a995dd..2d8f0d9f4eb 100644
--- a/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb
+++ b/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb
@@ -8,7 +8,7 @@ module ::TwoFactorAuthentication
before_action :ensure_enabled_2fa
# Locate the user we're editing
- prepend_before_action :find_user
+ before_action :find_user
before_action :find_device, only: %i[confirm make_default destroy]
diff --git a/modules/two_factor_authentication/spec/features/admin_delete_two_factor_device_spec.rb b/modules/two_factor_authentication/spec/features/admin_delete_two_factor_device_spec.rb
new file mode 100644
index 00000000000..4e33f66a5ba
--- /dev/null
+++ b/modules/two_factor_authentication/spec/features/admin_delete_two_factor_device_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require_relative "../spec_helper"
+
+# Regression test: find_user in BaseController used prepend_before_action, which ran it
+# before ApplicationController#user_setup could set User.current from the session.
+# User.visible defaults to User.current, so without User.current set to the admin,
+# the target user was not found (404).
+RSpec.describe "Admin deleting another user's 2FA device", :js,
+ with_settings: {
+ plugin_openproject_two_factor_authentication: { "active_strategies" => %i[developer totp] }
+ } do
+ let(:dialog) { Components::PasswordConfirmationDialog.new }
+ let(:admin_password) { "adminadmin!" * 2 }
+ let(:admin) { create(:admin, password: admin_password, password_confirmation: admin_password) }
+ let(:other_user) { create(:user) }
+ let!(:device) { create(:two_factor_authentication_device_totp, user: other_user, default: false, active: true) }
+
+ before do
+ # Use a real browser login so the session is established via the normal auth flow
+ # (user_setup callback), not via RequestStore stubbing. This exposes the
+ # prepend_before_action ordering bug where find_user ran before User.current was set.
+ login_with(admin.login, admin_password)
+ end
+
+ it "deletes the device" do
+ visit edit_user_path(other_user, tab: :two_factor_authentication)
+
+ expect(page).to have_css(".mobile-otp--two-factor-device-row", count: 1)
+
+ find(".two-factor--delete-button").click
+ dialog.confirm_flow_with(admin_password)
+
+ expect(page).to have_css(".generic-table--empty-row")
+ expect(other_user.otp_devices.reload).to be_empty
+ end
+end
diff --git a/modules/wikis/config/locales/crowdin/fr.yml b/modules/wikis/config/locales/crowdin/fr.yml
index 3cc343b39b1..bd767eb651b 100644
--- a/modules/wikis/config/locales/crowdin/fr.yml
+++ b/modules/wikis/config/locales/crowdin/fr.yml
@@ -22,23 +22,23 @@ fr:
one: Lien de la page de relation
other: Liens de la page de relation
wikis/xwiki_provider: Fournisseur XWiki
- permission_manage_wiki_page_links: Manage Wiki Page Links
- permission_view_wiki_page_links: View Wiki Page Links
+ permission_manage_wiki_page_links: Gérer les liens de la page Wiki
+ permission_view_wiki_page_links: Gérer les liens de la page Wiki
project_module_wiki_platforms: Fournisseurs de wiki
wikis:
buttons:
save_and_continue: Enregistrer et continuer
- wiki_page: Wiki page
+ wiki_page: Page wiki
inline_page_links_component:
empty_heading: Aucun lien intégré dans la page
empty_text: Les liens vers les pages wiki dans la description du lot de travaux apparaîtront automatiquement ici.
heading: Liens intégrés dans la page
page_link_component:
- remove: Remove page link
+ remove: Supprimer le lien de la page
relation_page_links_component:
- empty_heading: No related pages
- empty_text: Manually add links to other related wiki pages.
- heading: Related pages
+ empty_heading: Pas de pages liées
+ empty_text: Ajouter manuellement des liens vers d'autres pages wiki liées.
+ heading: Pages liées
admin:
wiki_providers:
index_description: Ajoutez un service wiki externe pour lier les lots de travaux à des pages wiki existantes ou en créer de nouvelles directement à partir d'OpenProject.
diff --git a/spec/controllers/members_controller_spec.rb b/spec/controllers/members_controller_spec.rb
index 42c8abf0550..5f4aaede6fd 100644
--- a/spec/controllers/members_controller_spec.rb
+++ b/spec/controllers/members_controller_spec.rb
@@ -211,6 +211,26 @@ RSpec.describe MembersController do
end
end
+ describe "#index" do
+ let(:role) { create(:project_role, permissions: [:manage_members]) }
+ let!(:member) { create(:member, project:, user:, roles: [role]) }
+
+ let!(:visible_group) { create(:group, members: [user]) }
+ let!(:hidden_group) { create(:group) }
+
+ before { login_as(user) }
+
+ it "only includes groups the user is a member of in the filter options" do
+ get :index, params: { project_id: project.id }
+
+ expect(response).to be_successful
+
+ groups = assigns(:members_filter_options)[:groups]
+ expect(groups).to include(visible_group)
+ expect(groups).not_to include(hidden_group)
+ end
+ end
+
describe "#create with reduced visibility" do
let(:project_permissions) { %i[manage_members invite_members_by_email] }
let!(:other_project) { create(:project) }
diff --git a/spec/features/work_packages/table/delete_work_packages_spec.rb b/spec/features/work_packages/table/delete_work_packages_spec.rb
index f84e7d2625a..fb78c1cd1ce 100644
--- a/spec/features/work_packages/table/delete_work_packages_spec.rb
+++ b/spec/features/work_packages/table/delete_work_packages_spec.rb
@@ -33,7 +33,7 @@ require "spec_helper"
RSpec.describe "Delete work package", :js do
let(:user) { create(:admin) }
let(:context_menu) { Components::WorkPackages::ContextMenu.new }
- let(:destroy_modal) { Components::WorkPackages::DestroyModal.new }
+ let(:destroy_modal) { Components::WorkPackages::DestroyModal.new(bulk_mode: true) }
before do
login_as(user)
@@ -85,7 +85,7 @@ RSpec.describe "Delete work package", :js do
context_menu.open_for(wp1)
context_menu.choose("Bulk delete")
- destroy_modal.confirm_children_deletion
+ destroy_modal.expect_listed(wp1, wp2, wp_child)
destroy_modal.confirm_deletion
loading_indicator_saveguard
diff --git a/spec/models/queries/work_packages/filter/group_filter_spec.rb b/spec/models/queries/work_packages/filter/group_filter_spec.rb
index 17279d11abe..7808cb6e90a 100644
--- a/spec/models/queries/work_packages/filter/group_filter_spec.rb
+++ b/spec/models/queries/work_packages/filter/group_filter_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe Queries::WorkPackages::Filter::GroupFilter do
describe "#allowed_values" do
before do
allow(Group)
- .to receive(:all)
+ .to receive(:visible)
.and_return [group]
end
@@ -81,7 +81,7 @@ RSpec.describe Queries::WorkPackages::Filter::GroupFilter do
before do
allow(Group)
- .to receive(:all)
+ .to receive(:visible)
.and_return([group, group2])
instance.values = [group2.id.to_s]
diff --git a/spec/support/components/work_packages/context_menu.rb b/spec/support/components/work_packages/context_menu.rb
index 0b5d1316d4d..1f77dff565c 100644
--- a/spec/support/components/work_packages/context_menu.rb
+++ b/spec/support/components/work_packages/context_menu.rb
@@ -74,10 +74,9 @@ module Components
def choose_delete_and_confirm_deletion
choose "Delete"
- # only handle the case where the modal does _not_ ask for descendants deletion confirmation
- within_modal(I18n.t("js.modals.destroy_work_package.title", label: "work package")) do
- click_button "Delete"
- end
+
+ dialog = ::Components::WorkPackages::DestroyModal.new
+ dialog.confirm_deletion
end
def expect_no_options(*options)
diff --git a/spec/support/components/work_packages/destroy_modal.rb b/spec/support/components/work_packages/destroy_modal.rb
index afd7b817e9e..d43d9fe1744 100644
--- a/spec/support/components/work_packages/destroy_modal.rb
+++ b/spec/support/components/work_packages/destroy_modal.rb
@@ -35,39 +35,36 @@ module Components
include Capybara::RSpecMatchers
include RSpec::Matchers
- def container
- "#wp_destroy_modal"
+ def initialize(bulk_mode: false)
+ @bulk_mode = bulk_mode
end
- def expect_listed(*wps)
- page.within(container) do
- if wps.length == 1
- wp = wps.first
- expect(page).to have_css("strong", text: "#{wp.subject} ##{wp.id}")
- else
- expect(page).to have_css(".danger-zone--warning",
- text: "Are you sure you want to delete the following work packages?")
- wps.each do |wp|
- expect(page).to have_css("li", text: "##{wp.id}#{wp.subject}")
- end
+ def dialog_css_selector
+ "dialog#wp-delete-dialog"
+ end
+
+ def within_dialog(&)
+ within(dialog_css_selector, &)
+ end
+
+ def expect_listed(*work_packages)
+ within_dialog do
+ work_packages.each do |work_package|
+ expect(page).to have_text(work_package.subject)
end
end
end
- def confirm_children_deletion
- page.within(container) do
- check "confirm-children-deletion"
- end
- end
-
def confirm_deletion
- page.within(container) do
- click_button "Delete"
+ within_dialog do
+ check "I understand that this deletion cannot be reversed"
+ expect(page).to have_button "Delete permanently", disabled: false
+ click_button "Delete permanently"
end
end
def cancel_deletion
- page.within(container) do
+ within_dialog do
click_button "Cancel"
end
end
diff --git a/spec/views/users/index.html.erb_spec.rb b/spec/views/users/index.html.erb_spec.rb
index 15fcb5af24b..ff8d74da9b5 100644
--- a/spec/views/users/index.html.erb_spec.rb
+++ b/spec/views/users/index.html.erb_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe "users/index" do
assign(:users, User.where(id: [admin.id, user.id]))
assign(:status, "all")
- assign(:groups, Group.all)
+ assign(:groups, Group.visible)
without_partial_double_verification do
allow(view).to receive_messages(current_user: admin, controller_name: "users", action_name: "index")