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 CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "goobs-frontend",
3
- "version": "0.8.29",
3
+ "version": "0.8.30",
4
4
  "type": "module",
5
- "description": "A comprehensive React-based UI library built on Material-UI, offering a wide range of customizable components including grids, typography, buttons, cards, forms, navigation, pricing tables, steppers, tooltips, accordions, and more. Designed for building responsive and consistent user interfaces with advanced features like form validation, theming, and code syntax highlighting.",
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
- "start": "next start",
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.5.1",
51
- "@storybook/addon-interactions": "^8.5.1",
52
- "@storybook/addon-onboarding": "8.5.1",
53
- "@storybook/blocks": "8.5.1",
54
- "@storybook/nextjs": "^8.5.1",
55
- "@storybook/react": "^8.5.1",
56
- "@storybook/test": "^8.5.1",
57
- "@types/react": "19.0.8",
58
- "@types/react-dom": "^19.0.3",
59
- "@typescript-eslint/eslint-plugin": "^8.21.0",
60
- "@typescript-eslint/parser": "^8.21.0",
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.19.0",
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.4.2",
68
- "react": "^19.0.0",
69
- "react-dom": "^19.0.0",
70
- "typescript": "^5.7.3"
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\components\ComplexTextEditor\SimpleEditor.tsx
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\components\ComplexTextEditor\index.tsx
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
- // For now, we only support 'simple' directly. You can expand to 'rich'/'markdown' if needed.
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
- // If you implement other modes (rich, markdown), handle them here.
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))
@@ -106,7 +106,7 @@ function DataGrid({
106
106
 
107
107
  <CustomToolbar
108
108
  buttons={buttons}
109
- dropdown={dropdowns?.[0]}
109
+ dropdowns={dropdowns?.[0] ? [dropdowns[0]] : undefined}
110
110
  searchbarProps={updatedSearchbarProps}
111
111
  rightCenterProps={
112
112
  selectedRows.length > 0
@@ -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?: boolean
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 { TextField, TextFieldProps } from '@mui/material'
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
- * Now accepts a standard ChangeEvent<HTMLInputElement>
10
- * so parent can do e.g. (event) => parseInt(event.target.value) ...
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
- const StyledTextField = styled(TextField)<{
22
- backgroundcolor?: string
23
- outlinecolor?: string
24
- fontcolor?: string
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
- <StyledTextField
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, useEffect } from 'react'
3
+ import React, { useState } from 'react'
4
4
 
5
- // Your PopupForm + ContentSection types
6
- import PopupForm from '../../../Form/Popup'
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
- // If variant="customer", store customerId + assignedEmployee (employeeIds)
68
- const [selectedAccount, setSelectedAccount] = useState('') // for "customer" or "company"
69
- const [selectedAdministrator, setSelectedAdministrator] = useState('') // for "company"
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
- const [unassignedTopics, setUnassignedTopics] = useState<string[]>([])
78
- const [assignedTopics, setAssignedTopics] = useState<string[]>([])
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) Initialize "topics" in the TransferList
100
+ // 2) Convert raw data to dropdown-friendly arrays
101
+ // (For the <Dropdown> fields, not the multiSelect.)
85
102
  // -------------------------------------------------------------------------
86
- useEffect(() => {
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" (placeholder). You could also supply these from props.
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
- // 4) TransferList handler
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 an Omit<Task, '_id'> that matches your Task interface fields
126
+ // Build Omit<Task, '_id'>
130
127
  const newTaskData: Omit<Task, '_id'> = {
131
128
  title: taskTitle,
132
129
  description: taskDescription,
133
- topicIds: assignedTopics,
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
- // 6) Type guard for filter(Boolean)-style usage
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
- // 7) Define the grids for ContentSection
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" => Customer Account + Assigned Employee
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" => Company Account + Assigned Administrator
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 (all variants)
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 (all variants)
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
- // TransferList
298
- transferlist: [
282
+ // ----------------------------
283
+ // MultiSelect for "Topics" + "Knowledgebase Articles"
284
+ // (Storing IDs)
285
+ // ----------------------------
286
+ multiSelect: [
299
287
  {
300
- leftItems: unassignedTopics,
301
- rightItems: assignedTopics,
302
- leftTitle: 'Unassigned Topics',
303
- rightTitle: 'Assigned Topics',
304
- onChange: handleTopicsChange,
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: 5, column: 1, columnwidth: '100%' },
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: 6, column: 1, columnwidth: '100%' },
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
- // 8) Render
357
+ // 6) Render
360
358
  // -------------------------------------------------------------------------
361
359
  return (
362
- <PopupForm
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
- renderOption={(liProps, option) => (
196
- <li {...liProps} style={{ color: black.main }}>
197
- <Typography
198
- fontvariant="merriparagraph"
199
- text={option.value.replace(/_/g, ' ')}
200
- fontcolor={black.main}
201
- />
202
- {option.attribute1 && (
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={`${option.attribute1}${
206
- option.attribute2 ? ` | ${option.attribute2}` : ''
207
- }`}
211
+ text={option.value.replace(/_/g, ' ')}
208
212
  fontcolor={black.main}
209
213
  />
210
- )}
211
- </li>
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: '40px',
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-end',
270
+ justifyContent: 'flex-start', // top-aligned so errors appear below
272
271
  width: '100%',
273
- height: '55px',
274
- overflow: 'hidden',
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
- * - `dropdown` (optional) -> <Right />
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
- dropdown?: DropdownProps
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
- dropdown,
31
+ dropdowns,
32
32
  }) => {
33
- // Three breakpoints:
34
- // 1) Desktop: width > 1024px
33
+ // 1) Mobile: <= 600px
35
34
  // 2) Tablet: 600px < width <= 1024px
36
- // 3) Mobile: width <= 600px
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
- // =============== DESKTOP (width > 1024px) ===============
41
- // Use a "wrap" layout so items never overlap;
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
- gap: 2, // Some spacing between items
47
+ justifyContent: 'space-between',
48
+ flexWrap: 'wrap',
49
+ gap: 2,
52
50
  width: '100%',
51
+ mb: 2,
53
52
  }}
54
53
  >
55
- {/* Buttons on the left */}
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
- {/* Row 2: RightCenter (if any) */}
88
- {rightCenterProps && (
89
- <Box sx={{ mt: 2 }}>
90
- <RightCenter {...rightCenterProps} />
91
- </Box>
92
- )}
93
-
94
- {/* Row 3: Dropdown (if any) */}
95
- {dropdown && (
96
- <Box sx={{ mt: 2 }}>
97
- <Right dropdown={dropdown} />
98
- </Box>
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
- // =============== TABLET (600px < width <= 1024px) ===============
105
- // Each item on its own row, with the searchbar explicitly centered.
106
- return (
107
- <Box sx={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
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
- <Left buttons={buttons} />
118
- </Box>
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
- {/* Row 2: Centered Searchbar */}
121
- {searchbarProps && (
110
+ {/* Right half: RightCenter + (multiple) dropdowns */}
122
111
  <Box
123
112
  sx={{
124
- mt: 2,
125
113
  display: 'flex',
126
- justifyContent: 'center', // center horizontally
114
+ alignItems: 'center',
115
+ gap: 2,
116
+ flexWrap: 'wrap',
127
117
  }}
128
118
  >
129
- <LeftCenter {...searchbarProps} />
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
- {/* Row 3: RightCenter */}
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 4: Dropdown */}
141
- {dropdown && (
142
- <Box sx={{ mt: 2 }}>
143
- <Right dropdown={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 on the right side. */
12
- dropdown?: DropdownProps
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
- {dropdown && (
29
- <Dropdown
30
- outlinecolor={black.main}
31
- fontcolor={black.main}
32
- shrunkfontcolor={black.main}
33
- onChange={() => console.log('Dropdown changed')}
34
- {...dropdown}
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
- dropdown: sampleDropdown,
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
- dropdown: sampleDropdown,
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 PopupForm, { PopupProps } from './components/Form/Popup'
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 PopupForm>
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 { PopupForm }
228
+ export { Popup }
229
229
  export { ContentSection }
230
230
  export { Accordion, AccordionSummary, AccordionDetails }
231
231
  export { Card }