Farmbot-Web-App/frontend/folders/component.tsx

340 lines
11 KiB
TypeScript

import React from "react";
import {
BlurableInput,
EmptyStateWrapper,
EmptyStateGraphic,
ColorPicker,
} from "../ui";
import {
FolderUnion,
FolderItemProps,
FolderNodeProps,
FolderProps,
FolderState,
AddFolderBtn,
AddSequenceProps,
ToggleFolderBtnProps,
FolderNodeState,
FolderPanelTopProps,
SequenceDropAreaProps,
FolderButtonClusterProps,
FolderNameInputProps,
SequenceDropAreaState,
} from "./constants";
import {
createFolder,
deleteFolder,
setFolderName,
toggleFolderOpenState,
toggleFolderEditState,
toggleAll,
updateSearchTerm,
addNewSequenceToFolder,
moveSequence,
setFolderColor,
dropSequence,
sequenceEditMaybeSave,
} from "./actions";
import { Link } from "../link";
import { urlFriendly, lastUrlChunk } from "../util";
import {
setActiveSequenceByName
} from "../sequences/set_active_sequence_by_name";
import { Popover } from "@blueprintjs/core";
import { t } from "../i18next_wrapper";
import { Content } from "../constants";
import { StepDragger, NULL_DRAGGER_ID } from "../draggable/step_dragger";
import { variableList } from "../sequences/locals_list/variable_support";
import { UUID } from "../resources/interfaces";
export const FolderListItem = (props: FolderItemProps) => {
const { sequence, movedSequenceUuid } = props;
const seqName = sequence.body.name;
const url = `/app/sequences/${urlFriendly(seqName) || ""}`;
const moveSource = movedSequenceUuid === sequence.uuid ? "move-source" : "";
const nameWithSaveIndicator = seqName + (sequence.specialStatus ? "*" : "");
const active = lastUrlChunk() === urlFriendly(seqName) ? "active" : "";
return <StepDragger
dispatch={props.dispatch}
step={{
kind: "execute",
args: { sequence_id: props.sequence.body.id || 0 },
body: variableList(props.variableData)
}}
intent="step_splice"
draggerId={NULL_DRAGGER_ID}
resourceUuid={sequence.uuid}>
<li className={`sequence-list-item ${active} ${moveSource}`}
draggable={true}>
<ColorPicker
current={sequence.body.color || "gray"}
onChange={color => sequenceEditMaybeSave(sequence, { color })} />
<Link to={url} key={sequence.uuid} onClick={setActiveSequenceByName}>
<p>{nameWithSaveIndicator}</p>
</Link>
<div className="sequence-list-item-icons">
{props.inUse &&
<i className="in-use fa fa-hdd-o" title={t(Content.IN_USE)} />}
<i className="fa fa-bars"
onMouseDown={() => props.startSequenceMove(sequence.uuid)}
onMouseUp={() => props.toggleSequenceMove(sequence.uuid)} />
</div>
</li>
</StepDragger>;
};
const ToggleFolderBtn = (props: ToggleFolderBtnProps) => {
return <button className="fb-button gray" onClick={props.onClick}>
<i className={`fa fa-chevron-${props.expanded ? "right" : "down"}`} />
</button>;
};
const AddFolderBtn = ({ folder, close }: AddFolderBtn) => {
return <button
className="fb-button green"
onClick={() => { close?.(); createFolder(folder); }}>
<div className="fa-stack fa-2x" title={"Create Subfolder"}>
<i className="fa fa-folder fa-stack-2x" />
<i className="fa fa-plus fa-stack-1x" />
</div>
</button>;
};
const AddSequenceBtn = ({ folderId, close }: AddSequenceProps) => {
return <button
className="fb-button green"
onClick={() => { close?.(); addNewSequenceToFolder(folderId); }}>
<div className="fa-stack fa-2x">
<i className="fa fa-server fa-stack-2x" />
<i className="fa fa-plus fa-stack-1x" />
</div>
</button>;
};
export const FolderButtonCluster =
({ node, close }: FolderButtonClusterProps) => {
return <div className="folder-button-cluster">
<button
className="fb-button red"
onClick={() => deleteFolder(node.id)}>
<i className="fa fa-trash" />
</button>
<button
className="fb-button gray"
onClick={() => { close(); toggleFolderEditState(node.id); }}>
<i className="fa fa-pencil" />
</button>
{node.kind !== "terminal" &&
<AddFolderBtn folder={{ parent_id: node.id }} close={close} />}
<AddSequenceBtn folderId={node.id} close={close} />
</div>;
};
export const FolderNameInput = ({ node }: FolderNameInputProps) =>
<div className="folder-name-input">
<BlurableInput value={node.name} autoFocus={true} autoSelect={true}
onCommit={e => {
setFolderName(node.id, e.currentTarget.value);
toggleFolderEditState(node.id);
}} />
<button
className="fb-button green"
onClick={() => toggleFolderEditState(node.id)}>
<i className="fa fa-check" />
</button>
</div>;
export class FolderNameEditor
extends React.Component<FolderNodeProps, FolderNodeState> {
state: FolderNodeState = { settingsOpen: false };
close = () => this.setState({ settingsOpen: false });
render() {
const { node } = this.props;
const settingsOpenClass = this.state.settingsOpen ? "open" : "";
return <div className={"folder-list-item"}>
<i className={`fa fa-chevron-${node.open ? "down" : "right"}`}
title={"Open/Close Folder"}
onClick={() => toggleFolderOpenState(node.id)} />
<ColorPicker
saucerIcon={"fa-folder"}
current={node.color}
onChange={color => setFolderColor(node.id, color)} />
<div className="folder-name">
{node.editing
? <FolderNameInput node={node} />
: <p>{node.name}</p>}
</div>
<Popover className="folder-settings-icon" usePortal={false}
isOpen={this.state.settingsOpen}>
<i className={`fa fa-ellipsis-v ${settingsOpenClass}`}
onClick={() =>
this.setState({ settingsOpen: !this.state.settingsOpen })} />
<FolderButtonCluster {...this.props} close={this.close} />
</Popover>
</div>;
}
}
const FolderNode = (props: FolderNodeProps) => {
const { node, sequences } = props;
const sequenceItems = node.content.map(seqUuid =>
<FolderListItem
sequence={sequences[seqUuid]}
key={"F" + seqUuid}
dispatch={props.dispatch}
variableData={props.sequenceMetas[seqUuid]}
inUse={!!props.resourceUsage[seqUuid]}
toggleSequenceMove={props.toggleSequenceMove}
startSequenceMove={props.startSequenceMove}
movedSequenceUuid={props.movedSequenceUuid} />);
const childFolders: FolderUnion[] = node.children || [];
const folderNodes = childFolders.map(folder =>
<FolderNode
node={folder}
key={folder.id}
sequences={sequences}
dispatch={props.dispatch}
sequenceMetas={props.sequenceMetas}
resourceUsage={props.resourceUsage}
movedSequenceUuid={props.movedSequenceUuid}
toggleSequenceMove={props.toggleSequenceMove}
startSequenceMove={props.startSequenceMove}
onMoveEnd={props.onMoveEnd} />);
return <div className="folder">
<FolderNameEditor {...props} />
{!!node.open && <ul className="in-folder-sequences">{sequenceItems}</ul>}
<SequenceDropArea
dropAreaVisible={!!props.movedSequenceUuid}
onMoveEnd={props.onMoveEnd}
toggleSequenceMove={props.toggleSequenceMove}
folderId={node.id}
folderName={node.name} />
{!!node.open && folderNodes}
</div>;
};
export class SequenceDropArea
extends React.Component<SequenceDropAreaProps, SequenceDropAreaState> {
state: SequenceDropAreaState = { hovered: false };
render() {
const { dropAreaVisible, folderId, onMoveEnd, folderName } = this.props;
const visible = dropAreaVisible ? "visible" : "";
const hovered = this.state.hovered ? "hovered" : "";
return <div
className={`folder-drop-area ${visible} ${hovered}`}
onClick={() => onMoveEnd(folderId)}
onDrop={e => {
this.setState({ hovered: false });
dropSequence(folderId)(e);
this.props.toggleSequenceMove();
}}
onDragOver={e => e.preventDefault()}
onDragEnter={() => this.setState({ hovered: true })}
onDragLeave={() => this.setState({ hovered: false })}>
{folderId
? `${t("Move into")} ${folderName}`
: t("Move out of folders")}
</div>;
}
}
export class Folders extends React.Component<FolderProps, FolderState> {
state: FolderState = { toggleDirection: false };
Graph = () => {
return <div className="folders">
{this.props.rootFolder.folders.map(grandparent => {
return <FolderNode
node={grandparent}
key={grandparent.id}
dispatch={this.props.dispatch}
sequenceMetas={this.props.sequenceMetas}
resourceUsage={this.props.resourceUsage}
movedSequenceUuid={this.state.movedSequenceUuid}
toggleSequenceMove={this.toggleSequenceMove}
startSequenceMove={this.startSequenceMove}
onMoveEnd={this.endSequenceMove}
sequences={this.props.sequences} />;
})}
</div>;
}
toggleAll = () => {
toggleAll(this.state.toggleDirection);
this.setState({ toggleDirection: !this.state.toggleDirection });
}
startSequenceMove = (seqUuid: UUID) => this.setState({
movedSequenceUuid: seqUuid,
stashedUuid: this.state.movedSequenceUuid,
})
toggleSequenceMove = (seqUuid?: UUID) => this.setState({
movedSequenceUuid: this.state.stashedUuid ? undefined : seqUuid,
})
endSequenceMove = (folderId: number) => {
moveSequence(this.state.movedSequenceUuid || "", folderId);
this.setState({ movedSequenceUuid: undefined });
}
rootSequences = () => this.props.rootFolder.noFolder.map(seqUuid =>
<FolderListItem
key={seqUuid}
dispatch={this.props.dispatch}
variableData={this.props.sequenceMetas[seqUuid]}
inUse={!!this.props.resourceUsage[seqUuid]}
sequence={this.props.sequences[seqUuid]}
toggleSequenceMove={this.toggleSequenceMove}
startSequenceMove={this.startSequenceMove}
movedSequenceUuid={this.state.movedSequenceUuid} />);
render() {
return <div className="folders-panel">
<FolderPanelTop
searchTerm={this.props.searchTerm}
toggleDirection={this.state.toggleDirection}
toggleAll={this.toggleAll} />
<EmptyStateWrapper
notEmpty={Object.values(this.props.sequences).length > 0
|| this.props.rootFolder.folders.length > 0}
graphic={EmptyStateGraphic.sequences}
title={t("No Sequences.")}
text={Content.NO_SEQUENCES}>
<ul className="sequences-not-in-folders">
{this.rootSequences()}
</ul>
<SequenceDropArea
dropAreaVisible={!!this.state.movedSequenceUuid}
onMoveEnd={this.endSequenceMove}
toggleSequenceMove={this.toggleSequenceMove}
folderId={0}
folderName={"none"} />
<this.Graph />
</EmptyStateWrapper>
</div>;
}
}
export const FolderPanelTop = (props: FolderPanelTopProps) =>
<div className="panel-top with-button">
<div className="thin-search-wrapper">
<div className="text-input-wrapper">
<i className="fa fa-search" />
<input
value={props.searchTerm || ""}
onChange={e => updateSearchTerm(e.currentTarget.value)}
type="text"
placeholder={t("Search sequences")} />
</div>
</div>
<ToggleFolderBtn
expanded={props.toggleDirection}
onClick={props.toggleAll} />
<AddFolderBtn />
<AddSequenceBtn />
</div>;