folders ui updates

pull/1639/head
gabrielburnworth 2019-12-21 11:47:19 -08:00
parent 307105c96e
commit 8d5218f67c
8 changed files with 278 additions and 129 deletions

View File

@ -41,22 +41,6 @@ body {
display: inline-block;
padding: 0.4rem;
}
.color-picker-item {
position: relative;
.active-border {
display: none;
}
&.active {
.active-border {
display: block;
position: absolute;
top: 2px;
left: 1px;
transform: scale(1.5);
color: $white;
}
}
}
}
.bp3-popover.help {

View File

@ -238,6 +238,12 @@
@media screen and (max-width: 767px) {
margin-left: -15px;
}
.panel-top {
margin-left: 1rem !important;
button {
margin-top: 0.7rem !important;
}
}
.panel-top,
.folder-button-cluster {
i {
@ -262,11 +268,41 @@
display: flex;
i {
width: 1.5rem !important;
line-height: 2rem !important;
}
}
ul {
margin-bottom: 0;
}
.folder-drop-area {
height: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left: 2rem;
padding-right: 2rem;
transition: height 0.5s ease-out,
padding-top 0.5s ease-out,
padding-bottom 0.5s ease-out;
transition-delay: 0.4s;
color: $gray;
font-weight: bold;
background: $white;
text-align: center;
cursor: pointer;
&.visible {
transition: height 0.3s ease-in,
padding-top 0.3s ease-in,
padding-bottom 0.3s ease-in;
transition-delay: 0.2s;
height: 3rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
&:hover, &.hovered {
color: $medium_gray;
}
}
.folders {
.folder > div:not(:first-child), ul {
margin-left: 1rem;
@ -277,24 +313,26 @@
display: flex;
position: relative;
width: 100%;
height: 3.5rem;
border-bottom: 1px solid $light_gray;
padding: 0.5rem;
cursor: pointer;
background: $lighter_gray;
border-left: 4px solid transparent;
&.active {
border-left: 3px solid $dark_gray;
border-left: 4px solid $dark_gray;
}
.fa-chevron-down, .fa-chevron-right {
z-index: 2;
width: 2rem;
position: absolute;
width: 3rem;
font-size: 1.1rem;
}
.folder-settings-icon,
.fa-arrows-v {
.fa-bars {
position: absolute;
right: 0;
}
.fa-arrows-v, .fa-ellipsis-v {
.fa-bars, .fa-ellipsis-v {
display: none;
}
.fa-ellipsis-v {
@ -303,13 +341,13 @@
}
}
&:hover {
.fa-arrows-v, .fa-ellipsis-v {
.fa-bars, .fa-ellipsis-v {
display: block;
}
}
i {
margin: 0;
line-height: 2.5rem;
line-height: 3.5rem;
width: 3rem;
text-align: center;
}
@ -319,27 +357,52 @@
width: 1.2rem;
height: 1.2rem;
}
a {
width: 100%;
}
p {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 1.2rem;
font-weight: bold;
width: 100%;
width: 75%;
padding: 0.5rem;
margin-left: 2rem;
padding-left: 0;
margin-left: 3rem;
line-height: 2.5rem;
}
.folder-name {
width: 100%;
margin-right: 3rem;
.input {
width: 90%;
margin-left: 2rem;
margin-left: 6rem;
p {
margin-left: 0;
}
.folder-name-input {
display: flex;
button {
top: 0.5rem;
width: auto;
height: 2rem;
i {
line-height: 0;
width: 1rem;
}
}
.input {
width: 90%;
margin: 0.3rem;
}
}
}
.sequence-list-item-icons {
display: flex;
margin-right: 3rem;
position: absolute;
right: 0;
.fa-hdd-o {
margin-right: 3rem;
}
}
button {
margin-left: 0.5rem;
@ -351,24 +414,34 @@
background: $white;
}
}
.folder-list-item,
.sequence-list-item {
padding-left: 2rem;
.saucer {
.bp3-popover-wrapper.color-picker {
position: absolute;
line-height: 0;
.bp3-popover-target {
width: 2rem;
height: 3.5rem;
}
}
padding-left: 3rem;
.saucer, .icon-saucer {
top: 0.55rem;
position: relative;
margin: auto;
margin-top: 0.6rem;
}
}
.folder-list-item {
.bp3-popover-wrapper {
position: absolute;
padding-left: 0;
.bp3-popover-wrapper.color-picker {
margin-left: 3rem;
}
.color-picker {
.bp3-popover-target {
margin-left: 2.5rem;
}
.icon-saucer {
top: 0;
left: 1.5rem;
line-height: 1.4rem;
margin-top: 0;
margin-left: 0.4rem;
}
}
}

View File

@ -1,6 +1,6 @@
import { RootFolderNode as Tree } from "./constants";
import { cloneAndClimb } from "./climb";
import { Color, SpecialStatus } from "farmbot";
import { Color, SpecialStatus, TaggedSequence } from "farmbot";
import { store } from "../redux/store";
import { initSave, destroy, edit, save, init } from "../api/crud";
import { Folder } from "farmbot/dist/resources/api_resources";
@ -11,6 +11,9 @@ import { t } from "../i18next_wrapper";
import { push } from "../history";
import { urlFriendly } from "../util";
import { setActiveSequenceByName } from "../sequences/set_active_sequence_by_name";
import { stepGet, STEP_DATATRANSFER_IDENTIFER } from "../draggable/actions";
import { joinKindAndId } from "../resources/reducer_support";
import { maybeGetSequence } from "../resources/selectors";
type TreePromise = Promise<Tree>;
@ -80,32 +83,42 @@ export const deleteFolder = (id: number) => {
return d(action) as ReturnType<typeof action>;
};
export const updateSearchTerm = (payload: string | undefined) => {
store.dispatch({
type: Actions.FOLDER_SEARCH,
payload
});
};
export const updateSearchTerm = (payload: string | undefined) =>
store.dispatch({ type: Actions.FOLDER_SEARCH, payload });
export const toggleFolderOpenState = (id: number) => Promise
.resolve(store.dispatch({ type: Actions.FOLDER_TOGGLE, payload: { id } }));
export const toggleFolderOpenState = (id: number) =>
store.dispatch({ type: Actions.FOLDER_TOGGLE, payload: { id } });
export const toggleFolderEditState =
(id: number) => Promise.resolve(store.dispatch({
type: Actions.FOLDER_TOGGLE_EDIT,
payload: { id }
}));
export const toggleFolderEditState = (id: number) =>
store.dispatch({ type: Actions.FOLDER_TOGGLE_EDIT, payload: { id } });
export const toggleAll = (payload: boolean) => Promise
.resolve(store.dispatch({ type: Actions.FOLDER_TOGGLE_ALL, payload }));
export const toggleAll = (payload: boolean) =>
store.dispatch({ type: Actions.FOLDER_TOGGLE_ALL, payload });
export const sequenceEditMaybeSave =
(sequence: TaggedSequence, update: Partial<TaggedSequence["body"]>) => {
const dispatch: Function = store.dispatch;
dispatch(edit(sequence, update));
if (sequence.specialStatus == SpecialStatus.SAVED) {
dispatch(save(sequence.uuid));
}
};
export function moveSequence(sequenceUuid: string, folder_id: number) {
const d: Function = store.dispatch;
const s = store.getState().resources.index.references[sequenceUuid];
if (s && s.kind === "Sequence") {
d(edit(s, { folder_id }));
if (s.specialStatus == SpecialStatus.SAVED) {
d(save(sequenceUuid));
}
sequenceEditMaybeSave(s, { folder_id });
}
}
export const dropSequence = (folder_id: number) =>
(e: React.DragEvent<HTMLElement>) => {
const key = e.dataTransfer.getData(STEP_DATATRANSFER_IDENTIFER);
const dispatch: Function = store.dispatch;
const dataXferObj = dispatch(stepGet(key));
const { sequence_id } = dataXferObj.value.args;
const ri = store.getState().resources.index;
const seqUuid = ri.byKindAndId[joinKindAndId("Sequence", sequence_id)];
const sequence = maybeGetSequence(ri, seqUuid);
if (sequence) { sequenceEditMaybeSave(sequence, { folder_id }); }
};

View File

@ -3,7 +3,6 @@ import {
BlurableInput,
EmptyStateWrapper,
EmptyStateGraphic,
Saucer,
ColorPicker,
} from "../ui";
import {
@ -17,6 +16,10 @@ import {
ToggleFolderBtnProps,
FolderNodeState,
FolderPanelTopProps,
SequenceDropAreaProps,
FolderButtonClusterProps,
FolderNameInputProps,
SequenceDropAreaState,
} from "./constants";
import {
createFolder,
@ -29,6 +32,8 @@ import {
addNewSequenceToFolder,
moveSequence,
setFolderColor,
dropSequence,
sequenceEditMaybeSave,
} from "./actions";
import { Link } from "../link";
import { urlFriendly, lastUrlChunk } from "../util";
@ -40,12 +45,13 @@ 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, onClick } = props;
const { sequence, movedSequenceUuid } = props;
const seqName = sequence.body.name;
const url = `/app/sequences/${urlFriendly(seqName) || ""}`;
const moveTarget = props.isMoveTarget ? "move-source" : "";
const moveSource = movedSequenceUuid === sequence.uuid ? "move-source" : "";
const nameWithSaveIndicator = seqName + (sequence.specialStatus ? "*" : "");
const active = lastUrlChunk() === urlFriendly(seqName) ? "active" : "";
return <StepDragger
@ -57,32 +63,35 @@ export const FolderListItem = (props: FolderItemProps) => {
}}
intent="step_splice"
draggerId={NULL_DRAGGER_ID}>
<Link to={url} key={sequence.uuid} onClick={setActiveSequenceByName}>
<li className={`sequence-list-item ${active} ${moveTarget}`}
draggable={true}>
<Saucer color={sequence.body.color || "gray"} active={false} />
<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>
<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-arrows-v"
onClick={() => onClick(sequence.uuid)} />
</div>
</li>
</Link>
</StepDragger>;
</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-${props.expanded ? "plus" : "minus"}-square`} />
<i className={`fa fa-chevron-${props.expanded ? "right" : "down"}`} />
</button>;
};
const AddFolderBtn = ({ folder }: AddFolderBtn) => {
const AddFolderBtn = ({ folder, close }: AddFolderBtn) => {
return <button
className="fb-button green"
onClick={() => createFolder(folder || {})}>
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" />
@ -90,10 +99,10 @@ const AddFolderBtn = ({ folder }: AddFolderBtn) => {
</button>;
};
const AddSequenceBtn = ({ folderId }: AddSequenceProps) => {
const AddSequenceBtn = ({ folderId, close }: AddSequenceProps) => {
return <button
className="fb-button green"
onClick={() => addNewSequenceToFolder(folderId)}>
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" />
@ -101,58 +110,62 @@ const AddSequenceBtn = ({ folderId }: AddSequenceProps) => {
</button>;
};
export const FolderButtonCluster = ({ node }: FolderNodeProps) => {
return <div className="folder-button-cluster">
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>;
};
const FolderNameInput = ({ node }: FolderNameInputProps) =>
<div className="folder-name-input">
<BlurableInput value={node.name} onCommit={e =>
setFolderName(node.id, e.currentTarget.value)} />
<button
className="fb-button red"
onClick={() => deleteFolder(node.id)}>
<i className="fa fa-trash" />
</button>
<button
className="fb-button gray"
className="fb-button green"
onClick={() => toggleFolderEditState(node.id)}>
<i className="fa fa-pencil" />
<i className="fa fa-check" />
</button>
{node.kind !== "terminal" &&
<AddFolderBtn folder={{ parent_id: node.id }} />}
<AddSequenceBtn folderId={node.id} />
</div>;
};
export class FolderNameEditor
extends React.Component<FolderNodeProps, FolderNodeState> {
state: FolderNodeState = { settingsOpen: false };
render() {
const { node } = this.props;
const moveModeTarget = this.props.movedSequenceUuid ? "move-target" : "";
const settingsOpenClass = this.state.settingsOpen ? "open" : "";
const nodeName = moveModeTarget ? t("CLICK TO MOVE HERE") : node.name;
const onClick = () =>
moveModeTarget ? this.props.onMoveEnd(node.id) : undefined;
const toggle = () =>
moveModeTarget ? undefined : toggleFolderOpenState(node.id);
return <div className={`folder-list-item ${moveModeTarget}`}
onClick={onClick}>
return <div className={"folder-list-item"}>
<i className={`fa fa-chevron-${node.open ? "down" : "right"}`}
title={"Open/Close Folder"}
onClick={toggle} />
onClick={() => toggleFolderOpenState(node.id)} />
<ColorPicker
saucerIcon={"fa-folder"}
current={node.color}
onChange={color => setFolderColor(node.id, color)} />
<div className="folder-name" onClick={toggle}>
<div className="folder-name">
{node.editing
? <BlurableInput value={nodeName} onCommit={e =>
setFolderName(node.id, e.currentTarget.value)
.then(() => toggleFolderEditState(node.id))} />
: <p>{nodeName}</p>}
? <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} />
<FolderButtonCluster {...this.props}
close={() => this.setState({ settingsOpen: false })} />
</Popover>
</div>;
}
@ -168,8 +181,9 @@ const FolderNode = (props: FolderNodeProps) => {
dispatch={props.dispatch}
variableData={props.sequenceMetas[seqUuid]}
inUse={!!props.resourceUsage[seqUuid]}
onClick={props.onMoveStart}
isMoveTarget={props.movedSequenceUuid === seqUuid} />);
toggleSequenceMove={props.toggleSequenceMove}
startSequenceMove={props.startSequenceMove}
movedSequenceUuid={props.movedSequenceUuid} />);
const childFolders: FolderUnion[] = node.children || [];
const folderNodes = childFolders.map(folder =>
@ -181,16 +195,48 @@ const FolderNode = (props: FolderNodeProps) => {
sequenceMetas={props.sequenceMetas}
resourceUsage={props.resourceUsage}
movedSequenceUuid={props.movedSequenceUuid}
onMoveStart={props.onMoveStart}
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 };
@ -204,7 +250,8 @@ export class Folders extends React.Component<FolderProps, FolderState> {
sequenceMetas={this.props.sequenceMetas}
resourceUsage={this.props.resourceUsage}
movedSequenceUuid={this.state.movedSequenceUuid}
onMoveStart={this.startSequenceMove}
toggleSequenceMove={this.toggleSequenceMove}
startSequenceMove={this.startSequenceMove}
onMoveEnd={this.endSequenceMove}
sequences={this.props.sequences} />;
})}
@ -216,9 +263,14 @@ export class Folders extends React.Component<FolderProps, FolderState> {
this.setState({ toggleDirection: !this.state.toggleDirection });
}
startSequenceMove = (seqUuid: string) => {
this.setState({ movedSequenceUuid: seqUuid });
}
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);
@ -232,8 +284,9 @@ export class Folders extends React.Component<FolderProps, FolderState> {
variableData={this.props.sequenceMetas[seqUuid]}
inUse={!!this.props.resourceUsage[seqUuid]}
sequence={this.props.sequences[seqUuid]}
onClick={this.startSequenceMove}
isMoveTarget={this.state.movedSequenceUuid === seqUuid} />);
toggleSequenceMove={this.toggleSequenceMove}
startSequenceMove={this.startSequenceMove}
movedSequenceUuid={this.state.movedSequenceUuid} />);
render() {
return <div className="folders-panel">
@ -250,6 +303,12 @@ export class Folders extends React.Component<FolderProps, FolderState> {
<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>;

View File

@ -73,6 +73,7 @@ export interface FolderNodeState {
export interface FolderState {
toggleDirection: boolean;
movedSequenceUuid?: string;
stashedUuid?: string;
}
export interface FolderPanelTopProps {
@ -85,28 +86,52 @@ export interface FolderNodeProps {
node: FolderUnion;
sequences: Record<string, TaggedSequence>;
movedSequenceUuid: string | undefined;
onMoveStart(sequenceUuid: string): void;
startSequenceMove(sequenceUuid: UUID): void;
toggleSequenceMove(sequenceUuid?: UUID): void;
onMoveEnd(folderId: number): void;
dispatch: Function;
resourceUsage: Record<UUID, boolean | undefined>;
sequenceMetas: Record<UUID, VariableNameSet | undefined>;
}
export interface FolderButtonClusterProps extends FolderNodeProps {
close(): void;
}
export interface FolderNameInputProps {
node: FolderUnion;
}
export interface FolderItemProps {
onClick(sequenceUuid: string): void;
startSequenceMove(sequenceUuid: UUID): void;
toggleSequenceMove(sequenceUuid?: UUID): void;
sequence: TaggedSequence;
isMoveTarget: boolean;
movedSequenceUuid: UUID | undefined;
dispatch: Function;
variableData: VariableNameSet | undefined;
inUse: boolean;
}
export interface SequenceDropAreaProps {
dropAreaVisible: boolean;
onMoveEnd(id: number): void;
toggleSequenceMove(sequenceUuid?: UUID): void;
folderId: number;
folderName: string;
}
export interface SequenceDropAreaState {
hovered: boolean;
}
export interface AddFolderBtn {
folder?: DeepPartial<Folder>;
close?(): void;
}
export interface AddSequenceProps {
folderId?: number;
close?(): void;
}
export interface ToggleFolderBtnProps {

View File

@ -165,7 +165,7 @@ export const resourceReducer =
.add<{ id: number }>(Actions.FOLDER_TOGGLE, (s, { payload }) => {
const { localMetaAttributes } = s.index.sequenceFolders;
const record = localMetaAttributes[parseInt("" + payload.id)];
record.open = !record.open;
record.open = !(record.open ?? true);
reindexFolders(s.index);
return s;
})

View File

@ -43,8 +43,7 @@ export class RawSequences extends React.Component<Props, {}> {
<Row>
<LeftPanel
className={`sequence-list-panel ${activeClasses}`}
title={t("Sequences")}
helpText={t(ToolTips.SEQUENCE_LIST)}>
title={t("Sequences")}>
<Folders {...this.props.folderData} dispatch={this.props.dispatch} />
</LeftPanel>
<CenterPanel

View File

@ -22,15 +22,11 @@ interface ColorPickerItemProps extends ColorPickerClusterProps {
}
const ColorPickerItem = (props: ColorPickerItemProps) => {
const isActive = props.color === props.current;
return <div className="color-picker-item-wrapper"
onClick={() => props.onChange(props.color)}>
{props.saucerIcon
? <div className={`color-picker-item ${isActive ? "active" : ""}`}>
<i className={`icon-saucer active-border fa ${props.saucerIcon}`} />
<i className={`icon-saucer fa ${props.saucerIcon} ${props.color}`} />
</div>
: <Saucer color={props.color} active={isActive} />}
? <i className={`icon-saucer fa ${props.saucerIcon} ${props.color}`} />
: <Saucer color={props.color} active={false} />}
</div>;
};