@campxdev/react-blueprint 1.8.9 → 1.9.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@campxdev/react-blueprint",
3
- "version": "1.8.9",
3
+ "version": "1.9.0",
4
4
  "main": "./export.ts",
5
5
  "dependencies": {
6
6
  "@emotion/react": "^11.13.3",
@@ -32,6 +32,7 @@
32
32
  "react-dom": "^18.3.1",
33
33
  "react-error-boundary": "^3.1.4",
34
34
  "react-hook-form": "^7.52.0",
35
+ "@hookform/resolvers": "^2.9.10",
35
36
  "react-joyride": "^2.8.2",
36
37
  "react-query": "^3.39.3",
37
38
  "react-redux": "^9.1.2",
@@ -41,7 +42,8 @@
41
42
  "redux": "^5.0.1",
42
43
  "storybook-dark-mode": "^4.0.1",
43
44
  "typescript": "^5.5.2",
44
- "web-vitals": "^2.1.0"
45
+ "web-vitals": "^2.1.0",
46
+ "yup": "^1.4.0"
45
47
  },
46
48
  "scripts": {
47
49
  "start": "react-scripts start",
@@ -6,23 +6,25 @@ import './ButtonLoader.css';
6
6
 
7
7
  export type ButtonProps = {
8
8
  loading?: boolean;
9
- color?: string;
10
9
  background?: string;
11
10
  hoverBackground?: string;
12
- } & Omit<MuiButtonProps, 'color'>;
11
+ textColor?: string;
12
+ } & MuiButtonProps;
13
13
 
