@campxdev/react-blueprint 1.6.3 → 1.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/preview.tsx +1 -2
- package/package.json +1 -1
- package/src/App.tsx +71 -46
- package/src/components/DataDisplay/ActivityLogView/ActivityLogView.tsx +233 -0
- package/src/components/DataDisplay/ActivityLogView/Icons.tsx +65 -0
- package/src/components/DataDisplay/ActivityLogView/service.tsx +101 -0
- package/src/components/DataDisplay/ActivityLogView/styles.tsx +52 -0
- package/src/components/DataDisplay/DataTable/DataTable.tsx +2 -0
- package/src/components/DataDisplay/export.ts +1 -0
- package/src/components/Layout/PageHeader/components/DensitySelector/DensitySelector.tsx +3 -3
- package/src/components/Layout/PageHeader/components/TableColumnsSelector/TableColumnsSelector.tsx +3 -5
- package/src/hooks/usePageHeader.ts +1 -1
- package/src/redux/export.ts +1 -2
- package/src/redux/reducers.ts +5 -0
- package/src/redux/slices/pageHeaderSlice.ts +1 -3
- package/src/redux/store.ts +3 -5
- package/src/stories/DataDisplay/ActivityLogView.stories.tsx +96 -0
package/.storybook/preview.tsx
CHANGED
|
@@ -4,9 +4,8 @@ import React from 'react';
|
|
|
4
4
|
import { Provider } from 'react-redux';
|
|
5
5
|
import { BrowserRouter } from 'react-router-dom';
|
|
6
6
|
import { useDarkMode } from 'storybook-dark-mode';
|
|
7
|
-
import { store } from '../src/redux/
|
|
7
|
+
import { store } from '../src/redux/store';
|
|
8
8
|
import { MuiThemeProvider } from '../src/themes/MuiThemeProvider';
|
|
9
|
-
|
|
10
9
|
import { DarkColorTokens } from '../src/themes/colorTokens/darkColorTokens';
|
|
11
10
|
import { LightColorTokens } from '../src/themes/colorTokens/lightColorTokens';
|
|
12
11
|
import { darkTheme } from '../src/themes/darkTheme';
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LocalizationProvider } from '@mui/x-date-pickers';
|
|
2
|
+
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3';
|
|
2
3
|
import './App.css';
|
|
3
|
-
import {
|
|
4
|
+
import { ActivityLogView } from './components/export';
|
|
4
5
|
|
|
5
6
|
interface RowType {
|
|
6
7
|
lastName: string;
|
|
@@ -17,50 +18,74 @@ function App() {
|
|
|
17
18
|
paddingLeft: '16px',
|
|
18
19
|
}}
|
|
19
20
|
>
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
21
|
+
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
|
22
|
+
<ActivityLogView
|
|
23
|
+
activitiesData={[
|
|
24
|
+
{
|
|
25
|
+
userName: 'ss',
|
|
26
|
+
action: 'create',
|
|
27
|
+
message: 's',
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
userName: 'ss',
|
|
32
|
+
action: 'update',
|
|
33
|
+
message: 's',
|
|
34
|
+
timestamp: new Date().toISOString(),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
userName: 'ss',
|
|
38
|
+
action: 'update',
|
|
39
|
+
message: 's',
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
userName: 'ss',
|
|
44
|
+
action: 'update',
|
|
45
|
+
message: 's',
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
userName: 'ss',
|
|
50
|
+
action: 'update',
|
|
51
|
+
message: 's',
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
userName: 'ss',
|
|
56
|
+
action: 'delete',
|
|
57
|
+
message: 's',
|
|
58
|
+
timestamp: new Date().toISOString(),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
userName: 'ss',
|
|
62
|
+
action: 'update',
|
|
63
|
+
message: 's',
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
userName: 'ss',
|
|
68
|
+
action: 'update',
|
|
69
|
+
message: 's',
|
|
70
|
+
timestamp: new Date().toISOString(),
|
|
71
|
+
},
|
|
72
|
+
]}
|
|
73
|
+
isFetchingNextPage={false}
|
|
74
|
+
fetchNextPage={function (): void {
|
|
75
|
+
// throw new Error('Function not implemented.');
|
|
76
|
+
}}
|
|
77
|
+
hasNextPage={undefined}
|
|
78
|
+
fromDate={null}
|
|
79
|
+
toDate={null}
|
|
80
|
+
setFromDate={function (date: Date | null): void {
|
|
81
|
+
// throw new Error('Function not implemented.');
|
|
82
|
+
}}
|
|
83
|
+
setToDate={function (date: Date | null): void {
|
|
84
|
+
// throw new Error('Function not implemented.');
|
|
85
|
+
}}
|
|
86
|
+
isLoading={false}
|
|
87
|
+
/>
|
|
88
|
+
</LocalizationProvider>
|
|
64
89
|
</div>
|
|
65
90
|
);
|
|
66
91
|
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { TimelineContent, TimelineItem, TimelineSeparator } from '@mui/lab';
|
|
2
|
+
import { Box, Stack, Typography, useTheme } from '@mui/material';
|
|
3
|
+
import { format, isToday } from 'date-fns';
|
|
4
|
+
import _ from 'lodash';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { Button, DatePicker, Spinner } from '../../export';
|
|
7
|
+
import {
|
|
8
|
+
ActivityIcon,
|
|
9
|
+
convertUTCtoIST,
|
|
10
|
+
NoDataFound,
|
|
11
|
+
ScrollToTopButton,
|
|
12
|
+
useIntersectionObserver,
|
|
13
|
+
} from './service';
|
|
14
|
+
import {
|
|
15
|
+
StyledActivityLogViewBox,
|
|
16
|
+
StyledIconBox,
|
|
17
|
+
StyledSectionTitle,
|
|
18
|
+
StyledSpinnerBox,
|
|
19
|
+
StyledTimeline,
|
|
20
|
+
StyledTimelineConnector,
|
|
21
|
+
StyledTimelineDot,
|
|
22
|
+
} from './styles';
|
|
23
|
+
|
|
24
|
+
export type ActivityAction = 'create' | 'update' | 'delete';
|
|
25
|
+
|
|
26
|
+
export interface Activity {
|
|
27
|
+
userName: string;
|
|
28
|
+
action: ActivityAction;
|
|
29
|
+
message: string;
|
|
30
|
+
timestamp: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ActivityLogProps {
|
|
34
|
+
activitiesData: Activity[];
|
|
35
|
+
isFetchingNextPage: boolean;
|
|
36
|
+
fetchNextPage: () => void;
|
|
37
|
+
hasNextPage: boolean | undefined;
|
|
38
|
+
fromDate: Date | null;
|
|
39
|
+
toDate: Date | null;
|
|
40
|
+
setFromDate: (date: Date | null) => void;
|
|
41
|
+
setToDate: (date: Date | null) => void;
|
|
42
|
+
isLoading: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ActivityLogFilter = ({
|
|
46
|
+
fromDate,
|
|
47
|
+
toDate,
|
|
48
|
+
setFromDate,
|
|
49
|
+
setToDate,
|
|
50
|
+
fetchNextPage,
|
|
51
|
+
isLoading,
|
|
52
|
+
}: {
|
|
53
|
+
fromDate: Date | null;
|
|
54
|
+
toDate: Date | null;
|
|
55
|
+
setFromDate: (date: Date | null) => void;
|
|
56
|
+
setToDate: (date: Date | null) => void;
|
|
57
|
+
fetchNextPage: () => void;
|
|
58
|
+
isLoading: boolean;
|
|
59
|
+
}) => {
|
|
60
|
+
const [localFromDate, setLocalFromDate] = useState(fromDate);
|
|
61
|
+
const [localToDate, setLocalToDate] = useState(toDate);
|
|
62
|
+
|
|
63
|
+
const handleFilterSubmit = () => {
|
|
64
|
+
setFromDate(localFromDate);
|
|
65
|
+
setToDate(localToDate);
|
|
66
|
+
fetchNextPage();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const clearFilters = () => {
|
|
70
|
+
setLocalFromDate(null);
|
|
71
|
+
setLocalToDate(null);
|
|
72
|
+
setFromDate(null);
|
|
73
|
+
setToDate(null);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
|
|
78
|
+
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
|
79
|
+
<DatePicker
|
|
80
|
+
key={`from-date-${localFromDate}`}
|
|
81
|
+
label="From Date"
|
|
82
|
+
value={localFromDate}
|
|
83
|
+
onChange={setLocalFromDate}
|
|
84
|
+
disabled={isLoading}
|
|
85
|
+
/>
|
|
86
|
+
<DatePicker
|
|
87
|
+
key={`to-date-${localToDate}`}
|
|
88
|
+
label="To Date"
|
|
89
|
+
value={localToDate}
|
|
90
|
+
onChange={setLocalToDate}
|
|
91
|
+
disabled={isLoading}
|
|
92
|
+
/>
|
|
93
|
+
<Stack spacing={1} direction="row" ml={1} mt={2}>
|
|
94
|
+
<Button
|
|
95
|
+
onClick={handleFilterSubmit}
|
|
96
|
+
variant="contained"
|
|
97
|
+
disabled={isLoading}
|
|
98
|
+
loading={isLoading}
|
|
99
|
+
>
|
|
100
|
+
Submit
|
|
101
|
+
</Button>
|
|
102
|
+
</Stack>
|
|
103
|
+
</Box>
|
|
104
|
+
<Box sx={{ display: 'flex', mt: 3.5 }}>
|
|
105
|
+
<Button onClick={clearFilters} disabled={isLoading}>
|
|
106
|
+
Clear
|
|
107
|
+
</Button>
|
|
108
|
+
</Box>
|
|
109
|
+
</Box>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const ActivityLogView = ({
|
|
114
|
+
activitiesData,
|
|
115
|
+
isFetchingNextPage,
|
|
116
|
+
fetchNextPage,
|
|
117
|
+
hasNextPage,
|
|
118
|
+
fromDate,
|
|
119
|
+
toDate,
|
|
120
|
+
setFromDate,
|
|
121
|
+
setToDate,
|
|
122
|
+
isLoading,
|
|
123
|
+
}: ActivityLogProps) => {
|
|
124
|
+
const theme = useTheme();
|
|
125
|
+
const lastItemRef = useIntersectionObserver(() => {
|
|
126
|
+
if (!isFetchingNextPage && hasNextPage) fetchNextPage();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const loading = isLoading && activitiesData.length === 0;
|
|
130
|
+
|
|
131
|
+
// Group activities by Today and Past Week and Older
|
|
132
|
+
const groupedActivities = _.groupBy(activitiesData, (activity) => {
|
|
133
|
+
const activityDate = convertUTCtoIST(activity.timestamp);
|
|
134
|
+
return isToday(activityDate)
|
|
135
|
+
? 'Today'
|
|
136
|
+
: activityDate > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
137
|
+
? 'Past Week'
|
|
138
|
+
: 'Older';
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const renderMessage = (activity: Activity) =>
|
|
142
|
+
activity.message.split('\n').map((part, index) => (
|
|
143
|
+
<Typography key={index} component="div">
|
|
144
|
+
{index === 0 && (
|
|
145
|
+
<span style={{ color: theme.palette.text.primary }}>
|
|
146
|
+
{activity.userName}
|
|
147
|
+
</span>
|
|
148
|
+
)}{' '}
|
|
149
|
+
{part.split(/('[^']*')/g).map((text, idx) => {
|
|
150
|
+
const isQuotedText = text.startsWith("'") && text.endsWith("'");
|
|
151
|
+
return (
|
|
152
|
+
<span
|
|
153
|
+
key={idx}
|
|
154
|
+
style={{
|
|
155
|
+
color: isQuotedText
|
|
156
|
+
? theme.palette.text.primary
|
|
157
|
+
: theme.palette.text.secondary,
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
{isQuotedText ? text.slice(1, -1) : text}
|
|
161
|
+
</span>
|
|
162
|
+
);
|
|
163
|
+
})}
|
|
164
|
+
</Typography>
|
|
165
|
+
));
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<StyledActivityLogViewBox className="scrollable-activity-log">
|
|
169
|
+
<ActivityLogFilter
|
|
170
|
+
fromDate={fromDate}
|
|
171
|
+
toDate={toDate}
|
|
172
|
+
setFromDate={setFromDate}
|
|
173
|
+
setToDate={setToDate}
|
|
174
|
+
fetchNextPage={fetchNextPage}
|
|
175
|
+
isLoading={isLoading}
|
|
176
|
+
/>
|
|
177
|
+
|
|
178
|
+
{loading ? (
|
|
179
|
+
<StyledSpinnerBox>
|
|
180
|
+
<Spinner />
|
|
181
|
+
</StyledSpinnerBox>
|
|
182
|
+
) : activitiesData.length === 0 ? (
|
|
183
|
+
<NoDataFound message={'No data found'} />
|
|
184
|
+
) : (
|
|
185
|
+
Object.entries(groupedActivities).map(([section, activities]) => (
|
|
186
|
+
<Box key={section}>
|
|
187
|
+
<StyledSectionTitle>{section}</StyledSectionTitle>
|
|
188
|
+
<StyledTimeline>
|
|
189
|
+
{activities.map((activity, index) => (
|
|
190
|
+
<TimelineItem
|
|
191
|
+
key={index}
|
|
192
|
+
ref={index === activities.length - 1 ? lastItemRef : null}
|
|
193
|
+
>
|
|
194
|
+
<TimelineSeparator>
|
|
195
|
+
<StyledTimelineDot>
|
|
196
|
+
<StyledIconBox>
|
|
197
|
+
<ActivityIcon type={activity.action} />
|
|
198
|
+
</StyledIconBox>
|
|
199
|
+
</StyledTimelineDot>
|
|
200
|
+
{index < activities.length - 1 && (
|
|
201
|
+
<StyledTimelineConnector />
|
|
202
|
+
)}
|
|
203
|
+
</TimelineSeparator>
|
|
204
|
+
<TimelineContent
|
|
205
|
+
sx={{
|
|
206
|
+
display: 'flex',
|
|
207
|
+
justifyContent: 'space-between',
|
|
208
|
+
marginTop: 1,
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
<Typography>{renderMessage(activity)}</Typography>
|
|
212
|
+
<Typography variant="caption">
|
|
213
|
+
{format(
|
|
214
|
+
convertUTCtoIST(activity.timestamp),
|
|
215
|
+
"d MMMM yyyy 'at' hh:mm a",
|
|
216
|
+
)}
|
|
217
|
+
</Typography>
|
|
218
|
+
</TimelineContent>
|
|
219
|
+
</TimelineItem>
|
|
220
|
+
))}
|
|
221
|
+
</StyledTimeline>
|
|
222
|
+
</Box>
|
|
223
|
+
))
|
|
224
|
+
)}
|
|
225
|
+
{isFetchingNextPage && !loading && (
|
|
226
|
+
<StyledSpinnerBox>
|
|
227
|
+
<Spinner />
|
|
228
|
+
</StyledSpinnerBox>
|
|
229
|
+
)}
|
|
230
|
+
<ScrollToTopButton />
|
|
231
|
+
</StyledActivityLogViewBox>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const DeleteIcon = () => {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="16"
|
|
6
|
+
height="16"
|
|
7
|
+
viewBox="0 0 16 16"
|
|
8
|
+
fill="none"
|
|
9
|
+
style={{ display: 'flex', alignItems: 'center' }}
|
|
10
|
+
>
|
|
11
|
+
<g clipPath="url(#clip0_2752_9015)">
|
|
12
|
+
<path
|
|
13
|
+
d="M10.7933 1.3335H5.20659C2.77992 1.3335 1.33325 2.78016 1.33325 5.20683V10.7868C1.33325 13.2202 2.77992 14.6668 5.20659 14.6668H10.7866C13.2133 14.6668 14.6599 13.2202 14.6599 10.7935V5.20683C14.6666 2.78016 13.2199 1.3335 10.7933 1.3335ZM10.6666 8.50016H5.33325C5.20064 8.50016 5.07347 8.44749 4.9797 8.35372C4.88593 8.25995 4.83325 8.13277 4.83325 8.00016C4.83325 7.86756 4.88593 7.74038 4.9797 7.64661C5.07347 7.55284 5.20064 7.50016 5.33325 7.50016H10.6666C10.7992 7.50016 10.9264 7.55284 11.0201 7.64661C11.1139 7.74038 11.1666 7.86756 11.1666 8.00016C11.1666 8.13277 11.1139 8.25995 11.0201 8.35372C10.9264 8.44749 10.7992 8.50016 10.6666 8.50016Z"
|
|
14
|
+
fill="#F2353C"
|
|
15
|
+
/>
|
|
16
|
+
</g>
|
|
17
|
+
<defs>
|
|
18
|
+
<clipPath id="clip0_2752_9015">
|
|
19
|
+
<rect width="16" height="16" fill="white" />
|
|
20
|
+
</clipPath>
|
|
21
|
+
</defs>
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const CreateIcon = () => {
|
|
27
|
+
return (
|
|
28
|
+
<svg
|
|
29
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
30
|
+
width="16"
|
|
31
|
+
height="16"
|
|
32
|
+
viewBox="0 0 16 16"
|
|
33
|
+
fill="none"
|
|
34
|
+
>
|
|
35
|
+
<path
|
|
36
|
+
d="M10.793 1.3335H5.20632C2.77965 1.3335 1.33325 2.7799 1.33325 5.20656V10.7868C1.33325 13.2199 2.77965 14.6668 5.20632 14.6668H10.7866C13.2133 14.6668 14.6602 13.2204 14.6602 10.7938V5.20656C14.6666 2.7799 13.2197 1.3335 10.793 1.3335ZM10.6666 8.4999H8.49965V10.6668C8.49965 10.7994 8.44697 10.9266 8.35321 11.0204C8.25944 11.1142 8.13226 11.1668 7.99965 11.1668C7.86704 11.1668 7.73987 11.1142 7.6461 11.0204C7.55233 10.9266 7.49965 10.7994 7.49965 10.6668V8.4999H5.33325C5.20064 8.4999 5.07347 8.44722 4.9797 8.35345C4.88593 8.25968 4.83325 8.1325 4.83325 7.9999C4.83325 7.86729 4.88593 7.74011 4.9797 7.64634C5.07347 7.55257 5.20064 7.4999 5.33325 7.4999H7.49965V5.3335C7.49965 5.20089 7.55233 5.07371 7.6461 4.97994C7.73987 4.88617 7.86704 4.8335 7.99965 4.8335C8.13226 4.8335 8.25944 4.88617 8.35321 4.97994C8.44697 5.07371 8.49965 5.20089 8.49965 5.3335V7.4999H10.6666C10.7992 7.4999 10.9264 7.55257 11.0201 7.64634C11.1139 7.74011 11.1666 7.86729 11.1666 7.9999C11.1666 8.1325 11.1139 8.25968 11.0201 8.35345C10.9264 8.44722 10.7992 8.4999 10.6666 8.4999Z"
|
|
37
|
+
fill="#4BAABE"
|
|
38
|
+
/>
|
|
39
|
+
</svg>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const UpdateIcon = () => {
|
|
44
|
+
return (
|
|
45
|
+
<svg
|
|
46
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
47
|
+
width="16"
|
|
48
|
+
height="16"
|
|
49
|
+
viewBox="0 0 16 16"
|
|
50
|
+
fill="none"
|
|
51
|
+
>
|
|
52
|
+
<g clipPath="url(#clip0_2752_9033)">
|
|
53
|
+
<path
|
|
54
|
+
d="M10.7933 1.3335H5.20659C2.77992 1.3335 1.33325 2.78016 1.33325 5.20683V10.7868C1.33325 13.2202 2.77992 14.6668 5.20659 14.6668H10.7866C13.2133 14.6668 14.6599 13.2202 14.6599 10.7935V5.20683C14.6666 2.78016 13.2199 1.3335 10.7933 1.3335ZM11.1866 6.46683L7.40659 10.2468C7.36025 10.2934 7.30519 10.3303 7.24456 10.3555C7.18392 10.3806 7.11891 10.3936 7.05325 10.3936C6.98759 10.3936 6.92258 10.3806 6.86195 10.3555C6.80131 10.3303 6.74625 10.2934 6.69992 10.2468L4.81325 8.36016C4.76413 8.31439 4.72473 8.25919 4.6974 8.19785C4.67007 8.13652 4.65538 8.07031 4.65419 8.00318C4.65301 7.93604 4.66536 7.86936 4.6905 7.8071C4.71565 7.74484 4.75308 7.68828 4.80056 7.6408C4.84804 7.59332 4.90459 7.5559 4.96685 7.53075C5.02911 7.5056 5.0958 7.49325 5.16293 7.49444C5.23007 7.49562 5.29628 7.51031 5.35761 7.53764C5.41894 7.56497 5.47414 7.60437 5.51992 7.6535L7.05325 9.18683L10.4799 5.76016C10.5747 5.67184 10.7001 5.62376 10.8296 5.62605C10.9591 5.62833 11.0827 5.68081 11.1743 5.77242C11.2659 5.86402 11.3184 5.98761 11.3207 6.11715C11.323 6.24668 11.2749 6.37205 11.1866 6.46683Z"
|
|
55
|
+
fill="#88B053"
|
|
56
|
+
/>
|
|
57
|
+
</g>
|
|
58
|
+
<defs>
|
|
59
|
+
<clipPath id="clip0_2752_9033">
|
|
60
|
+
<rect width="16" height="16" fill="white" />
|
|
61
|
+
</clipPath>
|
|
62
|
+
</defs>
|
|
63
|
+
</svg>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ArrowUpward, Info } from '@mui/icons-material';
|
|
2
|
+
import { Box, Fab, Typography } from '@mui/material';
|
|
3
|
+
import { addHours, addMinutes, parseISO } from 'date-fns';
|
|
4
|
+
import { useCallback, useRef } from 'react';
|
|
5
|
+
import { emptyListImage } from '../../../assets/images/svg';
|
|
6
|
+
import { StyledBox } from '../../Assets/ErrorPages/styles';
|
|
7
|
+
import { ActivityAction } from '../../export';
|
|
8
|
+
import { CreateIcon, DeleteIcon, UpdateIcon } from './Icons';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Converts a given UTC date string to Indian Standard Time (IST).
|
|
12
|
+
*
|
|
13
|
+
* @param dateString - The date string in ISO format to be converted.
|
|
14
|
+
* @returns The converted date object in IST.
|
|
15
|
+
*/
|
|
16
|
+
export const convertUTCtoIST = (dateString: string) => {
|
|
17
|
+
const date = parseISO(dateString);
|
|
18
|
+
return addMinutes(addHours(date, 5), 30);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const ActivityIcon = ({ type }: { type: ActivityAction }) => {
|
|
22
|
+
switch (type) {
|
|
23
|
+
case 'create':
|
|
24
|
+
return <CreateIcon />;
|
|
25
|
+
case 'delete':
|
|
26
|
+
return <DeleteIcon />;
|
|
27
|
+
case 'update':
|
|
28
|
+
return <UpdateIcon />;
|
|
29
|
+
default:
|
|
30
|
+
return <Info />;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const ScrollToTopButton = () => {
|
|
35
|
+
const handleScrollToTop = () => {
|
|
36
|
+
const container = document.querySelector('.scrollable-activity-log');
|
|
37
|
+
if (container) {
|
|
38
|
+
container.scrollTo({
|
|
39
|
+
top: 0,
|
|
40
|
+
behavior: 'smooth',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Box
|
|
47
|
+
sx={{
|
|
48
|
+
position: 'absolute',
|
|
49
|
+
bottom: 20,
|
|
50
|
+
right: 20,
|
|
51
|
+
zIndex: 1500,
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<Fab
|
|
55
|
+
color="primary"
|
|
56
|
+
size="small"
|
|
57
|
+
onClick={handleScrollToTop}
|
|
58
|
+
aria-label="Scroll to top"
|
|
59
|
+
>
|
|
60
|
+
<ArrowUpward />
|
|
61
|
+
</Fab>
|
|
62
|
+
</Box>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const NoDataFound = ({ message }: { message: string }) => {
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<StyledBox>
|
|
70
|
+
<img
|
|
71
|
+
src={emptyListImage}
|
|
72
|
+
alt="page not found"
|
|
73
|
+
width={'350px'}
|
|
74
|
+
style={{ margin: '20px' }}
|
|
75
|
+
/>
|
|
76
|
+
<Typography variant="subtitle1">{message}</Typography>
|
|
77
|
+
</StyledBox>
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function useIntersectionObserver(callback: () => void) {
|
|
83
|
+
const observer = useRef<IntersectionObserver | null>(null);
|
|
84
|
+
|
|
85
|
+
const handleObserver = useCallback(
|
|
86
|
+
(node: HTMLDivElement) => {
|
|
87
|
+
if (observer.current) observer.current.disconnect();
|
|
88
|
+
|
|
89
|
+
observer.current = new IntersectionObserver((entries) => {
|
|
90
|
+
if (entries[0].isIntersecting) {
|
|
91
|
+
callback();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (node) observer.current.observe(node);
|
|
96
|
+
},
|
|
97
|
+
[callback],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return handleObserver;
|
|
101
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Timeline,
|
|
3
|
+
TimelineConnector,
|
|
4
|
+
TimelineDot,
|
|
5
|
+
timelineItemClasses,
|
|
6
|
+
} from '@mui/lab';
|
|
7
|
+
import { Box, styled, Typography } from '@mui/material';
|
|
8
|
+
|
|
9
|
+
export const StyledSpinnerBox = styled(Box)({
|
|
10
|
+
display: 'flex',
|
|
11
|
+
justifyContent: 'center',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const StyledSectionTitle = styled(Typography)(({ theme }) => ({
|
|
16
|
+
fontSize: '12px',
|
|
17
|
+
color: theme.palette.text.secondary,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
export const StyledTimeline = styled(Timeline)({
|
|
21
|
+
[`& .${timelineItemClasses.root}:before`]: { flex: 0, padding: 0 },
|
|
22
|
+
[`& .${timelineItemClasses.root}:not(:first-of-type)`]: {
|
|
23
|
+
marginTop: '0px',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const StyledTimelineDot = styled(TimelineDot)(({ theme }) => ({
|
|
28
|
+
backgroundColor: theme.palette.surface.defaultBackground,
|
|
29
|
+
boxShadow: 'none',
|
|
30
|
+
width: 30,
|
|
31
|
+
padding: 0,
|
|
32
|
+
paddingLeft: 5,
|
|
33
|
+
height: 30,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
export const StyledTimelineConnector = styled(TimelineConnector)(
|
|
37
|
+
({ theme }) => ({
|
|
38
|
+
width: '1px',
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export const StyledActivityLogViewBox = styled(Box)({
|
|
43
|
+
'&::-webkit-scrollbar': { display: 'none' },
|
|
44
|
+
overflowY: 'auto',
|
|
45
|
+
maxHeight: '70vh',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const StyledIconBox = styled(Box)({
|
|
49
|
+
display: 'flex',
|
|
50
|
+
justifyContent: 'center',
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
});
|
|
@@ -2,7 +2,8 @@ import { MenuListProps, MenuProps, Stack, useTheme } from '@mui/material';
|
|
|
2
2
|
import { GridDensity } from '@mui/x-data-grid';
|
|
3
3
|
import { capitalize } from 'lodash';
|
|
4
4
|
import { useDispatch, useSelector } from 'react-redux';
|
|
5
|
-
import {
|
|
5
|
+
import { setDensity } from '../../../../../redux/slices/pageHeaderSlice';
|
|
6
|
+
import { RootState } from '../../../../../redux/store';
|
|
6
7
|
import { Button, DropdownMenu, Icons, Typography } from '../../../../export';
|
|
7
8
|
import { DensityAnchor } from '../Anchors';
|
|
8
9
|
|
|
@@ -18,8 +19,7 @@ export const DensitySelector = ({
|
|
|
18
19
|
}: DensitySelectorProps) => {
|
|
19
20
|
const dispatch = useDispatch();
|
|
20
21
|
const density = useSelector(
|
|
21
|
-
(state: RootState) =>
|
|
22
|
-
state.pageHeaderSlice[uniqueId]?.density || 'standard',
|
|
22
|
+
(state: RootState) => state.pageHeader[uniqueId]?.density || 'standard',
|
|
23
23
|
);
|
|
24
24
|
const gridDensity: GridDensity[] = ['compact', 'standard', 'comfortable'];
|
|
25
25
|
|
package/src/components/Layout/PageHeader/components/TableColumnsSelector/TableColumnsSelector.tsx
CHANGED
|
@@ -3,10 +3,8 @@ import { MenuListProps, MenuProps, Typography, useTheme } from '@mui/material';
|
|
|
3
3
|
import { GridColDef, GridColumnVisibilityModel } from '@mui/x-data-grid';
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
import { useDispatch, useSelector } from 'react-redux';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
setColumnVisibilityModel,
|
|
9
|
-
} from '../../../../../redux/export';
|
|
6
|
+
import { setColumnVisibilityModel } from '../../../../../redux/slices/pageHeaderSlice';
|
|
7
|
+
import { RootState } from '../../../../../redux/store';
|
|
10
8
|
import { Button, Icons, SearchBar, SingleCheckBox } from '../../../../export';
|
|
11
9
|
import { DropdownMenu } from '../../../../Navigation/export';
|
|
12
10
|
|
|
@@ -26,7 +24,7 @@ export const TableColumnsSelector = ({
|
|
|
26
24
|
|
|
27
25
|
const columnVisibilityModel = useSelector(
|
|
28
26
|
(state: RootState) =>
|
|
29
|
-
state.
|
|
27
|
+
state.pageHeader[uniqueId]?.columnVisibilityModel ||
|
|
30
28
|
columns.reduce((acc, column) => {
|
|
31
29
|
acc[column.field] = true;
|
|
32
30
|
return acc;
|
|
@@ -10,7 +10,7 @@ export const usePageHeader = () => {
|
|
|
10
10
|
const dispatch = useDispatch();
|
|
11
11
|
const filterState = useSelector(
|
|
12
12
|
(state: RootState) =>
|
|
13
|
-
state.
|
|
13
|
+
state.pageHeader[uuidRef.current] ||
|
|
14
14
|
({} as {
|
|
15
15
|
density: GridDensity;
|
|
16
16
|
columnVisibilityModel: GridColumnVisibilityModel;
|
package/src/redux/export.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export * from './
|
|
2
|
-
export * from './store';
|
|
1
|
+
export * from './reducers';
|
|
@@ -10,7 +10,7 @@ export type PageHeaderState = {
|
|
|
10
10
|
|
|
11
11
|
const initialState: PageHeaderState = {};
|
|
12
12
|
|
|
13
|
-
const pageHeaderSlice = createSlice({
|
|
13
|
+
export const pageHeaderSlice = createSlice({
|
|
14
14
|
name: 'pageHeader',
|
|
15
15
|
initialState,
|
|
16
16
|
reducers: {
|
|
@@ -31,5 +31,3 @@ const pageHeaderSlice = createSlice({
|
|
|
31
31
|
|
|
32
32
|
export const { setColumnVisibilityModel, setDensity, resetStateForUniqueId } =
|
|
33
33
|
pageHeaderSlice.actions;
|
|
34
|
-
|
|
35
|
-
export default pageHeaderSlice.reducer;
|
package/src/redux/store.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { configureStore } from '@reduxjs/toolkit';
|
|
2
|
-
import
|
|
1
|
+
import { combineReducers, configureStore } from '@reduxjs/toolkit';
|
|
2
|
+
import { reactBlueprintReducers } from './reducers';
|
|
3
3
|
|
|
4
4
|
export const store = configureStore({
|
|
5
|
-
reducer:
|
|
6
|
-
pageHeaderSlice,
|
|
7
|
-
},
|
|
5
|
+
reducer: combineReducers(reactBlueprintReducers),
|
|
8
6
|
});
|
|
9
7
|
|
|
10
8
|
export type RootState = ReturnType<typeof store.getState>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { LocalizationProvider } from '@mui/x-date-pickers';
|
|
2
|
+
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3';
|
|
3
|
+
import { action } from '@storybook/addon-actions';
|
|
4
|
+
import { Meta, StoryObj } from '@storybook/react/*';
|
|
5
|
+
import { Activity, ActivityLogView } from '../../components/export';
|
|
6
|
+
|
|
7
|
+
// Mock data for the story with diverse timestamps and actions
|
|
8
|
+
const activitiesData: Activity[] = [
|
|
9
|
+
{
|
|
10
|
+
userName: 'John Doe',
|
|
11
|
+
action: 'create',
|
|
12
|
+
message: "created 'Student Registration'",
|
|
13
|
+
timestamp: new Date().toISOString(), // Today
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
userName: 'Jane Smith',
|
|
17
|
+
action: 'update',
|
|
18
|
+
message: `updated 'Employee Record'
|
|
19
|
+
• 'Position' has been changed from 'Junior Developer' to 'Senior Developer'
|
|
20
|
+
• 'Salary' has been changed from '$50,000' to '$70,000'
|
|
21
|
+
• 'Department' has been changed from 'IT Support' to 'Software Development'
|
|
22
|
+
• 'Supervisor' has been changed from 'Mr. Adams' to 'Ms. Johnson'
|
|
23
|
+
• 'Work Hours' have been changed from '40' to '45'`,
|
|
24
|
+
timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), // 2 days ago (Past Week)
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
userName: 'Admin',
|
|
28
|
+
action: 'delete',
|
|
29
|
+
message: "deleted 'Annual Report'",
|
|
30
|
+
timestamp: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(), // 10 days ago (Older)
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
userName: 'Alice Johnson',
|
|
34
|
+
action: 'create',
|
|
35
|
+
message: "created 'Course Outline'",
|
|
36
|
+
timestamp: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), // 5 days ago (Past Week)
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
userName: 'Bob Brown',
|
|
40
|
+
action: 'update',
|
|
41
|
+
message: `updated 'Project Details'
|
|
42
|
+
• 'Project Name' has been changed from 'Website Revamp' to 'Mobile App Launch'
|
|
43
|
+
• 'Deadline' has been changed from 'June 2023' to 'August 2023'
|
|
44
|
+
• 'Team Members' have been changed from '5' to '8'
|
|
45
|
+
• 'Budget' has been changed from '$20,000' to '$35,000'
|
|
46
|
+
• 'Client Contact' has been changed from 'Mr. Lee' to 'Mrs. Gomez'`,
|
|
47
|
+
timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), // Yesterday (Past Week)
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
userName: 'Charlie Green',
|
|
51
|
+
action: 'delete',
|
|
52
|
+
message: "deleted 'Temporary Files'",
|
|
53
|
+
timestamp: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(), // 15 days ago (Older)
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
userName: 'Emily White',
|
|
57
|
+
action: 'update',
|
|
58
|
+
message: `updated 'Client Information'
|
|
59
|
+
• 'Client Name' has been changed from 'Acme Corp' to 'Globex Inc.'
|
|
60
|
+
• 'Contact Number' has been changed from '123-456-7890' to '987-654-3210'
|
|
61
|
+
• 'Address' has been changed from '123 Main St' to '456 Elm St'
|
|
62
|
+
• 'Email' has been changed from 'info@acme.com' to 'contact@globex.com'
|
|
63
|
+
• 'Account Manager' has been changed from 'John Smith' to 'Laura Adams'`,
|
|
64
|
+
timestamp: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days ago (Past Week)
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const meta: Meta<typeof ActivityLogView> = {
|
|
69
|
+
title: 'DataDisplay/ActivityLog',
|
|
70
|
+
component: ActivityLogView,
|
|
71
|
+
decorators: [
|
|
72
|
+
(Story) => (
|
|
73
|
+
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
|
74
|
+
<Story />
|
|
75
|
+
</LocalizationProvider>
|
|
76
|
+
),
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default meta;
|
|
81
|
+
|
|
82
|
+
type Story = StoryObj<typeof ActivityLogView>;
|
|
83
|
+
|
|
84
|
+
export const Default: Story = {
|
|
85
|
+
args: {
|
|
86
|
+
activitiesData,
|
|
87
|
+
isFetchingNextPage: false,
|
|
88
|
+
fetchNextPage: action('fetchNextPage'),
|
|
89
|
+
hasNextPage: true,
|
|
90
|
+
fromDate: null,
|
|
91
|
+
toDate: null,
|
|
92
|
+
setFromDate: action('setFromDate'),
|
|
93
|
+
setToDate: action('setToDate'),
|
|
94
|
+
isLoading: false,
|
|
95
|
+
},
|
|
96
|
+
};
|