diff --git a/Gemfile.lock b/Gemfile.lock index 77efa06d0d..f3dfc2d9a5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -235,6 +235,8 @@ GEM netrc (0.11.0) newrelic_rpm (9.2.2) nio4r (2.5.9) + nokogiri (1.14.3-x86_64-darwin) + racc (~> 1.4) nokogiri (1.14.3-x86_64-linux) racc (~> 1.4) oj (3.14.3) @@ -418,6 +420,7 @@ GEM zeitwerk (2.6.8) PLATFORMS + x86_64-darwin-21 x86_64-linux DEPENDENCIES diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index f73139ead6..0d0fe33cb7 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -26,7 +26,6 @@ //= require bp_property_tree //= require concepts //= require home -//= require fair_score //= require_tree ./helpers //= require_tree ./components //= require ontologies diff --git a/app/assets/javascripts/bp_admin.js b/app/assets/javascripts/bp_admin.js index 63ac8ae9e0..7e29b80be9 100644 --- a/app/assets/javascripts/bp_admin.js +++ b/app/assets/javascripts/bp_admin.js @@ -865,6 +865,220 @@ jQuery(".admin.index").ready(function() { }); // end: BUTTON onclick actions ----------------------------------- + + //============================================================== + // GROUPS MANAGEMENT + //============================================================== + displayGroups({}); + + // allow selecting of rows, except on link clicks + jQuery('#adminGroups tbody').on('click', 'tr', function(event) { + if (event.target.tagName.toLowerCase() != 'a') { + jQuery(this).toggleClass('selected'); + } + }); + + jQuery("div.groups_nav").html(` + + + New + + + Apply to Selected Rows:     + +      + + Go + + `); + + jQuery('#group_admin_action_submit').on('click', function(event) { + var action = jQuery('#group_admin_action').val(); + + if (!action) { + alertify.alert("Please choose an action to perform on the selected groups."); + return; + } + + switch(action) { + case "delete": + DeleteGroups.act(); + break; + } + }); + + jQuery(document).on("reveal.facebox", function (event) { + jQuery("#facebox form[data-collection=groups]").validate({ + errorClass: "groupFormError", + errorElement: "div", + rules: { + "group[name]": "required", + "group[acronym]": "required", + }, + messages: { + "group[name]": "Please enter a name", + "group[acronym]": "Please enter an acronym", + }, + }); + + }); + + jQuery('#group_new_action').on('click', function (event) { + jQuery.facebox({ + ajax: "/admin/groups/new?time=" + new Date().getTime() + }); + }); + + jQuery('#adminGroups').on('click', 'a.edit-group', function(event) { + jQuery.facebox({ + ajax: "/admin/groups/" + encodeURIComponent(event.target.dataset.groupName) + "/edit?time=" + new Date().getTime() + }); + }); + + + //============================================================== + // CATEGORIES MANAGEMENT + //============================================================== + displayCategories({}); + + // allow selecting of rows, except on link clicks + jQuery('#adminCategories tbody').on('click', 'tr', function(event) { + if (event.target.tagName.toLowerCase() != 'a') { + jQuery(this).toggleClass('selected'); + } + }); + + jQuery("div.categories_nav").html(` + + + New + + + Apply to Selected Rows:     + +      + + Go + + `); + + jQuery('#category_admin_action_submit').on('click', function(event) { + var action = jQuery('#category_admin_action').val(); + + if (!action) { + alertify.alert("Please choose an action to perform on the selected categories."); + return; + } + + switch(action) { + case "delete": + DeleteCategories.act(); + break; + } + }); + + jQuery(document).on("reveal.facebox", function (event) { + jQuery("#facebox form[data-collection=categories]").validate({ + errorClass: "categoryFormError", + errorElement: "div", + rules: { + "category[name]": "required", + "category[acronym]": "required", + }, + messages: { + "category[name]": "Please enter a name", + "category[acronym]": "Please enter an acronym", + }, + }); + + }); + + jQuery('#category_new_action').on('click', function (event) { + jQuery.facebox({ + ajax: "/admin/categories/new?time=" + new Date().getTime() + }); + }); + + jQuery('#adminCategories').on('click', 'a.edit-category', function(event) { + jQuery.facebox({ + ajax: "/admin/categories/" + encodeURIComponent(event.target.dataset.categoryName) + "/edit?time=" + new Date().getTime() + }); + }); + + //============================================================== + // MANAGEMENT COMMONS + //============================================================== + + jQuery(document).on("click", "#facebox a.dismiss-dialog", function (event) { + jQuery(document).trigger('close.facebox'); + }); + + jQuery(document).on('ajax:success', "#facebox form.admin-collection-form", (event, response, status, xhr) => { + jQuery(document).trigger('close.facebox'); + if (response && response.success) { + _showStatusMessages([response.success], [], [], false); + } + refreshCollection(event.target.dataset.collection); + }); + jQuery(document).on('ajax:error', "#facebox form.admin-collection-form", (event, xhr, status, error) => { + if (xhr.responseJSON) { + displayDialogErrorMessages(xhr.responseJSON) + } else { + displayDialogErrorMessages(status); + } + }); + + function refreshCollection(collectionName) { + switch (collectionName) { + case "groups": + displayGroups({}); + break; + case "categories": + displayCategories({}); + break; + default: + alertify.alert("Unable to refresh unknown collection '" + collectionName + "'"); + } + } + + function displayDialogErrorMessages(data, settings) { + settings ||= {} + + let append = settings.append || false; + + let errorListNode = jQuery("#facebox .alert-box ul"); + + if (!append) { + errorListNode.empty(); + } + + let messages = []; + if (typeof data == "string" || data instanceof String) { + messages.push(data) + } + if (typeof data == "object" && data.errors) { + messages.push.apply(messages, Object.values(data.errors)); + } + if (typeof data == "object" && data.status && data.status / 200 != 1) { + messages.push("Request error: " + data.statusText); + } + + for (let msg of messages) { + errorListNode.append(jQuery("
  • ").text(msg)) + } + + if (messages.length == 0) { + errorListNode.parents(".alert-box").hide(); + } else { + errorListNode.parents(".alert-box").show(); + } + } }); @@ -1040,4 +1254,338 @@ DeleteUsers.prototype.ajaxCall = function (username){ } DeleteUsers.act = function(user) { new DeleteUsers(user).ajaxCall(user); -}; \ No newline at end of file +}; + +/* groups part */ +function displayGroups(data, group) { + let ontTable = null; + let allRows + if (jQuery.fn.dataTable.isDataTable('#adminGroups')) { + ontTable = jQuery('#adminGroups').DataTable().ajax.reload(); + } else { + ontTable = jQuery("#adminGroups").DataTable({ + "ajax": { + "url": "/admin/groups", + "contentType": "application/json", + "dataSrc": function (json) { + return populateGroupRows(json); + } + }, + "rowCallback": function(row, data, index) { + var acronym = jQuery('td:nth-child(4)', row).text(); + + jQuery(row).attr("id", "tr_" + acronym); + }, + "initComplete": function(settings, json) { + }, + "columnDefs": [ + { + "targets": 0, + "searchable": true, + "title": "Name", + }, + { + "targets": 1, + "searchable": true, + "title": "Description", + }, + { + "targets": 2, + "searchable": true, + "title": "Created", + }, + { + "targets": 3, + "searchable": true, + "title": "Id", + }, + { + "targets": 4, + "searchable": false, + "orderable": false, + "title": "Count", + }, + { + "targets": 5, + "searchable": false, + "orderable": false, + "title": "Actions", + "width": "200px" + } + ], + "autoWidth": false, + "lengthChange": false, + "searching": true, + "language": { + "search": "Filter: ", + "emptyTable": "No groups available" + }, + "info": true, + "paging": true, + "pageLength": 100, + "ordering": true, + "responsive": true, + "dom": '<"groups_nav"><"top"fi>rtip', + "stripeClasses": ["", "alt"], + }); + } + return ontTable; +} + +function populateGroupRows(data) { + let groups = data['groups']; + let allRows = groups.map(group => { + let name = group['name']; + let description = group['description'] + let created = group['created']; + let id = group['acronym']; + let nb = [ + ' ' + group['ontologies'].length + ' ', + ]; + let actions = [ + 'Edit', + ] + return [name, description, created, id , nb, actions.join('|')]; + }) + + return allRows; +} + +function DeleteGroups() { +} + +DeleteGroups.act = function(groupName) { + let group2delete = jQuery("#adminGroups tr.selected td:nth-child(4)").map(function(index, value) { return value.textContent.trim();}).toArray(); + let confirmMsg = "You are about to delete the following groups:
    " + group2delete.join(",") + "

    Should I proceed?"; + alertify.confirm(confirmMsg, (e) => { + if (e) { + _clearStatusMessages(); + let success = []; + let errors = []; + let notices = []; + let errorState = false; + let deferredObj = jQuery.Deferred(); + let initialDeferredObj = deferredObj; + + for (let group of group2delete) { + fun = () => { return jQuery.ajax("/admin/groups/" + encodeURIComponent(group), { + method: "DELETE", + dataType: "json", + success: function(data, msg) { + var reg = /\s*,\s*/g; + + if (data.errors) { + errorState = true; + errors.push.apply(errors, data.errors); + } + + if (data.success) { + success.push(data.success); + } + + if (data.notices) { + notices.push.apply(notices, data.notices); + } + + _showStatusMessages(success, errors, notices, false); + }, + error: function(request, textStatus, errorThrown) { + errorState = true; + errors.push(request.status + ": " + errorThrown); + _showStatusMessages(success, errors, notices, false); + }, + complete: function(request, textStatus) { + if (errorState) { + jQuery("#tr_" + group).removeClass('selected'); + } + } + }) + }; + deferredObj = deferredObj.then(fun, fun); + } + // hide progress message and deselect rows after ALL operations have completed + deferredObj.always(function () { + jQuery("#adminGroups").DataTable().ajax.reload(); + }); + + initialDeferredObj.resolve(); + } + }); +} +/* categories part */ +function displayCategories(data, category) { + let ontTable = null; + let allRows + if (jQuery.fn.dataTable.isDataTable('#adminCategories')) { + ontTable = jQuery('#adminCategories').DataTable().ajax.reload(); + } else { + ontTable = jQuery("#adminCategories").DataTable({ + "ajax": { + "url": "/admin/categories", + "contentType": "application/json", + "dataSrc": function (json) { + return populateCategoryRows(json); + } + }, + "rowCallback": function(row, data, index) { + var acronym = jQuery('td:nth-child(4)', row).text(); + + jQuery(row).attr("id", "tr_" + acronym); + }, + "initComplete": function(settings, json) { + }, + "columnDefs": [ + { + "targets": 0, + "searchable": true, + "title": "Name", + }, + { + "targets": 1, + "searchable": true, + "title": "Description", + }, + { + "targets": 2, + "searchable": true, + "title": "Created", + }, + { + "targets": 3, + "searchable": true, + "title": "Id", + }, + { + "targets": 4, + "searchable": false, + "orderable": false, + "title": "Parent", + }, + { + "targets": 5, + "searchable": false, + "orderable": false, + "title": "Count", + }, + { + "targets": 6, + "searchable": false, + "orderable": false, + "title": "Actions", + "width": "250px" + } + ], + "autoWidth": false, + "lengthChange": false, + "searching": true, + "language": { + "search": "Filter: ", + "emptyTable": "No categories available" + }, + "info": true, + "paging": true, + "pageLength": 100, + "ordering": true, + "responsive": true, + "dom": '<"categories_nav"><"top"fi>rtip', + "stripeClasses": ["", "alt"], + }); + } + return ontTable; +} + +function populateCategoryRows(data) { + let categories = data['categories']; + let allRows = categories.map(category => { + let name = category['name']; + let description = category['description'] + let created = category['created']; + let id = category['acronym']; + let parentCategory = category['parentCategory']; + let nb = [ + ' ' + category['ontologies'].length + ' ', + ]; + let actions = [ + 'Edit', + ] + return [name, description, created, id , parentCategory, nb , actions.join('|')]; + }) + + return allRows; +} + +function DeleteCategories() { +} + +DeleteCategories.act = function(groupName) { + let category2delete = jQuery("#adminCategories tr.selected td:nth-child(4)").map(function(index, value) { return value.textContent.trim();}).toArray(); + let confirmMsg = "You are about to delete the following categories:
    " + category2delete.join(",") + "

    Should I proceed?"; + alertify.confirm(confirmMsg, (e) => { + if (e) { + _clearStatusMessages(); + let success = []; + let errors = []; + let notices = []; + let errorState = false; + let deferredObj = jQuery.Deferred(); + let initialDeferredObj = deferredObj; + + for (let category of category2delete) { + fun = () => { return jQuery.ajax("/admin/categories/" + encodeURIComponent(category), { + method: "DELETE", + dataType: "json", + success: function(data, msg) { + var reg = /\s*,\s*/g; + + if (data.errors) { + errorState = true; + errors.push.apply(errors, data.errors); + } + + if (data.success) { + success.push(data.success); + } + + if (data.notices) { + notices.push.apply(notices, data.notices); + } + + _showStatusMessages(success, errors, notices, false); + }, + error: function(request, textStatus, errorThrown) { + errorState = true; + errors.push(request.status + ": " + errorThrown); + _showStatusMessages(success, errors, notices, false); + }, + complete: function(request, textStatus) { + if (errorState) { + jQuery("#tr_" + category).removeClass('selected'); + } + } + }) + }; + deferredObj = deferredObj.then(fun, fun); + } + // hide progress message and deselect rows after ALL operations have completed + deferredObj.always(function () { + jQuery("#adminCategories").DataTable().ajax.reload(); + }); + + initialDeferredObj.resolve(); + } + }); +} + +/***************************** + * COMMON FUNCTIONS + *****************************/ +function _clearStatusMessages() { + jQuery("#progress_message").hide(); + jQuery("#success_message").hide(); + jQuery("#error_message").hide(); + jQuery("#info_message").hide(); + jQuery("#progress_message").html(""); + jQuery("#success_message").html(""); + jQuery("#error_message").html(""); + jQuery("#info_message").html(""); +} \ No newline at end of file diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 181e46e113..ad58c2c449 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -16,6 +16,7 @@ .error { background: #ffecec image-url("error.png") no-repeat 10px 50%; border: 1px solid #f5aca6; + padding-left: 30px; } .success { @@ -139,3 +140,8 @@ table.dataTable tbody tr.selected { margin-right: 8px; } } + +div.groupFormError { + color: red; + padding-top: 3px; +} diff --git a/app/components/alert_message_component.rb b/app/components/alert_message_component.rb index f2175b7e00..c25efdf683 100644 --- a/app/components/alert_message_component.rb +++ b/app/components/alert_message_component.rb @@ -2,7 +2,7 @@ class AlertMessageComponent < ViewComponent::Base include Turbo::FramesHelper - def initialize(id: '', type: 'info') + def initialize(id: '', message: nil, type: 'info', closeable: true) @id = id @type = "alert-#{type}" end diff --git a/app/components/alert_message_component/alert_message_component.html.haml b/app/components/alert_message_component/alert_message_component.html.haml index 19c0e89f01..c32ec7d3c7 100644 --- a/app/components/alert_message_component/alert_message_component.html.haml +++ b/app/components/alert_message_component/alert_message_component.html.haml @@ -1,4 +1,8 @@ -.alert.alert-dismissible.fade.show{:role => "alert", class: "#{@type}"} - = content - %button.close{"aria-label": "Close", "data-dismiss": "alert", type: "button", style: "background: transparent"} - %span{"aria-hidden" => "true"} × \ No newline at end of file +.alert.alert-dismissible.fade.show{:role => "alert", class: "#{@type}", style: "text-align: left;white-space: normal;"} + = @message || content + + - if @closeable + %button.close{"aria-label": "Close", "data-dismiss": "alert", type: "button", style: "background: transparent"} + %span{"aria-hidden" => "true"} × + + \ No newline at end of file diff --git a/app/components/select_input_component.rb b/app/components/select_input_component.rb index 872f7f41ee..9bcfe5d1df 100644 --- a/app/components/select_input_component.rb +++ b/app/components/select_input_component.rb @@ -2,13 +2,14 @@ class SelectInputComponent < ViewComponent::Base - def initialize(id:, name:, values:, selected:, multiple: false) + def initialize(id:, name:, values:, selected:, multiple: false, open_to_add_values: false) super @id = id @name = name @values = values @selected = selected @multiple = multiple + @open_to_add_values = open_to_add_values end def options_values diff --git a/app/components/select_input_component/select_input_component.html.haml b/app/components/select_input_component/select_input_component.html.haml index 2a9f7692cd..a46f0ad299 100644 --- a/app/components/select_input_component/select_input_component.html.haml +++ b/app/components/select_input_component/select_input_component.html.haml @@ -1,7 +1,7 @@ %div{:data => { controller: "select-input", 'select-input': {'multiple-value': @multiple.to_s}}} = select_tag(@name, options_values, { multiple: @multiple, class: "form-control", id: "select_#{@id}", data: { action: "select-input#toggleOtherValue", "select-input-target": "selectedValues" } }) - %div.d-flex.mt-1 + %div.d-flex.mt-1{style: "display:#{@open_to_add_values ? 'none !important;' : 'block;'}"} = text_field_tag("add_#{@id}", nil, :style => "margin-right: 1em;width: 16em;display: none;", :placeholder => "Or provide the value", data: {action: "keydown.enter->select-input#addValue", "select-input-target": "inputValueField"}, class: 'metadataInput form-control form-control-sm') diff --git a/app/controllers/admin/categories_controller.rb b/app/controllers/admin/categories_controller.rb new file mode 100644 index 0000000000..8d52212bb1 --- /dev/null +++ b/app/controllers/admin/categories_controller.rb @@ -0,0 +1,111 @@ +class Admin::CategoriesController < ApplicationController + include SubmissionUpdater + + layout :determine_layout + before_action :unescape_id, only: [:edit, :show, :update, :destroy] + before_action :authorize_admin + + CATEGORIES_URL = "#{LinkedData::Client.settings.rest_url}/categories" + ATTRIBUTE_TO_INCLUDE = 'name,acronym,created,description,parentCategory,ontologies' + + def index + response = _categories + render :json => response + end + + def new + @category = LinkedData::Client::Models::Category.new + + respond_to do |format| + format.html { render "new", :layout => false } + end + end + + def edit + @category = LinkedData::Client::Models::Category.find_by_acronym(params[:id], include:'name,acronym,created,description,parentCategory,ontologies' ).first + @ontologies_category = LinkedData::Client::Models::Ontology.all(include: 'acronym').map {|o|[o.acronym, o.id] } + respond_to do |format| + format.html { render "edit", :layout => false } + end + end + + def create + response = { errors: '', success: '' } + start = Time.now + begin + category = LinkedData::Client::Models::Category.new(values: category_params) + category_saved = category.save + if response_error?(category_saved) + response[:errors] = response_errors(category_saved) + else + response[:success] = "category successfully created in #{Time.now - start}s" + end + rescue Exception => e + response[:errors] = "Problem creating the category - #{e.message}" + end + render json: response, status: (response[:errors] == '' ? :created : :internal_server_error) + + end + + def update + response = { errors: '', success: ''} + start = Time.now + begin + category = LinkedData::Client::Models::Category.find_by_acronym(params[:id], include: ATTRIBUTE_TO_INCLUDE ).first + add_ontologies_to_object(category_params[:ontologies],category) if (category_params[:ontologies].present? && category_params[:ontologies].size > 0 && category_params[:ontologies].first != '') + delete_ontologies_from_object(category_params[:ontologies],category.ontologies,category) + category.update_from_params(category_params) + category_update = category.update + if response_error?(category_update) + response[:errors] = response_errors(category_update) + else + response[:success] = "category successfully updated in #{Time.now - start}s" + end + rescue Exception => e + response[:errors] = "Problem updating the category - #{e.message}" + end + render json: response, status: (response[:errors] == '' ? :ok : :internal_server_error) + end + + def destroy + response = { errors: '', success: ''} + start = Time.now + begin + category = LinkedData::Client::Models::Category.find_by_acronym(params[:id]).first + error_response = category.delete + + if response_error?(error_response) + response[:errors] = response_errors(error_response) + else + response[:success] = "category successfully deleted in #{Time.now - start}s" + end + rescue Exception => e + response[:errors] = "Problem deleting the category - #{e.message}" + end + render json: response, status: (response[:errors] == '' ? :ok : :internal_server_error) + end + + private + + def unescape_id + params[:id] = CGI.unescape(params[:id]) + end + + def category_params + params.require(:category).permit(:acronym, :name, :description, :parentCategory, {ontologies:[]}).to_h + end + + def _categories + response = { categories: Hash.new, errors: '', success: '' } + start = Time.now + begin + response[:categories] = JSON.parse(LinkedData::Client::HTTP.get(CATEGORIES_URL, { include: ATTRIBUTE_TO_INCLUDE }, raw: true)) + + response[:success] = "categories successfully retrieved in #{Time.now - start}s" + LOG.add :debug, "Categories - retrieved #{response[:categories].length} groups in #{Time.now - start}s" + rescue Exception => e + response[:errors] = "Problem retrieving categories - #{e.message}" + end + response + end +end diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb new file mode 100644 index 0000000000..3df27d2e02 --- /dev/null +++ b/app/controllers/admin/groups_controller.rb @@ -0,0 +1,111 @@ +class Admin::GroupsController < ApplicationController + include SubmissionUpdater + + layout :determine_layout + before_action :unescape_id, only: [:edit, :show, :update, :destroy] + before_action :authorize_admin + + GROUPS_URL = "#{LinkedData::Client.settings.rest_url}/groups" + + def index + response = _groups + render :json => response + end + + def new + @group = LinkedData::Client::Models::Group.new + + respond_to do |format| + format.html { render "new", :layout => false } + end + end + + def edit + @group = LinkedData::Client::Models::Group.find_by_acronym(params[:id]).first + @acronyms = @group.ontologies.map { |url| url.match(/\/([^\/]+)$/)[1] } + @ontologies_group = LinkedData::Client::Models::Ontology.all(include: 'acronym').map {|o|[o.acronym, o.id] } + respond_to do |format| + format.html { render "edit", :layout => false } + end + end + + def create + response = { errors: '', success: '' } + start = Time.now + begin + group = LinkedData::Client::Models::Group.new(values: group_params) + group_saved = group.save + if response_error?(group_saved) + response[:errors] = response_errors(group_saved) + else + response[:success] = "group successfully created in #{Time.now - start}s" + end + rescue Exception => e + response[:errors] = "Problem creating the group - #{e.message}" + end + render json: response, status: (response[:errors] == '' ? :created : :internal_server_error) + + end + + def update + response = { errors: '', success: ''} + start = Time.now + begin + group = LinkedData::Client::Models::Group.find_by_acronym(params[:id]).first + add_ontologies_to_object(group_params[:ontologies],group) if (group_params[:ontologies].present? && group_params[:ontologies].size > 0 && group_params[:ontologies].first != '') + delete_ontologies_from_object(group_params[:ontologies],group.ontologies,group) + group.update_from_params(group_params) + group_updated = group.update + if response_error?(group_updated) + response[:errors] = response_errors(group_updated) + else + response[:success] = "group successfully updated in #{Time.now - start}s" + end + rescue Exception => e + response[:errors] = "Problem updating the group - #{e.message}" + end + render json: response, status: (response[:errors] == '' ? :ok : :internal_server_error) + end + + def destroy + response = { errors: '', success: ''} + start = Time.now + begin + group = LinkedData::Client::Models::Group.find_by_acronym(params[:id]).first + error_response = group.delete + + if response_error?(error_response) + response[:errors] = response_errors(error_response) + else + response[:success] = "group successfully deleted in #{Time.now - start}s" + end + rescue Exception => e + response[:errors] = "Problem deleting the group - #{e.message}" + end + render json: response, status: (response[:errors] == '' ? :ok : :internal_server_error) + end + + private + + def unescape_id + params[:id] = CGI.unescape(params[:id]) + end + + def group_params + params.require(:group).permit(:acronym, :name, :description, {ontologies:[]}).to_h() + end + + def _groups + response = { groups: Hash.new, errors: '', success: '' } + start = Time.now + begin + response[:groups] = JSON.parse(LinkedData::Client::HTTP.get(GROUPS_URL, { include: 'all' }, raw: true)) + + response[:success] = "groups successfully retrieved in #{Time.now - start}s" + LOG.add :debug, "Groups - retrieved #{response[:groups].length} groups in #{Time.now - start}s" + rescue Exception => e + response[:errors] = "Problem retrieving groups - #{e.message}" + end + response + end +end diff --git a/app/controllers/concerns/submission_updater.rb b/app/controllers/concerns/submission_updater.rb index 3b1218fc33..2ab43c70a4 100644 --- a/app/controllers/concerns/submission_updater.rb +++ b/app/controllers/concerns/submission_updater.rb @@ -24,6 +24,33 @@ def update_submission(new_submission_hash) @submission.update(cache_refresh_all: false) end + def add_ontologies_to_object(ontologies,object) + ontologies.each do |ont| + next if object.ontologies.include?(ont) + ontology = LinkedData::Client::Models::Ontology.find(ont) + if object.type.match(/\/([^\/]+)$/)[1] == 'Group' + ontology.group.push(object.id) + else + ontology.hasDomain.push(object.id) + end + ontology.update + end + end + + def delete_ontologies_from_object(new_ontologies,old_ontologies,object) + new_ontologies = [] if new_ontologies.nil? + ontologies = old_ontologies - new_ontologies + ontologies.each do |ont| + ontology = LinkedData::Client::Models::Ontology.find(ont) + if object.type.match(/\/([^\/]+)$/)[1] == 'Group' + ontology.group.delete(object.id) + else + ontology.hasDomain.delete(object.id) + end + ontology.update + end + end + private def update_ontology_summary_only diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 2c7b8065c4..39c6432a4e 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -159,7 +159,7 @@ def account @user_ontologies = @user.customOntology @user_ontologies ||= [] - onts = LinkedData::Client::Models::Ontology.all + onts = LinkedData::Client::Models::Ontology.all(include_views: true); @admin_ontologies = onts.select { |o| o.administeredBy.include? @user.id } projects = LinkedData::Client::Models::Project.all diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index 07fdfe451b..cc473e0b3b 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -4,7 +4,7 @@ class OntologiesController < ApplicationController include InstancesHelper include ActionView::Helpers::NumberHelper include OntologiesHelper - include SchemesHelper + include SchemesHelper, ConceptsHelper include CollectionsHelper include MappingStatistics @@ -420,6 +420,7 @@ def update # Note: find_by_acronym includes ontology views @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology][:acronym] || params[:id]).first @ontology.update_from_params(ontology_params) + @ontology.viewOf = nil if @ontology.isView.eql? "0" error_response = @ontology.update if response_error?(error_response) @categories = LinkedData::Client::Models::Category.all diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 319e2dc363..86d9a2e6ae 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -250,7 +250,7 @@ def validate_update(params) if params[:username].nil? || params[:username].length < 1 errors << "Last name field is required" end - if ((!params[:orcidId].match(/^\d{4}+(-\d{4})+$/)) || (params[:orcidId].length != 19)) && !(params[:orcidId].nil? || params[:orcidId].length < 1) + if params[:orcidId].present? && ((!params[:orcidId].match(/^\d{4}-\d{4}-\d{4}-\d{4}$/)) || (params[:orcidId].length != 19)) errors << "Please enter a valide orcide id" end if !params[:password].eql?(params[:password_confirmation]) diff --git a/app/helpers/concepts_helper.rb b/app/helpers/concepts_helper.rb index 9d438348cb..b56489dc01 100644 --- a/app/helpers/concepts_helper.rb +++ b/app/helpers/concepts_helper.rb @@ -26,6 +26,22 @@ def get_concept_id(params, concept, root) end end + def sub_menu_active?(section) + params["sub_menu"]&.eql? section + end + + def sub_menu_active_class(section) + "active show" if sub_menu_active?(section) + end + + def default_sub_menu? + !sub_menu_active?('list') && !sub_menu_active?('date') + end + + def default_sub_menu_class + "active show" if default_sub_menu? + end + def concept_label(ont_id, cls_id) @ontology = LinkedData::Client::Models::Ontology.find(ont_id) @ontology ||= LinkedData::Client::Models::Ontology.find_by_acronym(ont_id).first diff --git a/app/helpers/ontologies_helper.rb b/app/helpers/ontologies_helper.rb index 6e43ba8193..ecb06e34de 100644 --- a/app/helpers/ontologies_helper.rb +++ b/app/helpers/ontologies_helper.rb @@ -416,5 +416,16 @@ def sections_to_show end sections end + def dispaly_complex_text(definitions) + html = "" + definitions.each do |definition| + if definition.is_a?(String) + html += '

    ' + definition + '

    ' + elsif definition.respond_to?(:uri) && definition.uri + html += '

    ' + definition.uri + '

    ' + end + end + return html.html_safe + end end diff --git a/app/javascript/controllers/fair_score_home_controller.js b/app/javascript/controllers/fair_score_home_controller.js new file mode 100644 index 0000000000..8b4883ad9e --- /dev/null +++ b/app/javascript/controllers/fair_score_home_controller.js @@ -0,0 +1,21 @@ +import { Controller } from "@hotwired/stimulus" +import {FairScorePrincipleBar, FairScoreCriteriaRadar, FairScoreChartContainer} from "../mixins/useFairScore"; +// Connects to data-controller="fair-score-home" +export default class extends Controller { + connect() { + let fairScoreBar = new FairScorePrincipleBar( 'ont-fair-scores-canvas') + let fairScoreRadar = new FairScoreCriteriaRadar( 'ont-fair-criteria-scores-canvas') + let fairContainer = new FairScoreChartContainer('fair-score-charts-container' , [ fairScoreRadar , fairScoreBar]) + let ontologies = jQuery("#ontology_ontologyId"); + + fairContainer.getFairScoreData("all") + ontologies.change( (e) => { + if(ontologies.val() !== null){ + fairContainer.getFairScoreData(ontologies.val().join(',')) + } else if(ontologies.val() === null){ + fairContainer.getFairScoreData("all") + } + e.preventDefault() + }) + } +} diff --git a/app/javascript/controllers/fair_score_landscape_controller.js b/app/javascript/controllers/fair_score_landscape_controller.js new file mode 100644 index 0000000000..e093f68ea1 --- /dev/null +++ b/app/javascript/controllers/fair_score_landscape_controller.js @@ -0,0 +1,20 @@ +import { Controller } from "@hotwired/stimulus" +import { FairScoreChartContainer, FairScoreCriteriaBar } from "../mixins/useFairScore"; +// Connects to data-controller="fair-score-landscape" +export default class extends Controller { + connect() { + let fairCriteriaBars = new FairScoreCriteriaBar('ont-fair-scores-criteria-bars-canvas') + let fairContainer = new FairScoreChartContainer('fair-score-charts-container' , [fairCriteriaBars]) + let ontologies = jQuery("#ontology_ontologyId"); + + fairContainer.getFairScoreData("all") + ontologies.change( (e) => { + if(ontologies.val() !== null){ + fairContainer.getFairScoreData(ontologies.val().join(',')) + } else if(ontologies.val() === null){ + fairContainer.getFairScoreData("all") + } + e.preventDefault() + }) + } +} diff --git a/app/javascript/controllers/fair_score_summary_controller.js b/app/javascript/controllers/fair_score_summary_controller.js new file mode 100644 index 0000000000..883e2cde62 --- /dev/null +++ b/app/javascript/controllers/fair_score_summary_controller.js @@ -0,0 +1,12 @@ +import { Controller } from "@hotwired/stimulus" +import {FairScorePrincipleBar, FairScoreCriteriaRadar, FairScoreChartContainer} from "../mixins/useFairScore"; +// Connects to data-controller="fair-score-summary" +export default class extends Controller { + connect() { + let fairScoreBar = new FairScorePrincipleBar( 'ont-fair-scores-canvas') + let fairScoreRadar = new FairScoreCriteriaRadar( 'ont-fair-criteria-scores-canvas') + let fairContainer = new FairScoreChartContainer('fair-score-charts-container' , [ fairScoreRadar , fairScoreBar]) + + fairContainer.getFairScoreData(window.location.pathname.split('/')[2]) + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 553504b0ac..83bcc541f1 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -13,6 +13,15 @@ application.register("class-search-auto-complete", ClassSearchAutoCompleteContro import ContainerSplitterController from "./container_splitter_controller" application.register("container-splitter", ContainerSplitterController) +import FairScoreHomeController from "./fair_score_home_controller" +application.register("fair-score-home", FairScoreHomeController) + +import FairScoreLandscapeController from "./fair_score_landscape_controller" +application.register("fair-score-landscape", FairScoreLandscapeController) + +import FairScoreSummaryController from "./fair_score_summary_controller" +application.register("fair-score-summary", FairScoreSummaryController) + import FormAutoCompleteController from "./form_auto_complete_controller" application.register("form-auto-complete", FormAutoCompleteController) diff --git a/app/assets/javascripts/fair_score.js b/app/javascript/mixins/useFairScore.js similarity index 90% rename from app/assets/javascripts/fair_score.js rename to app/javascript/mixins/useFairScore.js index c9186984d3..82aa516648 100644 --- a/app/assets/javascripts/fair_score.js +++ b/app/javascript/mixins/useFairScore.js @@ -1,7 +1,7 @@ - function round(val , base = 1){ return Math.floor( val * 100 * base) / 100 } + function getObtainedNotObtainedNA(scoresIn, portalMax , max , normalize = true){ const delimiter = (val) => (normalize ? val : 1) const notObtained = portalMax.map((x,i) => { @@ -391,17 +391,17 @@ class FairScoreCriteriaRadar extends FairScoreChart{ getFairScoreDataSet() { const scores = this.fairScoreChartCanvas.data('normalizedScores') - return [ - { - label: 'Fair score', - data: scores, - fill: true, - backgroundColor: 'rgba(151, 187, 205, 0.2)', - borderColor: 'rgba(151, 187, 205, 1)', - pointBorderColor: 'rgba(151, 187, 205, 1)', - pointBackgroundColor: 'rgba(151, 187, 205, 1)' - } - ] + return [ + { + label: 'Fair score', + data: scores, + fill: true, + backgroundColor: 'rgba(151, 187, 205, 0.2)', + borderColor: 'rgba(151, 187, 205, 1)', + pointBorderColor: 'rgba(151, 187, 205, 1)', + pointBackgroundColor: 'rgba(151, 187, 205, 1)' + } + ] } @@ -492,7 +492,7 @@ class FairScoreCriteriaBar extends FairScoreChart{ if (topOffset <= 0) topOffset = 0 - else if( (topOffset + tooltipEl.clientHeight) >= position.height) + else if( (topOffset + tooltipEl.clientHeight) >= position.height) topOffset = position.height - tooltipEl.clientHeight // Display, position, and set styles for font @@ -627,63 +627,5 @@ class FairScoreCriteriaBar extends FairScoreChart{ } } - -/* - For landscape - */ -jQuery('#fairness_assessment').ready(()=> { - let fairCriteriaBars = new FairScoreCriteriaBar('ont-fair-scores-criteria-bars-canvas') - let fairContainer = new FairScoreChartContainer('fair-score-charts-container' , [fairCriteriaBars]) - let ontologies = jQuery("#ontology_ontologyId"); - - fairContainer.getFairScoreData("all") - ontologies.change( (e) => { - if(ontologies.val() !== null){ - fairContainer.getFairScoreData(ontologies.val().join(',')) - } else if(ontologies.val() === null){ - fairContainer.getFairScoreData("all") - } - e.preventDefault() - }) - return false -}) - - - -/* - For the home - */ -jQuery('#fair-home').ready( function (e) { - - let fairScoreBar = new FairScorePrincipleBar( 'ont-fair-scores-canvas') - let fairScoreRadar = new FairScoreCriteriaRadar( 'ont-fair-criteria-scores-canvas') - let fairContainer = new FairScoreChartContainer('fair-score-charts-container' , [ fairScoreRadar , fairScoreBar]) - let ontologies = jQuery("#ontology_ontologyId"); - - fairContainer.getFairScoreData("all") - ontologies.change( (e) => { - if(ontologies.val() !== null){ - fairContainer.getFairScoreData(ontologies.val().join(',')) - } else if(ontologies.val() === null){ - fairContainer.getFairScoreData("all") - } - e.preventDefault() - }) - return false -}) - -/* - For the summary -*/ -jQuery('#fair-summary').ready( function (e) { - - let fairScoreBar = new FairScorePrincipleBar( 'ont-fair-scores-canvas') - let fairScoreRadar = new FairScoreCriteriaRadar( 'ont-fair-criteria-scores-canvas') - let fairContainer = new FairScoreChartContainer('fair-score-charts-container' , [ fairScoreRadar , fairScoreBar]) - - fairContainer.getFairScoreData(window.location.pathname.split('/')[2]) - - return false -}) - +export {round, getObtainedNotObtainedNA, FairScoreChartContainer, FairScoreChart, FairScorePrincipleBar, FairScoreCriteriaRadar, FairScoreCriteriaBar} diff --git a/app/views/admin/categories/_form.html.haml b/app/views/admin/categories/_form.html.haml new file mode 100644 index 0000000000..34bd8e82ae --- /dev/null +++ b/app/views/admin/categories/_form.html.haml @@ -0,0 +1,49 @@ +- new_record = @category.acronym.nil? +%div.alert-box.error{style: @errors.nil? ? "display: none" : nil } + %ul + - unless @errors.nil? + - for error in @errors + %li + = error +%div{:style => "width:500px"} + %span.asterik{:style => "float:right;"} * fields are required + %h3 #{title_text} + %table#category_form.form + %colgroup + %col + %col{style: "width: 100%"} + %tr + %th + Acronym + %span.asterik * + %td.top + - if new_record + = f.text_field :acronym, class: "form-control" + - else + = f.text_field :acronym, class: "form-control", readonly: true + %tr + %th + Name + %span.asterik * + %td.top + = f.text_field :name, class: "form-control" + %tr + %th + Description + %td.top + = f.text_area :description, class: "form-control" + - unless new_record + %tr + %th + Created + %td.top + = f.text_field :created, readonly: true, class: "form-control" + %tr + %th + Ontologies + %td.top + = render SelectInputComponent.new(id: "category_ontologies", name: "category[ontologies]", values: @ontologies_category, selected: @category.ontologies, multiple: true, open_to_add_values: true) + %div.d-flex.mt-1{:style => "display: none;"} + %div.mt-2 + = f.submit button_text, class: "btn btn-primary mr-sm-2 group-form-accept" + = link_to "Cancel", "javascript:;", class: "btn btn-primary dismiss-dialog" \ No newline at end of file diff --git a/app/views/admin/categories/edit.html.haml b/app/views/admin/categories/edit.html.haml new file mode 100644 index 0000000000..b203c02ce3 --- /dev/null +++ b/app/views/admin/categories/edit.html.haml @@ -0,0 +1,6 @@ +- @title = "Edit category" + +%div + = form_for :category, url: admin_categories_path + "/" + escape(@category.acronym), method: "PATCH", remote: true, data: { collection: "categories"}, html: {class: "admin-collection-form" } do |f| + = render partial: "form", locals: {f: f, title_text: "Edit category", + button_text: "Save", button_class: "edit-category"} diff --git a/app/views/admin/categories/new.html.haml b/app/views/admin/categories/new.html.haml new file mode 100644 index 0000000000..4d3b3082d0 --- /dev/null +++ b/app/views/admin/categories/new.html.haml @@ -0,0 +1,6 @@ +- @title = "Create new category" + +%div + = form_for :category, url: admin_categories_path, method: "POST", remote: true, data: { collection: "categories"}, html: {class: "admin-collection-form" } do |f| + = render partial: "form", locals: {f: f, title_text: "Create category", + button_text: "Create category" } diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml new file mode 100644 index 0000000000..ce5c958277 --- /dev/null +++ b/app/views/admin/groups/_form.html.haml @@ -0,0 +1,48 @@ +- new_record = @group.acronym.nil? +%div.alert-box.error{style: @errors.nil? ? "display: none" : nil } + %ul + - unless @errors.nil? + - for error in @errors + %li + = error +%div{:style => "width:500px"} + %span.asterik{:style => "float:right;"} * fields are required + %h3 #{title_text} + %table#group_form.form + %colgroup + %col + %col{style: "width: 100%"} + %tr + %th + Acronym + %span.asterik * + %td.top + - if new_record + = f.text_field :acronym, class: "form-control" + - else + = f.text_field :acronym, class: "form-control", readonly: true + %tr + %th + Name + %span.asterik * + %td.top + = f.text_field :name, class: "form-control" + %tr + %th + Description + %td.top + = f.text_area :description, class: "form-control" + - unless new_record + %tr + %th + Created + %td.top + = f.text_field :created, readonly: true, class: "form-control" + %tr + %th + Ontologies + %td.top + = render SelectInputComponent.new(id: "group_ontologies", name: "group[ontologies]", values: @ontologies_group , selected: @group.ontologies , multiple: true, open_to_add_values: true) + %div.mt-2 + = f.submit button_text, class: "btn btn-primary mr-sm-2 group-form-accept" + = link_to "Cancel", "javascript:;", class: "btn btn-primary dismiss-dialog" \ No newline at end of file diff --git a/app/views/admin/groups/edit.html.haml b/app/views/admin/groups/edit.html.haml new file mode 100644 index 0000000000..7269a374d0 --- /dev/null +++ b/app/views/admin/groups/edit.html.haml @@ -0,0 +1,6 @@ +- @title = "Edit group" + +%div + = form_for :group, url: admin_groups_path + "/" + escape(@group.acronym), method: "PATCH", remote: true, data: { collection: "groups"}, html: {class: "admin-collection-form" } do |f| + = render partial: "form", locals: {f: f, title_text: "Edit group", + button_text: "Save", button_class: "edit-group"} diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml new file mode 100644 index 0000000000..2a8bf5f425 --- /dev/null +++ b/app/views/admin/groups/new.html.haml @@ -0,0 +1,6 @@ +- @title = "Create new group" + +%div + = form_for :group, url: admin_groups_path, method: "POST", remote: true, data: { collection: "groups"}, html: {class: "admin-collection-form" } do |f| + = render partial: "form", locals: {f: f, title_text: "Create group", + button_text: "Create group" } diff --git a/app/views/admin/index.html.haml b/app/views/admin/index.html.haml index 0b796f2a10..a67c026743 100644 --- a/app/views/admin/index.html.haml +++ b/app/views/admin/index.html.haml @@ -1,4 +1,4 @@ -- @title = "BioPortal Administration" +- @title = "Administration" %div.row %div.col @@ -24,6 +24,10 @@ =link_to("Users", "#users", id: "users-admin-tab", class: "nav-link", role: "tab", data: { toggle: "tab", href: users_path() }, aria: { controls: "users", selected: "false" }) %li.nav-item =link_to("Metadata Administration", "#ontologies_metadata_curator", id: "ontologies_metadata_curator-admin-tab", class: "nav-link", role: "tab", data: { toggle: "tab"}, aria: { controls: "ontologies_metadata_curator", selected: "false" }) + %li.nav-item + =link_to("Groups", "#groups", id: "groups-admin-tab", class: "nav-link", role: "tab", data: { toggle: "tab", href: "groups" }, aria: { controls: "groups", selected: "false" }) + %li.nav-item + =link_to("Categories", "#categories", id: "categories-admin-tab", class: "nav-link", role: "tab", data: { toggle: "tab", href: "categories" }, aria: { controls: "categories", selected: "false" }) %div#adminTabContent.tab-content -# Site Administration tab @@ -103,6 +107,17 @@ %div.tab-pane.fade{id: "ontologies_metadata_curator", role: "tabpanel", aria: { labelledby: "ontologies_metadata_curator-admin-tab" }} = render partial: 'ontologies_metadata_curator/metadata_tab' + -# Groups tab + %div.tab-pane.fade{id: "groups", role: "tabpanel", aria: { labelledby: "groups-admin-tab" }} + %div.ontologies_list_container.mt-3 + %table#adminGroups.zebra{:cellpadding => "0", :cellspacing => "0", :width => "100%"} + + -# Categories tab + %div.tab-pane.fade{id: "categories", role: "tabpanel", aria: { labelledby: "categories-admin-tab" }} + %div.ontologies_list_container.mt-3 + %table#adminCategories.zebra{:cellpadding => "0", :cellspacing => "0", :width => "100%"} + + \ No newline at end of file diff --git a/app/views/concepts/_details.html.haml b/app/views/concepts/_details.html.haml index 56cea26439..2b225d586c 100644 --- a/app/views/concepts/_details.html.haml +++ b/app/views/concepts/_details.html.haml @@ -29,7 +29,7 @@ %tr %td{nowrap: ""} Definitions %td - %p= @concept.definition.join(" ") + = dispaly_complex_text(@concept.definition) - if @concept.obsolete? %tr %td{nowrap: ""} Obsolete diff --git a/app/views/concepts/_list.html.haml b/app/views/concepts/_list.html.haml index 83a4a27d3d..6b67d002f2 100644 --- a/app/views/concepts/_list.html.haml +++ b/app/views/concepts/_list.html.haml @@ -3,7 +3,7 @@ next_url: concept_list_url(@page.nextPage, request_collection_id, @ontology.acronym), current_page: @page.page, next_page: @page.nextPage, auto_click: @auto_click) do |c| - - concepts = c.collection.sort_by{|concept| [concept.prefLabel || concept.id]} + - concepts = c.collection.sort_by{|concept| [concept.prefLabel.capitalize || concept.id]} - if concepts && !concepts.empty? = raw tree_link_to_concept(child: concepts.shift, ontology_acronym: @ontology.acronym, active_style: c.auto_click? ? 'active' : '') - concepts.each do |concept| diff --git a/app/views/home/_fair_score_home.html.haml b/app/views/home/_fair_score_home.html.haml index 9e83a221ba..988ef5cf02 100644 --- a/app/views/home/_fair_score_home.html.haml +++ b/app/views/home/_fair_score_home.html.haml @@ -1,4 +1,4 @@ -%div.col +%div.col{data:{controller:"fair-score-home"}} %div.card.mb-3 %div.card-header.d-flex.justify-content-between %span diff --git a/app/views/landscape/_fair_score_landscape.html.haml b/app/views/landscape/_fair_score_landscape.html.haml index 7abc353f87..adac535099 100644 --- a/app/views/landscape/_fair_score_landscape.html.haml +++ b/app/views/landscape/_fair_score_landscape.html.haml @@ -31,7 +31,7 @@ This interface shows how an ontology or a group responded successfully to O’FAIRe FAIRness assessment questions %div See details for each ontologies on the specific ontology summary pages - %div.card-body + %div.card-body{data:{controller:"fair-score-landscape"}} %div#fair-score-charts-container.row %div.col %span.text-secondary.mt-4 diff --git a/app/views/ontologies/_fairs_score.html.haml b/app/views/ontologies/_fairs_score.html.haml index e3c29dd32f..26a98885c3 100644 --- a/app/views/ontologies/_fairs_score.html.haml +++ b/app/views/ontologies/_fairs_score.html.haml @@ -12,7 +12,7 @@ class: "btn btn-primary w-100", data: { show_modal_title_value: "FAIRness assessment questions", show_modal_size_value: 'modal-xl' }, ) - %div.statistics_container + %div.statistics_container{data:{controller:"fair-score-summary"}} %div = render partial: "shared/fair_score_radar" , locals: {data: @fair_scores_data} %div diff --git a/app/views/ontologies/concepts_browsers/_concepts_browser.html.haml b/app/views/ontologies/concepts_browsers/_concepts_browser.html.haml index 967c93e8cd..1c4c5128c2 100644 --- a/app/views/ontologies/concepts_browsers/_concepts_browser.html.haml +++ b/app/views/ontologies/concepts_browsers/_concepts_browser.html.haml @@ -1,18 +1,18 @@ %nav .nav.nav-tabs.text-center{:role => "tablist", style:"background-color: rgba(0, 0, 0, 0.03);"} - %a#concepts-tree-tab.nav-item.nav-link.active.flex-grow-1.border-radius-0{"data-toggle" => "tab", :href => "#concepts-tree-container", title: 'Hierarchy view', 'data-controller': "tooltip"} + %a#concepts-tree-tab.nav-item.nav-link.flex-grow-1.border-radius-0{"data-toggle" => "tab", :href => "#concepts-tree-container", title: 'Hierarchy view', 'data-controller': "tooltip", class: default_sub_menu_class} %img{src: asset_path('list-tree.svg') , style:'width: 25px; height: 25px'} - if skos? - %a#concepts-list-tab.nav-item.nav-link.flex-grow-1.border-radius-0{"data-toggle" => "tab", :href => "#concepts-list-container", title: 'Collection view', 'data-controller': "tooltip"} + %a#concepts-list-tab.nav-item.nav-link.flex-grow-1.border-radius-0{"data-toggle" => "tab", :href => "#concepts-list-container", title: 'Collection view', 'data-controller': "tooltip", class: sub_menu_active_class('list')} %i.fas.fa-list.text-dark{style:'font-size: 25px'} - %a#concepts-date-sort-tab.nav-item.nav-link.flex-grow-1.border-radius-0{"data-toggle" => "tab", :href => "#concepts-date-sort-container", title: 'Date view', 'data-controller': "tooltip"} + %a#concepts-date-sort-tab.nav-item.nav-link.flex-grow-1.border-radius-0{"data-toggle" => "tab", :href => "#concepts-date-sort-container", title: 'Date view', 'data-controller': "tooltip", class: sub_menu_active_class('date')} %i.far.fa-calendar-alt.text-dark{style:'font-size: 25px'} .tab-content.px-1.py-2 - #concepts-tree-container.tab-pane.fade.show.active + #concepts-tree-container.tab-pane.fade{class: default_sub_menu_class} = render partial: 'ontologies/concepts_browsers/concepts_tree' - if skos? - #concepts-list-container.tab-pane.fade + #concepts-list-container.tab-pane.fade{class: sub_menu_active_class('list')} = render partial: 'ontologies/concepts_browsers/concepts_list' - #concepts-date-sort-container.tab-pane.fade + #concepts-date-sort-container.tab-pane.fade{class: sub_menu_active_class('date')} = render partial: 'ontologies/concepts_browsers/concepts_date_sort' \ No newline at end of file diff --git a/app/views/ontologies/concepts_browsers/_concepts_list.html.haml b/app/views/ontologies/concepts_browsers/_concepts_list.html.haml index 07d48f6807..6fb3b288a1 100644 --- a/app/views/ontologies/concepts_browsers/_concepts_list.html.haml +++ b/app/views/ontologies/concepts_browsers/_concepts_list.html.haml @@ -13,5 +13,5 @@ data: {action: 'changed->turbo-frame#updateFrame'}} %div#sd_content.card.p-1{style: 'overflow-y: scroll; height: 60vh;'} - = render TurboFrameComponent.new(id: 'concepts_list_view-page-1', data: {'turbo-frame-target': 'frame'}) do + = render TurboFrameComponent.new(id: 'concepts_list_view-page-1', data: {'turbo-frame-target': 'frame'}, src:params[:concept_collections] ? "/ajax/classes/list?ontology_id=#{@ontology.acronym}&collection_id=#{params[:concept_collections]}" : '') do Please select a collection to display \ No newline at end of file diff --git a/app/views/shared/_ontology_picker_single.html.erb b/app/views/shared/_ontology_picker_single.html.erb index dbcb731252..53e2b4f9df 100644 --- a/app/views/shared/_ontology_picker_single.html.erb +++ b/app/views/shared/_ontology_picker_single.html.erb @@ -10,8 +10,17 @@ <%disabled ||= nil%> diff --git a/config/routes.rb b/config/routes.rb index 057297ac71..81b907f21f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -50,6 +50,8 @@ namespace :admin do resources :licenses, only: [:index, :create, :new] + resources :groups, only: [:index, :create, :new, :edit, :update, :destroy] + resources :categories, only: [:index, :create, :new, :edit, :update, :destroy] end resources :subscriptions