mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
chore[DevTools]: make clipboardWrite optional for chromium (#32262)
Addresses https://github.com/facebook/react/issues/32244. ### Chromium We will use [chrome.permissions](https://developer.chrome.com/docs/extensions/reference/api/permissions) for checking / requesting `clipboardWrite` permission before copying something to the clipboard. ### Firefox We will keep `clipboardWrite` as a required permission, because there is no reliable and working API for requesting optional permissions for extensions that are extending browser DevTools: - `chrome.permissions` is unavailable for devtools pages - https://bugzilla.mozilla.org/show_bug.cgi?id=1796933 - You can't call `chrome.permissions.request` from background, because this instruction has to be executed inside user-event callback, basically only initiated by user. I don't really want to come up with solutions like opening a new tab with a button that user has to click.
This commit is contained in:
parent
55b54b0d63
commit
221f3002ca
|
|
@ -500,6 +500,7 @@ module.exports = {
|
|||
'packages/react-devtools-shared/src/hook.js',
|
||||
'packages/react-devtools-shared/src/backend/console.js',
|
||||
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
|
||||
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
|
||||
],
|
||||
globals: {
|
||||
__IS_CHROME__: 'readonly',
|
||||
|
|
@ -507,6 +508,7 @@ module.exports = {
|
|||
__IS_EDGE__: 'readonly',
|
||||
__IS_NATIVE__: 'readonly',
|
||||
__IS_INTERNAL_VERSION__: 'readonly',
|
||||
chrome: 'readonly',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@
|
|||
"permissions": [
|
||||
"scripting",
|
||||
"storage",
|
||||
"tabs",
|
||||
"tabs"
|
||||
],
|
||||
"optional_permissions": [
|
||||
"clipboardWrite"
|
||||
],
|
||||
"host_permissions": [
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@
|
|||
"permissions": [
|
||||
"scripting",
|
||||
"storage",
|
||||
"tabs",
|
||||
"tabs"
|
||||
],
|
||||
"optional_permissions": [
|
||||
"clipboardWrite"
|
||||
],
|
||||
"host_permissions": [
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
import type {Node as ReactNode} from 'react';
|
||||
|
||||
export type ContextMenuItem = {
|
||||
onClick: () => void,
|
||||
onClick: () => mixed,
|
||||
content: ReactNode,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
currentBridgeProtocol,
|
||||
} from 'react-devtools-shared/src/bridge';
|
||||
import {StrictMode} from 'react-devtools-shared/src/frontend/types';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
|
||||
import type {
|
||||
Element,
|
||||
|
|
@ -1494,7 +1495,7 @@ export default class Store extends EventEmitter<{
|
|||
};
|
||||
|
||||
onSaveToClipboard: (text: string) => void = text => {
|
||||
copy(text);
|
||||
withPermissionsCheck({permissions: ['clipboardWrite']}, () => copy(text))();
|
||||
};
|
||||
|
||||
onBackendInitialized: () => void = () => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
ElementTypeClass,
|
||||
ElementTypeFunction,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
|
||||
import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||
|
|
@ -41,14 +42,18 @@ export default function InspectedElementContextTree({
|
|||
|
||||
const isReadOnly = type !== ElementTypeClass && type !== ElementTypeFunction;
|
||||
|
||||
const entries = context != null ? Object.entries(context) : null;
|
||||
if (entries !== null) {
|
||||
entries.sort(alphaSortEntries);
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isEmpty = entries === null || entries.length === 0;
|
||||
const entries = Object.entries(context);
|
||||
entries.sort(alphaSortEntries);
|
||||
const isEmpty = entries.length === 0;
|
||||
|
||||
const handleCopy = () => copy(serializeDataForCopy(((context: any): Object)));
|
||||
const handleCopy = withPermissionsCheck(
|
||||
{permissions: ['clipboardWrite']},
|
||||
() => copy(serializeDataForCopy(context)),
|
||||
);
|
||||
|
||||
// We add an object with a "value" key as a wrapper around Context data
|
||||
// so that we can use the shared <KeyValue> component to display it.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {alphaSortEntries, serializeDataForCopy} from '../utils';
|
|||
import Store from '../../store';
|
||||
import styles from './InspectedElementSharedStyles.css';
|
||||
import {ElementTypeClass} from 'react-devtools-shared/src/frontend/types';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
|
||||
import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||
|
|
@ -53,17 +54,19 @@ export default function InspectedElementPropsTree({
|
|||
const canRenamePaths =
|
||||
type === ElementTypeClass || canEditFunctionPropsRenamePaths;
|
||||
|
||||
const entries = props != null ? Object.entries(props) : null;
|
||||
if (entries === null) {
|
||||
// Skip the section for null props.
|
||||
// Skip the section for null props.
|
||||
if (props == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entries = Object.entries(props);
|
||||
entries.sort(alphaSortEntries);
|
||||
|
||||
const isEmpty = entries.length === 0;
|
||||
|
||||
const handleCopy = () => copy(serializeDataForCopy(((props: any): Object)));
|
||||
const handleCopy = withPermissionsCheck(
|
||||
{permissions: ['clipboardWrite']},
|
||||
() => copy(serializeDataForCopy(props)),
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-testname="InspectedElementPropsTree">
|
||||
|
|
@ -76,7 +79,7 @@ export default function InspectedElementPropsTree({
|
|||
)}
|
||||
</div>
|
||||
{!isEmpty &&
|
||||
(entries: any).map(([name, value]) => (
|
||||
entries.map(([name, value]) => (
|
||||
<KeyValue
|
||||
key={name}
|
||||
alphaSort={true}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {toNormalUrl} from 'jsc-safe-url';
|
|||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Skeleton from './Skeleton';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
|
||||
import type {Source as InspectedElementSource} from 'react-devtools-shared/src/shared/types';
|
||||
import styles from './InspectedElementSourcePanel.css';
|
||||
|
|
@ -59,7 +60,10 @@ function CopySourceButton({source, symbolicatedSourcePromise}: Props) {
|
|||
const symbolicatedSource = React.use(symbolicatedSourcePromise);
|
||||
if (symbolicatedSource == null) {
|
||||
const {sourceURL, line, column} = source;
|
||||
const handleCopy = () => copy(`${sourceURL}:${line}:${column}`);
|
||||
const handleCopy = withPermissionsCheck(
|
||||
{permissions: ['clipboardWrite']},
|
||||
() => copy(`${sourceURL}:${line}:${column}`),
|
||||
);
|
||||
|
||||
return (
|
||||
<Button onClick={handleCopy} title="Copy to clipboard">
|
||||
|
|
@ -69,7 +73,10 @@ function CopySourceButton({source, symbolicatedSourcePromise}: Props) {
|
|||
}
|
||||
|
||||
const {sourceURL, line, column} = symbolicatedSource;
|
||||
const handleCopy = () => copy(`${sourceURL}:${line}:${column}`);
|
||||
const handleCopy = withPermissionsCheck(
|
||||
{permissions: ['clipboardWrite']},
|
||||
() => copy(`${sourceURL}:${line}:${column}`),
|
||||
);
|
||||
|
||||
return (
|
||||
<Button onClick={handleCopy} title="Copy to clipboard">
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import KeyValue from './KeyValue';
|
|||
import {alphaSortEntries, serializeDataForCopy} from '../utils';
|
||||
import Store from '../../store';
|
||||
import styles from './InspectedElementSharedStyles.css';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
|
||||
import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||
|
|
@ -35,22 +36,23 @@ export default function InspectedElementStateTree({
|
|||
store,
|
||||
}: Props): React.Node {
|
||||
const {state, type} = inspectedElement;
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// HostSingleton and HostHoistable may have state that we don't want to expose to users
|
||||
const isHostComponent = type === ElementTypeHostComponent;
|
||||
|
||||
const entries = state != null ? Object.entries(state) : null;
|
||||
const isEmpty = entries === null || entries.length === 0;
|
||||
|
||||
const entries = Object.entries(state);
|
||||
const isEmpty = entries.length === 0;
|
||||
if (isEmpty || isHostComponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (entries !== null) {
|
||||
entries.sort(alphaSortEntries);
|
||||
}
|
||||
|
||||
const handleCopy = () => copy(serializeDataForCopy(((state: any): Object)));
|
||||
entries.sort(alphaSortEntries);
|
||||
const handleCopy = withPermissionsCheck(
|
||||
{permissions: ['clipboardWrite']},
|
||||
() => copy(serializeDataForCopy(state)),
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {serializeDataForCopy} from '../../utils';
|
|||
import AutoSizeInput from './AutoSizeInput';
|
||||
import styles from './StyleEditor.css';
|
||||
import {sanitizeForParse} from '../../../utils';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
|
||||
import type {Style} from './types';
|
||||
|
||||
|
|
@ -62,7 +63,10 @@ export default function StyleEditor({id, style}: Props): React.Node {
|
|||
|
||||
const keys = useMemo(() => Array.from(Object.keys(style)), [style]);
|
||||
|
||||
const handleCopy = () => copy(serializeDataForCopy(style));
|
||||
const handleCopy = withPermissionsCheck(
|
||||
{permissions: ['clipboardWrite']},
|
||||
() => copy(serializeDataForCopy(style)),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.StyleEditor}>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from 'react-devtools-timeline/src/utils/formatting';
|
||||
import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
|
||||
import {copy} from 'clipboard-js';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
|
||||
import styles from './SidebarEventInfo.css';
|
||||
|
||||
|
|
@ -53,7 +54,10 @@ function SchedulingEventInfo({eventInfo}: SchedulingEventProps) {
|
|||
<div className={styles.Row}>
|
||||
<label className={styles.Label}>Rendered by</label>
|
||||
<Button
|
||||
onClick={() => copy(componentStack)}
|
||||
onClick={withPermissionsCheck(
|
||||
{permissions: ['clipboardWrite']},
|
||||
() => copy(componentStack),
|
||||
)}
|
||||
title="Copy component stack to clipboard">
|
||||
<ButtonIcon type="copy" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import Button from './Button';
|
|||
import ButtonIcon from './ButtonIcon';
|
||||
import {copy} from 'clipboard-js';
|
||||
import styles from './UnsupportedBridgeProtocolDialog.css';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
|
||||
import type {BridgeProtocol} from 'react-devtools-shared/src/bridge';
|
||||
|
||||
|
|
@ -82,7 +83,10 @@ function DialogContent({
|
|||
<pre className={styles.NpmCommand}>
|
||||
{upgradeInstructions}
|
||||
<Button
|
||||
onClick={() => copy(upgradeInstructions)}
|
||||
onClick={withPermissionsCheck(
|
||||
{permissions: ['clipboardWrite']},
|
||||
() => copy(upgradeInstructions),
|
||||
)}
|
||||
title="Copy upgrade command to clipboard">
|
||||
<ButtonIcon type="copy" />
|
||||
</Button>
|
||||
|
|
@ -99,7 +103,10 @@ function DialogContent({
|
|||
<pre className={styles.NpmCommand}>
|
||||
{downgradeInstructions}
|
||||
<Button
|
||||
onClick={() => copy(downgradeInstructions)}
|
||||
onClick={withPermissionsCheck(
|
||||
{permissions: ['clipboardWrite']},
|
||||
() => copy(downgradeInstructions),
|
||||
)}
|
||||
title="Copy downgrade command to clipboard">
|
||||
<ButtonIcon type="copy" />
|
||||
</Button>
|
||||
|
|
|
|||
21
packages/react-devtools-shared/src/errors/PermissionNotGrantedError.js
vendored
Normal file
21
packages/react-devtools-shared/src/errors/PermissionNotGrantedError.js
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export class PermissionNotGrantedError extends Error {
|
||||
constructor() {
|
||||
super("User didn't grant the required permission to perform an action");
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, PermissionNotGrantedError);
|
||||
}
|
||||
|
||||
this.name = 'PermissionNotGrantedError';
|
||||
}
|
||||
}
|
||||
35
packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js
vendored
Normal file
35
packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {PermissionNotGrantedError} from 'react-devtools-shared/src/errors/PermissionNotGrantedError';
|
||||
|
||||
type SupportedPermission = 'clipboardWrite';
|
||||
type Permissions = Array<SupportedPermission>;
|
||||
type PermissionsOptions = {permissions: Permissions};
|
||||
|
||||
// browser.permissions is not available for DevTools pages in Firefox
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1796933
|
||||
// We are going to assume that requested permissions are not optional.
|
||||
export function withPermissionsCheck<T: (...$ReadOnlyArray<empty>) => mixed>(
|
||||
options: PermissionsOptions,
|
||||
callback: T,
|
||||
): T | (() => Promise<ReturnType<T>>) {
|
||||
if (!__IS_CHROME__ && !__IS_EDGE__) {
|
||||
return callback;
|
||||
} else {
|
||||
return async () => {
|
||||
const granted = await chrome.permissions.request(options);
|
||||
if (granted) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
return Promise.reject(new PermissionNotGrantedError());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import {copy} from 'clipboard-js';
|
|||
import prettyMilliseconds from 'pretty-ms';
|
||||
|
||||
import ContextMenuContainer from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenuContainer';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
|
||||
import {getBatchRange} from './utils/getBatchRange';
|
||||
import {moveStateToRange} from './view-base/utils/scrollState';
|
||||
|
|
@ -138,7 +139,9 @@ export default function CanvasPageContextMenu({
|
|||
content: 'Zoom to batch',
|
||||
},
|
||||
{
|
||||
onClick: () => copySummary(timelineData, measure),
|
||||
onClick: withPermissionsCheck({permissions: ['clipboardWrite']}, () =>
|
||||
copySummary(timelineData, measure),
|
||||
),
|
||||
content: 'Copy summary',
|
||||
},
|
||||
);
|
||||
|
|
@ -147,16 +150,19 @@ export default function CanvasPageContextMenu({
|
|||
if (flamechartStackFrame != null) {
|
||||
items.push(
|
||||
{
|
||||
onClick: () => copy(flamechartStackFrame.scriptUrl),
|
||||
onClick: withPermissionsCheck({permissions: ['clipboardWrite']}, () =>
|
||||
copy(flamechartStackFrame.scriptUrl),
|
||||
),
|
||||
content: 'Copy file path',
|
||||
},
|
||||
{
|
||||
onClick: () =>
|
||||
onClick: withPermissionsCheck({permissions: ['clipboardWrite']}, () =>
|
||||
copy(
|
||||
`line ${flamechartStackFrame.locationLine ?? ''}, column ${
|
||||
flamechartStackFrame.locationColumn ?? ''
|
||||
}`,
|
||||
),
|
||||
),
|
||||
content: 'Copy location',
|
||||
},
|
||||
);
|
||||
|
|
|
|||
2
scripts/flow/react-devtools.js
vendored
2
scripts/flow/react-devtools.js
vendored
|
|
@ -16,3 +16,5 @@ declare const __IS_FIREFOX__: boolean;
|
|||
declare const __IS_CHROME__: boolean;
|
||||
declare const __IS_EDGE__: boolean;
|
||||
declare const __IS_NATIVE__: boolean;
|
||||
|
||||
declare const chrome: any;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user