From a254d612258ebbd27f8abd8fa39a12f02fff53f3 Mon Sep 17 00:00:00 2001 From: Harriet Dashnow Date: Thu, 30 Oct 2025 15:54:00 -0600 Subject: [PATCH 1/8] first draft of STRtr-kit evidence schema --- data/STRtr-kit-evidence.schema.json | 152 ++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 data/STRtr-kit-evidence.schema.json diff --git a/data/STRtr-kit-evidence.schema.json b/data/STRtr-kit-evidence.schema.json new file mode 100644 index 00000000..283e91bf --- /dev/null +++ b/data/STRtr-kit-evidence.schema.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "STRtr-kit-evidence.schema", + "title": "STRtr-kit Evidence", + "description": "Evidence to support pathogenicity of tandem repeat loci in human disease. Each entry describes evidence from a single publication or similar source. There will typically be multiple entries per locus, each describing evidence from different sources.", + "citation_format": "In free text strings: 'Some text [@doi:12345; @pmid:12345]'. In regular lists: ['doi:12345', 'pmid:12345']", + "type": "object", + "properties": { + "id": { + "section": "Locus", + "title": "ID", + "description": "Unique identifier for the locus within STRchive in the form [disease_id]_[gene]. Additional characters may be added to the end of the ID to make it unique within STRchive, e.g. HFG_HOXA13-I, HFG_HOXA13-II.", + "placeholder": "[disease_id]_[gene]", + "examples": ["CANVAS_RFC1", "HFG_HOXA13-I", "HFG_HOXA13-II"], + "type": ["string"], + "pattern": "^[\\S]+_[\\S]+$" + }, + "disease_id": { + "section": "Locus", + "title": "Disease ID", + "description": "Disease abbreviation", + "examples": ["CANVAS"], + "type": ["string"] + }, + "gene": { + "section": "Locus", + "title": "Gene", + "description": "Gene symbol", + "examples": ["RFC1"], + "type": ["string"] + }, + "citation": { + "section": "Source", + "title": "Citation", + "description": "Citation for the source of the evidence", + "examples": ["pmid:29507423"], + "type": ["string", "null"], + "pattern": "^(?:doi|pmc|pmid|arxiv|isbn|url|mondo|omim|genereviews|malacard|orphanet|stripy|gnomad):.+$" + + }, + "genetic_evidence": { + "section": "Genetic Evidence", + "title": "Genetic Evidence", + "description": "One or more pieces of genetic evidence provided in the source", + "type": "array", + "uniqueItems": true, + "items": { + "title": "", + "type": "object", + "properties": { + "genetic_evidence_type": { + "title": "Genetic Evidence Type", + "description": "The type(s) of genetic evidence provided in the source", + "type": ["string", "null"], + "enum": [ + "probands", + "allele_effect", + "method", + "segregation", + "case_control" + ], + "enum_descriptions": { + "probands": "Unrelated Probands", + "allele_effect": "Relationship between allele size and/or motif sequence and phenotype e.g. age of onset and/or severity", + "method": "Method of predicting Pathogenicity", + "segregation": "Linkage Region for disease", + "case_control": "Case-control study" + } + }, + "probands_count": { + "title": "Number of probands", + "description": "Number of probands with the pathogenic expansion reported in the source", + "type": ["integer", "null"], + "minimum": 0 + }, + "points_genetic": { + "title": "Points (Genetic Evidence)", + "description": "Points assigned to this locus based on the genetic evidence provided in the source, according to the STRtr-kit scoring system.", + "type": ["number", "null"], + "minimum": 0, + "maximum": 12 + }, + "notes_genetic": { + "title": "Notes (Genetic Evidence)", + "description": "Additional notes about the genetic evidence provided in the source including point upgrades/downgrades", + "examples": [ + "Segregation analysis in 3 affected families with LOD score of 3.5" + ], + "type": ["string", "null"], + "multiline": true + } + }, + "required": ["genetic_evidence_type", "points_genetic"] + }, + "experimental_evidence": { + "section": "Experimental Evidence", + "title": "Experimental Evidence", + "description": "One or more pieces of experimental evidence provided in the source", + "type": "array", + "uniqueItems": true, + "items": { + "title": "", + "type": "object", + "properties": { + "experimental_evidence_type": { + "title": "Experimental Evidence Type", + "description": "The type(s) of experimental evidence provided in the source", + "type": ["string", "null"], + "enum": [ + "probands", + "allele_effect", + "method", + "segregation", + "case_control" + ], + "enum_descriptions": { + "function_biochemical": "Biochemical function", + "function_protein_interaction": "Protein Interaction function", + "function_regulatory_impact": "Regulatory Impact function", + "altfunction_patient_cells": "Functional Alteration in Patient Cells Model", + "altfunction_non_patient_cells": "Functional Alteration in Non-patient Cells Model", + "model_non_human": "Non-Human Model Organism", + "model_cell_culture": "Cell Culture Model", + "rescue_human_control": "Human (control) Rescue", + "rescue_non_human_model_organism": "Non-Human Model Organism Rescue", + "rescue_cell_culture": "Cell Culture Rescue", + "rescue_patient_cells": "Patient Cells Rescue" + } + }, + "points_experimental": { + "title": "Points (Experimental Evidence)", + "description": "Points assigned to this locus based on the experimental evidence provided in the source, according to the STRtr-kit scoring system.", + "type": ["number", "null"], + "minimum": 0, + "maximum": 6 + }, + "notes_experimental": { + "title": "Notes (Experimental Evidence)", + "description": "Additional notes about the experimental evidence provided in the source including point upgrades/downgrades", + "examples": [ + "Functional alteration demonstrated in patient-derived cell lines" + ], + "type": ["string", "null"], + "multiline": true + } + }, + "required": ["experimental_evidence_type", "points_experimental"] + } + } + } + } +} From a469ec6d7edeff3ec854f27d954ba869cc1d7973 Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Mon, 8 Dec 2025 11:17:36 -0500 Subject: [PATCH 2/8] create page --- data/STRchive-loci.schema.json | 6 +- data/STRtr-kit-evidence.schema.json | 62 ++++----- site/src/locus/EditForm.jsx | 16 +-- site/src/locus/EvidenceForm.jsx | 159 ++++++++++++++++++++++++ site/src/pages/loci/[id].astro | 25 +++- site/src/pages/loci/[id]/evidence.astro | 28 +++++ 6 files changed, 245 insertions(+), 51 deletions(-) create mode 100644 site/src/locus/EvidenceForm.jsx create mode 100644 site/src/pages/loci/[id]/evidence.astro diff --git a/data/STRchive-loci.schema.json b/data/STRchive-loci.schema.json index 833f805f..227c7eb6 100644 --- a/data/STRchive-loci.schema.json +++ b/data/STRchive-loci.schema.json @@ -7,7 +7,7 @@ "type": "object", "properties": { "id": { - "section": "Overview", + "section": "ID", "title": "ID", "description": "Unique identifier for the locus within STRchive in the form [disease_id]_[gene]. Additional characters may be added to the end of the ID to make it unique within STRchive, e.g. HFG_HOXA13-I, HFG_HOXA13-II.", "placeholder": "[disease_id]_[gene]", @@ -16,14 +16,14 @@ "pattern": "^[\\S]+_[\\S]+$" }, "disease_id": { - "section": "Overview", + "section": "ID", "title": "Disease ID", "description": "Disease abbreviation", "examples": ["CANVAS"], "type": ["string"] }, "gene": { - "section": "Overview", + "section": "ID", "title": "Gene", "description": "Gene symbol", "examples": ["RFC1"], diff --git a/data/STRtr-kit-evidence.schema.json b/data/STRtr-kit-evidence.schema.json index 283e91bf..cc614732 100644 --- a/data/STRtr-kit-evidence.schema.json +++ b/data/STRtr-kit-evidence.schema.json @@ -7,39 +7,24 @@ "type": "object", "properties": { "id": { - "section": "Locus", - "title": "ID", + "section": "Evidence", + "title": "Locus ID", "description": "Unique identifier for the locus within STRchive in the form [disease_id]_[gene]. Additional characters may be added to the end of the ID to make it unique within STRchive, e.g. HFG_HOXA13-I, HFG_HOXA13-II.", "placeholder": "[disease_id]_[gene]", "examples": ["CANVAS_RFC1", "HFG_HOXA13-I", "HFG_HOXA13-II"], "type": ["string"], "pattern": "^[\\S]+_[\\S]+$" }, - "disease_id": { - "section": "Locus", - "title": "Disease ID", - "description": "Disease abbreviation", - "examples": ["CANVAS"], - "type": ["string"] - }, - "gene": { - "section": "Locus", - "title": "Gene", - "description": "Gene symbol", - "examples": ["RFC1"], - "type": ["string"] - }, "citation": { - "section": "Source", + "section": "Evidence", "title": "Citation", "description": "Citation for the source of the evidence", "examples": ["pmid:29507423"], "type": ["string", "null"], "pattern": "^(?:doi|pmc|pmid|arxiv|isbn|url|mondo|omim|genereviews|malacard|orphanet|stripy|gnomad):.+$" - }, "genetic_evidence": { - "section": "Genetic Evidence", + "section": "Evidence", "title": "Genetic Evidence", "description": "One or more pieces of genetic evidence provided in the source", "type": "array", @@ -49,7 +34,7 @@ "type": "object", "properties": { "genetic_evidence_type": { - "title": "Genetic Evidence Type", + "title": "Type", "description": "The type(s) of genetic evidence provided in the source", "type": ["string", "null"], "enum": [ @@ -68,20 +53,20 @@ } }, "probands_count": { - "title": "Number of probands", + "title": "Probands", "description": "Number of probands with the pathogenic expansion reported in the source", "type": ["integer", "null"], "minimum": 0 }, "points_genetic": { - "title": "Points (Genetic Evidence)", + "title": "Points", "description": "Points assigned to this locus based on the genetic evidence provided in the source, according to the STRtr-kit scoring system.", "type": ["number", "null"], "minimum": 0, "maximum": 12 }, "notes_genetic": { - "title": "Notes (Genetic Evidence)", + "title": "Notes", "description": "Additional notes about the genetic evidence provided in the source including point upgrades/downgrades", "examples": [ "Segregation analysis in 3 affected families with LOD score of 3.5" @@ -91,9 +76,10 @@ } }, "required": ["genetic_evidence_type", "points_genetic"] - }, + } + }, "experimental_evidence": { - "section": "Experimental Evidence", + "section": "Evidence", "title": "Experimental Evidence", "description": "One or more pieces of experimental evidence provided in the source", "type": "array", @@ -103,15 +89,21 @@ "type": "object", "properties": { "experimental_evidence_type": { - "title": "Experimental Evidence Type", + "title": "Type", "description": "The type(s) of experimental evidence provided in the source", "type": ["string", "null"], "enum": [ - "probands", - "allele_effect", - "method", - "segregation", - "case_control" + "function_biochemical", + "function_protein_interaction", + "function_regulatory_impact", + "altfunction_patient_cells", + "altfunction_non_patient_cells", + "model_non_human", + "model_cell_culture", + "rescue_human_control", + "rescue_non_human_model_organism", + "rescue_cell_culture", + "rescue_patient_cells" ], "enum_descriptions": { "function_biochemical": "Biochemical function", @@ -128,14 +120,14 @@ } }, "points_experimental": { - "title": "Points (Experimental Evidence)", + "title": "Points", "description": "Points assigned to this locus based on the experimental evidence provided in the source, according to the STRtr-kit scoring system.", "type": ["number", "null"], "minimum": 0, "maximum": 6 }, "notes_experimental": { - "title": "Notes (Experimental Evidence)", + "title": "Notes", "description": "Additional notes about the experimental evidence provided in the source including point upgrades/downgrades", "examples": [ "Functional alteration demonstrated in patient-derived cell lines" @@ -145,8 +137,8 @@ } }, "required": ["experimental_evidence_type", "points_experimental"] - } } } - } + }, + "required": ["id", "disease_id", "gene"] } diff --git a/site/src/locus/EditForm.jsx b/site/src/locus/EditForm.jsx index 257cc5e0..c885c320 100644 --- a/site/src/locus/EditForm.jsx +++ b/site/src/locus/EditForm.jsx @@ -20,26 +20,26 @@ import schema from "~/STRchive-loci.schema.json"; /** add extra fields for edit metadata */ schema.properties = { "edit-name": { - section: "Edit", + section: "Overview", ...contactSchema.name, type: ["string", "null"], default: "", }, "edit-username": { - section: "Edit", + section: "Overview", ...contactSchema.username, type: ["string", "null"], pattern: "^@.+", default: "", }, "edit-email": { - section: "Edit", + section: "Overview", ...contactSchema.email, type: ["string", "null"], default: "", }, "edit-title": { - section: "Edit", + section: "Overview", title: "Edit Title", description: "Succinct title describing these changes", examples: ["Fix mechanism details", "Update disease onset information"], @@ -47,7 +47,7 @@ schema.properties = { default: null, }, "edit-description": { - section: "Edit", + section: "Overview", title: "Edit Description", description: "Summary of changes, justification for changes, uncertainty in literature, or anything else we should know for review. Please be detailed. Provide at least 2-3 sentences.", @@ -55,13 +55,13 @@ schema.properties = { "Currently, the disease mechanism details cite a recently retracted paper doi:123456. This edit corrects the reference and updates...", ], multiline: true, - type: "string", + type: ["string", "null"], default: null, }, ...schema.properties, }; -schema.required.push("edit-title", "edit-description"); +schema.required.push("edit-title"); /** new/edit locus form */ const EditForm = ({ heading, locus }) => { @@ -176,8 +176,8 @@ const EditForm = ({ heading, locus }) => { data={data} onChange={setData} sections={[ - "Edit", "Overview", + "ID", "Disease", "Locus", "Alleles", diff --git a/site/src/locus/EvidenceForm.jsx b/site/src/locus/EvidenceForm.jsx new file mode 100644 index 00000000..a4a8baee --- /dev/null +++ b/site/src/locus/EvidenceForm.jsx @@ -0,0 +1,159 @@ +import { useMemo } from "react"; +import { FaXmark } from "react-icons/fa6"; +import { LuBookCheck, LuSend } from "react-icons/lu"; +import { cloneDeep, isEqual, omitBy, startCase } from "lodash-es"; +import { useLocalStorage } from "@reactuses/core"; +import { createPR } from "@/api/pr"; +import Alert from "@/components/Alert"; +import Button from "@/components/Button"; +import { contactSchema } from "@/components/ContactForm"; +import Form from "@/components/Form"; +import Heading from "@/components/Heading"; +import Link from "@/components/Link"; +import SchemaForm from "@/components/SchemaForm"; +import { repo } from "@/layouts/meta"; +import { useQuery } from "@/util/hooks"; +import { shortenUrl } from "@/util/string"; +import loci from "~/STRchive-loci.json"; +import schema from "~/STRtr-kit-evidence.schema.json"; + +/** add extra fields for evidence metadata */ +schema.properties = { + "evidence-name": { + section: "Overview", + ...contactSchema.name, + type: ["string", "null"], + default: "", + }, + "evidence-username": { + section: "Overview", + ...contactSchema.username, + type: ["string", "null"], + pattern: "^@.+", + default: "", + }, + "evidence-email": { + section: "Overview", + ...contactSchema.email, + type: ["string", "null"], + default: "", + }, + "evidence-title": { + section: "Overview", + title: "Evidence Title", + description: "Succinct title describing these changes", + examples: ["Fix mechanism details", "Update disease onset information"], + type: "string", + default: null, + }, + "evidence-description": { + section: "Overview", + title: "Evidence Description", + description: + "Summary of changes, justification for changes, uncertainty in literature, or anything else we should know for review. Please be detailed. Provide at least 2-3 sentences.", + examples: [ + "Currently, the disease mechanism details cite a recently retracted paper doi:123456. This evidence corrects the reference and updates...", + ], + multiline: true, + type: ["string", "null"], + default: null, + }, + ...schema.properties, +}; + +schema.required.push("evidence-title"); + +/** new/evidence locus form */ +const EvidenceForm = ({ heading, locus }) => { + /** confirm with user before leaving page */ + // window.onbeforeunload = () => ""; + + /** unique storage key for this page and form */ + const storageKey = `evidence-locus-${locus?.id ?? "new"}`; + + /** form data state */ + let [data, setData] = useLocalStorage(storageKey, { + ["evidence-title"]: null, + ["evidence-description"]: null, + id: locus?.id ?? null, + }); + + /** was data loaded from storage */ + const storageExists = useMemo(() => { + const fromStorage = window.localStorage.getItem(storageKey); + /** if saved draft exists and is different from initial data */ + return fromStorage && !isEqual(JSON.parse(fromStorage), data); + }, []); + + /** submission query */ + const { + query: submit, + data: response, + status, + } = useQuery(async () => { + console.info(data); + }); + + return ( +
+
+ + + {heading} + + + + Every suggestion is reviewed by our team before inclusion in STRchive. + Please enter as much accurate information as possible. + + + {storageExists && ( +
+ Loaded saved draft + +
+ )} +
+ + submit(data)} + /> + +
+ + {status === "" && ( + <> + This will make a public pull request on{" "} + our GitHub. You'll get a link once it's + created. + + )} + {startCase(status)}{" "} + {response && ( + {shortenUrl(response.link)} + )} + + + +
+ + ); +}; + +export default EvidenceForm; diff --git a/site/src/pages/loci/[id].astro b/site/src/pages/loci/[id].astro index 84c76c8e..d621059c 100644 --- a/site/src/pages/loci/[id].astro +++ b/site/src/pages/loci/[id].astro @@ -4,7 +4,7 @@ import Layout from "@/layouts/Layout.astro"; import { FaCircleMinus, FaCirclePlus, FaRegCalendar } from "react-icons/fa6"; import { LiaBarcodeSolid, LiaSlashSolid } from "react-icons/lia"; -import { LuFeather } from "react-icons/lu"; +import { LuBookCheck, LuFeather } from "react-icons/lu"; import { TbDna2, TbVirus } from "react-icons/tb"; import Button from "@/components/Button"; import Heading from "@/components/Heading"; @@ -137,10 +137,25 @@ const blame = getJsonBlame( } - +
+ + + +
diff --git a/site/src/pages/loci/[id]/evidence.astro b/site/src/pages/loci/[id]/evidence.astro new file mode 100644 index 00000000..aa3c52d7 --- /dev/null +++ b/site/src/pages/loci/[id]/evidence.astro @@ -0,0 +1,28 @@ +--- +/** EDIT LOCUS PAGE */ + +import Layout from "@/layouts/Layout.astro"; +import TableOfContents from "@/components/TableOfContents"; +import EvidenceForm from "@/locus/EvidenceForm"; +import loci from "~/STRchive-loci.json"; + +/** generate pages for each datum, paths based on id */ +export const getStaticPaths = async () => + loci.map(({ id }) => ({ params: { id } })); + +/** current page loci id in url */ +const { id } = Astro.params; + +/** look up full locus entry from id */ +const locus = loci.find((locus) => locus.id === id) || {}; +--- + + + + + + From 79dd18d08aedc04fe2659dc10b07c87bdd7307f7 Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Mon, 8 Dec 2025 23:51:44 -0500 Subject: [PATCH 3/8] simple conditional schema prop support --- data/STRtr-kit-evidence.schema.json | 108 ++++++++-------------------- site/src/components/SchemaForm.jsx | 36 ++++++++-- 2 files changed, 62 insertions(+), 82 deletions(-) diff --git a/data/STRtr-kit-evidence.schema.json b/data/STRtr-kit-evidence.schema.json index cc614732..1bdbb36b 100644 --- a/data/STRtr-kit-evidence.schema.json +++ b/data/STRtr-kit-evidence.schema.json @@ -8,7 +8,7 @@ "properties": { "id": { "section": "Evidence", - "title": "Locus ID", + "title": "Locus", "description": "Unique identifier for the locus within STRchive in the form [disease_id]_[gene]. Additional characters may be added to the end of the ID to make it unique within STRchive, e.g. HFG_HOXA13-I, HFG_HOXA13-II.", "placeholder": "[disease_id]_[gene]", "examples": ["CANVAS_RFC1", "HFG_HOXA13-I", "HFG_HOXA13-II"], @@ -20,9 +20,10 @@ "title": "Citation", "description": "Citation for the source of the evidence", "examples": ["pmid:29507423"], - "type": ["string", "null"], + "type": ["string"], "pattern": "^(?:doi|pmc|pmid|arxiv|isbn|url|mondo|omim|genereviews|malacard|orphanet|stripy|gnomad):.+$" }, + "genetic_evidence": { "section": "Evidence", "title": "Genetic Evidence", @@ -33,7 +34,7 @@ "title": "", "type": "object", "properties": { - "genetic_evidence_type": { + "type": { "title": "Type", "description": "The type(s) of genetic evidence provided in the source", "type": ["string", "null"], @@ -52,20 +53,7 @@ "case_control": "Case-control study" } }, - "probands_count": { - "title": "Probands", - "description": "Number of probands with the pathogenic expansion reported in the source", - "type": ["integer", "null"], - "minimum": 0 - }, - "points_genetic": { - "title": "Points", - "description": "Points assigned to this locus based on the genetic evidence provided in the source, according to the STRtr-kit scoring system.", - "type": ["number", "null"], - "minimum": 0, - "maximum": 12 - }, - "notes_genetic": { + "notes": { "title": "Notes", "description": "Additional notes about the genetic evidence provided in the source including point upgrades/downgrades", "examples": [ @@ -75,70 +63,34 @@ "multiline": true } }, - "required": ["genetic_evidence_type", "points_genetic"] - } - }, - "experimental_evidence": { - "section": "Evidence", - "title": "Experimental Evidence", - "description": "One or more pieces of experimental evidence provided in the source", - "type": "array", - "uniqueItems": true, - "items": { - "title": "", - "type": "object", - "properties": { - "experimental_evidence_type": { - "title": "Type", - "description": "The type(s) of experimental evidence provided in the source", - "type": ["string", "null"], - "enum": [ - "function_biochemical", - "function_protein_interaction", - "function_regulatory_impact", - "altfunction_patient_cells", - "altfunction_non_patient_cells", - "model_non_human", - "model_cell_culture", - "rescue_human_control", - "rescue_non_human_model_organism", - "rescue_cell_culture", - "rescue_patient_cells" - ], - "enum_descriptions": { - "function_biochemical": "Biochemical function", - "function_protein_interaction": "Protein Interaction function", - "function_regulatory_impact": "Regulatory Impact function", - "altfunction_patient_cells": "Functional Alteration in Patient Cells Model", - "altfunction_non_patient_cells": "Functional Alteration in Non-patient Cells Model", - "model_non_human": "Non-Human Model Organism", - "model_cell_culture": "Cell Culture Model", - "rescue_human_control": "Human (control) Rescue", - "rescue_non_human_model_organism": "Non-Human Model Organism Rescue", - "rescue_cell_culture": "Cell Culture Rescue", - "rescue_patient_cells": "Patient Cells Rescue" + "required": ["type"], + "allOf": [ + { + "if": { "properties": { "type": { "const": "probands" } } }, + "then": { + "properties": { + "probands": { + "title": "Probands", + "description": "Number of probands with the pathogenic expansion reported in the source", + "type": ["integer"], + "minimum": 0 + }, + "points": { + "title": "Points", + "description": "Points assigned to this locus based on the genetic evidence provided in the source, according to the STRtr-kit scoring system.", + "type": ["number"], + "minimum": 0, + "maximum": 6, + "default": 0.5, + "calc": "#/probands * 0.5" + } + }, + "required": ["probands", "points"] } - }, - "points_experimental": { - "title": "Points", - "description": "Points assigned to this locus based on the experimental evidence provided in the source, according to the STRtr-kit scoring system.", - "type": ["number", "null"], - "minimum": 0, - "maximum": 6 - }, - "notes_experimental": { - "title": "Notes", - "description": "Additional notes about the experimental evidence provided in the source including point upgrades/downgrades", - "examples": [ - "Functional alteration demonstrated in patient-derived cell lines" - ], - "type": ["string", "null"], - "multiline": true } - }, - "required": ["experimental_evidence_type", "points_experimental"] + ] } } }, - "required": ["id", "disease_id", "gene"] + "required": ["id", "citation"] } diff --git a/site/src/components/SchemaForm.jsx b/site/src/components/SchemaForm.jsx index 93a507e6..182ba5f6 100644 --- a/site/src/components/SchemaForm.jsx +++ b/site/src/components/SchemaForm.jsx @@ -37,6 +37,8 @@ const SchemaForm = ({ schema, sections, data, onChange, children }) => { return rootNode.validate(data); }, [rootNode, data]); + // console.error(errors); + return ( <> {children &&
{children}
} @@ -147,6 +149,7 @@ const Field = ({ hide, multiline, combobox, + allOf, } = node.schema; /** explicitly hide field */ @@ -271,19 +274,44 @@ const Field = ({ else el?.setCustomValidity(""); }; - if (types.includes("object")) + if (types.includes("object")) { + /** regular child properties */ + const regular = Object.keys(node.schema.properties ?? {}).map((key) => [ + key, + /** child node */ + node.getChildSelection(key)[0], + ]); + + /** conditional child properties */ + const conditional = Object.entries( + allOf + /** are all conditions satisfied */ + ?.filter((conditional) => + Object.entries(conditional.if.properties).every( + ([key, value]) => get(data, join(path, key)) === value.const, + ), + ) + /** get "then" properties schema */ + .map((conditional) => conditional.then.properties) + .reduce((acc, props) => ({ ...acc, ...props }), {}) ?? {}, + ).map(([key, node]) => [ + key, + /** child node */ + compileSchema(node), + ]); + /** object group */ control = (
{level > 0 &&
{label}
} - {Object.keys(node.schema.properties).map((key) => { + {regular.concat(conditional).map(([key, node]) => { return ( ); - else if (types.includes("array")) { + } else if (types.includes("array")) { /** array group */ const items = get(data, path)?.length ?? 0; From a0a0b9df8f50727e916b0d2b98c71026b9ed2097 Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Tue, 9 Dec 2025 00:06:37 -0500 Subject: [PATCH 4/8] simplify form state with context --- site/src/components/SchemaForm.jsx | 114 ++++++++++++----------------- 1 file changed, 47 insertions(+), 67 deletions(-) diff --git a/site/src/components/SchemaForm.jsx b/site/src/components/SchemaForm.jsx index 182ba5f6..ccd57f8a 100644 --- a/site/src/components/SchemaForm.jsx +++ b/site/src/components/SchemaForm.jsx @@ -1,4 +1,10 @@ -import { cloneElement, Fragment, useMemo } from "react"; +import { + cloneElement, + createContext, + Fragment, + useContext, + useMemo, +} from "react"; import { FaArrowDown, FaArrowUp, FaPlus, FaTrash } from "react-icons/fa6"; import { compileSchema, draft2020, extendDraft } from "json-schema-library"; import { @@ -20,24 +26,24 @@ import TextBox from "@/components/TextBox"; import { makeList } from "@/util/format"; import classes from "./SchemaForm.module.css"; +const FormContext = createContext({}); + /** form automatically generated from json schema */ const SchemaForm = ({ schema, sections, data, onChange, children }) => { /** compile schema */ - const rootNode = useMemo( + const root = useMemo( () => compileSchema(schema, { drafts: [draft] }), [schema], ); /** if no initial data, generate blank values */ - data ??= rootNode.getData(null, { extendDefaults: false }); + data ??= root.getData(null, { extendDefaults: false }); /** revalidate when data changes */ const { errors } = useMemo(() => { - rootNode.context.data = data; - return rootNode.validate(data); - }, [rootNode, data]); - - // console.error(errors); + root.context.data = data; + return root.validate(data); + }, [root, data]); return ( <> @@ -47,15 +53,11 @@ const SchemaForm = ({ schema, sections, data, onChange, children }) => {
{section} - + + +
))} @@ -117,21 +119,15 @@ const draft = extendDraft(draft2020, { * @param {import("json-schema-library").SchemaNode} props.node * @param {import("json-schema-library").JsonError[]} props.errors */ -const Field = ({ - rootNode, - schema, - section, - node, - path = "", - data, - setData, - errors, -}) => { +const Field = ({ node, path = "" }) => { + /** top level form state */ + const form = useContext(FormContext); + /** are we at top level of schema */ const level = split(path).length; /** filter out fields that should not be displayed in this section */ - if (level === 1 && node.schema.section !== section) return; + if (level === 1 && node.schema.section !== form.section) return; /** schema props */ const { @@ -162,13 +158,13 @@ const Field = ({ const name = path; /** get nested data value from path */ - const value = get(data, path); + const value = get(form.data, path); /** set nested data value from path */ const onChange = (value) => { - let _data = cloneDeep(data); + let _data = cloneDeep(form.data); _data = set(_data, path, value); - setData(_data); + form.onChange(_data); }; /** help tooltip to show */ @@ -190,7 +186,8 @@ const Field = ({ /** is the field required to be set */ const required = - schema.required?.includes(split(name).pop()) && !types.includes("null"); + form.schema.required?.includes(split(name).pop()) && + !types.includes("null"); /** full label elements to show */ const label = ( @@ -201,7 +198,7 @@ const Field = ({ ); /** get validation errors associated with this field */ - const fieldErrors = errors.filter( + const fieldErrors = form.errors.filter( ({ data }) => data.pointer === path.replace(/^#?\/?/, "#/"), ); /** is there an error on this field */ @@ -288,7 +285,7 @@ const Field = ({ /** are all conditions satisfied */ ?.filter((conditional) => Object.entries(conditional.if.properties).every( - ([key, value]) => get(data, join(path, key)) === value.const, + ([key, value]) => get(form.data, join(path, key)) === value.const, ), ) /** get "then" properties schema */ @@ -305,25 +302,13 @@ const Field = ({
{level > 0 &&
{label}
} {regular.concat(conditional).map(([key, node]) => { - return ( - - ); + return ; })}
); } else if (types.includes("array")) { /** array group */ - const items = get(data, path)?.length ?? 0; + const items = get(form.data, path)?.length ?? 0; control = (
@@ -332,27 +317,22 @@ const Field = ({