Convert redux-devtools-inspector to TypeScript

This commit is contained in:
Nathan Bierema 2020-05-21 23:35:49 -05:00
parent 93d6bc4350
commit 0d7eab049b
42 changed files with 807 additions and 255 deletions

View File

@ -1,9 +1,10 @@
{ {
"presets": ["@babel/preset-env", "@babel/preset-react"], "presets": [
"@babel/env",
"@babel/react",
"@babel/typescript"
],
"plugins": [ "plugins": [
"@babel/plugin-transform-runtime", "@babel/proposal-class-properties"
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-export-default-from",
"@babel/plugin-proposal-do-expressions"
] ]
} }

View File

@ -0,0 +1,2 @@
lib
demo

View File

@ -0,0 +1,13 @@
module.exports = {
extends: '../../.eslintrc',
overrides: [
{
files: ['*.ts', '*.tsx'],
extends: '../../eslintrc.ts.react.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json']
}
}
]
};

View File

@ -1,8 +0,0 @@
static
src
demo
.*
webpack.config.js
index.html
*.gif
*.png

View File

@ -0,0 +1,2 @@
lib
demo

View File

@ -3,19 +3,25 @@
"version": "0.11.0", "version": "0.11.0",
"description": "Redux DevTools Diff Monitor", "description": "Redux DevTools Diff Monitor",
"scripts": { "scripts": {
"build": "npm run build:lib", "type-check": "tsc --noEmit",
"build:lib": "cross-env NODE_ENV=production babel src --out-dir lib", "type-check:watch": "npm run type-check -- --watch",
"build": "npm run build:types && npm run build:js",
"build:types": "tsc --emitDeclarationOnly",
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"build:demo": "cross-env NODE_ENV=production webpack -p", "build:demo": "cross-env NODE_ENV=production webpack -p",
"stats": "webpack --profile --json > stats.json", "stats": "webpack --profile --json > stats.json",
"start": "webpack-dev-server", "start": "webpack-dev-server",
"preversion": "npm run lint", "preversion": "npm run lint",
"version": "npm run build:demo && git add -A .", "version": "npm run build:demo && git add -A .",
"postversion": "git push", "postversion": "git push",
"prepare": "npm run build:lib", "prepare": "npm run build",
"prepublishOnly": "npm run build:lib", "prepublishOnly": "npm run build",
"gh": "git subtree push --prefix demo/dist origin gh-pages" "gh": "git subtree push --prefix demo/dist origin gh-pages"
}, },
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts",
"repository": { "repository": {
"url": "https://github.com/reduxjs/redux-devtools" "url": "https://github.com/reduxjs/redux-devtools"
}, },
@ -28,11 +34,17 @@
"@babel/plugin-transform-runtime": "^7.2.0", "@babel/plugin-transform-runtime": "^7.2.0",
"@babel/preset-env": "^7.3.1", "@babel/preset-env": "^7.3.1",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@types/dateformat": "^3.0.1",
"@types/dragula": "^3.7.0",
"@types/hex-rgba": "^1.0.0",
"@typescript-eslint/eslint-plugin": "^2.31.0",
"@typescript-eslint/parser": "^2.31.0",
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"base16": "^1.0.0", "base16": "^1.0.0",
"chokidar": "^1.6.1", "chokidar": "^1.6.1",
"clean-webpack-plugin": "^1.0.0", "clean-webpack-plugin": "^1.0.0",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"csstype": "^2.6.10",
"export-files-webpack-plugin": "0.0.1", "export-files-webpack-plugin": "0.0.1",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"lodash.shuffle": "^4.2.0", "lodash.shuffle": "^4.2.0",
@ -50,13 +62,14 @@
"redux-devtools": "^3.1.0", "redux-devtools": "^3.1.0",
"redux-devtools-dock-monitor": "^1.0.1", "redux-devtools-dock-monitor": "^1.0.1",
"redux-logger": "^2.5.2", "redux-logger": "^2.5.2",
"typescript": "^3.8.3",
"webpack": "^4.27.1", "webpack": "^4.27.1",
"webpack-cli": "^3.2.0", "webpack-cli": "^3.2.0",
"webpack-dev-server": "^3.1.14" "webpack-dev-server": "^3.1.14"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=15.0.0", "@types/react": "^16.3.0",
"react-dom": ">=15.0.0" "react": "^16.3.0"
}, },
"author": "Alexander <alexkuz@gmail.com> (http://kuzya.org/)", "author": "Alexander <alexkuz@gmail.com> (http://kuzya.org/)",
"contributors": [ "contributors": [
@ -64,6 +77,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/prop-types": "^15.6.2",
"babel-runtime": "^6.3.19", "babel-runtime": "^6.3.19",
"dateformat": "^1.0.12", "dateformat": "^1.0.12",
"hex-rgba": "^1.0.0", "hex-rgba": "^1.0.0",

View File

@ -1,3 +0,0 @@
{
"import": true
}

View File

@ -1,10 +1,18 @@
import React, { Component } from 'react'; import React, { Component, RefCallback } from 'react';
import dragula from 'react-dragula'; import dragula from 'react-dragula';
import ActionListRow from './ActionListRow'; import ActionListRow from './ActionListRow';
import ActionListHeader from './ActionListHeader'; import ActionListHeader from './ActionListHeader';
import shouldPureComponentUpdate from 'react-pure-render/function'; import shouldPureComponentUpdate from 'react-pure-render/function';
import { Action } from 'redux';
import { PerformAction } from 'redux-devtools';
import { StylingFunction } from 'react-base16-styling';
import { Drake } from 'dragula';
function getTimestamps(actions, actionIds, actionId) { function getTimestamps<A extends Action<unknown>>(
actions: { [actionId: number]: PerformAction<A> },
actionIds: number[],
actionId: number
) {
const idx = actionIds.indexOf(actionId); const idx = actionIds.indexOf(actionId);
const prevActionId = actionIds[idx - 1]; const prevActionId = actionIds[idx - 1];
@ -14,10 +22,40 @@ function getTimestamps(actions, actionIds, actionId) {
}; };
} }
export default class ActionList extends Component { interface Props<S, A extends Action<unknown>> {
actions: { [actionId: number]: PerformAction<A> };
actionIds: number[];
isWideLayout: boolean;
searchValue: string | undefined;
selectedActionId: number | null;
startActionId: number | null;
skippedActionIds: number[];
draggableActions: boolean;
hideMainButtons: boolean | undefined;
hideActionButtons: boolean | undefined;
styling: StylingFunction;
onSelect: (event: React.MouseEvent<HTMLDivElement>, actionId: number) => void;
onSearch: (value: string) => void;
onToggleAction: (actionId: number) => void;
onJumpToState: (actionId: number) => void;
onCommit: () => void;
onSweep: () => void;
onReorderAction: (actionId: number, beforeActionId: number) => void;
currentActionId: number;
lastActionId: number;
}
export default class ActionList<S, A extends Action<unknown>> extends Component<
Props<S, A>
> {
shouldComponentUpdate = shouldPureComponentUpdate; shouldComponentUpdate = shouldPureComponentUpdate;
componentWillReceiveProps(nextProps) { node?: HTMLDivElement | null;
scrollDown?: boolean;
drake?: Drake;
componentWillReceiveProps(nextProps: Props<S, A>) {
const node = this.node; const node = this.node;
if (!node) { if (!node) {
this.scrollDown = true; this.scrollDown = true;
@ -35,22 +73,22 @@ export default class ActionList extends Component {
this.scrollToBottom(); this.scrollToBottom();
if (!this.props.draggableActions) return; if (!this.props.draggableActions) return;
const container = this.node; const container = this.node!;
this.drake = dragula([container], { this.drake = dragula([container], {
copy: false, copy: false,
copySortSource: false, copySortSource: false,
mirrorContainer: container, mirrorContainer: container,
accepts: (el, target, source, sibling) => accepts: (el, target, source, sibling) =>
!sibling || parseInt(sibling.getAttribute('data-id')), !sibling || !!parseInt(sibling.getAttribute('data-id')!),
moves: (el, source, handle) => moves: (el, source, handle) =>
parseInt(el.getAttribute('data-id')) && !!parseInt(el!.getAttribute('data-id')!) &&
handle.className.indexOf('selectorButton') !== 0 !handle!.className.startsWith('selectorButton')
}).on('drop', (el, target, source, sibling) => { }).on('drop', (el, target, source, sibling) => {
let beforeActionId = this.props.actionIds.length; let beforeActionId = this.props.actionIds.length;
if (sibling && sibling.className.indexOf('gu-mirror') === -1) { if (sibling && !sibling.className.includes('gu-mirror')) {
beforeActionId = parseInt(sibling.getAttribute('data-id')); beforeActionId = parseInt(sibling.getAttribute('data-id')!);
} }
const actionId = parseInt(el.getAttribute('data-id')); const actionId = parseInt(el.getAttribute('data-id')!);
this.props.onReorderAction(actionId, beforeActionId); this.props.onReorderAction(actionId, beforeActionId);
}); });
} }
@ -69,7 +107,7 @@ export default class ActionList extends Component {
} }
} }
getRef = node => { getRef: RefCallback<HTMLDivElement> = node => {
this.node = node; this.node = node;
}; };
@ -95,10 +133,10 @@ export default class ActionList extends Component {
} = this.props; } = this.props;
const lowerSearchValue = searchValue && searchValue.toLowerCase(); const lowerSearchValue = searchValue && searchValue.toLowerCase();
const filteredActionIds = searchValue const filteredActionIds = searchValue
? actionIds.filter( ? actionIds.filter(id =>
id => (actions[id].action.type as string)
actions[id].action.type.toLowerCase().indexOf(lowerSearchValue) !== .toLowerCase()
-1 .includes(lowerSearchValue as string)
) )
: actionIds; : actionIds;
@ -129,20 +167,22 @@ export default class ActionList extends Component {
isSelected={ isSelected={
(startActionId !== null && (startActionId !== null &&
actionId >= startActionId && actionId >= startActionId &&
actionId <= selectedActionId) || actionId <= (selectedActionId as number)) ||
actionId === selectedActionId actionId === selectedActionId
} }
isInFuture={ isInFuture={
actionIds.indexOf(actionId) > actionIds.indexOf(currentActionId) actionIds.indexOf(actionId) > actionIds.indexOf(currentActionId)
} }
onSelect={e => onSelect(e, actionId)} onSelect={(e: React.MouseEvent<HTMLDivElement>) =>
onSelect(e, actionId)
}
timestamps={getTimestamps(actions, actionIds, actionId)} timestamps={getTimestamps(actions, actionIds, actionId)}
action={actions[actionId].action} action={actions[actionId].action}
onToggleClick={() => onToggleAction(actionId)} onToggleClick={() => onToggleAction(actionId)}
onJumpClick={() => onJumpToState(actionId)} onJumpClick={() => onJumpToState(actionId)}
onCommitClick={() => onCommit(actionId)} onCommitClick={() => onCommit()}
hideActionButtons={hideActionButtons} hideActionButtons={hideActionButtons}
isSkipped={skippedActionIds.indexOf(actionId) !== -1} isSkipped={skippedActionIds.includes(actionId)}
/> />
))} ))}
</div> </div>

