Skip to content

Commit

Permalink
[ENHC0010032] List samples within transfer dialog (#508)
Browse files Browse the repository at this point in the history
* starting to list samples in dialog

* creating itemsPerPage constant

* changing pagination to 100 items per page

* removing list from group samples controller & route

* playing with the idea of adding a loading skeleton like with lazy loading

* refactoring

* more refactoring

* stop making server requests if we don't need to

* adding tests

* adding a controller test

* removing unused has_next param

* adding translation

* displaying sample puid

* playing with spinner for long transfers

* setting a variable

* fixing the escape listener on finished transfer

* refactoring createHiddenInput

* changing height of scrollable list

* replacing dialog opacity with background blur

* replacing svg with viral_icon

* missing french translations
  • Loading branch information
ksierks authored Jun 13, 2024
1 parent 2296fae commit de3320f
Show file tree
Hide file tree
Showing 17 changed files with 264 additions and 82 deletions.
11 changes: 11 additions & 0 deletions app/controllers/projects/samples_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,17 @@ def select
end
end

def list
@page = params[:page].to_i
@samples = Sample.where(id: params[:sample_ids])

respond_to do |format|
format.turbo_stream do
render status: :ok
end
end
end

private

def sample
Expand Down
66 changes: 66 additions & 0 deletions app/javascript/controllers/infinite_scroll_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Controller } from "@hotwired/stimulus";
import { createHiddenInput } from '../utilities/form';

export default class extends Controller {
static outlets = ["selection"];
static targets = ["all", "pageForm", "pageFormContent", "scrollable"];
static values = {
fieldName: String,
pagedFieldName: String
}

#page = 1;

connect() {
this.allIds = this.selectionOutlet.getStoredSamples();
this.#makePagedHiddenInputs();
this.#makeAllHiddenInputs();
}

scroll() {
if (
this.scrollableTarget.scrollHeight - this.scrollableTarget.scrollTop <=
this.scrollableTarget.clientHeight + 1
) {
this.#makePagedHiddenInputs();
}
}

#makePagedHiddenInputs() {
const itemsPerPage = 100;
const start = (this.#page - 1) * itemsPerPage;
const end = this.#page * itemsPerPage;
const ids = this.allIds.slice(start, end);

if(ids && ids.length){
const fragment = document.createDocumentFragment();
for (const id of ids) {
fragment.appendChild(
createHiddenInput(this.pagedFieldNameValue, id),
);
}
fragment.appendChild(
createHiddenInput("page", this.#page),
);
fragment.appendChild(
createHiddenInput("format", "turbo_stream"),
);
this.pageFormContentTarget.innerHTML = "";
this.pageFormContentTarget.appendChild(fragment);
this.#page++;
this.pageFormTarget.requestSubmit();
}
}

#makeAllHiddenInputs() {
const fragment = document.createDocumentFragment();
for (const id of this.allIds) {
fragment.appendChild(createHiddenInput(this.fieldNameValue, id));
}
this.allTarget.appendChild(fragment);
}

clear(){
this.selectionOutlet.clear();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Controller } from "@hotwired/stimulus";
import { createHiddenInput } from '../../../../utilities/form';

//creates hidden fields within a form for selected files
export default class extends Controller {
Expand Down Expand Up @@ -28,15 +29,13 @@ export default class extends Controller {

if (value instanceof Array) {
for (let arrayValue of value) {
this.#addHiddenInput(
`${this.fieldNameValue}[${storageValueIndex}][]`,
arrayValue
this.fieldTarget.appendChild(
createHiddenInput(`${this.fieldNameValue}[${storageValueIndex}][]`, arrayValue)
);
}
} else {
this.#addHiddenInput(
`${this.fieldNameValue}[${storageValueIndex}]`,
value
this.fieldTarget.appendChild(
createHiddenInput(`${this.fieldNameValue}[${storageValueIndex}]`, value)
);
}
}
Expand All @@ -48,14 +47,4 @@ export default class extends Controller {
sessionStorage.removeItem(this.storageKeyValue);
}
}

#addHiddenInput(name, value) {
const element = document.createElement("input");
element.type = "hidden";
element.id = value;
element.name = name;
element.value = value;
element.ariaHidden = "true";
this.fieldTarget.appendChild(element);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Controller } from "@hotwired/stimulus";
import { createHiddenInput } from '../../../../utilities/form';

