2020-08-31 00:49:06 +03:00
|
|
|
import React, { PureComponent, RefCallback } from 'react';
|
|
|
|
import { Drake } from 'dragula';
|
2018-12-22 03:10:49 +03:00
|
|
|
import dragula from 'react-dragula';
|
2020-08-31 00:49:06 +03:00
|
|
|
import { Action } from 'redux';
|
2020-12-21 17:08:08 +03:00
|
|
|
import { PerformAction } from '@redux-devtools/core';
|
2020-08-31 00:49:06 +03:00
|
|
|
import { StylingFunction } from 'react-base16-styling';
|
2018-12-22 03:10:49 +03:00
|
|
|
import ActionListRow from './ActionListRow';
|
|
|
|
import ActionListHeader from './ActionListHeader';
|
|
|
|
|
2020-08-31 00:49:06 +03:00
|
|
|
function getTimestamps<A extends Action<unknown>>(
|
|
|
|
actions: { [actionId: number]: PerformAction<A> },
|
|
|
|
actionIds: number[],
|
|
|
|
actionId: number
|
|
|
|
) {
|
2018-12-22 03:10:49 +03:00
|
|
|
const idx = actionIds.indexOf(actionId);
|
|
|
|
const prevActionId = actionIds[idx - 1];
|
|
|
|
|
|
|
|
return {
|
|
|
|
current: actions[actionId].timestamp,
|
2020-08-08 23:26:39 +03:00
|
|
|
previous: idx ? actions[prevActionId].timestamp : 0,
|
2018-12-22 03:10:49 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-08-31 00:49:06 +03:00
|
|
|
interface Props<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;
|
|
|
|
onSearch: (value: string) => void;
|
|
|
|
onSelect: (e: React.MouseEvent<HTMLDivElement>, actionId: number) => 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<
|
|
|
|
A extends Action<unknown>
|
|
|
|
> extends PureComponent<Props<A>> {
|
|
|
|
node?: HTMLDivElement | null;
|
|
|
|
scrollDown?: boolean;
|
|
|
|
drake?: Drake;
|
2018-12-22 03:10:49 +03:00
|
|
|
|
2020-08-31 00:49:06 +03:00
|
|
|
UNSAFE_componentWillReceiveProps(nextProps: Props<A>) {
|
2018-12-22 03:10:49 +03:00
|
|
|
const node = this.node;
|
|
|
|
if (!node) {
|
|
|
|
this.scrollDown = true;
|
|
|
|
} else if (this.props.lastActionId !== nextProps.lastActionId) {
|
|
|
|
const { scrollTop, offsetHeight, scrollHeight } = node;
|
2019-01-10 21:51:14 +03:00
|
|
|
this.scrollDown =
|
|
|
|
Math.abs(scrollHeight - (scrollTop + offsetHeight)) < 50;
|
2018-12-22 03:10:49 +03:00
|
|
|
} else {
|
|
|
|
this.scrollDown = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
this.scrollDown = true;
|
|
|
|
this.scrollToBottom();
|
|
|
|
|
|
|
|
if (!this.props.draggableActions) return;
|
2020-08-31 00:49:06 +03:00
|
|
|
const container = this.node!;
|
2018-12-22 03:10:49 +03:00
|
|
|
this.drake = dragula([container], {
|
|
|
|
copy: false,
|
|
|
|
copySortSource: false,
|
|
|
|
mirrorContainer: container,
|
2019-01-10 21:51:14 +03:00
|
|
|
accepts: (el, target, source, sibling) =>
|
2020-08-31 00:49:06 +03:00
|
|
|
!sibling || !!parseInt(sibling.getAttribute('data-id')!),
|
2019-01-10 21:51:14 +03:00
|
|
|
moves: (el, source, handle) =>
|
2020-08-31 00:49:06 +03:00
|
|
|
!!parseInt(el!.getAttribute('data-id')!) &&
|
|
|
|
handle!.className.indexOf('selectorButton') !== 0,
|
2018-12-22 03:10:49 +03:00
|
|
|
}).on('drop', (el, target, source, sibling) => {
|
|
|
|
let beforeActionId = this.props.actionIds.length;
|
|
|
|
if (sibling && sibling.className.indexOf('gu-mirror') === -1) {
|
2020-08-31 00:49:06 +03:00
|
|
|
beforeActionId = parseInt(sibling.getAttribute('data-id')!);
|
2018-12-22 03:10:49 +03:00
|
|
|
}
|
2020-08-31 00:49:06 +03:00
|
|
|
const actionId = parseInt(el.getAttribute('data-id')!);
|
2019-01-10 21:51:14 +03:00
|
|
|
this.props.onReorderAction(actionId, beforeActionId);
|
2018-12-22 03:10:49 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
if (this.drake) this.drake.destroy();
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate() {
|
|
|
|
this.scrollToBottom();
|
|
|
|
}
|
|
|
|
|
|
|
|
scrollToBottom() {
|
|
|
|
if (this.scrollDown && this.node) {
|
|
|
|
this.node.scrollTop = this.node.scrollHeight;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-31 00:49:06 +03:00
|
|
|
getRef: RefCallback<HTMLDivElement> = (node) => {
|
2018-12-22 03:10:49 +03:00
|
|
|
this.node = node;
|
2019-01-10 21:51:14 +03:00
|
|
|
};
|
2018-12-22 03:10:49 +03:00
|
|
|
|
|
|
|
render() {
|
2019-01-10 21:51:14 +03:00
|
|
|
const {
|
|
|
|
styling,
|
|
|
|
actions,
|
|
|
|
actionIds,
|
|
|
|
isWideLayout,
|
|
|
|
onToggleAction,
|
|
|
|
skippedActionIds,
|
|
|
|
selectedActionId,
|
|
|
|
startActionId,
|
|
|
|
onSelect,
|
|
|
|
onSearch,
|
|
|
|
searchValue,
|
|
|
|
currentActionId,
|
|
|
|
hideMainButtons,
|
|
|
|
hideActionButtons,
|
|
|
|
onCommit,
|
|
|
|
onSweep,
|
2020-08-08 23:26:39 +03:00
|
|
|
onJumpToState,
|
2019-01-10 21:51:14 +03:00
|
|
|
} = this.props;
|
2018-12-22 03:10:49 +03:00
|
|
|
const lowerSearchValue = searchValue && searchValue.toLowerCase();
|
2019-01-10 21:51:14 +03:00
|
|
|
const filteredActionIds = searchValue
|
|
|
|
? actionIds.filter(
|
2020-08-08 23:26:39 +03:00
|
|
|
(id) =>
|
2020-08-31 00:49:06 +03:00
|
|
|
(actions[id].action.type as string)
|
|
|
|
.toLowerCase()
|
|
|
|
.indexOf(lowerSearchValue as string) !== -1
|
2019-01-10 21:51:14 +03:00
|
|
|
)
|
|
|
|
: actionIds;
|
2018-12-22 03:10:49 +03:00
|
|
|
|
|
|
|
return (
|
2019-01-10 21:51:14 +03:00
|
|
|
<div
|
|
|
|
key="actionList"
|
2021-10-22 17:49:53 +03:00
|
|
|
data-testid="actionList"
|
2019-01-10 21:51:14 +03:00
|
|
|
{...styling(
|
|
|
|
['actionList', isWideLayout && 'actionListWide'],
|
|
|
|
isWideLayout
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
<ActionListHeader
|
|
|
|
styling={styling}
|
2018-12-22 03:10:49 +03:00
|
|
|
onSearch={onSearch}
|
|
|
|
onCommit={onCommit}
|
|
|
|
onSweep={onSweep}
|
|
|
|
hideMainButtons={hideMainButtons}
|
|
|
|
hasSkippedActions={skippedActionIds.length > 0}
|
2019-01-10 21:51:14 +03:00
|
|
|
hasStagedActions={actionIds.length > 1}
|
|
|
|
/>
|
2021-10-22 17:49:53 +03:00
|
|
|
<div
|
|
|
|
data-testid="actionListRows"
|
|
|
|
{...styling('actionListRows')}
|
|
|
|
ref={this.getRef}
|
|
|
|
>
|
2020-08-08 23:26:39 +03:00
|
|
|
{filteredActionIds.map((actionId) => (
|
2019-01-10 21:51:14 +03:00
|
|
|
<ActionListRow
|
|
|
|
key={actionId}
|
2018-12-22 03:10:49 +03:00
|
|
|
styling={styling}
|
|
|
|
actionId={actionId}
|
|
|
|
isInitAction={!actionId}
|
|
|
|
isSelected={
|
2019-01-10 21:51:14 +03:00
|
|
|
(startActionId !== null &&
|
|
|
|
actionId >= startActionId &&
|
2020-08-31 00:49:06 +03:00
|
|
|
actionId <= (selectedActionId as number)) ||
|
2019-01-10 21:51:14 +03:00
|
|
|
actionId === selectedActionId
|
2018-12-22 03:10:49 +03:00
|
|
|
}
|
|
|
|
isInFuture={
|
|
|
|
actionIds.indexOf(actionId) > actionIds.indexOf(currentActionId)
|
|
|
|
}
|
2020-08-31 00:49:06 +03:00
|
|
|
onSelect={(e: React.MouseEvent<HTMLDivElement>) =>
|
|
|
|
onSelect(e, actionId)
|
|
|
|
}
|
2018-12-22 03:10:49 +03:00
|
|
|
timestamps={getTimestamps(actions, actionIds, actionId)}
|
|
|
|
action={actions[actionId].action}
|
|
|
|
onToggleClick={() => onToggleAction(actionId)}
|
|
|
|
onJumpClick={() => onJumpToState(actionId)}
|
2020-08-31 00:49:06 +03:00
|
|
|
onCommitClick={() => onCommit()}
|
2018-12-22 03:10:49 +03:00
|
|
|
hideActionButtons={hideActionButtons}
|
2019-01-10 21:51:14 +03:00
|
|
|
isSkipped={skippedActionIds.indexOf(actionId) !== -1}
|
|
|
|
/>
|
|
|
|
))}
|
2018-12-22 03:10:49 +03:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|