Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[59845] Update Datepicker for automatic scheduling mode #17349

Draft
wants to merge 66 commits into
base: feature/42388-new-automatic-scheduling-mode
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
126839a
Start on using Primer components inside the datepicker modal
HDinger Dec 4, 2024
43ee807
Remove unused angular components which are going to be replaced by Vi…
HDinger Dec 9, 2024
c076629
Use existing fields for now to get the modal working again
HDinger Dec 9, 2024
a52d9da
Render the form from rails and only the actual datepicker in angular.
HDinger Dec 10, 2024
8019ec7
Extract logic to update the edit field after the turboFrame was submi…
HDinger Dec 11, 2024
7549c0c
Extract logic of the progress modal to be reused for the datepicker m…
HDinger Dec 12, 2024
3b09e05
Update the dialog when the "ignore_non_working_days" checkbox is checked
HDinger Dec 16, 2024
2afceab
Update the dialog content when chaning the scheduling mode
HDinger Dec 16, 2024
39d3861
Cancel the editField correctly when canceling the dialog
HDinger Dec 16, 2024
a8a7bf8
First try on updating the datepicker when the fields above change
HDinger Dec 16, 2024
94b48ff
Correctly save the sheduling mode and duration
HDinger Dec 17, 2024
3534fae
Preserve values when updating the content via turboStream
HDinger Dec 17, 2024
bea6611
Disable the form in automatic scheduling mode
HDinger Dec 17, 2024
72b2610
Show additional tabs in datepicker dialog
HDinger Dec 17, 2024
677afdf
Do not update the fields so often
HDinger Dec 17, 2024
0f06725
Correctly parese the duration
HDinger Dec 17, 2024
9caba14
Revert commit d2a6ba2a04fe038f4f008214997e931091971cea
HDinger Dec 17, 2024
35aeb65
Improve event handling when datepicker field change so that also non-…
HDinger Dec 17, 2024
de7366a
Forbid editing the relations in the datepicker
HDinger Dec 18, 2024
e5db72e
Take care that something is shown when opening the duration field
HDinger Dec 18, 2024
78f3e13
Hack some roundtrip between the datpicker and the fields above (wip)
HDinger Dec 18, 2024
cf9bdc6
Allow duration to be editable in automatic mode
HDinger Dec 19, 2024
96d1e37
Avoid code repetition
HDinger Dec 19, 2024
077d3d1
Extract doubled code into a method
HDinger Dec 19, 2024
9a51d90
Please rubocop and eslint
HDinger Dec 19, 2024
1119399
Handle Milestones in new datepicker modal
HDinger Dec 20, 2024
b323d95
If the WP is automatically scheduled but there is no predecessor, we …
HDinger Dec 20, 2024
183ec10
Fine tune text and spaces
HDinger Jan 10, 2025
b4d3a73
Datepicker for WorkPackage Create case
HDinger Jan 13, 2025
5e741e8
Disbale saving when scheduling is not possible
HDinger Jan 13, 2025
dc1b4eb
Provide Blankslates for empty tabs
HDinger Jan 13, 2025
ff9d6c8
Avoid that the Dialog is jumping around when switching tabs and the c…
HDinger Jan 13, 2025
3874d82
Do not show the banner when there are no relations in manual mode
HDinger Jan 14, 2025
f2263b8
Start on mobile layout for Datepicker
HDinger Jan 14, 2025
40bc444
Extract code into a separate component for better structure
HDinger Jan 15, 2025
bf9f13e
Hide the UnderlineNav on mobile
HDinger Jan 15, 2025
5e13509
Take care that the datepicker scrolls on mobile
HDinger Jan 16, 2025
e8252e8
Add "Today" link for Datepicker fields
HDinger Jan 17, 2025
2bdbb99
Remove unused sass styles
HDinger Jan 17, 2025
2bd4e43
Start to re-add some test selectors and adapt tests to new structure
HDinger Jan 17, 2025
e97e624
Always allow editing the duration
HDinger Jan 17, 2025
37979f8
Set the test_selector correctly
HDinger Jan 17, 2025
77c5010
Define the currently active field correctly and respect that when sel…
HDinger Jan 20, 2025
0812676
Get tests running by waiting for the controller to be loaded before s…
HDinger Jan 20, 2025
c56efa0
Disable duration and NonWorkingDays as specified
HDinger Jan 20, 2025
036ef63
Adapt some more test selectors
HDinger Jan 20, 2025
783e8b8
Do not display 'Today' link for duration
cbliard Jan 20, 2025
11096f0
Fix rubocop complains
cbliard Jan 20, 2025
3294568
Ensure reinitialization of datepicker on change events is done only once
cbliard Jan 21, 2025
c83bc00
Fix some more tests
HDinger Jan 21, 2025
e5755a7
Fix progress popover features tests
cbliard Jan 21, 2025
300d1a2
Fix date conversion when date is not set
cbliard Jan 21, 2025
d46b12a
Take care that creation of Milestones is working correctly
HDinger Jan 21, 2025
aae2185
Disable a test until we have a 'clear' button on duration field
cbliard Jan 21, 2025
fcc2e9a
Show date form when the WorkPackage is in automatic scheduling mode a…
HDinger Jan 21, 2025
c1240ee
Improve datepicker behavior when choosing dates
cbliard Jan 21, 2025
88593fd
Add trailing unit for duration field
HDinger Jan 22, 2025
6671707
Add special logic for edge case in which only one date is given and t…
HDinger Jan 22, 2025
33a0cbb
Switch the focused date field if one of the values is empty
HDinger Jan 22, 2025
5a642be
Only change the focus when the triggered field was the combinedField.…
HDinger Jan 23, 2025
e33fccc
Move test to cuprite in order to wait for the idle network
HDinger Jan 23, 2025
4623aa0
Re-add selenium test
HDinger Jan 24, 2025
3624247
Fix some more test selectors
HDinger Jan 24, 2025
5ea2575
Fix some more test selectors
HDinger Jan 24, 2025
b14a44d
Correct test expectations for Banner messages in the datepicker
HDinger Jan 24, 2025
1bdede6
Preload DatePicker PreviewController to make tests pass
cbliard Jan 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/components/_index.sass
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
@import "work_packages/activities_tab/journals/item_component/details"
@import "work_packages/activities_tab/journals/item_component/add_reactions"
@import "work_packages/activities_tab/journals/item_component/reactions"
@import "work_packages/date_picker/banner_component"
@import "work_packages/date_picker/dialog_content_component"
@import "shares/modal_body_component"
@import "work_packages/reminder/modal_body_component"
@import "shares/invite_user_form_component"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ class WorkPackageRelationsTab::RelationComponent < ApplicationComponent
include ApplicationHelper
include OpPrimer::ComponentHelpers

