goobs-frontend 0.8.29 → 0.8.30
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 +20 -19
- package/src/components/ComplexTextEditor/SimpleEditor/index.tsx +20 -2
- package/src/components/ComplexTextEditor/index.tsx +21 -5
- package/src/components/Content/Structure/multiSelect/useMultiSelect.tsx +74 -0
- package/src/components/Content/index.tsx +5 -0
- package/src/components/DataGrid/index.tsx +1 -1
- package/src/components/DataGrid/utils/useToolbarSearchbar.tsx +6 -0
- package/src/components/Form/Popup/index.tsx +8 -4
- package/src/components/NumberField/index.tsx +13 -47
- package/src/components/ProjectBoard/forms/AddTask/client.tsx +70 -69
- package/src/components/SearchableDropdown/index.tsx +32 -19
- package/src/components/TextField/index.tsx +5 -5
- package/src/components/Toolbar/index.tsx +71 -67
- package/src/components/Toolbar/right/index.tsx +9 -13
- package/src/components/Toolbar/toolbar.stories.tsx +2 -2
- package/src/index.ts +3 -3
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goobs-frontend",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.30",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "A comprehensive React-based
|
|
5
|
+
"description": "A comprehensive React-based libary that extends the functionality of Material-UI",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "./src/index.ts",
|
|
8
8
|
"types": "./src/index.ts",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"dev": "next dev",
|
|
17
17
|
"build": "next build",
|
|
18
|
-
"
|
|
18
|
+
"serve": "next start",
|
|
19
|
+
"start": "storybook dev -p 6006",
|
|
19
20
|
"lint": "next lint",
|
|
20
21
|
"storybook": "storybook dev -p 6006",
|
|
21
22
|
"chromatic": "chromatic --project-token=chpt_2d34a5f45351b6c",
|
|
@@ -47,27 +48,27 @@
|
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@chromatic-com/storybook": "^3.2.4",
|
|
49
50
|
"@next/eslint-plugin-next": "^15.1.6",
|
|
50
|
-
"@storybook/addon-essentials": "^8
|
|
51
|
-
"@storybook/addon-interactions": "^8
|
|
52
|
-
"@storybook/addon-onboarding": "8
|
|
53
|
-
"@storybook/blocks": "8
|
|
54
|
-
"@storybook/nextjs": "^8
|
|
55
|
-
"@storybook/react": "^8
|
|
56
|
-
"@storybook/test": "^8
|
|
57
|
-
"@types/react": "19
|
|
58
|
-
"@types/react-dom": "^19
|
|
59
|
-
"@typescript-eslint/eslint-plugin": "^8
|
|
60
|
-
"@typescript-eslint/parser": "^8
|
|
51
|
+
"@storybook/addon-essentials": "^8",
|
|
52
|
+
"@storybook/addon-interactions": "^8",
|
|
53
|
+
"@storybook/addon-onboarding": "^8",
|
|
54
|
+
"@storybook/blocks": "^8",
|
|
55
|
+
"@storybook/nextjs": "^8",
|
|
56
|
+
"@storybook/react": "^8",
|
|
57
|
+
"@storybook/test": "^8",
|
|
58
|
+
"@types/react": "^19",
|
|
59
|
+
"@types/react-dom": "^19",
|
|
60
|
+
"@typescript-eslint/eslint-plugin": "^8",
|
|
61
|
+
"@typescript-eslint/parser": "^8",
|
|
61
62
|
"chromatic": "^11.25.1",
|
|
62
|
-
"eslint": "^9
|
|
63
|
+
"eslint": "^9",
|
|
63
64
|
"eslint-config-next": "^15.1.6",
|
|
64
65
|
"eslint-config-prettier": "^10.0.1",
|
|
65
66
|
"eslint-plugin-prettier": "^5.2.3",
|
|
66
67
|
"eslint-plugin-storybook": "^0.11.2",
|
|
67
|
-
"prettier": "^3
|
|
68
|
-
"react": "^19
|
|
69
|
-
"react-dom": "^19
|
|
70
|
-
"typescript": "^5
|
|
68
|
+
"prettier": "^3",
|
|
69
|
+
"react": "^19",
|
|
70
|
+
"react-dom": "^19",
|
|
71
|
+
"typescript": "^5"
|
|
71
72
|
},
|
|
72
73
|
"files": [
|
|
73
74
|
"src"
|
|
@@ -1,20 +1,35 @@
|
|
|
1
|
-
// src
|
|
1
|
+
// src/components/ComplexTextEditor/SimpleEditor.tsx
|
|
2
2
|
|
|
3
3
|
import React from 'react'
|
|
4
4
|
import { Box, TextField } from '@mui/material'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Extend the simple editor props to support TextField behaviors:
|
|
8
|
+
* - error
|
|
9
|
+
* - helperText
|
|
10
|
+
* - required
|
|
11
|
+
*/
|
|
6
12
|
type SimpleEditorProps = {
|
|
7
13
|
value: string
|
|
8
14
|
setValue: (value: string) => void
|
|
9
15
|
minRows?: number
|
|
10
16
|
label?: string
|
|
17
|
+
error?: boolean
|
|
18
|
+
helperText?: React.ReactNode
|
|
19
|
+
required?: boolean
|
|
11
20
|
}
|
|
12
21
|
|
|
22
|
+
/**
|
|
23
|
+
* A simple multiline text editor.
|
|
24
|
+
*/
|
|
13
25
|
const SimpleEditor: React.FC<SimpleEditorProps> = ({
|
|
14
26
|
value,
|
|
15
27
|
setValue,
|
|
16
28
|
minRows = 5,
|
|
17
29
|
label,
|
|
30
|
+
error,
|
|
31
|
+
helperText,
|
|
32
|
+
required,
|
|
18
33
|
}) => {
|
|
19
34
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
20
35
|
setValue(event.target.value)
|
|
@@ -31,10 +46,13 @@ const SimpleEditor: React.FC<SimpleEditorProps> = ({
|
|
|
31
46
|
<TextField
|
|
32
47
|
fullWidth
|
|
33
48
|
multiline
|
|
34
|
-
label={label}
|
|
35
49
|
variant="outlined"
|
|
36
50
|
minRows={minRows}
|
|
51
|
+
label={label}
|
|
37
52
|
value={value}
|
|
53
|
+
error={error}
|
|
54
|
+
helperText={helperText}
|
|
55
|
+
required={required}
|
|
38
56
|
onChange={handleChange}
|
|
39
57
|
sx={{
|
|
40
58
|
'& .MuiOutlinedInput-root': {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src
|
|
1
|
+
// src/components/ComplexTextEditor/index.tsx
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect } from 'react'
|
|
4
4
|
import { Box } from '@mui/material'
|
|
@@ -7,6 +7,11 @@ import ComplexToolbar, { EditorMode } from './Toolbars/Complex'
|
|
|
7
7
|
|
|
8
8
|
export type EditorType = 'simple' | 'markdown' | 'rich' | 'complex'
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* We extend ComplexTextEditorProps to allow the same
|
|
12
|
+
* text field behavior (error, helperText, required)
|
|
13
|
+
* that we introduced in SimpleEditor.
|
|
14
|
+
*/
|
|
10
15
|
export interface ComplexTextEditorProps {
|
|
11
16
|
value: string
|
|
12
17
|
onChange?: (val: string) => void
|
|
@@ -14,6 +19,9 @@ export interface ComplexTextEditorProps {
|
|
|
14
19
|
minRows?: number
|
|
15
20
|
accordion?: boolean
|
|
16
21
|
editorType?: EditorType
|
|
22
|
+
error?: boolean
|
|
23
|
+
helperText?: React.ReactNode
|
|
24
|
+
required?: boolean
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
const ComplexTextEditor: React.FC<ComplexTextEditorProps> = ({
|
|
@@ -22,15 +30,16 @@ const ComplexTextEditor: React.FC<ComplexTextEditorProps> = ({
|
|
|
22
30
|
label,
|
|
23
31
|
minRows = 5,
|
|
24
32
|
editorType = 'complex',
|
|
33
|
+
error,
|
|
34
|
+
helperText,
|
|
35
|
+
required,
|
|
25
36
|
}) => {
|
|
26
|
-
// If editorType is complex, start in simple mode
|
|
27
37
|
const [mode, setMode] = useState<EditorMode>(
|
|
28
38
|
editorType === 'complex' ? 'simple' : editorType
|
|
29
39
|
)
|
|
30
40
|
const [simpleText, setSimpleText] = useState(value)
|
|
31
41
|
|
|
32
42
|
useEffect(() => {
|
|
33
|
-
// If the input value changes from outside, update simpleText
|
|
34
43
|
setSimpleText(value)
|
|
35
44
|
}, [value])
|
|
36
45
|
|
|
@@ -42,7 +51,8 @@ const ComplexTextEditor: React.FC<ComplexTextEditorProps> = ({
|
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
const renderEditor = () => {
|
|
45
|
-
//
|
|
54
|
+
// Currently, we only support "simple" mode directly (and the "else" path).
|
|
55
|
+
// If you implement "rich"/"markdown", you'd handle it similarly.
|
|
46
56
|
if (mode === 'simple') {
|
|
47
57
|
return (
|
|
48
58
|
<SimpleEditor
|
|
@@ -50,17 +60,23 @@ const ComplexTextEditor: React.FC<ComplexTextEditorProps> = ({
|
|
|
50
60
|
setValue={handleSimpleTextChange}
|
|
51
61
|
minRows={minRows}
|
|
52
62
|
label={label}
|
|
63
|
+
error={error}
|
|
64
|
+
helperText={helperText}
|
|
65
|
+
required={required}
|
|
53
66
|
/>
|
|
54
67
|
)
|
|
55
68
|
}
|
|
56
69
|
|
|
57
|
-
//
|
|
70
|
+
// Default to "simple" if mode is something else unhandled
|
|
58
71
|
return (
|
|
59
72
|
<SimpleEditor
|
|
60
73
|
value={simpleText}
|
|
61
74
|
setValue={handleSimpleTextChange}
|
|
62
75
|
minRows={minRows}
|
|
63
76
|
label={label}
|
|
77
|
+
error={error}
|
|
78
|
+
helperText={helperText}
|
|
79
|
+
required={required}
|
|
64
80
|
/>
|
|
65
81
|
)
|
|
66
82
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import MultipleSelectChip, { MultiSelectChipProps } from '../../../MultiSelect'
|
|
4
|
+
import { columnconfig, cellconfig } from '../../../Grid'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extend MultiSelectChipProps for use in the grid system.
|
|
8
|
+
*/
|
|
9
|
+
export type ExtendedMultiSelectProps = MultiSelectChipProps & {
|
|
10
|
+
/**
|
|
11
|
+
* Additional configuration for the grid column.
|
|
12
|
+
*/
|
|
13
|
+
columnconfig?: Omit<columnconfig, 'component'> & {
|
|
14
|
+
component?: columnconfig['component']
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Configuration for the individual cell.
|
|
18
|
+
*/
|
|
19
|
+
cellconfig?: cellconfig
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface MultiSelectGridProps {
|
|
23
|
+
multiSelect?: ExtendedMultiSelectProps | ExtendedMultiSelectProps[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Custom hook to transform multiSelect props into `columnconfig` entries.
|
|
28
|
+
*/
|
|
29
|
+
const useMultiSelect = (
|
|
30
|
+
grid: MultiSelectGridProps
|
|
31
|
+
): columnconfig | columnconfig[] | null => {
|
|
32
|
+
const { multiSelect } = grid
|
|
33
|
+
|
|
34
|
+
if (!multiSelect) return null
|
|
35
|
+
|
|
36
|
+
// Helper function to build each columnconfig item
|
|
37
|
+
const renderMultiSelect = (
|
|
38
|
+
item: ExtendedMultiSelectProps,
|
|
39
|
+
index: number
|
|
40
|
+
): columnconfig => {
|
|
41
|
+
const { columnconfig: itemColumnConfig, cellconfig, ...restProps } = item
|
|
42
|
+
|
|
43
|
+
// Validate required grid configuration
|
|
44
|
+
if (
|
|
45
|
+
!itemColumnConfig ||
|
|
46
|
+
typeof itemColumnConfig !== 'object' ||
|
|
47
|
+
typeof itemColumnConfig.row !== 'number' ||
|
|
48
|
+
typeof itemColumnConfig.column !== 'number'
|
|
49
|
+
) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
'columnconfig must be an object with row and column as numbers'
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...itemColumnConfig,
|
|
57
|
+
cellconfig: {
|
|
58
|
+
...cellconfig,
|
|
59
|
+
},
|
|
60
|
+
component: (
|
|
61
|
+
<MultipleSelectChip key={`multiselect-${index}`} {...restProps} />
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// If it's an array, process each item; otherwise process a single item
|
|
67
|
+
if (Array.isArray(multiSelect)) {
|
|
68
|
+
return multiSelect.map(renderMultiSelect)
|
|
69
|
+
} else {
|
|
70
|
+
return renderMultiSelect(multiSelect, 0)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default useMultiSelect
|
|
@@ -75,6 +75,9 @@ import useAccordion, {
|
|
|
75
75
|
import useProjectBoard, {
|
|
76
76
|
ExtendedProjectBoardProps,
|
|
77
77
|
} from './Structure/projectboard/useProjectBoard'
|
|
78
|
+
import useMultiSelect, {
|
|
79
|
+
ExtendedMultiSelectProps,
|
|
80
|
+
} from './Structure/multiSelect/useMultiSelect'
|
|
78
81
|
|
|
79
82
|
/**
|
|
80
83
|
* Props for the ContentSection component.
|
|
@@ -118,6 +121,7 @@ export interface ContentSectionProps {
|
|
|
118
121
|
| ExtendedPhoneNumberFieldProps
|
|
119
122
|
| ExtendedPhoneNumberFieldProps[]
|
|
120
123
|
checkbox?: ExtendedCheckboxProps | ExtendedCheckboxProps[]
|
|
124
|
+
multiSelect?: ExtendedMultiSelectProps | ExtendedMultiSelectProps[]
|
|
121
125
|
}>
|
|
122
126
|
width?: number
|
|
123
127
|
}
|
|
@@ -162,6 +166,7 @@ const RenderContent: React.FC<
|
|
|
162
166
|
addToColumnConfigs(useDateField(props))
|
|
163
167
|
addToColumnConfigs(useProjectBoard(props))
|
|
164
168
|
addToColumnConfigs(useAccordion(props))
|
|
169
|
+
addToColumnConfigs(useMultiSelect(props))
|
|
165
170
|
addToColumnConfigs(useCheckbox(props))
|
|
166
171
|
addToColumnConfigs(usePhoneNumber(props))
|
|
167
172
|
addToColumnConfigs(useDropdown(props))
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React, { useCallback, useMemo, useState, useEffect } from 'react'
|
|
4
4
|
import type { ColumnDef, RowData } from '../types'
|
|
5
5
|
import type { SearchbarProps } from '../../Searchbar'
|
|
6
|
+
import * as palette from '../../../styles/palette'
|
|
6
7
|
|
|
7
8
|
interface UseSearchbarProps {
|
|
8
9
|
columns: ColumnDef[]
|
|
@@ -156,6 +157,11 @@ export const useSearchbar = ({
|
|
|
156
157
|
...searchbarProps,
|
|
157
158
|
value: searchValue,
|
|
158
159
|
onChange: handleSearchChange,
|
|
160
|
+
backgroundcolor: palette.semiTransparentWhite.main,
|
|
161
|
+
shrunkfontcolor: palette.white.main,
|
|
162
|
+
unshrunkfontcolor: palette.white.main,
|
|
163
|
+
shrunklabelposition: 'onNotch',
|
|
164
|
+
label: 'Search DataGrid',
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
return {
|
|
@@ -10,11 +10,13 @@ import { ExtendedTypographyProps } from '../../Content/Structure/typography/useG
|
|
|
10
10
|
export interface PopupProps {
|
|
11
11
|
open: boolean
|
|
12
12
|
/**
|
|
13
|
-
* Optional flag indicating the popup should be closed.
|
|
14
|
-
* This is just for observing the internal 'closed' state externally,
|
|
15
|
-
* or forcing the component closed from outside.
|
|
13
|
+
* Optional flag indicating the popup should be closed from the parent.
|
|
16
14
|
*/
|
|
17
|
-
close
|
|
15
|
+
close: boolean
|
|
16
|
+
/**
|
|
17
|
+
* Optional callback so the parent can be informed when user closes the dialog.
|
|
18
|
+
*/
|
|
19
|
+
onClose: () => void
|
|
18
20
|
title?: string
|
|
19
21
|
description?: string
|
|
20
22
|
grids?: ContentSectionProps['grids']
|
|
@@ -25,6 +27,7 @@ export interface PopupProps {
|
|
|
25
27
|
function Popup({
|
|
26
28
|
open,
|
|
27
29
|
close,
|
|
30
|
+
onClose, // <----- ADDED
|
|
28
31
|
title,
|
|
29
32
|
description,
|
|
30
33
|
grids,
|
|
@@ -109,6 +112,7 @@ function Popup({
|
|
|
109
112
|
const handleClose = () => {
|
|
110
113
|
setIsOpen(false)
|
|
111
114
|
setIsClosed(true)
|
|
115
|
+
onClose?.() // <----- CALL PARENT onClose IF PROVIDED
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
return (
|
|
@@ -1,57 +1,26 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
import React, { useState, useCallback } from 'react'
|
|
3
|
-
import
|
|
4
|
-
import { styled } from '@mui/material/styles'
|
|
3
|
+
import TextField, { TextFieldProps } from '../TextField'
|
|
5
4
|
|
|
6
5
|
export interface NumberFieldProps extends Omit<TextFieldProps, 'onChange'> {
|
|
7
6
|
initialValue?: string
|
|
8
7
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* A standard ChangeEvent<HTMLInputElement> so parent can do
|
|
9
|
+
* e.g. (event) => parseInt(event.target.value) ...
|
|
11
10
|
*/
|
|
12
11
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
|
13
|
-
backgroundcolor?: string
|
|
14
|
-
outlinecolor?: string
|
|
15
|
-
fontcolor?: string
|
|
16
12
|
label?: string
|
|
17
13
|
min?: number
|
|
18
14
|
max?: number
|
|
19
15
|
}
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}>(({ theme, backgroundcolor, outlinecolor, fontcolor }) => ({
|
|
26
|
-
'& .MuiOutlinedInput-root': {
|
|
27
|
-
backgroundColor: backgroundcolor || theme.palette.background.paper,
|
|
28
|
-
'& fieldset': {
|
|
29
|
-
borderColor: outlinecolor || theme.palette.primary.main,
|
|
30
|
-
},
|
|
31
|
-
'&:hover fieldset': {
|
|
32
|
-
borderColor: outlinecolor || theme.palette.primary.main,
|
|
33
|
-
},
|
|
34
|
-
'&.Mui-focused fieldset': {
|
|
35
|
-
borderColor: outlinecolor || theme.palette.primary.main,
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
'& .MuiInputLabel-root': {
|
|
39
|
-
color: fontcolor || theme.palette.text.primary,
|
|
40
|
-
'&.Mui-focused': {
|
|
41
|
-
color: fontcolor || theme.palette.primary.main,
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
'& .MuiInputBase-input': {
|
|
45
|
-
color: fontcolor || theme.palette.text.primary,
|
|
46
|
-
},
|
|
47
|
-
}))
|
|
48
|
-
|
|
17
|
+
/**
|
|
18
|
+
* A controlled numeric field that only allows digits
|
|
19
|
+
* and optionally enforces min/max constraints.
|
|
20
|
+
*/
|
|
49
21
|
const NumberField: React.FC<NumberFieldProps> = ({
|
|
50
22
|
initialValue = '',
|
|
51
23
|
onChange,
|
|
52
|
-
backgroundcolor,
|
|
53
|
-
outlinecolor,
|
|
54
|
-
fontcolor,
|
|
55
24
|
label,
|
|
56
25
|
min,
|
|
57
26
|
max,
|
|
@@ -63,13 +32,16 @@ const NumberField: React.FC<NumberFieldProps> = ({
|
|
|
63
32
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
64
33
|
const newValue = event.target.value.replace(/[^0-9]/g, '')
|
|
65
34
|
|
|
35
|
+
// Let parent know even if newValue is empty:
|
|
36
|
+
onChange?.(event)
|
|
37
|
+
|
|
66
38
|
if (newValue === '') {
|
|
67
39
|
setValue('')
|
|
68
|
-
onChange?.(event) // Pass empty string up
|
|
69
40
|
return
|
|
70
41
|
}
|
|
71
42
|
|
|
72
43
|
const numValue = parseInt(newValue, 10)
|
|
44
|
+
|
|
73
45
|
if (min !== undefined && numValue < min) {
|
|
74
46
|
setValue(String(min))
|
|
75
47
|
} else if (max !== undefined && numValue > max) {
|
|
@@ -77,24 +49,18 @@ const NumberField: React.FC<NumberFieldProps> = ({
|
|
|
77
49
|
} else {
|
|
78
50
|
setValue(newValue)
|
|
79
51
|
}
|
|
80
|
-
|
|
81
|
-
// Call parent's onChange so it knows about the new event/value
|
|
82
|
-
onChange?.(event)
|
|
83
52
|
},
|
|
84
53
|
[onChange, min, max]
|
|
85
54
|
)
|
|
86
55
|
|
|
87
56
|
return (
|
|
88
|
-
<
|
|
57
|
+
<TextField
|
|
89
58
|
value={value}
|
|
90
59
|
onChange={handleChange}
|
|
91
|
-
backgroundcolor={backgroundcolor}
|
|
92
|
-
outlinecolor={outlinecolor}
|
|
93
|
-
fontcolor={fontcolor}
|
|
94
60
|
label={label}
|
|
95
|
-
variant="outlined"
|
|
96
61
|
type="text"
|
|
97
62
|
inputMode="numeric"
|
|
63
|
+
variant="outlined"
|
|
98
64
|
{...rest}
|
|
99
65
|
/>
|
|
100
66
|
)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import React, { useState
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
import
|
|
5
|
+
// Popup with open/close
|
|
6
|
+
import Popup from '../../../Form/Popup'
|
|
7
7
|
import { ContentSectionProps } from '../../../Content'
|
|
8
8
|
|
|
9
9
|
// Types from "../index"
|
|
@@ -22,13 +22,27 @@ import type {
|
|
|
22
22
|
export type AddTaskVariant = 'customer' | 'company' | 'administrator'
|
|
23
23
|
|
|
24
24
|
export interface AddTaskProps {
|
|
25
|
+
/**
|
|
26
|
+
* Controls whether the Popup is open or closed.
|
|
27
|
+
*/
|
|
25
28
|
open: boolean
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* If `true`, force the Popup closed.
|
|
32
|
+
*/
|
|
33
|
+
close?: boolean
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Called when user closes the dialog (via Cancel, X button, backdrop, etc.).
|
|
37
|
+
*/
|
|
26
38
|
onClose: () => void
|
|
39
|
+
|
|
27
40
|
/**
|
|
28
41
|
* Called when the user clicks "Create Task."
|
|
29
42
|
* We pass back an Omit<Task, '_id'> that the parent can store.
|
|
30
43
|
*/
|
|
31
44
|
onSubmit: (newTask: Omit<Task, '_id'>) => void
|
|
45
|
+
|
|
32
46
|
/**
|
|
33
47
|
* Controls which fields appear:
|
|
34
48
|
* - 'customer'
|
|
@@ -50,6 +64,7 @@ export interface AddTaskProps {
|
|
|
50
64
|
|
|
51
65
|
const AddTask: React.FC<AddTaskProps> = ({
|
|
52
66
|
open,
|
|
67
|
+
close,
|
|
53
68
|
onClose,
|
|
54
69
|
onSubmit,
|
|
55
70
|
variant = 'customer',
|
|
@@ -57,6 +72,7 @@ const AddTask: React.FC<AddTaskProps> = ({
|
|
|
57
72
|
subStatuses,
|
|
58
73
|
topics,
|
|
59
74
|
schedulingQueues,
|
|
75
|
+
knowledgebaseArticles,
|
|
60
76
|
customers,
|
|
61
77
|
employees,
|
|
62
78
|
severityLevels,
|
|
@@ -64,38 +80,27 @@ const AddTask: React.FC<AddTaskProps> = ({
|
|
|
64
80
|
// -------------------------------------------------------------------------
|
|
65
81
|
// 1) Local state for fields
|
|
66
82
|
// -------------------------------------------------------------------------
|
|
67
|
-
//
|
|
68
|
-
const [
|
|
69
|
-
const [
|
|
70
|
-
const [selectedEmployee, setSelectedEmployee] = useState('') // for "customer"
|
|
83
|
+
const [selectedAccount, setSelectedAccount] = useState('') // For "customer" or "company"
|
|
84
|
+
const [selectedAdministrator, setSelectedAdministrator] = useState('') // For "company"
|
|
85
|
+
const [selectedEmployee, setSelectedEmployee] = useState('') // For "customer"
|
|
71
86
|
|
|
72
87
|
const [selectedSeverity, setSelectedSeverity] = useState('')
|
|
73
88
|
const [selectedQueue, setSelectedQueue] = useState('')
|
|
74
89
|
const [selectedStatus, setSelectedStatus] = useState('')
|
|
75
90
|
const [selectedSubStatus, setSelectedSubStatus] = useState('')
|
|
76
91
|
|
|
77
|
-
|
|
78
|
-
const [
|
|
92
|
+
// MultiSelect states for topics & knowledgebase articles (by ID)
|
|
93
|
+
const [selectedTopicIds, setSelectedTopicIds] = useState<string[]>([])
|
|
94
|
+
const [selectedArticleIds, setSelectedArticleIds] = useState<string[]>([])
|
|
79
95
|
|
|
80
96
|
const [taskTitle, setTaskTitle] = useState('')
|
|
81
97
|
const [taskDescription, setTaskDescription] = useState('')
|
|
82
98
|
|
|
83
99
|
// -------------------------------------------------------------------------
|
|
84
|
-
// 2)
|
|
100
|
+
// 2) Convert raw data to dropdown-friendly arrays
|
|
101
|
+
// (For the <Dropdown> fields, not the multiSelect.)
|
|
85
102
|
// -------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
// If you want all topics initially unassigned:
|
|
88
|
-
const allTopicValues = topics.map(t => t.topic)
|
|
89
|
-
setUnassignedTopics(allTopicValues)
|
|
90
|
-
setAssignedTopics([])
|
|
91
|
-
}, [topics])
|
|
92
|
-
|
|
93
|
-
// -------------------------------------------------------------------------
|
|
94
|
-
// 3) Convert raw data to dropdown-friendly arrays
|
|
95
|
-
// -------------------------------------------------------------------------
|
|
96
|
-
const severityOptions = severityLevels.map(sl => ({
|
|
97
|
-
value: sl._id, // or sl._id if you store it that way
|
|
98
|
-
}))
|
|
103
|
+
const severityOptions = severityLevels.map(sl => ({ value: sl._id }))
|
|
99
104
|
const statusOptions = statuses.map(s => ({ value: s._id }))
|
|
100
105
|
const subStatusOptions = subStatuses.map(s => ({ value: s._id }))
|
|
101
106
|
const queueOptions = schedulingQueues.map(q => ({ value: q._id }))
|
|
@@ -105,7 +110,7 @@ const AddTask: React.FC<AddTaskProps> = ({
|
|
|
105
110
|
attribute1: [c.firstName, c.lastName].filter(Boolean).join(' '),
|
|
106
111
|
}))
|
|
107
112
|
|
|
108
|
-
// Example "company accounts"
|
|
113
|
+
// Example "company accounts"
|
|
109
114
|
const companyAccountOptions = [{ value: 'AcmeInc' }, { value: 'TechCorp' }]
|
|
110
115
|
|
|
111
116
|
// For assigning employees or administrators
|
|
@@ -115,76 +120,56 @@ const AddTask: React.FC<AddTaskProps> = ({
|
|
|
115
120
|
}))
|
|
116
121
|
|
|
117
122
|
// -------------------------------------------------------------------------
|
|
118
|
-
//
|
|
119
|
-
// -------------------------------------------------------------------------
|
|
120
|
-
const handleTopicsChange = (left: string[], right: string[]) => {
|
|
121
|
-
setUnassignedTopics(left)
|
|
122
|
-
setAssignedTopics(right)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// -------------------------------------------------------------------------
|
|
126
|
-
// 5) Handle "Submit"
|
|
123
|
+
// 3) Handle "Submit"
|
|
127
124
|
// -------------------------------------------------------------------------
|
|
128
125
|
const handleSubmit = () => {
|
|
129
|
-
// Build
|
|
126
|
+
// Build Omit<Task, '_id'>
|
|
130
127
|
const newTaskData: Omit<Task, '_id'> = {
|
|
131
128
|
title: taskTitle,
|
|
132
129
|
description: taskDescription,
|
|
133
|
-
topicIds:
|
|
130
|
+
topicIds: selectedTopicIds, // store topic IDs
|
|
131
|
+
articleIds: selectedArticleIds, // store knowledgebase article IDs
|
|
134
132
|
}
|
|
135
133
|
|
|
136
|
-
// Severity => severityId
|
|
137
134
|
if (selectedSeverity) {
|
|
138
135
|
newTaskData.severityId = selectedSeverity
|
|
139
136
|
}
|
|
140
|
-
|
|
141
|
-
// Scheduling queue => schedulingQueueId
|
|
142
137
|
if (selectedQueue) {
|
|
143
138
|
newTaskData.schedulingQueueId = selectedQueue
|
|
144
139
|
}
|
|
145
|
-
|
|
146
|
-
// Status => statusId
|
|
147
140
|
if (selectedStatus) {
|
|
148
141
|
newTaskData.statusId = selectedStatus
|
|
149
142
|
}
|
|
150
|
-
|
|
151
|
-
// Substatus => substatusId
|
|
152
143
|
if (selectedSubStatus) {
|
|
153
144
|
newTaskData.substatusId = selectedSubStatus
|
|
154
145
|
}
|
|
155
146
|
|
|
156
147
|
// Variant-specific fields
|
|
157
148
|
if (variant === 'customer') {
|
|
158
|
-
// "Customer Account" => newTaskData.customerId
|
|
159
149
|
newTaskData.customerId = selectedAccount || undefined
|
|
160
|
-
// "Assigned Employee" => newTaskData.employeeIds = [selectedEmployee]
|
|
161
150
|
if (selectedEmployee) {
|
|
162
151
|
newTaskData.employeeIds = [selectedEmployee]
|
|
163
152
|
}
|
|
164
153
|
} else if (variant === 'company') {
|
|
165
|
-
// "Company Account" => newTaskData.companyId
|
|
166
154
|
newTaskData.companyId = selectedAccount || undefined
|
|
167
|
-
// "Assigned Administrator" => newTaskData.employeeIds = [selectedAdministrator]
|
|
168
155
|
if (selectedAdministrator) {
|
|
169
156
|
newTaskData.employeeIds = [selectedAdministrator]
|
|
170
157
|
}
|
|
171
|
-
} else if (variant === 'administrator') {
|
|
172
|
-
// e.g., no special account fields
|
|
173
|
-
// Possibly defaults or skip
|
|
174
158
|
}
|
|
175
159
|
|
|
160
|
+
// 'administrator' => no special account fields
|
|
176
161
|
onSubmit(newTaskData)
|
|
177
162
|
}
|
|
178
163
|
|
|
179
164
|
// -------------------------------------------------------------------------
|
|
180
|
-
//
|
|
165
|
+
// 4) Type guard for filter(Boolean)-style usage
|
|
181
166
|
// -------------------------------------------------------------------------
|
|
182
167
|
function isDefinedDropdown<T>(val: T | false | null | undefined): val is T {
|
|
183
168
|
return Boolean(val)
|
|
184
169
|
}
|
|
185
170
|
|
|
186
171
|
// -------------------------------------------------------------------------
|
|
187
|
-
//
|
|
172
|
+
// 5) Define the grids for ContentSection
|
|
188
173
|
// -------------------------------------------------------------------------
|
|
189
174
|
const mainGrid: ContentSectionProps['grids'][0] = {
|
|
190
175
|
grid: {
|
|
@@ -195,7 +180,7 @@ const AddTask: React.FC<AddTaskProps> = ({
|
|
|
195
180
|
dropdown: (
|
|
196
181
|
[
|
|
197
182
|
// ----------------------------
|
|
198
|
-
// "customer"
|
|
183
|
+
// "customer"
|
|
199
184
|
// ----------------------------
|
|
200
185
|
variant === 'customer' && {
|
|
201
186
|
label: 'Customer Account',
|
|
@@ -217,7 +202,7 @@ const AddTask: React.FC<AddTaskProps> = ({
|
|
|
217
202
|
},
|
|
218
203
|
|
|
219
204
|
// ----------------------------
|
|
220
|
-
// "company"
|
|
205
|
+
// "company"
|
|
221
206
|
// ----------------------------
|
|
222
207
|
variant === 'company' && {
|
|
223
208
|
label: 'Company Account',
|
|
@@ -239,7 +224,7 @@ const AddTask: React.FC<AddTaskProps> = ({
|
|
|
239
224
|
},
|
|
240
225
|
|
|
241
226
|
// ----------------------------
|
|
242
|
-
// row=2 => Severity, Queue
|
|
227
|
+
// row=2 => Severity, Queue
|
|
243
228
|
// ----------------------------
|
|
244
229
|
{
|
|
245
230
|
label: 'Severity Level',
|
|
@@ -261,7 +246,7 @@ const AddTask: React.FC<AddTaskProps> = ({
|
|
|
261
246
|
},
|
|
262
247
|
|
|
263
248
|
// ----------------------------
|
|
264
|
-
// row=3 => Status, Substatus
|
|
249
|
+
// row=3 => Status, Substatus
|
|
265
250
|
// ----------------------------
|
|
266
251
|
{
|
|
267
252
|
label: 'Status',
|
|
@@ -294,30 +279,43 @@ const AddTask: React.FC<AddTaskProps> = ({
|
|
|
294
279
|
)[]
|
|
295
280
|
).filter(isDefinedDropdown),
|
|
296
281
|
|
|
297
|
-
//
|
|
298
|
-
|
|
282
|
+
// ----------------------------
|
|
283
|
+
// MultiSelect for "Topics" + "Knowledgebase Articles"
|
|
284
|
+
// (Storing IDs)
|
|
285
|
+
// ----------------------------
|
|
286
|
+
multiSelect: [
|
|
299
287
|
{
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
288
|
+
label: 'Topics (by ID)',
|
|
289
|
+
/**
|
|
290
|
+
* We'll store the IDs in the multi-select to keep
|
|
291
|
+
* `selectedTopicIds` consistent with the Task interface (topicIds).
|
|
292
|
+
*/
|
|
293
|
+
options: topics.map(t => t._id),
|
|
294
|
+
defaultSelected: selectedTopicIds,
|
|
295
|
+
onChange: (newVals: string[]) => setSelectedTopicIds(newVals),
|
|
305
296
|
columnconfig: { row: 4, column: 1, columnwidth: '100%' },
|
|
306
297
|
},
|
|
298
|
+
{
|
|
299
|
+
label: 'Knowledgebase Articles (by ID)',
|
|
300
|
+
options: knowledgebaseArticles.map(a => a._id),
|
|
301
|
+
defaultSelected: selectedArticleIds,
|
|
302
|
+
onChange: (newVals: string[]) => setSelectedArticleIds(newVals),
|
|
303
|
+
columnconfig: { row: 5, column: 1, columnwidth: '100%' },
|
|
304
|
+
},
|
|
307
305
|
],
|
|
308
306
|
|
|
309
|
-
// Task Title
|
|
307
|
+
// row=6 => Task Title
|
|
310
308
|
textfield: [
|
|
311
309
|
{
|
|
312
310
|
label: 'Task Title',
|
|
313
311
|
value: taskTitle,
|
|
314
312
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
315
313
|
setTaskTitle(e.target.value),
|
|
316
|
-
columnconfig: { row:
|
|
314
|
+
columnconfig: { row: 6, column: 1, columnwidth: '100%' },
|
|
317
315
|
},
|
|
318
316
|
],
|
|
319
317
|
|
|
320
|
-
// Task Description
|
|
318
|
+
// row=7 => Task Description
|
|
321
319
|
complexeditor: [
|
|
322
320
|
{
|
|
323
321
|
label: 'Task Description',
|
|
@@ -325,7 +323,7 @@ const AddTask: React.FC<AddTaskProps> = ({
|
|
|
325
323
|
value: taskDescription,
|
|
326
324
|
minRows: 5,
|
|
327
325
|
onChange: (val: string) => setTaskDescription(val),
|
|
328
|
-
columnconfig: { row:
|
|
326
|
+
columnconfig: { row: 7, column: 1, columnwidth: '100%' },
|
|
329
327
|
},
|
|
330
328
|
],
|
|
331
329
|
}
|
|
@@ -342,7 +340,7 @@ const AddTask: React.FC<AddTaskProps> = ({
|
|
|
342
340
|
text: 'Cancel',
|
|
343
341
|
backgroundcolor: 'none',
|
|
344
342
|
fontcolor: 'black',
|
|
345
|
-
onClick: onClose,
|
|
343
|
+
onClick: onClose, // Tells the parent to close
|
|
346
344
|
columnconfig: { row: 1, column: 1 },
|
|
347
345
|
},
|
|
348
346
|
{
|
|
@@ -356,11 +354,14 @@ const AddTask: React.FC<AddTaskProps> = ({
|
|
|
356
354
|
}
|
|
357
355
|
|
|
358
356
|
// -------------------------------------------------------------------------
|
|
359
|
-
//
|
|
357
|
+
// 6) Render
|
|
360
358
|
// -------------------------------------------------------------------------
|
|
361
359
|
return (
|
|
362
|
-
<
|
|
360
|
+
<Popup
|
|
363
361
|
open={open}
|
|
362
|
+
// If close is undefined, default to false so Popup sees a boolean
|
|
363
|
+
close={close ?? false}
|
|
364
|
+
onClose={onClose}
|
|
364
365
|
title="Create Task"
|
|
365
366
|
width={700}
|
|
366
367
|
grids={[mainGrid, buttonGrid]}
|
|
@@ -44,15 +44,17 @@ const StyledAutocomplete = styled(
|
|
|
44
44
|
shrunkfontcolor?: string
|
|
45
45
|
unshrunkfontcolor?: string
|
|
46
46
|
shrunklabelposition?: 'onNotch' | 'aboveNotch'
|
|
47
|
-
}>(
|
|
48
|
-
|
|
47
|
+
}>(props => {
|
|
48
|
+
const {
|
|
49
49
|
outlinecolor,
|
|
50
50
|
fontcolor,
|
|
51
51
|
inputfontcolor,
|
|
52
52
|
shrunkfontcolor,
|
|
53
53
|
unshrunkfontcolor,
|
|
54
54
|
shrunklabelposition,
|
|
55
|
-
}
|
|
55
|
+
} = props
|
|
56
|
+
|
|
57
|
+
return {
|
|
56
58
|
'& .MuiOutlinedInput-root': {
|
|
57
59
|
overflow: 'visible',
|
|
58
60
|
minHeight: '45px',
|
|
@@ -87,8 +89,8 @@ const StyledAutocomplete = styled(
|
|
|
87
89
|
}),
|
|
88
90
|
},
|
|
89
91
|
},
|
|
90
|
-
}
|
|
91
|
-
)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
92
94
|
|
|
93
95
|
const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
94
96
|
label,
|
|
@@ -192,24 +194,35 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
192
194
|
option.value.replace(/_/g, ' ').slice(1)
|
|
193
195
|
)
|
|
194
196
|
}}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Add an explicit type to the `props` parameter here to avoid
|
|
199
|
+
* the “Unsafe array destructuring of a tuple element with an `any` value” error.
|
|
200
|
+
*/
|
|
201
|
+
renderOption={(
|
|
202
|
+
liProps: React.HTMLAttributes<HTMLLIElement> & { key?: React.Key },
|
|
203
|
+
option: DropdownOption
|
|
204
|
+
) => {
|
|
205
|
+
// Destructure `key` so it won't be spread
|
|
206
|
+
const { key, ...restLiProps } = liProps
|
|
207
|
+
return (
|
|
208
|
+
<li key={key} {...restLiProps} style={{ color: black.main }}>
|
|
203
209
|
<Typography
|
|
204
210
|
fontvariant="merriparagraph"
|
|
205
|
-
text={
|
|
206
|
-
option.attribute2 ? ` | ${option.attribute2}` : ''
|
|
207
|
-
}`}
|
|
211
|
+
text={option.value.replace(/_/g, ' ')}
|
|
208
212
|
fontcolor={black.main}
|
|
209
213
|
/>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
{option.attribute1 && (
|
|
215
|
+
<Typography
|
|
216
|
+
fontvariant="merriparagraph"
|
|
217
|
+
text={`${option.attribute1}${
|
|
218
|
+
option.attribute2 ? ` | ${option.attribute2}` : ''
|
|
219
|
+
}`}
|
|
220
|
+
fontcolor={black.main}
|
|
221
|
+
/>
|
|
222
|
+
)}
|
|
223
|
+
</li>
|
|
224
|
+
)
|
|
225
|
+
}}
|
|
213
226
|
renderInput={params => (
|
|
214
227
|
<TextField
|
|
215
228
|
{...params}
|
|
@@ -88,7 +88,7 @@ const StyledMuiTextField = styled(MuiTextField, {
|
|
|
88
88
|
}) => ({
|
|
89
89
|
'& .MuiOutlinedInput-root': {
|
|
90
90
|
minHeight: '40px',
|
|
91
|
-
height: '
|
|
91
|
+
height: 'auto', // allow vertical expansion
|
|
92
92
|
backgroundColor: backgroundcolor || 'inherit',
|
|
93
93
|
color: fontcolor || 'black',
|
|
94
94
|
'& .MuiSelect-icon': {
|
|
@@ -219,7 +219,6 @@ const TextField = React.memo<TextFieldProps>(props => {
|
|
|
219
219
|
<InputAdornment
|
|
220
220
|
position="end"
|
|
221
221
|
sx={{
|
|
222
|
-
// This styling ensures *all* icons (svg elements) in the end adornment are black
|
|
223
222
|
color: '#000000 !important',
|
|
224
223
|
'& svg': {
|
|
225
224
|
color: '#000000 !important',
|
|
@@ -268,10 +267,11 @@ const TextField = React.memo<TextFieldProps>(props => {
|
|
|
268
267
|
sx={{
|
|
269
268
|
display: 'flex',
|
|
270
269
|
flexDirection: 'column',
|
|
271
|
-
justifyContent: 'flex-
|
|
270
|
+
justifyContent: 'flex-start', // top-aligned so errors appear below
|
|
272
271
|
width: '100%',
|
|
273
|
-
|
|
274
|
-
|
|
272
|
+
marginTop: '15px',
|
|
273
|
+
height: 'auto', // allow expansion
|
|
274
|
+
overflow: 'visible', // ensure error messages are visible
|
|
275
275
|
...sx,
|
|
276
276
|
}}
|
|
277
277
|
onClick={handleClick}
|
|
@@ -15,134 +15,138 @@ import { SearchbarProps } from '../Searchbar'
|
|
|
15
15
|
* - `buttons` (optional) -> <Left />
|
|
16
16
|
* - `searchbarProps` (optional) -> <LeftCenter />
|
|
17
17
|
* - `rightCenterProps` (optional) -> <RightCenter />
|
|
18
|
-
* - `
|
|
18
|
+
* - `dropdowns` (optional, can be one or many) -> <Right /> (rendered in a loop)
|
|
19
19
|
*/
|
|
20
20
|
export interface CustomToolbarProps {
|
|
21
21
|
buttons?: CustomButtonProps[]
|
|
22
22
|
searchbarProps?: SearchbarProps
|
|
23
23
|
rightCenterProps?: RightCenterProps
|
|
24
|
-
|
|
24
|
+
dropdowns?: DropdownProps[] // <-- changed from "dropdown?" to "dropdowns?"
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const CustomToolbar: FC<CustomToolbarProps> = ({
|
|
28
28
|
buttons,
|
|
29
29
|
searchbarProps,
|
|
30
30
|
rightCenterProps,
|
|
31
|
-
|
|
31
|
+
dropdowns,
|
|
32
32
|
}) => {
|
|
33
|
-
//
|
|
34
|
-
// 1) Desktop: width > 1024px
|
|
33
|
+
// 1) Mobile: <= 600px
|
|
35
34
|
// 2) Tablet: 600px < width <= 1024px
|
|
36
|
-
// 3)
|
|
37
|
-
const isTabletOrBelow = useMediaQuery('(max-width:1024px)')
|
|
35
|
+
// 3) Desktop: > 1024px
|
|
38
36
|
const isMobile = useMediaQuery('(max-width:600px)')
|
|
37
|
+
const isTabletOrBelow = useMediaQuery('(max-width:1024px)')
|
|
39
38
|
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// if the searchbar is too big, it automatically wraps to a new line.
|
|
39
|
+
// ================== DESKTOP (width > 1024px) ==================
|
|
40
|
+
// Keep the searchbar visible, everything in one row.
|
|
43
41
|
if (!isTabletOrBelow) {
|
|
44
42
|
return (
|
|
45
43
|
<Box
|
|
46
44
|
sx={{
|
|
47
45
|
display: 'flex',
|
|
48
|
-
flexDirection: 'row',
|
|
49
|
-
flexWrap: 'wrap', // <-- Important to prevent overlap
|
|
50
46
|
alignItems: 'center',
|
|
51
|
-
|
|
47
|
+
justifyContent: 'space-between',
|
|
48
|
+
flexWrap: 'wrap',
|
|
49
|
+
gap: 2,
|
|
52
50
|
width: '100%',
|
|
51
|
+
mb: 2,
|
|
53
52
|
}}
|
|
54
53
|
>
|
|
55
|
-
{/* Buttons
|
|
56
|
-
<Left buttons={buttons} />
|
|
57
|
-
|
|
58
|
-
{/* Searchbar in the same row; will wrap if there's no room */}
|
|
59
|
-
{searchbarProps && <LeftCenter {...searchbarProps} />}
|
|
60
|
-
|
|
61
|
-
{/* RightCenter (e.g., ReusableSelector for selected rows) */}
|
|
62
|
-
{rightCenterProps && <RightCenter {...rightCenterProps} />}
|
|
63
|
-
|
|
64
|
-
{/* Dropdown on the far right (also wraps if needed) */}
|
|
65
|
-
{dropdown && <Right dropdown={dropdown} />}
|
|
66
|
-
</Box>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// =============== MOBILE (width <= 600px) ===============
|
|
71
|
-
// Hide the searchbar entirely; stack everything in separate rows if needed
|
|
72
|
-
if (isMobile) {
|
|
73
|
-
return (
|
|
74
|
-
<Box sx={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
|
|
75
|
-
{/* Row 1: Buttons (Left) */}
|
|
54
|
+
{/* Left half: Buttons + Searchbar */}
|
|
76
55
|
<Box
|
|
77
56
|
sx={{
|
|
78
57
|
display: 'flex',
|
|
79
58
|
alignItems: 'center',
|
|
80
|
-
flexWrap: 'wrap',
|
|
81
59
|
gap: 2,
|
|
60
|
+
flexWrap: 'wrap',
|
|
82
61
|
}}
|
|
83
62
|
>
|
|
84
63
|
<Left buttons={buttons} />
|
|
64
|
+
{searchbarProps && <LeftCenter {...searchbarProps} />}
|
|
85
65
|
</Box>
|
|
86
66
|
|
|
87
|
-
{/*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)}
|
|
67
|
+
{/* Right half: RightCenter + (multiple) dropdowns */}
|
|
68
|
+
<Box
|
|
69
|
+
sx={{
|
|
70
|
+
display: 'flex',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
gap: 2,
|
|
73
|
+
flexWrap: 'wrap',
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
{rightCenterProps && <RightCenter {...rightCenterProps} />}
|
|
77
|
+
{dropdowns?.map((dd, index) => <Right key={index} dropdown={dd} />)}
|
|
78
|
+
</Box>
|
|
100
79
|
</Box>
|
|
101
80
|
)
|
|
102
81
|
}
|
|
103
82
|
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{/* Row 1: Buttons (Left) */}
|
|
83
|
+
// ================== TABLET (600px < width <= 1024px) ==================
|
|
84
|
+
// Hide the searchbar, keep buttons on the left, rightCenter + dropdowns on the right.
|
|
85
|
+
if (!isMobile) {
|
|
86
|
+
return (
|
|
109
87
|
<Box
|
|
110
88
|
sx={{
|
|
111
89
|
display: 'flex',
|
|
112
90
|
alignItems: 'center',
|
|
91
|
+
justifyContent: 'space-between',
|
|
113
92
|
flexWrap: 'wrap',
|
|
114
93
|
gap: 2,
|
|
94
|
+
width: '100%',
|
|
95
|
+
mb: 2,
|
|
115
96
|
}}
|
|
116
97
|
>
|
|
117
|
-
|
|
118
|
-
|
|
98
|
+
{/* Left half: Buttons */}
|
|
99
|
+
<Box
|
|
100
|
+
sx={{
|
|
101
|
+
display: 'flex',
|
|
102
|
+
alignItems: 'center',
|
|
103
|
+
gap: 2,
|
|
104
|
+
flexWrap: 'wrap',
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<Left buttons={buttons} />
|
|
108
|
+
</Box>
|
|
119
109
|
|
|
120
|
-
|
|
121
|
-
{searchbarProps && (
|
|
110
|
+
{/* Right half: RightCenter + (multiple) dropdowns */}
|
|
122
111
|
<Box
|
|
123
112
|
sx={{
|
|
124
|
-
mt: 2,
|
|
125
113
|
display: 'flex',
|
|
126
|
-
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
gap: 2,
|
|
116
|
+
flexWrap: 'wrap',
|
|
127
117
|
}}
|
|
128
118
|
>
|
|
129
|
-
<
|
|
119
|
+
{rightCenterProps && <RightCenter {...rightCenterProps} />}
|
|
120
|
+
{dropdowns?.map((dd, index) => <Right key={index} dropdown={dd} />)}
|
|
130
121
|
</Box>
|
|
131
|
-
|
|
122
|
+
</Box>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
132
125
|
|
|
133
|
-
|
|
126
|
+
// ================== MOBILE (width <= 600px) ==================
|
|
127
|
+
// Hide the searchbar, stack everything vertically.
|
|
128
|
+
return (
|
|
129
|
+
<Box
|
|
130
|
+
sx={{ display: 'flex', flexDirection: 'column', width: '100%', mb: 2 }}
|
|
131
|
+
>
|
|
132
|
+
{/* Row 1: Buttons */}
|
|
133
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
134
|
+
<Left buttons={buttons} />
|
|
135
|
+
</Box>
|
|
136
|
+
|
|
137
|
+
{/* Row 2: RightCenter (selected rows actions) */}
|
|
134
138
|
{rightCenterProps && (
|
|
135
139
|
<Box sx={{ mt: 2 }}>
|
|
136
140
|
<RightCenter {...rightCenterProps} />
|
|
137
141
|
</Box>
|
|
138
142
|
)}
|
|
139
143
|
|
|
140
|
-
{/* Row
|
|
141
|
-
{
|
|
142
|
-
<Box sx={{ mt: 2 }}>
|
|
143
|
-
<Right dropdown={
|
|
144
|
+
{/* Row 3: (multiple) Dropdowns */}
|
|
145
|
+
{dropdowns?.map((dd, index) => (
|
|
146
|
+
<Box sx={{ mt: 2 }} key={index}>
|
|
147
|
+
<Right dropdown={dd} />
|
|
144
148
|
</Box>
|
|
145
|
-
)}
|
|
149
|
+
))}
|
|
146
150
|
</Box>
|
|
147
151
|
)
|
|
148
152
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// src/components/Toolbar/Right/index.tsx
|
|
2
|
-
|
|
3
1
|
'use client'
|
|
4
2
|
|
|
5
3
|
import React from 'react'
|
|
@@ -8,8 +6,8 @@ import Dropdown, { DropdownProps } from '../../Dropdown'
|
|
|
8
6
|
import { black } from '../../../styles/palette'
|
|
9
7
|
|
|
10
8
|
export interface RightProps {
|
|
11
|
-
/** A single dropdown to render
|
|
12
|
-
dropdown
|
|
9
|
+
/** A single dropdown to render. (We'll render multiple <Right> if needed.) */
|
|
10
|
+
dropdown: DropdownProps
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
function Right({ dropdown }: RightProps) {
|
|
@@ -25,15 +23,13 @@ function Right({ dropdown }: RightProps) {
|
|
|
25
23
|
width: '200px',
|
|
26
24
|
}}
|
|
27
25
|
>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/>
|
|
36
|
-
)}
|
|
26
|
+
<Dropdown
|
|
27
|
+
outlinecolor={black.main}
|
|
28
|
+
fontcolor={black.main}
|
|
29
|
+
shrunkfontcolor={black.main}
|
|
30
|
+
onChange={() => console.log('Dropdown changed')}
|
|
31
|
+
{...dropdown}
|
|
32
|
+
/>
|
|
37
33
|
</Box>
|
|
38
34
|
)
|
|
39
35
|
}
|
|
@@ -90,7 +90,7 @@ export const WithSearch: Story = {
|
|
|
90
90
|
export const WithDropdown: Story = {
|
|
91
91
|
args: {
|
|
92
92
|
buttons: [{ text: 'Only Button' }],
|
|
93
|
-
|
|
93
|
+
dropdowns: [sampleDropdown],
|
|
94
94
|
},
|
|
95
95
|
play: ({ canvasElement }) => {
|
|
96
96
|
const canvas = within(canvasElement)
|
|
@@ -115,7 +115,7 @@ export const FullSetup: Story = {
|
|
|
115
115
|
onShow: () => console.log('show'),
|
|
116
116
|
handleClose: () => console.log('close'),
|
|
117
117
|
},
|
|
118
|
-
|
|
118
|
+
dropdowns: [sampleDropdown],
|
|
119
119
|
},
|
|
120
120
|
play: ({ canvasElement }) => {
|
|
121
121
|
const canvas = within(canvasElement)
|
package/src/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ import RadioGroup, {
|
|
|
19
19
|
RadioOption,
|
|
20
20
|
RadioGroupProps,
|
|
21
21
|
} from './components/RadioGroup'
|
|
22
|
-
import
|
|
22
|
+
import Popup, { PopupProps } from './components/Form/Popup'
|
|
23
23
|
import Dialog, { DialogFormProps } from './components/Form/Dialog'
|
|
24
24
|
import ContentSection, { ContentSectionProps } from './components/Content'
|
|
25
25
|
import {
|
|
@@ -172,7 +172,7 @@ declare type ConfirmationCodeInputProps = React.ComponentProps<
|
|
|
172
172
|
typeof ConfirmationCodeInput
|
|
173
173
|
>
|
|
174
174
|
declare type RadioGroupComponentProps = React.ComponentProps<typeof RadioGroup>
|
|
175
|
-
declare type PopupFormComponentProps = React.ComponentProps<typeof
|
|
175
|
+
declare type PopupFormComponentProps = React.ComponentProps<typeof Popup>
|
|
176
176
|
declare type ContentSectionComponentProps = React.ComponentProps<
|
|
177
177
|
typeof ContentSection
|
|
178
178
|
>
|
|
@@ -225,7 +225,7 @@ export { CustomGrid }
|
|
|
225
225
|
export { Typography }
|
|
226
226
|
export { ConfirmationCodeInput }
|
|
227
227
|
export { RadioGroup }
|
|
228
|
-
export {
|
|
228
|
+
export { Popup }
|
|
229
229
|
export { ContentSection }
|
|
230
230
|
export { Accordion, AccordionSummary, AccordionDetails }
|
|
231
231
|
export { Card }
|