From 0435934429517fe1f34912a75fbb3a8460f87856 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 11 Jun 2026 07:50:17 -0400 Subject: [PATCH 1/5] Restore Zustand launch state loading Initialize the Zustand store before paint, reload feature data when feature-query fields change, ignore stale async loader responses, and keep reset defaults safe when option buckets are missing. Assisted-by: opencode:gpt-5.5 --- .../__tests__/useAvailableFeatures.test.jsx | 14 ++- app/launch/src/state/ApplicationState.jsx | 16 ++- app/launch/src/state/store.js | 104 ++++++++++++++---- 3 files changed, 106 insertions(+), 28 deletions(-) diff --git a/app/launch/src/components/Application/__tests__/useAvailableFeatures.test.jsx b/app/launch/src/components/Application/__tests__/useAvailableFeatures.test.jsx index 1dfc913..ff268fe 100644 --- a/app/launch/src/components/Application/__tests__/useAvailableFeatures.test.jsx +++ b/app/launch/src/components/Application/__tests__/useAvailableFeatures.test.jsx @@ -19,6 +19,13 @@ const MockSdk = { } return types }, + defaultIncludedFeatures: async ({ type }) => { + const types = MockTypes[type] + if (!types) { + throw new Error('Invalid Type') + } + return { features: [] } + }, } beforeEach(() => { @@ -80,11 +87,16 @@ TEST_DATA.forEach(({ initialData, hasError }) => { await new Promise((r) => setTimeout(r, 50)) }) - expect(container).toBeDefined() if (hasError) { expect(error).not.toBeNull() + expect(container.querySelector('.reloading').textContent).toEqual('[]') } else { expect(error).toBeNull() + expect(JSON.parse(container.querySelector('.reloading').textContent)).toEqual([ + 'a', + 'b', + 'c', + ]) } }) }) diff --git a/app/launch/src/state/ApplicationState.jsx b/app/launch/src/state/ApplicationState.jsx index 249ba90..bd5772d 100644 --- a/app/launch/src/state/ApplicationState.jsx +++ b/app/launch/src/state/ApplicationState.jsx @@ -1,22 +1,26 @@ -import { useEffect } from 'react' -import { useAppStore } from './store' +import { useLayoutEffect, useRef } from 'react' import { initializeStateFactory } from './factories/initializeState' +import { useAppStore } from './store' export default function ApplicationState({ - initialData, + initialData = {}, stateInitializer, children, }) { - useEffect(() => { + const initialized = useRef(false) + + useLayoutEffect(() => { + if (initialized.current) return + + initialized.current = true const initializer = typeof stateInitializer === 'function' ? stateInitializer : initializeStateFactory(initialData) - // Apply initial state to the store if (typeof initializer === 'function') { initializer(useAppStore) } - }, []) // eslint-disable-line react-hooks/exhaustive-deps + }, [initialData, stateInitializer]) return <>{children} } diff --git a/app/launch/src/state/store.js b/app/launch/src/state/store.js index 2aa98a9..979fde9 100644 --- a/app/launch/src/state/store.js +++ b/app/launch/src/state/store.js @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { create } from 'zustand' import { sharableLink } from '../helpers/Routing' @@ -11,6 +11,17 @@ const INITIAL_FORM_DATA_STORAGE_KEY = 'INITIAL_FORM_DATA' const versionLoader = loadVersions() +const optionsRequestKey = (state) => state.getBaseUrl() + +const featuresRequestKey = (state) => JSON.stringify({ + baseUrl: state.getBaseUrl(), + appType: state.appType, + javaVersion: state.javaVersion, + servlet: state.servlet, + gorm: state.gorm, + reloading: state.reloading, +}) + // Main application store export const useAppStore = create((set, get) => ({ // Initial values @@ -38,13 +49,16 @@ export const useAppStore = create((set, get) => ({ // Options (loaded from API) options: {}, optionsLoading: false, + optionsRequestId: 0, // Features availableFeatures: [], availableFeaturesLoading: false, availableFeaturesError: null, + availableFeaturesRequestId: 0, defaultIncludedFeatures: [], defaultIncludedFeaturesLoading: false, + defaultIncludedFeaturesRequestId: 0, // Actions setInitialValues: (values) => set({ initialValues: values }), @@ -143,12 +157,18 @@ export const useAppStore = create((set, get) => ({ loadOptions: async () => { const sdk = get().getSdk() if (!sdk) return - set({ optionsLoading: true }) + const requestId = get().optionsRequestId + 1 + const requestKey = optionsRequestKey(get()) + set({ optionsLoading: true, optionsRequestId: requestId }) try { const options = await sdk.selectOptions() - set({ options, optionsLoading: false }) - } catch (_e) { - set({ optionsLoading: false }) + if (get().optionsRequestId === requestId && optionsRequestKey(get()) === requestKey) { + set({ options, optionsLoading: false }) + } + } catch { + if (get().optionsRequestId === requestId && optionsRequestKey(get()) === requestKey) { + set({ optionsLoading: false }) + } } }, loadAvailableFeatures: async () => { @@ -159,41 +179,70 @@ export const useAppStore = create((set, get) => ({ set({ availableFeatures: [], availableFeaturesLoading: false }) return } - set({ availableFeaturesLoading: true, availableFeaturesError: null }) + const requestId = get().availableFeaturesRequestId + 1 + const requestKey = featuresRequestKey(get()) + set({ + availableFeaturesLoading: true, + availableFeaturesError: null, + availableFeaturesRequestId: requestId, + }) try { const { features } = await sdk.features({ type: appType, form }) - set({ availableFeatures: features, availableFeaturesLoading: false }) + if (get().availableFeaturesRequestId === requestId && featuresRequestKey(get()) === requestKey) { + set({ availableFeatures: features, availableFeaturesLoading: false }) + } } catch (error) { - set({ availableFeatures: [], availableFeaturesLoading: false, availableFeaturesError: error }) + if (get().availableFeaturesRequestId === requestId && featuresRequestKey(get()) === requestKey) { + set({ availableFeatures: [], availableFeaturesLoading: false, availableFeaturesError: error }) + } } }, loadDefaultIncludedFeatures: async () => { const sdk = get().getSdk() const appType = get().appType const form = get().getStarterForm() - if (!sdk || !form.type) { + if (!sdk || !appType) { set({ defaultIncludedFeatures: [] }) return } - set({ defaultIncludedFeaturesLoading: true }) + const requestId = get().defaultIncludedFeaturesRequestId + 1 + const requestKey = featuresRequestKey(get()) + set({ + defaultIncludedFeaturesLoading: true, + defaultIncludedFeaturesRequestId: requestId, + }) try { const { features } = await sdk.defaultIncludedFeatures({ type: appType, form }) - set({ defaultIncludedFeatures: features, defaultIncludedFeaturesLoading: false }) - } catch (_e) { - set({ defaultIncludedFeatures: [], defaultIncludedFeaturesLoading: false }) + if (get().defaultIncludedFeaturesRequestId === requestId && featuresRequestKey(get()) === requestKey) { + set({ defaultIncludedFeatures: features, defaultIncludedFeaturesLoading: false }) + } + } catch { + if (get().defaultIncludedFeaturesRequestId === requestId && featuresRequestKey(get()) === requestKey) { + set({ defaultIncludedFeatures: [], defaultIncludedFeaturesLoading: false }) + } } }, resetForm: async () => { - const options = get().options + let options = get().options + if (!options.type) { + await get().loadOptions() + options = get().options + } + const state = get() + const appType = options.type?.defaultOption?.value ?? state.appType + if (!appType) return + const optionDefault = (key, fallback) => + options[key]?.defaultOption?.value ?? fallback + const resets = formResets({}) set({ name: resets.name, package: resets.package, - appType: options.type?.defaultOption?.value, - servlet: options.servlet?.defaultOption?.value, - gorm: options.gorm?.defaultOption?.value, - reloading: options.reloading?.defaultOption?.value, - javaVersion: options.jdkVersion?.defaultOption?.value, + appType, + servlet: optionDefault('servlet', state.servlet), + gorm: optionDefault('gorm', state.gorm), + reloading: optionDefault('reloading', state.reloading), + javaVersion: optionDefault('jdkVersion', state.javaVersion), features: {}, }) }, @@ -481,7 +530,7 @@ export const useGetStarterForm = () => { } export const useResetStarterForm = () => { - return () => useAppStore.getState().resetForm() + return useCallback(() => useAppStore.getState().resetForm(), []) } // Load options when version changes @@ -499,6 +548,10 @@ export function useLoadOptionsEffect() { export function useLoadFeaturesEffect() { const appType = useAppStore((s) => s.appType) const selectedVersion = useAppStore((s) => s.selectedVersion) + const javaVersion = useAppStore((s) => s.javaVersion) + const servlet = useAppStore((s) => s.servlet) + const gorm = useAppStore((s) => s.gorm) + const reloading = useAppStore((s) => s.reloading) const loadAvailableFeatures = useAppStore((s) => s.loadAvailableFeatures) const loadDefaultIncludedFeatures = useAppStore((s) => s.loadDefaultIncludedFeatures) useEffect(() => { @@ -506,5 +559,14 @@ export function useLoadFeaturesEffect() { loadAvailableFeatures() loadDefaultIncludedFeatures() } - }, [appType, selectedVersion, loadAvailableFeatures, loadDefaultIncludedFeatures]) + }, [ + appType, + selectedVersion, + javaVersion, + servlet, + gorm, + reloading, + loadAvailableFeatures, + loadDefaultIncludedFeatures, + ]) } From 433bef3932d2239b4779689d2853789026642188 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 11 Jun 2026 07:51:12 -0400 Subject: [PATCH 2/5] Strengthen application launch smoke test Assert that the launch form and action row render instead of only checking for a defined test container. Assisted-by: opencode:gpt-5.5 --- .../components/Application/__tests__/Application.test.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/launch/src/components/Application/__tests__/Application.test.jsx b/app/launch/src/components/Application/__tests__/Application.test.jsx index c69c215..9b87208 100644 --- a/app/launch/src/components/Application/__tests__/Application.test.jsx +++ b/app/launch/src/components/Application/__tests__/Application.test.jsx @@ -1,7 +1,7 @@ import React from 'react' import { render } from '@testing-library/react' -import { useAppStore } from '../../../state/store' import ApplicationState from '../../../state/ApplicationState' +import { useAppStore } from '../../../state/store' import { App } from '../App' @@ -15,5 +15,7 @@ it(`Application Launches`, () => { ) - expect(container).toBeDefined() + + expect(container.querySelector('.mn-starter-form-main')).not.toBeNull() + expect(container.querySelector('.button-row')).not.toBeNull() }) From 144a62d307d3e3228a1a4bd92ec686f4bbb2825e Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 11 Jun 2026 07:52:14 -0400 Subject: [PATCH 3/5] Clean component lint issues Reorder MUI imports and memoize the feature modal keyboard handler factory result so lint can run cleanly. Assisted-by: opencode:gpt-5.5 --- app/launch/src/components/ErrorView/ErrorView.jsx | 3 +-- .../src/components/FeatureSelector/FeatureSelector.jsx | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/launch/src/components/ErrorView/ErrorView.jsx b/app/launch/src/components/ErrorView/ErrorView.jsx index 29c045b..e28e3e8 100644 --- a/app/launch/src/components/ErrorView/ErrorView.jsx +++ b/app/launch/src/components/ErrorView/ErrorView.jsx @@ -1,9 +1,8 @@ // ErrorView.js import React, { useState } from 'react' -import { Avatar, Snackbar, Alert } from '@mui/material' - import AssignmentIcon from '@mui/icons-material/Assignment' import AssignmentTurnedInIcon from '@mui/icons-material/AssignmentTurnedIn' +import { Alert, Avatar, Snackbar } from '@mui/material' import logo from '../../images/grails-white-icon.png' import { copyToClipboard } from '../../utility' diff --git a/app/launch/src/components/FeatureSelector/FeatureSelector.jsx b/app/launch/src/components/FeatureSelector/FeatureSelector.jsx index 3d0d4fc..3965477 100644 --- a/app/launch/src/components/FeatureSelector/FeatureSelector.jsx +++ b/app/launch/src/components/FeatureSelector/FeatureSelector.jsx @@ -1,5 +1,5 @@ // FeatureSelector.js -import React, { useCallback, useMemo, useRef, useState } from 'react' +import React, { useMemo, useRef, useState } from 'react' import { Button, @@ -118,8 +118,8 @@ export const FeatureSelectorModal = ({ theme = 'light' }) => { }, 300) } - const handleKeyDown = useCallback( - keyboardHandler.createKeyDownHandler(() => contentRef.current), + const handleKeyDown = useMemo( + () => keyboardHandler.createKeyDownHandler(() => contentRef.current), [] ) From 3622295fb5d906a85a9fcb0c38da1df844b071bf Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 11 Jun 2026 07:53:08 -0400 Subject: [PATCH 4/5] Fix launch tooling and local proxy Make local launch scripts cross-platform, wire the React hooks lint plugin, and update the Express 5 version feed route used by local browser QA. Assisted-by: opencode:gpt-5.5 --- app/launch/eslint.config.js | 10 ++- app/launch/package-lock.json | 105 +++++++++++++++++++++++++------ app/launch/package.json | 9 ++- dev-proxy-server/src/commands.js | 2 +- 4 files changed, 101 insertions(+), 25 deletions(-) diff --git a/app/launch/eslint.config.js b/app/launch/eslint.config.js index e582619..d2a7049 100644 --- a/app/launch/eslint.config.js +++ b/app/launch/eslint.config.js @@ -1,15 +1,17 @@ import js from '@eslint/js' import globals from 'globals' -import reactPlugin from 'eslint-plugin-react' import importPlugin from 'eslint-plugin-import' +import reactHooksPlugin from 'eslint-plugin-react-hooks' +import reactPlugin from 'eslint-plugin-react' export default [ js.configs.recommended, { files: ['**/*.{js,jsx}'], plugins: { - react: reactPlugin, import: importPlugin, + react: reactPlugin, + 'react-hooks': reactHooksPlugin, }, languageOptions: { ecmaVersion: 'latest', @@ -30,9 +32,11 @@ export default [ ...reactPlugin.configs.recommended.rules, // React 17+ automatic JSX transform does not require React in scope 'react/react-in-jsx-scope': 'off', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', // Project does not use PropTypes 'react/prop-types': 'off', - 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'no-unused-vars': ['error', { argsIgnorePattern: '^_', caughtErrors: 'none' }], 'no-case-declarations': 'warn', 'import/order': [ 'error', diff --git a/app/launch/package-lock.json b/app/launch/package-lock.json index fc62f11..5b24a7c 100644 --- a/app/launch/package-lock.json +++ b/app/launch/package-lock.json @@ -26,9 +26,11 @@ "@testing-library/react": "^16.3.2", "@vitejs/plugin-react": "^6.0.1", "baseline-browser-mapping": "^2.10.0", + "cross-env": "^10.1.0", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", "globals": "^17.6.0", "jsdom": "^29.1.1", "prettier": "^3.8.3", @@ -718,6 +720,13 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -2977,6 +2986,24 @@ } } }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3665,6 +3692,26 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -4220,6 +4267,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -7293,24 +7357,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -7324,6 +7370,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zustand": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", diff --git a/app/launch/package.json b/app/launch/package.json index f76086c..475b8d4 100755 --- a/app/launch/package.json +++ b/app/launch/package.json @@ -2,6 +2,7 @@ "name": "app", "version": "0.1.1", "private": true, + "type": "module", "homepage": ".", "dependencies": { "@emotion/react": "^11.14.0", @@ -22,8 +23,10 @@ "@testing-library/react": "^16.3.2", "@vitejs/plugin-react": "^6.0.1", "baseline-browser-mapping": "^2.10.0", + "cross-env": "^10.1.0", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^17.6.0", "jsdom": "^29.1.1", @@ -34,12 +37,12 @@ }, "scripts": { "start": "vite", - "start:local": "VITE_VERSION_FEED='http://localhost:8088' vite", + "start:local": "cross-env VITE_VERSION_FEED=http://localhost:8088/grails-version-feed.json vite", "build": "vite build", "preview": "vite preview", "format": "prettier --write \"**/*.+(js|jsx|json|css|md)\"", - "lint": "eslint './src/**/*.{js,jsx}'", - "lint:fix": "eslint './src/**/*.{js,jsx}' --fix", + "lint": "eslint \"./src/**/*.{js,jsx}\"", + "lint:fix": "eslint \"./src/**/*.{js,jsx}\" --fix", "test": "vitest run" }, "browserslist": { diff --git a/dev-proxy-server/src/commands.js b/dev-proxy-server/src/commands.js index 068bfc7..aea522a 100644 --- a/dev-proxy-server/src/commands.js +++ b/dev-proxy-server/src/commands.js @@ -10,7 +10,7 @@ function startVersionServer(feed, port = 8088) { const localhost = toLocalUrl(port); const app = express(); app.use(cors()); - app.get("/*splat", (req, res) => res.json(feed)); + app.get("/{*splat}", (req, res) => res.json(feed)); app.listen(port, () => { console.log(`Started version server on: ${localhost}`); console.table(feed.versions); From c3e90c9abb76c0b9253d7da31dbead792c5807d1 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 11 Jun 2026 08:16:17 -0400 Subject: [PATCH 5/5] Restore launch form visual parity Keep the launch form children hidden until the Zustand store has been initialized, restore production-style standard selects, and preserve the form row spacing used by start.grails.org. Assisted-by: opencode:gpt-5.5 --- app/launch/src/components/Select/Select.jsx | 13 ++++++++----- .../src/components/StarterForm/starter-form.css | 2 ++ app/launch/src/state/ApplicationState.jsx | 7 +++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/launch/src/components/Select/Select.jsx b/app/launch/src/components/Select/Select.jsx index 3e39948..12eef7b 100644 --- a/app/launch/src/components/Select/Select.jsx +++ b/app/launch/src/components/Select/Select.jsx @@ -14,17 +14,20 @@ const Select = ({ onChange, tabIndex = '0', }) => { + const controlId = id ?? name + return (
- - {label} + + {label} {options.map((opt, idx) => ( diff --git a/app/launch/src/components/StarterForm/starter-form.css b/app/launch/src/components/StarterForm/starter-form.css index 4835910..413f1fc 100644 --- a/app/launch/src/components/StarterForm/starter-form.css +++ b/app/launch/src/components/StarterForm/starter-form.css @@ -29,6 +29,8 @@ .mn-starter-form-main .mn-radio-row { margin-top: 20px; + padding-bottom: 4px; + padding-top: 4px; } .mn-starter-form-main .mn-radio .radio-row { diff --git a/app/launch/src/state/ApplicationState.jsx b/app/launch/src/state/ApplicationState.jsx index bd5772d..09e757a 100644 --- a/app/launch/src/state/ApplicationState.jsx +++ b/app/launch/src/state/ApplicationState.jsx @@ -1,4 +1,4 @@ -import { useLayoutEffect, useRef } from 'react' +import { useLayoutEffect, useRef, useState } from 'react' import { initializeStateFactory } from './factories/initializeState' import { useAppStore } from './store' @@ -8,6 +8,7 @@ export default function ApplicationState({ children, }) { const initialized = useRef(false) + const [ready, setReady] = useState(false) useLayoutEffect(() => { if (initialized.current) return @@ -20,7 +21,9 @@ export default function ApplicationState({ if (typeof initializer === 'function') { initializer(useAppStore) } + + setReady(true) }, [initialData, stateInitializer]) - return <>{children} + return ready ? <>{children} : null }