attr_reader :work_package, :relation, :child
attr_reader :work_package, :relation, :child, :editable

def initialize(work_package:,
relation:,
child: nil)
child: nil,
editable: true)
super()

@work_package = work_package
@relation = relation
@child = child
@editable = editable
end

def related_work_package
Expand All @@ -32,6 +34,8 @@ def should_render_edit_option?
end

def should_render_action_menu?
return false unless editable

if parent_child_relationship?
allowed_to_manage_subtasks?
else
Expand Down
15 changes: 15 additions & 0 deletions app/components/work_packages/date_picker/banner_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<%=
render(Primer::Alpha::Banner.new(description:,
classes: "wp-datepicker--banner_desktop rounded-top-2",
**banner_options)) do |banner|
banner.with_action_button(tag: :a, href: link, target: "_blank") { I18n.t("work_packages.datepicker_modal.show_relations") }
title
end
%>
<%=
render(Primer::Alpha::Banner.new(description: mobile_description,
classes: "wp-datepicker--banner_mobile rounded-top-2",
**banner_options)) do
mobile_title
end
%>
159 changes: 159 additions & 0 deletions app/components/work_packages/date_picker/banner_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#-- 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.
#++

# frozen_string_literal: true

module WorkPackages
module DatePicker
class BannerComponent < ApplicationComponent
def initialize(work_package:, manually_scheduled: true)
super

