Skip to content

Commit

Permalink
Allow renaming, duplication and deleting of Navigation menus from Bro…
Browse files Browse the repository at this point in the history
…wse Mode Sidebar (#50880)

* added more menu to navigation with delete menu option

* modal for the rename function

* prop refactoring, title on rename popup

* very ugly renaming of the menu

* duplicate function

* added snackbar notices to each action

* navigate to correct places on delete and duplicate. Added Copy after title on duplication

* i18n

Co-authored-by: Ben Dwyer <ben@scruffian.com>

* Another i18n

Co-authored-by: Ben Dwyer <ben@scruffian.com>

* Apply suggestions from code review

Co-authored-by: Ben Dwyer <ben@scruffian.com>

* Use self documenting var name

* Use consistent confirmatory action wording

* Refactor MoreMenu to component

* Extract Modal to seperate component

* Refactor change function

* Use local state and only edit record on “save” confirmation

* Localise state closer to where it’s needed

* Disable “Save” button until menu title is modified

* Remove file import introduced during rebase

* Handle Save errors

* Error handling for Delete action

* Force all actions to throw on error

* Add error handling for Duplicate.

Also pass error message and not error object.

* Improve loading feedback when saving or deleting

* Make prop naming consistent

* i18n of modal title

* Add confirmatory step prior to delete action

* Fix rename input focus border clipping

* Use default variant to get correct styles

* Make `Copy` translatable

* Updating confirmatory wording

---------

Co-authored-by: Dave Smith <getdavemail@gmail.com>
Co-authored-by: Ben Dwyer <ben@scruffian.com>
  • Loading branch information
3 people committed Jun 12, 2023
1 parent 799137d commit f634c45
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* WordPress dependencies
*/
import {
__experimentalHStack as HStack,
__experimentalVStack as VStack,
Button,
Modal,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';

export default function RenameModal( { onClose, onConfirm } ) {
return (
<Modal title={ __( 'Delete' ) } onRequestClose={ onClose }>
<form>
<VStack spacing="3">
<p>
{ __(
'Are you sure you want to delete this Navigation menu?'
) }
</p>
<HStack justify="right">
<Button variant="tertiary" onClick={ onClose }>
{ __( 'Cancel' ) }
</Button>

<Button
isDestructive
variant="primary"
type="submit"
onClick={ ( e ) => {
e.preventDefault();
onConfirm();

// Immediate close avoids ability to hit delete multiple times.
onClose();
} }
>
{ __( 'Confirm' ) }
</Button>
</HStack>
</VStack>
</form>
</Modal>
);
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
/**
* WordPress dependencies
*/
import { useEntityRecord } from '@wordpress/core-data';
import { useEntityRecord, store as coreStore } from '@wordpress/core-data';
import {
__experimentalUseNavigator as useNavigator,
Spinner,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { useCallback, useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { useSelect, useDispatch } from '@wordpress/data';
import { privateApis as routerPrivateApis } from '@wordpress/router';
import { BlockEditorProvider } from '@wordpress/block-editor';
import { createBlock } from '@wordpress/blocks';
import { decodeEntities } from '@wordpress/html-entities';

import { store as noticesStore } from '@wordpress/notices';

/**
* Internal dependencies
*/
Expand All @@ -25,24 +27,167 @@ import {
} from '../../utils/is-previewing-theme';
import { SidebarNavigationScreenWrapper } from '../sidebar-navigation-screen-navigation-menus';
import NavigationMenuContent from '../sidebar-navigation-screen-navigation-menus/navigation-menu-content';
import ScreenNavigationMoreMenu from './more-menu';

const { useHistory } = unlock( routerPrivateApis );
const noop = () => {};

export default function SidebarNavigationScreenNavigationMenu() {
const {
deleteEntityRecord,
saveEntityRecord,
editEntityRecord,
saveEditedEntityRecord,
} = useDispatch( coreStore );

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

const postType = `wp_navigation`;
const {
goTo,
params: { postId },
} = useNavigator();

const { record: navigationMenu, isResolving: isLoading } = useEntityRecord(
const { record: navigationMenu, isResolving } = useEntityRecord(
'postType',
postType,
postId
);

const { getEditedEntityRecord, isSaving, isDeleting } = useSelect(
( select ) => {
const {
isSavingEntityRecord,
isDeletingEntityRecord,
getEditedEntityRecord: getEditedEntityRecordSelector,
} = select( coreStore );

return {
isSaving: isSavingEntityRecord( 'postType', postType, postId ),
isDeleting: isDeletingEntityRecord(
'postType',
postType,
postId
),
getEditedEntityRecord: getEditedEntityRecordSelector,
};
},
[ postId, postType ]
);

const isLoading = isResolving || isSaving || isDeleting;

const menuTitle = navigationMenu?.title?.rendered || navigationMenu?.slug;

const handleSave = async ( edits = {} ) => {
// Prepare for revert in case of error.
const originalRecord = getEditedEntityRecord(
'postType',
'wp_navigation',
postId
);

// Apply the edits.
editEntityRecord( 'postType', postType, postId, edits );

// Attempt to persist.
try {
await saveEditedEntityRecord( 'postType', postType, postId, {
throwOnError: true,
} );
createSuccessNotice( __( 'Renamed Navigation menu' ), {
type: 'snackbar',
} );
} catch ( error ) {
// Revert to original in case of error.
editEntityRecord( 'postType', postType, postId, originalRecord );

createErrorNotice(
sprintf(
/* translators: %s: error message describing why the navigation menu could not be renamed. */
__( `Unable to rename Navigation menu (%s).` ),
error?.message
),

{
type: 'snackbar',
}
);
}
};

const handleDelete = async () => {
try {
await deleteEntityRecord(
'postType',
postType,
postId,
{
force: true,
},
{
throwOnError: true,
}
);
createSuccessNotice( __( 'Deleted Navigation menu' ), {
type: 'snackbar',
} );
goTo( '/navigation' );
} catch ( error ) {
createErrorNotice(
sprintf(
/* translators: %s: error message describing why the navigation menu could not be deleted. */
__( `Unable to delete Navigation menu (%s).` ),
error?.message
),

{
type: 'snackbar',
}
);
}
};
const handleDuplicate = async () => {
try {
const savedRecord = await saveEntityRecord(
'postType',
postType,
{
title: sprintf(
/* translators: %s: Navigation menu title */
__( '%s (Copy)' ),
menuTitle
),
content: navigationMenu?.content?.raw,
status: 'publish',
},
{
throwOnError: true,
}
);

if ( savedRecord ) {
createSuccessNotice( __( 'Duplicated Navigation menu' ), {
type: 'snackbar',
} );
goTo( `/navigation/${ postType }/${ savedRecord.id }` );
}
} catch ( error ) {
createErrorNotice(
sprintf(
/* translators: %s: error message describing why the navigation menu could not be deleted. */
__( `Unable to duplicate Navigation menu (%s).` ),
error?.message
),

{
type: 'snackbar',
}
);
}
};

if ( isLoading ) {
return (
<SidebarNavigationScreenWrapper
Expand Down Expand Up @@ -74,6 +219,14 @@ export default function SidebarNavigationScreenNavigationMenu() {

return (
<SidebarNavigationScreenWrapper
actions={
<ScreenNavigationMoreMenu
menuTitle={ decodeEntities( menuTitle ) }
onDelete={ handleDelete }
onSave={ handleSave }
onDuplicate={ handleDuplicate }
/>
}
title={ decodeEntities( menuTitle ) }
description={ __(
'Navigation menus are a curated collection of blocks that allow visitors to get around your site.'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* WordPress dependencies
*/
import { DropdownMenu, MenuItem, MenuGroup } from '@wordpress/components';
import { moreVertical } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import RenameModal from './rename-modal';
import DeleteModal from './delete-modal';

const POPOVER_PROPS = {
position: 'bottom right',
};

export default function ScreenNavigationMoreMenu( props ) {
const { onDelete, onSave, onDuplicate, menuTitle } = props;

const [ renameModalOpen, setRenameModalOpen ] = useState( false );
const [ deleteModalOpen, setDeleteModalOpen ] = useState( false );

const closeModals = () => {
setRenameModalOpen( false );
setDeleteModalOpen( false );
};
const openRenameModal = () => setRenameModalOpen( true );
const openDeleteModal = () => setDeleteModalOpen( true );

return (
<>
<DropdownMenu
className="sidebar-navigation__more-menu"
icon={ moreVertical }
popoverProps={ POPOVER_PROPS }
>
{ ( { onClose } ) => (
<div>
<MenuGroup>
<MenuItem
onClick={ () => {
openRenameModal();
// Close the dropdown after opening the modal.
onClose();
} }
>
{ __( 'Rename' ) }
</MenuItem>
<MenuItem
onClick={ () => {
onDuplicate();
onClose();
} }
>
{ __( 'Duplicate' ) }
</MenuItem>
<MenuItem
isDestructive
isTertiary
onClick={ () => {
openDeleteModal();

// Close the dropdown after opening the modal.
onClose();
} }
>
{ __( 'Delete' ) }
</MenuItem>
</MenuGroup>
</div>
) }
</DropdownMenu>

{ deleteModalOpen && (
<DeleteModal onClose={ closeModals } onConfirm={ onDelete } />
) }

{ renameModalOpen && (
<RenameModal
onClose={ closeModals }
menuTitle={ menuTitle }
onSave={ onSave }
/>
) }
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* WordPress dependencies
*/
import {
__experimentalHStack as HStack,
__experimentalVStack as VStack,
Button,
TextControl,
Modal,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';

export default function RenameModal( { menuTitle, onClose, onSave } ) {
const [ editedMenuTitle, setEditedMenuTitle ] = useState( menuTitle );

return (
<Modal title={ __( 'Rename' ) } onRequestClose={ onClose }>
<form className="sidebar-navigation__rename-modal-form">
<VStack spacing="3">
<TextControl
__nextHasNoMarginBottom
value={ editedMenuTitle }
placeholder={ __( 'Navigation title' ) }
onChange={ setEditedMenuTitle }
/>
<HStack justify="right">
<Button variant="tertiary" onClick={ onClose }>
{ __( 'Cancel' ) }
</Button>

<Button
disabled={ editedMenuTitle === menuTitle }
variant="primary"
type="submit"
onClick={ ( e ) => {
e.preventDefault();
onSave( { title: editedMenuTitle } );

// Immediate close avoids ability to hit save multiple times.
onClose();
} }
>
{ __( 'Save' ) }
</Button>
</HStack>
</VStack>
</form>
</Modal>
);
}
Loading

0 comments on commit f634c45

Please sign in to comment.