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 21 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
21 commits
Select commit Hold shift + click to select a range
54acf64
Start on using Primer components inside the datepicker modal
HDinger Dec 4, 2024
f27f490
Remove unused angular components which are going to be replaced by Vi…
HDinger Dec 9, 2024
d4a5c81
Use existing fields for now to get the modal working again
HDinger Dec 9, 2024
19a731a
Render the form from rails and only the actual datepicker in angular.
HDinger Dec 10, 2024
9149bd3
Extract logic to update the edit field after the turboFrame was submi…
HDinger Dec 11, 2024
a9cf431
Extract logic of the progress modal to be reused for the datepicker m…
HDinger Dec 12, 2024
a3824b1
Update the dialog when the "ignore_non_working_days" checkbox is checked
HDinger Dec 16, 2024
0b96e2f
Update the dialog content when chaning the scheduling mode
HDinger Dec 16, 2024
4cd3e66
Cancel the editField correctly when canceling the dialog
HDinger Dec 16, 2024
3e8ec27
First try on updating the datepicker when the fields above change
HDinger Dec 16, 2024
d823bfd
Correctly save the sheduling mode and duration
HDinger Dec 17, 2024
82d8461
Preserve values when updating the content via turboStream
HDinger Dec 17, 2024
fe6132a
Disable the form in automatic scheduling mode
HDinger Dec 17, 2024
9fda4e6
Show additional tabs in datepicker dialog
HDinger Dec 17, 2024
3499005
Do not update the fields so often
HDinger Dec 17, 2024
505be7e
Correctly parese the duration
HDinger Dec 17, 2024
6fc0e7f
Revert commit d2a6ba2a04fe038f4f008214997e931091971cea
HDinger Dec 17, 2024
37cf8c3
Improve event handling when datepicker field change so that also non-…
HDinger Dec 17, 2024
1442a18
Forbid editing the relations in the datepicker
HDinger Dec 18, 2024
bc132f9
Take care that something is shown when opening the duration field
HDinger Dec 18, 2024
9488170
Hack some roundtrip between the datpicker and the fields above (wip)
HDinger Dec 18, 2024
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
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
11 changes: 11 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,11 @@
<%=
render(Primer::Alpha::Banner.new(scheme:,
full: true,
icon: :info,
description:,
mb: 3,
classes: "rounded-top-2")) do |banner|
banner.with_action_button(tag: :a, href: link, target: "_blank") { I18n.t("work_packages.datepicker_modal.show_relations") }
title
end
%>
122 changes: 122 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,122 @@
#-- 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 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 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
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<%=
content_tag("turbo-frame", id: "wp-datepicker-dialog--content") do
component_wrapper(data: { "application-target": "dynamic",
controller: "work-packages--date-picker--preview" }) 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: "Overlay-body_autocomplete_height")) do
render(Primer::Alpha::UnderlinePanels.new(label: "Test navigation")) do |component|
component.with_tab(selected: true, id: "todo_1") do |tab|
tab.with_text { "Dates" }
tab.with_panel do
primer_form_with(
model: work_package,
url: work_package_datepicker_dialog_content_path,
method: :put,
id: DIALOG_FORM_ID,
data: { "work-packages--date-picker--preview-target": "form" },
html: { autocomplete: "off" },
) do |f|
flex_layout do |body|
body.with_row(mb: 3) do
flex_layout(align_items: :flex_end, justify_content: :space_between) do |first_row|
first_row.with_column do
concat(render(Primer::Alpha::FormControl.new(label: I18n.t("work_packages.datepicker_modal.mode.title"))) do |component|
component.with_input do
render(Primer::Alpha::SegmentedControl.new("aria-label": I18n.t("work_packages.datepicker_modal.mode.title"))) do |control|
control.with_item(tag: :a,
href: work_package_datepicker_dialog_content_path(params.merge(schedule_manually: true).permit!),
data: { turbo_stream: true },
label: I18n.t("work_packages.datepicker_modal.mode.manual"),
title: I18n.t("work_packages.datepicker_modal.mode.manual"),
selected: schedule_manually)
control.with_item(tag: :a,
href: work_package_datepicker_dialog_content_path(params.merge(schedule_manually: false).permit!),
data: { turbo_stream: true },
label: I18n.t("work_packages.datepicker_modal.mode.automatic"),
title: I18n.t("work_packages.datepicker_modal.mode.automatic"),
selected: !schedule_manually)
end
end
end)
concat(
f.hidden_field(:work_package,
name: "work_package[schedule_manually]",
value: schedule_manually)
)
concat(
f.hidden_field(:work_package,
name: "work_package[schedule_manually_touched]",
value: params[:schedule_manually].present?)
)
concat(
f.hidden_field(:work_package,
name: "work_package[initial][schedule_manually]",
value: work_package.schedule_manually)
)
end

first_row.with_column(mb: 1) do
render(Primer::Alpha::CheckBox.new(name: "work_package[ignore_non_working_days]",
label: I18n.t("work_packages.datepicker_modal.ignore_non_working_days.title"),
checked: !work_package.ignore_non_working_days,
disabled: disabled?,
value: 0,
unchecked_value: 1,
data: { "work-packages--date-picker--preview-target": "fieldInput",
action: "work-packages--date-picker--preview#markFieldAsTouched" }))
end
end
end

body.with_row(mb: 3) do
render(WorkPackages::DatePicker::Form.new(f,
work_package:,
focused_field:,
touched_field_map:,
disabled: disabled?))
end

body.with_row(mb: 3) do
helpers.angular_component_tag "opce-wp-modal-date-picker",
inputs: {
start_date: work_package.start_date,
due_date: work_package.due_date,
ignore_non_working_days: work_package.ignore_non_working_days,
schedule_manually:,
is_schedulable: !disabled?,
field_name: focused_field,
start_date_field_id: "work_package_start_date",
due_date_field_id: "work_package_due_date"
}
end
end
end
end
end

component.with_tab(id: "todo_2") do |tab|
tab.with_text { "Predecessors" }
tab.with_counter(count: follow_relations.count)
tab.with_panel do
render(border_box_container(padding: :condensed)) do |component|
follow_relations.each do |relation|
component.with_row(scheme: :default) do
render(WorkPackageRelationsTab::RelationComponent.new(work_package:,
relation:,
editable: false))
end
end
end
end
end

component.with_tab(id: "todo_3") do |tab|
tab.with_text { "Successors" }
tab.with_counter(count: precedes_relations.count)
tab.with_panel do
render(border_box_container(padding: :condensed)) do |component|
precedes_relations.each do |relation|
component.with_row(scheme: :default) do
render(WorkPackageRelationsTab::RelationComponent.new(work_package:,
relation:,
editable: false))
end
end
end
end
end

component.with_tab(id: "todo_3") do |tab|
tab.with_text { "Children" }
tab.with_counter(count: children.count)
tab.with_panel do
render(border_box_container(padding: :condensed)) do |component|
children.each do |child|
component.with_row(scheme: :default) do
render(WorkPackageRelationsTab::RelationComponent.new(work_package:,
relation: nil,
child:,
editable: false))
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" })) do
I18n.t("button_cancel")
end

footer.with_component(Primer::ButtonComponent.new(scheme: :primary, type: :submit, form: DIALOG_FORM_ID)) do
I18n.t("button_submit")
end
end
end
end
end
end
%>
Loading
Loading