Skip to content
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

Blocks: refactor deletion warnings dialog #58952

Merged
merged 9 commits into from
Feb 22, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
Button,
__experimentalHStack as HStack,
} from '@wordpress/components';
import { __, _n } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
Expand All @@ -17,10 +17,9 @@ import { store as blockEditorStore } from '../../store';
import { unlock } from '../../lock-unlock';

export function BlockRemovalWarningModal( { rules } ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still want to keep this component in "block-editor"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok with moving it, but then we still have the private actions, selectors and reducers for block removal warnings in the block editor package that are all the same family of APIs. I feel like it makes sense to keep those things together.

We could consider gradually moving it all across, but it'd mean changing how this feature integrates with the removeBlocks action pretty significantly. Will have a think about whether that's possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, let's keep it then :)

const { clientIds, selectPrevious, blockNamesForPrompt, messageType } =
useSelect( ( select ) =>
unlock( select( blockEditorStore ) ).getRemovalPromptData()
);
const { clientIds, selectPrevious, message } = useSelect( ( select ) =>
unlock( select( blockEditorStore ) ).getRemovalPromptData()
);

const {
clearBlockRemovalPrompt,
Expand All @@ -37,23 +36,10 @@ export function BlockRemovalWarningModal( { rules } ) {
};
}, [ rules, setBlockRemovalRules ] );