export default class extends Controller {
static targets = ["field"];
Expand All @@ -16,11 +17,9 @@ export default class extends Controller {
);
if (storageValues) {
for (const storageValue of storageValues) {
const element = document.createElement("input");
element.type = "hidden";
element.name = `sample[metadata][${storageValue}]`;
element.value = '';
this.fieldTarget.appendChild(element);
this.fieldTarget.appendChild(
createHiddenInput(`sample[metadata][${storageValue}]`, '')
);
}
}
}
Expand Down
14 changes: 7 additions & 7 deletions app/javascript/controllers/selection_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default class extends Controller {

this.element.setAttribute("data-controller-connected", "true");

const storageValue = this.#getStoredSamples();
const storageValue = this.getStoredSamples();

if (storageValue) {
this.#updateUI(storageValue);
Expand All @@ -36,7 +36,7 @@ export default class extends Controller {
}

actionLinkOutletConnected(outlet) {
const storageValue = this.#getStoredSamples();
const storageValue = this.getStoredSamples();
outlet.setDisabled(storageValue.length);
}

Expand Down Expand Up @@ -67,8 +67,12 @@ export default class extends Controller {
return this.#total;
}

getStoredSamples() {
return JSON.parse(sessionStorage.getItem(this.#storageKey)) || [];
}

#addOrRemove(add, storageValue) {
const newStorageValue = this.#getStoredSamples();
const newStorageValue = this.getStoredSamples();

if (add) {
newStorageValue.push(storageValue);
Expand All @@ -94,10 +98,6 @@ export default class extends Controller {
this.#updatedCounts(ids.length);
}

#getStoredSamples() {
return JSON.parse(sessionStorage.getItem(this.#storageKey)) || [];
}

#updateActionLinks(count) {
this.actionLinkOutlets.forEach((outlet) => {
outlet.setDisabled(count);
Expand Down
12 changes: 2 additions & 10 deletions app/javascript/controllers/sessionstorage_amend_form_controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Controller } from "@hotwired/stimulus";
import _ from "lodash";
import { createHiddenInput } from '../utilities/form';

export default class extends Controller {
static targets = ["field"];
Expand All @@ -20,7 +20,7 @@ export default class extends Controller {
if (storageValues) {
const fragment = document.createDocumentFragment();
storageValues.forEach((value) => {
fragment.appendChild(this.#createHiddenInput(value));
fragment.appendChild(createHiddenInput(this.fieldNameValue, value));
});
this.fieldTarget.appendChild(fragment);
}
Expand All @@ -29,12 +29,4 @@ export default class extends Controller {
clear() {
sessionStorage.removeItem(this.storageKeyValue);
}

#createHiddenInput(value) {
const element = document.createElement("input");
element.type = "hidden";
element.name = this.fieldNameValue;
element.value = value;
return element;
}
}
24 changes: 24 additions & 0 deletions app/javascript/controllers/spinner_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Controller } from "@hotwired/stimulus";

function preventEscapeListener(event) {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
}
}

export default class extends Controller {
static targets = ['submit'];

submitStart() {
document.addEventListener("keydown", preventEscapeListener, true);
document.querySelector(".dialog--close").classList.add("hidden");
document.querySelector("#spinner").classList.remove("hidden");
}

submitEnd() {
document.removeEventListener("keydown", preventEscapeListener, true);
document.querySelector(".dialog--close").classList.remove("hidden");
document.querySelector("#spinner").classList.add("hidden");
}
}
8 changes: 8 additions & 0 deletions app/javascript/utilities/form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function createHiddenInput(name, value) {
const element = document.createElement("input");
element.type = "hidden";
element.name = name;
element.value = value;
element.ariaHidden = "true";
return element;
}
4 changes: 4 additions & 0 deletions app/views/projects/samples/_list_sample.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div class="py-1">
<div class="font-semibold text-gray-900 dark:text-white"><%= sample.puid %></div>
<div class="font-normal text-gray-500 dark:text-gray-400"><%= sample.name %></div>
</div>
11 changes: 11 additions & 0 deletions app/views/projects/samples/list.turbo_stream.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<% if @page == 1 %>
<%= turbo_stream.replace "list_select_samples" do %>
<%= turbo_frame_tag "list_select_samples" %>
<% end %>
<% end %>

<%= turbo_stream.append("list_select_samples") do %>
<% @samples.each do |sample| %>
<%= render "list_sample", sample: sample %>
<% end %>
<% end %>
91 changes: 70 additions & 21 deletions app/views/projects/samples/transfers/_dialog.html.erb
Original file line number Diff line number Diff line change
@@ -1,29 +1,78 @@
<%= viral_dialog(open: open) do |dialog| %>
<%= dialog.with_header(title: t(".title")) %>
<%= dialog.with_section do %>
<%= turbo_frame_tag "transfer_samples_dialog_content" do %>
<%= form_for(:transfer, url: namespace_project_samples_transfer_path, method: :post) do |form| %>
<div
data-controller="sessionstorage-amend-form"
data-sessionstorage-amend-form-storage-key-value=<%=namespace_project_samples_url%>
data-sessionstorage-amend-form-target="field"
data-sessionstorage-amend-form-field-name-value="transfer[sample_ids][]"
/>
<div class="grid gap-4">
<div class="form-field">
<%= form.label :new_project_id, t(".new_project_id") %>
<%= form.collection_select(:new_project_id, @projects, :id, :full_path) %>
<div
data-controller="infinite-scroll"
data-infinite-scroll-selection-outlet='#samples-table'
data-infinite-scroll-field-name-value="transfer[sample_ids][]"
data-infinite-scroll-paged-field-name-value="sample_ids[]"
>
<%= form_with(
url: list_namespace_project_samples_path,
data: { "infinite-scroll-target": "pageForm" }
) do %>
<div data-infinite-scroll-target="pageFormContent"></div>
<% end %>
<div
class="overflow-y-auto max-h-[300px] mb-4"
data-action="scroll->infinite-scroll#scroll"
data-infinite-scroll-target="scrollable"
>
<%= turbo_frame_tag "list_select_samples" do %>
<div role="status" class="max-w-sm animate-pulse">
<% 4.times do %>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[330px] mb-2.5"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[300px] mb-2.5"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5"></div>
<% end %>
<span class="sr-only"><%= t(:".loading") %></span>
</div>
<div>
<%= form.submit t(".submit_button"),
class: "button button--size-default button--state-primary",
disabled: @projects.count.zero?,
data: {
action: "click->sessionstorage-amend-form#clear"
} %>
<% end %>
</div>

<%= turbo_frame_tag "transfer_samples_dialog_content" do %>
<%= form_for(:transfer, url: namespace_project_samples_transfer_path, method: :post,
data: {
controller: "spinner",
action:"turbo:submit-start->spinner#submitStart turbo:submit-end->spinner#submitEnd"
}
) do |form| %>
<div class="grid gap-4">
<div class="form-field">
<%= form.label :new_project_id, t(".new_project_id") %>
<%= form.collection_select(:new_project_id, @projects, :id, :full_path) %>
</div>
<div class="hidden" data-infinite-scroll-target="all"></div>
<div>
<%= form.submit t(".submit_button"),
class: "button button--size-default button--state-primary",
disabled: @projects.count.zero?,
data: {
action: "click->infinite-scroll#clear",
"spinner-target": "submit",
"turbo-submits-with": t(:".loading"),
} %>
</div>
</div>
</div>
<% end %>
<% end %>
<% end %>
</div>

<div
role="status"
id="spinner"
class="
hidden backdrop-blur-sm absolute h-full w-full -translate-x-1/2 -translate-y-1/2
top-2/4 left-1/2 grid place-items-center
"
>
<div class="grid place-items-center">
<%= viral_icon(name: :loading, classes: "animate-spin text-primary-500") %>
<span class="text-black dark:text-white"><%= t(:".spinner") %>.</span>
</div>
</div>

<% end %>
<% end %>
3 changes: 2 additions & 1 deletion app/views/projects/samples/transfers/create.turbo_stream.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
<% end %>
<%= turbo_stream.update "samples_dialog", partial: "dialog", locals: { open: false } %>
<% else %>
<%= turbo_stream.remove "list_select_samples" %>
<%= turbo_stream.replace "transfer_samples_dialog_content",
partial: "projects/samples/shared/errors",
locals: {
type: type,
message: message,
errors: errors
errors: errors,
} %>
<% end %>

Expand Down
2 changes: 2 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,8 @@ en:
title: Transfer Samples
new_project_id: Project to transfer samples to
submit_button: Submit
loading: Loading...
spinner: Transferring samples, this might take a while...
create:
success: Samples were successfully transferred.
error: "The following list of samples failed to transfer:"
Expand Down
Loading

0 comments on commit de3320f

Please sign in to comment.