Skip to content

Commit

Permalink
Migrate block editor insert usage to preferences store
Browse files Browse the repository at this point in the history
Update tests

Make updateInsertUsage a proper action that can be unit tested

Fix reusable block tests

Update test

Try fixing private actions with new store registration

Update tests

Add special handling for the insertUsage migration as it was performed later than others

Remove unused function

Add mark next change as expensive action to preferences store

Update debounce function to handle a longer debounce for expensive changes

Mark the insertUsage preference change as expensive

Make expensive calls use a trailing edge debounce

Fix duplicate keys in tests

Improve trailing edge test

Fix tests, and ensure options object is optional

Make updateInsertUsage a private API

Make markNextChangeAsExpensive a private API

Update docs

Update package-lock

Opt-in preferences as a core module using private apis

Do not unlock what is already unlocked

Remove time property from INSERT_BLOCKS and REPLACE_BLOCKS action objects

Rename `match` to `variation`

Rename file to create-async-debouncer

Add an extra param for defining the debounce of expensive requests

Add a default value of `false` for the `isExpensive` option

Make `__unstableGetInsertUsage` a private selector called `getInsertUsage`. Remove `__unstableGetInsertUsageForBlock`

Only run the migration when needed

Keep the `insertUsage` data at a maximum of 100 records

Fix linting issues

Update docs
  • Loading branch information