if ( ! blockNamesForPrompt ) {
if ( ! message ) {
return;
}

const message =
messageType === 'templates'
? _n(
'Deleting this block will stop your post or page content from displaying on this template. It is not recommended.',
'Deleting these blocks will stop your post or page content from displaying on this template. It is not recommended.',
blockNamesForPrompt.length
)
: _n(
'Deleting this block could break patterns on your site that have content linked to it. Are you sure you want to delete it?',
'Deleting these blocks could break patterns on your site that have content linked to them. Are you sure you want to delete them?',
blockNamesForPrompt.length
);

const onConfirmRemoval = () => {
privateRemoveBlocks( clientIds, selectPrevious, /* force */ true );
clearBlockRemovalPrompt();
Expand Down
103 changes: 34 additions & 69 deletions packages/block-editor/src/store/private-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,61 +123,36 @@ export const privateRemoveBlocks =
//
// @see https://github.com/WordPress/gutenberg/pull/51145
const rules = ! forceRemove && select.getBlockRemovalRules();
if ( rules ) {
const blockNamesForPrompt = new Set();

// Given a list of client IDs of blocks that the user intended to
// remove, perform a tree search (BFS) to find all block names
// corresponding to "important" blocks, i.e. blocks that require a
// removal prompt.
const queue = [ ...clientIds ];
let messageType = 'templates';
while ( queue.length ) {
const clientId = queue.shift();
const blockName = select.getBlockName( clientId );
if ( rules[ blockName ] ) {
blockNamesForPrompt.add( blockName );
}

if ( rules[ 'bindings/core/pattern-overrides' ] ) {
const parentPatternBlocks =
select.getBlockParentsByBlockName(
clientId,
'core/block'
);
// We only need to run this check when editing the original pattern, not pattern instances.
if ( parentPatternBlocks?.length > 0 ) {
continue;
}
const blockAttributes =
select.getBlockAttributes( clientId );
if (
blockAttributes?.metadata?.bindings &&
JSON.stringify(
blockAttributes.metadata.bindings
).includes( 'core/pattern-overrides' )
) {
blockNamesForPrompt.add( blockName );
messageType = 'patternOverrides';
}
if ( rules ) {
function flattenBlocks( blocks ) {
const result = [];
const stack = [ ...blocks ];
while ( stack.length ) {
const { innerBlocks, ...block } = stack.shift();
stack.push( ...innerBlocks );
result.push( block );
}

const innerBlocks = select.getBlockOrder( clientId );
queue.push( ...innerBlocks );
return result;
}

// If any such blocks were found, trigger the removal prompt and
// skip any other steps (thus postponing actual removal).
if ( blockNamesForPrompt.size ) {
dispatch(
displayBlockRemovalPrompt(
clientIds,
selectPrevious,
Array.from( blockNamesForPrompt ),
messageType
)
);
return;
const blockList = clientIds.map( select.getBlock );
const flattenedBlocks = flattenBlocks( blockList );

// Find the first message and use it.
let message;
for ( const rule of rules ) {
message = rule.callback( flattenedBlocks );
if ( message ) {
dispatch(
displayBlockRemovalPrompt(
clientIds,
selectPrevious,
message
)
);
return;
}
}
}

Expand Down Expand Up @@ -228,31 +203,21 @@ export const ensureDefaultBlock =
*
* Contrast with `setBlockRemovalRules`.
*
* @param {string|string[]} clientIds Client IDs of blocks to remove.
* @param {boolean} selectPrevious True if the previous block
* or the immediate parent
* (if no previous block exists)
* should be selected
* when a block is removed.
* @param {string[]} blockNamesForPrompt Names of the blocks that
* triggered the need for
* confirmation before removal.
* @param {string} messageType The type of message to display.
* @param {string|string[]} clientIds Client IDs of blocks to remove.
* @param {boolean} selectPrevious True if the previous block or the
* immediate parent (if no previous
* block exists) should be selected
* when a block is removed.
* @param {string} message Message to display in the prompt.
*
* @return {Object} Action object.
*/
function displayBlockRemovalPrompt(
clientIds,
selectPrevious,
blockNamesForPrompt,
messageType
) {
function displayBlockRemovalPrompt( clientIds, selectPrevious, message ) {
return {
type: 'DISPLAY_BLOCK_REMOVAL_PROMPT',
clientIds,
selectPrevious,
blockNamesForPrompt,
messageType,
message,
};
}

Expand Down
10 changes: 2 additions & 8 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1521,17 +1521,11 @@ export function isSelectionEnabled( state = true, action ) {
function removalPromptData( state = false, action ) {
switch ( action.type ) {
case 'DISPLAY_BLOCK_REMOVAL_PROMPT':
const {
clientIds,
selectPrevious,
blockNamesForPrompt,
messageType,
} = action;
const { clientIds, selectPrevious, message } = action;
return {
clientIds,
selectPrevious,
blockNamesForPrompt,
messageType,
message,
};
case 'CLEAR_BLOCK_REMOVAL_PROMPT':
return false;
Expand Down
11 changes: 0 additions & 11 deletions packages/edit-post/src/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import { useMemo } from '@wordpress/element';
import { SlotFillProvider } from '@wordpress/components';
import { store as coreStore } from '@wordpress/core-data';
import { CommandMenu } from '@wordpress/commands';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
Expand All @@ -25,14 +23,6 @@ import { unlock } from './lock-unlock';
import useNavigateToEntityRecord from './hooks/use-navigate-to-entity-record';

const { ExperimentalEditorProvider } = unlock( editorPrivateApis );
const { BlockRemovalWarningModal } = unlock( blockEditorPrivateApis );
// Prevent accidental removal of certain blocks, asking the user for
// confirmation.
const blockRemovalRules = {
'bindings/core/pattern-overrides': __(
'Blocks from synced patterns that can have overriden content.'
),
};

function Editor( {
postId: initialPostId,
Expand Down Expand Up @@ -108,7 +98,6 @@ function Editor( {
<CommandMenu />
<EditorInitialization postId={ currentPost.postId } />
<Layout initialPost={ initialPost } />
<BlockRemovalWarningModal rules={ blockRemovalRules } />
</ErrorBoundary>
<PostLockedModal />
</ExperimentalEditorProvider>
Expand Down
20 changes: 0 additions & 20 deletions packages/edit-site/src/components/editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
BlockBreadcrumb,
BlockToolbar,
store as blockEditorStore,
privateApis as blockEditorPrivateApis,
BlockInspector,
} from '@wordpress/block-editor';
import {
Expand Down Expand Up @@ -56,7 +55,6 @@ import SiteEditorCanvas from '../block-editor/site-editor-canvas';
import TemplatePartConverter from '../template-part-converter';
import { useSpecificEditorSettings } from '../block-editor/use-site-editor-settings';

const { BlockRemovalWarningModal } = unlock( blockEditorPrivateApis );
const {
ExperimentalEditorProvider: EditorProvider,
InserterSidebar,
Expand All @@ -74,21 +72,6 @@ const interfaceLabels = {
footer: __( 'Editor footer' ),
};

// Prevent accidental removal of certain blocks, asking the user for
// confirmation.
const blockRemovalRules = {
'core/query': __( 'Query Loop displays a list of posts or pages.' ),
'core/post-content': __(
'Post Content displays the content of a post or page.'
),
'core/post-template': __(
'Post Template displays each post or page in a Query Loop.'
),
'bindings/core/pattern-overrides': __(
'Blocks from synced patterns that can have overriden content.'
),
};

export default function Editor( { isLoading } ) {
const {
record: editedPost,
Expand Down Expand Up @@ -243,9 +226,6 @@ export default function Editor( { isLoading } ) {
<BlockToolbar hideDragHandle />
) }
<SiteEditorCanvas />
<BlockRemovalWarningModal
rules={ blockRemovalRules }
/>
<PatternModal />
</>
) }
Expand Down
92 changes: 92 additions & 0 deletions packages/editor/src/components/block-removal-warnings/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* WordPress dependencies
*/

import { _n } from '@wordpress/i18n';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { useMemo } from '@wordpress/element';

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

const { BlockRemovalWarningModal } = unlock( blockEditorPrivateApis );

// Prevent accidental removal of certain blocks, asking the user for confirmation first.
const TEMPLATE_BLOCKS = [
'core/post-content',
'core/post-template',
'core/query',
];
const BLOCK_REMOVAL_RULES = [
{
// Template blocks.
// The warning is only shown when a user manipulates templates or template parts.
postTypes: [ 'wp_template', 'wp_template_part' ],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this message needs to display when editing any other post types than the ones specified here? Or maybe it should be universal (happens for any post type).

callback( removedBlocks ) {
const removedTemplateBlocks = removedBlocks.filter( ( { name } ) =>
TEMPLATE_BLOCKS.includes( name )
);
if ( removedTemplateBlocks.length ) {
return _n(
'Deleting this block will stop your post or page content from displaying on this template. It is not recommended.',
'Some of the deleted blocks will stop your post or page content from displaying on this template. It is not recommended.',
Copy link
Contributor

@talldan talldan Feb 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I revised the copy that was in trunk as we don't know that all of the deleted blocks are dangerous to be removed, it could be four paragraphs and one query loop.

We could also go through all the permutations to make sure the messaging is completely correct, but I thought this would be sufficient.

removedBlocks.length
);
}
},
},
{
// Pattern overrides.
// The warning is only shown when the user edits a pattern.
postTypes: [ 'wp_block' ],
callback( removedBlocks ) {
const removedBlocksWithOverrides = removedBlocks.filter(
( { attributes } ) =>
attributes?.metadata?.bindings &&
Object.values( attributes.metadata.bindings ).some(
( binding ) =>
binding.source === 'core/pattern-overrides'
)
);
if ( removedBlocksWithOverrides.length ) {
return _n(
'The deleted block allows instance overrides. Removing it may result in content not displaying where this pattern is used. Are you sure you want to proceed?',
'Some of the deleted blocks allow instance overrides. Removing them may result in content not displaying where this pattern is used. Are you sure you want to proceed?',
removedBlocks.length
);
}
},
},
];

export default function BlockRemovalWarnings() {
const currentPostType = useSelect(
( select ) => select( editorStore ).getCurrentPostType(),
[]
);

const removalRulesForPostType = useMemo(
() =>
BLOCK_REMOVAL_RULES.filter( ( rule ) =>
rule.postTypes.includes( currentPostType )
),
[ currentPostType ]
);

// `BlockRemovalWarnings` is rendered in the editor provider, a shared component
// across react native and web. However, `BlockRemovalWarningModal` is web only.
// Check it exists before trying to render it.
if ( ! BlockRemovalWarningModal ) {
return null;
}

if ( ! removalRulesForPostType ) {
return null;
}

return <BlockRemovalWarningModal rules={ removalRulesForPostType } />;
}
2 changes: 2 additions & 0 deletions packages/editor/src/components/provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import DisableNonPageContentBlocks from './disable-non-page-content-blocks';
import NavigationBlockEditingMode from './navigation-block-editing-mode';
import { useHideBlocksFromInserter } from './use-hide-blocks-from-inserter';
import useCommands from '../commands';
import BlockRemovalWarnings from '../block-removal-warnings';

const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis );
const { PatternsMenuItems } = unlock( editPatternsPrivateApis );
Expand Down Expand Up @@ -264,6 +265,7 @@ export const ExperimentalEditorProvider = withRegistryProvider(
{ type === 'wp_navigation' && (
<NavigationBlockEditingMode />
) }
<BlockRemovalWarnings />
</BlockEditorProviderComponent>
</BlockContextProvider>
</EntityProvider>
Expand Down
Loading
Loading