@applica-software-guru/react-admin 1.3.142 → 1.3.144

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": "@applica-software-guru/react-admin",
3
- "version": "1.3.142",
3
+ "version": "1.3.144",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,5 @@
1
1
  import { Dialog, useMediaQuery, useTheme } from '@mui/material';
2
- import { UseGetIdentityResult, useGetIdentity } from 'ra-core';
2
+ import { UseGetIdentityResult, useGetIdentity, useAuthProvider } from 'ra-core';
3
3
  import { createContext, forwardRef, useCallback, useContext, useEffect, useMemo, useReducer, useState } from 'react';
4
4
  import { useMenuConfig, useThemeConfig } from '../../hooks';
5
5
 
@@ -130,6 +130,7 @@ const DefaultState: ILayoutState = {
130
130
  LayoutContext = createContext<ILayoutContext | undefined>(undefined);
131
131
 
132
132
  function LayoutProvider(props: ILayoutProviderProps) {
133
+ const authProvider = useAuthProvider();
133
134
  const identity = useGetIdentity() as UseGetIdentityResult,
134
135
  theme = useTheme(),
135
136
  downMd = useMediaQuery(theme.breakpoints.down('md')),
@@ -215,8 +216,10 @@ function LayoutProvider(props: ILayoutProviderProps) {
215
216
  }, [theme.palette.mode]);
216
217
 
217
218
  useEffect(() => {
218
- setNeedToChangePassword(identity?.data?.needToChangePassword === true);
219
- }, [identity?.data?.needToChangePassword]);
219
+ authProvider.isImpersonating().then((isImpersonating: boolean) => {
220
+ setNeedToChangePassword(!isImpersonating && identity?.data?.needToChangePassword === true);
221
+ });
222
+ }, [identity?.data?.needToChangePassword, authProvider]);
220
223
 
221
224
  return (
222
225
  <LayoutContext.Provider value={value}>
@@ -12,11 +12,12 @@ type IFormProps = IProviderProps &
12
12
  spacing?: number;
13
13
  tabsDisposition: Disposition;
14
14
  contentDisposition: Disposition;
15
+ errorCount: boolean;
15
16
  };
16
17
 
