fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

30
node_modules/@wordpress/redux-routine/src/index.js generated vendored Normal file
View File

@@ -0,0 +1,30 @@
/**
* Internal dependencies
*/
import isGenerator from './is-generator';
import createRuntime from './runtime';
/**
* Creates a Redux middleware, given an object of controls where each key is an
* action type for which to act upon, the value a function which returns either
* a promise which is to resolve when evaluation of the action should continue,
* or a value. The value or resolved promise value is assigned on the return
* value of the yield assignment. If the control handler returns undefined, the
* execution is not continued.
*
* @param {Record<string, (value: import('redux').AnyAction) => Promise<boolean> | boolean>} controls Object of control handlers.
*
* @return {import('redux').Middleware} Co-routine runtime
*/
export default function createMiddleware( controls = {} ) {
return ( store ) => {
const runtime = createRuntime( controls, store.dispatch );
return ( next ) => ( action ) => {
if ( ! isGenerator( action ) ) {
return next( action );
}
return runtime( action );
};
};
}

30
node_modules/@wordpress/redux-routine/src/is-action.js generated vendored Normal file
View File

@@ -0,0 +1,30 @@
/**
* External dependencies
*/
import { isPlainObject } from 'is-plain-object';
/* eslint-disable jsdoc/valid-types */
/**
* Returns true if the given object quacks like an action.
*
* @param {any} object Object to test
*
* @return {object is import('redux').AnyAction} Whether object is an action.
*/
export function isAction( object ) {
return isPlainObject( object ) && typeof object.type === 'string';
}
/**
* Returns true if the given object quacks like an action and has a specific
* action type
*
* @param {unknown} object Object to test
* @param {string} expectedType The expected type for the action.
*
* @return {object is import('redux').AnyAction} Whether object is an action and is of specific type.
*/
export function isActionOfType( object, expectedType ) {
/* eslint-enable jsdoc/valid-types */
return isAction( object ) && object.type === expectedType;
}

View File

@@ -0,0 +1,20 @@
/* eslint-disable jsdoc/valid-types */
/**
* Returns true if the given object is a generator, or false otherwise.
*
* @see https://www.ecma-international.org/ecma-262/6.0/#sec-generator-objects
*
* @param {any} object Object to test.
*
* @return {object is Generator} Whether object is a generator.
*/
export default function isGenerator( object ) {
/* eslint-enable jsdoc/valid-types */
// Check that iterator (next) and iterable (Symbol.iterator) interfaces are satisfied.
// These checks seem to be compatible with several generator helpers as well as the native implementation.
return (
!! object &&
typeof object[ Symbol.iterator ] === 'function' &&
typeof object.next === 'function'
);
}

17
node_modules/@wordpress/redux-routine/src/rungen.d.ts generated vendored Normal file
View File

@@ -0,0 +1,17 @@
declare module 'rungen' {
type Control = (
value: any,
next: any,
iterate: any,
yieldNext: ( result: boolean ) => void,
yieldError: ( err: Error ) => void
) => Promise< boolean > | boolean;
function create(
...args: any[]
): (
action: any,
successCallback: ( result: any ) => void,
errorCallaback: () => void
) => void;
}

72
node_modules/@wordpress/redux-routine/src/runtime.ts generated vendored Normal file
View File

@@ -0,0 +1,72 @@
/**
* External dependencies
*/
import type { Control } from 'rungen';
import { create } from 'rungen';
import isPromise from 'is-promise';
import type { Dispatch, AnyAction } from 'redux';
/**
* Internal dependencies
*/
import { isActionOfType, isAction } from './is-action';
/**
* Create a co-routine runtime.
*
* @param controls Object of control handlers.
* @param dispatch Unhandled action dispatch.
*/
export default function createRuntime(
controls: Record<
string,
( value: any ) => Promise< boolean > | boolean
> = {},
dispatch: Dispatch
) {
const rungenControls = Object.entries( controls ).map(
( [ actionType, control ] ): Control =>
( value, next, iterate, yieldNext, yieldError ) => {
if ( ! isActionOfType( value, actionType ) ) {
return false;
}
const routine = control( value );
if ( isPromise( routine ) ) {
// Async control routine awaits resolution.
routine.then( yieldNext, yieldError );
} else {
yieldNext( routine );
}
return true;
}
);
const unhandledActionControl = (
value: AnyAction | unknown,
next: () => void
) => {
if ( ! isAction( value ) ) {
return false;
}
dispatch( value );
next();
return true;
};
rungenControls.push( unhandledActionControl );
const rungenRuntime = create( rungenControls );
return ( action: AnyAction | Generator ) =>
new Promise( ( resolve, reject ) =>
rungenRuntime(
action,
( result ) => {
if ( isAction( result ) ) {
dispatch( result );
}
resolve( result );
},
reject
)
);
}

171
node_modules/@wordpress/redux-routine/src/test/index.js generated vendored Normal file
View File

