Skip to content

Commit

Permalink
[STRY0011998] Linelist Export: UI (#664)
Browse files Browse the repository at this point in the history
* chore: Working on setting up dropdown

* chore: Working dropdown for old data export

* start adding linelist ui

* Change sortable_list component to render single list for future flexibility

* Add add all and remove all buttons to modal

* add disabled submit, samples into collapse

* Add disable to linelist export button, add linelist export btn to group samples

* refactor sortable list component to use has_many, fix export description dialog to more universal

* fix current broken tests

* start adding data export ui tests

* add test previews for sortable_lists component

* add fr translations, cleanup

* more cleanup

* change styling of list elements in sortable lists

* more styling changes to list component

* fix styling for component preview

* change format param to linelist_format in anticipation of other future format params

* fix rubucop warning

* Add enable and disable states for add and remove all buttons in dialog, fix sortable scrolling

* fix rebase

* cleanup

* remove hover from list_component, remove forceFallback from controller

* attempt to fix flaky test

* fix other flaky tests

* try using only tbody as selector for flaky tests

* change selector in groups group links as well

* fix tests in data-exports-tests

* remove unnecessary unless conditional and fix disabled statement

---------

Co-authored-by: Chris Huynh <[email protected]>
  • Loading branch information
joshsadam and ChrisHuynh333 authored Aug 6, 2024
1 parent b601bbb commit 6d52583
Show file tree
Hide file tree
Showing 33 changed files with 1,005 additions and 235 deletions.
18 changes: 14 additions & 4 deletions app/components/viral/dropdown_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ class DropdownComponent < Viral::Component

# rubocop:disable Metrics/ParameterLists
def initialize(label: nil, tooltip: '', icon: nil, caret: false, trigger: TRIGGER_DEFAULT, skidding: 0,
distance: 10, dropdown_styles: '', **system_arguments)
distance: 10, dropdown_styles: '', action_link: false, action_link_value: nil, **system_arguments)
@distance = distance
@dropdown_styles = dropdown_styles
@label = label
@icon_name = icon
@caret = caret
@skidding = skidding
@action_link = action_link
@action_link_value = action_link_value
@trigger = TRIGGER_MAPPINGS[trigger]

