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

Patterns: Fix pattern categories on import #58926

Merged
merged 2 commits into from
Feb 13, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 27 additions & 11 deletions packages/edit-site/src/components/add-new-pattern/index.js
Original file line number Diff line number Diff line change
@@ -25,10 +25,11 @@ import {
PATTERN_DEFAULT_CATEGORY,
TEMPLATE_PART_POST_TYPE,
} from '../../utils/constants';
import usePatternCategories from '../sidebar-navigation-screen-patterns/use-pattern-categories';

const { useHistory, useLocation } = unlock( routerPrivateApis );
const { CreatePatternModal } = unlock( editPatternsPrivateApis );
const { CreatePatternModal, useAddPatternCategory } = unlock(
editPatternsPrivateApis
);

export default function AddNewPattern() {
const history = useHistory();
@@ -43,7 +44,6 @@ export default function AddNewPattern() {
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const patternUploadInputRef = useRef();
const { patternCategories } = usePatternCategories();

function handleCreatePattern( { pattern, categoryId } ) {
setShowPatternModal( false );
@@ -97,6 +97,7 @@ export default function AddNewPattern() {
title: __( 'Import pattern from JSON' ),
} );

const { categoryMap, findOrCreateTerm } = useAddPatternCategory();
return (
<>
<DropdownMenu
@@ -132,12 +133,23 @@ export default function AddNewPattern() {
const file = event.target.files?.[ 0 ];
if ( ! file ) return;
try {
const currentCategoryId =
params.categoryType !== TEMPLATE_PART_POST_TYPE &&
patternCategories.find(
( category ) =>
category.name === params.categoryId
)?.id;
let currentCategoryId;
// When we're not handling template parts, we should
// add or create the proper pattern category.
if ( params.categoryType !== TEMPLATE_PART_POST_TYPE ) {
const currentCategory = categoryMap
.values()
.find(
( term ) => term.name === params.categoryId
);
if ( !! currentCategory ) {
currentCategoryId =
currentCategory.id ||
( await findOrCreateTerm(
currentCategory.label
) );
}
}
const pattern = await createPatternFromFile(
file,
currentCategoryId
@@ -146,8 +158,12 @@ export default function AddNewPattern() {
);

// Navigate to the All patterns category for the newly created pattern
// if we're not on that page already.
if ( ! currentCategoryId ) {
// if we're not on that page already and if we're not in the `my-patterns`
// category.
if (
! currentCategoryId &&
params.categoryId !== 'my-patterns'
) {
history.push( {
path: `/patterns`,
categoryType: PATTERN_TYPES.theme,
84 changes: 5 additions & 79 deletions packages/patterns/src/components/create-pattern-modal.js
Original file line number Diff line number Diff line change
@@ -10,21 +10,17 @@ import {
ToggleControl,
} from '@wordpress/components';
import { __, _x } from '@wordpress/i18n';
import { useState, useMemo } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { store as coreStore } from '@wordpress/core-data';

/**
* Internal dependencies
*/
import { PATTERN_DEFAULT_CATEGORY, PATTERN_SYNC_TYPES } from '../constants';

/**
* Internal dependencies
*/
import { store as patternsStore } from '../store';
import CategorySelector, { CATEGORY_SLUG } from './category-selector';
import CategorySelector from './category-selector';
import { useAddPatternCategory } from '../private-hooks';
import { unlock } from '../lock-unlock';

export default function CreatePatternModal( {
@@ -59,47 +55,9 @@ export function CreatePatternModalContents( {

const [ isSaving, setIsSaving ] = useState( false );
const { createPattern } = unlock( useDispatch( patternsStore ) );
const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore );
const { createErrorNotice } = useDispatch( noticesStore );

const { corePatternCategories, userPatternCategories } = useSelect(
( select ) => {
const { getUserPatternCategories, getBlockPatternCategories } =
select( coreStore );

return {
corePatternCategories: getBlockPatternCategories(),
userPatternCategories: getUserPatternCategories(),
};
}
);

const categoryMap = useMemo( () => {
// Merge the user and core pattern categories and remove any duplicates.
const uniqueCategories = new Map();
userPatternCategories.forEach( ( category ) => {
uniqueCategories.set( category.label.toLowerCase(), {
label: category.label,
name: category.name,
id: category.id,
} );
} );

corePatternCategories.forEach( ( category ) => {
if (
! uniqueCategories.has( category.label.toLowerCase() ) &&
// There are two core categories with `Post` label so explicitly remove the one with
// the `query` slug to avoid any confusion.
category.name !== 'query'
) {
uniqueCategories.set( category.label.toLowerCase(), {
label: category.label,
name: category.name,
} );
}
} );
return uniqueCategories;
}, [ userPatternCategories, corePatternCategories ] );
const { categoryMap, findOrCreateTerm } = useAddPatternCategory();

async function onCreate( patternTitle, sync ) {
if ( ! title || isSaving ) {
@@ -137,38 +95,6 @@ export function CreatePatternModalContents( {
}
}

/**
* @param {string} term
* @return {Promise<number>} The pattern category id.
*/
async function findOrCreateTerm( term ) {
try {
const existingTerm = categoryMap.get( term.toLowerCase() );
if ( existingTerm && existingTerm.id ) {
return existingTerm.id;
}
// If we have an existing core category we need to match the new user category to the
// correct slug rather than autogenerating it to prevent duplicates, eg. the core `Headers`
// category uses the singular `header` as the slug.
const termData = existingTerm
? { name: existingTerm.label, slug: existingTerm.name }
: { name: term };
const newTerm = await saveEntityRecord(
'taxonomy',
CATEGORY_SLUG,
termData,
{ throwOnError: true }
);
invalidateResolution( 'getUserPatternCategories' );
return newTerm.id;
} catch ( error ) {
if ( error.code !== 'term_exists' ) {
throw error;
}

return error.data.term_id;
}
}
return (
<form
onSubmit={ ( event ) => {
2 changes: 2 additions & 0 deletions packages/patterns/src/private-apis.js
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ import PatternsMenuItems from './components';
import RenamePatternCategoryModal from './components/rename-pattern-category-modal';
import PartialSyncingControls from './components/partial-syncing-controls';
import ResetOverridesControl from './components/reset-overrides-control';
import { useAddPatternCategory } from './private-hooks';
import {
PATTERN_TYPES,
PATTERN_DEFAULT_CATEGORY,
@@ -35,6 +36,7 @@ lock( privateApis, {
RenamePatternCategoryModal,
PartialSyncingControls,
ResetOverridesControl,
useAddPatternCategory,
PATTERN_TYPES,
PATTERN_DEFAULT_CATEGORY,
PATTERN_USER_CATEGORY,
91 changes: 91 additions & 0 deletions packages/patterns/src/private-hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { useMemo } from '@wordpress/element';

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

/**
* Helper hook that creates a Map with the core and user patterns categories
* and removes any duplicates. It's used when we need to create new user
* categories when creating or importing patterns.
* This hook also provides a function to find or create a pattern category.
*
* @return {Object} The merged categories map and the callback function to find or create a category.
*/
export function useAddPatternCategory() {
const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore );
const { corePatternCategories, userPatternCategories } = useSelect(
( select ) => {
const { getUserPatternCategories, getBlockPatternCategories } =
select( coreStore );

return {
corePatternCategories: getBlockPatternCategories(),
userPatternCategories: getUserPatternCategories(),
};
},
[]
);
const categoryMap = useMemo( () => {
// Merge the user and core pattern categories and remove any duplicates.
const uniqueCategories = new Map();
userPatternCategories.forEach( ( category ) => {
uniqueCategories.set( category.label.toLowerCase(), {
label: category.label,
name: category.name,
id: category.id,
} );
} );

corePatternCategories.forEach( ( category ) => {
if (
! uniqueCategories.has( category.label.toLowerCase() ) &&
// There are two core categories with `Post` label so explicitly remove the one with
// the `query` slug to avoid any confusion.
category.name !== 'query'
) {
uniqueCategories.set( category.label.toLowerCase(), {
label: category.label,
name: category.name,
} );
}
} );
return uniqueCategories;
}, [ userPatternCategories, corePatternCategories ] );

async function findOrCreateTerm( term ) {
try {
const existingTerm = categoryMap.get( term.toLowerCase() );
if ( existingTerm?.id ) {
return existingTerm.id;
}
// If we have an existing core category we need to match the new user category to the
// correct slug rather than autogenerating it to prevent duplicates, eg. the core `Headers`
// category uses the singular `header` as the slug.
const termData = existingTerm
? { name: existingTerm.label, slug: existingTerm.name }
: { name: term };
const newTerm = await saveEntityRecord(
'taxonomy',
CATEGORY_SLUG,
termData,
{ throwOnError: true }
);
invalidateResolution( 'getUserPatternCategories' );
return newTerm.id;
} catch ( error ) {
if ( error.code !== 'term_exists' ) {
throw error;
}
return error.data.term_id;
}
}

return { categoryMap, findOrCreateTerm };
}

Unchanged files with check annotations Beta

( node, propName ) => ( node as any )[ propName ],
name
);
expect( [ type, hydratedAttr ] ).toEqual( [

Check failure on line 218 in test/e2e/specs/interactivity/directive-bind.spec.ts

GitHub Actions / Playwright - 6

[chromium] › interactivity/directive-bind.spec.ts:203:8 › data-wp-bind › attribute hydration › aria-disabled is correctly hydrated for different values

1) [chromium] › interactivity/directive-bind.spec.ts:203:8 › data-wp-bind › attribute hydration › aria-disabled is correctly hydrated for different values Error: expect(received).toEqual(expected) // deep equality - Expected - 1 + Received + 1 Array [ "false", - "false", + null, ] 216 | name 217 | ); > 218 | expect( [ type, hydratedAttr ] ).toEqual( [ | ^ 219 | type, 220 | attrValue, 221 | ] ); at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/interactivity/directive-bind.spec.ts:218:39
type,
attrValue,
] );