goobs-frontend 0.8.7 → 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,211 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { Box, Popover, IconButton } from '@mui/material'
5
+ import { useManageColumn } from '../utils/useManageColumn'
6
+ import CustomButton from '../../Button'
7
+ import Searchbar from '../../Searchbar'
8
+ import Checkbox from '../Checkbox'
9
+ import ShowHideEyeIcon from '../../Icons/ShowHideEye'
10
+ import { ColumnDef } from '../Table'
11
+ import Typography from '../../Typography'
12
+ import * as palette from '../../../styles/palette'
13
+
14
+ interface ManageColumnProps {
15
+ open?: boolean
16
+ handleClose?: () => void
17
+ columns: ColumnDef[]
18
+ }
19
+
20
+ function ManageColumns({
21
+ open = false,
22
+ handleClose = () => {},
23
+ columns,
24
+ }: ManageColumnProps) {
25
+ console.log('ManageColumns render:', { open, columns })
26
+
27
+ const {
28
+ handleAllCols,
29
+ toggleColumnState,
30
+ visibleColumns,
31
+ onSaveColumnView,
32
+ formatColumnName,
33
+ searchInput,
34
+ setSearchInput,
35
+ isAllChecked,
36
+ } = useManageColumn({
37
+ columns,
38
+ handleClose,
39
+ isPopupOpen: open,
40
+ })
41
+
42
+ const someColumnsVisible = React.useMemo(() => {
43
+ return (
44
+ columns.some(column => visibleColumns[column.field] === true) &&
45
+ !columns.every(column => visibleColumns[column.field] === true)
46
+ )
47
+ }, [columns, visibleColumns])
48
+
49
+ const handleEyeClick = (columnField: string) => {
50
+ console.log('Eye icon clicked:', {
51
+ field: columnField,
52
+ currentVisibility: visibleColumns[columnField],
53
+ allVisibility: visibleColumns,
54
+ })
55
+ toggleColumnState(columnField)
56
+ }
57
+
58
+ const handleCloseAndUpdate = () => {
59
+ console.log('handleCloseAndUpdate called')
60
+ handleClose?.()
61
+ setSearchInput('')
62
+ }
63
+
64
+ const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
65
+ console.log('Search value changed:', e.target.value)
66
+ setSearchInput(e.target.value)
67
+ }
68
+
69
+ const filteredColumns = columns.filter(column => {
70
+ const matches = formatColumnName(column.field)
71
+ .toLowerCase()
72
+ .includes(searchInput.toLowerCase())
73
+ console.log('Filtering column:', {
74
+ field: column.field,
75
+ searchInput,
76
+ matches,
77
+ })
78
+ return matches
79
+ })
80
+
81
+ console.log('Rendering ManageColumns with:', {
82
+ filteredColumns,
83
+ searchInput,
84
+ visibleColumns,
85
+ })
86
+
87
+ const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
88
+ console.log('Checkbox clicked in ManageColumns:', {
89
+ checked: event.target.checked,
90
+ indeterminate: event.target.indeterminate,
91
+ eventTarget: event.target,
92
+ })
93
+ event.stopPropagation()
94
+ handleAllCols(event.target.checked)
95
+ }
96
+
97
+ return (
98
+ <Popover
99
+ id="manage-columns-popover"
100
+ open={Boolean(open)}
101
+ onClose={handleCloseAndUpdate}
102
+ anchorOrigin={{
103
+ vertical: 'center',
104
+ horizontal: 'center',
105
+ }}
106
+ transformOrigin={{
107
+ vertical: 'center',
108
+ horizontal: 'center',
109
+ }}
110
+ sx={{
111
+ '& .MuiPaper-root': {
112
+ border: `1px solid ${palette.black.main}`,
113
+ borderRadius: 2,
114
+ minWidth: '250px',
115
+ boxShadow: 24,
116
+ },
117
+ }}
118
+ >
119
+ <Box
120
+ sx={{
121
+ p: 2,
122
+ bgcolor: palette.white.main,
123
+ display: 'flex',
124
+ flexDirection: 'column',
125
+ }}
126
+ >
127
+ <Typography
128
+ text="Manage Columns"
129
+ fontvariant="merriparagraph"
130
+ fontcolor={palette.black.main}
131
+ align="center"
132
+ sx={{ mb: 0 }}
133
+ />
134
+ <Box sx={{ mt: 1, mb: 0 }}>
135
+ <Searchbar
136
+ value={searchInput}
137
+ onChange={handleSearchChange}
138
+ placeholder="Search Columns"
139
+ iconcolor={palette.black.main}
140
+ outlinecolor={palette.black.main}
141
+ />
142
+ </Box>
143
+ <Box
144
+ sx={{
145
+ display: 'flex',
146
+ alignItems: 'center',
147
+ mt: 0,
148
+ mb: 0,
149
+ justifyContent: 'space-between',
150
+ }}
151
+ >
152
+ <Typography
153
+ text="All Columns"
154
+ fontvariant="merriparagraph"
155
+ fontcolor={palette.black.main}
156
+ sx={{ fontWeight: 'bold' }}
157
+ />
158
+ <Box sx={{ marginRight: '-4px' }}>
159
+ <Checkbox
160
+ checked={isAllChecked}
161
+ indeterminate={someColumnsVisible && !isAllChecked}
162
+ onChange={handleCheckboxChange}
163
+ />
164
+ </Box>
165
+ </Box>
166
+ <Box
167
+ sx={{ maxHeight: '160px', overflowY: 'auto', marginBottom: '10px' }}
168
+ >
169
+ {filteredColumns.map((column, index) => {
170
+ const isVisible = visibleColumns[column.field] === true
171
+ console.log('Rendering column row:', {
172
+ field: column.field,
173
+ visible: isVisible,
174
+ })
175
+ return (
176
+ <Box
177
+ key={index}
178
+ sx={{ display: 'flex', alignItems: 'center', mb: 1 }}
179
+ >
180
+ <Typography
181
+ text={formatColumnName(column.field)}
182
+ fontvariant="merriparagraph"
183
+ fontcolor={palette.black.main}
184
+ sx={{ flexGrow: 1, mr: 1 }}
185
+ />
186
+ <IconButton
187
+ onClick={() => handleEyeClick(column.field)}
188
+ size="small"
189
+ >
190
+ <ShowHideEyeIcon visible={isVisible} />
191
+ </IconButton>
192
+ </Box>
193
+ )
194
+ })}
195
+ </Box>
196
+ <CustomButton
197
+ text="Save"
198
+ backgroundcolor={palette.black.main}
199
+ variant="contained"
200
+ fontcolor={palette.white.main}
201
+ fontvariant="merriparagraph"
202
+ sx={{ mt: 0 }}
203
+ fullWidth
204
+ onClick={onSaveColumnView}
205
+ />
206
+ </Box>
207
+ </Popover>
208
+ )
209
+ }
210
+
211
+ export default ManageColumns
@@ -0,0 +1,182 @@
1
+ 'use client'
2
+ import React, { useState, useRef, useEffect } from 'react'
3
+ import { Popover, Stack, Paper, Box, IconButton } from '@mui/material'
4
+ import Typography from '../../Typography'
5
+ import DuplicateIcon from '@mui/icons-material/FileCopy'
6
+ import DeleteIcon from '@mui/icons-material/Delete'
7
+ import ExportIcon from '@mui/icons-material/Download'
8
+
9
+ type ModalType = 'duplicate' | 'delete' | 'export'
10
+
11
+ interface ManageRowProps {
12
+ open?: boolean
13
+ handleClose?: () => void
14
+ selectedRows?: string[]
15
+ rows?: Array<{ [key: string]: unknown }>
16
+ onDuplicate?: () => void
17
+ onDelete?: () => void
18
+ }
19
+
20
+ function ManageRow({
21
+ open = false,
22
+ handleClose = () => {},
23
+ selectedRows = [],
24
+ rows = [],
25
+ onDuplicate,
26
+ onDelete,
27
+ }: ManageRowProps) {
28
+ const [actionType, setActionType] = useState<ModalType | null>(null)
29
+ const [position, setPosition] = useState({ x: 0, y: 0 })
30
+ const popoverRef = useRef<HTMLDivElement>(null)
31
+
32
+ const iconMap: { [key in ModalType]: React.ReactElement } = {
33
+ duplicate: <DuplicateIcon />,
34
+ delete: <DeleteIcon />,
35
+ export: <ExportIcon />,
36
+ }
37
+
38
+ const handleActionSelection = (type: ModalType) => {
39
+ setActionType(type)
40
+ switch (type) {
41
+ case 'duplicate':
42
+ onDuplicate?.()
43
+ break
44
+ case 'delete':
45
+ onDelete?.()
46
+ break
47
+ case 'export':
48
+ handleExport()
49
+ break
50
+ }
51
+ handleClose()
52
+ }
53
+
54
+ const handleExport = () => {
55
+ const selectedData = rows.filter(row =>
56
+ selectedRows.includes(row.id as string)
57
+ )
58
+ const csvContent = selectedData
59
+ .map(row => Object.values(row).join(','))
60
+ .join('\n')
61
+
62
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
63
+ const link = document.createElement('a')
64
+ if (link.download !== undefined) {
65
+ const url = URL.createObjectURL(blob)
66
+ link.setAttribute('href', url)
67
+ link.setAttribute('download', 'exported_data.csv')
68
+ link.style.visibility = 'hidden'
69
+ document.body.appendChild(link)
70
+ link.click()
71
+ document.body.removeChild(link)
72
+ }
73
+ }
74
+
75
+ const actionButtons = (
76
+ <Stack component="div" spacing={0} direction="row" justifyContent="center">
77
+ {(['duplicate', 'delete', 'export'] as ModalType[]).map(type => (
78
+ <Box
79
+ key={type}
80
+ display="flex"
81
+ flexDirection="column"
82
+ alignItems="center"
83
+ >
84
+ <IconButton
85
+ size="small"
86
+ onClick={() => handleActionSelection(type)}
87
+ sx={{ color: 'black' }}
88
+ >
89
+ {iconMap[type]}
90
+ </IconButton>
91
+ <Typography
92
+ fontvariant="merriparagraph"
93
+ text={type.charAt(0).toUpperCase() + type.slice(1)}
94
+ />
95
+ </Box>
96
+ ))}
97
+ </Stack>
98
+ )
99
+
100
+ useEffect(() => {
101
+ const popover = popoverRef.current
102
+ if (!popover) return
103
+
104
+ let isDragging = false
105
+ let startX: number, startY: number
106
+
107
+ const handleMouseDown = (e: MouseEvent) => {
108
+ isDragging = true
109
+ startX = e.clientX - position.x
110
+ startY = e.clientY - position.y
111
+ document.addEventListener('mousemove', handleMouseMove)
112
+ document.addEventListener('mouseup', handleMouseUp)
113
+ }
114
+
115
+ const handleMouseMove = (e: MouseEvent) => {
116
+ if (!isDragging) return
117
+ const newX = e.clientX - startX
118
+ const newY = e.clientY - startY
119
+ setPosition({ x: newX, y: newY })
120
+ }
121
+
122
+ const handleMouseUp = () => {
123
+ isDragging = false
124
+ document.removeEventListener('mousemove', handleMouseMove)
125
+ document.removeEventListener('mouseup', handleMouseUp)
126
+ }
127
+
128
+ popover.addEventListener('mousedown', handleMouseDown)
129
+
130
+ return () => {
131
+ popover.removeEventListener('mousedown', handleMouseDown)
132
+ document.removeEventListener('mousemove', handleMouseMove)
133
+ document.removeEventListener('mouseup', handleMouseUp)
134
+ }
135
+ }, [position])
136
+
137
+ return (
138
+ <Popover
139
+ open={open}
140
+ onClose={handleClose}
141
+ anchorReference="anchorPosition"
142
+ anchorPosition={{ top: position.y, left: position.x }}
143
+ transformOrigin={{
144
+ vertical: 'top',
145
+ horizontal: 'left',
146
+ }}
147
+ slotProps={{
148
+ paper: {
149
+ ref: popoverRef,
150
+ component: Paper,
151
+ style: {
152
+ zIndex: 1300,
153
+ overflow: 'hidden',
154
+ display: 'flex',
155
+ alignItems: 'center',
156
+ justifyContent: 'space-between',
157
+ height: '60px',
158
+ minWidth: '560px',
159
+ padding: '0 10px',
160
+ cursor: 'move',
161
+ },
162
+ },
163
+ }}
164
+ >
165
+ <Box
166
+ flexGrow={1}
167
+ display="flex"
168
+ alignItems="center"
169
+ paddingLeft="16px"
170
+ paddingRight="16px"
171
+ >
172
+ <Typography
173
+ fontvariant="merriparagraph"
174
+ text={`${selectedRows.length} items selected`}
175
+ />
176
+ </Box>
177
+ {!actionType && actionButtons}
178
+ </Popover>
179
+ )
180
+ }
181
+
182
+ export default ManageRow
@@ -0,0 +1,227 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { CircularProgress } from '@mui/material'
5
+ import ContentSection from '../../Content'
6
+ import type { ContentSectionProps } from '../../Content'
7
+ import type { BorderProp } from '../../Grid'
8
+ import { useAtomValue } from 'jotai'
9
+ import { columnVisibilityAtom } from '../Jotai/atom'
10
+
11
+ export interface ColumnDef {
12
+ field: string
13
+ headerName: string
14
+ width?: number
15
+ flex?: number
16
+ renderCell?: (params: CellParams) => React.ReactNode
17
+ renderHeader?: (params: HeaderParams) => React.ReactNode
18
+ computedWidth?: number
19
+ headerText?: string
20
+ index?: number
21
+ }
22
+
23
+ export interface CellParams<T = unknown> {
24
+ row: RowData
25
+ value: T
26
+ field: string
27
+ rowIndex: number
28
+ columnIndex: number
29
+ }
30
+
31
+ export interface HeaderParams {
32
+ column: ColumnDef
33
+ columnIndex: number
34
+ }
35
+
36
+ export interface RowData {
37
+ id: string
38
+ [key: string]: unknown
39
+ }
40
+
41
+ export interface TableProps {
42
+ columns: ColumnDef[]
43
+ rows: RowData[]
44
+ loading?: boolean
45
+ page: number
46
+ pageSize: number
47
+ rowCount: number
48
+ onPageChange: (newPage: number) => void
49
+ onPageSizeChange: (newPageSize: number) => void
50
+ rowHeight?: number
51
+ headerHeight?: number
52
+ onRowClick?: (row: RowData) => void
53
+ selectedRows?: string[]
54
+ checkboxSelection?: boolean
55
+ onSelectionChange?: (selectedIds: string[]) => void
56
+ getCellText?: (params: CellParams) => string
57
+ getHeaderText?: (params: HeaderParams) => string
58
+ }
59
+
60
+ interface TableRef {
61
+ getAllColumns: () => ColumnDef[]
62
+ getSelectedRows: () => Map<string, RowData>
63
+ forceUpdate: () => void
64
+ setPage: (page: number) => void
65
+ setPageSize: (pageSize: number) => void
66
+ getRowIndex: (id: string) => number
67
+ isRowSelected: (id: string) => boolean
68
+ }
69
+
70
+ const DEFAULT_ROW_HEIGHT = 40
71
+
72
+ const Table = React.forwardRef<TableRef, TableProps>(
73
+ (
74
+ {
75
+ columns,
76
+ rows,
77
+ loading = false,
78
+ rowHeight = DEFAULT_ROW_HEIGHT,
79
+ onRowClick,
80
+ selectedRows = [],
81
+ },
82
+ ref
83
+ ) => {
84
+ const columnVisibility = useAtomValue(columnVisibilityAtom)
85
+
86
+ console.log('Table render:', {
87
+ columns,
88
+ rows,
89
+ columnVisibility,
90
+ selectedRows,
91
+ })
92
+
93
+ // Filter visible columns
94
+ const visibleColumns = columns.filter(column => {
95
+ const isVisible = columnVisibility[column.field] !== false
96
+ console.log(`Column ${column.field} visibility:`, isVisible)
97
+ return isVisible
98
+ })
99
+
100
+ React.useImperativeHandle(ref, () => ({
101
+ getAllColumns: () => visibleColumns,
102
+ getSelectedRows: () =>
103
+ new Map(
104
+ rows
105
+ .filter(row => selectedRows.includes(row.id))
106
+ .map(row => [row.id, row])
107
+ ),
108
+ forceUpdate: () => {},
109
+ setPage: () => {},
110
+ setPageSize: () => {},
111
+ getRowIndex: (id: string) => rows.findIndex(row => row.id === id),
112
+ isRowSelected: (id: string) => selectedRows.includes(id),
113
+ }))
114
+
115
+ if (loading) {
116
+ return (
117
+ <ContentSection
118
+ grids={[
119
+ {
120
+ grid: {
121
+ gridconfig: {
122
+ gridwidth: '100%',
123
+ gridname: 'loading-container',
124
+ alignment: 'left',
125
+ },
126
+ },
127
+ typography: {
128
+ columnconfig: {
129
+ row: 1,
130
+ column: 1,
131
+ },
132
+ component: CircularProgress as React.ElementType,
133
+ },
134
+ },
135
+ ]}
136
+ />
137
+ )
138
+ }
139
+
140
+ if (rows.length === 0) {
141
+ return (
142
+ <ContentSection
143
+ grids={[
144
+ {
145
+ grid: {
146
+ gridconfig: {
147
+ gridwidth: '100%',
148
+ gridname: 'empty-table',
149
+ alignment: 'left',
150
+ },
151
+ },
152
+ typography: {
153
+ columnconfig: {
154
+ row: 1,
155
+ column: 1,
156
+ },
157
+ text: 'No data available',
158
+ },
159
+ },
160
+ ]}
161
+ />
162
+ )
163
+ }
164
+
165
+ console.log('Table config - visibleColumns:', visibleColumns)
166
+
167
+ const tableConfig: ContentSectionProps = {
168
+ grids: [
169
+ {
170
+ grid: {
171
+ gridconfig: {
172
+ gridwidth: '100%',
173
+ gridname: 'data-table',
174
+ alignment: 'left',
175
+ },
176
+ },
177
+ typography: [
178
+ // Header row
179
+ ...visibleColumns.map((column, columnIndex) => ({
180
+ columnconfig: {
181
+ row: 1,
182
+ column: columnIndex + 1,
183
+ },
184
+ text: column.headerName,
185
+ cellconfig: {
186
+ border: 'solid' as BorderProp,
187
+ minHeight: `${rowHeight}px`,
188
+ width: column.width ? `${column.width}px` : '200px',
189
+ mobilewidth: '100%',
190
+ tabletwidth: '100%',
191
+ computerwidth: '100%',
192
+ },
193
+ })),
194
+ // Data rows
195
+ ...rows.flatMap((row, rowIndex) =>
196
+ visibleColumns.map((column, columnIndex) => ({
197
+ columnconfig: {
198
+ row: rowIndex + 2,
199
+ column: columnIndex + 1,
200
+ },
201
+ text: String(row[column.field] || ''),
202
+ cellconfig: {
203
+ border: 'solid' as BorderProp,
204
+ minHeight: `${rowHeight}px`,
205
+ width: column.width ? `${column.width}px` : '200px',
206
+ mobilewidth: '100%',
207
+ tabletwidth: '100%',
208
+ computerwidth: '100%',
209
+ onClick: onRowClick ? () => onRowClick(row) : undefined,
210
+ backgroundColor: selectedRows.includes(row.id)
211
+ ? 'rgba(0, 0, 0, 0.04)'
212
+ : undefined,
213
+ },
214
+ }))
215
+ ),
216
+ ],
217
+ },
218
+ ],
219
+ }
220
+
221
+ return <ContentSection {...tableConfig} />
222
+ }
223
+ )
224
+
225
+ Table.displayName = 'Table'
226
+
227
+ export default Table
@@ -0,0 +1,6 @@
1
+ import { Box, styled } from '@mui/material'
2
+
3
+ export const VerticalDivider = styled(Box)({
4
+ borderLeft: '2px solid black',
5
+ height: '20px',
6
+ })