Skip to content

Commit

Permalink
Merge pull request #874 from ElixirTeSS/collection-improvements
Browse files Browse the repository at this point in the history
Collection improvements
  • Loading branch information
fbacall committed Aug 1, 2023
2 parents e5d940e + 26973b8 commit 3ad2cd4
Show file tree
Hide file tree
Showing 18 changed files with 6,046 additions and 106 deletions.
3 changes: 3 additions & 0 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
//= require ardc_vocab_widget_v2
//= require select2
//= require ahoy
//= require sortable
//= require_tree ./templates
//= require_tree .
//= require_self
Expand Down Expand Up @@ -214,6 +215,8 @@ document.addEventListener("turbolinks:load", function() {

Tracking.init();

Collections.init();

$('.tess-expandable').each(function () {
var limit = this.dataset.heightLimit || 300;

Expand Down
112 changes: 61 additions & 51 deletions app/assets/javascripts/autocompleters.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,65 +50,75 @@ var Autocompleters = {

init: function () {
$("[data-role='autocompleter-group']").each(function () {
var element = this;
var existingValues = JSON.parse($(element).find('[data-role="autocompleter-existing"]').html()) || [];
var listElement = $(element).find('[data-role="autocompleter-list"]');
var inputElement = $(element).find('[data-role="autocompleter-input"]');
var url = $(element).data("url");
var prefix = $(element).data("prefix");
var labelField = $(element).data("labelField") || "title";
var idField = $(element).data("idField") || "id";
var singleton = $(element).data("singleton") || false;
var groupBy = $(element).data("groupBy") || false;
var templateName = $(element).data("template") ||
(singleton ? "autocompleter/singleton_resource" : "autocompleter/resource");
var transformFunction = Autocompleters.transformFunctions[$(element).data("transformFunction") || "default"];
Autocompleters.initGroup(this);
});
},

// Render the existing associations on page load
if (!listElement.children("li").length) {
for (var i = 0; i < existingValues.length; i++) {
listElement.append(HandlebarsTemplates[templateName](existingValues[i]));
}
initGroup: function (element, opts) {
var existingValues = JSON.parse($(element).find('[data-role="autocompleter-existing"]').html()) || [];
var listElement = $(element).find('[data-role="autocompleter-list"]');
var inputElement = $(element).find('[data-role="autocompleter-input"]');
var defaults = {
url: $(element).data("url"),
prefix: $(element).data("prefix"),
labelField: $(element).data("labelField") || "title",
idField: $(element).data("idField") || "id",
singleton: $(element).data("singleton") || false,
groupBy: $(element).data("groupBy") || false,
templateName: $(element).data("template"),
transformFunction: Autocompleters.transformFunctions[$(element).data("transformFunction") || "default"]
}
opts = Object.assign({}, defaults, opts);

if (singleton && existingValues.length) {
inputElement.hide();
}
opts.templateName = opts.templateName || (opts.singleton ? "autocompleter/singleton_resource" :
"autocompleter/resource");

// Render the existing associations on page load
if (!listElement.children("li").length) {
for (var i = 0; i < existingValues.length; i++) {
listElement.append(HandlebarsTemplates[opts.templateName](existingValues[i]));
}

inputElement.autocomplete({
serviceUrl: url,
dataType: "json",
deferRequestBy: 300, // Wait 300ms before submitting to stop search being flooded
paramName: "q",
groupBy: groupBy,
formatResult: Autocompleters.formatResultWithHint,
transformResult: function(response) {
return transformFunction(response, { labelField: labelField, idField: idField });
},
onSelect: function (suggestion) {
// Don't add duplicates
var id = suggestion.data.id;
if (!$("[data-id='" + id + "']", listElement).length) {
var obj = { item: suggestion.data.item };
if (prefix) {
obj.prefix = prefix;
}
if (opts.singleton && existingValues.length) {
inputElement.hide();
}
}

listElement.append(HandlebarsTemplates[templateName](obj));
if (singleton) {
inputElement.hide();
}
inputElement.autocomplete({
serviceUrl: opts.url,
dataType: "json",
deferRequestBy: 300, // Wait 300ms before submitting to stop search being flooded
paramName: "q",
groupBy: opts.groupBy,
formatResult: Autocompleters.formatResultWithHint,
transformResult: function(response) {
return opts.transformFunction(response, opts);
},
onSelect: function (suggestion) {
// Don't add duplicates
var id = suggestion.data.id;
if (!$("[data-id='" + id + "']", listElement).length) {
var obj = { item: suggestion.data.item };
if (opts.prefix) {
obj.prefix = opts.prefix;
}

$(this).val('').focus();
},
onSearchStart: function (query) {
inputElement.addClass("loading");
},
onSearchComplete: function () {
inputElement.removeClass("loading");
listElement.append(HandlebarsTemplates[opts.templateName](obj));
if (opts.singleton) {
inputElement.hide();
}
}
});

$(this).val('').focus();
const event = new CustomEvent('autocompleters:added', { bubbles: true, detail: { object: obj } });
listElement[0].dispatchEvent(event);
},
onSearchStart: function (query) {
inputElement.addClass("loading");
},
onSearchComplete: function () {
inputElement.removeClass("loading");
}
});
}
}
77 changes: 77 additions & 0 deletions app/assets/javascripts/collections.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
var Collections = {
init: function () {
$("[data-role='collection-items-group']").each(function () {
// Set up drag/drop
var list = $('.collection-items', $(this))[0];
const collectionItems = new Sortable.default(list, {
draggable: 'li.collection-item',
handle: '.collection-item-handle'
});

collectionItems.on('drag:stopped', function (e) {
// Re-compute orders after dropping.
Collections.recalculateOrder(e.data.sourceContainer);
});

// Set up autocompleter
var origTransform = Autocompleters.transformFunctions[$(this).data("transformFunction") || "default"];
Autocompleters.initGroup(this, {
resourceType: $(this).data("resourceType"),
transformFunction: function (response, config) {
var result = origTransform(response, config);
result.suggestions.forEach(function (sugg) {
sugg.data.item.resource_type = config.resourceType;
sugg.data.item.resource_id = sugg.data.item.id;
sugg.data.item.id = null;
sugg.data.id = sugg.data.item.resource_type + '-' + sugg.data.item.resource_id
});
return result;
}
});


$(this).on('autocompleters:added', function () {
// Re-compute orders after new item added.
Collections.recalculateOrder(this);
});

$(this).on('click', '[data-role="delete-collection-item"]', function (e) {
// If the collection item yet saved, just delete from the DOM,
// otherwise, apply visible styling to show it will be deleted, which can be
// undone if the button is clicked again
var item = $(this).closest('.collection-item');
var checkbox = $('input', $(this));
if (!checkbox.length) { // No checkbox is rendered if item is not persisted yet.
var list = $(this).closest('ul')[0];
// Re-compute orders after item removed.
item.remove();
Collections.recalculateOrder(list);
} else {
if (checkbox.is(':checked')) {
checkbox.prop('checked', false);
$('input[type=text]', item).prop('disabled', false);
item.removeClass('pending-delete');
} else {
checkbox.prop('checked', true);
$('input[type=text]', item).prop('disabled', true);
item.addClass('pending-delete');
}
}

return false;
});

// Calculate initial order - order from database may have gaps if items were deleted.
Collections.recalculateOrder(list);
});
},

recalculateOrder: function (container) {
var order = 1;
container.querySelectorAll('li.collection-item').forEach(function (li) {
li.querySelector('[data-role="item-order"]').value = order;
li.querySelector('.item-order-label').innerText = order;
order++;
});
}
}
24 changes: 24 additions & 0 deletions app/assets/javascripts/templates/autocompleter/collection_item.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<li data-id="{{ item.resource_type }}-{{ item.resource_id }}" class="collection-item">
<div class="collection-item-handle">
<i class="fa fa-sort" aria-hidden="true"></i>
<span class="item-order-label">{{ item.order }}</span>
<input type="hidden" name="{{ prefix }}[][order]" value="{{ item.order }}" data-role="item-order" />
</div>

<div class="collection-item-title">
<input type="hidden" name="{{ prefix }}[][id]" value="{{ item.id }}" />
<input type="hidden" name="{{ prefix }}[][resource_id]" value="{{ item.resource_id }}" />
<input type="hidden" name="{{ prefix }}[][resource_type]" value="{{ item.resource_type }}" />
<a href="{{ item.url }}" target="_blank">{{ item.title }}</a>
<input type="text" class="form-control input-sm" name="{{ prefix }}[][comment]" placeholder="Enter a comment" value="{{ item.comment }}">
</div>

<div>
<a href="#" class="btn btn-icon" data-role="delete-collection-item">
<i class="icon cross-icon icon-sm icon-greyscale"></i>
{{#if item.id}}
<input type="checkbox" name="{{ prefix }}[][_destroy]" style="display: none;" autocomplete="off">
{{/if}}
</a>
</div>
</li>
52 changes: 52 additions & 0 deletions app/assets/stylesheets/collection.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,55 @@
cursor: pointer;
}
}

.collection-items {
list-style: none;
padding-left: 0;

.collection-item {
display: flex;
align-items: center;
margin-bottom: 10px;
padding: 10px;
border-radius: $border-radius-base;

.collection-item-title {
flex-grow: 1;
flex-shrink: 1;
}

.collection-item-handle {
color: $text-muted;
padding: 0 15px;
user-select: none;
flex-shrink: 0;
align-self: stretch;
display: flex;
align-items: center;
gap: 5px;
}

&.pending-delete {
background-color: $alert-danger-bg;
text-decoration: line-through;
}
}
}

.collection-item-order-badge {
position: absolute;
left: -0.5em;
top: -0.5em;
border-radius: 50%;
width: 2em;
line-height: 2em;
height: 2em;
text-align: center;
background: $badge-bg;
color: $badge-color;
}

.collection-item-comment {
margin: 10px 0 0 0;
font-style: italic;
}
2 changes: 1 addition & 1 deletion app/assets/stylesheets/masonry.scss
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
margin-bottom: 0;
}

&.with-left-icon {
.with-left-icon {
display: flex;

.icon-container {
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/collections_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ def set_collection
# Never trust parameters from the scary internet, only allow the white list through.
def collection_params
params.require(:collection).permit(:title, :description, :image, :image_url, :public, { keywords: [] },
{ material_ids: [] }, { event_ids: [] })
{ material_ids: [] }, { event_ids: [] },
{ items_attributes: [:id, :resource_type, :resource_id, :order, :comment, :_destroy] })
end

# Filter collection items based on a type
Expand Down
8 changes: 8 additions & 0 deletions app/helpers/collections_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,12 @@ def item_fields(item_class)
[]
end
end

def item_order_badge(collection_item)
content_tag(:div, collection_item.order, class: 'collection-item-order-badge')
end

def item_comment(collection_item)
content_tag(:blockquote, collection_item.comment, class: 'collection-item-comment')
end
end
16 changes: 16 additions & 0 deletions app/models/collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ class Collection < ApplicationRecord
include Collaboratable

has_many :items, -> { order(:order) }, class_name: 'CollectionItem', inverse_of: :collection, dependent: :destroy
has_many :material_items, -> { where(resource_type: 'Material').order(:order) }, class_name: 'CollectionItem',
inverse_of: :collection
has_many :event_items, -> { where(resource_type: 'Event').order(:order) }, class_name: 'CollectionItem',
inverse_of: :collection
has_many :events, through: :items, source: :resource, source_type: 'Event', inverse_of: :collections
has_many :materials, through: :items, source: :resource, source_type: 'Material', inverse_of: :collections

Expand All @@ -17,6 +21,9 @@ class Collection < ApplicationRecord
# e.g. "James Bond " => "James Bond"
auto_strip_attributes :title, :description, :image_url, squish: false

accepts_nested_attributes_for :items, allow_destroy: true

after_validation :normalize_order
after_commit :index_items, if: :title_previously_changed?

validates :title, presence: true
Expand Down Expand Up @@ -95,4 +102,13 @@ def index_items

items.each(&:reindex_resource)
end

# Make sure order for each type goes from 1 to n with no gaps.
def normalize_order
indexes = Hash.new(0)
items.sort_by(&:order).each do |item|
next if item.marked_for_destruction?
item.order = indexes[item.resource_type] += 1
end
end
end
2 changes: 2 additions & 0 deletions app/views/collection_items/_collection_item.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<% resource = collection_item.resource %>
<%= render partial: resource, locals: { collection_item: collection_item } %>
Loading

0 comments on commit 3ad2cd4

Please sign in to comment.