/
githubmirror
/
gutenberg
Обзор
Документация
Войти
/
githubmirror
/
gutenberg
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
trunk
packages/core-data/src/queried-data/reducer.js
307 строк
9 KB
Marco Ciampini
ESLint: Replace strict config with bulk suppressions (#81248)
07 авг 2026, 14:34
Не верифицирован
07 авг 2026, 14:34
7f43eaf
Код
Авторство
О чём код?
import { combineReducers, keyedReducer } from '@wordpress/data'; import { compose } from '@wordpress/compose'; import { conservativeMapItem, ifMatchingAction, replaceAction } from '../utils'; import { DEFAULT_ENTITY_KEY } from '../entities'; import getQueryParts from './get-query-parts'; function getContextFromAction( action ) { const { query } = action; if ( ! query ) { return 'default'; } const queryParts = getQueryParts( query ); return queryParts.context; } /** * Returns a merged array of item IDs, given details of the received paginated * items. The array is sparse-like with `undefined` entries where holes exist. * * @param {number[]|undefined} itemIds Original item IDs (default empty array). * @param {number[]} nextItemIds Item IDs to merge. * @param {Object} options Options object. * @param {number} [options.page] Page of items merged. * @param {number} [options.offset] Offset of items merged. * @param {number} options.perPage Number of items per page. * * @return {number[]} Merged array of item IDs. */ export function getMergedItemIds( itemIds = [], nextItemIds, // The defaults for `page` and `perPage` are the same as in `getQueryParts`. { page = 1, offset, perPage = 10 } = {} ) { // If the query is unbounded, then `nextItemIds` is a complete replacement. if ( perPage === -1 ) { return nextItemIds; } const nextItemIdsStartIndex = offset ?? ( page - 1 ) * perPage; const nextItemIdsRange = Math.max( perPage, nextItemIds.length ); // If later page has already been received, default to the larger known // size of the existing array, else calculate as extending the existing. const size = Math.max( itemIds.length, nextItemIdsStartIndex + nextItemIds.length ); // Preallocate array since size is known. const mergedItemIds = new Array( size ); for ( let i = 0; i < size; i++ ) { // Preserve existing item ID except for subset of range of next items. // We need to check against the possible maximum upper boundary because // a page could receive fewer than what was previously stored. const isInNextItemsRange = i >= nextItemIdsStartIndex && i < nextItemIdsStartIndex + nextItemIdsRange; if ( isInNextItemsRange ) { mergedItemIds[ i ] = nextItemIds[ i - nextItemIdsStartIndex ]; } else { mergedItemIds[ i ] = itemIds[ i ]; } } return mergedItemIds; } /** * Helper function to filter out entities with certain IDs. * Entities are keyed by their ID. * * @param {Object} entities Entity objects, keyed by entity ID. * @param {Array} ids Entity IDs to filter out. * * @return {Object} Filtered entities. */ function removeEntitiesById( entities, ids ) { return Object.fromEntries( Object.entries( entities ).filter( ( [ id ] ) => ! ids.some( ( itemId ) => { if ( Number.isInteger( itemId ) ) { return itemId === +id; } return itemId === id; } ) ) ); } /** * Reducer tracking items state, keyed by ID. Items are assumed to be normal, * where identifiers are common across all queries. * * @param {Object} state Current state. * @param {Object} action Dispatched action. * * @return {Object} Next state. */ export function items( state = {}, action ) { switch ( action.type ) { case 'RECEIVE_ITEMS': { const context = getContextFromAction( action ); const key = action.key || DEFAULT_ENTITY_KEY; const itemsList = Array.isArray( action.items ) ? action.items : [ action.items ]; return { ...state, [ context ]: { ...state[ context ], ...Object.fromEntries( itemsList.map( ( item ) => [ item?.[ key ], conservativeMapItem( state?.[ context ]?.[ item?.[ key ] ], item ), ] ) ), }, }; } case 'REMOVE_ITEMS': return Object.fromEntries( Object.entries( state ).map( ( [ itemId, contextState ] ) => [ itemId, removeEntitiesById( contextState, action.itemIds ), ] ) ); } return state; } /** * Reducer tracking item completeness, keyed by ID. A complete item is one for * which all fields are known. This is used in supporting `_fields` queries, * where not all properties associated with an entity are necessarily returned. * In such cases, completeness is used as an indication of whether it would be * safe to use queried data for a non-`_fields`-limited request. * * @param {Object<string,Object<string,boolean>>} state Current state. * @param {Object} action Dispatched action. * * @return {Object<string,Object<string,boolean>>} Next state. */ export function itemIsComplete( state = {}, action ) { switch ( action.type ) { case 'RECEIVE_ITEMS': { const context = getContextFromAction( action ); const { query, key = DEFAULT_ENTITY_KEY } = action; const itemsList = Array.isArray( action.items ) ? action.items : [ action.items ]; // An item is considered complete if it is received without an associated // fields query. Ideally, this would be implemented in such a way where the // complete aggregate of all fields would satisfy completeness. Since the // fields are not consistent across all entities, this would require // introspection on the REST schema for each entity to know which fields // compose a complete item for that entity. const queryParts = query ? getQueryParts( query ) : {}; const isCompleteQuery = ! query || ! Array.isArray( queryParts.fields ); return { ...state, [ context ]: { ...state[ context ], ...itemsList.reduce( ( result, item ) => { const itemId = item?.[ key ]; // Defer to completeness if already assigned. Technically the // data may be outdated if receiving items for a field subset. result[ itemId ] = state?.[ context ]?.[ itemId ] || isCompleteQuery; return result; }, {} ), }, }; } case 'REMOVE_ITEMS': return Object.fromEntries( Object.entries( state ).map( ( [ itemId, contextState ] ) => [ itemId, removeEntitiesById( contextState, action.itemIds ), ] ) ); } return state; } /** * Reducer tracking queries state, keyed by stable query key. Each reducer * query object includes `itemIds` and `requestingPageByPerPage`. * * @param {Object} state Current state. * @param {Object} action Dispatched action. * * @return {Object} Next state. */ const receiveQueries = compose( [ // Limit to matching action type so we don't attempt to replace action on // an unhandled action. ifMatchingAction( ( action ) => 'query' in action ), // Inject query parts into action for use both in `keyedReducer` and reducer. replaceAction( ( action ) => { // `ifMatchingAction` still passes on initialization, where state is // undefined and a query is not assigned. Avoid attempting to parse // parts. `keyedReducer` will omit by lack of `stableKey`. if ( action.query ) { return { ...action, ...getQueryParts( action.query ), }; } return action; } ), keyedReducer( 'context' ), // Queries shape is shared, but keyed by query `stableKey` part. Original // reducer tracks only a single query object. keyedReducer( 'stableKey' ), ] )( ( state = {}, action ) => { if ( action.type !== 'RECEIVE_ITEMS' ) { return state; } // Single items don't have page or total count metadata // (only collection query responses do), so skip updating itemIds. if ( ! Array.isArray( action.items ) ) { return state; } const key = action.key ?? DEFAULT_ENTITY_KEY; return { itemIds: getMergedItemIds( state.itemIds, action.items.map( ( item ) => item?.[ key ] ).filter( Boolean ), { page: action.page, offset: action.offset, perPage: action.perPage, } ), meta: action.meta, }; } ); /** * Reducer tracking queries state. * * @param {Object} state Current state. * @param {Object} action Dispatched action. * * @return {Object} Next state. */ const queries = ( state = {}, action ) => { switch ( action.type ) { case 'RECEIVE_ITEMS': return receiveQueries( state, action ); case 'REMOVE_ITEMS': const removedItems = action.itemIds.reduce( ( result, itemId ) => { result[ itemId ] = true; return result; }, {} ); return Object.fromEntries( Object.entries( state ).map( ( [ queryGroup, contextQueries ] ) => [ queryGroup, Object.fromEntries( Object.entries( contextQueries ).map( ( [ query, queryItems ] ) => [ query, { ...queryItems, itemIds: queryItems.itemIds.filter( ( queryId ) => ! removedItems[ queryId ] ), }, ] ) ), ] ) ); default: return state; } }; export default combineReducers( { items, itemIsComplete, queries, } );