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

View File

@@ -0,0 +1,87 @@
# FormFileUpload
FormFileUpload is a component that allows users to select files from their local device.
## Usage
```jsx
import { FormFileUpload } from '@wordpress/components';
const MyFormFileUpload = () => (
<FormFileUpload
accept="image/*"
onChange={ ( event ) => console.log( event.currentTarget.files ) }
>
Upload
</FormFileUpload>
);
```
## Props
The component accepts the following props. Props not included in this set will be passed to the `Button` component.
### accept
A string passed to `input` element that tells the browser which file types can be upload to the upload by the user use. e.g: `image/*,video/*`.
More information about this string is available in https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers.
- Type: `String`
- Required: No
### children
Children are passed as children of `Button`.
- Type: `Boolean`
- Required: No
### icon
The icon to render. Supported values are: Dashicons (specified as strings), functions, Component instances and `null`.
- Type: `String|Function|Component|null`
- Required: No
- Default: `null`
### multiple
Whether to allow multiple selection of files or not.
- Type: `Boolean`
- Required: No
- Default: `false`
### onChange
Callback function passed directly to the `input` file element.
Select files will be available in `event.currentTarget.files`.
- Type: `Function`
- Required: Yes
### onClick
Callback function passed directly to the `input` file element.
This can be useful when you want to force a `change` event to fire when the user chooses the same file again. To do this, set the target value to an empty string in the `onClick` function.
```jsx
<FormFileUpload
onClick={ ( event ) => ( event.target.value = '' ) }
onChange={ onChange }
>
Upload
</FormFileUpload>
```
- Type: `Function`
- Required: No
### render
Optional callback function used to render the UI. If passed, the component does not render the default UI (a button) and calls this function to render it. The function receives an object with property `openFileDialog`, a function that, when called, opens the browser native file upload modal window.
- Type: `Function`
- Required: No

View File

@@ -0,0 +1,68 @@
/**
* WordPress dependencies
*/
import { useRef } from '@wordpress/element';
/**
* Internal dependencies
*/
import Button from '../button';
import type { WordPressComponentProps } from '../context';
import type { FormFileUploadProps } from './types';
/**
* FormFileUpload is a component that allows users to select files from their local device.
*
* ```jsx
* import { FormFileUpload } from '@wordpress/components';
*
* const MyFormFileUpload = () => (
* <FormFileUpload
* accept="image/*"
* onChange={ ( event ) => console.log( event.currentTarget.files ) }
* >
* Upload
* </FormFileUpload>
* );
* ```
*/
export function FormFileUpload( {
accept,
children,
multiple = false,
onChange,
onClick,
render,
...props
}: WordPressComponentProps< FormFileUploadProps, 'button', false > ) {
const ref = useRef< HTMLInputElement >( null );
const openFileDialog = () => {
ref.current?.click();
};
const ui = render ? (
render( { openFileDialog } )
) : (
<Button onClick={ openFileDialog } { ...props }>
{ children }
</Button>
);
return (
<div className="components-form-file-upload">
{ ui }
<input
type="file"
ref={ ref }
multiple={ multiple }
style={ { display: 'none' } }
accept={ accept }
onChange={ onChange }
onClick={ onClick }
data-testid="form-file-upload-input"
/>
</div>
);
}
export default FormFileUpload;

View File

@@ -0,0 +1,74 @@
/**
* External dependencies
*/
import type { Meta, StoryFn } from '@storybook/react';
/**
* WordPress dependencies
*/
import { upload as uploadIcon } from '@wordpress/icons';
/**
* Internal dependencies
*/
import FormFileUpload from '..';
const meta: Meta< typeof FormFileUpload > = {
title: 'Components/FormFileUpload',
component: FormFileUpload,
argTypes: {
icon: { control: { type: null } },
onChange: { action: 'onChange', control: { type: null } },
onClick: { control: { type: null } },
},
parameters: {
controls: { expanded: true },
docs: { canvas: { sourceState: 'shown' } },
},
};
export default meta;
const Template: StoryFn< typeof FormFileUpload > = ( props ) => {
return <FormFileUpload { ...props } />;
};
export const Default = Template.bind( {} );
Default.args = {
children: 'Select file',
};
export const RestrictFileTypes = Template.bind( {} );
RestrictFileTypes.args = {
...Default.args,
accept: 'image/*',
children: 'Select image',
};
export const AllowMultipleFiles = Template.bind( {} );
AllowMultipleFiles.args = {
...Default.args,
children: 'Select files',
multiple: true,
};
export const WithIcon = Template.bind( {} );
WithIcon.args = {
...Default.args,
children: 'Upload',
icon: uploadIcon,
};
/**
* Render a custom trigger button by passing a render function to the `render` prop.
*
* ```jsx
* ( { openFileDialog } ) => <button onClick={ openFileDialog }>Custom Upload Button</button>
* ```
*/
export const WithCustomRender = Template.bind( {} );
WithCustomRender.args = {
...Default.args,
render: ( { openFileDialog } ) => (
<button onClick={ openFileDialog }>Custom Upload Button</button>
),
};

