UNPKG

@wordpress/block-library

Version:
293 lines (263 loc) 8.04 kB
/** * WordPress dependencies */ import { useMemo, useState, useCallback } from '@wordpress/element'; import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; import { useDispatch, useSelect } from '@wordpress/data'; import { SelectControl, Button, FlexBlock, FlexItem, __experimentalHStack as HStack, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { decodeEntities } from '@wordpress/html-entities'; import { store as noticesStore } from '@wordpress/notices'; import { plus } from '@wordpress/icons'; /** * Internal dependencies */ import { createTemplatePartId } from '../../template-part/edit/utils/create-template-part-id'; import useCreateOverlayTemplatePart from './use-create-overlay'; import DeletedOverlayWarning from './deleted-overlay-warning'; import { NAVIGATION_OVERLAY_TEMPLATE_PART_AREA } from '../constants'; /** * Overlay Template Part Selector component. * * @param {Object} props Component props. * @param {string} props.overlay Currently selected overlay template part ID. * @param {Function} props.setAttributes Function to update block attributes. * @param {Function} props.onNavigateToEntityRecord Function to navigate to template part editor. * @return {JSX.Element} The overlay template part selector component. */ export default function OverlayTemplatePartSelector( { overlay, setAttributes, onNavigateToEntityRecord, } ) { const { records: templateParts, isResolving, hasResolved, } = useEntityRecords( 'postType', 'wp_template_part', { per_page: -1, } ); const { createErrorNotice } = useDispatch( noticesStore ); const currentTheme = useSelect( ( select ) => select( coreStore ).getCurrentTheme()?.stylesheet, [] ); // Track if we're currently creating a new overlay const [ isCreating, setIsCreating ] = useState( false ); // Filter template parts by overlay area const overlayTemplateParts = useMemo( () => { if ( ! templateParts ) { return []; } return templateParts.filter( ( templatePart ) => templatePart.area === NAVIGATION_OVERLAY_TEMPLATE_PART_AREA ); }, [ templateParts ] ); // Hook to create overlay template part const createOverlayTemplatePart = useCreateOverlayTemplatePart( overlayTemplateParts ); // Find the selected template part to get its title const selectedTemplatePart = useMemo( () => { if ( ! overlay || ! overlayTemplateParts ) { return null; } return overlayTemplateParts.find( ( templatePart ) => templatePart.slug === overlay ); }, [ overlay, overlayTemplateParts ] ); // Build options for SelectControl const options = useMemo( () => { const baseOptions = [ { label: __( 'None (default)' ), value: '', }, ]; if ( ! hasResolved || isResolving ) { return baseOptions; } const templatePartOptions = overlayTemplateParts.map( ( templatePart ) => { const label = templatePart.title?.rendered ? decodeEntities( templatePart.title.rendered ) : templatePart.slug; return { label, value: templatePart.slug, }; } ); // If an overlay is selected but not found in the list, add it as a "missing" option if ( overlay && ! selectedTemplatePart ) { templatePartOptions.unshift( { label: sprintf( /* translators: %s: Overlay slug. */ __( '%s (missing)' ), overlay ), value: overlay, } ); } return [ ...baseOptions, ...templatePartOptions ]; }, [ overlayTemplateParts, hasResolved, isResolving, overlay, selectedTemplatePart, ] ); const handleSelectChange = ( value ) => { setAttributes( { overlay: value || undefined, } ); }; const handleEditClick = () => { if ( ! overlay || ! selectedTemplatePart || ! onNavigateToEntityRecord ) { return; } // Resolve the full template part ID using theme // Default to current theme if not set const theme = selectedTemplatePart.theme || currentTheme; const templatePartId = createTemplatePartId( theme, overlay ); onNavigateToEntityRecord( { postId: templatePartId, postType: 'wp_template_part', } ); }; const handleCreateOverlay = useCallback( async () => { try { setIsCreating( true ); const templatePart = await createOverlayTemplatePart(); setAttributes( { overlay: templatePart.slug, } ); // Navigate to the new overlay for editing // Create the full ID using theme and slug if ( onNavigateToEntityRecord ) { const theme = templatePart.theme || currentTheme; const templatePartId = createTemplatePartId( theme, templatePart.slug ); onNavigateToEntityRecord( { postId: templatePartId, postType: 'wp_template_part', } ); } } catch ( error ) { // Error handling pattern matches CreateTemplatePartModalContents. // See: packages/fields/src/components/create-template-part-modal/index.tsx // The 'unknown_error' code check ensures generic error codes don't show // potentially confusing technical messages, instead showing a user-friendly fallback. const errorMessage = error instanceof Error && 'code' in error && error.message && error.code !== 'unknown_error' ? error.message : __( 'An error occurred while creating the overlay.' ); createErrorNotice( errorMessage, { type: 'snackbar' } ); } finally { setIsCreating( false ); } }, [ createOverlayTemplatePart, setAttributes, onNavigateToEntityRecord, createErrorNotice, currentTheme, ] ); const handleClearOverlay = useCallback( () => { setAttributes( { overlay: undefined } ); }, [ setAttributes ] ); const isCreateButtonDisabled = isResolving || isCreating; // Check if the selected overlay is missing (deleted) const isOverlayMissing = useMemo( () => { return ( overlay && hasResolved && ! isResolving && ! selectedTemplatePart ); }, [ overlay, hasResolved, isResolving, selectedTemplatePart ] ); // Build help text const helpText = useMemo( () => { if ( overlayTemplateParts.length === 0 && hasResolved ) { return __( 'No overlays found.' ); } return __( 'Select an overlay for navigation.' ); }, [ overlayTemplateParts.length, hasResolved ] ); // Tooltip/aria-label text for the edit button const editButtonLabel = useMemo( () => { return selectedTemplatePart ? sprintf( /* translators: %s: Overlay title. */ __( 'Edit overlay: %s' ), selectedTemplatePart.title?.rendered ? decodeEntities( selectedTemplatePart.title.rendered ) : selectedTemplatePart.slug ) : __( 'Edit overlay' ); }, [ selectedTemplatePart ] ); return ( <div className="wp-block-navigation__overlay-selector"> <Button size="small" icon={ plus } onClick={ handleCreateOverlay } disabled={ isCreateButtonDisabled } accessibleWhenDisabled isBusy={ isCreating } label={ __( 'Create new overlay template' ) } showTooltip className="wp-block-navigation__overlay-create-button" /> <HStack alignment="flex-start"> <FlexBlock> <SelectControl __next40pxDefaultSize __nextHasNoMarginBottom label={ __( 'Overlay template' ) } value={ overlay || '' } options={ options } onChange={ handleSelectChange } disabled={ isResolving } accessibleWhenDisabled help={ helpText } /> </FlexBlock> { overlay && hasResolved && selectedTemplatePart && ( <FlexItem> <Button __next40pxDefaultSize variant="secondary" onClick={ handleEditClick } disabled={ ! onNavigateToEntityRecord } accessibleWhenDisabled label={ editButtonLabel } showTooltip className="wp-block-navigation__overlay-edit-button" > { __( 'Edit' ) } </Button> </FlexItem> ) } </HStack> { isOverlayMissing && ( <DeletedOverlayWarning onClear={ handleClearOverlay } onCreate={ handleCreateOverlay } isCreating={ isCreating } /> ) } </div> ); }