diff --git a/app/e2e/author-css.spec.ts b/app/e2e/author-css.spec.ts index 9a0fde99a..68bef6c6d 100644 --- a/app/e2e/author-css.spec.ts +++ b/app/e2e/author-css.spec.ts @@ -30,28 +30,26 @@ test("/api/author/scat-css", async ({ request }) => { expect(data.items).toEqual([]); }); -test.describe.skip("/author/scat-css", () => { +test.describe("/author/set", () => { test.beforeAll(async ({ browser }) => { await uploadAndPublishDummySet(browser); return db().dropNonUserCollections; }); test.beforeEach(async ({ page }) => { - await page.goto("/author/scat-css"); + await page.goto("/author/set"); await page.locator("button:text-is(\"Edit\")").click(); - await page.locator("textarea[name=\"set\\.description\"]").fill( + await page.getByPlaceholder("Describe which changes have").fill( "Edited description", ); - await page.locator("button:has-text(\"Submit\")").click(); - await page.goto("/author/scat-css"); + await page.getByRole("button", { name: "Submit" }).click(); + await page.getByText("Saved set with id").waitFor({ state: "visible" }); + await page.goto("/author/set"); }); - // FIXME: Playwright is too fast in its actions, and the expect calls - // will receive incorrect data. We need to find a way to wait for all - // async calls (that e.g. reload the page data) to finish before - // continuing - test("A simple edit should result in a draft", async ({ page }) => { + await page.goto("/author/set"); + const table = page.locator("table:has(thead th:text(\"Version\"))"); // Status = draft @@ -68,6 +66,9 @@ test.describe.skip("/author/scat-css", () => { .click(); const table = page.locator("table:has(thead th:text(\"Version\"))"); + await page.locator("button:text-is(\"Delete\"):visible").waitFor({ + state: "hidden", + }); // Status = published expect(table.locator("td").nth(1)).toHaveText("published"); @@ -77,9 +78,9 @@ test.describe.skip("/author/scat-css", () => { ); }); -test.describe.skip("/author/scat-css/add", () => { +test.describe("/author/set/add", () => { test.beforeEach(async ({ page }) => { - await page.goto("/author/scat-css/add"); + await page.goto("/author/set/add"); }); test.describe("give empty name field", () => { test("after submit should show warning", async ({ page }) => { @@ -87,10 +88,10 @@ test.describe.skip("/author/scat-css/add", () => { await page.locator("button:has-text(\"Submit\")").click(); - const warning = page.locator( - "text=Name *must NOT have fewer than 1 characters", + const warning = page.getByText( + "String must contain at least 1 character(s)", ); - await expect(warning).toBeVisible(); + await expect(warning).toHaveCount(3); }); }); @@ -100,46 +101,59 @@ test.describe.skip("/author/scat-css/add", () => { name, ); await page.locator("button:has-text(\"Add\")").click(); + await page.locator("li").getByText(name).waitFor({ state: "visible" }); + await page.goto("/admin/users"); + await page.waitForLoadState("domcontentloaded"); await page - .locator("[aria-label=\"Memberships of admin\\@ng\\.lxcat\\.net\"]") + .getByRole("row", { name: "admin admin@ng.lxcat.net" }).locator("div") + .first() .click(); await page.locator(`text=${name}`).click(); } async function fillAddSetForm(page: Page) { - await page.goto("/author/scat-css/add"); + await page.goto("/author/set/add"); // General - await page.locator("input[name=\"set\\.name\"]").fill("My name"); - await page - .locator("select[name=\"set\\.contributor\"]") - .selectOption("MyOrg"); + await page.getByLabel("Name *").fill("My name"); + await page.getByLabel("Contributor").selectOption("MyOrg"); // States - await page.locator("button[role=\"tab\"]:has-text(\"States\")").click(); - await page.locator("[aria-label=\"Add a state\"]").click(); - await page.locator("input[name=\"set\\.states\\.s0\\.particle\"]").fill( - "Ar", + await page.getByRole("tab", { name: "Species" }).click(); + await page.getByRole("button", { name: "+" }).click(); + await page.locator("span.mantine-Accordion-label").first().click(); + await page.getByLabel("Species definition").fill( + "{\"type\": \"simple\", \"particle\": \"Ar\", \"charge\": 0}", ); - await page.locator("input[name=\"set\\.states\\.s0\\.charge\"]").fill("0"); // Processes - await page.locator("button[role=\"tab\"]:has-text(\"Processes\")").click(); - await page.locator("[aria-label=\"Add process\"]").click(); - await page.locator("[aria-label=\"Add data row to process\"]").click(); - await page - .locator("input[name=\"set\\.processes\\.0\\.data\\.0\\.0\"]") - .fill("1.2"); - await page - .locator("input[name=\"set\\.processes\\.0\\.data\\.0\\.1\"]") - .fill("3.4e-5"); - await page.locator("[aria-label=\"Add consumed reaction entry\"]").click(); + await page.getByRole("tab", { name: "Processes" }).click(); + await page.getByRole("button", { name: "Add process" }).click(); + await page.locator("span.mantine-Accordion-label").first().click(); + + // Add reactant + await page.getByRole("group", { name: "Reactants" }).getByRole("button") + .click(); await page - .locator( - "[aria-controls=\"set\\.processes\\.0\\.reaction\\.lhs\\.0\\.state\"]", - ) + .getByRole("group", { name: "Reactants" }) + .locator("button.mantine-UnstyledButton-root") + .nth(2) + .click(); + await page.getByRole("menuitem").click(); + + // Add product + await page.getByRole("group", { name: "Products" }).getByRole("button") .click(); await page - .locator("button[role=\"menuitem\"]:has-text(\"\\mathrm{Ar}\")") + .getByRole("group", { name: "Products" }) + .locator("button.mantine-UnstyledButton-root") + .nth(2) .click(); + await page.getByRole("menuitem").click(); + + // Add data entry + await page.getByRole("button", { name: "Add info object" }).click(); + await page.getByRole("button", { name: "Cross section" }).click(); + await page.locator("td").nth(1).locator("input").fill("1.2"); + await page.locator("td").nth(2).locator("input").fill("3.4e-5"); } test.describe("given minimal set", () => { @@ -153,7 +167,7 @@ test.describe.skip("/author/scat-css/add", () => { }); test("should have json document", async ({ page }) => { - await page.locator("button[role=\"tab\"]:has-text(\"JSON\")").click(); + await page.getByRole("tab", { name: "JSON" }).click(); const json = await page.locator("pre").innerText(); const expected = { name: "My name", @@ -166,36 +180,47 @@ test.describe.skip("/author/scat-css/add", () => { lhs: [ { count: 1, - state: "s0", + state: expect.any(String), + }, + ], + rhs: [ + { + count: 1, + state: expect.any(String), }, ], - rhs: [], reversible: false, - type_tags: [], + typeTags: [], }, - threshold: 0, - type: "LUT", - labels: ["Energy", "CrossSection"], - units: ["eV", "m^2"], - data: [[1.2, 3.4e-5]], - parameters: {}, + info: [ + { + type: "CrossSection", + threshold: 0, + references: [], + data: { + type: "LUT", + labels: ["Energy", "Cross Section"], + units: ["eV", "m^2"], + values: [[1.2, 3.4e-5]], + }, + }, + ], }, ], - states: { - s0: { - particle: "Ar", - charge: 0, - }, - }, + // TODO: This is not very strict, we can assert on the expected values. + states: expect.any(Object), references: {}, }; expect(JSON.parse(json)).toEqual(expected); }); test("after submit should have success message", async ({ page }) => { - await page.locator("button:has-text(\"Submit\")").click(); + await page + .getByPlaceholder("Describe which changes have") + .fill("Initial upload"); + await page.getByRole("button", { name: "Submit" }).click(); - await expect(page.locator(".status")).toContainText("Adding successful"); + await expect(page.getByText("Saved set with id")).toBeVisible(); }); }); }); diff --git a/app/e2e/global-setup.ts b/app/e2e/global-setup.ts index f42896fec..5e8c0b7f9 100644 --- a/app/e2e/global-setup.ts +++ b/app/e2e/global-setup.ts @@ -138,7 +138,7 @@ export async function uploadAndPublishDummySet( await page.waitForSelector(`div:has-text("${org}")`); // Add a set - await page.goto("/author/scat-css/addraw"); + await page.goto("/author/set/addraw"); const dummySet = await readFile( `../packages/database/src/test/seed/cross-sections/${file}`, { encoding: "utf8" }, @@ -148,8 +148,7 @@ export async function uploadAndPublishDummySet( await page.waitForSelector("span:has-text(\"Upload successful\")"); // Publish set - await page.goto("/author/scat-css"); - await page.reload(); // TODO sometimes no set is listed, use reload to give server some time as workaround + await page.goto("/author/set"); await page.waitForSelector("td:has-text(\"draft\")"); await page.locator("tbody button:has-text(\"Publish\")").click(); // Press publish in dialog diff --git a/app/package.json b/app/package.json index 6aaffb994..d1861ed8f 100644 --- a/app/package.json +++ b/app/package.json @@ -24,11 +24,10 @@ "@citation-js/plugin-csl": "^0.7.11", "@citation-js/plugin-doi": "^0.7.11", "@citation-js/plugin-ris": "^0.7.11", - "@hookform/error-message": "^2.0.1", - "@hookform/resolvers": "^3.4.2", "@lxcat/converter": "workspace:^", "@lxcat/database": "workspace:^", "@lxcat/schema": "workspace:^", + "@mantine/code-highlight": "^7.10.1", "@mantine/core": "^7.10.1", "@mantine/form": "^7.10.1", "@mantine/hooks": "^7.10.1", @@ -56,7 +55,6 @@ "plotly.js-basic-dist": "^2.33.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-hook-form": "^7.51.5", "react-latex-next": "^3.0.0", "react-plotly.js": "^2.6.0", "react-schemaorg": "^2.0.0", diff --git a/app/src/app/api/author/scat-css/[id]/route.ts b/app/src/app/api/author/scat-css/[id]/route.ts index ce5d498ac..5d06cf36e 100644 --- a/app/src/app/api/author/scat-css/[id]/route.ts +++ b/app/src/app/api/author/scat-css/[id]/route.ts @@ -5,7 +5,7 @@ import { LXCatID } from "@/shared/lxcatid"; import { db } from "@lxcat/database"; import { EditedLTPDocument } from "@lxcat/schema"; -import { z } from "zod"; +import { object, string } from "zod"; import { badRequestResponse, forbiddenResponse, @@ -17,14 +17,14 @@ import { hasAuthorRole, hasSessionOrAPIToken } from "../../../middleware/auth"; import { zodMiddleware } from "../../../middleware/zod"; import { RouteBuilder } from "../../../route-builder"; -export const postSchema = z.object({ - path: z.object({ id: LXCatID }), - body: z.object({ doc: EditedLTPDocument, message: z.string() }), +export const postSchema = object({ + path: object({ id: LXCatID }), + body: object({ doc: EditedLTPDocument, message: string().min(1) }), }); -export const deleteSchema = z.object({ - path: z.object({ id: LXCatID }), - body: z.object({ message: z.optional(z.string().min(1)) }), +export const deleteSchema = object({ + path: object({ id: LXCatID }), + body: object({ message: string().min(1) }), }); const postRouter = RouteBuilder diff --git a/app/src/app/api/author/scat-css/route.ts b/app/src/app/api/author/scat-css/route.ts index 918b4d4ed..464c4480d 100644 --- a/app/src/app/api/author/scat-css/route.ts +++ b/app/src/app/api/author/scat-css/route.ts @@ -22,10 +22,9 @@ const postRouter = RouteBuilder .getAffiliations(ctx.user.email) .then((affiliations) => affiliations.map(({ name }) => name)); - const doc = ctx.parsedParams.body; + const { doc, message } = ctx.parsedParams.body; if (affiliations.includes(doc.contributor)) { - // Add to CrossSectionSet with status=='draft' and version=='1' - const id = await db().createSet(doc, "draft"); + const id = await db().createSet(doc, "draft", 1, message); return okJsonResponse({ id }); } else { return forbiddenResponse({ @@ -33,7 +32,7 @@ const postRouter = RouteBuilder errors: [ { message: - `You are not a member of the ${ctx.parsedParams.body.contributor} organization.`, + `You are not a member of the ${doc.contributor} organization.`, }, ], }, diff --git a/app/src/app/api/author/scat-css/schemas.ts b/app/src/app/api/author/scat-css/schemas.ts index 75a10d15f..039a544fb 100644 --- a/app/src/app/api/author/scat-css/schemas.ts +++ b/app/src/app/api/author/scat-css/schemas.ts @@ -3,8 +3,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import { EditedLTPDocument } from "@lxcat/schema"; -import { z } from "zod"; +import { object, string } from "zod"; -export const querySchema = z.object({ - body: EditedLTPDocument, +export const querySchema = object({ + body: object({ doc: EditedLTPDocument, message: string().min(1) }), }); diff --git a/app/src/app/api/users/[user]/organizations/schemas.ts b/app/src/app/api/users/[user]/organizations/schemas.ts index 8338bb85f..533611453 100644 --- a/app/src/app/api/users/[user]/organizations/schemas.ts +++ b/app/src/app/api/users/[user]/organizations/schemas.ts @@ -9,5 +9,5 @@ export const querySchema = z.object({ path: z.object({ user: z.string(), }), - body: z.array(LXCatID).min(1), + body: z.array(LXCatID), }); diff --git a/app/src/app/author/set/[id]/edit/comment-section.tsx b/app/src/app/author/set/[id]/edit/comment-section.tsx new file mode 100644 index 000000000..004114704 --- /dev/null +++ b/app/src/app/author/set/[id]/edit/comment-section.tsx @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: LXCat team +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { MaybePromise } from "@/app/api/util"; +import { + ActionIcon, + Button, + Center, + Group, + Stack, + TextInput, +} from "@mantine/core"; +import { IconPlaylistAdd, IconTrash } from "@tabler/icons-react"; + +export const CommentSection = ( + { comments, onChange }: { + comments: Array | undefined; + onChange: (comments: Array | undefined) => MaybePromise; + }, +) => ( + + {comments?.map((comment, index) => { + return ( + + { + const newComments = [...comments]; + newComments[index] = event.currentTarget.value; + return onChange(newComments); + }} + /> + + onChange( + comments + ? comments.filter((_, curIndex) => curIndex !== index) + : undefined, + )} + > + + + + ); + })} +
+ +
+
+); diff --git a/app/src/app/author/set/[id]/edit/edit-form.tsx b/app/src/app/author/set/[id]/edit/edit-form.tsx index 58fd33ebe..c42af79c6 100644 --- a/app/src/app/author/set/[id]/edit/edit-form.tsx +++ b/app/src/app/author/set/[id]/edit/edit-form.tsx @@ -4,75 +4,46 @@ "use client"; +import { reference2bibliography } from "@/shared/cite"; import { KeyedOrganization } from "@lxcat/database/auth"; -import { EditedLTPDocument, type VersionedLTPDocument } from "@lxcat/schema"; -import { stateJSONSchema } from "@lxcat/schema/json-schema"; +import { EditedLTPDocument, Reference } from "@lxcat/schema"; import { AnySpeciesSerializable } from "@lxcat/schema/species"; import { - Accordion, Button, Checkbox, - Modal, + Group, NativeSelect, + Select, Space, Stack, Tabs, + Text, Textarea, TextInput, } from "@mantine/core"; import { createFormContext, zodResolver } from "@mantine/form"; -import { useDisclosure } from "@mantine/hooks"; -import { JSONSchema7 } from "json-schema"; -import { nanoid } from "nanoid"; -import { useState } from "react"; -import { FieldErrors, FieldPath, FieldValues, get } from "react-hook-form"; +import { useMemo, useState } from "react"; import { z } from "zod"; -import { Latex } from "../../../../../shared/latex"; -import { generateSpeciesForm, SpeciesForm } from "./form-factory"; -import { SpeciesNode, SpeciesPicker } from "./species-picker"; +import { JsonTab } from "./json-tab"; +import { ProcessTab } from "./process-tab"; +import { ReferenceTable } from "./reference-table"; +import { SpeciesTab } from "./species-tab"; const EditFormValues = z.object({ set: EditedLTPDocument, commitMessage: z.string().min(1), - meta: z.record(z.any()), }); export type EditFormValues = z.input; type EditFormProps = { - initialSet: VersionedLTPDocument; + initialSet: EditedLTPDocument; organizations: Array; }; -export const getError = ( - errors: FieldErrors, - name: FieldPath, -): string => { - const error = get(errors, name); - - return error ? error.message : ""; -}; - export const [FormProvider, useFormContext, useForm] = createFormContext< EditFormValues >(); -const emptySet = () => ({ - commitMessage: "", - set: { - $schema: "", - url: "", - termsOfUse: "", - name: "", - contributor: "", - description: "", - complete: false, - references: {}, - states: {}, - processes: [], - }, - meta: {}, -}); - export const EditForm = ( { initialSet, organizations }: EditFormProps, ) => { @@ -81,114 +52,64 @@ export const EditForm = ( validate: zodResolver(EditFormValues), initialValues: { commitMessage: "", - set: EditedLTPDocument.parse({ - ...initialSet, - contributor: initialSet.contributor.name, - }), - meta: { - set: { - states: Object.fromEntries( - Object.entries(initialSet.states).map(( - [key, state], - ) => { - const metaState = { - electronic: { - anyOf: "0", - vibrational: { anyOf: "0", rotational: { anyOf: "0" } }, - }, - }; - - if (state.type !== "simple" && state.type !== "unspecified") { - if (Array.isArray(state.electronic)) { - metaState.electronic.anyOf = "1"; - } else if ( - "vibrational" in state.electronic - && state.electronic.vibrational - ) { - if (Array.isArray(state.electronic.vibrational)) { - metaState.electronic.vibrational.anyOf = "1"; - } else if ( - typeof state.electronic.vibrational === "string" - ) { - metaState.electronic.vibrational.anyOf = "2"; - } else if ("rotational" in state.electronic.vibrational) { - if ( - Array.isArray(state.electronic.vibrational.rotational) - ) { - metaState.electronic.vibrational.rotational.anyOf = "1"; - } else if ( - typeof state.electronic.vibrational.rotational - === "string" - ) { - metaState.electronic.vibrational.rotational.anyOf = "2"; - } - } - } - } - - return [key, metaState]; - }), - ), - }, - }, + set: initialSet, }, - // { - // commitMessage: "", - // set: { - // name: "test", - // contributor: "TestContributor", - // description: "", - // complete: false, - // references: {}, - // states: { - // test: { - // type: "AtomLS", - // particle: "He", - // charge: 0, - // electronic: { - // config: [], - // term: { - // L: 0, - // S: 0, - // P: 1, - // J: 0, - // }, - // }, - // }, - // }, - // processes: [], - // }, - // meta: { - // set: { - // states: { - // test: { - // electronic: { - // anyOf: "0", - // vibrational: { anyOf: "0", rotational: { anyOf: "0" } }, - // }, - // }, - // }, - // }, - // }, - // }, }, ); const [activeTab, setActiveTab] = useState("general"); - const [ - speciesPickerOpened, - { open: openSpeciesPicker, close: closeSpeciesPicker }, - ] = useDisclosure(false); - const [selectedSpecies, setSelectedSpecies] = useState>( - [], + const { getInputProps } = form; + + const [processAccordionState, processAccordionOnChange] = useState< + string | null + >(null); + + const [submitMessage, setSubmitMessage] = useState(); + + const speciesMap = useMemo( + () => + Object.fromEntries( + Object.entries(form.values.set.states).map(( + [key, species], + ) => [key, AnySpeciesSerializable.parse(species).serialize().latex]), + ), + [form.values.set.states], ); - const { getInputProps } = form; + const referenceMap = useMemo(() => + Object.fromEntries( + Object.entries(form.values.set.references).map(([ + key, + value, + ]) => [key, reference2bibliography(value)]), + ), [form.values.set.references]); return (
{ + onSubmit={form.onSubmit(async (formData) => { + const url = `/api/author/scat-css/${formData.set._key ?? ""}`; + const body = JSON.stringify({ + doc: formData.set, + message: formData.commitMessage, + }); + const headers = new Headers({ + Accept: "application/json", + "Content-Type": "application/json", + }); + const init = { method: "POST", body, headers }; + const res = await fetch(url, init); + const data: unknown = await res.json(); + + if ( + typeof data === "object" && data && "id" in data + && typeof data.id === "string" + ) { + form.setFieldValue("set._key", data.id); + setSubmitMessage(`Saved set with id ${data.id}.`); + window.history.pushState(null, "", `/author/set/${data.id}/edit`); + } + // TODO: Handle user feedback. console.log(data); })} > @@ -196,10 +117,11 @@ export const EditForm = ( defaultValue="general" value={activeTab} onChange={setActiveTab} + keepMounted={false} > General - States + Species References Processes JSON @@ -217,7 +139,18 @@ export const EditForm = ( rows={10} {...getInputProps("set.description")} /> - + + + + +); + +const typeLabelMap = { "CrossSection": "Cross section" }; +const typeSelectData = Object + .entries(typeLabelMap) + .map(([value, label]) => ({ value, label })); + +const ProcessInfoItem = ( + { id, info, references, onChange, onDelete }: { + id: string; + info: ProcessInfo; + references: Record; + onChange: (info: ProcessInfo) => MaybePromise; + onDelete: () => MaybePromise; + }, +) => { + return ( + +
+ + {typeLabelMap[info.type]} + + + + +
+ + + - - -
- Excited - - -
- -
-

Term

- -
- -
-
- -
-
- -
-
- ( - onChange(parseInt(v))} - value={value === undefined ? 1 : value.toString()} - error={errorMsg( - errors, - `set.states.${label}.electronic.${eindex}.term.P`, - )} - > - - - - )} - /> -
-
- -
-
-
- - ) - : ( - - )} - - ); -}; - -const initialValue4AtomLS1 = () => ({ - scheme: "LS1", - config: { - core: { - scheme: "LS", - config: [{ n: 0, l: 0, occupance: 0 }], - term: { L: 0, S: 0, P: 1 }, - }, - excited: { - scheme: "LS", - config: [{ n: 0, l: 0, occupance: 0 }], - term: { L: 0, S: 0, P: 1 }, - }, - }, - term: { - L: 0, - K: 0, - S: 0, - P: 1, - J: 0, - }, -}); -const AtomLS1Form = ({ label }: { label: string }) => { - return ( - ( - - )} - /> - ); -}; - -const MolecularParityField = ({ - label, - eindex, -}: { - label: string; - eindex: number; -}) => { - const { - control, - formState: { errors }, - } = useFormContext(); - return ( -
- ( - onChange(value as "u" | "g")} - value={value as string} - error={errorMsg( - errors, - `set.states.${label}.electronic.${eindex}.parity`, - )} - > - - - - )} - /> -
- ); -}; - -// TODO Use SimpleVibrational here, as it is almost same as this component -const LinearTriatomVibrationalFieldItem = ({ - label, - eindex, - vindex, -}: { - label: string; - eindex: number; - vindex: number; -}) => { - const { watch, setValue } = useFormContext(); - const v = watch( - `set.states.${label}.electronic.${eindex}.vibrational.${vindex}.v`, - ); - const initialScheme = typeof v === "string" ? "simple" : "detailed"; - const [scheme, setScheme] = useState(initialScheme); - return ( -
- { - setScheme(v); - if (v === "simple") { - setValue( - `set.states.${label}.electronic.${eindex}.vibrational.${vindex}`, - { v: "" }, - ); - } else { - setValue( - `set.states.${label}.electronic.${eindex}.vibrational.${vindex}`, - { v: [0, 0, 0], rotational: [] }, - ); - } - }} - > - - - - {scheme === "simple" - ? ( - - ) - : ( - - )} -
- ); -}; - -const VibrationalSimpleFieldItem = ({ - label, - eindex, - vindex, -}: { - label: string; - eindex: number; - vindex: number; -}) => { - const { - control, - formState: { errors }, - } = useFormContext(); - - return ( - ( - - )} - /> - ); -}; - -const LinearTriatomRotationalArray = ({ - label, - eindex, - vindex, -}: { - label: string; - eindex: number; - vindex: number; -}) => { - const { - control, - register, - formState: { errors }, - } = useFormContext(); - const array = useFieldArray({ - control, - name: - `set.states.${label}.electronic.${eindex}.vibrational.${vindex}.rotational`, - }); - return ( -
- Rotational -
    - {array.fields.map((_, index) => ( - array.remove(index)} - > - - - ))} - -
- - -
- ); -}; - -const LinearTriatomVibrationalDetailedFieldItem = ({ - label, - eindex, - vindex, -}: { - label: string; - eindex: number; - vindex: number; -}) => { - const { - register, - formState: { errors }, - } = useFormContext(); - - return ( - <> - - -
- -
-
- -
-
- -
-
- - - ); -}; - -const LinearTriatomVibrationalField = ({ - label, - eindex, -}: { - label: string; - eindex: number; -}) => { - return ( - ( - - )} - /> - ); -}; - -const LinearElectronicForm = ({ - label, - eindex, -}: { - label: string; - eindex: number; -}) => { - const { - register, - control, - formState: { errors }, - } = useFormContext(); - return ( - <> -
- -
-
- -
-
- -
-
- ( - onChange(value as "+" | "-")} - value={value as string} - error={errorMsg( - errors, - `set.states.${label}.electronic.${eindex}.reflection`, - )} - > - - - - )} - /> -
- - ); -}; - -const ElectronicArray = ({ - label, - item, - initialValue, -}: { - label: string; - item: (label: string, eindex: number) => ReactNode; - initialValue: any; // TODO add generic type -}) => { - const { - control, - formState: { errors }, - } = useFormContext(); - const array = useFieldArray({ - control, - name: `set.states.${label}.electronic`, - }); - return ( -
-

Electronic

-
    - {array.fields.map((_field, index) => ( - array.remove(index)} - > - {item(label, index)} - - ))} - -
- -
- ); -}; - -const ArrayItem = ({ - removeTitle, - onRemove, - children, -}: { - removeTitle: string; - onRemove: () => void; - children: ReactNode; -}) => { - return ( -
  • -
    - {children} - -
    -
  • - ); -}; - -const LinearTriatomInversionCenterForm = ({ label }: { label: string }) => { - const initialValue = { e: "X", Lambda: 0, S: 0, parity: "g" }; - return ( - ( - - - - - - - - )} - /> - ); -}; - -const VibrationalArray = ({ - label, - eindex, - item, - initialValue, -}: { - label: string; - eindex: number; - item: (label: string, eindex: number, vindex: number) => ReactNode; - initialValue: any; -}) => { - const { - control, - formState: { errors }, - } = useFormContext(); - const array = useFieldArray({ - control, - name: `set.states.${label}.electronic.${eindex}.vibrational`, - }); - return ( -
    - Vibrational -
      - {array.fields.map((_, index) => ( - array.remove(index)} - > - {item(label, eindex, index)} - - ))} - -
    - - -
    - ); -}; - -const RotationalArray = ({ - label, - eindex, - vindex, -}: { - label: string; - eindex: number; - vindex: number; -}) => { - const { - control, - register, - formState: { errors }, - } = useFormContext(); - const array = useFieldArray({ - control, - name: - `set.states.${label}.electronic.${eindex}.vibrational.${vindex}.rotational`, - }); - return ( -
    - Rotational -
      - {array.fields.map((_, index) => ( - array.remove(index)} - > - - - ))} - -
    - - -
    - ); -}; - -type IScheme = "simple" | "detailed"; - -const SimpleVibrational = ({ - label, - eindex, - vindex, - children, -}: { - label: string; - eindex: number; - vindex: number; - children: ReactNode; -}) => { - const { getValues, setValue } = useFormContext(); - const av = getValues( - `set.states.${label}.electronic.${eindex}.vibrational.${vindex}.v`, - ); - const initialScheme = typeof av === "string" || Number.isNaN(av) - ? "simple" - : "detailed"; - const [scheme, setScheme] = useState(initialScheme); - return ( -
    - { - setScheme(v); - if (v === "simple") { - setValue( - `set.states.${label}.electronic.${eindex}.vibrational.${vindex}`, - { v: "" }, - ); - } else { - setValue( - `set.states.${label}.electronic.${eindex}.vibrational.${vindex}`, - { v: 0, rotational: [] }, - ); - } - }} - > - - - - {scheme === "simple" - ? ( - - ) - : children} -
    - ); -}; - -const DiatomicVibrationalForm = ({ - label, - eindex, -}: { - label: string; - eindex: number; -}) => { - const { - register, - formState: { errors }, - } = useFormContext(); - return ( - ( - - <> - - - - - )} - /> - ); -}; - -const SimpleElectronic = ({ - label, - eindex, - initialDetailedValue, - children, -}: { - label: string; - eindex: number; - initialDetailedValue: any; // TODO add generic type - children: ReactNode; -}) => { - const { - register, - setValue, - control, - formState: { errors }, - } = useFormContext(); - const electronicValue = useWatch({ - control, - name: `set.states.${label}.electronic.${eindex}`, - }); - const initialScheme = - "e" in electronicValue && Object.keys(electronicValue).length <= 1 - ? "simple" - : "detailed"; - const [scheme, setScheme] = useState(initialScheme); - return ( - <> - { - setScheme(v); - if (v === "simple") { - setValue( - `set.states.${label}.electronic`, - initialSimpleElectronic(), - ); - } else { - setValue( - `set.states.${label}.electronic.${eindex}`, - initialDetailedValue, - ); - } - }} - > - - - - {scheme === "simple" - ? ( -
    - -
    - ) - : children} - - ); -}; - -const HeteronuclearDiatomForm = ({ label }: { label: string }) => { - const initialValue = { e: "", Lambda: 0, S: 0 }; - return ( - ( - - - - - - - )} - /> - ); -}; - -const HomonuclearDiatomForm = ({ label }: { label: string }) => { - const initialValue = { e: "", Lambda: 0, S: 0, parity: "g" }; - return ( - ( - - - - - - - - )} - /> - ); -}; - -const StateForm = ({ - label, - onRemove, - expanded, -}: { - label: string; - onRemove: () => void; - expanded: boolean; -}) => { - const { - control, - setValue, - getValues, - formState: { errors }, - } = useFormContext(); - const state = useWatch({ name: `set.states.${label}` }); - // TODO label update based on whole state tricky as existing label (a key in states object) needs to be removed - const latex = useMemo(() => { - try { - return getStateLatex(state); - } catch (error) { - // incomplete state, ignore error and dont update id - return ""; - } - }, [state]); - - return ( - - - {latex} - - - {expanded && ( - <> -
    - ( - { - onChange(v); - if (v === "") { - // unset .type and .electronic - const { particle, charge } = getValues( - `set.states.${label}`, - ); - setValue(`set.states.${label}`, { - particle, - charge, - }); - } else { - setValue(`set.states.${label}.electronic`, []); - } - }} - value={value === undefined ? "" : value} - error={errorMsg(errors, `set.states.${label}.type`)} - > - - - - - - - - - )} - /> -
    - - {state.type === "AtomLS" && } - {state.type === "AtomJ1L2" && } - {state.type === "HeteronuclearDiatom" && ( - - )} - {state.type === "HomonuclearDiatom" && ( - - )} - {state.type === "LinearTriatomInversionCenter" && ( - - )} - {state.type === "AtomLS1" && } - -
    - - )} -
    -
    - ); -}; - -const ReferenceForm = ({ - label, - onRemove, -}: { - label: string; - onRemove: () => void; -}) => { - const { watch } = useFormContext(); - const reference = watch(`set.references.${label}`); - return ( -
  • - - -
  • - ); -}; - -const ImportDOIButton = ({ - onAdd, -}: { - onAdd: (newLabel: string, newReference: ReferenceRecord) => void; -}) => { - const [doi, setDoi] = useState(""); - const [open, setOpen] = useState(false); - async function onSubmit() { - // TODO resolving doi can take long time and timeout, should notify user when fetch fails - // TODO use mailto param to improve speed, see https://github.com/CrossRef/rest-api-doc#good-manners--more-reliable-service - const ref = await doi2csl(doi); - // TODO handle fetch/parse errors - const label = getReferenceLabel(ref); - onAdd(label, ref); - setOpen(false); - } - return ( -
    - - setOpen(false)} - title="Import reference based on DOI" - > - setDoi(e.target.value)} - placeholder="Enter DOI like 10.5284/1015681" - // DOI pattern from https://www.crossref.org/blog/dois-and-matching-regular-expressions/ - // Does not work for `10.3390/atoms9010016` - // pattern="^10.\d{4,9}/[-._;()/:A-Z0-9]+$" - /> - - - - -
    - ); -}; - -const ImportBibTeXDOIButton = ({ - onAdd, -}: { - onAdd: (refs: Record) => void; -}) => { - const [bibtex, setBibtex] = useState(""); - const [open, setOpen] = useState(false); - async function onSubmit() { - // TODO resolving doi can take long time and timeout, should notify user when fetch fails - const labelRefs = await bibtex2csl(bibtex); - - onAdd(labelRefs); - setOpen(false); - } - const placeholder = `Enter BibTeX like: -@Article{atoms9010016, - AUTHOR = {Carbone, Emile and Graef, Wouter and Hagelaar, Gerjan and Boer, Daan and Hopkins, Matthew M. and Stephens, Jacob C. and Yee, Benjamin T. and Pancheshnyi, Sergey and van Dijk, Jan and Pitchford, Leanne}, - TITLE = {Data Needs for Modeling Low-Temperature Non-Equilibrium Plasmas: The LXCat Project, History, Perspectives and a Tutorial}, - JOURNAL = {Atoms}, - VOLUME = {9}, - YEAR = {2021}, - NUMBER = {1}, - ARTICLE-NUMBER = {16}, - URL = {https://www.mdpi.com/2218-2004/9/1/16}, - ISSN = {2218-2004}, - DOI = {10.3390/atoms9010016} -}`; - return ( -
    - - setOpen(false)} - title="Import references based on BibTeX" - > -