From 585a989550deab84b1617e134cc0d8de2511b0d0 Mon Sep 17 00:00:00 2001 From: "Aaron (Qilong)" <173288704@qq.com> Date: Tue, 31 Mar 2026 15:45:09 -0400 Subject: [PATCH 1/6] Tech Debt clean up --- .eslintrc.json | 9 +- .github/workflows/npm-publish.yml | 4 +- package.json | 23 +- src/App.js | 170 +++++----- src/Dynamic.css | 21 +- src/Dynamic.js | 76 ++--- src/Static.js | 500 ++++++++++++++---------------- src/Toast.js | 56 ++-- tests/App.test.js | 37 ++- tests/Dynamic.test.js | 36 +++ tests/Static.test.js | 67 ++++ tests/e2e.test.js | 8 +- 12 files changed, 530 insertions(+), 477 deletions(-) create mode 100644 tests/Dynamic.test.js create mode 100644 tests/Static.test.js diff --git a/.eslintrc.json b/.eslintrc.json index b2093c6..65b3f79 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,8 +3,7 @@ "node": true, "browser": true, "es2021": true, - "jest": true, - "webextensions": true + "jest": true }, "extends": [ "eslint:recommended", @@ -25,11 +24,15 @@ "version": "detect" } }, + "globals": { + "chrome": "readonly" + }, "ignorePatterns": ["dist"], "rules": { "indent": ["error", 2], "quotes": ["error", "single"], "semi": ["error", "always"], - "jsx-quotes": ["error", "prefer-single"] + "jsx-quotes": ["error", "prefer-single"], + "no-console": "warn" } } diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index e25077f..85aa06f 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -19,7 +19,7 @@ jobs: with: node-version: 22 - name: Install dependencies - run: npm ci --force + run: npm ci - name: Build for development run: npm run build --if-present - name: Run Test @@ -36,7 +36,7 @@ jobs: node-version: 22 registry-url: https://registry.npmjs.org/ - name: Install dependencies - run: npm ci --force + run: npm ci - name: Build for production run: npm run production --if-present - name: Run Test diff --git a/package.json b/package.json index 205e4de..57c4943 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "main": "index.bundle.js", "dependencies": { "bootstrap": "^5.2.1", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-bootstrap": "^2.5.0", - "react-dom": "^18.2.0", - "react-scripts": "^5.0.1" + "react-dom": "^18.2.0" }, "scripts": { "lint:check": "eslint src/ tests/", @@ -33,15 +33,7 @@ "lic_direct": "npx @adsk/adsk-npm-license-puller --path . --app-name 'splash-screen' --verbose --about-box ./license_output/about-box_direct.html --about-box-type desktop --year $(date \"+%Y\") --paos ./license_output/paos_direct.csv", "lic_transitive": "npx @adsk/adsk-npm-license-puller --path . --app-name 'splash-screen' --verbose --about-box ./license_output/about-box_transitive.html --about-box-type desktop --transitive --year $(date \"+%Y\") --paos ./license_output/paos_transitive.csv", "generate_license": "npm run lic_direct && npm run lic_transitive", - "test:unit": "NODE_ENV=test jest tests/App.test.js tests/Toast.test.js", - "test:e2e": "playwright test tests/e2e.test.js", - "eject": "react-scripts eject" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] + "test:unit": "NODE_ENV=test jest tests/App.test.js tests/Toast.test.js tests/Dynamic.test.js tests/Static.test.js" }, "browserslist": { "production": [ @@ -56,9 +48,16 @@ ] }, "devDependencies": { + "@babel/core": "^7.23.0", + "@babel/preset-env": "^7.23.0", + "@babel/preset-react": "^7.22.0", "@playwright/test": "^1.49.0", "@testing-library/react": "^13.4.0", - "html-webpack-plugin": "^4.5.2", + "babel-loader": "^9.1.3", + "css-loader": "^6.8.1", + "html-webpack-plugin": "^5.6.0", + "style-loader": "^3.3.3", + "terser-webpack-plugin": "^5.3.10", "webpack": "^5.74.0", "webpack-cli": "^4.10.0" }, diff --git a/src/App.js b/src/App.js index d712fd8..c265013 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import Container from 'react-bootstrap/Container'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; @@ -6,116 +6,86 @@ import Col from 'react-bootstrap/Col'; import Dynamic from './Dynamic'; import Static from './Static'; import Toast from './Toast'; -import { base64DynamoLogo, base64DynamoBackground } from './encodedImages'; +import { base64DynamoLogo, base64DynamoBackground as importedBackground } from './encodedImages'; import './App.css'; import 'bootstrap/dist/css/bootstrap.min.css'; -class App extends React.Component { - constructor() { - super(); - this.setBackgroundImage(); - const noWebView = typeof chrome === 'undefined' || typeof chrome.webview === 'undefined'; - this.isDebugMode = noWebView && new URLSearchParams(window.location.search).has('debug'); - this.state = { - isChecked: false, - welcomeToDynamoTitle: 'Welcome to Dynamo!', - loadingDone: false, - signInStatus: false - }; - - //This is a reference to the DOM of the project that will be called in Dynamo to set the title of the splash screen (Defined by 'Welcome to Dynamo!' by default) - window.setLabels = this.setLabels.bind(this); - window.setLoadingDone = this.setLoadingDone.bind(this); - window.setSignInStatus = this.setSignInStatus.bind(this); - this.handleCheckedChange = this.handleCheckedChange.bind(this); - } +// '#base64BackgroundImage' is a build-time placeholder replaced by Dynamo's build system. +// If the placeholder was replaced (no longer contains '#'), use that value; otherwise +// fall back to the image imported from encodedImages. +const buildTimePlaceholder = '#base64BackgroundImage'; +const backgroundImage = buildTimePlaceholder.includes('#') ? importedBackground : buildTimePlaceholder; - handleCheckedChange = (checked) => { - this.setState({isChecked: checked}); - }; +const isDebugMode = (() => { + const noWebView = typeof chrome === 'undefined' || typeof chrome.webview === 'undefined'; + return noWebView && new URLSearchParams(window.location.search).has('debug'); +})(); - setBackgroundImage() { - let backgroundImage = '#base64BackgroundImage'; - if (!backgroundImage.includes('#')) - // eslint-disable-next-line no-import-assign - base64DynamoBackground = backgroundImage; - } +function App() { + const [welcomeToDynamoTitle, setWelcomeToDynamoTitle] = useState('Welcome to Dynamo!'); + const [loadingDone, setLoadingDone] = useState(false); + const [signInStatus, setSignInStatus] = useState(false); + const [labels, setLabels] = useState(undefined); - componentDidMount() { - document.addEventListener('keydown', this.handleKeyDown); + useEffect(() => { + window.setLabels = (newLabels) => { + setWelcomeToDynamoTitle(newLabels.welcomeToDynamoTitle); + setLabels(newLabels); + }; + window.setLoadingDone = () => setLoadingDone(true); + window.setSignInStatus = (val) => setSignInStatus(val.signInStatus === 'True'); - // Debug mode: auto-show Static when running outside Dynamo (no WebView2) - if (this.isDebugMode) { - console.log('[SplashScreen] Debug mode: no WebView2 detected, auto-loading Static'); - this.setLoadingDone(); + if (isDebugMode) { + setLoadingDone(true); } - } - - render() { - return ( - - - - - - -
- -
-
- -
- {this.state.welcomeToDynamoTitle} -
-
- -
- - - { - this.state.loadingDone ? - : - } - - - - - {this.state.loadingDone && } - - -
- -
- ); - } - - //This method sets the labels of the splash screen as an option of localization - setLabels(labels) { - this.setState({ - welcomeToDynamoTitle: labels.welcomeToDynamoTitle, - labels: labels - }); - } - - //Set the login status from Dynamo - setSignInStatus(val) { - this.setState({ - signInStatus: val.signInStatus === 'True' - }); - } + return () => { + delete window.setLabels; + delete window.setLoadingDone; + delete window.setSignInStatus; + }; + }, []); - //This method is called when the loading is done from Dynamo side - setLoadingDone = async () => { - this.setState({ - loadingDone: true - }); - }; + return ( + + + + + + +
+ +
+
+ +
+ {welcomeToDynamoTitle} +
+
+ +
+ + + {loadingDone + ? {}} + /> + : + } + + + + + {loadingDone && } + + +
+ +
+ ); } export default App; diff --git a/src/Dynamic.css b/src/Dynamic.css index fb29dc0..7954a16 100644 --- a/src/Dynamic.css +++ b/src/Dynamic.css @@ -25,23 +25,6 @@ background-image: linear-gradient(to right, rgba(255, 255, 255, 0.1), #38ABDF, #38ABDF); } -.progress-bar-indicator { - height: 100%; - border-radius: 25px; - -webkit-mask: linear-gradient(#fff 0 0); - mask: linear-gradient(#fff 0 0); -} - -.progress-bar-indicator::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-image: linear-gradient(to right, rgba(255, 255, 255, 0.1), #38ABDF, #38ABDF); -} - .dynamicOptions { margin-top: 50px; } @@ -50,7 +33,7 @@ font-size: 9px; } -.loadingDescription{ +.loadingDescription{ height: 3.6em; line-height: 1.8em; -} \ No newline at end of file +} diff --git a/src/Dynamic.js b/src/Dynamic.js index bf38705..2d95cf7 100644 --- a/src/Dynamic.js +++ b/src/Dynamic.js @@ -1,53 +1,43 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import './Dynamic.css'; -class Dynamic extends React.Component { - constructor() { - super(); - this.state = { - barSize: '0%', - dynamoVersion: '', - loadDescription: '', - loadingTime: '', - }; +function Dynamic() { + const [barSize, setBarSize] = useState('0%'); + const [dynamoVersion, setDynamoVersion] = useState(''); + const [loadDescription, setLoadDescription] = useState(''); + const [loadingTime, setLoadingTime] = useState(''); - //This is a reference to the DOM of the project that will be called in Dynamo to fill the loading properties - window.setBarProperties = this.setBarProperties.bind(this); - } + useEffect(() => { + window.setBarProperties = (version, description, size, time) => { + setDynamoVersion(version); + setLoadDescription(description); + setBarSize(size); + setLoadingTime(time); + }; + return () => { delete window.setBarProperties; }; + }, []); - render() { - return ( -
-
- Dynamo Core {this.state.dynamoVersion} -
-
-
-
-
-
-
- {this.state.loadDescription} -
-
-
-
- {this.state.loadingTime} + return ( +
+
+ Dynamo Core {dynamoVersion} +
+
+
+
- ); - } - - setBarProperties(dynamoVersion, loadDescription, barSize, loadingTime) { - this.setState({ - barSize: barSize, - dynamoVersion: dynamoVersion, - loadDescription: loadDescription, - loadingTime: loadingTime - }); - document.getElementsByClassName('progress-bar-indicator')[0].style.display = 'flex'; - } +
+ {loadDescription} +
+
+
+
+ {loadingTime} +
+
+ ); } export default Dynamic; diff --git a/src/Static.js b/src/Static.js index 9d239a8..c5630ce 100644 --- a/src/Static.js +++ b/src/Static.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import Container from 'react-bootstrap/Container'; import Row from 'react-bootstrap/Row'; @@ -15,290 +15,250 @@ const importStatusEnum = { success: 3, }; -let checked = false; - -class Static extends React.Component { - constructor(props) { - super(props); - - this.state = { - importStatus: importStatusEnum.none, - errorDescription: 'Something went wrong when importing your custom setting file. Please try again or proceed with default settings.', - signInTitle: this.props.signInStatus ? this.props.labels.signOutTitle : this.props.labels.signInTitle, - signInTooltip: this.props.signInStatus ? this.props.labels.signOutTooltip : this.props.labels.signInTooltip, - signInStatus: this.props.signInStatus, - loadingTime: 'Total loading time: ', - importSettingsTitle: this.props.labels.importSettingsTitle, - showRestartMessage: false +function Static({ signInStatus: initialSignInStatus, labels, onCheckedChange }) { + const [importStatus, setImportStatus] = useState(importStatusEnum.none); + const [errorDescription, setErrorDescription] = useState( + 'Something went wrong when importing your custom setting file. Please try again or proceed with default settings.' + ); + const [signInTitle, setSignInTitle] = useState( + initialSignInStatus ? labels.signOutTitle : labels.signInTitle + ); + const [signInTooltip, setSignInTooltip] = useState( + initialSignInStatus ? labels.signOutTooltip : labels.signInTooltip + ); + const [signInStatus, setSignInStatus] = useState(initialSignInStatus); + const [signInDisabled, setSignInDisabled] = useState(false); + const [loadingTime, setLoadingTime] = useState('Total loading time: '); + const [showRestartMessage, setShowRestartMessage] = useState(false); + + // checkedRef holds the checkbox value passed to LaunchDynamo — a ref avoids + // stale closure issues in the launchDynamo handler. + const checkedRef = useRef(false); + // labelsRef gives window methods stable access to the latest labels prop + // without needing to re-register those methods when labels changes. + const labelsRef = useRef(labels); + labelsRef.current = labels; + + const importSettingsLabelRef = useRef(null); + const importSettingsInputRef = useRef(null); + + useEffect(() => { + window.setImportStatus = (status) => { + setImportStatus(status.status); + setErrorDescription(status.errorDescription || ''); + if (status.status === importStatusEnum.success) { + setShowRestartMessage(true); + if (window.showToast) { + const msg = labelsRef.current?.restartMessage ?? Static.defaultProps.labels.restartMessage; + window.showToast(msg, 'success'); + } + } else if (status.status === importStatusEnum.none) { + setShowRestartMessage(false); + window.hideToast?.(); + } + }; + window.setTotalLoadingTime = (time) => setLoadingTime(time); + window.setEnableSignInButton = ({ enable }) => setSignInDisabled(enable !== 'True'); + window.handleSignInStateChange = (auth) => { + const isSignedIn = auth.status === 'True'; + setSignInStatus(isSignedIn); + setSignInTitle(isSignedIn ? labelsRef.current.signOutTitle : labelsRef.current.signInTitle); + setSignInTooltip(isSignedIn ? labelsRef.current.signOutTooltip : labelsRef.current.signInTooltip); + }; + window.showRestartMessage = () => { + setShowRestartMessage(true); + if (window.showToast) { + const msg = labelsRef.current?.restartMessage ?? Static.defaultProps.labels.restartMessage; + window.showToast(msg, 'success'); + } + }; + window.hideRestartMessage = () => { + setShowRestartMessage(false); + window.hideToast?.(); }; - window.setImportStatus = this.setImportStatus.bind(this); - window.setTotalLoadingTime = this.setTotalLoadingTime.bind(this); - window.setEnableSignInButton = this.setEnableSignInButton.bind(this); - window.handleSignInStateChange = this.handleSignInStateChange.bind(this); - window.showRestartMessage = this.showRestartMessage.bind(this); - window.hideRestartMessage = this.hideRestartMessage.bind(this); - this.handleChange = this.handleChange.bind(this); - } - - componentDidMount() { - document.addEventListener('keydown', this.handleKeyDown); - } - - //Every time the checkbox is clicked, this method is called - handleChange() { - checked = !checked; - this.props.onCheckedChange(checked); - } - - render() { - return ( - - - - - - - {this.state.signInTooltip} - }> - - - - - - - {this.state.showRestartMessage && ( - - )} - - - - - - - -
- {this.state.loadingTime} -
-
-
- ); - } - - //Opens a page to signin - signIn = async () => { - if (typeof chrome !== 'undefined' && chrome.webview !== undefined) { - if (this.state.signInStatus) { - let status = await chrome.webview.hostObjects.scriptObject.SignOut(); - this.setState({ - signInStatus: !status, - signInTitle: this.props.labels.signInTitle, - signInTooltip: this.props.labels.signInTooltip - }); + return () => { + delete window.setImportStatus; + delete window.setTotalLoadingTime; + delete window.setEnableSignInButton; + delete window.handleSignInStateChange; + delete window.showRestartMessage; + delete window.hideRestartMessage; + }; + }, []); + + useEffect(() => { + // The import settings control is a