diff --git a/components/font-size-picker/README.md b/components/font-size-picker/README.md index e3edbdf493911..a7061714465c9 100644 --- a/components/font-size-picker/README.md +++ b/components/font-size-picker/README.md @@ -14,7 +14,7 @@ function MyFontSizePicker() { return ( ( @@ -387,14 +388,17 @@ class ImageEdit extends Component { } } -export default withSelect( ( select, props ) => { - const { getMedia } = select( 'core' ); - const { getEditorSettings } = select( 'core/editor' ); - const { id } = props.attributes; - const { maxWidth } = getEditorSettings(); - - return { - image: id ? getMedia( id ) : null, - maxWidth, - }; -} )( ImageEdit ); +export default compose( [ + withSelect( ( select, props ) => { + const { getMedia } = select( 'core' ); + const { getEditorSettings } = select( 'core/editor' ); + const { id } = props.attributes; + const { maxWidth } = getEditorSettings(); + + return { + image: id ? getMedia( id ) : null, + maxWidth, + }; + } ), + withViewportMatch( { isLargeViewport: 'medium' } ), +] )( ImageEdit ); diff --git a/core-blocks/paragraph/index.js b/core-blocks/paragraph/index.js index 84addf44eb7ee..d7df95c47dc7e 100644 --- a/core-blocks/paragraph/index.js +++ b/core-blocks/paragraph/index.js @@ -233,11 +233,17 @@ class ParagraphBlock extends Component { } } onSplit={ insertBlocksAfter ? ( before, after, ...blocks ) => { - setAttributes( { content: before } ); - insertBlocksAfter( [ - ...blocks, - createBlock( 'core/paragraph', { content: after } ), - ] ); + if ( after ) { + blocks.push( createBlock( name, { content: after } ) ); + } + + insertBlocksAfter( blocks ); + + if ( before ) { + setAttributes( { content: before } ); + } else { + onReplace( [] ); + } } : undefined } diff --git a/core-data/actions.js b/core-data/actions.js index 4730edd73dc33..94ba26f21581f 100644 --- a/core-data/actions.js +++ b/core-data/actions.js @@ -3,23 +3,6 @@ */ import { castArray } from 'lodash'; -/** - * Returns an action object used in signalling that the request for a given - * data type has been made. - * - * @param {string} dataType Data type requested. - * @param {?string} subType Optional data sub-type. - * - * @return {Object} Action object. - */ -export function setRequested( dataType, subType ) { - return { - type: 'SET_REQUESTED', - dataType, - subType, - }; -} - /** * Returns an action object used in signalling that terms have been received * for a given taxonomy. diff --git a/core-data/index.js b/core-data/index.js index 195e0db4e6efd..87f51853b0d4d 100644 --- a/core-data/index.js +++ b/core-data/index.js @@ -12,6 +12,13 @@ import * as actions from './actions'; import * as resolvers from './resolvers'; import { default as entities, getMethodName } from './entities'; +/** + * The reducer key used by core data in store registration. + * + * @type {string} + */ +export const REDUCER_KEY = 'core'; + const createEntityRecordGetter = ( source ) => entities.reduce( ( result, entity ) => { const { kind, name } = entity; const methodName = getMethodName( kind, name ); @@ -22,7 +29,7 @@ const createEntityRecordGetter = ( source ) => entities.reduce( ( result, entity const entityResolvers = createEntityRecordGetter( resolvers ); const entitySelectors = createEntityRecordGetter( selectors ); -const store = registerStore( 'core', { +const store = registerStore( REDUCER_KEY, { reducer, actions, selectors: { ...selectors, ...entitySelectors }, diff --git a/core-data/reducer.js b/core-data/reducer.js index ea86b96907f60..12c83545ffccb 100644 --- a/core-data/reducer.js +++ b/core-data/reducer.js @@ -31,17 +31,6 @@ export function terms( state = {}, action ) { ...state, [ action.taxonomy ]: action.terms, }; - - case 'SET_REQUESTED': - const { dataType, subType: taxonomy } = action; - if ( dataType !== 'terms' || state.hasOwnProperty( taxonomy ) ) { - return state; - } - - return { - ...state, - [ taxonomy ]: null, - }; } return state; diff --git a/core-data/resolvers.js b/core-data/resolvers.js index 96cc471c06e5b..8efe1eb9b777b 100644 --- a/core-data/resolvers.js +++ b/core-data/resolvers.js @@ -7,7 +7,6 @@ import apiRequest from '@wordpress/api-request'; * Internal dependencies */ import { - setRequested, receiveTerms, receiveUserQuery, receiveEntityRecords, @@ -21,7 +20,6 @@ import { getEntity } from './entities'; * progress. */ export async function* getCategories() { - yield setRequested( 'terms', 'categories' ); const categories = await apiRequest( { path: '/wp/v2/categories?per_page=-1' } ); yield receiveTerms( 'categories', categories ); } diff --git a/core-data/selectors.js b/core-data/selectors.js index 9d6da24a2e9dc..c56c096ba5cc6 100644 --- a/core-data/selectors.js +++ b/core-data/selectors.js @@ -1,8 +1,32 @@ /** * External dependencies */ +import createSelector from 'rememo'; import { map } from 'lodash'; +/** + * WordPress dependencies + */ +import { select } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { REDUCER_KEY } from './'; + +/** + * Returns true if resolution is in progress for the core selector of the given + * name and arguments. + * + * @param {string} selectorName Core data selector name. + * @param {...*} args Arguments passed to selector. + * + * @return {boolean} Whether resolution is in progress. + */ +function isResolving( selectorName, ...args ) { + return select( 'core/data' ).isResolving( REDUCER_KEY, selectorName, ...args ); +} + /** * Returns all the available terms for the given taxonomy. * @@ -36,7 +60,7 @@ export function getCategories( state ) { * @return {boolean} Whether a request is in progress for taxonomy's terms. */ export function isRequestingTerms( state, taxonomy ) { - return state.terms[ taxonomy ] === null; + return isResolving( 'getTerms', taxonomy ); } /** @@ -47,8 +71,8 @@ export function isRequestingTerms( state, taxonomy ) { * * @return {boolean} Whether a request is in progress for categories. */ -export function isRequestingCategories( state ) { - return isRequestingTerms( state, 'categories' ); +export function isRequestingCategories() { + return isResolving( 'getCategories' ); } /** @@ -70,11 +94,14 @@ export function getAuthors( state ) { * * @return {Array} Users list. */ -export function getUserQueryResults( state, queryID ) { - const queryResults = state.users.queries[ queryID ]; +export const getUserQueryResults = createSelector( + ( state, queryID ) => { + const queryResults = state.users.queries[ queryID ]; - return map( queryResults, ( id ) => state.users.byId[ id ] ); -} + return map( queryResults, ( id ) => state.users.byId[ id ] ); + }, + ( state, queryID ) => [ state.users.queries[ queryID ], state.users.byId ] +); /** * Returns the Entity's record object by key. diff --git a/core-data/test/reducer.js b/core-data/test/reducer.js index 60b4564d14f91..5d608b025910c 100644 --- a/core-data/test/reducer.js +++ b/core-data/test/reducer.js @@ -27,45 +27,6 @@ describe( 'terms()', () => { categories: [ { id: 1 } ], } ); } ); - - it( 'assigns requested taxonomy to null', () => { - const originalState = deepFreeze( {} ); - const state = terms( originalState, { - type: 'SET_REQUESTED', - dataType: 'terms', - subType: 'categories', - } ); - - expect( state ).toEqual( { - categories: null, - } ); - } ); - - it( 'does not assign requested taxonomy to null if received', () => { - const originalState = deepFreeze( { - categories: [ { id: 1 } ], - } ); - const state = terms( originalState, { - type: 'SET_REQUESTED', - dataType: 'terms', - subType: 'categories', - } ); - - expect( state ).toEqual( { - categories: [ { id: 1 } ], - } ); - } ); - - it( 'does not assign requested taxonomy if not terms data type', () => { - const originalState = deepFreeze( {} ); - const state = terms( originalState, { - type: 'SET_REQUESTED', - dataType: 'foo', - subType: 'categories', - } ); - - expect( state ).toEqual( {} ); - } ); } ); describe( 'entities', () => { diff --git a/core-data/test/resolvers.js b/core-data/test/resolvers.js index 3c4bcca1a7d19..fd996fc261cf5 100644 --- a/core-data/test/resolvers.js +++ b/core-data/test/resolvers.js @@ -7,7 +7,7 @@ import apiRequest from '@wordpress/api-request'; * Internal dependencies */ import { getCategories, getEntityRecord } from '../resolvers'; -import { setRequested, receiveTerms, receiveEntityRecords } from '../actions'; +import { receiveTerms, receiveEntityRecords } from '../actions'; jest.mock( '@wordpress/api-request' ); @@ -24,8 +24,6 @@ describe( 'getCategories', () => { it( 'yields with requested terms', async () => { const fulfillment = getCategories(); - const requested = ( await fulfillment.next() ).value; - expect( requested.type ).toBe( setRequested().type ); const received = ( await fulfillment.next() ).value; expect( received ).toEqual( receiveTerms( 'categories', CATEGORIES ) ); } ); diff --git a/core-data/test/selectors.js b/core-data/test/selectors.js index 12ab66bcea356..28aea5c40971a 100644 --- a/core-data/test/selectors.js +++ b/core-data/test/selectors.js @@ -6,7 +6,13 @@ import deepFreeze from 'deep-freeze'; /** * Internal dependencies */ -import { getTerms, isRequestingTerms, getEntityRecord } from '../selectors'; +import { getTerms, isRequestingCategories, getEntityRecord } from '../selectors'; +import { select } from '@wordpress/data'; + +jest.mock( '@wordpress/data', () => ( { + ...require.requireActual( '@wordpress/data' ), + select: jest.fn().mockReturnValue( {} ), +} ) ); describe( 'getTerms()', () => { it( 'returns value of terms by taxonomy', () => { @@ -24,35 +30,39 @@ describe( 'getTerms()', () => { } ); } ); -describe( 'isRequestingTerms()', () => { - it( 'returns false if never requested', () => { - const state = deepFreeze( { - terms: {}, - } ); +describe( 'isRequestingCategories()', () => { + beforeAll( () => { + select( 'core/data' ).isResolving = jest.fn().mockReturnValue( false ); + } ); - const result = isRequestingTerms( state, 'categories' ); - expect( result ).toBe( false ); + afterAll( () => { + select( 'core/data' ).isResolving.mockRestore(); } ); - it( 'returns false if terms received', () => { - const state = deepFreeze( { - terms: { - categories: [ { id: 1 } ], - }, - } ); + function setIsResolving( isResolving ) { + select( 'core/data' ).isResolving.mockImplementation( + ( reducerKey, selectorName ) => ( + isResolving && + reducerKey === 'core' && + selectorName === 'getCategories' + ) + ); + } - const result = isRequestingTerms( state, 'categories' ); + it( 'returns false if never requested', () => { + const result = isRequestingCategories(); expect( result ).toBe( false ); } ); - it( 'returns true if requesting', () => { - const state = deepFreeze( { - terms: { - categories: null, - }, - } ); + it( 'returns false if categories resolution finished', () => { + setIsResolving( false ); + const result = isRequestingCategories(); + expect( result ).toBe( false ); + } ); - const result = isRequestingTerms( state, 'categories' ); + it( 'returns true if categories resolution started', () => { + setIsResolving( true ); + const result = isRequestingCategories(); expect( result ).toBe( true ); } ); } ); diff --git a/data/index.js b/data/index.js index eaafa51cd4d3c..5b441a912bb87 100644 --- a/data/index.js +++ b/data/index.js @@ -3,7 +3,6 @@ */ import { combineReducers, createStore } from 'redux'; import { flowRight, without, mapValues, overEvery } from 'lodash'; -import EquivalentKeyMap from 'equivalent-key-map'; /** * WordPress dependencies @@ -14,6 +13,8 @@ import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies */ +import registerDataStore from './store'; + export { loadAndPersist, withRehydratation } from './persist'; /** @@ -130,51 +131,30 @@ export function registerSelectors( reducerKey, newSelectors ) { * @param {Object} newResolvers Resolvers to register. */ export function registerResolvers( reducerKey, newResolvers ) { - const createResolver = ( selector, key ) => { + const { hasStartedResolution } = select( 'core/data' ); + const { startResolution, finishResolution } = dispatch( 'core/data' ); + + const createResolver = ( selector, selectorName ) => { // Don't modify selector behavior if no resolver exists. - if ( ! newResolvers.hasOwnProperty( key ) ) { + if ( ! newResolvers.hasOwnProperty( selectorName ) ) { return selector; } const store = stores[ reducerKey ]; // Normalize resolver shape to object. - let resolver = newResolvers[ key ]; + let resolver = newResolvers[ selectorName ]; if ( ! resolver.fulfill ) { resolver = { fulfill: resolver }; } - /** - * To ensure that fulfillment occurs only once per arguments set - * (even for deeply "equivalent" arguments), track calls. - * - * @type {EquivalentKeyMap} - */ - const fulfilledByEquivalentArgs = new EquivalentKeyMap(); - - /** - * Returns true if resolver fulfillment has already occurred for an - * equivalent set of arguments. Includes side effect when returning - * false to ensure the next invocation returns true. - * - * @param {Array} args Arguments set. - * - * @return {boolean} Whether fulfillment has already occurred. - */ - function hasBeenFulfilled( args ) { - const hasArguments = fulfilledByEquivalentArgs.has( args ); - if ( ! hasArguments ) { - fulfilledByEquivalentArgs.set( args, true ); - } - - return hasArguments; - } - async function fulfill( ...args ) { - if ( hasBeenFulfilled( args ) ) { + if ( hasStartedResolution( reducerKey, selectorName, args ) ) { return; } + startResolution( reducerKey, selectorName, args ); + // At this point, selectors have already been pre-bound to inject // state, it would not be otherwise provided to fulfill. const state = store.getState(); @@ -193,6 +173,8 @@ export function registerResolvers( reducerKey, newResolvers ) { store.dispatch( maybeAction ); } } + + finishResolution( reducerKey, selectorName, args ); } if ( typeof resolver.isFulfilled === 'function' ) { @@ -485,3 +467,5 @@ export function toAsyncIterable( object ) { } }() ); } + +registerDataStore(); diff --git a/data/store/actions.js b/data/store/actions.js new file mode 100644 index 0000000000000..a386aae4eca61 --- /dev/null +++ b/data/store/actions.js @@ -0,0 +1,37 @@ +/** + * Returns an action object used in signalling that selector resolution has + * started. + * + * @param {string} reducerKey Registered store reducer key. + * @param {string} selectorName Name of selector for which resolver triggered. + * @param {...*} args Arguments to associate for uniqueness. + * + * @return {Object} Action object. + */ +export function startResolution( reducerKey, selectorName, args ) { + return { + type: 'START_RESOLUTION', + reducerKey, + selectorName, + args, + }; +} + +/** + * Returns an action object used in signalling that selector resolution has + * completed. + * + * @param {string} reducerKey Registered store reducer key. + * @param {string} selectorName Name of selector for which resolver triggered. + * @param {...*} args Arguments to associate for uniqueness. + * + * @return {Object} Action object. + */ +export function finishResolution( reducerKey, selectorName, args ) { + return { + type: 'FINISH_RESOLUTION', + reducerKey, + selectorName, + args, + }; +} diff --git a/data/store/index.js b/data/store/index.js new file mode 100644 index 0000000000000..417babf5a51a8 --- /dev/null +++ b/data/store/index.js @@ -0,0 +1,16 @@ +/** + * Internal dependencies + */ +import { registerStore } from '../'; + +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as actions from './actions'; + +export default function registerDataStore() { + registerStore( 'core/data', { + reducer, + actions, + selectors, + } ); +} diff --git a/data/store/reducer.js b/data/store/reducer.js new file mode 100644 index 0000000000000..378535ce44972 --- /dev/null +++ b/data/store/reducer.js @@ -0,0 +1,38 @@ +/** + * External dependencies + */ +import { flowRight } from 'lodash'; +import EquivalentKeyMap from 'equivalent-key-map'; + +/** + * Internal dependencies + */ +import { onSubKey } from './utils'; + +/** + * Reducer function returning next state for selector resolution, object form: + * + * reducerKey -> selectorName -> EquivalentKeyMap + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @returns {Object} Next state. + */ +const isResolved = flowRight( [ + onSubKey( 'reducerKey' ), + onSubKey( 'selectorName' ), +] )( ( state = new EquivalentKeyMap(), action ) => { + switch ( action.type ) { + case 'START_RESOLUTION': + case 'FINISH_RESOLUTION': + const isStarting = action.type === 'START_RESOLUTION'; + const nextState = new EquivalentKeyMap( state ); + nextState.set( action.args, isStarting ); + return nextState; + } + + return state; +} ); + +export default isResolved; diff --git a/data/store/selectors.js b/data/store/selectors.js new file mode 100644 index 0000000000000..8f5c9d18de2fe --- /dev/null +++ b/data/store/selectors.js @@ -0,0 +1,71 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; + +/** + * Returns the raw `isResolving` value for a given reducer key, selector name, + * and arguments set. May be undefined if the selector has never been resolved + * or not resolved for the given set of arguments, otherwise true or false for + * resolution started and completed respectively. + * + * @param {Object} state Data state. + * @param {string} reducerKey Registered store reducer key. + * @param {string} selectorName Selector name. + * @param {Array} args Arguments passed to selector. + * + * @return {?boolean} isResolving value. + */ +export function getIsResolving( state, reducerKey, selectorName, args ) { + const map = get( state, [ reducerKey, selectorName ] ); + if ( ! map ) { + return; + } + + return map.get( args ); +} + +/** + * Returns true if resolution has already been triggered for a given reducer + * key, selector name, and arguments set. + * + * @param {Object} state Data state. + * @param {string} reducerKey Registered store reducer key. + * @param {string} selectorName Selector name. + * @param {?Array} args Arguments passed to selector (default `[]`). + * + * @return {boolean} Whether resolution has been triggered. + */ +export function hasStartedResolution( state, reducerKey, selectorName, args = [] ) { + return getIsResolving( state, reducerKey, selectorName, args ) !== undefined; +} + +/** + * Returns true if resolution has completed for a given reducer key, selector + * name, and arguments set. + * + * @param {Object} state Data state. + * @param {string} reducerKey Registered store reducer key. + * @param {string} selectorName Selector name. + * @param {?Array} args Arguments passed to selector. + * + * @return {boolean} Whether resolution has completed. + */ +export function hasFinishedResolution( state, reducerKey, selectorName, args = [] ) { + return getIsResolving( state, reducerKey, selectorName, args ) === false; +} + +/** + * Returns true if resolution has been triggered but has not yet completed for + * a given reducer key, selector name, and arguments set. + * + * @param {Object} state Data state. + * @param {string} reducerKey Registered store reducer key. + * @param {string} selectorName Selector name. + * @param {?Array} args Arguments passed to selector. + * + * @return {boolean} Whether resolution is in progress. + */ +export function isResolving( state, reducerKey, selectorName, args = [] ) { + return getIsResolving( state, reducerKey, selectorName, args ) === true; +} diff --git a/data/store/test/reducer.js b/data/store/test/reducer.js new file mode 100644 index 0000000000000..4844420dab750 --- /dev/null +++ b/data/store/test/reducer.js @@ -0,0 +1,47 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import reducer from '../reducer'; + +describe( 'reducer', () => { + it( 'should default to an empty object', () => { + const state = reducer( undefined, {} ); + + expect( state ).toEqual( {} ); + } ); + + it( 'should return with started resolution', () => { + const state = reducer( undefined, { + type: 'START_RESOLUTION', + reducerKey: 'test', + selectorName: 'getFoo', + args: [], + } ); + + // { test: { getFoo: EquivalentKeyMap( [] => true ) } } + expect( state.test.getFoo.get( [] ) ).toBe( true ); + } ); + + it( 'should return with finished resolution', () => { + const original = reducer( undefined, { + type: 'START_RESOLUTION', + reducerKey: 'test', + selectorName: 'getFoo', + args: [], + } ); + const state = reducer( deepFreeze( original ), { + type: 'FINISH_RESOLUTION', + reducerKey: 'test', + selectorName: 'getFoo', + args: [], + } ); + + // { test: { getFoo: EquivalentKeyMap( [] => false ) } } + expect( state.test.getFoo.get( [] ) ).toBe( false ); + } ); +} ); diff --git a/data/store/test/selectors.js b/data/store/test/selectors.js new file mode 100644 index 0000000000000..1af4bf9b8adf1 --- /dev/null +++ b/data/store/test/selectors.js @@ -0,0 +1,120 @@ +/** + * External dependencies + */ +import EquivalentKeyMap from 'equivalent-key-map'; + +/** + * Internal dependencies + */ +import { + getIsResolving, + hasStartedResolution, + hasFinishedResolution, + isResolving, +} from '../selectors'; + +describe( 'getIsResolving', () => { + it( 'should return undefined if no state by reducerKey, selectorName', () => { + const state = {}; + const result = getIsResolving( state, 'test', 'getFoo', [] ); + + expect( result ).toBe( undefined ); + } ); + + it( 'should return undefined if state by reducerKey, selectorName, but not args', () => { + const state = { + test: { + getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), + }, + }; + const result = getIsResolving( state, 'test', 'getFoo', [ 'bar' ] ); + + expect( result ).toBe( undefined ); + } ); + + it( 'should return value by reducerKey, selectorName', () => { + const state = { + test: { + getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), + }, + }; + const result = getIsResolving( state, 'test', 'getFoo', [] ); + + expect( result ).toBe( true ); + } ); +} ); + +describe( 'hasStartedResolution', () => { + it( 'returns false if not has started', () => { + const state = {}; + const result = hasStartedResolution( state, 'test', 'getFoo', [] ); + + expect( result ).toBe( false ); + } ); + + it( 'returns true if has started', () => { + const state = { + test: { + getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), + }, + }; + const result = hasStartedResolution( state, 'test', 'getFoo', [] ); + + expect( result ).toBe( true ); + } ); +} ); + +describe( 'hasFinishedResolution', () => { + it( 'returns false if not has finished', () => { + const state = { + test: { + getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), + }, + }; + const result = hasFinishedResolution( state, 'test', 'getFoo', [] ); + + expect( result ).toBe( false ); + } ); + + it( 'returns true if has finished', () => { + const state = { + test: { + getFoo: new EquivalentKeyMap( [ [ [], false ] ] ), + }, + }; + const result = hasFinishedResolution( state, 'test', 'getFoo', [] ); + + expect( result ).toBe( true ); + } ); +} ); + +describe( 'isResolving', () => { + it( 'returns false if not has started', () => { + const state = {}; + const result = isResolving( state, 'test', 'getFoo', [] ); + + expect( result ).toBe( false ); + } ); + + it( 'returns false if has finished', () => { + const state = { + test: { + getFoo: new EquivalentKeyMap( [ [ [], false ] ] ), + }, + }; + const result = isResolving( state, 'test', 'getFoo', [] ); + + expect( result ).toBe( false ); + } ); + + it( 'returns true if has started but not finished', () => { + const state = { + test: { + getFoo: new EquivalentKeyMap( [ [ [], true ] ] ), + }, + }; + const result = isResolving( state, 'test', 'getFoo', [] ); + + expect( result ).toBe( true ); + } ); +} ); diff --git a/data/store/test/utils.js b/data/store/test/utils.js new file mode 100644 index 0000000000000..97490250d4d3a --- /dev/null +++ b/data/store/test/utils.js @@ -0,0 +1,39 @@ +/** + * Internal dependencies + */ +import { onSubKey } from '../utils'; + +describe( 'onSubKey', () => { + function createEnhancedReducer( actionProperty ) { + const enhanceReducer = onSubKey( actionProperty ); + return enhanceReducer( ( state, action ) => 'Called by ' + action.caller ); + } + + it( 'should default to an empty object', () => { + const reducer = createEnhancedReducer( 'caller' ); + const nextState = reducer( undefined, { type: '@@INIT' } ); + + expect( nextState ).toEqual( {} ); + } ); + + it( 'should ignore actions where property not present', () => { + const state = {}; + const reducer = createEnhancedReducer( 'caller' ); + const nextState = reducer( state, { type: 'DO_FOO' } ); + + expect( nextState ).toBe( state ); + } ); + + it( 'should key by action property', () => { + const reducer = createEnhancedReducer( 'caller' ); + + let state = Object.freeze( {} ); + state = reducer( state, { type: 'DO_FOO', caller: 1 } ); + state = reducer( state, { type: 'DO_FOO', caller: 2 } ); + + expect( state ).toEqual( { + 1: 'Called by 1', + 2: 'Called by 2', + } ); + } ); +} ); diff --git a/data/store/utils.js b/data/store/utils.js new file mode 100644 index 0000000000000..d793855683d65 --- /dev/null +++ b/data/store/utils.js @@ -0,0 +1,28 @@ +/** + * Higher-order reducer creator which creates a combined reducer object, keyed + * by a property on the action object. + * + * @param {string} actionProperty Action property by which to key object. + * + * @return {Function} Higher-order reducer. + */ +export const onSubKey = ( actionProperty ) => ( reducer ) => ( state = {}, action ) => { + // Retrieve subkey from action. Do not track if undefined; useful for cases + // where reducer is scoped by action shape. + const key = action[ actionProperty ]; + if ( key === undefined ) { + return state; + } + + // Avoid updating state if unchanged. Note that this also accounts for a + // reducer which returns undefined on a key which is not yet tracked. + const nextKeyState = reducer( state[ key ], action ); + if ( nextKeyState === state[ key ] ) { + return state; + } + + return { + ...state, + [ key ]: nextKeyState, + }; +}; diff --git a/data/test/index.js b/data/test/index.js index d48bc13720544..7042011e9b006 100644 --- a/data/test/index.js +++ b/data/test/index.js @@ -2,6 +2,7 @@ * External dependencies */ import { mount } from 'enzyme'; +import { castArray } from 'lodash'; /** * WordPress dependencies @@ -32,6 +33,11 @@ jest.mock( '@wordpress/utils', () => ( { deprecated: jest.fn(), } ) ); +// Mock data store to prevent self-initialization, as it needs to be reset +// between tests of `registerResolvers` by replacement (new `registerStore`). +jest.mock( '../store', () => () => {} ); +const registerDataStore = require.requireActual( '../store' ).default; + describe( 'registerStore', () => { it( 'should be shorthand for reducer, actions, selectors registration', () => { const store = registerStore( 'butcher', { @@ -79,6 +85,10 @@ describe( 'registerReducer', () => { } ); describe( 'registerResolvers', () => { + beforeEach( () => { + registerDataStore(); + } ); + const unsubscribes = []; afterEach( () => { let unsubscribe; @@ -93,6 +103,18 @@ describe( 'registerResolvers', () => { return unsubscribe; } + function subscribeUntil( predicates ) { + predicates = castArray( predicates ); + + return new Promise( ( resolve ) => { + subscribeWithUnsubscribe( () => { + if ( predicates.every( ( predicate ) => predicate() ) ) { + resolve(); + } + } ); + } ); + } + it( 'should not do anything for selectors which do not have resolvers', () => { registerReducer( 'demo', ( state = 'OK' ) => state ); registerSelectors( 'demo', { @@ -197,7 +219,7 @@ describe( 'registerResolvers', () => { select( 'demo' ).getPage( 4, {} ); } ); - it( 'should resolve action to dispatch', ( done ) => { + it( 'should resolve action to dispatch', () => { registerReducer( 'demo', ( state = 'NOTOK', action ) => { return action.type === 'SET_OK' ? 'OK' : state; } ); @@ -208,19 +230,17 @@ describe( 'registerResolvers', () => { getValue: () => ( { type: 'SET_OK' } ), } ); - subscribeWithUnsubscribe( () => { - try { - expect( select( 'demo' ).getValue() ).toBe( 'OK' ); - done(); - } catch ( error ) { - done( error ); - } - } ); + const promise = subscribeUntil( [ + () => select( 'demo' ).getValue() === 'OK', + () => select( 'core/data' ).hasFinishedResolution( 'demo', 'getValue' ), + ] ); select( 'demo' ).getValue(); + + return promise; } ); - it( 'should resolve mixed type action array to dispatch', ( done ) => { + it( 'should resolve mixed type action array to dispatch', () => { registerReducer( 'counter', ( state = 0, action ) => { return action.type === 'INCREMENT' ? state + 1 : state; } ); @@ -234,16 +254,17 @@ describe( 'registerResolvers', () => { ], } ); - subscribeWithUnsubscribe( () => { - if ( select( 'counter' ).getCount() === 2 ) { - done(); - } - } ); + const promise = subscribeUntil( [ + () => select( 'counter' ).getCount() === 2, + () => select( 'core/data' ).hasFinishedResolution( 'counter', 'getCount' ), + ] ); select( 'counter' ).getCount(); + + return promise; } ); - it( 'should resolve generator action to dispatch', ( done ) => { + it( 'should resolve generator action to dispatch', () => { registerReducer( 'demo', ( state = 'NOTOK', action ) => { return action.type === 'SET_OK' ? 'OK' : state; } ); @@ -256,19 +277,17 @@ describe( 'registerResolvers', () => { }, } ); - subscribeWithUnsubscribe( () => { - try { - expect( select( 'demo' ).getValue() ).toBe( 'OK' ); - done(); - } catch ( error ) { - done( error ); - } - } ); + const promise = subscribeUntil( [ + () => select( 'demo' ).getValue() === 'OK', + () => select( 'core/data' ).hasFinishedResolution( 'demo', 'getValue' ), + ] ); select( 'demo' ).getValue(); + + return promise; } ); - it( 'should resolve promise action to dispatch', ( done ) => { + it( 'should resolve promise action to dispatch', () => { registerReducer( 'demo', ( state = 'NOTOK', action ) => { return action.type === 'SET_OK' ? 'OK' : state; } ); @@ -279,16 +298,14 @@ describe( 'registerResolvers', () => { getValue: () => Promise.resolve( { type: 'SET_OK' } ), } ); - subscribeWithUnsubscribe( () => { - try { - expect( select( 'demo' ).getValue() ).toBe( 'OK' ); - done(); - } catch ( error ) { - done( error ); - } - } ); + const promise = subscribeUntil( [ + () => select( 'demo' ).getValue() === 'OK', + () => select( 'core/data' ).hasFinishedResolution( 'demo', 'getValue' ), + ] ); select( 'demo' ).getValue(); + + return promise; } ); it( 'should resolve promise non-action to dispatch', ( done ) => { @@ -315,7 +332,7 @@ describe( 'registerResolvers', () => { } ); } ); - it( 'should resolve async iterator action to dispatch', ( done ) => { + it( 'should resolve async iterator action to dispatch', () => { registerReducer( 'counter', ( state = 0, action ) => { return action.type === 'INCREMENT' ? state + 1 : state; } ); @@ -329,16 +346,17 @@ describe( 'registerResolvers', () => { }, } ); - subscribeWithUnsubscribe( () => { - if ( select( 'counter' ).getCount() === 2 ) { - done(); - } - } ); + const promise = subscribeUntil( [ + () => select( 'counter' ).getCount() === 2, + () => select( 'core/data' ).hasFinishedResolution( 'counter', 'getCount' ), + ] ); select( 'counter' ).getCount(); + + return promise; } ); - it( 'should not dispatch resolved promise action on subsequent selector calls', ( done ) => { + it( 'should not dispatch resolved promise action on subsequent selector calls', () => { registerReducer( 'demo', ( state = 'NOTOK', action ) => { return action.type === 'SET_OK' && state === 'NOTOK' ? 'OK' : 'NOTOK'; } ); @@ -349,17 +367,12 @@ describe( 'registerResolvers', () => { getValue: () => Promise.resolve( { type: 'SET_OK' } ), } ); - subscribeWithUnsubscribe( () => { - try { - expect( select( 'demo' ).getValue() ).toBe( 'OK' ); - done(); - } catch ( error ) { - done( error ); - } - } ); + const promise = subscribeUntil( () => select( 'demo' ).getValue() === 'OK' ); select( 'demo' ).getValue(); select( 'demo' ).getValue(); + + return promise; } ); } ); @@ -383,7 +396,7 @@ describe( 'select', () => { } ); describe( 'withSelect', () => { - let wrapper; + let wrapper, store; const unsubscribes = []; afterEach( () => { @@ -399,7 +412,7 @@ describe( 'withSelect', () => { } ); function subscribeWithUnsubscribe( ...args ) { - const unsubscribe = subscribe( ...args ); + const unsubscribe = store.subscribe( ...args ); unsubscribes.push( unsubscribe ); return unsubscribe; } @@ -500,7 +513,7 @@ describe( 'withSelect', () => { // until after the current listener stack is called, we don't attempt // to setState on an unmounting `withSelect` component. It will fail if // an attempt is made to `setState` on an unmounted component. - const store = registerReducer( 'counter', ( state = 0, action ) => { + store = registerReducer( 'counter', ( state = 0, action ) => { if ( action.type === 'increment' ) { return state + 1; } @@ -526,7 +539,7 @@ describe( 'withSelect', () => { } ); it( 'should not rerun selection on unchanging state', () => { - const store = registerReducer( 'unchanging', ( state = {} ) => state ); + store = registerReducer( 'unchanging', ( state = {} ) => state ); registerSelectors( 'unchanging', { getState: ( state ) => state, diff --git a/docs/blocks/introducing-attributes-and-editable-fields.md b/docs/blocks/introducing-attributes-and-editable-fields.md index 509593527acdc..0fa04d5b028cf 100644 --- a/docs/blocks/introducing-attributes-and-editable-fields.md +++ b/docs/blocks/introducing-attributes-and-editable-fields.md @@ -52,6 +52,7 @@ registerBlockType( 'gutenberg-boilerplate-es5/hello-world-step-03', { var content = props.attributes.content; return el( RichText.Content, { + tagName: 'p', className: props.className, value: content } ); @@ -99,7 +100,7 @@ registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-03', { const { content } = attributes; return ( - + ); }, } ); diff --git a/docs/outreach/meetups.md b/docs/outreach/meetups.md index 273db72991006..58cd8bbb72c99 100644 --- a/docs/outreach/meetups.md +++ b/docs/outreach/meetups.md @@ -2,7 +2,7 @@ A list of meetups about Gutenberg so far: -- [Gutenberg and the Future of WordPress](https://www.meetup.com/Vancouver-WordPress-Meetup-Group/events/241575161/), Vancouver, CA +- [Gutenberg and the Future of WordPress](https://www.meetup.com/Vancouver-WordPress-Meetup-Group/events/241575161/), Vancouver, Canada - [Page builders and the upcoming Gutenberg Editor](https://www.meetup.com/Turku-WordPress-Meetup/events/241195076/), Turku, Finland - [Discussion about Gutenberg](https://www.facebook.com/events/278785795934302/), Andria, Italy - [Plugins and Gutenberg](https://wpleeds.co.uk/events/plugins-gutenberg-wordpress-leeds-july-2017/), Leeds, UK @@ -13,4 +13,5 @@ A list of meetups about Gutenberg so far: - [WordPress & JavaScript: Let's talk Gutenberg!](https://www.meetup.com/WordPress-Lahore/events/246446478/), Lahore, PK - [The state of Gutenberg](https://www.meetup.com/WP-Porto/events/245585131/), Porto, Portugal - [Discuss and learn about the new WordPress Editor : Gutenberg](https://www.meetup.com/Pune-WordPress-Knowledge-Exchange/events/248496830/), Pune, India +- [An Introduction to Gutenberg](https://www.meetup.com/Okanagan-WordPress-Meetup/events/249167218/), Vernon, BC, Canada - [WordPress 5.0 - Gutenberg is upon us](https://www.meetup.com/WordPress-Perth/events/249490075/), Perth, Australia diff --git a/docs/reference/deprecated.md b/docs/reference/deprecated.md index 5dfac97977b45..650c71b8e135a 100644 --- a/docs/reference/deprecated.md +++ b/docs/reference/deprecated.md @@ -6,6 +6,7 @@ Gutenberg's deprecation policy is intended to support backwards-compatibility fo - `wp.blocks.withEditorSettings` is removed. Please use the data module to access the editor settings `wp.data.select( "core/editor" ).getEditorSettings()`. - All DOM utils in `wp.utils.*` are removed. Please use `wp.dom.*` instead. - `isPrivate: true` has been removed from the Block API. Please use `supports.inserter: false` instead. + - `wp.utils.isExtraSmall` function removed. Please use `wp.viewport.*` instead. ## 3.0.0 diff --git a/docs/reference/testing-overview.md b/docs/reference/testing-overview.md index 303f5fa1522f3..fcfc951cfd90f 100644 --- a/docs/reference/testing-overview.md +++ b/docs/reference/testing-overview.md @@ -43,7 +43,7 @@ Keep your tests in a `test` folder in your working directory. The test file shou Only test files (with at least one test case) should live directly under `/test`. If you need to add external mocks or fixtures, place them in a sub folder, for example: -* `test/mocks/[file-name.js` +* `test/mocks/[file-name].js` * `test/fixtures/[file-name].js` ### Importing tests diff --git a/edit-post/components/visual-editor/style.scss b/edit-post/components/visual-editor/style.scss index 5c2b7abcfbd4b..5c8343bb96649 100644 --- a/edit-post/components/visual-editor/style.scss +++ b/edit-post/components/visual-editor/style.scss @@ -37,6 +37,7 @@ margin: #{ -1 * $block-spacing } auto -50px; } +// The base width of blocks .edit-post-visual-editor .editor-block-list__block { margin-left: auto; margin-right: auto; @@ -65,25 +66,21 @@ } } -// This is a focus style shown for blocks that need an indicator even when in an isEditing state -// like for example an image block that receives arrowkey focus. -.edit-post-visual-editor .editor-block-list__block:not( .is-selected ) { - .editor-block-list__block-edit { - box-shadow: 0 0 0 0 $white, 0 0 0 0 $dark-gray-900; - transition: .1s box-shadow .05s; - } - - &:focus .editor-block-list__block-edit { - box-shadow: 0 0 0 1px $white, 0 0 0 3px $dark-gray-900; - } -} - +// The base width of the title should match that of blocks even if it isn't a block .edit-post-visual-editor .editor-post-title { margin-left: auto; margin-right: auto; - max-width: $content-width + ( 2 * $block-side-ui-padding ); + max-width: $content-width; - .editor-post-permalink { + @include break-small() { + > div { + margin-left: -$block-side-ui-padding; + margin-right: -$block-side-ui-padding; + } + } + + + /*.editor-post-permalink { left: $block-padding; right: $block-padding; color: $dark-gray-900; @@ -96,6 +93,19 @@ left: $block-side-ui-padding; right: $block-side-ui-padding; } + }*/ +} + +// This is a focus style shown for blocks that need an indicator even when in an isEditing state +// like for example an image block that receives arrowkey focus. +.edit-post-visual-editor .editor-block-list__block:not( .is-selected ) { + .editor-block-list__block-edit { + box-shadow: 0 0 0 0 $white, 0 0 0 0 $dark-gray-900; + transition: .1s box-shadow .05s; + } + + &:focus .editor-block-list__block-edit { + box-shadow: 0 0 0 1px $white, 0 0 0 3px $dark-gray-900; } } diff --git a/editor/components/block-list/style.scss b/editor/components/block-list/style.scss index abd7532a32fef..d1696ef9b0b27 100644 --- a/editor/components/block-list/style.scss +++ b/editor/components/block-list/style.scss @@ -189,7 +189,7 @@ left: 0; // use opacity to work in various editor styles - border-left: 1px solid $dark-opacity-light-500; + border-left: 1px solid $dark-opacity-light-500; .is-dark-theme & { border-left-color: $light-opacity-light-500; @@ -223,6 +223,7 @@ bottom: -$block-padding; left: -$block-padding; outline: 1px solid transparent; + pointer-events: none; } } @@ -231,7 +232,7 @@ &.is-selected > .editor-block-list__block-edit:before { // use opacity to work in various editor styles outline: 1px solid $dark-opacity-light-500; - + .is-dark-theme & { outline-color: $light-opacity-light-500; } @@ -259,7 +260,7 @@ // use opacity to work in various editor styles mix-blend-mode: multiply; - + .is-dark-theme & { mix-blend-mode: soft-light; } @@ -697,10 +698,10 @@ // use opacity to work in various editor styles background-clip: padding-box; box-sizing: padding-box; - border: 1px solid $dark-opacity-light-500; + border: 1px solid $dark-opacity-light-500; .is-dark-theme & { - border-color: $light-opacity-light-500; + border-color: $light-opacity-light-500; } // this prevents floats from messing up the position diff --git a/editor/components/default-block-appender/style.scss b/editor/components/default-block-appender/style.scss index 68420d7a4e392..d1e83c312784f 100644 --- a/editor/components/default-block-appender/style.scss +++ b/editor/components/default-block-appender/style.scss @@ -9,7 +9,6 @@ $empty-paragraph-height: $text-editor-font-size * 4; margin: 0; max-width: none; // fixes a bleed issue from the admin padding: $block-padding; - height: $empty-paragraph-height; font-size: $editor-font-size; font-family: $editor-font; cursor: text; @@ -18,6 +17,11 @@ $empty-paragraph-height: $text-editor-font-size * 4; outline: 1px solid transparent; transition: 0.2s outline; + // match the height of an empty paragraph + // to prevent margins from collapsing we add 1px padding top and bottom to both .editor-block-list__block and .editor-block-list__block-edit (coming to a total of 4px extra) + // @todo: revisit when we allow margins to collapse + height: $empty-paragraph-height + 4px; + // use opacity to work in various editor styles color: $dark-opacity-300; diff --git a/editor/components/post-publish-panel/index.js b/editor/components/post-publish-panel/index.js index b248c57e1b96d..57468078a6683 100644 --- a/editor/components/post-publish-panel/index.js +++ b/editor/components/post-publish-panel/index.js @@ -45,7 +45,7 @@ class PostPublishPanel extends Component { componentDidUpdate( prevProps ) { // Automatically collapse the publish sidebar when a post // is published and the user makes an edit. - if ( prevProps.isPublished && this.props.isDirty ) { + if ( prevProps.isPublished && ! this.props.isSaving && this.props.isDirty ) { this.props.onClose(); } } diff --git a/editor/components/post-title/style.scss b/editor/components/post-title/style.scss index 690f7f86d3043..354f5e6494ebb 100644 --- a/editor/components/post-title/style.scss +++ b/editor/components/post-title/style.scss @@ -1,6 +1,10 @@ .editor-post-title { position: relative; padding: 5px 0; + + @include break-small() { + padding: 5px $block-side-ui-padding; + } .editor-post-title__input { display: block; @@ -35,4 +39,9 @@ top: -35px; left: 0; right: 0; + + @include break-small() { + left: $block-side-ui-padding; + right: $block-side-ui-padding; + } } diff --git a/editor/components/rich-text/index.js b/editor/components/rich-text/index.js index 4818cb14a8624..30546fc98d9b9 100644 --- a/editor/components/rich-text/index.js +++ b/editor/components/rich-text/index.js @@ -308,17 +308,17 @@ export class RichText extends Component { // Note: a pasted file may have the URL as plain text. if ( item && ! HTML ) { const blob = item.getAsFile ? item.getAsFile() : item; - const isEmptyEditor = this.isEmpty(); const content = rawHandler( { HTML: ``, mode: 'BLOCKS', tagName: this.props.tagName, } ); + const shouldReplace = this.props.onReplace && this.isEmpty(); // Allows us to ask for this information when we get a report. window.console.log( 'Received item:\n\n', blob ); - if ( isEmptyEditor && this.props.onReplace ) { + if ( shouldReplace ) { // Necessary to allow the paste bin to be removed without errors. this.props.setTimeout( () => this.props.onReplace( content ) ); } else if ( this.props.onSplit ) { @@ -346,6 +346,9 @@ export class RichText extends Component { */ onPastePreProcess( event ) { const HTML = this.isPlainTextPaste ? '' : event.content; + + event.preventDefault(); + // Allows us to ask for this information when we get a report. window.console.log( 'Received HTML:\n\n', HTML ); window.console.log( 'Received plain text:\n\n', this.pastedPlainText ); @@ -365,17 +368,15 @@ export class RichText extends Component { // Allows us to ask for this information when we get a report. window.console.log( 'Created link:\n\n', pastedText ); - event.preventDefault(); - return; } } - const isEmptyEditor = this.isEmpty(); + const shouldReplace = this.props.onReplace && this.isEmpty(); let mode = 'INLINE'; - if ( isEmptyEditor && this.props.onReplace ) { + if ( shouldReplace ) { mode = 'BLOCKS'; } else if ( this.props.onSplit ) { mode = 'AUTO'; @@ -390,20 +391,16 @@ export class RichText extends Component { } ); if ( typeof content === 'string' ) { - // Let MCE process further with the given content. - event.content = content; + this.editor.insertContent( content ); } else if ( this.props.onSplit ) { - // Abort pasting to split the content - event.preventDefault(); - if ( ! content.length ) { return; } - if ( isEmptyEditor && this.props.onReplace ) { + if ( shouldReplace ) { this.props.onReplace( content ); } else { - this.splitContent( content ); + this.splitContent( content, { paste: true } ); } } } @@ -596,9 +593,10 @@ export class RichText extends Component { * before the selection. Sends the elements after the selection to the `onSplit` * handler. * - * @param {Array} blocks The blocks to add after the split point. + * @param {Array} blocks The blocks to add after the split point. + * @param {Object} context The context for splitting. */ - splitContent( blocks = [] ) { + splitContent( blocks = [], context = {} ) { if ( ! this.props.onSplit ) { return; } @@ -620,10 +618,17 @@ export class RichText extends Component { const afterFragment = afterRange.extractContents(); const { format } = this.props; - const before = domToFormat( filterEmptyNodes( beforeFragment.childNodes ), format, this.editor ); - const after = domToFormat( filterEmptyNodes( afterFragment.childNodes ), format, this.editor ); + let before = domToFormat( filterEmptyNodes( beforeFragment.childNodes ), format, this.editor ); + let after = domToFormat( filterEmptyNodes( afterFragment.childNodes ), format, this.editor ); + + if ( context.paste ) { + before = this.isEmpty( before ) ? null : before; + after = this.isEmpty( after ) ? null : after; + } this.restoreContentAndSplit( before, after, blocks ); + } else if ( context.paste ) { + this.restoreContentAndSplit( null, null, blocks ); } else { this.restoreContentAndSplit( [], [], blocks ); } @@ -774,10 +779,11 @@ export class RichText extends Component { /** * Returns true if the field is currently empty, or false otherwise. * + * @param {Array} value Content to check. + * * @return {boolean} Whether field is empty. */ - isEmpty() { - const { value } = this.props; + isEmpty( value = this.props.value ) { return ! value || ! value.length; } diff --git a/gutenberg.php b/gutenberg.php index 21d98c59ce32a..eb1a05467c8c5 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,7 +3,7 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the new block editor in core. - * Version: 2.8.0 + * Version: 2.9.1 * Author: Gutenberg Team * * @package gutenberg diff --git a/lerna.json b/lerna.json index 35c103174e89d..e339091b21bf6 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "lerna": "2.11.0", "commands": { "publish": { - "message": "chore(release): publish %s" + "message": "chore(release): publish" } }, "packages": [ diff --git a/lib/client-assets.php b/lib/client-assets.php index 8d4f6838f57cf..956c32a21e66c 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -226,7 +226,18 @@ function gutenberg_register_scripts_and_styles() { wp_register_script( 'wp-core-blocks', gutenberg_url( 'build/core-blocks/index.js' ), - array( 'wp-element', 'wp-components', 'wp-utils', 'wp-blocks', 'wp-editor', 'wp-i18n', 'editor', 'wp-core-data', 'lodash' ), + array( + 'editor', + 'lodash', + 'wp-blocks', + 'wp-components', + 'wp-core-data', + 'wp-element', + 'wp-editor', + 'wp-i18n', + 'wp-utils', + 'wp-viewport', + ), filemtime( gutenberg_dir_path() . 'build/core-blocks/index.js' ), true ); @@ -1082,7 +1093,10 @@ function gutenberg_editor_scripts_and_styles( $hook ) { * Remove this in Gutenberg 3.1 */ function polyfill_blocks_module_in_scripts() { - wp_enqueue_script( 'wp-editor' ); + if ( is_admin() ) { + wp_enqueue_script( 'wp-editor' ); + } } add_action( 'enqueue_block_editor_assets', 'polyfill_blocks_module_in_scripts', 9 ); +add_action( 'enqueue_block_assets', 'polyfill_blocks_module_in_scripts', 9 ); diff --git a/lib/rest-api.php b/lib/rest-api.php index a9a12fa49fb34..bfd2d8b5ac756 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -21,66 +21,6 @@ function gutenberg_register_rest_routes() { } add_action( 'rest_api_init', 'gutenberg_register_rest_routes' ); -/** - * Includes the value for the custom field `post_type_capabities` inside the REST API response of user. - * - * TODO: This is a temporary solution. Next step would be to edit the WP_REST_Users_Controller, - * once merged into Core. - * - * @since ? - * - * @param array $user An array containing user properties. - * @param string $name The name of the custom field. - * @param WP_REST_Request $request Full details about the REST API request. - * @return object The Post Type capabilities. - */ -function gutenberg_get_post_type_capabilities( $user, $name, $request ) { - $post_type = $request->get_param( 'post_type' ); - $value = new stdClass; - - if ( ! empty( $user['id'] ) && $post_type && post_type_exists( $post_type ) ) { - // The Post Type object contains the Post Type's specific caps. - $post_type_object = get_post_type_object( $post_type ); - - // Loop in the Post Type's caps to validate the User's caps for it. - foreach ( $post_type_object->cap as $post_cap => $post_type_cap ) { - // Ignore caps requiring a post ID. - if ( in_array( $post_cap, array( 'edit_post', 'read_post', 'delete_post' ) ) ) { - continue; - } - - // Set the User's post type capability. - $value->{$post_cap} = user_can( $user['id'], $post_type_cap ); - } - } - - return $value; -} - -/** - * Adds the custom field `post_type_capabities` to the REST API response of user. - * - * TODO: This is a temporary solution. Next step would be to edit the WP_REST_Users_Controller, - * once merged into Core. - * - * @since ? - */ -function gutenberg_register_rest_api_post_type_capabilities() { - register_rest_field( 'user', - 'post_type_capabilities', - array( - 'get_callback' => 'gutenberg_get_post_type_capabilities', - 'schema' => array( - 'description' => __( 'Post Type capabilities for the user.', 'gutenberg' ), - 'type' => 'object', - 'context' => array( 'edit' ), - 'readonly' => true, - ), - ) - ); -} -add_action( 'rest_api_init', 'gutenberg_register_rest_api_post_type_capabilities' ); - /** * Make sure oEmbed REST Requests apply the WP Embed security mechanism for WordPress embeds. * @@ -723,7 +663,7 @@ function gutenberg_filter_request_after_callbacks( $response, $handler, $request } } } - // Handle POST /wp/v2/tags (and non-hiearchical taxonomies) when user + // Handle POST /wp/v2/tags (and non-hierarchical taxonomies) when user // can assign_terms but not manage terms. Users should be able to create // terms. if ( 'rest_cannot_create' === $response->get_error_code() diff --git a/package-lock.json b/package-lock.json index 2ca45302535ae..85d9e60c78ce8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "2.8.0", + "version": "2.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -240,9 +240,9 @@ "dev": true }, "@types/node": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.1.0.tgz", - "integrity": "sha512-sELcX/cJHwRp8kn4hYSvBxKGJ+ubl3MvS8VJQe5gz/sp7CifYxsiCxIJ35wMIYyGVMgfO2AzRa8UcVReAcJRlw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.1.1.tgz", + "integrity": "sha512-n7wxy8r2tjVcrzZoKJlyZmi1C1VhXGHAGhDEO1iqp7fbsTSsDF3dVA50KFsPg77EXqzNJqbzcna8Mi4m7a1lyw==", "dev": true }, "@webassemblyjs/ast": { @@ -2517,15 +2517,15 @@ } }, "caniuse-db": { - "version": "1.0.30000841", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000841.tgz", - "integrity": "sha1-26QAiVmQNI4t47cXlaUOg38Ts/Y=", + "version": "1.0.30000843", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000843.tgz", + "integrity": "sha1-T36FAfVX3JvNN90zrIWQXHZe/sI=", "dev": true }, "caniuse-lite": { - "version": "1.0.30000841", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000841.tgz", - "integrity": "sha512-LeOGLEY4hl6xZc/xMYOrVmSrHOybyHWNShFN51qCmDXo69nEGKHTJTfe6jdWe4hLxSJcwEIYtKHFFh93fF/kNA==", + "version": "1.0.30000843", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000843.tgz", + "integrity": "sha512-1ntiW826MhRBmM0CeI7w1cQr16gxwOoM8doJWh3BFalPZoKWdZXs27Bc04xth/3NR1/wNXn9cpP4F92lVenCvg==", "dev": true }, "capture-exit": { @@ -4410,9 +4410,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.46", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.46.tgz", - "integrity": "sha1-AOheIidUFaiHUF5KtJc3GU8YubA=", + "version": "1.3.47", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.47.tgz", + "integrity": "sha1-dk6IfKkQTQGgrI6r7n38DizhQQQ=", "dev": true }, "elegant-spinner": { @@ -4580,18 +4580,18 @@ } }, "enzyme-to-json": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.3.3.tgz", - "integrity": "sha1-7eRZOPswnNh+vUOG9gx1RSVRWgc=", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.3.4.tgz", + "integrity": "sha1-Z8YEDpMRgvGDQYry659DIyWKp38=", "dev": true, "requires": { "lodash": "^4.17.4" } }, "equivalent-key-map": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/equivalent-key-map/-/equivalent-key-map-0.1.1.tgz", - "integrity": "sha512-VfHxntFFcApMyX3TTEQg+nuDPoiGgs1WfYm1JN+d9HUM6Shxp2d7LrMrDmdiybYZVs7U8HM9mqAHpwE9/X8pSQ==" + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/equivalent-key-map/-/equivalent-key-map-0.2.0.tgz", + "integrity": "sha512-F6m+4Th/DEUexGY+OGZoMsq33u8CfLzW9kCuryIugZS4sO4niQIBjVUM3yvWy4oaBUB0hM7uVDBlZhreVAPfcg==" }, "errno": { "version": "0.1.7", @@ -5571,6 +5571,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "dev": true, "optional": true, "requires": { "nan": "^2.9.2", @@ -5580,20 +5581,24 @@ "abbrev": { "version": "1.1.1", "bundled": true, + "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "dev": true }, "aproba": { "version": "1.2.0", "bundled": true, + "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.4", "bundled": true, + "dev": true, "optional": true, "requires": { "delegates": "^1.0.0", @@ -5602,11 +5607,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5615,28 +5622,34 @@ "chownr": { "version": "1.0.1", "bundled": true, + "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "dev": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "dev": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "dev": true }, "core-util-is": { "version": "1.0.2", "bundled": true, + "dev": true, "optional": true }, "debug": { "version": "2.6.9", "bundled": true, + "dev": true, "optional": true, "requires": { "ms": "2.0.0" @@ -5645,21 +5658,25 @@ "deep-extend": { "version": "0.5.1", "bundled": true, + "dev": true, "optional": true }, "delegates": { "version": "1.0.0", "bundled": true, + "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", "bundled": true, + "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", "bundled": true, + "dev": true, "optional": true, "requires": { "minipass": "^2.2.1" @@ -5668,11 +5685,13 @@ "fs.realpath": { "version": "1.0.0", "bundled": true, + "dev": true, "optional": true }, "gauge": { "version": "2.7.4", "bundled": true, + "dev": true, "optional": true, "requires": { "aproba": "^1.0.3", @@ -5688,6 +5707,7 @@ "glob": { "version": "7.1.2", "bundled": true, + "dev": true, "optional": true, "requires": { "fs.realpath": "^1.0.0", @@ -5701,11 +5721,13 @@ "has-unicode": { "version": "2.0.1", "bundled": true, + "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.21", "bundled": true, + "dev": true, "optional": true, "requires": { "safer-buffer": "^2.1.0" @@ -5714,6 +5736,7 @@ "ignore-walk": { "version": "3.0.1", "bundled": true, + "dev": true, "optional": true, "requires": { "minimatch": "^3.0.4" @@ -5722,6 +5745,7 @@ "inflight": { "version": "1.0.6", "bundled": true, + "dev": true, "optional": true, "requires": { "once": "^1.3.0", @@ -5730,16 +5754,19 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "dev": true }, "ini": { "version": "1.3.5", "bundled": true, + "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5747,22 +5774,26 @@ "isarray": { "version": "1.0.0", "bundled": true, + "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", "bundled": true, + "dev": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, + "dev": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -5771,6 +5802,7 @@ "minizlib": { "version": "1.1.0", "bundled": true, + "dev": true, "optional": true, "requires": { "minipass": "^2.2.1" @@ -5779,6 +5811,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "dev": true, "requires": { "minimist": "0.0.8" } @@ -5786,11 +5819,13 @@ "ms": { "version": "2.0.0", "bundled": true, + "dev": true, "optional": true }, "needle": { "version": "2.2.0", "bundled": true, + "dev": true, "optional": true, "requires": { "debug": "^2.1.2", @@ -5801,6 +5836,7 @@ "node-pre-gyp": { "version": "0.10.0", "bundled": true, + "dev": true, "optional": true, "requires": { "detect-libc": "^1.0.2", @@ -5818,6 +5854,7 @@ "nopt": { "version": "4.0.1", "bundled": true, + "dev": true, "optional": true, "requires": { "abbrev": "1", @@ -5827,11 +5864,13 @@ "npm-bundled": { "version": "1.0.3", "bundled": true, + "dev": true, "optional": true }, "npm-packlist": { "version": "1.1.10", "bundled": true, + "dev": true, "optional": true, "requires": { "ignore-walk": "^3.0.1", @@ -5841,6 +5880,7 @@ "npmlog": { "version": "4.1.2", "bundled": true, + "dev": true, "optional": true, "requires": { "are-we-there-yet": "~1.1.2", @@ -5851,16 +5891,19 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true }, "object-assign": { "version": "4.1.1", "bundled": true, + "dev": true, "optional": true }, "once": { "version": "1.4.0", "bundled": true, + "dev": true, "requires": { "wrappy": "1" } @@ -5868,16 +5911,19 @@ "os-homedir": { "version": "1.0.2", "bundled": true, + "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", "bundled": true, + "dev": true, "optional": true }, "osenv": { "version": "0.1.5", "bundled": true, + "dev": true, "optional": true, "requires": { "os-homedir": "^1.0.0", @@ -5887,16 +5933,19 @@ "path-is-absolute": { "version": "1.0.1", "bundled": true, + "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", "bundled": true, + "dev": true, "optional": true }, "rc": { "version": "1.2.7", "bundled": true, + "dev": true, "optional": true, "requires": { "deep-extend": "^0.5.1", @@ -5908,6 +5957,7 @@ "minimist": { "version": "1.2.0", "bundled": true, + "dev": true, "optional": true } } @@ -5915,6 +5965,7 @@ "readable-stream": { "version": "2.3.6", "bundled": true, + "dev": true, "optional": true, "requires": { "core-util-is": "~1.0.0", @@ -5929,6 +5980,7 @@ "rimraf": { "version": "2.6.2", "bundled": true, + "dev": true, "optional": true, "requires": { "glob": "^7.0.5" @@ -5936,36 +5988,43 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true + "bundled": true, + "dev": true }, "safer-buffer": { "version": "2.1.2", "bundled": true, + "dev": true, "optional": true }, "sax": { "version": "1.2.4", "bundled": true, + "dev": true, "optional": true }, "semver": { "version": "5.5.0", "bundled": true, + "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", "bundled": true, + "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", "bundled": true, + "dev": true, "optional": true }, "string-width": { "version": "1.0.2", "bundled": true, + "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5975,6 +6034,7 @@ "string_decoder": { "version": "1.1.1", "bundled": true, + "dev": true, "optional": true, "requires": { "safe-buffer": "~5.1.0" @@ -5983,6 +6043,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5990,11 +6051,13 @@ "strip-json-comments": { "version": "2.0.1", "bundled": true, + "dev": true, "optional": true }, "tar": { "version": "4.4.1", "bundled": true, + "dev": true, "optional": true, "requires": { "chownr": "^1.0.1", @@ -6009,11 +6072,13 @@ "util-deprecate": { "version": "1.0.2", "bundled": true, + "dev": true, "optional": true }, "wide-align": { "version": "1.1.2", "bundled": true, + "dev": true, "optional": true, "requires": { "string-width": "^1.0.2" @@ -6021,11 +6086,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true }, "yallist": { "version": "3.0.2", - "bundled": true + "bundled": true, + "dev": true } } }, @@ -8339,9 +8406,9 @@ "integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c=" }, "js-base64": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz", - "integrity": "sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz", + "integrity": "sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ==", "dev": true }, "js-beautify": { @@ -9754,7 +9821,8 @@ "nan": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "dev": true }, "nanomatch": { "version": "1.2.9", @@ -12894,9 +12962,9 @@ } }, "pumpify": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.0.tgz", - "integrity": "sha512-UWi0klDoq8xtVzlMRgENV9F7iCTZExaJQSQL187UXsxpk9NnrKGqTqqUNYAKGOzucSOxs2+jUnRNI+rLviPhJg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "dev": true, "requires": { "duplexify": "^3.6.0", diff --git a/package.json b/package.json index 54f009d8f9880..2f9b5c8b9cde3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "2.8.0", + "version": "2.9.1", "private": true, "description": "A new WordPress editor experience", "repository": "git+https://github.com/WordPress/gutenberg.git", @@ -28,7 +28,7 @@ "dom-react": "2.2.1", "dom-scroll-into-view": "1.2.1", "element-closest": "2.0.2", - "equivalent-key-map": "0.1.1", + "equivalent-key-map": "0.2.0", "escape-string-regexp": "1.0.5", "eslint-plugin-wordpress": "git://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.git#1774343f6226052a46b081e01db3fca8793cc9f1", "hpq": "1.2.0", @@ -100,9 +100,6 @@ "webpack-cli": "2.1.3", "webpack-rtl-plugin": "github:yoavf/webpack-rtl-plugin#develop" }, - "optionalDependencies": { - "fsevents": "1.2.4" - }, "babel": { "presets": [ "@wordpress/default" diff --git a/test/e2e/specs/publishing.test.js b/test/e2e/specs/publishing.test.js new file mode 100644 index 0000000000000..7d210f8ed8de3 --- /dev/null +++ b/test/e2e/specs/publishing.test.js @@ -0,0 +1,40 @@ +/** + * Internal dependencies + */ +import '../support/bootstrap'; +import { newPost, newDesktopBrowserPage, wait } from '../support/utils'; + +describe( 'Publishing', () => { + beforeAll( async () => { + await newDesktopBrowserPage(); + } ); + + beforeEach( async () => { + await newPost(); + } ); + + it( 'Should publish a post and close the panel once we start editing again', async () => { + await page.type( '.editor-post-title__input', 'E2E Test Post' ); + + // Opens the publish panel + await page.click( '.editor-post-publish-panel__toggle' ); + + // Wait for a second ( wait for the animation ) + await wait( 1000 ); + + // Publish the post + await page.click( '.editor-post-publish-button' ); + + // A success notice should show up + page.waitForSelector( '.notice-success' ); + + // The post publish panel is visible + expect( await page.$( '.editor-post-publish-panel' ) ).not.toBeNull(); + + // Start editing again + await page.type( '.editor-post-title__input', ' (Updated)' ); + + // The post publish panel is not visible anymore + expect( await page.$( '.editor-post-publish-panel' ) ).toBeNull(); + } ); +} ); diff --git a/test/e2e/support/utils.js b/test/e2e/support/utils.js index 1f6f0950323aa..0e164154d656a 100644 --- a/test/e2e/support/utils.js +++ b/test/e2e/support/utils.js @@ -99,3 +99,14 @@ export async function pressWithModifier( modifier, key ) { await page.keyboard.press( key ); return page.keyboard.up( modifier ); } + +/** + * Promise setTimeout Wrapper. + * + * @param {number} timeout Timeout in milliseconds + * + * @return {Promise} Promise resolving after the given timeout + */ +export async function wait( timeout ) { + return new Promise( ( resolve ) => setTimeout( resolve, timeout ) ); +} diff --git a/utils/dom-deprecated.js b/utils/deprecated.js similarity index 88% rename from utils/dom-deprecated.js rename to utils/deprecated.js index cbc1c9f0be722..7b4b5d3ff9d5c 100644 --- a/utils/dom-deprecated.js +++ b/utils/deprecated.js @@ -40,3 +40,13 @@ export const remove = wrapFunction( 'remove' ); export const replace = wrapFunction( 'replace' ); export const replaceTag = wrapFunction( 'replaceTag' ); export const unwrap = wrapFunction( 'unwrap' ); + +export function isExtraSmall() { + deprecated( 'wp.utils.isExtraSmall', { + version: '3.1', + alternative: 'wp.viewport.*', + plugin: 'Gutenberg', + } ); + + return window && window.innerWidth < 782; +} diff --git a/utils/deprecation.js b/utils/deprecation.js index 176664fdaef7d..ecd16c0128e4b 100644 --- a/utils/deprecation.js +++ b/utils/deprecation.js @@ -7,13 +7,15 @@ * @param {?string} options.alternative Feature to use instead * @param {?string} options.plugin Plugin name if it's a plugin feature * @param {?string} options.link Link to documentation + * @param {?string} options.hint Additional message to help transition away from the deprecated feature. */ -export function deprecated( feature, { version, alternative, plugin, link } = {} ) { +export function deprecated( feature, { version, alternative, plugin, link, hint } = {} ) { const pluginMessage = plugin ? ` from ${ plugin }` : ''; const versionMessage = version ? `${ pluginMessage } in ${ version }` : ''; const useInsteadMessage = alternative ? ` Please use ${ alternative } instead.` : ''; const linkMessage = link ? ` See: ${ link }` : ''; - const message = `${ feature } is deprecated and will be removed${ versionMessage }.${ useInsteadMessage }${ linkMessage }`; + const hintMessage = hint ? ` Note: ${ hint }` : ''; + const message = `${ feature } is deprecated and will be removed${ versionMessage }.${ useInsteadMessage }${ linkMessage }${ hintMessage }`; // eslint-disable-next-line no-console console.warn( message ); diff --git a/utils/index.js b/utils/index.js index ed1ce1520ff8e..368c296ee3009 100644 --- a/utils/index.js +++ b/utils/index.js @@ -2,12 +2,10 @@ * Internal dependencies */ import * as keycodes from './keycodes'; -import * as viewPort from './viewport'; import { decodeEntities } from './entities'; export { decodeEntities }; export { keycodes }; -export { viewPort }; export * from './blob-cache'; export * from './deprecation'; @@ -15,4 +13,4 @@ export * from './mediaupload'; export * from './terms'; // Deprecations -export * from './dom-deprecated'; +export * from './deprecated'; diff --git a/utils/test/deprecation.js b/utils/test/deprecation.js index 534399386d9b1..56c6f925d7634 100644 --- a/utils/test/deprecation.js +++ b/utils/test/deprecation.js @@ -52,4 +52,17 @@ describe( 'deprecated', () => { 'Eating meat is deprecated and will be removed from the earth in the future. Please use vegetables instead. See: https://en.wikipedia.org/wiki/Vegetarianism' ); } ); + + it( 'should show a deprecation warning with a hint', () => { + deprecated( 'Eating meat', { + version: 'the future', + alternative: 'vegetables', + plugin: 'the earth', + hint: 'You may find it beneficial to transition gradually.', + } ); + + expect( console ).toHaveWarnedWith( + 'Eating meat is deprecated and will be removed from the earth in the future. Please use vegetables instead. Note: You may find it beneficial to transition gradually.' + ); + } ); } ); diff --git a/utils/viewport.js b/utils/viewport.js deleted file mode 100644 index c992d6ef4d878..0000000000000 --- a/utils/viewport.js +++ /dev/null @@ -1,3 +0,0 @@ -export function isExtraSmall() { - return window && window.innerWidth < 782; -}