/** * JavaScript code for the "Edit" section integration of the "Header Bar". * * @package TablePress * @subpackage Views JavaScript * @author Tobias Bäthge * @since 3.3.0 */ /** * WordPress dependencies. */ import { useCallback, useEffect, useRef, useState } from 'react'; import { Button, __experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis Icon, KeyboardShortcuts, Modal, Spinner, Tooltip, __experimentalVStack as VStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis withNotices, } from '@wordpress/components'; import { useCopyToClipboard } from '@wordpress/compose'; import { useDispatch } from '@wordpress/data'; import { applyFilters } from '@wordpress/hooks'; import { __, _n, sprintf } from '@wordpress/i18n'; import { arrowUp, blockTable, box, copySmall, download, info, moreVertical, settings, shortcode, siteLogo, trash } from '@wordpress/icons'; import { displayShortcut, shortcutAriaLabel } from '@wordpress/keycodes'; import { store as noticesStore } from '@wordpress/notices'; import { Menu } from '@wordpress/ui'; /** * Internal dependencies. */ import { initializeReactComponentInPortal } from '../common/react-loader'; import processAjaxRequest from '../common/ajax-request'; import { Notifications } from '../common/notifications'; import { TablePressIconSimple } from '../../img/tablepress-icon'; /** * Saves the table changes to the server. * * @param {Object} props Function parameters. * @param {Object} props.screenData Screen data. * @param {Function} props.updateScreenData Callback to update the screen data. * @param {Object} props.tableOptions Table options. * @param {Object} props.tableMeta Table meta data. * @param {Function} props.updateTableMeta Callback to update the table meta. * @param {Function} props.noticeOperations Callbacks for working with notices. * @param {Object} props.noticesStoreDispatch Dispatch function for the notices store. (Optional, only for notices created by keyboard shortcuts.) */ const saveTableChanges = ( { screenData, updateScreenData, tableOptions, tableMeta, updateTableMeta, noticeOperations, noticesStoreDispatch } ) => { // Validate input fields. if ( ! applyFilters( 'tablepress.optionsValidateFields', true, tableOptions ) ) { return; } // Collect information about hidden rows and columns. tp.helpers.visibility.update(); // Prepare the data for the AJAX request. const requestData = { action: 'tablepress_save_table', _ajax_nonce: tp.nonces.edit_table, tablepress: { id: tp.table.id, new_id: tableMeta.id, name: tableMeta.name, description: tableMeta.description, data: JSON.stringify( tp.editor.options.data ), options: JSON.stringify( tableOptions ), visibility: JSON.stringify( tp.table.visibility ), number: { rows: tp.editor.options.data.length, columns: tp.editor.options.columns.length, }, }, }; /** * Callback for handling specifics of a successful save on this screen. * * @param {Object} data */ const onSuccessfulRequest = ( data ) => { // Saving was successful, so the original ID has changed to the (maybe) new ID -> we need to adjust all occurrences. if ( tp.table.id !== data.table_id && window?.history?.pushState ) { // Update URL, but only if the table ID changed, to not get dummy entries in the browser history. window.history.pushState( '', '', window.location.href.replace( /table_id=[a-zA-Z0-9_-]+/gi, `table_id=${ data.table_id }` ) ); } tp.table.id = data.table_id; updateTableMeta( { id: data.table_id, lastModified: data.last_modified, lastEditor: data.last_editor, } ); // Update the nonces. [ 'copy', 'delete', 'edit', 'preview' ].forEach( action => { tp.nonces[ `${action}_table` ] = data[ `new_${action}_nonce` ]; } ); // Update the "Export", "Copy", "Delete", and "Preview" URLs in the screen data. const updatedUrls = {}; updatedUrls.exportUrl = screenData.exportUrl.replace( /table_id=[a-zA-Z0-9_-]+/g, `table_id=${ data.table_id }` ); [ 'copy', 'delete', 'preview' ].forEach( ( action ) => { updatedUrls[ `${ action }Url` ] = screenData[ `${ action }Url` ] .replace( /item=[a-zA-Z0-9_-]+/g, `item=${ data.table_id }` ) // Updates both the "item" and the "return_item" parameters. .replace( /&_wpnonce=[a-zA-Z0-9]+/g, `&_wpnonce=${ tp.nonces[ `${ action }_table` ] }` ); } ); updateScreenData( updatedUrls ); tp.helpers.unsaved_changes.unset(); const actionMessages = {}; actionMessages.success_save = __( 'The table was saved successfully.', 'tablepress' ); actionMessages.success_save_success_id_change = actionMessages.success_save + ' ' + __( 'The table ID was changed.', 'tablepress' ); actionMessages.success_save_error_id_change = actionMessages.success_save + ' ' + __( 'The table ID could not be changed, probably because the new ID is already in use!', 'tablepress' ); if ( 'success_save_error_id_change' === data.message && data.error_details ) { const errorIntroduction = __( 'These errors were encountered:', 'tablepress' ); actionMessages.success_save_error_id_change = `
${ actionMessages.success_save_error_id_change }
${ errorIntroduction }
${ data.error_details }`; } const notice = { status: ( data.message.includes( 'error' ) ) ? 'error' : 'success', content: actionMessages[ data.message ], type: ( data.message.includes( 'error' ) ) ? 'notice' : 'snackbar', }; return { notice }; }; const setBusyState = ( isBusy ) => updateScreenData( { isSaving: isBusy } ); processAjaxRequest( { requestData, onSuccessfulRequest, setBusyState, noticeOperations, noticesStoreDispatch } ); }; /** * Shows a preview of the table. * * @param {Object} props Function parameters. * @param {Function} props.updateScreenData Callback to update the screen data. * @param {Object} props.tableOptions Table options. * @param {Object} props.tableMeta Table meta data. * @param {Function} props.noticeOperations Callbacks for working with notices. */ const showPreview = ( { updateScreenData, tableOptions, tableMeta, noticeOperations } ) => { // For tables without unsaved changes, directly show an externally rendered table from a URL in an iframe in a Modal. if ( ! tp.made_changes ) { updateScreenData( { previewIsOpen: true, previewSrcDoc: '', } ); return; } /* For tables with unsaved changes, get the table preview HTML code for the iframe via AJAX. */ // Update information about hidden rows and columns. tp.helpers.visibility.update(); // Prepare the data for the AJAX request. const requestData = { action: 'tablepress_preview_table', _ajax_nonce: tp.nonces.preview_table, tablepress: { id: tp.table.id, new_id: tableMeta.id, name: tableMeta.name, description: tableMeta.description, data: JSON.stringify( tp.editor.options.data ), options: JSON.stringify( tableOptions ), visibility: JSON.stringify( tp.table.visibility ), number: { rows: tp.editor.options.data.length, columns: tp.editor.options.columns.length, }, }, }; /** * Callback for handling specifics of a successful save on this screen. * * @param {Object} data */ const onSuccessfulRequest = ( data ) => { updateScreenData( { previewIsOpen: true, previewSrcDoc: `
${ data.head_html }${ data.body_html }`, } ); return { notice: null }; }; const setBusyState = ( isBusy ) => updateScreenData( { previewIsLoading: isBusy } ); processAjaxRequest( { requestData, onSuccessfulRequest, setBusyState, noticeOperations } ); }; /** * Returns the Confirm Delete Modal component's JSX markup. * * @param {Object} props Component props. * @param {string} props.title Title of the modal. * @param {string} props.deleteUrl URL to delete the table. * @param {Function} props.closeConfirmDeleteModal Callback to close the Confirm Delete modal. * @return {Object} Confirm Delete Modal component. */ const ConfirmDeleteModal = ( { title, deleteUrl, closeConfirmDeleteModal } ) => { const cancelButtonRef = useRef(); const deleteButtonRef = useRef(); const handleEnter = useCallback( ( event ) => { // Avoid triggering the action when a button is focused, as this can cause a double submission. const isCancelOrDeleteButton = event.target === cancelButtonRef.current || event.target === deleteButtonRef.current; if ( ! isCancelOrDeleteButton && 'Enter' === event.key ) { deleteButtonRef.current.click(); } }, [], ); return (