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 @@ -
-
{{text.title}}
- -
- @if (singleWorkPackage) { -

- - {{ singleWorkPackage.subject }} #{{ singleWorkPackage.id }} - ? -

- @if (singleWorkPackageChildren && singleWorkPackageChildren.length > 0) { -
-

- - : - -

-
    - @for (child of singleWorkPackageChildren; track child) { -
  • -
    - # - -
    -
  • - } -
-

- -

-
- } - } - @if (workPackages.length > 1) { -

- - -

- - } - @if (mustConfirmChildren) { -
- -
- } -
- -
-
- - -
-
-
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")