admin管理员组

文章数量:1123036

enter image description here I am self-taught so any advices / improvements will be highly appreciated. I use Data Grid from MUI and react-focus-on library. I also use zustand store. I have csv table component where I have data table and when data table is in focus and the data inside is being changed those data are updated in const rows. When user clicks outside of csv table component I need those data to be stored/updated in the zustand store. When user is editing data in the table I don't want to update data in the store all the time because it would cause rerender all the time (if you see any improvement here any advices would be appreciated).

The problem is when used is finished with editing a cell in the data table and clicks outside from csv table component onClickOutside is triggered where I handle what is supposed to happen but it's triggered earlier then processRowUpdate or onCellEditStop in data grid what is a problem because when user is done with editing a cell I need to update the data and that wont happen when processRowUpdate or onCellEditStop is not triggered. I tried to solve it with flags but in the code what I have now I have problem that when I update rows in handleRowEdit and then persistChanges is called there I have old rows data. But it's a bad workaround what I don't want to use if there is better solution for onFocus.

Any ideas how can tell on focus to wait for datagrid to be finished and then trigger onClickOutside? Thank you.

const CsvTable = () => {
    const {
        models,
        selectedFileKey,
        editModeAllowed,
        loadingEditorArea,
        selectedCustomer,
        isDarkMode,
        selectedCsvTableRow
    } = usePersistStore(useShallow((state) => ({
        models: state.models,
        selectedFileKey: state.selectedFileKey,
        editModeAllowed: state.menuAccess,
        loadingEditorArea: state.loadingEditorArea,
        selectedCustomer: state.selectedCustomer,
        isDarkMode: state.isDarkMode,
        selectedCsvTableRow: state.selectedCsvTableRow
    })))
    const theme = useTheme()
    const [rows, setRows] = useState(getDataGridRows(models[selectedCustomer][selectedFileKey].csvTableData))
    const [modifiedRows, setModifiedRows] = useState<Row[]>([])
    const ref = useRef<any>(null)
    const readOnlyTable = selectedFileKey.includes('libs') || selectedFileKey.includes('tmp') || !editModeAllowed

    const dataColumns = useMemo(() => [
            ...models[selectedCustomer][selectedFileKey].csvTableColumns.map((column: string) => ({
                field: column,
                headerName: column,
                editable: !readOnlyTable,
            })),
        ], [selectedCustomer, selectedFileKey, editModeAllowed]
    )

    interface Row {
        [key: string]: any
    }

    const [saveChanges, setSaveChanges] = useState(false)
    let cellEditStopped = false
    let handleRowEditFinished = false

    const handleRowEdit = (updatedRow: Partial<Row>) => {
        markFileAsEditedIfNecessary()
        const updatedRows = updateRows(updatedRow)
        setRows(updatedRows)
        modifiedRows.push(updatedRow)
        if (!handleRowEditFinished) {
            handleRowEditFinished = true
            onCellEditStop()
        }
        return updatedRow
    }

    const onCellEditStop = (row?: any) => {
        console.log(row)
        if (!handleRowEditFinished) {
            return
        }
        if (!cellEditStopped) {
            cellEditStopped = true
            handleOutsideClick()
        }
    }

    const handleOutsideClick = () => {
        if (!cellEditStopped) {
            return
        }
        persistChanges(rows) // Save changes when clicked outside
    }

    const markFileAsEditedIfNecessary = () => {
        const fileData = models[selectedCustomer][selectedFileKey]
        if (!fileData.fileEdited) {
            fileSystemService.markFileAsEdited(selectedCustomer, selectedFileKey)
        }
    }

    const updateRows = (updatedRow: Partial<Row>) => {
        return rows.map(row =>
            row.id === updatedRow.id ? {...row, ...updatedRow} : row
        )
    }

    console.log('saveChanges: ', saveChanges)

    const persistChanges = (updatedRows: Partial<Row[]>) => {
        console.log(modifiedRows)
        const store = usePersistStore.getState()
        const modelsCopy = {...store.models[selectedCustomer]}
        const updatedCsvContent = fileSystemService.updateCsv(
            modelsCopy[selectedFileKey].content,
            modifiedRows
        )
        modelsCopy[selectedFileKey] = {
            ...modelsCopy[selectedFileKey],
            csvTableData: updatedRows,
            content: updatedCsvContent,
        }

        store.setModels(selectedCustomer, modelsCopy)
        setSaveChanges(false)
        cellEditStopped = false
        console.log('save')
    }

    const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
    const [menuOpen, setMenuOpen] = React.useState(false)
    const [menuPosition, setMenuPosition] = useState<any>(null)

    const handleRowContextMenu = (event: React.MouseEvent<HTMLElement>) => {
        event.preventDefault()
        //console.log(event.currentTarget.dataset.id)
        if (event.currentTarget.dataset.id) {
            const id: GridRowSelectionModel = [event.currentTarget.dataset.id]
            usePersistStore.getState().setSelectedCsvTableRow(id)
        }
        setMenuPosition({
            mouseX: event.clientX,
            mouseY: event.clientY,
        })
        setAnchorEl(event.currentTarget)
        setMenuOpen(true)
    }

    const addRow = () => {
        setMenuOpen(false)

        const rdeStore = usePersistStore.getState()
        const models = rdeStore.models
        const csvTableData = models[selectedCustomer][selectedFileKey].csvTableData

        const newRowId = Number(selectedCsvTableRow[0])
        const newCsvTableData = insertEmptyRow(csvTableData, newRowId)
        setRows(getDataGridRows(newCsvTableData))
        usePersistStore.getState().setCsvTableData(selectedCustomer, selectedFileKey, newCsvTableData)
    }

    
    function isRowEmpty<T extends Record<string, any>>(row: T): boolean {
        return Object.entries(row)
            .filter(([key]) => key !== "id")
            .every(([, value]) => value === "")
    }
    
    function deleteRowByIndex<T extends Record<string, any>>(data: T[], rowIdToDelete: number, fileContent: string): {
        updatedData: T[];
        updatedFileContent: string
    } {
        const rowIndex = data.findIndex(row => row.id === rowIdToDelete);
        if (rowIndex === -1) {
            throw new Error(`Row with ID ${rowIdToDelete} not found.`)
        }
        const rowToDelete = data[rowIndex];
        if (isRowEmpty(rowToDelete)) {
            const updatedData = data.filter(row => row.id !== rowIdToDelete)
            const reindexedData = updatedData.map((row, index) => ({...row, id: index}))
            return {updatedData: reindexedData, updatedFileContent: fileContent}
        }
        markFileAsEditedIfNecessary()
        const lines = fileContent.split("\n")
        lines.splice(rowIndex + 1, 1)
        const updatedFileContent = lines.join("\n")
        const updatedData = data.filter(row => row.id !== rowIdToDelete)
        const reindexedData = updatedData.map((row, index) => ({...row, id: index}))
        return {updatedData: reindexedData, updatedFileContent}
    }


    const removeRow = () => {
        setMenuOpen(false)
        const rdeStore = usePersistStore.getState()
        const models = rdeStore.models
        const csvTableData = models[selectedCustomer][selectedFileKey].csvTableData
        const fileContent = models[selectedCustomer][selectedFileKey].content
        const rowIdToDelete = Number(selectedCsvTableRow[0])
        const {updatedData, updatedFileContent} = deleteRowByIndex(csvTableData, rowIdToDelete, fileContent)
        setRows(getDataGridRows(updatedData))
        usePersistStore.getState().setCsvTableData(selectedCustomer, selectedFileKey, updatedData)
        usePersistStore.getState().setContent(selectedCustomer, selectedFileKey, updatedFileContent)
    }

    function insertEmptyRow<T extends { id: number }>(data: T[], position: number): T[] {
        const emptyRow: T = Object.keys(data[0]).reduce((acc, key) => {
            acc[key as keyof T] = key === 'id' ? position + 1 : '' as any
            return acc
        }, {} as T)
        const newData: T[] = []
        for (let i = 0; i < data.length; i++) {
            const item = data[i]
            newData.push(item)

            if (i === position) {
                newData.push(emptyRow)
            }
        }

        return newData.map((item, index) => ({
            ...item,
            id: index
        }))
    }

    const handleClose = () => {
        setMenuPosition(null)
        setMenuOpen(false)
    }

    function CustomUserItem(props: GridColumnMenuItemProps) {
        const {myCustomHandler, myCustomValue} = props;
        return (
            <MenuItem onClick={myCustomHandler}>
                <ListItemIcon>
                    <SettingsApplicationsIcon fontSize="small"/>
                </ListItemIcon>
                <ListItemText>{myCustomValue}</ListItemText>
            </MenuItem>
        );
    }

    function CustomColumnMenu(props: GridColumnMenuProps) {
        return (
            <GridColumnMenu
                {...props}
                slots={{
                    // Add new item
                    columnMenuUserItem: CustomUserItem,
                }}
                slotProps={{
                    columnMenuUserItem: {
                        // set `displayOrder` for new item
                        displayOrder: 15,
                        // pass additional props
                        myCustomValue: 'Do custom action',
                        myCustomHandler: () => alert('Custom handler fired'),
                    },
                }}
            />
        );
    }

    return <>
        {loadingEditorArea ? (
            <div className="tree-directory">
                <ProgressSpinner className="progress-spinner flex justify-content-center" strokeWidth="4"
                                 animationDuration=".5s"/>
            </div>) : (
            <FocusOn
                enabled={saveChanges}
                onClickOutside={handleOutsideClick}
                onEscapeKey={() => setSaveChanges(false)}
            >
                <Box
                    sx={{
                        display: 'flex',
                        flex: 1,
                        position: 'absolute',
                        height: '-webkit-fill-available',
                        width: '-webkit-fill-available',
                        marginRight: '5px'
                    }}
                    ref={ref}
                   >
                    <DataGrid
                        onColumnHeaderContextMenu={() => console.log('something')}
                        onColumnHeaderClick={() => console.log('header click')}
                        autosizeOptions={{
                            includeOutliers: true,
                            includeHeaders: true,
                        }}
                        rowSelectionModel={selectedCsvTableRow}
                        rows={rows}
                        columns={dataColumns}
                        onRowSelectionModelChange={(selectedRow) => usePersistStore.getState().setSelectedCsvTableRow(selectedRow)}
                        getRowId={(row) => row.id}
                        processRowUpdate={(updatedRow) => handleRowEdit(updatedRow)}
                        onCellEditStart={() => {
                            setSaveChanges(true)
                        }}
                        onCellEditStop={(rowNode) => {
                            console.log(rowNode)
                            onCellEditStop(rowNode.row)
                        }}
                        initialState={{
                            pagination: {
                                paginationModel: {
                                    pageSize: 25,
                                },
                            },
                        }}
                        slotProps={{
                            toolbar: {
                                showQuickFilter: true,
                            },
                            row: {
                                onContextMenu: handleRowContextMenu,
                                style: {cursor: 'context-menu'},
                            },
                        }}
                        slots={readOnlyTable ? undefined : { columnMenu: CustomColumnMenu }}
                        density="compact"
                        pageSizeOptions={[10, 25, 35, 50]}
                        showCellVerticalBorder
                        showColumnVerticalBorder
                        //disableDensitySelector
                        disableColumnSelector={false}
                        autosizeOnMount
                        sx={{
                            border: theme.palette.mode === 'dark' ?
                                '1px solid rgba(255, 255, 255, 0.12)'
                                : '1px solid rgba(0, 0, 0, 0.12)',
                            '& .MuiDataGrid-columnHeaders': {
                                backgroundColor: theme.palette.mode === 'light'
                                    ? '#f0f0f0'
                                    : '#262626',
                            },
                            '& .MuiDataGrid-virtualScroller': {
                                backgroundColor: theme.palette.mode === 'light'
                                    ? '#f9f9f9'
                                    : '#393a39',
                            },
                            '& .MuiDataGrid-withBorderColor': {
                                backgroundColor: theme.palette.mode === 'light'
                                    ? '#f0f0f0'
                                    : '#262626',
                            },
                        }}
                    />
                    {!readOnlyTable ?
                        <Menu
                            open={menuOpen}
                            onClose={handleClose}
                            anchorEl={anchorEl}
                            anchorReference="anchorPosition"
                            anchorPosition={
                                menuPosition ? {top: menuPosition.mouseY, left: menuPosition.mouseX} : undefined
                            }
                            slotProps={{
                                paper: {
                                    elevation: 0,
                                    sx: {
                                        backgroundColor: isDarkMode ? '#f5f5f5' : '#393a39',
                                        color: isDarkMode ? 'black' : 'white',
                                        overflow: 'visible',
                                        filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
                                        mt: 1.5,
                                        '&::before': {
                                            content: '""',
                                            display: 'block',
                                            position: 'absolute',
                                            top: 0,
                                            right: 14,
                                            width: 10,
                                            height: 10,
                                            bgcolor: isDarkMode ? '#f5f5f5' : '#393a39',
                                            transform: 'translateY(-50%) rotate(45deg)',
                                            zIndex: 0,
                                        },
                                    },
                                },
                            }}
                        >
                            <MenuItem onClick={addRow}>Add row</MenuItem>
                            <MenuItem onClick={removeRow}>Delete row</MenuItem>
                        </Menu>
                        : ''
                    }
                </Box>
            </FocusOn>
        )}
    </>
}

本文标签: reactjsMaterial UI data grid with react focusStack Overflow