Skip to content

Commit

Permalink
[126] settings page and organization structure
Browse files Browse the repository at this point in the history
  • Loading branch information
pro100Koss committed Nov 8, 2024
1 parent b6f4584 commit 98d5ea8
Show file tree
Hide file tree
Showing 11 changed files with 170 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import OrganizationTreeNode from '@/components/OrganizationStructureTree/Organiz
import OrganizationTreeDropPreview from '@/components/OrganizationStructureTree/OrganizationTreeDropPreview.tsx';
import OrganizationTreePlaceholder from '@/components/OrganizationStructureTree/OrganizationTreePlaceholder.tsx';
import Button from '@/components/ui/Button.tsx';
import { getMaxId } from '@/components/OrganizationStructureTree/OrganizationTreeUtils.ts';
import { getMaxId } from '@/components/OrganizationStructureTree/OrganizationTreeUtils';

interface OrganizationStructureTreeProps {
tree: NodeModel[];
Expand All @@ -15,6 +15,8 @@ interface OrganizationStructureTreeProps {

function OrganizationTree({ tree, onTreeChanged }: OrganizationStructureTreeProps) {
const [treeData, setTreeData] = useState<NodeModel[]>(JSON.parse(JSON.stringify(tree)));
const [isDragging, setIsDragging] = useState(false);

const handleDrop = (newTree: NodeModel[]) => {
setTreeData(newTree);
onTreeChanged?.(newTree);
Expand All @@ -37,6 +39,14 @@ function OrganizationTree({ tree, onTreeChanged }: OrganizationStructureTreeProp
onTreeChanged?.(newTree);
};

const onDragStart = () => {
setIsDragging(true);
};

const onDragEnd = () => {
setIsDragging(false);
};

return (
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<div>
Expand All @@ -51,10 +61,14 @@ function OrganizationTree({ tree, onTreeChanged }: OrganizationStructureTreeProp
return true;
}
}}
render={(node, options) => <OrganizationTreeNode node={node} onEdit={onEditNode} onDelete={onDeleteNode} {...options} />}
render={(node, options) => (
<OrganizationTreeNode node={node} onEdit={onEditNode} onDelete={onDeleteNode} disableHover={isDragging} {...options} />
)}
dragPreviewRender={(monitorProps) => <OrganizationTreeDropPreview monitorProps={monitorProps} />}
placeholderRender={(node, { depth }) => <OrganizationTreePlaceholder node={node} depth={depth} />}
onDrop={handleDrop}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
classes={{
draggingSource: 'opacity-30',
placeholder: 'relative',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { NodeModel, useDragOver } from '@minoru/react-dnd-treeview';
import Icon from '@/components/ui/Icon.tsx';
import TextField from '@/components/ui/TextField.tsx';
import Button from '@/components/ui/Button.tsx';
import { cn } from '@/lib/utils.ts';

type Props = {
node: NodeModel;
depth: number;
isOpen: boolean;
hasChild: boolean;
disableHover: boolean;
onToggle: (id: NodeModel['id']) => void;
onEdit: (id: NodeModel['id'], text: string) => void;
onDelete: (id: NodeModel['id']) => void;
Expand Down Expand Up @@ -69,15 +71,19 @@ function OrganizationTreeNode(props: Props) {

return (
<div
className="h-10 leading-10"
className={cn('h-10 leading-10', { 'hover:bg-accent': !props.disableHover })}
style={{ paddingInlineStart: indent }}
{...dragOverProps}
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
{...dragOverProps}
>
<div className="flex flex-row items-center">
<div className="scale-150 pr-1" onClick={handleToggle}>
{props.hasChild ? <Icon icon={props.isOpen ? 'TriangleDown' : 'TriangleRight'} /> : <Icon icon="Dot" />}
{props.hasChild ? (
<Icon icon={props.isOpen ? 'TriangleDown' : 'TriangleRight'} className="cursor-pointer hover:text-primary" />
) : (
<Icon icon="Dot" />
)}
</div>
<div className="w-full">
{isEditing ? (
Expand Down
7 changes: 7 additions & 0 deletions frontend/spa/src/components/SettingsPage/MenuItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { RadixIcons } from '@/components/ui/Icon.tsx';

export interface MenuItem {
title: string;
value: string;
icon?: RadixIcons;
}
38 changes: 21 additions & 17 deletions frontend/spa/src/components/SettingsPage/SettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import SettingsPageMenu from '@/components/SettingsPage/SettingsPageMenu.tsx';

interface MenuItem {
title: string;
value: string;
}
import Icon from '@/components/ui/Icon.tsx';
import { MenuItem } from '@/components/SettingsPage/MenuItem.ts';
import { ActiveTabContext } from '@/context/ActiveTabContext';

interface SettingsPageProps {
menu: MenuItem[];
activeTab: string;
onActiveTabChanged?: (value: string) => void;
onActiveTabChanged: (value: string) => void;
children: React.ReactNode;
}

Expand All @@ -21,20 +19,26 @@ function SettingsPage({ activeTab, menu, children, onActiveTabChanged }: Setting
};

return (
<div className="settings-page flex flex-row bg-white dark:bg-black/20 rounded shadow h-[calc(100vh-260px)]">
<div className="settings-page__nav flex-shrink-0 w-[300px] border-0 border-r-2 border-r-accent">
<SettingsPageMenu items={menu} activeTab={activeTab} onChange={onChangeTab} />
</div>
<div className="settings-page__content w-full">
<div className="px-10 pt-4">
<h1 className="text-center">{activeItem?.title}</h1>
<div className="py-4">
<hr />
<ActiveTabContext.Provider value={{ activeTab: activeItem, setActiveTab: onActiveTabChanged }}>
<div className="settings-page flex flex-row bg-white dark:bg-black/20 rounded shadow h-min-[calc(100vh-260px)]">
<div className="settings-page__nav flex-shrink-0 w-[150px] md:w-[300px] border-0 border-r-2 border-r-accent">
<div className="px-4 pt-4">
<h1 className="flex flex-row gap-2 h-10 items-center">
<Icon icon="HamburgerMenu" size="md" />
Menu
</h1>

<div className="py-4">
<hr />
</div>
</div>
{children}
<SettingsPageMenu items={menu} activeTab={activeTab} onChange={onChangeTab} />
</div>
<div className="settings-page__content w-full">
<div className="px-10 pt-4">{children}</div>
</div>
</div>
</div>
</ActiveTabContext.Provider>
);
}

Expand Down
30 changes: 30 additions & 0 deletions frontend/spa/src/components/SettingsPage/SettingsPageHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';

import Icon from '@/components/ui/Icon.tsx';
import { useActiveTab } from '@/context/ActiveTabContext.tsx';

interface SettingsPageHeaderProps {
children?: React.ReactNode;
}

function SettingsPageHeader(props: SettingsPageHeaderProps) {
const { activeTab } = useActiveTab();

return (
<>
<div className="flex flex-row justify-between">
<h1 className="flex flex-row gap-2 items-center h-10">
{activeTab?.icon && <Icon icon={activeTab.icon} size="md" />}
{activeTab?.title}
</h1>

<div>{props.children}</div>
</div>
<div className="py-4">
<hr />
</div>
</>
);
}

export default SettingsPageHeader;
20 changes: 19 additions & 1 deletion frontend/spa/src/components/ui/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export type RadixIcons = RemoveIconSuffix<keyof typeof icons>;

export interface IconProps {
icon: RadixIcons;
size?: number | 'sm' | 'md' | 'lg';
className?: string;
}

function Icon({ icon, ...props }: IconProps) {
Expand All @@ -14,7 +16,23 @@ function Icon({ icon, ...props }: IconProps) {
console.error(`Icon "${icon}" not found`);
return null;
}
return <IconComponent {...props} />;

const size = { width: 15, height: 15 };
if (props.size && typeof props.size === 'number') {
size.width = props.size;
size.height = props.size;
} else if (props.size === 'sm') {
size.width = 8;
size.height = 8;
} else if (props.size === 'md') {
size.width = 24;
size.height = 24;
} else if (props.size === 'lg') {
size.width = 36;
size.height = 36;
}

return <IconComponent {...props} {...size} />;
}

export default Icon;
22 changes: 17 additions & 5 deletions frontend/spa/src/components/views/OrganizationSettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useState } from 'react';
import { NodeModel } from '@minoru/react-dnd-treeview';

import OrganizationTree from '@/components/OrganizationStructureTree/OrganizationTree.tsx';
import Button from '@/components/ui/Button.tsx';
import SettingsPageHeader from '@/components/SettingsPage/SettingsPageHeader.tsx';

function OrganizationSettingsView() {
const savedTree = localStorage.getItem('organizationTree');
Expand All @@ -11,15 +13,25 @@ function OrganizationSettingsView() {

const onTreeChanged = (newTree: NodeModel[]) => {
setTree(newTree);
};

localStorage.setItem('organizationTree', JSON.stringify(newTree));
const onClickSave = () => {
localStorage.setItem('organizationTree', JSON.stringify(tree));
};

return (
<div>
OrganizationSettingsView
<OrganizationTree tree={tree} onTreeChanged={onTreeChanged} />
</div>
<>
<SettingsPageHeader>
<Button onClick={onClickSave}>Save</Button>
</SettingsPageHeader>
<div className="flex flex-row">
<div className="w-full"></div>
<div className="shrink-0 w-[400px]">
<h3>Structure:</h3>
<OrganizationTree tree={tree} onTreeChanged={onTreeChanged} />
</div>
</div>
</>
);
}

Expand Down
26 changes: 15 additions & 11 deletions frontend/spa/src/components/views/ProfileSettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useState } from 'react';

import themeService, { Themes } from '@/services/ThemeService.ts';
import Radio from '@/components/ui/Radio.tsx';
import SettingsPageHeader from '@/components/SettingsPage/SettingsPageHeader.tsx';

function ProfileSettingsView() {
const [themes] = useState([
Expand All @@ -22,20 +23,23 @@ function ProfileSettingsView() {
}, []);

return (
<div>
<div className="mt-4">
<div className="flex flex-row">
<div className="min-w-[200px]">Theme:</div>
<div>
<Radio items={themes} value={theme || 'auto'} layout="horizontal" onValueChange={onThemeChanged} />
<>
<SettingsPageHeader />
<div>
<div className="mt-4">
<div className="flex flex-row">
<div className="min-w-[200px]">Theme:</div>
<div>
<Radio items={themes} value={theme || 'auto'} layout="horizontal" onValueChange={onThemeChanged} />
</div>
</div>
<div className="flex flex-row">
<div className="min-w-[200px]">Other settings....</div>
<div>...</div>
</div>
</div>
<div className="flex flex-row">
<div className="min-w-[200px]">Other settings....</div>
<div>...</div>
</div>
</div>
</div>
</>
);
}

Expand Down
8 changes: 4 additions & 4 deletions frontend/spa/src/components/views/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { useNavigate, useLocation } from 'react-router-dom';
import SettingsPage from '@/components/SettingsPage/SettingsPage.tsx';
import ProfileSettingsView from '@/components/views/ProfileSettingsView.tsx';
import OrganizationSettingsView from '@/components/views/OrganizationSettingsView.tsx';
import { MenuItem } from '@/components/SettingsPage/MenuItem.ts';

const navigationItems = [
{ title: 'Profile', value: 'profile' },
{ title: 'Organization', value: 'organization' },
const navigationItems: MenuItem[] = [
{ title: 'Profile', value: 'profile', icon: 'Avatar' },
{ title: 'Organization', value: 'organization', icon: 'Backpack' },
];

function SettingsView() {
Expand All @@ -18,7 +19,6 @@ function SettingsView() {
useEffect(() => {
const path = location.pathname.split('/').pop();
if (path && navigationItems.some((item) => item.value === path)) {
console.log('ROUTE CHANGED', path);
setActiveTab(path);
}
}, [location]);
Expand Down
21 changes: 21 additions & 0 deletions frontend/spa/src/context/ActiveTabContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createContext, useContext } from 'react';

import { MenuItem } from '@/components/SettingsPage/MenuItem.ts';

interface ActiveTabContextProps {
activeTab?: MenuItem;
setActiveTab: (value: string) => void;
}

export const ActiveTabContext = createContext<ActiveTabContextProps>({
activeTab: undefined,
setActiveTab: () => {},
});

export const useActiveTab = () => {
const context = useContext(ActiveTabContext);
if (!context) {
throw new Error('useActiveTab must be used within an ActiveTabProvider');
}
return context;
};
19 changes: 11 additions & 8 deletions frontend/spa/src/store/store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

type User = {
name: string;
Expand All @@ -17,11 +18,13 @@ type Actions = {
setUser: (user: User | null) => void;
};

export const useAuthStore = create<AuthState & Actions>((set) => ({
isAuthenticated: false,
isInitialized: false,
user: null,
setIsAuthenticated: (value: boolean) => set({ isAuthenticated: value }),
setIsInitialized: (value: boolean) => set({ isInitialized: value }),
setUser: (user: User | null) => set({ user }),
}));
export const useAuthStore = create<AuthState & Actions>()(
devtools((set) => ({
isAuthenticated: false,
isInitialized: false,
user: null,
setIsAuthenticated: (value: boolean) => set({ isAuthenticated: value }),
setIsInitialized: (value: boolean) => set({ isInitialized: value }),
setUser: (user: User | null) => set({ user }),
}))
);

0 comments on commit 98d5ea8

Please sign in to comment.