talldan committed Jul 15, 2024
1 parent ac01698 commit 6eb84df
Show file tree
Hide file tree
Showing 25 changed files with 723 additions and 384 deletions.
5 changes: 2 additions & 3 deletions packages/block-editor/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -373,11 +373,11 @@ export const replaceBlocks =
type: 'REPLACE_BLOCKS',
clientIds,
blocks,
time: Date.now(),
indexToSelect,
initialPosition,
meta,
} );
dispatch.updateInsertUsage( blocks );
// To avoid a focus loss when removing the last block, assure there is
// always a default block if the last of the blocks have been removed.
dispatch.ensureDefaultBlock();
Expand Down Expand Up @@ -572,12 +572,12 @@ export const insertBlocks =
}
}
if ( allowedBlocks.length ) {
dispatch.updateInsertUsage( blocks );
dispatch( {
type: 'INSERT_BLOCKS',
blocks: allowedBlocks,
index,
rootClientId,
time: Date.now(),
updateSelection,
initialPosition: updateSelection ? initialPosition : null,
meta,
Expand Down Expand Up @@ -1394,7 +1394,6 @@ export function replaceInnerBlocks(
blocks,
updateSelection,
initialPosition: updateSelection ? initialPosition : null,
time: Date.now(),
};
}

Expand Down
4 changes: 0 additions & 4 deletions packages/block-editor/src/store/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
*/
import { __, _x } from '@wordpress/i18n';

export const PREFERENCES_DEFAULTS = {
insertUsage: {},
};

/**
* The default editor settings
*
Expand Down
7 changes: 2 additions & 5 deletions packages/block-editor/src/store/defaults.native.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
/**
* Internal dependencies
*/
import {
PREFERENCES_DEFAULTS,
SETTINGS_DEFAULTS as SETTINGS,
} from './defaults.js';
import { SETTINGS_DEFAULTS as SETTINGS } from './defaults.js';

const SETTINGS_DEFAULTS = {
...SETTINGS,
Expand All @@ -20,4 +17,4 @@ const SETTINGS_DEFAULTS = {
},
};

export { PREFERENCES_DEFAULTS, SETTINGS_DEFAULTS };
export { SETTINGS_DEFAULTS };
20 changes: 2 additions & 18 deletions packages/block-editor/src/store/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
import { createReduxStore, registerStore } from '@wordpress/data';
import { createReduxStore, register } from '@wordpress/data';

/**
* Internal dependencies
Expand Down Expand Up @@ -32,24 +32,8 @@ export const storeConfig = {
*/
export const store = createReduxStore( STORE_NAME, {
...storeConfig,
persist: [ 'preferences' ],
} );

// We will be able to use the `register` function once we switch
// the "preferences" persistence to use the new preferences package.
const registeredStore = registerStore( STORE_NAME, {
...storeConfig,
persist: [ 'preferences' ],
} );
unlock( registeredStore ).registerPrivateActions( privateActions );
unlock( registeredStore ).registerPrivateSelectors( privateSelectors );

// TODO: Remove once we switch to the `register` function (see above).
//
// Until then, private functions also need to be attached to the original
// `store` descriptor in order to avoid unit tests failing, which could happen
// when tests create new registries in which they register stores.
//
// @see https://github.com/WordPress/gutenberg/pull/51145#discussion_r1239999590
unlock( store ).registerPrivateActions( privateActions );
unlock( store ).registerPrivateSelectors( privateSelectors );
register( store );
70 changes: 70 additions & 0 deletions packages/block-editor/src/store/private-actions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/**
* WordPress dependencies
*/
import { store as blocksStore } from '@wordpress/blocks';
import { Platform } from '@wordpress/element';
import { store as preferencesStore } from '@wordpress/preferences';

/**
* Internal dependencies
Expand Down Expand Up @@ -382,3 +384,71 @@ export const modifyContentLockBlock =
focusModeToRevert
);
};

/**
* Updates the inserter usage statistics in the preferences store.
*
* Note: this function is an internal and not intended to ever be made
* non-private.
*
* @param {Array} blocks The array of blocks that were inserted.
*/
export const updateInsertUsage =
( blocks ) =>
( { registry } ) => {
const previousInsertUsage =
registry.select( preferencesStore ).get( 'core', 'insertUsage' ) ??
{};

const time = Date.now();

let updatedInsertUsage = blocks.reduce( ( previousState, block ) => {
const { attributes, name: blockName } = block;
let id = blockName;
const variation = registry
.select( blocksStore )
.getActiveBlockVariation( blockName, attributes );

if ( variation?.name ) {
id += '/' + variation.name;
}

if ( blockName === 'core/block' ) {
id += '/' + attributes.ref;
}

const previousCount = previousState?.[ id ]?.count ?? 0;

return {
...previousState,
[ id ]: {
time,
count: previousCount + 1,
},
};
}, previousInsertUsage );

// Ensure the list of blocks doesn't grow above `limit` items.
// This is to ensure the preferences store data doesn't grow too big
// given it's persisted in the database.
const limit = 100;
const entries = Object.entries( updatedInsertUsage );
if ( entries.length > limit ) {
// Most recently inserted blocks first.
entries.sort( ( entryLeft, entryRight ) => {
const [ , { time: timeLeft } ] = entryLeft;
const [ , { time: timeRight } ] = entryRight;
return timeRight - timeLeft;
} );

// Slice an array of items that are the newest and convert them
// back into object form.
const entriesToKeep = entries.slice( 0, limit );
updatedInsertUsage = Object.fromEntries( entriesToKeep );
}

registry.dispatch( preferencesStore );
registry
.dispatch( preferencesStore )
.set( 'core', 'insertUsage', updatedInsertUsage );
};
19 changes: 19 additions & 0 deletions packages/block-editor/src/store/private-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* WordPress dependencies
*/
import { createSelector, createRegistrySelector } from '@wordpress/data';
import { store as preferencesStore } from '@wordpress/preferences';

/**
* Internal dependencies
Expand Down Expand Up @@ -31,6 +32,8 @@ import {

export { getBlockSettings } from './get-block-settings';

const EMPTY_OBJECT = {};

/**
* Returns true if the block interface is hidden, or false otherwise.
*
Expand Down Expand Up @@ -511,3 +514,19 @@ export function getTemporarilyEditingAsBlocks( state ) {
export function getTemporarilyEditingFocusModeToRevert( state ) {
return state.temporarilyEditingFocusModeRevert;
}

/**
* Return all insert usage stats.
*
* This is only exported since registry selectors need to be exported. It's marked
* as unstable so that it's not considered part of the public API.
*
* @return {Object<string,Object>} An object with an `id` key representing the type
* of block and an object value that contains
* block insertion statistics.
*/
export const getInsertUsage = createRegistrySelector(
( registrySelect ) => () =>
registrySelect( preferencesStore ).get( 'core', 'insertUsage' ) ??
EMPTY_OBJECT
);
59 changes: 3 additions & 56 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import fastDeepEqual from 'fast-deep-equal/es6';
* WordPress dependencies
*/
import { pipe } from '@wordpress/compose';
import { combineReducers, select } from '@wordpress/data';
import { store as blocksStore } from '@wordpress/blocks';
import { combineReducers } from '@wordpress/data';

/**
* Internal dependencies
*/
import { PREFERENCES_DEFAULTS, SETTINGS_DEFAULTS } from './defaults';
import { SETTINGS_DEFAULTS } from './defaults';
import { insertAt, moveTo } from './array';

const identity = ( x ) => x;
Expand Down Expand Up @@ -1676,58 +1676,6 @@ export function settings( state = SETTINGS_DEFAULTS, action ) {
return state;
}

/**
* Reducer returning the user preferences.
*
* @param {Object} state Current state.
* @param {Object} action Dispatched action.
*
* @return {string} Updated state.
*/
export function preferences( state = PREFERENCES_DEFAULTS, action ) {
switch ( action.type ) {
case 'INSERT_BLOCKS':
case 'REPLACE_BLOCKS': {
const nextInsertUsage = action.blocks.reduce(
( prevUsage, block ) => {
const { attributes, name: blockName } = block;
let id = blockName;
// If a block variation match is found change the name to be the same with the
// one that is used for block variations in the Inserter (`getItemFromVariation`).
const match = select( blocksStore ).getActiveBlockVariation(
blockName,
attributes
);
if ( match?.name ) {
id += '/' + match.name;
}
if ( blockName === 'core/block' ) {
id += '/' + attributes.ref;
}

return {
...prevUsage,
[ id ]: {
time: action.time,
count: prevUsage[ id ]
? prevUsage[ id ].count + 1
: 1,
},
};
},
state.insertUsage
);

return {
...state,
insertUsage: nextInsertUsage,
};
}
}

return state;
}

/**
* Reducer returning an object where each key is a block client ID, its value
* representing the settings for its nested blocks.
Expand Down Expand Up @@ -2083,7 +2031,6 @@ const combinedReducers = combineReducers( {
insertionPoint,
template,
settings,
preferences,
lastBlockAttributesChange,
lastFocus,
editorMode,
Expand Down
28 changes: 9 additions & 19 deletions packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { unlock } from '../lock-unlock';

import {
getContentLockingParent,
getInsertUsage,
getTemporarilyEditingAsBlocks,
getTemporarilyEditingFocusModeToRevert,
} from './private-selectors';
Expand Down Expand Up @@ -1819,20 +1820,6 @@ export function canLockBlockType( state, nameOrType ) {
return !! state.settings?.canLockBlocks;
}

/**
* Returns information about how recently and frequently a block has been inserted.
*
* @param {Object} state Global application state.
* @param {string} id A string which identifies the insert, e.g. 'core/block/12'
*
* @return {?{ time: number, count: number }} An object containing `time` which is when the last
* insert occurred as a UNIX epoch, and `count` which is
* the number of inserts that have occurred.
*/
function getInsertUsage( state, id ) {
return state.preferences.insertUsage?.[ id ] ?? null;
}

/**
* Returns whether we can show a block type in the inserter
*
Expand All @@ -1859,7 +1846,8 @@ const canIncludeBlockTypeInInserter = ( state, blockType, rootClientId ) => {
*/
const getItemFromVariation = ( state, item ) => ( variation ) => {
const variationId = `${ item.id }/${ variation.name }`;
const { time, count = 0 } = getInsertUsage( state, variationId ) || {};
const insertUsage = getInsertUsage();
const { time, count = 0 } = insertUsage?.[ variationId ] ?? {};
return {
...item,
id: variationId,
Expand Down Expand Up @@ -1934,7 +1922,8 @@ const buildBlockTypeItem =
).some( ( { name } ) => name === blockType.name );
}

const { time, count = 0 } = getInsertUsage( state, id ) || {};
const insertUsage = getInsertUsage();
const { time, count = 0 } = insertUsage?.[ id ] ?? {};
const blockItemBase = {
id,
name: blockType.name,
Expand Down Expand Up @@ -2003,7 +1992,8 @@ export const getInserterItems = createRegistrySelector( ( select ) =>
}
: symbol;
const id = `core/block/${ reusableBlock.id }`;
const { time, count = 0 } = getInsertUsage( state, id ) || {};
const insertUsage = getInsertUsage();
const { time, count = 0 } = insertUsage?.[ id ] ?? {};
const frecency = calculateFrecency( time, count );

return {
Expand Down Expand Up @@ -2147,7 +2137,7 @@ export const getInserterItems = createRegistrySelector( ( select ) =>
getBlockTypes(),
unlock( select( STORE_NAME ) ).getReusableBlocks(),
state.blocks.order,
state.preferences.insertUsage,
getInsertUsage(),
...getInsertBlockTypeDependants( state, rootClientId ),
]
)
Expand Down Expand Up @@ -2214,7 +2204,7 @@ export const getBlockTransformItems = createSelector(
},
( state, blocks, rootClientId ) => [
getBlockTypes(),
state.preferences.insertUsage,
getInsertUsage(),
...getInsertBlockTypeDependants( state, rootClientId ),
]
);
Expand Down
Loading

0 comments on commit 6eb84df

Please sign in to comment.