Merge pull request #17569 from opf/feature/59885-cancellation-dialogs-recurring-meetings

[#59885] Cancellation dialogs for one-time and recurring meetings
This commit is contained in:
Alexander Brandon Coles
2025-02-03 13:50:37 -03:00
committed by GitHub
34 changed files with 1305 additions and 95 deletions
@@ -0,0 +1,46 @@
<%#-- 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::DangerConfirmationDialog.new(
id:,
title:,
form_arguments: {
action: polymorphic_path([@project, @meeting.becomes(Meeting)]),
method: :delete,
data: { turbo: true }
}
)) do |dialog|
dialog.with_confirmation_message do |message|
message.with_heading(tag: :h2) { heading }
message.with_description_content(confirmation_message)
end
dialog.with_confirmation_check_box_content(I18n.t("text_permanent_delete_confirmation_checkbox_label"))
end
%>
@@ -0,0 +1,73 @@
# 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 Meetings
class DeleteDialogComponent < ApplicationComponent
include ApplicationHelper
include OpTurbo::Streamable
def initialize(meeting:, project:)
super
@meeting = meeting
@project = project
end
delegate :recurring_meeting, to: :@meeting
private
def id = "delete-meeting-dialog"
def title
if recurring_meeting.present?
I18n.t("meeting.delete_dialog.occurrence.title")
else
I18n.t("meeting.delete_dialog.one_time.title")
end
end
def heading
if recurring_meeting.present?
I18n.t("meeting.delete_dialog.occurrence.heading")
else
I18n.t("meeting.delete_dialog.one_time.heading")
end
end
def confirmation_message
if recurring_meeting.present?
t("meeting.delete_dialog.occurrence.confirmation_message_html", title: recurring_meeting.title)
else
t("meeting.delete_dialog.one_time.confirmation_message_html")
end
end
end
end
@@ -95,9 +95,10 @@
menu.with_item(label: delete_label,
scheme: :danger,
href: meeting_path(@meeting),
form_arguments: {
method: :delete, data: { confirm: t("text_are_you_sure"), turbo: 'false' }
href: polymorphic_path([:delete_dialog, @project, @meeting.becomes(Meeting)]),
tag: :a,
content_arguments: {
data: { controller: "async-dialog" }
}) do |item|
item.with_leading_visual_icon(icon: :trash)
end if delete_enabled?
@@ -30,6 +30,8 @@
module Meetings
class RowComponent < ::OpPrimer::BorderBoxRowComponent
delegate :current_project, to: :table
def project_name
helpers.link_to_project model.project, {}, {}, false
end
@@ -140,22 +142,15 @@ module Meetings
menu.with_item(label: recurring_meeting.present? ? I18n.t(:label_recurring_meeting_delete) : I18n.t(:label_meeting_delete),
scheme: :danger,
href: meeting_path(model),
form_arguments: {
method: :delete, data: { confirm: delete_confirm_message, turbo: false }
href: polymorphic_path([:delete_dialog, current_project, model.becomes(Meeting)]),
tag: :a,
content_arguments: {
data: { controller: "async-dialog" }
}) do |item|
item.with_leading_visual_icon(icon: :trash)
end
end
def delete_confirm_message
if recurring_meeting.present?
I18n.t(:label_recurring_meeting_delete_confirmation, name: recurring_meeting.title)
else
I18n.t("text_are_you_sure")
end
end
def recurring_label
render(Primer::BaseComponent.new(tag: :span, color: :muted)) do
concat render(Primer::Beta::Octicon.new(icon: :iterations, mr: 1, ml: 1))
@@ -0,0 +1,46 @@
<%#-- 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::DangerConfirmationDialog.new(
id:,
title:,
form_arguments: {
action: polymorphic_path([@project, @recurring_meeting]),
method: :delete,
data: { turbo: true }
}
)) do |dialog|
dialog.with_confirmation_message do |message|
message.with_heading(tag: :h2) { I18n.t("recurring_meeting.delete_dialog.heading") }
message.with_description_content(confirmation_message)
end
dialog.with_confirmation_check_box_content(I18n.t("text_permanent_delete_confirmation_checkbox_label"))
end
%>
@@ -0,0 +1,55 @@
# 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 RecurringMeetings
class DeleteDialogComponent < ApplicationComponent
include ApplicationHelper
include OpTurbo::Streamable
def initialize(recurring_meeting:, project:)
super
@recurring_meeting = recurring_meeting
@project = project
end
private
def id = "delete-recurring-meeting-dialog"
def title = I18n.t("recurring_meeting.delete_dialog.title")
def confirmation_message
t("recurring_meeting.delete_dialog.confirmation_message_html",
title: @recurring_meeting.title,
count: @recurring_meeting.remaining_occurrences&.count || 0) # actually ♾️, but we show same message as for 0
end
end
end
@@ -0,0 +1,51 @@
<%#-- 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::DangerConfirmationDialog.new(
id:,
title:,
confirm_button_text:,
cancel_button_text:,
form_arguments: {
action: polymorphic_path(
[:destroy_scheduled, @project, @scheduled_meeting.recurring_meeting],
start_time: @scheduled_meeting.start_time.iso8601
),
method: :delete,
data: { turbo: true }
}
)) do |dialog|
dialog.with_confirmation_message do |message|
message.with_heading(tag: :h2) { I18n.t("recurring_meeting.scheduled_delete_dialog.heading") }
message.with_description_content(confirmation_message)
end
dialog.with_confirmation_check_box_content(I18n.t("text_permanent_delete_confirmation_checkbox_label"))
end
%>
@@ -0,0 +1,55 @@
# 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 RecurringMeetings
class DeleteScheduledDialogComponent < ApplicationComponent
include ApplicationHelper
include OpTurbo::Streamable
def initialize(scheduled_meeting:, project:)
super
@scheduled_meeting = scheduled_meeting
@project = project
end
private
def id = "delete-scheduled-recurring-meeting-dialog"
def title = I18n.t("recurring_meeting.scheduled_delete_dialog.title")
def confirmation_message = t("recurring_meeting.scheduled_delete_dialog.confirmation_message_html")
def confirm_button_text = I18n.t("recurring_meeting.scheduled_delete_dialog.confirm_button")
def cancel_button_text = I18n.t(:button_close)
end
end
@@ -31,7 +31,7 @@ See COPYRIGHT and LICENSE files for more details.
render(Primer::OpenProject::DangerConfirmationDialog.new(
id: "recurring-meetings-end-series-dialog",
confirm_button_text: I18n.t(:label_recurring_meeting_series_end_now),
title: I18n.t(:text_are_you_sure),
title: I18n.t("recurring_meeting.end_series_dialog.title"),
form_arguments: {
action: end_series_recurring_meeting_path(@series),
method: :post
@@ -177,9 +177,10 @@ module RecurringMeetings
menu.with_item(
label: past? ? I18n.t(:label_recurring_meeting_delete) : I18n.t(:label_recurring_meeting_cancel),
scheme: :danger,
href: current_project_meeting_path(meeting),
form_arguments: {
method: :delete, data: { confirm: I18n.t(:label_recurring_occurrence_delete_confirmation), turbo: false }
href: polymorphic_path([:delete_dialog, current_project, meeting.becomes(Meeting)]),
tag: :a,
content_arguments: {
data: { controller: "async-dialog" }
}
) do |item|
item.with_leading_visual_icon(icon: :trash)
@@ -192,9 +193,11 @@ module RecurringMeetings
menu.with_item(
label: I18n.t(:label_recurring_meeting_cancel),
scheme: :danger,
href: delete_scheduled_recurring_meeting_path(model.recurring_meeting.id, start_time: model.start_time.iso8601),
form_arguments: {
method: :post, data: { confirm: I18n.t("text_are_you_sure"), turbo: false }
href: polymorphic_path([:delete_scheduled_dialog, current_project, model.recurring_meeting],
start_time: model.start_time.iso8601),
tag: :a,
content_arguments: {
data: { controller: "async-dialog" }
}
) do |item|
item.with_leading_visual_icon(icon: :trash)
@@ -61,10 +61,11 @@
menu.with_item(
label: I18n.t(:label_recurring_meeting_series_delete),
href: polymorphic_path([@project, @meeting]),
href: polymorphic_path([:delete_dialog, @project, @meeting]),
scheme: :danger,
form_arguments: {
method: :delete, data: { confirm: t("text_are_you_sure"), turbo: 'false' }
tag: :a,
content_arguments: {
data: { controller: 'async-dialog' }
}
) do |item|
item.with_leading_visual_icon(icon: :trash)
@@ -27,12 +27,13 @@
#++
class MeetingsController < ApplicationController
before_action :load_and_authorize_in_optional_project, only: %i[index new new_dialog show create history]
before_action :load_and_authorize_in_optional_project, only: %i[index new new_dialog show create delete_dialog destroy]
before_action :verify_activities_module_activated, only: %i[history]
before_action :determine_date_range, only: %i[history]
before_action :determine_author, only: %i[history]
before_action :build_meeting, only: %i[new new_dialog]
before_action :find_meeting, except: %i[index new create new_dialog]
before_action :set_project, only: %i[copy history update update_participants]
before_action :set_activity, only: %i[history]
before_action :find_copy_from_meeting, only: %i[create]
before_action :convert_params, only: %i[create update update_participants]
@@ -78,8 +79,9 @@ class MeetingsController < ApplicationController
else
render(Meetings::ShowComponent.new(meeting: @meeting, project: @project), layout: true)
end
elsif @meeting.agenda.present? && @meeting.agenda.locked?
params[:tab] ||= "minutes"
else
@project = @meeting.project
params[:tab] ||= "minutes" if @meeting.agenda.present? && @meeting.agenda.locked?
end
end
end
@@ -177,6 +179,13 @@ class MeetingsController < ApplicationController
end
end
def delete_dialog
respond_with_dialog Meetings::DeleteDialogComponent.new(
meeting: @meeting,
project: @project
)
end
def destroy # rubocop:disable Metrics/AbcSize
recurring = @meeting.recurring_meeting
@@ -203,6 +212,7 @@ class MeetingsController < ApplicationController
render turbo_stream: @turbo_streams
end
format.html do
@project = @meeting.project
render :edit
end
end
@@ -418,11 +428,14 @@ class MeetingsController < ApplicationController
@meeting = Meeting
.includes([:project, :author, { participants: :user }, :agenda, :minutes])
.find(params[:id])
@project = @meeting.project
rescue ActiveRecord::RecordNotFound
render_404
end
def set_project
@project = @meeting.project
end
def convert_params # rubocop:disable Metrics/AbcSize
# We do some preprocessing of `meeting_params` that we will store in this
# instance variable.
@@ -9,13 +9,15 @@ class RecurringMeetingsController < ApplicationController
include OpTurbo::DialogStreamHelper
before_action :find_meeting,
only: %i[show update details_dialog destroy edit init
delete_scheduled template_completed download_ics notify end_series end_series_dialog]
only: %i[show update details_dialog delete_dialog destroy edit init
delete_scheduled_dialog destroy_scheduled template_completed download_ics notify end_series
end_series_dialog]
before_action :find_optional_project,
only: %i[index show new create update details_dialog destroy edit delete_scheduled notify]
only: %i[index show new create update details_dialog delete_dialog destroy edit delete_scheduled_dialog
destroy_scheduled notify]
before_action :authorize_global, only: %i[index new create]
before_action :authorize, except: %i[index new create]
before_action :get_scheduled_meeting, only: %i[delete_scheduled]
before_action :get_scheduled_meeting, only: %i[delete_scheduled_dialog destroy_scheduled]
before_action :convert_params, only: %i[create update]
before_action :check_template_completable, only: %i[template_completed]
@@ -158,6 +160,13 @@ class RecurringMeetingsController < ApplicationController
redirect_to action: :show
end
def delete_dialog
respond_with_dialog RecurringMeetings::DeleteDialogComponent.new(
recurring_meeting: @recurring_meeting,
project: @project
)
end
def destroy
if @recurring_meeting.destroy
flash[:notice] = I18n.t(:notice_successful_delete)
@@ -189,8 +198,15 @@ class RecurringMeetingsController < ApplicationController
redirect_to action: :show, id: @recurring_meeting, status: :see_other
end
def delete_scheduled
if @scheduled.update(cancelled: true)
def delete_scheduled_dialog
respond_with_dialog RecurringMeetings::DeleteScheduledDialogComponent.new(
scheduled_meeting: @scheduled_meeting,
project: @project
)
end
def destroy_scheduled
if @scheduled_meeting.update(cancelled: true)
flash[:notice] = I18n.t(:notice_successful_cancel)
else
flash[:error] = I18n.t(:error_failed_to_delete_entry)
@@ -274,9 +290,9 @@ class RecurringMeetingsController < ApplicationController
end
def get_scheduled_meeting
@scheduled = @recurring_meeting.scheduled_meetings.find_or_initialize_by(start_time: params[:start_time])
@scheduled_meeting = @recurring_meeting.scheduled_meetings.find_or_initialize_by(start_time: params[:start_time])
render_400 unless @scheduled.meeting_id.nil?
render_400 unless @scheduled_meeting.meeting_id.nil?
end
def find_optional_project
@@ -33,9 +33,6 @@ class Meeting::ProjectAutocompleter < ApplicationForm
id: "project_id",
label: Project.model_name.human,
required: true,
data: {
"test-selector": "project_id"
},
autocomplete_options: {
with_search_icon: true,
openDirectly: false,
@@ -44,7 +41,10 @@ class Meeting::ProjectAutocompleter < ApplicationForm
inputName: "project_id",
inputValue: @project&.id,
appendTo: "#new-meeting-dialog",
filters: [{ name: "user_action", operator: "=", values: ["meetings/create"] }]
filters: [{ name: "user_action", operator: "=", values: ["meetings/create"] }],
data: {
"test-selector": "project_id"
}
}
)
end
@@ -82,7 +82,7 @@ See COPYRIGHT and LICENSE files for more details.
<% if authorize_for(:meetings, :destroy) %>
<li>
<%= link_to(t(:button_delete),
meeting_path(@meeting),
polymorphic_path([@project, @meeting]),
class: 'icon-context icon-delete',
method: :delete,
data: { confirm: t(:text_are_you_sure) }) %>
+37 -7
View File
@@ -136,13 +136,6 @@ en:
label_recurring_meeting_copy: "Copy as one-off"
label_recurring_meeting_cancel: "Cancel this occurrence"
label_recurring_meeting_delete: "Delete occurrence"
label_recurring_meeting_delete_confirmation: >
This meeting is part of a series called %{name}.
This will only delete this particular occurrence and not the entire series.
Do you want to continue?
label_recurring_occurrence_delete_confirmation: >
Any meeting information not in the template will be lost.
Do you want to continue?
label_recurring_meeting_restore: "Restore this occurrence"
label_recurring_meeting_schedule: "Schedule"
label_recurring_meeting_next_occurrence: "Next occurrence"
@@ -225,6 +218,19 @@ en:
structured_text: "Organize your meeting as a dynamic list of agenda items, optionally linking them to a work package."
structured_text_copy: "Copying a meeting will currently not copy the associated meeting agenda items, just the details"
copied: "Copied from Meeting #%{id}"
delete_dialog:
one_time:
title: "Delete meeting"
heading: "Delete this meeting?"
confirmation_message_html: >
This action is not reversible. Please proceed with caution.
occurrence:
title: "Delete meeting occurrence"
heading: "Delete this meeting occurrence?"
confirmation_message_html: >
This meeting is part of a series called <strong>%{title}</strong>.
This will only delete this particular occurrence and not the entire series.
Do you want to continue?
meeting_section:
untitled_title: "Untitled section"
@@ -284,6 +290,30 @@ en:
The following meetings are planned in the recurring meeting schedule but are not open yet.
Every time a planned meeting starts, the next one will automatically be opened for you.
You can also open planned meetings manually to import the template and start editing the agenda.
delete_dialog:
title: "Delete meeting series"
heading: "Permanently delete this meeting series?"
confirmation_message_html:
zero: >
Deleting <strong>%{title}</strong> will delete any meetings in this series.
This action is not reversible. Please proceed with caution.
one: >
Deleting <strong>%{title}</strong> will also delete one meeting in this series.
This action is not reversible. Please proceed with caution.
other: >
Deleting <strong>%{title}</strong> will delete all %{count} meetings in this series.
This action is not reversible. Please proceed with caution.
scheduled_delete_dialog:
title: "Cancel meeting occurrence"
heading: "Cancel this meeting occurrence?"
confirmation_message_html: >
Any meeting information not in the template will be lost.
Do you want to continue?
confirm_button: "Cancel occurrence"
end_series_dialog:
title: "End meeting series"
notice_successful_notification: "Notification sent successfully"
notice_timezone_missing: No time zone is set and %{zone} is assumed. To choose your time zone, please click here.
+15 -3
View File
@@ -28,12 +28,21 @@
Rails.application.routes.draw do
resources :projects, only: %i[] do
resources :meetings, only: %i[index new create show] do
resources :meetings, only: %i[index new create show destroy] do
collection do
get "menu" => "meetings/menus#show"
end
member do
get :delete_dialog
end
end
resources :recurring_meetings, only: %i[index new create show destroy] do
member do
get :delete_dialog
get :delete_scheduled_dialog
delete :destroy_scheduled
end
end
resources :recurring_meetings, only: %i[index new create show destroy]
end
resources :work_packages, only: %i[] do
@@ -60,8 +69,10 @@ Rails.application.routes.draw do
member do
get :details_dialog
get :download_ics
get :delete_dialog
get :delete_scheduled_dialog
post :init
post :delete_scheduled
delete :destroy_scheduled
post :template_completed
post :notify
post :end_series
@@ -86,6 +97,7 @@ Rails.application.routes.draw do
put :change_state
post :notify
get :history
get :delete_dialog
end
resources :agenda_items, controller: "meeting_agenda_items" do
collection do
@@ -70,8 +70,8 @@ module OpenProject::Meeting
require: :member
permission :delete_meetings,
{
meetings: [:destroy],
recurring_meetings: %i[destroy delete_scheduled]
meetings: %i[delete_dialog destroy],
recurring_meetings: %i[delete_dialog destroy delete_scheduled_dialog destroy_scheduled]
},
permissible_on: :project,
require: :member
@@ -0,0 +1,92 @@
# 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.
#++
require "rails_helper"
RSpec.describe Meetings::DeleteDialogComponent, type: :component do
include Rails.application.routes.url_helpers
let(:project) { build_stubbed(:project) }
let(:user) { build_stubbed(:user) }
subject do
render_inline(described_class.new(meeting:, project:))
page
end
before do
login_as(user)
end
describe "dialog form" do
let(:meeting) { build_stubbed(:meeting, project:) }
context "without a current project" do
let(:project) { nil }
it "renders the correct form action" do
expect(subject).to have_element "form", action: meeting_path(meeting)
end
end
context "with a current project" do
let(:project) { build_stubbed(:project) }
it "renders the correct form action" do
expect(subject).to have_element "form", action: project_meeting_path(project, meeting)
end
end
end
describe "with a one-off meeting" do
let(:meeting) { build_stubbed(:meeting, project:) }
it "shows a heading" do
expect(subject).to have_text "Delete this meeting?"
end
it "shows a simple confirmation message" do
expect(subject).to have_text "This action is not reversible. Please proceed with caution."
end
end
context "with an associated recurring/templated meeting" do
let(:series) { build_stubbed(:recurring_meeting) }
let(:meeting) { build_stubbed(:structured_meeting_template, recurring_meeting: series) }
it "shows a heading" do
expect(subject).to have_text "Delete this meeting occurrence?"
end
it "shows a message that the meeting is part of a series" do
expect(subject).to have_text "meeting is part of a series"
end
end
end
@@ -0,0 +1,134 @@
# 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.
#++
require "rails_helper"
RSpec.describe Meetings::RowComponent, type: :component do
include Rails.application.routes.url_helpers
let(:project) { build_stubbed(:project) }
let(:table) do
instance_double(Meetings::TableComponent, columns: [], grid_class: "test", has_actions?: true, current_project:)
end
let(:current_project) { nil }
let(:user) { build_stubbed(:user) }
subject do
render_inline(described_class.new(row: meeting, table:))
page
end
before do
login_as(user)
end
describe "actions" do
context "with default permissions" do
context "with a one-off meeting" do
let(:meeting) { build_stubbed(:meeting, project:) }
it "shows default menu items" do
expect(subject).to have_link "Download iCalendar event"
end
end
context "with an associated recurring/templated meeting" do
let(:series) { build_stubbed(:recurring_meeting, project:) }
let(:meeting) { build_stubbed(:structured_meeting_template, recurring_meeting: series, project:) }
it "shows default menu items" do
expect(subject).to have_link "View meeting series"
expect(subject).to have_link "Download iCalendar event"
end
end
end
context "with project delete meetings permissions" do
before do
mock_permissions_for(user) do |mock|
mock.allow_in_project(:delete_meetings, project:)
end
end
context "with a one-off meeting" do
let(:meeting) { build_stubbed(:meeting, project:) }
context "without a current project" do
it "shows delete menu item" do
expect(subject).to have_link "Delete meeting", href: delete_dialog_meeting_path(meeting)
end
end
context "with a current project" do
let(:current_project) { project }
it "shows delete menu item" do
expect(subject).to have_link "Delete meeting", href: delete_dialog_project_meeting_path(project, meeting)
end
end
end
context "with an associated recurring/templated meeting" do
let(:series) { build_stubbed(:recurring_meeting, project:) }
let(:meeting) { build_stubbed(:structured_meeting_template, recurring_meeting: series, project:) }
context "without a current project" do
it "shows delete menu item" do
expect(subject).to have_link "Delete occurrence", href: delete_dialog_meeting_path(meeting)
end
end
context "with a current project" do
let(:current_project) { project }
it "shows delete menu item" do
expect(subject).to have_link "Delete occurrence", href: delete_dialog_project_meeting_path(project, meeting)
end
end
end
end
context "with project create meetings permissions" do
before do
mock_permissions_for(user) do |mock|
mock.allow_in_project(:create_meetings, project:)
end
end
context "with a one-off meeting" do
let(:meeting) { build_stubbed(:meeting, project:) }
it "shows copy menu item" do
expect(subject).to have_link "Copy meeting"
end
end
end
end
end
@@ -0,0 +1,93 @@
# 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.
#++
require "rails_helper"
RSpec.describe RecurringMeetings::DeleteDialogComponent, type: :component do
include Rails.application.routes.url_helpers
let(:project) { build_stubbed(:project) }
let(:recurring_meeting) { build_stubbed(:recurring_meeting, project:, end_after: :iterations, iterations: 6) }
let(:meeting) { build_stubbed(:structured_meeting_template, recurring_meeting:) }
let(:user) { build_stubbed(:user) }
subject do
render_inline(described_class.new(recurring_meeting:, project:))
page
end
before do
login_as(user)
end
describe "dialog form" do
context "without a current project" do
let(:project) { nil }
it "renders the correct form action" do
expect(subject).to have_element "form", action: recurring_meeting_path(recurring_meeting)
end
end
context "with a current project" do
let(:project) { build_stubbed(:project) }
it "renders the correct form action" do
expect(subject).to have_element "form", action: project_recurring_meeting_path(project, recurring_meeting)
end
end
end
it "shows a heading" do
expect(subject).to have_text "Permanently delete this meeting series?"
end
context "with a meeting series that ends - with several remaining meetings" do
it "shows a confirmation message with a count of the remaining meetings" do
expect(subject).to have_text "will delete all 6 meetings in this series"
end
end
context "with a meeting series that ends - with one remaining meeting" do
let(:recurring_meeting) { build_stubbed(:recurring_meeting, project:, end_after: :iterations, iterations: 1) }
it "shows a confirmation message mentioning one remaining meeting" do
expect(subject).to have_text "will also delete one meeting in this series"
end
end
context "with a meeting series that never ends" do
let(:recurring_meeting) { build_stubbed(:recurring_meeting, project:, end_after: :never) }
it "shows a confirmation message without a count" do
expect(subject).to have_text "will delete any meetings in this series"
end
end
end
@@ -0,0 +1,77 @@
# 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.
#++
require "rails_helper"
RSpec.describe RecurringMeetings::DeleteScheduledDialogComponent, type: :component do
include Rails.application.routes.url_helpers
let(:project) { build_stubbed(:project) }
let(:recurring_meeting) { build_stubbed(:recurring_meeting, project:, end_after: :iterations, iterations: 6) }
let(:start_time) { 1.day.from_now }
let(:scheduled_meeting) { recurring_meeting.scheduled_meetings.find_or_initialize_by(start_time: start_time) }
let(:meeting) { build_stubbed(:structured_meeting_template, recurring_meeting:) }
let(:user) { build_stubbed(:user) }
subject do
render_inline(described_class.new(scheduled_meeting:, project:))
page
end
before do
login_as(user)
end
describe "dialog form" do
context "without a current project" do
let(:project) { nil }
it "renders the correct form action" do
expect(subject).to have_element "form", action: destroy_scheduled_recurring_meeting_path(
recurring_meeting, start_time: start_time.iso8601
)
end
end
context "with a current project" do
let(:project) { build_stubbed(:project) }
it "renders the correct form action" do
expect(subject).to have_element "form", action: destroy_scheduled_project_recurring_meeting_path(
project, recurring_meeting, start_time: start_time.iso8601
)
end
end
end
it "shows a heading" do
expect(subject).to have_text "Cancel this meeting occurrence?"
end
end
@@ -0,0 +1,108 @@
# 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.
#++
require "rails_helper"
RSpec.describe RecurringMeetings::RowComponent, type: :component do
include Rails.application.routes.url_helpers
let(:project) { build_stubbed(:project) }
let(:table) do
instance_double(RecurringMeetings::TableComponent,
columns: [], grid_class: "test", has_actions?: true, current_project:)
end
let(:recurring_meeting) { build_stubbed(:recurring_meeting, project:) }
let(:current_project) { nil }
let(:user) { build_stubbed(:user) }
subject do
render_inline(described_class.new(row: scheduled_meeting, table:))
page
end
before do
login_as(user)
end
describe "actions" do
context "with project delete meetings permissions" do
before do
mock_permissions_for(user) do |mock|
mock.allow_in_project(:delete_meetings, project:)
end
end
context "with a scheduled meeting" do
let(:scheduled_meeting) { build_stubbed(:scheduled_meeting, :scheduled, recurring_meeting:) }
context "without a current project" do
it "shows cancel menu item" do
expect(subject).to have_link "Cancel this occurrence",
href: delete_scheduled_dialog_recurring_meeting_path(
recurring_meeting, start_time: scheduled_meeting.start_time.iso8601
)
end
end
context "with a current project" do
let(:current_project) { project }
it "shows cancel menu item" do
expect(subject).to have_link "Cancel this occurrence",
href: delete_scheduled_dialog_project_recurring_meeting_path(
project, recurring_meeting, start_time: scheduled_meeting.start_time.iso8601
)
end
end
end
context "with an instantiated meeting" do
let(:scheduled_meeting) { build_stubbed(:scheduled_meeting, recurring_meeting:, meeting:) }
let(:meeting) { build_stubbed(:meeting) }
context "without a current project" do
it "shows cancel menu item" do
expect(subject).to have_link "Cancel this occurrence",
href: delete_dialog_meeting_path(scheduled_meeting.meeting)
end
end
context "with a current project" do
let(:current_project) { project }
it "shows cancel menu item" do
expect(subject).to have_link "Cancel this occurrence",
href: delete_dialog_project_meeting_path(project, scheduled_meeting.meeting)
end
end
end
end
end
end
@@ -73,7 +73,7 @@ RSpec.describe "Recurring meetings CRUD",
end
let(:current_user) { user }
let(:show_page) { Pages::RecurringMeeting::Show.new(meeting) }
let(:show_page) { Pages::RecurringMeeting::Show.new(meeting, project:) }
let(:meetings_page) { Pages::Meetings::Index.new(project:) }
before do
@@ -87,13 +87,17 @@ RSpec.describe "Recurring meetings CRUD",
it "can delete a recurring meeting from the show page and return to the index page" do
show_page.visit!
click_on "recurring-meeting-action-menu"
show_page.delete_meeting_series
show_page.within_modal "Delete meeting series" do
check "I understand that this deletion cannot be reversed", allow_label_click: true
accept_confirm(I18n.t("text_are_you_sure")) do
click_on "Delete meeting series"
click_on "Delete permanently"
end
expect(page).to have_current_path meetings_path # check path
expect(page).to have_current_path project_meetings_path(project)
expect_flash(type: :success, message: "Successful deletion.")
show_page.expect_no_meeting date: "12/31/2024 01:30 PM"
end
it "can use the 'Create from template' button" do
@@ -110,28 +114,48 @@ RSpec.describe "Recurring meetings CRUD",
show_page.expect_open_meeting date: "01/07/2025 01:30 PM"
end
it "can cancel an occurrence" do
it "can cancel an occurrence from the show page" do
show_page.visit!
accept_confirm(I18n.t(:label_recurring_occurrence_delete_confirmation)) do
show_page.cancel_occurrence date: "12/31/2024 01:30 PM"
show_page.cancel_occurrence date: "12/31/2024 01:30 PM"
show_page.within_modal "Delete meeting occurrence" do
check "I understand that this deletion cannot be reversed", allow_label_click: true
click_on "Delete permanently"
end
expect_flash(type: :success, message: "Successful cancellation.")
expect(page).to have_current_path(show_page.project_path)
expect(page).to have_current_path(show_page.path)
show_page.expect_no_open_meeting date: "12/31/2024 01:30 PM"
show_page.expect_cancelled_meeting date: "12/31/2024 01:30 PM"
end
it "can cancel a scheduled occurrence from the show page" do
show_page.visit!
show_page.cancel_scheduled_occurrence date: "01/07/2025 01:30 PM"
show_page.within_modal "Cancel meeting occurrence" do
check "I understand that this deletion cannot be reversed", allow_label_click: true
click_on "Cancel occurrence"
end
expect_flash(type: :success, message: "Successful cancellation.")
expect(page).to have_current_path(show_page.path)
show_page.expect_cancelled_meeting date: "01/07/2025 01:30 PM"
end
it "can edit the details of a recurring meeting" do
show_page.visit!
show_page.expect_subtitle text: "Every week on Tuesday at 01:30 PM, ends on 01/14/2025"
show_page.edit_meeting_series
show_page.in_edit_dialog do
show_page.within_modal "Edit Meeting" do
page.select("Daily", from: "Frequency")
meetings_page.set_start_time "11:00"
page.select("a number of occurrences", from: "Meeting series ends")
@@ -153,11 +177,17 @@ RSpec.describe "Recurring meetings CRUD",
show_page.expect_scheduled_meeting date: "01/07/2025 01:30 PM"
show_page.expect_scheduled_actions date: "01/07/2025 01:30 PM"
accept_confirm(I18n.t(:label_recurring_occurrence_delete_confirmation)) do
show_page.cancel_occurrence date: "12/31/2024 01:30 PM"
show_page.cancel_occurrence date: "12/31/2024 01:30 PM"
show_page.within_modal "Delete meeting occurrence" do
check "I understand that this deletion cannot be reversed", allow_label_click: true
click_on "Delete permanently"
end
wait_for_network_idle
expect_flash(type: :success, message: "Successful cancellation.")
expect(page).to have_current_path(show_page.path)
show_page.expect_cancelled_meeting date: "12/31/2024 01:30 PM"
show_page.expect_cancelled_actions date: "12/31/2024 01:30 PM"
end
@@ -75,18 +75,18 @@ RSpec.describe "Recurring meetings end series", :js do
it "can end the meeting early" do
show_page.visit!
page.find_test_selector("recurring-meeting-action-menu").click
click_on "End meeting series"
expect(page).to have_css("#recurring-meetings-end-series-dialog")
expect(page).to have_text("Ending the series will delete any future open or scheduled meeting occurrences")
show_page.end_meeting_series
show_page.within_modal "End meeting series" do
expect(page).to have_text("Ending the series will delete any future open or scheduled meeting occurrences")
retry_block do
check "I understand that this deletion cannot be reversed"
expect(page).to have_checked_field("I understand that this deletion cannot be reversed")
retry_block do
check "I understand that this deletion cannot be reversed", allow_label_click: true
expect(page).to have_checked_field("I understand that this deletion cannot be reversed")
end
click_on "End series now"
end
click_on "End series now"
expect(page).to have_current_path recurring_meeting_path(meeting)
expect(page).to have_text("Nothing to display")
end
@@ -0,0 +1,138 @@
# 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.
#++
require "spec_helper"
require_relative "../../support/pages/meetings/new"
require_relative "../../support/pages/structured_meeting/show"
require_relative "../../support/pages/recurring_meeting/show"
require_relative "../../support/pages/meetings/index"
RSpec.describe "Recurring meetings global CRUD", :js do
include Components::Autocompleter::NgSelectAutocompleteHelpers
before_all do
travel_to(Date.new(2024, 12, 1))
end
after(:all) do # rubocop:disable RSpec/BeforeAfterAll
travel_back
end
shared_let(:project) { create(:project, enabled_module_names: %w[meetings]) }
shared_let(:user) do
create :user,
lastname: "First",
preferences: { time_zone: "Etc/UTC" },
member_with_permissions: { project => %i[view_meetings create_meetings edit_meetings delete_meetings] }
end
shared_let(:other_user) do
create(:user,
lastname: "Second",
member_with_permissions: { project => %i[view_meetings] })
end
shared_let(:no_member_user) do
create(:user,
lastname: "Third")
end
shared_let(:meeting) do
create :recurring_meeting,
project:,
start_time: "2024-12-31T13:30:00Z",
duration: 1.5,
frequency: "weekly",
end_after: "specific_date",
end_date: "2025-01-15",
author: user
end
let(:current_user) { user }
let(:show_page) { Pages::RecurringMeeting::Show.new(meeting, project: nil) }
let(:meetings_page) { Pages::Meetings::Index.new(project: nil) }
before do
travel_to(Date.new(2024, 12, 1))
login_as current_user
# Assuming the first init job has run
RecurringMeetings::InitNextOccurrenceJob.perform_now(meeting, meeting.first_occurrence.to_time)
end
it "can delete a recurring meeting from the show page and return to the index page" do
show_page.visit!
show_page.delete_meeting_series
show_page.within_modal "Delete meeting series" do
check "I understand that this deletion cannot be reversed", allow_label_click: true
click_on "Delete permanently"
end
expect(page).to have_current_path meetings_path
expect_flash(type: :success, message: "Successful deletion.")
show_page.expect_no_meeting date: "12/31/2024 01:30 PM"
end
it "can cancel an occurrence from the show page" do
show_page.visit!
show_page.cancel_occurrence date: "12/31/2024 01:30 PM"
show_page.within_modal "Delete meeting occurrence" do
check "I understand that this deletion cannot be reversed", allow_label_click: true
click_on "Delete permanently"
end
expect_flash(type: :success, message: "Successful cancellation.")
expect(page).to have_current_path(show_page.path)
show_page.expect_no_open_meeting date: "12/31/2024 01:30 PM"
show_page.expect_cancelled_meeting date: "12/31/2024 01:30 PM"
end
it "can cancel a scheduled occurrence from the show page" do
show_page.visit!
show_page.cancel_scheduled_occurrence date: "01/07/2025 01:30 PM"
show_page.within_modal "Cancel meeting occurrence" do
check "I understand that this deletion cannot be reversed", allow_label_click: true
click_on "Cancel occurrence"
end
expect_flash(type: :success, message: "Successful cancellation.")
expect(page).to have_current_path(show_page.path)
show_page.expect_cancelled_meeting date: "01/07/2025 01:30 PM"
end
end
@@ -209,13 +209,18 @@ RSpec.describe "Structured meetings CRUD",
end
it "can delete a meeting and get back to the index page" do
click_on("op-meetings-header-action-trigger")
show_page.trigger_dropdown_menu_item "Delete meeting"
show_page.expect_modal "Delete meeting"
accept_confirm(I18n.t("text_are_you_sure")) do
click_on "Delete meeting"
show_page.within_modal "Delete meeting" do
check "I understand that this deletion cannot be reversed", allow_label_click: true
click_on "Delete permanently"
end
expect(page).to have_current_path project_meetings_path(project)
expect_flash(type: :success, message: "Successful deletion.")
end
context "when exporting as ICS" do
@@ -315,7 +320,7 @@ RSpec.describe "Structured meetings CRUD",
# check for copied participants with attended status reset
page.find_test_selector("manage-participants-button").click
expect(page).to have_css("#edit-participants-dialog")
expect(page).to have_modal("Participants")
expect(page).to have_field(id: "checkbox_invited_#{other_user.id}", checked: true)
expect(page).to have_field(id: "checkbox_attended_#{other_user.id}", checked: false)
@@ -0,0 +1,107 @@
# 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.
#++
require "spec_helper"
require_relative "../../support/pages/meetings/new"
require_relative "../../support/pages/structured_meeting/show"
require_relative "../../support/pages/meetings/index"
RSpec.describe "Structured meetings global CRUD", :js do
include Components::Autocompleter::NgSelectAutocompleteHelpers
shared_let(:project) { create(:project, enabled_module_names: %w[meetings work_package_tracking]) }
shared_let(:user) do
create(:user,
lastname: "First",
member_with_permissions: { project => %i[view_meetings create_meetings edit_meetings delete_meetings manage_agendas
view_work_packages] }).tap do |u|
u.pref[:time_zone] = "Etc/UTC"
u.save!
end
end
shared_let(:other_user) do
create(:user,
lastname: "Second",
member_with_permissions: { project => %i[view_meetings view_work_packages] })
end
shared_let(:no_member_user) do
create(:user,
lastname: "Third")
end
shared_let(:work_package) do
create(:work_package, project:, subject: "Important task")
end
let(:current_user) { user }
let(:new_page) { Pages::Meetings::New.new(project) }
let(:meeting) { StructuredMeeting.last }
let(:show_page) { Pages::StructuredMeeting::Show.new(meeting) }
let(:meetings_page) { Pages::Meetings::Index.new(project: nil) }
before do |test|
login_as current_user
meetings_page.visit!
expect(page).to have_current_path(meetings_page.path) # rubocop:disable RSpec/ExpectInHook
meetings_page.click_on "add-meeting-button"
meetings_page.click_on "One-time"
meetings_page.set_project project
meetings_page.set_title "Some title"
meetings_page.set_start_date "2013-03-28"
meetings_page.set_start_time "13:30"
meetings_page.set_duration "1.5"
if test.metadata[:checked]
expect(page).to have_unchecked_field "send_notifications" # rubocop:disable RSpec/ExpectInHook
check "send_notifications"
end
meetings_page.click_create
end
it "can delete a meeting and get back to the index page" do
show_page.trigger_dropdown_menu_item "Delete meeting"
show_page.expect_modal "Delete meeting"
show_page.within_modal "Delete meeting" do
check "I understand that this deletion cannot be reversed", allow_label_click: true
click_on "Delete permanently"
end
expect(page).to have_current_path meetings_path
expect_flash(type: :success, message: "Successful deletion.")
end
end
@@ -90,7 +90,7 @@ RSpec.describe "Recurring meetings show",
describe "upcoming tab" do
let!(:upcoming_open_meeting) do
create(:structured_meeting, recurring_meeting:, start_time: Time.zone.today + 10.hours, state: :open)
create(:structured_meeting, recurring_meeting:, start_time: Time.zone.today + 1.day + 10.hours, state: :open)
end
let!(:open_meeting) do
create :scheduled_meeting,
@@ -130,7 +130,7 @@ module Pages::Meetings
def expect_delete_action(meeting)
within more_menu(meeting) do
expect(page).to have_button("Delete meeting")
expect(page).to have_link("Delete meeting")
end
end
@@ -32,18 +32,18 @@ module Pages::RecurringMeeting
class Show < ::Pages::Meetings::Base
attr_accessor :meeting
def initialize(meeting)
super
def initialize(meeting, project: nil)
super(project)
self.meeting = meeting
end
def path
recurring_meeting_path(meeting)
end
def project_path
project_recurring_meeting_path(meeting.project, meeting)
if project
project_recurring_meeting_path(project, meeting)
else
recurring_meeting_path(meeting)
end
end
def expect_scheduled_meeting(date:)
@@ -100,6 +100,17 @@ module Pages::RecurringMeeting
click_on "more-button"
click_on "Cancel this occurrence"
end
expect_modal("Delete meeting occurrence")
end
def cancel_scheduled_occurrence(date:)
within("li", text: date) do
click_on "more-button"
click_on "Cancel this occurrence"
end
expect_modal("Cancel meeting occurrence")
end
def expect_subtitle(text:)
@@ -110,11 +121,25 @@ module Pages::RecurringMeeting
page.find_test_selector("recurring-meeting-action-menu").click
click_on "Edit meeting series"
expect(page).to have_css("#new-meeting-dialog")
expect_modal("Edit Meeting")
end
def in_edit_dialog(&)
page.within("#new-meeting-dialog", &)
def delete_meeting_series
page.find_test_selector("recurring-meeting-action-menu").click
click_on "Delete meeting series"
expect_modal("Delete meeting series")
end
def end_meeting_series
page.find_test_selector("recurring-meeting-action-menu").click
click_on "End meeting series"
expect_modal("End meeting series")
end
def expect_modal(...)
expect(page).to have_modal(...)
end
def expect_no_meeting(date:)
@@ -41,7 +41,7 @@ module Pages::StructuredMeeting::Mobile
within(meeting_details_container) do
click_link_or_button "Show all"
end
expect(page).to have_css("#edit-participants-dialog")
expect(page).to have_modal("Participants")
end
end
end
@@ -63,6 +63,10 @@ module Pages::StructuredMeeting
end
end
def expect_modal(...)
expect(page).to have_modal(...)
end
def expect_no_add_form
expect(page).not_to have_test_selector("#meeting-agenda-items-form-component")
end
@@ -204,11 +208,11 @@ module Pages::StructuredMeeting
def open_participant_form
page.find_test_selector("manage-participants-button").click
expect(page).to have_css("#edit-participants-dialog")
expect_modal("Participants")
end
def in_participant_form(&)
page.within("#edit-participants-dialog", &)
page.within_modal("Participants", &)
end
def expect_participant(participant, invited: false, attended: false, editable: true)
@@ -128,7 +128,7 @@ RSpec.describe "custom fields of type hierarchy", :js do
hierarchy_page.open_action_menu_for("Phoenix Squad")
click_on "Delete"
expect(page).to have_test_selector("op-custom-fields--delete-item-dialog")
check "I understand that this deletion cannot be reversed"
check "I understand that this deletion cannot be reversed", allow_label_click: true
click_on "Delete permanently"
expect(page).not_to have_test_selector("op-custom-fields--delete-item-dialog")
expect(page).to have_test_selector("op-custom-fields--hierarchy-item", count: 1)