diff --git a/.erb/configs/webpack.config.base.mts b/.erb/configs/webpack.config.base.mts index 35d05b1..c40c8d2 100644 --- a/.erb/configs/webpack.config.base.mts +++ b/.erb/configs/webpack.config.base.mts @@ -15,6 +15,10 @@ const configuration: webpack.Configuration = { module: { rules: [ + { + test: /\.m?js$/, + type: 'javascript/auto', + }, { test: /\.[jt]sx?$/, exclude: /node_modules/, diff --git a/package-lock.json b/package-lock.json index ced765f..18592a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,14 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@ant-design/icons": "^5.6.1", + "@ant-design/icons": "^6.0.0", "@ant-design/v5-patch-for-react-19": "^1.0.3", "@monaco-editor/react": "^4.7.0", "@reduxjs/toolkit": "^2.8.2", - "@rjsf/antd": "^5.24.13", - "@rjsf/core": "^5.24.13", - "@rjsf/utils": "^5.24.13", - "@rjsf/validator-ajv8": "^5.24.13", + "@rjsf/antd": "^6.1.1", + "@rjsf/core": "^6.1.1", + "@rjsf/utils": "^6.1.1", + "@rjsf/validator-ajv8": "^6.1.1", "antd": "^5.25.2", "electron-debug": "^4.0.0", "fast-json-patch": "^3.1.1", @@ -28,6 +28,7 @@ "react-markdown": "^10.1.0", "react-redux": "^9.2.0", "react-router-dom": "^7.9.1", + "remeda": "^2.32.0", "zod": "^4.1.10" }, "devDependencies": { @@ -166,17 +167,15 @@ } }, "node_modules/@ant-design/icons": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", - "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.0.tgz", + "integrity": "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==", "license": "MIT", - "peer": true, "dependencies": { - "@ant-design/colors": "^7.0.0", + "@ant-design/colors": "^8.0.0", "@ant-design/icons-svg": "^4.4.0", - "@babel/runtime": "^7.24.8", - "classnames": "^2.2.6", - "rc-util": "^5.31.1" + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" }, "engines": { "node": ">=8" @@ -192,6 +191,24 @@ "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", "license": "MIT" }, + "node_modules/@ant-design/icons/node_modules/@ant-design/colors": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.0.tgz", + "integrity": "sha512-6YzkKCw30EI/E9kHOIXsQDHmMvTllT8STzjMb4K2qzit33RW2pqCJP0sk+hidBntXxE+Vz4n1+RvCTfBw6OErw==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/icons/node_modules/@ant-design/fast-color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.0.tgz", + "integrity": "sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA==", + "license": "MIT", + "engines": { + "node": ">=8.x" + } + }, "node_modules/@ant-design/react-slick": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", @@ -4937,6 +4954,20 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@rc-component/util": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.4.0.tgz", + "integrity": "sha512-LQlShcJKu0p3JUTAenKrWtqVW0+c4PJKedOqEaef9gTVL70O3cG4xZJ7VXfm0blGzORKFEkd3oQGalaUBNZ3Lg==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", @@ -4964,84 +4995,85 @@ } }, "node_modules/@rjsf/antd": { - "version": "5.24.13", - "resolved": "https://registry.npmjs.org/@rjsf/antd/-/antd-5.24.13.tgz", - "integrity": "sha512-UiWE8xoBxxCoe/SEkdQEmL5E6z3I1pw0+y0dTyGt8SHfAxxFc4/OWn7tKOAiNsKCXgf83t0JKn6CHWLD01sAdQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@rjsf/antd/-/antd-6.1.1.tgz", + "integrity": "sha512-dlu0iFmT101NnB8yS902N2JR1gpgkgLoF8OFvXvhoV+kFQ+aBZ2E23FgLTNszw53Aa1JFwvO0rU4VlYpOoWOkw==", "license": "Apache-2.0", "dependencies": { "classnames": "^2.5.1", "lodash": "^4.17.21", "lodash-es": "^4.17.21", - "rc-picker": "2.7.6" + "rc-picker": "^4.11.3" }, "engines": { - "node": ">=14" + "node": ">=20" }, "peerDependencies": { - "@ant-design/icons": "^4.0.0 || ^5.0.0", - "@rjsf/core": "^5.24.x", - "@rjsf/utils": "^5.24.x", - "antd": "^4.24.0 || ^5.8.5", + "@ant-design/icons": "^6.0.0", + "@rjsf/core": "^6.x", + "@rjsf/utils": "^6.x", + "antd": "^5.8.5", "dayjs": "^1.8.0", - "react": "^16.14.0 || >=17" + "react": ">=18" } }, "node_modules/@rjsf/core": { - "version": "5.24.13", - "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.24.13.tgz", - "integrity": "sha512-ONTr14s7LFIjx2VRFLuOpagL76sM/HPy6/OhdBfq6UukINmTIs6+aFN0GgcR0aXQHFDXQ7f/fel0o/SO05Htdg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.1.1.tgz", + "integrity": "sha512-HkaKQJwcLyb7zTRB6H1PFYbStxKrw+53/x/Ho2tNeIiZpw1hm8qvNm5EQEAARFjffdyF/RdDszHtmBOESwT7fQ==", "license": "Apache-2.0", "peer": true, "dependencies": { "lodash": "^4.17.21", "lodash-es": "^4.17.21", - "markdown-to-jsx": "^7.4.1", + "markdown-to-jsx": "^8.0.0", "prop-types": "^15.8.1" }, "engines": { - "node": ">=14" + "node": ">=20" }, "peerDependencies": { - "@rjsf/utils": "^5.24.x", - "react": "^16.14.0 || >=17" + "@rjsf/utils": "^6.x", + "react": ">=18" } }, "node_modules/@rjsf/utils": { - "version": "5.24.13", - "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.13.tgz", - "integrity": "sha512-rNF8tDxIwTtXzz5O/U23QU73nlhgQNYJ+Sv5BAwQOIyhIE2Z3S5tUiSVMwZHt0julkv/Ryfwi+qsD4FiE5rOuw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.1.1.tgz", + "integrity": "sha512-608UcT5rgsQLqc2ZVSktjeycdQ8lWmsXtNYshIMdk6VfQ7YTps8rVEvbzBqfaytLrv4xQXsHr+2cIDmYypgXIQ==", "license": "Apache-2.0", "peer": true, "dependencies": { + "fast-uri": "^3.1.0", "json-schema-merge-allof": "^0.8.1", "jsonpointer": "^5.0.1", "lodash": "^4.17.21", "lodash-es": "^4.17.21", - "react-is": "^18.2.0" + "react-is": "^18.3.1" }, "engines": { - "node": ">=14" + "node": ">=20" }, "peerDependencies": { - "react": "^16.14.0 || >=17" + "react": ">=18" } }, "node_modules/@rjsf/validator-ajv8": { - "version": "5.24.13", - "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.24.13.tgz", - "integrity": "sha512-oWHP7YK581M8I5cF1t+UXFavnv+bhcqjtL1a7MG/Kaffi0EwhgcYjODrD8SsnrhncsEYMqSECr4ZOEoirnEUWw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-6.1.1.tgz", + "integrity": "sha512-I4M8CJxINwNO+SSqg3QTh73r39Xl/sABl9N0k+M8sHpUrAKxeDe+oOG0HSoMrLcGBQHEGD0vwKg10Vzl/wraKA==", "license": "Apache-2.0", "dependencies": { - "ajv": "^8.12.0", + "ajv": "^8.17.1", "ajv-formats": "^2.1.1", "lodash": "^4.17.21", "lodash-es": "^4.17.21" }, "engines": { - "node": ">=14" + "node": ">=20" }, "peerDependencies": { - "@rjsf/utils": "^5.24.x" + "@rjsf/utils": "^6.x" } }, "node_modules/@rolldown/pluginutils": { @@ -7492,43 +7524,24 @@ "react-dom": ">=16.9.0" } }, - "node_modules/antd/node_modules/rc-picker": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", - "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", + "node_modules/antd/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.24.7", - "@rc-component/trigger": "^2.0.0", - "classnames": "^2.2.1", - "rc-overflow": "^1.3.2", - "rc-resize-observer": "^1.4.0", - "rc-util": "^5.43.0" + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" }, "engines": { - "node": ">=8.x" + "node": ">=8" }, "peerDependencies": { - "date-fns": ">= 2.x", - "dayjs": ">= 1.x", - "luxon": ">= 3.x", - "moment": ">= 2.x", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - }, - "peerDependenciesMeta": { - "date-fns": { - "optional": true - }, - "dayjs": { - "optional": true - }, - "luxon": { - "optional": true - }, - "moment": { - "optional": true - } + "react": ">=16.0.0", + "react-dom": ">=16.0.0" } }, "node_modules/anymatch": { @@ -9035,6 +9048,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -9847,22 +9869,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, "node_modules/dayjs": { "version": "1.11.18", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", @@ -10337,12 +10343,6 @@ "dev": true, "license": "MIT" }, - "node_modules/dom-align": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.4.tgz", - "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==", - "license": "MIT" - }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -13697,6 +13697,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "license": "MIT" + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -14764,15 +14770,20 @@ } }, "node_modules/markdown-to-jsx": { - "version": "7.7.13", - "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.13.tgz", - "integrity": "sha512-DiueEq2bttFcSxUs85GJcQVrOr0+VVsPfj9AEUPqmExJ3f8P/iQNvZHltV4tm1XVhu1kl0vWBZWT3l99izRMaA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-8.0.0.tgz", + "integrity": "sha512-hWEaRxeCDjes1CVUQqU+Ov0mCqBqkGhLKjL98KdbwHSgEWZZSJQeGlJQatVfeZ3RaxrfTrZZ3eczl2dhp5c/pA==", "license": "MIT", "engines": { "node": ">= 10" }, "peerDependencies": { "react": ">= 0.14.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } } }, "node_modules/marked": { @@ -15779,15 +15790,6 @@ "node": ">=10" } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/monaco-editor": { "version": "0.54.0", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz", @@ -17880,23 +17882,6 @@ "node": ">=0.10.0" } }, - "node_modules/rc-align": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-4.0.15.tgz", - "integrity": "sha512-wqJtVH60pka/nOX7/IspElA8gjPNQKIx/ZqJ6heATCkXpe1Zg4cPVrMD2vC96wjsFFL8WsmhPbx9tdMo1qqlIA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "2.x", - "dom-align": "^1.7.0", - "rc-util": "^5.26.0", - "resize-observer-polyfill": "^1.5.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, "node_modules/rc-cascader": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", @@ -18166,26 +18151,42 @@ } }, "node_modules/rc-picker": { - "version": "2.7.6", - "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-2.7.6.tgz", - "integrity": "sha512-H9if/BUJUZBOhPfWcPeT15JUI3/ntrG9muzERrXDkSoWmDj4yzmBvumozpxYrHwjcKnjyDGAke68d+whWwvhHA==", + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.10.1", + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.1", - "date-fns": "2.x", - "dayjs": "1.x", - "moment": "^2.24.0", - "rc-trigger": "^5.0.4", - "rc-util": "^5.37.0", - "shallowequal": "^1.1.0" + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" }, "engines": { "node": ">=8.x" }, "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } } }, "node_modules/rc-progress": { @@ -18439,26 +18440,6 @@ "react-dom": "*" } }, - "node_modules/rc-trigger": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-5.3.4.tgz", - "integrity": "sha512-mQv+vas0TwKcjAO2izNPkqR4j86OemLRmvL2nOzdP9OWNWA1ivoTt5hzFqYNW9zACwmTezRiN8bttrC7cZzYSw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "classnames": "^2.2.6", - "rc-align": "^4.0.0", - "rc-motion": "^2.0.0", - "rc-util": "^5.19.2" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, "node_modules/rc-upload": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.9.2.tgz", @@ -18892,6 +18873,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remeda": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.32.0.tgz", + "integrity": "sha512-BZx9DsT4FAgXDTOdgJIc5eY6ECIXMwtlSPQoPglF20ycSWigttDDe88AozEsPPT4OWk5NujroGSBC1phw5uU+w==", + "license": "MIT", + "dependencies": { + "type-fest": "^4.41.0" + } + }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -19846,12 +19836,6 @@ "node": ">=8" } }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", - "license": "MIT" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -21454,9 +21438,7 @@ "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=16" }, diff --git a/package.json b/package.json index ca980e0..e27ef61 100644 --- a/package.json +++ b/package.json @@ -37,14 +37,14 @@ ] }, "dependencies": { - "@ant-design/icons": "^5.6.1", + "@ant-design/icons": "^6.0.0", "@ant-design/v5-patch-for-react-19": "^1.0.3", "@monaco-editor/react": "^4.7.0", "@reduxjs/toolkit": "^2.8.2", - "@rjsf/antd": "^5.24.13", - "@rjsf/core": "^5.24.13", - "@rjsf/utils": "^5.24.13", - "@rjsf/validator-ajv8": "^5.24.13", + "@rjsf/antd": "^6.1.1", + "@rjsf/core": "^6.1.1", + "@rjsf/utils": "^6.1.1", + "@rjsf/validator-ajv8": "^6.1.1", "antd": "^5.25.2", "electron-debug": "^4.0.0", "fast-json-patch": "^3.1.1", @@ -56,6 +56,7 @@ "react-markdown": "^10.1.0", "react-redux": "^9.2.0", "react-router-dom": "^7.9.1", + "remeda": "^2.32.0", "zod": "^4.1.10" }, "devDependencies": { diff --git a/src/common/invokables/jsons.ts b/src/common/invokables/jsons.ts index 2d7637d..b038ab6 100644 --- a/src/common/invokables/jsons.ts +++ b/src/common/invokables/jsons.ts @@ -53,12 +53,14 @@ const JSON_SCHEMA_SCHEMA = z .object({ title: z.optional(z.string()), description: z.optional(z.string()), - items: z - .object({ - title: z.optional(z.string()), - description: z.optional(z.string()), - }) - .catchall(z.any()), + items: z.optional( + z + .object({ + title: z.optional(z.string()), + description: z.optional(z.string()), + }) + .catchall(z.any()), + ), }) .catchall(z.any()); diff --git a/src/renderer/EditorRoutes.tsx b/src/renderer/EditorRoutes.tsx index 1bafe53..95de70c 100644 --- a/src/renderer/EditorRoutes.tsx +++ b/src/renderer/EditorRoutes.tsx @@ -15,15 +15,28 @@ import { } from './components/form/StringReferenceWidget'; import { JsonForm } from './components/JsonForm'; import { JsonItemsForm } from './components/JsonItemsForm'; -import { JsonStrategicMapForm } from './components/StrategicMapForm'; +import { + JsonStrategicMapForm, + makeStrategicMapFormPropsForProperties, + makeStrategicMapFormPropsForProperty, +} from './components/StrategicMapForm'; import { Dashboard } from './components/Dashboard'; import { MercPreview } from './components/content/MercPreview'; import { ItemPreview } from './components/content/ItemPreview'; import { + makeResourceReference, resourceReferenceToGraphics, resourceReferenceToSound, } from './components/form/ResourceReferenceWidget'; import { StiPreview } from './components/content/StiPreview'; +import { ResourceType } from './lib/resourceType'; +import { makeMultiSectorSelectorWidget } from './components/form/MultiSectorSelectorWidget'; +import { mergeDeep } from 'remeda'; +import { UiSchema } from '@rjsf/utils'; +import { InventoryGraphicsField } from './components/form/InventoryGraphicsField'; +import { SamSitesAirControlForm } from './components/SamSitesAirControlForm'; +import { MovementCostsForm } from './components/MovementCostsForm'; +import { NormalizedSectorId } from './components/content/StrategicMap'; const baseItemProps = [ 'itemIndex', @@ -60,6 +73,16 @@ const baseItemFlags = [ 'bUnaerodynamic', 'bSinks', ]; +const baseItemUiSchema: UiSchema = { + inventoryGraphics: { + small: { + 'ui:field': InventoryGraphicsField, + }, + big: { + 'ui:field': InventoryGraphicsField, + }, + }, +}; export interface Route { id: string; @@ -134,15 +157,14 @@ export const MENU: Readonly>> = [ 'army-garrison-groups.json', 'Garrison Groups', JsonStrategicMapForm, - { + mergeDeep(makeStrategicMapFormPropsForProperty('sector'), { uiSchema: { 'ui:order': ['sector', 'composition'], - sector: { 'ui:disabled': true }, composition: { 'ui:widget': stringReferenceToArmyCompositions, }, }, - }, + }), ), makeFileItem( 'army-gun-choice-normal.json', @@ -152,6 +174,7 @@ export const MENU: Readonly>> = [ uiSchema: { items: { items: { + 'ui:label': false, 'ui:widget': stringReferenceToWeapons, }, }, @@ -166,6 +189,7 @@ export const MENU: Readonly>> = [ uiSchema: { items: { items: { + 'ui:label': false, 'ui:widget': stringReferenceToWeapons, }, }, @@ -246,6 +270,7 @@ export const MENU: Readonly>> = [ makeFileItem('smoke-effects.json', 'Smoke Effects', JsonItemsForm, { name: 'name', preview: (item: any) => , + canAddNewItem: false, uiSchema: { 'ui:order': [ 'name', @@ -261,6 +286,15 @@ export const MENU: Readonly>> = [ 'affectsMonsters', 'affectsRobot', ], + graphics: { + 'ui:widget': resourceReferenceToGraphics, + }, + staticGraphics: { + 'ui:widget': resourceReferenceToGraphics, + }, + dissipatingGraphics: { + 'ui:widget': resourceReferenceToGraphics, + }, }, }), ], @@ -299,6 +333,7 @@ export const MENU: Readonly>> = [ children: [ makeFileItem('ammo-types.json', 'Ammo Types', JsonItemsForm, { name: 'internalName', + canAddNewItem: false, uiSchema: { 'ui:order': ['index', 'internalName'] }, }), makeFileItem('armours.json', 'Armours', JsonItemsForm, { @@ -319,6 +354,7 @@ export const MENU: Readonly>> = [ ...baseItemFlags, ], + ...baseItemUiSchema, }, }), makeFileItem('calibres.json', 'Calibres', JsonItemsForm, { @@ -371,6 +407,7 @@ export const MENU: Readonly>> = [ 'isPressureTriggered', ...baseItemFlags, ], + ...baseItemUiSchema, calibre: { 'ui:widget': stringReferenceToExplosiveCalibres, }, @@ -392,6 +429,7 @@ export const MENU: Readonly>> = [ 'ubClassIndex', ...baseItemFlags, ], + ...baseItemUiSchema, }, }), makeFileItem('magazines.json', 'Magazines', JsonItemsForm, { @@ -409,6 +447,7 @@ export const MENU: Readonly>> = [ 'dontUseAsDefaultMagazine', ...baseItemFlags, ], + ...baseItemUiSchema, ammoType: { 'ui:widget': stringReferenceToAmmoTypes, }, @@ -425,7 +464,8 @@ export const MENU: Readonly>> = [ 'Tactical Map Item Replacements', JsonItemsForm, { - name: (item: any) => `${item.from} to ${item.to}`, + name: (item: any) => + `${item.from ?? 'unknown'} to ${item.to ?? 'unknown'}`, uiSchema: { from: { oneOf: [ @@ -480,6 +520,7 @@ export const MENU: Readonly>> = [ 'attachment_UnderGLauncher', ...baseItemFlags, ], + ...baseItemUiSchema, calibre: { 'ui:widget': stringReferenceToCalibres, }, @@ -498,21 +539,29 @@ export const MENU: Readonly>> = [ }, makeFileItem('loading-screens.json', 'Loading Screens', JsonItemsForm, { name: 'internalName', - uiSchema: { 'ui:order': ['internalName', 'filename'] }, + preview: (item) => , + uiSchema: { + 'ui:order': ['internalName', 'filename'], + filename: { + 'ui:widget': makeResourceReference({ + type: ResourceType.Graphics, + prefix: ['loadscreens'], + postProcess: (filename) => `/${filename}`, + }), + }, + }, }), makeFileItem( 'loading-screens-mapping.json', 'Loading Screens Mapping', JsonStrategicMapForm, - { + mergeDeep(makeStrategicMapFormPropsForProperties('sector', 'sectorLevel'), { uiSchema: { 'ui:order': ['sector', 'sectorLevel', 'day', 'night'], - sector: { 'ui:disabled': true }, - sectorLevel: { 'ui:disabled': true }, day: { 'ui:widget': stringReferenceToLoadingScreens }, night: { 'ui:widget': stringReferenceToLoadingScreens }, }, - }, + }), ), { type: 'Submenu', @@ -548,8 +597,35 @@ export const MENU: Readonly>> = [ }, }, ), - makeFileItem('mercs-relations.json', 'Relations', JsonItemsForm, { - name: 'internalName', + makeFileItem('mercs-relations.json', 'Opinions', JsonItemsForm, { + name: 'profile', + preview: (item: any) => , + uiSchema: { + 'ui:order': ['profile', 'relations'], + profile: { + 'ui:widget': stringReferenceToMercProfiles, + }, + relations: { + items: { + 'ui:order': [ + 'target', + 'opinion', + 'friend1', + 'friend2', + 'enemy1', + 'enemy2', + 'eventualFriend', + 'resistanceToBefriending', + 'eventualEnemy', + 'resistanceToMakingEnemy', + 'tolerance', + ], + target: { + 'ui:widget': stringReferenceToMercProfiles, + }, + }, + }, + }, }), makeFileItem('mercs-profile-info.json', 'Profiles', JsonItemsForm, { name: 'internalName', @@ -640,6 +716,7 @@ export const MENU: Readonly>> = [ k, { items: { + 'ui:label': false, 'ui:widget': resourceReferenceToSound, }, }, @@ -729,7 +806,7 @@ export const MENU: Readonly>> = [ 'Shipping Destinations', JsonItemsForm, { - name: 'locationId', + name: (item) => item.locationId.toString(), uiSchema: { 'ui:order': [ 'locationId', @@ -758,18 +835,17 @@ export const MENU: Readonly>> = [ 'strategic-bloodcat-placements.json', 'Bloodcat Placements', JsonStrategicMapForm, - { + mergeDeep(makeStrategicMapFormPropsForProperty('sector'), { uiSchema: { 'ui:order': ['sector', 'bloodCatPlacements'], - sector: { 'ui:disabled': true }, }, - }, + }), ), makeFileItem( 'strategic-bloodcat-spawns.json', 'Bloodcat Spawns', JsonStrategicMapForm, - { + mergeDeep(makeStrategicMapFormPropsForProperty('sector'), { uiSchema: { 'ui:order': [ 'sector', @@ -779,41 +855,58 @@ export const MENU: Readonly>> = [ 'isArena', 'isLair', ], - sector: { 'ui:disabled': true }, }, - }, + }), ), makeFileItem('strategic-fact-params.json', 'Fact Params', JsonItemsForm, { - name: 'fact', + name: (item) => item.fact.toString(), }), makeFileItem( 'strategic-map-cache-sectors.json', 'Weapon Cache Sectors', JsonForm, - {}, + { + uiSchema: { + 'ui:order': ['sectors', 'numTroops', 'numTroopsVariance'], + sectors: { + 'ui:widget': makeMultiSectorSelectorWidget({ + extractSectorFromItem: (value: string) => [value, 0], + transformSectorToItem: (value) => value[0], + }), + }, + }, + }, ), makeFileItem( 'strategic-map-creature-lairs.json', 'Creature Lairs', - JsonItemsForm, + JsonStrategicMapForm, { - name: (item: any) => item.entranceSector[0], + extractSectorFromItem: (item: any) => item.entranceSector ?? null, + transformSectorToItem: (sector: NormalizedSectorId) => ({ + entranceSector: sector, + }), + initialLevel: 1, + canChangeLevel: true, uiSchema: { 'ui:order': [ + 'entranceSector', 'lairId', 'associatedMineId', - 'entranceSector', 'warpExit', 'sectors', 'attackSectors', ], + entranceSector: { + 'ui:disabled': true, + }, }, }, ), makeFileItem( 'strategic-map-movement-costs.json', 'Movement Costs', - JsonForm, + MovementCostsForm, {}, ), makeFileItem( @@ -834,31 +927,36 @@ export const MENU: Readonly>> = [ profile: { 'ui:widget': stringReferenceToMercProfiles, }, + sectors: { + 'ui:widget': makeMultiSectorSelectorWidget({ + extractSectorFromItem: (sector: any) => [sector, 0], + transformSectorToItem: (sector: any) => sector[0], + }), + }, }, }, ), makeFileItem( 'strategic-map-sam-sites-air-control.json', 'Sam Sites Air Control', - JsonForm, + SamSitesAirControlForm, {}, ), makeFileItem( 'strategic-map-sam-sites.json', 'Sam Sites', JsonStrategicMapForm, - { + mergeDeep(makeStrategicMapFormPropsForProperty('sector'), { uiSchema: { 'ui:order': ['sector', 'gridNos'], - sector: { 'ui:disabled': true }, }, - }, + }), ), makeFileItem( 'strategic-map-secrets.json', 'Secrets', JsonStrategicMapForm, - { + mergeDeep(makeStrategicMapFormPropsForProperty('sector'), { uiSchema: { 'ui:order': [ 'sector', @@ -867,24 +965,27 @@ export const MENU: Readonly>> = [ 'secretMapIcon', 'isSAMSite', ], - sector: { 'ui:disabled': true }, + secretMapIcon: { + 'ui:widget': resourceReferenceToGraphics, + }, }, - }, + }), ), makeFileItem( 'strategic-map-sectors-descriptions.json', 'Sector Descriptions', JsonStrategicMapForm, - { - uiSchema: { - 'ui:order': ['sector', 'sectorLevel', 'landType'], - sector: { 'ui:disabled': true }, - sectorLevel: { 'ui:disabled': true }, + mergeDeep( + makeStrategicMapFormPropsForProperties('sector', 'sectorLevel'), + { + uiSchema: { + 'ui:order': ['sector', 'sectorLevel', 'landType'], + }, }, - }, + ), ), makeFileItem('strategic-map-towns.json', 'Towns', JsonItemsForm, { - name: 'townId', + name: 'internalName', uiSchema: { 'ui:order': [ 'townId', @@ -893,6 +994,12 @@ export const MENU: Readonly>> = [ 'townPoint', 'isMilitiaTrainingAllowed', ], + sectors: { + 'ui:widget': makeMultiSectorSelectorWidget({ + extractSectorFromItem: (sector: string) => [sector, 0], + transformSectorToItem: (sector) => sector[0], + }), + }, }, }), makeFileItem( @@ -905,43 +1012,56 @@ export const MENU: Readonly>> = [ 'strategic-map-underground-sectors.json', 'Underground Sectors', JsonStrategicMapForm, - { + mergeDeep( + makeStrategicMapFormPropsForProperties('sector', 'sectorLevel'), + { + initialLevel: 1, + uiSchema: { + 'ui:order': [ + 'sector', + 'sectorLevel', + 'adjacentSectors', + 'numTroops', + 'numTroopsVariance', + 'numElites', + 'numElitesVariance', + 'numCreatures', + 'numCreaturesVariance', + ], + }, + }, + ), + ), + makeFileItem( + 'strategic-mines.json', + 'Mines', + JsonStrategicMapForm, + mergeDeep(makeStrategicMapFormPropsForProperty('entranceSector'), { uiSchema: { 'ui:order': [ - 'sector', - 'sectorLevel', - 'adjacentSectors', - 'numTroops', - 'numTroopsVariance', - 'numElites', - 'numElitesVariance', - 'numCreatures', - 'numCreaturesVariance', + 'entranceSector', + 'associatedTownId', + 'associatedTown', + 'mineType', + 'minimumMineProduction', + 'noDepletion', + 'delayDepletion', + 'headMinerAssigned', + 'faceDisplayYOffset', + 'mineSectors', ], - sector: { 'ui:disabled': true }, - sectorLevel: { 'ui:disabled': true }, + associatedTown: { 'ui:widget': stringReferenceToTowns }, + mineSectors: { + 'ui:widget': makeMultiSectorSelectorWidget({ + initialLevel: 1, + canChangeLevel: true, + extractSectorFromItem: (sector: any) => sector, + transformSectorToItem: (sector) => sector, + }), + }, }, - }, + }), ), - makeFileItem('strategic-mines.json', 'Mines', JsonStrategicMapForm, { - property: 'entranceSector', - uiSchema: { - 'ui:order': [ - 'entranceSector', - 'associatedTownId', - 'associatedTown', - 'mineType', - 'minimumMineProduction', - 'noDepletion', - 'delayDepletion', - 'headMinerAssigned', - 'faceDisplayYOffset', - 'mineSectors', - ], - entranceSector: { 'ui:disabled': true }, - associatedTown: { 'ui:widget': stringReferenceToTowns }, - }, - }), ], }, makeFileItem( @@ -949,12 +1069,11 @@ export const MENU: Readonly>> = [ 'Tactical Npc Action Params', JsonItemsForm, { - name: 'actionCode', + name: (item) => item.actionCode.toString(), }, ), makeFileItem('vehicles.json', 'Vehicles', JsonItemsForm, { name: 'profile', - preview: (item: any) => , uiSchema: { 'ui:order': [ 'profile', @@ -966,6 +1085,8 @@ export const MENU: Readonly>> = [ ], profile: { 'ui:widget': stringReferenceToMercProfiles }, armourType: { 'ui:widget': stringReferenceToArmours }, + enterSound: { 'ui:widget': resourceReferenceToSound }, + moveSound: { 'ui:widget': resourceReferenceToSound }, }, }), ]; diff --git a/src/renderer/components/EditorLayout.tsx b/src/renderer/components/EditorLayout.tsx index 2da9bd9..ce60519 100644 --- a/src/renderer/components/EditorLayout.tsx +++ b/src/renderer/components/EditorLayout.tsx @@ -78,7 +78,9 @@ function SideMenu() { sorted.sort((a, b) => a.label.localeCompare(b.label, 'en', { ignorePunctuation: true }), ); - return [dashboard, ...sorted].map((r) => routeToItem(navigate, '', r)); + return [...(dashboard ? [dashboard] : []), ...sorted].map((r) => + routeToItem(navigate, '', r), + ); }, [navigate]); return ( diff --git a/src/renderer/components/JsonForm.tsx b/src/renderer/components/JsonForm.tsx index cd5c5ce..4d5cdfb 100644 --- a/src/renderer/components/JsonForm.tsx +++ b/src/renderer/components/JsonForm.tsx @@ -66,7 +66,7 @@ export function JsonForm({ file, uiSchema }: JsonFormProps) { return ( - {contents} + {contents} ); } diff --git a/src/renderer/components/JsonItemsForm.tsx b/src/renderer/components/JsonItemsForm.tsx index 4209fae..4ae733a 100644 --- a/src/renderer/components/JsonItemsForm.tsx +++ b/src/renderer/components/JsonItemsForm.tsx @@ -1,6 +1,5 @@ import { useCallback, useMemo, JSX, memo } from 'react'; -import { Collapse, Space } from 'antd'; - +import { Collapse, Flex } from 'antd'; import { JsonSchemaForm } from './JsonSchemaForm'; import { FullSizeLoader } from './FullSizeLoader'; import './JsonItemsForm.css'; @@ -17,6 +16,10 @@ import { } from '../hooks/files'; import { ErrorAlert } from './ErrorAlert'; import { TextEditorOr } from './TextEditor'; +import { useAppDispatch } from '../hooks/state'; +import { addJsonItem } from '../state/files'; +import { AddNewButton } from './form/AddNewButton'; +import { RemoveButton } from './form/RemoveButton'; type PreviewFn = (item: any) => JSX.Element | string | null; @@ -49,10 +52,13 @@ const ItemFormHeader = memo(function ItemFormHeader({ const p = useMemo(() => (preview ? preview(value) : null), [preview, value]); return ( - - {p} - {label} - + + + {p} + {label} + + + ); }); @@ -135,9 +141,9 @@ const FormItems = memo(function FormItems({ }, [file, name, numItems, preview, uiSchema]); return ( - + {items} - + ); }); @@ -146,6 +152,8 @@ export interface JsonItemsFormProps { name: NameOrPreviewFn; preview?: PreviewFn; uiSchema?: UiSchema; + canAddNewItem?: boolean; + getNewItem?: () => object; } export const JsonItemsForm = memo(function JsonItemsForm({ @@ -153,10 +161,23 @@ export const JsonItemsForm = memo(function JsonItemsForm({ name, preview, uiSchema, + canAddNewItem, + getNewItem, }: JsonItemsFormProps) { + const dispatch = useAppDispatch(); const loading = useFileLoading(file); const error = useFileLoadingError(file); const numItems = useFileJsonNumberOfItems(file); + const addNewItem = useCallback(() => { + dispatch( + addJsonItem({ filename: file, value: getNewItem ? getNewItem() : {} }), + ); + }, [dispatch, file, getNewItem]); + const addButton = useMemo(() => { + const render = typeof canAddNewItem === 'undefined' ? true : canAddNewItem; + if (!render) return null; + return ; + }, [addNewItem, canAddNewItem]); const content = useMemo(() => { if (numItems == null) { return ; @@ -164,16 +185,19 @@ export const JsonItemsForm = memo(function JsonItemsForm({ return ( <> - + + + {addButton} + ); - }, [file, name, numItems, preview, uiSchema]); + }, [addButton, file, name, numItems, preview, uiSchema]); if (error) { return ; diff --git a/src/renderer/components/JsonSchemaForm.tsx b/src/renderer/components/JsonSchemaForm.tsx index 54d0e0d..c571012 100644 --- a/src/renderer/components/JsonSchemaForm.tsx +++ b/src/renderer/components/JsonSchemaForm.tsx @@ -1,117 +1,41 @@ -import { IChangeEvent, withTheme } from '@rjsf/core'; -import { UiSchema, FieldTemplateProps, FieldProps } from '@rjsf/utils'; +import { FormProps, IChangeEvent, withTheme } from '@rjsf/core'; +import { UiSchema } from '@rjsf/utils'; import { Theme as AntdTheme } from '@rjsf/antd'; -import { Form } from 'antd'; -import ReactMarkdown from 'react-markdown'; -import { memo, useMemo } from 'react'; import validator from '@rjsf/validator-ajv8'; +import { useMemo } from 'react'; -export interface DescriptionFieldProps extends Partial { - description?: string; -} - -const MarkdownDescriptionField = memo(function MarkdownDescriptionField({ - id, - description, -}: DescriptionFieldProps) { - if (!description) { - return null; - } - return ( -
- {description} -
- ); -}); - -const HORIZONTAL_LABEL_COL = { span: 6 }; -const HORIZONTAL_WRAPPER_COL = { span: 18 }; - -// Cloned from Antd theme with some changes -const MarkdownFieldTemplate = memo(function MarkdownFieldTemplate({ - children, - // classNames, - // description, - // disabled, - displayLabel, - // errors, - // fields, - formContext, - // help, - hidden, - id, - label, - // onDropPropertyClick, - // onKeyChange, - rawDescription, - rawErrors, - // rawHelp, - // readonly, - required, - schema, -}: // uiSchema, -FieldTemplateProps) { - const { colon, wrapperStyle } = formContext; - const fieldErrors = useMemo(() => { - if (!rawErrors) { - return null; - } - return [...Array.from(new Set(rawErrors))].map((error: any) => ( -
{error}
- )); - }, [id, rawErrors]); - const renderedDescription = useMemo(() => { - if (!rawDescription) { - return null; - } - return {rawDescription}; - }, [rawDescription]); +const RjsfForm = withTheme(AntdTheme); - if (hidden) { - return
{children}
; - } - - return id === 'root' ? ( - children - ) : ( - - {children} - - ); -}); - -const RjsfForm = withTheme({ - ...AntdTheme, - widgets: { - ...AntdTheme.widgets, - // CheckboxWidget: CheckboxWidgetWithDescription, - }, - fields: { - ...AntdTheme.fields, - DescriptionField: MarkdownDescriptionField, +const DEFAULT_UI_SCHEMA: UiSchema = { + 'ui:globalOptions': { + enableMarkdownInDescription: true, + enableMarkdownInHelp: true, }, - templates: { - ...AntdTheme.templates, - FieldTemplate: MarkdownFieldTemplate, +}; + +const DEFAULT_PROPS: Pick< + FormProps, + | 'experimental_defaultFormStateBehavior' + | 'showErrorList' + | 'validator' + | 'liveValidate' + | 'liveOmit' + | 'noHtml5Validate' +> = { + showErrorList: false, + noHtml5Validate: true, + validator: validator, + liveValidate: 'onChange', + liveOmit: 'onChange', + experimental_defaultFormStateBehavior: { + arrayMinItems: { + populate: 'never', + mergeExtraDefaults: false, + }, + emptyObjectFields: 'skipDefaults', + allOf: 'skipDefaults', }, -}); +}; export interface JsonSchemaFormProps { idPrefix?: string; @@ -132,6 +56,14 @@ export function JsonSchemaForm({ onChange, onSubmit, }: JsonSchemaFormProps) { + const appliedUiSchema: UiSchema = useMemo( + () => ({ + ...DEFAULT_UI_SCHEMA, + ...uiSchema, + }), + [uiSchema], + ); + return ( ); } diff --git a/src/renderer/components/MovementCostsForm.tsx b/src/renderer/components/MovementCostsForm.tsx new file mode 100644 index 0000000..560b9fd --- /dev/null +++ b/src/renderer/components/MovementCostsForm.tsx @@ -0,0 +1,241 @@ +import { useCallback, useMemo, useState } from 'react'; +import { + useFileJson, + useFileLoading, + useFileLoadingError, +} from '../hooks/files'; +import { + coordsFromSectorIdString, + NormalizedSectorId, + StrategicMap, +} from './content/StrategicMap'; +import { EditorContent } from './EditorContent'; +import { ErrorAlert } from './ErrorAlert'; +import { FullSizeLoader } from './FullSizeLoader'; +import { TextEditorOr } from './TextEditor'; +import { Flex, Typography } from 'antd'; +import { JsonFormHeader } from './form/JsonFormHeader'; +import { JsonSchema } from 'src/common/invokables/jsons'; +import { JsonSchemaForm } from './JsonSchemaForm'; +import { UiSchema } from '@rjsf/utils'; +import { IChangeEvent } from '@rjsf/core'; +import { clone } from 'remeda'; + +const TRAVERSABILITY_ENUM_JSON_SCHEMA: JsonSchema = { + type: 'string', + enum: [ + 'A', + 'AR', + 'C', + 'CR', + 'D', + 'DR', + 'E', + 'F', + 'FR', + 'G', + 'H', + 'HR', + 'N', + 'P', + 'PR', + 'R', + 'S', + 'SR', + 'T', + 'TR', + 'W', + 'WR', + 'WT', + 'X', + ], +}; + +const TRAVERSABILITY_ENUM_TITLES = [ + 'SAND', + 'SAND_ROAD', + 'COASTAL', + 'COASTAL_ROAD', + 'DENSE', + 'DENSE_ROAD', + 'EDGEOFWORLD', + 'FARMLAND', + 'FARMLAND_ROAD', + 'GROUNDBARRIER', + 'HILLS', + 'HILLS_ROAD', + 'NS_RIVER', + 'PLAINS', + 'PLAINS_ROAD', + 'ROAD', + 'SPARSE', + 'SPARSE_ROAD', + 'TROPICS', + 'TROPICS_ROAD', + 'SWAMP', + 'SWAMP_ROAD', + 'WATER', + 'TOWN', +]; + +const MOVEMENT_COSTS_JSON_SCHEMA = { + type: 'object', + properties: { + traverseWest: { + title: 'Traverse West', + description: 'The cost to traverse to the sector in the west.', + ...TRAVERSABILITY_ENUM_JSON_SCHEMA, + }, + traverseEast: { + title: 'Traverse East', + description: 'The cost to traverse to the sector in the east.', + ...TRAVERSABILITY_ENUM_JSON_SCHEMA, + }, + traverseSouth: { + title: 'Traverse South', + description: 'The cost to traverse to the sector in the south.', + ...TRAVERSABILITY_ENUM_JSON_SCHEMA, + }, + traverseNorth: { + title: 'Traverse North', + description: 'The cost to traverse to the sector in the north.', + ...TRAVERSABILITY_ENUM_JSON_SCHEMA, + }, + traverseThrough: { + title: 'Traverse Through', + description: 'The cost to traverse through this sector.', + ...TRAVERSABILITY_ENUM_JSON_SCHEMA, + }, + travelRating: { + type: 'number', + minimum: 0, + maximum: 255, + }, + }, + required: [ + 'traverseWest', + 'traverseEast', + 'traverseSouth', + 'traverseNorth', + 'traverseThrough', + 'travelRating', + ], +}; + +const MOVEMENT_COSTS_UI_SCHEMA: UiSchema = { + 'ui:order': [ + 'traverseThrough', + 'traverseNorth', + 'traverseSouth', + 'traverseWest', + 'traverseEast', + 'travelRating', + ], + traverseWest: { 'ui:enumNames': TRAVERSABILITY_ENUM_TITLES }, + traverseEast: { 'ui:enumNames': TRAVERSABILITY_ENUM_TITLES }, + traverseSouth: { 'ui:enumNames': TRAVERSABILITY_ENUM_TITLES }, + traverseNorth: { 'ui:enumNames': TRAVERSABILITY_ENUM_TITLES }, + traverseThrough: { 'ui:enumNames': TRAVERSABILITY_ENUM_TITLES }, +}; + +interface MovementCostsFormProps { + file: string; +} + +function MovementCosts({ + file, + value, + onChange, +}: MovementCostsFormProps & { + value: any; + onChange: (value: any) => unknown; +}) { + const [selectedSector, setSelectedSector] = + useState(null); + const coords = useMemo( + () => (selectedSector ? coordsFromSectorIdString(selectedSector[0]) : null), + [selectedSector], + ); + const content = useMemo(() => { + if (!coords) { + return {}; + } + return { + traverseThrough: value.traverseThrough[coords[1]][coords[0]], + traverseNorth: value.traverseNS[coords[1]][coords[0]], + traverseSouth: value.traverseNS[coords[1] + 1][coords[0]], + traverseWest: value.traverseWE[coords[1]][coords[0]], + traverseEast: value.traverseWE[coords[1]][coords[0] + 1], + travelRating: value.travelRatings[coords[1]][coords[0]], + }; + }, [value, coords]); + const handleChange = useCallback( + (newContent: IChangeEvent) => { + if (!coords) { + return; + } + const n = clone(value); + n.traverseThrough[coords[1]][coords[0]] = + newContent.formData.traverseThrough; + n.traverseNS[coords[1]][coords[0]] = newContent.formData.traverseNorth; + n.traverseNS[coords[1] + 1][coords[0]] = + newContent.formData.traverseSouth; + n.traverseWE[coords[1]][coords[0]] = newContent.formData.traverseWest; + n.traverseWE[coords[1]][coords[0] + 1] = newContent.formData.traverseEast; + n.travelRatings[coords[1]][coords[0]] = newContent.formData.travelRating; + onChange(n); + }, + [coords, onChange, value], + ); + const contentElement = useMemo(() => { + if (!coords) { + return ( + + Select sector to view and edit movement costs. + + ); + } + return ( + + ); + }, [content, coords, handleChange]); + + return ( + + +
+ + {contentElement} +
+
+ ); +} + +export function MovementCostsForm({ file }: MovementCostsFormProps) { + const loading = useFileLoading(file); + const error = useFileLoadingError(file); + const [value, update] = useFileJson(file); + + if (loading == null || loading) { + return ; + } + if (error) { + return ; + } + + return ( + + + + + + ); +} diff --git a/src/renderer/components/SamSitesAirControlForm.tsx b/src/renderer/components/SamSitesAirControlForm.tsx new file mode 100644 index 0000000..1cb2d90 --- /dev/null +++ b/src/renderer/components/SamSitesAirControlForm.tsx @@ -0,0 +1,144 @@ +import { clone } from 'remeda'; +import { + useFileJson, + useFileLoading, + useFileLoadingError, +} from '../hooks/files'; +import { EditorContent } from './EditorContent'; +import { ErrorAlert } from './ErrorAlert'; +import { FullSizeLoader } from './FullSizeLoader'; +import { TextEditorOr } from './TextEditor'; +import { useMemo, useState } from 'react'; +import { + coordsFromSectorIdString, + HIGHLIGHT_COLORS, + NormalizedSectorId, + sectorIdStringFromCoords, + StrategicMap, +} from './content/StrategicMap'; +import { Badge, Flex, Select, Space, Typography } from 'antd'; +import { JsonFormHeader } from './form/JsonFormHeader'; + +interface SamSitesAirControlProps { + file: string; +} + +function SamSitesAirControl({ + file, + samSites, + value, + onChange, +}: { + samSites: any[]; + value: any[][]; + onChange: (value: any[][]) => unknown; +} & SamSitesAirControlProps) { + const [selectedSite, setSelectedSite] = useState(0); + const selectedSector = useMemo(() => { + return samSites[selectedSite]?.sector + ? ([samSites[selectedSite]?.sector as string, 0] as NormalizedSectorId) + : undefined; + }, [samSites, selectedSite]); + const samSiteOptions = useMemo( + () => + samSites.map((site, index) => { + const color = HIGHLIGHT_COLORS[index % HIGHLIGHT_COLORS.length]; + return { + label: ( + + + {site.sector} + + ), + value: index, + color, + }; + }), + [samSites], + ); + const highlightedSectorIds = useMemo(() => { + const result: { [color: string]: NormalizedSectorId[] } = {}; + + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const idx = value[y]?.[x] ?? 0; + const color = idx !== 0 ? samSiteOptions[idx - 1]?.color : undefined; + + if (idx !== -1 && color) { + result[color] = result[color] || []; + result[color].push([sectorIdStringFromCoords(x, y), 0]); + } + } + } + + return result; + }, [samSiteOptions, value]); + const handleSectorClick = (sectorId: NormalizedSectorId) => { + const coordinates = coordsFromSectorIdString(sectorId[0]); + if (!coordinates) return; + const newValue = clone(value); + const [x, y] = coordinates; + if (newValue[y] === undefined || newValue[y][x] === undefined) { + return; + } + newValue[y][x] = newValue[y][x] === selectedSite + 1 ? 0 : selectedSite + 1; + onChange(newValue); + }; + + return ( + + +
+ + + Select a SAM site below and click on the map to change sectors. + + + ); +} + +export function InventoryGraphicsField({ + name, + fieldPathId, + formData, + onChange, + schema, + registry, + onBlur, + onFocus, +}: FieldProps) { + const path: string = formData?.path ?? ''; + const subImageIndex = formData?.subImageIndex ?? 0; + const handleResourceChange = useCallback( + (resource: string) => { + onChange({ path: resource, subImageIndex: 0 }, fieldPathId.path); + }, + [fieldPathId, onChange], + ); + const handleSubitemChange = useCallback( + (index: number) => { + const subImageIndex = index !== 0 ? index : undefined; + onChange({ path, subImageIndex }, fieldPathId.path); + }, + [fieldPathId.path, onChange, path], + ); + const selector = useMemo(() => { + return ( + + ); + }, [handleSubitemChange, path, subImageIndex]); + + return ( + path} + id={fieldPathId.$id} + name={name} + label="Graphics" + onBlur={onBlur} + onFocus={onFocus} + options={{}} + schema={schema} + registry={registry} + /> + ); +} diff --git a/src/renderer/components/form/MultiSectorSelectorWidget.tsx b/src/renderer/components/form/MultiSectorSelectorWidget.tsx new file mode 100644 index 0000000..23aa7f5 --- /dev/null +++ b/src/renderer/components/form/MultiSectorSelectorWidget.tsx @@ -0,0 +1,70 @@ +import { WidgetProps } from '@rjsf/utils'; +import { + DEFAULT_HIGHLIGHT_COLOR, + NormalizedSectorId, + StrategicMap, +} from '../content/StrategicMap'; +import { useCallback, useMemo, useState } from 'react'; +import { Flex } from 'antd'; +import { find, isDeepEqual } from 'remeda'; + +interface ExtraProps { + initialLevel?: number; + canChangeLevel?: boolean; + extractSectorFromItem?: (value: T) => NormalizedSectorId; + transformSectorToItem?: (sectorId: NormalizedSectorId) => T; +} + +type MultiSectorSelectorWidgetProps = WidgetProps & ExtraProps; + +function MultiSectorSelectorWidget({ + initialLevel = 0, + canChangeLevel = false, + extractSectorFromItem, + transformSectorToItem, + value, + onChange, +}: MultiSectorSelectorWidgetProps) { + const [level, setLevel] = useState(initialLevel); + const highlightedSectorIds = useMemo( + () => ({ + [DEFAULT_HIGHLIGHT_COLOR]: value.map( + extractSectorFromItem ?? ((s: T) => s), + ), + }), + [value, extractSectorFromItem], + ); + const handleSectorClick = useCallback( + (sectorId: NormalizedSectorId) => { + const transformed = transformSectorToItem + ? transformSectorToItem(sectorId) + : sectorId; + const newValue = find(value, (s) => isDeepEqual(s, transformed)) + ? value.filter((s: T) => !isDeepEqual(s, transformed)) + : [...value, transformed]; + onChange(newValue); + }, + [transformSectorToItem, value, onChange], + ); + + return ( + + + + ); +} + +export const makeMultiSectorSelectorWidget = ( + extraProps: ExtraProps, +) => { + return function MultiSectorSelectorWidgetWrapper( + props: MultiSectorSelectorWidgetProps, + ) { + return ; + }; +}; diff --git a/src/renderer/components/form/RemoveButton.tsx b/src/renderer/components/form/RemoveButton.tsx new file mode 100644 index 0000000..f5f1dc2 --- /dev/null +++ b/src/renderer/components/form/RemoveButton.tsx @@ -0,0 +1,53 @@ +import { DeleteOutlined } from '@ant-design/icons'; +import { Button, Modal } from 'antd'; +import { MouseEventHandler, useCallback, useState } from 'react'; +import { useAppDispatch } from '../../hooks/state'; +import { removeJsonItem } from '../../state/files'; + +interface RemoveButtonProps { + file: string; + index: number; + label?: string; +} + +export function RemoveButton({ file, index, label }: RemoveButtonProps) { + const dispatch = useAppDispatch(); + const [showModal, setShowModal] = useState(false); + const handleClick: MouseEventHandler = useCallback((ev) => { + setShowModal(true); + ev.stopPropagation(); + }, []); + const handleConfirm: MouseEventHandler = useCallback( + (ev) => { + dispatch( + removeJsonItem({ + filename: file, + index, + }), + ); + setShowModal(false); + ev.stopPropagation(); + }, + [dispatch, file, index], + ); + const handleCancel: MouseEventHandler = useCallback((ev) => { + setShowModal(false); + ev.stopPropagation(); + }, []); + + return ( + <> + + + Removing the item cannot be undone. Are you sure? + + + ); +} diff --git a/src/renderer/components/form/ResourceReferenceWidget.tsx b/src/renderer/components/form/ResourceReferenceWidget.tsx index 9f7805f..eb0ad35 100644 --- a/src/renderer/components/form/ResourceReferenceWidget.tsx +++ b/src/renderer/components/form/ResourceReferenceWidget.tsx @@ -8,40 +8,64 @@ import { ResourceType } from '../../lib/resourceType'; interface ResourceReferenceWidgetProps extends WidgetProps { resourceType: ResourceType; + pathPrefix: string[]; + postProcess: (path: string) => string; + preview?: React.ReactNode; } export function ResourceReferenceWidget({ resourceType, + pathPrefix, + postProcess, value, onChange, + preview, }: ResourceReferenceWidgetProps) { const [modalIsOpen, setModalOpen] = useState(false); const openModal = useCallback(() => setModalOpen(true), []); const closeModal = useCallback(() => setModalOpen(false), []); const onSelect = useCallback( (path: string) => { - onChange(path); + onChange(postProcess(path)); closeModal(); }, - [closeModal, onChange], + [closeModal, onChange, postProcess], ); - const preview = useMemo(() => { + const onInputChange = useCallback( + (e: React.ChangeEvent) => { + onChange(e.target.value); + }, + [onChange], + ); + const previewElement = useMemo(() => { + if (preview) { + return preview; + } + const trimmed = (value ?? '').startsWith('/') ? value.substring(1) : value; if (resourceType === ResourceType.Sound) { - return ; + return ( + 0 ? '/' : ''}${trimmed}`} + /> + ); } if (resourceType === ResourceType.Graphics) { - return ; + return ( + 0 ? '/' : ''}${trimmed}`} + /> + ); } return null; - }, [resourceType, value]); + }, [preview, pathPrefix, resourceType, value]); return ( - {preview} + {previewElement} @@ -50,19 +74,37 @@ export function ResourceReferenceWidget({ onSelect={onSelect} onCancel={closeModal} resourceType={resourceType} + pathPrefix={pathPrefix} /> ); } -function resourceReference(type: ResourceType = ResourceType.Any) { +export function makeResourceReference({ + type = ResourceType.Any, + prefix = [], + postProcess = (path: string) => path, +}: { + type: ResourceType; + prefix?: string[]; + postProcess?: (path: string) => string; +}) { return function ResourceReference(props: WidgetProps) { - return ; + return ( + + ); }; } -export const resourceReferenceToSound = resourceReference(ResourceType.Sound); +export const resourceReferenceToSound = makeResourceReference({ + type: ResourceType.Sound, +}); -export const resourceReferenceToGraphics = resourceReference( - ResourceType.Graphics, -); +export const resourceReferenceToGraphics = makeResourceReference({ + type: ResourceType.Graphics, +}); diff --git a/src/renderer/components/form/StringReferenceWidget.tsx b/src/renderer/components/form/StringReferenceWidget.tsx index d9ee653..4e4b3a5 100644 --- a/src/renderer/components/form/StringReferenceWidget.tsx +++ b/src/renderer/components/form/StringReferenceWidget.tsx @@ -53,7 +53,7 @@ export function StringReferenceWidget({ const fileResults = values[key] as Array; if (!fileResults) continue; for (const item of fileResults) { - const value: string = item[references[key].property]; + const value: string = item[references[key].property] ?? ''; let label: JSX.Element | string | null = value; if (references[key].preview) { label = ( diff --git a/src/renderer/hooks/files.tsx b/src/renderer/hooks/files.tsx index b4e8508..2f39c3d 100644 --- a/src/renderer/hooks/files.tsx +++ b/src/renderer/hooks/files.tsx @@ -12,7 +12,7 @@ import { AppState } from '../state/store'; import { memoize } from 'proxy-memoize'; import { JsonRoot, JsonSchema } from '../../common/invokables/jsons'; -type UseFilesRequest = { [key: PropertyKey]: string }; +type UseFilesRequest = { [key: string]: string }; type UseFilesResult = { [key in keyof R]: V; @@ -90,7 +90,7 @@ export function useFilesJson( function selectFilesJson(s) { const values: { [key in keyof R]: JsonRoot | null } = {} as any; for (const key in files) { - const open = s.files.open[files[key]]; + const open = s.files.open[files[key]!]; if (!open || open.editMode === 'text') { values[key] = null; } else { @@ -105,7 +105,7 @@ export function useFilesJson( (file: keyof R, value: JsonRoot) => { dispatch( changeJson({ - filename: files[file], + filename: files[file]!, value, }), ); @@ -116,7 +116,7 @@ export function useFilesJson( useEffect(() => { for (const key in files) { if (loading[key] === null) { - dispatch(loadJSON(files[key])); + dispatch(loadJSON(files[key]!)); } } }, [dispatch, files, loading]); @@ -239,12 +239,10 @@ export function useFileText( ]; } -export function useFileJsonItemSchema( - filename: string, -): Record | null { - const schema = useFileSchema(filename); - if (!schema) return null; - return schema.items ?? null; +export function useFileJsonItemSchema(filename: string): JsonSchema | null { + return useAppSelector(function selectFileSchema(s) { + return s.files.disk[filename]?.data?.itemSchema ?? null; + }); } export function useFileJsonNumberOfItems(filename: string): number | null { diff --git a/src/renderer/lib/resourceType.tsx b/src/renderer/lib/resourceType.tsx index bbe02b4..fe17e64 100644 --- a/src/renderer/lib/resourceType.tsx +++ b/src/renderer/lib/resourceType.tsx @@ -14,7 +14,10 @@ export function resourceTypeFromFilename(filename: string): ResourceType { if (split.length < 2) { return ResourceType.Any; } - const extension = split[split.length - 1].toLowerCase(); + const extension = split[split.length - 1]?.toLowerCase(); + if (!extension) { + return ResourceType.Any; + } return mapping[extension] ?? ResourceType.Any; } diff --git a/src/renderer/state/files.tsx b/src/renderer/state/files.tsx index 6a324be..5dba956 100644 --- a/src/renderer/state/files.tsx +++ b/src/renderer/state/files.tsx @@ -22,6 +22,7 @@ import { JsonSchema, } from '../../common/invokables/jsons'; import { InvokableOutput } from 'src/common/invokables'; +import { isArray, omit, splice } from 'remeda'; export type SaveMode = 'patch' | 'replace'; @@ -30,6 +31,7 @@ export type EditMode = 'visual' | 'text'; interface JsonFile { saveMode: SaveMode; schema: JsonSchema; + itemSchema: JsonSchema | null; vanilla: JsonRoot; mod: JsonRoot | null; patch: JsonPatch | null; @@ -192,10 +194,39 @@ const filesSlice = createSlice({ ) => { const { filename, index, value } = action.payload; const open = state.open[filename]; - if (!open || open.editMode !== 'visual' || !Array.isArray(open.value)) { + if (!open || open.editMode !== 'visual' || !isArray(open.value)) { return; } - (open.value as Array)[index] = value; + open.value[index] = value; + open.modified = isModified(state, filename); + }, + addJsonItem: ( + state, + action: PayloadAction<{ filename: string; value: any }>, + ) => { + const { filename, value } = action.payload; + const open = state.open[filename]; + if (!open || open.editMode !== 'visual' || !isArray(open.value)) { + return; + } + open.value = [...open.value, value]; + open.modified = isModified(state, filename); + }, + removeJsonItem: ( + state, + action: PayloadAction<{ filename: string; index: number }>, + ) => { + const { filename, index } = action.payload; + const open = state.open[filename]; + if ( + !open || + open.editMode !== 'visual' || + !isArray(open.value) || + typeof open.value[index] === 'undefined' + ) { + return; + } + open.value = splice(open.value, index, 1, []); open.modified = isModified(state, filename); }, changeSaveMode( @@ -286,12 +317,15 @@ const filesSlice = createSlice({ }, }, extraReducers: (builder) => { - const transform = (data: InvokableOutput) => { + const transform = (data: InvokableOutput): JsonFile => { const saveMode: SaveMode = data.value ? 'replace' : 'patch'; const applied = applyPatch(data.value ?? data.vanilla, data.patch ?? []); return { schema: data.schema, + itemSchema: data.schema.items + ? omit(data.schema.items, ['title', 'description']) + : null, vanilla: data.vanilla, mod: data.value, patch: data.patch, @@ -348,6 +382,8 @@ export const { changeText, changeJson, changeJsonItem, + addJsonItem, + removeJsonItem, changeSaveMode, changeEditMode, } = filesSlice.actions; diff --git a/tsconfig.json b/tsconfig.json index 5ac7e75..2ebe00e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "moduleResolution": "node", "esModuleInterop": true, "allowSyntheticDefaultImports": true, + "noUncheckedIndexedAccess": true, "resolveJsonModule": true, "allowJs": true, "outDir": ".erb/dll"