@system_arguments = default_system_arguments(system_arguments)
Expand All @@ -32,11 +34,19 @@ def initialize(label: nil, tooltip: '', icon: nil, caret: false, trigger: TRIGGE
# rubocop:enable Metrics/ParameterLists

def default_system_arguments(args)
data = { 'viral--dropdown-target': 'trigger' }

if @action_link
data = data.merge({
action: 'turbo:morph-element->action-link#idempotentConnect',
turbo_stream: true,
controller: 'action-link',
action_link_required_value: @action_link_value
})
end
args.merge({
id: "dd-#{SecureRandom.hex(10)}",
data: {
'viral--dropdown-target': 'trigger'
},
data:,
tag: :button
})
end
Expand Down
23 changes: 23 additions & 0 deletions app/components/viral/sortable_list/list_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div class="<%= @system_arguments[:container_classes] %>">
<%= title %>
<ul
id="<%= id %>"
data-controller="sortable-list"
data-sortable-list-group-name-value="<%= group %>"
class="<%= @system_arguments[:list_classes] %>"
>
<% list_items.each do |list_item| %>
<li
id="<%= list_item %>"
class="
block
w-full
px-4
py-2
border-b
border-slate-200
cursor-move
dark:border-slate-600"><%= list_item %></li>
<% end %>
</ul>
</div>
25 changes: 25 additions & 0 deletions app/components/viral/sortable_list/list_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Viral
module SortableList
# This component creates the individual lists for the sortable_lists_component.
class ListComponent < Viral::Component
attr_reader :id, :group, :title, :list_items

# If creating multiple lists to utilize the same list values, assign them the same group
def initialize(id: nil, group: nil, title: nil, list_items: [], **system_arguments)
@id = id
@group = group
@title = title
@list_items = list_items
@system_arguments = system_arguments
@system_arguments[:list_classes] =
class_names(system_arguments[:list_classes],
'border border-slate-300 rounded-md block
dark:bg-slate-800 dark:border-slate-600 max-h-[225px] min-h-[225px]')
@system_arguments[:container_classes] =
class_names(system_arguments[:container_classes], 'text-slate-900 dark:text-white')
end
end
end
end
13 changes: 13 additions & 0 deletions app/components/viral/sortable_lists_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div>
<div class="flex justify-between block text-sm font-medium text-slate-900 dark:text-white">
<%= title %>
</div>
<div class="text-sm font-medium text-base leading-relaxed text-slate-500 dark:text-slate-400">
<%= description %>
</div>
</div>
<div class="flex">
<% lists.each do |list| %>
<%= list %>
<% end %>
</div>
15 changes: 15 additions & 0 deletions app/components/viral/sortable_lists_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Viral
# This component creates the sortable_lists.
class SortableListsComponent < Viral::Component
attr_reader :title, :description

renders_many :lists, Viral::SortableList::ListComponent

def initialize(title: nil, description: nil)
@title = title
@description = description
end
end
end
12 changes: 10 additions & 2 deletions app/controllers/data_exports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class DataExportsController < ApplicationController # rubocop:disable Metrics/Cl

before_action :data_export, only: %i[destroy show]
before_action :data_exports, only: %i[index destroy]
before_action :namespace, only: :new
before_action :current_page
before_action :set_default_tab, only: :show

Expand All @@ -17,7 +18,7 @@ def index; end
def show
authorize! @data_export, to: :read_export?

return if @data_export.manifest.empty?
return if @data_export.manifest.empty? || @data_export.export_type == 'linelist'

@manifest = JSON.parse(@data_export.manifest)
end
Expand Down Expand Up @@ -93,7 +94,8 @@ def destroy # rubocop:disable Metrics/MethodLength, Metrics/AbcSize

def data_export_params
params.require(:data_export).permit(:name, :export_type, :email_notification,
export_parameters: [:format, :namespace_id, { ids: [], metadata_fields: [] }])
export_parameters: [:linelist_format, :namespace_id,
{ ids: [], metadata_fields: [] }])
end

def data_export
Expand All @@ -104,6 +106,12 @@ def data_exports
@data_exports = DataExport.where(user: current_user)
end

def namespace
return unless params[:export_type] == 'linelist'

@namespace = Namespace.find(params[:namespace_id])
end

def current_page
@current_page = t(:'general.default_sidebar.data_exports')
end
Expand Down
1 change: 1 addition & 0 deletions app/helpers/view_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module ViewHelper
prefixed_text_input: 'Viral::Form::Prefixed::TextInputComponent',
pill: 'Viral::PillComponent',
select: 'Viral::Form::SelectComponent',
sortable_lists: 'Viral::SortableListsComponent',
tabs: 'Viral::TabsComponent',
text_input: 'Viral::Form::TextInputComponent',
time_ago: 'Viral::TimeAgoComponent',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Controller } from "@hotwired/stimulus";
import { createHiddenInput } from "utilities/form";

