mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Delete dialog primer
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,117 @@
|
||||
# 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:)
|
||||
super
|
||||
@work_packages = work_packages
|
||||
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))
|
||||
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,99 @@
|
||||
# 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:)
|
||||
super
|
||||
@work_package = work_package
|
||||
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])
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
else
|
||||
WorkPackages::BulkDeleteDialogComponent.new(work_packages: @work_packages)
|
||||
end
|
||||
|
||||
respond_with_dialog component
|
||||
end
|
||||
|
||||
def edit
|
||||
setup_edit
|
||||
@@ -76,7 +88,9 @@ class WorkPackages::BulkController < ApplicationController
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to (project_work_packages_path(@work_packages.first.project))
|
||||
redirect_back_or_to(project_work_packages_path(@work_packages.first.project),
|
||||
status: :see_other,
|
||||
allow_other_host: false)
|
||||
end
|
||||
format.json do
|
||||
head :ok
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1046,13 +1046,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,11 @@ export class PathHelperService {
|
||||
return `${this.workPackagesPath(null)}/bulk`;
|
||||
}
|
||||
|
||||
public workPackagesBulkDeleteDialogPath(ids:string[]) {
|
||||
const params = ids.map((id) => `ids[]=${encodeURIComponent(id)}`).join('&');
|
||||
return `${this.workPackagesPath(null)}/bulk/delete_dialog?${params}`;
|
||||
}
|
||||
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -12,12 +12,10 @@ 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';
|
||||
@@ -41,7 +39,6 @@ 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);
|
||||
@@ -97,7 +94,10 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
this.opModalService.show(WpDestroyModalComponent, this.injector, { workPackages: [this.workPackage] });
|
||||
void this.turboRequests.request(
|
||||
this.PathHelper.workPackagesBulkDeleteDialogPath([this.workPackage.id!]),
|
||||
{ method: 'GET' },
|
||||
);
|
||||
break;
|
||||
case 'log_time':
|
||||
void this.turboRequests.request(this.PathHelper.timeEntryWorkPackageDialog(this.workPackage.id!), { method: 'GET' });
|
||||
|
||||
+2
-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,8 @@ 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) as string[];
|
||||
void this.turboRequests.request(this.pathHelper.workPackagesBulkDeleteDialogPath(ids), { method: 'GET' });
|
||||
}
|
||||
|
||||
private editSelectedWorkPackages(link:any) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user