@work_package = work_package
@manually_scheduled = manually_scheduled
end

private

def scheme
@manually_scheduled ? :warning : :default
end

def link
gantt_index_path(
query_props: {
c: %w[id subject type status assignee project startDate dueDate],
tll: '{"left":"startDate","right":"subject","farRight":null}',
tzl: "auto",
t: "id:asc",
tv: true,
hi: true,
f: [
{ "n" => "id", "o" => "=", "v" => all_relational_wp_ids }
]
}.to_json.freeze
)
end

def title
if @manually_scheduled
I18n.t("work_packages.datepicker_modal.banner.title.manually_scheduled")
elsif children.any?
I18n.t("work_packages.datepicker_modal.banner.title.automatic_with_children")
elsif predecessor_relations.any?
I18n.t("work_packages.datepicker_modal.banner.title.automatic_with_predecessor")
end
end

def mobile_title
if @manually_scheduled
I18n.t("work_packages.datepicker_modal.banner.title.manual_mobile")
else
I18n.t("work_packages.datepicker_modal.banner.title.automatic_mobile")
end
end

def description
if @manually_scheduled
if children.any?
return I18n.t("work_packages.datepicker_modal.banner.description.manual_with_children")
elsif predecessor_relations.any?
if overlapping_predecessor?
return I18n.t("work_packages.datepicker_modal.banner.description.manual_overlap_with_predecessors")
elsif predecessor_with_large_gap?
return I18n.t("work_packages.datepicker_modal.banner.description.manual_gap_between_predecessors")
end
end
end

I18n.t("work_packages.datepicker_modal.banner.description.click_on_show_relations_to_open_gantt",
button_name: I18n.t("work_packages.datepicker_modal.show_relations"))
end

def mobile_description
text = if @manually_scheduled
I18n.t("work_packages.datepicker_modal.banner.description.manual_mobile")
else
I18n.t("work_packages.datepicker_modal.banner.description.automatic_mobile")
end

"#{text} #{render(Primer::Beta::Link.new(tag: :a, href: link, target: '_blank')) do
I18n.t('work_packages.datepicker_modal.show_relations')
end}".html_safe
end

def overlapping_predecessor?
predecessor_work_packages.any? { |wp| wp.due_date.after?(@work_package.start_date) }
end

def predecessor_with_large_gap?
sorted = predecessor_work_packages.sort_by(&:due_date)
sorted.last.due_date.before?(@work_package.start_date - 2)
end

def predecessor_relations
@predecessor_relations ||= @work_package.follows_relations
end

def predecessor_work_packages
@predecessor_work_packages ||= predecessor_relations
.includes(:to)
.map(&:to)
end

def children
@children ||= @work_package.children
end

def all_relational_wp_ids
@work_package
.relations
.pluck(:from_id, :to_id)
.flatten
.uniq
end

def test_selector
if scheme == :warning
"op-modal-banner-warning"
else
"op-modal-banner-info"
end
end

def banner_options
{
scheme:,
full: true,
icon: :info,
test_selector:
}
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.wp-datepicker--banner

@media screen and (min-width: $breakpoint-sm)
&_mobile
display: none

@media screen and (max-width: $breakpoint-sm)
&_desktop
display: none
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<%=
content_tag("turbo-frame", id: "wp-datepicker-dialog--content") do
component_wrapper(data: { "application-target": "dynamic",
controller: "work-packages--date-picker--preview",
test_selector: "op-datepicker-modal" },
class: "wp-datepicker-dialog--content") do
component_collection do |collection|
if show_banner?
collection.with_component(WorkPackages::DatePicker::BannerComponent.new(work_package:, manually_scheduled: schedule_manually))
end

