mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Merge branch 'dev' into code-maintenance/73798-remove-scrum_projects-feature-flag
This commit is contained in:
@@ -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 %>
|
||||
@@ -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
|
||||
@@ -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 %>
|
||||
@@ -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
|
||||
@@ -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
|
||||
%>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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é.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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?"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
<div
|
||||
class="spot-modal"
|
||||
data-indicator-name="modal"
|
||||
id="wp_destroy_modal"
|
||||
>
|
||||
<div id="spotModalTitle" class="spot-modal--header">{{text.title}}</div>
|
||||
|
||||
<div class="spot-modal--body spot-container">
|
||||
@if (singleWorkPackage) {
|
||||
<p>
|
||||
<span [textContent]="text.single_text"></span>
|
||||
<strong> {{ singleWorkPackage.subject }} #{{ singleWorkPackage.id }}</strong>
|
||||
<span>?</span>
|
||||
</p>
|
||||
@if (singleWorkPackageChildren && singleWorkPackageChildren.length > 0) {
|
||||
<div>
|
||||
<p class="danger-zone--warning">
|
||||
<span class="icon-context icon-error"></span>
|
||||
<strong [textContent]="text.warning"></strong>:
|
||||
<span [textContent]="text.hasChildren(singleWorkPackage)"></span>
|
||||
</p>
|
||||
<ul>
|
||||
@for (child of singleWorkPackageChildren; track child) {
|
||||
<li class="spot-list--item">
|
||||
<div class="spot-list--item-title">
|
||||
#<span [textContent]="child.id"></span>
|
||||
<span [textContent]="child.subject || ''"></span>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<p>
|
||||
<span [textContent]="text.deletesChildren"></span>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (workPackages.length > 1) {
|
||||
<p class="danger-zone--warning">
|
||||
<span class="icon-context icon-error"></span>
|
||||
<strong [textContent]="text.bulk_text"></strong>
|
||||
</p>
|
||||
<ul>
|
||||
@for (wp of workPackages; track wp) {
|
||||
<li class="spot-list--item">
|
||||
<div class="spot-list--item-title">
|
||||
#<span [textContent]="wp.id"></span>
|
||||
<span [textContent]="wp.subject"></span>
|
||||
@if (children(wp).length > 0) {
|
||||
<strong>(+ {{ text.childCount(wp) }})</strong>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
@if (mustConfirmChildren) {
|
||||
<div>
|
||||
<label class="form--label-with-check-box -no-ellipsis">
|
||||
<div class="form--check-box-container">
|
||||
<input type="checkbox"
|
||||
name="confirm-children-deletion"
|
||||
id="confirm-children-deletion"
|
||||
[(ngModel)]="childrenDeletionConfirmed"
|
||||
class="form--check-box"/>
|
||||
</div>
|
||||
{{ text.label_confirm_children_deletion }}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="spot-action-bar">
|
||||
<div class="spot-action-bar--right">
|
||||
<button
|
||||
class="button button_no-margin spot-modal--cancel-button spot-action-bar--action"
|
||||
[textContent]="text.cancel"
|
||||
(click)="closeMe($event)"
|
||||
></button>
|
||||
<button
|
||||
class="button button_no-margin -danger spot-action-bar--action"
|
||||
[attr.disabled]="busy || blockedDueToUnconfirmedChildren || undefined"
|
||||
(click)="confirmDeletion($event)">
|
||||
<svg trash-icon size="small" />
|
||||
{{ text.confirm }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
+13
-5
@@ -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;
|
||||
|
||||
+3
-5
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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]
|
||||
|
||||
|
||||
+37
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user