@centreon/ui 23.10.39 → 23.10.40

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.
Files changed (57) hide show
  1. package/package.json +2 -1
  2. package/src/ThemeProvider/index.tsx +0 -24
  3. package/src/ThemeProvider/palettes.ts +36 -6
  4. package/src/api/useMutationQuery/index.ts +16 -6
  5. package/src/components/Form/AccessRights/AccessRights.resource.ts +45 -0
  6. package/src/components/Form/AccessRights/AccessRightsForm.stories.tsx +59 -0
  7. package/src/components/Form/AccessRights/AccessRightsForm.styles.ts +21 -0
  8. package/src/components/Form/AccessRights/AccessRightsForm.tsx +67 -0
  9. package/src/components/Form/AccessRights/AccessRightsFormActions.tsx +80 -0
  10. package/src/components/Form/AccessRights/Input/AddAction.tsx +31 -0
  11. package/src/components/Form/AccessRights/Input/ContactAccessRightInput.stories.tsx +54 -0
  12. package/src/components/Form/AccessRights/Input/ContactAccessRightInput.tsx +72 -0
  13. package/src/components/Form/AccessRights/Input/ContactAccessRightsInput.styles.ts +22 -0
  14. package/src/components/Form/AccessRights/Input/ContactInputField.tsx +105 -0
  15. package/src/components/Form/AccessRights/Input/RoleInputField.tsx +29 -0
  16. package/src/components/Form/AccessRights/List/ContactAccessRightsList.stories.tsx +97 -0
  17. package/src/components/Form/AccessRights/List/ContactAccessRightsList.styles.ts +71 -0
  18. package/src/components/Form/AccessRights/List/ContactAccessRightsList.tsx +51 -0
  19. package/src/components/Form/AccessRights/List/ContactAccessRightsListItem.stories.tsx +116 -0
  20. package/src/components/Form/AccessRights/List/ContactAccessRightsListItem.tsx +118 -0
  21. package/src/components/Form/AccessRights/List/ContactAccessRightsListItemSkeleton.tsx +26 -0
  22. package/src/components/Form/AccessRights/List/ContactAccessRightsListSkeleton.tsx +28 -0
  23. package/src/components/Form/AccessRights/Stats/AccessRightsStats.styles.ts +18 -0
  24. package/src/components/Form/AccessRights/Stats/AccessRightsStats.tsx +41 -0
  25. package/src/components/Form/AccessRights/__fixtures__/contactAccessRight.mock.ts +52 -0
  26. package/src/components/Form/AccessRights/common/GroupLabel.styles.ts +18 -0
  27. package/src/components/Form/AccessRights/common/GroupLabel.tsx +15 -0
  28. package/src/components/Form/AccessRights/common/Input.styles.ts +48 -0
  29. package/src/components/Form/AccessRights/common/RoleInputSelect.styles.ts +11 -0
  30. package/src/components/Form/AccessRights/common/RoleInputSelect.tsx +57 -0
  31. package/src/components/Form/AccessRights/index.ts +3 -0
  32. package/src/components/Form/AccessRights/useAccessRightsForm.test.tsx +531 -0
  33. package/src/components/Form/AccessRights/useAccessRightsForm.tsx +282 -0
  34. package/src/components/Form/AccessRights/useAccessRightsForm.utils.ts +41 -0
  35. package/src/components/Form/Dashboard/DashboardForm.styles.ts +1 -7
  36. package/src/components/Form/Dashboard/DashboardForm.tsx +9 -12
  37. package/src/components/Form/Form.resource.ts +1 -0
  38. package/src/components/Form/Form.styles.ts +17 -0
  39. package/src/components/Form/{Dashboard/DashboardFormActions.tsx → FormActions.tsx} +17 -16
  40. package/src/components/Form/index.ts +1 -0
  41. package/src/components/Header/PageHeader/PageHeader.stories.tsx +9 -0
  42. package/src/components/List/Item/Avatar.tsx +21 -0
  43. package/src/components/List/Item/AvatarSkeleton.tsx +17 -0
  44. package/src/components/List/Item/ListItem.stories.tsx +41 -0
  45. package/src/components/List/Item/ListItem.styles.ts +57 -0
  46. package/src/components/List/Item/ListItem.tsx +32 -0
  47. package/src/components/List/Item/Text.tsx +25 -0
  48. package/src/components/List/Item/TextSkeleton.tsx +22 -0
  49. package/src/components/List/Item/index.ts +14 -0
  50. package/src/components/List/List.stories.tsx +70 -0
  51. package/src/components/List/List.styles.ts +8 -0
  52. package/src/components/List/List.tsx +19 -0
  53. package/src/components/List/index.ts +6 -0
  54. package/src/components/Modal/ModalActions.tsx +4 -1
  55. package/src/components/index.ts +1 -0
  56. package/src/utils/index.ts +1 -0
  57. package/src/utils/useInfiniteScrollListing.ts +106 -0
