|
| 1 | +/** |
| 2 | + * WordPress dependencies |
| 3 | + */ |
| 4 | +import { useEffect, useState } from '@wordpress/element'; |
| 5 | +import { useSelect } from '@wordpress/data'; |
| 6 | +import { _n, __, sprintf } from '@wordpress/i18n'; |
| 7 | +import { store as coreStore } from '@wordpress/core-data'; |
| 8 | +import { displayShortcut, rawShortcut } from '@wordpress/keycodes'; |
| 9 | +import { check } from '@wordpress/icons'; |
| 10 | +import { EntitiesSavedStates } from '@wordpress/editor'; |
| 11 | +import { Button, Modal, Tooltip } from '@wordpress/components'; |
| 12 | + |
| 13 | +/** |
| 14 | + * Internal dependencies |
| 15 | + */ |
| 16 | +import './style.scss'; |
| 17 | +import useSaveShortcut from '../save-panel/use-save-shortcut'; |
| 18 | + |
| 19 | +export default function SaveButton() { |
| 20 | + const [ isSaveViewOpen, setIsSaveViewOpened ] = useState( false ); |
| 21 | + const { isSaving, dirtyEntityRecordsCount } = useSelect( ( select ) => { |
| 22 | + const { isSavingEntityRecord, __experimentalGetDirtyEntityRecords } = |
| 23 | + select( coreStore ); |
| 24 | + const dirtyEntityRecords = __experimentalGetDirtyEntityRecords(); |
| 25 | + return { |
| 26 | + isSaving: dirtyEntityRecords.some( ( record ) => |
| 27 | + isSavingEntityRecord( record.kind, record.name, record.key ) |
| 28 | + ), |
| 29 | + dirtyEntityRecordsCount: dirtyEntityRecords.length, |
| 30 | + }; |
| 31 | + }, [] ); |
| 32 | + const [ showSavedState, setShowSavedState ] = useState( false ); |
| 33 | + |
| 34 | + useEffect( () => { |
| 35 | + if ( isSaving ) { |
| 36 | + // Proactively expect to show saved state. This is done once save |
| 37 | + // starts to avoid race condition where setting it after would cause |
| 38 | + // the button to be unmounted before state is updated. |
| 39 | + setShowSavedState( true ); |
| 40 | + } |
| 41 | + }, [ isSaving ] ); |
| 42 | + |
| 43 | + const hasChanges = dirtyEntityRecordsCount > 0; |
| 44 | + |
| 45 | + // Handle save failure case: If we were showing saved state but saving |
| 46 | + // failed, reset to show changes again. |
| 47 | + useEffect( () => { |
| 48 | + if ( ! isSaving && hasChanges ) { |
| 49 | + setShowSavedState( false ); |
| 50 | + } |
| 51 | + }, [ isSaving, hasChanges ] ); |
| 52 | + |
| 53 | + function hideSavedState() { |
| 54 | + if ( showSavedState ) { |
| 55 | + setShowSavedState( false ); |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + const shouldShowButton = hasChanges || showSavedState; |
| 60 | + |
| 61 | + useSaveShortcut( { openSavePanel: () => setIsSaveViewOpened( true ) } ); |
| 62 | + |
| 63 | + if ( ! shouldShowButton ) { |
| 64 | + return null; |
| 65 | + } |
| 66 | + |
| 67 | + const isInSavedState = showSavedState && ! hasChanges; |
| 68 | + const disabled = isSaving || isInSavedState; |
| 69 | + |
| 70 | + const getLabel = () => { |
| 71 | + if ( isInSavedState ) { |
| 72 | + return __( 'Saved' ); |
| 73 | + } |
| 74 | + return sprintf( |
| 75 | + // translators: %d: number of unsaved changes (number). |
| 76 | + _n( |
| 77 | + 'Review %d change…', |
| 78 | + 'Review %d changes…', |
| 79 | + dirtyEntityRecordsCount |
| 80 | + ), |
| 81 | + dirtyEntityRecordsCount |
| 82 | + ); |
| 83 | + }; |
| 84 | + const label = getLabel(); |
| 85 | + |
| 86 | + return ( |
| 87 | + <> |
| 88 | + <Tooltip |
| 89 | + text={ hasChanges ? label : undefined } |
| 90 | + shortcut={ displayShortcut.primary( 's' ) } |
| 91 | + > |
| 92 | + <Button |
| 93 | + variant="primary" |
| 94 | + size="compact" |
| 95 | + onClick={ () => setIsSaveViewOpened( true ) } |
| 96 | + onBlur={ hideSavedState } |
| 97 | + disabled={ disabled } |
| 98 | + accessibleWhenDisabled |
| 99 | + isBusy={ isSaving } |
| 100 | + aria-keyshortcuts={ rawShortcut.primary( 's' ) } |
| 101 | + className="boot-save-button" |
| 102 | + icon={ isInSavedState ? check : undefined } |
| 103 | + > |
| 104 | + { label } |
| 105 | + </Button> |
| 106 | + </Tooltip> |
| 107 | + { isSaveViewOpen && ( |
| 108 | + <Modal |
| 109 | + title={ __( 'Review changes' ) } |
| 110 | + onRequestClose={ () => setIsSaveViewOpened( false ) } |
| 111 | + size="small" |
| 112 | + > |
| 113 | + <EntitiesSavedStates |
| 114 | + close={ () => setIsSaveViewOpened( false ) } |
| 115 | + variant="inline" |
| 116 | + /> |
| 117 | + </Modal> |
| 118 | + ) } |
| 119 | + </> |
| 120 | + ); |
| 121 | +} |
0 commit comments