diff --git a/app/components/message_attachment_component.html.erb b/app/components/message_attachment_component.html.erb index a0b6f8e82..765607733 100644 --- a/app/components/message_attachment_component.html.erb +++ b/app/components/message_attachment_component.html.erb @@ -59,7 +59,7 @@ <% if @message_attachment.nested_message_objects.any? %> -
+
<%= render NestedMessageAttachmentComponent.with_collection(@message_attachment.nested_message_objects) %>
<% end %> diff --git a/app/components/message_draft_attachments_list_component.html.erb b/app/components/message_draft_attachments_list_component.html.erb index 77d476680..b8f4dc0b4 100644 --- a/app/components/message_draft_attachments_list_component.html.erb +++ b/app/components/message_draft_attachments_list_component.html.erb @@ -60,7 +60,7 @@
<% if @message.attachments.any? %> -
+
<%= render MessageAttachmentComponent.with_collection(@message.attachments.order(updated_at: :desc)) %>
<% end %> diff --git a/app/components/message_draft_body_component.html.erb b/app/components/message_draft_body_component.html.erb index 238651303..ab67a78c6 100644 --- a/app/components/message_draft_body_component.html.erb +++ b/app/components/message_draft_body_component.html.erb @@ -15,8 +15,8 @@ "class": "mb-3 pt-0 w-full" } ) do %> - <%= text_field_tag message_title_id, @message.title, placeholder: "Predmet", "data-action": "change->messageDrafts#update", disabled: !@message.editable?, class:"mb-3 px-3 py-4 placeholder-slate-300 text-slate-í00 relative bg-white bg-white rounded-lg text-base border-0 shadow outline-none focus:outline-none focus:ring w-full" %> - <%= text_area_tag message_text_id, @message.metadata["message_body"], placeholder: "Text", "data-action": "change->messageDrafts#update", rows: 10, disabled: !@message.editable?, class:"px-3 py-4 placeholder-slate-300 text-slate-í00 relative bg-white bg-white rounded-lg text-base border-0 shadow outline-none focus:outline-none focus:ring w-full h-full" %> + <%= text_field_tag message_title_id, @message.title, placeholder: "Predmet", "data-action": "change->messageDrafts#update", disabled: !@message.editable?, class:"mb-3 px-3 py-4 placeholder-slate-300 text-slate-900 relative bg-white bg-white rounded-lg text-base border-0 shadow outline-none focus:outline-none focus:ring w-full" %> + <%= text_area_tag message_text_id, @message.metadata["message_body"], placeholder: "Text", "data-action": "change->messageDrafts#update", rows: 10, disabled: !@message.editable?, class:"px-3 py-4 placeholder-slate-300 text-slate-900 relative bg-white bg-white rounded-lg text-base border-0 shadow outline-none focus:outline-none focus:ring w-full h-full" %> <% end %> <% else %>
diff --git a/app/components/message_thread_component.html.erb b/app/components/message_thread_component.html.erb index 477e01060..71d399145 100644 --- a/app/components/message_thread_component.html.erb +++ b/app/components/message_thread_component.html.erb @@ -1,5 +1,6 @@
<%= render MessageThreadHeaderComponent.new(@message_thread, @thread_tags_with_deletable_flag) %> + <%= render MessageThreadNoteComponent.new(@message_thread_note) %>
<% @flash.each do |type, msg| %>
px-4 py-3" role="alert"> diff --git a/app/components/message_thread_component.rb b/app/components/message_thread_component.rb index f68cae77c..cfade7287 100644 --- a/app/components/message_thread_component.rb +++ b/app/components/message_thread_component.rb @@ -4,5 +4,6 @@ def initialize(message_thread:, thread_tags_with_deletable_flag:, thread_message @thread_tags_with_deletable_flag = thread_tags_with_deletable_flag @thread_messages = thread_messages @flash = flash + @message_thread_note = @message_thread.message_thread_note end end diff --git a/app/components/message_thread_note_component.html.erb b/app/components/message_thread_note_component.html.erb new file mode 100644 index 000000000..a2df39ec1 --- /dev/null +++ b/app/components/message_thread_note_component.html.erb @@ -0,0 +1,39 @@ +
> + +
diff --git a/app/components/message_thread_note_component.rb b/app/components/message_thread_note_component.rb new file mode 100644 index 000000000..17fc34997 --- /dev/null +++ b/app/components/message_thread_note_component.rb @@ -0,0 +1,6 @@ +class MessageThreadNoteComponent < ViewComponent::Base + def initialize(message_thread_note) + @message_thread_note = message_thread_note + @message_thread = @message_thread_note.message_thread + end +end diff --git a/app/components/nested_message_attachment_component.html.erb b/app/components/nested_message_attachment_component.html.erb index 808d57273..6d3a3afea 100644 --- a/app/components/nested_message_attachment_component.html.erb +++ b/app/components/nested_message_attachment_component.html.erb @@ -1,4 +1,4 @@ -
+
<%= link_to message_message_object_nested_message_object_path(@nested_message_attachment.message_object.message, @nested_message_attachment.message_object, @nested_message_attachment), data: { turbo_frame: '_top' }, class: "flex justify-start items-center gap-4 overflow-clip" do %>
diff --git a/app/controllers/message_thread_notes_controller.rb b/app/controllers/message_thread_notes_controller.rb new file mode 100644 index 000000000..4d8cb4646 --- /dev/null +++ b/app/controllers/message_thread_notes_controller.rb @@ -0,0 +1,38 @@ +class MessageThreadNotesController < ApplicationController + before_action :set_message_thread, only: %i[update create] + before_action :set_message_thread_note, only: %i[update] + + def update + authorize @message_thread_note + if @message_thread_note.update(message_thread_note_params) + redirect_back_or_to message_threads_path(@message_thread), notice: 'Note was successfully updated' + else + render :edit, status: :unprocessable_entity + end + end + + def create + @message_thread_note = @message_thread.build_message_thread_note(message_thread_note_params) + authorize @message_thread_note + + if @message_thread_note.save + redirect_back_or_to message_threads_path(@message_thread), notice: 'Note was successfully created' + else + render :new, status: :unprocessable_entity + end + end + + private + + def set_message_thread + @message_thread = policy_scope(MessageThread).find(params[:message_thread_id]) + end + + def set_message_thread_note + @message_thread_note = policy_scope(MessageThreadNote).find(params[:id]) + end + + def message_thread_note_params + params.require(:message_thread_note).permit(:note, :last_updated_at) + end +end diff --git a/app/controllers/message_threads_controller.rb b/app/controllers/message_threads_controller.rb index 5005926dc..7284eba6e 100644 --- a/app/controllers/message_threads_controller.rb +++ b/app/controllers/message_threads_controller.rb @@ -10,6 +10,7 @@ def show set_thread_tags_with_deletable_flag @flash = flash @thread_messages = @message_thread.messages_visible_to_user(Current.user).order(delivered_at: :asc) + @message_thread_note = @message_thread.message_thread_note || @message_thread.build_message_thread_note end def update diff --git a/app/lib/event_bus.rb b/app/lib/event_bus.rb index 559fd4b6f..a05ed2741 100644 --- a/app/lib/event_bus.rb +++ b/app/lib/event_bus.rb @@ -42,6 +42,14 @@ def self.reset! EventBus.subscribe :message_thread_changed, ->(message_thread) { Searchable::ReindexMessageThreadJob.perform_later(message_thread.id) } + +EventBus.subscribe :message_thread_note_created, ->(note) { + Searchable::ReindexMessageThreadJob.perform_later(note.message_thread_id) +} +EventBus.subscribe :message_thread_note_changed, ->(note) { + Searchable::ReindexMessageThreadJob.perform_later(note.message_thread_id) +} + EventBus.subscribe :message_thread_tag_changed, ->(message_thread_tag) { Searchable::ReindexMessageThreadJob.perform_later(message_thread_tag.message_thread_id) } EventBus.subscribe :tag_renamed, ->(tag) { Searchable::ReindexMessageThreadsWithTagIdJob.perform_later(tag.id) } diff --git a/app/models/message_thread.rb b/app/models/message_thread.rb index 807b2a745..d95bdab8b 100644 --- a/app/models/message_thread.rb +++ b/app/models/message_thread.rb @@ -23,6 +23,7 @@ def find_or_create_by_uuid!(uuid:) has_many :message_threads_tags, dependent: :destroy has_many :tag_users, through: :message_threads_tags has_many :merge_identifiers, class_name: 'MessageThreadMergeIdentifier', dependent: :destroy + has_one :message_thread_note, dependent: :destroy attr_accessor :search_highlight @@ -52,25 +53,37 @@ def mark_all_messages_read def self.merge_threads transaction do - target_thread = self.first - self.all.each do |thread| - if thread != target_thread - thread.merge_identifiers.update_all(message_thread_id: target_thread.id) - target_thread.last_message_delivered_at = [target_thread.last_message_delivered_at, thread.last_message_delivered_at].max - target_thread.delivered_at = [target_thread.delivered_at, thread.delivered_at].min - thread.messages.each do |message| - message.thread = target_thread - message.save! - end - thread.tags.each do |tag| - target_thread.tags.push(tag) unless target_thread.tags.include?(tag) - end - - thread.reload - thread.destroy! - end + target_thread = first + all.each do |thread| + thread.merge_thread_into(target_thread) if thread != target_thread end + target_thread.message_thread_note&.save! target_thread.save! end end + + def merge_thread_into(target_thread) + merge_identifiers.update_all(message_thread_id: target_thread.id) + merge_dates(target_thread) + messages.update_all(message_thread_id: target_thread.id) + tags.each { |tag| target_thread.tags.push(tag) unless target_thread.tags.include?(tag) } + merge_notes(target_thread) + destroy! + end + + def merge_dates(target_thread) + target_thread.last_message_delivered_at = [target_thread.last_message_delivered_at, + last_message_delivered_at].max + target_thread.delivered_at = [target_thread.delivered_at, delivered_at].min + end + + def merge_notes(target_thread) + return unless message_thread_note&.note + + if target_thread.message_thread_note + target_thread.message_thread_note.note = "#{target_thread.message_thread_note.note.rstrip}\n-----\n#{message_thread_note.note}" + else + target_thread.build_message_thread_note(note: message_thread_note.note) + end + end end diff --git a/app/models/message_thread_note.rb b/app/models/message_thread_note.rb new file mode 100644 index 000000000..cfa9c3a87 --- /dev/null +++ b/app/models/message_thread_note.rb @@ -0,0 +1,6 @@ +class MessageThreadNote < ApplicationRecord + belongs_to :message_thread + + after_create_commit ->(note) { EventBus.publish(:message_thread_note_created, note) } + after_update_commit ->(note) { EventBus.publish(:message_thread_note_changed, note) } +end diff --git a/app/models/searchable/indexer.rb b/app/models/searchable/indexer.rb index fa36ef381..259123989 100644 --- a/app/models/searchable/indexer.rb +++ b/app/models/searchable/indexer.rb @@ -8,6 +8,7 @@ class Searchable::Indexer def self.index_message_thread(message_thread) record = ::Searchable::MessageThread.find_or_initialize_by(message_thread_id: message_thread.id) record.title = Searchable::IndexHelpers.searchable_string(message_thread.title) + record.note = Searchable::IndexHelpers.searchable_string(message_thread.message_thread_note&.note.to_s) record.tag_ids = message_thread.tags.map(&:id) record.tag_names = Searchable::IndexHelpers.searchable_string(message_thread.tags.map(&:name).join(' ').gsub(/[:\/]/, " ")) record.content = message_thread.messages.map { |message| message_to_searchable_string(message) }.join(' ') diff --git a/app/models/searchable/message_thread.rb b/app/models/searchable/message_thread.rb index b4022ea1e..5de325e61 100644 --- a/app/models/searchable/message_thread.rb +++ b/app/models/searchable/message_thread.rb @@ -4,7 +4,7 @@ class Searchable::MessageThread < ApplicationRecord include PgSearch::Model pg_search_scope :pg_search_all, - against: [:title, :content, :tag_names], + against: [:title, :content, :note, :tag_names], using: { tsearch: { highlight: { diff --git a/app/policies/message_thread_note_policy.rb b/app/policies/message_thread_note_policy.rb new file mode 100644 index 000000000..976802df1 --- /dev/null +++ b/app/policies/message_thread_note_policy.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class MessageThreadNotePolicy < ApplicationPolicy + attr_reader :user, :message_thread_note + + def initialize(user, message_thread_note) + @user = user + @message_thread_note = message_thread_note + end + + class Scope < Scope + def resolve + scope.joins(:message_thread).where(message_thread: Pundit.policy_scope(@user, MessageThread)) + end + end + + def update? + true + end + + def create? + true + end +end diff --git a/config/routes.rb b/config/routes.rb index 44246f4f0..88354352d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,6 +52,7 @@ end get :search_available_tags, on: :member resources :messages + resources :message_thread_notes end resources :message_threads_tags diff --git a/db/migrate/20231012094539_create_message_thread_notes.rb b/db/migrate/20231012094539_create_message_thread_notes.rb new file mode 100644 index 000000000..554839980 --- /dev/null +++ b/db/migrate/20231012094539_create_message_thread_notes.rb @@ -0,0 +1,9 @@ +class CreateMessageThreadNotes < ActiveRecord::Migration[7.0] + def change + create_table :message_thread_notes do |t| + t.references :message_thread, null: false, foreign_key: true + t.text :note + t.timestamps + end + end +end diff --git a/db/migrate/20231016140436_add_note_to_searchable_message_thread.rb b/db/migrate/20231016140436_add_note_to_searchable_message_thread.rb new file mode 100644 index 000000000..2304e76d8 --- /dev/null +++ b/db/migrate/20231016140436_add_note_to_searchable_message_thread.rb @@ -0,0 +1,11 @@ +class AddNoteToSearchableMessageThread < ActiveRecord::Migration[7.0] + def change + add_column :searchable_message_threads, :note, :string + + Searchable::MessageThread.reset_column_information + + Searchable::MessageThread.update_all(note: "") + + change_column_null :searchable_message_threads, :note, false + end +end diff --git a/db/schema.rb b/db/schema.rb index 842ced72f..ebc5c6a59 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_10_06_114400) do +ActiveRecord::Schema[7.0].define(version: 2023_10_16_140436) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -285,6 +285,14 @@ t.index ["uuid", "tenant_id"], name: "index_message_thread_merge_identifiers_on_uuid_and_tenant_id", unique: true end + create_table "message_thread_notes", force: :cascade do |t| + t.bigint "message_thread_id", null: false + t.text "note" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["message_thread_id"], name: "index_message_thread_notes_on_message_thread_id" + end + create_table "message_threads", force: :cascade do |t| t.bigint "folder_id", null: false t.string "title", null: false @@ -358,6 +366,7 @@ t.datetime "updated_at", null: false t.integer "tenant_id", null: false t.integer "box_id", null: false + t.string "note", null: false t.index ["message_thread_id"], name: "index_searchable_message_threads_on_message_thread_id", unique: true end @@ -446,6 +455,7 @@ add_foreign_key "message_objects", "messages" add_foreign_key "message_thread_merge_identifiers", "message_threads" add_foreign_key "message_thread_merge_identifiers", "tenants" + add_foreign_key "message_thread_notes", "message_threads" add_foreign_key "message_threads", "folders" add_foreign_key "message_threads_tags", "message_threads" add_foreign_key "message_threads_tags", "tags" diff --git a/test/fixtures/message_thread_notes.yml b/test/fixtures/message_thread_notes.yml new file mode 100644 index 000000000..cdd3d33b1 --- /dev/null +++ b/test/fixtures/message_thread_notes.yml @@ -0,0 +1,7 @@ +one: + message_thread: one + note: "Note1" + +two: + message_thread: two + note: "Note2" \ No newline at end of file diff --git a/test/models/message_thread_test.rb b/test/models/message_thread_test.rb index d2c36baca..32d981432 100644 --- a/test/models/message_thread_test.rb +++ b/test/models/message_thread_test.rb @@ -119,4 +119,13 @@ class MessageThreadTest < ActiveSupport::TestCase assert_raises(ActiveRecord::RecordInvalid) { thread.save! } end + + test 'should contain notes from both merged threads after merge' do + threads = MessageThread.where(id: [message_threads(:two).id, message_threads(:one).id]) + + threads.merge_threads + + assert_match 'Note1', message_threads(:two).message_thread_note.note + assert_match 'Note2', message_threads(:two).message_thread_note.note + end end