mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Set up POC for infinite scrolling on AT (Messy Commit!)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+25
@@ -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
|
||||
%>
|
||||
@@ -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
|
||||
@@ -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
|
||||
%>
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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:)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user