collection.with_component(Primer::Alpha::Dialog::Body.new(classes: "wp-datepicker-dialog--body")) do
render(Primer::Alpha::UnderlinePanels.new(label: I18n.t("work_packages.datepicker_modal.tabs.aria_label"),
classes: "wp-datepicker-dialog--UnderlineNav")) do |component|
component.with_tab(selected: true, id: "wp-datepicker-dialog--content-tab--dates") do |tab|
tab.with_text { I18n.t("work_packages.datepicker_modal.tabs.dates") }
tab.with_panel do
render(WorkPackages::DatePicker::FormContentComponent.new(form_id: DIALOG_FORM_ID,
show_date_form: content_editable?,
work_package:,
schedule_manually:,
focused_field:,
touched_field_map:))
end
end

additional_tabs.each do |tab|
component.with_tab(id: "wp-datepicker-dialog--content-tab--#{tab[:key]}") do |tab_content|
tab_content.with_text { I18n.t("work_packages.datepicker_modal.tabs.#{tab[:key]}") }
tab_content.with_counter(count: tab[:relations].count)
tab_content.with_panel do
if tab[:relations].any?
render(border_box_container(padding: :condensed)) do |box|
tab[:relations].visible.each do |relation|
box.with_row(scheme: :default) do
render(WorkPackageRelationsTab::RelationComponent.new(work_package:,
relation: (relation unless tab[:is_child_relation?]),
child: (relation if tab[:is_child_relation?]),
editable: false))
end
end
end
else
render(Primer::Beta::Blankslate.new(border: true)) do |component|
component.with_visual_icon(icon: :book, size: :medium)
component.with_heading(tag: :h2) { I18n.t("work_packages.datepicker_modal.tabs.blankslate.#{tab[:key]}.title") }
component.with_description { I18n.t("work_packages.datepicker_modal.tabs.blankslate.#{tab[:key]}.description") }
end
end
end
end
end
end
end

collection.with_component(Primer::Alpha::Dialog::Footer.new) do
component_collection do |footer|
footer.with_component(Primer::ButtonComponent.new(data: { action: "work-packages--date-picker--preview#cancel" },
test_selector: "op-datepicker-modal--action")) do
I18n.t("button_cancel")
end

footer.with_component(Primer::ButtonComponent.new(scheme: :primary,
type: :submit,
form: DIALOG_FORM_ID,
test_selector: "op-datepicker-modal--action",
disabled: !content_editable?)) do
I18n.t("button_save")
end
end
end
end
end
end
%>
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#-- 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.
#++

# frozen_string_literal: true

module WorkPackages
module DatePicker
class DialogContentComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable

DIALOG_FORM_ID = "datepicker-form"

attr_accessor :work_package, :schedule_manually, :focused_field, :touched_field_map

def initialize(work_package:, schedule_manually: true, focused_field: :start_date, touched_field_map: {})
super

@work_package = work_package
@schedule_manually = ActiveModel::Type::Boolean.new.cast(schedule_manually)
@focused_field = focused_field
@touched_field_map = touched_field_map
end

private

def precedes_relations
@precedes_relations ||= work_package.precedes_relations
end

def follows_relations
@follows_relations ||= work_package.follows_relations
end

def children
@children ||= work_package.children
end

def additional_tabs
[
{
key: "predecessors",
relations: follows_relations
},
{
key: "successors",
relations: precedes_relations
},
{
key: "children",
relations: children,
is_child_relation?: true
}
]
end

def content_editable?
@schedule_manually || follows_relations.any? || children.any?
end

def show_banner?
@schedule_manually || has_relations?
end

def has_relations?
precedes_relations.any? || follows_relations.any? || children.any?
end
end
end
end
Loading
Loading