Skip to content

Commit 83134cc

Browse files
committed
feat: orgchat
1 parent 8bf8bf3 commit 83134cc

11 files changed

Lines changed: 1135 additions & 2 deletions

File tree

src/components/AdvancedComponents/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export * from './CommentSection';
44
export * from './EditableField';
55
export * from './AppSidebar';
66
export * from './app-header';
7-
export * from './notifications-panel';
7+
export * from './notifications-panel';
8+
export * from './org-chart-component';
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import type { Meta, StoryObj } from '@storybook/react'
2+
import { OrgChart } from './OrgChart'
3+
import {
4+
smallTeamData,
5+
mediumTeamData,
6+
singlePersonData,
7+
deepHierarchyData,
8+
wideHierarchyData,
9+
emptyData,
10+
} from './mockData'
11+
12+
const meta = {
13+
title: 'Advanced Components/OrgChart',
14+
component: OrgChart,
15+
parameters: {
16+
layout: 'fullscreen',
17+
docs: {
18+
description: {
19+
component:
20+
'An organizational chart component that displays hierarchical user relationships. Integrates with PayloadCMS3 user data.',
21+
},
22+
},
23+
},
24+
tags: ['autodocs'],
25+
argTypes: {
26+
users: {
27+
description: 'Array of user objects with manager relationships',
28+
control: { type: 'object' },
29+
},
30+
onNodeClick: {
31+
description: 'Callback function when a user node is clicked',
32+
action: 'nodeClicked',
33+
},
34+
expandable: {
35+
description: 'Whether nodes can be collapsed/expanded',
36+
control: { type: 'boolean' },
37+
},
38+
initiallyExpanded: {
39+
description: 'Whether nodes start in expanded state',
40+
control: { type: 'boolean' },
41+
},
42+
className: {
43+
description: 'Additional CSS classes',
44+
control: { type: 'text' },
45+
},
46+
},
47+
} satisfies Meta<typeof OrgChart>
48+
49+
export default meta
50+
type Story = StoryObj<typeof meta>
51+
52+
/**
53+
* Small team example with 6 people in a simple hierarchy.
54+
* Perfect for startups or small departments.
55+
*/
56+
export const SmallTeam: Story = {
57+
args: {
58+
users: smallTeamData,
59+
expandable: false,
60+
initiallyExpanded: true,
61+
},
62+
}
63+
64+
/**
65+
* Medium-sized team with multiple branches and managers.
66+
* Shows how the component handles 14 people across different departments.
67+
*/
68+
export const MediumTeam: Story = {
69+
args: {
70+
users: mediumTeamData,
71+
expandable: false,
72+
initiallyExpanded: true,
73+
},
74+
}
75+
76+
/**
77+
* Single person organization (e.g., solo founder or CEO).
78+
* Demonstrates the component with minimal data.
79+
*/
80+
export const SinglePerson: Story = {
81+
args: {
82+
users: singlePersonData,
83+
expandable: false,
84+
initiallyExpanded: true,
85+
},
86+
}
87+
88+
/**
89+
* Deep hierarchy with 6 levels of reporting.
90+
* Shows vertical scaling capabilities.
91+
*/
92+
export const DeepHierarchy: Story = {
93+
args: {
94+
users: deepHierarchyData,
95+
expandable: false,
96+
initiallyExpanded: true,
97+
},
98+
}
99+
100+
/**
101+
* Wide hierarchy with many people at the same level.
102+
* Demonstrates horizontal scaling with 6 direct reports.
103+
*/
104+
export const WideHierarchy: Story = {
105+
args: {
106+
users: wideHierarchyData,
107+
expandable: false,
108+
initiallyExpanded: true,
109+
},
110+
}
111+
112+
/**
113+
* Expandable/collapsible nodes for better navigation in large organizations.
114+
* Click the arrow buttons to expand or collapse branches.
115+
*/
116+
export const ExpandableNodes: Story = {
117+
args: {
118+
users: mediumTeamData,
119+
expandable: true,
120+
initiallyExpanded: true,
121+
},
122+
}
123+
124+
/**
125+
* Expandable nodes that start collapsed.
126+
* Useful for very large organizations where you want to progressively reveal structure.
127+
*/
128+
export const InitiallyCollapsed: Story = {
129+
args: {
130+
users: mediumTeamData,
131+
expandable: true,
132+
initiallyExpanded: false,
133+
},
134+
}
135+
136+
/**
137+
* Interactive example with click handling.
138+
* Open the Actions panel to see click events.
139+
*/
140+
export const WithClickHandler: Story = {
141+
args: {
142+
users: smallTeamData,
143+
expandable: false,
144+
initiallyExpanded: true,
145+
onNodeClick: (user) => {
146+
console.log('Clicked user:', user)
147+
alert(`Clicked: ${user.name} (${user.email})`)
148+
},
149+
},
150+
}
151+
152+
/**
153+
* Empty state when no users are provided.
154+
*/
155+
export const EmptyState: Story = {
156+
args: {
157+
users: emptyData,
158+
expandable: false,
159+
initiallyExpanded: true,
160+
},
161+
}
162+
163+
/**
164+
* Custom styling example with additional CSS classes.
165+
*/
166+
export const CustomStyling: Story = {
167+
args: {
168+
users: smallTeamData,
169+
expandable: false,
170+
initiallyExpanded: true,
171+
className: 'bg-muted/50 rounded-lg',
172+
},
173+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
import { OrgChartProps } from './types';
3+
import { buildOrgTree } from './utils';
4+
import { OrgTree } from './OrgTree';
5+
6+
/**
7+
* OrgChart Component
8+
*
9+
* Displays an organizational hierarchy chart based on user data.
10+
*
11+
* @param users - Array of user objects with manager relationships
12+
* @param onNodeClick - Optional callback when a node is clicked
13+
* @param className - Optional additional CSS classes
14+
* @param expandable - Whether nodes can be collapsed/expanded (default: false)
15+
* @param initiallyExpanded - Whether nodes start expanded (default: true)
16+
*/
17+
export const OrgChart: React.FC<OrgChartProps> = ({
18+
users,
19+
onNodeClick,
20+
className = '',
21+
expandable = false,
22+
initiallyExpanded = true,
23+
}) => {
24+
// Build hierarchical tree from flat user list
25+
const orgTree = buildOrgTree(users);
26+
27+
if (users.length === 0) {
28+
return (
29+
<div className={`flex items-center justify-center p-8 ${className}`}>
30+
<div className="text-center">
31+
<p className="text-muted-foreground">No users to display</p>
32+
</div>
33+
</div>
34+
);
35+
}
36+
37+
if (orgTree.length === 0) {
38+
return (
39+
<div className={`flex items-center justify-center p-8 ${className}`}>
40+
<div className="text-center">
41+
<p className="text-muted-foreground">Unable to build organization tree</p>
42+
<p className="text-xs text-muted-foreground mt-1">
43+
Check that manager relationships are properly configured
44+
</p>
45+
</div>
46+
</div>
47+
);
48+
}
49+
50+
return (
51+
<div className={`org-chart w-full overflow-x-auto ${className}`}>
52+
<div className="inline-block min-w-full p-8">
53+
<OrgTree
54+
nodes={orgTree}
55+
onNodeClick={onNodeClick}
56+
expandable={expandable}
57+
initiallyExpanded={initiallyExpanded}
58+
/>
59+
</div>
60+
</div>
61+
);
62+
};
63+
64+
export default OrgChart;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React from 'react';
2+
import { OrgNodeProps } from './types';
3+
import { getInitials, getRoleBadgeColor } from './utils';
4+
import { jobRoleLabels } from './types';
5+
6+
export const OrgNode: React.FC<OrgNodeProps> = ({
7+
node,
8+
onNodeClick,
9+
expandable = false,
10+
isExpanded = true,
11+
onToggleExpand,
12+
}) => {
13+
const hasChildren = node.children.length > 0;
14+
const showExpandButton = expandable && hasChildren;
15+
16+
return (
17+
<div className="flex flex-col items-center">
18+
{/* Node Card */}
19+
<div
20+
className={`
21+
relative bg-card border border-border rounded-lg shadow-sm
22+
transition-all duration-200 hover:shadow-md hover:border-primary/50
23+
${onNodeClick ? 'cursor-pointer' : ''}
24+
w-64 p-4
25+
`}
26+
onClick={() => onNodeClick?.(node)}
27+
>
28+
{/* Profile Section */}
29+
<div className="flex items-start gap-3">
30+
{/* Avatar */}
31+
<div className="flex-shrink-0">
32+
{node.profilePicture?.url ? (
33+
<img
34+
src={node.profilePicture.url}
35+
alt={node.profilePicture.alt || node.name}
36+
className="w-12 h-12 rounded-full object-cover border-2 border-primary/20"
37+
/>
38+
) : (
39+
<div className="w-12 h-12 rounded-full bg-primary/10 border-2 border-primary/20 flex items-center justify-center">
40+
<span className="text-sm font-semibold text-primary">
41+
{getInitials(node.name)}
42+
</span>
43+
</div>
44+
)}
45+
</div>
46+
47+
{/* Info */}
48+
<div className="flex-1 min-w-0">
49+
<h3 className="font-semibold text-card-foreground text-sm truncate">
50+
{node.name}
51+
</h3>
52+
<p className="text-xs text-muted-foreground truncate">
53+
{node.email}
54+
</p>
55+
{node.jobRole && (
56+
<div className="mt-2">
57+
<span
58+
className={`
59+
inline-block px-2 py-0.5 rounded text-xs font-medium border
60+
${getRoleBadgeColor(node.jobRole)}
61+
`}
62+
>
63+
{jobRoleLabels[node.jobRole]}
64+
</span>
65+
</div>
66+
)}
67+
</div>
68+
</div>
69+
70+
{/* About Section */}
71+
{node.about && (
72+
<div className="mt-3 pt-3 border-t border-border">
73+
<p className="text-xs text-muted-foreground line-clamp-2">
74+
{node.about}
75+
</p>
76+
</div>
77+
)}
78+
79+
{/* Expand/Collapse Button */}
80+
{showExpandButton && (
81+
<button
82+
onClick={(e) => {
83+
e.stopPropagation();
84+
onToggleExpand?.();
85+
}}
86+
className="absolute -bottom-3 left-1/2 -translate-x-1/2 w-6 h-6 rounded-full bg-card border-2 border-border hover:border-primary/50 flex items-center justify-center transition-colors"
87+
aria-label={isExpanded ? 'Collapse' : 'Expand'}
88+
>
89+
<svg
90+
className={`w-3 h-3 text-muted-foreground transition-transform ${
91+
isExpanded ? 'rotate-180' : ''
92+
}`}
93+
fill="none"
94+
viewBox="0 0 24 24"
95+
stroke="currentColor"
96+
>
97+
<path
98+
strokeLinecap="round"
99+
strokeLinejoin="round"
100+
strokeWidth={2}
101+
d="M19 9l-7 7-7-7"
102+
/>
103+
</svg>
104+
</button>
105+
)}
106+
</div>
107+
108+
{/* Children Indicator Line */}
109+
{hasChildren && isExpanded && (
110+
<div className="w-0.5 h-8 bg-border" />
111+
)}
112+
</div>
113+
);
114+
};

0 commit comments

Comments
 (0)