diff --git a/README.md b/README.md index eebdd1a..9a413b5 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/packages/flow-client/src/app/app.tsx b/packages/flow-client/src/app/app.tsx index e0b5c00..979d2c3 100644 --- a/packages/flow-client/src/app/app.tsx +++ b/packages/flow-client/src/app/app.tsx @@ -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 { diff --git a/packages/flow-client/src/app/components/builder/builder.tsx b/packages/flow-client/src/app/components/builder/builder.tsx index 29be021..64e52cc 100644 --- a/packages/flow-client/src/app/components/builder/builder.tsx +++ b/packages/flow-client/src/app/components/builder/builder.tsx @@ -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 { @@ -42,7 +45,7 @@ const StyledBuilder = styled.div` .secondary-sidebar { border-left-style: solid; - width: 200px; + flex: 0 0 200px; } `; diff --git a/packages/flow-client/src/app/components/builder/tab-manager.tsx b/packages/flow-client/src/app/components/builder/tab-manager.tsx index 56391da..dd19918 100644 --- a/packages/flow-client/src/app/components/builder/tab-manager.tsx +++ b/packages/flow-client/src/app/components/builder/tab-manager.tsx @@ -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 ( - -
- {openFlows.map(tabId => ( -
switchTab(tabId)} - > - {tabId} - { - e.stopPropagation(); // Prevent tab switch when closing - closeTab(tabId); - }} - > - × - + +
+
+
+ {openFlows.map(flowId => ( +
switchTab(flowId)} + > +

+ {flows.find(flow => flow.id === flowId) + ?.label ?? '...'} +

+ { + e.stopPropagation(); // Prevent tab switch when closing + closeTab(flowId); + }} + > + + +
+ ))}
- ))} +
+ +
diff --git a/packages/flow-client/src/app/redux/modules/flow/flow.logic.spec.ts b/packages/flow-client/src/app/redux/modules/flow/flow.logic.spec.ts index 37384ff..8f3763b 100644 --- a/packages/flow-client/src/app/redux/modules/flow/flow.logic.spec.ts +++ b/packages/flow-client/src/app/redux/modules/flow/flow.logic.spec.ts @@ -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', diff --git a/packages/flow-client/src/app/redux/modules/flow/flow.logic.ts b/packages/flow-client/src/app/redux/modules/flow/flow.logic.ts index 8365b20..f163480 100644 --- a/packages/flow-client/src/app/redux/modules/flow/flow.logic.ts +++ b/packages/flow-client/src/app/redux/modules/flow/flow.logic.ts @@ -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 diff --git a/packages/flow-client/src/main.tsx b/packages/flow-client/src/main.tsx index ad2d1af..f402f77 100644 --- a/packages/flow-client/src/main.tsx +++ b/packages/flow-client/src/main.tsx @@ -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%; } * {