From 16a31c5610c6b892460c1fd6d792932294b83c5e Mon Sep 17 00:00:00 2001 From: Oliver Roick Date: Tue, 10 Sep 2024 11:33:12 +1000 Subject: [PATCH 1/2] Pre-select github repo and ref --- src/ImageBuilder.jsx | 15 +- src/ProfileForm.jsx | 2 + src/ProfileForm.test.js | 368 ++++++++++++++++++-------------- src/ProfileOptions.jsx | 9 +- src/ResourceSelect.jsx | 3 +- src/components/form/fields.jsx | 3 + src/form.css | 5 + src/hooks/useRefField.js | 6 +- src/hooks/useRepositoryField.js | 13 +- src/state.js | 38 +++- src/utils/index.js | 7 + 11 files changed, 286 insertions(+), 183 deletions(-) create mode 100644 src/utils/index.js diff --git a/src/ImageBuilder.jsx b/src/ImageBuilder.jsx index 12a6159..0480e26 100644 --- a/src/ImageBuilder.jsx +++ b/src/ImageBuilder.jsx @@ -1,5 +1,6 @@ -import { useEffect, useState, useRef } from "react"; +import { useEffect, useState, useRef, useContext } from "react"; import Select from "react-select"; +import { SpawnerFormContext } from "./state"; import useRepositoryField from "./hooks/useRepositoryField"; import useRefField from "./hooks/useRefField"; @@ -91,9 +92,10 @@ function ImageLogs({ setTerm, setFitAddon }) { } export function ImageBuilder({ name }) { + const { binderRepo, ref: repoRef, setCustomOption } = useContext(SpawnerFormContext); const { repo, repoId, repoFieldProps, repoError, repoIsValidating } = - useRepositoryField(); - const { ref, refError, refFieldProps, refIsLoading } = useRefField(repoId); + useRepositoryField(binderRepo); + const { ref, refError, refFieldProps, refIsLoading } = useRefField(repoId, repoRef); const repoFieldRef = useRef(); const branchFieldRef = useRef(); @@ -102,6 +104,13 @@ export function ImageBuilder({ name }) { const [term, setTerm] = useState(null); const [fitAddon, setFitAddon] = useState(null); + useEffect(() => { + if (setCustomOption) { + repoFieldRef.current.setAttribute("value", binderRepo); + branchFieldRef.current.value = repoRef; + } + }, [binderRepo, repoRef, setCustomOption]); + const handleBuildStart = async () => { if (!repo) { repoFieldRef.current.focus(); diff --git a/src/ProfileForm.jsx b/src/ProfileForm.jsx index f5a02a9..e593a5d 100644 --- a/src/ProfileForm.jsx +++ b/src/ProfileForm.jsx @@ -17,6 +17,7 @@ function Form() { profile: selectedProfile, setProfile, profileList, + paramsError } = useContext(SpawnerFormContext); const [formError, setFormError] = useState(""); @@ -46,6 +47,7 @@ function Form() { aria-description="First, select the profile; second, configure the options for the selected profile." > {formError &&
{formError}
} + {paramsError &&
{paramsError}
} { - render( - - - , - ); +describe("Profile form", () => { + test("image and resource fields initially not tabable", async () => { + render( + + + , + ); - const imageField = screen.getByLabelText("Image"); - expect(imageField.tabIndex).toEqual(-1); + const imageField = screen.getByLabelText("Image"); + expect(imageField.tabIndex).toEqual(-1); - const resourceField = screen.getByLabelText("Resource Allocation"); - expect(resourceField.tabIndex).toEqual(-1); -}); - -test("image and resource fields tabable", async () => { - const user = userEvent.setup(); - - render( - - - , - ); - - const radio = screen.getByRole("radio", { - name: "CPU only No GPU, only CPU", + const resourceField = screen.getByLabelText("Resource Allocation"); + expect(resourceField.tabIndex).toEqual(-1); }); - await user.click(radio); - const imageField = screen.getByLabelText("Image"); - expect(imageField.tabIndex).toEqual(0); + test("image and resource fields tabable", async () => { + const user = userEvent.setup(); - const resourceField = screen.getByLabelText("Resource Allocation"); - expect(resourceField.tabIndex).toEqual(0); -}); + render( + + + , + ); -test("custom image field is required", async () => { - const user = userEvent.setup(); + const radio = screen.getByRole("radio", { + name: "CPU only No GPU, only CPU", + }); + await user.click(radio); - render( - - - , - ); + const imageField = screen.getByLabelText("Image"); + expect(imageField.tabIndex).toEqual(0); - const radio = screen.getByRole("radio", { - name: "CPU only No GPU, only CPU", + const resourceField = screen.getByLabelText("Resource Allocation"); + expect(resourceField.tabIndex).toEqual(0); }); - await user.click(radio); - const imageField = screen.getByLabelText("Image"); - await user.click(imageField); - await user.click(screen.getByText("Specify an existing docker image")); + test("custom image field is required", async () => { + const user = userEvent.setup(); - const customImageField = screen.getByLabelText("Custom image"); - await user.click(customImageField); - await user.click(document.body); + render( + + + , + ); - expect(screen.getByText("Enter a value.")).toBeInTheDocument(); -}); + const radio = screen.getByRole("radio", { + name: "CPU only No GPU, only CPU", + }); + await user.click(radio); -test("custom image field needs specific format", async () => { - const user = userEvent.setup(); + const imageField = screen.getByLabelText("Image"); + await user.click(imageField); + await user.click(screen.getByText("Specify an existing docker image")); - render( - - - , - ); + const customImageField = screen.getByLabelText("Custom image"); + await user.click(customImageField); + await user.click(document.body); - const radio = screen.getByRole("radio", { - name: "CPU only No GPU, only CPU", + expect(screen.getByText("Enter a value.")).toBeInTheDocument(); }); - await user.click(radio); - - const imageField = screen.getByLabelText("Image"); - await user.click(imageField); - await user.click(screen.getByText("Specify an existing docker image")); - - const customImageField = screen.getByLabelText("Custom image"); - await user.type(customImageField, "abc"); - await user.click(document.body); - - expect( - screen.getByText( - "Must be a publicly available docker image, of form :", - ), - ).toBeInTheDocument(); -}); - -test("custom image field accepts specific format", async () => { - const user = userEvent.setup(); - render( - - - , - ); + test("custom image field needs specific format", async () => { + const user = userEvent.setup(); + + render( + + + , + ); + + const radio = screen.getByRole("radio", { + name: "CPU only No GPU, only CPU", + }); + await user.click(radio); + + const imageField = screen.getByLabelText("Image"); + await user.click(imageField); + await user.click(screen.getByText("Specify an existing docker image")); + + const customImageField = screen.getByLabelText("Custom image"); + await user.type(customImageField, "abc"); + await user.click(document.body); + + expect( + screen.getByText( + "Must be a publicly available docker image, of form :", + ), + ).toBeInTheDocument(); + }); - const radio = screen.getByRole("radio", { - name: "CPU only No GPU, only CPU", + test("custom image field accepts specific format", async () => { + const user = userEvent.setup(); + + render( + + + , + ); + + const radio = screen.getByRole("radio", { + name: "CPU only No GPU, only CPU", + }); + await user.click(radio); + + const imageField = screen.getByLabelText("Image"); + await user.click(imageField); + await user.click(screen.getByText("Specify an existing docker image")); + + const customImageField = screen.getByLabelText("Custom image"); + await user.type(customImageField, "abc:123"); + await user.click(document.body); + + expect(screen.queryByText("Enter a value.")).not.toBeInTheDocument(); + expect( + screen.queryByText( + "Must be a publicly available docker image, of form :", + ), + ).not.toBeInTheDocument(); }); - await user.click(radio); - - const imageField = screen.getByLabelText("Image"); - await user.click(imageField); - await user.click(screen.getByText("Specify an existing docker image")); - - const customImageField = screen.getByLabelText("Custom image"); - await user.type(customImageField, "abc:123"); - await user.click(document.body); - - expect(screen.queryByText("Enter a value.")).not.toBeInTheDocument(); - expect( - screen.queryByText( - "Must be a publicly available docker image, of form :", - ), - ).not.toBeInTheDocument(); -}); -test("Multiple profiles renders", async () => { - const user = userEvent.setup(); + test("Multiple profiles renders", async () => { + const user = userEvent.setup(); - render( - - - , - ); + render( + + + , + ); - const radio = screen.getByRole("radio", { name: "GPU Nvidia Tesla T4 GPU" }); - await user.click(radio); + const radio = screen.getByRole("radio", { name: "GPU Nvidia Tesla T4 GPU" }); + await user.click(radio); - const imageField = screen.getByLabelText("Image - GPU"); - expect(imageField.tabIndex).toEqual(0); - expect(screen.getByLabelText("Resource Allocation - GPU").tabIndex).toEqual( - 0, - ); + const imageField = screen.getByLabelText("Image - GPU"); + expect(imageField.tabIndex).toEqual(0); + expect(screen.getByLabelText("Resource Allocation - GPU").tabIndex).toEqual( + 0, + ); - const smallImageField = screen.getByLabelText("Image"); - await user.click(smallImageField); - await user.click(screen.getByText("Specify an existing docker image")); + const smallImageField = screen.getByLabelText("Image"); + await user.click(smallImageField); + await user.click(screen.getByText("Specify an existing docker image")); - const customImageField = screen.getByLabelText("Custom image"); - await user.click(customImageField); - await user.click(document.body); + const customImageField = screen.getByLabelText("Custom image"); + await user.click(customImageField); + await user.click(document.body); - expect(screen.queryByText("Enter a value.")).toBeInTheDocument(); + expect(screen.queryByText("Enter a value.")).toBeInTheDocument(); - expect(smallImageField.tabIndex).toEqual(0); - expect(screen.getByLabelText("Resource Allocation").tabIndex).toEqual(0); - expect(imageField.tabIndex).toEqual(-1); - expect(screen.getByLabelText("Resource Allocation - GPU").tabIndex).toEqual( - -1, - ); -}); + expect(smallImageField.tabIndex).toEqual(0); + expect(screen.getByLabelText("Resource Allocation").tabIndex).toEqual(0); + expect(imageField.tabIndex).toEqual(-1); + expect(screen.getByLabelText("Resource Allocation - GPU").tabIndex).toEqual( + -1, + ); + }); -test("select with no options should not render", () => { - render( - - - , - ); - expect(screen.queryByLabelText("Image - No options")).not.toBeInTheDocument(); -}); + test("select with no options should not render", () => { + render( + + + , + ); + expect(screen.queryByLabelText("Image - No options")).not.toBeInTheDocument(); + }); -test("profile marked as default is selected by default", () => { - const { container } = render( - - - , - ); - const hiddenRadio = container.querySelector('[name="profile"]'); - expect(hiddenRadio.value).toEqual("custom"); - const defaultRadio = screen.getByRole("radio", { - name: "Bring your own image Specify your own docker image", + test("profile marked as default is selected by default", () => { + const { container } = render( + + + , + ); + const hiddenRadio = container.querySelector('[name="profile"]'); + expect(hiddenRadio.value).toEqual("custom"); + const defaultRadio = screen.getByRole("radio", { + name: "Bring your own image Specify your own docker image", + }); + expect(defaultRadio.checked).toBeTruthy(); + const nonDefaultRadio = screen.getByRole("radio", { + name: "GPU Nvidia Tesla T4 GPU", + }); + expect(nonDefaultRadio.checked).toBeFalsy(); }); - expect(defaultRadio.checked).toBeTruthy(); - const nonDefaultRadio = screen.getByRole("radio", { - name: "GPU Nvidia Tesla T4 GPU", + + test("having dynamic_image_building enabled and no other choices shows dropdown", async () => { + const user = userEvent.setup(); + + render( + + + , + ); + const select = screen.getByLabelText("Image - dynamic image building"); + await user.click(select); + expect(screen.getByText("Build your own image")).toBeInTheDocument(); + expect(screen.getAllByText("Other...").length).toEqual(2); // There are two selects with the "Other..." label defined + }); +}) + +describe("Profile form with URL Params", () => { + beforeEach(() => { + const location = { + ...window.location, + search: '?binderProvider=gh&binderRepo=org/repo&ref=v1.0', + }; + Object.defineProperty(window, 'location', { + writable: true, + value: location, + }) }); - expect(nonDefaultRadio.checked).toBeFalsy(); -}); -test("having dynamic_image_building enabled and no other choices shows dropdown", async () => { - const user = userEvent.setup(); - - render( - - - , - ); - const select = screen.getByLabelText("Image - dynamic image building"); - await user.click(select); - expect(screen.getByText("Build your own image")).toBeInTheDocument(); - expect(screen.getAllByText("Other...").length).toEqual(2); // There are two selects with the "Other..." label defined + test("preselects values", async () => { + fetch + .mockResponseOnce("") + .mockResponseOnce(JSON.stringify([{ name: "main" }, { name: "develop" }])) + .mockResponseOnce(JSON.stringify([{ name: "v1.0" }])); + + render( + + + , + ); + + const radio = screen.getByRole("radio", { + name: "Build custom environment Dynamic Image building + unlisted choice", + }); + expect(radio.checked).toBeTruthy(); + + expect(screen.getByLabelText("Repository").value).toEqual("org/repo"); + await waitFor(() => expect(fetch.mock.calls[2][0]).toEqual( + "https://api.github.com/repos/org/repo/tags", + )); + + await waitFor(() => expect(screen.getByText("v1.0")).toBeInTheDocument()); + }); }); diff --git a/src/ProfileOptions.jsx b/src/ProfileOptions.jsx index 515e63e..aa3ef0d 100644 --- a/src/ProfileOptions.jsx +++ b/src/ProfileOptions.jsx @@ -1,13 +1,6 @@ import ResourceSelect from "./ResourceSelect"; import { ImageBuilder } from "./ImageBuilder"; - -function hasDynamicImageBuilding(key, option) { - return ( - key === "image" && - option.dynamic_image_building?.enabled && - option.unlisted_choice?.enabled - ); -} +import { hasDynamicImageBuilding } from "./utils" export function ProfileOptions({ config, profile }) { return ( diff --git a/src/ResourceSelect.jsx b/src/ResourceSelect.jsx index 760983b..24b3869 100644 --- a/src/ResourceSelect.jsx +++ b/src/ResourceSelect.jsx @@ -6,6 +6,7 @@ import { SelectField, TextField } from "./components/form/fields"; function ResourceSelect({ id, profile, config, customOptions = [] }) { const { display_name, unlisted_choice } = config; + const { setCustomOption } = useContext(SpawnerFormContext); const { options, defaultOption, hasDefaultChoices } = useSelectOptions( config, customOptions, @@ -15,7 +16,7 @@ function ResourceSelect({ id, profile, config, customOptions = [] }) { const FIELD_ID_UNLISTED = `${FIELD_ID}--unlisted-choice`; const isActive = selectedProfile?.slug === profile; - const [value, setValue] = useState(defaultOption?.value); + const [value, setValue] = useState(setCustomOption ? "--extra-selectable-item" : defaultOption?.value); const [unlistedChoiceValue, setUnlistedChoiceValue] = useState(""); if (!options.length > 0) { diff --git a/src/components/form/fields.jsx b/src/components/form/fields.jsx index 8a5a508..40937a8 100644 --- a/src/components/form/fields.jsx +++ b/src/components/form/fields.jsx @@ -50,6 +50,8 @@ export function SelectField({ const required = !!validate.required; const error = validateField(value, validate, touched); + const selectedOption = options.find(({ value: optionVal }) => optionVal === value); + return ( ); diff --git a/src/form.css b/src/form.css index 3910bc8..3af65e1 100644 --- a/src/form.css +++ b/src/form.css @@ -97,6 +97,11 @@ margin-bottom: 1rem; } +.profile-form-warning { + color: #eab308; + margin-bottom: 1rem; +} + .btn-jupyter { margin-top: 0.5rem; } diff --git a/src/hooks/useRefField.js b/src/hooks/useRefField.js index c872cb9..09e84ce 100644 --- a/src/hooks/useRefField.js +++ b/src/hooks/useRefField.js @@ -8,8 +8,8 @@ function fetchRef(repository, refType) { ); } -export default function useRefField(repository) { - const [value, setValue] = useState(""); +export default function useRefField(repository, defaultValue) { + const [value, setValue] = useState(defaultValue); const [options, setOptions] = useState(); const [error, setError] = useState(); const [isLoading, setIsLoading] = useState(); @@ -17,7 +17,7 @@ export default function useRefField(repository) { const selectedOption = useMemo(() => { if (!value || !options) return; return options.find((option) => option.value === value); - }, [value]); + }, [value, options]); useEffect(() => { setIsLoading(true); diff --git a/src/hooks/useRepositoryField.js b/src/hooks/useRepositoryField.js index 5363fd5..2d92019 100644 --- a/src/hooks/useRepositoryField.js +++ b/src/hooks/useRepositoryField.js @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; function extractOrgAndRepo(value) { let orgRepoString; @@ -19,12 +19,19 @@ function extractOrgAndRepo(value) { return orgRepoString; } -export default function useRepositoryField() { - const [value, setValue] = useState(""); +export default function useRepositoryField(defaultValue) { + const [value, setValue] = useState(defaultValue); const [error, setError] = useState(); const [repoId, setRepoId] = useState(); const [isValidating, setIsValidating] = useState(false); + useEffect(() => { + if (defaultValue) { + // Automatically validate the value if the defaultValue is set + onBlur(); + } + }, [defaultValue]); + const validate = async () => { setIsValidating(true); setError(); diff --git a/src/state.js b/src/state.js index 35bf6d6..1bc0f10 100644 --- a/src/state.js +++ b/src/state.js @@ -1,7 +1,15 @@ -import { createContext, useMemo, useState } from "react"; +import { createContext, useEffect, useMemo, useState } from "react"; +import { hasDynamicImageBuilding } from "./utils" export const SpawnerFormContext = createContext(); +function isDynamicImageProfile(profile) { + const { profile_options } = profile; + return Object.entries(profile_options).some( + ([key, option]) => hasDynamicImageBuilding(key, option) + ); +} + export const SpawnerFormProvider = ({ children }) => { const profileList = window.profileList; const defaultProfile = @@ -12,10 +20,38 @@ export const SpawnerFormProvider = ({ children }) => { return profileList.find(({ slug }) => slug === selectedProfile); }, [selectedProfile]); + const params = new Proxy(new URLSearchParams(window.location.search), { + get: (searchParams, prop) => searchParams.get(prop), + }); + const { binderProvider, binderRepo, ref } = params; + + const paramsError = useMemo(() => { + if(binderProvider && binderRepo && ref) { + const profilesWithDynamicImageBuilding = profileList.filter(isDynamicImageProfile); + if (profilesWithDynamicImageBuilding.length > 1) { + return "Unable to pre-select dynamic image building." + } + } + }, [binderProvider, binderRepo, ref]); + + const setCustomOption = binderProvider && binderRepo && ref && !paramsError; + + useEffect(() => { + if(setCustomOption) { + const dynamicImageProfile = profileList.find(isDynamicImageProfile); + setProfile(dynamicImageProfile.slug); + } + }, [setCustomOption]); + const value = { profileList, profile, setProfile, + binderProvider, + binderRepo, + ref, + paramsError, + setCustomOption }; return ( diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..a7d3e27 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,7 @@ +export function hasDynamicImageBuilding(key, option) { + return ( + key === "image" && + option.dynamic_image_building?.enabled && + option.unlisted_choice?.enabled + ); +} From 15c555b1e3602f98781d6d535714773fb5edf91f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 01:41:01 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/ImageBuilder.jsx | 11 +++++++++-- src/ProfileForm.jsx | 2 +- src/ProfileForm.test.js | 24 +++++++++++++++--------- src/ProfileOptions.jsx | 2 +- src/ResourceSelect.jsx | 4 +++- src/components/form/fields.jsx | 4 +++- src/state.js | 18 ++++++++++-------- 7 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/ImageBuilder.jsx b/src/ImageBuilder.jsx index 0480e26..0261454 100644 --- a/src/ImageBuilder.jsx +++ b/src/ImageBuilder.jsx @@ -92,10 +92,17 @@ function ImageLogs({ setTerm, setFitAddon }) { } export function ImageBuilder({ name }) { - const { binderRepo, ref: repoRef, setCustomOption } = useContext(SpawnerFormContext); + const { + binderRepo, + ref: repoRef, + setCustomOption, + } = useContext(SpawnerFormContext); const { repo, repoId, repoFieldProps, repoError, repoIsValidating } = useRepositoryField(binderRepo); - const { ref, refError, refFieldProps, refIsLoading } = useRefField(repoId, repoRef); + const { ref, refError, refFieldProps, refIsLoading } = useRefField( + repoId, + repoRef, + ); const repoFieldRef = useRef(); const branchFieldRef = useRef(); diff --git a/src/ProfileForm.jsx b/src/ProfileForm.jsx index e593a5d..88754ec 100644 --- a/src/ProfileForm.jsx +++ b/src/ProfileForm.jsx @@ -17,7 +17,7 @@ function Form() { profile: selectedProfile, setProfile, profileList, - paramsError + paramsError, } = useContext(SpawnerFormContext); const [formError, setFormError] = useState(""); diff --git a/src/ProfileForm.test.js b/src/ProfileForm.test.js index cb36bb0..d458f8d 100644 --- a/src/ProfileForm.test.js +++ b/src/ProfileForm.test.js @@ -134,7 +134,9 @@ describe("Profile form", () => { , ); - const radio = screen.getByRole("radio", { name: "GPU Nvidia Tesla T4 GPU" }); + const radio = screen.getByRole("radio", { + name: "GPU Nvidia Tesla T4 GPU", + }); await user.click(radio); const imageField = screen.getByLabelText("Image - GPU"); @@ -167,7 +169,9 @@ describe("Profile form", () => { , ); - expect(screen.queryByLabelText("Image - No options")).not.toBeInTheDocument(); + expect( + screen.queryByLabelText("Image - No options"), + ).not.toBeInTheDocument(); }); test("profile marked as default is selected by default", () => { @@ -201,18 +205,18 @@ describe("Profile form", () => { expect(screen.getByText("Build your own image")).toBeInTheDocument(); expect(screen.getAllByText("Other...").length).toEqual(2); // There are two selects with the "Other..." label defined }); -}) +}); describe("Profile form with URL Params", () => { beforeEach(() => { const location = { ...window.location, - search: '?binderProvider=gh&binderRepo=org/repo&ref=v1.0', + search: "?binderProvider=gh&binderRepo=org/repo&ref=v1.0", }; - Object.defineProperty(window, 'location', { + Object.defineProperty(window, "location", { writable: true, value: location, - }) + }); }); test("preselects values", async () => { @@ -233,9 +237,11 @@ describe("Profile form with URL Params", () => { expect(radio.checked).toBeTruthy(); expect(screen.getByLabelText("Repository").value).toEqual("org/repo"); - await waitFor(() => expect(fetch.mock.calls[2][0]).toEqual( - "https://api.github.com/repos/org/repo/tags", - )); + await waitFor(() => + expect(fetch.mock.calls[2][0]).toEqual( + "https://api.github.com/repos/org/repo/tags", + ), + ); await waitFor(() => expect(screen.getByText("v1.0")).toBeInTheDocument()); }); diff --git a/src/ProfileOptions.jsx b/src/ProfileOptions.jsx index aa3ef0d..6302ce6 100644 --- a/src/ProfileOptions.jsx +++ b/src/ProfileOptions.jsx @@ -1,6 +1,6 @@ import ResourceSelect from "./ResourceSelect"; import { ImageBuilder } from "./ImageBuilder"; -import { hasDynamicImageBuilding } from "./utils" +import { hasDynamicImageBuilding } from "./utils"; export function ProfileOptions({ config, profile }) { return ( diff --git a/src/ResourceSelect.jsx b/src/ResourceSelect.jsx index 24b3869..18f90b5 100644 --- a/src/ResourceSelect.jsx +++ b/src/ResourceSelect.jsx @@ -16,7 +16,9 @@ function ResourceSelect({ id, profile, config, customOptions = [] }) { const FIELD_ID_UNLISTED = `${FIELD_ID}--unlisted-choice`; const isActive = selectedProfile?.slug === profile; - const [value, setValue] = useState(setCustomOption ? "--extra-selectable-item" : defaultOption?.value); + const [value, setValue] = useState( + setCustomOption ? "--extra-selectable-item" : defaultOption?.value, + ); const [unlistedChoiceValue, setUnlistedChoiceValue] = useState(""); if (!options.length > 0) { diff --git a/src/components/form/fields.jsx b/src/components/form/fields.jsx index 40937a8..9825ffe 100644 --- a/src/components/form/fields.jsx +++ b/src/components/form/fields.jsx @@ -50,7 +50,9 @@ export function SelectField({ const required = !!validate.required; const error = validateField(value, validate, touched); - const selectedOption = options.find(({ value: optionVal }) => optionVal === value); + const selectedOption = options.find( + ({ value: optionVal }) => optionVal === value, + ); return ( diff --git a/src/state.js b/src/state.js index 1bc0f10..5d261c1 100644 --- a/src/state.js +++ b/src/state.js @@ -1,12 +1,12 @@ import { createContext, useEffect, useMemo, useState } from "react"; -import { hasDynamicImageBuilding } from "./utils" +import { hasDynamicImageBuilding } from "./utils"; export const SpawnerFormContext = createContext(); function isDynamicImageProfile(profile) { const { profile_options } = profile; - return Object.entries(profile_options).some( - ([key, option]) => hasDynamicImageBuilding(key, option) + return Object.entries(profile_options).some(([key, option]) => + hasDynamicImageBuilding(key, option), ); } @@ -26,10 +26,12 @@ export const SpawnerFormProvider = ({ children }) => { const { binderProvider, binderRepo, ref } = params; const paramsError = useMemo(() => { - if(binderProvider && binderRepo && ref) { - const profilesWithDynamicImageBuilding = profileList.filter(isDynamicImageProfile); + if (binderProvider && binderRepo && ref) { + const profilesWithDynamicImageBuilding = profileList.filter( + isDynamicImageProfile, + ); if (profilesWithDynamicImageBuilding.length > 1) { - return "Unable to pre-select dynamic image building." + return "Unable to pre-select dynamic image building."; } } }, [binderProvider, binderRepo, ref]); @@ -37,7 +39,7 @@ export const SpawnerFormProvider = ({ children }) => { const setCustomOption = binderProvider && binderRepo && ref && !paramsError; useEffect(() => { - if(setCustomOption) { + if (setCustomOption) { const dynamicImageProfile = profileList.find(isDynamicImageProfile); setProfile(dynamicImageProfile.slug); } @@ -51,7 +53,7 @@ export const SpawnerFormProvider = ({ children }) => { binderRepo, ref, paramsError, - setCustomOption + setCustomOption, }; return (