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,130 @@
import { createElement } from "react";
/**
* External dependencies
*/
import { SafeAreaView, TouchableOpacity, View } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
/**
* WordPress dependencies
*/
import { useEffect, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { BottomSheet, Icon } from '@wordpress/components';
import { getProtocol, isURL, prependHTTP } from '@wordpress/url';
import { link, cancelCircleFilled } from '@wordpress/icons';
import { usePreferredColorSchemeStyle } from '@wordpress/compose';
/**
* Internal dependencies
*/
import LinkPickerResults from './link-picker-results';
import NavBar from '../bottom-sheet/nav-bar';
import styles from './styles.scss';
// This creates a search suggestion for adding a url directly.
export const createDirectEntry = value => {
let type = 'URL';
const protocol = getProtocol(value)?.toLowerCase() || '';
if (protocol.includes('mailto')) {
type = 'mailto';
}
if (protocol.includes('tel')) {
type = 'tel';
}
if (value?.startsWith('#')) {
type = 'internal';
}
return {
isDirectEntry: true,
title: value,
url: type === 'URL' ? prependHTTP(value) : value,
type
};
};
const getURLFromClipboard = async () => {
const text = await Clipboard.getString();
return !!text && isURL(text) ? text : '';
};
export const LinkPicker = ({
value: initialValue,
onLinkPicked,
onCancel: cancel
}) => {
const [value, setValue] = useState(initialValue);
const [clipboardUrl, setClipboardUrl] = useState('');
const directEntry = createDirectEntry(value);
// The title of a direct entry is displayed as the raw input value, but if we
// are replacing empty text, we want to use the generated url.
const pickLink = ({
title,
url,
isDirectEntry
}) => {
onLinkPicked({
title: isDirectEntry ? url : title,
url
});
};
const onSubmit = () => {
pickLink(directEntry);
};
const clear = () => {
setValue('');
setClipboardUrl('');
};
const omniCellStyle = usePreferredColorSchemeStyle(styles.omniCell, styles.omniCellDark);
const iconStyle = usePreferredColorSchemeStyle(styles.icon, styles.iconDark);
useEffect(() => {
getURLFromClipboard().then(setClipboardUrl).catch(() => setClipboardUrl(''));
}, []);
// TODO: Localize the accessibility label.
// TODO: Decide on if `LinkSuggestionItemCell` with `isDirectEntry` makes sense.
return createElement(SafeAreaView, {
style: styles.safeArea
}, createElement(NavBar, null, createElement(NavBar.DismissButton, {
onPress: cancel
}), createElement(NavBar.Heading, null, __('Link to')), createElement(NavBar.ApplyButton, {
onPress: onSubmit
})), createElement(View, {
style: styles.contentContainer
}, createElement(BottomSheet.Cell, {
icon: link,
style: omniCellStyle,
valueStyle: styles.omniInput,
value: value,
placeholder: __('Search or type URL'),
autoCapitalize: "none",
autoCorrect: false,
keyboardType: "url",
onChangeValue: setValue,
onSubmit: onSubmit
/* eslint-disable-next-line jsx-a11y/no-autofocus */,
autoFocus: true,
separatorType: "none"
}, value !== '' && createElement(TouchableOpacity, {
onPress: clear,
style: styles.clearIcon
}, createElement(Icon, {
icon: cancelCircleFilled,
fill: iconStyle.color,
size: 24
}))), !!clipboardUrl && clipboardUrl !== value && createElement(BottomSheet.LinkSuggestionItemCell, {
accessible: true,
accessibilityLabel: sprintf( /* translators: Copy URL from the clipboard, https://sample.url */
__('Copy URL from the clipboard, %s'), clipboardUrl),
suggestion: {
type: 'clipboard',
url: clipboardUrl,
isDirectEntry: true
},
onLinkPicked: pickLink
}), !!value && createElement(LinkPickerResults, {
query: value,
onLinkPicked: pickLink,
directEntry: directEntry
})));
};
//# sourceMappingURL=index.native.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,139 @@
import { createElement } from "react";
/**
* External dependencies
*/
import { ActivityIndicator, FlatList, View } from 'react-native';
/**
* WordPress dependencies
*/
import { BottomSheet, BottomSheetConsumer } from '@wordpress/components';
import { debounce } from '@wordpress/compose';
import { useState, useEffect, useRef } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import styles from './styles.scss';
const PER_PAGE = 20;
const REQUEST_DEBOUNCE_DELAY = 400;
const MINIMUM_QUERY_SIZE = 2;
const meetsThreshold = query => MINIMUM_QUERY_SIZE <= query.length;
export default function LinkPickerResults({
query,
onLinkPicked,
directEntry
}) {
const [links, setLinks] = useState([directEntry]);
const [hasAllSuggestions, setHasAllSuggestions] = useState(false);
const nextPage = useRef(1);
const pendingRequest = useRef();
const clearRequest = () => {
pendingRequest.current = null;
};
// A stable debounced function to fetch suggestions and append.
const {
fetchMoreSuggestions
} = useSelect(select => {
const {
getSettings
} = select('core/block-editor');
const fetchLinkSuggestions = async ({
search
}) => {
if (meetsThreshold(search)) {
return await getSettings().__experimentalFetchLinkSuggestions(search, {
page: nextPage.current,
type: 'post',
perPage: PER_PAGE
});
}
};
const fetchMore = async ({
query: search,
links: currentSuggestions
}) => {
// Return early if we've already detected the end of data or we are
// already awaiting a response.
if (hasAllSuggestions || pendingRequest.current) {
return;
}
const request = fetchLinkSuggestions({
search
});
pendingRequest.current = request;
const suggestions = await request;
// Only update links for the most recent request.
if (suggestions && request === pendingRequest.current) {
// Since we don't have the response header, we check if the results
// are truncated to determine we've reached the end.
if (suggestions.length < PER_PAGE) {
setHasAllSuggestions(true);
}
setLinks([...currentSuggestions, ...suggestions]);
nextPage.current++;
}
clearRequest();
};
return {
fetchMoreSuggestions: debounce(fetchMore, REQUEST_DEBOUNCE_DELAY)
};
// Disable eslint rule for now, to avoid introducing a regression
// (see https://github.com/WordPress/gutenberg/pull/23922#discussion_r1170634879).
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Prevent setting state when unmounted.
useEffect(() => clearRequest, []);
// Any time the query changes, we reset pagination.
useEffect(() => {
clearRequest();
nextPage.current = 1;
setHasAllSuggestions(false);
setLinks([directEntry]);
fetchMoreSuggestions({
query,
links: [directEntry]
});
// Disable reason: deferring this refactor to the native team.
// see https://github.com/WordPress/gutenberg/pull/41166
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]);
const onEndReached = () => fetchMoreSuggestions({
query,
links
});
const spinner = !hasAllSuggestions && meetsThreshold(query) && createElement(View, {
style: styles.spinner,
testID: "link-picker-loading"
}, createElement(ActivityIndicator, {
animating: true
}));
return createElement(BottomSheetConsumer, null, ({
listProps
}) => createElement(FlatList, {
data: links,
keyboardShouldPersistTaps: "always",
renderItem: ({
item
}) => createElement(BottomSheet.LinkSuggestionItemCell, {
suggestion: item,
onLinkPicked: onLinkPicked
}),
keyExtractor: ({
url,
type
}) => `${url}-${type}`,
onEndReached: onEndReached,
onEndReachedThreshold: 0.1,
initialNumToRender: PER_PAGE,
ListFooterComponent: spinner,
...listProps,
contentContainerStyle: [...listProps.contentContainerStyle, styles.list]
}));
}
//# sourceMappingURL=link-picker-results.native.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,63 @@
import { createElement } from "react";
/**
* External dependencies
*/
import { Keyboard } from 'react-native';
import { useNavigation, useRoute } from '@react-navigation/native';
/**
* WordPress dependencies
*/
import { useEffect, useMemo, useRef } from '@wordpress/element';
/**
* Internal dependencies
*/
import { LinkPicker } from './';
const LinkPickerScreen = ({
returnScreenName
}) => {
const navigation = useNavigation();
const route = useRoute();
const navigateToLinkTimeoutRef = useRef(null);
const navigateBackTimeoutRef = useRef(null);
const onLinkPicked = ({
url,
title
}) => {
Keyboard.dismiss();
navigateToLinkTimeoutRef.current = setTimeout(() => {
navigation.navigate(returnScreenName, {
inputValue: url,
text: title
});
}, 100);
};
const onCancel = () => {
Keyboard.dismiss();
navigateBackTimeoutRef.current = setTimeout(() => {
navigation.goBack();
}, 100);
};
useEffect(() => {
return () => {
clearTimeout(navigateToLinkTimeoutRef.current);
clearTimeout(navigateBackTimeoutRef.current);
};
}, []);
const {
inputValue
} = route.params;
return useMemo(() => {
return createElement(LinkPicker, {
value: inputValue,
onLinkPicked: onLinkPicked,
onCancel: onCancel
});
// Disable reason: deferring this refactor to the native team.
// see https://github.com/WordPress/gutenberg/pull/41166
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputValue]);
};
export default LinkPickerScreen;
//# sourceMappingURL=link-picker-screen.native.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["Keyboard","useNavigation","useRoute","useEffect","useMemo","useRef","LinkPicker","LinkPickerScreen","returnScreenName","navigation","route","navigateToLinkTimeoutRef","navigateBackTimeoutRef","onLinkPicked","url","title","dismiss","current","setTimeout","navigate","inputValue","text","onCancel","goBack","clearTimeout","params","createElement","value"],"sources":["@wordpress/components/src/mobile/link-picker/link-picker-screen.native.js"],"sourcesContent":["/**\n * External dependencies\n */\nimport { Keyboard } from 'react-native';\nimport { useNavigation, useRoute } from '@react-navigation/native';\n\n/**\n * WordPress dependencies\n */\nimport { useEffect, useMemo, useRef } from '@wordpress/element';\n\n/**\n * Internal dependencies\n */\nimport { LinkPicker } from './';\n\nconst LinkPickerScreen = ( { returnScreenName } ) => {\n\tconst navigation = useNavigation();\n\tconst route = useRoute();\n\tconst navigateToLinkTimeoutRef = useRef( null );\n\tconst navigateBackTimeoutRef = useRef( null );\n\n\tconst onLinkPicked = ( { url, title } ) => {\n\t\tKeyboard.dismiss();\n\t\tnavigateToLinkTimeoutRef.current = setTimeout( () => {\n\t\t\tnavigation.navigate( returnScreenName, {\n\t\t\t\tinputValue: url,\n\t\t\t\ttext: title,\n\t\t\t} );\n\t\t}, 100 );\n\t};\n\n\tconst onCancel = () => {\n\t\tKeyboard.dismiss();\n\t\tnavigateBackTimeoutRef.current = setTimeout( () => {\n\t\t\tnavigation.goBack();\n\t\t}, 100 );\n\t};\n\n\tuseEffect( () => {\n\t\treturn () => {\n\t\t\tclearTimeout( navigateToLinkTimeoutRef.current );\n\t\t\tclearTimeout( navigateBackTimeoutRef.current );\n\t\t};\n\t}, [] );\n\n\tconst { inputValue } = route.params;\n\treturn useMemo( () => {\n\t\treturn (\n\t\t\t<LinkPicker\n\t\t\t\tvalue={ inputValue }\n\t\t\t\tonLinkPicked={ onLinkPicked }\n\t\t\t\tonCancel={ onCancel }\n\t\t\t/>\n\t\t);\n\t\t// Disable reason: deferring this refactor to the native team.\n\t\t// see https://github.com/WordPress/gutenberg/pull/41166\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [ inputValue ] );\n};\n\nexport default LinkPickerScreen;\n"],"mappings":";AAAA;AACA;AACA;AACA,SAASA,QAAQ,QAAQ,cAAc;AACvC,SAASC,aAAa,EAAEC,QAAQ,QAAQ,0BAA0B;;AAElE;AACA;AACA;AACA,SAASC,SAAS,EAAEC,OAAO,EAAEC,MAAM,QAAQ,oBAAoB;;AAE/D;AACA;AACA;AACA,SAASC,UAAU,QAAQ,IAAI;AAE/B,MAAMC,gBAAgB,GAAGA,CAAE;EAAEC;AAAiB,CAAC,KAAM;EACpD,MAAMC,UAAU,GAAGR,aAAa,CAAC,CAAC;EAClC,MAAMS,KAAK,GAAGR,QAAQ,CAAC,CAAC;EACxB,MAAMS,wBAAwB,GAAGN,MAAM,CAAE,IAAK,CAAC;EAC/C,MAAMO,sBAAsB,GAAGP,MAAM,CAAE,IAAK,CAAC;EAE7C,MAAMQ,YAAY,GAAGA,CAAE;IAAEC,GAAG;IAAEC;EAAM,CAAC,KAAM;IAC1Cf,QAAQ,CAACgB,OAAO,CAAC,CAAC;IAClBL,wBAAwB,CAACM,OAAO,GAAGC,UAAU,CAAE,MAAM;MACpDT,UAAU,CAACU,QAAQ,CAAEX,gBAAgB,EAAE;QACtCY,UAAU,EAAEN,GAAG;QACfO,IAAI,EAAEN;MACP,CAAE,CAAC;IACJ,CAAC,EAAE,GAAI,CAAC;EACT,CAAC;EAED,MAAMO,QAAQ,GAAGA,CAAA,KAAM;IACtBtB,QAAQ,CAACgB,OAAO,CAAC,CAAC;IAClBJ,sBAAsB,CAACK,OAAO,GAAGC,UAAU,CAAE,MAAM;MAClDT,UAAU,CAACc,MAAM,CAAC,CAAC;IACpB,CAAC,EAAE,GAAI,CAAC;EACT,CAAC;EAEDpB,SAAS,CAAE,MAAM;IAChB,OAAO,MAAM;MACZqB,YAAY,CAAEb,wBAAwB,CAACM,OAAQ,CAAC;MAChDO,YAAY,CAAEZ,sBAAsB,CAACK,OAAQ,CAAC;IAC/C,CAAC;EACF,CAAC,EAAE,EAAG,CAAC;EAEP,MAAM;IAAEG;EAAW,CAAC,GAAGV,KAAK,CAACe,MAAM;EACnC,OAAOrB,OAAO,CAAE,MAAM;IACrB,OACCsB,aAAA,CAACpB,UAAU;MACVqB,KAAK,EAAGP,UAAY;MACpBP,YAAY,EAAGA,YAAc;MAC7BS,QAAQ,EAAGA;IAAU,CACrB,CAAC;IAEH;IACA;IACA;EACD,CAAC,EAAE,CAAEF,UAAU,CAAG,CAAC;AACpB,CAAC;AAED,eAAeb,gBAAgB"}