Skip to content

Commit

Permalink
Patterns: Add rename/delete options for pattern categories in site ed…
Browse files Browse the repository at this point in the history
…itor (#55035)
  • Loading branch information
aaronrobertshaw authored Oct 13, 2023
1 parent 68a33b5 commit ef21f20
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 7 deletions.
3 changes: 2 additions & 1 deletion packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { camelCase } from 'change-case';
* WordPress dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import { decodeEntities } from '@wordpress/html-entities';
import apiFetch from '@wordpress/api-fetch';

/**
Expand Down Expand Up @@ -656,7 +657,7 @@ export const getUserPatternCategories =
const mappedPatternCategories =
patternCategories?.map( ( userCategory ) => ( {
...userCategory,
label: userCategory.name,
label: decodeEntities( userCategory.name ),
name: userCategory.slug,
} ) ) || [];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* WordPress dependencies
*/
import {
MenuItem,
__experimentalConfirmDialog as ConfirmDialog,
} from '@wordpress/components';
import { store as coreStore } from '@wordpress/core-data';
import { useDispatch } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
import { __, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { privateApis as routerPrivateApis } from '@wordpress/router';

/**
* Internal dependencies
*/
import { unlock } from '../../lock-unlock';
import { PATTERN_TYPES, PATTERN_DEFAULT_CATEGORY } from '../../utils/constants';

const { useHistory } = unlock( routerPrivateApis );

export default function DeleteCategoryMenuItem( { category, onClose } ) {
const [ isModalOpen, setIsModalOpen ] = useState( false );
const history = useHistory();

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

const onDelete = async () => {
try {
await deleteEntityRecord(
'taxonomy',
'wp_pattern_category',
category.id,
{ force: true },
{ throwOnError: true }
);

// Prevent the need to refresh the page to get up-to-date categories
// and pattern categorization.
invalidateResolution( 'getUserPatternCategories' );
invalidateResolution( 'getEntityRecords', [
'postType',
PATTERN_TYPES.user,
{ per_page: -1 },
] );

createSuccessNotice(
sprintf(
/* translators: The pattern category's name */
__( '"%s" deleted.' ),
category.label
),
{ type: 'snackbar', id: 'pattern-category-delete' }
);

onClose?.();
history.push( {
path: `/patterns`,
categoryType: PATTERN_TYPES.theme,
categoryId: PATTERN_DEFAULT_CATEGORY,
} );
} catch ( error ) {
const errorMessage =
error.message && error.code !== 'unknown_error'
? error.message
: __(
'An error occurred while deleting the pattern category.'
);

createErrorNotice( errorMessage, {
type: 'snackbar',
id: 'pattern-category-delete',
} );
}
};

return (
<>
<MenuItem isDestructive onClick={ () => setIsModalOpen( true ) }>
{ __( 'Delete' ) }
</MenuItem>
<ConfirmDialog
isOpen={ isModalOpen }
onConfirm={ onDelete }
onCancel={ () => setIsModalOpen( false ) }
confirmButtonText={ __( 'Delete' ) }
className="edit-site-patterns__delete-modal"
>
{ sprintf(
// translators: %s: The pattern category's name.
__(
'Are you sure you want to delete the category "%s"? The patterns will not be deleted.'
),
decodeEntities( category.label )
) }
</ConfirmDialog>
</>
);
}
48 changes: 42 additions & 6 deletions packages/edit-site/src/components/page-patterns/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@
* WordPress dependencies
*/
import {
__experimentalVStack as VStack,
DropdownMenu,
MenuGroup,
__experimentalHStack as HStack,
__experimentalHeading as Heading,
__experimentalText as Text,
__experimentalVStack as VStack,
} from '@wordpress/components';
import { store as editorStore } from '@wordpress/editor';
import { useSelect } from '@wordpress/data';
import { __, sprintf } from '@wordpress/i18n';
import { moreVertical } from '@wordpress/icons';

/**
* Internal dependencies
*/
import RenameCategoryMenuItem from './rename-category-menu-item';
import DeleteCategoryMenuItem from './delete-category-menu-item';
import usePatternCategories from '../sidebar-navigation-screen-patterns/use-pattern-categories';
import { TEMPLATE_PART_POST_TYPE, PATTERN_TYPES } from '../../utils/constants';

Expand All @@ -28,15 +35,15 @@ export default function PatternsHeader( {
[]
);

let title, description;
let title, description, patternCategory;
if ( type === TEMPLATE_PART_POST_TYPE ) {
const templatePartArea = templatePartAreas.find(
( area ) => area.area === categoryId
);
title = templatePartArea?.label;
description = templatePartArea?.description;
} else if ( type === PATTERN_TYPES.theme ) {
const patternCategory = patternCategories.find(
patternCategory = patternCategories.find(
( category ) => category.name === categoryId
);
title = patternCategory?.label;
Expand All @@ -47,9 +54,38 @@ export default function PatternsHeader( {

return (
<VStack className="edit-site-patterns__section-header">
<Heading as="h2" level={ 4 } id={ titleId }>
{ title }
</Heading>
<HStack justify="space-between">
<Heading as="h2" level={ 4 } id={ titleId }>
{ title }
</Heading>
{ !! patternCategory?.id && (
<DropdownMenu
icon={ moreVertical }
label={ __( 'Actions' ) }
toggleProps={ {
className: 'edit-site-patterns__button',
describedBy: sprintf(
/* translators: %s: pattern category name */
__( 'Action menu for %s pattern category' ),
title
),
} }
>
{ ( { onClose } ) => (
<MenuGroup>
<RenameCategoryMenuItem
category={ patternCategory }
onClose={ onClose }
/>
<DeleteCategoryMenuItem
category={ patternCategory }
onClose={ onClose }
/>
</MenuGroup>
) }
</DropdownMenu>
) }
</HStack>
{ description ? (
<Text variant="muted" as="p" id={ descriptionId }>
{ description }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* WordPress dependencies
*/
import { MenuItem } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { privateApis as patternsPrivateApis } from '@wordpress/patterns';

/**
* Internal dependencies
*/
import { unlock } from '../../lock-unlock';

const { RenamePatternCategoryModal } = unlock( patternsPrivateApis );

export default function RenameCategoryMenuItem( { category, onClose } ) {
const [ isModalOpen, setIsModalOpen ] = useState( false );

// User created pattern categories have their properties updated when
// retrieved via `getUserPatternCategories`. The rename modal expects an
// object that will match the pattern category entity.
const normalizedCategory = {
id: category.id,
slug: category.slug,
name: category.label,
};

return (
<>
<MenuItem onClick={ () => setIsModalOpen( true ) }>
{ __( 'Rename' ) }
</MenuItem>
{ isModalOpen && (
<RenamePatternCategoryModal
category={ normalizedCategory }
onClose={ () => {
setIsModalOpen( false );
onClose();
} }
overlayClassName="edit-site-list__rename-modal"
/>
) }
</>
);
}
8 changes: 8 additions & 0 deletions packages/edit-site/src/components/page-patterns/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@
background: $gray-900;
padding: $grid-unit-40 $grid-unit-40 $grid-unit-20;
z-index: z-index(".edit-site-patterns__header");

.edit-site-patterns__button {
color: $gray-600;
}
}

.edit-site-patterns__section {
Expand Down Expand Up @@ -218,3 +222,7 @@
.edit-site-patterns__no-results {
color: $gray-600;
}

.edit-site-patterns__delete-modal {
width: $modal-width-small;
}
121 changes: 121 additions & 0 deletions packages/patterns/src/components/rename-pattern-category-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* WordPress dependencies
*/
import {
Modal,
Button,
TextControl,
__experimentalHStack as HStack,
__experimentalVStack as VStack,
} from '@wordpress/components';
import { store as coreStore } from '@wordpress/core-data';
import { useDispatch } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
import { __ } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';

/**
* Internal dependencies
*/
import { CATEGORY_SLUG } from './category-selector';

export default function RenamePatternCategoryModal( {
category,
onClose,
onError,
onSuccess,
...props
} ) {
const [ name, setName ] = useState( decodeEntities( category.name ) );
const [ isSaving, setIsSaving ] = useState( false );

const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore );

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

const onRename = async ( event ) => {
event.preventDefault();

if ( ! name || name === category.name || isSaving ) {
return;
}

try {
setIsSaving( true );

// User pattern category properties may differ as they can be
// normalized for use alongside template part areas, core pattern
// categories etc. As a result we won't just destructure the passed
// category object.
const savedRecord = await saveEntityRecord(
'taxonomy',
CATEGORY_SLUG,
{
id: category.id,
slug: category.slug,
name,
}
);

invalidateResolution( 'getUserPatternCategories' );
onSuccess?.( savedRecord );
onClose();

createSuccessNotice( __( 'Pattern category renamed.' ), {
type: 'snackbar',
id: 'pattern-category-update',
} );
} catch ( error ) {
onError?.();
createErrorNotice( error.message, {
type: 'snackbar',
id: 'pattern-category-update',
} );
} finally {
setIsSaving( false );
setName( '' );
}
};

const onRequestClose = () => {
onClose();
setName( '' );
};

return (
<Modal
title={ __( 'Rename' ) }
onRequestClose={ onRequestClose }
{ ...props }
>
<form onSubmit={ onRename }>
<VStack spacing="5">
<TextControl
__nextHasNoMarginBottom
label={ __( 'Name' ) }
value={ name }
onChange={ setName }
required
/>
<HStack justify="right">
<Button variant="tertiary" onClick={ onRequestClose }>
{ __( 'Cancel' ) }
</Button>
<Button
variant="primary"
type="submit"
aria-disabled={
! name || name === category.name || isSaving
}
isBusy={ isSaving }
>
{ __( 'Save' ) }
</Button>
</HStack>
</VStack>
</form>
</Modal>
);
}
Loading

0 comments on commit ef21f20

Please sign in to comment.