@judo/components 0.1.1-alpha.3

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.
@@ -0,0 +1,206 @@
1
+ import { createContext, useContext, useState } from 'react';
2
+ import type { ReactNode } from 'react';
3
+ import { ConfirmationDialog } from './ConfirmationDialog';
4
+ import { FilterDialog } from './FilterDialog';
5
+ import { PageDialog } from './PageDialog';
6
+ import { RangeDialog } from './RangeDialog';
7
+ import type {
8
+ ConfirmDialogProviderContext,
9
+ DialogProviderProps,
10
+ FilterDialogProviderContext,
11
+ OpenRangeDialogProps,
12
+ PageDialogProviderContext,
13
+ RangeDialogProviderContext,
14
+ } from '@judo/components-api';
15
+ import type { Filter, FilterOption } from '@judo/components-api';
16
+ import type { JudoStored, QueryCustomizer } from '@judo/data-api-common';
17
+
18
+ // @ts-ignore
19
+ const PageDialogContextState = createContext<PageDialogProviderContext>();
20
+
21
+ // @ts-ignore
22
+ const ConfirmDialogContextState = createContext<ConfirmDialogProviderContext>();
23
+
24
+ // @ts-ignore
25
+ const RangeDialogContextState = createContext<RangeDialogProviderContext>();
26
+
27
+ // @ts-ignore
28
+ const FilterDialogContextState = createContext<FilterDialogProviderContext>();
29
+
30
+ const usePageDialog = () => {
31
+ const context = useContext(PageDialogContextState);
32
+
33
+ if (context === undefined) {
34
+ throw new Error('useConfirmDialog was used outside of its Provider');
35
+ }
36
+
37
+ return context;
38
+ };
39
+
40
+ const useConfirmDialog = () => {
41
+ const context = useContext(ConfirmDialogContextState);
42
+
43
+ if (context === undefined) {
44
+ throw new Error('useConfirmDialog was used outside of its Provider');
45
+ }
46
+
47
+ return context;
48
+ };
49
+
50
+ const useRangeDialog = () => {
51
+ const context = useContext(RangeDialogContextState);
52
+
53
+ if (context === undefined) {
54
+ throw new Error('useRangeDialog was used outside of its Provider');
55
+ }
56
+
57
+ return context;
58
+ };
59
+
60
+ const useFilterDialog = () => {
61
+ const context = useContext(FilterDialogContextState);
62
+
63
+ if (context === undefined) {
64
+ throw new Error('useFilterDialog was used outside of its Provider');
65
+ }
66
+
67
+ return context;
68
+ };
69
+
70
+ const DialogProvider = ({ children }: DialogProviderProps) => {
71
+ // Page Dialog
72
+ const [isOpenPageDialog, setIsOpenPageDialog] = useState(false);
73
+ const [pageDialog, setPageDialog] = useState<ReactNode>();
74
+
75
+ const handleClosePageDialog = () => {
76
+ setIsOpenPageDialog(false);
77
+ };
78
+
79
+ const handleOpenPageDialog = async (page: ReactNode) => {
80
+ setIsOpenPageDialog(true);
81
+ return new Promise<void>((resolve) => {
82
+ setPageDialog(<PageDialog page={page} handleClose={handleClosePageDialog} open={true} resolve={resolve} />);
83
+ });
84
+ };
85
+
86
+ const pageDialogContext: PageDialogProviderContext = {
87
+ openPageDialog: handleOpenPageDialog,
88
+ };
89
+
90
+ // Range Dialog
91
+ const [isOpenRangeDialog, setIsOpenRangeDialog] = useState(false);
92
+ const [rangeDialog, setRangeDialog] = useState<ReactNode>();
93
+
94
+ const handleCloseRangeDialog = () => {
95
+ setIsOpenRangeDialog(false);
96
+ };
97
+
98
+ const handleOpenRangeDialog = async <T extends JudoStored<T>, U extends QueryCustomizer<T>>({
99
+ columns,
100
+ defaultSortField,
101
+ rangeCall,
102
+ single = false,
103
+ alreadySelectedItems,
104
+ filterOptions,
105
+ initialQueryCustomizer,
106
+ }: OpenRangeDialogProps<T, U>) => {
107
+ setIsOpenRangeDialog(true);
108
+
109
+ return new Promise<T[] | T>((resolve) => {
110
+ setRangeDialog(
111
+ <RangeDialog<T, U>
112
+ handleClose={handleCloseRangeDialog}
113
+ open={true}
114
+ resolve={resolve}
115
+ columns={columns}
116
+ defaultSortField={defaultSortField}
117
+ rangeCall={rangeCall}
118
+ single={single}
119
+ alreadySelectedItems={alreadySelectedItems}
120
+ filterOptions={filterOptions}
121
+ initalQueryCustomizer={initialQueryCustomizer}
122
+ />,
123
+ );
124
+ });
125
+ };
126
+
127
+ const customDialogContext: RangeDialogProviderContext = {
128
+ openRangeDialog: handleOpenRangeDialog,
129
+ };
130
+
131
+ // Confirmation Dialog
132
+ const [isOpenConfirmDialog, setIsOpenConfirmDialog] = useState(false);
133
+ const [confirmDialog, setConfirmDialog] = useState<ReactNode>();
134
+
135
+ const handleCloseConfirmDialog = () => {
136
+ setIsOpenConfirmDialog(false);
137
+ return false;
138
+ };
139
+
140
+ const handleOpenConfirmDialog = async (confirmationMessage: string | ReactNode, title?: string | ReactNode) => {
141
+ setIsOpenConfirmDialog(true);
142
+
143
+ return new Promise<boolean>((resolve) => {
144
+ setConfirmDialog(
145
+ <ConfirmationDialog
146
+ confirmationMessage={confirmationMessage}
147
+ title={title}
148
+ handleClose={handleCloseConfirmDialog}
149
+ open={true}
150
+ resolve={resolve}
151
+ />,
152
+ );
153
+ });
154
+ };
155
+
156
+ const confirmDialogContext: ConfirmDialogProviderContext = {
157
+ openConfirmDialog: handleOpenConfirmDialog,
158
+ };
159
+
160
+ // Filter dialog
161
+ const [isOpenFilterDialog, setIsOpenFilterDialog] = useState(false);
162
+ const [filterDialog, setFilterDialog] = useState<ReactNode>();
163
+
164
+ const handleCloseFilterDialog = () => {
165
+ setIsOpenFilterDialog(false);
166
+ return false;
167
+ };
168
+
169
+ const handleOpenFilterDialog = async (filterOptions: FilterOption[], filters?: Filter[]) => {
170
+ setIsOpenFilterDialog(true);
171
+
172
+ return new Promise<Filter[]>((resolve) => {
173
+ setFilterDialog(
174
+ <FilterDialog
175
+ filters={filters}
176
+ filterOptions={filterOptions}
177
+ handleClose={handleCloseFilterDialog}
178
+ open={true}
179
+ resolve={resolve}
180
+ />,
181
+ );
182
+ });
183
+ };
184
+
185
+ const filterDialogContext: FilterDialogProviderContext = {
186
+ openFilterDialog: handleOpenFilterDialog,
187
+ };
188
+
189
+ return (
190
+ <PageDialogContextState.Provider value={pageDialogContext}>
191
+ <ConfirmDialogContextState.Provider value={confirmDialogContext}>
192
+ <RangeDialogContextState.Provider value={customDialogContext}>
193
+ <FilterDialogContextState.Provider value={filterDialogContext}>
194
+ {children}
195
+ {isOpenPageDialog && pageDialog}
196
+ {isOpenConfirmDialog && confirmDialog}
197
+ {isOpenRangeDialog && rangeDialog}
198
+ {isOpenFilterDialog && filterDialog}
199
+ </FilterDialogContextState.Provider>
200
+ </RangeDialogContextState.Provider>
201
+ </ConfirmDialogContextState.Provider>
202
+ </PageDialogContextState.Provider>
203
+ );
204
+ };
205
+
206
+ export { DialogProvider, useConfirmDialog, useRangeDialog, useFilterDialog, usePageDialog };
@@ -0,0 +1,424 @@
1
+ import { mdiCalendarClock, mdiCalendarMonth, mdiFormatTextVariant, mdiNumeric } from '@mdi/js';
2
+ import Icon from '@mdi/react';
3
+ import { Close } from '@mui/icons-material';
4
+ import {
5
+ Dialog,
6
+ DialogTitle,
7
+ DialogContent,
8
+ DialogContentText,
9
+ DialogActions,
10
+ Button,
11
+ Slide,
12
+ Box,
13
+ Container,
14
+ Grid,
15
+ TextField,
16
+ MenuItem,
17
+ Checkbox,
18
+ FormControlLabel,
19
+ InputAdornment,
20
+ IconButton,
21
+ Typography,
22
+ } from '@mui/material';
23
+ import type { TransitionProps } from '@mui/material/transitions';
24
+ import { DatePicker, DateTimePicker } from '@mui/x-date-pickers';
25
+ import { forwardRef, useEffect, useRef, useState } from 'react';
26
+ import type { ChangeEvent, ReactElement, Ref } from 'react';
27
+ import type {
28
+ Filter,
29
+ FilterDialogProps,
30
+ FilterInputProps,
31
+ FilterOperatorProps,
32
+ FilterProps,
33
+ Operation,
34
+ } from '@judo/components-api';
35
+ import { FilterType } from '@judo/components-api';
36
+ import { dateToJudoDateString, exists } from '@judo/utilities';
37
+ import { mainContainerPadding } from '@judo/theme';
38
+ import { _BooleanOperation, _EnumerationOperation, _NumericOperation, _StringOperation } from '@judo/data-api-common';
39
+ import { DropdownButton } from '../DropdownButton';
40
+ import TrinaryLogicCombobox from '../TrinaryLogicCombobox';
41
+
42
+ const getDefaultOperator = (filterType: FilterType) => {
43
+ switch (filterType) {
44
+ case FilterType.boolean:
45
+ return _BooleanOperation['equals'];
46
+ case FilterType.date:
47
+ return _NumericOperation['equal'];
48
+ case FilterType.dateTime:
49
+ return _NumericOperation['equal'];
50
+ // case FilterType.time:
51
+ // return _NumericOperation['equal'];
52
+ case FilterType.enumeration:
53
+ return _EnumerationOperation['equals'];
54
+ case FilterType.numeric:
55
+ return _NumericOperation['equal'];
56
+ case FilterType.string:
57
+ return _StringOperation['equal'];
58
+ case FilterType.trinaryLogic:
59
+ return _BooleanOperation['equals'];
60
+ }
61
+ };
62
+
63
+ const getOperationEnumValue = (filter: Filter, operator: string) => {
64
+ switch (filter.filterOption.filterType) {
65
+ case FilterType.boolean:
66
+ return _BooleanOperation[operator as keyof typeof _BooleanOperation];
67
+ case FilterType.date:
68
+ return _NumericOperation[operator as keyof typeof _NumericOperation];
69
+ case FilterType.dateTime:
70
+ return _NumericOperation[operator as keyof typeof _NumericOperation];
71
+ // case FilterType.time:
72
+ // return _NumericOperation[operator as keyof typeof _NumericOperation];
73
+ case FilterType.enumeration:
74
+ return _EnumerationOperation[operator as keyof typeof _BooleanOperation];
75
+ case FilterType.numeric:
76
+ return _NumericOperation[operator as keyof typeof _NumericOperation];
77
+ case FilterType.string:
78
+ return _StringOperation[operator as keyof typeof _StringOperation];
79
+ case FilterType.trinaryLogic:
80
+ return _BooleanOperation[operator as keyof typeof _BooleanOperation];
81
+ }
82
+ };
83
+
84
+ const getOperatorsByFilter = (filter: Filter): string[] => {
85
+ switch (filter.filterOption.filterType) {
86
+ case FilterType.boolean:
87
+ return Object.values(_BooleanOperation);
88
+ case FilterType.date:
89
+ return Object.values(_NumericOperation);
90
+ case FilterType.dateTime:
91
+ return Object.values(_NumericOperation);
92
+ // case FilterType.time:
93
+ // return Object.values(_NumericOperation);
94
+ case FilterType.enumeration:
95
+ return Object.values(_EnumerationOperation);
96
+ case FilterType.numeric:
97
+ return Object.values(_NumericOperation);
98
+ case FilterType.string:
99
+ return Object.values(_StringOperation);
100
+ case FilterType.trinaryLogic:
101
+ return Object.values(_BooleanOperation);
102
+ }
103
+ };
104
+
105
+ const FilterOperator = ({ filter, setFilterOperator }: FilterOperatorProps) => {
106
+ const onChangeHandler = (event: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
107
+ setFilterOperator(filter, getOperationEnumValue(filter, event.target.value));
108
+ };
109
+
110
+ return (
111
+ <TextField
112
+ name={'operation'}
113
+ id={'operation'}
114
+ label={'Operation'}
115
+ select
116
+ value={filter.filterBy.operator}
117
+ onChange={onChangeHandler}
118
+ >
119
+ {getOperatorsByFilter(filter).map((item) => (
120
+ <MenuItem key={item} value={item}>
121
+ {/* TODO: do not forget localization here*/}
122
+ {item}
123
+ </MenuItem>
124
+ ))}
125
+ </TextField>
126
+ );
127
+ };
128
+
129
+ const FilterInput = ({ filter, setFilterValue }: FilterInputProps) => {
130
+ if (filter.filterOption.filterType === FilterType.enumeration && !exists(filter.filterOption.enumValues)) {
131
+ throw new Error(`Missing enumValues from FilterOptions of ${filter.filterOption.attributeName}`);
132
+ }
133
+
134
+ return (
135
+ <>
136
+ {(() => {
137
+ switch (filter.filterOption.filterType) {
138
+ case FilterType.boolean:
139
+ return (
140
+ <FormControlLabel
141
+ control={
142
+ <Checkbox
143
+ checked={!!filter.filterBy.value}
144
+ onChange={(event) => setFilterValue(filter, !!event.target.value)}
145
+ />
146
+ }
147
+ label={filter.filterOption.attributeName}
148
+ />
149
+ );
150
+ case FilterType.date:
151
+ return (
152
+ <DatePicker
153
+ renderInput={(props) => <TextField {...props} />}
154
+ label={filter.filterOption.attributeName}
155
+ value={filter.filterBy.value ?? null}
156
+ onChange={(newValue) => setFilterValue(filter, dateToJudoDateString(newValue))}
157
+ InputProps={{
158
+ startAdornment: (
159
+ <InputAdornment position="start">
160
+ <Icon path={mdiCalendarMonth} size={1} />
161
+ </InputAdornment>
162
+ ),
163
+ }}
164
+ />
165
+ );
166
+ case FilterType.dateTime:
167
+ return (
168
+ <DateTimePicker
169
+ renderInput={(props) => <TextField {...props} />}
170
+ label={filter.filterOption.attributeName}
171
+ value={filter.filterBy.value ?? null}
172
+ onChange={(newValue) => setFilterValue(filter, newValue)}
173
+ InputProps={{
174
+ startAdornment: (
175
+ <InputAdornment position="start">
176
+ <Icon path={mdiCalendarClock} size={1} />
177
+ </InputAdornment>
178
+ ),
179
+ }}
180
+ />
181
+ );
182
+ // case FilterType.time:
183
+ // return (
184
+ // <TextField
185
+ // label={filter.filterOption.attributeName}
186
+ // value={filter.filterBy.value}
187
+ // onChange={(event) => setFilterValue(filter, event.target.value)}
188
+ // InputProps={{
189
+ // startAdornment: (
190
+ // <InputAdornment position="start">
191
+ // <Icon path={mdiClockOutline} size={1} />
192
+ // </InputAdornment>
193
+ // ),
194
+ // }}
195
+ // />
196
+ // );
197
+ case FilterType.enumeration:
198
+ return (
199
+ <TextField
200
+ label={filter.filterOption.attributeName}
201
+ value={filter.filterBy.value}
202
+ select
203
+ onChange={(event) => setFilterValue(filter, event.target.value)}
204
+ >
205
+ {filter.filterOption.enumValues?.map((item) => (
206
+ <MenuItem key={item} value={item}>
207
+ {item}
208
+ </MenuItem>
209
+ ))}
210
+ </TextField>
211
+ );
212
+ case FilterType.numeric:
213
+ return (
214
+ <TextField
215
+ label={filter.filterOption.attributeName}
216
+ type="number"
217
+ value={filter.filterBy.value}
218
+ onChange={(event) => setFilterValue(filter, Number(event.target.value))}
219
+ InputProps={{
220
+ startAdornment: (
221
+ <InputAdornment position="start">
222
+ <Icon path={mdiNumeric} size={1} />
223
+ </InputAdornment>
224
+ ),
225
+ }}
226
+ />
227
+ );
228
+ case FilterType.string:
229
+ return (
230
+ <TextField
231
+ label={filter.filterOption.attributeName}
232
+ value={filter.filterBy.value}
233
+ onChange={(event) => setFilterValue(filter, event.target.value)}
234
+ InputProps={{
235
+ startAdornment: (
236
+ <InputAdornment position="start">
237
+ <Icon path={mdiFormatTextVariant} size={1} />
238
+ </InputAdornment>
239
+ ),
240
+ }}
241
+ />
242
+ );
243
+ case FilterType.trinaryLogic:
244
+ return (
245
+ <TrinaryLogicCombobox
246
+ label={filter.filterOption.attributeName}
247
+ value={filter.filterBy.value}
248
+ onChange={(value) => setFilterValue(filter, value)}
249
+ />
250
+ );
251
+ }
252
+ })()}
253
+ </>
254
+ );
255
+ };
256
+
257
+ const FilterRow = ({ filter, closeHandler, setFilterOperator, setFilterValue }: FilterProps) => {
258
+ return (
259
+ <Grid item container spacing={2} alignItems={'center'}>
260
+ <Grid item xs={4}>
261
+ {filter && <FilterOperator filter={filter} setFilterOperator={setFilterOperator} />}
262
+ </Grid>
263
+ <Grid item xs={7}>
264
+ {filter && <FilterInput filter={filter} setFilterValue={setFilterValue} />}
265
+ </Grid>
266
+ <Grid item xs={1}>
267
+ <IconButton onClick={() => closeHandler(filter)}>
268
+ <Close />
269
+ </IconButton>
270
+ </Grid>
271
+ </Grid>
272
+ );
273
+ };
274
+
275
+ const Transition = forwardRef(function Transition(
276
+ props: TransitionProps & {
277
+ children: ReactElement<any, any>;
278
+ },
279
+ ref: Ref<unknown>,
280
+ ) {
281
+ return <Slide direction="left" ref={ref} {...props} />;
282
+ });
283
+
284
+ export const FilterDialog = ({ filters, filterOptions, resolve, open, handleClose }: FilterDialogProps) => {
285
+ const descriptionElementRef = useRef<HTMLElement>(null);
286
+ const [tempFilters, setTempFilters] = useState<Filter[]>(filters ?? []);
287
+
288
+ useEffect(() => {
289
+ if (open) {
290
+ const { current: descriptionElement } = descriptionElementRef;
291
+ if (descriptionElement !== null) {
292
+ descriptionElement.focus();
293
+ }
294
+ }
295
+ }, [open]);
296
+
297
+ const updateFilterValue = (filter: Filter, value: any) => {
298
+ setTempFilters((prevTempFilters) => {
299
+ return prevTempFilters.map((tempFilter) => {
300
+ if (filter.id === tempFilter.id) {
301
+ return {
302
+ ...tempFilter,
303
+ filterBy: { value: value, operator: tempFilter.filterBy.operator },
304
+ };
305
+ }
306
+
307
+ return tempFilter;
308
+ });
309
+ });
310
+ };
311
+
312
+ const updateFilterOperator = (filter: Filter, operator: Operation) => {
313
+ setTempFilters((prevTempFilters) => {
314
+ return prevTempFilters.map((tempFilter) => {
315
+ if (filter.id === tempFilter.id) {
316
+ return {
317
+ ...tempFilter,
318
+ filterBy: { value: tempFilter.filterBy.value, operator: operator },
319
+ };
320
+ }
321
+
322
+ return tempFilter;
323
+ });
324
+ });
325
+ };
326
+
327
+ const filterCloseHandler = (filter: Filter) => {
328
+ setTempFilters((prevTempFilters) => [...prevTempFilters.filter((tempFilter) => tempFilter.id !== filter.id)]);
329
+ };
330
+
331
+ const cancel = () => {
332
+ handleClose();
333
+ resolve(undefined);
334
+ };
335
+
336
+ const ok = () => {
337
+ handleClose();
338
+ resolve(tempFilters);
339
+ };
340
+
341
+ return (
342
+ <Dialog
343
+ open={open}
344
+ onClose={cancel}
345
+ scroll="paper"
346
+ TransitionComponent={Transition}
347
+ disableEnforceFocus
348
+ fullWidth
349
+ maxWidth="sm"
350
+ sx={{
351
+ '& .MuiDialog-container': {
352
+ justifyContent: 'flex-end',
353
+ },
354
+ }}
355
+ PaperProps={{
356
+ sx: {
357
+ m: 0,
358
+ height: '100%',
359
+ },
360
+ }}
361
+ >
362
+ <DialogTitle id="scroll-dialog-title">
363
+ <Typography component="span" color="text.primary" variant="h5">
364
+ Filters
365
+ </Typography>
366
+ </DialogTitle>
367
+ <DialogContent dividers={true}>
368
+ <DialogContentText id="scroll-dialog-description" ref={descriptionElementRef} tabIndex={-1}>
369
+ <Container component="main" maxWidth="xs">
370
+ <Box sx={mainContainerPadding}>
371
+ <Grid container spacing={2}>
372
+ {tempFilters.map((filter) => (
373
+ <FilterRow
374
+ key={filter.id}
375
+ filter={filter}
376
+ closeHandler={filterCloseHandler}
377
+ setFilterOperator={updateFilterOperator}
378
+ setFilterValue={updateFilterValue}
379
+ />
380
+ ))}
381
+ <Grid item container>
382
+ <DropdownButton
383
+ fullWidth={true}
384
+ showDropdownIcon={false}
385
+ menuItems={filterOptions.map((filterOption) => {
386
+ return {
387
+ label: filterOption.label ?? filterOption.attributeName,
388
+ onClick: () =>
389
+ setTempFilters((prevTempFilters) => [
390
+ ...prevTempFilters,
391
+ {
392
+ id: prevTempFilters.length,
393
+ filterOption: {
394
+ attributeName: filterOption.attributeName,
395
+ label: filterOption.label,
396
+ filterType: filterOption.filterType,
397
+ },
398
+ filterBy: {
399
+ operator: getDefaultOperator(filterOption.filterType),
400
+ },
401
+ },
402
+ ]),
403
+ };
404
+ })}
405
+ >
406
+ Add new filter
407
+ </DropdownButton>
408
+ </Grid>
409
+ </Grid>
410
+ </Box>
411
+ </Container>
412
+ </DialogContentText>
413
+ </DialogContent>
414
+ <DialogActions>
415
+ <Button fullWidth variant="outlined" onClick={cancel}>
416
+ Cancel
417
+ </Button>
418
+ <Button fullWidth onClick={ok}>
419
+ Apply {'(' + tempFilters.length + ')'}
420
+ </Button>
421
+ </DialogActions>
422
+ </Dialog>
423
+ );
424
+ };
@@ -0,0 +1,40 @@
1
+ import { Dialog, DialogContent, DialogContentText, DialogActions, Button } from '@mui/material';
2
+ import { useEffect, useRef } from 'react';
3
+ import type { ReactNode } from 'react';
4
+
5
+ interface PageDialogProps {
6
+ page: ReactNode;
7
+ open: boolean;
8
+ handleClose: () => void;
9
+ resolve: () => void;
10
+ }
11
+
12
+ export const PageDialog = ({ page, open, handleClose, resolve }: PageDialogProps) => {
13
+ const descriptionElementRef = useRef<HTMLElement>(null);
14
+ useEffect(() => {
15
+ if (open) {
16
+ const { current: descriptionElement } = descriptionElementRef;
17
+ if (descriptionElement !== null) {
18
+ descriptionElement.focus();
19
+ }
20
+ }
21
+ }, [open]);
22
+
23
+ const ok = () => {
24
+ resolve();
25
+ handleClose();
26
+ };
27
+
28
+ return (
29
+ <Dialog open={open} onClose={ok} scroll="paper">
30
+ <DialogContent dividers={true}>
31
+ <DialogContentText ref={descriptionElementRef} tabIndex={-1}>
32
+ {page}
33
+ </DialogContentText>
34
+ </DialogContent>
35
+ <DialogActions>
36
+ <Button onClick={ok}>Ok</Button>
37
+ </DialogActions>
38
+ </Dialog>
39
+ );
40
+ };