mirror of
https://github.com/4ian/GDevelop.git
synced 2026-10-12 02:01:17 +00:00
@@ -250,7 +250,7 @@ blocks:
|
|||||||
(cd GDJS && git checkout package-lock.json && npm i)
|
(cd GDJS && git checkout package-lock.json && npm i)
|
||||||
- cd newIDE/app
|
- cd newIDE/app
|
||||||
- npm run postinstall
|
- npm run postinstall
|
||||||
- npm run analyze-test-coverage
|
- npm run test
|
||||||
|
|
||||||
- name: GDJS tests
|
- name: GDJS tests
|
||||||
commands:
|
commands:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// This file customizes webpack configuration for react-app-rewired.
|
// This file customizes webpack configuration for react-app-rewired.
|
||||||
|
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
webpack: function override(config, env) {
|
webpack: function override(config, env) {
|
||||||
@@ -16,12 +17,16 @@ module.exports = {
|
|||||||
// but it is not a blocking issue, so we ignore them.
|
// but it is not a blocking issue, so we ignore them.
|
||||||
config.ignoreWarnings = [/Failed to parse source map/];
|
config.ignoreWarnings = [/Failed to parse source map/];
|
||||||
|
|
||||||
|
config.resolve.plugins = config.resolve.plugins.filter(
|
||||||
|
(plugin) => !(plugin instanceof ModuleScopePlugin)
|
||||||
|
);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
|
||||||
jest: function(config) {
|
jest: function(config) {
|
||||||
config.transformIgnorePatterns = [
|
config.transformIgnorePatterns = [
|
||||||
'<rootDir>/node_modules/(?!react-markdown|unified|remark-parse|mdast-util-from-markdown|micromark|decode-named-character-reference|remark-rehype|trim-lines|hast-util-whitespace|remark-gfm|mdast-util-gfm|mdast-util-find-and-replace|mdast-util-to-markdown|markdown-table|is-plain-obj)',
|
'<rootDir>/node_modules/(?!react-markdown|vfile|unist-.*|unified|bail|trough|character-entities|remark-parse|mdast-util-.*|micromark|decode-named-character-reference|remark-rehype|property-information|hast-util-.*|space-separated-tokens|comma-separated-tokens|ccount|escape-string-regexp|trim-lines|hast-util-whitespace|remark-gfm|mdast-util-gfm|mdast-util-find-and-replace|mdast-util-to-markdown|markdown-table|is-plain-obj)',
|
||||||
];
|
];
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
Vendored
+23
@@ -690,3 +690,26 @@ type ReactDOM$SVGElementJSXIntrinsic = {
|
|||||||
},
|
},
|
||||||
...
|
...
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// React 18 createRoot API
|
||||||
|
declare module 'react-dom/client' {
|
||||||
|
declare type RootOptions = {
|
||||||
|
onRecoverableError?: (error: mixed) => void,
|
||||||
|
identifierPrefix?: string,
|
||||||
|
...
|
||||||
|
};
|
||||||
|
declare type Root = {
|
||||||
|
render(children: React$Node): void,
|
||||||
|
unmount(): void,
|
||||||
|
...
|
||||||
|
};
|
||||||
|
declare function createRoot(
|
||||||
|
container: Element | Document | DocumentFragment,
|
||||||
|
options?: RootOptions
|
||||||
|
): Root;
|
||||||
|
declare function hydrateRoot(
|
||||||
|
container: Document | Element,
|
||||||
|
initialChildren: React$Node,
|
||||||
|
options?: RootOptions
|
||||||
|
): Root;
|
||||||
|
}
|
||||||
|
|||||||
Generated
+18286
-11597
File diff suppressed because it is too large
Load Diff
@@ -73,13 +73,13 @@
|
|||||||
"posthog-js": "1.275.2",
|
"posthog-js": "1.275.2",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"qr-creator": "^1.0.0",
|
"qr-creator": "^1.0.0",
|
||||||
"react": "16.14.0",
|
"react": "18.2.0",
|
||||||
"react-color": "2.13.8",
|
"react-color": "2.13.8",
|
||||||
"react-dnd": "7.7.0",
|
"react-dnd": "7.7.0",
|
||||||
"react-dnd-html5-backend": "7.7.0",
|
"react-dnd-html5-backend": "7.7.0",
|
||||||
"react-dnd-multi-backend": "3.2.2",
|
"react-dnd-multi-backend": "3.2.2",
|
||||||
"react-dnd-touch-backend": "0.8.3",
|
"react-dnd-touch-backend": "0.8.3",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "18.2.0",
|
||||||
"react-error-boundary": "^1.2.0",
|
"react-error-boundary": "^1.2.0",
|
||||||
"react-inner-image-zoom": "^3.0.2",
|
"react-inner-image-zoom": "^3.0.2",
|
||||||
"react-instantsearch-hooks": "^6.41.0",
|
"react-instantsearch-hooks": "^6.41.0",
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
"react-mosaic-component": "github:4ian/react-mosaic#v3.1.0",
|
"react-mosaic-component": "github:4ian/react-mosaic#v3.1.0",
|
||||||
"react-share": "^4.4.0",
|
"react-share": "^4.4.0",
|
||||||
"react-sortable-hoc": "1.5.0",
|
"react-sortable-hoc": "1.5.0",
|
||||||
"react-test-renderer": "16.14.0",
|
"react-test-renderer": "18.2.0",
|
||||||
"react-virtualized": "9.21.1",
|
"react-virtualized": "9.21.1",
|
||||||
"react-window": "1.8.9",
|
"react-window": "1.8.9",
|
||||||
"recharts": "^2.10.0",
|
"recharts": "^2.10.0",
|
||||||
@@ -103,7 +103,8 @@
|
|||||||
"url-search-params": "^1.0.2",
|
"url-search-params": "^1.0.2",
|
||||||
"wavesurfer.js": "^7.8.6",
|
"wavesurfer.js": "^7.8.6",
|
||||||
"xxhashjs": "^0.2.2",
|
"xxhashjs": "^0.2.2",
|
||||||
"yaml": "^2.8.2"
|
"yaml": "^2.8.2",
|
||||||
|
"@material-ui/styles": "4.11.5"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"pixi-spine": {
|
"pixi-spine": {
|
||||||
@@ -114,7 +115,9 @@
|
|||||||
"@pixi/mesh": "7.4.2",
|
"@pixi/mesh": "7.4.2",
|
||||||
"@pixi/mesh-extras": "7.4.2",
|
"@pixi/mesh-extras": "7.4.2",
|
||||||
"@pixi/sprite": "7.4.2"
|
"@pixi/sprite": "7.4.2"
|
||||||
}
|
},
|
||||||
|
"react": "$react",
|
||||||
|
"react-dom": "$react-dom"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "patch-package && cd ../../GDJS && npm install && cd ../newIDE/app && npm run import-resources",
|
"postinstall": "patch-package && cd ../../GDJS && npm install && cd ../newIDE/app && npm run import-resources",
|
||||||
|
|||||||
@@ -0,0 +1,335 @@
|
|||||||
|
/**
|
||||||
|
* Idempotent codemod script to upgrade newIDE/app from React 16 to React 18.
|
||||||
|
*
|
||||||
|
* What it does:
|
||||||
|
* 1. Updates package.json: react, react-dom, react-test-renderer to 18.2.0
|
||||||
|
* and adds npm overrides so third-party packages accept React 18
|
||||||
|
* 2. Updates src/index.js: ReactDOM.render() -> createRoot().render()
|
||||||
|
* 3. Adds Flow type declaration for react-dom/client in flow-libs/react-dom.js
|
||||||
|
* 4. Renames deprecated lifecycle methods to UNSAFE_ prefix (React 17 requirement):
|
||||||
|
* - componentWillMount -> UNSAFE_componentWillMount
|
||||||
|
* - componentWillReceiveProps -> UNSAFE_componentWillReceiveProps
|
||||||
|
* - componentWillUpdate -> UNSAFE_componentWillUpdate
|
||||||
|
* 5. Fixes getFillAutomaticallyFunction in InAppTutorialStepDisplayer.js:
|
||||||
|
* The old native-value-setter + blur/input event trick no longer works in
|
||||||
|
* React 18. Replaced with focus + select + document.execCommand('insertText')
|
||||||
|
* which goes through the browser's native input path.
|
||||||
|
* 6. Fixes ClickAwayListener in GenericExpressionField/index.js:
|
||||||
|
* MUI v4's ClickAwayListener defaults to onClick/onTouchEnd, which in React 18
|
||||||
|
* causes the popper to close immediately when opened. Using onMouseDown/onTouchStart
|
||||||
|
* avoids the race condition with React 18's event delegation.
|
||||||
|
*
|
||||||
|
* What it does NOT do (deprecated but still functional in React 18):
|
||||||
|
* - ReactDOM.findDOMNode: deprecated, warns in StrictMode, but still works.
|
||||||
|
* Replacing it requires significant refactoring (forwardRef/callback refs).
|
||||||
|
* - UNSAFE_ lifecycle methods themselves: they work in React 18, just with
|
||||||
|
* StrictMode warnings. Migrating them to getDerivedStateFromProps/
|
||||||
|
* componentDidMount is a separate task.
|
||||||
|
* - e.persist() removal: no calls found in the codebase.
|
||||||
|
*
|
||||||
|
* Re-running this script on an already-upgraded codebase is a no-op.
|
||||||
|
*/
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const appRoot = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
function readFile(relativePath) {
|
||||||
|
return fs.readFileSync(path.join(appRoot, relativePath), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFile(relativePath, content) {
|
||||||
|
fs.writeFileSync(path.join(appRoot, relativePath), content, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 1. Update package.json versions
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
function updatePackageJson() {
|
||||||
|
const filePath = 'package.json';
|
||||||
|
const pkg = JSON.parse(readFile(filePath));
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
const depUpdates = {
|
||||||
|
react: '18.2.0',
|
||||||
|
'react-dom': '18.2.0',
|
||||||
|
'react-test-renderer': '18.2.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [dep, version] of Object.entries(depUpdates)) {
|
||||||
|
if (pkg.dependencies && pkg.dependencies[dep] && pkg.dependencies[dep] !== version) {
|
||||||
|
pkg.dependencies[dep] = version;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add overrides so that all third-party packages use our React 18,
|
||||||
|
// even if their peerDependencies only declare React 16/17 support.
|
||||||
|
// The "$react" / "$react-dom" syntax means "use the version from
|
||||||
|
// this package.json's own dependencies".
|
||||||
|
if (!pkg.overrides) pkg.overrides = {};
|
||||||
|
const requiredOverrides = {
|
||||||
|
react: '$react',
|
||||||
|
'react-dom': '$react-dom',
|
||||||
|
};
|
||||||
|
for (const [key, value] of Object.entries(requiredOverrides)) {
|
||||||
|
if (pkg.overrides[key] !== value) {
|
||||||
|
pkg.overrides[key] = value;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @material-ui/styles is a dependency of @material-ui/core but its
|
||||||
|
// peerDependencies only declare React ^16 || ^17. This causes npm to
|
||||||
|
// nest it inside @material-ui/core/node_modules instead of hoisting it.
|
||||||
|
// Since the app code imports directly from '@material-ui/styles', we
|
||||||
|
// need it hoisted. Adding it as a direct dependency forces hoisting.
|
||||||
|
if (!pkg.dependencies['@material-ui/styles']) {
|
||||||
|
pkg.dependencies['@material-ui/styles'] = '4.11.5';
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
writeFile(filePath, JSON.stringify(pkg, null, 2) + '\n');
|
||||||
|
console.log('[package.json] Updated react, react-dom, react-test-renderer to 18.2.0 with overrides');
|
||||||
|
} else {
|
||||||
|
console.log('[package.json] Already up to date.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 2. Update src/index.js to use createRoot from react-dom/client
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
function updateIndexJs() {
|
||||||
|
const filePath = 'src/index.js';
|
||||||
|
let content = readFile(filePath);
|
||||||
|
|
||||||
|
// Already upgraded?
|
||||||
|
if (content.includes('react-dom/client')) {
|
||||||
|
console.log('[src/index.js] Already upgraded to createRoot.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace import
|
||||||
|
content = content.replace(
|
||||||
|
"import ReactDOM from 'react-dom';",
|
||||||
|
"// $FlowFixMe[cannot-resolve-module] - react-dom/client is available in React 18\nimport { createRoot } from 'react-dom/client';"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace ReactDOM.render(<Bootstrapper />, rootElement) with createRoot(rootElement).render(<Bootstrapper />)
|
||||||
|
content = content.replace(
|
||||||
|
/ReactDOM\.render\(<Bootstrapper \/>, rootElement\)/,
|
||||||
|
'createRoot(rootElement).render(<Bootstrapper />)'
|
||||||
|
);
|
||||||
|
|
||||||
|
writeFile(filePath, content);
|
||||||
|
console.log('[src/index.js] Upgraded to createRoot.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 3. Add Flow type declaration for react-dom/client
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
function updateFlowTypes() {
|
||||||
|
const filePath = 'flow-libs/react-dom.js';
|
||||||
|
let content = readFile(filePath);
|
||||||
|
|
||||||
|
if (content.includes('react-dom/client')) {
|
||||||
|
console.log('[flow-libs/react-dom.js] Already has react-dom/client declaration.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const declaration = `
|
||||||
|
// React 18 createRoot API
|
||||||
|
declare module 'react-dom/client' {
|
||||||
|
declare type RootOptions = {
|
||||||
|
onRecoverableError?: (error: mixed) => void,
|
||||||
|
identifierPrefix?: string,
|
||||||
|
...
|
||||||
|
};
|
||||||
|
declare type Root = {
|
||||||
|
render(children: React$Node): void,
|
||||||
|
unmount(): void,
|
||||||
|
...
|
||||||
|
};
|
||||||
|
declare function createRoot(
|
||||||
|
container: Element | Document | DocumentFragment,
|
||||||
|
options?: RootOptions
|
||||||
|
): Root;
|
||||||
|
declare function hydrateRoot(
|
||||||
|
container: Document | Element,
|
||||||
|
initialChildren: React$Node,
|
||||||
|
options?: RootOptions
|
||||||
|
): Root;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
content = content.trimEnd() + '\n' + declaration;
|
||||||
|
writeFile(filePath, content);
|
||||||
|
console.log('[flow-libs/react-dom.js] Added react-dom/client declaration.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 4. Rename deprecated lifecycle methods to UNSAFE_ prefix
|
||||||
|
// React 17 removed the non-prefixed versions of these methods.
|
||||||
|
// See: https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
function renameUnsafeLifecycleMethods() {
|
||||||
|
const srcDir = path.join(appRoot, 'src');
|
||||||
|
const deprecatedMethods = [
|
||||||
|
'componentWillMount',
|
||||||
|
'componentWillReceiveProps',
|
||||||
|
'componentWillUpdate',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Build regex that matches the method name NOT already preceded by UNSAFE_
|
||||||
|
// Matches patterns like:
|
||||||
|
// componentWillMount() {
|
||||||
|
// componentWillMount () {
|
||||||
|
// componentWillMount= ... (arrow function assignment, unlikely but safe)
|
||||||
|
const patterns = deprecatedMethods.map(method => ({
|
||||||
|
method,
|
||||||
|
// Negative lookbehind ensures we don't double-prefix UNSAFE_
|
||||||
|
regex: new RegExp(`(?<!UNSAFE_)\\b(${method})\\b`, 'g'),
|
||||||
|
replacement: `UNSAFE_${method}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let totalReplacements = 0;
|
||||||
|
|
||||||
|
function processFile(filePath) {
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
if (ext !== '.js' && ext !== '.jsx' && ext !== '.ts' && ext !== '.tsx') return;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileChanged = false;
|
||||||
|
for (const { method, regex, replacement } of patterns) {
|
||||||
|
const matches = content.match(regex);
|
||||||
|
if (matches) {
|
||||||
|
content = content.replace(regex, replacement);
|
||||||
|
fileChanged = true;
|
||||||
|
totalReplacements += matches.length;
|
||||||
|
const relPath = path.relative(appRoot, filePath);
|
||||||
|
console.log(` ${relPath}: ${method} -> UNSAFE_${method} (${matches.length}x)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileChanged) {
|
||||||
|
fs.writeFileSync(filePath, content, 'utf8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkDir(dir) {
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
||||||
|
walkDir(fullPath);
|
||||||
|
} else {
|
||||||
|
processFile(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walkDir(srcDir);
|
||||||
|
|
||||||
|
if (totalReplacements === 0) {
|
||||||
|
console.log('[lifecycle methods] No deprecated lifecycle methods found (already renamed).');
|
||||||
|
} else {
|
||||||
|
console.log(`[lifecycle methods] Renamed ${totalReplacements} deprecated lifecycle method(s) to UNSAFE_ prefix.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 5. Fix getFillAutomaticallyFunction for React 18
|
||||||
|
// The old approach used the native value setter + dispatchEvent('blur'/'input')
|
||||||
|
// to programmatically fill React-controlled inputs. This no longer triggers
|
||||||
|
// React 18's onChange. Replace with execCommand('insertText') which goes
|
||||||
|
// through the browser's native input path.
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
function fixGetFillAutomaticallyFunction() {
|
||||||
|
const filePath = 'src/InAppTutorial/InAppTutorialStepDisplayer.js';
|
||||||
|
let content = readFile(filePath);
|
||||||
|
|
||||||
|
// Already upgraded?
|
||||||
|
if (content.includes("document.execCommand('insertText'")) {
|
||||||
|
console.log('[InAppTutorialStepDisplayer] getFillAutomaticallyFunction already fixed.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match the old native-value-setter pattern (handles both 'blur' and 'input' event variants).
|
||||||
|
// Uses [\s\S]*? instead of [^}]* to handle nested braces (e.g. { bubbles: true }).
|
||||||
|
const oldPattern =
|
||||||
|
/const valuePropertyDescriptor = Object\.getOwnPropertyDescriptor\(\s*elementWithId\.constructor\.prototype,\s*'value'\s*\);\s*if \(!valuePropertyDescriptor\) return undefined;\s*const valueSetter = valuePropertyDescriptor\.set;\s*if \(!valueSetter\) return undefined;\s*return \(\) => \{[\s\S]*?\n\s*\};/;
|
||||||
|
|
||||||
|
if (!oldPattern.test(content)) {
|
||||||
|
console.log('[InAppTutorialStepDisplayer] Could not find old getFillAutomaticallyFunction pattern (may need manual update).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCode = `return () => {
|
||||||
|
// Focus the element, select all existing content, then use
|
||||||
|
// execCommand to insert text. This goes through the browser's
|
||||||
|
// native input path, which React 18 correctly picks up.
|
||||||
|
elementWithId.focus();
|
||||||
|
elementWithId.select();
|
||||||
|
document.execCommand('insertText', false, valueEquals);
|
||||||
|
};`;
|
||||||
|
|
||||||
|
content = content.replace(oldPattern, newCode);
|
||||||
|
writeFile(filePath, content);
|
||||||
|
console.log('[InAppTutorialStepDisplayer] Fixed getFillAutomaticallyFunction for React 18.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 6. Fix ClickAwayListener in GenericExpressionField for React 18
|
||||||
|
// MUI v4's ClickAwayListener defaults to onClick/onTouchEnd for detecting
|
||||||
|
// clicks outside. In React 18 (with createRoot), the event delegation moved
|
||||||
|
// from document to the React root container. This causes the ClickAwayListener's
|
||||||
|
// document-level handler to fire on the same click that opened the popper,
|
||||||
|
// immediately closing it. Using onMouseDown/onTouchStart avoids this.
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
function fixClickAwayListenerInGenericExpressionField() {
|
||||||
|
const filePath = 'src/EventsSheet/ParameterFields/GenericExpressionField/index.js';
|
||||||
|
let content = readFile(filePath);
|
||||||
|
|
||||||
|
// Already upgraded?
|
||||||
|
if (content.includes('mouseEvent="onMouseDown"')) {
|
||||||
|
console.log('[GenericExpressionField] ClickAwayListener already fixed.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldPattern = '<ClickAwayListener onClickAway={this._handleRequestClose}>';
|
||||||
|
if (!content.includes(oldPattern)) {
|
||||||
|
console.log('[GenericExpressionField] Could not find ClickAwayListener pattern (may need manual update).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCode = `<ClickAwayListener
|
||||||
|
onClickAway={this._handleRequestClose}
|
||||||
|
// Needed since React 18 to avoid what seems to be the immediate closing of the popper.
|
||||||
|
mouseEvent="onMouseDown"
|
||||||
|
touchEvent="onTouchStart"
|
||||||
|
>`;
|
||||||
|
|
||||||
|
content = content.replace(oldPattern, newCode);
|
||||||
|
writeFile(filePath, content);
|
||||||
|
console.log('[GenericExpressionField] Fixed ClickAwayListener for React 18.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Run all steps
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
console.log('Upgrading newIDE/app to React 18...\n');
|
||||||
|
updatePackageJson();
|
||||||
|
updateIndexJs();
|
||||||
|
updateFlowTypes();
|
||||||
|
renameUnsafeLifecycleMethods();
|
||||||
|
fixGetFillAutomaticallyFunction();
|
||||||
|
fixClickAwayListenerInGenericExpressionField();
|
||||||
|
console.log('\nDone. Run `npm install` to apply dependency changes.');
|
||||||
@@ -16,6 +16,7 @@ import { useAsyncLazyMemo } from '../Utils/UseLazyMemo';
|
|||||||
import { retryIfFailed } from '../Utils/RetryIfFailed';
|
import { retryIfFailed } from '../Utils/RetryIfFailed';
|
||||||
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||||
import { useInterval } from '../Utils/UseInterval';
|
import { useInterval } from '../Utils/UseInterval';
|
||||||
|
import useForceUpdate from '../Utils/UseForceUpdate';
|
||||||
|
|
||||||
type EditorFunctionCallResultsStorage = {|
|
type EditorFunctionCallResultsStorage = {|
|
||||||
getEditorFunctionCallResults: (
|
getEditorFunctionCallResults: (
|
||||||
@@ -28,62 +29,71 @@ type EditorFunctionCallResultsStorage = {|
|
|||||||
clearEditorFunctionCallResults: (aiRequestId: string) => void,
|
clearEditorFunctionCallResults: (aiRequestId: string) => void,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
|
type EditorFunctionCallResultsPerRequest = {
|
||||||
|
[aiRequestId: string]: ?Array<EditorFunctionCallResult>,
|
||||||
|
};
|
||||||
|
|
||||||
const useEditorFunctionCallResultsStorage = (): EditorFunctionCallResultsStorage => {
|
const useEditorFunctionCallResultsStorage = (): EditorFunctionCallResultsStorage => {
|
||||||
const [
|
const editorFunctionCallResultsPerRequestRef = React.useRef<EditorFunctionCallResultsPerRequest>(
|
||||||
editorFunctionCallResultsPerRequest,
|
{}
|
||||||
setEditorFunctionCallResultsPerRequest,
|
);
|
||||||
] = React.useState<{
|
const forceUpdate = useForceUpdate();
|
||||||
[aiRequestId: string]: Array<EditorFunctionCallResult>,
|
|
||||||
}>({});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getEditorFunctionCallResults: React.useCallback(
|
getEditorFunctionCallResults: React.useCallback(
|
||||||
(aiRequestId: string): Array<EditorFunctionCallResult> | null =>
|
(aiRequestId: string): Array<EditorFunctionCallResult> | null =>
|
||||||
editorFunctionCallResultsPerRequest[aiRequestId] || null,
|
editorFunctionCallResultsPerRequestRef.current[aiRequestId] || null,
|
||||||
[editorFunctionCallResultsPerRequest]
|
[]
|
||||||
),
|
),
|
||||||
addEditorFunctionCallResults: React.useCallback(
|
addEditorFunctionCallResults: React.useCallback(
|
||||||
(
|
(
|
||||||
aiRequestId: string,
|
aiRequestId: string,
|
||||||
editorFunctionCallResults: EditorFunctionCallResult[]
|
editorFunctionCallResults: EditorFunctionCallResult[]
|
||||||
) => {
|
) => {
|
||||||
let computedResults: EditorFunctionCallResult[] = [];
|
const previousState = editorFunctionCallResultsPerRequestRef.current;
|
||||||
setEditorFunctionCallResultsPerRequest(prevState => {
|
const existingEditorFunctionCallResults = (
|
||||||
const existingEditorFunctionCallResults = (
|
previousState[aiRequestId] || []
|
||||||
prevState[aiRequestId] || []
|
).filter(existingEditorFunctionCallResult => {
|
||||||
).filter(existingEditorFunctionCallResult => {
|
return !editorFunctionCallResults.some(editorFunctionCallResult => {
|
||||||
return !editorFunctionCallResults.some(editorFunctionCallResult => {
|
return (
|
||||||
return (
|
editorFunctionCallResult.call_id ===
|
||||||
editorFunctionCallResult.call_id ===
|
existingEditorFunctionCallResult.call_id
|
||||||
existingEditorFunctionCallResult.call_id
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
computedResults = [
|
|
||||||
...existingEditorFunctionCallResults,
|
|
||||||
...editorFunctionCallResults,
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prevState,
|
|
||||||
[aiRequestId]: computedResults,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const computedResults = [
|
||||||
|
...existingEditorFunctionCallResults,
|
||||||
|
...editorFunctionCallResults,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Store results in the ref so that they are visible immediately
|
||||||
|
// to any code that calls getEditorFunctionCallResults — even within
|
||||||
|
// the same async tick. Without this, React 18 automatic batching
|
||||||
|
// would defer the state update, causing other callbacks (e.g. the
|
||||||
|
// processing effect) to read stale data and potentially re-process
|
||||||
|
// the same function calls.
|
||||||
|
// forceUpdate() schedules a re-render so the UI stays in sync.
|
||||||
|
editorFunctionCallResultsPerRequestRef.current = {
|
||||||
|
...editorFunctionCallResultsPerRequestRef.current,
|
||||||
|
[aiRequestId]: computedResults,
|
||||||
|
};
|
||||||
|
forceUpdate();
|
||||||
|
|
||||||
return computedResults;
|
return computedResults;
|
||||||
},
|
},
|
||||||
[]
|
[forceUpdate]
|
||||||
),
|
),
|
||||||
clearEditorFunctionCallResults: React.useCallback((aiRequestId: string) => {
|
clearEditorFunctionCallResults: React.useCallback(
|
||||||
setEditorFunctionCallResultsPerRequest(
|
(aiRequestId: string) => {
|
||||||
editorFunctionCallResultsPerRequest => ({
|
editorFunctionCallResultsPerRequestRef.current = {
|
||||||
...editorFunctionCallResultsPerRequest,
|
...editorFunctionCallResultsPerRequestRef.current,
|
||||||
// $FlowFixMe[incompatible-type]
|
|
||||||
[aiRequestId]: null,
|
[aiRequestId]: null,
|
||||||
})
|
};
|
||||||
);
|
forceUpdate();
|
||||||
}, []),
|
},
|
||||||
|
[forceUpdate]
|
||||||
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -269,47 +279,57 @@ export const useAiRequestsStorage = (): AiRequestStorage => {
|
|||||||
[profile]
|
[profile]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [aiRequestSendStates, setAiRequestSendStates] = React.useState<{
|
// Store send states in a ref so that isSendingAiRequest reads are
|
||||||
|
// immediately consistent — even within the same async tick.
|
||||||
|
// Without this, React 18 automatic batching would defer the state
|
||||||
|
// update from setSendingAiRequest, causing guards (e.g. in
|
||||||
|
// onSendMessage) to read stale data and potentially trigger
|
||||||
|
// duplicate API calls.
|
||||||
|
// forceUpdate() schedules a re-render so the UI stays in sync.
|
||||||
|
const aiRequestSendStatesRef = React.useRef<{
|
||||||
[string]: AiRequestSendState,
|
[string]: AiRequestSendState,
|
||||||
}>({});
|
}>({});
|
||||||
|
const forceUpdateForSendStates = useForceUpdate();
|
||||||
const isSendingAiRequest = React.useCallback(
|
const isSendingAiRequest = React.useCallback(
|
||||||
(aiRequestId: string | null) =>
|
(aiRequestId: string | null) =>
|
||||||
!!aiRequestSendStates[aiRequestId || ''] &&
|
!!aiRequestSendStatesRef.current[aiRequestId || ''] &&
|
||||||
aiRequestSendStates[aiRequestId || ''].isSending,
|
aiRequestSendStatesRef.current[aiRequestId || ''].isSending,
|
||||||
[aiRequestSendStates]
|
[]
|
||||||
);
|
);
|
||||||
const getLastSendError = React.useCallback(
|
const getLastSendError = React.useCallback(
|
||||||
(aiRequestId: string | null) =>
|
(aiRequestId: string | null) =>
|
||||||
(aiRequestSendStates[aiRequestId || ''] &&
|
(aiRequestSendStatesRef.current[aiRequestId || ''] &&
|
||||||
aiRequestSendStates[aiRequestId || ''].lastSendError) ||
|
aiRequestSendStatesRef.current[aiRequestId || ''].lastSendError) ||
|
||||||
null,
|
null,
|
||||||
[aiRequestSendStates]
|
[]
|
||||||
);
|
);
|
||||||
const setSendingAiRequest = React.useCallback(
|
const setSendingAiRequest = React.useCallback(
|
||||||
(aiRequestId: string | null, isSending: boolean) => {
|
(aiRequestId: string | null, isSending: boolean) => {
|
||||||
const aiRequestIdToSet: string = aiRequestId || '';
|
const aiRequestIdToSet: string = aiRequestId || '';
|
||||||
setAiRequestSendStates(aiRequestSendStates => ({
|
aiRequestSendStatesRef.current = {
|
||||||
...aiRequestSendStates,
|
...aiRequestSendStatesRef.current,
|
||||||
[aiRequestIdToSet]: {
|
[aiRequestIdToSet]: {
|
||||||
isSending,
|
isSending,
|
||||||
lastSendError: null,
|
lastSendError: null,
|
||||||
},
|
},
|
||||||
}));
|
};
|
||||||
|
forceUpdateForSendStates();
|
||||||
},
|
},
|
||||||
[setAiRequestSendStates]
|
[forceUpdateForSendStates]
|
||||||
);
|
);
|
||||||
const setLastSendError = React.useCallback(
|
const setLastSendError = React.useCallback(
|
||||||
(aiRequestId: string | null, lastSendError: ?Error) => {
|
(aiRequestId: string | null, lastSendError: ?Error) => {
|
||||||
const aiRequestIdToSet: string = aiRequestId || '';
|
const aiRequestIdToSet: string = aiRequestId || '';
|
||||||
setAiRequestSendStates(aiRequestSendStates => ({
|
aiRequestSendStatesRef.current = {
|
||||||
...aiRequestSendStates,
|
...aiRequestSendStatesRef.current,
|
||||||
[aiRequestIdToSet]: {
|
[aiRequestIdToSet]: {
|
||||||
isSending: false,
|
isSending: false,
|
||||||
lastSendError,
|
lastSendError,
|
||||||
},
|
},
|
||||||
}));
|
};
|
||||||
|
forceUpdateForSendStates();
|
||||||
},
|
},
|
||||||
[setAiRequestSendStates]
|
[forceUpdateForSendStates]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -617,13 +617,14 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
|||||||
editorFunctionCallResults: Array<EditorFunctionCallResult>,
|
editorFunctionCallResults: Array<EditorFunctionCallResult>,
|
||||||
mode?: 'chat' | 'agent' | 'orchestrator',
|
mode?: 'chat' | 'agent' | 'orchestrator',
|
||||||
|}) => {
|
|}) => {
|
||||||
if (
|
if (!profile || !selectedAiRequestId || !selectedAiRequest) return;
|
||||||
!profile ||
|
|
||||||
!selectedAiRequestId ||
|
if (isSendingAiRequest(selectedAiRequestId)) {
|
||||||
!selectedAiRequest ||
|
console.info(
|
||||||
isSendingAiRequest(selectedAiRequestId)
|
'Skipping send for AI request: another send is already in progress.'
|
||||||
)
|
);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Read the results from the editor that applied the function calls.
|
// Read the results from the editor that applied the function calls.
|
||||||
// and transform them into the output that will be stored on the AI request.
|
// and transform them into the output that will be stored on the AI request.
|
||||||
@@ -642,8 +643,18 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
|||||||
|
|
||||||
// If anything is not finished yet, stop there (we only send all
|
// If anything is not finished yet, stop there (we only send all
|
||||||
// results at once, AI do not support partial results).
|
// results at once, AI do not support partial results).
|
||||||
if (hasUnfinishedResult) return;
|
if (hasUnfinishedResult) {
|
||||||
if (hasFunctionsCallsToProcess) return;
|
console.info(
|
||||||
|
'Skipping send for AI request: some function call results are not finished yet.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasFunctionsCallsToProcess) {
|
||||||
|
console.info(
|
||||||
|
'Skipping send for AI request: there are still function calls to process.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If nothing to send, stop there.
|
// If nothing to send, stop there.
|
||||||
if (functionCallOutputs.length === 0 && !userMessage) return;
|
if (functionCallOutputs.length === 0 && !userMessage) return;
|
||||||
@@ -767,6 +778,7 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error while sending AI request message:', error);
|
||||||
// TODO: update the label of the button to send again.
|
// TODO: update the label of the button to send again.
|
||||||
setLastSendError(selectedAiRequestId, error);
|
setLastSendError(selectedAiRequestId, error);
|
||||||
setIsSendingUserMessage(false);
|
setIsSendingUserMessage(false);
|
||||||
|
|||||||
@@ -161,68 +161,113 @@ export const useProcessFunctionCalls = ({
|
|||||||
resourceManagementProps,
|
resourceManagementProps,
|
||||||
});
|
});
|
||||||
const { generateEvents } = useGenerateEvents({ project });
|
const { generateEvents } = useGenerateEvents({ project });
|
||||||
|
// In-memory guard against duplicate processing of the same function call.
|
||||||
|
//
|
||||||
|
// The main protection is marking calls as "working" in the ref-backed
|
||||||
|
// results store (see useEditorFunctionCallResultsStorage). However,
|
||||||
|
// React 18 can re-run an effect before a forceUpdate() re-render has
|
||||||
|
// propagated (e.g. StrictMode double-invocations in development, or a
|
||||||
|
// polling update to selectedAiRequest that recreates onProcessFunctionCalls
|
||||||
|
// while the previous invocation is still awaiting).
|
||||||
|
// This Set acts as an immediate, synchronous lock keyed by
|
||||||
|
// "<requestId>:<callId>" so a call that is already being processed is
|
||||||
|
// never started a second time.
|
||||||
|
const inFlightFunctionCallIdsRef = React.useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
const onProcessFunctionCalls = React.useCallback(
|
const onProcessFunctionCalls = React.useCallback(
|
||||||
async (functionCalls: Array<AiRequestMessageAssistantFunctionCall>) => {
|
async (functionCalls: Array<AiRequestMessageAssistantFunctionCall>) => {
|
||||||
if (!selectedAiRequest || !isReadyToProcessFunctionCalls) return;
|
if (!selectedAiRequest || !isReadyToProcessFunctionCalls) return;
|
||||||
if (selectedAiRequest.status === 'suspended') return;
|
if (selectedAiRequest.status === 'suspended') return;
|
||||||
|
|
||||||
|
const functionCallsToProcess = functionCalls.filter(
|
||||||
|
functionCall =>
|
||||||
|
!inFlightFunctionCallIdsRef.current.has(
|
||||||
|
`${selectedAiRequest.id}:${functionCall.call_id}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (functionCallsToProcess.length === 0) {
|
||||||
|
console.info(
|
||||||
|
'All function calls are already being processed (in-flight guard), skipping.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock these call IDs so concurrent invocations skip them.
|
||||||
|
functionCallsToProcess.forEach(functionCall => {
|
||||||
|
inFlightFunctionCallIdsRef.current.add(
|
||||||
|
`${selectedAiRequest.id}:${functionCall.call_id}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
addEditorFunctionCallResults(
|
addEditorFunctionCallResults(
|
||||||
selectedAiRequest.id,
|
selectedAiRequest.id,
|
||||||
functionCalls.map(functionCall => ({
|
functionCallsToProcess.map(functionCall => ({
|
||||||
status: 'working',
|
status: 'working',
|
||||||
call_id: functionCall.call_id,
|
call_id: functionCall.call_id,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
try {
|
||||||
results,
|
const {
|
||||||
createdSceneNames,
|
results,
|
||||||
createdProject,
|
createdSceneNames,
|
||||||
} = await processEditorFunctionCalls({
|
createdProject,
|
||||||
project,
|
} = await processEditorFunctionCalls({
|
||||||
editorCallbacks,
|
project,
|
||||||
// $FlowFixMe[incompatible-type]
|
editorCallbacks,
|
||||||
toolOptions: selectedAiRequest.toolOptions || null,
|
// $FlowFixMe[incompatible-type]
|
||||||
i18n,
|
toolOptions: selectedAiRequest.toolOptions || null,
|
||||||
functionCalls: functionCalls.map(functionCall => ({
|
i18n,
|
||||||
name: functionCall.name,
|
functionCalls: functionCallsToProcess.map(functionCall => ({
|
||||||
arguments: functionCall.arguments,
|
name: functionCall.name,
|
||||||
call_id: functionCall.call_id,
|
arguments: functionCall.arguments,
|
||||||
})),
|
call_id: functionCall.call_id,
|
||||||
relatedAiRequestId: selectedAiRequest.id,
|
})),
|
||||||
getRelatedAiRequestLastMessages: () =>
|
relatedAiRequestId: selectedAiRequest.id,
|
||||||
getLastMessagesFromAiRequestOutput(selectedAiRequest.output),
|
getRelatedAiRequestLastMessages: () =>
|
||||||
generateEvents,
|
getLastMessagesFromAiRequestOutput(selectedAiRequest.output),
|
||||||
onSceneEventsModifiedOutsideEditor,
|
generateEvents,
|
||||||
onInstancesModifiedOutsideEditor,
|
onSceneEventsModifiedOutsideEditor,
|
||||||
onObjectsModifiedOutsideEditor,
|
onInstancesModifiedOutsideEditor,
|
||||||
onObjectGroupsModifiedOutsideEditor,
|
onObjectsModifiedOutsideEditor,
|
||||||
ensureExtensionInstalled,
|
onObjectGroupsModifiedOutsideEditor,
|
||||||
onWillInstallExtension,
|
ensureExtensionInstalled,
|
||||||
onExtensionInstalled,
|
onWillInstallExtension,
|
||||||
searchAndInstallAsset,
|
onExtensionInstalled,
|
||||||
searchAndInstallResources,
|
searchAndInstallAsset,
|
||||||
});
|
searchAndInstallResources,
|
||||||
|
});
|
||||||
|
|
||||||
// If the request was suspended while we were processing, discard the
|
// If the request was suspended while we were processing, discard the
|
||||||
// results — we don't want to re-populate the cleared results or send
|
// results — we don't want to re-populate the cleared results or send
|
||||||
// anything to a suspended request.
|
// anything to a suspended request.
|
||||||
if (results.some(r => r.status === 'aborted')) {
|
if (results.some(r => r.status === 'aborted')) {
|
||||||
return;
|
console.info(
|
||||||
|
'Some function call results were aborted (request was likely suspended during processing), discarding all results.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newResults = addEditorFunctionCallResults(
|
||||||
|
selectedAiRequest.id,
|
||||||
|
results
|
||||||
|
);
|
||||||
|
|
||||||
|
// We may have processed everything, so try to send the results
|
||||||
|
// to the backend.
|
||||||
|
await onSendEditorFunctionCallResults(newResults, {
|
||||||
|
createdSceneNames,
|
||||||
|
createdProject,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// Release the lock so these calls can be retried if needed
|
||||||
|
// (e.g. after an error or a suspension).
|
||||||
|
functionCallsToProcess.forEach(functionCall => {
|
||||||
|
inFlightFunctionCallIdsRef.current.delete(
|
||||||
|
`${selectedAiRequest.id}:${functionCall.call_id}`
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newResults = addEditorFunctionCallResults(
|
|
||||||
selectedAiRequest.id,
|
|
||||||
results
|
|
||||||
);
|
|
||||||
|
|
||||||
// We may have processed everything, so try to send the results
|
|
||||||
// to the backend.
|
|
||||||
await onSendEditorFunctionCallResults(newResults, {
|
|
||||||
createdSceneNames,
|
|
||||||
createdProject,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
i18n,
|
i18n,
|
||||||
|
|||||||
@@ -666,7 +666,12 @@ export default class ExpressionField extends React.Component<Props, State> {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{this._inputElement && this.state.popoverOpen && (
|
{this._inputElement && this.state.popoverOpen && (
|
||||||
<ClickAwayListener onClickAway={this._handleRequestClose}>
|
<ClickAwayListener
|
||||||
|
onClickAway={this._handleRequestClose}
|
||||||
|
// Needed since React 18 to avoid what seems to be the immediate closing of the popper.
|
||||||
|
mouseEvent="onMouseDown"
|
||||||
|
touchEvent="onTouchStart"
|
||||||
|
>
|
||||||
<Popper
|
<Popper
|
||||||
style={popoverStyle}
|
style={popoverStyle}
|
||||||
open={this.state.popoverOpen}
|
open={this.state.popoverOpen}
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export default class ExportLauncher extends Component<Props, State> {
|
|||||||
payWithCredits?: boolean,
|
payWithCredits?: boolean,
|
||||||
|}) => Promise<void>;
|
|}) => Promise<void>;
|
||||||
|
|
||||||
componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
// Fetch limits when the export launcher is opened, to ensure we display the
|
// Fetch limits when the export launcher is opened, to ensure we display the
|
||||||
// latest limits.
|
// latest limits.
|
||||||
this.props.authenticatedUser.onRefreshLimits();
|
this.props.authenticatedUser.onRefreshLimits();
|
||||||
|
|||||||
@@ -312,18 +312,13 @@ function InAppTutorialStepDisplayer({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const valuePropertyDescriptor = Object.getOwnPropertyDescriptor(
|
|
||||||
elementWithId.constructor.prototype,
|
|
||||||
'value'
|
|
||||||
);
|
|
||||||
if (!valuePropertyDescriptor) return undefined;
|
|
||||||
const valueSetter = valuePropertyDescriptor.set;
|
|
||||||
if (!valueSetter) return undefined;
|
|
||||||
return () => {
|
return () => {
|
||||||
valueSetter.call(elementWithId, valueEquals);
|
// Focus the element, select all existing content, then use
|
||||||
// Trigger blur to make sure the value is taken into account
|
// execCommand to insert text. This goes through the browser's
|
||||||
// by the React input.
|
// native input path, which React 18 correctly picks up.
|
||||||
elementWithId.dispatchEvent(new Event('blur', { bubbles: true }));
|
elementWithId.focus();
|
||||||
|
elementWithId.select();
|
||||||
|
document.execCommand('insertText', false, valueEquals);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[nextStepTrigger, elementWithId]
|
[nextStepTrigger, elementWithId]
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import 'element-closest';
|
import 'element-closest';
|
||||||
// $FlowFixMe[missing-export]
|
// $FlowFixMe[missing-export]
|
||||||
import React, { Component, type Element } from 'react';
|
import React, { Component, type Element } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
// $FlowFixMe[cannot-resolve-module] - react-dom/client is available in React 18
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
import Authentication from './Utils/GDevelopServices/Authentication';
|
import Authentication from './Utils/GDevelopServices/Authentication';
|
||||||
import {
|
import {
|
||||||
sendProgramOpening,
|
sendProgramOpening,
|
||||||
@@ -170,7 +171,7 @@ class Bootstrapper extends Component<{}, State> {
|
|||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
if (rootElement) {
|
if (rootElement) {
|
||||||
GD_STARTUP_TIMES.push(['reactDOMRenderCall', performance.now()]);
|
GD_STARTUP_TIMES.push(['reactDOMRenderCall', performance.now()]);
|
||||||
ReactDOM.render(<Bootstrapper />, rootElement);
|
createRoot(rootElement).render(<Bootstrapper />);
|
||||||
} else console.error('No root element defined in index.html');
|
} else console.error('No root element defined in index.html');
|
||||||
|
|
||||||
registerServiceWorker();
|
registerServiceWorker();
|
||||||
|
|||||||
Reference in New Issue
Block a user