This commit is contained in:
Ahmed Bouhuolia
2026-04-03 11:51:34 +02:00
parent dfdbd7af6c
commit 6e04440fbd
8 changed files with 245 additions and 48 deletions

View File

@@ -5,6 +5,7 @@ import { Switch, Route } from 'react-router';
import '@/style/pages/Dashboard/Dashboard.scss';
import { Sidebar } from '@/containers/Dashboard/Sidebar/Sidebar';
import { WorkspacesSidebar } from '@/containers/Dashboard/WorkspacesSidebar/WorkspacesSidebar';
import DashboardContent from '@/components/Dashboard/DashboardContent';
import DialogsContainer from '@/components/DialogsContainer';
import PreferencesPage from '@/components/Preferences/PreferencesPage';
@@ -21,10 +22,13 @@ import { DashboardSockets } from './DashboardSockets';
*/
function DashboardPreferences() {
return (
<DashboardSplitPane>
<Sidebar />
<PreferencesPage />
</DashboardSplitPane>
<div className="dashboard-layout">
<WorkspacesSidebar />
<DashboardSplitPane>
<Sidebar />
<PreferencesPage />
</DashboardSplitPane>
</div>
);
}
@@ -33,10 +37,13 @@ function DashboardPreferences() {
*/
function DashboardAnyPage() {
return (
<DashboardSplitPane>
<Sidebar />
<DashboardContent />
</DashboardSplitPane>
<div className="dashboard-layout">
<WorkspacesSidebar />
<DashboardSplitPane>
<Sidebar />
<DashboardContent />
</DashboardSplitPane>
</div>
);
}

View File

@@ -1,19 +1,10 @@
// @ts-nocheck
import { Button, Popover, Menu, Position } from '@blueprintjs/core';
import { Icon } from '@/components';
import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization';
import { useAuthenticatedAccount } from '@/hooks/query';
import { compose, firstLettersArgs } from '@/utils';
// Popover modifiers.
const POPOVER_MODIFIERS = {
offset: { offset: '28, 8' },
};
import { compose } from '@/utils';
/**
* Sideabr head.
* Sidebar head.
*/
function SidebarHeadJSX({
// #withCurrentOrganization
@@ -25,39 +16,12 @@ function SidebarHeadJSX({
return (
<div className="sidebar__head">
<div className="sidebar__head-organization">
<Popover
modifiers={POPOVER_MODIFIERS}
boundary={'window'}
content={
<Menu className={'menu--dashboard-organization'}>
<div class="org-item">
<div class="org-item__logo">
{firstLettersArgs(...(organization.name || '').split(' '))}{' '}
</div>
<div class="org-item__name">{organization.name}</div>
</div>
</Menu>
}
position={Position.BOTTOM}
minimal={true}
>
<Button
className="title"
rightIcon={<Icon icon={'caret-down-16'} size={16} />}
>
{organization.name}
</Button>
</Popover>
<div className="title">{organization.name}</div>
<span class="subtitle">{user.full_name}</span>
</div>
<div className="sidebar__head-logo">
<Icon
icon={'mini-bigcapital'}
width={28}
height={28}
className="bigcapital--alt"
/>
<span className="bigcapital--alt">BC</span>
</div>
</div>
);

View File

@@ -0,0 +1,73 @@
// @ts-nocheck
import React from 'react';
import { Tooltip, Position, Spinner } from '@blueprintjs/core';
import { useWorkspaces } from '@/hooks/query';
import { useAuthOrganizationId } from '@/hooks/state';
import { useSwitchOrganization } from '@/hooks/useSwitchOrganization';
import { firstLettersArgs } from '@/utils';
import classNames from 'classnames';
import '@/style/containers/Dashboard/WorkspacesSidebar.scss';
/**
* Single workspace icon button.
*/
function WorkspaceIcon({ workspace, isActive, onClick }) {
const name = workspace.metadata?.name || workspace.organizationId;
const initials = firstLettersArgs(...(name || '').split(' '));
const isDisabled = !workspace.isReady || workspace.isBuildRunning;
return (
<Tooltip
content={name}
position={Position.RIGHT}
minimal
className="workspaces-sidebar__item-tooltip"
>
<button
className={classNames('workspaces-sidebar__item', {
'is-active': isActive,
'is-disabled': isDisabled,
})}
onClick={() => !isDisabled && onClick(workspace.organizationId)}
disabled={isDisabled}
>
{workspace.isBuildRunning ? (
<Spinner size={16} />
) : (
<span className="workspaces-sidebar__item-initials">{initials}</span>
)}
</button>
</Tooltip>
);
}
/**
* Workspaces sidebar container.
*/
export function WorkspacesSidebar() {
const { data: workspaces, isLoading } = useWorkspaces();
const activeOrganizationId = useAuthOrganizationId();
const switchOrganization = useSwitchOrganization();
return (
<div className="workspaces-sidebar">
<div className="workspaces-sidebar__list">
{isLoading ? (
<div className="workspaces-sidebar__loading">
<Spinner size={20} />
</div>
) : (
workspaces?.map((workspace) => (
<WorkspaceIcon
key={workspace.organizationId}
workspace={workspace}
isActive={workspace.organizationId === activeOrganizationId}
onClick={switchOrganization}
/>
))
)}
</div>
</div>
);
}

View File

@@ -23,6 +23,7 @@ export * from './exchangeRates';
export * from './contacts';
export * from './subscriptions';
export * from './organization';
export * from './workspaces';
export * from './landedCost';
export * from './UniversalSearch/UniversalSearch';
export * from './GenericResource';

View File

@@ -0,0 +1,22 @@
// @ts-nocheck
import { useRequestQuery } from '../useQueryRequest';
/**
* Retrieve workspaces of the authenticated user.
*/
export function useWorkspaces(props) {
return useRequestQuery(
['workspaces'],
{ method: 'get', url: 'workspaces' },
{
select: (res) => res.data.workspaces,
initialDataUpdatedAt: 0,
initialData: {
data: {
workspaces: [],
},
},
...props,
},
);
}

View File

@@ -0,0 +1,25 @@
// @ts-nocheck
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useQueryClient } from 'react-query';
import { setCookie } from '@/utils';
import { setOrganizationId } from '@/store/authentication/authentication.actions';
/**
* Switches the active organization by updating the cookie,
* Redux state, clearing the query cache, and reloading the app.
*/
export function useSwitchOrganization() {
const dispatch = useDispatch();
const queryClient = useQueryClient();
return useCallback(
(organizationId: string) => {
setCookie('organization_id', organizationId);
dispatch(setOrganizationId(organizationId));
queryClient.removeQueries();
window.location.assign('/');
},
[dispatch, queryClient],
);
}

View File

@@ -0,0 +1,96 @@
@import 'src/style/_base.scss';
.workspaces-sidebar {
width: 64px;
height: 100vh;
background: $sidebar-background;
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 0;
border-right: 1px solid rgba(255, 255, 255, 0.05);
z-index: $sidebar-zindex + 1;
&__list {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
width: 100%;
}
&__item {
width: 40px;
height: 40px;
border-radius: 8px;
border: 0;
background: rgba(255, 255, 255, 0.1);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.15s ease, transform 0.1s ease, border-radius 0.15s ease;
position: relative;
&:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.05);
}
&.is-active {
background: rgba(255, 255, 255, 0.9);
color: $sidebar-background;
border-radius: 12px;
&::before {
content: '';
position: absolute;
left: -8px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 24px;
background: #fff;
border-radius: 0 2px 2px 0;
}
}
&.is-disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover {
background: rgba(255, 255, 255, 0.1);
transform: none;
}
}
.bp4-spinner {
.bp4-spinner-head {
stroke: rgba(255, 255, 255, 0.8);
}
}
&.is-active .bp4-spinner .bp4-spinner-head {
stroke: $sidebar-background;
}
}
&__item-initials {
font-size: 14px;
font-weight: 600;
line-height: 1;
}
&__loading {
display: flex;
align-items: center;
justify-content: center;
padding-top: 20px;
.bp4-spinner .bp4-spinner-head {
stroke: rgba(255, 255, 255, 0.8);
}
}
}

View File

@@ -1,6 +1,15 @@
@import '../../_base.scss';
$dashboard-views-bar-height: 44px;
.dashboard-layout {
display: flex;
height: 100vh;
> .split-pane {
flex: 1 1 auto;
}
}
.dashboard {
display: flex;
height: 100vh;