mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-07-22 14:09:46 +03:00
Merge b3c88840d5
into 8979004b53
This commit is contained in:
commit
96e8022a82
|
@ -1,6 +1,6 @@
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import JSONArrow from './JSONArrow';
|
import JSONArrow from './JSONArrow';
|
||||||
import type { CircularCache, CommonInternalProps } from './types';
|
import type { CircularCache, CommonInternalProps, KeyPath } from './types';
|
||||||
|
|
||||||
interface Props extends CommonInternalProps {
|
interface Props extends CommonInternalProps {
|
||||||
data: unknown;
|
data: unknown;
|
||||||
|
@ -13,12 +13,25 @@ interface Props extends CommonInternalProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ItemRange(props: Props) {
|
export default function ItemRange(props: Props) {
|
||||||
const { styling, from, to, renderChildNodes, nodeType } = props;
|
const {
|
||||||
|
styling,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
renderChildNodes,
|
||||||
|
nodeType,
|
||||||
|
keyPath,
|
||||||
|
searchResultPath,
|
||||||
|
level,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState<boolean>(false);
|
const [userExpanded, setUserExpanded] = useState<boolean>(false);
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
setExpanded(!expanded);
|
setUserExpanded(!userExpanded);
|
||||||
}, [expanded]);
|
}, [userExpanded]);
|
||||||
|
|
||||||
|
const expanded =
|
||||||
|
userExpanded ||
|
||||||
|
containsSearchResult(keyPath, from, to, searchResultPath, level);
|
||||||
|
|
||||||
return expanded ? (
|
return expanded ? (
|
||||||
<div {...styling('itemRange', expanded)}>
|
<div {...styling('itemRange', expanded)}>
|
||||||
|
@ -37,3 +50,15 @@ export default function ItemRange(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const containsSearchResult = (
|
||||||
|
ownKeyPath: KeyPath,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
resultKeyPath: KeyPath,
|
||||||
|
level: number,
|
||||||
|
): boolean => {
|
||||||
|
const searchLevel = level > 1 ? level - 1 : level;
|
||||||
|
const nextKey = Number(resultKeyPath[searchLevel]);
|
||||||
|
return !isNaN(nextKey) && nextKey >= from && nextKey <= to;
|
||||||
|
};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import JSONArrow from './JSONArrow';
|
|
||||||
import getCollectionEntries from './getCollectionEntries';
|
|
||||||
import JSONNode from './JSONNode';
|
|
||||||
import ItemRange from './ItemRange';
|
import ItemRange from './ItemRange';
|
||||||
import type { CircularCache, CommonInternalProps } from './types';
|
import JSONArrow from './JSONArrow';
|
||||||
|
import JSONNode from './JSONNode';
|
||||||
|
import getCollectionEntries from './getCollectionEntries';
|
||||||
|
import type { CircularCache, CommonInternalProps, KeyPath } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders nested values (eg. objects, arrays, lists, etc.)
|
* Renders nested values (eg. objects, arrays, lists, etc.)
|
||||||
|
@ -110,17 +110,21 @@ export default function JSONNestedNode(props: Props) {
|
||||||
nodeType,
|
nodeType,
|
||||||
nodeTypeIndicator,
|
nodeTypeIndicator,
|
||||||
shouldExpandNodeInitially,
|
shouldExpandNodeInitially,
|
||||||
|
searchResultPath,
|
||||||
styling,
|
styling,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState<boolean>(
|
const [userExpanded, setUserExpanded] = useState<boolean>(
|
||||||
// calculate individual node expansion if necessary
|
// calculate individual node expansion if necessary
|
||||||
isCircular ? false : shouldExpandNodeInitially(keyPath, data, level),
|
isCircular ? false : shouldExpandNodeInitially(keyPath, data, level),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (expandable) setExpanded(!expanded);
|
if (expandable) setUserExpanded(!userExpanded);
|
||||||
}, [expandable, expanded]);
|
}, [expandable, userExpanded]);
|
||||||
|
|
||||||
|
const expanded =
|
||||||
|
userExpanded || containsSearchResult(keyPath, searchResultPath, level);
|
||||||
|
|
||||||
const renderedChildren =
|
const renderedChildren =
|
||||||
expanded || (hideRoot && level === 0)
|
expanded || (hideRoot && level === 0)
|
||||||
|
@ -175,3 +179,15 @@ export default function JSONNestedNode(props: Props) {
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const containsSearchResult = (
|
||||||
|
ownKeyPath: KeyPath,
|
||||||
|
resultKeyPath: KeyPath,
|
||||||
|
level: number,
|
||||||
|
): boolean => {
|
||||||
|
const searchLevel = level > 0 ? level - 1 : level;
|
||||||
|
const currKey = [...ownKeyPath].reverse()[searchLevel];
|
||||||
|
return resultKeyPath && currKey !== undefined
|
||||||
|
? resultKeyPath[searchLevel] === currKey.toString()
|
||||||
|
: false;
|
||||||
|
};
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
// port by Daniele Zannotti http://www.github.com/dzannotti <dzannotti@me.com>
|
// port by Daniele Zannotti http://www.github.com/dzannotti <dzannotti@me.com>
|
||||||
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
import type { StylingValue, Theme } from 'react-base16-styling';
|
||||||
|
import { invertTheme } from 'react-base16-styling';
|
||||||
import JSONNode from './JSONNode';
|
import JSONNode from './JSONNode';
|
||||||
import createStylingFromTheme from './createStylingFromTheme';
|
import createStylingFromTheme from './createStylingFromTheme';
|
||||||
import { invertTheme } from 'react-base16-styling';
|
|
||||||
import type { StylingValue, Theme } from 'react-base16-styling';
|
|
||||||
import type {
|
import type {
|
||||||
CommonExternalProps,
|
CommonExternalProps,
|
||||||
GetItemString,
|
GetItemString,
|
||||||
|
@ -41,6 +41,8 @@ export function JSONTree({
|
||||||
labelRenderer = defaultLabelRenderer,
|
labelRenderer = defaultLabelRenderer,
|
||||||
valueRenderer = identity,
|
valueRenderer = identity,
|
||||||
shouldExpandNodeInitially = expandRootNode,
|
shouldExpandNodeInitially = expandRootNode,
|
||||||
|
isSearchInProgress = false,
|
||||||
|
searchResultPath = [],
|
||||||
hideRoot = false,
|
hideRoot = false,
|
||||||
getItemString = defaultItemString,
|
getItemString = defaultItemString,
|
||||||
postprocessValue = identity,
|
postprocessValue = identity,
|
||||||
|
@ -64,6 +66,8 @@ export function JSONTree({
|
||||||
labelRenderer={labelRenderer}
|
labelRenderer={labelRenderer}
|
||||||
valueRenderer={valueRenderer}
|
valueRenderer={valueRenderer}
|
||||||
shouldExpandNodeInitially={shouldExpandNodeInitially}
|
shouldExpandNodeInitially={shouldExpandNodeInitially}
|
||||||
|
searchResultPath={searchResultPath}
|
||||||
|
isSearchInProgress={isSearchInProgress}
|
||||||
hideRoot={hideRoot}
|
hideRoot={hideRoot}
|
||||||
getItemString={getItemString}
|
getItemString={getItemString}
|
||||||
postprocessValue={postprocessValue}
|
postprocessValue={postprocessValue}
|
||||||
|
@ -75,16 +79,16 @@ export function JSONTree({
|
||||||
}
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
|
CommonExternalProps,
|
||||||
|
GetItemString,
|
||||||
|
IsCustomNode,
|
||||||
Key,
|
Key,
|
||||||
KeyPath,
|
KeyPath,
|
||||||
GetItemString,
|
|
||||||
LabelRenderer,
|
LabelRenderer,
|
||||||
ValueRenderer,
|
|
||||||
ShouldExpandNodeInitially,
|
|
||||||
PostprocessValue,
|
PostprocessValue,
|
||||||
IsCustomNode,
|
ShouldExpandNodeInitially,
|
||||||
SortObjectKeys,
|
SortObjectKeys,
|
||||||
Styling,
|
Styling,
|
||||||
CommonExternalProps,
|
ValueRenderer,
|
||||||
} from './types';
|
} from './types';
|
||||||
export type { StylingValue };
|
export type { StylingValue };
|
||||||
|
|
|
@ -47,6 +47,8 @@ export interface CommonExternalProps {
|
||||||
labelRenderer: LabelRenderer;
|
labelRenderer: LabelRenderer;
|
||||||
valueRenderer: ValueRenderer;
|
valueRenderer: ValueRenderer;
|
||||||
shouldExpandNodeInitially: ShouldExpandNodeInitially;
|
shouldExpandNodeInitially: ShouldExpandNodeInitially;
|
||||||
|
searchResultPath: KeyPath;
|
||||||
|
isSearchInProgress: boolean;
|
||||||
hideRoot: boolean;
|
hideRoot: boolean;
|
||||||
getItemString: GetItemString;
|
getItemString: GetItemString;
|
||||||
postprocessValue: PostprocessValue;
|
postprocessValue: PostprocessValue;
|
||||||
|
|
|
@ -1,31 +1,33 @@
|
||||||
|
import { LiftedAction, LiftedState } from '@redux-devtools/core';
|
||||||
import { SchemeName, ThemeName } from '@redux-devtools/ui';
|
import { SchemeName, ThemeName } from '@redux-devtools/ui';
|
||||||
import { AuthStates, States } from 'socketcluster-client/lib/clientsocket';
|
import { Action } from 'redux';
|
||||||
import { REHYDRATE } from 'redux-persist';
|
import { REHYDRATE } from 'redux-persist';
|
||||||
|
import { AuthStates, States } from 'socketcluster-client/lib/clientsocket';
|
||||||
import {
|
import {
|
||||||
CHANGE_SECTION,
|
CHANGE_SECTION,
|
||||||
|
CHANGE_STATE_TREE_SETTINGS,
|
||||||
CHANGE_THEME,
|
CHANGE_THEME,
|
||||||
SELECT_INSTANCE,
|
CLEAR_NOTIFICATION,
|
||||||
SELECT_MONITOR,
|
ERROR,
|
||||||
UPDATE_MONITOR_STATE,
|
EXPORT,
|
||||||
|
GET_REPORT_ERROR,
|
||||||
|
GET_REPORT_REQUEST,
|
||||||
|
GET_REPORT_SUCCESS,
|
||||||
LIFTED_ACTION,
|
LIFTED_ACTION,
|
||||||
MONITOR_ACTION,
|
MONITOR_ACTION,
|
||||||
EXPORT,
|
REMOVE_INSTANCE,
|
||||||
TOGGLE_SYNC,
|
SELECT_INSTANCE,
|
||||||
TOGGLE_SLIDER,
|
SELECT_MONITOR,
|
||||||
|
SET_PERSIST,
|
||||||
|
SET_STATE,
|
||||||
|
SHOW_NOTIFICATION,
|
||||||
TOGGLE_DISPATCHER,
|
TOGGLE_DISPATCHER,
|
||||||
TOGGLE_PERSIST,
|
TOGGLE_PERSIST,
|
||||||
GET_REPORT_REQUEST,
|
TOGGLE_SLIDER,
|
||||||
SHOW_NOTIFICATION,
|
TOGGLE_SYNC,
|
||||||
CLEAR_NOTIFICATION,
|
UPDATE_MONITOR_STATE,
|
||||||
UPDATE_STATE,
|
|
||||||
UPDATE_REPORTS,
|
UPDATE_REPORTS,
|
||||||
REMOVE_INSTANCE,
|
UPDATE_STATE,
|
||||||
SET_STATE,
|
|
||||||
GET_REPORT_ERROR,
|
|
||||||
GET_REPORT_SUCCESS,
|
|
||||||
ERROR,
|
|
||||||
SET_PERSIST,
|
|
||||||
CHANGE_STATE_TREE_SETTINGS,
|
|
||||||
} from '../constants/actionTypes';
|
} from '../constants/actionTypes';
|
||||||
import {
|
import {
|
||||||
AUTH_ERROR,
|
AUTH_ERROR,
|
||||||
|
@ -43,12 +45,9 @@ import {
|
||||||
SUBSCRIBE_SUCCESS,
|
SUBSCRIBE_SUCCESS,
|
||||||
UNSUBSCRIBE,
|
UNSUBSCRIBE,
|
||||||
} from '../constants/socketActionTypes';
|
} from '../constants/socketActionTypes';
|
||||||
import { Action } from 'redux';
|
|
||||||
import { Features, State } from '../reducers/instances';
|
import { Features, State } from '../reducers/instances';
|
||||||
import { MonitorStateMonitorState } from '../reducers/monitor';
|
import { MonitorStateMonitorState } from '../reducers/monitor';
|
||||||
import { LiftedAction } from '@redux-devtools/core';
|
|
||||||
import { Data } from '../reducers/reports';
|
import { Data } from '../reducers/reports';
|
||||||
import { LiftedState } from '@redux-devtools/core';
|
|
||||||
|
|
||||||
let monitorReducer: (
|
let monitorReducer: (
|
||||||
monitorProps: unknown,
|
monitorProps: unknown,
|
||||||
|
@ -86,6 +85,7 @@ export function changeTheme(data: ChangeThemeData): ChangeThemeAction {
|
||||||
interface ChangeStateTreeSettingsFormData {
|
interface ChangeStateTreeSettingsFormData {
|
||||||
readonly sortAlphabetically: boolean;
|
readonly sortAlphabetically: boolean;
|
||||||
readonly disableCollection: boolean;
|
readonly disableCollection: boolean;
|
||||||
|
readonly enableSearchPanel: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChangeStateTreeSettingsData {
|
interface ChangeStateTreeSettingsData {
|
||||||
|
@ -96,6 +96,7 @@ export interface ChangeStateTreeSettingsAction {
|
||||||
readonly type: typeof CHANGE_STATE_TREE_SETTINGS;
|
readonly type: typeof CHANGE_STATE_TREE_SETTINGS;
|
||||||
readonly sortAlphabetically: boolean;
|
readonly sortAlphabetically: boolean;
|
||||||
readonly disableCollection: boolean;
|
readonly disableCollection: boolean;
|
||||||
|
readonly enableSearchPanel: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeStateTreeSettings(
|
export function changeStateTreeSettings(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect, ResolveThunks } from 'react-redux';
|
|
||||||
import { Container, Form } from '@redux-devtools/ui';
|
import { Container, Form } from '@redux-devtools/ui';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { ResolveThunks, connect } from 'react-redux';
|
||||||
import { changeStateTreeSettings } from '../../actions';
|
import { changeStateTreeSettings } from '../../actions';
|
||||||
import { StoreState } from '../../reducers';
|
import { StoreState } from '../../reducers';
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ export class StateTree extends Component<Props> {
|
||||||
const formData = {
|
const formData = {
|
||||||
sortAlphabetically: stateTree.sortAlphabetically,
|
sortAlphabetically: stateTree.sortAlphabetically,
|
||||||
disableCollection: stateTree.disableCollection,
|
disableCollection: stateTree.disableCollection,
|
||||||
|
enableSearchPanel: stateTree.enableSearchPanel,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -30,6 +31,10 @@ export class StateTree extends Component<Props> {
|
||||||
title: 'Disable collapsing of nodes',
|
title: 'Disable collapsing of nodes',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
|
enableSearchPanel: {
|
||||||
|
title: 'Show search panel in State tab',
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { withTheme } from 'styled-components';
|
|
||||||
import { LiftedAction, LiftedState } from '@redux-devtools/core';
|
import { LiftedAction, LiftedState } from '@redux-devtools/core';
|
||||||
|
import { ThemeFromProvider } from '@redux-devtools/ui';
|
||||||
|
import React, { Component } from 'react';
|
||||||
import { Action } from 'redux';
|
import { Action } from 'redux';
|
||||||
import getMonitor from '../utils/getMonitor';
|
import { withTheme } from 'styled-components';
|
||||||
import { InitMonitorAction } from '../actions';
|
import { InitMonitorAction } from '../actions';
|
||||||
import { Features, State } from '../reducers/instances';
|
import { Features, State } from '../reducers/instances';
|
||||||
import { MonitorStateMonitorState } from '../reducers/monitor';
|
import { MonitorStateMonitorState } from '../reducers/monitor';
|
||||||
import { ThemeFromProvider } from '@redux-devtools/ui';
|
|
||||||
import { StateTreeSettings } from '../reducers/stateTreeSettings';
|
import { StateTreeSettings } from '../reducers/stateTreeSettings';
|
||||||
|
import getMonitor from '../utils/getMonitor';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
monitor: string;
|
monitor: string;
|
||||||
|
@ -118,6 +118,7 @@ class DevTools extends Component<Props> {
|
||||||
disableStateTreeCollection={
|
disableStateTreeCollection={
|
||||||
this.props.stateTreeSettings.disableCollection
|
this.props.stateTreeSettings.disableCollection
|
||||||
}
|
}
|
||||||
|
enableSearchPanel={this.props.stateTreeSettings.enableSearchPanel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import { CHANGE_STATE_TREE_SETTINGS } from '../constants/actionTypes';
|
|
||||||
import { StoreAction } from '../actions';
|
import { StoreAction } from '../actions';
|
||||||
|
import { CHANGE_STATE_TREE_SETTINGS } from '../constants/actionTypes';
|
||||||
|
|
||||||
export interface StateTreeSettings {
|
export interface StateTreeSettings {
|
||||||
readonly sortAlphabetically: boolean;
|
readonly sortAlphabetically: boolean;
|
||||||
readonly disableCollection: boolean;
|
readonly disableCollection: boolean;
|
||||||
|
readonly enableSearchPanel: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stateTreeSettings(
|
export function stateTreeSettings(
|
||||||
state: StateTreeSettings = {
|
state: StateTreeSettings = {
|
||||||
sortAlphabetically: false,
|
sortAlphabetically: false,
|
||||||
disableCollection: false,
|
disableCollection: false,
|
||||||
|
enableSearchPanel: false,
|
||||||
},
|
},
|
||||||
action: StoreAction,
|
action: StoreAction,
|
||||||
) {
|
) {
|
||||||
|
@ -17,6 +19,7 @@ export function stateTreeSettings(
|
||||||
return {
|
return {
|
||||||
sortAlphabetically: action.sortAlphabetically,
|
sortAlphabetically: action.sortAlphabetically,
|
||||||
disableCollection: action.disableCollection,
|
disableCollection: action.disableCollection,
|
||||||
|
enableSearchPanel: action.enableSearchPanel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Base16Theme } from 'redux-devtools-themes';
|
|
||||||
import { Action } from 'redux';
|
|
||||||
import type { StylingFunction } from 'react-base16-styling';
|
|
||||||
import type { LabelRenderer } from 'react-json-tree';
|
|
||||||
import { PerformAction } from '@redux-devtools/core';
|
import { PerformAction } from '@redux-devtools/core';
|
||||||
import { Delta } from 'jsondiffpatch';
|
import { Delta } from 'jsondiffpatch';
|
||||||
import { DEFAULT_STATE, DevtoolsInspectorState } from './redux';
|
import React, { Component } from 'react';
|
||||||
|
import type { StylingFunction } from 'react-base16-styling';
|
||||||
|
import type { LabelRenderer } from 'react-json-tree';
|
||||||
|
import { Action } from 'redux';
|
||||||
|
import { Base16Theme } from 'redux-devtools-themes';
|
||||||
import ActionPreviewHeader from './ActionPreviewHeader';
|
import ActionPreviewHeader from './ActionPreviewHeader';
|
||||||
|
import { DEFAULT_STATE, DevtoolsInspectorState } from './redux';
|
||||||
|
import ActionTab from './tabs/ActionTab';
|
||||||
import DiffTab from './tabs/DiffTab';
|
import DiffTab from './tabs/DiffTab';
|
||||||
import StateTab from './tabs/StateTab';
|
import StateTab from './tabs/StateTab';
|
||||||
import ActionTab from './tabs/ActionTab';
|
|
||||||
|
|
||||||
export interface TabComponentProps<S, A extends Action<string>> {
|
export interface TabComponentProps<S, A extends Action<string>> {
|
||||||
labelRenderer: LabelRenderer;
|
labelRenderer: LabelRenderer;
|
||||||
|
@ -23,6 +23,7 @@ export interface TabComponentProps<S, A extends Action<string>> {
|
||||||
isWideLayout: boolean;
|
isWideLayout: boolean;
|
||||||
sortStateTreeAlphabetically: boolean;
|
sortStateTreeAlphabetically: boolean;
|
||||||
disableStateTreeCollection: boolean;
|
disableStateTreeCollection: boolean;
|
||||||
|
enableSearchPanel: boolean;
|
||||||
dataTypeKey: string | symbol | undefined;
|
dataTypeKey: string | symbol | undefined;
|
||||||
delta: Delta | null | undefined | false;
|
delta: Delta | null | undefined | false;
|
||||||
action: A;
|
action: A;
|
||||||
|
@ -74,6 +75,7 @@ interface Props<S, A extends Action<string>> {
|
||||||
onSelectTab: (tabName: string) => void;
|
onSelectTab: (tabName: string) => void;
|
||||||
sortStateTreeAlphabetically: boolean;
|
sortStateTreeAlphabetically: boolean;
|
||||||
disableStateTreeCollection: boolean;
|
disableStateTreeCollection: boolean;
|
||||||
|
enableSearchPanel: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ActionPreview<S, A extends Action<string>> extends Component<
|
class ActionPreview<S, A extends Action<string>> extends Component<
|
||||||
|
@ -107,6 +109,7 @@ class ActionPreview<S, A extends Action<string>> extends Component<
|
||||||
updateMonitorState,
|
updateMonitorState,
|
||||||
sortStateTreeAlphabetically,
|
sortStateTreeAlphabetically,
|
||||||
disableStateTreeCollection,
|
disableStateTreeCollection,
|
||||||
|
enableSearchPanel,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const renderedTabs: Tab<S, A>[] =
|
const renderedTabs: Tab<S, A>[] =
|
||||||
|
@ -141,6 +144,7 @@ class ActionPreview<S, A extends Action<string>> extends Component<
|
||||||
isWideLayout,
|
isWideLayout,
|
||||||
sortStateTreeAlphabetically,
|
sortStateTreeAlphabetically,
|
||||||
disableStateTreeCollection,
|
disableStateTreeCollection,
|
||||||
|
enableSearchPanel,
|
||||||
dataTypeKey,
|
dataTypeKey,
|
||||||
delta,
|
delta,
|
||||||
action,
|
action,
|
||||||
|
|
|
@ -1,25 +1,20 @@
|
||||||
import React, { PureComponent } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Base16Theme } from 'redux-devtools-themes';
|
|
||||||
import {
|
|
||||||
getBase16Theme,
|
|
||||||
invertTheme,
|
|
||||||
StylingFunction,
|
|
||||||
} from 'react-base16-styling';
|
|
||||||
import {
|
import {
|
||||||
ActionCreators,
|
ActionCreators,
|
||||||
LiftedAction,
|
LiftedAction,
|
||||||
LiftedState,
|
LiftedState,
|
||||||
} from '@redux-devtools/core';
|
} from '@redux-devtools/core';
|
||||||
import { Action, Dispatch } from 'redux';
|
|
||||||
import { Delta, DiffContext } from 'jsondiffpatch';
|
import { Delta, DiffContext } from 'jsondiffpatch';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
import {
|
import {
|
||||||
createStylingFromTheme,
|
StylingFunction,
|
||||||
base16Themes,
|
getBase16Theme,
|
||||||
} from './utils/createStylingFromTheme';
|
invertTheme,
|
||||||
|
} from 'react-base16-styling';
|
||||||
|
import { Action, Dispatch } from 'redux';
|
||||||
|
import { Base16Theme } from 'redux-devtools-themes';
|
||||||
import ActionList from './ActionList';
|
import ActionList from './ActionList';
|
||||||
import ActionPreview, { Tab } from './ActionPreview';
|
import ActionPreview, { Tab } from './ActionPreview';
|
||||||
import getInspectedState from './utils/getInspectedState';
|
|
||||||
import createDiffPatcher from './createDiffPatcher';
|
import createDiffPatcher from './createDiffPatcher';
|
||||||
import {
|
import {
|
||||||
DevtoolsInspectorAction,
|
DevtoolsInspectorAction,
|
||||||
|
@ -27,6 +22,11 @@ import {
|
||||||
reducer,
|
reducer,
|
||||||
updateMonitorState,
|
updateMonitorState,
|
||||||
} from './redux';
|
} from './redux';
|
||||||
|
import {
|
||||||
|
base16Themes,
|
||||||
|
createStylingFromTheme,
|
||||||
|
} from './utils/createStylingFromTheme';
|
||||||
|
import getInspectedState from './utils/getInspectedState';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
|
@ -153,6 +153,7 @@ export interface ExternalProps<S, A extends Action<string>> {
|
||||||
invertTheme: boolean;
|
invertTheme: boolean;
|
||||||
sortStateTreeAlphabetically: boolean;
|
sortStateTreeAlphabetically: boolean;
|
||||||
disableStateTreeCollection: boolean;
|
disableStateTreeCollection: boolean;
|
||||||
|
enableSearchPanel: boolean;
|
||||||
dataTypeKey?: string | symbol;
|
dataTypeKey?: string | symbol;
|
||||||
tabs: Tab<S, A>[] | ((tabs: Tab<S, A>[]) => Tab<S, A>[]);
|
tabs: Tab<S, A>[] | ((tabs: Tab<S, A>[]) => Tab<S, A>[]);
|
||||||
}
|
}
|
||||||
|
@ -181,6 +182,7 @@ export interface DevtoolsInspectorProps<S, A extends Action<string>>
|
||||||
hideActionButtons?: boolean;
|
hideActionButtons?: boolean;
|
||||||
sortStateTreeAlphabetically: boolean;
|
sortStateTreeAlphabetically: boolean;
|
||||||
disableStateTreeCollection: boolean;
|
disableStateTreeCollection: boolean;
|
||||||
|
enableSearchPanel: boolean;
|
||||||
invertTheme: boolean;
|
invertTheme: boolean;
|
||||||
dataTypeKey?: string | symbol;
|
dataTypeKey?: string | symbol;
|
||||||
tabs: Tab<S, A>[] | ((tabs: Tab<S, A>[]) => Tab<S, A>[]);
|
tabs: Tab<S, A>[] | ((tabs: Tab<S, A>[]) => Tab<S, A>[]);
|
||||||
|
@ -226,6 +228,7 @@ class DevtoolsInspector<S, A extends Action<string>> extends PureComponent<
|
||||||
invertTheme: PropTypes.bool,
|
invertTheme: PropTypes.bool,
|
||||||
sortStateTreeAlphabetically: PropTypes.bool,
|
sortStateTreeAlphabetically: PropTypes.bool,
|
||||||
disableStateTreeCollection: PropTypes.bool,
|
disableStateTreeCollection: PropTypes.bool,
|
||||||
|
enableSearchPanel: PropTypes.bool,
|
||||||
skippedActionIds: PropTypes.array,
|
skippedActionIds: PropTypes.array,
|
||||||
dataTypeKey: PropTypes.any,
|
dataTypeKey: PropTypes.any,
|
||||||
tabs: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
|
tabs: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
|
||||||
|
@ -313,6 +316,7 @@ class DevtoolsInspector<S, A extends Action<string>> extends PureComponent<
|
||||||
hideActionButtons,
|
hideActionButtons,
|
||||||
sortStateTreeAlphabetically,
|
sortStateTreeAlphabetically,
|
||||||
disableStateTreeCollection,
|
disableStateTreeCollection,
|
||||||
|
enableSearchPanel,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { selectedActionId, startActionId, searchValue, tabName } =
|
const { selectedActionId, startActionId, searchValue, tabName } =
|
||||||
monitorState;
|
monitorState;
|
||||||
|
@ -374,6 +378,7 @@ class DevtoolsInspector<S, A extends Action<string>> extends PureComponent<
|
||||||
dataTypeKey,
|
dataTypeKey,
|
||||||
sortStateTreeAlphabetically,
|
sortStateTreeAlphabetically,
|
||||||
disableStateTreeCollection,
|
disableStateTreeCollection,
|
||||||
|
enableSearchPanel,
|
||||||
}}
|
}}
|
||||||
monitorState={this.props.monitorState}
|
monitorState={this.props.monitorState}
|
||||||
updateMonitorState={this.updateMonitorState}
|
updateMonitorState={this.updateMonitorState}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { StylingFunction } from 'react-base16-styling';
|
||||||
|
import React, { ReactElement } from 'react';
|
||||||
|
|
||||||
|
export const BUTTON_DIRECTION = {
|
||||||
|
LEFT: 'left',
|
||||||
|
RIGHT: 'right',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
interface JumpSearchResultButtonProps {
|
||||||
|
buttonDirection: string;
|
||||||
|
buttonDisabled: boolean;
|
||||||
|
styling: StylingFunction;
|
||||||
|
jumpToNewResult: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function JumpSearchResultButton({
|
||||||
|
buttonDirection,
|
||||||
|
buttonDisabled,
|
||||||
|
styling,
|
||||||
|
jumpToNewResult,
|
||||||
|
}: JumpSearchResultButtonProps): ReactElement {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...styling('jumpResultButton')}
|
||||||
|
onClick={() => jumpToNewResult()}
|
||||||
|
disabled={buttonDisabled}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
{...styling('jumpResultButtonArrow')}
|
||||||
|
viewBox="4 0 14 18"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
{buttonDirection === BUTTON_DIRECTION.LEFT ? (
|
||||||
|
<path d="M15.41 16.09l-4.58-4.59 4.58-4.59-1.41-1.41-6 6 6 6z" />
|
||||||
|
) : (
|
||||||
|
<path d="M8.59 16.34l4.58-4.59-4.58-4.59 1.41-1.41 6 6-6 6z" />
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JumpSearchResultButton;
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React, { ReactElement } from 'react';
|
||||||
|
import { StylingFunction } from 'react-base16-styling';
|
||||||
|
|
||||||
|
export interface SearchBarProps {
|
||||||
|
onChange: (s: string) => void;
|
||||||
|
text: string;
|
||||||
|
className?: string;
|
||||||
|
styling: StylingFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchBar({
|
||||||
|
onChange,
|
||||||
|
text,
|
||||||
|
className,
|
||||||
|
styling,
|
||||||
|
}: SearchBarProps): ReactElement {
|
||||||
|
return (
|
||||||
|
<div className={`search-bar ${className || ''}`}>
|
||||||
|
<input
|
||||||
|
{...styling('searchInput')}
|
||||||
|
placeholder={'Search'}
|
||||||
|
value={text}
|
||||||
|
type={'text'}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchBar;
|
|
@ -0,0 +1,163 @@
|
||||||
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
|
import { StylingFunction } from 'react-base16-styling';
|
||||||
|
import { searchInObject } from '../utils/objectSearch';
|
||||||
|
import { Value } from '../utils/searchWorker';
|
||||||
|
import JumpSearchResultButton, {
|
||||||
|
BUTTON_DIRECTION,
|
||||||
|
} from './JumpSearchResultButton';
|
||||||
|
import SearchBar from './SearchBar';
|
||||||
|
|
||||||
|
export interface SearchQuery {
|
||||||
|
queryText: string;
|
||||||
|
location: {
|
||||||
|
keys: boolean;
|
||||||
|
values: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL_QUERY: SearchQuery = {
|
||||||
|
queryText: '',
|
||||||
|
location: {
|
||||||
|
keys: true,
|
||||||
|
values: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SearchPanelProps {
|
||||||
|
state: Value;
|
||||||
|
onSubmit: (result: {
|
||||||
|
searchResult: string[];
|
||||||
|
searchInProgress: boolean;
|
||||||
|
}) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
styling: StylingFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchStatus = 'done' | 'pending' | 'unset';
|
||||||
|
|
||||||
|
function SearchPanel({
|
||||||
|
onSubmit,
|
||||||
|
onReset,
|
||||||
|
styling,
|
||||||
|
state,
|
||||||
|
}: SearchPanelProps): ReactElement {
|
||||||
|
const [query, setQuery] = useState<SearchQuery>(INITIAL_QUERY);
|
||||||
|
const [searchStatus, setSearchStatus] = useState<SearchStatus>('unset');
|
||||||
|
const [results, setResults] = useState<string[][] | undefined>(undefined);
|
||||||
|
const [resultIndex, setResultIndex] = useState(0);
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
setSearchStatus('pending');
|
||||||
|
const result = await searchInObject(state, query);
|
||||||
|
setResults(result.map((r) => r.split('.')));
|
||||||
|
setResultIndex(0);
|
||||||
|
setSearchStatus('done');
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
setQuery(INITIAL_QUERY);
|
||||||
|
setSearchStatus('unset');
|
||||||
|
setResults(undefined);
|
||||||
|
setResultIndex(0);
|
||||||
|
onReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
results &&
|
||||||
|
onSubmit({ searchResult: results[0] || [], searchInProgress: true });
|
||||||
|
}, [results, onSubmit]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'search-panel'} {...styling('searchPanel')}>
|
||||||
|
<SearchBar
|
||||||
|
text={query.queryText}
|
||||||
|
onChange={(text: string) => setQuery({ ...query, queryText: text })}
|
||||||
|
styling={styling}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
{...styling('searchPanelParameterSelection')}
|
||||||
|
type={'checkbox'}
|
||||||
|
checked={query.location.keys}
|
||||||
|
onChange={(event) =>
|
||||||
|
setQuery({
|
||||||
|
...query,
|
||||||
|
location: { ...query.location, keys: event.target.checked },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>Keys</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
{...styling('searchPanelParameterSelection')}
|
||||||
|
type={'checkbox'}
|
||||||
|
checked={query.location.values}
|
||||||
|
onChange={(event) =>
|
||||||
|
setQuery({
|
||||||
|
...query,
|
||||||
|
location: { ...query.location, values: event.target.checked },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>Values</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
{...styling('searchButton')}
|
||||||
|
onClick={() => handleSubmit()}
|
||||||
|
disabled={
|
||||||
|
(!query.location.keys && !query.location.values) || !query.queryText
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Go
|
||||||
|
</button>
|
||||||
|
{searchStatus === 'pending' && 'Searching...'}
|
||||||
|
{searchStatus === 'done' && (
|
||||||
|
<>
|
||||||
|
<div {...styling('jumpResultContainer')}>
|
||||||
|
<JumpSearchResultButton
|
||||||
|
buttonDirection={BUTTON_DIRECTION.LEFT}
|
||||||
|
buttonDisabled={!results || results.length < 2}
|
||||||
|
styling={styling}
|
||||||
|
jumpToNewResult={() => {
|
||||||
|
if (!results) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newIndex =
|
||||||
|
resultIndex - 1 < 0 ? results.length - 1 : resultIndex - 1;
|
||||||
|
setResultIndex(newIndex);
|
||||||
|
onSubmit({
|
||||||
|
searchResult: results[newIndex] || [],
|
||||||
|
searchInProgress: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<JumpSearchResultButton
|
||||||
|
buttonDirection={BUTTON_DIRECTION.RIGHT}
|
||||||
|
buttonDisabled={!results || results.length < 2}
|
||||||
|
styling={styling}
|
||||||
|
jumpToNewResult={() => {
|
||||||
|
if (!results) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newIndex = (resultIndex + 1) % results.length || 0;
|
||||||
|
setResultIndex(newIndex);
|
||||||
|
onSubmit({
|
||||||
|
searchResult: results[newIndex] || [],
|
||||||
|
searchInProgress: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{results &&
|
||||||
|
`${results.length ? resultIndex + 1 : 0}/${results.length}`}
|
||||||
|
</div>
|
||||||
|
<button {...styling('searchButton')} onClick={() => reset()}>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchPanel;
|
|
@ -1,10 +1,16 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
import { JSONTree } from 'react-json-tree';
|
import { JSONTree } from 'react-json-tree';
|
||||||
import { Action } from 'redux';
|
import { Action } from 'redux';
|
||||||
|
import { TabComponentProps } from '../ActionPreview';
|
||||||
|
import SearchPanel from '../searchPanel/SearchPanel';
|
||||||
import getItemString from './getItemString';
|
import getItemString from './getItemString';
|
||||||
import getJsonTreeTheme from './getJsonTreeTheme';
|
import getJsonTreeTheme from './getJsonTreeTheme';
|
||||||
import { TabComponentProps } from '../ActionPreview';
|
|
||||||
|
interface SearchState {
|
||||||
|
searchResult: string[];
|
||||||
|
searchInProgress: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const StateTab: React.FunctionComponent<
|
const StateTab: React.FunctionComponent<
|
||||||
TabComponentProps<any, Action<string>>
|
TabComponentProps<any, Action<string>>
|
||||||
|
@ -18,20 +24,86 @@ const StateTab: React.FunctionComponent<
|
||||||
isWideLayout,
|
isWideLayout,
|
||||||
sortStateTreeAlphabetically,
|
sortStateTreeAlphabetically,
|
||||||
disableStateTreeCollection,
|
disableStateTreeCollection,
|
||||||
}) => (
|
enableSearchPanel,
|
||||||
<JSONTree
|
}) => {
|
||||||
labelRenderer={labelRenderer}
|
const [searchState, setSearchState] = useState<SearchState>({
|
||||||
theme={getJsonTreeTheme(base16Theme)}
|
searchResult: [],
|
||||||
data={nextState}
|
searchInProgress: false,
|
||||||
getItemString={(type, data) =>
|
});
|
||||||
getItemString(styling, type, data, dataTypeKey, isWideLayout)
|
|
||||||
|
const displayedResult = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
enableSearchPanel &&
|
||||||
|
searchState.searchInProgress &&
|
||||||
|
displayedResult.current
|
||||||
|
) {
|
||||||
|
displayedResult.current.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center',
|
||||||
|
inline: 'center',
|
||||||
|
});
|
||||||
|
setSearchState({ ...searchState, searchInProgress: false });
|
||||||
}
|
}
|
||||||
invertTheme={invertTheme}
|
}, [searchState, setSearchState, enableSearchPanel]);
|
||||||
hideRoot
|
|
||||||
sortObjectKeys={sortStateTreeAlphabetically}
|
return (
|
||||||
{...(disableStateTreeCollection ? { collectionLimit: 0 } : {})}
|
<>
|
||||||
/>
|
{enableSearchPanel && (
|
||||||
);
|
<SearchPanel
|
||||||
|
onSubmit={setSearchState}
|
||||||
|
onReset={() =>
|
||||||
|
setSearchState({
|
||||||
|
searchResult: [],
|
||||||
|
searchInProgress: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
styling={styling}
|
||||||
|
state={nextState}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<JSONTree
|
||||||
|
labelRenderer={(keyPath, nodeType, expanded, expandable) => {
|
||||||
|
return isMatch(searchState.searchResult, [...keyPath].reverse()) ? (
|
||||||
|
<span {...styling('queryResultLabel')} ref={displayedResult}>
|
||||||
|
{labelRenderer(keyPath, nodeType, expanded, expandable)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
labelRenderer(keyPath, nodeType, expanded, expandable)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
theme={getJsonTreeTheme(base16Theme)}
|
||||||
|
data={nextState}
|
||||||
|
getItemString={(type, data) =>
|
||||||
|
getItemString(styling, type, data, dataTypeKey, isWideLayout)
|
||||||
|
}
|
||||||
|
invertTheme={invertTheme}
|
||||||
|
hideRoot
|
||||||
|
sortObjectKeys={sortStateTreeAlphabetically}
|
||||||
|
{...(disableStateTreeCollection ? { collectionLimit: 0 } : {})}
|
||||||
|
isSearchInProgress={searchState.searchInProgress}
|
||||||
|
searchResultPath={searchState.searchResult}
|
||||||
|
valueRenderer={(raw, value, ...keyPath) => {
|
||||||
|
return isMatch(searchState.searchResult, [...keyPath].reverse()) ? (
|
||||||
|
<span {...styling('queryResult')} ref={displayedResult}>
|
||||||
|
{raw as string}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>{raw as string}</span>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMatch = (resultPath: string[], nodePath: (string | number)[]) => {
|
||||||
|
return (
|
||||||
|
resultPath.length === nodePath.length &&
|
||||||
|
resultPath.every((result, index) => result === nodePath[index].toString())
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
StateTab.propTypes = {
|
StateTab.propTypes = {
|
||||||
nextState: PropTypes.any.isRequired,
|
nextState: PropTypes.any.isRequired,
|
||||||
|
|
|
@ -17,6 +17,7 @@ const colorMap = (theme: Base16Theme) => ({
|
||||||
SELECTED_BACKGROUND_COLOR: rgba(theme.base03, 20),
|
SELECTED_BACKGROUND_COLOR: rgba(theme.base03, 20),
|
||||||
SKIPPED_BACKGROUND_COLOR: rgba(theme.base03, 10),
|
SKIPPED_BACKGROUND_COLOR: rgba(theme.base03, 10),
|
||||||
HEADER_BACKGROUND_COLOR: rgba(theme.base03, 30),
|
HEADER_BACKGROUND_COLOR: rgba(theme.base03, 30),
|
||||||
|
HEADER_BACKGROUND_COLOR_OPAQUE: rgba(theme.base03, 100),
|
||||||
HEADER_BORDER_COLOR: rgba(theme.base03, 20),
|
HEADER_BORDER_COLOR: rgba(theme.base03, 20),
|
||||||
BORDER_COLOR: rgba(theme.base03, 50),
|
BORDER_COLOR: rgba(theme.base03, 50),
|
||||||
LIST_BORDER_COLOR: rgba(theme.base03, 50),
|
LIST_BORDER_COLOR: rgba(theme.base03, 50),
|
||||||
|
@ -34,6 +35,7 @@ const colorMap = (theme: Base16Theme) => ({
|
||||||
LINK_COLOR: rgba(theme.base0E, 90),
|
LINK_COLOR: rgba(theme.base0E, 90),
|
||||||
LINK_HOVER_COLOR: theme.base0E,
|
LINK_HOVER_COLOR: theme.base0E,
|
||||||
ERROR_COLOR: theme.base08,
|
ERROR_COLOR: theme.base08,
|
||||||
|
SEARCH_BUTTON_COLOR: rgba(theme.base00, 50),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Color = keyof ReturnType<typeof colorMap>;
|
type Color = keyof ReturnType<typeof colorMap>;
|
||||||
|
@ -267,6 +269,95 @@ const getSheetFromColorMap = (map: ColorMap) => ({
|
||||||
'border-bottom-color': map.HEADER_BORDER_COLOR,
|
'border-bottom-color': map.HEADER_BORDER_COLOR,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
searchPanel: {
|
||||||
|
display: 'flex',
|
||||||
|
'flex-wrap': 'wrap',
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
width: '100%',
|
||||||
|
'z-index': 1,
|
||||||
|
gap: '1em',
|
||||||
|
padding: '5px 10px',
|
||||||
|
'align-items': 'center',
|
||||||
|
'border-bottom-width': '1px',
|
||||||
|
'border-bottom-style': 'solid',
|
||||||
|
|
||||||
|
'background-color': map.HEADER_BACKGROUND_COLOR_OPAQUE,
|
||||||
|
'border-bottom-color': map.HEADER_BORDER_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
searchPanelParameterSelection: {
|
||||||
|
display: 'inline-grid',
|
||||||
|
color: map.SEARCH_BUTTON_COLOR,
|
||||||
|
width: '1.15em',
|
||||||
|
height: '1.15em',
|
||||||
|
border: '0.15em solid currentColor',
|
||||||
|
'border-radius': '0.15em',
|
||||||
|
transform: 'translateY(-0.075em)',
|
||||||
|
'place-content': 'center',
|
||||||
|
appearance: 'none',
|
||||||
|
'background-color': map.SEARCH_BUTTON_COLOR,
|
||||||
|
|
||||||
|
'&::before': {
|
||||||
|
'background-color': map.TEXT_COLOR,
|
||||||
|
'transform-origin': 'bottom left',
|
||||||
|
'clip-path':
|
||||||
|
'polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%)',
|
||||||
|
content: '""',
|
||||||
|
width: '0.65em',
|
||||||
|
height: '0.65em',
|
||||||
|
transform: 'scale(0)',
|
||||||
|
transition: '120ms transform ease-in-out',
|
||||||
|
},
|
||||||
|
'&:checked::before': {
|
||||||
|
transform: 'scale(1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
searchInput: {
|
||||||
|
'background-color': 'white',
|
||||||
|
opacity: 0.9,
|
||||||
|
'border-color': 'transparent',
|
||||||
|
'border-radius': '4px',
|
||||||
|
height: '1.2rem',
|
||||||
|
},
|
||||||
|
|
||||||
|
searchButton: {
|
||||||
|
'background-color': map.SEARCH_BUTTON_COLOR,
|
||||||
|
'border-color': 'transparent',
|
||||||
|
'border-radius': '4px',
|
||||||
|
height: '1.56rem',
|
||||||
|
color: map.TEXT_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
jumpResultContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
gap: '1em',
|
||||||
|
'align-items': 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
jumpResultButton: {
|
||||||
|
'background-color': map.SEARCH_BUTTON_COLOR,
|
||||||
|
'border-color': 'transparent',
|
||||||
|
'border-radius': '4px',
|
||||||
|
},
|
||||||
|
|
||||||
|
jumpResultButtonArrow: {
|
||||||
|
cursor: 'hand',
|
||||||
|
fill: map.TEXT_COLOR,
|
||||||
|
width: '0.6rem',
|
||||||
|
height: '1rem',
|
||||||
|
},
|
||||||
|
|
||||||
|
queryResult: {
|
||||||
|
'background-color': '#FFFF00',
|
||||||
|
},
|
||||||
|
|
||||||
|
queryResultLabel: {
|
||||||
|
'background-color': '#FFFF00',
|
||||||
|
'text-indent': 0,
|
||||||
|
},
|
||||||
|
|
||||||
tabSelector: {
|
tabSelector: {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { SearchQuery } from '../searchPanel/SearchPanel';
|
||||||
|
import { Value } from './searchWorker';
|
||||||
|
|
||||||
|
export function searchInObject(
|
||||||
|
objectToSearch: Value,
|
||||||
|
query: SearchQuery,
|
||||||
|
): Promise<string[]> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const worker = new Worker(new URL('./searchWorker.js', import.meta.url));
|
||||||
|
|
||||||
|
worker.onmessage = (event: MessageEvent<string[]>) => {
|
||||||
|
resolve(event.data);
|
||||||
|
worker.terminate();
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage({ objectToSearch, query });
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { SearchQuery } from '../searchPanel/SearchPanel';
|
||||||
|
|
||||||
|
type PrimitiveValue = string | number | boolean | undefined | null;
|
||||||
|
export type Value =
|
||||||
|
| PrimitiveValue
|
||||||
|
| Value[]
|
||||||
|
| { [key: string | number]: Value };
|
||||||
|
|
||||||
|
function getKeys(object: Value): string[] {
|
||||||
|
if (object === undefined || object === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['string', 'number', 'boolean'].includes(typeof object)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(object)) {
|
||||||
|
return [...object.keys()].map((el) => el.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKeyPaths(object: Value): string[] {
|
||||||
|
const keys = getKeys(object) as (keyof object)[];
|
||||||
|
const appendKey = (path: string, branch: string | number): string =>
|
||||||
|
path ? `${path}.${branch}` : `${branch}`;
|
||||||
|
|
||||||
|
return keys.flatMap((key) => [
|
||||||
|
key,
|
||||||
|
...getKeyPaths(object?.[key]).map((subKey) => appendKey(key, subKey)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatchable = (value: Value): boolean => {
|
||||||
|
return (
|
||||||
|
value === undefined ||
|
||||||
|
value === null ||
|
||||||
|
['string', 'number', 'boolean'].includes(typeof value)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function doSearch(objectToSearch: Value, query: SearchQuery): string[] {
|
||||||
|
const {
|
||||||
|
queryText,
|
||||||
|
location: { keys: matchKeys, values: matchValues },
|
||||||
|
} = query;
|
||||||
|
|
||||||
|
if (!matchKeys && !matchValues) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyPaths = getKeyPaths(objectToSearch);
|
||||||
|
const getValueFromKeyPath = (obj: Value, path: string): Value => {
|
||||||
|
return path
|
||||||
|
.split('.')
|
||||||
|
.reduce(
|
||||||
|
(intermediateObj, key) => intermediateObj?.[key as keyof object],
|
||||||
|
obj,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const match = (value: Value, searchText: string): boolean => {
|
||||||
|
return isMatchable(value) && String(value).includes(searchText);
|
||||||
|
};
|
||||||
|
|
||||||
|
return keyPaths.filter((keyPath) => {
|
||||||
|
const valuesToCheck = [];
|
||||||
|
matchKeys && valuesToCheck.push(keyPath.split('.').splice(-1)[0]);
|
||||||
|
matchValues &&
|
||||||
|
valuesToCheck.push(getValueFromKeyPath(objectToSearch, keyPath));
|
||||||
|
|
||||||
|
return valuesToCheck.some((value) => match(value, queryText));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = (
|
||||||
|
event: MessageEvent<{ objectToSearch: Value; query: SearchQuery }>,
|
||||||
|
) => {
|
||||||
|
const { objectToSearch, query } = event.data;
|
||||||
|
const result = doSearch(objectToSearch, query);
|
||||||
|
self.postMessage(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {};
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"extends": "../../tsconfig.react.base.json",
|
"extends": "../../tsconfig.react.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"module": "ES2022",
|
||||||
"outDir": "lib/types",
|
"outDir": "lib/types",
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue
Block a user