14
14
  export const Button = ({
15
15
  loading = false,
16
16
  color,
17
17
  background,
18
18
  hoverBackground,
19
+ textColor,
19
20
  ...props
20
21
  }: ButtonProps) => {
21
22
  return (
22
23
  <MuiButton
23
24
  {...props}
25
+ color={color}
24
26
  sx={{
25
- color,
27
+ color: textColor,
26
28
  background,
27
29
  ':hover': {
28
30
  background: hoverBackground,
@@ -15,7 +15,7 @@ export const MenuFooter = ({ onClick }: { onClick: () => void }) => {
15
15
  margin: '0px 16px',
16
16
  }}
17
17
  disableRipple
18
- color={theme.palette.text.tertiary}
18
+ textColor={theme.palette.text.tertiary}
19
19
  >
20
20
  Clear
21
21
  </Button>
@@ -125,7 +125,7 @@ export const SingleFilter = ({
125
125
  ? theme.palette.highlight.greenBackground
126
126
  : theme.palette.surface.grey
127
127
  }
128
- color={theme.palette.text.primary}
128
+ textColor={theme.palette.text.primary}
129
129
  endIcon={<KeyboardArrowDown />}
130
130
  >
131
131
  {validValue
@@ -1,4 +1,4 @@
1
- import { Box, Divider, Stack, useTheme } from '@mui/material';
1
+ import { Divider, Stack, useTheme } from '@mui/material';
2
2
  import { GridColDef } from '@mui/x-data-grid';
3
3
  import { motion } from 'framer-motion';
4
4
  import {
@@ -9,48 +9,27 @@ import {
9
9
  useState,
10
10
  } from 'react';
11
11
  import { useDispatch, useSelector } from 'react-redux';
12
+ import { useSearchParams } from 'react-router-dom';
12
13
  import { setFilterByNameForUniqueId } from '../../../redux/slices/pageHeaderSlice';
13
14
  import { RootState } from '../../../redux/store';
14
- import {
15
- DensitySelector,
16
- Icons,
17
- TableColumnsSelector,
18
- Typography,
19
- } from '../../export';
15
+ import { DensitySelector, TableColumnsSelector } from '../../export';
20
16
  import { FiltersAnchor } from './components/Anchors';
21
17
  import { ManageFilters } from './components/ManageFilters/ManageFilters';
22
18
  import { SearchBar } from './components/SearchBar';
19
+ import { Views, ViewsProps, ViewTab } from './components/Views/Views';
20
+ import { ViewsActions } from './components/Views/ViewsActions';
23
21
 
24
22
  interface PageHeaderProps {
25
23
  uniqueId?: string;
26
24
  viewsSlot?: ReactNode;
27
25
  filterComponents?: ReactElement[];
26
+ viewsProps?: Omit<ViewsProps, 'uniqueId'>;
28
27
  actions?: ReactNode[];
29
28
  columns?: GridColDef[];
30
29
  searchText?: string;
31
30
  defaultShowLabels?: string[];
32
31
  }
33
32
 
34
- const ViewTab = ({ title }: { title: string }) => {
35
- const theme = useTheme();
36
- return (
37
- <Stack>
38
- <Stack direction="row" gap={1} alignItems="center" padding="8px 0px">
39
- <Icons.ViewsIcon />
40
- <Typography variant="subtitle3">{title}</Typography>
41
- </Stack>
42
- <Divider
43
- style={{
44
- background: theme.palette.tertiary.main,
45
- height: '4px',
46
- width: '92px',
47
- borderRadius: '48px',
48
- }}
49
- />
50
- </Stack>
51
- );
52
- };
53
-
54
33
  const motionDivVariants = {
55
34
  collapsed: { height: '48px' },
56
35
  expanded: { height: '108px' },
@@ -62,11 +41,16 @@ export const PageHeader = ({
62
41
  filterComponents = [],
63
42
  defaultShowLabels = [],
64
43
  columns,
65
- viewsSlot = <Box></Box>,
44
+ viewsSlot = <ViewTab title="Default View" active={true} onClick={() => {}} />,
66
45
  searchText = 'Search',
46
+ viewsProps,
67
47
  }: PageHeaderProps) => {
68
48
  const [expanded, setExpanded] = useState(false);
69
49
 
50
+ const [searchParams] = useSearchParams();
51
+
52
+ const viewId = searchParams.get('viewId');
53
+
70
54
  const allFilterLabels = filterComponents.map(
71
55
  (filter) => filter.props['label'] || 'Label Not Found',
72
56
  );
@@ -134,10 +118,21 @@ export const PageHeader = ({
134
118
  direction="row"
135
119
  width="100%"
136
120
  alignItems="center"
137
- justifyContent="space-between"
121
+ justifyContent="flex-end"
138
122
  height="48px"
139
123
  >
140
- {isTableMode ? <ViewTab title="Default View" /> : viewsSlot}
124
+ {isTableMode && viewsProps ? (
125
+ <Views {...viewsProps} uniqueId={uniqueId} />
126
+ ) : (
127
+ <Stack
128
+ direction="row"
129
+ gap={2}
130
+ justifyContent="flex-start"
131
+ width="100%"
132
+ >
133
+ {viewsSlot}
134
+ </Stack>
135
+ )}
141
136
 
142
137
  <Stack direction="row" alignItems="center">
143
138
  {isTableMode && (
@@ -166,42 +161,57 @@ export const PageHeader = ({
166
161
  />
167
162
  )}
168
163
  {expanded && (
169
- <Stack direction="row" width="100%" alignItems="center" gap={2}>
170
- {finalFilterComponents.map((filter, index) => (
171
- <Fragment key={index}>
172
- {wrapProps(filter)}
173
- {index < finalFilterComponents.length - 1 && (
174
- <Divider
175
- orientation="vertical"
176
- style={{
177
- height: '20px',
178
- background: theme.palette.border.primary,
179
- }}
180
- />
164
+ <Stack
165
+ direction="row"
166
+ width="100%"
167
+ alignItems="center"
168
+ justifyContent="space-between"
169
+ >
170
+ <Stack
171
+ key={viewId ?? 'default'}
172
+ direction={'row'}
173
+ alignItems="center"
174
+ gap={2}
175
+ >
176
+ {finalFilterComponents.map((filter, index) => (
177
+ <Fragment key={index}>
178
+ {wrapProps(filter)}
179
+ {index < finalFilterComponents.length - 1 && (
180
+ <Divider
181
+ orientation="vertical"
182
+ style={{
183
+ height: '20px',
184
+ background: theme.palette.border.primary,
185
+ }}
186
+ />
187
+ )}
188
+ </Fragment>
189
+ ))}
190
+ <ManageFilters
191
+ filterLabels={filterComponents.map((filter) => {
192
+ return filter.props['label'] || 'Label Not Found';
193
+ })}
194
+ selectedFilterLabels={finalFilterComponents.map(
195
+ (filter) => filter.props['label'] || 'Label Not Found',
181
196
  )}
182
- </Fragment>
183
- ))}
184
- <ManageFilters
185
- filterLabels={filterComponents.map((filter) => {
186
- return filter.props['label'] || 'Label Not Found';
187
- })}
188
- selectedFilterLabels={finalFilterComponents.map(
189
- (filter) => filter.props['label'] || 'Label Not Found',
190
- )}
191
- handleCheckboxChange={function (
192
- label: string,
193
- checked: boolean,
194
- ): void {
195
- if (checked) {
196
- setHiddenLabels(hiddenLabels.filter((l) => label !== l));
197
- } else {
198
- setHiddenLabels([...hiddenLabels, label]);
199
- }
200
- }}
201
- handleReset={() => {
202
- setHiddenLabels(initialHiddenLabels);
203
- }}
204
- />
197
+ handleCheckboxChange={function (
198
+ label: string,
199
+ checked: boolean,
200
+ ): void {
201
+ if (checked) {
202
+ setHiddenLabels(hiddenLabels.filter((l) => label !== l));
203
+ } else {
204
+ setHiddenLabels([...hiddenLabels, label]);
205
+ }
206
+ }}
207
+ handleReset={() => {
208
+ setHiddenLabels(initialHiddenLabels);
209
+ }}
210
+ />
211
+ </Stack>
212
+ {viewsProps && uniqueId && (
213
+ <ViewsActions {...viewsProps} uniqueId={uniqueId} />
214
+ )}
205
215
  </Stack>
206
216
  )}
207
217
  </motion.div>
@@ -42,7 +42,7 @@ export const ManageFiltersMenuFooter = ({
42
42
  margin: '0px 16px',
43
43
  }}
44
44
  disableRipple
45
- color={theme.palette.text.tertiary}
45
+ textColor={theme.palette.text.tertiary}
46
46
  >
47
47
  Reset
48
48
  </Button>
@@ -51,7 +51,7 @@ export const TableColumnsSelectorMenuFooter = ({
51
51
  margin: '0px 16px',
52
52
  }}
53
53
  disableRipple
54
- color={theme.palette.text.tertiary}
54
+ textColor={theme.palette.text.tertiary}
55
55
  >
56
56
  Reset
57
57
  </Button>
@@ -0,0 +1,74 @@
1
+ import { yupResolver } from '@hookform/resolvers/yup';
2
+ import { useForm } from 'react-hook-form';
3
+ import { useMutation, useQueryClient } from 'react-query';
4
+ import { useSelector } from 'react-redux';
5
+ import { useSearchParams } from 'react-router-dom';
6
+ import * as yup from 'yup';
7
+ import { RootState } from '../../../../../redux/store';
8
+ import { FormControlWrapper, TextField } from '../../../../export';
9
+ import { ViewsProps } from './Views';
10
+
11
+ export type CreateViewFormProps = {
12
+ close: () => void;
13
+ } & ViewsProps;
14
+
15
+ export const CreateViewForm = ({
16
+ axios,
17
+ type,
18
+ close,
19
+ uniqueId,
20
+ }: CreateViewFormProps) => {
21
+ const schema = yup.object().shape({
22
+ name: yup.string().required('Name is required'),
23
+ });
24
+
25
+ const { control, handleSubmit } = useForm<{ name: string }>({
26
+ resolver: yupResolver(schema),
27
+ });
28
+ const [searchParams, setSearchParams] = useSearchParams();
29
+
30
+ const state = useSelector((state: RootState) => state.pageHeader[uniqueId]);
31
+
32
+ const createView = async (name: string) => {
33
+ return axios.post(`admin/views`, {
34
+ name,
35
+ type,
36
+ filters: state.filters,
37
+ columnVisibilityModel: state.columnVisibilityModel,
38
+ density: state.density,
39
+ });
40
+ };
41
+
42
+ const queryClient = useQueryClient();
43
+
44
+ const { mutate, isLoading } = useMutation(createView, {
45
+ onSuccess: (res) => {
46
+ close();
47
+ queryClient.invalidateQueries(type);
48
+ searchParams.set('viewId', res.data.viewId);
49
+ setSearchParams(searchParams);
50
+ },
51
+ });
52
+
53
+ const onSubmit = (data: { name: string }) => {
54
+ mutate(data.name);
55
+ };
56
+ return (
57
+ <FormControlWrapper
58
+ control={control}
59
+ formActionProps={{
60
+ submitButtonProps: {
61
+ title: 'Save',
62
+ onClick: handleSubmit(onSubmit),
63
+ loading: isLoading,
64
+ },
65
+ cancelButtonProps: {
66
+ title: 'Cancel',
67
+ onClick: close,
68
+ },
69
+ }}
70
+ >
71
+ <TextField name="name" label="View Name" />
72
+ </FormControlWrapper>
73
+ );
74
+ };
@@ -0,0 +1,113 @@
1
+ import { Axios } from 'axios';
2
+
3
+ import { Divider, Stack, useTheme } from '@mui/material';
4
+ import { useQuery } from 'react-query';
5
+ import { useDispatch } from 'react-redux';
6
+ import { useSearchParams } from 'react-router-dom';
7
+ import {
8
+ PageHeaderSingleState,
9
+ resetStateForUniqueId,
10
+ setViewForUniqueId,
11
+ } from '../../../../../redux/slices/pageHeaderSlice';
12
+ import { Icons, Spinner, Typography } from '../../../../export';
13
+
14
+ export type ViewsProps = {
15
+ type: string;
16
+ axios: Axios;
17
+ uniqueId: string;
18
+ };
19
+
20
+ export type View = {
21
+ id: string;
22
+ name: string;
23
+ slug: string;
24
+ type: string;
25
+ } & PageHeaderSingleState;
26
+
27
+ export const ViewTab = ({
28
+ title,
29
+ active,
30
+ onClick,
31
+ }: {
32
+ title: string;
33
+ active: boolean;
34
+ onClick: () => void;
35
+ }) => {
36
+ const theme = useTheme();
37
+ return (
38
+ <Stack onClick={onClick} sx={{ cursor: 'pointer' }} minWidth="80px">
39
+ <Stack direction="row" gap={1} alignItems="center" padding="8px 0px">
40
+ <Icons.ViewsIcon />
41
+ <Typography variant="subtitle3">{title}</Typography>
42
+ </Stack>
43
+
44
+ <Divider
45
+ style={{
46
+ background: theme.palette.tertiary.main,
47
+ height: '4px',
48
+ width: '100%',
49
+ borderRadius: '48px',
50
+ display: active ? 'block' : 'none',
51
+ }}
52
+ />
53
+ </Stack>
54
+ );
55
+ };
56
+
57
+ export const Views = ({ type, axios, uniqueId }: ViewsProps) => {
58
+ const [searchParams, setSearchParams] = useSearchParams();
59
+
60
+ const viewId = searchParams.get('viewId');
61
+ const dispatch = useDispatch();
62
+ const { data, isLoading } = useQuery(type, async (): Promise<View[]> => {
63
+ return axios
64
+ .get(`admin/views/user-views/${type}`)
65
+ .then((data) => data.data);
66
+ });
67
+
68
+ const resetState = () => {
69
+ dispatch(
70
+ resetStateForUniqueId({
71
+ uniqueId: uniqueId,
72
+ }),
73
+ );
74
+ };
75
+
76
+ const setViewState = (view: View) => {
77
+ dispatch(
78
+ setViewForUniqueId({
79
+ uniqueId: uniqueId,
80
+ view: view,
81
+ }),
82
+ );
83
+ };
84
+
85
+ if (isLoading) return <Spinner />;
86
+
87
+ return (
88
+ <Stack direction="row" gap={2} justifyContent="flex-start" width="100%">
89
+ <ViewTab
90
+ title="Default View"
91
+ active={!viewId}
92
+ onClick={() => {
93
+ resetState();
94
+ searchParams.delete('viewId');
95
+ setSearchParams(searchParams);
96
+ }}
97
+ />
98
+ {data &&
99
+ data.map((view: View, index: number) => (
100
+ <ViewTab
101
+ title={view.name}
102
+ active={view.id == viewId}
103
+ onClick={() => {
104
+ resetState();
105
+ setViewState(view);
106
+ searchParams.set('viewId', view.id);
107
+ setSearchParams(searchParams);
108
+ }}
109
+ />
110
+ ))}
111
+ </Stack>
112
+ );
113
+ };
@@ -0,0 +1,61 @@
1
+ import { useMutation, useQueryClient } from 'react-query';
2
+ import { useDispatch } from 'react-redux';
3
+ import { useSearchParams } from 'react-router-dom';
4
+ import { resetStateForUniqueId } from '../../../../../redux/slices/pageHeaderSlice';
5
+ import { Button, DialogButton } from '../../../../export';
6
+ import { CreateViewForm } from './CreateViewForm';
7
+ import { ViewsProps } from './Views';
8
+
9
+ export const ViewsActions = (viewsProps: ViewsProps) => {
10
+ const [searchParams, setSearchParams] = useSearchParams();
11
+
12
+ const dispatch = useDispatch();
13
+
14
+ const viewId = searchParams.get('viewId');
15
+
16
+ const deleteView = async () => {
17
+ return viewsProps.axios.delete(`admin/views/${viewId}`);
18
+ };
19
+ const queryClient = useQueryClient();
20
+ const resetState = () => {
21
+ dispatch(
22
+ resetStateForUniqueId({
23
+ uniqueId: viewsProps.uniqueId,
24
+ }),
25
+ );
26
+ };
27
+
28
+ const { mutate, isLoading } = useMutation(deleteView, {
29
+ onSuccess: () => {
30
+ queryClient.invalidateQueries(viewsProps.type);
31
+ resetState();
32
+ searchParams.delete('viewId');
33
+ setSearchParams(searchParams);
34
+ },
35
+ });
36
+ return viewId ? (
37
+ <Button
38
+ variant="outlined"
39
+ color="error"
40
+ loading={isLoading}
41
+ onClick={() => {
42
+ mutate();
43
+ }}
44
+ >
45
+ Delete View
46
+ </Button>
47
+ ) : (
48
+ <DialogButton
49
+ anchor={({ open }) => (
50
+ <Button variant="contained" color="secondary" onClick={open}>
51
+ Save View
52
+ </Button>
53
+ )}
54
+ title="Save View"
55
+ content={({ close }) => {
56
+ return <CreateViewForm close={close} {...viewsProps} />;
57
+ }}
58
+ maxWidth="sm"
59
+ />
60
+ );
61
+ };
@@ -59,7 +59,7 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
59
59
  close();
60
60
  }}
61
61
  variant="text"
62
- color={theme.palette.text.tertiary}
62
+ textColor={theme.palette.text.tertiary}
63
63
  >
64
64
  {cancelButtonText}
65
65
  </Button>
@@ -94,6 +94,18 @@ export const pageHeaderSlice = createSlice({
94
94
  },
95
95
  };
96
96
  },
97
+ setViewForUniqueId: (state, action) => {
98
+ const { uniqueId, view } = action.payload;
99
+ state[uniqueId] = {
100
+ ...state[uniqueId],
101
+ filters: {
102
+ ...state[uniqueId].filters,
103
+ ...view.filters,
104
+ },
105
+ columnVisibilityModel: view.columnVisibilityModel,
106
+ density: view.density,
107
+ };
108
+ },
97
109
  },
98
110
  });
99
111
 
@@ -106,4 +118,5 @@ export const {
106
118
  setLimitForUniqueId,
107
119
  setOffsetForUniqueId,
108
120
  setFilterByNameForUniqueId,
121
+ setViewForUniqueId,
109
122
  } = pageHeaderSlice.actions;
@@ -10,6 +10,7 @@ export const DarkColorTokens = {
10
10
  main: ColorPalette.BrandColors.Violet700,
11
11
  light: ColorPalette.BrandColors.Violet550,
12
12
  dark: ColorPalette.BrandColors.Violet900,
13
+ contrastText: ColorPalette.BrandColors.Violet400,
13
14
  },
14
15
  tertiary: { main: ColorPalette.BrandColors.Crimson },
15
16
  text: {
@@ -10,6 +10,7 @@ export const LightColorTokens = {
10
10
  main: ColorPalette.BrandColors.Violet200,
11
11
  light: ColorPalette.BrandColors.Violet100,
12
12
  dark: ColorPalette.BrandColors.Violet250,
13
+ contrastText: ColorPalette.BrandColors.Violet600,
13
14
  },
14
15
  tertiary: { main: ColorPalette.BrandColors.Crimson },
15
16
  text: {