@campxdev/react-blueprint 1.8.9 → 1.9.1

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.1",
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
  );
@@ -137,7 +121,13 @@ export const PageHeader = ({
137
121
  justifyContent="space-between"
138
122
  height="48px"
139
123
  >
140
- {isTableMode ? <ViewTab title="Default View" /> : viewsSlot}
124
+ {isTableMode && viewsProps ? (
125
+ <Views {...viewsProps} uniqueId={uniqueId} />
126
+ ) : (
127
+ <Stack direction="row" gap={2} justifyContent="flex-start">
128
+ {viewsSlot}
129
+ </Stack>
130
+ )}
141
131
 
142
132
  <Stack direction="row" alignItems="center">
143
133
  {isTableMode && (
@@ -166,42 +156,57 @@ export const PageHeader = ({
166
156
  />
167
157
  )}
168
158
  {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
- />
159
+ <Stack
160
+ direction="row"
161
+ width="100%"
162
+ alignItems="center"
163
+ justifyContent="space-between"
164
+ >
165
+ <Stack
166
+ key={viewId ?? 'default'}
167
+ direction={'row'}
168
+ alignItems="center"
169
+ gap={2}
170
+ >
171
+ {finalFilterComponents.map((filter, index) => (
172
+ <Fragment key={index}>
173
+ {wrapProps(filter)}
174
+ {index < finalFilterComponents.length - 1 && (
175
+ <Divider
176
+ orientation="vertical"
177
+ style={{
178
+ height: '20px',
179
+ background: theme.palette.border.primary,
180
+ }}
181
+ />
182
+ )}
183
+ </Fragment>
184
+ ))}
185
+ <ManageFilters
186
+ filterLabels={filterComponents.map((filter) => {
187
+ return filter.props['label'] || 'Label Not Found';
188
+ })}
189
+ selectedFilterLabels={finalFilterComponents.map(
190
+ (filter) => filter.props['label'] || 'Label Not Found',
181
191
  )}
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
- />
192
+ handleCheckboxChange={function (
193
+ label: string,
194
+ checked: boolean,
195
+ ): void {
196
+ if (checked) {
197
+ setHiddenLabels(hiddenLabels.filter((l) => label !== l));
198
+ } else {
199
+ setHiddenLabels([...hiddenLabels, label]);
200
+ }
201
+ }}
202
+ handleReset={() => {
203
+ setHiddenLabels(initialHiddenLabels);
204
+ }}
205
+ />
206
+ </Stack>
207
+ {viewsProps && uniqueId && (
208
+ <ViewsActions {...viewsProps} uniqueId={uniqueId} />
209
+ )}
205
210
  </Stack>
206
211
  )}
207
212
  </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">
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: {