Skip to content

Commit

Permalink
Project Attachments: Index, Table Component, Upload (#755)
Browse files Browse the repository at this point in the history
* add controller, index view, and attachments table component

* begin upload functionality

* fix turbo_stream response for upload modal, add broadcast to attachment, fix sort for puid column

* chore: fix sortlink for ransack_alias

* add sort to attachments metadata and byte size

* add breadcrump for project files

* make filename column also sticky

* start search bar for files

* fix table, fix filter

* add sticky table footer

* add fr translations

* add tests, fix some tests

* remove search for this iteration

* cleanup table component of unnecessary params

* remove members-tabs selector now that tab id is moved

* revert ransack cahnges, add separate sorting for filename and bytesize

* cleanup table component

* fix test

* run normalize

* add additional permissions test

* cleanup

* change back file name

* comment out select_attachments ability until we need multi-select

* change limit component to reconstruct url

* remove turbo frame tag

* fix existing tests

* fix tests after rebase

* cleanup limit component

* remove accidental code

* comment out destroy action until backend is ready

* fix test with new attachment fixture

* add layout fix call to be false on index

---------

Co-authored-by: Eric Enns <[email protected]>
  • Loading branch information
ChrisHuynh333 and ericenns authored Sep 17, 2024
1 parent 4227254 commit e02746c
Show file tree
Hide file tree
Showing 34 changed files with 1,009 additions and 85 deletions.
16 changes: 5 additions & 11 deletions app/assets/stylesheets/application.tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ body {

.form-field select,
.form-field select option {
@apply border-slate-300 text-slate-900 sm:text-sm rounded-md focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-slate-800 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500;
@apply border-slate-300 text-slate-900 sm:text-sm rounded-md focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-slate-800 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500;
}

.form-field select:has(option[value=""]:checked) {
Expand Down Expand Up @@ -313,9 +313,7 @@ body {
}

/*GROUPS LIST TREE*/
.namespace-tree-container
> .namespace-list-tree
> .namespace-entry.has-children:first-child {
.namespace-tree-container > .namespace-list-tree > .namespace-entry.has-children:first-child {
border-top: 0;
}

Expand Down Expand Up @@ -369,9 +367,7 @@ body {
@apply border-t-2 border-slate-200 dark:border-slate-600;
}

.namespace-list-tree
.namespace-list-tree
.namespace-entry:last-child::before {
.namespace-list-tree .namespace-list-tree .namespace-entry:last-child::before {
height: auto;
top: 30px;
bottom: 0;
Expand All @@ -394,9 +390,7 @@ body {
@apply border-t border-slate-200 dark:border-slate-600;
}

.namespace-list-tree
.namespace-entry.has-children
> .namespace-entry-contents:hover {
.namespace-list-tree .namespace-entry.has-children > .namespace-entry-contents:hover {
@apply cursor-pointer border-slate-50 dark:border-slate-600 bg-slate-50 dark:bg-slate-600;
}

Expand Down Expand Up @@ -500,7 +494,7 @@ div.field_with_errors > :is(select) {
display: none;
}

.samples-app {
.fixed-table-component {
height: calc(100vh - 50px - 32px);
max-height: calc(100vh - 50px - 32px);
display: flex;
Expand Down
209 changes: 209 additions & 0 deletions app/components/attachments/table_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<%= render Viral::BaseComponent.new(**wrapper_arguments) do %>
<%= render Viral::BaseComponent.new(**system_arguments) do %>
<table
class='
w-full text-sm text-left rtl:text-right text-slate-500 dark:text-slate-400
whitespace-nowrap
'
>
<thead
class='
text-xs uppercase text-slate-700 bg-slate-50 dark:bg-slate-700
dark:text-slate-300 sticky top-0 z-10
'
>
<tr>
<% @columns.each_with_index do |column, index| %>
<%= render_cell(
tag: 'th',
scope: 'col',
classes: class_names('px-3 py-3', 'sticky left-0 min-w-56 max-w-56 z-10 bg-slate-50 dark:bg-slate-700': index.zero?, 'sticky left-56 z-10 bg-slate-50 dark:bg-slate-700': index == 1)
) do %>
<% if index.zero? and @abilities[:select_attachments] %>
<%= check_box_tag "select-page",
title: t(:".select_page"),
"aria-label": t(:".select_page"),
data: {
action: "input->selection#togglePage",
controller: "filters",
selection_target: "selectPage",
},
class:
"w-4 h-4 mr-2.5 text-primary-600 bg-slate-100 border-slate-300 rounded focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-slate-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600" %>
<% end %>
<% if column == :id %>
<%= render Ransack::SortComponent.new(
ransack_obj: @q,
label: t(".#{column}"),
url: helpers.sorting_url(@q, "puid"),
field: "puid",
data: {
turbo_action: "replace",
},
) %>
<% elsif column == :byte_size || column == :filename %>
<%= render Ransack::SortComponent.new(
ransack_obj: @q,
label: t(".#{column}"),
url: helpers.sorting_url(@q, "file_blob_#{column}"),
field: "file_blob_#{column}",
data: {
turbo_action: "replace",
},
) %>
<% elsif column == :format || column == :type %>
<%= render Ransack::SortComponent.new(
ransack_obj: @q,
label: column,
url:
helpers.sorting_url(@q, URI.encode_www_form_component("metadata_#{column}")),
field: "metadata_#{column}",
data: {
turbo_action: "replace",
},
) %>
<% else %>
<%= render Ransack::SortComponent.new(
ransack_obj: @q,
label: t(".#{column}"),
url: helpers.sorting_url(@q, column),
field: column,
data: {
turbo_action: "replace",
},
) %>
<% end %>
<% end %>
<% end %>
<% if @renders_row_actions %>
<%= render_cell(
tag: 'th',
scope: 'col',
classes: class_names('px-3 py-3 bg-slate-50 dark:bg-slate-700 sticky right-0')
) do %>
<%= t(".actions") %>
<% end %>
<% end %>
</tr>
</thead>
<tbody
class='
overflow-y-auto bg-white divide-y divide-slate-200 dark:bg-slate-800
dark:divide-slate-700
'
>
<% @attachments.each do |attachment| %>
<%= render Viral::BaseComponent.new(**row_arguments(attachment)) do %>
<% @columns.each_with_index do |column, index| %>
<%= render_cell(
tag: index.zero? ? 'th' :'td',
scope: index.zero? ? 'row' : nil,
classes: class_names('px-3 py-3', 'sticky left-0 min-w-56 max-w-56 bg-slate-50 dark:bg-slate-700': index.zero?, 'sticky left-56 bg-slate-50 dark:bg-slate-700': index == 1)
) do %>
<% if index.zero? && @abilities[:select_attachments] %>
<%= check_box_tag "attachment_ids[]",
attachment.id,
nil,
id: dom_id(attachment),
"aria-label": attachment.file.filename.to_s,
data: {
action: "input->selection#toggle",
selection_target: "rowSelection",
},
class:
"w-4 h-4 mr-2.5 text-primary-600 bg-slate-100 border-slate-300 rounded focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-slate-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600" %>
<% end %>
<% if column == :id %>
<%= attachment.puid %>
<% elsif column == :filename %>
<div class="flex items-center">
<%= viral_icon(name: :document_text, color: :subdued, classes: "h-6 w-6 ml-0 mr-2") %>
<span>
<%= link_to attachment.file.filename,
rails_blob_path(attachment.file),
data: {
turbo: false,
},
class: "text-slate-900 dark:text-slate-100 font-semibold hover:underline" %>
</span>
</div>
<% elsif column == :format || column == :type %>
<%= viral_pill(
text: attachment.metadata[column.to_s],
color: helpers.find_pill_color_for_attachment(attachment, column.to_s),
) %>
<% elsif column == :byte_size %>
<%= number_to_human_size(attachment.file.blob.byte_size) %>
<% elsif column == :created_at%>
<%= helpers.local_time_ago(attachment.created_at) %>
<% end %>
<% end %>
<% end %>
<% if @renders_row_actions %>
<%= render_cell(
tag: 'td',
classes: class_names('px-3 py-3 sticky right-0 bg-white dark:bg-slate-800 z-5 space-x-2')
) do %>
<% if @row_actions[:destroy] %>
<%= link_to(
t('.remove'),
namespace_project_attachment_path(
id: attachment.id
),
data: {
turbo_stream: true,
turbo_method: :delete,
turbo_confirm: t('.delete_confirm', name: attachment.file.filename.to_s),
},
class:
"font-medium text-blue-600 underline dark:text-blue-500 hover:no-underline cursor-pointer",
) %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
</tbody>
<% if @abilities[:select_attachments] && !@attachments.empty? %>
<tfoot class="sticky bottom-0 z-10">
<tr class="border-t dark:border-slate-700 border-slate-200">
<td
class="
sticky left-0 z-10 px-6 py-3 bg-slate-50 dark:bg-slate-900
"
colspan="3"
>
<span>
<%= t(".counts.attachments") %>:
<strong data-action="turbo:morph-element->selection#idempotentConnect"><%= @pagy.count %></strong>
</span>
<span>
<%= t(".counts.selected") %>:
<strong
data-selection-target="selected"
data-action="
turbo:morph-element->selection#idempotentConnect
"
>0</strong>
</span>
</td>
<td colspan="100%" class="px-3 py-3 bg-slate-50 dark:bg-slate-900"></td>
</tr>
</tfoot>
<% end %>
</table>
<% end %>
<% unless @attachments.empty? %>
<div class="flex items-center justify-between">
<%= render Viral::Pagy::LimitComponent.new(@pagy, item: t(".limit.item")) %>
<%= render Viral::Pagy::PaginationComponent.new(@pagy) %>
</div>
<% end %>
<div class="empty_state_message">
<%= viral_empty(
title: @empty[:title],
description: @empty[:description],
icon_name: :document_text,
) %>
</div>
<% end %>
72 changes: 72 additions & 0 deletions app/components/attachments/table_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

require 'ransack/helpers/form_helper'

module Attachments
# Component for rendering a table of Attachments
class TableComponent < Component
include Ransack::Helpers::FormHelper

# rubocop:disable Naming/MethodParameterName,Metrics/ParameterLists
def initialize(
attachments,
pagy,
q,
project,
row_actions: false,
abilities: {},
empty: {},
**system_arguments
)
@attachments = attachments
@pagy = pagy
@q = q
@project = project
@abilities = abilities
@row_actions = row_actions
@empty = empty
@renders_row_actions = @row_actions.select { |_key, value| value }.count.positive?
@system_arguments = system_arguments

@columns = columns
end
# rubocop:enable Naming/MethodParameterName,Metrics/ParameterLists

def system_arguments
{ tag: 'div' }.deep_merge(@system_arguments).tap do |args|
args[:id] = 'attachments-table'
args[:classes] = class_names(args[:classes], 'overflow-auto scrollbar')
if @abilities[:select_attachments]
args[:data] ||= {}
args[:data][:controller] = 'selection'
args[:data][:'selection-total-value'] = @pagy.count
args[:data][:'selection-action-link-outlet'] = '.action-link'
end
end
end

def wrapper_arguments
{
tag: 'div',
classes: class_names('table-container flex flex-col shrink min-h-0')
}
end

def row_arguments(attachment)
{ tag: 'tr' }.tap do |args|
args[:classes] = class_names('bg-white', 'border-b', 'dark:bg-slate-800', 'dark:border-slate-700')
args[:id] = attachment.id
end
end

def render_cell(**arguments, &)
render(Viral::BaseComponent.new(**arguments), &)
end

private

def columns
%i[id filename format type byte_size created_at]
end
end
end
2 changes: 1 addition & 1 deletion app/components/viral/pagy/limit_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<span><%= t(".items", items: @item) %></span>
<%= viral_dropdown(label: @pagy.limit) do |dropdown| %>
<% Pagy::DEFAULT[:limits].each do |limit| %>
<% dropdown.with_item(label: limit, url: "?limit=#{limit}") %>
<% dropdown.with_item(label: limit, url: current_url_with_limit(limit)) %>
<% end %>
<% end %>
<span><%== t(".summary", to: @pagy.to, from: @pagy.from, count: @pagy.count) %></span>
Expand Down
15 changes: 15 additions & 0 deletions app/components/viral/pagy/limit_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ def initialize(pagy, item:)
@pagy = pagy
@item = item
end

def current_url_with_limit(limit)
current_url = request.original_url
if current_url.include? '?'
split_url = current_url.split('?')
base_url = split_url[0]
reconstructed_params = "?limit=#{limit}"
split_url[1].split('&').each do |param|
reconstructed_params << "&#{param}" unless param.include? 'limit='
end
"#{base_url}#{reconstructed_params}"
else
"#{current_url}?limit=#{limit}"
end
end
end
end
end
3 changes: 2 additions & 1 deletion app/components/viral/tabs_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<div
id="<%= id %>"
class="
flex text-sm font-medium text-center text-slate-500 border-b border-slate-200
dark:text-slate-400 dark:border-slate-700
Expand All @@ -22,6 +23,6 @@
</div>
<% end %>
</div>
<div role="region" id="<%= id %>" aria-live="polite">
<div role="region" aria-live="polite">
<%= tab_content %>
</div>
Loading

0 comments on commit e02746c

Please sign in to comment.