@campxdev/shared 1.4.8 → 1.4.9

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/shared",
3
- "version": "1.4.8",
3
+ "version": "1.4.9",
4
4
  "main": "./exports.ts",
5
5
  "scripts": {
6
6
  "start": "react-scripts start",
@@ -50,10 +50,10 @@
50
50
  "react-toastify": "^9.0.1",
51
51
  "styled-components": "^5.3.5",
52
52
  "swiper": "^8.1.5",
53
+ "use-immer": "^0.8.1",
53
54
  "yup": "^0.32.11"
54
55
  },
55
56
  "devDependencies": {
56
- "@types/react-flatpickr": "^3.8.8",
57
57
  "@storybook/addon-actions": "^6.5.14",
58
58
  "@storybook/addon-essentials": "^6.5.14",
59
59
  "@storybook/addon-interactions": "^6.5.14",
@@ -67,6 +67,7 @@
67
67
  "@types/js-cookie": "^3.0.2",
68
68
  "@types/node": "^18.11.8",
69
69
  "@types/react": "^18.0.25",
70
+ "@types/react-flatpickr": "^3.8.8",
70
71
  "@typescript-eslint/eslint-plugin": "^5.35.1",
71
72
  "@typescript-eslint/parser": "^5.35.1",
72
73
  "eslint": "^8.23.0",
@@ -1,34 +1,41 @@
1
- import { Box, FormGroup, FormGroupProps, FormHelperText } from '@mui/material'
1
+ import {
2
+ Box,
3
+ BoxProps,
4
+ FormGroup,
5
+ FormGroupProps,
6
+ FormHelperText,
7
+ } from '@mui/material'
2
8
  import { ReactNode } from 'react'
3
9
  import { Controller } from 'react-hook-form'
4
10
  import { FormLabel, SingleCheckbox } from '../Input'
11
+ import { IOption } from '../Input/types'
5
12
 
6
13
  interface Props extends FormGroupProps {
7
14
  label?: ReactNode
8
15
  name: string
9
16
  control: any
10
- options: Array<{ label: ReactNode; value: any }>
17
+ options: IOption[]
11
18
  required?: boolean
12
19
  row?: boolean
20
+ containerProps: BoxProps
13
21
  }
14
22
 
15
- export default function FormMultiCheckbox(props: Props) {
16
- const {
17
- name,
18
- control,
19
- label = '',
20
- options = [],
21
- required = false,
22
- row = true,
23
- ...rest
24
- } = props
25
-
23
+ export default function FormMultiCheckbox({
24
+ name,
25
+ control,
26
+ label = '',
27
+ options = [],
28
+ required = false,
29
+ row = true,
30
+ containerProps,
31
+ ...rest
32
+ }: Props) {
26
33
  return (
27
34
  <Controller
28
35
  name={name}
29
36
  control={control}
30
37
  render={({ field, fieldState: { error } }) => (
31
- <Box width="100%">
38
+ <Box width="100%" {...containerProps}>
32
39
  <FormLabel label={label} name={name} required={required} />
33
40
  <FormGroup row={row} sx={{ flexWrap: 'wrap' }} {...rest}>
34
41
  {options?.map((item, index) => (
@@ -1,15 +1,18 @@
1
1
  import { ReactNode } from 'react'
2
2
  import { Controller } from 'react-hook-form'
3
3
  import { MultiSelect } from '../Input'
4
+ import { IOption } from '../Input/types'
4
5
 
5
6
  interface MultiSelectProps {
6
7
  control: any
7
8
  label: ReactNode
8
9
  name: string
9
- options: { label: ReactNode; value: any }[]
10
+ options: IOption[]
10
11
  placeholder?: string
11
12
  loading?: boolean
12
13
  required?: boolean
14
+ value?: IOption
15
+ onChange?: (value: IOption[]) => void
13
16
  }
14
17
 
15
18
  export default function FormMultiSelect({
@@ -1,4 +1,4 @@
1
- import { Box, FormHelperText, RadioGroupProps } from '@mui/material'
1
+ import { Box, BoxProps, RadioGroupProps } from '@mui/material'
2
2
  import { ReactNode } from 'react'
3
3
  import { Controller } from 'react-hook-form'
4
4
  import { RadioGroup } from '../Input'
@@ -11,18 +11,24 @@ interface Props extends RadioGroupProps {
11
11
  row?: boolean
12
12
  required?: boolean
13
13
  options: { value: any; label: string | ReactNode }[]
14
- Container?: any
14
+ containerProps?: BoxProps
15
15
  }
16
16
 
17
- export default function FormRadioGroup(props: Props) {
18
- const { name, control, label, options = [], row = false, ...rest } = props
19
-
17
+ export default function FormRadioGroup({
18
+ name,
19
+ control,
20
+ label,
21
+ options = [],
22
+ row = false,
23
+ containerProps,
24
+ ...rest
25
+ }: Props) {
20
26
  return (
21
27
  <Controller
22
28
  name={name}
23
29
  control={control}
24
30
  render={({ field, fieldState: { error } }) => (
25
- <Box width="100%">
31
+ <Box width="100%" {...containerProps}>
26
32
  <RadioGroup
27
33
  row={row}
28
34
  name={name}
@@ -32,7 +38,6 @@ export default function FormRadioGroup(props: Props) {
32
38
  onChange={field.onChange}
33
39
  {...rest}
34
40
  />
35
- {error?.message && <FormHelperText>{error.message}</FormHelperText>}
36
41
  </Box>
37
42
  )}
38
43
  />
@@ -9,16 +9,22 @@ interface Props extends CheckboxProps {
9
9
  control: any
10
10
  }
11
11
 
12
- export default function FormSingleCheckbox(props: Props) {
13
- const { name, control, label = '', ...rest } = props
12
+ export default function FormSingleCheckbox({
13
+ name,
14
+ control,
15
+ label,
16
+ onChange,
17
+ checked,
18
+ ...rest
19
+ }: Props) {
14
20
  return (
15
21
  <Controller
16
22
  name={name}
17
23
  control={control}
18
24
  render={({ field, fieldState: { error } }) => (
19
25
  <SingleCheckbox
20
- checked={field.value}
21
- onChange={field.onChange}
26
+ checked={checked ?? field.value}
27
+ onChange={onChange ?? field.onChange}
22
28
  label={label}
23
29
  {...rest}
24
30
  />
@@ -16,8 +16,9 @@ export default function FormSingleSelect(props: Props) {
16
16
  options = [],
17
17
  control,
18
18
  label,
19
+ onChange,
20
+ value,
19
21
  firstItemEmpty = false,
20
- size = 'medium',
21
22
  } = props
22
23
 
23
24
  const inputOptions = firstItemEmpty
@@ -30,11 +31,10 @@ export default function FormSingleSelect(props: Props) {
30
31
  control={control}
31
32
  render={({ field, fieldState: { error } }) => (
32
33
  <SingleSelect
33
- size={size}
34
34
  label={label}
35
35
  name={name}
36
- value={field.value}
37
- onChange={field.onChange}
36
+ onChange={onChange ?? field.onChange}
37
+ value={value ?? field?.value}
38
38
  options={inputOptions}
39
39
  error={!!error}
40
40
  helperText={error?.message}
@@ -8,9 +8,6 @@ type MyTextFieldProps = MuiTextFieldProps & {
8
8
  label?: string
9
9
  name: string
10
10
  required?: boolean
11
- handleChange?: React.ChangeEventHandler<
12
- HTMLTextAreaElement | HTMLInputElement
13
- >
14
11
  }
15
12
 
16
13
  export default function FormTextField({
@@ -18,18 +15,20 @@ export default function FormTextField({
18
15
  control,
19
16
  label,
20
17
  required,
18
+ onChange,
19
+ value,
21
20
  ...rest
22
21
  }: MyTextFieldProps) {
23
22
  return (
24
23
  <Controller
25
24
  control={control}
26
25
  name={name}
27
- render={({ field: { onChange, value }, fieldState: { error } }) => (
26
+ render={({ field, fieldState: { error } }) => (
28
27
  <TextField
29
28
  name={name}
30
29
  label={label}
31
- onChange={onChange}
32
- value={value}
30
+ onChange={onChange ?? field.onChange}
31
+ value={value ?? field?.value}
33
32
  required={required}
34
33
  error={error ? true : false}
35
34
  helperText={error ? error.message : null}
@@ -0,0 +1,216 @@
1
+ import { Close } from '@mui/icons-material'
2
+ import { Box, Checkbox, CircularProgress, Typography } from '@mui/material'
3
+ import _ from 'lodash'
4
+ import { ReactNode, useCallback, useEffect, useState } from 'react'
5
+ import { useQuery } from 'react-query'
6
+ import { useImmer } from 'use-immer'
7
+ import FormLabel from '../FormLabel'
8
+ import { BpCheckedIcon, BpIcon } from '../SingleCheckbox'
9
+ import TextField from '../TextField'
10
+ import { IOption } from '../types'
11
+ import {
12
+ StyledAnchor,
13
+ StyledListContainer,
14
+ StyledListItem,
15
+ StyledPopover,
16
+ StyledSelectedChip,
17
+ } from './styles'
18
+
19
+ const Anchor = ({ handleClick, id, label, required }) => {
20
+ return (
21
+ <Box>
22
+ <FormLabel label={label} name={id ?? ''} required={required} />
23
+ <StyledAnchor id={id} onClick={handleClick}>
24
+ <Typography variant="subtitle1">Click to search</Typography>
25
+ </StyledAnchor>
26
+ </Box>
27
+ )
28
+ }
29
+
30
+ const SelectedItems = ({ selectedItems, selectedLabel, onDelete }) => {
31
+ if (!selectedItems?.length) return null
32
+ return (
33
+ <Box marginTop="10px">
34
+ <FormLabel label={selectedLabel} name="form-label" required={false} />
35
+ <Box sx={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
36
+ {selectedItems?.map((item, index) => (
37
+ <StyledSelectedChip
38
+ deleteIcon={<Close />}
39
+ key={index}
40
+ label={item?.label}
41
+ variant={'outlined'}
42
+ onDelete={() => onDelete(item)}
43
+ />
44
+ ))}
45
+ </Box>
46
+ </Box>
47
+ )
48
+ }
49
+
50
+ const OptionsList = ({ loading, menuItems, onSelected, selected }) => {
51
+ const isIncluded = (id) => {
52
+ return selected?.options?.find((item) => item.value == id)
53
+ }
54
+
55
+ const handleClick = (selectedOption) => {
56
+ if (isIncluded(selectedOption.value)) {
57
+ onSelected(
58
+ selected?.options?.filter(
59
+ (item) => item.value !== selectedOption.value,
60
+ ),
61
+ )
62
+ } else {
63
+ onSelected([...selected.options, selectedOption])
64
+ }
65
+ }
66
+
67
+ if (loading)
68
+ return <CircularProgress size={16} sx={{ textAlign: 'center' }} />
69
+
70
+ if (!menuItems)
71
+ return (
72
+ <Typography
73
+ sx={{ padding: '10px' }}
74
+ variant="subtitle1"
75
+ textAlign={'center'}
76
+ >
77
+ Start typing to search
78
+ </Typography>
79
+ )
80
+ if (!menuItems?.length)
81
+ return (
82
+ <Typography
83
+ sx={{ padding: '10px' }}
84
+ variant="subtitle1"
85
+ textAlign={'center'}
86
+ >
87
+ No results found
88
+ </Typography>
89
+ )
90
+
91
+ return (
92
+ <>
93
+ {menuItems?.map((option, index) => (
94
+ <StyledListItem key={index} onClick={() => handleClick(option)}>
95
+ <Checkbox
96
+ icon={isIncluded(option?.value) ? <BpCheckedIcon /> : <BpIcon />}
97
+ />
98
+ {option.label}
99
+ </StyledListItem>
100
+ ))}
101
+ </>
102
+ )
103
+ }
104
+
105
+ type SearchInputProps = {
106
+ onChange: (value: IOption[]) => void
107
+ initialOptions?: IOption[]
108
+ searchQueryKey?: string
109
+ selectedLabel?: ReactNode
110
+ label: ReactNode
111
+ fetchFn: (params: { [x: string]: string }) => Promise<IOption[]>
112
+ required?: boolean
113
+ }
114
+
115
+ export default function AsyncSearchSelect({
116
+ label,
117
+ onChange,
118
+ initialOptions = [],
119
+ searchQueryKey = 'search',
120
+ selectedLabel = 'Selected Items',
121
+ fetchFn,
122
+ required,
123
+ }: SearchInputProps) {
124
+ const [search, setSearch] = useState('')
125
+ const [width, setWidth] = useState(300)
126
+
127
+ const { data: searchResults, isLoading } = useQuery(
128
+ ['search', search],
129
+ (params: any) => fetchFn({ ...params, [searchQueryKey]: search }),
130
+ {
131
+ enabled: Boolean(search),
132
+ },
133
+ )
134
+
135
+ const [selected, setSelected] = useImmer({
136
+ options: [],
137
+ })
138
+
139
+ const [open, setOpen] = useState(false)
140
+ const [anchorEl, setAnchorEl] = useState<any>(null)
141
+ const id = open ? 'simple-popover' : undefined
142
+
143
+ const onSearchChange = (e) => {
144
+ setSearch(e.target.value)
145
+ }
146
+
147
+ const handleClick = (event) => {
148
+ const anchorWidth = event.target.getBoundingClientRect().width
149
+ setWidth(anchorWidth)
150
+ setOpen(true)
151
+ setAnchorEl(event.currentTarget)
152
+ }
153
+
154
+ const handleClose = () => {
155
+ setOpen(false)
156
+ }
157
+
158
+ const onSelected = (newValues) => {
159
+ setSelected((s) => {
160
+ s.options = newValues
161
+ })
162
+ if (onChange) onChange(newValues)
163
+ }
164
+
165
+ const onDeleteSelectedItem = (option) => {
166
+ onSelected(selected?.options?.filter((item) => item.value !== option.value))
167
+ }
168
+
169
+ const debouncedChange = useCallback(_.debounce(onSearchChange, 300), [])
170
+
171
+ useEffect(() => {
172
+ setSelected((s) => {
173
+ s.options = initialOptions
174
+ })
175
+ }, [initialOptions?.length])
176
+
177
+ return (
178
+ <Box>
179
+ <Anchor
180
+ handleClick={handleClick}
181
+ id={id}
182
+ label={label}
183
+ required={required}
184
+ />
185
+ <SelectedItems
186
+ onDelete={onDeleteSelectedItem}
187
+ selectedItems={selected?.options}
188
+ selectedLabel={selectedLabel}
189
+ />
190
+ <StyledPopover
191
+ popoverWidth={width}
192
+ open={open}
193
+ id={id}
194
+ anchorEl={anchorEl}
195
+ onClose={handleClose}
196
+ >
197
+ <Box sx={{ padding: '14px' }}>
198
+ <TextField
199
+ size="small"
200
+ onChange={debouncedChange}
201
+ placeholder="Search"
202
+ autoFocus={true}
203
+ />
204
+ <StyledListContainer>
205
+ <OptionsList
206
+ loading={isLoading}
207
+ menuItems={searchResults}
208
+ onSelected={onSelected}
209
+ selected={selected}
210
+ />
211
+ </StyledListContainer>
212
+ </Box>
213
+ </StyledPopover>
214
+ </Box>
215
+ )
216
+ }
@@ -0,0 +1 @@
1
+ export { default } from './AsyncSearchSelect'
@@ -0,0 +1,105 @@
1
+ import {
2
+ alpha,
3
+ Box,
4
+ Chip,
5
+ ListItem,
6
+ Popover,
7
+ PopoverProps,
8
+ styled,
9
+ } from '@mui/material'
10
+
11
+ export const StyledPopover = styled((props: PopoverProps) => (
12
+ <Popover
13
+ elevation={0}
14
+ anchorPosition={{
15
+ left: 0,
16
+ top: 0,
17
+ }}
18
+ anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
19
+ transitionDuration={150}
20
+ {...props}
21
+ />
22
+ ))<{ popoverWidth: number }>(({ theme, popoverWidth }) => ({
23
+ '& .MuiPaper-root': {
24
+ width: popoverWidth,
25
+ borderRadius: '10px',
26
+ boxShadow: '0px 4px 16px #0000000F',
27
+ marginTop: '1px',
28
+ '& .MuiList-root': {
29
+ padding: 0,
30
+ '& li': {
31
+ height: '60px',
32
+ borderBottom: (theme) => theme.borders.grayLight,
33
+ ':hover': {
34
+ backgroundColor: 'rgba(0, 0, 0, 0.025)',
35
+ },
36
+ },
37
+ '& > :last-child': {
38
+ borderBottom: 'none',
39
+ },
40
+ },
41
+ },
42
+ }))
43
+
44
+ export const StyledListContainer = styled(Box)(({ theme }) => ({
45
+ maxHeight: 'calc(100vh - 400px)',
46
+ overflowY: 'auto',
47
+ paddingTop: '10px',
48
+ '&::-webkit-scrollbar': {
49
+ width: '0.5em',
50
+ height: '0.5em',
51
+ },
52
+
53
+ '&::-webkit-scrollbar-thumb': {
54
+ backgroundColor: 'rgba(0, 0, 0, 0.15)',
55
+ borderRadius: '3px',
56
+
57
+ '&:hover': {
58
+ background: 'rgba(0, 0, 0, 0.2)',
59
+ },
60
+ },
61
+ }))
62
+
63
+ export const StyledListItem = styled(ListItem)(({ theme }) => ({
64
+ height: '40px',
65
+ cursor: 'pointer',
66
+ padding: '10px',
67
+ background: 'none',
68
+ '&.Mui-focused': {
69
+ background: 'none',
70
+ },
71
+ '& .MuiCheckbox-root': {
72
+ padding: 0,
73
+ marginRight: '10px',
74
+ },
75
+ }))
76
+
77
+ export const StyledAnchor = styled(Box)(({ theme }) => ({
78
+ height: '50px',
79
+ border: theme.borders.grayLight,
80
+ borderRadius: '10px',
81
+ padding: '14px',
82
+ cursor: 'pointer',
83
+ ':hover': {
84
+ borderColor: alpha(theme.palette.common.black, 0.3),
85
+ },
86
+ transition: 'border-color 200ms ease',
87
+ '& > .MuiTypography-root': {
88
+ fontSize: '14px',
89
+ pointerEvents: 'none',
90
+ },
91
+ }))
92
+
93
+ export const StyledSelectedChip = styled(Chip)(({ theme }) => ({
94
+ height: '40px',
95
+ background: '#F8F8F8',
96
+ border: '1px solid #D6D6D6',
97
+ '& .MuiSvgIcon-root': {
98
+ color: theme.palette.secondary.main,
99
+ fontSize: '15px',
100
+ margin: '0 14px',
101
+ '&:hover': {
102
+ color: theme.palette.secondary.main,
103
+ },
104
+ },
105
+ }))
@@ -36,7 +36,7 @@ export default function DateTimePicker({
36
36
  <Flatpickr
37
37
  options={{
38
38
  enableTime: true,
39
- dateFormat: 'd-m-Y h:i:K',
39
+ dateFormat: 'd-m-Y h:i K',
40
40
  minDate,
41
41
  maxDate,
42
42
  minTime,
@@ -9,7 +9,7 @@ export default function FormLabel({ label, required, name }) {
9
9
  component="label"
10
10
  sx={{
11
11
  color: alpha('#121212', 0.5),
12
- marginBottom: '2px',
12
+ lineHeight: 2,
13
13
  '& span': {
14
14
  color: (theme) => theme.palette.error.main,
15
15
  },
@@ -62,6 +62,7 @@ const StyledPopper = styled(Popper)(({ theme }) => ({
62
62
  },
63
63
  '& .MuiCheckbox-root': {
64
64
  padding: 0,
65
+ marginRight: '10px',
65
66
  },
66
67
  },
67
68
  '&::-webkit-scrollbar': {
@@ -114,7 +115,6 @@ export default function MultiSelect({
114
115
  <Checkbox
115
116
  icon={<BpIcon />}
116
117
  checkedIcon={<BpCheckedIcon />}
117
- style={{ marginRight: 8 }}
118
118
  checked={selected}
119
119
  />
120
120
  {option.label}
@@ -2,6 +2,7 @@ import {
2
2
  Box,
3
3
  BoxProps,
4
4
  FormControlLabel,
5
+ FormHelperText,
5
6
  Radio,
6
7
  RadioGroup as MuiRadioGroup,
7
8
  RadioGroupProps,
@@ -58,6 +59,8 @@ interface Props extends RadioGroupProps {
58
59
  required?: boolean
59
60
  options: { value: any; label: string | ReactNode }[]
60
61
  containerProps?: BoxProps
62
+ error?: boolean
63
+ helperText?: string | null
61
64
  }
62
65
 
63
66
  export default function RadioGroup(props: Props) {
@@ -70,6 +73,8 @@ export default function RadioGroup(props: Props) {
70
73
  value,
71
74
  onChange,
72
75
  containerProps,
76
+ error,
77
+ helperText,
73
78
  ...rest
74
79
  } = props
75
80
  return (
@@ -93,6 +98,7 @@ export default function RadioGroup(props: Props) {
93
98
  />
94
99
  ))}
95
100
  </MuiRadioGroup>
101
+ {error && <FormHelperText>{helperText}</FormHelperText>}
96
102
  </Box>
97
103
  )
98
104
  }
@@ -51,19 +51,20 @@ interface Props extends CheckboxProps {
51
51
  disabled?: boolean
52
52
  label: ReactNode
53
53
  name?: string
54
- size?: 'small' | 'medium'
55
- sx?: any
56
54
  checked: boolean
57
55
  onChange: (e) => void
58
56
  }
59
57
 
60
- export default function SingleCheckbox(props: Props) {
61
- const { checked, size = 'medium', label = '', sx, onChange } = props
58
+ export default function SingleCheckbox({
59
+ checked,
60
+ label,
61
+ onChange,
62
+ ...props
63
+ }: Props) {
62
64
  return (
63
65
  <FormControlLabel
64
66
  control={
65
67
  <Checkbox
66
- size={size}
67
68
  checked={checked}
68
69
  onChange={onChange}
69
70
  checkedIcon={<BpCheckedIcon />}
@@ -33,7 +33,7 @@ export default function TimePicker({
33
33
  options={{
34
34
  enableTime: true,
35
35
  noCalendar: true,
36
- dateFormat: 'h:i:K',
36
+ dateFormat: 'h:i K',
37
37
  minTime,
38
38
  maxTime,
39
39
  }}
@@ -0,0 +1,3 @@
1
+ import { ReactNode } from 'react'
2
+
3
+ export type IOption = { label: ReactNode; value: any }