mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-09-25 05:26:42 +03:00
Merge ab0381934a
into f0330162e6
This commit is contained in:
commit
085be3aa9f
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1 +1 @@
|
|||
* text=auto eol=lf
|
||||
* text=auto eol=lf
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -9,4 +9,4 @@ coverage
|
|||
.idea
|
||||
.eslintcache
|
||||
!packages/redux-devtools-slider-monitor/examples/todomvc/dist/index.html
|
||||
.nx
|
||||
.nx
|
|
@ -0,0 +1,29 @@
|
|||
import React, { HTMLAttributes } from 'react';
|
||||
|
||||
export type CopyIconProps = Omit<
|
||||
HTMLAttributes<SVGElement>,
|
||||
'xmlns' | 'children' | 'viewBox'
|
||||
>;
|
||||
|
||||
/* eslint-disable max-len */
|
||||
/**
|
||||
* @see import { IoCopySharp } from "react-icons/io5";
|
||||
*/
|
||||
export function CopyIcon(props: CopyIconProps): React.JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 512 512"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path d="M456 480H136a24 24 0 0 1-24-24V128a16 16 0 0 1 16-16h328a24 24 0 0 1 24 24v320a24 24 0 0 1-24 24z"></path>
|
||||
<path d="M112 80h288V56a24 24 0 0 0-24-24H60a28 28 0 0 0-28 28v316a24 24 0 0 0 24 24h24V112a32 32 0 0 1 32-32z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
/* eslint-enable max-len */
|
|
@ -1,15 +1,19 @@
|
|||
import { createSelector, Selector } from '@reduxjs/toolkit';
|
||||
import React, { ReactNode, PureComponent } from 'react';
|
||||
import { Action, AnyAction } from 'redux';
|
||||
import { Action, AnyAction, Dispatch } from 'redux';
|
||||
import type { KeyPath, ShouldExpandNodeInitially } from 'react-json-tree';
|
||||
import { QueryPreviewTabs } from '../types';
|
||||
import { QueryPreviewTabs, RtkResourceInfo } from '../types';
|
||||
import { renderTabPanelButtonId, renderTabPanelId } from '../utils/a11y';
|
||||
import { emptyRecord, identity } from '../utils/object';
|
||||
import { TreeView, TreeViewProps } from './TreeView';
|
||||
import { RTKActionButtons } from './RTKQueryActions';
|
||||
|
||||
export interface QueryPreviewActionsProps {
|
||||
isWideLayout: boolean;
|
||||
actionsOfQuery: AnyAction[];
|
||||
dispatch?: Dispatch<AnyAction>;
|
||||
resInfo?: RtkResourceInfo | null;
|
||||
liftedDispatch?: Dispatch<AnyAction>; // Add lifted dispatch for DevTools
|
||||
}
|
||||
|
||||
const keySep = ' - ';
|
||||
|
@ -79,15 +83,23 @@ export class QueryPreviewActions extends PureComponent<QueryPreviewActionsProps>
|
|||
};
|
||||
|
||||
render(): ReactNode {
|
||||
const { isWideLayout, actionsOfQuery } = this.props;
|
||||
const { isWideLayout, actionsOfQuery, dispatch, liftedDispatch, resInfo } =
|
||||
this.props;
|
||||
|
||||
return (
|
||||
<TreeView
|
||||
rootProps={rootProps}
|
||||
data={this.selectFormattedActions(actionsOfQuery)}
|
||||
isWideLayout={isWideLayout}
|
||||
shouldExpandNodeInitially={this.shouldExpandNodeInitially}
|
||||
/>
|
||||
<>
|
||||
<RTKActionButtons
|
||||
dispatch={dispatch}
|
||||
liftedDispatch={liftedDispatch}
|
||||
data={this.selectFormattedActions(actionsOfQuery)}
|
||||
/>
|
||||
<TreeView
|
||||
rootProps={rootProps}
|
||||
data={this.selectFormattedActions(actionsOfQuery)}
|
||||
isWideLayout={isWideLayout}
|
||||
shouldExpandNodeInitially={this.shouldExpandNodeInitially}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
import { Button, Toolbar } from '@redux-devtools/ui';
|
||||
import React from 'react';
|
||||
import { AnyAction, Dispatch } from 'redux';
|
||||
|
||||
interface RTKActionButtonsProps {
|
||||
dispatch?: Dispatch<AnyAction>;
|
||||
liftedDispatch?: Dispatch<AnyAction>;
|
||||
data: Record<string, AnyAction>;
|
||||
}
|
||||
|
||||
const ACTION_TYPES = {
|
||||
REFRESH: 'REFRESH',
|
||||
FULFILLED: 'FULFILLED',
|
||||
TRIGGER_FETCHING: 'TRIGGER_FETCHING',
|
||||
} as const;
|
||||
|
||||
const REFRESH_TIMEOUT_MS = 1000;
|
||||
|
||||
export const RTKActionButtons: React.FC<RTKActionButtonsProps> = ({
|
||||
dispatch,
|
||||
liftedDispatch,
|
||||
data,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const pending = Object.values(data).filter(
|
||||
(action) => action.meta?.requestStatus === 'pending',
|
||||
);
|
||||
|
||||
const fulfilled = Object.values(data).filter(
|
||||
(action) => action.meta?.requestStatus === 'fulfilled',
|
||||
);
|
||||
|
||||
// Get the most recent actions
|
||||
const recentPending = pending[pending.length - 1];
|
||||
const recentFulfilled = fulfilled[fulfilled.length - 1];
|
||||
|
||||
const handleAction = (
|
||||
actionType: keyof typeof ACTION_TYPES,
|
||||
action: AnyAction,
|
||||
fulfilledAction?: AnyAction,
|
||||
) => {
|
||||
setIsLoading(true);
|
||||
if (liftedDispatch) {
|
||||
try {
|
||||
const performAction = {
|
||||
type: 'PERFORM_ACTION' as const,
|
||||
action,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
liftedDispatch(performAction);
|
||||
|
||||
// For refresh actions, automatically dispatch fulfilled action after delay
|
||||
if (actionType === ACTION_TYPES.REFRESH && fulfilledAction) {
|
||||
setTimeout(() => {
|
||||
const resolveAction = {
|
||||
type: 'PERFORM_ACTION' as const,
|
||||
action: fulfilledAction,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
liftedDispatch(resolveAction);
|
||||
setIsLoading(false);
|
||||
}, REFRESH_TIMEOUT_MS);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in liftedDispatch:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
try {
|
||||
dispatch(action);
|
||||
|
||||
// For refresh actions, automatically dispatch fulfilled action after delay
|
||||
if (actionType === ACTION_TYPES.REFRESH && fulfilledAction) {
|
||||
setTimeout(() => {
|
||||
dispatch(fulfilledAction);
|
||||
setIsLoading(false);
|
||||
}, REFRESH_TIMEOUT_MS);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in dispatch:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Toolbar
|
||||
borderPosition="bottom"
|
||||
style={{ alignItems: 'center' }}
|
||||
key={JSON.stringify(data)}
|
||||
>
|
||||
<label
|
||||
css={(theme) => ({
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
color: theme.TEXT_COLOR,
|
||||
marginRight: '8px',
|
||||
})}
|
||||
>
|
||||
RTK Query Actions:
|
||||
</label>
|
||||
|
||||
<Button
|
||||
mark={isLoading && 'base0D'}
|
||||
onClick={() =>
|
||||
handleAction(ACTION_TYPES.REFRESH, recentPending, recentFulfilled)
|
||||
}
|
||||
disabled={isLoading || !recentPending || !recentFulfilled}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
mark={isLoading && 'base0D'}
|
||||
onClick={() =>
|
||||
handleAction(ACTION_TYPES.TRIGGER_FETCHING, recentPending)
|
||||
}
|
||||
disabled={isLoading || !recentPending || !recentFulfilled}
|
||||
>
|
||||
Fetching
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleAction(ACTION_TYPES.FULFILLED, recentFulfilled);
|
||||
setIsLoading(false);
|
||||
}}
|
||||
disabled={!recentFulfilled}
|
||||
>
|
||||
Fulfill
|
||||
</Button>
|
||||
</Toolbar>
|
||||
);
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import type { Interpolation, Theme } from '@emotion/react';
|
||||
import { Dispatch, AnyAction } from 'redux';
|
||||
import {
|
||||
QueryPreviewTabs,
|
||||
RtkResourceInfo,
|
||||
|
@ -61,6 +62,8 @@ export interface QueryPreviewProps<S = unknown> {
|
|||
readonly isWideLayout: boolean;
|
||||
readonly selectorsSource: SelectorsSource<S>;
|
||||
readonly selectors: InspectorSelectors<S>;
|
||||
readonly dispatch?: Dispatch<AnyAction>;
|
||||
readonly liftedDispatch?: Dispatch<AnyAction>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,9 +114,12 @@ const MappedApiPreview = mapProps<QueryPreviewTabProps, QueryPreviewApiProps>(
|
|||
const MappedQueryPreviewActions = mapProps<
|
||||
QueryPreviewTabProps,
|
||||
QueryPreviewActionsProps
|
||||
>(({ isWideLayout, selectorsSource, selectors }) => ({
|
||||
>(({ isWideLayout, selectorsSource, selectors, resInfo, ...props }) => ({
|
||||
isWideLayout,
|
||||
actionsOfQuery: selectors.selectActionsOfCurrentQuery(selectorsSource),
|
||||
dispatch: (props as any).dispatch,
|
||||
liftedDispatch: (props as any).liftedDispatch,
|
||||
resInfo,
|
||||
}))(QueryPreviewActions);
|
||||
|
||||
const tabs: ReadonlyArray<
|
||||
|
|
|
@ -217,6 +217,8 @@ class RtkQueryInspector<S, A extends Action<string>> extends PureComponent<
|
|||
onTabChange={this.handleTabChange}
|
||||
isWideLayout={isWideLayout}
|
||||
hasNoApis={hasNoApi}
|
||||
dispatch={this.props.dispatch}
|
||||
liftedDispatch={this.props.dispatch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,11 +1,58 @@
|
|||
import React from 'react';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import type { GetItemString, LabelRenderer } from 'react-json-tree';
|
||||
import { isCollection, isIndexed, isKeyed } from 'immutable';
|
||||
import isIterable from '../utils/isIterable';
|
||||
import { DATA_TYPE_KEY } from '../monitor-config';
|
||||
import { Button, Toolbar } from '@redux-devtools/ui';
|
||||
import { CopyIcon } from '../components/CopyIcon';
|
||||
|
||||
const IS_IMMUTABLE_KEY = '@@__IS_IMMUTABLE__@@';
|
||||
|
||||
const copyToClipboard = async (data: any) => {
|
||||
try {
|
||||
const text =
|
||||
typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||
await navigator.clipboard.writeText(text);
|
||||
console.log('Copied to clipboard');
|
||||
} catch (err) {
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value =
|
||||
typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
console.log('Copied to clipboard');
|
||||
}
|
||||
};
|
||||
|
||||
const CopyButton: React.FC<{ data: any }> = ({ data }) => {
|
||||
const iconStyle: CSSProperties = {
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
};
|
||||
|
||||
return (
|
||||
<Toolbar
|
||||
noBorder
|
||||
style={{
|
||||
display: 'inline',
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void copyToClipboard(data);
|
||||
}}
|
||||
>
|
||||
<CopyIcon style={iconStyle} />
|
||||
</Button>
|
||||
</Toolbar>
|
||||
);
|
||||
};
|
||||
|
||||
function isImmutable(value: unknown) {
|
||||
return isKeyed(value) || isIndexed(value) || isCollection(value);
|
||||
}
|
||||
|
@ -79,6 +126,7 @@ export const getItemString: GetItemString = (type: string, data: any) => (
|
|||
? `${data[DATA_TYPE_KEY] as string} `
|
||||
: ''}
|
||||
{getText(type, data, false, undefined)}
|
||||
<CopyButton data={data} />
|
||||
</span>
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user