diff --git a/packages/react-devtools-core/webpack.standalone.js b/packages/react-devtools-core/webpack.standalone.js index 9d7c1fc34a685..234d638353f43 100644 --- a/packages/react-devtools-core/webpack.standalone.js +++ b/packages/react-devtools-core/webpack.standalone.js @@ -30,6 +30,7 @@ const __DEV__ = NODE_ENV === 'development'; const DEVTOOLS_VERSION = getVersionString(); +const EDITOR_URL = process.env.EDITOR_URL || null; const LOGGING_URL = process.env.LOGGING_URL || null; const featureFlagTarget = @@ -83,6 +84,7 @@ module.exports = { __TEST__: NODE_ENV === 'test', 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, + 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 'process.env.LOGGING_URL': `"${LOGGING_URL}"`, 'process.env.NODE_ENV': `"${NODE_ENV}"`, diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index 99b1d18d3a999..ac7c51f3bdc5e 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -32,6 +32,7 @@ const __DEV__ = NODE_ENV === 'development'; const DEVTOOLS_VERSION = getVersionString(process.env.DEVTOOLS_VERSION); +const EDITOR_URL = process.env.EDITOR_URL || null; const LOGGING_URL = process.env.LOGGING_URL || null; const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss'; @@ -92,6 +93,7 @@ module.exports = { __TEST__: NODE_ENV === 'test', 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, + 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 'process.env.LOGGING_URL': `"${LOGGING_URL}"`, 'process.env.NODE_ENV': `"${NODE_ENV}"`, diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index bc38a8792e4c4..e24eb9091328d 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -20,6 +20,8 @@ if (!NODE_ENV) { const __DEV__ = NODE_ENV === 'development'; +const EDITOR_URL = process.env.EDITOR_URL || null; + const DEVTOOLS_VERSION = getVersionString(); const babelOptions = { @@ -76,6 +78,7 @@ module.exports = { __TEST__: NODE_ENV === 'test', 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-inline"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, + 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 'process.env.NODE_ENV': `"${NODE_ENV}"`, 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`, diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 3c3aaae2fc461..798bdb9d900e3 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -32,6 +32,9 @@ export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY = export const SESSION_STORAGE_LAST_SELECTION_KEY = 'React::DevTools::lastSelection'; +export const LOCAL_STORAGE_OPEN_IN_EDITOR_URL = + 'React::DevTools::openInEditorUrl'; + export const LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY = 'React::DevTools::parseHookNames'; diff --git a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js index 8239a765f90b6..b384018671d92 100644 --- a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js +++ b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js @@ -19,6 +19,7 @@ export type IconType = | 'copy' | 'delete' | 'down' + | 'editor' | 'expanded' | 'export' | 'filter' @@ -72,6 +73,9 @@ export default function ButtonIcon({className = '', type}: Props) { case 'down': pathData = PATH_DOWN; break; + case 'editor': + pathData = PATH_EDITOR; + break; case 'expanded': pathData = PATH_EXPANDED; break; @@ -268,3 +272,7 @@ const PATH_VIEW_DOM = ` const PATH_VIEW_SOURCE = ` M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z `; + +const PATH_EDITOR = ` + M7 5h10v2h2V3c0-1.1-.9-1.99-2-1.99L7 1c-1.1 0-2 .9-2 2v4h2V5zm8.41 11.59L20 12l-4.59-4.59L14 8.83 17.17 12 14 15.17l1.41 1.42zM10 15.17L6.83 12 10 8.83 8.59 7.41 4 12l4.59 4.59L10 15.17zM17 19H7v-2H5v4c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2v-4h-2v2z +`; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js index f63da7b54a67d..9ebd30a6ec0b3 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js @@ -8,7 +8,7 @@ */ import * as React from 'react'; -import {useCallback, useContext} from 'react'; +import {useCallback, useContext, useSyncExternalStore} from 'react'; import {TreeDispatcherContext, TreeStateContext} from './TreeContext'; import {BridgeContext, StoreContext, OptionsContext} from '../context'; import Button from '../Button'; @@ -20,6 +20,8 @@ import {ElementTypeSuspense} from 'react-devtools-shared/src/types'; import CannotSuspendWarningMessage from './CannotSuspendWarningMessage'; import InspectedElementView from './InspectedElementView'; import {InspectedElementContext} from './InspectedElementContext'; +import {getOpenInEditorURL} from '../../../utils'; +import {LOCAL_STORAGE_OPEN_IN_EDITOR_URL} from '../../../constants'; import styles from './InspectedElement.css'; @@ -123,6 +125,21 @@ export default function InspectedElementWrapper(_: Props) { inspectedElement != null && inspectedElement.canToggleSuspense; + const editorURL = useSyncExternalStore( + function subscribe(callback) { + window.addEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback); + return function unsubscribe() { + window.removeEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback); + }; + }, + function getState() { + return getOpenInEditorURL(); + }, + ); + + const canOpenInEditor = + editorURL && inspectedElement != null && inspectedElement.source != null; + const toggleErrored = useCallback(() => { if (inspectedElement == null || targetErrorBoundaryID == null) { return; @@ -198,6 +215,18 @@ export default function InspectedElementWrapper(_: Props) { } }, [bridge, dispatch, element, isSuspended, modalDialogDispatch, store]); + const onOpenInEditor = useCallback(() => { + const source = inspectedElement?.source; + if (source == null || editorURL == null) { + return; + } + + const url = new URL(editorURL); + url.href = url.href.replace('{path}', source.fileName); + url.href = url.href.replace('{line}', String(source.lineNumber)); + window.open(url); + }, [inspectedElement, editorURL]); + if (element === null) { return (
@@ -223,7 +252,14 @@ export default function InspectedElementWrapper(_: Props) { {element.displayName}
- + {canOpenInEditor && ( + + )} {canToggleError && ( ( + LOCAL_STORAGE_OPEN_IN_EDITOR_URL, + getDefaultOpenInEditorURL(), + ); + const [componentFilters, setComponentFilters] = useState< Array, >(() => [...store.componentFilters]); @@ -271,6 +278,19 @@ export default function ComponentsSettings(_: {||}) { (may be slow) + +
Hide components where...
diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css index 3ddaf13907702..9c0ee028c9968 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css @@ -14,6 +14,10 @@ margin-bottom: 0; } +.OpenInURLSetting { + margin: 0.5rem 0; +} + .OptionGroup { display: inline-flex; flex-direction: row; @@ -30,6 +34,10 @@ margin-right: 0.5rem; } +.Spacer { + height: 0.5rem; +} + .Select { } diff --git a/packages/react-devtools-shared/src/devtools/views/hooks.js b/packages/react-devtools-shared/src/devtools/views/hooks.js index 76dad56774540..1a34373fc7e19 100644 --- a/packages/react-devtools-shared/src/devtools/views/hooks.js +++ b/packages/react-devtools-shared/src/devtools/views/hooks.js @@ -170,6 +170,9 @@ export function useLocalStorage( value instanceof Function ? (value: any)(storedValue) : value; setStoredValue(valueToStore); localStorageSetItem(key, JSON.stringify(valueToStore)); + + // Notify listeners that this setting has changed. + window.dispatchEvent(new Event(key)); } catch (error) { console.log(error); } diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 84c977f19d4e7..0c50374ae30b6 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -34,6 +34,7 @@ import { import {ElementTypeRoot} from 'react-devtools-shared/src/types'; import { LOCAL_STORAGE_FILTER_PREFERENCES_KEY, + LOCAL_STORAGE_OPEN_IN_EDITOR_URL, LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS, LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY, LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY, @@ -386,6 +387,22 @@ export function setShowInlineWarningsAndErrors(value: boolean): void { ); } +export function getDefaultOpenInEditorURL(): string { + return typeof process.env.EDITOR_URL === 'string' + ? process.env.EDITOR_URL + : ''; +} + +export function getOpenInEditorURL(): string { + try { + const raw = localStorageGetItem(LOCAL_STORAGE_OPEN_IN_EDITOR_URL); + if (raw != null) { + return JSON.parse(raw); + } + } catch (error) {} + return getDefaultOpenInEditorURL(); +} + export function separateDisplayNameAndHOCs( displayName: string | null, type: ElementType, diff --git a/packages/react-devtools-shell/webpack.config.js b/packages/react-devtools-shell/webpack.config.js index 19f53f5d08b09..18c55d1d8b740 100644 --- a/packages/react-devtools-shell/webpack.config.js +++ b/packages/react-devtools-shell/webpack.config.js @@ -24,6 +24,8 @@ if (!TARGET) { process.exit(1); } +const EDITOR_URL = process.env.EDITOR_URL || null; + const builtModulesDir = resolve( __dirname, '..', @@ -69,6 +71,7 @@ const config = { __PROFILE__: false, __TEST__: NODE_ENV === 'test', 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, + 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-shell"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,