From d62fff035b0b9e54eacac94dbf30b27add855d0c Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 25 Nov 2024 10:20:33 +0100 Subject: [PATCH 01/16] [#59037] Primerize "Log time" dialog https://community.openproject.org/work_packages/59037 From d70cdc295c8d845e7115e663e29e208fd0aa7bf2 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 25 Nov 2024 10:25:39 +0100 Subject: [PATCH 02/16] cleanup: remove duplicated branch in CostlogController --- modules/costs/app/controllers/costlog_controller.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/costs/app/controllers/costlog_controller.rb b/modules/costs/app/controllers/costlog_controller.rb index 386ff2436898..c304d15259e1 100644 --- a/modules/costs/app/controllers/costlog_controller.rb +++ b/modules/costs/app/controllers/costlog_controller.rb @@ -102,9 +102,6 @@ def find_project elsif params[:work_package_id] @work_package = WorkPackage.find(params[:work_package_id]) @project = @work_package.project - elsif params[:work_package_id] - @work_package = WorkPackage.find(params[:work_package_id]) - @project = @work_package.project elsif params[:project_id] @project = Project.find(params[:project_id]) else From acc55897de735bcb2fea792597bb9f0f89abcc36 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 26 Nov 2024 18:24:45 +0100 Subject: [PATCH 03/16] add skeleton for TimeEntriesController --- .../projects/time_entries_controller.rb | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 modules/costs/app/controllers/projects/time_entries_controller.rb diff --git a/modules/costs/app/controllers/projects/time_entries_controller.rb b/modules/costs/app/controllers/projects/time_entries_controller.rb new file mode 100644 index 000000000000..0f6a6ab3a344 --- /dev/null +++ b/modules/costs/app/controllers/projects/time_entries_controller.rb @@ -0,0 +1,39 @@ +#-- 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 Projects + class TimeEntriesController < ApplicationController + before_action :require_login + + def dialog; end + + def create; end + + def update; end + end +end From 8ad7a3c6c8bf19c654d127a96d2c77662aff21b8 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 27 Nov 2024 10:11:39 +0100 Subject: [PATCH 04/16] Add routes for time entry tracking --- .../costs/app/controllers/projects/time_entries_controller.rb | 1 + modules/costs/config/routes.rb | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/modules/costs/app/controllers/projects/time_entries_controller.rb b/modules/costs/app/controllers/projects/time_entries_controller.rb index 0f6a6ab3a344..dc696494dd09 100644 --- a/modules/costs/app/controllers/projects/time_entries_controller.rb +++ b/modules/costs/app/controllers/projects/time_entries_controller.rb @@ -29,6 +29,7 @@ module Projects class TimeEntriesController < ApplicationController before_action :require_login + before_action :load_and_authorize_in_optional_project def dialog; end diff --git a/modules/costs/config/routes.rb b/modules/costs/config/routes.rb index 1619251f6eab..ad24189bd33b 100644 --- a/modules/costs/config/routes.rb +++ b/modules/costs/config/routes.rb @@ -33,6 +33,10 @@ resources :hourly_rates, only: %i[show edit update] do post :set_rate, on: :member end + + resources :time_entries, only: %i[create update] do + get :dialog, on: :collection + end end scope "my" do From e012483a78778a3cc1b66820ecb30825fa710706 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 27 Nov 2024 12:59:43 +0100 Subject: [PATCH 05/16] Add a stub for the dialog --- .../entry_dialog_component.html.erb | 6 +++ .../time_entries/entry_dialog_component.rb | 47 +++++++++++++++++++ .../projects/time_entries_controller.rb | 16 ++++++- .../time_entries/dialog.turbo_stream.erb | 3 ++ modules/costs/config/routes.rb | 8 ++-- 5 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 modules/costs/app/components/time_entries/entry_dialog_component.html.erb create mode 100644 modules/costs/app/components/time_entries/entry_dialog_component.rb create mode 100644 modules/costs/app/views/projects/time_entries/dialog.turbo_stream.erb diff --git a/modules/costs/app/components/time_entries/entry_dialog_component.html.erb b/modules/costs/app/components/time_entries/entry_dialog_component.html.erb new file mode 100644 index 000000000000..81c48c761360 --- /dev/null +++ b/modules/costs/app/components/time_entries/entry_dialog_component.html.erb @@ -0,0 +1,6 @@ +<%= render(Primer::Alpha::Dialog.new(title: 'Log Time', size: :large, id: MODAL_ID)) do |d| %> + <% d.with_header(variant: :large, mb: 3) %> + <%= d.with_body do %> +

Hello, World!

+ <% end %> +<% end %> diff --git a/modules/costs/app/components/time_entries/entry_dialog_component.rb b/modules/costs/app/components/time_entries/entry_dialog_component.rb new file mode 100644 index 000000000000..d35afcd7c98f --- /dev/null +++ b/modules/costs/app/components/time_entries/entry_dialog_component.rb @@ -0,0 +1,47 @@ +# 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 TimeEntries + class EntryDialogComponent < ApplicationComponent + include OpTurbo::Streamable + + MODAL_ID = "time-entry-dialog" + + def initialize(time_entry:, open: false) + super() + @time_entry = time_entry + @open = open + end + + private + + attr_reader :time_entry, :open + end +end diff --git a/modules/costs/app/controllers/projects/time_entries_controller.rb b/modules/costs/app/controllers/projects/time_entries_controller.rb index dc696494dd09..8eff458e2e25 100644 --- a/modules/costs/app/controllers/projects/time_entries_controller.rb +++ b/modules/costs/app/controllers/projects/time_entries_controller.rb @@ -28,10 +28,22 @@ module Projects class TimeEntriesController < ApplicationController + include OpTurbo::ComponentStream + include OpTurbo::DialogStreamHelper + before_action :require_login - before_action :load_and_authorize_in_optional_project + before_action :find_project_by_project_id + + authorization_checked! :dialog, :create, :update - def dialog; end + def dialog + @time_entry = if params[:time_entry_id] + # TODO: Properly handle authorization + TimeEntry.find_by(id: params[:time_entry_id]) + else + TimeEntry.new(project: @project) + end + end def create; end diff --git a/modules/costs/app/views/projects/time_entries/dialog.turbo_stream.erb b/modules/costs/app/views/projects/time_entries/dialog.turbo_stream.erb new file mode 100644 index 000000000000..d3773dd67c93 --- /dev/null +++ b/modules/costs/app/views/projects/time_entries/dialog.turbo_stream.erb @@ -0,0 +1,3 @@ +<%= turbo_stream.dialog do + render(TimeEntries::EntryDialogComponent.new(time_entry: @entry)) +end %> diff --git a/modules/costs/config/routes.rb b/modules/costs/config/routes.rb index ad24189bd33b..07ff015f75e1 100644 --- a/modules/costs/config/routes.rb +++ b/modules/costs/config/routes.rb @@ -33,10 +33,6 @@ resources :hourly_rates, only: %i[show edit update] do post :set_rate, on: :member end - - resources :time_entries, only: %i[create update] do - get :dialog, on: :collection - end end scope "my" do @@ -47,6 +43,10 @@ namespace "settings" do resource :time_entry_activities, only: %i[show update] end + + resources :time_entries, only: %i[create update] do + get :dialog, on: :collection + end end scope "work_packages/:work_package_id", as: "work_packages" do From 2e87cd9f6616dc2ef764506521a556df062f40b6 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 27 Nov 2024 12:59:56 +0100 Subject: [PATCH 06/16] Add a temporary link to log time to the project overview --- modules/overviews/app/views/overviews/overviews/show.html.erb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/overviews/app/views/overviews/overviews/show.html.erb b/modules/overviews/app/views/overviews/overviews/show.html.erb index bd1d6114ccb7..04a5c20cfbcc 100644 --- a/modules/overviews/app/views/overviews/overviews/show.html.erb +++ b/modules/overviews/app/views/overviews/overviews/show.html.erb @@ -60,4 +60,7 @@ end %> +<%# TEMPORARY #%> +<%= link_to "log time", dialog_project_time_entries_path(@project), data: { controller: "async-dialog", test_selector: "toggle-log-time-dialog-button" } %> + From ec1491f32f5487596c418d7823300ed0da3152a7 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 28 Nov 2024 15:57:10 +0100 Subject: [PATCH 07/16] temp: also add a link to edit a time entry --- modules/overviews/app/views/overviews/overviews/show.html.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/overviews/app/views/overviews/overviews/show.html.erb b/modules/overviews/app/views/overviews/overviews/show.html.erb index 04a5c20cfbcc..14a7e02f0adb 100644 --- a/modules/overviews/app/views/overviews/overviews/show.html.erb +++ b/modules/overviews/app/views/overviews/overviews/show.html.erb @@ -62,5 +62,6 @@ <%# TEMPORARY #%> <%= link_to "log time", dialog_project_time_entries_path(@project), data: { controller: "async-dialog", test_selector: "toggle-log-time-dialog-button" } %> +<%= link_to "edit log time", dialog_project_time_entries_path(@project, time_entry_id: TimeEntry.first.id), data: { controller: "async-dialog", test_selector: "toggle-log-time-dialog-button" } %> From 2802c9e9c9669ab4424f5535e3478ec1a73599fc Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 28 Nov 2024 15:57:33 +0100 Subject: [PATCH 08/16] Add body to the dialog and build a form --- .../entry_dialog_component.html.erb | 5 +- .../time_entries/time_entry_form.rb | 4 + .../time_entry_form_component.html.erb | 71 ++++++++++++++++ .../time_entries/time_entry_form_component.rb | 81 +++++++++++++++++++ .../time_entries/dialog.turbo_stream.erb | 2 +- 5 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 modules/costs/app/components/time_entries/time_entry_form.rb create mode 100644 modules/costs/app/components/time_entries/time_entry_form_component.html.erb create mode 100644 modules/costs/app/components/time_entries/time_entry_form_component.rb diff --git a/modules/costs/app/components/time_entries/entry_dialog_component.html.erb b/modules/costs/app/components/time_entries/entry_dialog_component.html.erb index 81c48c761360..aa1e7808f0a8 100644 --- a/modules/costs/app/components/time_entries/entry_dialog_component.html.erb +++ b/modules/costs/app/components/time_entries/entry_dialog_component.html.erb @@ -1,6 +1,5 @@ <%= render(Primer::Alpha::Dialog.new(title: 'Log Time', size: :large, id: MODAL_ID)) do |d| %> <% d.with_header(variant: :large, mb: 3) %> - <%= d.with_body do %> -

Hello, World!

- <% end %> + <%- pp(time_entry) %> + <%= render(TimeEntries::TimeEntryFormComponent.new(time_entry: time_entry)) %> <% end %> diff --git a/modules/costs/app/components/time_entries/time_entry_form.rb b/modules/costs/app/components/time_entries/time_entry_form.rb new file mode 100644 index 000000000000..695f0d27ab0c --- /dev/null +++ b/modules/costs/app/components/time_entries/time_entry_form.rb @@ -0,0 +1,4 @@ +module TimeEntries + class TimeEntryForm < ApplicationForm + end +end diff --git a/modules/costs/app/components/time_entries/time_entry_form_component.html.erb b/modules/costs/app/components/time_entries/time_entry_form_component.html.erb new file mode 100644 index 000000000000..e81e59cee2c3 --- /dev/null +++ b/modules/costs/app/components/time_entries/time_entry_form_component.html.erb @@ -0,0 +1,71 @@ +<%#-- 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. + +++#%> + +<%= + component_wrapper do + primer_form_with(**form_options) do |form| + component_collection do |collection| + collection.with_component(Primer::Alpha::Dialog::Body.new( + aria: { label: I18n.t("my.access_token.new_access_token_dialog_title") } + )) do + flex_layout(my: 3) do |body| + body.with_row do + render(Primer::Alpha::Banner.new(scheme: :warning)) do + I18n.t("my.access_token.new_access_token_dialog_attention_text") + end + end + + body.with_row(mt: 3) do + render(Primer::Beta::Text.new(tag: :p)) do + I18n.t("my.access_token.new_access_token_dialog_text") + end + end + + body.with_row do + content_tag(:pre, time_entry.pretty_inspect) + # render(My::AccessToken::NewAccessTokenForm.new(form)) + end + end + end + + collection.with_component(Primer::Alpha::Dialog::Footer.new) do + component_collection do |footer| + footer.with_component(Primer::ButtonComponent.new(data: { 'close-dialog-id': "time-entry-dialog" })) do + I18n.t("button_cancel") + end + + footer.with_component(Primer::ButtonComponent.new(scheme: :primary, type: :submit, test_selector: "create-api-token-button")) do + I18n.t("my.access_token.new_access_token_dialog_submit_button_text") + end + end + end + end + end + end +%> diff --git a/modules/costs/app/components/time_entries/time_entry_form_component.rb b/modules/costs/app/components/time_entries/time_entry_form_component.rb new file mode 100644 index 000000000000..365c3c669e54 --- /dev/null +++ b/modules/costs/app/components/time_entries/time_entry_form_component.rb @@ -0,0 +1,81 @@ +# 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 TimeEntries + class TimeEntryFormComponent < ApplicationComponent + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers + + def initialize(time_entry:) + super() + @time_entry = time_entry + end + + private + + attr_reader :time_entry + + delegate :project, :work_package, to: :time_entry + + def show_work_package_field? + work_package.blank? + end + + def show_user_field? + # Only allow setting a different user, when the user has the + # permission to log time for others in the project + User.current.allowed_in_project?(:log_time, project) + end + + def show_start_and_end_time_fields? + TimeEntry.can_track_start_and_end_time? + end + + def form_options + base = { + model: time_entry, + data: { turbo: true } + } + + if time_entry.persisted? + base.merge({ + url: project_time_entry_path(project, time_entry), + method: :post + }) + else + + base.merge({ + url: project_time_entries_path(project), + method: :post + }) + end + end + end +end diff --git a/modules/costs/app/views/projects/time_entries/dialog.turbo_stream.erb b/modules/costs/app/views/projects/time_entries/dialog.turbo_stream.erb index d3773dd67c93..bf0c45699789 100644 --- a/modules/costs/app/views/projects/time_entries/dialog.turbo_stream.erb +++ b/modules/costs/app/views/projects/time_entries/dialog.turbo_stream.erb @@ -1,3 +1,3 @@ <%= turbo_stream.dialog do - render(TimeEntries::EntryDialogComponent.new(time_entry: @entry)) + render(TimeEntries::EntryDialogComponent.new(time_entry: @time_entry)) end %> From 09800aa3f7aad915709d680c55ca9f0a92e227a5 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 29 Nov 2024 13:17:07 +0100 Subject: [PATCH 09/16] extract logic to render custom fields into a concern --- .../custom_fields/custom_field_rendering.rb | 82 +++++++++++++++++++ app/forms/projects/custom_fields/form.rb | 65 ++------------- 2 files changed, 90 insertions(+), 57 deletions(-) create mode 100644 app/forms/custom_fields/custom_field_rendering.rb diff --git a/app/forms/custom_fields/custom_field_rendering.rb b/app/forms/custom_fields/custom_field_rendering.rb new file mode 100644 index 000000000000..ea3294810a82 --- /dev/null +++ b/app/forms/custom_fields/custom_field_rendering.rb @@ -0,0 +1,82 @@ +module CustomFields::CustomFieldRendering + include ActiveSupport::Concern + + def render_custom_fields(form:) + custom_fields.each do |custom_field| + form.fields_for(:custom_field_values) do |builder| + custom_field_input(builder, custom_field) + end + end + end + + # override if you want to pass more attributes + def additional_custom_field_input_arguments + {} + end + + def custom_fields + raise NotImplementedError, "#custom_fields method needs to be overwritten and provide all custom fields we want to show" + end + + private + + def custom_field_input(builder, custom_field) + if custom_field.multi_value? + multi_value_custom_field_input(builder, custom_field) + else + single_value_custom_field_input(builder, custom_field) + end + end + + def form_arguments(custom_field) + { + custom_field: custom_field, + object: model, + wrapper_id: @wrapper_id + }.merge(additional_custom_field_input_arguments) + end + + # TBD: transform inputs called below to primer form dsl instead of form classes? + # TODOS: + # - initial values for user inputs are not displayed + # - allow/disallow-non-open version setting is not yet respected in the version selector + # - rich text editor is not yet supported + + def single_value_custom_field_input(builder, custom_field) + form_args = form_arguments(custom_field) + + case custom_field.field_format + when "string", "link" + CustomFields::Inputs::String.new(builder, **form_args) + when "text" + CustomFields::Inputs::Text.new(builder, **form_args) + when "int" + CustomFields::Inputs::Int.new(builder, **form_args) + when "float" + CustomFields::Inputs::Float.new(builder, **form_args) + when "list" + CustomFields::Inputs::SingleSelectList.new(builder, **form_args) + when "date" + CustomFields::Inputs::Date.new(builder, **form_args) + when "bool" + CustomFields::Inputs::Bool.new(builder, **form_args) + when "user" + CustomFields::Inputs::SingleUserSelectList.new(builder, **form_args) + when "version" + CustomFields::Inputs::SingleVersionSelectList.new(builder, **form_args) + end + end + + def multi_value_custom_field_input(builder, custom_field) + form_args = form_arguments(custom_field) + + case custom_field.field_format + when "list" + CustomFields::Inputs::MultiSelectList.new(builder, **form_args) + when "user" + CustomFields::Inputs::MultiUserSelectList.new(builder, **form_args) + when "version" + CustomFields::Inputs::MultiVersionSelectList.new(builder, **form_args) + end + end +end diff --git a/app/forms/projects/custom_fields/form.rb b/app/forms/projects/custom_fields/form.rb index 23dd74316571..1dfa26da5851 100644 --- a/app/forms/projects/custom_fields/form.rb +++ b/app/forms/projects/custom_fields/form.rb @@ -27,12 +27,10 @@ #++ module Projects::CustomFields class Form < ApplicationForm + include CustomFields::CustomFieldRendering + form do |custom_fields_form| - custom_fields.each do |custom_field| - custom_fields_form.fields_for(:custom_field_values) do |builder| - custom_field_input(builder, custom_field) - end - end + render_custom_fields(form: custom_fields_form) end def initialize(project:, custom_field_section: nil, custom_field: nil, wrapper_id: nil) @@ -48,6 +46,11 @@ def initialize(project:, custom_field_section: nil, custom_field: nil, wrapper_i end end + # override since we want to add the model with @project + def additional_custom_field_input_arguments + { model: @project } + end + private def custom_fields @@ -62,57 +65,5 @@ def custom_fields @project.available_custom_fields end end - - def custom_field_input(builder, custom_field) - if custom_field.multi_value? - multi_value_custom_field_input(builder, custom_field) - else - single_value_custom_field_input(builder, custom_field) - end - end - - # TBD: transform inputs called below to primer form dsl instead of form classes? - # TODOS: - # - initial values for user inputs are not displayed - # - allow/disallow-non-open version setting is not yet respected in the version selector - # - rich text editor is not yet supported - - def single_value_custom_field_input(builder, custom_field) - form_args = { custom_field:, object: @project, wrapper_id: @wrapper_id } - - case custom_field.field_format - when "string", "link" - CustomFields::Inputs::String.new(builder, **form_args) - when "text" - CustomFields::Inputs::Text.new(builder, **form_args) - when "int" - CustomFields::Inputs::Int.new(builder, **form_args) - when "float" - CustomFields::Inputs::Float.new(builder, **form_args) - when "list" - CustomFields::Inputs::SingleSelectList.new(builder, **form_args) - when "date" - CustomFields::Inputs::Date.new(builder, **form_args) - when "bool" - CustomFields::Inputs::Bool.new(builder, **form_args) - when "user" - CustomFields::Inputs::SingleUserSelectList.new(builder, **form_args) - when "version" - CustomFields::Inputs::SingleVersionSelectList.new(builder, **form_args) - end - end - - def multi_value_custom_field_input(builder, custom_field) - form_args = { custom_field:, object: @project, wrapper_id: @wrapper_id } - - case custom_field.field_format - when "list" - CustomFields::Inputs::MultiSelectList.new(builder, **form_args) - when "user" - CustomFields::Inputs::MultiUserSelectList.new(builder, **form_args) - when "version" - CustomFields::Inputs::MultiVersionSelectList.new(builder, **form_args) - end - end end end From 4242348fb9218152181e616d4691e5bd7d683939 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 29 Nov 2024 13:17:48 +0100 Subject: [PATCH 10/16] implement the form --- .../entry_dialog_component.html.erb | 1 - .../time_entries/time_entry_form.rb | 42 +++++++++++++++++++ .../time_entry_form_component.html.erb | 24 ++--------- .../time_entries/time_entry_form_component.rb | 14 ------- 4 files changed, 46 insertions(+), 35 deletions(-) diff --git a/modules/costs/app/components/time_entries/entry_dialog_component.html.erb b/modules/costs/app/components/time_entries/entry_dialog_component.html.erb index aa1e7808f0a8..fc783e2b8452 100644 --- a/modules/costs/app/components/time_entries/entry_dialog_component.html.erb +++ b/modules/costs/app/components/time_entries/entry_dialog_component.html.erb @@ -1,5 +1,4 @@ <%= render(Primer::Alpha::Dialog.new(title: 'Log Time', size: :large, id: MODAL_ID)) do |d| %> <% d.with_header(variant: :large, mb: 3) %> - <%- pp(time_entry) %> <%= render(TimeEntries::TimeEntryFormComponent.new(time_entry: time_entry)) %> <% end %> diff --git a/modules/costs/app/components/time_entries/time_entry_form.rb b/modules/costs/app/components/time_entries/time_entry_form.rb index 695f0d27ab0c..09d6879cf479 100644 --- a/modules/costs/app/components/time_entries/time_entry_form.rb +++ b/modules/costs/app/components/time_entries/time_entry_form.rb @@ -1,4 +1,46 @@ module TimeEntries class TimeEntryForm < ApplicationForm + include CustomFields::CustomFieldRendering + + form do |f| + if show_user_field? + f.text_field name: :user_id, label: "User" + end + f.text_field name: :spent_on, label: "Date" + f.group(layout: :horizontal) do |g| + g.text_field name: :start_time, label: "Start time" + g.text_field name: :end_time, label: "Finish time" + end + f.text_field name: :hours, label: "Hours" + if show_work_package_field? + f.text_field name: :work_package_id, label: "Work package" + end + f.text_field name: :activity, label: "Activity" + f.text_field name: :comments, label: "Comments" + + render_custom_fields(form: f) + end + + private + + delegate :project, :work_package, to: :model + + def custom_fields + @custom_fields ||= model.available_custom_fields + end + + def show_work_package_field? + work_package.blank? + end + + def show_user_field? + # Only allow setting a different user, when the user has the + # permission to log time for others in the project + User.current.allowed_in_project?(:log_time, project) + end + + def show_start_and_end_time_fields? + TimeEntry.can_track_start_and_end_time? + end end end diff --git a/modules/costs/app/components/time_entries/time_entry_form_component.html.erb b/modules/costs/app/components/time_entries/time_entry_form_component.html.erb index e81e59cee2c3..f09289b50280 100644 --- a/modules/costs/app/components/time_entries/time_entry_form_component.html.erb +++ b/modules/costs/app/components/time_entries/time_entry_form_component.html.erb @@ -31,26 +31,10 @@ See COPYRIGHT and LICENSE files for more details. component_wrapper do primer_form_with(**form_options) do |form| component_collection do |collection| - collection.with_component(Primer::Alpha::Dialog::Body.new( - aria: { label: I18n.t("my.access_token.new_access_token_dialog_title") } - )) do - flex_layout(my: 3) do |body| - body.with_row do - render(Primer::Alpha::Banner.new(scheme: :warning)) do - I18n.t("my.access_token.new_access_token_dialog_attention_text") - end - end - - body.with_row(mt: 3) do - render(Primer::Beta::Text.new(tag: :p)) do - I18n.t("my.access_token.new_access_token_dialog_text") - end - end - - body.with_row do - content_tag(:pre, time_entry.pretty_inspect) - # render(My::AccessToken::NewAccessTokenForm.new(form)) - end + collection.with_component(Primer::Alpha::Dialog::Body.new) do + component_collection do |coll| + # coll.with_component content_tag(:pre, time_entry.pretty_inspect) + coll.with_component render(TimeEntries::TimeEntryForm.new(form)) end end diff --git a/modules/costs/app/components/time_entries/time_entry_form_component.rb b/modules/costs/app/components/time_entries/time_entry_form_component.rb index 365c3c669e54..b41aaf120f8f 100644 --- a/modules/costs/app/components/time_entries/time_entry_form_component.rb +++ b/modules/costs/app/components/time_entries/time_entry_form_component.rb @@ -44,20 +44,6 @@ def initialize(time_entry:) delegate :project, :work_package, to: :time_entry - def show_work_package_field? - work_package.blank? - end - - def show_user_field? - # Only allow setting a different user, when the user has the - # permission to log time for others in the project - User.current.allowed_in_project?(:log_time, project) - end - - def show_start_and_end_time_fields? - TimeEntry.can_track_start_and_end_time? - end - def form_options base = { model: time_entry, From 17a1117cd106144fe695707991863cf6da135b5b Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 29 Nov 2024 13:28:09 +0100 Subject: [PATCH 11/16] also change wrapper id to have it only in the project custom fields form --- app/forms/custom_fields/custom_field_rendering.rb | 3 +-- app/forms/projects/custom_fields/form.rb | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/forms/custom_fields/custom_field_rendering.rb b/app/forms/custom_fields/custom_field_rendering.rb index ea3294810a82..f2218da41de0 100644 --- a/app/forms/custom_fields/custom_field_rendering.rb +++ b/app/forms/custom_fields/custom_field_rendering.rb @@ -31,8 +31,7 @@ def custom_field_input(builder, custom_field) def form_arguments(custom_field) { custom_field: custom_field, - object: model, - wrapper_id: @wrapper_id + object: model }.merge(additional_custom_field_input_arguments) end diff --git a/app/forms/projects/custom_fields/form.rb b/app/forms/projects/custom_fields/form.rb index 1dfa26da5851..c31da673d571 100644 --- a/app/forms/projects/custom_fields/form.rb +++ b/app/forms/projects/custom_fields/form.rb @@ -48,7 +48,7 @@ def initialize(project:, custom_field_section: nil, custom_field: nil, wrapper_i # override since we want to add the model with @project def additional_custom_field_input_arguments - { model: @project } + { model: @project, wrapper_id: @wrapper_id } end private From 89fc7eb34ec0aa75afd1bf3c8eb222c0f064ad60 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 29 Nov 2024 13:35:16 +0100 Subject: [PATCH 12/16] add call to super --- app/forms/custom_fields/inputs/base/input.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/forms/custom_fields/inputs/base/input.rb b/app/forms/custom_fields/inputs/base/input.rb index 6e30709cc0f0..22e3b8a56ac2 100644 --- a/app/forms/custom_fields/inputs/base/input.rb +++ b/app/forms/custom_fields/inputs/base/input.rb @@ -32,6 +32,8 @@ class CustomFields::Inputs::Base::Input < ApplicationForm attr_reader :options def initialize(custom_field:, object:, **options) + super() + @custom_field = custom_field @object = object @options = options From 75017a2ed1410e1c45c3d3703f9506c34052557a Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 29 Nov 2024 14:05:05 +0100 Subject: [PATCH 13/16] add stimulus controller --- .../dynamic/time-entry.controller.ts | 44 +++++++++++++++++++ .../entry_dialog_component.html.erb | 2 +- .../time_entries/time_entry_form.rb | 4 +- 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 frontend/src/stimulus/controllers/dynamic/time-entry.controller.ts diff --git a/frontend/src/stimulus/controllers/dynamic/time-entry.controller.ts b/frontend/src/stimulus/controllers/dynamic/time-entry.controller.ts new file mode 100644 index 000000000000..35a1287e46ec --- /dev/null +++ b/frontend/src/stimulus/controllers/dynamic/time-entry.controller.ts @@ -0,0 +1,44 @@ +/* + * -- 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 { Controller } from '@hotwired/stimulus'; + +export default class TimeEntryController extends Controller { + static targets = ['startTimeInput', 'endTimeInput', 'hoursInput']; + + declare readonly startTimeInputTarget:HTMLInputElement; + declare readonly endTimeInputTarget:HTMLInputElement; + declare readonly choursInputTarget:HTMLInputElement; + + startTimeInputTargetConnected() { + // console.log('We have a start input'); + this.startTimeInputTarget.value = '12:00'; + } +} diff --git a/modules/costs/app/components/time_entries/entry_dialog_component.html.erb b/modules/costs/app/components/time_entries/entry_dialog_component.html.erb index fc783e2b8452..c53510ac2725 100644 --- a/modules/costs/app/components/time_entries/entry_dialog_component.html.erb +++ b/modules/costs/app/components/time_entries/entry_dialog_component.html.erb @@ -1,4 +1,4 @@ -<%= render(Primer::Alpha::Dialog.new(title: 'Log Time', size: :large, id: MODAL_ID)) do |d| %> +<%= render(Primer::Alpha::Dialog.new(title: 'Log Time', size: :large, id: MODAL_ID, data: { "controller" => "time-entry", "application-target" => "dynamic" })) do |d| %> <% d.with_header(variant: :large, mb: 3) %> <%= render(TimeEntries::TimeEntryFormComponent.new(time_entry: time_entry)) %> <% end %> diff --git a/modules/costs/app/components/time_entries/time_entry_form.rb b/modules/costs/app/components/time_entries/time_entry_form.rb index 09d6879cf479..f7291a1e3de8 100644 --- a/modules/costs/app/components/time_entries/time_entry_form.rb +++ b/modules/costs/app/components/time_entries/time_entry_form.rb @@ -8,7 +8,9 @@ class TimeEntryForm < ApplicationForm end f.text_field name: :spent_on, label: "Date" f.group(layout: :horizontal) do |g| - g.text_field name: :start_time, label: "Start time" + g.text_field name: :start_time, + label: "Start time", + data: { "time-entry-target" => "startTimeInput" } g.text_field name: :end_time, label: "Finish time" end f.text_field name: :hours, label: "Hours" From 60f1771ff77394cb11974f4fd3ae64cd797b131a Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 29 Nov 2024 14:58:53 +0100 Subject: [PATCH 14/16] fix generating input field names correctly for names with camelized names --- app/forms/custom_fields/inputs/multi_select_list.rb | 10 ++++++---- .../custom_fields/inputs/multi_user_select_list.rb | 7 ++++--- .../custom_fields/inputs/multi_version_select_list.rb | 10 ++++++---- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/forms/custom_fields/inputs/multi_select_list.rb b/app/forms/custom_fields/inputs/multi_select_list.rb index 832ddb524b49..85434337830a 100644 --- a/app/forms/custom_fields/inputs/multi_select_list.rb +++ b/app/forms/custom_fields/inputs/multi_select_list.rb @@ -31,16 +31,18 @@ class CustomFields::Inputs::MultiSelectList < CustomFields::Inputs::Base::Autoco # autocompleter does not set key with blank value if nothing is selected or input is cleared # in order to let acts_as_customizable handle the clearing of the value, we need to set the value to blank via a hidden field # which sends blank if autocompleter is cleared - custom_value_form.hidden(**input_attributes.merge( + custom_value_form.hidden( + **input_attributes, scope_name_to_model: false, - name: "#{@object.class.name.downcase}[custom_field_values][#{input_attributes[:name]}][]", + name: "#{@object.model_name.element}[custom_field_values][#{input_attributes[:name]}][]", value: - )) + ) custom_value_form.autocompleter(**input_attributes) do |list| @custom_field.custom_options.each do |custom_option| list.option( - label: custom_option.value, value: custom_option.id, + label: custom_option.value, + value: custom_option.id, selected: selected?(custom_option) ) end diff --git a/app/forms/custom_fields/inputs/multi_user_select_list.rb b/app/forms/custom_fields/inputs/multi_user_select_list.rb index f086b8b8948e..dfc7e691dd8f 100644 --- a/app/forms/custom_fields/inputs/multi_user_select_list.rb +++ b/app/forms/custom_fields/inputs/multi_user_select_list.rb @@ -33,11 +33,12 @@ class CustomFields::Inputs::MultiUserSelectList < CustomFields::Inputs::Base::Au # autocompleter does not set key with blank value if nothing is selected or input is cleared # in order to let acts_as_customizable handle the clearing of the value, we need to set the value to blank via a hidden field # which sends blank if autocompleter is cleared - custom_value_form.hidden(**input_attributes.merge( + custom_value_form.hidden( + **input_attributes, scope_name_to_model: false, - name: "#{@object.class.name.downcase}[custom_field_values][#{input_attributes[:name]}][]", + name: "#{@object.model_name.element}[custom_field_values][#{input_attributes[:name]}][]", value: - )) + ) custom_value_form.autocompleter(**input_attributes) end diff --git a/app/forms/custom_fields/inputs/multi_version_select_list.rb b/app/forms/custom_fields/inputs/multi_version_select_list.rb index f03ce7672f6c..2740a117e840 100644 --- a/app/forms/custom_fields/inputs/multi_version_select_list.rb +++ b/app/forms/custom_fields/inputs/multi_version_select_list.rb @@ -35,16 +35,18 @@ class CustomFields::Inputs::MultiVersionSelectList < CustomFields::Inputs::Base: # autocompleter does not set key with blank value if nothing is selected or input is cleared # in order to let acts_as_customizable handle the clearing of the value, we need to set the value to blank via a hidden field # which sends blank if autocompleter is cleared - custom_value_form.hidden(**input_attributes.merge( + custom_value_form.hidden( + **input_attributes, scope_name_to_model: false, - name: "#{@object.class.name.downcase}[custom_field_values][#{input_attributes[:name]}][]", + name: "#{@object.model_name.element}[custom_field_values][#{input_attributes[:name]}][]", value: - )) + ) custom_value_form.autocompleter(**input_attributes) do |list| assignable_custom_field_values(@custom_field).each do |version| list.option( - label: version.name, value: version.id, + label: version.name, + value: version.id, selected: selected?(version) ) end From f207f061b4ae3c0d3f8a478e5c517bbbbbbaf56b Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 29 Nov 2024 15:30:10 +0100 Subject: [PATCH 15/16] correct filter search depending on object and add wrapper for dialog --- .../inputs/base/autocomplete/user_query_utils.rb | 11 +++++++++-- .../app/components/time_entries/time_entry_form.rb | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/forms/custom_fields/inputs/base/autocomplete/user_query_utils.rb b/app/forms/custom_fields/inputs/base/autocomplete/user_query_utils.rb index e2d17241aed6..72f91a4d873a 100644 --- a/app/forms/custom_fields/inputs/base/autocomplete/user_query_utils.rb +++ b/app/forms/custom_fields/inputs/base/autocomplete/user_query_utils.rb @@ -51,10 +51,17 @@ def search_key end def filters - [ + filters = [ { name: "type", operator: "=", values: ["User", "Group", "PlaceholderUser"] }, - { name: "member", operator: "=", values: [@object.id.to_s] }, { name: "status", operator: "!", values: [Principal.statuses["locked"].to_s] } ] + + if @object.is_a?(Project) + filters << { name: "member", operator: "=", values: [@object.id.to_s] } + elsif @object.respond_to?(:project_id) + filters << { name: "member", operator: "=", values: [@object.project_id.to_s] } + end + + filters end end diff --git a/modules/costs/app/components/time_entries/time_entry_form.rb b/modules/costs/app/components/time_entries/time_entry_form.rb index f7291a1e3de8..b1915ef3f94f 100644 --- a/modules/costs/app/components/time_entries/time_entry_form.rb +++ b/modules/costs/app/components/time_entries/time_entry_form.rb @@ -23,6 +23,10 @@ class TimeEntryForm < ApplicationForm render_custom_fields(form: f) end + def additional_custom_field_input_arguments + { wrapper_id: "time-entry-dialog" } + end + private delegate :project, :work_package, to: :model From 6b529b93b955dd47b33c1d1a59efa94a4df2660e Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 29 Nov 2024 16:34:52 +0100 Subject: [PATCH 16/16] do not show temp links in overviews in tests --- .../views/overviews/overviews/show.html.erb | 131 ++++++++++-------- 1 file changed, 77 insertions(+), 54 deletions(-) diff --git a/modules/overviews/app/views/overviews/overviews/show.html.erb b/modules/overviews/app/views/overviews/overviews/show.html.erb index 14a7e02f0adb..54d027b60f25 100644 --- a/modules/overviews/app/views/overviews/overviews/show.html.erb +++ b/modules/overviews/app/views/overviews/overviews/show.html.erb @@ -2,66 +2,89 @@ <% end -%> -<%= - render(Primer::OpenProject::PageHeader.new( +<%= render( + Primer::OpenProject::PageHeader.new( data: { - 'controller': 'overview-header', - 'application-target': 'dynamic', - turbo: true - } - )) do |header| - header.with_title(variant: :medium) { t("overviews.label") } - header.with_breadcrumbs( - [ - { href: project_path(@project), text: @project.name }, - t("overviews.label") - ] - ) - favored = @project.favored_by?(User.current) - header.with_action_icon_button( - icon: favored ? "star-fill" : "star", - mobile_icon: favored ? "star-fill" : "star", - size: :medium, - tag: :a, - href: build_favorite_path(@project, format: :html), - data: { method: favored ? :delete : :post }, - classes: favored ? "op-primer--star-icon" : "", + controller: "overview-header", + "application-target": "dynamic", + turbo: true, + }, + ), +) do |header| + header.with_title(variant: :medium) { t("overviews.label") } + header.with_breadcrumbs( + [ + { href: project_path(@project), text: @project.name }, + t("overviews.label"), + ], + ) + favored = @project.favored_by?(User.current) + header.with_action_icon_button( + icon: favored ? "star-fill" : "star", + mobile_icon: favored ? "star-fill" : "star", + size: :medium, + tag: :a, + href: build_favorite_path(@project, format: :html), + data: { + method: favored ? :delete : :post, + }, + classes: favored ? "op-primer--star-icon" : "", + label: favored ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite), + aria: { label: favored ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite), - aria: { label: favored ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) }, - test_selector: 'project-favorite-button' - ) - - header.with_action_menu(menu_arguments: { anchor_align: :end }, - button_arguments: { icon: "op-kebab-vertical", "aria-label": t(:label_menu) }) do |menu| + }, + test_selector: "project-favorite-button", + ) - if User.current.allowed_in_project?(:select_project_custom_fields, @project) - menu.with_item( - label: t(:label_project_attribute_manage_link), - href: project_settings_project_custom_fields_path(@project), - data: { turbo: false } - ) do |item| - item.with_leading_visual_icon(icon: :pencil) - end - end - - if User.current.allowed_in_project?(:archive_project, @project) - menu.with_item( - label: t(:label_archive_project), - href: project_archive_path(@project, status: '', name: @project.name), - content_arguments: { - data: { method: :post, turbo: false, confirm: t('project.archive.are_you_sure', name: @project.name) }, - } - ) do |item| - item.with_leading_visual_icon(icon: :lock) - end - end + header.with_action_menu( + menu_arguments: { + anchor_align: :end, + }, + button_arguments: { + icon: "op-kebab-vertical", + "aria-label": t(:label_menu), + }, + ) do |menu| + if User.current.allowed_in_project?(:select_project_custom_fields, @project) + menu.with_item( + label: t(:label_project_attribute_manage_link), + href: project_settings_project_custom_fields_path(@project), + data: { + turbo: false, + }, + ) { |item| item.with_leading_visual_icon(icon: :pencil) } + end + if User.current.allowed_in_project?(:archive_project, @project) + menu.with_item( + label: t(:label_archive_project), + href: project_archive_path(@project, status: "", name: @project.name), + content_arguments: { + data: { + method: :post, + turbo: false, + confirm: t("project.archive.are_you_sure", name: @project.name), + }, + }, + ) { |item| item.with_leading_visual_icon(icon: :lock) } end end -%> +end %> -<%# TEMPORARY #%> -<%= link_to "log time", dialog_project_time_entries_path(@project), data: { controller: "async-dialog", test_selector: "toggle-log-time-dialog-button" } %> -<%= link_to "edit log time", dialog_project_time_entries_path(@project, time_entry_id: TimeEntry.first.id), data: { controller: "async-dialog", test_selector: "toggle-log-time-dialog-button" } %> +<%# TEMPORARY # %> +<%- if Rails.env.development? %> + <%= link_to "log time", + dialog_project_time_entries_path(@project), + data: { + controller: "async-dialog", + test_selector: "toggle-log-time-dialog-button", + } %> + <%= link_to "edit log time", + dialog_project_time_entries_path(@project, time_entry_id: TimeEntry.first.id), + data: { + controller: "async-dialog", + test_selector: "toggle-log-time-dialog-button", + } %> +<% end %>