Skip to content

Commit

Permalink
[Site Editor]: Consolidate save button functionality (WordPress#60077)
Browse files Browse the repository at this point in the history
Co-authored-by: ntsekouras <[email protected]>
Co-authored-by: youknowriad <[email protected]>
Co-authored-by: jameskoster <[email protected]>
Co-authored-by: Mamaduka <[email protected]>
Co-authored-by: annezazu <[email protected]>
Co-authored-by: SaxonF <[email protected]>
Co-authored-by: richtabor <[email protected]>
  • Loading branch information
8 people authored and cbravobernal committed Apr 9, 2024
1 parent 7abf3ed commit 9f455d9
Show file tree
Hide file tree
Showing 12 changed files with 249 additions and 337 deletions.
22 changes: 16 additions & 6 deletions packages/e2e-test-utils-playwright/src/editor/site-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@
*/
import type { Editor } from './index';

interface Options {
// If the only dirty entity is the current one, skip opening the save panel.
isOnlyCurrentEntityDirty?: boolean;
}

/**
* Save entities in the site editor. Assumes the editor is in a dirty state.
*
* @param this
* @param options
*/
export async function saveSiteEditorEntities( this: Editor ) {
export async function saveSiteEditorEntities(
this: Editor,
options: Options = {}
) {
const editorTopBar = this.page.getByRole( 'region', {
name: 'Editor top bar',
} );
Expand All @@ -19,11 +28,12 @@ export async function saveSiteEditorEntities( this: Editor ) {
.getByRole( 'button', { name: 'Save', exact: true } )
.click();

// Second Save button in the entities panel.
await savePanel
.getByRole( 'button', { name: 'Save', exact: true } )
.click();

if ( ! options.isOnlyCurrentEntityDirty ) {
// Second Save button in the entities panel.
await savePanel
.getByRole( 'button', { name: 'Save', exact: true } )
.click();
}
await this.page
.getByRole( 'button', { name: 'Dismiss this notice' } )
.getByText( 'Site updated.' )
Expand Down
81 changes: 55 additions & 26 deletions packages/edit-site/src/components/save-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { Button } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { __, _n, sprintf } from '@wordpress/i18n';
import { store as coreStore } from '@wordpress/core-data';
import { displayShortcut } from '@wordpress/keycodes';
import { privateApis as routerPrivateApis } from '@wordpress/router';
import {
useEntitiesSavedStatesIsDirty,
store as editorStore,
} from '@wordpress/editor';

/**
* Internal dependencies
Expand All @@ -15,30 +20,30 @@ import {
currentlyPreviewingTheme,
isPreviewingTheme,
} from '../../utils/is-previewing-theme';
import { unlock } from '../../lock-unlock';

const { useLocation } = unlock( routerPrivateApis );

export default function SaveButton( {
className = 'edit-site-save-button__button',
variant = 'primary',
showTooltip = true,
defaultLabel,
showReviewMessage,
icon,
size,
__next40pxDefaultSize = false,
} ) {
const { isDirty, isSaving, isSaveViewOpen, previewingThemeName } =
useSelect( ( select ) => {
const {
__experimentalGetDirtyEntityRecords,
isSavingEntityRecord,
isResolving,
} = select( coreStore );
const dirtyEntityRecords = __experimentalGetDirtyEntityRecords();
const { params } = useLocation();
const { setIsSaveViewOpened } = useDispatch( editSiteStore );
const { saveDirtyEntities } = unlock( useDispatch( editorStore ) );
const { dirtyEntityRecords } = useEntitiesSavedStatesIsDirty();
const { isSaving, isSaveViewOpen, previewingThemeName } = useSelect(
( select ) => {
const { isSavingEntityRecord, isResolving } = select( coreStore );
const { isSaveViewOpened } = select( editSiteStore );
const isActivatingTheme = isResolving( 'activateTheme' );
const currentlyPreviewingThemeId = currentlyPreviewingTheme();

return {
isDirty: dirtyEntityRecords.length > 0,
isSaving:
dirtyEntityRecords.some( ( record ) =>
isSavingEntityRecord(
Expand All @@ -55,12 +60,26 @@ export default function SaveButton( {
?.name?.rendered
: undefined,
};
}, [] );
const { setIsSaveViewOpened } = useDispatch( editSiteStore );

const activateSaveEnabled = isPreviewingTheme() || isDirty;
const disabled = isSaving || ! activateSaveEnabled;

},
[ dirtyEntityRecords ]
);
const hasDirtyEntities = !! dirtyEntityRecords.length;
let isOnlyCurrentEntityDirty;
// Check if the current entity is the only entity with changes.
// We have some extra logic for `wp_global_styles` for now, that
// is used in navigation sidebar.
if ( dirtyEntityRecords.length === 1 ) {
if ( params.postId ) {
isOnlyCurrentEntityDirty =
`${ dirtyEntityRecords[ 0 ].key }` === params.postId &&
dirtyEntityRecords[ 0 ].name === params.postType;
} else if ( params.path?.includes( 'wp_global_styles' ) ) {
isOnlyCurrentEntityDirty =
dirtyEntityRecords[ 0 ].name === 'globalStyles';
}
}
const disabled =
isSaving || ( ! hasDirtyEntities && ! isPreviewingTheme() );
const getLabel = () => {
if ( isPreviewingTheme() ) {
if ( isSaving ) {
Expand All @@ -71,40 +90,50 @@ export default function SaveButton( {
);
} else if ( disabled ) {
return __( 'Saved' );
} else if ( isDirty ) {
} else if ( hasDirtyEntities ) {
return sprintf(
/* translators: %s: The name of theme to be activated. */
__( 'Activate %s & Save' ),
previewingThemeName
);
}

return sprintf(
/* translators: %s: The name of theme to be activated. */
__( 'Activate %s' ),
previewingThemeName
);
}

if ( isSaving ) {
return __( 'Saving' );
} else if ( disabled ) {
}
if ( disabled ) {
return __( 'Saved' );
} else if ( defaultLabel ) {
return defaultLabel;
}
if ( ! isOnlyCurrentEntityDirty && showReviewMessage ) {
return sprintf(
// translators: %d: number of unsaved changes (number).
_n(
'Review %d change…',
'Review %d changes…',
dirtyEntityRecords.length
),
dirtyEntityRecords.length
);
}
return __( 'Save' );
};
const label = getLabel();