View File

@@ -0,0 +1,90 @@
/**
* External dependencies
*/
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import FormFileUpload from '..';
/**
* Browser dependencies
*/
const { File } = window;
// @testing-library/user-event considers changing <input type="file"> to a string as a change, but it do not occur on real browsers, so the comparisons will be against this result
const fakePath = expect.objectContaining( {
target: expect.objectContaining( {
value: 'C:\\fakepath\\hello.png',
} ),
} );
describe( 'FormFileUpload', () => {
it( 'should show an Icon Button and a hidden input', () => {
render(
<FormFileUpload onChange={ () => {} }>
My Upload Button
</FormFileUpload>
);
const button = screen.getByText( 'My Upload Button' );
const input = screen.getByTestId( 'form-file-upload-input' );
expect( button ).toBeInTheDocument();
expect( input ).toHaveStyle( 'display: none' );
} );
it( 'should not fire a change event after selecting the same file', async () => {
const user = userEvent.setup();
const onChange = jest.fn();
render(
<FormFileUpload onChange={ onChange }>
My Upload Button
</FormFileUpload>
);
const file = new File( [ 'hello' ], 'hello.png', {
type: 'image/png',
} );
const input = screen.getByTestId( 'form-file-upload-input' );
await user.upload( input, file );
await user.upload( input, file );
expect( onChange ).toHaveBeenCalledTimes( 1 );
expect( onChange ).toHaveBeenCalledWith( fakePath );
} );
it( 'should fire a change event after selecting the same file if the value was reset in between', async () => {
const user = userEvent.setup();
const onChange = jest.fn();
render(
<FormFileUpload
onClick={ jest.fn( ( e ) => ( e.currentTarget.value = '' ) ) }
onChange={ onChange }
>
My Upload Button
</FormFileUpload>
);
const file = new File( [ 'hello' ], 'hello.png', {
type: 'image/png',
} );
const input = screen.getByTestId( 'form-file-upload-input' );
await user.upload( input, file );
expect( onChange ).toHaveBeenNthCalledWith( 1, fakePath );
await user.upload( input, file );
expect( onChange ).toHaveBeenNthCalledWith( 2, fakePath );
} );
} );

View File

@@ -0,0 +1,64 @@
/**
* External dependencies
*/
import type { ComponentProps, InputHTMLAttributes, ReactNode } from 'react';
/**
* Internal dependencies
*/
import type Icon from '../icon';
// TODO: Replace `children` and `icon` types with props from Button once Button is typed.
export type FormFileUploadProps = {
/**
* A string passed to `input` element that tells the browser which file types can be
* upload to the upload by the user use. e.g: `image/*,video/*`.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers.
*/
accept?: InputHTMLAttributes< HTMLInputElement >[ 'accept' ];
/**
* Children are passed as children of `Button`.
*/
children?: ReactNode;
/**
* The icon to render in the `Button`.
*/
icon?: ComponentProps< typeof Icon >[ 'icon' ];
/**
* Whether to allow multiple selection of files or not.
*/
multiple?: InputHTMLAttributes< HTMLInputElement >[ 'multiple' ];
/**
* Callback function passed directly to the `input` file element.
*
* Select files will be available in `event.currentTarget.files`.
*/
onChange: InputHTMLAttributes< HTMLInputElement >[ 'onChange' ];
/**
* Callback function passed directly to the `input` file element.
*
* This can be useful when you want to force a `change` event to fire when
* the user chooses the same file again. To do this, set the target value to
* an empty string in the `onClick` function.
*
* ```jsx
* <FormFileUpload
* onClick={ ( event ) => ( event.target.value = '' ) }
* onChange={ onChange }
* >
* Upload
* </FormFileUpload>
* ```
*/
onClick?: InputHTMLAttributes< HTMLInputElement >[ 'onClick' ];
/**
* Optional callback function used to render the UI.
*
* If passed, the component does not render the default UI (a button) and
* calls this function to render it. The function receives an object with
* property `openFileDialog`, a function that, when called, opens the browser
* native file upload modal window.
*/
render?: ( arg: { openFileDialog: () => void } ) => ReactNode;
};