From 4aec3ec42f4817cb93496e8febd4e242279b6c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szczepanik?= Date: Wed, 1 May 2024 00:30:34 +0200 Subject: [PATCH] Apply changes from catalog upstream Changes introduced in catalog upstream were introduced by running `datalad catalog-create -c docs --force` and using Git to stage or discard specific changes. The only file which needed staging in chunks was index.html, and one property had to be added to the config file by hand (catalog_url). This uses: datalad/datalad-catalog/commit/821b12f51302b9a830dee00ac5f4f247aed0532e Closes #93, fixes #92, fixes #88 --- docs/README.md | 8 +- docs/assets/app.js | 27 +- docs/assets/app_component_contexttab.js | 33 ++ docs/assets/app_component_dataset.js | 503 +++++++++++++++++++---- docs/assets/app_globals.js | 61 ++- docs/assets/app_router.js | 6 +- docs/assets/style.css | 2 +- docs/config.json | 1 + docs/index.html | 1 + docs/schema/jsonschema_dataset.json | 5 + docs/templates/context-tab-template.html | 70 ++++ docs/templates/dataset-template.html | 174 ++++---- 12 files changed, 675 insertions(+), 216 deletions(-) create mode 100644 docs/assets/app_component_contexttab.js create mode 100644 docs/templates/context-tab-template.html diff --git a/docs/README.md b/docs/README.md index a2e6b79de..ca8417139 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,16 +20,16 @@ The `artwork` and `assets` directories contain images and web assets (such as Ja ## Serving the content -Since this site is self-contained and static, no further build processes, server-side implementations, or access to content delivery networks (CDNs) are necessary in order to serve the content. All that is needed is a simple HTTP server. +Since this site is self-contained and static, no further build processes or access to content delivery networks (CDNs) are necessary in order to serve the content. All that is needed is a simple HTTP server with one specific addition - a custom redirect. This is required because the application makes use of [Vue Router's history mode](https://v3.router.vuejs.org/guide/essentials/history-mode.html#html5-history-mode), which requires a server-side redirect configuration to deal with the fact that a VueJS application is actually a single-page app. -This can be achieved locally, for example using Python: +For serving the content locally, this is already taken care of in `datalad-catalog`, and you can simply run the following: ```bash cd path/to/catalog/directory -python3 -m http.server +datalad catalog-serve -c . ``` -The content can also be hosted and served online. A straightforward and free way to achieve this is via GitHub and [GitHub Pages](https://pages.github.com/). After publishing this content as a GitHub repository, you can activate GitHub Pages in the repository's settings. See detailed instructions [here](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site). +The content can also be hosted and served online. A straightforward way to achieve this, and one which has a free tier, is via [Netlify](https://www.netlify.com/) (the common alternative, [GitHub Pages](https://pages.github.com/), does not currently support server-side redirects). After publishing this content, e.g. as a GitHub repository, you can link that repository to a site on Netlify. [See here](https://docs.netlify.com/routing/redirects/) how to set up redirects with Netlify. Of course, the site can also be served from your preferred server setup, as long as page redirects can be supported. ## Maintaining content diff --git a/docs/assets/app.js b/docs/assets/app.js index 17e195849..858c95bc8 100644 --- a/docs/assets/app.js +++ b/docs/assets/app.js @@ -11,6 +11,7 @@ var datacat = new Vue({ links: {}, dataset_options: {}, config_ready: false, + catalog_config: {}, }, methods: { gotoHome() { @@ -22,25 +23,9 @@ var datacat = new Vue({ gotoExternal(dest) { window.open(dest); }, - async load() { - // Load templates - await Promise.all( - Object.keys(template_paths).map(async (key, index) => { - url = template_dir + "/" + template_paths[key] - fetch(url). - then(response => { - return response.text(); - }). - then(text => { - console.log('template loaded: '+key) - console.log(text) - document.getElementById(key).innerHTML = text; - }); - }) - ) - } }, beforeCreate() { + console.debug("Executing lifecycle hook: beforeCreate") fetch(config_file) .then((response) => { if (response.ok) { @@ -54,6 +39,14 @@ var datacat = new Vue({ }) .then((responseJson) => { obj = responseJson; + // first ensure that the config has all required fields; + // if some are missing, fill them in from default_config + for (const [key, value] of Object.entries(default_config)) { + if (!obj.hasOwnProperty(key) ) { + obj[key] = value; + } + } + this.catalog_config = obj // set social links this.social_links = obj.social_links // set dataset options diff --git a/docs/assets/app_component_contexttab.js b/docs/assets/app_component_contexttab.js new file mode 100644 index 000000000..efe28aa98 --- /dev/null +++ b/docs/assets/app_component_contexttab.js @@ -0,0 +1,33 @@ +// Component definition: an "additional tab" with context +Vue.component('context-tab-body', function (resolve, reject) { + url = template_dir + "/context-tab-template.html" + fetch(url). + then(response => { + return response.text(); + }). + then(text => { + resolve( + { + template: text, + props: { + tabby: Object, + }, + data: function () { + return { + context_tab_ready: false, + }; + }, + computed: {}, + methods: { + toUpperString(str_in) { + return str_in.charAt(0).toUpperCase() + str_in.slice(1) + } + }, + async created() { + this.context_tab_ready = true; + // console.log(new_tab) + } + } + ) + }); +}); \ No newline at end of file diff --git a/docs/assets/app_component_dataset.js b/docs/assets/app_component_dataset.js index 339c5614b..acb2a2351 100644 --- a/docs/assets/app_component_dataset.js +++ b/docs/assets/app_component_dataset.js @@ -14,6 +14,9 @@ const datasetView = () => dataPath: [], showCopyTooltip: false, showCopyCiteTooltip: false, + showCopyDatasetAliasTooltip: false, + showCopyDatasetIdTooltip: false, + showCopyDatasetFullTooltip: false, tabIndex: 0, sort_name: true, sort_modified: true, @@ -34,20 +37,18 @@ const datasetView = () => files_ready: false, tags_ready: false, description_ready: false, - description_display: "", citation_busy: false, citation_text: "", invalid_doi: false, - // show_backbutton: false, }; }, watch: { subdatasets_ready: function (newVal, oldVal) { if (newVal) { - console.log("Watched property: subdatasets_ready = true") - console.log("Subdatasets have been fetched:"); + console.debug("Watched property: subdatasets_ready = true") + console.debug("Subdatasets have been fetched:"); this.subdatasets = this.selectedDataset.subdatasets; - console.log(this.subdatasets); + console.debug(this.subdatasets); tags = this.tag_options; if (this.subdatasets) { this.subdatasets.forEach((subds, index) => { @@ -57,20 +58,25 @@ const datasetView = () => ); } }); - } + } this.tag_options = tags; this.tag_options_filtered = this.tag_options; this.tag_options_available = this.tag_options; this.tags_ready = true; + } }, dataset_ready: function (newVal, oldVal) { - // TODO: some of these methods/steps should be moved to the generatpr tool. e.g. shortname + // The code in this function is executed once the properties, + // children, variables, and everything related to the currently + // selected dataset is deemed "ready". This means subdatasets + // have been fetched and some subdataset properties have been + // collected to assist dataset-level UX (including search tags) if (newVal) { - console.log("Watched property: dataset_ready = true") - console.log("Active dataset:"); + console.debug("Watched property: dataset_ready = true") + console.debug("Active dataset:"); dataset = this.selectedDataset; - console.log(this.selectedDataset); + console.debug(this.selectedDataset); disp_dataset = {}; // Set name to unknown if not available if (!dataset.hasOwnProperty("name") || !dataset["name"]) { @@ -158,6 +164,13 @@ const datasetView = () => } } else { disp_dataset.url = dataset.url; + if (disp_dataset.url && dataset.url.toLowerCase().indexOf("gin.g-node") >= 0) { + disp_dataset.is_gin = true; + disp_dataset.url = disp_dataset.url.replace('ssh://', ''); + disp_dataset.url = disp_dataset.url.replace('git@gin.g-node.org:', 'https://gin.g-node.org'); + disp_dataset.url = disp_dataset.url.replace('git@gin.g-node.org', 'https://gin.g-node.org'); + disp_dataset.url = disp_dataset.url.replace('.git', ''); + } } // Description if ( @@ -202,19 +215,40 @@ const datasetView = () => else { disp_dataset.show_export = false } - // Additional display and definitions - disp_dataset.additional_tabs = dataset.additional_display - disp_dataset.display_tabs = [] - disp_dataset.additional_tab_defs = dataset.additional_display_definitions - disp_dataset.additional_tab_count = - Array.isArray(disp_dataset.additional_tabs) ? disp_dataset.additional_tabs.length : 0 - for (var t=0; t 0 ) { + disp_dataset.show_binder_button = true + } + + // Set correct URL query string to mirrorif keyword(s) included in query parameters + if (this.$route.query.hasOwnProperty("keyword")) { + query_keywords = this.$route.query.keyword + // if included keywords(s) not null or empty string/array/object + if (query_keywords) { + console.debug("Keywords in query parameter taken from vue route:") + console.debug(query_keywords) + // if included keywords(s) = array + if ((query_keywords instanceof Array || Array.isArray(query_keywords)) + && query_keywords.length > 0) { + // add all search tags + for (const el of query_keywords) { + console.log(`adding to search tags: ${el}`) + this.addSearchTag(el) + } + } else { + this.addSearchTag(query_keywords) + } + } } - disp_dataset.additional_tab_render = {} - // Write main derived variable and set to ready - this.displayData = disp_dataset; - this.display_ready = true; // Add json-ld data to head var scripttag = document.getElementById("structured-data") @@ -224,14 +258,56 @@ const datasetView = () => scripttag.setAttribute("id", "structured-data"); document.head.appendChild(scripttag); } + keys_to_populate = [ + "name", // Text + "description", // Text + "alternateName", // Text + "creator", // Person or Organization + "citation", // Text or CreativeWork + "funder", // Person or Organization + "hasPart", // URL or Dataset + // "isPartOf", // URL or Dataset + "identifier", // URL, Text, or PropertyValue + // "isAccessibleForFree", // Boolean + "keywords", // Text + "license", // URL or CreativeWork + // "measurementTechnique", // Text or URL + "sameAs", // URL + // "spatialCoverage", // Text or Place + // "temporalCoverage", // Text + // "variableMeasured", // Text or PropertyValue + "version", // Text or Number + // "url", // URL + "includedInDataCatalog", // DataCatalog + // "distribution", // DataDownload + ] obj = { "@context": "https://schema.org/", "@type": "Dataset", - "name": this.displayData.display_name ? this.displayData.display_name : "", - "description": this.selectedDataset.description ? this.selectedDataset.description : "" } - scripttag.textContent = JSON.stringify(obj); + for (var k=0; k { + if(response.status == 404) { + this.selectedDataset.has_id_path = false + } else { + this.selectedDataset.has_id_path = true + } + }) + .catch(error => { + // do nothing + }) + // Write main derived variable and set to ready + this.displayData = disp_dataset; + this.display_ready = true; + console.debug("Watched property function completed: dataset_ready = true") } }, }, @@ -305,14 +381,189 @@ const datasetView = () => } else { return true } - } + }, }, methods: { newTabActivated(newTabIndex, prevTabIndex, bvEvent) { var tabs = this.selectedDataset.available_tabs + console.debug( + "%c> USER INPUT: new tab selected (%s)", + "color: white; font-style: italic; background-color: blue", + tabs[newTabIndex] + ); if (tabs[newTabIndex] == 'content') { this.getFiles() } + // Update URL query string whenever a new tab is selected + this.updateURLQueryString(this.$route, newTabIndex) + }, + updateURLQueryString(current_route, tab_index = null) { + // This function is called from: + // - addSearchTag(): when adding a search tag (a.k.a. keyword) - WITHOUT tab_index param + // - removeSearchTag(): when removing a search tag (a.k.a. keyword) - WITHOUT tab_index param + // - newTabActivated(): when a new tab is selected or programmatically activated - WITH tab_index param + console.debug("---\nUpdating URL query string\n---") + console.debug("- Argument tab_index: %i", tab_index) + console.debug("- Before: Vue Route query params: %s", JSON.stringify(Object.assign({}, current_route.query))) + let url_qp = new URL(document.location.toString()).searchParams + console.debug("- Before: URL query string: %s", url_qp.toString()) + var query_tab + if (Number.isInteger(tab_index)) { + query_tab = this.selectedDataset.available_tabs[tab_index]; + } else { + default_tab = this.$root.selectedDataset.config?.dataset_options?.default_tab + query_tab = url_qp.get("tab") ? url_qp.get("tab") : (default_tab ? default_tab : "content") + } + query_string = '?tab=' + query_tab + if (this.search_tags.length > 0) { + for (const element of this.search_tags) { + query_string = query_string + '&keyword=' + element + } + } + history.replaceState( + {}, + null, + current_route.path + query_string + ) + console.debug("- After: Vue Route query params: %s", JSON.stringify(Object.assign({}, this.$route.query))) + let url_qp2 = new URL(document.location.toString()).searchParams + console.debug("- After: URL query string: %s", url_qp2.toString()) + }, + getRichData(key, selectedDS, displayDS) { + switch (key) { + case "name": + return displayDS.display_name ? displayDS.display_name : "" + case "description": + return selectedDS.description ? selectedDS.description : "" + case "alternateName": + // use alias if present + return [selectedDS.alias ? selectedDS.alias : ""] + case "creator": + // authors + return selectedDS.authors?.map( (auth) => { + return { + "@type": "Person", + "givenName": auth.givenName ? auth.givenName : null, + "familyName": auth.familyName ? auth.familyName : null, + "name": auth.name ? auth.name : null, + "sameAs": this.getAuthorORCID(auth), + } + }) + case "citation": + // from publications + return selectedDS.publications?.map( (pub) => { + return pub.doi + }) + case "funder": + // from funding + return selectedDS.funding?.map( (fund) => { + var fund_obj = { + "@type": "Organization", + "name": fund.funder ? fund.funder : (fund.name ? fund.name : (fund.description ? fund.description : null)), + } + var sameas = this.getFunderSameAs(fund) + if (sameas) { + fund_obj["sameAs"] = sameas + } + return fund_obj + }) + case "hasPart": + // from subdatasets + var parts = selectedDS.subdatasets?.map( (ds) => { + return { + "@type": "Dataset", + "name": ds.dirs_from_path[ds.dirs_from_path.length - 1] + } + }) + return parts.length ? parts : null + // case "isPartOf": + case "identifier": + // use DOI + return selectedDS.doi ? selectedDS.doi : null + // "isAccessibleForFree", // Boolean + case "keywords": + return selectedDS.keywords?.length ? selectedDS.keywords : null + case "license": + return selectedDS.license?.url ? selectedDS.license.url : null + // "measurementTechnique", // Text or URL + case "sameAs": + // homepage + if (selectedDS.additional_display && selectedDS.additional_display.length) { + for (var t=0; t 0) { + orcid_element = author.identifiers.filter( + (x) => x.name === "ORCID" + ); + if (orcid_element.length > 0) { + orcid_code = orcid_element[0].identifier + const prefix = "https://orcid.org/" + return orcid_code.indexOf(prefix) >= 0 ? orcid_code : prefix + orcid_code + } else { + return null + } + } else { + return null + } + }, + getFunderSameAs(fund) { + const common_funders = [ + { + "name": "Deutsche Forschungsgemeinschaft", + "alternate_name": "DFG", + "ror": "https://ror.org/018mejw64" + }, + { + "name": "National Science Foundation", + "alternate_name": "NSF", + "ror": "https://ror.org/021nxhr62" + } + ] + for (var i=0; i= 0 || + fund.name?.indexOf(cf.name) >= 0 || + fund.description?.indexOf(cf.name) >= 0 || + fund.funder?.indexOf(cf.alternate_name) >= 0 || + fund.name?.indexOf(cf.alternate_name) >= 0 || + fund.description?.indexOf(cf.alternate_name) >= 0 ) { + return cf.ror + } + } + return null }, copyCloneCommand(index) { // https://stackoverflow.com/questions/60581285/execcommand-is-now-obsolete-whats-the-alternative @@ -333,6 +584,35 @@ const datasetView = () => this.showCopyTooltip = false; }, 1000); }, + copyDatasetURL(url_type) { + // https://stackoverflow.com/questions/60581285/execcommand-is-now-obsolete-whats-the-alternative + // https://www.sitepoint.com/clipboard-api/ + urlmap = { + 'alias': "showCopyDatasetAliasTooltip", + 'id': "showCopyDatasetIdTooltip", + 'full': "showCopyDatasetFullTooltip", + } + selectText = document.getElementById(url_type + "_url").textContent; + selectText = '\n ' + selectText + ' \n\n ' + selectText = selectText.replace(/^\s+|\s+$/g, ''); + navigator.clipboard + .writeText(selectText) + .then(() => {}) + .catch((error) => { + alert(`Copy failed! ${error}`); + }); + this[urlmap[url_type]] = true; + }, + hideURLTooltipLater(url_type) { + setTimeout(() => { + urlmap = { + 'alias': "showCopyDatasetAliasTooltip", + 'id': "showCopyDatasetIdTooltip", + 'full': "showCopyDatasetFullTooltip", + } + this[urlmap[url_type]] = false; + }, 1000); + }, copyCitationText(index) { // https://stackoverflow.com/questions/60581285/execcommand-is-now-obsolete-whats-the-alternative // https://www.sitepoint.com/clipboard-api/ @@ -351,8 +631,12 @@ const datasetView = () => }, 1000); }, async selectDataset(event, obj, objId, objVersion, objPath) { - console.log("Inside selectDataset") - console.log(event) + console.debug( + "%c> USER INPUT: dataset selected", + "color: white; font-style: italic; background-color: blue", + ); + console.debug("Inside selectDataset") + console.debug(event) event.preventDefault() var newBrowserTab = event.ctrlKey || event.metaKey || (event.button == 1) if (obj != null) { @@ -369,15 +653,28 @@ const datasetView = () => dataset_id: objId, dataset_version: objVersion, }, + query: {}, } // before navigation, clear filtering options this.clearFilters() // now navigate if (newBrowserTab) { const routeData = router.resolve(route_info); + console.log(routeData) window.open(routeData.href, '_blank'); } else { + this.search_tags = [] + // The following commented out code is an attempt to fix remaining + // bugs with query strings that remain in the url when navigating to + // a subdataset. This code tries to set the query string to null first, + // by replacing the state, before pushing the next route via vue router. + // It didn't seem to solve the issue, but should be investigated more. + // history.replaceState( + // {}, + // null, + // this.$route.path + // ) router.push(route_info); } } else { @@ -390,30 +687,11 @@ const datasetView = () => this.search_tags = [] this.clearSearchTagText() }, - selectDescription(desc) { - if (desc.content.startsWith("path:")) { - this.description_ready = false; - filepath = desc.content.split(":")[1]; - extension = "." + filepath.split(".")[1]; - desc_file = getFilePath( - this.selectedDataset.dataset_id, - this.selectedDataset.dataset_version, - desc.path, - extension - ); - fetch(desc_file) - .then((response) => response.blob()) - .then((blob) => blob.text()) - .then((markdown) => { - this.description_display = marked.parse(markdown); - this.description_ready = true; - }); - } else { - this.description_display = desc.content; - this.description_ready = true; - } - }, gotoHome() { + console.debug( + "%c> USER INPUT: home selected", + "color: white; font-style: italic; background-color: blue", + ); // if there is NO home page set: // - if there is a tab name in the URL, navigate to current // - else: don't navigate, only "reset" @@ -431,20 +709,18 @@ const datasetView = () => }, } if (!this.catalogHasHome()) { - if (this.$route.params.tab_name) { - router.push(current_route_info) - } else { this.clearFilters(); this.tabIndex = this.getDefaultTabIdx(); - } + // Note: no need to call updateURLQueryString() here because a change to + // this.tabIndex will automatically call newTabActivated() (because of + // v-model="tabIndex"), which in turn calls updateURLQueryString(). } else { if (this.currentIsHome()) { - if (this.$route.params.tab_name) { - router.push(current_route_info) - } else { - this.clearFilters(); - this.tabIndex = this.getDefaultTabIdx(); - } + this.clearFilters(); + this.tabIndex = this.getDefaultTabIdx(); + // Note: no need to call updateURLQueryString() here because a change to + // this.tabIndex will automatically call newTabActivated() (because of + // v-model="tabIndex"), which in turn calls updateURLQueryString(). } else { router.push({ name: "home" }); } @@ -482,10 +758,9 @@ const datasetView = () => openWithBinder(dataset_url, current_dataset) { const environment_url = "https://mybinder.org/v2/gh/datalad/datalad-binder/main"; - var content_url = "https://github.com/jsheunis/datalad-notebooks"; - var content_repo_name = "datalad-notebooks"; - var notebook_name = "download_data_with_datalad_python.ipynb"; - + const content_url = "https://github.com/jsheunis/datalad-notebooks"; + const content_repo_name = "datalad-notebooks"; + const notebook_name = "download_data_with_datalad_python.ipynb"; if (current_dataset.hasOwnProperty("notebooks") && current_dataset.notebooks.length > 0) { // until including the functionality to select from multiple notebooks in a dropdown, just select the first one notebook = current_dataset.notebooks[0] @@ -493,7 +768,6 @@ const datasetView = () => content_repo_name = content_url.substring(content_url.lastIndexOf('/') + 1) notebook_name = notebook.notebook_path } - binder_url = environment_url + "?urlpath=git-pull%3Frepo%3D" + @@ -525,6 +799,7 @@ const datasetView = () => this.search_tags.push(option); this.clearSearchTagText(); this.filterTags(); + this.updateURLQueryString(this.$route) }, removeSearchTag(tag) { idx = this.search_tags.indexOf(tag); @@ -532,6 +807,7 @@ const datasetView = () => this.search_tags.splice(idx, 1); } this.filterTags(); + this.updateURLQueryString(this.$route) }, clearSearchTagText() { this.tag_text = ""; @@ -582,8 +858,9 @@ const datasetView = () => fetch(doi, { headers }) .then((response) => response.text()) .then((data) => { - this.selectedDataset.citation_text = data; - console.log(data); + // strip html tags from response text + let doc = new DOMParser().parseFromString(data, 'text/html'); + this.selectedDataset.citation_text = doc.body.textContent || ""; this.citation_busy = false; }); } else { @@ -606,6 +883,7 @@ const datasetView = () => // If a tab parameter is supplied via the router, navigate to that tab if // part of available tabs, otherwise default tab else { + tab_param = Array.isArray(tab_param) ? tab_param[0] : tab_param selectTab = available_tabs.indexOf(tab_param.toLowerCase()) if (selectTab >= 0) { this.tabIndex = selectTab; @@ -620,15 +898,40 @@ const datasetView = () => var idx = this.selectedDataset.available_tabs.indexOf(default_tab.toLowerCase()) return idx >= 0 ? idx : 0 }, + clickBackButton() { + console.debug( + "%c> USER INPUT: backbutton clicked", + "color: white; font-style: italic; background-color: blue", + ); + history.back() + }, }, async beforeRouteUpdate(to, from, next) { - console.log("Executing navigation guard: beforeRouteUpdate") + console.debug("Executing navigation guard: beforeRouteUpdate") this.subdatasets_ready = false; this.dataset_ready = false; - file = getFilePath(to.params.dataset_id, to.params.dataset_version, null); response = await fetch(file); text = await response.text(); + response_obj = JSON.parse(text); + // if the object.type is redirect (i.e. the url parameter is an alias for or ID + // of the dataset) replace the current route with one containing the actual id + // and optionally version + if (response_obj["type"] == "redirect") { + route_params = { + dataset_id: response_obj.dataset_id, + } + if (response_obj.dataset_version) { + route_params.dataset_version = response_obj.dataset_version + } + const replace_route_info = { + name: "dataset", + params: route_params, + query: to.query, + } + await router.replace(replace_route_info) + return; + } this.$root.selectedDataset = JSON.parse(text); this.$root.selectedDataset.name = this.$root.selectedDataset.name ? this.$root.selectedDataset.name @@ -649,8 +952,6 @@ const datasetView = () => this.$root.selectedDataset.keywords = this.$root.selectedDataset.keywords ? this.$root.selectedDataset.keywords : []; - this.dataset_ready = true; - if ( this.$root.selectedDataset.hasOwnProperty("subdatasets") && this.$root.selectedDataset.subdatasets instanceof Array && @@ -731,9 +1032,9 @@ const datasetView = () => this.$root.selectedDataset.has_files = false; } // Now list all tabs and set the correct one - // order in DOM: content, subdatasets, publications, funding, provenance, + // order in DOM: content, datasets, publications, funding, provenance, sDs = this.$root.selectedDataset - available_tabs = ['content', 'subdatasets'] + available_tabs = ['content', 'datasets'] standard_tabs = ['publications', 'funding', 'provenance'] // add available standard tabs for (var t=0; t this.$root.selectedDataset.config = config; } // Set the correct tab to be rendered + correct_tab = to.query.hasOwnProperty("tab") ? to.query.tab : null this.setCorrectTab( - to.params.tab_name, + correct_tab, available_tabs_lower, this.$root.selectedDataset.config?.dataset_options?.default_tab ) + this.dataset_ready = true; + console.debug("Finished navigation guard: beforeRouteUpdate") next(); }, async created() { - console.log("Executing lifecycle hook: created") + this.dataset_ready = false; + this.subdatasets_ready = false; + console.debug("Executing lifecycle hook: created") // fetch superfile in order to set id and version on $root homefile = metadata_dir + "/super.json"; homeresponse = await fetch(homefile); @@ -797,8 +1103,26 @@ const datasetView = () => return; } text = await response.text(); + response_obj = JSON.parse(text); + // if the object.type is redirect (i.e. the url parameter is an alias for or ID + // of the dataset) replace the current route with one containing the actual id + // and optionally version + if (response_obj["type"] == "redirect") { + route_params = { + dataset_id: response_obj.dataset_id, + } + if (response_obj.dataset_version) { + route_params.dataset_version = response_obj.dataset_version + } + const replace_route_info = { + name: "dataset", + params: route_params, + query: this.$route.query, + } + await router.replace(replace_route_info) + return; + } app.selectedDataset = JSON.parse(text); - this.dataset_ready = true; if ( this.$root.selectedDataset.hasOwnProperty("subdatasets") && this.$root.selectedDataset.subdatasets instanceof Array && @@ -863,9 +1187,9 @@ const datasetView = () => this.$root.selectedDataset.has_files = false; } // Now list all tabs and set the correct one - // order in DOM: content, subdatasets, publications, funding, provenance, + // order in DOM: content, datasets, publications, funding, provenance, sDs = this.$root.selectedDataset - available_tabs = ['content', 'subdatasets'] + available_tabs = ['content', 'datasets'] standard_tabs = ['publications', 'funding', 'provenance'] // add available standard tabs for (var t=0; t config = JSON.parse(configtext); this.$root.selectedDataset.config = config; } - // Set the correct tab to be rendered + // --- + // Note for future: Handle route query parameters (tab and keyword) here? + // --- + // This point in the code is reached from an explicit URL navigation to a dataset + // (via home and/or via alias and/or via dataset-id, or directly to a dataset-version. + // This means that explicit query parameters will be in the $route object. + // (Note: when the same point is reached in the beforeRouteUpdate function, + // it means that navigation happened from within the catalog (i.e. not explicitly / externally) + // This means a new dataset page will be opened from the content tab or from the datasets tab of + // the dataset that is being navigated away from. This means we do not want keyword or tab parameters to pass through. + correct_tab = this.$route.query.hasOwnProperty("tab") ? this.$route.query.tab : null this.setCorrectTab( - this.$route.params.tab_name, + correct_tab, available_tabs_lower, this.$root.selectedDataset.config?.dataset_options?.default_tab ) + this.dataset_ready = true; + console.debug("Finished lifecycle hook: created") }, mounted() { - console.log("Executing lifecycle hook: mounted") + console.debug("Executing lifecycle hook: mounted") this.tag_options_filtered = this.tag_options; this.tag_options_available = this.tag_options; + console.debug("Finished lifecycle hook: mounted") } } - }) + }) \ No newline at end of file diff --git a/docs/assets/app_globals.js b/docs/assets/app_globals.js index 8991d7dcc..c9da91402 100644 --- a/docs/assets/app_globals.js +++ b/docs/assets/app_globals.js @@ -9,16 +9,17 @@ const superdatasets_file = metadata_dir + "/super.json"; const SPLIT_INDEX = 3; const SHORT_NAME_LENGTH = 0; // number of characters in name to display, zero if all const default_config = { - catalog_name: "DataCat", + catalog_name: "DataCat Demo", + catalog_url: "https://datalad-catalog.netlify.app/", link_color: "#fba304", link_hover_color: "#af7714", logo_path: "/artwork/catalog_logo.svg", social_links: { about: null, - documentation: null, - github: null, - mastodon: null, - x: null + documentation: "https://docs.datalad.org/projects/catalog/en/latest/", + github: "https://github.com/datalad/datalad-catalog", + mastodon: "https://fosstodon.org/@datalad", + x: "https://x.com/datalad" }, dataset_options: { include_metadata_export: true, @@ -56,15 +57,24 @@ async function grabSubDatasets(app) { function getFilePath(dataset_id, dataset_version, path, ext = ".json") { // Get node file location from dataset id, dataset version, and node path // using a file system structure similar to RIA stores - file = metadata_dir + "/" + dataset_id + "/" + dataset_version; - blob = dataset_id + "-" + dataset_version; - if (path) { - blob = blob + "-" + path; + // - dataset_id is required, all the other parameters are optional + // - dataset_id could either be an actual dataset ID or an alias + file = metadata_dir + "/" + dataset_id + blob = dataset_id + if (dataset_version) { + file = file + "/" + dataset_version; + blob = blob + "-" + dataset_version; + // path to file only makes sense with the context of a dataset id AND version + if (path) { + blob = blob + "-" + path; + } + blob = md5(blob); + blob_parts = [blob.substring(0, SPLIT_INDEX), blob.substring(SPLIT_INDEX)]; + return file + "/" + blob_parts[0] + "/" + blob_parts[1] + ext; + } else { + blob = md5(blob); + return file + "/" + blob + ext; } - blob = md5(blob); - blob_parts = [blob.substring(0, SPLIT_INDEX), blob.substring(SPLIT_INDEX)]; - file = file + "/" + blob_parts[0] + "/" + blob_parts[1] + ext; - return file; } function getRelativeFilePath(dataset_id, dataset_version, path) { @@ -94,6 +104,25 @@ async function checkFileExists(url) { } } -function toUpperString(str_in) { - return str_in.charAt(0).toUpperCase() + str_in.slice(1) -} \ No newline at end of file +function pruneObject(obj) { + const newObj = {}; + Object.entries(obj).forEach(([k, v]) => { + if (typeof v === 'object' && !Array.isArray(v) && v !== null) { + newObj[k] = pruneObject(v); + } else if ((v instanceof Array || Array.isArray(v)) && v.length > 0) { + newArr = [] + for (const el of v) { + if (typeof el === 'object' && !Array.isArray(el) && el !== null) { + newArr.push(pruneObject(el)) + } else if (el != null) { + newArr.push(el) + } + } + newObj[k] = newArr; + } else if (v != null) { + newObj[k] = obj[k]; + } + }); + return newObj; +} + diff --git a/docs/assets/app_router.js b/docs/assets/app_router.js index ada205acc..e1aa65e9b 100644 --- a/docs/assets/app_router.js +++ b/docs/assets/app_router.js @@ -8,6 +8,7 @@ const routes = [ path: "/", name: "home", beforeEnter: (to, from, next) => { + console.debug("Executing navigation guard: beforeEnter - route '/')") const superfile = metadata_dir + "/super.json"; // https://www.dummies.com/programming/php/using-xmlhttprequest-class-properties/ var rawFile = new XMLHttpRequest(); @@ -22,6 +23,7 @@ const routes = [ dataset_id: superds["dataset_id"], dataset_version: superds["dataset_version"], }, + query: to.query, }); next(); } else if (rawFile.status === 404) { @@ -36,7 +38,7 @@ const routes = [ }, }, { - path: "/dataset/:dataset_id/:dataset_version/:tab_name?", + path: "/dataset/:dataset_id/:dataset_version?", component: datasetView, name: "dataset", }, @@ -45,7 +47,7 @@ const routes = [ path: '/:catchAll(.*)', component: notFound, name: '404' - } + }, ]; // Create router diff --git a/docs/assets/style.css b/docs/assets/style.css index aa41df19c..8a535b3a8 100644 --- a/docs/assets/style.css +++ b/docs/assets/style.css @@ -249,4 +249,4 @@ li[class="item"]:last-child { .dataset-nav-item { padding-left: 5px; border-left: 2px solid #a9afb440; -} +} \ No newline at end of file diff --git a/docs/config.json b/docs/config.json index cc7e40e3e..c94726c1f 100644 --- a/docs/config.json +++ b/docs/config.json @@ -1,5 +1,6 @@ { "catalog_name": "DataCat", + "catalog_url": "https://data.sfb1451.de", "logo_path": "", "link_color": "#142f64", "link_hover_color": "#1f99d6", diff --git a/docs/index.html b/docs/index.html index 64705672f..1b6b65553 100644 --- a/docs/index.html +++ b/docs/index.html @@ -73,6 +73,7 @@ + diff --git a/docs/schema/jsonschema_dataset.json b/docs/schema/jsonschema_dataset.json index f45909c77..19b6cf64d 100644 --- a/docs/schema/jsonschema_dataset.json +++ b/docs/schema/jsonschema_dataset.json @@ -31,6 +31,11 @@ "title": "Short name", "type": "string" }, + "alias": { + "description": "An alias of the dataset, used for shortened URL access within the catalog", + "title": "Alias", + "type": "string" + }, "description": { "description": "A 1-2 paragraph description of the dataset", "title": "Description", diff --git a/docs/templates/context-tab-template.html b/docs/templates/context-tab-template.html new file mode 100644 index 000000000..ffc2a6baf --- /dev/null +++ b/docs/templates/context-tab-template.html @@ -0,0 +1,70 @@ + + + + + + + {{ toUpperString(content_key) }} + + +   + + {{ tabby.content?.['@context']?.[content_key] }} + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/templates/dataset-template.html b/docs/templates/dataset-template.html index f691d7443..d9f53c636 100644 --- a/docs/templates/dataset-template.html +++ b/docs/templates/dataset-template.html @@ -9,7 +9,7 @@ - +
@@ -40,12 +40,13 @@ View on GitHub  View on GIN  Cite  - Export metadata  - binder_logoExplore with Binder + Export metadata  + binder_logoExplore with Binder  + Share  - Request access  - Request access + Request access  + Request access
@@ -123,6 +124,66 @@

Cite dataset

Invalid DOI + + + +
Alias URL
+
+ + + + + {{window.location.origin + '/dataset/' + selectedDataset.alias}} + + + + + + + + +
+
+ +
ID URL
+
+ + + + {{window.location.origin + '/dataset/' + selectedDataset.dataset_id}} + + + + + + + + +
+
+
Full URL
+
+ + + + {{window.location.origin + 'dataset/' + selectedDataset.dataset_id + '/' + selectedDataset.dataset_version}} + + + + + + + + +
+

@@ -131,8 +192,7 @@

Cite dataset

Description: - {{desc.source}} 
- + {{desc}}
{{selectedDataset.description}}
@@ -145,7 +205,7 @@

Cite dataset

Properties:
- Subdatasets: {{selectedDataset.subdatasets_available_count}}  + Datasets: {{selectedDataset.subdatasets_available_count}}  {{display_property.name}}: {{display_property.value}}  @@ -192,9 +252,9 @@

Dataset not found

- +
@@ -202,7 +262,7 @@

Dataset not found

- There are no subdatasets listed for the current dataset + There are no datasets listed for the current dataset @@ -335,9 +395,8 @@
{{pub.title}}
There are no funding sources listed for the current dataset - + - - + -