const onClick = isOnlyCurrentEntityDirty
? () => saveDirtyEntities( { dirtyEntityRecords } )
: () => setIsSaveViewOpened( true );
return (
<Button
variant={ variant }
className={ className }
aria-disabled={ disabled }
aria-expanded={ isSaveViewOpen }
isBusy={ isSaving }
onClick={ disabled ? undefined : () => setIsSaveViewOpened( true ) }
onClick={ disabled ? undefined : onClick }
label={ label }
/*
* We want the tooltip to show the keyboard shortcut only when the
Expand Down
184 changes: 20 additions & 164 deletions packages/edit-site/src/components/save-hub/index.js
Original file line number Diff line number Diff line change
@@ -1,186 +1,42 @@
/**
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { Button, __experimentalHStack as HStack } from '@wordpress/components';
import { __, sprintf, _n } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { __experimentalHStack as HStack } from '@wordpress/components';
import { store as coreStore } from '@wordpress/core-data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { check } from '@wordpress/icons';
import { privateApis as routerPrivateApis } from '@wordpress/router';
import { store as noticesStore } from '@wordpress/notices';

/**
* Internal dependencies
*/
import SaveButton from '../save-button';
import { isPreviewingTheme } from '../../utils/is-previewing-theme';
import { unlock } from '../../lock-unlock';
import { NAVIGATION_POST_TYPE } from '../../utils/constants';

const { useLocation } = unlock( routerPrivateApis );

const PUBLISH_ON_SAVE_ENTITIES = [
{
kind: 'postType',
name: NAVIGATION_POST_TYPE,
},
];

export default function SaveHub() {
const saveNoticeId = 'site-edit-save-notice';
const { params } = useLocation();

const { __unstableMarkLastChangeAsPersistent } =
useDispatch( blockEditorStore );

const { createSuccessNotice, createErrorNotice, removeNotice } =
useDispatch( noticesStore );

const { dirtyCurrentEntity, countUnsavedChanges, isDirty, isSaving } =
useSelect(
( select ) => {
const {
__experimentalGetDirtyEntityRecords,
isSavingEntityRecord,
} = select( coreStore );
const dirtyEntityRecords =
__experimentalGetDirtyEntityRecords();
let calcDirtyCurrentEntity = null;

if ( dirtyEntityRecords.length === 1 ) {
// if we are on global styles
if ( params.path?.includes( 'wp_global_styles' ) ) {
calcDirtyCurrentEntity = dirtyEntityRecords.find(
( record ) => record.name === 'globalStyles'
);
}
// if we are on pages
else if ( params.postId ) {
calcDirtyCurrentEntity = dirtyEntityRecords.find(
( record ) =>
record.name === params.postType &&
String( record.key ) === params.postId
);
}
}

return {
dirtyCurrentEntity: calcDirtyCurrentEntity,
isDirty: dirtyEntityRecords.length > 0,
isSaving: dirtyEntityRecords.some( ( record ) =>
isSavingEntityRecord(
record.kind,
record.name,
record.key
)
),
countUnsavedChanges: dirtyEntityRecords.length,
};
},
[ params.path, params.postType, params.postId ]
const { isDisabled, isSaving } = useSelect( ( select ) => {
const { __experimentalGetDirtyEntityRecords, isSavingEntityRecord } =
select( coreStore );
const dirtyEntityRecords = __experimentalGetDirtyEntityRecords();
const _isSaving = dirtyEntityRecords.some( ( record ) =>
isSavingEntityRecord( record.kind, record.name, record.key )
);

const {
editEntityRecord,
saveEditedEntityRecord,
__experimentalSaveSpecifiedEntityEdits: saveSpecifiedEntityEdits,
} = useDispatch( coreStore );

const disabled = isSaving || ( ! isDirty && ! isPreviewingTheme() );

// if we have only one unsaved change and it matches current context, we can show a more specific label
let label = dirtyCurrentEntity
? __( 'Save' )
: sprintf(
// translators: %d: number of unsaved changes (number).
_n(
'Review %d change…',
'Review %d changes…',
countUnsavedChanges
),
countUnsavedChanges
);

if ( isSaving ) {
label = __( 'Saving' );
}

const { homeUrl } = useSelect( ( select ) => {
const {
getUnstableBase, // Site index.
} = select( coreStore );
return {
homeUrl: getUnstableBase()?.home,
isSaving: _isSaving,
isDisabled:
_isSaving ||
( ! dirtyEntityRecords.length && ! isPreviewingTheme() ),
};
}, [] );

const saveCurrentEntity = async () => {
if ( ! dirtyCurrentEntity ) return;

removeNotice( saveNoticeId );
const { kind, name, key, property } = dirtyCurrentEntity;

try {
if ( 'root' === dirtyCurrentEntity.kind && 'site' === name ) {
await saveSpecifiedEntityEdits( 'root', 'site', undefined, [
property,
] );
} else {
if (
PUBLISH_ON_SAVE_ENTITIES.some(
( typeToPublish ) =>
typeToPublish.kind === kind &&
typeToPublish.name === name
)
) {
editEntityRecord( kind, name, key, { status: 'publish' } );
}

await saveEditedEntityRecord( kind, name, key );
}

__unstableMarkLastChangeAsPersistent();

createSuccessNotice( __( 'Site updated.' ), {
type: 'snackbar',
actions: [
{
label: __( 'View site' ),
url: homeUrl,
},
],
id: saveNoticeId,
} );
} catch ( error ) {
createErrorNotice( `${ __( 'Saving failed.' ) } ${ error }` );
}
};

return (
<HStack className="edit-site-save-hub" alignment="right" spacing={ 4 }>
{ dirtyCurrentEntity ? (
<Button
variant="primary"
onClick={ saveCurrentEntity }
isBusy={ isSaving }
disabled={ isSaving }
aria-disabled={ isSaving }
className="edit-site-save-hub__button"
__next40pxDefaultSize
>
{ label }
</Button>
) : (
<SaveButton
className="edit-site-save-hub__button"
variant={ disabled ? null : 'primary' }
showTooltip={ false }
icon={ disabled && ! isSaving ? check : null }
defaultLabel={ label }
__next40pxDefaultSize
/>
) }
<SaveButton
className="edit-site-save-hub__button"
variant={ isDisabled ? null : 'primary' }
showTooltip={ false }
icon={ isDisabled && ! isSaving ? check : null }
showReviewMessage
__next40pxDefaultSize
/>
</HStack>
);
}
Loading

0 comments on commit 9f455d9

Please sign in to comment.