diff --git a/Gemfile b/Gemfile index 0101fe068..45b631ba2 100644 --- a/Gemfile +++ b/Gemfile @@ -39,6 +39,7 @@ gem 'jwt' gem 'stimulus-rails' gem 'jsbundling-rails' gem 'pdf-reader' +gem "acts_as_list" gem 'grover' # Monitoring diff --git a/Gemfile.lock b/Gemfile.lock index 4362ba58e..51dbba4a4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,6 +76,8 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) + acts_as_list (1.1.0) + activerecord (>= 4.2) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) afm (0.2.2) @@ -508,6 +510,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + acts_as_list annotate bootsnap (>= 1.4.4) brakeman diff --git a/app/components/common/icon_component.html.erb b/app/components/common/icon_component.html.erb index 623f6628f..f68c46e36 100644 --- a/app/components/common/icon_component.html.erb +++ b/app/components/common/icon_component.html.erb @@ -1,3 +1,9 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="<%= @stroke_width %>" stroke="currentColor" class="shrink-0 <%= @classes.present? ? @classes : "w-6 h-6" %>"> - <path stroke-linecap="round" stroke-linejoin="round" d="<%= @svg %>" /> +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="<%= @stroke_width %>" stroke="currentColor" class="shrink-0 <%= @classes.presence || "w-6 h-6" %>"> + <% if @svg.class == Array %> + <% @svg.each do |svg| %> + <path stroke-linecap="round" stroke-linejoin="round" d="<%= svg %>" /> + <% end %> + <% else %> + <path stroke-linecap="round" stroke-linejoin="round" d="<%= @svg %>" /> + <% end %> </svg> diff --git a/app/components/common/icon_component.rb b/app/components/common/icon_component.rb index a4d509ca3..e02049fba 100644 --- a/app/components/common/icon_component.rb +++ b/app/components/common/icon_component.rb @@ -16,6 +16,7 @@ class IconComponent < ViewComponent::Base "bell-slash" => "M9.143 17.082a24.248 24.248 0 003.844.148m-3.844-.148a23.856 23.856 0 01-5.455-1.31 8.964 8.964 0 002.3-5.542m3.155 6.852a3 3 0 005.667 1.97m1.965-2.277L21 21m-4.225-4.225a23.81 23.81 0 003.536-1.003A8.967 8.967 0 0118 9.75V9A6 6 0 006.53 6.53m10.245 10.245L6.53 6.53M3 3l3.53 3.53", "chat-bubble-left-right" => "M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155", "chevron-down" => "m19.5 8.25-7.5 7.5-7.5-7.5", + "chevron-up" => "M4.5 15.75l7.5-7.5 7.5 7.5", "archive-box" => "M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z", "archive-box-x-mark" => "m20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5m6 4.125 2.25 2.25m0 0 2.25 2.25M12 13.875l2.25-2.25M12 13.875l-2.25 2.25M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z", "trash" => "M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0", @@ -23,6 +24,8 @@ class IconComponent < ViewComponent::Base "exclamation-triangle" => "M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z", "clock" => "M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z", "cloud-arrow-down" => "M12 9.75v6.75m0 0-3-3m3 3 3-3m-8.25 6a4.5 4.5 0 0 1-1.41-8.775 5.25 5.25 0 0 1 10.233-2.33 3 3 0 0 1 3.758 3.848A3.752 3.752 0 0 1 18 19.5H6.75Z", + "tag" => ["M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z", "M6 6h.008v.008H6V6Z"], + "bookmark" => "M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z", "paper-airplane" => "M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5", "document-arrow-down" => "M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z", "document-text" => "M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z", diff --git a/app/components/filters/filter_form_component.html.erb b/app/components/filters/filter_form_component.html.erb index 76b3d5a19..d0782ce07 100644 --- a/app/components/filters/filter_form_component.html.erb +++ b/app/components/filters/filter_form_component.html.erb @@ -10,15 +10,26 @@ <%= render Common::CloseButtonComponent.new(link_to: filters_path) %> </div> <%= form_with model: @filter, data: { turbo_frame: "_top" } do |form| %> + <%= form.hidden_field :type %> <div class="flex flex-col justify-start items-start self-stretch w-96"> <div class="flex justify-start items-center self-stretch gap-4 p-6"> <div class="flex flex-col justify-start items-start self-stretch flex-grow overflow-hidden gap-2 rounded-md"> - <%= form.text_field :name, placeholder: "Názov filtra", class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" %> + <%= form.label :name, "Názov filtra" %> + <%= form.text_field :name, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" %> </div> </div> + <% if @filter.is_a? FulltextFilter %> + <div class="flex justify-start items-center self-stretch gap-4 p-6 border-t-0 border-r-0 border-b border-l-0 border-gray-200"> + <div class="flex flex-col justify-start items-start self-stretch flex-grow overflow-hidden gap-2 rounded-md"> + <%= form.label :query, "Dopyt na vyhľadávanie" %> + <%= form.text_field :query, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" %> + </div> + </div> + <% end %> <div class="flex justify-start items-center self-stretch gap-4 p-6 border-t-0 border-r-0 border-b border-l-0 border-gray-200"> <div class="flex flex-col justify-start items-start self-stretch flex-grow overflow-hidden gap-2 rounded-md"> - <%= form.text_field :query, placeholder: "Dopyt na vyhľadávanie", class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" %> + <%= form.label :icon, "Ikona" %> + <%= form.select :icon, helpers.icon_select_options, {}, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" %> </div> </div> </div> diff --git a/app/components/filters/filters_list_row_component.html.erb b/app/components/filters/filters_list_row_component.html.erb index a83295cdd..c85612097 100644 --- a/app/components/filters/filters_list_row_component.html.erb +++ b/app/components/filters/filters_list_row_component.html.erb @@ -1,20 +1,28 @@ <div class="self-stretch p-6 border-b border-gray-200 justify-start items-center gap-4 inline-flex"> <div class="relative inline-flex items-center justify-center w-16 h-16 overflow-hidden bg-blue-600 rounded-full"> - <span class="text-white text-2xl font-normal"><%= @filter.name[0] %></span> + <% if (@filter.icon || @filter.tag&.icon).presence %> + <%= render Common::IconComponent.new(@filter.icon || @filter.tag.icon, classes: "w-6 h-6 stroke-gray-300", stroke_width: 2) %> + <% else %> + <span class="text-white text-2xl font-normal"><%= @filter.name[0] %></span> + <% end %> </div> <div class="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex"> <div class="text-center text-gray-900 text-lg font-medium leading-loose"> <%= render Common::InlineRenameComponent.new(name: @filter.name, model: @filter) %> </div> <div class="text-center text-gray-500 text-base font-normal leading-normal"> - <%= @filter.query %> + <% if @filter.is_a? TagFilter %> + <%= render Common::TagComponent.new(@filter.tag) %> + <% else %> + <%= @filter.query %> + <% end %> </div> </div> <div class="justify-start items-start gap-2 flex"> <%= link_to edit_filter_path(@filter), title: "Upraviť filter" do %> <%= render Common::EditButtonComponent.new %> <% end %> - <%= link_to filter_path(@filter), title: "Zmazať filter", data: { turbo_confirm: "Naozaj?", turbo_method: :delete } do %> + <%= button_to filter_path(@filter), title: "Zmazať filter", method: :delete, data: { turbo_confirm: "Naozaj chcete zmazať filter?" } do %> <%= render Common::DeleteButtonComponent.new %> <% end %> </div> diff --git a/app/components/layout/filter_list_component.html.erb b/app/components/layout/filter_list_component.html.erb index 0c3bfe5bf..c7f28080f 100644 --- a/app/components/layout/filter_list_component.html.erb +++ b/app/components/layout/filter_list_component.html.erb @@ -1,14 +1,30 @@ <% if @filters.present? %> - <div class="flex flex-col justify-start items-start gap-2" data-test="filters"> - <div class="flex justify-start items-start px-4 py-2"> - <p class="text-sm text-center text-gray-400">Filtre</p> - </div> + <div + <% if @sortable %> + data-controller="sortable" + data-sortable-url-value="<%= sort_filters_path %>" + data-sortable-draggable-class=".item" + data-sortable-handle-class=".handle" + <% end %> + class="w-full flex flex-col justify-start items-start gap-2" + data-test="filters" + > + <% if @sortable %> + <%= link_to 'Sort', '', class: 'hidden', data: { turbo_method: :patch, sortable_target: 'submit', url: sort_filters_path } %> + <% end %> + <% if @label.present? %> + <div class="flex justify-start items-start px-4 py-2"> + <p class="text-sm text-center text-gray-400"><%= @label %></p> + </div> + <% end %> <% @filters.each do |filter| %> - <% url = message_threads_path(q: filter.query) %> - <%= link_to url, class: "text-gray-700 hover:text-indigo-600 hover:bg-gray-50 group w-72 flex gap-x-3 rounded-md p-2 px-4 text-sm leading-6 font-semibold data-[active=true]:bg-gray-50 data-[active=true]:text-indigo-600", data: { active: current_page?(url) } do %> - <%= render Icons::BookmarkComponent.new %> - <p class="truncate text-base font-medium"><%= filter.name %></p> - <% end %> + <%= render TW::SidebarMenuItemComponent.new( + name: filter.name, + url: filtered_message_threads_path(filter:), + icon: icon_for(filter), + variant: :light, + classes: "item #{!@sortable ? 'pl-4' : ''}", + ) %> <% end %> </div> -<% end %> +<% end %> \ No newline at end of file diff --git a/app/components/layout/filter_list_component.rb b/app/components/layout/filter_list_component.rb index d3ba8594b..a77931d34 100644 --- a/app/components/layout/filter_list_component.rb +++ b/app/components/layout/filter_list_component.rb @@ -1,5 +1,20 @@ class Layout::FilterListComponent < ViewComponent::Base - def initialize(filters:) + include MessageThreadHelper + + def initialize(label: nil, filters:, sortable: false) + @label = label @filters = filters + @sortable = sortable + end + + def icon_for(filter) + return Common::IconComponent.new(filter.icon) if filter.icon.present? + + if filter.tag.present? + return Common::IconComponent.new(filter.tag.icon) if filter.tag.icon.present? + return Icons::TagComponent.new + end + + Icons::BookmarkComponent.new end end diff --git a/app/components/message_options_component.html.erb b/app/components/message_options_component.html.erb index 06127ebd5..0a36d3c43 100644 --- a/app/components/message_options_component.html.erb +++ b/app/components/message_options_component.html.erb @@ -32,12 +32,12 @@ <% if @mode == :thread_view && @message.collapsible? %> <% if @message.collapsed %> <%= button_to message_path(@message), params: { collapsed: false }, method: :patch, class: 'whitespace-nowrap flex gap-3', role: 'menu-item', tabindex: -1 do %> - <%= render Icons::ChevronDownComponent.new(css_classes: "w-5 h-5") %> + <%= render Common::IconComponent.new("chevron-down", classes: "w-5 h-5") %> Vždy zobrazovať <% end %> <% else %> <%= button_to message_path(@message), params: { collapsed: true }, method: :patch, class: 'whitespace-nowrap flex gap-3', role: 'menu-item', tabindex: -1 do %> - <%= render Icons::ChevronUpComponent.new(css_classes: "w-5 h-5") %> + <%= render Common::IconComponent.new("chevron-up", classes: "w-5 h-5") %> Zbaliť a už nerozbaľovať <% end %> <% end %> diff --git a/app/components/message_threads_bulk_actions_component.html.erb b/app/components/message_threads_bulk_actions_component.html.erb index cff6cc728..b73c25a4a 100644 --- a/app/components/message_threads_bulk_actions_component.html.erb +++ b/app/components/message_threads_bulk_actions_component.html.erb @@ -1,8 +1,7 @@ <%= tag.turbo_frame id: "bulk_actions" do %> <div class="flex justify-stretch items-center gap-4 px-4 lg:py-4 border-t-0 border-r-0 border-b border-l-0 border-gray-200 min-h-[3rem] sm:min-h-[5rem]"> <%= check_box_tag("", nil, false, { class: "hidden sm:block h-4 w-4 rounded border-gray-300 text-blue-500 focus:ring-0", type: "checkbox", id: "checkbox-all", data: { action: "all-checkboxes#toggle", "all-checkboxes-target": "checkbox" } }) %> - - <span class="grow sm:text-xl text-base font-semibold text-left text-gray-900"><%= @ids.present? ? t(:selected_message, count: @ids.count) : "Správy v schránke" %> + <span class="grow sm:text-xl text-base font-semibold text-left text-gray-900"><%= title %> <% if @filter %> <% if @filter_subscription %> <%= link_to edit_filter_filter_subscription_path(@filter, @filter_subscription), title: "Nastaviť notifikácie", "data-turbo-frame": :modal, form_class: "inline" do %> @@ -25,7 +24,7 @@ <div> <button type="button" data-action="dropdown#toggle click@window->dropdown#hide" class="inline-flex w-full justify-center items-center gap-x-1.5 rounded-md bg-white px-2 py-1 sm:px-3 sm:py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" id="menu-button" aria-expanded="true" aria-haspopup="true"> Hromadné akcie - <%= render Icons::ChevronDownComponent.new(css_classes: "w-4 h-4 text-gray-400", stroke_width: 2) %> + <%= render Common::IconComponent.new('chevron-down', classes: "w-4 h-4 text-gray-400", stroke_width: 2) %> </button> </div> @@ -127,7 +126,7 @@ <div> <button type="button" data-action="dropdown#toggle click@window->dropdown#hide" class="inline-flex w-full justify-center items-center gap-x-1.5 rounded-md bg-white px-2 py-1 sm:px-3 sm:py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" id="menu-button" aria-expanded="true" aria-haspopup="true"> Vytvoriť novú správu - <%= render Icons::ChevronDownComponent.new(css_classes: "w-4 h-4 text-gray-400", stroke_width: 2) %> + <%= render Common::IconComponent.new('chevron-down', classes: "w-4 h-4 text-gray-400", stroke_width: 2) %> </button> </div> diff --git a/app/components/message_threads_bulk_actions_component.rb b/app/components/message_threads_bulk_actions_component.rb index 90f5e523b..7b847f216 100644 --- a/app/components/message_threads_bulk_actions_component.rb +++ b/app/components/message_threads_bulk_actions_component.rb @@ -1,8 +1,18 @@ class MessageThreadsBulkActionsComponent < ViewComponent::Base - def initialize(ids:, signable:, filter: nil, filter_subscription: nil) + def initialize(ids: nil, signable:, filter: nil, query: nil, filter_subscription: nil) @ids = ids @signable = signable @filter = filter + @query = query @filter_subscription = filter_subscription end + + def title + return t(:selected_message, count: @ids.count) if @ids.present? + return @filter.name if @filter.present? && @filter.is_a?(EverythingFilter) + return "Správy z filtra '#{@filter.name}'" if @filter.present? + return "Hľadaný výraz '#{@query}'" if @query.present? + + "Správy v schránke" + end end diff --git a/app/components/message_threads_table_component.html.erb b/app/components/message_threads_table_component.html.erb index dd789a1c4..3659a8b5b 100644 --- a/app/components/message_threads_table_component.html.erb +++ b/app/components/message_threads_table_component.html.erb @@ -1,6 +1,6 @@ <div class="flex flex-col justify-stretch items-stretch gap-4 sm:p-4"> <div class="flex flex-col justify-stretch items-stretch sm:rounded-md bg-white sm:border sm:border-gray-200" data-controller="form all-checkboxes"> - <%= render MessageThreadsBulkActionsComponent.new(ids: [], filter: @filter, filter_subscription: @filter_subscription, signable: Current.user.signer?) %> + <%= render MessageThreadsBulkActionsComponent.new(ids: [], filter:, query:, filter_subscription:, signable: Current.user.signer?) %> <%= form_with url: bulk_actions_message_threads_path, data: { "form-target": "form", "all-checkboxes-target": "form" } do %> <ul role="list" id="message_threads" data-controller="visited-links" class="divide-y divide-gray-100"> <% message_threads.each do |message_thread| %> diff --git a/app/components/message_threads_table_component.rb b/app/components/message_threads_table_component.rb index 5254c5708..c8114d03f 100644 --- a/app/components/message_threads_table_component.rb +++ b/app/components/message_threads_table_component.rb @@ -2,8 +2,11 @@ class MessageThreadsTableComponent < ViewComponent::Base renders_many :message_threads renders_one :next_page_area - def initialize(filter:, filter_subscription:) + attr_reader :filter, :query, :filter_subscription + + def initialize(filter: nil, query: nil, filter_subscription:) @filter = filter + @query = query @filter_subscription = filter_subscription end end diff --git a/app/components/settings/user_filter_visibilities/list_component.html.erb b/app/components/settings/user_filter_visibilities/list_component.html.erb new file mode 100644 index 000000000..86362c676 --- /dev/null +++ b/app/components/settings/user_filter_visibilities/list_component.html.erb @@ -0,0 +1,11 @@ +<div class="w-full p-4 flex-col justify-start items-start gap-4 inline-flex"> + <div class="self-stretch bg-white rounded-md border border-gray-200 flex-col justify-start items-start flex"> + <div class="flex-col self-stretch p-6 border-b border-gray-200 justify-start items-start gap-4 inline-flex"> + <div class="grow shrink basis-0 text-gray-900 text-xl font-semibold leading-[35px]">Filtre</div> + Nastavte si osobnú preferenciu viditeľnosti filtrov v ľavom menu + </div> + <div class="self-stretch flex-col justify-start items-start flex"> + <%= render Settings::UserFilterVisibilities::ListRowComponent.with_collection(@visibilities) %> + </div> + </div> +</div> diff --git a/app/components/settings/user_filter_visibilities/list_component.rb b/app/components/settings/user_filter_visibilities/list_component.rb new file mode 100644 index 000000000..b21ad237e --- /dev/null +++ b/app/components/settings/user_filter_visibilities/list_component.rb @@ -0,0 +1,5 @@ +class Settings::UserFilterVisibilities::ListComponent < ViewComponent::Base + def initialize(visibilities) + @visibilities = visibilities + end +end diff --git a/app/components/settings/user_filter_visibilities/list_row_component.html.erb b/app/components/settings/user_filter_visibilities/list_row_component.html.erb new file mode 100644 index 000000000..b60639d1a --- /dev/null +++ b/app/components/settings/user_filter_visibilities/list_row_component.html.erb @@ -0,0 +1,51 @@ +<div class="self-stretch p-6 border-b border-gray-200 items-center gap-4 inline-flex"> + <div class="grow shrink basis-0 gap-1"> + <div class="text-gray-900 text-lg font-medium leading-loose w-fit"> + <%= @filter.name %> + </div> + </div> + <% if @visibility.new_record? %> + <%= form_with model: [:settings, Current.user.user_filter_visibilities.new(filter: @filter)], method: :post do |form| %> + <%= form.hidden_field :filter_id, value: @filter.id %> + <%= form.hidden_field :visible, value: !@visibility.visible %> + <%= form.button class: class_names('relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2', { "bg-gray-200": @visibility.hidden, "bg-indigo-600": @visibility.visible }), role: :switch, aria: { checked: @visibility.hidden.to_s } do %> + <span class="sr-only">Use setting</span> + <span + aria-hidden="true" + class="<%= class_names( + 'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-2', + { 'translate-x-0': @visibility.hidden, 'translate-x-5': @visibility.visible } + ) %>" + ></span> + <% end %> + <% end %> + <% else %> + <%= form_with model: [:settings, @visibility], method: :patch do |form| %> + <%= form.hidden_field :visible, value: !@visibility.visible %> + <%= form.button class: class_names('relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2', { "bg-gray-200": @visibility.hidden, "bg-indigo-600": @visibility.visible }), role: :switch, aria: { checked: @visibility.hidden.to_s } do %> + <span class="sr-only">Use setting</span> + <span + aria-hidden="true" + class="<%= class_names( + 'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out', + { 'translate-x-0': @visibility.hidden, 'translate-x-5': @visibility.visible } + ) %>" + ></span> + <% end %> + <% end %> + <%= form_with model: [:move_higher, :settings, @visibility], method: :post do |form| %> + <%= form.button do %> + <div class="flex justify-center items-center flex-grow-0 flex-shrink-0 relative overflow-hidden gap-2.5 px-3.5 py-2.5 rounded-md bg-white border border-gray-300 hover:bg-gray-100"> + <%= render Common::IconComponent.new("chevron-up") %> + </div> + <% end %> + <% end %> + <%= form_with model: [:move_lower, :settings, @visibility], method: :post do |form| %> + <%= form.button do %> + <div class="flex justify-center items-center flex-grow-0 flex-shrink-0 relative overflow-hidden gap-2.5 px-3.5 py-2.5 rounded-md bg-white border border-gray-300 hover:bg-gray-100"> + <%= render Common::IconComponent.new("chevron-down") %> + </div> + <% end %> + <% end %> + <% end %> +</div> diff --git a/app/components/settings/user_filter_visibilities/list_row_component.rb b/app/components/settings/user_filter_visibilities/list_row_component.rb new file mode 100644 index 000000000..99cf8ca6e --- /dev/null +++ b/app/components/settings/user_filter_visibilities/list_row_component.rb @@ -0,0 +1,7 @@ +class Settings::UserFilterVisibilities::ListRowComponent < ViewComponent::Base + with_collection_parameter :visibility + def initialize(visibility:) + @visibility = visibility + @filter = visibility.filter + end +end diff --git a/app/components/t_w/create_filter_component.html.erb b/app/components/t_w/create_filter_component.html.erb index 60fe2fa46..23ce9a2cc 100644 --- a/app/components/t_w/create_filter_component.html.erb +++ b/app/components/t_w/create_filter_component.html.erb @@ -11,6 +11,7 @@ </div> </div> + <%= f.hidden_field :type %> <%= f.hidden_field :query %> <%= render Common::ModalActionsComponent.new do |actions| %> diff --git a/app/components/t_w/sidebar_menu_item_component.html.erb b/app/components/t_w/sidebar_menu_item_component.html.erb index a5888f916..d68d22672 100644 --- a/app/components/t_w/sidebar_menu_item_component.html.erb +++ b/app/components/t_w/sidebar_menu_item_component.html.erb @@ -1,6 +1,19 @@ -<%= link_to @url, class: "items-center text-gray-700 hover:text-indigo-600 hover:bg-gray-50 w-full group flex gap-x-3 rounded-md p-4 leading-6 font-semibold data-[active=true]:bg-gray-50 data-[active=true]:text-indigo-600", data: { active: current_page?(@url, check_parameters: true) } do %> - <% if @icon_component %> - <%= render @icon_component %> +<div + class="flex items-center text-gray-700 hover:text-indigo-600 hover:bg-gray-50 w-full group rounded-md leading-6 font-semibold data-[active=true]:bg-gray-50 data-[active=true]:text-indigo-600 <%= @classes %>" + <%= tag.attributes(data: { active: current_page?(@url, check_parameters: true) } ) %> +> + <% if leading %> + <%= leading %> <% end %> - <%= @name %> -<% end %> + <%= link_to \ + @url, + class: "w-full flex gap-x-3 py-4 grow" do %> + <% if @icon_component %> + <%= render @icon_component %> + <% end %> + <span class="truncate text-base <%= light? ? 'font-medium' : '' %>"><%= @name %></span> + <% end %> + <% if trailing %> + <%= trailing %> + <% end %> +</div> diff --git a/app/components/t_w/sidebar_menu_item_component.rb b/app/components/t_w/sidebar_menu_item_component.rb index 868c2e347..4a05ee7c5 100644 --- a/app/components/t_w/sidebar_menu_item_component.rb +++ b/app/components/t_w/sidebar_menu_item_component.rb @@ -1,7 +1,20 @@ class TW::SidebarMenuItemComponent < ViewComponent::Base - def initialize(name:, url:, icon:) + renders_one :leading + renders_one :trailing + + def initialize(name:, url:, icon:, variant: :regular, classes: 'pl-4') @name = name @url = url @icon_component = icon + @variant = variant + @classes = classes + end + + def regular? + @variant == :regular + end + + def light? + @variant == :light end end diff --git a/app/components/t_w/top_navigation_component.html.erb b/app/components/t_w/top_navigation_component.html.erb index 0a616cf2f..586a3885f 100644 --- a/app/components/t_w/top_navigation_component.html.erb +++ b/app/components/t_w/top_navigation_component.html.erb @@ -10,10 +10,11 @@ <div class="flex justify-start items-stretch grow relative gap-3 rounded-md bg-white"> <%= form_with url: message_threads_path, method: :get, html: { class: 'relative flex flex-1' } do |f| %> <%= render Icons::MagnifyingGlassComponent.gray %> - <%= f.search_field :q, id: "search", value: params[:q], placeholder: 'Vyhľadaj správu', class: 'block h-full w-full border-0 py-0 pr-0 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-base' %> + <%= f.hidden_field :filter_id, value: query_params[:filter_id] %> + <%= f.search_field :q, id: "search", value: query, placeholder: 'Vyhľadaj správu', class: 'block h-full w-full border-0 py-0 pr-0 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-base' %> <% end %> - <% if params[:q].present? %> - <%= link_to new_filter_path(query: params[:q]), data: { turbo_frame: "modal" }, class: "flex items-center gap-2 text-gray-500 hover:text-gray-900", title: "Pridať filter" do %> + <% if query.present? %> + <%= link_to new_filter_path(query:), data: { turbo_frame: "modal" }, class: "flex items-center gap-2 text-gray-500 hover:text-gray-900", title: "Pridať filter" do %> <%= render Icons::BookmarkComponent.new %> <% end %> <% end %> diff --git a/app/components/t_w/top_navigation_component.rb b/app/components/t_w/top_navigation_component.rb index 13b29e70f..2898fc7e5 100644 --- a/app/components/t_w/top_navigation_component.rb +++ b/app/components/t_w/top_navigation_component.rb @@ -1,2 +1,17 @@ class TW::TopNavigationComponent < ViewComponent::Base + include MessageThreadHelper + + def query + Searchable::QueryBuilder.new( + filter_id: nil, + query: query_params[:q], + user: Current.user + ).build + end + + def query_params + params + .permit(:filter_id, :q) + .slice(:filter_id, :q) + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 24bdd32ee..7ec65e093 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -13,8 +13,8 @@ def pundit_user def set_menu_context return unless Current.user - @tags = policy_scope(Tag, policy_scope_class: TagPolicy::ScopeListable).where(visible: true).order(:name) - @filters = policy_scope(Filter, policy_scope_class: FilterPolicy::ScopeShowable).order(:position) - @menu = SidebarMenu.new(controller_name, action_name, { tags: @tags, filters: @filters }).menu + @filters = Current.user.visible_filters + @menu = SidebarMenu.new(controller_name, action_name, tags: @tags, filters: @filters).menu + @current_tenant_boxes_count = Current.tenant.boxes.count end end diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index f8d0b806b..42d759e90 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -11,10 +11,10 @@ def new authorize Filter if params[:query].present? - @filter = Filter.new(query: params[:query]) + @filter = FulltextFilter.new(query: params[:query]) render :new_in_modal else - @filter = Filter.new + @filter = FulltextFilter.new render :new end end @@ -22,17 +22,17 @@ def new def create authorize Filter - @filter = Current.tenant.filters.build(filter_params.merge({author_id: Current.user.id})) + @filter = Current.tenant.filters.build(filter_params.merge({ author_id: Current.user.id })) if @filter.save flash[:notice] = 'Filter bol úspešne vytvorený' if params[:to] == 'search' - redirect_to message_threads_path(q: @filter.query) + redirect_to helpers.filtered_message_threads_path(filter: @filter) else redirect_to filters_path end else if params[:to] == 'search' - redirect_to message_threads_path(q: @filter.query), alert: 'Filter sa nepodarilo vytvoriť :(' + redirect_to helpers.filtered_message_threads_path(query: @filter.query), alert: 'Filter sa nepodarilo vytvoriť :(' else render :new end @@ -59,10 +59,29 @@ def destroy redirect_to filters_path, notice: 'Filter bol úspešne odstránený' end + def sort + filters = filter_scope + .where(id: params[:filter_ids]) + .reorder('') + .in_order_of(:id, params[:filter_ids]) + + filters.map do |filter| + authorize filter + end + + Filter.transaction do + filters.map.with_index do |filter, i| + filter.update!(position: i + 1) + end + end + + redirect_back fallback_location: filters_path + end + private def filter_params - params.require(:filter).permit(:name, :query) + params.require(:filter).permit(:name, :query, :icon, :type) end def set_filter @@ -70,6 +89,6 @@ def set_filter end def filter_scope - policy_scope(Filter, policy_scope_class: FilterPolicy::ScopeEditable).order(:position) + policy_scope(Filter, policy_scope_class: FilterPolicy::ScopeEditable).includes(:tag).order(:position) end end diff --git a/app/controllers/message_threads_controller.rb b/app/controllers/message_threads_controller.rb index 1c1f6b832..f6f857ea7 100644 --- a/app/controllers/message_threads_controller.rb +++ b/app/controllers/message_threads_controller.rb @@ -31,6 +31,15 @@ def update def index authorize MessageThread + if search_params[:filter_id].present? + @filter = policy_scope(Filter, policy_scope_class: FilterPolicy::ScopeShowable).find_by(id: search_params[:filter_id]) + else + if Current.user.visible_filters.any? + redirect_to message_threads_path(search_params.merge(filter_id: Current.user.visible_filters.first.id)) + end + end + + @query = search_params[:q] end def scroll @@ -70,13 +79,20 @@ def merge_threads(message_thread_ids) end def load_threads + query = Searchable::QueryBuilder.new( + filter: @filter, + filter_id: search_params[:filter_id], + query: search_params[:q], + user: Current.user, + ).build + cursor = MessageThreadCollection.init_cursor(search_params[:cursor]) result = MessageThreadCollection.all( scope: message_thread_policy_scope.includes(:tags, :box), search_permissions: search_permissions, - query: search_params[:q], + query:, cursor: cursor ) @@ -133,7 +149,7 @@ def message_thread_params end def search_params - params.permit(:q, :format, cursor: MessageThreadCollection::CURSOR_PARAMS) + params.permit(:q, :format, :filter_id, cursor: MessageThreadCollection::CURSOR_PARAMS) end def set_thread_tags diff --git a/app/controllers/settings/user_filter_visibilities_controller.rb b/app/controllers/settings/user_filter_visibilities_controller.rb new file mode 100644 index 000000000..1dcaf1895 --- /dev/null +++ b/app/controllers/settings/user_filter_visibilities_controller.rb @@ -0,0 +1,55 @@ +class Settings::UserFilterVisibilitiesController < ApplicationController + before_action :set_user_filter_visibility, only: [:update, :move_higher, :move_lower, :destroy] + + def index + authorize UserFilterVisibility + + set_user_filter_visibilities + end + + def create + authorize UserFilterVisibility + Current.user.user_filter_visibilities.create(user_filter_visibility_params).tap do |visibility| + visibility.move_to_bottom + end + redirect_back fallback_location: request.referer + end + + def update + authorize @user_filter_visibility + @user_filter_visibility.update(user_filter_visibility_params) + redirect_back fallback_location: request.referer + end + + def move_higher + authorize @user_filter_visibility + @user_filter_visibility.move_higher + redirect_back fallback_location: request.referer + end + + def move_lower + authorize @user_filter_visibility + @user_filter_visibility.move_lower + redirect_back fallback_location: request.referer + end + + def destroy + authorize @user_filter_visibility + @user_filter_visibility.destroy + redirect_back fallback_location: request.referer + end + + private + + def set_user_filter_visibilities + @user_filter_visibilities = Current.user.build_filter_visibilities! + end + + def set_user_filter_visibility + @user_filter_visibility = policy_scope(UserFilterVisibility).find(params[:id]) + end + + def user_filter_visibility_params + params.require(:user_filter_visibility).permit(:filter_id, :visible) + end +end diff --git a/app/helpers/message_thread_helper.rb b/app/helpers/message_thread_helper.rb index f036423a5..99cd43c5a 100644 --- a/app/helpers/message_thread_helper.rb +++ b/app/helpers/message_thread_helper.rb @@ -1,4 +1,13 @@ module MessageThreadHelper + def filtered_message_threads_path(filter: nil, query: nil) + args = { + filter_id: filter&.id, + q: query + }.compact + + message_threads_path(args) + end + def self.show_recipient?(message_thread) !message_thread.box.single_recipient? && message_thread.is_outbox && message_thread.recipient.present? end diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index d670cfc75..6fcaa5eb3 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -28,6 +28,9 @@ application.register("form", FormController) import MessageDraftsController from "./message_drafts_controller" application.register("message-drafts", MessageDraftsController) +import SortableController from "./sortable_controller" +application.register("sortable", SortableController) + import DropzoneController from "./dropzone_controller" application.register("dropzone", DropzoneController) diff --git a/app/javascript/controllers/sortable_controller.js b/app/javascript/controllers/sortable_controller.js new file mode 100644 index 000000000..21317f1ac --- /dev/null +++ b/app/javascript/controllers/sortable_controller.js @@ -0,0 +1,33 @@ +import {Controller} from "@hotwired/stimulus" +import {Sortable} from "@shopify/draggable"; +import {post} from '@rails/request.js' + +export default class extends Controller { + static classes = ["draggable", "handle"] + static targets = ["item", "submit"] + static values = {url: String} + + connect() { + this.sortable = new Sortable(this.element, { + draggable: this.draggableClass, + handle: this.handleClass, + classes: { + "source:dragging": "invisible", + } + }) + + this.sortable.on('drag:stopped', async (e) => { + // As of now, this is the only way to submit a PATCH request with Rails so Turbo updates the pages without a full reload + + const link = this.submitTarget; + + const urlSearchParams = new URLSearchParams({_method: 'patch'}); + this.itemTargets.map((item) => { + urlSearchParams.append('filter_ids[]', item.dataset.id); + }); + link.href = this.urlValue + '?' + urlSearchParams.toString(); + + link.click() + }); + } +} diff --git a/app/lib/sidebar_menu.rb b/app/lib/sidebar_menu.rb index 365bf319b..96560792c 100644 --- a/app/lib/sidebar_menu.rb +++ b/app/lib/sidebar_menu.rb @@ -1,30 +1,40 @@ class SidebarMenu include Rails.application.routes.url_helpers - def initialize(controller, action, parameters = nil) + def initialize(controller, _action, parameters = nil, filters: [], tags: []) @parameters = parameters - @menu = initial_structure(controller, action) + @filters = filters + @menu = current_menu(controller) end attr_reader :menu private - def initial_structure(controller, _action) - return admin_menu + site_admin_menu if controller.in? %w[groups users tags tag_groups automation_rules boxes api_connections filters automation_webhooks] + def current_menu(controller) + return admin_menu + site_admin_menu if Current.user.admin? && controller.in?(%w[groups users tags tag_groups automation_rules boxes api_connections filters automation_webhooks user_filter_visibilities]) + return settings_menu if controller.in? %w[filters tags user_filter_visibilities] default_main_menu end def default_main_menu [ - TW::SidebarMenuItemComponent.new(name: 'Všetky správy', url: message_threads_path, icon: Common::IconComponent.new("envelope")), - Layout::FilterListComponent.new(filters: @parameters[:filters]), - Layout::TagListComponent.new(tags: @parameters[:tags]), + Layout::FilterListComponent.new(filters: @filters), TW::SidebarMenuItemComponent.new(name: 'Nastavenia', url: filters_path, icon: Icons::CogSixToothComponent.new) ] end + def settings_menu + [ + Layout::BackToBoxComponent.new, + TW::SidebarMenuDividerComponent.new(name: 'Nastavenia'), + TW::SidebarMenuItemComponent.new(name: 'Filtre', url: filters_path, icon: Icons::BookmarkComponent.new), + TW::SidebarMenuItemComponent.new(name: 'Pravidlá', url: settings_automation_rules_path, icon: Icons::FunnelComponent.new), + TW::SidebarMenuItemComponent.new(name: 'Viditeľnosť filtrov', url: settings_user_filter_visibilities_path, icon: Common::IconComponent.new("bookmark")) + ] + end + def admin_menu return [] unless Current.user.admin? @@ -33,6 +43,7 @@ def admin_menu TW::SidebarMenuDividerComponent.new(name: 'Nastavenia'), TW::SidebarMenuItemComponent.new(name: 'Filtre', url: filters_path, icon: Icons::BookmarkComponent.new), TW::SidebarMenuItemComponent.new(name: 'Pravidlá', url: settings_automation_rules_path, icon: Icons::FunnelComponent.new), + TW::SidebarMenuItemComponent.new(name: 'Viditeľnosť filtrov', url: settings_user_filter_visibilities_path, icon: Common::IconComponent.new("tag")), TW::SidebarMenuDividerComponent.new(name: 'Administrácia'), TW::SidebarMenuItemComponent.new(name: 'Používatelia', url: admin_tenant_users_path(Current.tenant), icon: Icons::UsersComponent.new), TW::SidebarMenuItemComponent.new(name: 'Prístup', url: admin_tenant_tag_groups_path(Current.tenant), icon: Icons::LockClosedComponent.new), diff --git a/app/models/everything_filter.rb b/app/models/everything_filter.rb new file mode 100644 index 000000000..e1522cf78 --- /dev/null +++ b/app/models/everything_filter.rb @@ -0,0 +1,34 @@ +# == Schema Information +# +# Table name: filters +# +# id :bigint not null, primary key +# icon :string +# name :string not null +# position :integer not null +# query :string +# type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# author_id :bigint +# tag_id :bigint +# tenant_id :bigint not null +# +class EverythingFilter < TagFilter + before_validation :set_everything_tag + after_create :move_to_top + + def self.model_name + Filter.model_name + end + + def query + nil + end + + private + + def set_everything_tag + self.tag = tenant.everything_tag + end +end diff --git a/app/models/filter.rb b/app/models/filter.rb index e64e2e8ef..5ccdf02a6 100644 --- a/app/models/filter.rb +++ b/app/models/filter.rb @@ -3,24 +3,35 @@ # Table name: filters # # id :bigint not null, primary key +# icon :string # name :string not null # position :integer not null -# query :string not null +# query :string +# type :string not null # created_at :datetime not null # updated_at :datetime not null -# author_id :bigint not null +# author_id :bigint +# tag_id :bigint # tenant_id :bigint not null # class Filter < ApplicationRecord include AuditableEvents + include Iconized - belongs_to :author, class_name: 'User' + belongs_to :author, class_name: 'User', optional: true belongs_to :tenant + belongs_to :tag, optional: true + has_many :user_filter_visibilities, inverse_of: :filter, dependent: :destroy - validates :tenant_id, :author_id, :name, :query, presence: true + validates :tenant_id, :name, presence: true before_create :fill_position + scope :visible_for, -> (user) { joins(:user_filter_visibilities) + .where(user_filter_visibilities: { visible: true, user: user}) } + + acts_as_list scope: :tenant_id + def fill_position return if position.present? diff --git a/app/models/fulltext_filter.rb b/app/models/fulltext_filter.rb new file mode 100644 index 000000000..b9589760a --- /dev/null +++ b/app/models/fulltext_filter.rb @@ -0,0 +1,23 @@ +# == Schema Information +# +# Table name: filters +# +# id :bigint not null, primary key +# icon :string +# name :string not null +# position :integer not null +# query :string +# type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# author_id :bigint +# tag_id :bigint +# tenant_id :bigint not null +# +class FulltextFilter < Filter + validates :query, presence: true + + def self.model_name + Filter.model_name + end +end diff --git a/app/models/searchable/query_builder.rb b/app/models/searchable/query_builder.rb new file mode 100644 index 000000000..a984f3ce6 --- /dev/null +++ b/app/models/searchable/query_builder.rb @@ -0,0 +1,24 @@ +class Searchable::QueryBuilder + attr_reader :filter, :filter_id, :query, :user + + def initialize(filter: nil, filter_id: nil, query: nil, user: nil) + @filter = filter + @filter_id = filter_id + @query = query + @user = user + end + + def build + if filter.nil? && filter_id.present? + @filter = FilterPolicy::ScopeShowable.new(user, Filter).resolve.find_by(id: filter_id) + end + + [ + filter&.query, + query, + ] + .compact + .map(&:strip) + .join(' ') + end +end \ No newline at end of file diff --git a/app/models/tag.rb b/app/models/tag.rb index 3fcad2b2d..fa2e5a161 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -30,6 +30,8 @@ class Tag < ApplicationRecord has_many :automation_actions, class_name: "Automation::Action", as: :action_object, dependent: :restrict_with_error has_many :message_objects_tags, dependent: :destroy has_many :message_objects, through: :message_objects_tags + has_many :filters, dependent: :destroy + has_many :user_filter_visibilities, through: :filters validates :name, presence: true validates :name, uniqueness: { scope: :tenant_id, case_sensitive: false } @@ -44,6 +46,20 @@ class Tag < ApplicationRecord scope :archived, -> { where(type: ArchivedTag.to_s) } after_update_commit ->(tag) { EventBus.publish(:tag_renamed, tag) if previous_changes.key?("name") } + after_update_commit ->(tag) do + tag.filters.each do |filter| + filter.user_filter_visibilities.update_all(visible: false) + end if previous_changes.key?("visible") && !tag.visible + end + + after_create ->(tag) do + TagFilter.create!( + tenant: tag.tenant, + author: tag.owner, + name: tag.name, + tag:, + ) + end def assign_to_message_object(message_object) message_object.assign_tag(self) diff --git a/app/models/tag_filter.rb b/app/models/tag_filter.rb new file mode 100644 index 000000000..5109feaa4 --- /dev/null +++ b/app/models/tag_filter.rb @@ -0,0 +1,31 @@ +# == Schema Information +# +# Table name: filters +# +# id :bigint not null, primary key +# icon :string +# name :string not null +# position :integer not null +# query :string +# type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# author_id :bigint +# tag_id :bigint +# tenant_id :bigint not null +# +class TagFilter < Filter + belongs_to :tag, optional: false + + def name + self[:name] || tag.name + end + + def query + "label:(#{tag.name})" + end + + def self.model_name + Filter.model_name + end +end diff --git a/app/models/tenant.rb b/app/models/tenant.rb index 1b33c4ddf..9320d2112 100644 --- a/app/models/tenant.rb +++ b/app/models/tenant.rb @@ -106,8 +106,8 @@ def create_default_objects create_admin_group!(name: "admins") create_signer_group!(name: "signers") - create_draft_tag!(name: "Rozpracované", visible: true) create_everything_tag!(name: "Všetky správy", visible: false) + create_draft_tag!(name: "Rozpracované", visible: true) create_archived_tag!(name: "Archivované", color: "green", icon: "archive-box", visible: true) create_signature_requested_tag!(name: "Na podpis", visible: true, color: "yellow", icon: "pencil") create_signed_tag!(name: "Podpísané", visible: true, color: "green", icon: "fingerprint") diff --git a/app/models/user.rb b/app/models/user.rb index da15962f7..5d76ab068 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,6 +13,7 @@ # tenant_id :bigint # class User < ApplicationRecord + include Pundit::Authorization include AuditableEvents belongs_to :tenant @@ -26,6 +27,7 @@ class User < ApplicationRecord has_many :filters, foreign_key: :author_id has_many :filter_subscriptions has_many :notifications + has_many :user_filter_visibilities, dependent: :destroy validates_presence_of :name, :email validates_uniqueness_of :name, :email, scope: :tenant_id, case_sensitive: false @@ -48,10 +50,10 @@ def user_group def accessible_tags Tag.where( TagGroup.select(1) - .joins(:group_memberships) - .where("tag_groups.tag_id = tags.id") - .where(group_memberships: { user_id: id }) - .arel.exists + .joins(:group_memberships) + .where("tag_groups.tag_id = tags.id") + .where(group_memberships: { user_id: id }) + .arel.exists ) end @@ -78,8 +80,36 @@ def update_notifications_retention end end + def visible_filters + build_filter_visibilities!.where(visible: true).map(&:filter) + end + + def build_filter_visibilities! + filters = policy_scope(Filter, policy_scope_class: FilterPolicy::ScopeShowable).order(:position) + visibilities = user_filter_visibilities.order(:position) + build_user_filter_visibilities(filters, visibilities) + end + + def pundit_user + self + end + private + def build_user_filter_visibilities(filters, visibilities) + user_filters_without_visibilities = filters.where.not(id: visibilities.pluck(:filter_id)) + new_visibilities = user_filters_without_visibilities.map.with_index do |filter, i| + last_position = visibilities.last&.position || 0 + visible = filter.is_a?(FulltextFilter) || filter.author_id.nil? + user_filter_visibilities.new(filter:, visible:, position: last_position + 1 + i) + end + + all_visibilities = visibilities.to_a + new_visibilities + all_visibilities.map(&:save!) + user_filter_visibilities.where(id: all_visibilities).includes(filter: :tag).order(:position) + end + + def delete_user_group user_group.destroy end diff --git a/app/models/user_filter_visibility.rb b/app/models/user_filter_visibility.rb new file mode 100644 index 000000000..d6519dce2 --- /dev/null +++ b/app/models/user_filter_visibility.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: user_filter_visibilities +# +# id :bigint not null, primary key +# position :integer +# visible :boolean default(TRUE), not null +# created_at :datetime not null +# updated_at :datetime not null +# filter_id :bigint not null +# user_id :bigint not null +# +class UserFilterVisibility < ApplicationRecord + belongs_to :user + belongs_to :filter + + acts_as_list scope: :user_id + + def hidden + !visible + end +end diff --git a/app/policies/filter_policy.rb b/app/policies/filter_policy.rb index ac38487e3..02312a42d 100644 --- a/app/policies/filter_policy.rb +++ b/app/policies/filter_policy.rb @@ -10,7 +10,7 @@ def initialize(user, filter) class ScopeEditable < Scope def resolve - scoped = scope.where(tenant_id: Current.tenant) + scoped = scope.where(tenant_id: Current.tenant).visible_for(@user) return scoped if @user.admin? @@ -20,7 +20,19 @@ def resolve class ScopeShowable < Scope def resolve - scope.where(tenant_id: Current.tenant) + scoped = scope.where(tenant_id: Current.tenant) + + return scoped if @user.admin? + + scoped.left_joins(:tag) + .where( + TagGroup + .select(1) + .joins(:group_memberships) + .where("tag_groups.tag_id = tags.id") + .where(group_memberships: { user_id: @user.id }) + .arel.exists) + .or(scoped.where(author_id: [nil, @user.id])) end end @@ -48,6 +60,10 @@ def destroy? @user.admin? || is_author_current_user? end + def sort? + is_author_current_user? + end + private def is_author_current_user? diff --git a/app/policies/user_filter_visibility_policy.rb b/app/policies/user_filter_visibility_policy.rb new file mode 100644 index 000000000..a3885a313 --- /dev/null +++ b/app/policies/user_filter_visibility_policy.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class UserFilterVisibilityPolicy < ApplicationPolicy + attr_reader :user, :user_filter_visibility + + def initialize(user, user_filter_visibility) + @user = user + @user_filter_visibility = user_filter_visibility + end + + class Scope < Scope + def resolve + scope.where(user: Current.user) + end + end + + def index? + true + end + + def destroy? + owner? + end + + def create? + true + end + + def update? + owner? + end + + def move_higher? + update? + end + + def move_lower? + update? + end + + private + + def owner? + @user_filter_visibility.user == Current.user + end +end diff --git a/app/views/message_threads/index.html.erb b/app/views/message_threads/index.html.erb index 7d9f1f4e0..4f8b5cc98 100644 --- a/app/views/message_threads/index.html.erb +++ b/app/views/message_threads/index.html.erb @@ -4,7 +4,9 @@ <% end %> <% end %> -<%= render MessageThreadsTableComponent.new(filter: @filter, filter_subscription: @filter_subscription) do |component| %> +<%= render Common::FlashComponent.new %> + +<%= render MessageThreadsTableComponent.new(filter: @filter, query: @query, filter_subscription: @filter_subscription) do |component| %> <% component.with_message_thread do %> <%= render MessageThreadsTableRowComponent.with_collection(@message_threads) %> <% end %> diff --git a/app/views/settings/user_filter_visibilities/index.html.erb b/app/views/settings/user_filter_visibilities/index.html.erb new file mode 100644 index 000000000..10e38a30b --- /dev/null +++ b/app/views/settings/user_filter_visibilities/index.html.erb @@ -0,0 +1 @@ +<%= render Settings::UserFilterVisibilities::ListComponent.new(@user_filter_visibilities) %> diff --git a/config/routes.rb b/config/routes.rb index 5e230a182..1dcdb4dd9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,6 +22,12 @@ end resources :tags resource :profile + resources :user_filter_visibilities do + member do + post :move_higher + post :move_lower + end + end end namespace :admin do @@ -142,6 +148,13 @@ end resources :filters do + member do + patch :pin + patch :unpin + end + + patch :sort, on: :collection + resources :filter_subscriptions end diff --git a/db/migrate/20231211132422_add_type_to_filters.rb b/db/migrate/20231211132422_add_type_to_filters.rb new file mode 100644 index 000000000..6d3a884e6 --- /dev/null +++ b/db/migrate/20231211132422_add_type_to_filters.rb @@ -0,0 +1,19 @@ +class AddTypeToFilters < ActiveRecord::Migration[7.1] + def up + add_column :filters, :icon, :string, null: true + add_column :filters, :type, :string, null: true + + Filter.where(type: nil).update_all(type: 'FulltextFilter') + + change_column_null :filters, :name, false + + add_index :filters, :type + end + + def down + Filter.where.not(type: 'FulltextFilter').delete_all + + remove_column :filters, :icon + remove_column :filters, :type + end +end diff --git a/db/migrate/20231211133725_add_tag_to_filters.rb b/db/migrate/20231211133725_add_tag_to_filters.rb new file mode 100644 index 000000000..30c564abe --- /dev/null +++ b/db/migrate/20231211133725_add_tag_to_filters.rb @@ -0,0 +1,5 @@ +class AddTagToFilters < ActiveRecord::Migration[7.1] + def change + add_reference :filters, :tag, null: true, foreign_key: true + end +end diff --git a/db/migrate/20231211134125_change_query_null_on_filters.rb b/db/migrate/20231211134125_change_query_null_on_filters.rb new file mode 100644 index 000000000..af7943c92 --- /dev/null +++ b/db/migrate/20231211134125_change_query_null_on_filters.rb @@ -0,0 +1,5 @@ +class ChangeQueryNullOnFilters < ActiveRecord::Migration[7.1] + def change + change_column_null :filters, :query, true + end +end diff --git a/db/migrate/20231211140731_add_is_pinned_to_filters.rb b/db/migrate/20231211140731_add_is_pinned_to_filters.rb new file mode 100644 index 000000000..73bf7e13d --- /dev/null +++ b/db/migrate/20231211140731_add_is_pinned_to_filters.rb @@ -0,0 +1,6 @@ +class AddIsPinnedToFilters < ActiveRecord::Migration[7.1] + def change + add_column :filters, :is_pinned, :boolean, null: false, default: false + add_index :filters, :is_pinned + end +end diff --git a/db/migrate/20231213100122_remove_index_filters_on_tenant_id_and_position.rb b/db/migrate/20231213100122_remove_index_filters_on_tenant_id_and_position.rb new file mode 100644 index 000000000..d28127e87 --- /dev/null +++ b/db/migrate/20231213100122_remove_index_filters_on_tenant_id_and_position.rb @@ -0,0 +1,5 @@ +class RemoveIndexFiltersOnTenantIdAndPosition < ActiveRecord::Migration[7.1] + def change + remove_index :filters, [:tenant_id, :position], unique: true + end +end diff --git a/db/migrate/20240103124622_user_hidden_items.rb b/db/migrate/20240103124622_user_hidden_items.rb new file mode 100644 index 000000000..04a4be064 --- /dev/null +++ b/db/migrate/20240103124622_user_hidden_items.rb @@ -0,0 +1,10 @@ +class UserHiddenItems < ActiveRecord::Migration[7.1] + def change + create_table :user_hidden_items do |t| + t.references :user, null: false, foreign_key: true + t.string :user_hideable_type, null: false + t.bigint :user_hideable_id + t.timestamps + end + end +end diff --git a/db/migrate/20240523122734_rename_user_hidden_item_to_user_item_visibility.rb b/db/migrate/20240523122734_rename_user_hidden_item_to_user_item_visibility.rb new file mode 100644 index 000000000..967807a6c --- /dev/null +++ b/db/migrate/20240523122734_rename_user_hidden_item_to_user_item_visibility.rb @@ -0,0 +1,5 @@ +class RenameUserHiddenItemToUserItemVisibility < ActiveRecord::Migration[7.1] + def change + rename_table :user_hidden_items, :user_item_visibilities + end +end diff --git a/db/migrate/20240523123256_rename_user_item_visibilities_user_hideable_to_user_item.rb b/db/migrate/20240523123256_rename_user_item_visibilities_user_hideable_to_user_item.rb new file mode 100644 index 000000000..035c6f24c --- /dev/null +++ b/db/migrate/20240523123256_rename_user_item_visibilities_user_hideable_to_user_item.rb @@ -0,0 +1,6 @@ +class RenameUserItemVisibilitiesUserHideableToUserItem < ActiveRecord::Migration[7.1] + def change + rename_column :user_item_visibilities, :user_hideable_type, :user_item_type + rename_column :user_item_visibilities, :user_hideable_id, :user_item_id + end +end diff --git a/db/migrate/20240523123432_add_visible_and_position_to_user_item_visibilities.rb b/db/migrate/20240523123432_add_visible_and_position_to_user_item_visibilities.rb new file mode 100644 index 000000000..5cfa349c5 --- /dev/null +++ b/db/migrate/20240523123432_add_visible_and_position_to_user_item_visibilities.rb @@ -0,0 +1,6 @@ +class AddVisibleAndPositionToUserItemVisibilities < ActiveRecord::Migration[7.1] + def change + add_column :user_item_visibilities, :visible, :boolean, null: false, default: true + add_column :user_item_visibilities, :position, :integer, null: true + end +end diff --git a/db/migrate/20240617090128_rename_user_item_visibilities_to_user_filter_visibilities.rb b/db/migrate/20240617090128_rename_user_item_visibilities_to_user_filter_visibilities.rb new file mode 100644 index 000000000..703544f46 --- /dev/null +++ b/db/migrate/20240617090128_rename_user_item_visibilities_to_user_filter_visibilities.rb @@ -0,0 +1,5 @@ +class RenameUserItemVisibilitiesToUserFilterVisibilities < ActiveRecord::Migration[7.1] + def change + rename_table :user_item_visibilities, :user_filter_visibilities + end +end diff --git a/db/migrate/20240617090456_rename_user_item_to_filter_on_user_filter_visibilities.rb b/db/migrate/20240617090456_rename_user_item_to_filter_on_user_filter_visibilities.rb new file mode 100644 index 000000000..081affd61 --- /dev/null +++ b/db/migrate/20240617090456_rename_user_item_to_filter_on_user_filter_visibilities.rb @@ -0,0 +1,28 @@ +class RenameUserItemToFilterOnUserFilterVisibilities < ActiveRecord::Migration[7.1] + def change + add_reference :user_filter_visibilities, :filter, foreign_key: true, null: true + + reversible do |dir| + dir.up do + UserFilterVisibility.reset_column_information + UserFilterVisibility.find_each do |visibility| + if visibility.user_item_type == 'Filter' + visibility.update!(filter_id: visibility.user_item_id) + else + visibility.destroy! + end + end + end + + dir.down do + UserFilterVisibility.reset_column_information + UserFilterVisibility.find_each do |visibility| + visibility.update!(user_item_id: visibility.filter_id, user_item_type: visibility.filter.class.name) + end + end + end + + change_column_null :user_filter_visibilities, :filter_id, false + remove_reference :user_filter_visibilities, :user_item, polymorphic: true, index: true + end +end diff --git a/db/migrate/20240619084104_change_type_null_on_filters.rb b/db/migrate/20240619084104_change_type_null_on_filters.rb new file mode 100644 index 000000000..f7f241c01 --- /dev/null +++ b/db/migrate/20240619084104_change_type_null_on_filters.rb @@ -0,0 +1,14 @@ +class ChangeTypeNullOnFilters < ActiveRecord::Migration[7.1] + def change + reversible do |dir| + dir.up do + Filter.find_each do |filter| + if filter.type.nil? + filter.update!(type: 'FulltextFilter') + end + end + end + end + change_column_null :filters, :type, false + end +end diff --git a/db/migrate/20240624114023_change_author_id_null_on_filters.rb b/db/migrate/20240624114023_change_author_id_null_on_filters.rb new file mode 100644 index 000000000..b9a89d10f --- /dev/null +++ b/db/migrate/20240624114023_change_author_id_null_on_filters.rb @@ -0,0 +1,5 @@ +class ChangeAuthorIdNullOnFilters < ActiveRecord::Migration[7.1] + def change + change_column_null :filters, :author_id, true + end +end diff --git a/db/migrate/20240624114024_create_tag_filters_from_tags.rb b/db/migrate/20240624114024_create_tag_filters_from_tags.rb new file mode 100644 index 000000000..e534fa493 --- /dev/null +++ b/db/migrate/20240624114024_create_tag_filters_from_tags.rb @@ -0,0 +1,30 @@ +class CreateTagFiltersFromTags < ActiveRecord::Migration[7.1] + def up + Tenant.find_each do |tenant| + tenant.tags.visible.find_each do |tag| + TagFilter.create!( + tenant:, + author: tag.owner, + name: tag.name, + tag:, + ) + end + + tenant.everything_tag.tap do |tag| + EverythingFilter.create!( + tenant:, + author: tag.owner, + name: tag.name, + tag:, + ).tap do |filter| + filter.move_to_top + end + end + end + end + + def down + TagFilter.destroy_all + EverythingFilter.destroy_all + end +end diff --git a/db/migrate/20240814102352_add_unique_index_on_uuid_message_thread_id_to_messages.rb b/db/migrate/20240814102352_add_unique_index_on_uuid_message_thread_id_to_messages.rb index 95e651bf0..f1a19578f 100644 --- a/db/migrate/20240814102352_add_unique_index_on_uuid_message_thread_id_to_messages.rb +++ b/db/migrate/20240814102352_add_unique_index_on_uuid_message_thread_id_to_messages.rb @@ -2,5 +2,9 @@ class AddUniqueIndexOnUuidMessageThreadIdToMessages < ActiveRecord::Migration[7. def up add_index :messages, [:uuid, :message_thread_id], unique: true end + + def down + remove_index :messages, [:uuid, :message_thread_id], unique: true + end end diff --git a/db/migrate/20240925134557_remove_is_pinned_from_filters.rb b/db/migrate/20240925134557_remove_is_pinned_from_filters.rb new file mode 100644 index 000000000..87b1976e5 --- /dev/null +++ b/db/migrate/20240925134557_remove_is_pinned_from_filters.rb @@ -0,0 +1,10 @@ +class RemoveIsPinnedFromFilters < ActiveRecord::Migration[7.1] + def up + remove_column :filters, :is_pinned + end + + def down + add_column :filters, :is_pinned, :boolean, null: false, default: false + add_index :filters, :is_pinned + end +end diff --git a/db/schema.rb b/db/schema.rb index ba2de7a6b..411dbbbe2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -192,15 +192,19 @@ create_table "filters", force: :cascade do |t| t.bigint "tenant_id", null: false - t.bigint "author_id", null: false + t.bigint "author_id" t.string "name", null: false - t.string "query", null: false + t.string "query" t.integer "position", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "icon" + t.string "type", null: false + t.bigint "tag_id" t.index ["author_id"], name: "index_filters_on_author_id" - t.index ["tenant_id", "position"], name: "index_filters_on_tenant_id_and_position", unique: true + t.index ["tag_id"], name: "index_filters_on_tag_id" t.index ["tenant_id"], name: "index_filters_on_tenant_id" + t.index ["type"], name: "index_filters_on_type" end create_table "folders", force: :cascade do |t| @@ -616,6 +620,17 @@ t.datetime "updated_at", null: false end + create_table "user_filter_visibilities", force: :cascade do |t| + t.bigint "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "visible", default: true, null: false + t.integer "position" + t.bigint "filter_id", null: false + t.index ["filter_id"], name: "index_user_filter_visibilities_on_filter_id" + t.index ["user_id"], name: "index_user_filter_visibilities_on_user_id" + end + create_table "users", force: :cascade do |t| t.bigint "tenant_id" t.string "email", null: false @@ -646,6 +661,7 @@ add_foreign_key "filter_subscriptions", "filters" add_foreign_key "filter_subscriptions", "tenants" add_foreign_key "filter_subscriptions", "users" + add_foreign_key "filters", "tags" add_foreign_key "filters", "tenants", on_delete: :cascade add_foreign_key "filters", "users", column: "author_id", on_delete: :cascade add_foreign_key "folders", "boxes" @@ -686,5 +702,7 @@ add_foreign_key "tags", "tenants" add_foreign_key "tags", "users", column: "owner_id" add_foreign_key "upvs_form_related_documents", "upvs_forms" + add_foreign_key "user_filter_visibilities", "filters" + add_foreign_key "user_filter_visibilities", "users" add_foreign_key "users", "tenants" end diff --git a/package.json b/package.json index 529308ac5..e561bd7fe 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@hotwired/stimulus": "^3.2.1", "@hotwired/turbo-rails": "^7.0.0", "@rails/request.js": "^0.0.9", + "@shopify/draggable": "^1.1.3", "esbuild": "^0.19.8", "puppeteer": "^23.6.0", "tailwindcss-stimulus-components": "^4.0.4" diff --git a/test/fixtures/filters.yml b/test/fixtures/filters.yml index df801de55..56ac41f19 100644 --- a/test/fixtures/filters.yml +++ b/test/fixtures/filters.yml @@ -1,22 +1,70 @@ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html -one: +ssd_all: + tenant: ssd + name: Everything + position: 1 + type: 'EverythingFilter' + tag: ssd_everything + +ssd_general: tenant: ssd author: basic name: With General text query: general - position: 1 + position: 2 + type: 'FulltextFilter' -two: +ssd_legal: tenant: ssd - author: admin + author: basic name: With Legal text query: Legal - position: 2 + position: 3 + type: 'FulltextFilter' + +ssd_without_author: + tenant: ssd + name: Other + position: 4 + tag: ssd_other + type: 'TagFilter' + +ssd_hidden: + tenant: ssd + author: basic + name: Hidden + position: 5 + tag: ssd_construction + type: 'TagFilter' + +ssd_external: + tenant: ssd + author: basic + name: External + position: 6 + tag: ssd_external + type: 'TagFilter' + +ssd_general_two: + tenant: ssd + author: ssd_signer + name: With General text + query: general + position: 6 + type: 'FulltextFilter' + +solver_all: + tenant: solver + name: Everything + position: 1 + type: 'EverythingFilter' + tag: solver_everything -solver_one: +solver_urgent: tenant: solver author: solver_other name: Urgent query: urgent - position: 3 + position: 4 + type: 'FulltextFilter' diff --git a/test/helpers/auth_helper.rb b/test/helpers/auth_helper.rb index 657e576a3..644b47837 100644 --- a/test/helpers/auth_helper.rb +++ b/test/helpers/auth_helper.rb @@ -6,7 +6,13 @@ def sign_in_as(user_fixture_name) click_on "Prihlásiť cez Google" - assert_text "Správy v schránke" + filter = Filter.where(tenant_id: users(user_fixture_name).tenant).order(:position).first + + if filter + assert_text filter.name + else + assert_text "Správy v schránke" + end end def mock_omni_auth_with_user(user_fixture_name) diff --git a/test/models/filter_subscription_test.rb b/test/models/filter_subscription_test.rb index 134d1df36..adb896a1d 100644 --- a/test/models/filter_subscription_test.rb +++ b/test/models/filter_subscription_test.rb @@ -3,7 +3,7 @@ class FilterSubscriptionTest < ActiveSupport::TestCase test "new thread matching subscription fires event" do user = users(:admin) - filter = filters(:one) + filter = filters(:ssd_general) s = FilterSubscription.create!(tenant: user.tenant, user: user, filter: filter, events: ["Notifications::NewMessageThread", "Notifications::NewMessage"]) m = Govbox::Message.create_message_with_thread!(govbox_messages(:one)) diff --git a/test/system/filters_test.rb b/test/system/filters_test.rb index 1166cfb5b..c0d562dad 100644 --- a/test/system/filters_test.rb +++ b/test/system/filters_test.rb @@ -3,7 +3,7 @@ class FiltersTest < ApplicationSystemTestCase setup do Searchable::MessageThread.reindex_all - sign_in_as(:basic) + sign_in_as(:ssd_signer) end test "user can create a filter" do @@ -41,7 +41,7 @@ class FiltersTest < ApplicationSystemTestCase assert_text "General" accept_alert do - click_link "Zmazať filter" + click_button "Zmazať filter" end assert_no_text "General" diff --git a/test/system/message_threads_test.rb b/test/system/message_threads_test.rb index 81ea09313..0297294c0 100644 --- a/test/system/message_threads_test.rb +++ b/test/system/message_threads_test.rb @@ -32,7 +32,6 @@ class MessageThreadsTest < ApplicationSystemTestCase within_tags do assert_text "Finance" - assert_text "Legal" assert_text "Other" end end @@ -42,31 +41,24 @@ class MessageThreadsTest < ApplicationSystemTestCase assert_text "SD Services" within_tags do + assert_text "Archivovane" assert_text "Finance" end end within_sidebar do within_filters do + assert_text "Everything" assert_text "With General text" assert_text "With Legal text" + assert_text "Other" # other tenant refute_text "Urgent" - end - - within_tags do - assert_text "Finance" - assert_text "Legal" - assert_text "ExtVisible" - assert_text "Print" # non visible refute_text "Hidden" refute_text "External" - - # other tenant - refute_text "Special" end end end @@ -99,7 +91,7 @@ class MessageThreadsTest < ApplicationSystemTestCase assert_no_selector "#next_page_area" within_sidebar do - within_tags do + within_filters do click_link "Legal" end end diff --git a/test/system/notifications_test.rb b/test/system/notifications_test.rb index cf2dca43c..8deda7473 100644 --- a/test/system/notifications_test.rb +++ b/test/system/notifications_test.rb @@ -10,8 +10,8 @@ class NotificationsTest < ApplicationSystemTestCase visit message_threads_path click_link "With General text" - - click_link "Nastaviť notifikácie" + assert_text "General agenda SSD" + click_link "Nastaviť notifikácie" if page.has_link?("Nastaviť notifikácie") check "Nová konverzácia" check "Nová správa" diff --git a/yarn.lock b/yarn.lock index 4144e92d6..c7b230a16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -235,6 +235,11 @@ resolved "https://registry.yarnpkg.com/@rails/request.js/-/request.js-0.0.9.tgz#89e2a575405dc07eb8a9b3d2fe04289e1f057cd0" integrity sha512-VleYUyrA3rwKMvYnz7MI9Ada85Vekjb/WVz7NuGgDO24Y3Zy9FFSpDMQW+ea/tlftD+CdX/W/sUosRA9/HkDOQ== +"@shopify/draggable@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@shopify/draggable/-/draggable-1.1.3.tgz#4c1d0415629198ec11d27b776dda0fc89eb5efa2" + integrity sha512-okZG6FTOX1HpfToN2uFVXTicum+NtHFGEY+0MFkZsdChP5qvLSlkffQormMxoDQVIDsK+jso3yz03tteIJ/A0w== + "@tailwindcss/aspect-ratio@^0.4.2": version "0.4.2" resolved "https://registry.yarnpkg.com/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz#9ffd52fee8e3c8b20623ff0dcb29e5c21fb0a9ba"