From cc7d8a75c0eb832fdbd4f5be2b039b7b1f0029e5 Mon Sep 17 00:00:00 2001 From: alexk111 Date: Tue, 10 Jun 2025 19:53:39 +0400 Subject: [PATCH 1/9] Init state & menu for TopBar & PrjSwitcherPos --- src/common/base/menu.ts | 1 + .../useCases/appMenu/initAppMenu.ts | 77 ++++++++++++++++--- .../setProjectSwitcherPosition.ts | 33 ++++++++ .../application/useCases/toggleTopBar.ts | 30 ++++++++ src/renderer/base/state/ui.ts | 14 +++- src/renderer/init.ts | 6 ++ .../useCases/appMenu/initAppMenu.spec.ts | 28 ++++--- .../setProjectSwitcherPosition.spec.ts | 53 +++++++++++++ .../application/useCases/toggleTopBar.spec.ts | 49 ++++++++++++ tests/renderer/base/state/app.spec.ts | 2 + .../renderer/base/state/fixtures/appState.ts | 3 + .../base/state/fixtures/projectSwitcher.ts | 5 +- .../topBar/projectSwitcher.spec.tsx | 37 ++++----- .../ui/components/topBar/topBar.spec.tsx | 25 +++--- 14 files changed, 308 insertions(+), 55 deletions(-) create mode 100644 src/renderer/application/useCases/projectSwitcher/setProjectSwitcherPosition.ts create mode 100644 src/renderer/application/useCases/toggleTopBar.ts create mode 100644 tests/renderer/application/useCases/projectSwitcher/setProjectSwitcherPosition.spec.ts create mode 100644 tests/renderer/application/useCases/toggleTopBar.spec.ts diff --git a/src/common/base/menu.ts b/src/common/base/menu.ts index 6749243..495eb80 100644 --- a/src/common/base/menu.ts +++ b/src/common/base/menu.ts @@ -14,6 +14,7 @@ export interface MenuItemCommon> { readonly label?: string; readonly role?: MenuItemRole; readonly type?: MenuItemType; + readonly checked?: boolean; readonly icon?: string; readonly enabled?: boolean; readonly submenu?: ReadonlyArray; diff --git a/src/renderer/application/useCases/appMenu/initAppMenu.ts b/src/renderer/application/useCases/appMenu/initAppMenu.ts index 538ec35..0e2104b 100644 --- a/src/renderer/application/useCases/appMenu/initAppMenu.ts +++ b/src/renderer/application/useCases/appMenu/initAppMenu.ts @@ -14,6 +14,9 @@ import { OpenAboutUseCase } from '@/application/useCases/about/openAbout'; import { ShellProvider } from '@/application/interfaces/shellProvider'; import { OpenProjectManagerUseCase } from '@/application/useCases/projectManager/openProjectManager'; import { OpenAppManagerUseCase } from '@/application/useCases/appManager/openAppManager'; +import { ProjectSwitcherPos } from '@/base/state/ui'; +import { ToggleTopBarUseCase } from '@/application/useCases/toggleTopBar'; +import { SetProjectSwitcherPositionUseCase } from '@/application/useCases/projectSwitcher/setProjectSwitcherPosition'; const urlDownload = 'https://freeter.io/v2/download'; const urlTwitter = 'https://twitter.com/FreeterApp'; @@ -28,6 +31,8 @@ type Deps = { shellProvider: ShellProvider; toggleEditModeUseCase: ToggleEditModeUseCase; toggleMenuBarUseCase: ToggleMenuBarUseCase; + toggleTopBarUseCase: ToggleTopBarUseCase; + setProjectSwitcherPositionUseCase: SetProjectSwitcherPositionUseCase; openApplicationSettingsUseCase: OpenApplicationSettingsUseCase; openAboutUseCase: OpenAboutUseCase; openProjectManagerUseCase: OpenProjectManagerUseCase; @@ -42,6 +47,8 @@ export function createInitAppMenuUseCase({ shellProvider, toggleEditModeUseCase, toggleMenuBarUseCase, + toggleTopBarUseCase, + setProjectSwitcherPositionUseCase, openApplicationSettingsUseCase, openAboutUseCase, openProjectManagerUseCase, @@ -118,17 +125,61 @@ export function createInitAppMenuUseCase({ ] } - const createMenuView: (editMode: boolean, menuBar: boolean) => MenuItem = (editMode, menuBar) => ({ + const createMenuView: ( + editMode: boolean, + menuBar: boolean, + topBar: boolean, + prjSwitcherPos: ProjectSwitcherPos, + ) => MenuItem = (editMode, menuBar, topBar, prjSwitcherPos) => ({ label: '&View', submenu: [ - { role: 'togglefullscreen' }, - ...(!isMac - ? [{ - label: menuBar ? 'Hide Menu Bar (ALT to restore)' : 'Show Menu Bar', - doAction: async () => toggleMenuBarUseCase() - }] - : [] - ), + { + label: 'Appearance', + submenu: [ + { role: 'togglefullscreen' }, + ...(!isMac + ? [{ + label: menuBar ? 'Hide Menu Bar (ALT to restore)' : 'Show Menu Bar', + doAction: async () => toggleMenuBarUseCase() + }] + : [] + ), + { + label: topBar ? 'Hide Top Bar' : 'Show Top Bar', + doAction: async () => toggleTopBarUseCase() + }, + itemSeparator, + { + label: 'Project Switcher Position', + submenu: [ + { + label: 'On Top Bar', + type: 'radio', + checked: prjSwitcherPos === ProjectSwitcherPos.TopBar, + doAction: async () => setProjectSwitcherPositionUseCase(ProjectSwitcherPos.TopBar) + }, + { + label: 'On Tab Bar (Left)', + type: 'radio', + checked: prjSwitcherPos === ProjectSwitcherPos.TabBarLeft, + doAction: async () => setProjectSwitcherPositionUseCase(ProjectSwitcherPos.TabBarLeft) + }, + { + label: 'On Tab Bar (Right)', + type: 'radio', + checked: prjSwitcherPos === ProjectSwitcherPos.TabBarRight, + doAction: async () => setProjectSwitcherPositionUseCase(ProjectSwitcherPos.TabBarRight) + }, + { + label: 'Hidden', + type: 'radio', + checked: prjSwitcherPos === ProjectSwitcherPos.Hidden, + doAction: async () => setProjectSwitcherPositionUseCase(ProjectSwitcherPos.Hidden) + }, + ] + }, + ] + }, itemSeparator, { accelerator: 'CmdOrCtrl+E', @@ -210,16 +261,20 @@ export function createInitAppMenuUseCase({ isLoading: state.isLoading, editMode: state.ui.editMode, menuBar: state.ui.menuBar, + topBar: state.ui.topBar, + prjSwitcherPos: state.ui.projectSwitcher.pos }), ({ isLoading, editMode, - menuBar + menuBar, + topBar, + prjSwitcherPos }) => { if (!isLoading) { appMenu.setMenu([ (isMac ? menuApp : menuFile), menuEdit, - createMenuView(editMode, menuBar), + createMenuView(editMode, menuBar, topBar, prjSwitcherPos), menuHelp, ...(isDevMode ? [menuDev] diff --git a/src/renderer/application/useCases/projectSwitcher/setProjectSwitcherPosition.ts b/src/renderer/application/useCases/projectSwitcher/setProjectSwitcherPosition.ts new file mode 100644 index 0000000..a20ad33 --- /dev/null +++ b/src/renderer/application/useCases/projectSwitcher/setProjectSwitcherPosition.ts @@ -0,0 +1,33 @@ +/* + * Copyright: (c) 2024, Alex Kaul + * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + */ + +import { AppStore } from '@/application/interfaces/store'; +import { ProjectSwitcherPos } from '@/base/state/ui'; + +type Deps = { + appStore: AppStore; +} + +export function createSetProjectSwitcherPositionUseCase({ + appStore +}: Deps) { + const useCase = (newPos: ProjectSwitcherPos) => { + const state = appStore.get(); + appStore.set({ + ...state, + ui: { + ...state.ui, + projectSwitcher: { + ...state.ui.projectSwitcher, + pos: newPos + } + } + }) + } + + return useCase; +} + +export type SetProjectSwitcherPositionUseCase = ReturnType; diff --git a/src/renderer/application/useCases/toggleTopBar.ts b/src/renderer/application/useCases/toggleTopBar.ts new file mode 100644 index 0000000..e9d9ef2 --- /dev/null +++ b/src/renderer/application/useCases/toggleTopBar.ts @@ -0,0 +1,30 @@ +/* + * Copyright: (c) 2024, Alex Kaul + * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + */ + +import { AppStore } from '@/application/interfaces/store'; + +type Deps = { + appStore: AppStore; +} + +export function createToggleTopBarUseCase({ + appStore +}: Deps) { + const useCase = () => { + const state = appStore.get(); + const { topBar } = state.ui; + appStore.set({ + ...state, + ui: { + ...state.ui, + topBar: !topBar + } + }) + } + + return useCase; +} + +export type ToggleTopBarUseCase = ReturnType; diff --git a/src/renderer/base/state/ui.ts b/src/renderer/base/state/ui.ts index 66c53d5..c12f822 100644 --- a/src/renderer/base/state/ui.ts +++ b/src/renderer/base/state/ui.ts @@ -88,9 +88,17 @@ export interface ProjectManagerState { duplicateProjectIds: Record | null; } +export enum ProjectSwitcherPos { + Hidden = 0, + TopBar = 1, + TabBarLeft = 2, + TabBarRight = 3, +} + export interface ProjectSwitcherState { projectIds: EntityIdList; currentProjectId: EntityId; + pos: ProjectSwitcherPos; } export interface AppManagerState { @@ -190,9 +198,11 @@ export interface MemSaverState { workflowTimeouts: Record; } + export interface UiState { editMode: boolean; menuBar: boolean; + topBar: boolean; appConfig: AppConfig; apps: AppsState; dragDrop: DragDropState; @@ -210,6 +220,7 @@ export function createUiState(): UiState { dragDrop: {}, editMode: false, menuBar: true, + topBar: false, appConfig: { mainHotkey: 'CmdOrCtrl+Shift+F', memSaver: { @@ -267,7 +278,8 @@ export function createUiState(): UiState { }, projectSwitcher: { currentProjectId: '', - projectIds: [] + projectIds: [], + pos: ProjectSwitcherPos.TabBarLeft, }, shelf: { widgetList: [] diff --git a/src/renderer/init.ts b/src/renderer/init.ts index 6c21aa9..b78bd75 100644 --- a/src/renderer/init.ts +++ b/src/renderer/init.ts @@ -140,6 +140,8 @@ import { createOpenAppManagerUseCase } from '@/application/useCases/appManager/o import { createShowOpenFileDialogUseCase } from '@/application/useCases/dialog/showOpenFileDialog'; import { createInitMemSaverUseCase } from '@/application/useCases/memSaver/initMemSaver'; import { createDeactivateWorkflowUseCase } from '@/application/useCases/memSaver/deactivateWorkflow'; +import { createToggleTopBarUseCase } from '@/application/useCases/toggleTopBar'; +import { createSetProjectSwitcherPositionUseCase } from '@/application/useCases/projectSwitcher/setProjectSwitcherPosition'; function prepareDataStorageForRenderer(dataStorage: DataStorage): DataStorageRenderer { return setTextOnlyIfChanged(withJson(dataStorage)); @@ -226,6 +228,8 @@ async function createUseCases(store: ReturnType) { const toggleEditModeUseCase = createToggleEditModeUseCase(deps); const toggleMenuBarUseCase = createToggleMenuBarUseCase(deps); + const toggleTopBarUseCase = createToggleTopBarUseCase(deps); + const setProjectSwitcherPositionUseCase = createSetProjectSwitcherPositionUseCase(deps); const clickContextMenuItemUseCase = createClickContextMenuItemUseCase(); const osContextMenuProvider = createOsContextMenuProvider({ clickContextMenuItemUseCase }); @@ -327,6 +331,8 @@ async function createUseCases(store: ReturnType) { shellProvider, toggleEditModeUseCase, toggleMenuBarUseCase, + toggleTopBarUseCase, + setProjectSwitcherPositionUseCase, openApplicationSettingsUseCase, openAboutUseCase, openAppManagerUseCase, diff --git a/tests/renderer/application/useCases/appMenu/initAppMenu.spec.ts b/tests/renderer/application/useCases/appMenu/initAppMenu.spec.ts index 483c39e..88bcdd3 100644 --- a/tests/renderer/application/useCases/appMenu/initAppMenu.spec.ts +++ b/tests/renderer/application/useCases/appMenu/initAppMenu.spec.ts @@ -29,6 +29,8 @@ async function setup(initState: AppState, opts?: { }) const toggleEditModeUseCase = jest.fn(); const toggleMenuBarUseCase = jest.fn(); + const toggleTopBarUseCase = jest.fn(); + const setProjectSwitcherPositionUseCase = jest.fn(); const openApplicationSettingsUseCase = jest.fn(); const openAboutUseCase = jest.fn(); const openAppManagerUseCase = jest.fn(); @@ -40,10 +42,12 @@ async function setup(initState: AppState, opts?: { shellProvider: shellProviderMock, toggleEditModeUseCase, toggleMenuBarUseCase, + toggleTopBarUseCase, + setProjectSwitcherPositionUseCase, openApplicationSettingsUseCase, openAboutUseCase, openAppManagerUseCase, - openProjectManagerUseCase + openProjectManagerUseCase, }); return { appStore, @@ -52,6 +56,8 @@ async function setup(initState: AppState, opts?: { initAppMenuUseCase, toggleEditModeUseCase, toggleMenuBarUseCase, + toggleTopBarUseCase, + setProjectSwitcherPositionUseCase, openAboutUseCase, } } @@ -65,7 +71,7 @@ describe('initAppMenuUseCase()', () => { initAppMenuUseCase(); - expect(appMenuProviderMock.setAutoHide).toBeCalledTimes(1); + expect(appMenuProviderMock.setAutoHide).toHaveBeenCalledTimes(1); }); it('should subscribe to state changes and call appMenu\'s setAutoHide() with right params, when the menuBar state value changes', async () => { @@ -86,7 +92,7 @@ describe('initAppMenuUseCase()', () => { } }) - expect(appMenuProviderMock.setAutoHide).toBeCalledTimes(2); + expect(appMenuProviderMock.setAutoHide).toHaveBeenCalledTimes(2); expect(appMenuProviderMock.setAutoHide).toHaveBeenNthCalledWith(2, false); appStore.set({ @@ -118,7 +124,7 @@ describe('initAppMenuUseCase()', () => { } }) - expect(appMenuProviderMock.setAutoHide).toBeCalledTimes(1); + expect(appMenuProviderMock.setAutoHide).toHaveBeenCalledTimes(1); }); it('should not call appMenu\'s setAutoHide(), when the new state has isLoading=true', async () => { @@ -133,7 +139,7 @@ describe('initAppMenuUseCase()', () => { appStore.set({ ...state, isLoading: true, ui: { ...state.ui, menuBar: false } }) - expect(appMenuProviderMock.setAutoHide).toBeCalledTimes(1); + expect(appMenuProviderMock.setAutoHide).toHaveBeenCalledTimes(1); }); it('should call appMenu\'s setAutoHide(), when the new state has isLoading=undefined', async () => { @@ -148,7 +154,7 @@ describe('initAppMenuUseCase()', () => { appStore.set({ ...state, ui: { ...state.ui, menuBar: false } }) - expect(appMenuProviderMock.setAutoHide).toBeCalledTimes(2); + expect(appMenuProviderMock.setAutoHide).toHaveBeenCalledTimes(2); }); it('should call appMenu\'s setMenu() right after call', async () => { @@ -159,7 +165,7 @@ describe('initAppMenuUseCase()', () => { initAppMenuUseCase(); - expect(appMenuProviderMock.setMenu).toBeCalledTimes(1); + expect(appMenuProviderMock.setMenu).toHaveBeenCalledTimes(1); }); it('should subscribe to state changes and call appMenu\'s setMenu(), when the new state has changes the app menu depends on', async () => { @@ -180,7 +186,7 @@ describe('initAppMenuUseCase()', () => { } }) - expect(appMenuProviderMock.setMenu).toBeCalledTimes(2); + expect(appMenuProviderMock.setMenu).toHaveBeenCalledTimes(2); }); it('should not call appMenu\'s setMenu(), when the new state does not have changes the app menu depends on', async () => { @@ -197,7 +203,7 @@ describe('initAppMenuUseCase()', () => { ...state, }) - expect(appMenuProviderMock.setMenu).toBeCalledTimes(1); + expect(appMenuProviderMock.setMenu).toHaveBeenCalledTimes(1); }); it('should not call appMenu\'s setMenu(), when the new state has isLoading=true', async () => { @@ -212,7 +218,7 @@ describe('initAppMenuUseCase()', () => { appStore.set({ ...state, isLoading: true, ui: { ...state.ui, editMode: false } }) - expect(appMenuProviderMock.setMenu).toBeCalledTimes(1); + expect(appMenuProviderMock.setMenu).toHaveBeenCalledTimes(1); }); it('should call appMenu\'s setMenu(), when the new state has isLoading=undefined', async () => { @@ -227,6 +233,6 @@ describe('initAppMenuUseCase()', () => { appStore.set({ ...state, ui: { ...state.ui, editMode: false } }) - expect(appMenuProviderMock.setMenu).toBeCalledTimes(2); + expect(appMenuProviderMock.setMenu).toHaveBeenCalledTimes(2); }); }) diff --git a/tests/renderer/application/useCases/projectSwitcher/setProjectSwitcherPosition.spec.ts b/tests/renderer/application/useCases/projectSwitcher/setProjectSwitcherPosition.spec.ts new file mode 100644 index 0000000..d140741 --- /dev/null +++ b/tests/renderer/application/useCases/projectSwitcher/setProjectSwitcherPosition.spec.ts @@ -0,0 +1,53 @@ +/* + * Copyright: (c) 2024, Alex Kaul + * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + */ + +import { createSetProjectSwitcherPositionUseCase } from '@/application/useCases/projectSwitcher/setProjectSwitcherPosition'; +import { AppState } from '@/base/state/app'; +import { ProjectSwitcherPos } from '@/base/state/ui'; +import { fixtureAppState } from '@tests/base/state/fixtures/appState'; +import { fixtureProjectSwitcher } from '@tests/base/state/fixtures/projectSwitcher'; +import { fixtureAppStore } from '@tests/data/fixtures/appStore'; + +async function setup(initState: AppState) { + const [appStore] = await fixtureAppStore(initState); + const setProjectSwitcherPosition = createSetProjectSwitcherPositionUseCase({ + appStore + }); + return { + appStore, + setProjectSwitcherPosition + } +} + +describe('setProjectSwitcherPositionUseCase()', () => { + it('should set new pos state', async () => { + const initState = fixtureAppState({ + ui: { + projectSwitcher: fixtureProjectSwitcher({ + pos: ProjectSwitcherPos.TabBarLeft + }) + } + }); + const newPos = ProjectSwitcherPos.TopBar + const expectState: AppState = { + ...initState, + ui: { + ...initState.ui, + projectSwitcher: { + ...initState.ui.projectSwitcher, + pos: newPos + } + } + } + const { + appStore, + setProjectSwitcherPosition + } = await setup(initState) + + setProjectSwitcherPosition(newPos); + + expect(appStore.get()).toEqual(expectState); + }) +}) diff --git a/tests/renderer/application/useCases/toggleTopBar.spec.ts b/tests/renderer/application/useCases/toggleTopBar.spec.ts new file mode 100644 index 0000000..a1107cc --- /dev/null +++ b/tests/renderer/application/useCases/toggleTopBar.spec.ts @@ -0,0 +1,49 @@ +/* + * Copyright: (c) 2024, Alex Kaul + * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + */ + +import { createToggleTopBarUseCase } from '@/application/useCases/toggleTopBar'; +import { AppState } from '@/base/state/app'; +import { fixtureAppState } from '@tests/base/state/fixtures/appState'; +import { fixtureAppStore } from '@tests/data/fixtures/appStore'; + +async function setup(initState: AppState) { + const [appStore] = await fixtureAppStore(initState); + const toggleTopBarUseCase = createToggleTopBarUseCase({ + appStore + }); + return { + appStore, + toggleTopBarUseCase + } +} + +describe('toggleTopBarUseCase()', () => { + it('should toggle topBar state', async () => { + const initState = fixtureAppState({ + ui: { + topBar: false + } + }); + const expectState: AppState = { + ...initState, + ui: { + ...initState.ui, + topBar: true + } + } + const { + appStore, + toggleTopBarUseCase + } = await setup(initState) + + toggleTopBarUseCase(); + + expect(appStore.get()).toEqual(expectState); + + toggleTopBarUseCase(); + + expect(appStore.get()).toEqual(initState); + }) +}) diff --git a/tests/renderer/base/state/app.spec.ts b/tests/renderer/base/state/app.spec.ts index 362c77d..1a994cc 100644 --- a/tests/renderer/base/state/app.spec.ts +++ b/tests/renderer/base/state/app.spec.ts @@ -122,6 +122,7 @@ describe('AppState', () => { appConfig: state.ui.appConfig, apps: state.ui.apps, menuBar: state.ui.menuBar, + topBar: state.ui.topBar, projectSwitcher: state.ui.projectSwitcher, shelf: state.ui.shelf } @@ -166,6 +167,7 @@ describe('AppState', () => { appConfig: fixtureAppConfig({ mainHotkey: 'Accelerator' }), apps: fixtureApps({ appIds: ['APP1', 'APP2'] }), menuBar: false, + topBar: true, projectSwitcher: fixtureProjectSwitcher({ currentProjectId: 'B1', projectIds: ['B1', 'B2'] }), shelf: fixtureShelf({ widgetList: [{ id: 'A1', widgetId: 'A1' }] }) } diff --git a/tests/renderer/base/state/fixtures/appState.ts b/tests/renderer/base/state/fixtures/appState.ts index 1f01ef3..9acda14 100644 --- a/tests/renderer/base/state/fixtures/appState.ts +++ b/tests/renderer/base/state/fixtures/appState.ts @@ -4,6 +4,7 @@ */ import { AppState } from '@/base/state/app'; +import { ProjectSwitcherPos } from '@/base/state/ui'; import { StateInStore } from '@common/application/interfaces/store'; import { deepFreeze } from '@common/helpers/deepFreeze'; import { fixtureEntitiesState } from '@tests/base/state/fixtures/entitiesState'; @@ -14,6 +15,7 @@ const appState: AppState = { dragDrop: {}, editMode: false, menuBar: true, + topBar: true, appConfig: { mainHotkey: '', memSaver: { @@ -55,6 +57,7 @@ const appState: AppState = { projectSwitcher: { currentProjectId: '', projectIds: [], + pos: ProjectSwitcherPos.TabBarLeft }, shelf: { widgetList: [] diff --git a/tests/renderer/base/state/fixtures/projectSwitcher.ts b/tests/renderer/base/state/fixtures/projectSwitcher.ts index 0582d87..792c2d9 100644 --- a/tests/renderer/base/state/fixtures/projectSwitcher.ts +++ b/tests/renderer/base/state/fixtures/projectSwitcher.ts @@ -3,12 +3,13 @@ * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) */ -import { ProjectSwitcherState } from '@/base/state/ui'; +import { ProjectSwitcherPos, ProjectSwitcherState } from '@/base/state/ui'; import { deepFreeze } from '@common/helpers/deepFreeze'; const projectSwitcherState: ProjectSwitcherState = { currentProjectId: 'SOME-ID', - projectIds: [] + projectIds: [], + pos: ProjectSwitcherPos.TabBarLeft } export const fixtureProjectSwitcher = (testData?: Partial): ProjectSwitcherState => deepFreeze({ diff --git a/tests/renderer/ui/components/topBar/projectSwitcher.spec.tsx b/tests/renderer/ui/components/topBar/projectSwitcher.spec.tsx index 86cde14..d7c7967 100644 --- a/tests/renderer/ui/components/topBar/projectSwitcher.spec.tsx +++ b/tests/renderer/ui/components/topBar/projectSwitcher.spec.tsx @@ -11,6 +11,7 @@ import { fireEvent, render, screen, act } from '@testing-library/react'; import { fixtureProjectSettingsA } from '@tests/base/fixtures/project'; import { fixtureAppState } from '@tests/base/state/fixtures/appState'; import { fixtureProjectAInColl, fixtureProjectBInColl } from '@tests/base/state/fixtures/entitiesState'; +import { fixtureProjectSwitcher } from '@tests/base/state/fixtures/projectSwitcher'; import { fixtureAppStore } from '@tests/data/fixtures/appStore'; async function setup( @@ -63,10 +64,10 @@ describe('', () => { } }, ui: { - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ currentProjectId: idA, projectIds: [idA] - } + }) } })) expect(screen.getByRole('combobox')).toBeEnabled(); @@ -84,10 +85,10 @@ describe('', () => { } }, ui: { - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ currentProjectId: idA, projectIds: [idA, idB] - } + }) } })) expect(screen.getAllByRole('option').length).toBe(3); @@ -102,10 +103,10 @@ describe('', () => { } }, ui: { - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ currentProjectId: idA, projectIds: [idA] - } + }) } })) const elOpt = screen.getAllByRole('option')[0]; @@ -123,10 +124,10 @@ describe('', () => { } }, ui: { - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ currentProjectId: '', projectIds: [idA] - } + }) } })) @@ -143,10 +144,10 @@ describe('', () => { } }, ui: { - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ currentProjectId: 'NO-SUCH-ID', projectIds: [idA] - } + }) } })) @@ -163,10 +164,10 @@ describe('', () => { } }, ui: { - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ currentProjectId: idA, projectIds: [idA] - } + }) } })) @@ -184,10 +185,10 @@ describe('', () => { } }, ui: { - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ currentProjectId: idA, projectIds: [idB, idA] - } + }) } })) @@ -208,10 +209,10 @@ describe('', () => { } }, ui: { - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ currentProjectId: idB, projectIds: [idA, idB] - } + }) } }) const {appStore} = await setup(state) @@ -244,10 +245,10 @@ describe('', () => { } }, ui: { - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ currentProjectId: idA, projectIds: [idB, idA] - } + }) } })) diff --git a/tests/renderer/ui/components/topBar/topBar.spec.tsx b/tests/renderer/ui/components/topBar/topBar.spec.tsx index 4185517..675c4da 100644 --- a/tests/renderer/ui/components/topBar/topBar.spec.tsx +++ b/tests/renderer/ui/components/topBar/topBar.spec.tsx @@ -11,6 +11,7 @@ import { fixtureAppStore } from '@tests/data/fixtures/appStore'; import { createAppStateHook } from '@/ui/hooks/appState'; import { fixtureAppState } from '@tests/base/state/fixtures/appState'; import { fixtureProjectAInColl, fixtureWorkflowAInColl } from '@tests/base/state/fixtures/entitiesState'; +import { fixtureProjectSwitcher } from '@tests/base/state/fixtures/projectSwitcher'; const strEditModeToggle = 'Edit Mode Toggle'; const strProjectSwitcher = 'Project Switcher'; @@ -80,10 +81,10 @@ describe('', () => { }, ui: { editMode: true, - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ projectIds: [projectId], currentProjectId: projectId - } + }) } })); expect(screen.getByText(strPalette)).toBeInTheDocument(); @@ -97,10 +98,10 @@ describe('', () => { }, ui: { editMode: true, - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ projectIds: [], currentProjectId: '' - } + }) } })); expect(screen.queryByText(strPalette)).not.toBeInTheDocument(); @@ -115,10 +116,10 @@ describe('', () => { }, ui: { editMode: true, - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ projectIds: [projectId], currentProjectId: projectId - } + }) } })); expect(screen.queryByText(strPalette)).not.toBeInTheDocument(); @@ -132,10 +133,10 @@ describe('', () => { }, ui: { editMode: true, - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ projectIds: ['no such id'], currentProjectId: 'no such id' - } + }) } })); expect(screen.queryByText(strPalette)).not.toBeInTheDocument(); @@ -150,10 +151,10 @@ describe('', () => { }, ui: { editMode: true, - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ projectIds: [projectId], currentProjectId: projectId - } + }) } })); expect(screen.queryByText(strPalette)).not.toBeInTheDocument(); @@ -171,10 +172,10 @@ describe('', () => { }, ui: { editMode: false, - projectSwitcher: { + projectSwitcher: fixtureProjectSwitcher({ projectIds: [projectId], currentProjectId: projectId - } + }) } })); expect(screen.queryByText(strPalette)).not.toBeInTheDocument(); From d48f1c55afa5b48268bfdec814a6bf4dd513c880 Mon Sep 17 00:00:00 2001 From: alexk111 Date: Wed, 11 Jun 2025 14:27:18 +0400 Subject: [PATCH 2/9] Add Edit Mode Toggle Position --- .../useCases/appMenu/initAppMenu.ts | 45 ++++++++++++++++-- .../useCases/setEditTogglePosition.ts | 30 ++++++++++++ src/renderer/base/state/ui.ts | 8 ++++ src/renderer/init.ts | 3 ++ src/renderer/ui/components/app/app.tsx | 2 +- .../useCases/appMenu/initAppMenu.spec.ts | 2 + .../useCases/setEditTogglePosition.spec.ts | 47 +++++++++++++++++++ tests/renderer/base/state/app.spec.ts | 3 ++ .../renderer/base/state/fixtures/appState.ts | 3 +- 9 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 src/renderer/application/useCases/setEditTogglePosition.ts create mode 100644 tests/renderer/application/useCases/setEditTogglePosition.spec.ts diff --git a/src/renderer/application/useCases/appMenu/initAppMenu.ts b/src/renderer/application/useCases/appMenu/initAppMenu.ts index 0e2104b..e77c366 100644 --- a/src/renderer/application/useCases/appMenu/initAppMenu.ts +++ b/src/renderer/application/useCases/appMenu/initAppMenu.ts @@ -14,9 +14,10 @@ import { OpenAboutUseCase } from '@/application/useCases/about/openAbout'; import { ShellProvider } from '@/application/interfaces/shellProvider'; import { OpenProjectManagerUseCase } from '@/application/useCases/projectManager/openProjectManager'; import { OpenAppManagerUseCase } from '@/application/useCases/appManager/openAppManager'; -import { ProjectSwitcherPos } from '@/base/state/ui'; +import { EditTogglePos, ProjectSwitcherPos } from '@/base/state/ui'; import { ToggleTopBarUseCase } from '@/application/useCases/toggleTopBar'; import { SetProjectSwitcherPositionUseCase } from '@/application/useCases/projectSwitcher/setProjectSwitcherPosition'; +import { SetEditTogglePositionUseCase } from '@/application/useCases/setEditTogglePosition'; const urlDownload = 'https://freeter.io/v2/download'; const urlTwitter = 'https://twitter.com/FreeterApp'; @@ -33,6 +34,7 @@ type Deps = { toggleMenuBarUseCase: ToggleMenuBarUseCase; toggleTopBarUseCase: ToggleTopBarUseCase; setProjectSwitcherPositionUseCase: SetProjectSwitcherPositionUseCase; + setEditTogglePositionUseCase: SetEditTogglePositionUseCase; openApplicationSettingsUseCase: OpenApplicationSettingsUseCase; openAboutUseCase: OpenAboutUseCase; openProjectManagerUseCase: OpenProjectManagerUseCase; @@ -49,6 +51,7 @@ export function createInitAppMenuUseCase({ toggleMenuBarUseCase, toggleTopBarUseCase, setProjectSwitcherPositionUseCase, + setEditTogglePositionUseCase, openApplicationSettingsUseCase, openAboutUseCase, openProjectManagerUseCase, @@ -130,7 +133,8 @@ export function createInitAppMenuUseCase({ menuBar: boolean, topBar: boolean, prjSwitcherPos: ProjectSwitcherPos, - ) => MenuItem = (editMode, menuBar, topBar, prjSwitcherPos) => ({ + editTogglePos: EditTogglePos, + ) => MenuItem = (editMode, menuBar, topBar, prjSwitcherPos, editTogglePos) => ({ label: '&View', submenu: [ { @@ -178,6 +182,35 @@ export function createInitAppMenuUseCase({ }, ] }, + { + label: 'Edit Mode Toggle Position', + submenu: [ + { + label: 'On Top Bar', + type: 'radio', + checked: editTogglePos === EditTogglePos.TopBar, + doAction: async () => setEditTogglePositionUseCase(EditTogglePos.TopBar) + }, + { + label: 'On Tab Bar (Left)', + type: 'radio', + checked: editTogglePos === EditTogglePos.TabBarLeft, + doAction: async () => setEditTogglePositionUseCase(EditTogglePos.TabBarLeft) + }, + { + label: 'On Tab Bar (Right)', + type: 'radio', + checked: editTogglePos === EditTogglePos.TabBarRight, + doAction: async () => setEditTogglePositionUseCase(EditTogglePos.TabBarRight) + }, + { + label: 'Hidden', + type: 'radio', + checked: editTogglePos === EditTogglePos.Hidden, + doAction: async () => setEditTogglePositionUseCase(EditTogglePos.Hidden) + }, + ] + }, ] }, itemSeparator, @@ -262,19 +295,21 @@ export function createInitAppMenuUseCase({ editMode: state.ui.editMode, menuBar: state.ui.menuBar, topBar: state.ui.topBar, - prjSwitcherPos: state.ui.projectSwitcher.pos + prjSwitcherPos: state.ui.projectSwitcher.pos, + editTogglePos: state.ui.editTogglePos, }), ({ isLoading, editMode, menuBar, topBar, - prjSwitcherPos + prjSwitcherPos, + editTogglePos }) => { if (!isLoading) { appMenu.setMenu([ (isMac ? menuApp : menuFile), menuEdit, - createMenuView(editMode, menuBar, topBar, prjSwitcherPos), + createMenuView(editMode, menuBar, topBar, prjSwitcherPos, editTogglePos), menuHelp, ...(isDevMode ? [menuDev] diff --git a/src/renderer/application/useCases/setEditTogglePosition.ts b/src/renderer/application/useCases/setEditTogglePosition.ts new file mode 100644 index 0000000..1a1db8b --- /dev/null +++ b/src/renderer/application/useCases/setEditTogglePosition.ts @@ -0,0 +1,30 @@ +/* + * Copyright: (c) 2024, Alex Kaul + * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + */ + +import { AppStore } from '@/application/interfaces/store'; +import { EditTogglePos } from '@/base/state/ui'; + +type Deps = { + appStore: AppStore; +} + +export function createSetEditTogglePositionUseCase({ + appStore +}: Deps) { + const useCase = (newPos: EditTogglePos) => { + const state = appStore.get(); + appStore.set({ + ...state, + ui: { + ...state.ui, + editTogglePos: newPos + } + }) + } + + return useCase; +} + +export type SetEditTogglePositionUseCase = ReturnType; diff --git a/src/renderer/base/state/ui.ts b/src/renderer/base/state/ui.ts index c12f822..b267bc2 100644 --- a/src/renderer/base/state/ui.ts +++ b/src/renderer/base/state/ui.ts @@ -198,11 +198,18 @@ export interface MemSaverState { workflowTimeouts: Record; } +export enum EditTogglePos { + Hidden = 0, + TopBar = 1, + TabBarLeft = 2, + TabBarRight = 3, +} export interface UiState { editMode: boolean; menuBar: boolean; topBar: boolean; + editTogglePos: EditTogglePos; appConfig: AppConfig; apps: AppsState; dragDrop: DragDropState; @@ -221,6 +228,7 @@ export function createUiState(): UiState { editMode: false, menuBar: true, topBar: false, + editTogglePos: EditTogglePos.TabBarRight, appConfig: { mainHotkey: 'CmdOrCtrl+Shift+F', memSaver: { diff --git a/src/renderer/init.ts b/src/renderer/init.ts index b78bd75..2070f7d 100644 --- a/src/renderer/init.ts +++ b/src/renderer/init.ts @@ -142,6 +142,7 @@ import { createInitMemSaverUseCase } from '@/application/useCases/memSaver/initM import { createDeactivateWorkflowUseCase } from '@/application/useCases/memSaver/deactivateWorkflow'; import { createToggleTopBarUseCase } from '@/application/useCases/toggleTopBar'; import { createSetProjectSwitcherPositionUseCase } from '@/application/useCases/projectSwitcher/setProjectSwitcherPosition'; +import { createSetEditTogglePositionUseCase } from '@/application/useCases/setEditTogglePosition'; function prepareDataStorageForRenderer(dataStorage: DataStorage): DataStorageRenderer { return setTextOnlyIfChanged(withJson(dataStorage)); @@ -230,6 +231,7 @@ async function createUseCases(store: ReturnType) { const toggleMenuBarUseCase = createToggleMenuBarUseCase(deps); const toggleTopBarUseCase = createToggleTopBarUseCase(deps); const setProjectSwitcherPositionUseCase = createSetProjectSwitcherPositionUseCase(deps); + const setEditTogglePositionUseCase = createSetEditTogglePositionUseCase(deps); const clickContextMenuItemUseCase = createClickContextMenuItemUseCase(); const osContextMenuProvider = createOsContextMenuProvider({ clickContextMenuItemUseCase }); @@ -333,6 +335,7 @@ async function createUseCases(store: ReturnType) { toggleMenuBarUseCase, toggleTopBarUseCase, setProjectSwitcherPositionUseCase, + setEditTogglePositionUseCase, openApplicationSettingsUseCase, openAboutUseCase, openAppManagerUseCase, diff --git a/src/renderer/ui/components/app/app.tsx b/src/renderer/ui/components/app/app.tsx index fed47f2..0d3bf67 100644 --- a/src/renderer/ui/components/app/app.tsx +++ b/src/renderer/ui/components/app/app.tsx @@ -40,7 +40,7 @@ export function createAppComponent({ : - {'You don\'t have any projects. Use the Manage Projects '} {' button at the Top Bar to create a first one.'} + {'You don\'t have any projects. Use the Manage Projects '} {' button above to create a first one.'} } diff --git a/tests/renderer/application/useCases/appMenu/initAppMenu.spec.ts b/tests/renderer/application/useCases/appMenu/initAppMenu.spec.ts index 88bcdd3..688f6ce 100644 --- a/tests/renderer/application/useCases/appMenu/initAppMenu.spec.ts +++ b/tests/renderer/application/useCases/appMenu/initAppMenu.spec.ts @@ -31,6 +31,7 @@ async function setup(initState: AppState, opts?: { const toggleMenuBarUseCase = jest.fn(); const toggleTopBarUseCase = jest.fn(); const setProjectSwitcherPositionUseCase = jest.fn(); + const setEditTogglePositionUseCase = jest.fn(); const openApplicationSettingsUseCase = jest.fn(); const openAboutUseCase = jest.fn(); const openAppManagerUseCase = jest.fn(); @@ -44,6 +45,7 @@ async function setup(initState: AppState, opts?: { toggleMenuBarUseCase, toggleTopBarUseCase, setProjectSwitcherPositionUseCase, + setEditTogglePositionUseCase, openApplicationSettingsUseCase, openAboutUseCase, openAppManagerUseCase, diff --git a/tests/renderer/application/useCases/setEditTogglePosition.spec.ts b/tests/renderer/application/useCases/setEditTogglePosition.spec.ts new file mode 100644 index 0000000..6e72f76 --- /dev/null +++ b/tests/renderer/application/useCases/setEditTogglePosition.spec.ts @@ -0,0 +1,47 @@ +/* + * Copyright: (c) 2024, Alex Kaul + * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + */ + +import { createSetEditTogglePositionUseCase } from '@/application/useCases/setEditTogglePosition'; +import { AppState } from '@/base/state/app'; +import { EditTogglePos } from '@/base/state/ui'; +import { fixtureAppState } from '@tests/base/state/fixtures/appState'; +import { fixtureAppStore } from '@tests/data/fixtures/appStore'; + +async function setup(initState: AppState) { + const [appStore] = await fixtureAppStore(initState); + const setEditTogglePosition = createSetEditTogglePositionUseCase({ + appStore + }); + return { + appStore, + setEditTogglePosition + } +} + +describe('setEditTogglePositionUseCase()', () => { + it('should set new pos state', async () => { + const initState = fixtureAppState({ + ui: { + editTogglePos: EditTogglePos.TabBarRight + } + }); + const newPos = EditTogglePos.TopBar + const expectState: AppState = { + ...initState, + ui: { + ...initState.ui, + editTogglePos: newPos + } + } + const { + appStore, + setEditTogglePosition + } = await setup(initState) + + setEditTogglePosition(newPos); + + expect(appStore.get()).toEqual(expectState); + }) +}) diff --git a/tests/renderer/base/state/app.spec.ts b/tests/renderer/base/state/app.spec.ts index 1a994cc..a954df8 100644 --- a/tests/renderer/base/state/app.spec.ts +++ b/tests/renderer/base/state/app.spec.ts @@ -12,6 +12,7 @@ import { fixtureAppAInColl, fixtureAppBInColl, fixtureProjectAInColl, fixturePro import { fixtureProjectSwitcher } from '@tests/base/state/fixtures/projectSwitcher' import { fixtureShelf } from '@tests/base/state/fixtures/shelf' import { fixtureApps } from '@tests/base/state/fixtures/apps'; +import { EditTogglePos } from '@/base/state/ui'; type Settings = { prop: string; @@ -123,6 +124,7 @@ describe('AppState', () => { apps: state.ui.apps, menuBar: state.ui.menuBar, topBar: state.ui.topBar, + editTogglePos: state.ui.editTogglePos, projectSwitcher: state.ui.projectSwitcher, shelf: state.ui.shelf } @@ -168,6 +170,7 @@ describe('AppState', () => { apps: fixtureApps({ appIds: ['APP1', 'APP2'] }), menuBar: false, topBar: true, + editTogglePos: EditTogglePos.TabBarRight, projectSwitcher: fixtureProjectSwitcher({ currentProjectId: 'B1', projectIds: ['B1', 'B2'] }), shelf: fixtureShelf({ widgetList: [{ id: 'A1', widgetId: 'A1' }] }) } diff --git a/tests/renderer/base/state/fixtures/appState.ts b/tests/renderer/base/state/fixtures/appState.ts index 9acda14..f063509 100644 --- a/tests/renderer/base/state/fixtures/appState.ts +++ b/tests/renderer/base/state/fixtures/appState.ts @@ -4,7 +4,7 @@ */ import { AppState } from '@/base/state/app'; -import { ProjectSwitcherPos } from '@/base/state/ui'; +import { EditTogglePos, ProjectSwitcherPos } from '@/base/state/ui'; import { StateInStore } from '@common/application/interfaces/store'; import { deepFreeze } from '@common/helpers/deepFreeze'; import { fixtureEntitiesState } from '@tests/base/state/fixtures/entitiesState'; @@ -16,6 +16,7 @@ const appState: AppState = { editMode: false, menuBar: true, topBar: true, + editTogglePos: EditTogglePos.TabBarRight, appConfig: { mainHotkey: '', memSaver: { From 82b8b81a1485342b611d0133ae19a99e5e618ad3 Mon Sep 17 00:00:00 2001 From: alexk111 Date: Wed, 11 Jun 2025 14:32:51 +0400 Subject: [PATCH 3/9] Move Edit Mode from View to Edit --- .../useCases/appMenu/initAppMenu.ts | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/renderer/application/useCases/appMenu/initAppMenu.ts b/src/renderer/application/useCases/appMenu/initAppMenu.ts index e77c366..c284908 100644 --- a/src/renderer/application/useCases/appMenu/initAppMenu.ts +++ b/src/renderer/application/useCases/appMenu/initAppMenu.ts @@ -109,9 +109,17 @@ export function createInitAppMenuUseCase({ ] } - const menuEdit: MenuItem = { + const createMenuEdit: ( + editMode: boolean, + ) => MenuItem = (editMode) => ({ label: '&Edit', submenu: [ + { + accelerator: 'CmdOrCtrl+E', + label: `${editMode ? 'Disable' : 'Enable'} Edit Mode`, + doAction: async () => toggleEditModeUseCase() + }, + itemSeparator, { role: 'undo' }, { @@ -126,15 +134,14 @@ export function createInitAppMenuUseCase({ role: 'selectAll' } ] - } + }) const createMenuView: ( - editMode: boolean, menuBar: boolean, topBar: boolean, prjSwitcherPos: ProjectSwitcherPos, editTogglePos: EditTogglePos, - ) => MenuItem = (editMode, menuBar, topBar, prjSwitcherPos, editTogglePos) => ({ + ) => MenuItem = (menuBar, topBar, prjSwitcherPos, editTogglePos) => ({ label: '&View', submenu: [ { @@ -214,12 +221,6 @@ export function createInitAppMenuUseCase({ ] }, itemSeparator, - { - accelerator: 'CmdOrCtrl+E', - label: `${editMode ? 'Disable' : 'Enable'} Edit Mode`, - doAction: async () => toggleEditModeUseCase() - }, - itemSeparator, { label: 'Manage Projects', doAction: async () => openProjectManagerUseCase() @@ -308,8 +309,8 @@ export function createInitAppMenuUseCase({ if (!isLoading) { appMenu.setMenu([ (isMac ? menuApp : menuFile), - menuEdit, - createMenuView(editMode, menuBar, topBar, prjSwitcherPos, editTogglePos), + createMenuEdit(editMode), + createMenuView(menuBar, topBar, prjSwitcherPos, editTogglePos), menuHelp, ...(isDevMode ? [menuDev] From 7ebe856647cb89858d2647bf3ede31aae76c35b6 Mon Sep 17 00:00:00 2001 From: alexk111 Date: Wed, 11 Jun 2025 16:07:58 +0400 Subject: [PATCH 4/9] Show/Hide Top Bar --- src/renderer/ui/components/app/app.module.scss | 14 ++++++++++---- src/renderer/ui/components/app/app.tsx | 8 ++++---- src/renderer/ui/components/app/appViewModel.ts | 9 ++++++--- .../ui/components/topBar/topBar.module.scss | 5 +---- .../workflowSwitcher/workflowSwitcher.module.scss | 14 +++++--------- .../workflowSwitcher/workflowSwitcher.tsx | 8 ++++---- .../worktable/widgetLayout/widgetLayout.tsx | 4 ++-- .../ui/components/worktable/worktable.module.scss | 7 ++----- src/renderer/ui/components/worktable/worktable.tsx | 4 ++-- 9 files changed, 36 insertions(+), 37 deletions(-) diff --git a/src/renderer/ui/components/app/app.module.scss b/src/renderer/ui/components/app/app.module.scss index 2481fe9..30f49c3 100644 --- a/src/renderer/ui/components/app/app.module.scss +++ b/src/renderer/ui/components/app/app.module.scss @@ -3,13 +3,19 @@ * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) */ -.no-projects { - background-color: var(--freeter-inAppNoteNoProjectsBackground); +.main-screen { + display: flex; + flex-direction: column; position: fixed; - top: 62px; - left: 0; + top: 0; right: 0; bottom: 0; + left: 0; +} + +.no-projects { + background-color: var(--freeter-inAppNoteNoProjectsBackground); + flex-grow: 1; } .manage-icon { diff --git a/src/renderer/ui/components/app/app.tsx b/src/renderer/ui/components/app/app.tsx index 0d3bf67..ad27ee0 100644 --- a/src/renderer/ui/components/app/app.tsx +++ b/src/renderer/ui/components/app/app.tsx @@ -27,12 +27,12 @@ export function createAppComponent({ useAppViewModel }: Deps) { function App() { - const {modalScreens, hasModalScreens, hasProjects, contextMenuHandler, uiThemeId} = useAppViewModel(); + const {modalScreens, hasModalScreens, hasProjects, contextMenuHandler, uiThemeId, hasTopBar} = useAppViewModel(); return (
-
- +
+ {hasTopBar && } { hasProjects ? <> @@ -40,7 +40,7 @@ export function createAppComponent({ : - {'You don\'t have any projects. Use the Manage Projects '} {' button above to create a first one.'} + {'You don\'t have any projects. Use the Manage Projects '} {' button above (or under the View menu) to create a first one.'} }
diff --git a/src/renderer/ui/components/app/appViewModel.ts b/src/renderer/ui/components/app/appViewModel.ts index 848fc39..bfb1dec 100644 --- a/src/renderer/ui/components/app/appViewModel.ts +++ b/src/renderer/ui/components/app/appViewModel.ts @@ -39,7 +39,8 @@ export function createAppViewModelHook({ projects, workflows, modalScreensOrder, - uiTheme + uiTheme, + hasTopBar ] = useAppState(state => [ state.ui.editMode, state.ui.projectSwitcher.projectIds, @@ -47,7 +48,8 @@ export function createAppViewModelHook({ state.entities.projects, state.entities.workflows, state.ui.modalScreens.order, - state.ui.appConfig.uiTheme + state.ui.appConfig.uiTheme, + state.ui.topBar ]) const projectList = useAppState.useEntityList(state => state.entities.projects, projectIds); @@ -92,7 +94,8 @@ export function createAppViewModelHook({ modalScreens, hasModalScreens, contextMenuHandler, - uiThemeId + uiThemeId, + hasTopBar } } diff --git a/src/renderer/ui/components/topBar/topBar.module.scss b/src/renderer/ui/components/topBar/topBar.module.scss index 3987fb3..8add555 100644 --- a/src/renderer/ui/components/topBar/topBar.module.scss +++ b/src/renderer/ui/components/topBar/topBar.module.scss @@ -5,14 +5,11 @@ .top-bar { display: flex; - position: fixed; z-index: 3; background-color: var(--freeter-topBarBackground); border-top: 1px solid var(--freeter-topBarBorder); border-bottom: 1px solid var(--freeter-topBarBorder); - top: 0; - left: 0; - right: 0; + margin-bottom: -1px; } .top-bar-section { diff --git a/src/renderer/ui/components/workflowSwitcher/workflowSwitcher.module.scss b/src/renderer/ui/components/workflowSwitcher/workflowSwitcher.module.scss index 78505d1..4c8aa68 100644 --- a/src/renderer/ui/components/workflowSwitcher/workflowSwitcher.module.scss +++ b/src/renderer/ui/components/workflowSwitcher/workflowSwitcher.module.scss @@ -6,27 +6,23 @@ .workflow-switcher-bar { background-color: var(--freeter-workflowSwitcherBackground); border-bottom: 1px solid var(--freeter-workflowSwitcherBorder); - position: fixed; z-index: 2; - top: 62px; - left: 0; - right: 0; box-sizing: content-box; height: 36px; + width: 100%; + display: flex; + flex-direction: row; } .workflow-switcher { + flex-grow: 1; display: flex; margin: 0; padding: 0; - position: absolute; overflow-x: auto; overflow-y: hidden; - top: -1px; - left: 0px; - right: 0px; - bottom: -1px; box-sizing: content-box; + margin-bottom: -1px; &.is-drop-area { background-color: var(--freeter-workflowSwitcherDropAreaBackground); } diff --git a/src/renderer/ui/components/workflowSwitcher/workflowSwitcher.tsx b/src/renderer/ui/components/workflowSwitcher/workflowSwitcher.tsx index 9f3d035..04060c7 100644 --- a/src/renderer/ui/components/workflowSwitcher/workflowSwitcher.tsx +++ b/src/renderer/ui/components/workflowSwitcher/workflowSwitcher.tsx @@ -44,9 +44,9 @@ export function createWorkflowSwitcherComponent({ dontShowActionBar, } = useWorkflowSwitcherViewModel(); - return workflows ? ( + return (
-
-
+
}
- ) :
+ ) } return memo(WorkflowSwitcher); diff --git a/src/renderer/ui/components/worktable/widgetLayout/widgetLayout.tsx b/src/renderer/ui/components/worktable/widgetLayout/widgetLayout.tsx index 182034f..cf40a29 100644 --- a/src/renderer/ui/components/worktable/widgetLayout/widgetLayout.tsx +++ b/src/renderer/ui/components/worktable/widgetLayout/widgetLayout.tsx @@ -55,10 +55,10 @@ export function createWidgetLayoutComponent({ ? {'The workflow is empty. Enable Edit Mode with '} - {' button at the Top Bar to edit it.'} + {' button above (or under the Edit menu) to edit it.'} : - {'Click or drag a widget from the Add/Paste Widget at the Top Bar to add it to the workflow.'} + {'Click or drag a widget from the Add/Paste Widget above to add it to the workflow.'} )}
{'The project does not have any workflows. Enable Edit Mode with '} - {' button at the Top Bar to edit it.'} + {' button above (or under the Edit menu) to edit it.'} : {'Click '} - {' button at the Workflow Bar above to add a workflow to the project.'} + {' button at the Tab Bar above to add a workflow to the project.'} ) :
Date: Wed, 11 Jun 2025 21:23:50 +0400 Subject: [PATCH 5/9] Update UI to support the appearance settings --- src/renderer/init.ts | 16 ++++--- .../editModeToggle/editModeToggle.tsx | 0 .../editModeToggle/editModeToggleViewModel.ts | 0 .../{topBar => }/editModeToggle/index.ts | 0 .../manageProjectsButton/index.ts | 0 .../manageProjectsButton.tsx | 0 .../manageProjectsButtonViewModel.ts | 0 .../components/{topBar => }/palette/index.ts | 0 .../{topBar => }/palette/palette.module.scss | 0 .../{topBar => }/palette/palette.tsx | 0 .../{topBar => }/palette/paletteItem.tsx | 0 .../{topBar => }/palette/paletteViewModel.ts | 0 .../{topBar => }/projectSwitcher/index.ts | 0 .../projectSwitcher.module.scss | 2 - .../projectSwitcher/projectSwitcher.tsx | 10 +++-- .../projectSwitcherViewModel.ts | 0 .../ui/components/topBar/topBar.module.scss | 6 ++- src/renderer/ui/components/topBar/topBar.tsx | 24 +++++++---- .../ui/components/topBar/topBarViewModel.ts | 9 ++++ .../workflowSwitcher.module.scss | 28 ++++++++++++ .../workflowSwitcher/workflowSwitcher.tsx | 43 +++++++++++++++++++ .../workflowSwitcherViewModel.ts | 24 ++++++++++- .../{topBar => }/editModeToggle.spec.tsx | 2 +- .../{topBar => }/manageProjects.spec.tsx | 2 +- .../components/{topBar => }/palette.spec.tsx | 4 +- .../{topBar => }/projectSwitcher.spec.tsx | 2 +- 26 files changed, 145 insertions(+), 27 deletions(-) rename src/renderer/ui/components/{topBar => }/editModeToggle/editModeToggle.tsx (100%) rename src/renderer/ui/components/{topBar => }/editModeToggle/editModeToggleViewModel.ts (100%) rename src/renderer/ui/components/{topBar => }/editModeToggle/index.ts (100%) rename src/renderer/ui/components/{topBar => }/manageProjectsButton/index.ts (100%) rename src/renderer/ui/components/{topBar => }/manageProjectsButton/manageProjectsButton.tsx (100%) rename src/renderer/ui/components/{topBar => }/manageProjectsButton/manageProjectsButtonViewModel.ts (100%) rename src/renderer/ui/components/{topBar => }/palette/index.ts (100%) rename src/renderer/ui/components/{topBar => }/palette/palette.module.scss (100%) rename src/renderer/ui/components/{topBar => }/palette/palette.tsx (100%) rename src/renderer/ui/components/{topBar => }/palette/paletteItem.tsx (100%) rename src/renderer/ui/components/{topBar => }/palette/paletteViewModel.ts (100%) rename src/renderer/ui/components/{topBar => }/projectSwitcher/index.ts (100%) rename src/renderer/ui/components/{topBar => }/projectSwitcher/projectSwitcher.module.scss (89%) rename src/renderer/ui/components/{topBar => }/projectSwitcher/projectSwitcher.tsx (72%) rename src/renderer/ui/components/{topBar => }/projectSwitcher/projectSwitcherViewModel.ts (100%) rename tests/renderer/ui/components/{topBar => }/editModeToggle.spec.tsx (97%) rename tests/renderer/ui/components/{topBar => }/manageProjects.spec.tsx (94%) rename tests/renderer/ui/components/{topBar => }/palette.spec.tsx (98%) rename tests/renderer/ui/components/{topBar => }/projectSwitcher.spec.tsx (99%) diff --git a/src/renderer/init.ts b/src/renderer/init.ts index 2070f7d..7666afa 100644 --- a/src/renderer/init.ts +++ b/src/renderer/init.ts @@ -15,8 +15,8 @@ import { createDropOnWorktableLayoutUseCase } from '@/application/useCases/dragD import { createResizeLayoutItemUseCase, createResizeLayoutItemEndUseCase, createResizeLayoutItemStartUseCase } from '@/application/useCases/worktable/resizeLayoutItem'; import { uuidv4IdGenerator } from '@/infra/idGenerator/uuidv4IdGenerator'; import { createAppComponent } from '@/ui/components/app'; -import { createPaletteComponent } from '@/ui/components/topBar/palette'; -import { createPaletteViewModelHook } from '@/ui/components/topBar/palette/paletteViewModel'; +import { createPaletteComponent } from '@/ui/components/palette'; +import { createPaletteViewModelHook } from '@/ui/components/palette/paletteViewModel'; import { createTopBarComponent } from '@/ui/components/topBar'; import { createShelfComponent, createShelfItemComponent } from '@/ui/components/topBar/shelf'; import { createShelfViewModelHook } from '@/ui/components/topBar/shelf/shelfViewModel'; @@ -24,7 +24,7 @@ import { createWorktableComponent } from '@/ui/components/worktable'; import { createWidgetLayoutComponent, createWidgetLayoutItemComponent } from '@/ui/components/worktable/widgetLayout'; import { createWidgetLayoutViewModelHook } from '@/ui/components/worktable/widgetLayout/widgetLayoutViewModel'; import { createWorkflowSwitcherComponent, createWorkflowSwitcherViewModelHook } from '@/ui/components/workflowSwitcher'; -import { createProjectSwitcherComponent, createProjectSwitcherViewModelHook } from '@/ui/components/topBar/projectSwitcher'; +import { createProjectSwitcherComponent, createProjectSwitcherViewModelHook } from '@/ui/components/projectSwitcher'; import { createSwitchProjectUseCase } from '@/application/useCases/projectSwitcher/switchProject'; import { createSwitchWorkflowUseCase } from '@/application/useCases/workflowSwitcher/switchWorkflow'; import { createDragOverWorkflowSwitcherUseCase } from '@/application/useCases/dragDrop/dragOverWorkflowSwitcher'; @@ -38,7 +38,7 @@ import { createCloseWidgetSettingsUseCase } from '@/application/useCases/widgetS import { createSaveWidgetSettingsUseCase } from '@/application/useCases/widgetSettings/saveWidgetSettings'; import { createWidgetByIdComponent } from '@/ui/components/widget/widgetById'; import { createWidgetByIdViewModelHook } from '@/ui/components/widget/widgetByIdViewModel'; -import { createEditModeToggleComponent, createEditModeToggleViewModelHook } from '@/ui/components/topBar/editModeToggle'; +import { createEditModeToggleComponent, createEditModeToggleViewModelHook } from '@/ui/components/editModeToggle'; import { createToggleEditModeUseCase } from '@/application/useCases/toggleEditMode'; import { createToggleMenuBarUseCase } from '@/application/useCases/toggleMenuBar'; import { createAppStore } from '@/data/appStore'; @@ -77,7 +77,7 @@ import { createToggleDeletionInProjectManagerUseCase } from '@/application/useCa import { createUpdateProjectSettingsInProjectManagerUseCase } from '@/application/useCases/projectManager/updateProjectSettingsInProjectManager'; import { createUpdateProjectsOrderInProjectManagerUseCase } from '@/application/useCases/projectManager/updateProjectsOrderInProjectManager'; import { createCloseProjectManagerUseCase } from '@/application/useCases/projectManager/closeProjectManager'; -import { createManageProjectsButtonComponent, createManageProjectsButtonViewModelHook } from '@/ui/components/topBar/manageProjectsButton'; +import { createManageProjectsButtonComponent, createManageProjectsButtonViewModelHook } from '@/ui/components/manageProjectsButton'; import { createOpenProjectManagerUseCase } from '@/application/useCases/projectManager/openProjectManager'; import { createAddWorkflowUseCase } from '@/application/useCases/workflowSwitcher/addWorkflow'; import { createRenameWorkflowUseCase } from '@/application/useCases/workflowSwitcher/renameWorkflow'; @@ -611,7 +611,11 @@ function createUI(stateHooks: ReturnType, useCases: Awaite const useWorkflowSwitcherViewModel = createWorkflowSwitcherViewModelHook(deps); const WorkflowSwitcher = createWorkflowSwitcherComponent({ - useWorkflowSwitcherViewModel + useWorkflowSwitcherViewModel, + EditModeToggle, + ManageProjectsButton, + Palette, + ProjectSwitcher, }) const useWorktableViewModel = createWorktableViewModelHook(deps); const Worktable = createWorktableComponent({ diff --git a/src/renderer/ui/components/topBar/editModeToggle/editModeToggle.tsx b/src/renderer/ui/components/editModeToggle/editModeToggle.tsx similarity index 100% rename from src/renderer/ui/components/topBar/editModeToggle/editModeToggle.tsx rename to src/renderer/ui/components/editModeToggle/editModeToggle.tsx diff --git a/src/renderer/ui/components/topBar/editModeToggle/editModeToggleViewModel.ts b/src/renderer/ui/components/editModeToggle/editModeToggleViewModel.ts similarity index 100% rename from src/renderer/ui/components/topBar/editModeToggle/editModeToggleViewModel.ts rename to src/renderer/ui/components/editModeToggle/editModeToggleViewModel.ts diff --git a/src/renderer/ui/components/topBar/editModeToggle/index.ts b/src/renderer/ui/components/editModeToggle/index.ts similarity index 100% rename from src/renderer/ui/components/topBar/editModeToggle/index.ts rename to src/renderer/ui/components/editModeToggle/index.ts diff --git a/src/renderer/ui/components/topBar/manageProjectsButton/index.ts b/src/renderer/ui/components/manageProjectsButton/index.ts similarity index 100% rename from src/renderer/ui/components/topBar/manageProjectsButton/index.ts rename to src/renderer/ui/components/manageProjectsButton/index.ts diff --git a/src/renderer/ui/components/topBar/manageProjectsButton/manageProjectsButton.tsx b/src/renderer/ui/components/manageProjectsButton/manageProjectsButton.tsx similarity index 100% rename from src/renderer/ui/components/topBar/manageProjectsButton/manageProjectsButton.tsx rename to src/renderer/ui/components/manageProjectsButton/manageProjectsButton.tsx diff --git a/src/renderer/ui/components/topBar/manageProjectsButton/manageProjectsButtonViewModel.ts b/src/renderer/ui/components/manageProjectsButton/manageProjectsButtonViewModel.ts similarity index 100% rename from src/renderer/ui/components/topBar/manageProjectsButton/manageProjectsButtonViewModel.ts rename to src/renderer/ui/components/manageProjectsButton/manageProjectsButtonViewModel.ts diff --git a/src/renderer/ui/components/topBar/palette/index.ts b/src/renderer/ui/components/palette/index.ts similarity index 100% rename from src/renderer/ui/components/topBar/palette/index.ts rename to src/renderer/ui/components/palette/index.ts diff --git a/src/renderer/ui/components/topBar/palette/palette.module.scss b/src/renderer/ui/components/palette/palette.module.scss similarity index 100% rename from src/renderer/ui/components/topBar/palette/palette.module.scss rename to src/renderer/ui/components/palette/palette.module.scss diff --git a/src/renderer/ui/components/topBar/palette/palette.tsx b/src/renderer/ui/components/palette/palette.tsx similarity index 100% rename from src/renderer/ui/components/topBar/palette/palette.tsx rename to src/renderer/ui/components/palette/palette.tsx diff --git a/src/renderer/ui/components/topBar/palette/paletteItem.tsx b/src/renderer/ui/components/palette/paletteItem.tsx similarity index 100% rename from src/renderer/ui/components/topBar/palette/paletteItem.tsx rename to src/renderer/ui/components/palette/paletteItem.tsx diff --git a/src/renderer/ui/components/topBar/palette/paletteViewModel.ts b/src/renderer/ui/components/palette/paletteViewModel.ts similarity index 100% rename from src/renderer/ui/components/topBar/palette/paletteViewModel.ts rename to src/renderer/ui/components/palette/paletteViewModel.ts diff --git a/src/renderer/ui/components/topBar/projectSwitcher/index.ts b/src/renderer/ui/components/projectSwitcher/index.ts similarity index 100% rename from src/renderer/ui/components/topBar/projectSwitcher/index.ts rename to src/renderer/ui/components/projectSwitcher/index.ts diff --git a/src/renderer/ui/components/topBar/projectSwitcher/projectSwitcher.module.scss b/src/renderer/ui/components/projectSwitcher/projectSwitcher.module.scss similarity index 89% rename from src/renderer/ui/components/topBar/projectSwitcher/projectSwitcher.module.scss rename to src/renderer/ui/components/projectSwitcher/projectSwitcher.module.scss index 557ab69..cfaf5c4 100644 --- a/src/renderer/ui/components/topBar/projectSwitcher/projectSwitcher.module.scss +++ b/src/renderer/ui/components/projectSwitcher/projectSwitcher.module.scss @@ -4,13 +4,11 @@ */ .project-switcher { - height: 36px; line-height: 24px; font-family: 'Roboto Condensed'; font-size: 16px; min-width: 140px; max-width: 280px; - margin-right: 12px; option[disabled] { display: none; } diff --git a/src/renderer/ui/components/topBar/projectSwitcher/projectSwitcher.tsx b/src/renderer/ui/components/projectSwitcher/projectSwitcher.tsx similarity index 72% rename from src/renderer/ui/components/topBar/projectSwitcher/projectSwitcher.tsx rename to src/renderer/ui/components/projectSwitcher/projectSwitcher.tsx index 7fe2774..f60878b 100644 --- a/src/renderer/ui/components/topBar/projectSwitcher/projectSwitcher.tsx +++ b/src/renderer/ui/components/projectSwitcher/projectSwitcher.tsx @@ -3,7 +3,7 @@ * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) */ -import { ProjectSwitcherViewModelHook } from '@/ui/components/topBar/projectSwitcher/projectSwitcherViewModel'; +import { ProjectSwitcherViewModelHook } from '@/ui/components/projectSwitcher/projectSwitcherViewModel'; import clsx from 'clsx'; import * as styles from './projectSwitcher.module.scss'; @@ -11,10 +11,14 @@ type Deps = { useProjectSwitcherViewModel: ProjectSwitcherViewModelHook } +export type ProjectSwitcherProps = React.PropsWithChildren, HTMLSelectElement>>; + export function createProjectSwitcherComponent({ useProjectSwitcherViewModel }: Deps) { - function ProjectSwitcher() { + function ProjectSwitcher({ + className + }: ProjectSwitcherProps) { const { currentProjectId, projects, @@ -25,7 +29,7 @@ export function createProjectSwitcherComponent({ return (