-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow renaming, duplication and deleting of Navigation menus from Browse Mode Sidebar #50880
Changes from 30 commits
93fdc55
b733e50
b4614b1
120bb55
bc52937
76b0822
180a897
884f832
546c62e
847c300
e11d696
827cf11
ef67830
d9609bb
a757dfd
734ae9f
49d5957
2da85f6
58f4ef6
cb73bc6
efc15a5
ef7bfb9
2446d46
63a98df
97be8f1
71e2af1
c3d7ca9
6bfccb3
1f60256
eac8d1c
e49e2ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 wish to delete this Navigation menu?' | ||
getdave marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) } | ||
</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 | ||||||
*/ | ||||||
|
@@ -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, | ||||||
} ); | ||||||
Comment on lines
+85
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole edit then save flow seems really clunky. I'm not sure if there is a better way but if there is I'm keen to learn. Currently we're having to save a copy of the record prior to editing in order that we can revert afterwards. Note that using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, this was confusing to me too! |
||||||
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 | ||||||
|
@@ -74,6 +219,14 @@ export default function SidebarNavigationScreenNavigationMenu() { | |||||
|
||||||
return ( | ||||||
<SidebarNavigationScreenWrapper | ||||||
actions={ | ||||||
<ScreenNavigationMoreMenu | ||||||
menuTitle={ decodeEntities( menuTitle ) } | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Let's pass the raw value here and allow the |
||||||
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.' | ||||||
|
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 ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll need to |
||
|
||
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> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jasmussen Are you happy with this wording or would you prefer
want
overwish
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small preference for want, but not strong at all.