export default class extends Controller {

static targets = ["field", "submitBtn", "addAll", "removeAll"];

static values = {
selectedList: {
type: String,
},
availableList: {
type: String,
}
};

#disabledClasses = ["pointer-events-none", "cursor-not-allowed", "text-slate-300", "dark:text-slate-700"];
#enabledClasses = ["underline", "hover:no-underline"]

connect() {
this.availableList = document.getElementById(this.availableListValue)
this.selectedList = document.getElementById(this.selectedListValue)
this.fullListItems = this.availableList.querySelectorAll("li")
this.selectedList.addEventListener("mouseover", () => { this.#checkButtonStates() })
this.availableList.addEventListener("mouseover", () => { this.#checkButtonStates() })
}

addAll() {
for (const item of this.fullListItems) {
this.selectedList.append(item)
}
this.#checkButtonStates()
}

removeAll() {
for (const item of this.fullListItems) {
this.availableList.append(item)
}
this.#checkButtonStates()
}

#checkButtonStates() {
const selected_metadata = this.selectedList.querySelectorAll("li")
const available_metadata = this.availableList.querySelectorAll("li")
if (selected_metadata.length == 0) {
this.#setSubmitButtonDisableState(true)
this.#setAddOrRemoveButtonDisableState(this.removeAllTarget, true)
this.#setAddOrRemoveButtonDisableState(this.addAllTarget, false)
} else if (available_metadata.length == 0) {
this.#setSubmitButtonDisableState(false)
this.#setAddOrRemoveButtonDisableState(this.removeAllTarget, false)
this.#setAddOrRemoveButtonDisableState(this.addAllTarget, true)
} else {
this.#setSubmitButtonDisableState(false)
this.#setAddOrRemoveButtonDisableState(this.removeAllTarget, false)
this.#setAddOrRemoveButtonDisableState(this.addAllTarget, false)
}
}

#setSubmitButtonDisableState(disableState) {
this.submitBtnTarget.disabled = !(!disableState && this.selectedList.querySelectorAll("li").length > 0);
}

#setAddOrRemoveButtonDisableState(button, disableState) {
if (disableState && !button.classList.contains("pointer-events-none")) {
button.classList.remove(...this.#enabledClasses)
button.classList.add(...this.#disabledClasses)
button.setAttribute("aria-disabled", "true")
} else if (!disableState && button.classList.contains("pointer-events-none")) {
button.classList.remove(...this.#disabledClasses)
button.classList.add(...this.#enabledClasses)
button.removeAttribute("aria-disabled")
}
}

constructMetadataParams() {
const metadata_fields = this.selectedList.querySelectorAll("li")

for (const metadata_field of metadata_fields) {
this.fieldTarget.appendChild(
createHiddenInput(
`data_export[export_parameters][metadata_fields][]`,
metadata_field.innerText
)
);
}
}
}
26 changes: 26 additions & 0 deletions app/javascript/controllers/sortable_list_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Controller } from "@hotwired/stimulus"
import { Sortable } from "sortablejs";

export default class extends Controller {
static values = {
groupName: String,
scrollSensitivity: {
type: Number,
default: 50,
},
scrollSpeed: {
type: Number,
default: 8,
}
}
connect() {
this.sortable = new Sortable(this.element, {
scroll: true,
scrollSensitivity: this.scrollSensitivityValue, // The number of px from div edge where scroll begins
scrollSpeed: this.scrollSpeedValue,
bubbleScroll: true,
group: this.groupNameValue,
animation: 100
})
}
}
4 changes: 2 additions & 2 deletions app/jobs/data_exports/create_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def create_export(data_export)

def attach_export(data_export, export)
filename = if data_export.export_type == 'linelist'
"#{data_export.id}.#{data_export.export_parameters['format']}"
"#{data_export.id}.#{data_export.export_parameters['linelist_format']}"
else
"#{data_export.id}.zip"
end
Expand Down Expand Up @@ -199,7 +199,7 @@ def create_linelist_spreadsheet(data_export)
else
Sample.where(id: data_export.export_parameters['ids'])
end
if data_export.export_parameters['format'] == 'csv'
if data_export.export_parameters['linelist_format'] == 'csv'
write_csv_export(data_export, samples, namespace_type)
else
write_xlsx_export(data_export, samples, namespace_type)
Expand Down
4 changes: 2 additions & 2 deletions app/models/data_export.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ def validate_linelist_export_parameters
end

def validate_linelist_format
if export_parameters.key?('format')
return if %w[xlsx csv].include?(export_parameters['format'])
if export_parameters.key?('linelist_format')
return if %w[xlsx csv].include?(export_parameters['linelist_format'])

errors.add(:export_parameters,
I18n.t('activerecord.errors.models.data_export.attributes.export_parameters.invalid_file_format'))
Expand Down
Loading

0 comments on commit 6d52583

Please sign in to comment.