diff --git a/app/components/work_packages/activities_tab/index_component.html.erb b/app/components/work_packages/activities_tab/index_component.html.erb index fcacece9d01..73c5feb176b 100644 --- a/app/components/work_packages/activities_tab/index_component.html.erb +++ b/app/components/work_packages/activities_tab/index_component.html.erb @@ -1,50 +1,33 @@ <%= - if deferred - render( - WorkPackages::ActivitiesTab::Journals::IndexComponent.new(work_package:, filter:, deferred:) - ) - else - component_wrapper(tag: "turbo-frame") do - flex_layout(classes: "work-packages-activities-tab-index-component") do |activities_tab_wrapper_container| - activities_tab_wrapper_container.with_row(classes: "work-packages-activities-tab-index-component--errors") do - render( - WorkPackages::ActivitiesTab::ErrorStreamComponent.new - ) - end + component_wrapper(tag: "turbo-frame") do + flex_layout(classes: "work-packages-activities-tab-index-component") do |activities_tab_wrapper_container| + activities_tab_wrapper_container.with_row(classes: "work-packages-activities-tab-index-component--errors") do + render(error_stream_component) + end - activities_tab_wrapper_container.with_row(id: index_content_wrapper_key, data: wrapper_data_attributes) do - flex_layout do |activities_tab_container| - activities_tab_container.with_row(mb: 2) do - render( - WorkPackages::ActivitiesTab::Journals::FilterAndSortingComponent.new( - work_package:, - filter: - ) - ) + activities_tab_wrapper_container.with_row(id: index_content_wrapper_key, data: wrapper_data_attributes) do + flex_layout do |activities_tab_container| + activities_tab_container.with_row(mb: 2) do + render(filter_and_sorting_component) + end + + activities_tab_container.with_row(flex_layout: true, classes: "work-packages-activities-tab-index-component--content-container", mt: 3) do |journals_wrapper_container| + journals_wrapper_container.with_row( + classes: "work-packages-activities-tab-index-component--journals-container work-packages-activities-tab-index-component--journals-container_with-initial-input-compensation", + data: { "work-packages--activities-tab--index-target": "journalsContainer" } + ) do + render(list_journals_component) end - activities_tab_container.with_row(flex_layout: true, classes: "work-packages-activities-tab-index-component--content-container", mt: 3) do |journals_wrapper_container| + if adding_comment_allowed? journals_wrapper_container.with_row( - classes: "work-packages-activities-tab-index-component--journals-container work-packages-activities-tab-index-component--journals-container_with-initial-input-compensation", - data: { "work-packages--activities-tab--index-target": "journalsContainer" } + id: add_comment_wrapper_key, + classes: "work-packages-activities-tab-index-component--input-container work-packages-activities-tab-index-component--input-container_sort-#{journal_sorting}", + p: 3, + bg: :subtle, + data: add_comment_wrapper_data_attributes ) do - render( - WorkPackages::ActivitiesTab::Journals::IndexComponent.new(work_package:, filter:) - ) - end - - if adding_comment_allowed? - journals_wrapper_container.with_row( - id: add_comment_wrapper_key, - classes: "work-packages-activities-tab-index-component--input-container work-packages-activities-tab-index-component--input-container_sort-#{journal_sorting}", - p: 3, - bg: :subtle, - data: add_comment_wrapper_data_attributes - ) do - render( - WorkPackages::ActivitiesTab::Journals::NewComponent.new(work_package:, filter:, last_server_timestamp:) - ) - end + render(add_journal_component) end end end diff --git a/app/components/work_packages/activities_tab/index_component.rb b/app/components/work_packages/activities_tab/index_component.rb index c1bd41061ef..574870697f5 100644 --- a/app/components/work_packages/activities_tab/index_component.rb +++ b/app/components/work_packages/activities_tab/index_component.rb @@ -37,13 +37,13 @@ module WorkPackages include WorkPackages::ActivitiesTab::SharedHelpers include WorkPackages::ActivitiesTab::StimulusControllers - def initialize(work_package:, last_server_timestamp:, filter: :all, deferred: false) + def initialize(work_package:, journals:, last_server_timestamp:, filter: :all) super @work_package = work_package + @journals = journals @filter = filter @last_server_timestamp = last_server_timestamp - @deferred = deferred end def self.wrapper_key = "work-package-activities-tab-content" @@ -51,9 +51,25 @@ module WorkPackages def self.add_comment_wrapper_key = "work-packages-activities-tab-add-comment-component" delegate :index_content_wrapper_key, :add_comment_wrapper_key, to: :class + def filter_and_sorting_component + WorkPackages::ActivitiesTab::Journals::FilterAndSortingComponent.new(work_package:, filter:) + end + + def list_journals_component + WorkPackages::ActivitiesTab::Journals::IndexComponent.new(work_package:, journals:, filter:) + end + + def add_journal_component + WorkPackages::ActivitiesTab::Journals::NewComponent.new(work_package:, filter:, last_server_timestamp:) + end + + def error_stream_component + WorkPackages::ActivitiesTab::ErrorStreamComponent.new + end + private - attr_reader :work_package, :filter, :last_server_timestamp, :deferred + attr_reader :work_package, :journals, :filter, :last_server_timestamp def wrapper_data_attributes # rubocop:disable Metrics/AbcSize stimulus_controllers = { diff --git a/app/components/work_packages/activities_tab/journals/index_component.html.erb b/app/components/work_packages/activities_tab/journals/index_component.html.erb index 662bde4bd7a..1894da30916 100644 --- a/app/components/work_packages/activities_tab/journals/index_component.html.erb +++ b/app/components/work_packages/activities_tab/journals/index_component.html.erb @@ -1,75 +1,41 @@ <%= - if deferred - helpers.turbo_frame_tag("work-package-activities-tab-content-older-journals") do - flex_layout do |older_journals_container| - older_journals.each do |record| - older_journals_container.with_row do - if record.is_a?(Changeset) - render(WorkPackages::ActivitiesTab::Journals::RevisionComponent.new(changeset: record, filter:)) - else - render( - WorkPackages::ActivitiesTab::Journals::ItemComponent.new( - journal: record, filter:, - grouped_emoji_reactions: wp_journals_grouped_emoji_reactions[record.id] - ) - ) + component_wrapper(class: "work-packages-activities-tab-journals-index-component") do + flex_layout(data: { test_selector: "op-wp-journals-#{filter}-#{journal_sorting}" }) do |journals_index_wrapper_container| + journals_index_wrapper_container.with_row( + classes: "work-packages-activities-tab-journals-index-component--journals-inner-container", + mb: inner_container_margin_bottom + ) do + flex_layout( + id: insert_target_modifier_id, + data: { test_selector: "op-wp-journals-container" } + ) do |journals_index_container| + if empty_state? + journals_index_container.with_row(mt: 2, mb: 3) do + render(WorkPackages::ActivitiesTab::Journals::EmptyComponent.new) + end + end + + if journal_sorting_asc? + journals_index_container.with_row do + render(infinite_scroll_component) + end + end + + journals_index_container.with_row do + render(page_component) + end + + if journal_sorting_desc? + journals_index_container.with_row do + render(infinite_scroll_component) end end end end - end - else - component_wrapper(class: "work-packages-activities-tab-journals-index-component") do - flex_layout(data: { test_selector: "op-wp-journals-#{filter}-#{journal_sorting}" }) do |journals_index_wrapper_container| - journals_index_wrapper_container.with_row( - classes: "work-packages-activities-tab-journals-index-component--journals-inner-container", - mb: inner_container_margin_bottom - ) do - flex_layout( - id: insert_target_modifier_id, - data: { test_selector: "op-wp-journals-container" } - ) do |journals_index_container| - if empty_state? - journals_index_container.with_row(mt: 2, mb: 3) do - render( - WorkPackages::ActivitiesTab::Journals::EmptyComponent.new - ) - end - end - if !journal_sorting_desc? && journals.count > MAX_RECENT_JOURNALS - journals_index_container.with_row do - helpers.turbo_frame_tag("work-package-activities-tab-content-older-journals", src: work_package_activities_path(work_package, filter:, deferred: true)) - end - end - - recent_journals.each do |record| - journals_index_container.with_row do - if record.is_a?(Changeset) - render(WorkPackages::ActivitiesTab::Journals::RevisionComponent.new(changeset: record, filter:)) - else - render( - WorkPackages::ActivitiesTab::Journals::ItemComponent.new( - journal: record, filter:, - grouped_emoji_reactions: wp_journals_grouped_emoji_reactions[record.id] - ) - ) - end - end - end - - if journal_sorting_desc? && journals.count > MAX_RECENT_JOURNALS - journals_index_container.with_row do - helpers.turbo_frame_tag("work-package-activities-tab-content-older-journals", src: work_package_activities_path(work_package, filter:, deferred: true)) - end - end - end - end - - unless empty_state? - journals_index_wrapper_container - .with_row(classes: "work-packages-activities-tab-journals-index-component--stem-connection") - end + unless empty_state? + journals_index_wrapper_container + .with_row(classes: "work-packages-activities-tab-journals-index-component--stem-connection") end end end diff --git a/app/components/work_packages/activities_tab/journals/index_component.rb b/app/components/work_packages/activities_tab/journals/index_component.rb index 04fc8edb5d8..af4d63d873a 100644 --- a/app/components/work_packages/activities_tab/journals/index_component.rb +++ b/app/components/work_packages/activities_tab/journals/index_component.rb @@ -32,99 +32,46 @@ module WorkPackages module ActivitiesTab module Journals class IndexComponent < ApplicationComponent - MAX_RECENT_JOURNALS = 30 - include ApplicationHelper include OpPrimer::ComponentHelpers include OpTurbo::Streamable include WorkPackages::ActivitiesTab::SharedHelpers - def initialize(work_package:, filter: :all, deferred: false) + def initialize(work_package:, journals:, filter: :all) super @work_package = work_package + @journals = journals @filter = filter - @deferred = deferred end + def infinite_scroll_component + WorkPackages::ActivitiesTab::Journals::InfiniteScrollComponent.new(work_package:) + end + + def page_component + WorkPackages::ActivitiesTab::Journals::PageComponent.new(journals:, emoji_reactions:, page: 1, filter:) + end + + def self.insert_target_modifier_id = "work-package-journal-days" + delegate :insert_target_modifier_id, to: :class + private - attr_reader :work_package, :filter, :deferred + attr_reader :work_package, :journals, :filter def insert_target_modified? true end - def insert_target_modifier_id - "work-package-journal-days" - end - - def journal_sorting_desc? - journal_sorting == "desc" - end - - def base_journals - combine_and_sort_records(fetch_journals, fetch_revisions) - end - - def fetch_journals - API::V3::Activities::ActivityEagerLoadingWrapper.wrap( - work_package - .journals - .internal_visible - .includes(:user, :customizable_journals, :attachable_journals, :storable_journals, :notifications) - .reorder(version: journal_sorting) - .with_sequence_version - ) - end - - def fetch_revisions - work_package.changesets.includes(:user, :repository) - end - - def combine_and_sort_records(journals, revisions) - (journals + revisions).sort_by do |record| - timestamp = record_timestamp(record) - journal_sorting_desc? ? [-timestamp, -record.id] : [timestamp, record.id] - end - end - - def record_timestamp(record) - if record.is_a?(API::V3::Activities::ActivityEagerLoadingWrapper) - record.created_at&.to_i - elsif record.is_a?(Changeset) - record.committed_on.to_i - end - end - - def journals - base_journals - end - - def recent_journals - if journal_sorting_desc? - base_journals.first(MAX_RECENT_JOURNALS) - else - base_journals.last(MAX_RECENT_JOURNALS) - end - end - - def older_journals - if journal_sorting_desc? - base_journals.drop(MAX_RECENT_JOURNALS) - else - base_journals.take(base_journals.size - MAX_RECENT_JOURNALS) - end - end - def journal_with_notes work_package .journals .where.not(notes: "") end - def wp_journals_grouped_emoji_reactions - @wp_journals_grouped_emoji_reactions ||= + def emoji_reactions + @emoji_reactions ||= EmojiReactions::GroupedQueries.grouped_work_package_journals_emoji_reactions_by_reactable(work_package) end diff --git a/app/components/work_packages/activities_tab/journals/infinite_scroll_component.html.erb b/app/components/work_packages/activities_tab/journals/infinite_scroll_component.html.erb new file mode 100644 index 00000000000..269edae0380 --- /dev/null +++ b/app/components/work_packages/activities_tab/journals/infinite_scroll_component.html.erb @@ -0,0 +1,25 @@ +<%= + component_wrapper( + data: { + controller: "infinite-scroll", + infinite_scroll_insert_target_id_value: insert_target_id, + infinite_scroll_page_url_value: page_streams_url, + infinite_scroll_current_page_value: 1, # initial page + infinite_scroll_is_last_page_value: false # initial value + } + ) do + flex_layout( + data: { infinite_scroll_target: "skeleton" }, + class: "work-packages-activities-tab-journals-item-component", + bg: :default + ) do |skeleton| + skeleton.with_row(mb: 2) do + render(Primer::Alpha::SkeletonBox.new(width: "25%", height: "40px")) + end + + skeleton.with_row(mb: 3) do + render(Primer::Alpha::SkeletonBox.new(width: "100%", height: "150px")) + end + end + end +%> diff --git a/app/components/work_packages/activities_tab/journals/infinite_scroll_component.rb b/app/components/work_packages/activities_tab/journals/infinite_scroll_component.rb new file mode 100644 index 00000000000..a83d8495deb --- /dev/null +++ b/app/components/work_packages/activities_tab/journals/infinite_scroll_component.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module WorkPackages + module ActivitiesTab + module Journals + class InfiniteScrollComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(work_package:) + super + @work_package = work_package + end + + private + + attr_reader :work_package + + def insert_target_id + WorkPackages::ActivitiesTab::Journals::IndexComponent.insert_target_modifier_id + end + + def page_streams_url + page_streams_work_package_activities_path(work_package, format: :turbo_stream) + end + end + end + end +end diff --git a/app/components/work_packages/activities_tab/journals/page_component.html.erb b/app/components/work_packages/activities_tab/journals/page_component.html.erb new file mode 100644 index 00000000000..cc02170f5fc --- /dev/null +++ b/app/components/work_packages/activities_tab/journals/page_component.html.erb @@ -0,0 +1,20 @@ +<%= + component_wrapper do + flex_layout do |flex_container| + journals.each do |record| + flex_container.with_row do + if record.is_a?(Changeset) + render(WorkPackages::ActivitiesTab::Journals::RevisionComponent.new(changeset: record, filter:)) + else + render( + WorkPackages::ActivitiesTab::Journals::ItemComponent.new( + journal: record, filter:, + grouped_emoji_reactions: emoji_reactions.fetch(record.id, {}) + ) + ) + end + end + end + end + end +%> diff --git a/app/components/work_packages/activities_tab/journals/page_component.rb b/app/components/work_packages/activities_tab/journals/page_component.rb new file mode 100644 index 00000000000..86a4a612260 --- /dev/null +++ b/app/components/work_packages/activities_tab/journals/page_component.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module WorkPackages + module ActivitiesTab + module Journals + class PageComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + include WorkPackages::ActivitiesTab::SharedHelpers + + def initialize(journals:, emoji_reactions:, page:, filter: :all) + super + + @journals = journals + @emoji_reactions = emoji_reactions + @filter = filter + @page = page + end + + def render? + journals.any? + end + + def wrapper_uniq_by + page + end + + private + + attr_reader :journals, :emoji_reactions, :page, :filter + end + end + end +end diff --git a/app/components/work_packages/activities_tab/shared_helpers.rb b/app/components/work_packages/activities_tab/shared_helpers.rb index b9a03043d47..b603fe4f610 100644 --- a/app/components/work_packages/activities_tab/shared_helpers.rb +++ b/app/components/work_packages/activities_tab/shared_helpers.rb @@ -61,6 +61,14 @@ module WorkPackages end end + def journal_sorting_asc? + journal_sorting == "asc" + end + + def journal_sorting_desc? + journal_sorting == "desc" + end + def journal_sorting User.current.preference&.comments_sorting || OpenProject::Configuration.default_comment_sort_order end diff --git a/app/controllers/concerns/op_turbo/component_stream.rb b/app/controllers/concerns/op_turbo/component_stream.rb index be727ca7ecb..2332542f07d 100644 --- a/app/controllers/concerns/op_turbo/component_stream.rb +++ b/app/controllers/concerns/op_turbo/component_stream.rb @@ -71,6 +71,15 @@ module OpTurbo ) end + def insert_via_turbo_stream(action:, component:, target_component:) + case action + when :append + append_via_turbo_stream(component:, target_component:) + when :prepend + prepend_via_turbo_stream(component:, target_component:) + end + end + def append_via_turbo_stream(component:, target_component:) turbo_streams << target_component.insert_as_turbo_stream(component:, view_context:, action: :append) end @@ -104,6 +113,14 @@ module OpTurbo turbo_streams << instance.render_as_turbo_stream(view_context:, action: :flash) end + def set_dataset_attributes_via_turbo_stream(target, **attributes) + attributes.each do |attribute, value| + turbo_streams << OpTurbo::StreamComponent + .new(action: :set_dataset_attribute, target:, attribute:, value:) + .render_in(view_context) + end + end + def scroll_into_view_via_turbo_stream(target, behavior: :auto, block: :start) turbo_streams << OpTurbo::StreamComponent .new(action: :scroll_into_view, target:, behavior:, block:) diff --git a/app/controllers/work_packages/activities_tab_controller.rb b/app/controllers/work_packages/activities_tab_controller.rb index 7a538028f36..55c6310589f 100644 --- a/app/controllers/work_packages/activities_tab_controller.rb +++ b/app/controllers/work_packages/activities_tab_controller.rb @@ -29,6 +29,7 @@ # ++ class WorkPackages::ActivitiesTabController < ApplicationController + include Pagy::Backend include OpTurbo::ComponentStream include FlashMessagesOutputSafetyHelper @@ -37,19 +38,44 @@ class WorkPackages::ActivitiesTabController < ApplicationController before_action :find_journal, only: %i[edit cancel_edit update toggle_reaction] before_action :set_filter before_action :authorize + before_action :initialize_pagination, only: %i[index page_streams update_filter update_sorting] def index render( WorkPackages::ActivitiesTab::IndexComponent.new( work_package: @work_package, + journals: @paginated_journals, filter: @filter, - last_server_timestamp: get_current_server_timestamp, - deferred: ActiveRecord::Type::Boolean.new.cast(params[:deferred]) + last_server_timestamp: get_current_server_timestamp ), layout: false ) end + def page_streams + insert_via_turbo_stream( + target_component: WorkPackages::ActivitiesTab::Journals::IndexComponent.new( + work_package: @work_package, + journals: Journal.none # we do not need to pass any journals here since we just want the component key + ), + component: WorkPackages::ActivitiesTab::Journals::PageComponent.new( + journals: @paginated_journals, + emoji_reactions: wp_journals_emoji_reactions, + page: @paginator.page, + filter: @filter + ), + action: journal_sorting.asc? ? :append : :prepend + ) + + set_dataset_attributes_via_turbo_stream( + WorkPackages::ActivitiesTab::Journals::InfiniteScrollComponent.wrapper_key, + infinite_scroll_current_page_value: @paginator.page, + infinite_scroll_is_last_page_value: @paginator.next.blank? + ) + + respond_with_turbo_streams + end + def update_streams set_last_server_timestamp_to_headers @@ -173,6 +199,48 @@ class WorkPackages::ActivitiesTabController < ApplicationController private + def initialize_pagination + @paginator, @paginated_journals = pagy_array(base_journals, items: 30) + # For UI display: if user wants "oldest first" UI, reverse the array + @paginated_journals = @paginated_journals.reverse if journal_sorting.asc? + end + + def base_journals + combine_and_sort_records(fetch_journals, fetch_revisions) + end + + def fetch_journals + API::V3::Activities::ActivityEagerLoadingWrapper.wrap(fetch_ar_journals) + end + + def fetch_ar_journals + @work_package + .journals + .internal_visible + .includes(:user, :customizable_journals, :attachable_journals, :storable_journals, :notifications) + .reorder(version: :desc) # Always fetch newest first for pagination + .with_sequence_version + end + + def fetch_revisions + @work_package.changesets.includes(:user, :repository) + end + + def combine_and_sort_records(journals, revisions) + (journals + revisions).sort_by do |record| + timestamp = record_timestamp(record) + [-timestamp, -record.id] # Always sort DESC (newest first) + end + end + + def record_timestamp(record) + if record.is_a?(API::V3::Activities::ActivityEagerLoadingWrapper) + record.created_at&.to_i + elsif record.is_a?(Changeset) + record.committed_on.to_i + end + end + def respond_with_error(error_message) respond_to do |format| # turbo_frame requests (tab is initially rendered and an error occured) are handled below @@ -218,7 +286,8 @@ class WorkPackages::ActivitiesTabController < ApplicationController end def journal_sorting - User.current.preference&.comments_sorting || OpenProject::Configuration.default_comment_sort_order + ActiveSupport::StringInquirer + .new(User.current.preference&.comments_sorting || OpenProject::Configuration.default_comment_sort_order) end def sanitized_journal_notes @@ -287,6 +356,7 @@ class WorkPackages::ActivitiesTabController < ApplicationController replace_via_turbo_stream( component: WorkPackages::ActivitiesTab::IndexComponent.new( work_package: @work_package, + journals: @paginated_journals, filter: @filter, last_server_timestamp: get_current_server_timestamp ) @@ -297,6 +367,7 @@ class WorkPackages::ActivitiesTabController < ApplicationController update_via_turbo_stream( component: WorkPackages::ActivitiesTab::Journals::IndexComponent.new( work_package: @work_package, + journals: @paginated_journals, filter: @filter ) ) @@ -339,7 +410,7 @@ class WorkPackages::ActivitiesTabController < ApplicationController rerender_updated_journals(journals, last_update_timestamp, grouped_emoji_reactions, editing_journals) rerender_journals_with_updated_notification(journals, last_update_timestamp, grouped_emoji_reactions, editing_journals) - append_or_prepend_journals(journals, last_update_timestamp, grouped_emoji_reactions) + insert_latest_journals_via_turbo_stream(journals, last_update_timestamp, grouped_emoji_reactions) if journals.present? remove_potential_empty_state @@ -348,13 +419,11 @@ class WorkPackages::ActivitiesTabController < ApplicationController end def generate_work_package_journals_emoji_reactions_update_streams - wp_journal_emoji_reactions = - EmojiReactions::GroupedQueries.grouped_work_package_journals_emoji_reactions_by_reactable(@work_package) @work_package.journals.each do |journal| update_via_turbo_stream( component: WorkPackages::ActivitiesTab::Journals::ItemComponent::Reactions.new( journal:, - grouped_emoji_reactions: wp_journal_emoji_reactions[journal.id] || {} + grouped_emoji_reactions: wp_journals_emoji_reactions[journal.id] || {} ) ) end @@ -404,32 +473,21 @@ class WorkPackages::ActivitiesTabController < ApplicationController end end - def append_or_prepend_journals(journals, last_update_timestamp, grouped_emoji_reactions) - journals.where("created_at > ?", last_update_timestamp).find_each do |journal| - append_or_prepend_latest_journal_via_turbo_stream(journal, grouped_emoji_reactions.fetch(journal.id, {})) - end - end - - def append_or_prepend_latest_journal_via_turbo_stream(journal, grouped_emoji_reactions) + def insert_latest_journals_via_turbo_stream(journals, last_update_timestamp, emoji_reactions) target_component = WorkPackages::ActivitiesTab::Journals::IndexComponent.new( work_package: @work_package, + journals: Journal.none, # we do not need to pass any journals here since we just want the component key filter: @filter ) - component = WorkPackages::ActivitiesTab::Journals::ItemComponent.new( - journal:, filter: @filter, grouped_emoji_reactions: - ) - - stream_config = { - target_component:, - component: - } - - # Append or prepend the new journal depending on the sorting - if journal_sorting == "asc" - append_via_turbo_stream(**stream_config) - else - prepend_via_turbo_stream(**stream_config) + journals.where("created_at > ?", last_update_timestamp).find_each do |journal| + insert_via_turbo_stream( + target_component:, + component: WorkPackages::ActivitiesTab::Journals::ItemComponent.new( + journal:, filter: @filter, grouped_emoji_reactions: emoji_reactions.fetch(journal.id, {}) + ), + action: journal_sorting.asc? ? :append : :prepend + ) end end @@ -467,6 +525,11 @@ class WorkPackages::ActivitiesTabController < ApplicationController ) end + def wp_journals_emoji_reactions + @wp_journals_emoji_reactions ||= EmojiReactions::GroupedQueries + .grouped_work_package_journals_emoji_reactions_by_reactable(@work_package) + end + def grouped_emoji_reactions_for_journal EmojiReactions::GroupedQueries .grouped_emoji_reactions_by_reactable(reactable: @journal)[@journal.id] diff --git a/config/initializers/pagy.rb b/config/initializers/pagy.rb index a21dc471b3e..d13c0b2f494 100644 --- a/config/initializers/pagy.rb +++ b/config/initializers/pagy.rb @@ -61,7 +61,7 @@ # Array extra: Paginate arrays efficiently, avoiding expensive array-wrapping and without overriding # See https://ddnexus.github.io/pagy/docs/extras/array -# require 'pagy/extras/array' +require "pagy/extras/array" # Calendar extra: Add pagination filtering by calendar time unit (year, quarter, month, week, day) # See https://ddnexus.github.io/pagy/docs/extras/calendar @@ -105,7 +105,7 @@ # Keyset extra: Paginate with the Pagy keyset pagination technique # See https://ddnexus.github.io/pagy/docs/extras/keyset -# require 'pagy/extras/keyset' +require "pagy/extras/keyset" # Meilisearch extra: Paginate `Meilisearch` result objects # See https://ddnexus.github.io/pagy/docs/extras/meilisearch diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index abd50c324bd..8b9fe2fc7ba 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -261,7 +261,7 @@ Rails.application.reloader.to_prepare do work_packages: %i[show index show_conflict_flash_message share_upsell], work_packages_api: [:get], "work_packages/reports": %i[report report_details], - "work_packages/activities_tab": %i[index update_streams update_sorting update_filter], + "work_packages/activities_tab": %i[index page_streams update_streams update_sorting update_filter], "work_packages/menus": %i[show], "work_packages/hover_card": %i[show], work_package_relations_tab: %i[index], diff --git a/config/routes.rb b/config/routes.rb index 7a7665d1432..7549787d8e2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -735,6 +735,7 @@ Rails.application.routes.draw do end collection do + get :page_streams get :update_streams get :update_filter # filter not persisted put :update_sorting # sorting is persisted