View File

@ -1,10 +1,25 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import RightSlider from './RightSlider'; import RightSlider from './RightSlider';
import { StylingFunction } from 'react-base16-styling';
const getActiveButtons = hasSkippedActions => const getActiveButtons = (hasSkippedActions: boolean): ('Sweep' | 'Commit')[] =>
[hasSkippedActions && 'Sweep', 'Commit'].filter(a => a); [hasSkippedActions && 'Sweep', 'Commit'].filter(a => a) as (
| 'Sweep'
| 'Commit'
)[];
const ActionListHeader = ({ interface Props {
styling: StylingFunction;
onSearch: (value: string) => void;
hasSkippedActions: boolean;
hasStagedActions: boolean;
onCommit: () => void;
onSweep: () => void;
hideMainButtons: boolean | undefined;
}
const ActionListHeader: React.FunctionComponent<Props> = ({
styling, styling,
onSearch, onSearch,
hasSkippedActions, hasSkippedActions,
@ -48,4 +63,14 @@ const ActionListHeader = ({
</div> </div>
); );
ActionListHeader.propTypes = {
styling: PropTypes.func.isRequired,
onSearch: PropTypes.func.isRequired,
hasSkippedActions: PropTypes.bool.isRequired,
hasStagedActions: PropTypes.bool.isRequired,
onCommit: PropTypes.func.isRequired,
onSweep: PropTypes.func.isRequired,
hideMainButtons: PropTypes.bool
};
export default ActionListHeader; export default ActionListHeader;

View File

@ -1,14 +1,41 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { PropTypes } from 'prop-types'; import PropTypes from 'prop-types';
import shouldPureComponentUpdate from 'react-pure-render/function'; import shouldPureComponentUpdate from 'react-pure-render/function';
import dateformat from 'dateformat'; import dateformat from 'dateformat';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import RightSlider from './RightSlider'; import RightSlider from './RightSlider';
import { StylingFunction } from 'react-base16-styling';
import { Action } from 'redux';
const BUTTON_SKIP = 'Skip'; const BUTTON_SKIP = 'Skip';
const BUTTON_JUMP = 'Jump'; const BUTTON_JUMP = 'Jump';
export default class ActionListRow extends Component { type Button = typeof BUTTON_SKIP | typeof BUTTON_JUMP;
interface Props<A extends Action<unknown>> {
styling: StylingFunction;
isSelected: boolean;
action: A;
actionId: number;
isInitAction: boolean;
onSelect: React.MouseEventHandler<HTMLDivElement> | undefined;
timestamps: { current: number; previous: number };
isSkipped: boolean;
isInFuture: boolean;
hideActionButtons: boolean | undefined;
onToggleClick: () => void;
onJumpClick: () => void;
onCommitClick: () => void;
}
interface State {
hover: boolean;
}
export default class ActionListRow<A extends Action<unknown>> extends Component<
Props<A>,
State
> {
state = { hover: false }; state = { hover: false };
static propTypes = { static propTypes = {
@ -44,18 +71,24 @@ export default class ActionListRow extends Component {
const timeDelta = timestamps.current - timestamps.previous; const timeDelta = timestamps.current - timestamps.previous;
const showButtons = (hover && !isInitAction) || isSkipped; const showButtons = (hover && !isInitAction) || isSkipped;
const isButtonSelected = btn => btn === BUTTON_SKIP && isSkipped; const isButtonSelected = (btn: Button) => btn === BUTTON_SKIP && isSkipped;
let actionType = action.type; let actionType = action.type;
if (typeof actionType === 'undefined') actionType = '<UNDEFINED>'; if (typeof actionType === 'undefined') actionType = '<UNDEFINED>';
else if (actionType === null) actionType = '<NULL>'; else if (actionType === null) actionType = '<NULL>';
else actionType = actionType.toString() || '<EMPTY>'; else actionType = (actionType as string).toString() || '<EMPTY>';
return ( return (
<div <div
onClick={onSelect} onClick={onSelect}
onMouseEnter={!hideActionButtons && this.handleMouseEnter} onMouseEnter={
onMouseLeave={!hideActionButtons && this.handleMouseLeave} (!hideActionButtons &&
this.handleMouseEnter) as React.MouseEventHandler<HTMLDivElement>
}
onMouseLeave={
(!hideActionButtons &&
this.handleMouseLeave) as React.MouseEventHandler<HTMLDivElement>
}
onMouseDown={this.handleMouseDown} onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseEnter} onMouseUp={this.handleMouseEnter}
data-id={actionId} data-id={actionId}
@ -76,7 +109,7 @@ export default class ActionListRow extends Component {
isSkipped && 'actionListItemNameSkipped' isSkipped && 'actionListItemNameSkipped'
])} ])}
> >
{actionType} {actionType as string}
</div> </div>
{hideActionButtons ? ( {hideActionButtons ? (
<RightSlider styling={styling} shown> <RightSlider styling={styling} shown>
@ -103,12 +136,12 @@ export default class ActionListRow extends Component {
</RightSlider> </RightSlider>
<RightSlider styling={styling} shown={showButtons} rotate> <RightSlider styling={styling} shown={showButtons} rotate>
<div {...styling('actionListItemSelector')}> <div {...styling('actionListItemSelector')}>
{[BUTTON_JUMP, BUTTON_SKIP].map( {([BUTTON_JUMP, BUTTON_SKIP] as const).map(
btn => btn =>
(!isInitAction || btn !== BUTTON_SKIP) && ( (!isInitAction || btn !== BUTTON_SKIP) && (
<div <div
key={btn} key={btn}
onClick={this.handleButtonClick.bind(this, btn)} onClick={e => this.handleButtonClick(btn, e)}
{...styling( {...styling(
[ [
'selectorButton', 'selectorButton',
@ -131,7 +164,7 @@ export default class ActionListRow extends Component {
); );
} }
handleButtonClick(btn, e) { handleButtonClick = (btn: Button, e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation(); e.stopPropagation();
switch (btn) { switch (btn) {
@ -142,10 +175,10 @@ export default class ActionListRow extends Component {
this.props.onJumpClick(); this.props.onJumpClick();
break; break;
} }
} };
handleMouseEnter = e => { handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
if (this.hover) return; if (this.state.hover) return;
this.handleMouseLeave.cancel(); this.handleMouseLeave.cancel();
this.handleMouseEnterDebounced(e.buttons); this.handleMouseEnterDebounced(e.buttons);
}; };
@ -160,8 +193,13 @@ export default class ActionListRow extends Component {
if (this.state.hover) this.setState({ hover: false }); if (this.state.hover) this.setState({ hover: false });
}, 100); }, 100);
handleMouseDown = e => { handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target.className.indexOf('selectorButton') === 0) return; if (
((e.target as unknown) as { className: string[] }).className.indexOf(
'selectorButton'
) === 0
)
return;
this.handleMouseLeave(); this.handleMouseLeave();
}; };
} }

View File

@ -1,9 +1,41 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { DEFAULT_STATE } from './redux'; import { DEFAULT_STATE, MonitorState } from './redux';
import ActionPreviewHeader from './ActionPreviewHeader'; import ActionPreviewHeader from './ActionPreviewHeader';
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'; import ActionTab from './tabs/ActionTab';
import { Base16Theme, StylingFunction } from 'react-base16-styling';
import { Delta } from 'jsondiffpatch';
import { Action } from 'redux';
import { PerformAction } from 'redux-devtools';
export interface TabComponentProps<S, A extends Action<unknown>> {
labelRenderer: (
keyPath: (string | number)[],
nodeType: string,
expanded: boolean,
expandable: boolean
) => React.ReactNode;
styling: StylingFunction;
computedStates: { state: S; error?: string }[];
actions: { [actionId: number]: PerformAction<A> };
selectedActionId: number | null;
startActionId: number | null;
base16Theme: Base16Theme;
invertTheme: boolean;
isWideLayout: boolean;
dataTypeKey: string | undefined;
delta: Delta | null | undefined | false;
action: A;
nextState: S;
monitorState: MonitorState;
updateMonitorState: (monitorState: Partial<MonitorState>) => void;
}
export interface Tab<S, A extends Action<unknown>> {
name: string;
component: React.ComponentType<TabComponentProps<S, A>>;
}
const DEFAULT_TABS = [ const DEFAULT_TABS = [
{ {
@ -20,7 +52,32 @@ const DEFAULT_TABS = [
} }
]; ];
class ActionPreview extends Component { interface Props<S, A extends Action<unknown>> {
styling: StylingFunction;
delta: Delta | null | undefined | false;
error: string | undefined;
nextState: S;
onInspectPath: (path: (string | number)[]) => void;
inspectedPath: (string | number)[];
tabName: string;
isWideLayout: boolean;
onSelectTab: (tabName: string) => void;
action: A;
actions: { [actionId: number]: PerformAction<A> };
selectedActionId: number | null;
startActionId: number | null;
computedStates: { state: S; error?: string }[];
base16Theme: Base16Theme;
invertTheme: boolean;
tabs: Tab<S, A>[] | ((tabs: Tab<S, A>[]) => Tab<S, A>[]);
dataTypeKey: string | undefined;
monitorState: MonitorState;
updateMonitorState: (monitorState: Partial<MonitorState>) => void;
}
class ActionPreview<S, A extends Action<unknown>> extends Component<
Props<S, A>
> {
static defaultProps = { static defaultProps = {
tabName: DEFAULT_STATE.tabName tabName: DEFAULT_STATE.tabName
}; };
@ -49,21 +106,21 @@ class ActionPreview extends Component {
updateMonitorState updateMonitorState
} = this.props; } = this.props;
const renderedTabs = const renderedTabs: Tab<S, A>[] =
typeof tabs === 'function' typeof tabs === 'function'
? tabs(DEFAULT_TABS) ? tabs(DEFAULT_TABS as Tab<S, A>[])
: tabs : tabs
? tabs ? tabs
: DEFAULT_TABS; : (DEFAULT_TABS as Tab<S, A>[]);
const { component: TabComponent } = const { component: TabComponent } =
renderedTabs.find(tab => tab.name === tabName) || renderedTabs.find(tab => tab.name === tabName)! ||
renderedTabs.find(tab => tab.name === DEFAULT_STATE.tabName); renderedTabs.find(tab => tab.name === DEFAULT_STATE.tabName)!;
return ( return (
<div key="actionPreview" {...styling('actionPreview')}> <div key="actionPreview" {...styling('actionPreview')}>
<ActionPreviewHeader <ActionPreviewHeader
tabs={renderedTabs} tabs={(renderedTabs as unknown) as Tab<unknown, Action<unknown>>[]}
{...{ styling, inspectedPath, onInspectPath, tabName, onSelectTab }} {...{ styling, inspectedPath, onInspectPath, tabName, onSelectTab }}
/> />
{!error && ( {!error && (
@ -94,7 +151,11 @@ class ActionPreview extends Component {
); );
} }
labelRenderer = ([key, ...rest], nodeType, expanded) => { labelRenderer = (
[key, ...rest]: (string | number)[],
nodeType: string,
expanded: boolean
) => {
const { styling, onInspectPath, inspectedPath } = this.props; const { styling, onInspectPath, inspectedPath } = this.props;
return ( return (

View File

@ -1,16 +1,32 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { StylingFunction } from 'react-base16-styling';
import { Action } from 'redux';
import { Tab } from './ActionPreview';
const ActionPreviewHeader = ({ interface Props<S, A extends Action<unknown>> {
styling: StylingFunction;
inspectedPath: (string | number)[];
onInspectPath: (path: (string | number)[]) => void;
tabName: string;
onSelectTab: (tabName: string) => void;
tabs: Tab<S, A>[];
}
const ActionPreviewHeader: React.FunctionComponent<Props<
unknown,
Action<unknown>
>> = <S, A extends Action<unknown>>({
styling, styling,
inspectedPath, inspectedPath,
onInspectPath, onInspectPath,
tabName, tabName,
onSelectTab, onSelectTab,
tabs tabs
}) => ( }: Props<S, A>) => (
<div key="previewHeader" {...styling('previewHeader')}> <div key="previewHeader" {...styling('previewHeader')}>
<div {...styling('tabSelector')}> <div {...styling('tabSelector')}>
{tabs.map(tab => ( {tabs.map((tab: Tab<S, A>) => (
<div <div
onClick={() => onSelectTab(tab.name)} onClick={() => onSelectTab(tab.name)}
key={tab.name} key={tab.name}
@ -39,7 +55,7 @@ const ActionPreviewHeader = ({
) : ( ) : (
tabName tabName
)} )}
{inspectedPath.map((key, idx) => {inspectedPath.map((key: string | number, idx: number) =>
idx === inspectedPath.length - 1 ? ( idx === inspectedPath.length - 1 ? (
<span key={key}>{key}</span> <span key={key}>{key}</span>
) : ( ) : (
@ -57,4 +73,13 @@ const ActionPreviewHeader = ({
</div> </div>
); );
ActionPreviewHeader.propTypes = {
styling: PropTypes.func.isRequired,
inspectedPath: PropTypes.array.isRequired,
onInspectPath: PropTypes.func.isRequired,
tabName: PropTypes.string.isRequired,
onSelectTab: PropTypes.func.isRequired,
tabs: PropTypes.array.isRequired
};
export default ActionPreviewHeader; export default ActionPreviewHeader;

View File

@ -1,17 +1,29 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { PropTypes } from 'prop-types'; import PropTypes from 'prop-types';
import { import {
createStylingFromTheme, createStylingFromTheme,
base16Themes base16Themes
} from './utils/createStylingFromTheme'; } from './utils/createStylingFromTheme';
import shouldPureComponentUpdate from 'react-pure-render/function'; import shouldPureComponentUpdate from 'react-pure-render/function';
import ActionList from './ActionList'; import ActionList from './ActionList';
import ActionPreview from './ActionPreview'; import ActionPreview, { Tab } from './ActionPreview';
import getInspectedState from './utils/getInspectedState'; import getInspectedState from './utils/getInspectedState';
import createDiffPatcher from './createDiffPatcher'; import createDiffPatcher from './createDiffPatcher';
import { getBase16Theme } from 'react-base16-styling'; import {
import { reducer, updateMonitorState } from './redux'; Base16Theme,
import { ActionCreators } from 'redux-devtools'; getBase16Theme,
StylingFunction,
Theme
} from 'react-base16-styling';
import {
MonitorAction,
MonitorState,
reducer,
updateMonitorState
} from './redux';
import { ActionCreators, LiftedAction, LiftedState } from 'redux-devtools';
import { Action, Dispatch } from 'redux';
import { Delta, DiffContext } from 'jsondiffpatch';
const { const {
commit, commit,
@ -22,21 +34,43 @@ const {
reorderAction reorderAction
} = ActionCreators; } = ActionCreators;
function getLastActionId(props) { export interface Props<S, A extends Action<unknown>>
extends LiftedState<S, A, MonitorState> {
dispatch: Dispatch<
MonitorAction | LiftedAction<S, A, MonitorState, MonitorAction>
>;
select: (state: S) => unknown;
supportImmutable: boolean;
draggableActions: boolean;
tabs: Tab<S, A>[] | ((tabs: Tab<S, A>[]) => Tab<S, A>[]);
theme: Theme;
invertTheme: boolean;
diffObjectHash?: (item: any, index: number) => string;
diffPropertyFilter?: (name: string, context: DiffContext) => boolean;
dataTypeKey?: string;
hideMainButtons?: boolean;
hideActionButtons?: boolean;
}
function getLastActionId<S, A extends Action<unknown>>(props: Props<S, A>) {
return props.stagedActionIds[props.stagedActionIds.length - 1]; return props.stagedActionIds[props.stagedActionIds.length - 1];
} }
function getCurrentActionId(props, monitorState) { function getCurrentActionId<S, A extends Action<unknown>>(
props: Props<S, A>,
monitorState: MonitorState
) {
return monitorState.selectedActionId === null return monitorState.selectedActionId === null
? props.stagedActionIds[props.currentStateIndex] ? props.stagedActionIds[props.currentStateIndex]
: monitorState.selectedActionId; : monitorState.selectedActionId;
} }
function getFromState( function getFromState<S>(
actionIndex, actionIndex: number,
stagedActionIds, stagedActionIds: number[],
computedStates, computedStates: { state: S; error?: string }[],
monitorState monitorState: MonitorState
) { ) {
const { startActionId } = monitorState; const { startActionId } = monitorState;
if (startActionId === null) { if (startActionId === null) {
@ -47,7 +81,10 @@ function getFromState(
return computedStates[fromStateIdx]; return computedStates[fromStateIdx];
} }
function createIntermediateState(props, monitorState) { function createIntermediateState<S, A extends Action<unknown>>(
props: Props<S, A>,
monitorState: MonitorState
) {
const { const {
supportImmutable, supportImmutable,
computedStates, computedStates,
@ -97,15 +134,27 @@ function createIntermediateState(props, monitorState) {
}; };
} }
function createThemeState(props) { function createThemeState<S, A extends Action<unknown>>(props: Props<S, A>) {
const base16Theme = getBase16Theme(props.theme, base16Themes); const base16Theme = getBase16Theme(props.theme, base16Themes)!;
const styling = createStylingFromTheme(props.theme, props.invertTheme); const styling = createStylingFromTheme(props.theme, props.invertTheme);
return { base16Theme, styling }; return { base16Theme, styling };
} }
export default class DevtoolsInspector extends Component { interface State<S, A extends Action<unknown>> {
constructor(props) { isWideLayout: boolean;
themeState: { base16Theme: Base16Theme; styling: StylingFunction };
delta: Delta | null | undefined | false;
nextState: S;
action: A;
error: string | undefined;
}
export default class DevtoolsInspector<
S,
A extends Action<unknown>
> extends Component<Props<S, A>, State<S, A>> {
constructor(props: Props<S, A>) {
super(props); super(props);
this.state = { this.state = {
...createIntermediateState(props, props.monitorState), ...createIntermediateState(props, props.monitorState),
@ -142,7 +191,7 @@ export default class DevtoolsInspector extends Component {
static update = reducer; static update = reducer;
static defaultProps = { static defaultProps = {
select: state => state, select: (state: unknown) => state,
supportImmutable: false, supportImmutable: false,
draggableActions: true, draggableActions: true,
theme: 'inspector', theme: 'inspector',
@ -151,29 +200,35 @@ export default class DevtoolsInspector extends Component {
shouldComponentUpdate = shouldPureComponentUpdate; shouldComponentUpdate = shouldPureComponentUpdate;
updateSizeTimeout?: number;
inspectorRef?: HTMLDivElement | null;
componentDidMount() { componentDidMount() {
this.updateSizeMode(); this.updateSizeMode();
this.updateSizeTimeout = setInterval(this.updateSizeMode.bind(this), 150); this.updateSizeTimeout = window.setInterval(
this.updateSizeMode.bind(this),
150
);
} }
componentWillUnmount() { componentWillUnmount() {
clearTimeout(this.updateSizeTimeout); clearTimeout(this.updateSizeTimeout);
} }
updateMonitorState = monitorState => { updateMonitorState = (monitorState: Partial<MonitorState>) => {
this.props.dispatch(updateMonitorState(monitorState)); this.props.dispatch(updateMonitorState(monitorState));
}; };
updateSizeMode() { updateSizeMode() {
const isWideLayout = this.inspectorRef.offsetWidth > 500; const isWideLayout = this.inspectorRef!.offsetWidth > 500;
if (isWideLayout !== this.state.isWideLayout) { if (isWideLayout !== this.state.isWideLayout) {
this.setState({ isWideLayout }); this.setState({ isWideLayout });
} }
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps: Props<S, A>) {
let nextMonitorState = nextProps.monitorState; const nextMonitorState = nextProps.monitorState;
const monitorState = this.props.monitorState; const monitorState = this.props.monitorState;
if ( if (
@ -197,7 +252,7 @@ export default class DevtoolsInspector extends Component {
} }
} }
inspectorCreateRef = node => { inspectorCreateRef: React.RefCallback<HTMLDivElement> = node => {
this.inspectorRef = node; this.inspectorRef = node;
}; };
@ -287,7 +342,9 @@ export default class DevtoolsInspector extends Component {
monitorState={this.props.monitorState} monitorState={this.props.monitorState}
updateMonitorState={this.updateMonitorState} updateMonitorState={this.updateMonitorState}
styling={styling} styling={styling}
onInspectPath={this.handleInspectPath.bind(this, inspectedPathType)} onInspectPath={keyPath =>
this.handleInspectPath(inspectedPathType, keyPath)
}
inspectedPath={monitorState[inspectedPathType]} inspectedPath={monitorState[inspectedPathType]}
onSelectTab={this.handleSelectTab} onSelectTab={this.handleSelectTab}
/> />
@ -295,11 +352,11 @@ export default class DevtoolsInspector extends Component {
); );
} }
handleToggleAction = actionId => { handleToggleAction = (actionId: number) => {
this.props.dispatch(toggleAction(actionId)); this.props.dispatch(toggleAction(actionId));
}; };
handleJumpToState = actionId => { handleJumpToState = (actionId: number) => {
if (jumpToAction) { if (jumpToAction) {
this.props.dispatch(jumpToAction(actionId)); this.props.dispatch(jumpToAction(actionId));
} else { } else {
@ -309,7 +366,7 @@ export default class DevtoolsInspector extends Component {
} }
}; };
handleReorderAction = (actionId, beforeActionId) => { handleReorderAction = (actionId: number, beforeActionId: number) => {
if (reorderAction) if (reorderAction)
this.props.dispatch(reorderAction(actionId, beforeActionId)); this.props.dispatch(reorderAction(actionId, beforeActionId));
}; };
@ -322,11 +379,14 @@ export default class DevtoolsInspector extends Component {
this.props.dispatch(sweep()); this.props.dispatch(sweep());
}; };
handleSearch = val => { handleSearch = (val: string) => {
this.updateMonitorState({ searchValue: val }); this.updateMonitorState({ searchValue: val });
}; };
handleSelectAction = (e, actionId) => { handleSelectAction = (
e: React.MouseEvent<HTMLDivElement>,
actionId: number
) => {
const { monitorState } = this.props; const { monitorState } = this.props;
let startActionId; let startActionId;
let selectedActionId; let selectedActionId;
@ -365,11 +425,14 @@ export default class DevtoolsInspector extends Component {
this.updateMonitorState({ startActionId, selectedActionId }); this.updateMonitorState({ startActionId, selectedActionId });
}; };
handleInspectPath = (pathType, path) => { handleInspectPath = (
pathType: 'inspectedActionPath' | 'inspectedStatePath',
path: (string | number)[]
) => {
this.updateMonitorState({ [pathType]: path }); this.updateMonitorState({ [pathType]: path });
}; };
handleSelectTab = tabName => { handleSelectTab = (tabName: string) => {
this.updateMonitorState({ tabName }); this.updateMonitorState({ tabName });
}; };
} }

View File

@ -1,21 +0,0 @@
import React from 'react';
import { PropTypes } from 'prop-types';
const RightSlider = ({ styling, shown, children, rotate }) => (
<div
{...styling([
'rightSlider',
shown && 'rightSliderShown',
rotate && 'rightSliderRotate',
rotate && shown && 'rightSliderRotateShown'
])}
>
{children}
</div>
);
RightSlider.propTypes = {
shown: PropTypes.bool
};
export default RightSlider;

View File

@ -0,0 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StylingFunction } from 'react-base16-styling';
interface Props {
styling: StylingFunction;
shown?: boolean;
children: React.ReactNode;
rotate?: boolean;
}
const RightSlider: React.FunctionComponent<Props> = ({
styling,
shown,
children,
rotate
}) => (
<div
{...styling([
'rightSlider',
shown && 'rightSliderShown',
rotate && 'rightSliderRotate',
rotate && shown && 'rightSliderRotateShown'
])}
>
{children}
</div>
);
RightSlider.propTypes = {
styling: PropTypes.func.isRequired,
shown: PropTypes.bool,
children: PropTypes.any.isRequired,
rotate: PropTypes.bool
};
export default RightSlider;

View File

@ -0,0 +1,61 @@
declare module 'base16' {
export interface Base16Theme {
scheme?: string;
author?: string;
base00: string;
base01: string;
base02: string;
base03: string;
base04: string;
base05: string;
base06: string;
base07: string;
base08: string;
base09: string;
base0A: string;
base0B: string;
base0C: string;
base0D: string;
base0E: string;
base0F: string;
}
export const threezerotwofour: Base16Theme;
export const apathy: Base16Theme;
export const ashes: Base16Theme;
export const atelierDune: Base16Theme;
export const atelierForest: Base16Theme;
export const atelierHeath: Base16Theme;
export const atelierLakeside: Base16Theme;
export const atelierSeaside: Base16Theme;
export const bespin: Base16Theme;
export const brewer: Base16Theme;
export const bright: Base16Theme;
export const chalk: Base16Theme;
export const codeschool: Base16Theme;
export const colors: Base16Theme;
const _default: Base16Theme;
export default _default;
export const eighties: Base16Theme;
export const embers: Base16Theme;
export const flat: Base16Theme;
export const google: Base16Theme;
export const grayscale: Base16Theme;
export const greenscreen: Base16Theme;
export const harmonic: Base16Theme;
export const hopscotch: Base16Theme;
export const isotope: Base16Theme;
export const marrakesh: Base16Theme;
export const mocha: Base16Theme;
export const monokai: Base16Theme;
export const ocean: Base16Theme;
export const paraiso: Base16Theme;
export const pop: Base16Theme;
export const railscasts: Base16Theme;
export const shapeshifter: Base16Theme;
export const solarized: Base16Theme;
export const summerfruit: Base16Theme;
export const tomorrow: Base16Theme;
export const tube: Base16Theme;
export const twilight: Base16Theme;
}

View File

@ -1,28 +1,37 @@
import { DiffPatcher } from 'jsondiffpatch/src/diffpatcher'; import { DiffContext, DiffPatcher } from 'jsondiffpatch';
const defaultObjectHash = (o, idx) => const defaultObjectHash = (o: any, idx: number) =>
(o === null && '$$null') || (o === null && '$$null') ||
(o && (o.id || o.id === 0) && `$$id:${JSON.stringify(o.id)}`) || (o && (o.id || o.id === 0) && `$$id:${JSON.stringify(o.id)}`) ||
(o && (o._id || o._id === 0) && `$$_id:${JSON.stringify(o._id)}`) || (o && (o._id || o._id === 0) && `$$_id:${JSON.stringify(o._id)}`) ||
'$$index:' + idx; '$$index:' + idx;
const defaultPropertyFilter = (name, context) => const defaultPropertyFilter = (name: string, context: DiffContext) =>
typeof context.left[name] !== 'function' && typeof context.left[name] !== 'function' &&
typeof context.right[name] !== 'function'; typeof context.right[name] !== 'function';
const defaultDiffPatcher = new DiffPatcher({ const defaultDiffPatcher = new DiffPatcher({
arrays: { detectMove: false }, arrays: { detectMove: false } as {
detectMove: boolean;
includeValueOnMove: boolean;
},
objectHash: defaultObjectHash, objectHash: defaultObjectHash,
propertyFilter: defaultPropertyFilter propertyFilter: defaultPropertyFilter
}); });
export default function createDiffPatcher(objectHash, propertyFilter) { export default function createDiffPatcher(
objectHash: ((item: any, index: number) => string) | undefined,
propertyFilter: ((name: string, context: DiffContext) => boolean) | undefined
) {
if (!objectHash && !propertyFilter) { if (!objectHash && !propertyFilter) {
return defaultDiffPatcher; return defaultDiffPatcher;
} }
return new DiffPatcher({ return new DiffPatcher({
arrays: { detectMove: false }, arrays: { detectMove: false } as {
detectMove: boolean;
includeValueOnMove: boolean;
},
objectHash: objectHash || defaultObjectHash, objectHash: objectHash || defaultObjectHash,
propertyFilter: propertyFilter || defaultPropertyFilter propertyFilter: propertyFilter || defaultPropertyFilter
}); });

View File

@ -1 +0,0 @@
export default from './DevtoolsInspector';

View File

@ -0,0 +1,2 @@
import DevtoolsInspector from './DevtoolsInspector';
export default DevtoolsInspector;

View File

@ -0,0 +1,6 @@
declare module 'jss-nested' {
import { Plugin } from 'jss';
const jssNested: () => Plugin;
export default jssNested;
}

View File

@ -0,0 +1,6 @@
declare module 'jss-vendor-prefixer' {
import { Plugin } from 'jss';
const jssVendorPrefixer: () => Plugin;
export default jssVendorPrefixer;
}

View File

@ -0,0 +1,57 @@
declare module 'jss' {
import * as css from 'csstype';
// TODO: Type data better, currently typed as any for allowing to override it
type FnValue<R> = R | ((data: any) => R);
type NormalCssProperties = css.Properties<string | number>;
type CssProperties = {
[K in keyof NormalCssProperties]: FnValue<NormalCssProperties[K]>;
};
// Jss Style definitions
type JssStyleP<S> = CssProperties & {
[key: string]: FnValue<JssValue | S>;
};
export type JssStyle = JssStyleP<
JssStyleP<JssStyleP<JssStyleP<JssStyleP<JssStyleP<JssStyleP<void>>>>>>
>;
export type Styles<Name extends string | number | symbol = string> = Record<
Name,
JssStyle | string
>;
export type Classes<Name extends string | number | symbol = string> = Record<
Name,
string
>;
export type JssValue =
| string
| number
| Array<string | number | Array<string | number> | '!important'>
| null
| false;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Plugin {}
export interface StyleSheet<
RuleName extends string | number | symbol = string | number | symbol
> {
classes: Classes<RuleName>;
attach(): this;
detach(): this;
}
export interface Jss {
createStyleSheet<Name extends string | number | symbol>(
styles: Partial<Styles<Name>>
): StyleSheet<Name>;
use(...plugins: Plugin[]): this;
}
const jss: Jss;
export default jss;
}

View File

@ -0,0 +1,6 @@
declare module 'react-base16-styling' {
export function getBase16Theme(
theme: Theme,
base16Themes?: Base16Theme[] | null
): Base16Theme | undefined | null;
}

View File

@ -0,0 +1,8 @@
declare module 'react-dragula' {
import { DragulaOptions, Drake } from 'dragula';
export default function(
containers: Array<HTMLElement>,
options: DragulaOptions
): Drake;
}

View File

@ -0,0 +1,6 @@
declare module 'react-pure-render/function' {
export default function shouldPureComponentUpdate(
nextProps: unknown,
nextState: unknown
): boolean;
}

View File

@ -0,0 +1,5 @@
declare module 'redux-devtools-themes' {
import { Base16Theme } from 'base16';
export * from 'base16';
export const nicinabox: Base16Theme;
}

View File

@ -1,28 +0,0 @@
const UPDATE_MONITOR_STATE = '@@redux-devtools-inspector/UPDATE_MONITOR_STATE';
export const DEFAULT_STATE = {
selectedActionId: null,
startActionId: null,
inspectedActionPath: [],
inspectedStatePath: [],
tabName: 'Diff'
};
export function updateMonitorState(monitorState) {
return { type: UPDATE_MONITOR_STATE, monitorState };
}
function reduceUpdateState(state, action) {
return action.type === UPDATE_MONITOR_STATE
? {
...state,
...action.monitorState
}
: state;
}
export function reducer(props, state = DEFAULT_STATE, action) {
return {
...reduceUpdateState(state, action)
};
}

View File

@ -0,0 +1,52 @@
import { Action } from 'redux';
import { Props } from './DevtoolsInspector';
const UPDATE_MONITOR_STATE = '@@redux-devtools-inspector/UPDATE_MONITOR_STATE';
interface UpdateMonitorStateAction {
type: typeof UPDATE_MONITOR_STATE;
monitorState: Partial<MonitorState>;
}
export type MonitorAction = UpdateMonitorStateAction;
export interface MonitorState {
readonly selectedActionId: number | null;
readonly startActionId: number | null;
readonly inspectedActionPath: (string | number)[];
readonly inspectedStatePath: (string | number)[];
readonly tabName: string;
readonly searchValue?: string;
}
export const DEFAULT_STATE: MonitorState = {
selectedActionId: null,
startActionId: null,
inspectedActionPath: [],
inspectedStatePath: [],
tabName: 'Diff'
};
export function updateMonitorState(
monitorState: Partial<MonitorState>
): UpdateMonitorStateAction {
return { type: UPDATE_MONITOR_STATE, monitorState };
}
function reduceUpdateState(state: MonitorState, action: MonitorAction) {
return action.type === UPDATE_MONITOR_STATE
? {
...state,
...action.monitorState
}
: state;
}
export function reducer<S, A extends Action<unknown>>(
props: Props<S, A>,
state = DEFAULT_STATE,
action: MonitorAction
) {
return {
...reduceUpdateState(state, action)
};
}

View File

@ -1,9 +1,15 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import JSONTree from 'react-json-tree'; import JSONTree from 'react-json-tree';
import getItemString from './getItemString'; import getItemString from './getItemString';
import getJsonTreeTheme from './getJsonTreeTheme'; import getJsonTreeTheme from './getJsonTreeTheme';
import { TabComponentProps } from '../ActionPreview';
import { Action } from 'redux';
const ActionTab = ({ const ActionTab: React.FunctionComponent<TabComponentProps<
unknown,
Action<unknown>
>> = ({
action, action,
styling, styling,
base16Theme, base16Theme,
@ -24,4 +30,14 @@ const ActionTab = ({
/> />
); );
ActionTab.propTypes = {
action: PropTypes.any.isRequired,
styling: PropTypes.func.isRequired,
base16Theme: PropTypes.any.isRequired,
invertTheme: PropTypes.bool.isRequired,
labelRenderer: PropTypes.func.isRequired,
dataTypeKey: PropTypes.string,
isWideLayout: PropTypes.bool.isRequired
};
export default ActionTab; export default ActionTab;

View File

@ -1,24 +0,0 @@
import React from 'react';
import JSONDiff from './JSONDiff';
const DiffTab = ({
delta,
styling,
base16Theme,
invertTheme,
labelRenderer,
isWideLayout
}) => (
<JSONDiff
{...{
delta,
styling,
base16Theme,
invertTheme,
labelRenderer,
isWideLayout
}}
/>
);
export default DiffTab;

View File

@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import JSONDiff from './JSONDiff';
import { TabComponentProps } from '../ActionPreview';
import { Action } from 'redux';
const DiffTab: React.FunctionComponent<TabComponentProps<
unknown,
Action<unknown>
>> = ({
delta,
styling,
base16Theme,
invertTheme,
labelRenderer,
isWideLayout,
dataTypeKey
}) => (
<JSONDiff
{...{
delta,
styling,
base16Theme,
invertTheme,
labelRenderer,
isWideLayout,
dataTypeKey
}}
/>
);
DiffTab.propTypes = {
delta: PropTypes.any,
styling: PropTypes.func.isRequired,
base16Theme: PropTypes.any.isRequired,
invertTheme: PropTypes.bool.isRequired,
labelRenderer: PropTypes.func.isRequired,
isWideLayout: PropTypes.bool.isRequired,
dataTypeKey: PropTypes.string
};
export default DiffTab;

View File

@ -3,8 +3,10 @@ import JSONTree from 'react-json-tree';
import stringify from 'javascript-stringify'; import stringify from 'javascript-stringify';
import getItemString from './getItemString'; import getItemString from './getItemString';
import getJsonTreeTheme from './getJsonTreeTheme'; import getJsonTreeTheme from './getJsonTreeTheme';
import { Delta } from 'jsondiffpatch';
import { Base16Theme, StylingFunction } from 'react-base16-styling';
function stringifyAndShrink(val, isWideLayout) { function stringifyAndShrink(val: any, isWideLayout?: boolean) {
if (val === null) { if (val === null) {
return 'null'; return 'null';
} }
@ -19,18 +21,22 @@ function stringifyAndShrink(val, isWideLayout) {
return str.length > 22 ? `${str.substr(0, 15)}${str.substr(-5)}` : str; return str.length > 22 ? `${str.substr(0, 15)}${str.substr(-5)}` : str;
} }
const expandFirstLevel = (keyName, data, level) => level <= 1; const expandFirstLevel = (
keyName: (string | number)[],
data: any,
level: number
) => level <= 1;
function prepareDelta(value) { function prepareDelta(value: any) {
if (value && value._t === 'a') { if (value && value._t === 'a') {
const res = {}; const res: { [key: string]: any } = {};
for (let key in value) { for (const key in value) {
if (key !== '_t') { if (key !== '_t') {
if (key[0] === '_' && !value[key.substr(1)]) { if (key.startsWith('_') && !value[key.substr(1)]) {
res[key.substr(1)] = value[key]; res[key.substr(1)] = value[key];
} else if (value['_' + key]) { } else if (value['_' + key]) {
res[key] = [value['_' + key][0], value[key][0]]; res[key] = [value['_' + key][0], value[key][0]];
} else if (!value['_' + key] && key[0] !== '_') { } else if (!value['_' + key] && !key.startsWith('_')) {
res[key] = value[key]; res[key] = value[key];
} }
} }
@ -41,14 +47,33 @@ function prepareDelta(value) {
return value; return value;
} }
export default class JSONDiff extends Component { interface Props {
delta: Delta | null | undefined | false;
styling: StylingFunction;
base16Theme: Base16Theme;
invertTheme: boolean;
labelRenderer: (
keyPath: (string | number)[],
nodeType: string,
expanded: boolean,
expandable: boolean
) => React.ReactNode;
isWideLayout: boolean;
dataTypeKey: string | undefined;
}
interface State {
data: any;
}
export default class JSONDiff extends Component<Props, State> {
state = { data: {} }; state = { data: {} };
componentDidMount() { componentDidMount() {
this.updateData(); this.updateData();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: Props) {
if (prevProps.delta !== this.props.delta) { if (prevProps.delta !== this.props.delta) {
this.updateData(); this.updateData();
} }
@ -84,7 +109,7 @@ export default class JSONDiff extends Component {
); );
} }
getItemString = (type, data) => getItemString = (type: string, data: any) =>
getItemString( getItemString(
this.props.styling, this.props.styling,
type, type,
@ -94,10 +119,10 @@ export default class JSONDiff extends Component {
true true
); );
valueRenderer = (raw, value) => { valueRenderer = (raw: any, value: any) => {
const { styling, isWideLayout } = this.props; const { styling, isWideLayout } = this.props;
function renderSpan(name, body) { function renderSpan(name: string, body: string) {
return ( return (
<span key={name} {...styling(['diff', name])}> <span key={name} {...styling(['diff', name])}>
{body} {body}

View File

@ -1,9 +1,15 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import JSONTree from 'react-json-tree'; import JSONTree from 'react-json-tree';
import getItemString from './getItemString'; import getItemString from './getItemString';
import getJsonTreeTheme from './getJsonTreeTheme'; import getJsonTreeTheme from './getJsonTreeTheme';
import { TabComponentProps } from '../ActionPreview';
import { Action } from 'redux';
const StateTab = ({ const StateTab: React.FunctionComponent<TabComponentProps<
any,
Action<unknown>
>> = ({
nextState, nextState,
styling, styling,
base16Theme, base16Theme,
@ -24,4 +30,14 @@ const StateTab = ({
/> />
); );
StateTab.propTypes = {
nextState: PropTypes.any.isRequired,
styling: PropTypes.func.isRequired,
base16Theme: PropTypes.any.isRequired,
invertTheme: PropTypes.bool.isRequired,
labelRenderer: PropTypes.func.isRequired,
dataTypeKey: PropTypes.string,
isWideLayout: PropTypes.bool.isRequired
};
export default StateTab; export default StateTab;

View File

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { Iterable } from 'immutable'; import { Iterable } from 'immutable';
import isIterable from '../utils/isIterable'; import isIterable from '../utils/isIterable';
import { StylingFunction } from 'react-base16-styling';
const IS_IMMUTABLE_KEY = '@@__IS_IMMUTABLE__@@'; const IS_IMMUTABLE_KEY = '@@__IS_IMMUTABLE__@@';
function isImmutable(value) { function isImmutable(value: any) {
return ( return (
Iterable.isKeyed(value) || Iterable.isKeyed(value) ||
Iterable.isIndexed(value) || Iterable.isIndexed(value) ||
@ -12,7 +13,7 @@ function isImmutable(value) {
); );
} }
function getShortTypeString(val, diff) { function getShortTypeString(val: any, diff: boolean | undefined) {
if (diff && Array.isArray(val)) { if (diff && Array.isArray(val)) {
val = val[val.length === 2 ? 1 : 0]; val = val[val.length === 2 ? 1 : 0];
} }
@ -38,7 +39,12 @@ function getShortTypeString(val, diff) {
} }
} }
function getText(type, data, isWideLayout, isDiff) { function getText(
type: string,
data: any,
isWideLayout: boolean,
isDiff: boolean | undefined
) {
if (type === 'Object') { if (type === 'Object') {
const keys = Object.keys(data); const keys = Object.keys(data);
if (!isWideLayout) return keys.length ? '{…}' : '{}'; if (!isWideLayout) return keys.length ? '{…}' : '{}';
@ -55,7 +61,7 @@ function getText(type, data, isWideLayout, isDiff) {
const str = data const str = data
.slice(0, 4) .slice(0, 4)
.map(val => getShortTypeString(val, isDiff)) .map((val: any) => getShortTypeString(val, isDiff))
.concat(data.length > 4 ? ['…'] : []) .concat(data.length > 4 ? ['…'] : [])
.join(', '); .join(', ');
@ -66,12 +72,12 @@ function getText(type, data, isWideLayout, isDiff) {
} }
const getItemString = ( const getItemString = (
styling, styling: StylingFunction,
type, type: string,
data, data: any,
dataTypeKey, dataTypeKey: string | undefined,
isWideLayout, isWideLayout: boolean,
isDiff isDiff?: boolean
) => ( ) => (
<span {...styling('treeItemHint')}> <span {...styling('treeItemHint')}>
{data[IS_IMMUTABLE_KEY] ? 'Immutable' : ''} {data[IS_IMMUTABLE_KEY] ? 'Immutable' : ''}

View File

@ -1,6 +1,10 @@
export default function getJsonTreeTheme(base16Theme) { import { Base16Theme, StylingConfig, StylingValue } from 'react-base16-styling';
export default function getJsonTreeTheme(
base16Theme: Base16Theme
): StylingConfig {
return { return {
extend: base16Theme, extend: base16Theme as StylingValue,
nestedNode: ({ style }, keyPath, nodeType, expanded) => ({ nestedNode: ({ style }, keyPath, nodeType, expanded) => ({
style: { style: {
...style, ...style,

View File

@ -1,7 +1,7 @@
import jss from 'jss'; import jss, { Styles, StyleSheet } from 'jss';
import jssVendorPrefixer from 'jss-vendor-prefixer'; import jssVendorPrefixer from 'jss-vendor-prefixer';
import jssNested from 'jss-nested'; import jssNested from 'jss-nested';
import { createStyling } from 'react-base16-styling'; import { Base16Theme, createStyling } from 'react-base16-styling';
import rgba from 'hex-rgba'; import rgba from 'hex-rgba';
import inspector from '../themes/inspector'; import inspector from '../themes/inspector';
import * as reduxThemes from 'redux-devtools-themes'; import * as reduxThemes from 'redux-devtools-themes';
@ -10,7 +10,7 @@ import * as inspectorThemes from '../themes';
jss.use(jssVendorPrefixer()); jss.use(jssVendorPrefixer());
jss.use(jssNested()); jss.use(jssNested());
const colorMap = theme => ({ const colorMap = (theme: Base16Theme) => ({
TEXT_COLOR: theme.base06, TEXT_COLOR: theme.base06,
TEXT_PLACEHOLDER_COLOR: rgba(theme.base06, 60), TEXT_PLACEHOLDER_COLOR: rgba(theme.base06, 60),
BACKGROUND_COLOR: theme.base00, BACKGROUND_COLOR: theme.base00,
@ -36,7 +36,12 @@ const colorMap = theme => ({
ERROR_COLOR: theme.base08 ERROR_COLOR: theme.base08
}); });
const getSheetFromColorMap = map => ({ type Color = keyof ReturnType<typeof colorMap>;
type ColorMap = {
[color in Color]: string;
};
const getSheetFromColorMap = (map: ColorMap): Partial<Styles> => ({
inspector: { inspector: {
display: 'flex', display: 'flex',
'flex-direction': 'column', 'flex-direction': 'column',
@ -386,9 +391,9 @@ const getSheetFromColorMap = map => ({
} }
}); });
let themeSheet; let themeSheet: StyleSheet;
const getDefaultThemeStyling = theme => { const getDefaultThemeStyling = (theme: Base16Theme) => {
if (themeSheet) { if (themeSheet) {
themeSheet.detach(); themeSheet.detach();
} }
@ -400,7 +405,10 @@ const getDefaultThemeStyling = theme => {
return themeSheet.classes; return themeSheet.classes;
}; };
export const base16Themes = { ...reduxThemes, ...inspectorThemes }; export const base16Themes = ({
...reduxThemes,
...inspectorThemes
} as unknown) as Base16Theme[];
export const createStylingFromTheme = createStyling(getDefaultThemeStyling, { export const createStylingFromTheme = createStyling(getDefaultThemeStyling, {
defaultBase16: inspector, defaultBase16: inspector,

View File

@ -1,30 +0,0 @@
function deepMapCached(obj, f, ctx, cache) {
cache.push(obj);
if (Array.isArray(obj)) {
return obj.map(function(val, key) {
val = f.call(ctx, val, key);
return typeof val === 'object' && cache.indexOf(val) === -1
? deepMapCached(val, f, ctx, cache)
: val;
});
} else if (typeof obj === 'object') {
const res = {};
for (const key in obj) {
let val = obj[key];
if (val && typeof val === 'object') {
val = f.call(ctx, val, key);
res[key] =
cache.indexOf(val) === -1 ? deepMapCached(val, f, ctx, cache) : val;
} else {
res[key] = f.call(ctx, val, key);
}
}
return res;
} else {
return obj;
}
}
export default function deepMap(obj, f, ctx) {
return deepMapCached(obj, f, ctx, []);
}

View File

@ -1,10 +1,10 @@
import { Iterable, fromJS } from 'immutable'; import { Iterable, fromJS } from 'immutable';
import isIterable from './isIterable'; import isIterable from './isIterable';
function iterateToKey(obj, key) { function iterateToKey(obj: any, key: string | number) {
// maybe there's a better way, dunno // maybe there's a better way, dunno
let idx = 0; let idx = 0;
for (let entry of obj) { for (const entry of obj) {
if (Array.isArray(entry)) { if (Array.isArray(entry)) {
if (entry[0] === key) return entry[1]; if (entry[0] === key) return entry[1];
} else { } else {
@ -15,11 +15,15 @@ function iterateToKey(obj, key) {
} }
} }
export default function getInspectedState(state, path, convertImmutable) { export default function getInspectedState<S>(
state: S,
path: (string | number)[],
convertImmutable: boolean
): S {
state = state =
path && path.length path && path.length
? { ? ({
[path[path.length - 1]]: path.reduce((s, key) => { [path[path.length - 1]]: path.reduce((s: any, key) => {
if (!s) { if (!s) {
return s; return s;
} }
@ -32,7 +36,7 @@ export default function getInspectedState(state, path, convertImmutable) {
return s[key]; return s[key];
}, state) }, state)
} } as S)
: state; : state;
if (convertImmutable) { if (convertImmutable) {

View File

@ -1,4 +1,4 @@
export default function isIterable(obj) { export default function isIterable(obj: any) {
return ( return (
obj !== null && obj !== null &&
typeof obj === 'object' && typeof obj === 'object' &&

View File

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.react.base.json",
"compilerOptions": {
"outDir": "lib"
},
"include": ["src"]
}