From da607f83ce549109face24aaf0df331ab3255355 Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Fri, 8 Mar 2024 14:48:18 -0800 Subject: [PATCH] Improve embargoing and license specification process --- schemas/json/dandi/create.json | 17 +++- src/renderer/src/stories/JSONSchemaForm.js | 12 ++- src/renderer/src/stories/JSONSchemaInput.js | 93 ++++++++++++++----- src/renderer/src/stories/Search.js | 59 +++++++++--- .../guided-mode/data/GuidedPathExpansion.js | 6 +- .../src/stories/pages/uploads/UploadsPage.js | 16 +++- 6 files changed, 159 insertions(+), 44 deletions(-) diff --git a/schemas/json/dandi/create.json b/schemas/json/dandi/create.json index 0cc1b18a9..dc25095ac 100644 --- a/schemas/json/dandi/create.json +++ b/schemas/json/dandi/create.json @@ -14,6 +14,7 @@ }, "embargo_status": { + "title": "Would you like to embargo this Dandiset?", "type": "boolean", "description": "Embargoed Dandisets are hidden from public access until a specific time period has elapsed. Uploading data to the DANDI archive under embargo requires a relevant NIH award number, and the data will be automatically published when the embargo period expires.", "default": false @@ -36,15 +37,27 @@ "license": { "type": "array", + "description": "Provide a set of licenses for this Dandiset. If you are unsure which license to use, we recommend using the CC0 1.0 license.", "items": { "type": "string", + "enumLinks": { + "spdx:CC0-1.0": "https://creativecommons.org/public-domain/cc0/", + "spdx:CC-BY-4.0": "https://creativecommons.org/licenses/by/4.0/deed.en" + }, + "enumKeywords": { + "spdx:CC0-1.0": ["No Rights Reserved"], + "spdx:CC-BY-4.0": ["Attribution 4.0 International"] + }, + "enumLabels": { + "spdx:CC0-1.0": "CC0 1.0", + "spdx:CC-BY-4.0": "CC BY 4.0" + }, "enum": [ "spdx:CC0-1.0", "spdx:CC-BY-4.0" ] }, - "uniqueItems": true, - "description": "Licenses associated with the item.
Note: DANDI only supports a subset of Creative Commons Licenses applicable to datasets" + "uniqueItems": true }, "nih_award_number": { diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index d0b3a592d..9bb95d4e4 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -817,12 +817,14 @@ export class JSONSchemaForm extends LitElement { const value = parent[name]; - const skipValidation = !this.validateEmptyValues && value === undefined; + + const skipValidation = this.validateEmptyValues === null && value === undefined; + const validateArgs = input.pattern || skipValidation ? [] : [value, schema]; + // Run validation functions const jsonSchemaErrors = validateArgs.length === 2 ? this.validateSchema(...validateArgs, name) : []; - - const valid = skipValidation ? true : await this.validateOnChange(name, parent, pathToValidate, value); + const valid = skipValidation ? true : await this.validateOnChange(name, parent, pathToValidate, value); if (valid === null) return null; // Skip validation / data change if the value is null @@ -879,7 +881,9 @@ export class JSONSchemaForm extends LitElement { type: "error", missing: true, }); - } else if (this.validateEmptyValues === null) { + } + + else if (this.validateEmptyValues === null) { warnings.push({ message: `${schema.title ?? header(name)} is a suggested property.`, type: "warning", diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index b9a884e34..342d97c22 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -353,7 +353,6 @@ export class JSONSchemaInput extends LitElement { :host { margin-top: 1.45rem; - display: block; } :host(.invalid) .guided--input { @@ -489,6 +488,7 @@ export class JSONSchemaInput extends LitElement { return { schema: { type: Object, reflect: false }, validateEmptyValue: { type: Boolean, reflect: true }, + required: { type: Boolean, reflect: true }, }; } @@ -499,7 +499,7 @@ export class JSONSchemaInput extends LitElement { // pattern // showLabel controls = []; - required = false; + // required; validateOnChange = true; constructor(props) { @@ -848,24 +848,6 @@ export class JSONSchemaInput extends LitElement { const allowPatternProperties = isPatternProperties(this.pattern); const allowAdditionalProperties = isAdditionalProperties(this.pattern); - const addButton = new Button({ - size: "small", - }); - - addButton.innerText = `Add ${canAddProperties ? "Property" : "Item"}`; - - const buttonDiv = document.createElement("div"); - Object.assign(buttonDiv.style, { width: "fit-content" }); - buttonDiv.append(addButton); - - const disableButton = ({ message, submessage }) => { - addButton.setAttribute("disabled", true); - tippy(buttonDiv, { - content: `
${message}
${submessage}
`, - allowHTML: true, - }); - }; - // Provide default item types if (isArray) { const hasItemsRef = "items" in schema && "$ref" in schema.items; @@ -874,11 +856,55 @@ export class JSONSchemaInput extends LitElement { } const itemSchema = this.form?.getSchema ? this.form.getSchema("items", schema) : schema["items"]; - + const fileSystemFormat = isFilesystemSelector(name, itemSchema?.format); if (fileSystemFormat) return createFilesystemSelector(fileSystemFormat); // Create tables if possible - else if (itemSchema?.type === "object" && this.renderTable) { + else if (itemSchema?.type === "string") { + console.error('JUST SEARCH STRINGS') + + const list = new List({ + items: this.value, + onChange: ({ items }) => { + this.#updateData(fullPath, items.length ? items.map((o) => o.value) : undefined); + if (validateOnChange) this.#triggerValidation(name, path); + }, + }); + + + const search = new Search({ + options: itemSchema.enum.map((v) => { + return { + key: v, + value: v, + label: itemSchema.enumLabels?.[v] ?? v, + keywords: itemSchema.enumKeywords?.[v], + description: itemSchema.enumDescriptions?.[v], + link: itemSchema.enumLinks?.[v], + }; + }), + value: this.value, + listMode: schema.strict === false ? 'click' : "append", + showAllWhenEmpty: false, + onSelect: async ({ label, value }) => { + if (!value) return + if (schema.uniqueItems && this.value && this.value.includes(value)) return + list.add({ content: label, value }) + // search.value = '' + // search.requestUpdate() + }, + }); + + list.classList.add("schema-input"); + + search.style.height = "auto"; + + console.log(search) + + return html`
${search}${list}
`; + + } else if (itemSchema?.type === "object" && this.renderTable) { + const instanceThis = this; function updateFunction(path, value = this.data) { @@ -897,6 +923,25 @@ export class JSONSchemaInput extends LitElement { if (table) return table; } + const addButton = new Button({ + size: "small", + }); + + addButton.innerText = `Add ${canAddProperties ? "Property" : "Item"}`; + + const buttonDiv = document.createElement("div"); + Object.assign(buttonDiv.style, { width: "fit-content" }); + buttonDiv.append(addButton); + + const disableButton = ({ message, submessage }) => { + addButton.setAttribute("disabled", true); + tippy(buttonDiv, { + content: `
${message}
${submessage}
`, + allowHTML: true, + }); + }; + + const list = (this.#list = new List({ items: this.#mapToList(), @@ -965,6 +1010,8 @@ export class JSONSchemaInput extends LitElement { category: schema.enumCategories?.[v], label: schema.enumLabels?.[v] ?? v, keywords: schema.enumKeywords?.[v], + description: schema.enumDescriptions?.[v], + link: schema.enumLinks?.[v], }; }); @@ -978,7 +1025,7 @@ export class JSONSchemaInput extends LitElement { keywords: schema.enumKeywords?.[this.value], }, showAllWhenEmpty: false, - listMode: "click", + listMode: "input", onSelect: async ({ value, key }) => { const result = value ?? key; this.#updateData(fullPath, result); diff --git a/src/renderer/src/stories/Search.js b/src/renderer/src/stories/Search.js index 70e601896..5a7abb119 100644 --- a/src/renderer/src/stories/Search.js +++ b/src/renderer/src/stories/Search.js @@ -6,6 +6,9 @@ import searchSVG from "./assets/search.svg?raw"; import tippy from "tippy.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; + +const ALTERNATIVE_MODES = ["input", "append"]; + export class Search extends LitElement { constructor({ value, @@ -25,13 +28,15 @@ export class Search extends LitElement { this.headerStyles = headerStyles; if (onSelect) this.onSelect = onSelect; - document.addEventListener("click", () => this.submit()); + // document.addEventListener("click", () => this.#close()); } - submit = () => { - if (this.listMode === "click" && this.getAttribute("interacted") === "true") { + #close = () => { + if (this.listMode === "input" && this.getAttribute("interacted") === "true") { this.setAttribute("interacted", false); this.#onSelect(this.getSelectedOption()); + } else if (this.listMode !== 'list') { + this.setAttribute("active", false); } }; @@ -104,7 +109,7 @@ export class Search extends LitElement { overflow: auto; } - :host([listmode="click"]) ul { + :host([listmode="input"]) ul, :host([listmode="append"]) ul { position: absolute; top: 38px; left: 0; @@ -125,7 +130,17 @@ export class Search extends LitElement { height: 20px; } - :host([listmode="click"]) svg { + a { + text-decoration: none; + } + + a:after { + content: "🔗"; + padding-left: 2px; + font-size: 60%; + } + + :host([listmode="input"]) svg, :host([listmode="append"]) svg { position: absolute; top: 50%; padding: 0px; @@ -218,7 +233,8 @@ export class Search extends LitElement { }); this.#initialize(); - if (this.listMode !== "click") this.#populate(); + + if (!ALTERNATIVE_MODES.includes(this.listMode)) this.#populate(); } onSelect = (id, value) => {}; @@ -230,7 +246,7 @@ export class Search extends LitElement { #onSelect = (option) => { const input = this.shadowRoot.querySelector("input"); - if (this.listMode === "click") { + if (this.listMode === "input") { input.value = this.#displayValue(option); this.setAttribute("active", false); return this.onSelect(option); @@ -303,6 +319,8 @@ export class Search extends LitElement { this.setAttribute("interacted", true); }; + #ignore = false + render() { this.categories = {}; @@ -352,6 +370,7 @@ export class Search extends LitElement { listItemElement.addEventListener("click", (clickEvent) => { clickEvent.stopPropagation(); + if (this.#ignore) return this.#ignore = false this.#onSelect(option); }); @@ -363,13 +382,19 @@ export class Search extends LitElement { label.classList.add("label"); label.innerText = option.label; - const info = document.createElement("span"); - if (option.description) { + if (option.description || option.link) { + const info = option.link ? document.createElement('a') : document.createElement("span"); + if (option.link) { + info.setAttribute("data-link", true); + info.href = option.link; + info.target = "_blank"; + } + info.innerText = "ℹī¸"; label.append(info); - tippy(info, { + if (option.description) tippy(info, { content: `

${option.description}

`, allowHTML: true, placement: "right", @@ -443,7 +468,7 @@ export class Search extends LitElement { })}> { clickEvent.stopPropagation(); - if (this.listMode === "click") { + if (ALTERNATIVE_MODES.includes(this.listMode)) { const input = clickEvent.target.value; this.#populate(input); } @@ -455,8 +480,16 @@ export class Search extends LitElement { }} @blur=${(blurEvent) => { - if (blurEvent.relatedTarget.classList.contains("option")) return; - this.submit(); + const relatedTarget = blurEvent.relatedTarget; + if (relatedTarget) { + if (relatedTarget.classList.contains("option")) return; + if (relatedTarget.hasAttribute("data-link")) { + this.#ignore = true + return + } + } + + this.#close(); }} > diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js index 8f88aec22..b815e3554 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js @@ -115,7 +115,11 @@ export async function autocompleteFormatString(path) { } } } else { - if (!parent.path || !parent.path.includes(value)) + + if (!parent.path) return; + if (!value) return; + + if (!parent.path.includes(value)) return [ { type: "error", diff --git a/src/renderer/src/stories/pages/uploads/UploadsPage.js b/src/renderer/src/stories/pages/uploads/UploadsPage.js index 102d9a5de..3cae6925f 100644 --- a/src/renderer/src/stories/pages/uploads/UploadsPage.js +++ b/src/renderer/src/stories/pages/uploads/UploadsPage.js @@ -55,12 +55,26 @@ export async function createDandiset(results = {}) { paddingBottom: "0px", }); + const updateNIHInput = (embargoed) => { + const nihInput = form.getFormElement([ "nih_award_number" ]); + + // Show the NIH input if embargo is set + if (embargoed) nihInput.removeAttribute("hidden"); + else nihInput.setAttribute("hidden", ""); + + // Make the NIH input required if embargo is set + nihInput.required = embargoed; + } + const form = new JSONSchemaForm({ schema: dandiCreateSchema, results, + validateEmptyValues: false, // Only show errors after submission validateOnChange: async (name, parent) => { const value = parent[name]; + if (name === 'embargo_status') return updateNIHInput(value); + if (name === "nih_award_number") { if (value) return awardNumberValidator(value) || [{ type: "error", message: AWARD_VALIDATION_FAIL_MESSAGE }]; @@ -83,7 +97,7 @@ export async function createDandiset(results = {}) { content.append(form); modal.append(content); - + modal.onClose = async () => notify("Dandiset was not created.", "error"); return new Promise((resolve) => {