Skip to content

Commit

Permalink
FM-02 Flow Tabs (#23)
Browse files Browse the repository at this point in the history
* Start FM-02

* Apply main background color to entire app

* Don't override existing flow with default properties in flow.logic

* Update styles of tab-manager and add new tab button that creates flow

* Finish FM-02
  • Loading branch information
JoshuaCWebDeveloper authored Apr 30, 2024
1 parent 99fc258 commit 2101c67
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 54 deletions.
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,28 @@ The backlog is organized by epic, with each task having a unique ID, description
- Implement a dynamic tab system at the top of the interface where each tab represents an open flow.
- Ensure that each tab can host an instance of the flow-canvas component, maintaining the state of each flow independently.
- Provide functionality to add, close, and switch tabs without losing work, with prompt saving or caching options.
- Integrate with the backend to load and save flow states as users switch between them, ensuring data consistency and integrity.
- Load and save flow states as users switch between them, ensuring data consistency and integrity.
- **Implementation Details**:
1. **Dynamic Tab Creation**:
- Modify the `openFlow` action to ensure a new tab is created when a new flow is opened. This might involve checking for duplicates and only adding unique flow IDs.
- Ensure the UI updates to reflect the addition of new tabs dynamically.
2. **Enhanced Tab Switching**:
- Improve the `switchTab` function to handle large numbers of tabs efficiently, possibly including optimizations for re-rendering only the necessary components.
3. **Tab Persistence and State Management**:
- Ensure that each tab maintains its state independently when switching between tabs. This involves careful management of the flow state in Redux to prevent cross-contamination between different flow states.
4. **Close Tab Functionality**:
- Enhance the `closeTab` function to handle edge cases such as closing the last tab, or the first tab, and setting a new active tab appropriately.
- Implement UI feedback for tab closing, such as confirmation dialogs if unsaved changes exist.
5. **UI Enhancements**:
- Implement visual indicators for unsaved changes or errors within each tab.
- Consider adding features like reordering tabs via drag-and-drop if not already supported.
- **FM-03**: Implement File Tree in Primary Sidebar
- **Description**: Create a file tree section within the primary sidebar to list and manage all available flows.
- **Priority**: High
- **Technical Requirements**:
- Develop a collapsible file tree that displays all available flows categorized by projects or folders.
- Allow users to interact with the file tree to open a flow in a new tab, delete a flow, or create a new flow.
- Integrate file tree actions with the backend to reflect changes in real-time, ensuring that the file tree is always up-to-date with the latest files and folders.
- Integrate file tree actions with state to reflect changes in real-time, ensuring that the file tree is always up-to-date with the latest files and folders.
- Implement search functionality within the file tree to allow users to quickly find specific flows.

#### Epic: Subflows
Expand Down Expand Up @@ -276,8 +290,7 @@ The backlog is organized by epic, with each task having a unique ID, description
| To Do | In Progress | In Review | Done |
| ----- | ----------- | --------- | ----- |
| | | | FM-01 |
| FM-02 | | | |
| FM-03 | | | |
| FM-03 | | | FM-02 |

### Progress Tracking

Expand Down
1 change: 1 addition & 0 deletions packages/flow-client/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import themes from './themes';
// StyledApp defines the main application container styles.
// It ensures the flow canvas takes up the full viewport height for better visibility.
const StyledApp = styled.div`
background-color: var(--color-background-main);
height: 100vh; // Full viewport height
header {
Expand Down
13 changes: 8 additions & 5 deletions packages/flow-client/src/app/components/builder/builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,26 @@ const StyledBuilder = styled.div`
display: flex;
flex-direction: row;
position: relative;
overflow: hidden;
height: calc(100% - 60px);
width: 100%;
.primary-sidebar {
border-right-style: solid;
width: 150px;
flex: 0 0 150px;
}
.center {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.tab-list {
border-top-style: solid;
flex: 0 0 40px;
.tab-container {
flex: 0 0 35px;
min-height: 0;
}
.console-panel {
Expand All @@ -42,7 +45,7 @@ const StyledBuilder = styled.div`
.secondary-sidebar {
border-left-style: solid;
width: 200px;
flex: 0 0 200px;
}
`;

Expand Down
181 changes: 139 additions & 42 deletions packages/flow-client/src/app/components/builder/tab-manager.tsx
Original file line number Diff line number Diff line change
@@ -1,88 +1,185 @@
import React from 'react';
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { FlowCanvas } from '../flow-canvas/flow-canvas';
import { v4 as uuidv4 } from 'uuid';

import { useAppSelector } from '../../redux/hooks';
import {
builderActions,
selectOpenFlows,
selectActiveFlow,
selectOpenFlows,
} from '../../redux/modules/builder/builder.slice';
import { flowActions, selectFlows } from '../../redux/modules/flow/flow.slice';
import { FlowCanvas } from '../flow-canvas/flow-canvas';

// Styled components for the tab manager
const StyledTabManager = styled.div`
flex-grow: 1;
display: flex;
flex-direction: column;
text-wrap: nowrap;
.tab-container {
background-color: var(--color-background-element-light);
display: flex;
overflow: hidden;
}
.tab-content {
height: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: var(--color-background-element-medium)
var(--color-background-element-light);
}
.tab-list {
display: flex;
background-color: var(--color-background-element-light);
border: 1px none var(--color-border-light);
color: var(--color-text-sharp);
padding: 0.5rem;
font-size: 0.8em;
padding: 0;
height: 100%;
}
.tab-item {
margin-right: 0.5rem;
flex: 0;
margin-right: 1rem;
cursor: pointer;
padding: 0.5rem;
padding: 0.5rem 1rem;
display: flex;
align-items: center;
&:hover {
background-color: #555;
}
margin: 0;
border-radius: 0;
text-wrap: nowrap;
transition: background-color 0.3s;
.close-btn {
margin-left: 10px;
color: red;
color: var(--color-danger);
cursor: pointer;
display: inline-block;
line-height: 0;
padding: 0;
border-radius: 2px;
visibility: hidden;
width: 20px;
height: 20px;
i {
padding: 3px 5px;
width: 100%;
height: 100%;
}
&:hover {
background-color: var(--color-background-element-medium);
}
}
&:hover .close-btn,
&.active-tab .close-btn {
visibility: visible;
}
&:hover {
border-bottom: 1px solid var(--color-border-light);
}
&.active-tab {
background-color: var(--color-background-main);
border-top: 2px solid var(--color-border-sharp);
}
}
.active-tab {
background-color: #777;
.new-tab {
border: 0;
color: var(--color-text-sharp);
cursor: pointer;
padding: 0.5rem 1rem;
display: flex;
align-items: center;
margin: 0;
border-radius: 0;
background-color: var(--color-background-element-light);
&:hover {
/* background-color: var(--color-background-element-medium); */
border-bottom: 1px solid var(--color-border-light);
/* border: 0; */
}
&:active {
background-color: var(--color-background-element-medium);
}
}
`;

export const TabManager = () => {
const dispatch = useDispatch();
const openFlows = useSelector(selectOpenFlows);
const activeFlow = useSelector(selectActiveFlow);
const flows = useAppSelector(selectFlows);
const openFlows = useAppSelector(selectOpenFlows);
const activeFlow = useAppSelector(selectActiveFlow);
const [flowCounter, setFlowCounter] = useState(0);

// Function to switch the active tab
const switchTab = (tabId: string) => {
dispatch(builderActions.setActiveFlow(tabId));
};

// Function to close a tab
const closeTab = (tabId: string) => {
dispatch(builderActions.closeFlow(tabId));
};

const createNewTab = () => {
const flowId = uuidv4();
dispatch(
flowActions.addEntity({
id: flowId,
type: 'tab',
label: `New Flow${flowCounter ? ` ${flowCounter}` : ''}`,
disabled: false,
info: '',
env: [],
})
);
dispatch(builderActions.openFlow(flowId));
setFlowCounter(flowCounter + 1);
};

return (
<StyledTabManager className="tab-manager">
<div className="tab-list">
{openFlows.map(tabId => (
<div
key={tabId}
className={`tab-item ${
tabId === activeFlow ? 'active-tab' : ''
}`}
onClick={() => switchTab(tabId)}
>
{tabId}
<span
className="close-btn"
onClick={e => {
e.stopPropagation(); // Prevent tab switch when closing
closeTab(tabId);
}}
>
&times;
</span>
<StyledTabManager>
<div className="tab-container">
<div className="tab-content">
<div className="tab-list">
{openFlows.map(flowId => (
<div
key={flowId}
className={`tab-item ${
flowId === activeFlow ? 'active-tab' : ''
}`}
onClick={() => switchTab(flowId)}
>
<p>
{flows.find(flow => flow.id === flowId)
?.label ?? '...'}
</p>
<span
className="close-btn"
onClick={e => {
e.stopPropagation(); // Prevent tab switch when closing
closeTab(flowId);
}}
>
<i className="fa fa-times"></i>
</span>
</div>
))}
</div>
))}
</div>

<button className="new-tab" onClick={createNewTab}>
<i className="fa fa-plus"></i>
</button>
</div>

<FlowCanvas key={activeFlow} flowId={activeFlow ?? undefined} />
Expand Down
41 changes: 41 additions & 0 deletions packages/flow-client/src/app/redux/modules/flow/flow.logic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,47 @@ describe('flow.logic', () => {
);
});

it('should not override existing flow properties with new ones', async () => {
const existingFlow = {
id: 'flow1',
label: 'Existing Flow Label',
type: 'tab',
extras: { detail: 'Existing details' },
disabled: false,
info: '',
env: [],
} as FlowEntity;

const serializedGraph = {
id: 'flow1',
extras: { detail: 'New details' },
layers: [],
offsetX: 0,
offsetY: 0,
zoom: 100,
gridSize: 20,
locked: false,
selected: false,
} as SerializedGraph;

mockedSelectEntityById.mockReturnValue(existingFlow);

await flowLogic.updateFlowFromSerializedGraph(serializedGraph)(
mockDispatch,
mockGetState
);

expect(mockDispatch).toHaveBeenCalledWith(
flowActions.upsertEntity(
expect.objectContaining({
id: 'flow1',
label: 'Existing Flow Label', // Ensure the label is not overridden
extras: { detail: 'Existing details' }, // Ensure extras are not overridden
})
)
);
});

it('correctly creates a node from a serialized graph', async () => {
const serializedGraph = {
id: 'flow1',
Expand Down
4 changes: 2 additions & 2 deletions packages/flow-client/src/app/redux/modules/flow/flow.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ export class FlowLogic {
)
);

// Create a flow entity to represent the graph
const flowEntity: FlowEntity = {
// get existing flow entity or create new one
const flowEntity = selectEntityById(getState(), graph.id) ?? {
id: graph.id,
type: 'tab',
label: 'My Flow', // Example label, could be dynamic
Expand Down
3 changes: 2 additions & 1 deletion packages/flow-client/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ const GlobalStyle = createGlobalStyle`
body, html, #root {
margin: 0;
padding: 0;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
height: 100%;
width: 100%;
}
* {
Expand Down

0 comments on commit 2101c67

Please sign in to comment.