diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 20c34ccfb3..a775d05f1d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -421,6 +421,8 @@ def using_captcha? def get_class(params) + lang = params[:language]&.upcase&.to_sym + if @ontology.flat? ignore_concept_param = params[:conceptid].nil? || @@ -437,7 +439,7 @@ def get_class(params) @concept.children = [] else # Display only the requested class in the tree - @concept = @ontology.explore.single_class({full: true}, params[:conceptid]) + @concept = @ontology.explore.single_class({full: true, lang: lang }, params[:conceptid]) @concept.children = [] end @root = LinkedData::Client::Models::Class.new @@ -452,7 +454,7 @@ def get_class(params) if ignore_concept_param # get the top level nodes for the root # TODO_REV: Support views? Replace old view call: @ontology.top_level_classes(view) - roots = @ontology.explore.roots(concept_schemes: params[:concept_schemes]) + roots = @ontology.explore.roots(concept_schemes: params[:concept_schemes], lang: lang) if roots.nil? || roots.empty? LOG.add :debug, "Missing roots for #{@ontology.acronym}" not_found("Missing roots for #{@ontology.acronym}") @@ -463,7 +465,7 @@ def get_class(params) # get the initial concept to display root_child = @root.children.first - @concept = root_child.explore.self(full: true) + @concept = root_child.explore.self(full: true, lang: lang) # Some ontologies have "too many children" at their root. These will not process and are handled here. if @concept.nil? LOG.add :debug, "Missing class #{root_child.links.self}" @@ -471,16 +473,16 @@ def get_class(params) end else # if the id is coming from a param, use that to get concept - @concept = @ontology.explore.single_class({full: true}, params[:conceptid]) + @concept = @ontology.explore.single_class({full: true, lang: lang}, params[:conceptid]) if @concept.nil? || @concept.errors LOG.add :debug, "Missing class #{@ontology.acronym} / #{params[:conceptid]}" not_found("Missing class #{@ontology.acronym} / #{params[:conceptid]}") end # Create the tree - rootNode = @concept.explore.tree(include: "prefLabel,hasChildren,obsolete", concept_schemes: params[:concept_schemes]) + rootNode = @concept.explore.tree(include: "prefLabel,hasChildren,obsolete", concept_schemes: params[:concept_schemes], lang: lang) if rootNode.nil? || rootNode.empty? - roots = @ontology.explore.roots(concept_schemes: params[:concept_schemes]) + roots = @ontology.explore.roots(concept_schemes: params[:concept_schemes], lang: lang) if roots.nil? || roots.empty? LOG.add :debug, "Missing roots for #{@ontology.acronym}" not_found("Missing roots for #{@ontology.acronym}") diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index 12035ac7fc..d3058da73a 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -28,8 +28,7 @@ def index @app_name = 'FacetedBrowsing' @app_dir = '/browse' @base_path = @app_dir - ontologies = LinkedData::Client::Models::Ontology.all( -include: LinkedData::Client::Models::Ontology.include_params + ',viewOf', include_views: true, display_context: false) + ontologies = LinkedData::Client::Models::Ontology.all(include: LinkedData::Client::Models::Ontology.include_params + ',viewOf', include_views: true, display_context: false) ontologies_hash = Hash[ontologies.map {|o| [o.id, o] }] @admin = session[:user] ? session[:user].admin? : false @development = Rails.env.development? @@ -39,8 +38,7 @@ def index # The attributes used when retrieving the submission. We are not retrieving all attributes to be faster browse_attributes = 'ontology,acronym,submissionStatus,description,pullLocation,creationDate,released,name,naturalLanguage,hasOntologyLanguage,hasFormalityLevel,isOfType,contact' - submissions = LinkedData::Client::Models::OntologySubmission.all(include_views: true, display_links: false, -display_context: false, include: browse_attributes) + submissions = LinkedData::Client::Models::OntologySubmission.all(include_views: true, display_links: false,display_context: false, include: browse_attributes) submissions_map = Hash[submissions.map {|sub| [sub.ontology.acronym, sub] }] @categories = LinkedData::Client::Models::Category.all(display_links: false, display_context: false) @@ -153,8 +151,8 @@ def classes get_class(params) if @submission.hasOntologyLanguage == 'SKOS' - @schemes = get_schemes(@ontology) - @collections = get_collections(@ontology, add_colors: true) + @schemes = get_schemes(params, @ontology) + @collections = get_collections(params, @ontology, add_colors: true) else @instance_details, type = get_instance_and_type(params[:instanceid]) unless @instance_details.empty? || type.nil? || concept_id_param_exist?(params) @@ -236,8 +234,7 @@ def mappings def new @ontology = LinkedData::Client::Models::Ontology.new - @ontologies = LinkedData::Client::Models::Ontology.all(include: 'acronym', include_views: true, -display_links: false, display_context: false) + @ontologies = LinkedData::Client::Models::Ontology.all(include: 'acronym', include_views: true,display_links: false, display_context: false) @categories = LinkedData::Client::Models::Category.all @groups = LinkedData::Client::Models::Group.all @user_select_list = LinkedData::Client::Models::User.all.map {|u| [u.username, u.id]} @@ -266,9 +263,9 @@ def instances end def schemes - @schemes = get_schemes(@ontology) + @schemes = get_schemes(params, @ontology) scheme_id = params[:scheme_id] || @submission_latest.URI || nil - @scheme = get_scheme(@ontology, scheme_id) if scheme_id + @scheme = get_scheme(params, @ontology, scheme_id) if scheme_id if request.xhr? render partial: 'ontologies/sections/schemes', layout: false @@ -278,9 +275,9 @@ def schemes end def collections - @collections = get_collections(@ontology) + @collections = get_collections(params, @ontology) collection_id = params[:collection_id] - @collection = get_collection(@ontology, collection_id) if collection_id + @collection = get_collection(params, @ontology, collection_id) if collection_id if request.xhr? render partial: 'ontologies/sections/collections', layout: false @@ -292,6 +289,7 @@ def collections # GET /ontologies/ACRONYM # GET /ontologies/1.xml def show + # Hack to make ontologyid and conceptid work in addition to id and ontology params params[:id] = params[:id].nil? ? params[:ontologyid] : params[:id] @@ -340,6 +338,10 @@ def show # Get the latest submission (not necessarily the latest 'ready' submission) @submission_latest = @ontology.explore.latest_submission rescue @ontology.explore.latest_submission(include: '') + + submission_lang = get_submission_languages(@submission_latest.naturalLanguage) + + @submission_lang_options = transform_langs_to_select_options(submission_lang, params[:language]) # Is the ontology downloadable? @ont_restricted = ontology_restricted?(@ontology.acronym) @@ -459,6 +461,25 @@ def widgets private + + def get_submission_languages(submission_natural_language = []) + submission_natural_language.map { |natural_language| natural_language["iso639"] && natural_language.split('/').last }.compact + end + + def transform_langs_to_select_options(langs = [], current_lang = nil) + # Transform each language into a select option + options = langs.map do |lang| + lang = lang.split('/').last + [lang.capitalize, lang.upcase, { selected: lang.casecmp(current_lang) == 0 }] + end + + # If none of the languages are marked as default, mark "All" as default + options.unshift(['All', nil, { selected: options.none? { |option| option[2][:selected] } }]) + + options + end + + def ontology_params p = params.require(:ontology).permit(:name, :acronym, { administeredBy:[] }, :viewingRestriction, { acl:[] }, { hasDomain:[] }, :isView, :viewOf, :subscribe_notifications, {group:[]}) diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index b2c20a2ba3..c01ad0ba83 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -1,14 +1,16 @@ module CollectionsHelper - def get_collections(ontology, add_colors: false) - collections = ontology.explore.collections + def get_collections(params, ontology, add_colors: false) + lang = params[:language]&.upcase&.to_sym + collections = ontology.explore.collections({ include: 'all', lang: lang }) generate_collections_colors(collections) if add_colors collections end - def get_collection(ontology, collection_uri) - ontology.explore.collections({ include: 'all' },collection_uri) + def get_collection(params, ontology, collection_uri) + lang = params[:language]&.upcase&.to_sym + ontology.explore.collections({ include: 'all', lang: lang},collection_uri) end def get_collection_label(collection) diff --git a/app/helpers/ontologies_helper.rb b/app/helpers/ontologies_helper.rb index 6e43ba8193..f0c5bd7ae6 100644 --- a/app/helpers/ontologies_helper.rb +++ b/app/helpers/ontologies_helper.rb @@ -2,6 +2,9 @@ module OntologiesHelper REST_URI = $REST_URL API_KEY = $API_KEY + LANGUAGE_FILTERABLE_SECTIONS = ['classes', 'schemes', 'collections'] + + def additional_details return "" if $ADDITIONAL_ONTOLOGY_DETAILS.nil? || $ADDITIONAL_ONTOLOGY_DETAILS[@ontology.acronym].nil? @@ -386,6 +389,10 @@ def selected_section?(section_title) current_section.eql?(section_title) end + def allowed_to_show_language_filter?(section) + LANGUAGE_FILTERABLE_SECTIONS.include?(section) + end + def lazy_load_section(section_title, &block) if current_section.eql?(section_title) block.call diff --git a/app/helpers/schemes_helper.rb b/app/helpers/schemes_helper.rb index 799e52b131..acb732c148 100644 --- a/app/helpers/schemes_helper.rb +++ b/app/helpers/schemes_helper.rb @@ -1,11 +1,13 @@ module SchemesHelper - def get_schemes(ontology) - ontology.explore.schemes + def get_schemes(params, ontology) + lang = params[:language]&.upcase&.to_sym + ontology.explore.schemes({ include: 'all', lang: lang }) end - def get_scheme(ontology, scheme_uri) - ontology.explore.schemes({ include: 'all' }, scheme_uri) + def get_scheme(params, ontology, scheme_uri) + lang = params[:language]&.upcase&.to_sym + ontology.explore.schemes({ include: 'all', lang: lang }, scheme_uri) end def get_scheme_label(scheme) diff --git a/app/javascript/controllers/chosen_controller.js b/app/javascript/controllers/chosen_controller.js index 38d1a092d4..3293c985fd 100644 --- a/app/javascript/controllers/chosen_controller.js +++ b/app/javascript/controllers/chosen_controller.js @@ -61,9 +61,6 @@ export default class extends Controller { chosenClose.style.position = "unset" chosenClose.style.margin = "auto" - - - } }) } diff --git a/app/javascript/controllers/history_controller.js b/app/javascript/controllers/history_controller.js index d339c32f82..48518c64ac 100644 --- a/app/javascript/controllers/history_controller.js +++ b/app/javascript/controllers/history_controller.js @@ -1,5 +1,5 @@ -import {Controller} from "@hotwired/stimulus" -import {HistoryService} from "../mixins/useHistory"; +import { Controller } from "@hotwired/stimulus" +import { HistoryService } from "../mixins/useHistory"; // Connects to data-controller="history" export default class extends Controller { @@ -7,9 +7,9 @@ export default class extends Controller { this.history = new HistoryService() } updateURL(event) { - const newData = event.detail.data - if (newData !== undefined) { - this.history.updateHistory(document.location.pathname + document.location.search, newData) + const { data } = event.detail + if (data !== undefined && Object.keys(data).length > 0) { + this.history.updateHistory(document.location.pathname + document.location.search, data) } } diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 553504b0ac..448e9b76f4 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -25,6 +25,9 @@ application.register("label-ajax", LabelAjaxController) import LabelsAjaxContainerController from "./labels_ajax_container_controller" application.register("labels-ajax-container", LabelsAjaxContainerController) +import LanguageChangeController from "./language_change_controller" +application.register("language-change", LanguageChangeController) + import LoadChartController from "./load_chart_controller" application.register("load-chart", LoadChartController) @@ -43,6 +46,9 @@ application.register("simple-tree", SimpleTreeController) import SkosCollectionColorsController from "./skos_collection_colors_controller" application.register("skos-collection-colors", SkosCollectionColorsController) +import TabChangeController from "./tab_change_controller" +application.register("tab-change", TabChangeController) + import TooltipController from "./tooltip_controller" application.register("tooltip", TooltipController) diff --git a/app/javascript/controllers/language_change_controller.js b/app/javascript/controllers/language_change_controller.js new file mode 100644 index 0000000000..446a37a7f8 --- /dev/null +++ b/app/javascript/controllers/language_change_controller.js @@ -0,0 +1,18 @@ +import { Controller } from "@hotwired/stimulus" +import { showLoader } from "../mixins/showLoader"; + +export default class extends Controller { + + static targets = ["sections"] + + + onChange(event) { + showLoader(this.sectionsTarget); + + const url = new URL(window.location.href); + url.searchParams.set('language', event.target.value); + + Turbo.visit(url.toString()); + } + +} diff --git a/app/javascript/controllers/tab_change_controller.js b/app/javascript/controllers/tab_change_controller.js new file mode 100644 index 0000000000..3b0c50b3cc --- /dev/null +++ b/app/javascript/controllers/tab_change_controller.js @@ -0,0 +1,35 @@ +import { Controller } from "@hotwired/stimulus" +import { showLoader } from "../mixins/showLoader"; + +export default class extends Controller { + + + static targets = ["sections"] + + + onClick(event) { + + const anchorElement = event.target.closest('a'); + + // add active class to the clicked tab + + if (anchorElement) { + + showLoader(this.sectionsTarget); + + anchorElement.classList.add('active'); + + // remove active class from the other tabs + const otherTabs = anchorElement.parentElement.parentElement.querySelectorAll('a'); + otherTabs.forEach(tab => { + if (tab !== anchorElement) { + tab.classList.remove('active'); + } + }); + + const href = anchorElement.getAttribute('href'); + Turbo.visit(href); + } + + } +} diff --git a/app/javascript/controllers/turbo_frame_controller.js b/app/javascript/controllers/turbo_frame_controller.js index 821bb4fc58..7fbf6603da 100644 --- a/app/javascript/controllers/turbo_frame_controller.js +++ b/app/javascript/controllers/turbo_frame_controller.js @@ -1,11 +1,11 @@ -import {Controller} from "@hotwired/stimulus" -import {HistoryService} from "../mixins/useHistory"; +import { Controller } from "@hotwired/stimulus" +import { HistoryService } from "../mixins/useHistory"; // Connects to data-controller="turbo-frame" export default class extends Controller { static values = { url: String, - placeHolder: {type: String, default: 'Nothing loaded'}, + placeHolder: { type: String, default: 'Nothing loaded' }, } static targets = ['frame'] @@ -14,13 +14,21 @@ export default class extends Controller { } updateFrame(event) { - const newData = event.detail.data - const values = Object.entries(newData)[0][1] - if (values.filter(x => x.length !== 0).length === 0) { + const { data } = event.detail + const values = Object.values(data) + + // remove null and empty values + values.filter((value) => value !== "" || value !== null) + + if (values.length === 0) { this.frame.innerHTML = this.placeHolderValue } else { this.frame.innerHTML = "" - this.urlValue = new HistoryService().getUpdatedURL(this.urlValue, newData); + + this.urlValue ||= window.location.pathname + window.location.search; + + this.urlValue = new HistoryService().getUpdatedURL(this.urlValue, data); + this.frame.src = this.urlValue } } diff --git a/app/javascript/mixins/showLoader.js b/app/javascript/mixins/showLoader.js new file mode 100644 index 0000000000..2235b28c08 --- /dev/null +++ b/app/javascript/mixins/showLoader.js @@ -0,0 +1,16 @@ +const loaderHtml = ` +
+
+
+
+
Loading
+
+
+
+
+`; + +export const showLoader = (element) => { + element.innerHTML = loaderHtml; +} + diff --git a/app/javascript/mixins/useHistory.js b/app/javascript/mixins/useHistory.js index 24a59cbec7..9117735888 100644 --- a/app/javascript/mixins/useHistory.js +++ b/app/javascript/mixins/useHistory.js @@ -1,5 +1,8 @@ export class HistoryService { + unWantedData = ['turbo', 'controller', 'target', 'value'] + + constructor() { this.history = History } @@ -20,27 +23,32 @@ export class HistoryService { } getUpdatedURL(currentUrl, newData) { - const url = new URL(currentUrl, document.location.origin) - const urlParams = url.searchParams - this.#updateURLFromState(urlParams, this.getState()) + const base = document.location.origin + const url = new URL(currentUrl, base) + + this.#updateURLFromState(url.searchParams, this.getState()) + + const wantedData = this.#filterUnwantedData(newData, this.unWantedData); - this.#filterUnwantedData(newData).forEach(([updatedParam, newValue]) => { - newValue = Array.isArray(newValue) ? newValue : [newValue] - if (newValue !== null && Array.from(newValue).length > 0) { - urlParams.set(updatedParam, newValue.join(',')) - } - }) + wantedData.forEach(([updatedParam, newValue]) => { + if (newValue === null) { + url.searchParams.delete(updatedParam) + } else { + newValue = Array.isArray(newValue) ? newValue : [newValue] + url.searchParams.set(updatedParam, newValue.join(',')) + } + }); + return url.pathname + url.search } - #filterUnwantedData(newData){ - const unWantedData = ['turbo', 'controller', 'target', 'value'] - return Object.entries(newData).filter(([key]) => unWantedData.filter(x => key.toLowerCase().includes(x)).length === 0) + #filterUnwantedData(data, unWantedData) { + return Object.entries(data).filter(([key]) => !unWantedData.some(uw => key.toLowerCase().includes(uw.toLowerCase()))) } - #initStateFromUrl(currentUrl) { + #initStateFromUrl(currentUrl) { const url = new URL(currentUrl, document.location.origin) const urlParams = url.searchParams const oldState = this.getState().data diff --git a/app/views/concepts/_show.html.haml b/app/views/concepts/_show.html.haml index 71f02f9a30..040134fec0 100644 --- a/app/views/concepts/_show.html.haml +++ b/app/views/concepts/_show.html.haml @@ -1,5 +1,6 @@ = render TurboFrameComponent.new(id: 'concept_show', data: {controller:'labels-ajax-container', 'action': 'turbo:before-fetch-request->labels-ajax-container#abortAll', 'labels-ajax-container-label-ajax-outlet': '#concept_show a[data-controller="label-ajax"]'}) do + / When we have an ontology with a flat hierarchy, we initially disable the tabs because we don't have a class to display - if @concept.id.eql?("bp_fake_root") %div{:style => "padding: 100px 0; font-size: larger; font-weight: bold; text-align: center;"} diff --git a/app/views/layouts/_ontology_viewer.html.haml b/app/views/layouts/_ontology_viewer.html.haml index 709267865b..729ebc79e2 100644 --- a/app/views/layouts/_ontology_viewer.html.haml +++ b/app/views/layouts/_ontology_viewer.html.haml @@ -94,33 +94,43 @@ = link_to(sub.documentation, "aria-label": "Ontology documentation", title: "Ontology documentation", target: "_blank") do %i.fas.fa-lg.fa-book-reader{"aria-hidden": true} - unless sub.publication.nil? - = link_to(sub.publication, "aria-label": "Ontology publications", title: "Ontology publications", target: "_blank") do - %i.fas.fa-lg.fa-book{"aria-hidden": true} + - sub.publication.each do |pub| + = link_to(pub, "aria-label": "Ontology publications", title: "Ontology publications", target: "_blank") do + %i.fas.fa-lg.fa-book{"aria-hidden": true} - if @ontology.admin?(session[:user]) = link_to(edit_ontology_path(@ontology.acronym), "aria-label": "Edit ontology details", title: "Edit ontology details") do %i.fas.fa-lg.fa-user-edit - %div.row.pb-4 - %div.col - %div.card - %div.card-header - - sections = sections_to_show - -# Tabbed navigation bar for ontology content sections - %ul.nav.nav-tabs.card-header-tabs{id: "navbar-ontology", role: "tablist"} - - sections.each do |section| - %li.nav-item - = link_to(section_name(section) , ontology_path(@ontology.acronym, p: section), - id: "ont-#{section}-tab", class: "nav-link #{selected_section?(section) ? 'active show' : ''}", - data: {toggle: "tab", target: "#ont_#{section}_content", 'bp-ont-page': section , - 'bp-ont-page-name': ontology_viewer_page_name(@ontology.name, @concept&.prefLabel || '', section) }) - %div.card-body - %div.tab-content - - sections.each do |section| - %div.tab-pane{id: "ont_#{section}_content", class: selected_section?(section) ? 'active show' : ''} - = lazy_load_section(section) do - - yield + = turbo_frame_tag 'sections', data: { controller: "language-change tab-change"} do + %div.row.pb-4 + %div.col + %div.card + %div.card-header + - sections = sections_to_show + -# Tabbed navigation bar for ontology content sections + %div{style: "display: flex; justify-content: space-between;"} + %ul.nav.nav-tabs.card-header-tabs{id: "navbar-ontology", role: "tablist"} + - sections.each do |section| + %li.nav-item + = link_to(section_name(section) , ontology_path(@ontology.acronym, p: section), + class: "nav-link #{selected_section?(section) ? 'active show' : ''}", + data: {'bp-ont-page': section , action: "click->tab-change#onClick", + 'bp-ont-page-name': ontology_viewer_page_name(@ontology.name, @concept&.prefLabel || '', section) }) + + - if allowed_to_show_language_filter?(current_section) + = select_tag :category, options_for_select(@submission_lang_options), data: { action: "change->language-change#onChange"}, id: "category-select", style: "background-color: #f2f2f2; color: #333; font-size: 14px; padding: 2px 6px 2px 6px; border: 1px solid #ccc; border-radius: 4px;outline: none;" + + %div.card-body + %div.tab-content{"data-target": "language-change.sections tab-change.sections"} + - sections.each do |section| + %div.tab-pane{id: "ont_#{section}_content", class: selected_section?(section) ? 'active show' : ''} + = lazy_load_section(section) { yield } + = render partial: "layouts/footer" + + + diff --git a/app/views/ontologies/sections/visualize.html.haml b/app/views/ontologies/sections/visualize.html.haml index 14dee34229..08bac952a3 100644 --- a/app/views/ontologies/sections/visualize.html.haml +++ b/app/views/ontologies/sections/visualize.html.haml @@ -1,4 +1,4 @@ -= turbo_frame_tag 'classes' do += turbo_frame_tag 'concepts' do - unless @error - @title = "#{@ontology.name} - #{@concept.prefLabel}" - @new_term_request_ontologies