17
18
  function Form(props: IFormProps) {
18
19
  const { tabsDisposition, contentDisposition, spacing = 2 } = props,
19
- providerProps = _.pick(props, ['syncWithLocation']),
20
+ providerProps = _.pick(props, ['syncWithLocation', 'errorCount']),
20
21
  baseFormProps = _.omit(props, ['tabsDisposition', 'contentDisposition', 'syncWithLocation']),
21
22
  { top: sidebarTop, bottom: sidebarBottom } = useSidebarChildren(props),
22
23
  contentChildren = useBaseItemChildren(props);
@@ -1,6 +1,17 @@
1
1
  import _ from 'lodash';
2
2
  import { Avatar } from '../../@extended';
3
- import { Card, Collapse, List, ListItem, ListItemProps, ListItemIcon, ListItemText, ListItemButton, ListItemAvatar } from '@mui/material';
3
+ import {
4
+ Card,
5
+ Collapse,
6
+ List,
7
+ ListItem,
8
+ ListItemProps,
9
+ ListItemIcon,
10
+ ListItemText,
11
+ ListItemButton,
12
+ ListItemAvatar,
13
+ Badge
14
+ } from '@mui/material';
4
15
  import { useSetActiveItem, useSyncWithLocation } from './Provider';
5
16
  import { IItem } from './types';
6
17
  import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -44,7 +55,7 @@ function NavMenu() {
44
55
  function NavMenuItem(props: INavMenuItemProps) {
45
56
  const translate = useTranslate() ?? _.identity,
46
57
  navMenuItemProps = useNavMenuItem(props),
47
- { selected, label, icon, onClick: _onClick, badge, items, isGroup, level } = navMenuItemProps,
58
+ { selected, label, icon, onClick: _onClick, badge, items, isGroup, level, errors = 0 } = navMenuItemProps,
48
59
  { spacing } = useThemeConfig(),
49
60
  [open, setOpen] = useState(selected),
50
61
  onClick = useCallback(
@@ -68,7 +79,18 @@ function NavMenuItem(props: INavMenuItemProps) {
68
79
  <ListItem {...listItemProps} disablePadding>
69
80
  <ListItemButton selected={selected && !isGroup} onClick={onClick} sx={{ pl: spacing * level }}>
70
81
  {hasIcon && <ListItemIcon>{icon}</ListItemIcon>}
71
- <ListItemText inset={!hasIcon} primary={translate(label)} />
82
+ <ListItemText
83
+ inset={!hasIcon}
84
+ primary={
85
+ errors === 0 ? (
86
+ translate(label)
87
+ ) : (
88
+ <Badge variant="dot" color="error" sx={{ '& > .MuiBadge-badge': { transform: 'scale(1) translate(125%, 0)' } }}>
89
+ {translate(label)}
90
+ </Badge>
91
+ )
92
+ }
93
+ />
72
94
  {badge && (
73
95
  <ListItemAvatar>
74
96
  <Avatar type="filled" size="xs" color={badge.color ?? 'default'}>
@@ -10,16 +10,19 @@ enum ActionType {
10
10
  SET_SYNC_WITH_LOCATION = 'setSyncWithLocation',
11
11
  SET_ACTIVE_ITEM = 'setActiveItem',
12
12
  ADD_ITEM = 'addItem',
13
- REMOVE_ITEM = 'removeItem'
13
+ REMOVE_ITEM = 'removeItem',
14
+ SET_ERROR_COUNT = 'setErrorCount'
14
15
  }
15
16
 
16
17
  type IProviderProps = React.PropsWithChildren<{
17
18
  syncWithLocation?: boolean;
18
19
  rootMatchString?: string;
20
+ errorCount?: boolean;
19
21
  }>;
20
22
  type IState = {
21
23
  formRootPath?: string;
22
24
  syncWithLocation: boolean;
25
+ errorCount: boolean;
23
26
  items: Array<IItem>;
24
27
  activeItem?: string;
25
28
  };
@@ -32,6 +35,10 @@ type IAction =
32
35
  type: ActionType.SET_SYNC_WITH_LOCATION;
33
36
  payload: boolean;
34
37
  }
38
+ | {
39
+ type: ActionType.SET_ERROR_COUNT;
40
+ payload: boolean;
41
+ }
35
42
  | {
36
43
  type: ActionType.SET_ACTIVE_ITEM;
37
44
  payload?: string;
@@ -58,6 +65,8 @@ function reducer(state: IState, action: IAction): IState {
58
65
  return _.extend(newState, { formRootPath: payload });
59
66
  case ActionType.SET_SYNC_WITH_LOCATION:
60
67
  return _.extend(newState, { syncWithLocation: payload });
68
+ case ActionType.SET_ERROR_COUNT:
69
+ return _.extend(newState, { errorCount: payload });
61
70
  case ActionType.SET_ACTIVE_ITEM:
62
71
  return _.includes(getItemsIds(state.items), payload) ? _.extend(newState, { activeItem: payload }) : newState;
63
72
  case ActionType.ADD_ITEM: {
@@ -91,6 +100,7 @@ function reducer(state: IState, action: IAction): IState {
91
100
 
92
101
  const DefaultState: IState = {
93
102
  syncWithLocation: true,
103
+ errorCount: true,
94
104
  items: [],
95
105
  activeItem: undefined
96
106
  };
@@ -99,6 +109,7 @@ const Context = createContext<IContext | undefined>(undefined);
99
109
 
100
110
  function Provider(props: IProviderProps) {
101
111
  const syncWithLocation = Boolean(props.syncWithLocation ?? true),
112
+ errorCount = Boolean(props.errorCount ?? true),
102
113
  { rootMatchString } = props,
103
114
  { pathname } = useLocation(),
104
115
  [state, dispatch] = useReducer(reducer, _.cloneDeep(DefaultState)),
@@ -140,6 +151,13 @@ function Provider(props: IProviderProps) {
140
151
  });
141
152
  }, [syncWithLocation, dispatch]);
142
153
 
154
+ useEffect(() => {
155
+ dispatch({
156
+ type: ActionType.SET_ERROR_COUNT,
157
+ payload: Boolean(errorCount)
158
+ });
159
+ }, [errorCount, dispatch]);
160
+
143
161
  useEffect(() => {
144
162
  if (syncWithLocation && formRootPath !== undefined) {
145
163
  let locationItem = pathname.replace(formRootPath, '').replace(new RegExp(/^\/?/), '');
@@ -201,6 +219,11 @@ function useSyncWithLocation(): boolean {
201
219
  return state.syncWithLocation;
202
220
  }
203
221
 
222
+ function useErrorCount(): boolean {
223
+ const state = useFormState();
224
+ return state.errorCount;
225
+ }
226
+
204
227
  function useSetActiveItem(): (activeItem: string) => void {
205
228
  const dispatch = useFormDispatch(),
206
229
  setActiveItem = useCallback(
@@ -246,5 +269,15 @@ function useRemoveItem(): (item: string | IItem) => void {
246
269
  return removeItem;
247
270
  }
248
271
 
249
- export { Provider, useItems, useSyncWithLocation, useFormRootPath, useActiveItem, useSetActiveItem, useAddItem, useRemoveItem };
272
+ export {
273
+ Provider,
274
+ useItems,
275
+ useSyncWithLocation,
276
+ useFormRootPath,
277
+ useActiveItem,
278
+ useSetActiveItem,
279
+ useAddItem,
280
+ useRemoveItem,
281
+ useErrorCount
282
+ };
250
283
  export type { IProviderProps };
@@ -1,22 +1,53 @@
1
1
  import _ from 'lodash';
2
2
  import { Box } from '@mui/material';
3
- import React, { Children, cloneElement, isValidElement, useEffect, useMemo } from 'react';
4
- import { useAddItem, useRemoveItem } from './Provider';
3
+ import React, { Children, ReactElement, ReactNode, cloneElement, isValidElement, useEffect, useMemo } from 'react';
4
+ import { useAddItem, useRemoveItem, useErrorCount } from './Provider';
5
5
  import { Optional } from 'src/types';
6
6
  import { IItem } from './types';
7
7
  import { useIsActive } from './hooks';
8
8
  import { getId } from './utils';
9
+ import { useFormState } from 'react-hook-form';
9
10
 
10
- type IBaseItemProps = React.PropsWithChildren<Optional<IItem, 'id' | 'index'>>;
11
+ type IBaseItemProps = React.PropsWithChildren<Optional<IItem, 'id' | 'index'> & { sources: Array<string> }>;
11
12
  type ITabProps = IBaseItemProps;
12
13
  type IGroupProps = IBaseItemProps;
13
14
 
15
+ function walkChildren(children: ReactNode = [], callback: (el: ReactElement) => void) {
16
+ const _children = _.isArray(children) ? children : [children],
17
+ validChildren = _.filter(_children, (child) => isValidElement(child));
18
+ _.each(validChildren, (child) => {
19
+ callback(child);
20
+ walkChildren(child?.props?.children ?? [], callback);
21
+ });
22
+ }
23
+
14
24
  function BaseItem(props: IBaseItemProps) {
15
- const { label, icon, badge, index = 0 } = props,
25
+ const { errors } = useFormState(),
26
+ countErrors = useErrorCount(),
27
+ { label, icon, badge, index = 0, children } = props,
16
28
  id = getId(props),
17
29
  addItem = useAddItem(),
18
30
  removeItem = useRemoveItem(),
19
- visible = useIsActive(id);
31
+ visible = useIsActive(id),
32
+ sources: Array<string> = [];
33
+
34
+ if (countErrors) {
35
+ if (props.sources !== undefined) {
36
+ sources.push(...props.sources);
37
+ } else {
38
+ walkChildren(children, (el) => {
39
+ if (el?.props?.source !== undefined) {
40
+ sources.push(el.props.source);
41
+ }
42
+ });
43
+ }
44
+ }
45
+
46
+ const errorsCount = _.chain(sources)
47
+ .uniq()
48
+ .map((s) => _.get(errors, s))
49
+ .reject((s) => s === undefined)
50
+ .value().length;
20
51
 
21
52
  useEffect(() => {
22
53
  addItem({
@@ -24,12 +55,13 @@ function BaseItem(props: IBaseItemProps) {
24
55
  index: index,
25
56
  label: label,
26
57
  icon: icon,
27
- badge: badge
58
+ badge: badge,
59
+ errors: errorsCount
28
60
  });
29
61
  return () => {
30
62
  removeItem(id);
31
63
  };
32
- }, [addItem, removeItem, label, icon, id, badge, index]);
64
+ }, [addItem, removeItem, label, icon, id, badge, index, errorsCount]);
33
65
 
34
66
  /* All tabs are rendered (not only the one in focus), to allow validation
35
67
  on tabs not in focus. The tabs receive a `hidden` property, which they'll
@@ -6,6 +6,7 @@ import {
6
6
  useActiveItem,
7
7
  useItems,
8
8
  useSyncWithLocation,
9
+ useErrorCount,
9
10
  useSetActiveItem,
10
11
  useAddItem,
11
12
  useRemoveItem,
@@ -21,6 +22,7 @@ type IForm = typeof Form & {
21
22
  Provider: typeof Provider;
22
23
  useItems: typeof useItems;
23
24
  useSyncWithLocation: typeof useSyncWithLocation;
25
+ useErrorCount: typeof useErrorCount;
24
26
  useFormRootPath: typeof useFormRootPath;
25
27
  useActiveItem: typeof useActiveItem;
26
28
  useSetActiveItem: typeof useSetActiveItem;
@@ -44,6 +46,7 @@ DefaultForm.Content = Content;
44
46
  DefaultForm.Provider = Provider;
45
47
  DefaultForm.useItems = useItems;
46
48
  DefaultForm.useSyncWithLocation = useSyncWithLocation;
49
+ DefaultForm.useErrorCount = useErrorCount;
47
50
  DefaultForm.useFormRootPath = useFormRootPath;
48
51
  DefaultForm.useActiveItem = useActiveItem;
49
52
  DefaultForm.useSetActiveItem = useSetActiveItem;
@@ -15,6 +15,7 @@ type IItem = {
15
15
  content: string | number;
16
16
  color?: 'default' | 'error' | 'info' | 'primary' | 'secondary' | 'success' | 'warning';
17
17
  };
18
+ errors?: number;
18
19
  };
19
20
 
20
21
  export type { Disposition, IItem };