feat(inspector-monitor): add noop action toggle button

This commit is contained in:
FaberVitale 2021-07-24 12:56:08 +02:00
parent 326cfdf217
commit 9c63c712fc
6 changed files with 214 additions and 58 deletions

View File

@ -6,6 +6,8 @@ import { PerformAction } from '@redux-devtools/core';
import { StylingFunction } from 'react-base16-styling'; import { StylingFunction } from 'react-base16-styling';
import ActionListRow from './ActionListRow'; import ActionListRow from './ActionListRow';
import ActionListHeader from './ActionListHeader'; import ActionListHeader from './ActionListHeader';
import { filterActions } from './utils/filters';
import { ActionForm } from './redux';
function getTimestamps<A extends Action<unknown>>( function getTimestamps<A extends Action<unknown>>(
actions: { [actionId: number]: PerformAction<A> }, actions: { [actionId: number]: PerformAction<A> },
@ -21,15 +23,16 @@ function getTimestamps<A extends Action<unknown>>(
}; };
} }
interface Props<A extends Action<unknown>> { interface Props<S, A extends Action<unknown>> {
actions: { [actionId: number]: PerformAction<A> }; actions: { [actionId: number]: PerformAction<A> };
actionIds: number[]; actionIds: number[];
isWideLayout: boolean; isWideLayout: boolean;
searchValue: string | undefined;
selectedActionId: number | null; selectedActionId: number | null;
startActionId: number | null; startActionId: number | null;
skippedActionIds: number[]; skippedActionIds: number[];
computedStates: { state: S; error?: string }[];
draggableActions: boolean; draggableActions: boolean;
actionForm: ActionForm;
hideMainButtons: boolean | undefined; hideMainButtons: boolean | undefined;
hideActionButtons: boolean | undefined; hideActionButtons: boolean | undefined;
styling: StylingFunction; styling: StylingFunction;
@ -40,18 +43,20 @@ interface Props<A extends Action<unknown>> {
onCommit: () => void; onCommit: () => void;
onSweep: () => void; onSweep: () => void;
onReorderAction: (actionId: number, beforeActionId: number) => void; onReorderAction: (actionId: number, beforeActionId: number) => void;
onActionFormChange: (formValues: Partial<ActionForm>) => void;
currentActionId: number; currentActionId: number;
lastActionId: number; lastActionId: number;
} }
export default class ActionList< export default class ActionList<
S,
A extends Action<unknown> A extends Action<unknown>
> extends PureComponent<Props<A>> { > extends PureComponent<Props<S, A>> {
node?: HTMLDivElement | null; node?: HTMLDivElement | null;
scrollDown?: boolean; scrollDown?: boolean;
drake?: Drake; drake?: Drake;
UNSAFE_componentWillReceiveProps(nextProps: Props<A>) { UNSAFE_componentWillReceiveProps(nextProps: Props<S, A>) {
const node = this.node; const node = this.node;
if (!node) { if (!node) {
this.scrollDown = true; this.scrollDown = true;
@ -119,23 +124,16 @@ export default class ActionList<
startActionId, startActionId,
onSelect, onSelect,
onSearch, onSearch,
searchValue,
currentActionId, currentActionId,
hideMainButtons, hideMainButtons,
hideActionButtons, hideActionButtons,
onCommit, onCommit,
onSweep, onSweep,
actionForm,
onActionFormChange,
onJumpToState, onJumpToState,
} = this.props; } = this.props;
const lowerSearchValue = searchValue && searchValue.toLowerCase(); const filteredActionIds = filterActions<unknown, A>(this.props);
const filteredActionIds = searchValue
? actionIds.filter(
(id) =>
(actions[id].action.type as string)
.toLowerCase()
.indexOf(lowerSearchValue as string) !== -1
)
: actionIds;
return ( return (
<div <div
@ -150,6 +148,8 @@ export default class ActionList<
onSearch={onSearch} onSearch={onSearch}
onCommit={onCommit} onCommit={onCommit}
onSweep={onSweep} onSweep={onSweep}
actionForm={actionForm}
onActionFormChange={onActionFormChange}
hideMainButtons={hideMainButtons} hideMainButtons={hideMainButtons}
hasSkippedActions={skippedActionIds.length > 0} hasSkippedActions={skippedActionIds.length > 0}
hasStagedActions={actionIds.length > 1} hasStagedActions={actionIds.length > 1}

View File

@ -2,6 +2,7 @@ import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { StylingFunction } from 'react-base16-styling'; import { StylingFunction } from 'react-base16-styling';
import RightSlider from './RightSlider'; import RightSlider from './RightSlider';
import { ActionForm } from './redux';
const getActiveButtons = (hasSkippedActions: boolean): ('Sweep' | 'Commit')[] => const getActiveButtons = (hasSkippedActions: boolean): ('Sweep' | 'Commit')[] =>
[hasSkippedActions && 'Sweep', 'Commit'].filter( [hasSkippedActions && 'Sweep', 'Commit'].filter(
@ -16,6 +17,8 @@ interface Props {
hideMainButtons: boolean | undefined; hideMainButtons: boolean | undefined;
hasSkippedActions: boolean; hasSkippedActions: boolean;
hasStagedActions: boolean; hasStagedActions: boolean;
actionForm: ActionForm;
onActionFormChange: (formValues: Partial<ActionForm>) => void;
} }
const ActionListHeader: FunctionComponent<Props> = ({ const ActionListHeader: FunctionComponent<Props> = ({
@ -26,41 +29,66 @@ const ActionListHeader: FunctionComponent<Props> = ({
onCommit, onCommit,
onSweep, onSweep,
hideMainButtons, hideMainButtons,
}) => ( actionForm,
<div {...styling('actionListHeader')}> onActionFormChange,
<input }) => {
{...styling('actionListHeaderSearch')} const { isNoopFilterActive } = actionForm;
onChange={(e) => onSearch(e.target.value)}
placeholder="filter..." return (
/> <>
{!hideMainButtons && ( <div {...styling('actionListHeader')}>
<div {...styling('actionListHeaderWrapper')}> <input
<RightSlider shown={hasStagedActions} styling={styling}> {...styling('actionListHeaderSearch')}
<div {...styling('actionListHeaderSelector')}> onChange={(e) => onSearch(e.target.value)}
{getActiveButtons(hasSkippedActions).map((btn) => ( placeholder="filter..."
<div />
key={btn} {!hideMainButtons && (
onClick={() => <div {...styling('actionListHeaderWrapper')}>
({ <RightSlider shown={hasStagedActions} styling={styling}>
Commit: onCommit, <div {...styling('actionListHeaderSelector')}>
Sweep: onSweep, {getActiveButtons(hasSkippedActions).map((btn) => (
}[btn]()) <div
} key={btn}
{...styling( onClick={() =>
['selectorButton', 'selectorButtonSmall'], ({
false, Commit: onCommit,
true Sweep: onSweep,
)} }[btn]())
> }
{btn} {...styling(
['selectorButton', 'selectorButtonSmall'],
false,
true
)}
>
{btn}
</div>
))}
</div> </div>
))} </RightSlider>
</div> </div>
</RightSlider> )}
</div> </div>
)} <div {...styling(['actionListHeader', 'actionListHeaderSecondRow'])}>
</div> <button
); title="Toggle visibility of noop actions"
aria-label="Toggle visibility of noop actions"
aria-pressed={!isNoopFilterActive}
onClick={() =>
onActionFormChange({ isNoopFilterActive: !isNoopFilterActive })
}
type="button"
{...styling(
['selectorButton', 'selectorButtonSmall', !isNoopFilterActive && 'selectorButtonSelected'],
isNoopFilterActive
)}
>
noop
</button>
</div>
</>
);
};
ActionListHeader.propTypes = { ActionListHeader.propTypes = {
styling: PropTypes.func.isRequired, styling: PropTypes.func.isRequired,
@ -70,6 +98,11 @@ ActionListHeader.propTypes = {
hideMainButtons: PropTypes.bool, hideMainButtons: PropTypes.bool,
hasSkippedActions: PropTypes.bool.isRequired, hasSkippedActions: PropTypes.bool.isRequired,
hasStagedActions: PropTypes.bool.isRequired, hasStagedActions: PropTypes.bool.isRequired,
actionForm: PropTypes.shape({
searchValue: PropTypes.string.isRequired,
isNoopFilterActive: PropTypes.bool.isRequired,
}).isRequired,
onActionFormChange: PropTypes.func.isRequired,
}; };
export default ActionListHeader; export default ActionListHeader;

View File

@ -22,6 +22,8 @@ import ActionPreview, { Tab } from './ActionPreview';
import getInspectedState from './utils/getInspectedState'; import getInspectedState from './utils/getInspectedState';
import createDiffPatcher from './createDiffPatcher'; import createDiffPatcher from './createDiffPatcher';
import { import {
ActionForm,
changeActionFormValues,
DevtoolsInspectorAction, DevtoolsInspectorAction,
DevtoolsInspectorState, DevtoolsInspectorState,
reducer, reducer,
@ -249,6 +251,10 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
this.props.dispatch(updateMonitorState(monitorState)); this.props.dispatch(updateMonitorState(monitorState));
}; };
handleActionFormChange = (formValues: Partial<ActionForm>) => {
this.props.dispatch(changeActionFormValues(formValues));
};
updateSizeMode() { updateSizeMode() {
const isWideLayout = this.inspectorRef!.offsetWidth > 500; const isWideLayout = this.inspectorRef!.offsetWidth > 500;
@ -301,7 +307,7 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
hideMainButtons, hideMainButtons,
hideActionButtons, hideActionButtons,
} = this.props; } = this.props;
const { selectedActionId, startActionId, searchValue, tabName } = const { selectedActionId, startActionId, actionForm, tabName } =
monitorState; monitorState;
const inspectedPathType = const inspectedPathType =
tabName === 'Action' ? 'inspectedActionPath' : 'inspectedStatePath'; tabName === 'Action' ? 'inspectedActionPath' : 'inspectedStatePath';
@ -323,7 +329,6 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
actions, actions,
actionIds, actionIds,
isWideLayout, isWideLayout,
searchValue,
selectedActionId, selectedActionId,
startActionId, startActionId,
skippedActionIds, skippedActionIds,
@ -332,7 +337,10 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
hideActionButtons, hideActionButtons,
styling, styling,
}} }}
actionForm={actionForm}
computedStates={computedStates}
onSearch={this.handleSearch} onSearch={this.handleSearch}
onActionFormChange={this.handleActionFormChange}
onSelect={this.handleSelectAction} onSelect={this.handleSelectAction}
onToggleAction={this.handleToggleAction} onToggleAction={this.handleToggleAction}
onJumpToState={this.handleJumpToState} onJumpToState={this.handleJumpToState}
@ -400,7 +408,7 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
}; };
handleSearch = (val: string) => { handleSearch = (val: string) => {
this.updateMonitorState({ searchValue: val }); this.handleActionFormChange({ searchValue: val });
}; };
handleSelectAction = ( handleSelectAction = (

View File

@ -4,17 +4,38 @@ import { DevtoolsInspectorProps } from './DevtoolsInspector';
const UPDATE_MONITOR_STATE = const UPDATE_MONITOR_STATE =
'@@redux-devtools-inspector-monitor/UPDATE_MONITOR_STATE'; '@@redux-devtools-inspector-monitor/UPDATE_MONITOR_STATE';
const ACTION_FORM_VALUE_CHANGE =
'@@redux-devtools-inspector-monitor/ACTION_FORM_VALUE_CHANGE';
export interface ActionForm {
searchValue: string;
isNoopFilterActive: boolean;
}
export interface UpdateMonitorStateAction { export interface UpdateMonitorStateAction {
type: typeof UPDATE_MONITOR_STATE; type: typeof UPDATE_MONITOR_STATE;
monitorState: Partial<DevtoolsInspectorState>; monitorState: Partial<DevtoolsInspectorState>;
} }
export interface ChangeActionFormAction {
type: typeof ACTION_FORM_VALUE_CHANGE;
formValues: Partial<ActionForm>;
}
export function updateMonitorState( export function updateMonitorState(
monitorState: Partial<DevtoolsInspectorState> monitorState: Partial<DevtoolsInspectorState>
): UpdateMonitorStateAction { ): UpdateMonitorStateAction {
return { type: UPDATE_MONITOR_STATE, monitorState }; return { type: UPDATE_MONITOR_STATE, monitorState };
} }
export type DevtoolsInspectorAction = UpdateMonitorStateAction; export function changeActionFormValues(
formValues: Partial<ActionForm>
): ChangeActionFormAction {
return { type: ACTION_FORM_VALUE_CHANGE, formValues };
}
export type DevtoolsInspectorAction =
| UpdateMonitorStateAction
| ChangeActionFormAction;
export interface DevtoolsInspectorState { export interface DevtoolsInspectorState {
selectedActionId: number | null; selectedActionId: number | null;
@ -22,7 +43,7 @@ export interface DevtoolsInspectorState {
inspectedActionPath: (string | number)[]; inspectedActionPath: (string | number)[];
inspectedStatePath: (string | number)[]; inspectedStatePath: (string | number)[];
tabName: string; tabName: string;
searchValue?: string; actionForm: ActionForm;
} }
export const DEFAULT_STATE: DevtoolsInspectorState = { export const DEFAULT_STATE: DevtoolsInspectorState = {
@ -31,18 +52,33 @@ export const DEFAULT_STATE: DevtoolsInspectorState = {
inspectedActionPath: [], inspectedActionPath: [],
inspectedStatePath: [], inspectedStatePath: [],
tabName: 'Diff', tabName: 'Diff',
actionForm: {
searchValue: '',
isNoopFilterActive: false,
},
}; };
function reduceUpdateState( function internalMonitorActionReducer(
state: DevtoolsInspectorState, state: DevtoolsInspectorState,
action: DevtoolsInspectorAction action: DevtoolsInspectorAction
) { ): DevtoolsInspectorState {
return action.type === UPDATE_MONITOR_STATE switch (action.type) {
? { case UPDATE_MONITOR_STATE:
return {
...state, ...state,
...action.monitorState, ...action.monitorState,
} };
: state; case ACTION_FORM_VALUE_CHANGE:
return {
...state,
actionForm: {
...state.actionForm,
...action.formValues,
},
};
default:
return state;
}
} }
export function reducer<S, A extends Action<unknown>>( export function reducer<S, A extends Action<unknown>>(
@ -51,6 +87,6 @@ export function reducer<S, A extends Action<unknown>>(
action: DevtoolsInspectorAction action: DevtoolsInspectorAction
) { ) {
return { return {
...reduceUpdateState(state, action), ...internalMonitorActionReducer(state, action),
}; };
} }

View File

@ -83,6 +83,10 @@ const getSheetFromColorMap = (map: ColorMap) => ({
'border-color': map.LIST_BORDER_COLOR, 'border-color': map.LIST_BORDER_COLOR,
}, },
actionListHeaderSecondRow: {
padding: '5px 10px',
},
actionListRows: { actionListRows: {
overflow: 'auto', overflow: 'auto',

View File

@ -0,0 +1,75 @@
import type { Action } from 'redux';
import type { LiftedState, PerformAction } from '@redux-devtools/core';
import { ActionForm } from '../redux';
type ComputedStates<S> = LiftedState<
S,
Action<unknown>,
unknown
>['computedStates'];
function isNoopAction<S>(
actionId: number,
computedStates: ComputedStates<S>
): boolean {
return (
actionId === 0 ||
computedStates[actionId]?.state === computedStates[actionId - 1]?.state
);
}
function filterStateChangingAction<S>(
actionIds: number[],
computedStates: ComputedStates<S>
): number[] {
return actionIds.filter(
(actionId) => !isNoopAction(actionId, computedStates)
);
}
function filterActionsBySearchValue<A extends Action<unknown>>(
searchValue: string | undefined,
actionIds: number[],
actions: Record<number, PerformAction<A>>
): number[] {
const lowerSearchValue = searchValue && searchValue.toLowerCase();
if (!lowerSearchValue) {
return actionIds;
}
return actionIds.filter((id) => {
const type = actions[id].action.type;
return (
type != null &&
`${type as string}`.toLowerCase().includes(lowerSearchValue)
);
});
}
export interface FilterActionsPayload<S, A extends Action<unknown>> {
readonly actionIds: number[];
readonly actions: Record<number, PerformAction<A>>;
readonly computedStates: ComputedStates<S>;
readonly actionForm: ActionForm;
}
export function filterActions<S, A extends Action<unknown>>({
actionIds,
actions,
computedStates,
actionForm,
}: FilterActionsPayload<S, A>): number[] {
let output = filterActionsBySearchValue(
actionForm.searchValue,
actionIds,
actions
);
if (actionForm.isNoopFilterActive) {
output = filterStateChangingAction(actionIds, computedStates);
}
return output;
}