@@ -0,0 +1,282 @@
1
+ import { ReactElement, ReactNode, useEffect, useRef } from 'react';
2
+
3
+ import {
4
+ atom,
5
+ createStore,
6
+ Provider,
7
+ useAtom,
8
+ useAtomValue,
9
+ useSetAtom,
10
+ useStore
11
+ } from 'jotai';
12
+
13
+ import {
14
+ ContactAccessRightResource,
15
+ ContactAccessRightStateResource,
16
+ ContactGroupResource,
17
+ ContactResource,
18
+ RoleResource
19
+ } from './AccessRights.resource';
20
+ import {
21
+ createInitialState,
22
+ sortOnAddedStateFirstAndContactName
23
+ } from './useAccessRightsForm.utils';
24
+
25
+ /** state */
26
+
27
+ type loadingState = 'idle' | 'loading';
28
+
29
+ const loadingStatusAtom = atom<loadingState>('idle');
30
+
31
+ type formOptions = {
32
+ contacts: Array<ContactResource | ContactGroupResource>;
33
+ roles: Array<RoleResource>;
34
+ };
35
+
36
+ const formOptionsAtom = atom<formOptions>({
37
+ contacts: [],
38
+ roles: []
39
+ });
40
+
41
+ const contactAccessRightsAtom = atom<Array<ContactAccessRightStateResource>>(
42
+ []
43
+ );
44
+
45
+ type StateStats = {
46
+ added: number;
47
+ removed: number;
48
+ updated: number;
49
+ };
50
+
51
+ const contactAccessRightsStateAtom = atom<{
52
+ isDirty: boolean;
53
+ isLoading: boolean;
54
+ stats: StateStats;
55
+ }>((get) => ({
56
+ isDirty: get(contactAccessRightsAtom).some(
57
+ ({ state }) => state !== 'unchanged'
58
+ ),
59
+ isLoading: get(loadingStatusAtom) === 'loading',
60
+ stats: {
61
+ added: get(contactAccessRightsAtom).filter(({ state }) => state === 'added')
62
+ .length,
63
+ removed: get(contactAccessRightsAtom).filter(
64
+ ({ state }) => state === 'removed'
65
+ ).length,
66
+ updated: get(contactAccessRightsAtom).filter(
67
+ ({ state }) => state === 'updated'
68
+ ).length
69
+ }
70
+ }));
71
+
72
+ type callbacks = {
73
+ onSubmit?: (values: Array<ContactAccessRightStateResource>) => void;
74
+ };
75
+
76
+ const callbacksAtom = atom<callbacks>({});
77
+
78
+ /** provider */
79
+
80
+ export type AccessRightsFormProviderProps = {
81
+ children: ReactNode;
82
+ initialValues?: Array<ContactAccessRightResource>;
83
+ loadingStatus?: loadingState;
84
+ onSubmit?: callbacks['onSubmit'];
85
+ options?: formOptions;
86
+ };
87
+
88
+ const AccessRightsFormProvider = ({
89
+ children,
90
+ initialValues,
91
+ options,
92
+ loadingStatus = 'idle',
93
+ onSubmit
94
+ }: AccessRightsFormProviderProps): ReactElement => {
95
+ const store = useRef(createStore()).current;
96
+
97
+ const setLoadingStatus = useSetAtom(loadingStatusAtom, { store });
98
+ const setFormOptions = useSetAtom(formOptionsAtom, { store });
99
+ const setContactAccessRights = useSetAtom(contactAccessRightsAtom, { store });
100
+ const setCallbacks = useSetAtom(callbacksAtom, { store });
101
+
102
+ useEffect(() => {
103
+ setLoadingStatus(loadingStatus);
104
+ }, [loadingStatus]);
105
+
106
+ useEffect(() => {
107
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
108
+ options && setFormOptions(options);
109
+ }, [options]);
110
+
111
+ useEffect(() => {
112
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
113
+ onSubmit && setCallbacks({ onSubmit });
114
+ }, [onSubmit]);
115
+
116
+ useEffect(() => {
117
+ setContactAccessRights(
118
+ initialValues ? createInitialState(initialValues) : []
119
+ );
120
+ }, [initialValues]);
121
+
122
+ return <Provider store={store}>{children}</Provider>;
123
+ };
124
+
125
+ /** hook */
126
+
127
+ type UseAccessRightsForm = {
128
+ addContactAccessRight: (
129
+ contactAccessRight: ContactAccessRightResource
130
+ ) => void;
131
+ contactAccessRights: Array<ContactAccessRightStateResource>;
132
+ isDirty: boolean;
133
+ isLoading: boolean;
134
+ options: formOptions;
135
+ removeContactAccessRight: (
136
+ contactAccessRight: ContactAccessRightResource,
137
+ restore?: boolean
138
+ ) => void;
139
+ stats: StateStats;
140
+ submit: () => void;
141
+ updateContactAccessRight: (
142
+ contactAccessRight: ContactAccessRightResource
143
+ ) => void;
144
+ };
145
+
146
+ const useAccessRightsForm = (): UseAccessRightsForm => {
147
+ const store = useStore();
148
+
149
+ const [contactAccessRights, setContactAccessRights] = useAtom(
150
+ contactAccessRightsAtom,
151
+ { store }
152
+ );
153
+ const { isLoading, isDirty, stats } = useAtomValue(
154
+ contactAccessRightsStateAtom,
155
+ { store }
156
+ );
157
+ const options = useAtomValue(formOptionsAtom, { store });
158
+ const callbacks = useAtomValue(callbacksAtom, { store });
159
+
160
+ const addContactAccessRight = (
161
+ contactAccessRight: ContactAccessRightResource
162
+ ): void =>
163
+ setContactAccessRights((prev) => {
164
+ const isContactAlreadyAdded = prev.some(
165
+ ({ contactAccessRight: c }) =>
166
+ c.contact?.id === contactAccessRight.contact?.id
167
+ );
168
+
169
+ return isContactAlreadyAdded
170
+ ? prev
171
+ : [
172
+ ...prev,
173
+ {
174
+ contactAccessRight,
175
+ state: 'added',
176
+ stateHistory: []
177
+ } as ContactAccessRightStateResource
178
+ ].sort(sortOnAddedStateFirstAndContactName);
179
+ });
180
+
181
+ const removeContactAccessRight = (
182
+ contactAccessRight: ContactAccessRightResource,
183
+ restore = false
184
+ ): void =>
185
+ setContactAccessRights((prev) => {
186
+ const contactAccessRightIndex = prev.findIndex(
187
+ ({ contactAccessRight: prevContactAccessRight }) =>
188
+ prevContactAccessRight.contact?.id === contactAccessRight.contact?.id
189
+ );
190
+
191
+ if (contactAccessRightIndex === -1) {
192
+ return prev;
193
+ }
194
+
195
+ const { stateHistory } = prev[contactAccessRightIndex];
196
+
197
+ if (restore) {
198
+ return [
199
+ ...prev.slice(0, contactAccessRightIndex),
200
+ {
201
+ ...prev[contactAccessRightIndex],
202
+ state: stateHistory[stateHistory.length - 1],
203
+ stateHistory: [...stateHistory, prev[contactAccessRightIndex].state]
204
+ } as ContactAccessRightStateResource,
205
+ ...prev.slice(contactAccessRightIndex + 1)
206
+ ];
207
+ }
208
+
209
+ if (
210
+ prev[contactAccessRightIndex].state === 'added' ||
211
+ stateHistory[0] === 'added'
212
+ ) {
213
+ return [
214
+ ...prev.slice(0, contactAccessRightIndex),
215
+ ...prev.slice(contactAccessRightIndex + 1)
216
+ ];
217
+ }
218
+
219
+ return [
220
+ ...prev.slice(0, contactAccessRightIndex),
221
+ {
222
+ ...prev[contactAccessRightIndex],
223
+ state: 'removed',
224
+ stateHistory: [...stateHistory, prev[contactAccessRightIndex].state]
225
+ } as ContactAccessRightStateResource,
226
+ ...prev.slice(contactAccessRightIndex + 1)
227
+ ];
228
+ });
229
+
230
+ const updateContactAccessRight = (
231
+ contactAccessRight: ContactAccessRightResource
232
+ ): void =>
233
+ setContactAccessRights((prev) => {
234
+ const contactAccessRightIndex = prev.findIndex(
235
+ ({ contactAccessRight: prevContactAccessRight }) =>
236
+ prevContactAccessRight.contact?.id === contactAccessRight.contact?.id
237
+ );
238
+
239
+ if (contactAccessRightIndex === -1) {
240
+ return prev;
241
+ }
242
+
243
+ const { stateHistory } = prev[contactAccessRightIndex];
244
+
245
+ const isAdded =
246
+ prev[contactAccessRightIndex].state === 'added' ||
247
+ stateHistory[0] === 'added';
248
+ const isUpdated =
249
+ prev[contactAccessRightIndex].state === 'updated' ||
250
+ stateHistory.findIndex((state) => state === 'updated') <
251
+ stateHistory.findIndex((state) => state === 'unchanged');
252
+
253
+ // eslint-disable-next-line no-nested-ternary
254
+ const state = isAdded ? 'added' : isUpdated ? 'unchanged' : 'updated';
255
+
256
+ return [
257
+ ...prev.slice(0, contactAccessRightIndex),
258
+ {
259
+ contactAccessRight,
260
+ state,
261
+ stateHistory: [...stateHistory, prev[contactAccessRightIndex].state]
262
+ } as ContactAccessRightStateResource,
263
+ ...prev.slice(contactAccessRightIndex + 1)
264
+ ];
265
+ });
266
+
267
+ const submit = (): void => callbacks.onSubmit?.(contactAccessRights);
268
+
269
+ return {
270
+ addContactAccessRight,
271
+ contactAccessRights,
272
+ isDirty,
273
+ isLoading,
274
+ options,
275
+ removeContactAccessRight,
276
+ stats,
277
+ submit,
278
+ updateContactAccessRight
279
+ };
280
+ };
281
+
282
+ export { AccessRightsFormProvider, useAccessRightsForm };
@@ -0,0 +1,41 @@
1
+ import { ContactAccessRightStateResource } from './AccessRights.resource';
2
+
3
+ export const sortOnContactName = (
4
+ a: ContactAccessRightStateResource,
5
+ b: ContactAccessRightStateResource
6
+ ): number =>
7
+ a.contactAccessRight.contact?.name.localeCompare(
8
+ b.contactAccessRight.contact?.name ?? ''
9
+ ) || 0;
10
+
11
+ export const sortOnAddedStateFirstAndContactName = (
12
+ a: ContactAccessRightStateResource,
13
+ b: ContactAccessRightStateResource
14
+ ): number => {
15
+ if (a.state === 'added' && b.state !== 'added') {
16
+ return -1;
17
+ }
18
+ if (a.state !== 'added' && b.state === 'added') {
19
+ return 1;
20
+ }
21
+
22
+ return (
23
+ a.contactAccessRight.contact?.name.localeCompare(
24
+ b.contactAccessRight.contact?.name ?? ''
25
+ ) || 0
26
+ );
27
+ };
28
+
29
+ export const createInitialState = (
30
+ values
31
+ ): Array<ContactAccessRightStateResource> =>
32
+ values
33
+ ?.map(
34
+ (contactAccessRight) =>
35
+ ({
36
+ contactAccessRight,
37
+ state: 'unchanged',
38
+ stateHistory: []
39
+ } as ContactAccessRightStateResource)
40
+ )
41
+ .sort(sortOnAddedStateFirstAndContactName);
@@ -1,12 +1,6 @@
1
1
  import { makeStyles } from 'tss-react/mui';
