From 300331419eaaab62139a08113334b0345a2d28f7 Mon Sep 17 00:00:00 2001 From: Andrew Cherniavskii Date: Fri, 15 Mar 2024 22:49:44 +0100 Subject: [PATCH] [DataGridPremium] Add support for confirmation before clipboard paste (#12225) --- .../clipboard/ClipboardPasteEvents.js | 71 ++++++++++++++++++ .../clipboard/ClipboardPasteEvents.tsx | 72 +++++++++++++++++++ .../ClipboardPasteEvents.tsx.preview | 10 --- docs/data/data-grid/clipboard/clipboard.md | 17 ++++- .../x/api/data-grid/data-grid-premium.json | 4 ++ .../data-grid-premium/data-grid-premium.json | 4 ++ .../src/DataGridPremium/DataGridPremium.tsx | 8 +++ .../clipboard/useGridClipboardImport.ts | 20 +++++- .../src/models/dataGridPremiumProps.ts | 8 +++ 9 files changed, 201 insertions(+), 13 deletions(-) delete mode 100644 docs/data/data-grid/clipboard/ClipboardPasteEvents.tsx.preview diff --git a/docs/data/data-grid/clipboard/ClipboardPasteEvents.js b/docs/data/data-grid/clipboard/ClipboardPasteEvents.js index 299a2ea33c05a..a755c662bc739 100644 --- a/docs/data/data-grid/clipboard/ClipboardPasteEvents.js +++ b/docs/data/data-grid/clipboard/ClipboardPasteEvents.js @@ -1,6 +1,12 @@ import * as React from 'react'; import { DataGridPremium } from '@mui/x-data-grid-premium'; import { useDemoData } from '@mui/x-data-grid-generator'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import Button from '@mui/material/Button'; export default function ClipboardPasteEvents() { const { data } = useDemoData({ @@ -28,6 +34,19 @@ export default function ClipboardPasteEvents() { }, }; + const confirm = useConfirm(); + const confirmPaste = React.useCallback(() => { + return new Promise((resolve, reject) => { + confirm.open((confirmed) => { + if (confirmed) { + resolve(); + } else { + reject(); + } + }); + }); + }, [confirm]); + return (
setLoading(true)} onClipboardPasteEnd={() => setLoading(false)} ignoreValueFormatterDuringExport + disableRowSelectionOnClick /> + + + {'Are you sure you want to paste?'} + + + + This will overwrite the selected cells. + + + + + + +
); } + +const useConfirm = () => { + const [isOpen, setIsOpen] = React.useState(false); + const callbackRef = React.useRef(null); + + const open = React.useCallback((callback) => { + setIsOpen(true); + callbackRef.current = callback; + }, []); + + const cancel = React.useCallback(() => { + setIsOpen(false); + callbackRef.current?.(false); + callbackRef.current = null; + }, []); + + const confirm = React.useCallback(() => { + setIsOpen(false); + callbackRef.current?.(true); + callbackRef.current = null; + }, []); + + return { + open, + isOpen, + cancel, + confirm, + }; +}; diff --git a/docs/data/data-grid/clipboard/ClipboardPasteEvents.tsx b/docs/data/data-grid/clipboard/ClipboardPasteEvents.tsx index 0139a1fc04293..3c3b16913c61c 100644 --- a/docs/data/data-grid/clipboard/ClipboardPasteEvents.tsx +++ b/docs/data/data-grid/clipboard/ClipboardPasteEvents.tsx @@ -1,6 +1,12 @@ import * as React from 'react'; import { DataGridPremium, DataGridPremiumProps } from '@mui/x-data-grid-premium'; import { useDemoData } from '@mui/x-data-grid-generator'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import Button from '@mui/material/Button'; export default function ClipboardPasteEvents() { const { data } = useDemoData({ @@ -30,6 +36,19 @@ export default function ClipboardPasteEvents() { }, }; + const confirm = useConfirm(); + const confirmPaste = React.useCallback<() => Promise>(() => { + return new Promise((resolve, reject) => { + confirm.open((confirmed) => { + if (confirmed) { + resolve(); + } else { + reject(); + } + }); + }); + }, [confirm]); + return (
setLoading(true)} onClipboardPasteEnd={() => setLoading(false)} ignoreValueFormatterDuringExport + disableRowSelectionOnClick /> + + + + {'Are you sure you want to paste?'} + + + + This will overwrite the selected cells. + + + + + + +
); } + +const useConfirm = () => { + const [isOpen, setIsOpen] = React.useState(false); + const callbackRef = React.useRef<((confirmed: boolean) => void) | null>(null); + + const open = React.useCallback((callback: (confirmed: boolean) => void) => { + setIsOpen(true); + callbackRef.current = callback; + }, []); + + const cancel = React.useCallback(() => { + setIsOpen(false); + callbackRef.current?.(false); + callbackRef.current = null; + }, []); + + const confirm = React.useCallback(() => { + setIsOpen(false); + callbackRef.current?.(true); + callbackRef.current = null; + }, []); + + return { + open, + isOpen, + cancel, + confirm, + }; +}; diff --git a/docs/data/data-grid/clipboard/ClipboardPasteEvents.tsx.preview b/docs/data/data-grid/clipboard/ClipboardPasteEvents.tsx.preview deleted file mode 100644 index 0c6f364b0155b..0000000000000 --- a/docs/data/data-grid/clipboard/ClipboardPasteEvents.tsx.preview +++ /dev/null @@ -1,10 +0,0 @@ - setLoading(true)} - onClipboardPasteEnd={() => setLoading(false)} - ignoreValueFormatterDuringExport -/> \ No newline at end of file diff --git a/docs/data/data-grid/clipboard/clipboard.md b/docs/data/data-grid/clipboard/clipboard.md index c9a922c10b10a..78f953a889869 100644 --- a/docs/data/data-grid/clipboard/clipboard.md +++ b/docs/data/data-grid/clipboard/clipboard.md @@ -87,7 +87,22 @@ For convenience, you can also listen to these events using their respective prop - `onClipboardPasteStart` - `onClipboardPasteEnd` -The demo below shows how to use these events to display a loading indicator while the clipboard paste operation is in progress: +Additionally, there is the `onBeforeClipboardPasteStart` prop, which is called before the clipboard paste operation starts +and can be used to cancel or confirm the paste operation: + +```tsx +const onBeforeClipboardPasteStart = async () => { + const confirmed = window.confirm('Are you sure you want to paste?'); + if (!confirmed) { + throw new Error('Paste operation cancelled'); + } +}; + +; +``` + +The demo below uses the [`Dialog`](/material-ui/react-dialog/) component for paste confirmation. +If confirmed, the Data Grid displays a loading indicator during the paste operation. {{"demo": "ClipboardPasteEvents.js", "bg": "inline"}} diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json index ba181402628c8..559aa75b2e3f4 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -232,6 +232,10 @@ "describedArgs": ["model", "details"] } }, + "onBeforeClipboardPasteStart": { + "type": { "name": "func" }, + "signature": { "type": "function(params: object) => void", "describedArgs": ["params"] } + }, "onCellClick": { "type": { "name": "func" }, "signature": { diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index 7f8c28d5b7175..96b941032c92d 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -249,6 +249,10 @@ "details": "Additional details for this callback." } }, + "onBeforeClipboardPasteStart": { + "description": "Callback fired before the clipboard paste operation starts. Use it to confirm or cancel the paste operation.", + "typeDescriptions": { "params": "Params passed to the callback." } + }, "onCellClick": { "description": "Callback fired when any cell is clicked.", "typeDescriptions": { diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index ad6815061d5c1..a411583a776c3 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -543,6 +543,14 @@ DataGridPremiumRaw.propTypes = { * @param {GridCallbackDetails} details Additional details for this callback. */ onAggregationModelChange: PropTypes.func, + /** + * Callback fired before the clipboard paste operation starts. + * Use it to confirm or cancel the paste operation. + * @param {object} params Params passed to the callback. + * @param {string[][]} params.data The raw pasted data split by rows and cells. + * @returns {Promise} A promise that resolves to confirm the paste operation, and rejects to cancel it. + */ + onBeforeClipboardPasteStart: PropTypes.func, /** * Callback fired when any cell is clicked. * @param {GridCellParams} params With all properties from [[GridCellParams]]. diff --git a/packages/x-data-grid-premium/src/hooks/features/clipboard/useGridClipboardImport.ts b/packages/x-data-grid-premium/src/hooks/features/clipboard/useGridClipboardImport.ts index fe95b6cc33d9c..be1527ee8797e 100644 --- a/packages/x-data-grid-premium/src/hooks/features/clipboard/useGridClipboardImport.ts +++ b/packages/x-data-grid-premium/src/hooks/features/clipboard/useGridClipboardImport.ts @@ -21,6 +21,7 @@ import { useGridRegisterPipeProcessor, getPublicApiRef, isPasteShortcut, + useGridLogger, } from '@mui/x-data-grid/internals'; import { GRID_DETAIL_PANEL_TOGGLE_FIELD, GRID_REORDER_COL_DEF } from '@mui/x-data-grid-pro'; import { unstable_debounce as debounce } from '@mui/utils'; @@ -318,6 +319,7 @@ export const useGridClipboardImport = ( | 'onClipboardPasteEnd' | 'splitClipboardPastedText' | 'disableClipboardPaste' + | 'onBeforeClipboardPasteStart' >, ): void => { const processRowUpdate = props.processRowUpdate; @@ -325,9 +327,12 @@ export const useGridClipboardImport = ( const getRowId = props.getRowId; const enableClipboardPaste = !props.disableClipboardPaste; const rootEl = apiRef.current.rootElementRef?.current; + const logger = useGridLogger(apiRef, 'useGridClipboardImport'); const splitClipboardPastedText = props.splitClipboardPastedText; + const { pagination, onBeforeClipboardPasteStart } = props; + const handlePaste = React.useCallback>( async (params, event) => { if (!enableClipboardPaste) { @@ -360,6 +365,15 @@ export const useGridClipboardImport = ( return; } + if (onBeforeClipboardPasteStart) { + try { + await onBeforeClipboardPasteStart({ data: pastedData }); + } catch (error) { + logger.debug('Clipboard paste operation cancelled'); + return; + } + } + const cellUpdater = new CellValueUpdater({ apiRef, processRowUpdate, @@ -377,7 +391,7 @@ export const useGridClipboardImport = ( updateCell: (...args) => { cellUpdater.updateCell(...args); }, - pagination: props.pagination, + pagination, }); cellUpdater.applyUpdates(); @@ -390,7 +404,9 @@ export const useGridClipboardImport = ( enableClipboardPaste, rootEl, splitClipboardPastedText, - props.pagination, + pagination, + onBeforeClipboardPasteStart, + logger, ], ); diff --git a/packages/x-data-grid-premium/src/models/dataGridPremiumProps.ts b/packages/x-data-grid-premium/src/models/dataGridPremiumProps.ts index add53ef3b59d4..ee3b030e670ea 100644 --- a/packages/x-data-grid-premium/src/models/dataGridPremiumProps.ts +++ b/packages/x-data-grid-premium/src/models/dataGridPremiumProps.ts @@ -169,6 +169,14 @@ export interface DataGridPremiumPropsWithoutDefaultValue void; + /** + * Callback fired before the clipboard paste operation starts. + * Use it to confirm or cancel the paste operation. + * @param {object} params Params passed to the callback. + * @param {string[][]} params.data The raw pasted data split by rows and cells. + * @returns {Promise} A promise that resolves to confirm the paste operation, and rejects to cancel it. + */ + onBeforeClipboardPasteStart?: (params: { data: string[][] }) => Promise; /** * Callback fired when the clipboard paste operation starts. */