Skip to content

Commit 21f4314

Browse files
committed
feat: add WorkspaceRecentActivityModal component
Signed-off-by: amitamrutiya <[email protected]>
1 parent f8805a8 commit 21f4314

File tree

2 files changed

+243
-0
lines changed

2 files changed

+243
-0
lines changed
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import HistoryIcon from '@mui/icons-material/History';
2+
import { useCallback, useEffect, useRef, useState } from 'react';
3+
import {
4+
Avatar,
5+
Box,
6+
CircularProgress,
7+
Divider,
8+
List,
9+
ListItem,
10+
ListItemAvatar,
11+
ListItemText,
12+
Typography
13+
} from '../../base';
14+
import { iconLarge, iconXSmall } from '../../constants/iconsSizes';
15+
import { DesignIcon, EnvironmentIcon, TeamsIcon, ViewIcon, WorkspaceIcon } from '../../icons';
16+
import { getFormatDate, getFullFormattedTime } from '../../utils';
17+
import { CustomTooltip } from '../CustomTooltip';
18+
import { Modal, ModalBody, ModalFooter } from '../Modal';
19+
20+
interface EventData {
21+
created_at: string;
22+
description: string;
23+
first_name: string;
24+
last_name: string;
25+
avatar_url: string;
26+
}
27+
28+
interface EventsResponse {
29+
data: EventData[];
30+
page: number;
31+
total_count: number;
32+
}
33+
34+
interface RecentActivityModalProps {
35+
workspaceId: string;
36+
workspaceName: string;
37+
open: boolean;
38+
handleClose: () => void;
39+
useGetEventsOfWorkspaceQuery: (
40+
params: {
41+
workspaceId: string;
42+
page: number;
43+
pagesize: number;
44+
},
45+
options?: { skip: boolean }
46+
) => {
47+
data?: EventsResponse;
48+
isLoading: boolean;
49+
isFetching: boolean;
50+
};
51+
}
52+
53+
const WorkspaceRecentActivityModal: React.FC<RecentActivityModalProps> = ({
54+
workspaceId,
55+
workspaceName,
56+
open,
57+
handleClose,
58+
useGetEventsOfWorkspaceQuery
59+
}) => {
60+
const [page, setPage] = useState<number>(0);
61+
const _pageSize = 25;
62+
const [allEvents, setAllEvents] = useState<EventData[]>([]);
63+
const [hasMore, setHasMore] = useState<boolean>(true);
64+
const _observer = useRef<IntersectionObserver | null>(null);
65+
const _loaderRef = useRef<HTMLDivElement | null>(null);
66+
67+
const {
68+
data: eventsData,
69+
isLoading: isEventsLoading,
70+
isFetching
71+
} = useGetEventsOfWorkspaceQuery(
72+
{
73+
workspaceId,
74+
page: page,
75+
pagesize: _pageSize
76+
},
77+
{ skip: !open }
78+
);
79+
80+
// Update events when data is fetched
81+
useEffect(() => {
82+
if (eventsData) {
83+
if (page === 0) {
84+
setAllEvents(eventsData.data);
85+
} else {
86+
setAllEvents((prev) => [...prev, ...eventsData.data]);
87+
}
88+
89+
// Check if we've loaded all events
90+
setHasMore((eventsData.page + 1) * _pageSize < eventsData.total_count);
91+
}
92+
}, [eventsData, page, _pageSize]);
93+
94+
// Reset pagination when modal opens
95+
useEffect(() => {
96+
if (open) {
97+
setPage(0);
98+
setAllEvents([]);
99+
setHasMore(true);
100+
}
101+
}, [open]);
102+
103+
// Callback for the IntersectionObserver
104+
const lastEventElementRef = useCallback(
105+
(node: HTMLDivElement | null) => {
106+
if (isEventsLoading || isFetching) return;
107+
108+
// Disconnect previous observer if it exists
109+
if (_observer.current) _observer.current.disconnect();
110+
111+
// Create a new observer
112+
_observer.current = new IntersectionObserver((entries) => {
113+
// If the loader element is visible and we have more data to load
114+
if (entries[0].isIntersecting && hasMore) {
115+
setPage((prevPage) => prevPage + 1);
116+
}
117+
});
118+
119+
// Observe the loader element
120+
if (node) _observer.current.observe(node);
121+
},
122+
[isEventsLoading, isFetching, hasMore]
123+
);
124+
125+
const getImage = (description: string) => {
126+
const availableTypes = ['design', 'view', 'environment', 'team'];
127+
const type = availableTypes.find((type) => description.includes(type));
128+
129+
switch (type) {
130+
case 'design':
131+
return <DesignIcon {...iconXSmall} />;
132+
case 'view':
133+
return <ViewIcon {...iconXSmall} />;
134+
case 'environment':
135+
return <EnvironmentIcon {...iconXSmall} />;
136+
case 'team':
137+
return <TeamsIcon {...iconXSmall} fill="" />;
138+
default:
139+
return <WorkspaceIcon {...iconXSmall} />;
140+
}
141+
};
142+
143+
return (
144+
<>
145+
<Modal
146+
title={`"${workspaceName}" Recent Activity`}
147+
open={open}
148+
closeModal={handleClose}
149+
headerIcon={<HistoryIcon />}
150+
maxWidth="md"
151+
>
152+
<ModalBody style={{ maxHeight: '40rem' }}>
153+
{page === 0 && isEventsLoading ? (
154+
<Box display="flex" justifyContent="center" padding={4}>
155+
<CircularProgress />
156+
</Box>
157+
) : allEvents.length > 0 ? (
158+
<>
159+
{allEvents.map((data, index) => (
160+
<List
161+
sx={{ width: '100%', padding: '0' }}
162+
key={`${data.created_at}-${data.description}-${index}`}
163+
>
164+
<ListItem
165+
style={{ padding: '0' }}
166+
alignItems="flex-start"
167+
secondaryAction={
168+
<Box display={'flex'} flexDirection="column" alignItems="flex-end" gap={0.2}>
169+
{getImage(data.description)}
170+
<CustomTooltip title={getFullFormattedTime(data.created_at)}>
171+
<div>
172+
<Typography
173+
variant="caption"
174+
style={{
175+
display: 'flex',
176+
alignItems: 'center',
177+
fontStyle: 'italic'
178+
}}
179+
>
180+
Updated At: {getFormatDate(data.created_at)}
181+
</Typography>
182+
</div>
183+
</CustomTooltip>
184+
</Box>
185+
}
186+
>
187+
<ListItemAvatar
188+
style={{
189+
minWidth: '0',
190+
marginTop: '0.75rem',
191+
marginRight: '1rem'
192+
}}
193+
>
194+
<Avatar alt={data.first_name} src={data.avatar_url} sx={iconLarge} />
195+
</ListItemAvatar>
196+
<ListItemText
197+
primary={data.first_name + ' ' + data.last_name}
198+
secondary={<>{data.description}</>}
199+
/>
200+
</ListItem>
201+
<Divider />
202+
</List>
203+
))}
204+
205+
{/* The loader element that will trigger the next page load */}
206+
<div
207+
ref={hasMore ? lastEventElementRef : null}
208+
style={{ height: '20px', width: '100%' }}
209+
/>
210+
211+
{isFetching && (
212+
<Box display="flex" justifyContent="center" padding={2} ref={_loaderRef}>
213+
<CircularProgress size={24} />
214+
</Box>
215+
)}
216+
217+
{!hasMore && allEvents.length > 0 && (
218+
<Typography variant="body2" color="textSecondary" style={{ textAlign: 'center' }}>
219+
No more activities to load
220+
</Typography>
221+
)}
222+
</>
223+
) : (
224+
<Box display="flex" justifyContent="center" padding={4}>
225+
<Typography variant="body2" color="textSecondary">
226+
No recent activity found for this workspace.
227+
</Typography>
228+
</Box>
229+
)}
230+
</ModalBody>
231+
<ModalFooter variant="filled">
232+
<></>
233+
</ModalFooter>
234+
</Modal>
235+
</>
236+
);
237+
};
238+
239+
export default WorkspaceRecentActivityModal;

src/custom/Workspaces/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import AssignmentModal from './AssignmentModal';
22
import DesignTable from './DesignTable';
33
import EnvironmentTable from './EnvironmentTable';
44
import WorkspaceCard from './WorkspaceCard';
5+
import WorkspaceEnvironmentSelection from './WorkspaceEnvironmentSelection';
6+
import WorkspaceRecentActivityModal from './WorkspaceRecentActivityModal';
57
import WorkspaceTeamsTable from './WorkspaceTeamsTable';
68
import WorkspaceViewsTable from './WorkspaceViewsTable';
79
import useDesignAssignment from './hooks/useDesignAssignment';
@@ -21,6 +23,8 @@ export {
2123
useTeamAssignment,
2224
useViewAssignment,
2325
WorkspaceCard,
26+
WorkspaceEnvironmentSelection,
27+
WorkspaceRecentActivityModal,
2428
WorkspaceTeamsTable,
2529
WorkspaceViewsTable
2630
};

0 commit comments

Comments
 (0)