@@ -0,0 +1,171 @@
/**
* External dependencies
*/
import { createStore, applyMiddleware } from 'redux';
/**
* Internal dependencies
*/
import createMiddleware from '../';
describe( 'createMiddleware', () => {
function createStoreWithMiddleware( middleware ) {
const reducer = ( state = null, action ) => action.nextState || state;
return createStore( reducer, applyMiddleware( middleware ) );
}
it( 'should not alter dispatch flow of uncontrolled action', () => {
const middleware = createMiddleware();
const store = createStoreWithMiddleware( middleware );
store.dispatch( { type: 'CHANGE', nextState: 1 } );
expect( store.getState() ).toBe( 1 );
} );
it( 'should dispatch yielded actions', () => {
const middleware = createMiddleware();
const store = createStoreWithMiddleware( middleware );
function* createAction() {
yield { type: 'CHANGE', nextState: 1 };
}
store.dispatch( createAction() );
expect( store.getState() ).toBe( 1 );
} );
it( 'should continue only once control condition resolves', async () => {
const middleware = createMiddleware( {
WAIT: () => new Promise( ( resolve ) => resolve() ),
} );
const store = createStoreWithMiddleware( middleware );
function* createAction() {
yield { type: 'WAIT' };
yield { type: 'CHANGE', nextState: 1 };
}
await store.dispatch( createAction() );
expect( store.getState() ).toBe( 1 );
} );
it( 'should throw if promise rejects', async () => {
expect.hasAssertions();
const middleware = createMiddleware( {
WAIT_FAIL: () =>
new Promise( ( resolve, reject ) => reject( 'Message' ) ),
} );
const store = createStoreWithMiddleware( middleware );
function* createAction() {
try {
yield { type: 'WAIT_FAIL' };
} catch ( error ) {
// eslint-disable-next-line jest/no-conditional-expect
expect( error ).toBe( 'Message' );
}
}
await store.dispatch( createAction() );
} );
it( 'should throw if promise throws', () => {
expect.hasAssertions();
const middleware = createMiddleware( {
WAIT_FAIL: () =>
new Promise( () => {
throw new Error( 'Message' );
} ),
} );
const store = createStoreWithMiddleware( middleware );
function* createAction() {
try {
yield { type: 'WAIT_FAIL' };
} catch ( error ) {
// eslint-disable-next-line jest/no-conditional-expect
expect( error.message ).toBe( 'Message' );
}
}
return store.dispatch( createAction() );
} );
// Currently this test will not error even under conditions producing it but
// instead will have an uncaught error/warning printed in the cli console:
// - (node:37109) UnhandledPromiseRejectionWarning: TypeError: Cannot read
// property 'type' of null (and others)
// See this github thread for context:
// https://github.com/facebook/jest/issues/3251
it( 'should handle a null returned from a caught promise error', () => {
expect.hasAssertions();
const middleware = createMiddleware( {
WAIT_FAIL: () =>
new Promise( () => {
throw new Error( 'Message' );
} ),
} );
const store = createStoreWithMiddleware( middleware );
function* createAction() {
try {
yield { type: 'WAIT_FAIL' };
} catch ( error ) {
// eslint-disable-next-line jest/no-conditional-expect
expect( error.message ).toBe( 'Message' );
return null;
}
}
store.dispatch( createAction() );
} );
it( 'assigns sync controlled return value into yield assignment', () => {
const middleware = createMiddleware( {
RETURN_TWO: () => 2,
} );
const store = createStoreWithMiddleware( middleware );
function* createAction() {
const nextState = yield { type: 'RETURN_TWO' };
yield { type: 'CHANGE', nextState };
}
store.dispatch( createAction() );
expect( store.getState() ).toBe( 2 );
} );
it( 'assigns async controlled return value into yield assignment', async () => {
const middleware = createMiddleware( {
WAIT: ( action ) =>
new Promise( ( resolve ) => {
resolve( action.value );
} ),
} );
const store = createStoreWithMiddleware( middleware );
function* createAction() {
const nextState = yield { type: 'WAIT', value: 2 };
return { type: 'CHANGE', nextState };
}
await store.dispatch( createAction() );
expect( store.getState() ).toBe( 2 );
} );
it(
'does not recurse when action like object returns from a sync ' +
'control',
() => {
const post = { type: 'post' };
const middleware = createMiddleware( {
UPDATE: () => post,
} );
const store = createStoreWithMiddleware( middleware );
function* getPostAction() {
const nextState = yield { type: 'UPDATE' };
return { type: 'CHANGE', nextState };
}
store.dispatch( getPostAction() );
expect( store.getState() ).toEqual( post );
}
);
} );

View File

@@ -0,0 +1,32 @@
/**
* Internal dependencies
*/
import { isAction, isActionOfType } from '../is-action';
const nonActions = [ null, [], 42, 'foo', () => {}, { foo: 'bar' } ];
const validAction = { type: 'winner' };
describe( 'isAction()', () => {
it( 'should return false if not an action', () => {
nonActions.forEach( ( value ) => {
expect( isAction( value ) ).toBe( false );
} );
} );
it( 'should return true if an action', () => {
expect( isAction( validAction ) ).toBe( true );
} );
} );
describe( 'isActionOfType', () => {
it( 'should return false if not an action', () => {
nonActions.forEach( ( value ) => {
expect( isActionOfType( value, 'foo' ) ).toBe( false );
} );
} );
it( 'should return false if is an action but not of correct type', () => {
expect( isActionOfType( validAction, 'loser' ) ).toBe( false );
} );
it( 'should return true if is an action and of correct type', () => {
expect( isActionOfType( validAction, 'winner' ) ).toBe( true );
} );
} );

View File

@@ -0,0 +1,36 @@
/**
* Internal dependencies
*/
import isGenerator from '../is-generator';
describe( 'isGenerator', () => {
test.each( [
[ undefined ],
[ null ],
[ 10 ],
[ 'foo' ],
[ [ 0, 1, 2, 3 ] ],
[ function func() {} ],
[ function* generatorFunc() {} ],
] )( 'should return false for %p', ( value ) => {
expect( isGenerator( value ) ).toBe( false );
} );
it( 'should return false if an imposter!', () => {
const value = { next() {} };
expect( isGenerator( value ) ).toBe( false );
} );
it( 'should return false if an async generator', () => {
const value = ( async function* () {} )();
expect( isGenerator( value ) ).toBe( false );
} );
it( 'should return true if a generator', () => {
const value = ( function* () {} )();
expect( isGenerator( value ) ).toBe( true );
} );
} );