goobs-frontend 0.8.6 → 0.8.8

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,267 @@
1
+ 'use client'
2
+ import React, { useState, useEffect } from 'react'
3
+ import { Box, Alert, CircularProgress } from '@mui/material'
4
+ import { useAtom, useSetAtom } from 'jotai'
5
+ import {
6
+ columnVisibilityAtom,
7
+ columnsAtom,
8
+ columnVisibilityActions,
9
+ } from './Jotai/atom'
10
+ import CustomToolbar from '../Toolbar'
11
+ import Table, { ColumnDef, RowData } from './Table'
12
+ import CustomFooter from './Footer'
13
+ import ManageRow from './ManageRow'
14
+ import type { CustomButtonProps } from '../Button'
15
+ import type { DropdownProps } from '../Dropdown'
16
+ import type { SearchbarProps } from '../Searchbar'
17
+ import { woad } from '../../styles/palette'
18
+ import { useSearchbar } from './utils/useSearchbar'
19
+
20
+ export interface DatagridProps {
21
+ columns: ColumnDef[]
22
+ rows: RowData[]
23
+ buttons?: CustomButtonProps[]
24
+ dropdowns?: Omit<DropdownProps, 'onChange'>[]
25
+ searchbarProps?: Omit<SearchbarProps, 'onChange' | 'value'>
26
+ onRefresh?: () => void
27
+ loading?: boolean
28
+ error?: Error | null
29
+ onDuplicate?: () => void
30
+ onDelete?: () => void
31
+ checkboxSelection?: boolean
32
+ selectedRows?: string[]
33
+ onSelectionChange?: (selectedIds: string[]) => void
34
+ }
35
+
36
+ function DataGrid({
37
+ columns,
38
+ rows: providedRows,
39
+ buttons,
40
+ dropdowns,
41
+ searchbarProps,
42
+ loading = false,
43
+ error = null,
44
+ onDuplicate,
45
+ onDelete,
46
+ }: DatagridProps) {
47
+ console.log('DataGrid render:', { columns, providedRows })
48
+
49
+ const [rows, setRows] = useState<RowData[]>(providedRows || [])
50
+ const [page, setPage] = useState(0)
51
+ const [pageSize, setPageSize] = useState(10)
52
+ const [selectedRows, setSelectedRows] = useState<string[]>([])
53
+ const [manageRowOpen, setManageRowOpen] = useState(false)
54
+ const [searchValue, setSearchValue] = useState('')
55
+ const tableRef = React.useRef(null)
56
+ const initialized = React.useRef(false)
57
+
58
+ // Jotai state setup
59
+ const setColumns = useSetAtom(columnsAtom)
60
+ const [columnVisibility] = useAtom(columnVisibilityAtom)
61
+ const updateVisibility = useSetAtom(columnVisibilityActions)
62
+
63
+ const {
64
+ handleSearchChange,
65
+ filteredRows,
66
+ visibleColumns: searchVisibleColumns,
67
+ tags,
68
+ } = useSearchbar({
69
+ columns,
70
+ rows,
71
+ searchValue,
72
+ setSearchValue,
73
+ })
74
+
75
+ // Update Jotai visibility state when search changes columns visibility
76
+ useEffect(() => {
77
+ if (tags.length > 0) {
78
+ const newVisibility: { [key: string]: boolean } = {}
79
+ columns.forEach(column => {
80
+ newVisibility[column.field] = searchVisibleColumns.has(column.field)
81
+ })
82
+ console.log('Updating column visibility from search:', newVisibility)
83
+ updateVisibility({
84
+ type: 'save',
85
+ newState: newVisibility,
86
+ })
87
+ }
88
+ }, [searchVisibleColumns, columns, updateVisibility, tags])
89
+
90
+ useEffect(() => {
91
+ setRows(providedRows || [])
92
+ }, [providedRows])
93
+
94
+ // Initialize columns and visibility in Jotai
95
+ useEffect(() => {
96
+ if (!initialized.current) {
97
+ console.log('Initializing columns and visibility:', columns)
98
+ setColumns(columns.map(col => col.field))
99
+
100
+ // Initialize visibility for new columns
101
+ const initialVisibility: { [key: string]: boolean } = {}
102
+ columns.forEach(column => {
103
+ if (columnVisibility[column.field] === undefined) {
104
+ initialVisibility[column.field] = true
105
+ }
106
+ })
107
+
108
+ if (Object.keys(initialVisibility).length > 0) {
109
+ console.log(
110
+ 'Setting initial visibility for columns:',
111
+ initialVisibility
112
+ )
113
+ updateVisibility({
114
+ type: 'save',
115
+ newState: { ...columnVisibility, ...initialVisibility },
116
+ })
117
+ }
118
+
119
+ initialized.current = true
120
+ }
121
+ }, [columns, setColumns, columnVisibility, updateVisibility])
122
+
123
+ const handleRowClick = (row: RowData) => {
124
+ console.log('Row clicked:', row)
125
+ const newSelection = selectedRows.includes(row.id)
126
+ ? selectedRows.filter(id => id !== row.id)
127
+ : [...selectedRows, row.id]
128
+
129
+ setSelectedRows(newSelection)
130
+ setManageRowOpen(newSelection.length > 0)
131
+ }
132
+
133
+ const handleSelectionChange = (selectedIds: string[]) => {
134
+ console.log('Selection changed:', selectedIds)
135
+ setSelectedRows(selectedIds)
136
+ setManageRowOpen(selectedIds.length > 0)
137
+ }
138
+
139
+ const updatedSearchbarProps = {
140
+ ...searchbarProps,
141
+ value: searchValue,
142
+ onChange: handleSearchChange,
143
+ }
144
+
145
+ // Get visible columns based on both Jotai state and search
146
+ const visibleColumns = columns.filter(column => {
147
+ const isVisibleInJotai = columnVisibility[column.field] !== false
148
+ const isVisibleInSearch =
149
+ tags.length === 0 || searchVisibleColumns.has(column.field)
150
+ const isVisible = isVisibleInJotai && isVisibleInSearch
151
+
152
+ console.log(`Column ${column.field} visibility:`, {
153
+ jotai: isVisibleInJotai,
154
+ search: isVisibleInSearch,
155
+ final: isVisible,
156
+ })
157
+
158
+ return isVisible
159
+ })
160
+
161
+ // Calculate visible rows based on current page and pageSize
162
+ const startIndex = page * pageSize
163
+ const visibleRows = filteredRows.slice(startIndex, startIndex + pageSize)
164
+
165
+ console.log('DataGrid rendering with:', {
166
+ visibleColumns,
167
+ visibleRows,
168
+ page,
169
+ pageSize,
170
+ selectedRows,
171
+ })
172
+
173
+ if (loading) {
174
+ return (
175
+ <Box
176
+ sx={{
177
+ position: 'relative',
178
+ marginTop: '60px',
179
+ display: 'flex',
180
+ justifyContent: 'center',
181
+ alignItems: 'center',
182
+ height: 'calc(100vh - 60px)',
183
+ width: 'calc(100vh)',
184
+ backgroundColor: woad.main,
185
+ }}
186
+ >
187
+ <CircularProgress />
188
+ </Box>
189
+ )
190
+ }
191
+
192
+ return (
193
+ <>
194
+ <Box
195
+ sx={{
196
+ position: 'relative',
197
+ marginLeft: '250px',
198
+ marginTop: '60px',
199
+ display: 'flex',
200
+ flexDirection: 'column',
201
+ height: 'calc(100vh - 60px)',
202
+ width: 'calc(100%)',
203
+ overflow: 'hidden',
204
+ p: 0,
205
+ m: 0,
206
+ backgroundColor: woad.main,
207
+ }}
208
+ >
209
+ {error && (
210
+ <Alert severity="error" sx={{ mb: 2 }}>
211
+ {error.message}
212
+ </Alert>
213
+ )}
214
+
215
+ <CustomToolbar
216
+ buttons={buttons}
217
+ dropdowns={dropdowns}
218
+ searchbarProps={updatedSearchbarProps}
219
+ />
220
+
221
+ <Box
222
+ sx={{
223
+ flexGrow: 1,
224
+ overflow: 'auto',
225
+ width: '100%',
226
+ display: 'flex',
227
+ flexDirection: 'column',
228
+ }}
229
+ >
230
+ <Table
231
+ ref={tableRef}
232
+ columns={visibleColumns}
233
+ rows={visibleRows}
234
+ loading={loading}
235
+ page={page}
236
+ pageSize={pageSize}
237
+ rowCount={filteredRows.length}
238
+ onPageChange={setPage}
239
+ onPageSizeChange={setPageSize}
240
+ onRowClick={handleRowClick}
241
+ selectedRows={selectedRows}
242
+ onSelectionChange={handleSelectionChange}
243
+ checkboxSelection
244
+ />
245
+
246
+ <CustomFooter
247
+ page={page}
248
+ pageSize={pageSize}
249
+ rowCount={filteredRows.length}
250
+ onPageChange={setPage}
251
+ onPageSizeChange={setPageSize}
252
+ columns={columns}
253
+ />
254
+ </Box>
255
+ </Box>
256
+
257
+ <ManageRow
258
+ open={manageRowOpen}
259
+ handleClose={() => setManageRowOpen(false)}
260
+ onDuplicate={onDuplicate}
261
+ onDelete={onDelete}
262
+ />
263
+ </>
264
+ )
265
+ }
266
+
267
+ export default DataGrid
@@ -0,0 +1,138 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+ import { useAtom, useSetAtom } from 'jotai'
3
+ import { columnVisibilityAtom, columnVisibilityActions } from '../Jotai/atom'
4
+ import type { ColumnDef } from '../Table'
5
+
6
+ type ColumnVisibilityModel = { [key: string]: boolean }
7
+
8
+ interface UseManageColumnProps {
9
+ columns: ColumnDef[]
10
+ handleClose: () => void
11
+ isPopupOpen: boolean
12
+ initialSearchInput?: string
13
+ }
14
+
15
+ export const useManageColumn = ({
16
+ columns,
17
+ handleClose,
18
+ isPopupOpen,
19
+ initialSearchInput = '',
20
+ }: UseManageColumnProps) => {
21
+ const [tempVisibleColumns, setTempVisibleColumns] =
22
+ useState<ColumnVisibilityModel>({})
23
+ const [columnVisibility] = useAtom(columnVisibilityAtom)
24
+ const updateVisibility = useSetAtom(columnVisibilityActions)
25
+ const [searchInput, setSearchInput] = useState(initialSearchInput)
26
+ const [isAllChecked, setIsAllChecked] = useState(true)
27
+ const initialized = useRef(false)
28
+
29
+ console.log('useManageColumn hook render:', {
30
+ isPopupOpen,
31
+ columnVisibility,
32
+ tempVisibleColumns,
33
+ searchInput,
34
+ })
35
+
36
+ useEffect(() => {
37
+ if (isPopupOpen) {
38
+ const currentVisibility: ColumnVisibilityModel = {}
39
+ columns.forEach(column => {
40
+ currentVisibility[column.field] = columnVisibility[column.field] ?? true
41
+ })
42
+ console.log('Initializing tempVisibleColumns:', currentVisibility)
43
+ setTempVisibleColumns(currentVisibility)
44
+ setIsAllChecked(
45
+ columns.every(column => currentVisibility[column.field] === true)
46
+ )
47
+ initialized.current = true
48
+ }
49
+ }, [isPopupOpen, columns, columnVisibility])
50
+
51
+ const handleAllCols = useCallback(
52
+ (checked: boolean) => {
53
+ console.log('handleAllCols called with checked:', checked)
54
+ setIsAllChecked(checked)
55
+
56
+ setTempVisibleColumns(prev => {
57
+ const newVisibility: ColumnVisibilityModel = {}
58
+ columns.forEach(column => {
59
+ newVisibility[column.field] = checked
60
+ })
61
+
62
+ console.log('handleAllCols:', {
63
+ before: prev,
64
+ after: newVisibility,
65
+ checked,
66
+ })
67
+
68
+ return newVisibility
69
+ })
70
+ },
71
+ [columns]
72
+ )
73
+
74
+ const toggleColumnState = useCallback(
75
+ (field: string) => {
76
+ setTempVisibleColumns(prev => {
77
+ const newState = {
78
+ ...prev,
79
+ [field]: !prev[field],
80
+ }
81
+
82
+ // Update isAllChecked based on new state
83
+ const areAllVisible = columns.every(column =>
84
+ field === column.field ? newState[field] : prev[column.field]
85
+ )
86
+ setIsAllChecked(areAllVisible)
87
+
88
+ console.log('toggleColumnState:', {
89
+ field,
90
+ before: prev[field],
91
+ after: newState[field],
92
+ allState: newState,
93
+ areAllVisible,
94
+ })
95
+ return newState
96
+ })
97
+ },
98
+ [columns]
99
+ )
100
+
101
+ const onSaveColumnView = useCallback(() => {
102
+ console.log('Saving column visibility state:', tempVisibleColumns)
103
+ updateVisibility({ type: 'save', newState: tempVisibleColumns })
104
+ handleClose()
105
+ }, [tempVisibleColumns, updateVisibility, handleClose])
106
+
107
+ const formatColumnName = useCallback((fieldName: string): string => {
108
+ const formatted = fieldName
109
+ .replace(/([A-Z])/g, ' $1')
110
+ .replace(/^./, str => str.toUpperCase())
111
+ .trim()
112
+ console.log('Formatting column name:', { fieldName, formatted })
113
+ return formatted
114
+ }, [])
115
+
116
+ const handlePageUnload = useCallback(() => {
117
+ console.log('Page unload - closing manage columns')
118
+ handleClose()
119
+ }, [handleClose])
120
+
121
+ useEffect(() => {
122
+ window.addEventListener('beforeunload', handlePageUnload)
123
+ return () => {
124
+ window.removeEventListener('beforeunload', handlePageUnload)
125
+ }
126
+ }, [handlePageUnload])
127
+
128
+ return {
129
+ handleAllCols,
130
+ toggleColumnState,
131
+ visibleColumns: tempVisibleColumns,
132
+ onSaveColumnView,
133
+ formatColumnName,
134
+ searchInput,
135
+ setSearchInput,
136
+ isAllChecked,
137
+ }
138
+ }
@@ -0,0 +1,122 @@
1
+ import React, { useCallback, useMemo, useState, useEffect } from 'react'
2
+ import type { ColumnDef, RowData } from '../Table'
3
+
4
+ interface UseSearchbarProps {
5
+ columns: ColumnDef[]
6
+ rows: RowData[]
7
+ searchValue: string
8
+ setSearchValue: (value: string) => void
9
+ }
10
+
11
+ export const useSearchbar = ({
12
+ columns,
13
+ rows,
14
+ searchValue,
15
+ setSearchValue,
16
+ }: UseSearchbarProps) => {
17
+ const [tags, setTags] = useState<string[]>([])
18
+
19
+ useEffect(() => {
20
+ console.log('Search value changed:', searchValue)
21
+ setTags(searchValue.trim() ? searchValue.toLowerCase().split(' ') : [])
22
+ }, [searchValue])
23
+
24
+ const handleSearchChange = useCallback(
25
+ (event: React.ChangeEvent<HTMLInputElement>) => {
26
+ const newValue = event.target.value
27
+ console.log('Search input changed:', {
28
+ oldValue: searchValue,
29
+ newValue,
30
+ })
31
+ setSearchValue(newValue)
32
+ },
33
+ [searchValue, setSearchValue]
34
+ )
35
+
36
+ const filteredRows = useMemo(() => {
37
+ if (!searchValue.trim()) {
38
+ return rows
39
+ }
40
+
41
+ const searchTerms = searchValue.toLowerCase().trim().split(' ')
42
+
43
+ return rows.filter(row => {
44
+ return searchTerms.some(term => {
45
+ return columns.some(column => {
46
+ // Check column header name
47
+ const headerMatch = column.headerName?.toLowerCase().includes(term)
48
+
49
+ // Check column field name
50
+ const fieldMatch = column.field.toLowerCase().includes(term)
51
+
52
+ // Check cell value
53
+ const cellValue = row[column.field]
54
+ const valueMatch =
55
+ cellValue != null && String(cellValue).toLowerCase().includes(term)
56
+
57
+ console.log('Searching:', {
58
+ field: column.field,
59
+ header: column.headerName,
60
+ value: cellValue,
61
+ searchTerm: term,
62
+ headerMatch,
63
+ fieldMatch,
64
+ valueMatch,
65
+ })
66
+
67
+ return headerMatch || fieldMatch || valueMatch
68
+ })
69
+ })
70
+ })
71
+ }, [rows, searchValue, columns])
72
+
73
+ const visibleColumns = useMemo(() => {
74
+ if (!searchValue.trim()) {
75
+ return new Set(columns.map(col => col.field))
76
+ }
77
+
78
+ const searchTerms = searchValue.toLowerCase().trim().split(' ')
79
+
80
+ return new Set(
81
+ columns
82
+ .filter(col => {
83
+ return searchTerms.some(term => {
84
+ // Check column header name
85
+ const headerMatch = col.headerName?.toLowerCase().includes(term)
86
+
87
+ // Check column field name
88
+ const fieldMatch = col.field.toLowerCase().includes(term)
89
+
90
+ // Check if any row has matching data in this column
91
+ const hasMatchingData = filteredRows.some(row => {
92
+ const cellValue = row[col.field]
93
+ return (
94
+ cellValue != null &&
95
+ String(cellValue).toLowerCase().includes(term)
96
+ )
97
+ })
98
+
99
+ console.log('Column visibility check:', {
100
+ field: col.field,
101
+ header: col.headerName,
102
+ searchTerm: term,
103
+ headerMatch,
104
+ fieldMatch,
105
+ hasMatchingData,
106
+ })
107
+
108
+ return headerMatch || fieldMatch || hasMatchingData
109
+ })
110
+ })
111
+ .map(col => col.field)
112
+ )
113
+ }, [columns, searchValue, filteredRows])
114
+
115
+ return {
116
+ searchValue,
117
+ handleSearchChange,
118
+ filteredRows,
119
+ visibleColumns,
120
+ tags,
121
+ }
122
+ }
@@ -0,0 +1,63 @@
1
+ 'use client'
2
+ import React from 'react'
3
+ import { Box } from '@mui/material'
4
+ import type { DatagridProps } from '../../DataGrid'
5
+ import DataGrid from '../../DataGrid'
6
+
7
+ export interface FormDataGridProps {
8
+ title: string
9
+ description: string
10
+ datagrid: DatagridProps
11
+ }
12
+
13
+ function FormDataGrid({ title, description, datagrid }: FormDataGridProps) {
14
+ return (
15
+ <Box
16
+ sx={{
17
+ width: '100%',
18
+ height: 'auto',
19
+ overflow: 'hidden',
20
+ '& *': {
21
+ overflow: 'hidden !important',
22
+ },
23
+ }}
24
+ >
25
+ <Box
26
+ sx={{
27
+ marginTop: 1,
28
+ marginBottom: 1,
29
+ width: '100%',
30
+ }}
31
+ >
32
+ <Box
33
+ sx={{
34
+ marginBottom: 0.5,
35
+ width: '100%',
36
+ textAlign: 'left',
37
+ fontFamily: 'Merriweather',
38
+ fontSize: '1.5rem',
39
+ fontWeight: 400,
40
+ color: 'black',
41
+ }}
42
+ >
43
+ {title}
44
+ </Box>
45
+ <Box
46
+ sx={{
47
+ width: '100%',
48
+ textAlign: 'left',
49
+ fontFamily: 'Merriweather',
50
+ fontSize: '1.25rem',
51
+ fontWeight: 400,
52
+ color: 'black',
53
+ }}
54
+ >
55
+ {description}
56
+ </Box>
57
+ </Box>
58
+ <DataGrid {...datagrid} />
59
+ </Box>
60
+ )
61
+ }
62
+
63
+ export default FormDataGrid