2
2
 
3
- const useStyles = makeStyles()((theme) => ({
4
- actions: {
5
- display: 'flex',
6
- flexDirection: 'row',
7
- gap: theme.spacing(2),
8
- justifyContent: 'flex-end'
9
- },
3
+ const useStyles = makeStyles()(() => ({
10
4
  dashboardForm: {
11
5
  display: 'flex',
12
6
  flexDirection: 'column',
@@ -1,4 +1,4 @@
1
- import React, { ReactElement, useCallback, useMemo } from 'react';
1
+ import { ReactElement, useCallback, useMemo } from 'react';
2
2
 
3
3
  import * as Yup from 'yup';
4
4
  import { useTranslation } from 'react-i18next';
@@ -6,18 +6,15 @@ import { useTranslation } from 'react-i18next';
6
6
  import { InputType } from '../../../Form/Inputs/models';
7
7
  import { Form, FormProps } from '../../../Form';
8
8
  import { FormVariant } from '../Form.models';
9
+ import { FormActions, FormActionsProps } from '../FormActions';
9
10
 
10
11
  import { useStyles } from './DashboardForm.styles';
11
12
  import {
13
+ labelCharacters,
12
14
  labelMustBeAtLeast,
13
15
  labelMustBeMost,
14
- labelRequired,
15
- labelCharacters
16
+ labelRequired
16
17
  } from './translatedLabels';
17
- import {
18
- DashboardFormActions,
19
- DashboardFormActionsProps
20
- } from './DashboardFormActions';
21
18
  import { DashboardResource } from './Dashboard.resource';
22
19
 
23
20
  export type DashboardFormProps = {
@@ -25,10 +22,10 @@ export type DashboardFormProps = {
25
22
  onSubmit?: FormProps<DashboardResource>['submit'];
26
23
  resource?: DashboardResource;
27
24
  variant?: FormVariant;
28
- } & Pick<DashboardFormActionsProps, 'onCancel'>;
25
+ } & Pick<FormActionsProps, 'onCancel'>;
29
26
 
30
27
  export type DashboardFormLabels = {
31
- actions: DashboardFormActionsProps['labels'];
28
+ actions: FormActionsProps['labels'];
32
29
  entity: Required<DashboardResource>;
33
30
  };
34
31
 
@@ -83,9 +80,9 @@ const DashboardForm = ({
83
80
  [resource, labels, onSubmit]
84
81
  );
85
82
 
86
- const FormActions = useCallback(
83
+ const Actions = useCallback(
87
84
  () => (
88
- <DashboardFormActions
85
+ <FormActions<DashboardResource>
89
86
  labels={labels?.actions}
90
87
  variant={variant}
91
88
  onCancel={onCancel}
@@ -96,7 +93,7 @@ const DashboardForm = ({
96
93
 
97
94
  return (
98
95
  <div className={classes.dashboardForm}>
99
- <Form<DashboardResource> {...formProps} Buttons={FormActions} />
96
+ <Form<DashboardResource> {...formProps} Buttons={Actions} />
100
97
  </div>
101
98
  );
102
99
  };
@@ -0,0 +1 @@
1
+ export type GenericResource = unknown;
@@ -0,0 +1,17 @@
1
+ import { makeStyles } from 'tss-react/mui';
2
+
3
+ const useStyles = makeStyles()((theme) => ({
4
+ actions: {
5
+ '&, & > span': {
6
+ display: 'flex',
7
+ flexDirection: 'row',
8
+ gap: theme.spacing(2)
9
+ },
10
+ '&:has( > span)': {
11
+ justifyContent: 'space-between'
12
+ },
13
+ justifyContent: 'flex-end'
14
+ }
15
+ }));
16
+
17
+ export { useStyles };
@@ -1,48 +1,49 @@
1
- import React, { ReactElement } from 'react';
1
+ import { ReactElement } from 'react';
2
2
 
3
3
  import { useFormikContext } from 'formik';
4
4
 
5
- import { Button } from '../../Button';
6
- import { FormVariant } from '../Form.models';
5
+ import { Button } from '../Button';
7
6
 
8
- import { useStyles } from './DashboardForm.styles';
9
- import { DashboardResource } from './Dashboard.resource';
7
+ import { FormVariant } from './Form.models';
8
+ import { useStyles } from './Form.styles';
10
9
 
11
- export type DashboardFormActionsProps = {
12
- labels: DashboardFormActionsLabels;
10
+ export type FormActionsProps = {
11
+ labels: FormActionsLabels;
13
12
  onCancel: () => void;
14
13
  variant: FormVariant;
15
14
  };
16
15
 
17
- export type DashboardFormActionsLabels = {
16
+ export type FormActionsLabels = {
18
17
  cancel: string;
19
18
  submit: Record<FormVariant, string>;
20
19
  };
21
20
 
22
- const DashboardFormActions = ({
21
+ const FormActions = <TResource extends object>({
23
22
  labels,
24
23
  onCancel,
25
24
  variant
26
- }: DashboardFormActionsProps): ReactElement => {
25
+ }: FormActionsProps): ReactElement => {
27
26
  const { classes } = useStyles();
28
27
  const { isSubmitting, dirty, isValid, submitForm } =
29
- useFormikContext<DashboardResource>();
28
+ useFormikContext<TResource>();
30
29
 
31
30
  return (
32
31
  <div className={classes.actions}>
33
32
  <Button
34
- aria-label="cancel"
33
+ aria-label={labels.cancel}
34
+ data-testid="cancel"
35
35
  disabled={isSubmitting}
36
- size="small"
36
+ size="medium"
37
37
  variant="secondary"
38
38
  onClick={() => onCancel?.()}
39
39
  >
40
40
  {labels.cancel}
41
41
  </Button>
42
42
  <Button
43
- aria-label="submit"
43
+ aria-label={labels.submit[variant]}
44
+ data-testid="submit"
44
45
  disabled={isSubmitting || !dirty || !isValid}
45
- size="small"
46
+ size="medium"
46
47
  type="submit"
47
48
  variant="primary"
48
49
  onClick={submitForm}
@@ -53,4 +54,4 @@ const DashboardFormActions = ({
53
54
  );
54
55
  };
55
56
 
56
- export { DashboardFormActions };
57
+ export { FormActions };
@@ -1,2 +1,3 @@
1
1
  export * from './Form.models';
2
2
  export * from './Dashboard';
3
+ export * from './AccessRights';
@@ -1,6 +1,7 @@
1
1
  import { Meta, StoryObj } from '@storybook/react';
2
2
 
3
3
  import {
4
+ Edit as EditIcon,
4
5
  Settings as SettingsIcon,
5
6
  StarOutline as StarOutlineIcon
6
7
  } from '@mui/icons-material';
@@ -82,6 +83,14 @@ export const WithMenu: Story = {
82
83
  >
83
84
  Manage
84
85
  </Button>
86
+ <Button
87
+ icon={<EditIcon />}
88
+ iconVariant="start"
89
+ size="small"
90
+ variant="ghost"
91
+ >
92
+ Edit
93
+ </Button>
85
94
  </PageHeader.Actions>
86
95
  </PageHeader>
87
96
  )
@@ -0,0 +1,21 @@
1
+ import { ReactElement } from 'react';
2
+
3
+ import {
4
+ Avatar as MuiAvatar,
5
+ ListItemAvatar as MuiListItemAvatar
6
+ } from '@mui/material';
7
+
8
+ import { useStyles } from './ListItem.styles';
9
+
10
+ type AvatarProps = {
11
+ children: ReactElement | string;
12
+ };
13
+ export const Avatar = ({ children }: AvatarProps): ReactElement => {
14
+ const { classes } = useStyles();
15
+
16
+ return (
17
+ <MuiListItemAvatar className={classes.avatar} data-element="avatar">
18
+ <MuiAvatar>{children}</MuiAvatar>
19
+ </MuiListItemAvatar>
20
+ );
21
+ };
@@ -0,0 +1,17 @@
1
+ import { ReactElement } from 'react';
2
+
3
+ import { Skeleton as MuiSkeleton } from '@mui/material';
4
+
5
+ import { useStyles } from './ListItem.styles';
6
+
7
+ export const AvatarSkeleton = (): ReactElement => {
8
+ const { classes } = useStyles();
9
+
10
+ return (
11
+ <MuiSkeleton
12
+ animation="wave"
13
+ className={classes.avatar}
14
+ variant="circular"
15
+ />
16
+ );
17
+ };
@@ -0,0 +1,41 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { ListItem } from '.';
4
+
5
+ const meta: Meta<typeof ListItem> = {
6
+ component: ListItem
7
+ };
8
+
9
+ export default meta;
10
+ type Story = StoryObj<typeof ListItem>;
11
+
12
+ export const Default: Story = {
13
+ args: {
14
+ children: 'Default'
15
+ }
16
+ };
17
+
18
+ export const WithTextAndAvatar: Story = {
19
+ args: {
20
+ children: (
21
+ <>
22
+ <ListItem.Avatar>AV</ListItem.Avatar>
23
+ <ListItem.Text
24
+ primaryText="Primary text"
25
+ secondaryText="Secondary text"
26
+ />
27
+ </>
28
+ )
29
+ }
30
+ };
31
+
32
+ export const AsLoadingState: Story = {
33
+ args: {
34
+ children: (
35
+ <>
36
+ <ListItem.Avatar.Skeleton />
37
+ <ListItem.Text.Skeleton secondaryText />
38
+ </>
39
+ )
40
+ }
41
+ };
@@ -0,0 +1,57 @@
1
+ import { makeStyles } from 'tss-react/mui';
2
+
3
+ export const useStyles = makeStyles()((theme) => ({
4
+ avatar: {
5
+ '&, &.MuiListItemAvatar-root, & > div': {
6
+ color: theme.palette.primary.contrastText,
7
+ fontSize: '1rem',
8
+ height: '2.25rem',
9
+ width: '2.25rem'
10
+ },
11
+ disabled: {
12
+ backgroundColor: theme.palette.action.disabled
13
+ },
14
+ minWidth: 'unset'
15
+ },
16
+ listItem: {
17
+ alignItems: 'center',
18
+ display: 'flex',
19
+ flexGrow: 1,
20
+ gap: theme.spacing(2),
21
+ maxWidth: '520px',
22
+ overflow: 'hidden',
23
+ paddingBottom: theme.spacing(1),
24
+ paddingTop: theme.spacing(1)
25
+ },
26
+ text: {
27
+ '&, &.MuiListItemText-root': {
28
+ '& .MuiListItemText-primary': {
29
+ fontSize: '0.875rem'
30
+ },
31
+ '& .MuiListItemText-secondary': {
32
+ fontSize: '0.625rem'
33
+ },
34
+ '& > *': {
35
+ overflow: 'hidden',
36
+ textOverflow: 'ellipsis',
37
+ textWrap: 'nowrap'
38
+ },
39
+ margin: 0
40
+ },
41
+ disabled: {
42
+ color: theme.palette.action.disabled
43
+ }
44
+ },
45
+ textSkeleton: {
46
+ '& span:nth-child(1)': {
47
+ fontSize: '0.875rem'
48
+ },
49
+ '& span:nth-child(2)': {
50
+ fontSize: '0.625rem',
51
+ width: '60%'
52
+ },
53
+ display: 'flex',
54
+ flexDirection: 'column',
55
+ minWidth: '40%'
56
+ }
57
+ }));
@@ -0,0 +1,32 @@
1
+ import { ForwardedRef, forwardRef, ReactElement, ReactNode } from 'react';
2
+
3
+ import { ListItem as MuiListItem } from '@mui/material';
4
+
5
+ import { useStyles } from './ListItem.styles';
6
+
7
+ type ListItemProps = {
8
+ action?: ReactElement;
9
+ children: ReactNode | Array<ReactNode>;
10
+ className?: string;
11
+ };
12
+
13
+ export const ListItem = forwardRef(
14
+ (
15
+ { action, children, className, ...attr }: ListItemProps,
16
+ ref?: ForwardedRef<HTMLLIElement>
17
+ ) => {
18
+ const { classes, cx } = useStyles();
19
+
20
+ return (
21
+ <MuiListItem
22
+ disableGutters
23
+ className={cx(classes.listItem, className)}
24
+ ref={ref}
25
+ secondaryAction={action}
26
+ {...attr}
27
+ >
28
+ {children}
29
+ </MuiListItem>
30
+ );
31
+ }
32
+ );