diff --git a/docs/pages/api-docs/data-grid.md b/docs/pages/api-docs/data-grid.md index ef1221be3dbd..7be86e278c35 100644 --- a/docs/pages/api-docs/data-grid.md +++ b/docs/pages/api-docs/data-grid.md @@ -28,6 +28,7 @@ import { DataGrid } from '@material-ui/data-grid'; | disableExtendRowFullWidth | boolean | false | If `true`, rows will not be extended to fill the full width of the grid container. | | disableSelectionOnClick | boolean | false | If `true`, the selection on click on a row or cell is disabled. | | error | any | | An error that will turn the grid into its error state and display the error component. | +| editRowsModel | GridEditRowsModel | undefined | Set the edit rows model of the grid. | | getRowId | GridRowIdGetter | (row)=> row.id | A function that allows the grid to retrieve the row id. | | headerHeight | number | 56 | Set the height in pixel of the column headers in the grid. | | hideFooter | boolean | false | If `true`, the footer component is hidden. | @@ -35,6 +36,7 @@ import { DataGrid } from '@material-ui/data-grid'; | hideFooterRowCount | boolean | false | If `true`, the row count in the footer is hidden. | | hideFooterSelectedRowCount | boolean | false | If `true`, the selected row count in the footer is hidden. | | icons | IconsOptions | | Set of icons used in the grid. | +| isCellEditable | (params: GridCellParams) => boolean; | | Callback fired when a cell is rendered, returns true if the cell is editable. | | loading | boolean | false | If `true`, a loading overlay is displayed. | | localeText | GridLocaleText | | Set of text labels used in the grid. You can find all the translation keys supported in [the source](https://github.com/mui-org/material-ui-x/blob/HEAD/packages/grid/_modules_/grid/constants/localeTextConstants.ts) in the GitHub repository. | | logger | Logger | null | Pass a custom logger in the components that implements the 'Logger' interface. | @@ -42,8 +44,12 @@ import { DataGrid } from '@material-ui/data-grid'; | nonce | string | | Nonce of the inline styles for [Content Security Policy](https://www.w3.org/TR/2016/REC-CSP2-20161215/#script-src-the-nonce-attribute). | | onCellClick | (param: GridCellParams) => void | | Callback fired when a click event comes from a cell element. | | onCellHover | (param: GridCellParams) => void | | Callback fired when a hover event comes from a cell element. | +| onCellModeChange | (params: GridCellModeChangeParams) => void | | Callback fired when the cell mode changed. | | onColumnHeaderClick | (param: GridColParams) => void | | Callback fired when a click event comes from a column header element. | | onError | (args: any) => void | | Callback fired when an exception is thrown in the grid, or when the `showError` API method is called. | +| onEditCellChange | (params: GridEditCellParams) => void | | Callback fired when the edit cell value changed. | +| onEditCellChangeCommitted | (params: GridEditCellParams) => void | | Callback fired when the cell changes are committed. | +| onEditRowModelChange | (params: GridEditRowModelParams) => void | | Callback fired when the EditRowModel changed. | | onFilterModelChange | (params: GridFilterModelParams) => void | | Callback fired when the Filter model changes before the filters are applied. | | onPageChange | (param: GridPageChangeParams) => void | | Callback fired when the current page has changed. | | onPageSizeChange | (param: GridPageChangeParams) => void | | Callback fired when the page size has changed. | diff --git a/docs/pages/api-docs/x-grid.md b/docs/pages/api-docs/x-grid.md index 6b877ce6d723..a105537ee169 100644 --- a/docs/pages/api-docs/x-grid.md +++ b/docs/pages/api-docs/x-grid.md @@ -32,13 +32,14 @@ import { XGrid } from '@material-ui/x-grid'; | disableMultipleSelection | boolean | false | If `true`, multiple selection using the CTRL or CMD key is disabled. | | disableSelectionOnClick | boolean | false | If `true`, the selection on click on a row or cell is disabled. | | error | any | | An error that will turn the grid into its error state and display the error component. | -| getRowId | GridRowIdGetter | (row)=> row.id | A function that allows the grid to retrieve the row id. | +| editRowsModel | GridEditRowsModel | undefined | Set the edit rows model of the grid. || getRowId | GridRowIdGetter | (row)=> row.id | A function that allows the grid to retrieve the row id. | | headerHeight | number | 56 | Set the height in pixel of the column headers in the grid. | | hideFooter | boolean | false | If `true`, the footer component is hidden. | | hideFooterPagination | boolean | false | If `true`, the pagination component in the footer is hidden. | | hideFooterRowCount | boolean | false | If `true`, the row count in the footer is hidden. | | hideFooterSelectedRowCount | boolean | false | If `true`, the selected row count in the footer is hidden. | | icons | IconsOptions | | Set of icons used in the grid. | +| isCellEditable | (params: GridCellParams) => boolean; | | Callback fired when a cell is rendered, returns true if the cell is editable. | | loading | boolean | false | If `true`, a loading overlay is displayed.. | | localeText | GridLocaleText | | Set of text labels used in the grid. You can find all the translation keys supported in [the source](https://github.com/mui-org/material-ui-x/blob/HEAD/packages/grid/_modules_/grid/constants/localeTextConstants.ts) in the GitHub repository. | | logger | Logger | null | Pass a custom logger in the components that implements the 'Logger' interface. | @@ -46,8 +47,12 @@ import { XGrid } from '@material-ui/x-grid'; | nonce | string | | Nonce of the inline styles for [Content Security Policy](https://www.w3.org/TR/2016/REC-CSP2-20161215/#script-src-the-nonce-attribute). | | onCellClick | (param: GridCellParams) => void | | Callback fired when a click event comes from a cell element. | | onCellHover | (param: GridCellParams) => void | | Callback fired when a hover event comes from a cell element. | +| onCellModeChange | (params: GridCellModeChangeParams) => void | | Callback fired when the cell mode changed. | | onColumnHeaderClick | (param: GridColParams) => void | | Callback fired when a click event comes from a column header element. | | onError | (args: any) => void | | Callback fired when an exception is thrown in the grid, or when the `showError` API method is called. | +| onEditCellChange | (params: GridEditCellParams) => void | | Callback fired when the edit cell value changed. | +| onEditCellChangeCommitted | (params: GridEditCellParams) => void | | Callback fired when the cell changes are committed. | +| onEditRowModelChange | (params: GridEditRowModelParams) => void | | Callback fired when the EditRowModel changed. | | onFilterModelChange | (params: GridFilterModelParams) => void | | Callback fired when the Filter model changes before the filters are applied. | | onPageChange | (param: GridPageChangeParams) => void | | Callback fired when the current page has changed. | | onPageSizeChange | (param: GridPageChangeParams) => void | | Callback fired when the page size has changed. | diff --git a/packages/grid/_modules_/grid/GridComponent.tsx b/packages/grid/_modules_/grid/GridComponent.tsx index 2e76e5da0489..15489cdfe0f2 100644 --- a/packages/grid/_modules_/grid/GridComponent.tsx +++ b/packages/grid/_modules_/grid/GridComponent.tsx @@ -22,6 +22,7 @@ import { useGridState } from './hooks/features/core/useGridState'; import { useGridPagination } from './hooks/features/pagination/useGridPagination'; import { useGridPreferencesPanel } from './hooks/features/preferencesPanel/useGridPreferencesPanel'; import { useGridRows } from './hooks/features/rows/useGridRows'; +import { useGridEditRows } from './hooks/features/rows/useGridEditRows'; import { useGridSorting } from './hooks/features/sorting/useGridSorting'; import { useGridApiRef } from './hooks/features/useGridApiRef'; import { useGridColumnReorder } from './hooks/features/columnReorder'; @@ -75,6 +76,7 @@ export const GridComponent = React.forwardRef = React.memo((props) => { @@ -25,15 +26,16 @@ export const GridCell: React.FC = React.memo((props) => { children, colIndex, cssClass, - hasFocus, field, formattedValue, + hasFocus, + height, + isEditable, rowIndex, showRightBorder, tabIndex, value, width, - height, } = props; const valueToRender = formattedValue || value; @@ -50,11 +52,13 @@ export const GridCell: React.FC = React.memo((props) => { ref={cellRef} className={classnames(GRID_CELL_CSS_CLASS, cssClass, `MuiDataGrid-cell${capitalize(align)}`, { 'MuiDataGrid-withBorder': showRightBorder, + 'MuiDataGrid-cellEditable': isEditable, })} role="cell" data-value={value} data-field={field} data-rowindex={rowIndex} + data-editable={isEditable} aria-colindex={colIndex} style={{ minWidth: width, diff --git a/packages/grid/_modules_/grid/components/GridRowCells.tsx b/packages/grid/_modules_/grid/components/GridRowCells.tsx index 7c24c22cc368..d8af0d65cf69 100644 --- a/packages/grid/_modules_/grid/components/GridRowCells.tsx +++ b/packages/grid/_modules_/grid/components/GridRowCells.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { gridEditRowsStateSelector } from '../hooks/features/rows/gridEditRowsSelector'; import { GridCellClassParams, GridColumns, @@ -51,6 +52,7 @@ export const GridRowCells: React.FC = React.memo((props) => { } = props; const api = React.useContext(GridApiContext); const rowHeight = useGridSelector(api, gridDensityRowHeightSelector); + const editRowsState = useGridSelector(api, gridEditRowsStateSelector); const cellsProps = columns.slice(firstColIdx, lastColIdx + 1).map((column, colIdx) => { const isLastColumn = firstColIdx + colIdx === columns.length - 1; @@ -66,6 +68,7 @@ export const GridRowCells: React.FC = React.memo((props) => { rowModel: row, colDef: column, rowIndex, + colIndex: colIdx, value, api: api!.current!, }); @@ -84,11 +87,8 @@ export const GridRowCells: React.FC = React.memo((props) => { cssClassProp = { cssClass: `${cssClassProp.cssClass} ${cssClass}` }; } + const editCellState = editRowsState[row.id] && editRowsState[row.id][column.field]; let cellComponent: React.ReactElement | null = null; - if (column.renderCell) { - cellComponent = column.renderCell(cellParams); - cssClassProp = { cssClass: `${cssClassProp.cssClass} MuiDataGrid-cellWithRenderer` }; - } if (column.valueGetter) { // Value getter override the original value @@ -98,9 +98,21 @@ export const GridRowCells: React.FC = React.memo((props) => { let formattedValueProp = {}; if (column.valueFormatter) { + // TODO add formatted value to cellParams? formattedValueProp = { formattedValue: column.valueFormatter(cellParams) }; } + if (editCellState == null && column.renderCell) { + cellComponent = column.renderCell(cellParams); + cssClassProp = { cssClass: `${cssClassProp.cssClass} MuiDataGrid-cellWithRenderer` }; + } + + if (editCellState != null && column.renderEditCell) { + const params = { ...cellParams, ...editCellState }; + cellComponent = column.renderEditCell(params); + cssClassProp = { cssClass: `${cssClassProp.cssClass} MuiDataGrid-cellEditing` }; + } + const cellProps: GridCellProps & { children: any } = { value, field: column.field, @@ -114,6 +126,7 @@ export const GridRowCells: React.FC = React.memo((props) => { rowIndex, colIndex: colIdx + firstColIdx, children: cellComponent, + isEditable: cellParams.isEditable, hasFocus: cellFocus !== null && cellFocus.rowIndex === rowIndex && diff --git a/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts b/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts index 3b7db046ea20..9f084f45f8b9 100644 --- a/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts +++ b/packages/grid/_modules_/grid/components/containers/GridRootStyles.ts @@ -230,6 +230,17 @@ export const useStyles = makeStyles( whiteSpace: 'nowrap', borderBottom: `1px solid ${borderColor}`, }, + '& .MuiDataGrid-editCellInputBase': { + ...theme.typography.body2, + border: `1px solid ${borderColor}`, + borderRadius: theme.shape.borderRadius, + '& :focus': { + borderColor: theme.palette.text.primary, + outline: 0, + // boxShadow: getThemePaletteMode(theme.palette) === 'light' ? null : '0 0 0 100px #266798 inset', + boxShadow: 'inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 8px rgb(102 175 233 / 60%)', + }, + }, // The very last cell '& .MuiDataGrid-colCellWrapper .MuiDataGrid-cell': { borderBottom: 'none', diff --git a/packages/grid/_modules_/grid/components/editCell/EditInputCell.tsx b/packages/grid/_modules_/grid/components/editCell/EditInputCell.tsx new file mode 100644 index 000000000000..7f6cbecf4bd1 --- /dev/null +++ b/packages/grid/_modules_/grid/components/editCell/EditInputCell.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import InputBase, { InputBaseProps } from '@material-ui/core/InputBase'; +import { GridCellParams } from '../../models/params/gridCellParams'; +import { formatDateToLocalInputDate, isDate, mapColDefTypeToInputType } from '../../utils/utils'; +import { GridEditRowUpdate } from '../../models/gridEditRowModel'; +import { GridEditRowApi } from '../../models/api/gridEditRowApi'; + +export function EditInputCell(props: GridCellParams & InputBaseProps) { + const { + value, + api, + field, + row, + colDef, + getValue, + rowIndex, + colIndex, + isEditable, + ...inputBaseProps + } = props; + + const editRowApi = api as GridEditRowApi; + const [valueState, setValueState] = React.useState(value); + + const onValueChange = React.useCallback( + (event) => { + const newValue = event.target.value; + const update: GridEditRowUpdate = {}; + update[field] = { + value: colDef.type === 'date' || colDef.type === 'dateTime' ? new Date(newValue) : newValue, + }; + setValueState(newValue); + editRowApi.setEditCellProps(row.id, update); + }, + [editRowApi, colDef.type, field, row.id], + ); + + const onKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (!inputBaseProps.error && event.key === 'Enter') { + const update: GridEditRowUpdate = {}; + update[field] = { value }; + editRowApi.commitCellChange(row.id, update); + } + + if (event.key === 'Escape') { + editRowApi.setCellMode(row.id, field, 'view'); + } + }, + [inputBaseProps.error, row.id, field, value, editRowApi], + ); + + const inputType = mapColDefTypeToInputType(colDef.type); + const formattedValue = + valueState && isDate(valueState) + ? formatDateToLocalInputDate({ value: valueState, withTime: colDef.type === 'dateTime' }) + : valueState; + + React.useEffect(() => { + setValueState(value); + }, [value]); + + return ( + + ); +} +export const renderEditInputCell = (params) => ; diff --git a/packages/grid/_modules_/grid/constants/eventsConstants.ts b/packages/grid/_modules_/grid/constants/eventsConstants.ts index cc33f5db1be3..4dab2c660ba8 100644 --- a/packages/grid/_modules_/grid/constants/eventsConstants.ts +++ b/packages/grid/_modules_/grid/constants/eventsConstants.ts @@ -1,6 +1,7 @@ // Web standard events export const GRID_RESIZE = 'resize'; export const GRID_CLICK = 'click'; +export const GRID_DOUBLE_CLICK = 'dblclick'; export const GRID_MOUSE_HOVER = 'mouseover'; export const GRID_FOCUS_OUT = 'focusout'; export const GRID_KEYDOWN = 'keydown'; @@ -8,13 +9,19 @@ export const GRID_KEYUP = 'keyup'; export const GRID_SCROLL = 'scroll'; export const GRID_DRAGEND = 'dragend'; -// XGRID events +// GRID events +export const GRID_CELL_CHANGE = 'cellChange'; +export const GRID_CELL_CHANGE_COMMITTED = 'cellChangeCommitted'; +export const GRID_CELL_MODE_CHANGE = 'cellModeChange'; +export const GRID_EDIT_ROW_MODEL_CHANGE = 'editRowModelChange'; export const GRID_COMPONENT_ERROR = 'componentError'; export const GRID_UNMOUNT = 'unmount'; export const GRID_ELEMENT_FOCUS_OUT = 'gridFocusOut'; export const GRID_CELL_CLICK = 'cellClick'; +export const GRID_DOUBLE_CELL_CLICK = 'doubleCellClick'; export const GRID_CELL_HOVER = 'cellHover'; export const GRID_ROW_CLICK = 'rowClick'; +export const GRID_DOUBLE_ROW_CLICK = 'doubleRowClick'; export const GRID_ROW_HOVER = 'rowHover'; export const GRID_ROW_SELECTED = 'rowSelected'; export const GRID_SELECTION_CHANGED = 'selectionChange'; diff --git a/packages/grid/_modules_/grid/hooks/features/core/gridState.ts b/packages/grid/_modules_/grid/hooks/features/core/gridState.ts index 7aefe4a31280..7105fc2f9497 100644 --- a/packages/grid/_modules_/grid/hooks/features/core/gridState.ts +++ b/packages/grid/_modules_/grid/hooks/features/core/gridState.ts @@ -4,6 +4,7 @@ import { GridScrollBarState, GridViewportSizeState, } from '../../../models/gridContainerProps'; +import { GridEditRowsModel } from '../../../models/gridEditRowModel'; import { DEFAULT_GRID_OPTIONS, GridOptions } from '../../../models/gridOptions'; import { ColumnMenuState } from '../columnMenu/columnMenuState'; import { @@ -32,6 +33,7 @@ import { export interface GridState { rows: InternalGridRowsState; + editRows: GridEditRowsModel; pagination: PaginationState; options: GridOptions; isScrolling: boolean; @@ -53,6 +55,7 @@ export interface GridState { export const getInitialGridState: () => GridState = () => ({ rows: getInitialGridRowState(), + editRows: {}, pagination: GRID_INITIAL_PAGINATION_STATE, options: DEFAULT_GRID_OPTIONS, isScrolling: false, diff --git a/packages/grid/_modules_/grid/hooks/features/keyboard/useGridKeyboard.ts b/packages/grid/_modules_/grid/hooks/features/keyboard/useGridKeyboard.ts index db58b0a7e9c9..edfd643e2ec1 100644 --- a/packages/grid/_modules_/grid/hooks/features/keyboard/useGridKeyboard.ts +++ b/packages/grid/_modules_/grid/hooks/features/keyboard/useGridKeyboard.ts @@ -12,6 +12,7 @@ import { GridCellIndexCoordinates } from '../../../models/gridCell'; import { findParentElementFromClassName, getIdFromRowElem, + getRowEl, isGridCellRoot, } from '../../../utils/domUtils'; import { @@ -211,10 +212,7 @@ export const useGridKeyboard = ( ); const handleCopy = React.useCallback(() => { - const rowEl = findParentElementFromClassName( - document.activeElement as HTMLDivElement, - GRID_ROW_CSS_CLASS, - )! as HTMLElement; + const rowEl = getRowEl(document.activeElement)!; const rowId = getIdFromRowElem(rowEl); const isRowSelected = selectionState[rowId]; diff --git a/packages/grid/_modules_/grid/hooks/features/rows/gridEditRowsSelector.ts b/packages/grid/_modules_/grid/hooks/features/rows/gridEditRowsSelector.ts new file mode 100644 index 000000000000..a0c5a9bd9968 --- /dev/null +++ b/packages/grid/_modules_/grid/hooks/features/rows/gridEditRowsSelector.ts @@ -0,0 +1,3 @@ +import { GridState } from '../core/gridState'; + +export const gridEditRowsStateSelector = (state: GridState) => state.editRows; diff --git a/packages/grid/_modules_/grid/hooks/features/rows/index.ts b/packages/grid/_modules_/grid/hooks/features/rows/index.ts index decfdced5b42..e2f92894b076 100644 --- a/packages/grid/_modules_/grid/hooks/features/rows/index.ts +++ b/packages/grid/_modules_/grid/hooks/features/rows/index.ts @@ -1,3 +1,5 @@ export * from './gridRowsSelector'; export * from './gridRowsState'; export * from './useGridRows'; +export * from './gridEditRowsSelector'; +export * from './useGridEditRows'; diff --git a/packages/grid/_modules_/grid/hooks/features/rows/useGridEditRows.ts b/packages/grid/_modules_/grid/hooks/features/rows/useGridEditRows.ts new file mode 100644 index 000000000000..ebbc87ad2633 --- /dev/null +++ b/packages/grid/_modules_/grid/hooks/features/rows/useGridEditRows.ts @@ -0,0 +1,239 @@ +import * as React from 'react'; +import { + GRID_CELL_MODE_CHANGE, + GRID_CELL_CHANGE, + GRID_CELL_CHANGE_COMMITTED, + GRID_EDIT_ROW_MODEL_CHANGE, +} from '../../../constants/eventsConstants'; +import { GridApiRef } from '../../../models/api/gridApiRef'; +import { GridEditRowApi } from '../../../models/api/gridEditRowApi'; +import { GridCellMode } from '../../../models/gridCell'; +import { GridEditRowsModel, GridEditRowUpdate } from '../../../models/gridEditRowModel'; +import { GridFeatureModeConstant } from '../../../models/gridFeatureMode'; +import { GridRowId } from '../../../models/gridRows'; +import { GridCellParams } from '../../../models/params/gridCellParams'; +import { + GridCellModeChangeParams, + GridEditCellParams, + GridEditRowModelParams, +} from '../../../models/params/gridEditCellParams'; +import { buildGridCellParams } from '../../../utils/paramsUtils'; +import { useGridApiEventHandler } from '../../root/useGridApiEventHandler'; +import { useGridApiMethod } from '../../root/useGridApiMethod'; +import { optionsSelector } from '../../utils/optionsSelector'; +import { useGridSelector } from '../core/useGridSelector'; +import { useGridState } from '../core/useGridState'; + +export function useGridEditRows(apiRef: GridApiRef) { + const [, setGridState, forceUpdate] = useGridState(apiRef); + const options = useGridSelector(apiRef, optionsSelector); + + const getCellValue = React.useCallback( + (id: GridRowId, field: string) => { + const colDef = apiRef.current.getColumnFromField(field); + const rowModel = apiRef.current.getRowFromId(id); + + if (!colDef || !colDef.valueGetter) { + return rowModel[field]; + } + + return colDef.valueGetter( + buildGridCellParams({ + value: rowModel[field], + colDef, + rowModel, + api: apiRef.current, + }), + ); + }, + [apiRef], + ); + + const setCellEditMode = React.useCallback( + (id, field) => { + setGridState((state) => { + if (state.editRows[id] && state.editRows[id][field]) { + return state; + } + + const currentCellEditState: GridEditRowsModel = { ...state.editRows }; + currentCellEditState[id] = { ...currentCellEditState[id] } || {}; + currentCellEditState[id][field] = { value: getCellValue(id, field) }; + + const newEditRowsState: GridEditRowsModel = { ...state.editRows, ...currentCellEditState }; + + return { ...state, editRows: newEditRowsState }; + }); + forceUpdate(); + apiRef.current.publishEvent(GRID_CELL_MODE_CHANGE, { + id, + field, + mode: 'edit', + api: apiRef.current, + }); + + const editRowParams: GridEditRowModelParams = { + api: apiRef.current, + model: apiRef.current.getState().editRows, + }; + apiRef.current.publishEvent(GRID_EDIT_ROW_MODEL_CHANGE, editRowParams); + }, + [apiRef, forceUpdate, getCellValue, setGridState], + ); + + const setCellViewMode = React.useCallback( + (id, field) => { + setGridState((state) => { + const newEditRowsState: GridEditRowsModel = { ...state.editRows }; + + if (!newEditRowsState[id] || !newEditRowsState[id][field]) { + return state; + } + + if (newEditRowsState[id][field]) { + delete newEditRowsState[id][field]; + if (!Object.keys(newEditRowsState[id]).length) { + delete newEditRowsState[id]; + } + } + return { ...state, editRows: newEditRowsState }; + }); + forceUpdate(); + const params: GridCellModeChangeParams = { + id, + field, + mode: 'view', + api: apiRef.current, + }; + apiRef.current.publishEvent(GRID_CELL_MODE_CHANGE, params); + const editRowParams: GridEditRowModelParams = { + api: apiRef.current, + model: apiRef.current.getState().editRows, + }; + apiRef.current.publishEvent(GRID_EDIT_ROW_MODEL_CHANGE, editRowParams); + }, + [apiRef, forceUpdate, setGridState], + ); + + const setCellMode = React.useCallback( + (id, field, mode: GridCellMode) => { + if (mode === 'edit') { + setCellEditMode(id, field); + } else { + setCellViewMode(id, field); + } + }, + [setCellEditMode, setCellViewMode], + ); + + const isCellEditable = React.useCallback( + (params: GridCellParams) => { + return params.colDef.editable && (!options.isCellEditable || options.isCellEditable(params)); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [options.isCellEditable], + ); + + const commitCellChange = React.useCallback( + (id: GridRowId, update: GridEditRowUpdate) => { + if (options.editMode === GridFeatureModeConstant.server) { + const params: GridEditCellParams = { api: apiRef.current, id, update }; + apiRef.current.publishEvent(GRID_CELL_CHANGE_COMMITTED, params); + return; + } + const field = Object.keys(update).find((key) => key !== 'id')!; + const rowUpdate = { id }; + rowUpdate[field] = update[field].value; + apiRef.current.updateRows([rowUpdate]); + apiRef.current.setCellMode(id, field, 'view'); + }, + [apiRef, options.editMode], + ); + + const setEditCellProps = React.useCallback( + (id: GridRowId, update: GridEditRowUpdate) => { + if (options.editMode === GridFeatureModeConstant.server) { + const params: GridEditCellParams = { api: apiRef.current, id, update }; + apiRef.current.publishEvent(GRID_CELL_CHANGE, params); + return; + } + setGridState((state) => { + const editRowsModel: GridEditRowsModel = { ...state.editRows }; + editRowsModel[id] = { + ...state.editRows[id], + ...update, + }; + return { ...state, editRows: editRowsModel }; + }); + forceUpdate(); + const params: GridEditRowModelParams = { + api: apiRef.current, + model: apiRef.current.getState().editRows, + }; + apiRef.current.publishEvent(GRID_EDIT_ROW_MODEL_CHANGE, params); + }, + [apiRef, forceUpdate, options.editMode, setGridState], + ); + + const setEditRowsModel = React.useCallback( + (editRows: GridEditRowsModel) => { + setGridState((state) => { + const newState = { ...state, editRows }; + return newState; + }); + forceUpdate(); + }, + [forceUpdate, setGridState], + ); + // TODO cleanup params What should we put? + const onEditRowModelChange = React.useCallback( + (handler: (param: GridEditRowModelParams) => void): (() => void) => { + return apiRef.current.subscribeEvent(GRID_EDIT_ROW_MODEL_CHANGE, handler); + }, + [apiRef], + ); + const onCellModeChange = React.useCallback( + (handler: (param: GridCellModeChangeParams) => void): (() => void) => { + return apiRef.current.subscribeEvent(GRID_CELL_MODE_CHANGE, handler); + }, + [apiRef], + ); + const onEditCellChange = React.useCallback( + (handler: (param: GridEditCellParams) => void): (() => void) => { + return apiRef.current.subscribeEvent(GRID_CELL_CHANGE, handler); + }, + [apiRef], + ); + const onEditCellChangeCommitted = React.useCallback( + (handler: (param: GridEditCellParams) => void): (() => void) => { + return apiRef.current.subscribeEvent(GRID_CELL_CHANGE_COMMITTED, handler); + }, + [apiRef], + ); + + useGridApiEventHandler(apiRef, GRID_CELL_CHANGE, options.onEditCellChange); + useGridApiEventHandler(apiRef, GRID_CELL_CHANGE_COMMITTED, options.onEditCellChangeCommitted); + useGridApiEventHandler(apiRef, GRID_CELL_MODE_CHANGE, options.onCellModeChange); + useGridApiEventHandler(apiRef, GRID_EDIT_ROW_MODEL_CHANGE, options.onEditRowModelChange); + + useGridApiMethod( + apiRef, + { + getCellValue, + setCellMode, + onEditRowModelChange, + onCellModeChange, + onEditCellChangeCommitted, + onEditCellChange, + isCellEditable, + commitCellChange, + setEditCellProps, + setEditRowsModel, + }, + 'EditRowApi', + ); + + React.useEffect(() => { + apiRef.current.setEditRowsModel(options.editRowsModel || {}); + }, [apiRef, options.editRowsModel]); +} diff --git a/packages/grid/_modules_/grid/hooks/features/rows/useGridRows.ts b/packages/grid/_modules_/grid/hooks/features/rows/useGridRows.ts index f04f44c429a6..0586d9513e28 100644 --- a/packages/grid/_modules_/grid/hooks/features/rows/useGridRows.ts +++ b/packages/grid/_modules_/grid/hooks/features/rows/useGridRows.ts @@ -9,7 +9,7 @@ import { GridRowApi } from '../../../models/api/gridRowApi'; import { checkGridRowHasId, GridRowModel, - RowModelUpdate, + GridRowModelUpdate, GridRowId, GridRowsProp, GridRowIdGetter, @@ -133,7 +133,7 @@ export const useGridRows = ( ); const updateRows = React.useCallback( - (updates: RowModelUpdate[]) => { + (updates: GridRowModelUpdate[]) => { // we removes duplicate updates. A server can batch updates, and send several updates for the same row in one fn call. const uniqUpdates = updates.reduce((uniq, update) => { const udpateWithId = addGridRowId(update, getRowIdProp); @@ -158,13 +158,16 @@ export const useGridRows = ( addedRows.push(partialRow); return; } - Object.assign(internalRowsState.current.idRowsLookup[id], { + const lookup = { ...internalRowsState.current.idRowsLookup }; + + lookup[id] = { ...oldRow, ...partialRow, - }); + }; + internalRowsState.current.idRowsLookup = lookup; }); - setGridState((state) => ({ ...state, rows: internalRowsState.current })); + setGridState((state) => ({ ...state, rows: { ...internalRowsState.current } })); if (deletedRows.length > 0 || addedRows.length > 0) { deletedRows.forEach((row) => { diff --git a/packages/grid/_modules_/grid/hooks/root/useEvents.ts b/packages/grid/_modules_/grid/hooks/root/useEvents.ts index f5e5c2fe93b7..2a10d3416583 100644 --- a/packages/grid/_modules_/grid/hooks/root/useEvents.ts +++ b/packages/grid/_modules_/grid/hooks/root/useEvents.ts @@ -25,6 +25,9 @@ import { GRID_ELEMENT_FOCUS_OUT, GRID_COMPONENT_ERROR, GRID_STATE_CHANGE, + GRID_DOUBLE_CELL_CLICK, + GRID_DOUBLE_ROW_CLICK, + GRID_DOUBLE_CLICK, } from '../../constants/eventsConstants'; import { GRID_CELL_CSS_CLASS, GRID_ROW_CSS_CLASS } from '../../constants/cssClassesConstants'; import { findParentElementFromClassName, getIdFromRowElem, isGridCell } from '../../utils/domUtils'; @@ -113,6 +116,24 @@ export function useEvents(gridRootRef: React.RefObject, apiRef: [apiRef, getEventParams], ); + const onDoubleClickHandler = React.useCallback( + (event: MouseEvent) => { + const eventParams = getEventParams(event); + + if (!eventParams) { + return; + } + + if (eventParams.cell) { + apiRef.current.publishEvent(GRID_DOUBLE_CELL_CLICK, eventParams.cell); + } + if (eventParams.row) { + apiRef.current.publishEvent(GRID_DOUBLE_ROW_CLICK, eventParams.row); + } + }, + [apiRef, getEventParams], + ); + const onHoverHandler = React.useCallback( (event: MouseEvent) => { const eventParams = getEventParams(event); @@ -175,6 +196,9 @@ export function useEvents(gridRootRef: React.RefObject, apiRef: useGridApiEventHandler(apiRef, GRID_COLUMN_HEADER_CLICK, options.onColumnHeaderClick); useGridApiEventHandler(apiRef, GRID_CELL_CLICK, options.onCellClick); useGridApiEventHandler(apiRef, GRID_ROW_CLICK, options.onRowClick); + useGridApiEventHandler(apiRef, GRID_DOUBLE_CELL_CLICK, options.onCellDoubleClick); + useGridApiEventHandler(apiRef, GRID_DOUBLE_ROW_CLICK, options.onRowDoubleClick); + useGridApiEventHandler(apiRef, GRID_CELL_HOVER, options.onCellHover); useGridApiEventHandler(apiRef, GRID_ROW_HOVER, options.onRowHover); useGridApiEventHandler(apiRef, GRID_COMPONENT_ERROR, options.onError); @@ -188,6 +212,7 @@ export function useEvents(gridRootRef: React.RefObject, apiRef: const gridRootElem = gridRootRef.current; gridRootElem.addEventListener(GRID_CLICK, onClickHandler, { capture: true }); + gridRootElem.addEventListener(GRID_DOUBLE_CLICK, onDoubleClickHandler, { capture: true }); gridRootElem.addEventListener(GRID_MOUSE_HOVER, onHoverHandler, { capture: true }); gridRootElem.addEventListener(GRID_FOCUS_OUT, onFocusOutHandler); @@ -214,6 +239,7 @@ export function useEvents(gridRootRef: React.RefObject, apiRef: getHandler, logger, onClickHandler, + onDoubleClickHandler, onHoverHandler, onFocusOutHandler, apiRef, diff --git a/packages/grid/_modules_/grid/models/api/gridApi.ts b/packages/grid/_modules_/grid/models/api/gridApi.ts index a9ec556108d3..9204c3997af6 100644 --- a/packages/grid/_modules_/grid/models/api/gridApi.ts +++ b/packages/grid/_modules_/grid/models/api/gridApi.ts @@ -2,6 +2,7 @@ import { ColumnMenuApi } from './columnMenuApi'; import { ColumnResizeApi } from './columnResizeApi'; import { ComponentsApi } from './gridComponentsApi'; import { FilterApi } from './filterApi'; +import { GridEditRowApi } from './gridEditRowApi'; import { PreferencesPanelApi } from './preferencesPanelApi'; import { GridRowApi } from './gridRowApi'; import { GridColumnApi } from './gridColumnApi'; @@ -26,6 +27,7 @@ export type GridApi = GridCoreApi & GridDensityApi & GridEventsApi & GridRowApi & + GridEditRowApi & GridColumnApi & ColumnReorderApi & GridSelectionApi & diff --git a/packages/grid/_modules_/grid/models/api/gridEditRowApi.ts b/packages/grid/_modules_/grid/models/api/gridEditRowApi.ts new file mode 100644 index 000000000000..0ee6b9e70960 --- /dev/null +++ b/packages/grid/_modules_/grid/models/api/gridEditRowApi.ts @@ -0,0 +1,65 @@ +import { GridCellMode, GridCellValue } from '../gridCell'; +import { GridEditRowsModel, GridEditRowUpdate } from '../gridEditRowModel'; +import { GridRowId } from '../gridRows'; +import { GridCellParams } from '../params/gridCellParams'; +import { + GridCellModeChangeParams, + GridEditCellParams, + GridEditRowModelParams, +} from '../params/gridEditCellParams'; + +export interface GridEditRowApi { + /** + * Set the edit rows model of the grid. + * @param GridEditRowsModel + */ + setEditRowsModel: (model: GridEditRowsModel) => void; + /** + * Set the cellMode of a cell. + * @param GridRowId + * @param string + * @param 'edit' | 'view' + */ + setCellMode: (id: GridRowId, field: string, mode: GridCellMode) => void; + /** + * Returns true if the cell is editable. + * @param params + */ + isCellEditable: (params: GridCellParams) => boolean; + /** + * Set the edit cell input props. + * @param update + */ + setEditCellProps: (id: GridRowId, update: GridEditRowUpdate) => void; + /** + * Commit the cell value changes to update the cell value. + * @param update + */ + commitCellChange: (id: GridRowId, update: GridEditRowUpdate) => void; + /** + * Get the cell value of a row and field. + * @param id + * @param field + */ + getCellValue: (id: GridRowId, field: string) => GridCellValue; + /** + * Callback fired when the EditRowModel changed. + * @param handler + */ + onEditRowModelChange: (handler: (param: GridEditRowModelParams) => void) => void; + /** + * Callback fired when the cell mode changed. + * @param handler + */ + onCellModeChange: (handler: (param: GridCellModeChangeParams) => void) => void; + /** + * Callback fired when the cell changes are committed. + * @param handler + */ + onEditCellChangeCommitted: (handler: (param: GridEditCellParams) => void) => void; + /** + * Callback fired when the edit cell value changed. + * @param handler + */ + onEditCellChange: (handler: (param: GridEditCellParams) => void) => void; +} diff --git a/packages/grid/_modules_/grid/models/api/gridRowApi.ts b/packages/grid/_modules_/grid/models/api/gridRowApi.ts index 27c1cb74123d..8b0cbe2f00d9 100644 --- a/packages/grid/_modules_/grid/models/api/gridRowApi.ts +++ b/packages/grid/_modules_/grid/models/api/gridRowApi.ts @@ -1,4 +1,4 @@ -import { GridRowModel, GridRowId, RowModelUpdate } from '../gridRows'; +import { GridRowModel, GridRowId, GridRowModelUpdate } from '../gridRows'; /** * The Row API interface that is available in the grid [[apiRef]]. @@ -26,7 +26,7 @@ export interface GridRowApi { * Update any properties of the current set of GridRowData[]. * @param updates */ - updateRows: (updates: RowModelUpdate[]) => void; + updateRows: (updates: GridRowModelUpdate[]) => void; /** * Get the GridRowId of a row at a specific position. * @param index diff --git a/packages/grid/_modules_/grid/models/api/index.ts b/packages/grid/_modules_/grid/models/api/index.ts index 884b9d0db946..032328ea976f 100644 --- a/packages/grid/_modules_/grid/models/api/index.ts +++ b/packages/grid/_modules_/grid/models/api/index.ts @@ -4,6 +4,7 @@ export * from './gridColumnApi'; export * from './gridComponentsApi'; export * from './gridDensityApi'; export * from './gridEventsApi'; +export * from './gridEditRowApi'; export * from './gridApi'; export * from './gridPaginationApi'; export * from './gridRowApi'; diff --git a/packages/grid/_modules_/grid/models/colDef/gridColDef.ts b/packages/grid/_modules_/grid/models/colDef/gridColDef.ts index 4a0823ef24ec..3bd0888cb258 100644 --- a/packages/grid/_modules_/grid/models/colDef/gridColDef.ts +++ b/packages/grid/_modules_/grid/models/colDef/gridColDef.ts @@ -52,6 +52,11 @@ export interface GridColDef { * @default true */ resizable?: boolean; + /** + * If `true`, the cells of the column are editable. + * @default true + */ + editable?: boolean; /** * A comparator function used to sort rows. */ @@ -88,6 +93,11 @@ export interface GridColDef { * @param params */ renderCell?: (params: GridCellParams) => React.ReactElement; + /** + * Allows to override the component rendered in edit cell mode for this column. + * @param params + */ + renderEditCell?: (params: GridCellParams) => React.ReactElement; /** * Class name that will be added in the column header cell. */ diff --git a/packages/grid/_modules_/grid/models/colDef/gridStringColDef.ts b/packages/grid/_modules_/grid/models/colDef/gridStringColDef.ts index aa9913fc558c..29fa09b27717 100644 --- a/packages/grid/_modules_/grid/models/colDef/gridStringColDef.ts +++ b/packages/grid/_modules_/grid/models/colDef/gridStringColDef.ts @@ -1,3 +1,4 @@ +import { renderEditInputCell } from '../../components/editCell/EditInputCell'; import { gridStringNumberComparer } from '../../utils/sortingUtils'; import { GridColTypeDef } from './gridColDef'; import { getGridStringOperators } from './gridStringOperators'; @@ -12,4 +13,5 @@ export const GRID_STRING_COL_DEF: GridColTypeDef = { type: 'string', align: 'left', filterOperators: getGridStringOperators(), + renderEditCell: renderEditInputCell, }; diff --git a/packages/grid/_modules_/grid/models/gridCell.ts b/packages/grid/_modules_/grid/models/gridCell.ts index 84455990dc81..9190d9f2bc81 100644 --- a/packages/grid/_modules_/grid/models/gridCell.ts +++ b/packages/grid/_modules_/grid/models/gridCell.ts @@ -1,3 +1,8 @@ +/** + * The mode of the cell. + */ +export type GridCellMode = 'edit' | 'view'; + /** * The cell value type. */ diff --git a/packages/grid/_modules_/grid/models/gridEditRowModel.ts b/packages/grid/_modules_/grid/models/gridEditRowModel.ts new file mode 100644 index 000000000000..2757b7ead47e --- /dev/null +++ b/packages/grid/_modules_/grid/models/gridEditRowModel.ts @@ -0,0 +1,10 @@ +import { GridCellValue } from './gridCell'; + +export interface GridEditCellProps { + value: GridCellValue; + [prop: string]: any; +} + +export type GridEditRowUpdate = { [field: string]: GridEditCellProps }; + +export type GridEditRowsModel = { [rowId: string]: GridEditRowUpdate }; diff --git a/packages/grid/_modules_/grid/models/gridOptions.tsx b/packages/grid/_modules_/grid/models/gridOptions.tsx index 329d97e2ae3b..cf13904586a6 100644 --- a/packages/grid/_modules_/grid/models/gridOptions.tsx +++ b/packages/grid/_modules_/grid/models/gridOptions.tsx @@ -5,6 +5,7 @@ import { GridLocaleText } from './api/gridLocaleTextApi'; import { GridColumnTypesRecord } from './colDef/gridColTypeDef'; import { getGridDefaultColumnTypes } from './colDef/gridDefaultColumnTypes'; import { GridDensity, GridDensityTypes } from './gridDensity'; +import { GridEditRowsModel } from './gridEditRowModel'; import { GridFeatureMode, GridFeatureModeConstant } from './gridFeatureMode'; import { GridCellParams } from './params/gridCellParams'; import { GridColParams } from './params/gridColParams'; @@ -16,6 +17,11 @@ import { GridSelectionModelChangeParams } from './params/gridSelectionModelChang import { GridSortModelParams } from './params/gridSortModelParams'; import { GridSelectionModel } from './gridSelectionModel'; import { GridSortDirection, GridSortModel } from './gridSortModel'; +import { + GridCellModeChangeParams, + GridEditCellParams, + GridEditRowModelParams, +} from './params/gridEditCellParams'; // TODO add multiSortKey /** @@ -158,6 +164,12 @@ export interface GridOptions { * Set it to 'server' if you would like to handle filtering on the server-side. */ filterMode?: GridFeatureMode; + /** + * Edit cell or rows can be processed on the server or client-side. + * Set it to 'client' if you would like to handle editing on the client-side. + * Set it to 'server' if you would like to handle editing on the server-side. + */ + editMode?: GridFeatureMode; /** * If `true`, the footer component is hidden. * @default false @@ -215,6 +227,11 @@ export interface GridOptions { * @param param With all properties from [[GridCellParams]]. */ onCellClick?: (param: GridCellParams) => void; + /** + * Callback fired when a double click event comes from a cell element. + * @param param With all properties from [[CellParams]]. + */ + onCellDoubleClick?: (param: GridCellParams) => void; /** * Callback fired when a hover event comes from a cell element. * @param param With all properties from [[GridCellParams]]. @@ -225,6 +242,11 @@ export interface GridOptions { * @param param With all properties from [[GridRowParams]]. */ onRowClick?: (param: GridRowParams) => void; + /** + * Callback fired when a click event comes from a row container element. + * @param param With all properties from [[RowParams]]. + */ + onRowDoubleClick?: (param: GridRowParams) => void; /** * Callback fired when a hover event comes from a row container element. * @param param With all properties from [[GridRowParams]]. @@ -273,6 +295,34 @@ export interface GridOptions { * Callback fired when the state of the grid is updated. */ onStateChange?: (params: any) => void; + /** + * Set the edit rows model of the grid. + */ + editRowsModel?: GridEditRowsModel; + /** + * Callback fired when a cell is rendered, returns true if the cell is editable. + */ + isCellEditable?: (params: GridCellParams) => boolean; + /** + * Callback fired when the EditRowModel changed. + * @param handler + */ + onEditRowModelChange?: (params: GridEditRowModelParams) => void; + /** + * Callback fired when the cell mode changed. + * @param handler + */ + onCellModeChange?: (params: GridCellModeChangeParams) => void; + /** + * Callback fired when the edit cell value changed. + * @param handler + */ + onEditCellChange?: (params: GridEditCellParams) => void; + /** + * Callback fired when the cell changes are committed. + * @param handler + */ + onEditCellChangeCommitted?: (params: GridEditCellParams) => void; /** * Extend native column types with your new column types. */ diff --git a/packages/grid/_modules_/grid/models/gridRows.ts b/packages/grid/_modules_/grid/models/gridRows.ts index 7b9ec3b1b13d..dc64e8a7a1ba 100644 --- a/packages/grid/_modules_/grid/models/gridRows.ts +++ b/packages/grid/_modules_/grid/models/gridRows.ts @@ -8,7 +8,7 @@ export type GridRowModel = ObjectWithId & GridRowData; export type GridUpdateAction = 'delete'; -export interface RowModelUpdate extends GridRowData { +export interface GridRowModelUpdate extends GridRowData { _action?: GridUpdateAction; } diff --git a/packages/grid/_modules_/grid/models/index.ts b/packages/grid/_modules_/grid/models/index.ts index 20738a83accf..4d85995ec11f 100644 --- a/packages/grid/_modules_/grid/models/index.ts +++ b/packages/grid/_modules_/grid/models/index.ts @@ -1,6 +1,7 @@ export * from './colDef'; export * from './gridContainerProps'; export * from './elementSize'; +export * from './gridEditRowModel'; export * from './gridFeatureMode'; export * from './gridFilterItem'; export * from './gridFilterOperator'; diff --git a/packages/grid/_modules_/grid/models/params/gridCellParams.ts b/packages/grid/_modules_/grid/models/params/gridCellParams.ts index a07889eb4162..de00904d825f 100644 --- a/packages/grid/_modules_/grid/models/params/gridCellParams.ts +++ b/packages/grid/_modules_/grid/models/params/gridCellParams.ts @@ -35,9 +35,17 @@ export interface GridCellParams { */ rowIndex?: number; /** - * GridApiRef that let you manipulate the grid. + * The column index that the current cell belongs to. + */ + colIndex?: number; + /** + * GridApi that let you manipulate the grid. */ api: any; + /** + * If true, the cell is editable. + */ + isEditable?: boolean; } /** diff --git a/packages/grid/_modules_/grid/models/params/gridEditCellParams.ts b/packages/grid/_modules_/grid/models/params/gridEditCellParams.ts new file mode 100644 index 000000000000..437aabc9604a --- /dev/null +++ b/packages/grid/_modules_/grid/models/params/gridEditCellParams.ts @@ -0,0 +1,21 @@ +import { GridCellMode } from '../gridCell'; +import { GridEditRowsModel, GridEditRowUpdate } from '../gridEditRowModel'; +import { GridRowId } from '../gridRows'; + +export interface GridEditCellParams { + api: any; + id: GridRowId; + update: GridEditRowUpdate; +} + +export interface GridCellModeChangeParams { + id: GridRowId; + field: string; + api: any; + mode: GridCellMode; +} + +export interface GridEditRowModelParams { + model: GridEditRowsModel; + api: any; +} diff --git a/packages/grid/_modules_/grid/models/params/index.ts b/packages/grid/_modules_/grid/models/params/index.ts index 58d70977b070..3a35a6058fa1 100644 --- a/packages/grid/_modules_/grid/models/params/index.ts +++ b/packages/grid/_modules_/grid/models/params/index.ts @@ -1,4 +1,5 @@ export * from './gridCellParams'; +export * from './gridEditCellParams'; export * from './gridColParams'; export * from './gridBaseComponentProps'; export * from './gridFilterModelParams'; diff --git a/packages/grid/_modules_/grid/utils/domUtils.ts b/packages/grid/_modules_/grid/utils/domUtils.ts index f28bfdd2a1ed..1a45e1b3872d 100644 --- a/packages/grid/_modules_/grid/utils/domUtils.ts +++ b/packages/grid/_modules_/grid/utils/domUtils.ts @@ -2,6 +2,7 @@ import { GRID_CELL_CSS_CLASS, GRID_DATA_CONTAINER_CSS_CLASS, GRID_HEADER_CELL_TITLE_CSS_CLASS, + GRID_ROW_CSS_CLASS, } from '../constants/cssClassesConstants'; import { GridCellIndexCoordinates } from '../models/gridCell'; @@ -13,6 +14,13 @@ export function findParentElementFromClassName(elem: Element, className: string) return elem.closest(`.${className}`); } +export function getRowEl(cell?: Element | null): HTMLElement | null { + if (!cell) { + return null; + } + return findParentElementFromClassName(cell as HTMLDivElement, GRID_ROW_CSS_CLASS)! as HTMLElement; +} + export function isGridCellRoot(elem: Element | null): boolean { return elem != null && elem.classList.contains(GRID_CELL_CSS_CLASS); } @@ -32,6 +40,10 @@ export function getIdFromRowElem(rowEl: Element): string { return rowEl.getAttribute('data-id')!; } +export function getFieldFromCellElem(cellEl: Element): string { + return cellEl.getAttribute('data-field')!; +} + export function getFieldFromHeaderElem(colCellEl: Element): string { return colCellEl.getAttribute('data-field')!; } diff --git a/packages/grid/_modules_/grid/utils/mergeUtils.ts b/packages/grid/_modules_/grid/utils/mergeUtils.ts index 3f6a32b6464b..04a69ce097c2 100644 --- a/packages/grid/_modules_/grid/utils/mergeUtils.ts +++ b/packages/grid/_modules_/grid/utils/mergeUtils.ts @@ -9,11 +9,10 @@ export function mergeGridColTypes( const hydratedOptionColTypes: GridColumnTypesRecord = {}; Object.entries(mergedColTypes).forEach(([colType, colTypeDef]: [string, any]) => { - if (colTypeDef.extendType) { - colTypeDef = { ...mergedColTypes[colTypeDef.extendType], ...colTypeDef, type: colType }; - } else { - colTypeDef = { ...mergedColTypes[DEFAULT_GRID_COL_TYPE_KEY], ...colTypeDef, type: colType }; - } + colTypeDef = { + ...mergedColTypes[colTypeDef.extendType || DEFAULT_GRID_COL_TYPE_KEY], + ...colTypeDef, + }; hydratedOptionColTypes[colType] = colTypeDef; }); diff --git a/packages/grid/_modules_/grid/utils/paramsUtils.ts b/packages/grid/_modules_/grid/utils/paramsUtils.ts index 5f959be73798..f2430026b1e0 100644 --- a/packages/grid/_modules_/grid/utils/paramsUtils.ts +++ b/packages/grid/_modules_/grid/utils/paramsUtils.ts @@ -11,6 +11,7 @@ export function buildGridCellParams({ element, value, rowIndex, + colIndex, rowModel, colDef, api, @@ -18,11 +19,12 @@ export function buildGridCellParams({ rowModel: GridRowModel; colDef: GridColDef; rowIndex?: number; + colIndex?: number; value: GridCellValue; api: GridApi; element?: HTMLElement; }): GridCellParams { - return { + const params: GridCellParams = { element, value, field: colDef?.field, @@ -60,8 +62,14 @@ export function buildGridCellParams({ row: rowModel, colDef, rowIndex, + colIndex: colIndex || (colDef && api.getColumnIndex(colDef.field, true)), api, }; + const isEditableAttr = element && element.getAttribute('data-editable'); + params.isEditable = + isEditableAttr != null ? isEditableAttr === 'true' : colDef && api.isCellEditable(params); + + return params; } export function buildGridRowParams({ diff --git a/packages/grid/_modules_/grid/utils/utils.ts b/packages/grid/_modules_/grid/utils/utils.ts index 8d7f3c62d7d6..63ba488962cf 100644 --- a/packages/grid/_modules_/grid/utils/utils.ts +++ b/packages/grid/_modules_/grid/utils/utils.ts @@ -1,5 +1,6 @@ import * as styles from '@material-ui/core/styles'; import isDeepEqual from '../lib/lodash/isDeepEqual'; +import { GridCellValue } from '../models/gridCell'; export { isDeepEqual }; @@ -12,6 +13,21 @@ export function isDate(value: any): value is Date { return value instanceof Date; } +export function formatDateToLocalInputDate({ + value, + withTime, +}: { + value: GridCellValue; + withTime: boolean; +}) { + if (isDate(value)) { + const offset = value.getTimezoneOffset(); + const localDate = new Date(value.getTime() - offset * 60 * 1000); + return localDate.toISOString().substr(0, withTime ? 16 : 10); + } + return value; +} + export function isArray(value: any): value is Array { return Array.isArray(value); } @@ -60,3 +76,16 @@ export function localStorageAvailable() { return false; } } +export function mapColDefTypeToInputType(type: string) { + switch (type) { + case 'string': + return 'text'; + case 'number': + case 'date': + return type; + case 'dateTime': + return 'datetime-local'; + default: + return 'text'; + } +} diff --git a/packages/grid/x-grid-data-generator/src/renderer/renderCountry.tsx b/packages/grid/x-grid-data-generator/src/renderer/renderCountry.tsx index e150584630de..efa2500c58bf 100644 --- a/packages/grid/x-grid-data-generator/src/renderer/renderCountry.tsx +++ b/packages/grid/x-grid-data-generator/src/renderer/renderCountry.tsx @@ -5,7 +5,7 @@ import { GridCellParams } from '@material-ui/x-grid'; // ISO 3166-1 alpha-2 // ⚠️ No support for IE 11 function countryToFlag(isoCode: string) { - return typeof String.fromCodePoint !== 'undefined' + return typeof String.fromCodePoint !== 'undefined' && isoCode ? isoCode .toUpperCase() .replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397)) @@ -42,7 +42,7 @@ const Country = React.memo(function Country(props: CountryProps) { return (
- {countryToFlag(value.code)} + {value.code && countryToFlag(value.code)} {value.label}
); diff --git a/packages/grid/x-grid/src/tests/apiRef.XGrid.test.tsx b/packages/grid/x-grid/src/tests/apiRef.XGrid.test.tsx index f84f5aa55852..dcad48a0e19b 100644 --- a/packages/grid/x-grid/src/tests/apiRef.XGrid.test.tsx +++ b/packages/grid/x-grid/src/tests/apiRef.XGrid.test.tsx @@ -1,8 +1,14 @@ -import { GridApiRef, GridRowData, useGridApiRef, XGrid } from '@material-ui/x-grid'; +import { + GridApiRef, + GridComponentProps, + GridRowData, + useGridApiRef, + XGrid, +} from '@material-ui/x-grid'; import { expect } from 'chai'; import * as React from 'react'; import { useFakeTimers } from 'sinon'; -import { getColumnValues } from 'test/utils/helperFn'; +import { getCell, getColumnValues } from 'test/utils/helperFn'; import { createClientRenderStrictMode } from 'test/utils'; describe(' - apiRef', () => { @@ -46,11 +52,16 @@ describe(' - apiRef', () => { let apiRef: GridApiRef; - const TestCase = () => { + const TestCase = (props: Partial) => { apiRef = useGridApiRef(); return (
- +
); }; @@ -157,4 +168,34 @@ describe(' - apiRef', () => { expect(apiRef.current.getDataAsCsv()).to.equal('Brand\r\nNike\r\nAdidas\r\nPuma'); }); + + it('should allow to switch between cell mode', () => { + baselineProps.columns = baselineProps.columns.map((col) => ({ ...col, editable: true })); + + render(); + apiRef!.current.setCellMode(1, 'brand', 'edit'); + const cell = getCell(1, 0); + + expect(cell.classList.contains('MuiDataGrid-cellEditable')).to.equal(true); + expect(cell.classList.contains('MuiDataGrid-cellEditing')).to.equal(true); + expect(cell.querySelector('input')!.value).to.equal('Adidas'); + + apiRef!.current.setCellMode(1, 'brand', 'view'); + expect(cell.classList.contains('MuiDataGrid-cellEditable')).to.equal(true); + expect(cell.classList.contains('MuiDataGrid-cellEditing')).to.equal(false); + expect(cell.querySelector('input')).to.equal(null); + }); + + it('isCellEditable should add the class MuiDataGrid-cellEditable to editable cells but not prevent a cell from switching mode', () => { + baselineProps.columns = baselineProps.columns.map((col) => ({ ...col, editable: true })); + + render( params.value === 'Adidas'} />); + const cellNike = getCell(0, 0); + expect(cellNike!.classList.contains('MuiDataGrid-cellEditable')).to.equal(false); + const cellAdidas = getCell(1, 0); + expect(cellAdidas!.classList.contains('MuiDataGrid-cellEditable')).to.equal(true); + + apiRef!.current.setCellMode(0, 'brand', 'edit'); + expect(cellNike.classList.contains('MuiDataGrid-cellEditing')).to.equal(true); + }); }); diff --git a/packages/grid/x-grid/src/tests/rows.XGrid.test.tsx b/packages/grid/x-grid/src/tests/rows.XGrid.test.tsx index 042200a72780..803d6f36ffd3 100644 --- a/packages/grid/x-grid/src/tests/rows.XGrid.test.tsx +++ b/packages/grid/x-grid/src/tests/rows.XGrid.test.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import { createClientRenderStrictMode } from 'test/utils'; import { useFakeTimers } from 'sinon'; import { expect } from 'chai'; -import { getColumnValues } from 'test/utils/helperFn'; -import { GridApiRef, useGridApiRef, XGrid } from '@material-ui/x-grid'; +import { getCell, getColumnValues } from 'test/utils/helperFn'; +import { GridApiRef, GridColDef, GridRowData, useGridApiRef, XGrid } from '@material-ui/x-grid'; describe(' - Rows ', () => { let clock; @@ -18,7 +18,7 @@ describe(' - Rows ', () => { // TODO v5: replace with createClientRender const render = createClientRenderStrictMode(); - const baselineProps = { + const baselineProps: { columns: GridColDef[]; rows: GridRowData[] } = { rows: [ { clientId: 'c1', @@ -70,5 +70,33 @@ describe(' - Rows ', () => { expect(getColumnValues(2)).to.deep.equal(['11', '30', '31']); }); }); + + it('should allow to switch between cell mode', () => { + let apiRef: GridApiRef; + const editableProps = { ...baselineProps }; + editableProps.columns = editableProps.columns.map((col) => ({ ...col, editable: true })); + const getRowId = (row) => `${row.clientId}`; + + const Test = () => { + apiRef = useGridApiRef(); + return ( +
+ +
+ ); + }; + render(); + apiRef!.current.setCellMode('c2', 'first', 'edit'); + const cell = getCell(1, 1); + + expect(cell).to.have.class('MuiDataGrid-cellEditable'); + expect(cell).to.have.class('MuiDataGrid-cellEditing'); + expect(cell.querySelector('input')!.value).to.equal('Jack'); + apiRef!.current.setCellMode('c2', 'first', 'view'); + + expect(cell).to.have.class('MuiDataGrid-cellEditable'); + expect(cell).not.to.have.class('MuiDataGrid-cellEditing'); + expect(cell.querySelector('input')).to.equal(null); + }); }); }); diff --git a/packages/storybook/.storybook/preview.tsx b/packages/storybook/.storybook/preview.tsx index 0fa4e545bfbc..b9f540b2b564 100644 --- a/packages/storybook/.storybook/preview.tsx +++ b/packages/storybook/.storybook/preview.tsx @@ -9,7 +9,7 @@ LicenseInfo.setLicenseKey( ); configureActions({ - depth: 3, + depth: 6, limit: 10, }); diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 041a3ef42c48..46eea8009ffc 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -41,5 +41,11 @@ "source-map-loader": "^1.0.2", "string-replace-loader": "^3.0.1", "ts-loader": "^8.0.2" - } + }, + "browserslist": [ + ">0.3%", + "not ie 11", + "not dead", + "not op_mini all" + ] } diff --git a/packages/storybook/src/stories/grid-rows.stories.tsx b/packages/storybook/src/stories/grid-rows.stories.tsx index bdd78709ecfa..9058a2855e6d 100644 --- a/packages/storybook/src/stories/grid-rows.stories.tsx +++ b/packages/storybook/src/stories/grid-rows.stories.tsx @@ -5,14 +5,19 @@ import Popper from '@material-ui/core/Popper'; import Paper from '@material-ui/core/Paper'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; import { + GridCellValue, GridCellParams, + GridEditRowsModel, + GridLoadIcon, GridColDef, isOverflown, GridRowData, useGridApiRef, XGrid, + GridEditCellParams, } from '@material-ui/x-grid'; import { useDemoData } from '@material-ui/x-grid-data-generator'; +import { action } from '@storybook/addon-actions'; import { randomInt } from '../data/random-generator'; export default { @@ -347,3 +352,280 @@ export function ExpendRowCell() { ); } + +// Requirements +// TODO demo with Cell edit with value getter +// Todo demo with cell not editable according to value +// demo with cell edit validation, email, username(serverside) + +const baselineEditProps = { + rows: [ + { + id: 0, + firstname: 'Damien', + lastname: 'Tassone', + email: 'damien@material-ui.com', + username: 'Damo', + lastLogin: new Date(), + age: 25, + DOB: new Date(1996, 10, 2), + meetup: new Date(2020, 2, 25, 10, 50, 0), + }, + { + id: 1, + firstname: 'Jon', + lastname: 'Wood', + email: 'jon@material-ui.com', + username: 'jon', + lastLogin: new Date(), + age: 25, + DOB: new Date(1992, 1, 20), + meetup: new Date(2020, 4, 15, 10, 50, 0), + }, + { + id: 2, + firstname: 'James', + lastname: 'Smith', + email: 'james@material-ui.com', + username: 'smithhhh', + lastLogin: new Date(), + age: 25, + DOB: new Date(1986, 0, 12), + meetup: new Date(2020, 3, 5, 10, 50, 0), + }, + ], + columns: [ + { field: 'firstname', editable: true }, + { field: 'lastname', editable: true }, + { + field: 'fullname', + editable: true, + valueGetter: ({ row }) => `${row.firstname} ${row.lastname}`, + }, + { field: 'username', editable: true }, + { field: 'email', editable: true, width: 150 }, + { field: 'age', width: 50, type: 'number', editable: true }, + { field: 'DOB', width: 120, type: 'date', editable: true }, + { field: 'meetup', width: 180, type: 'dateTime', editable: true }, + { field: 'lastLogin', width: 180, type: 'dateTime', editable: false }, + ], +}; +function validateEmail(email) { + const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(String(email).toLowerCase()); +} + +const useEditCellStyles = makeStyles({ + root: { + '& .MuiDataGrid-cellEditable': { + backgroundColor: 'rgba(184,250,158,0.19)', + color: '#1a3e72', + }, + '& .MuiDataGrid-cellEditing': { + backgroundColor: 'rgb(255,215,115, 0.19)', + color: '#1a3e72', + }, + '& .Mui-error': { + backgroundColor: 'rgb(126,10,15, 0.1)', + color: '#750f0f', + }, + }, +}); + +export function EditRowsControl() { + const apiRef = useGridApiRef(); + const classes = useEditCellStyles(); + + const [selectedCell, setSelectedCell] = React.useState<[string, string, GridCellValue] | null>( + null, + ); + const [isEditable, setIsEditable] = React.useState(false); + const [editRowsModel, setEditRowsModel] = React.useState({}); + + const editRow = React.useCallback(() => { + if (!selectedCell) { + return; + } + const [id, field, value] = selectedCell; + + setEditRowsModel((state) => { + const editRowState: GridEditRowsModel = { ...state }; + editRowState[id] = editRowState[id] ? { ...editRowState[id] } : {}; + editRowState[id][field] = { value }; + + return { ...state, ...editRowState }; + }); + }, [selectedCell]); + + const onCellClick = React.useCallback((params: GridCellParams) => { + setSelectedCell([params.row.id!.toString(), params.field, params.value]); + setIsEditable(!!params.isEditable); + }, []); + + const onCellDoubleClick = React.useCallback( + (params: GridCellParams) => { + if (params.isEditable) { + apiRef.current.setCellMode(params.row.id!.toString(), params.field, 'edit'); + } + }, + [apiRef], + ); + + const isCellEditable = React.useCallback((params: GridCellParams) => params.row.id !== 0, []); + + const onEditCellChange = React.useCallback( + ({ id, update }: GridEditCellParams) => { + if (update.email) { + const isValid = validateEmail(update.email.value); + const newState = {}; + newState[id] = { + ...editRowsModel[id], + email: { ...update.email, error: !isValid }, + }; + newState[id].email.value += 'EXTERRRR'; + setEditRowsModel((state) => ({ ...state, ...newState })); + return; + } + const newState = {}; + newState[id] = { + ...editRowsModel[id], + ...update, + }; + setEditRowsModel((state) => ({ ...state, ...newState })); + }, + [editRowsModel], + ); + + const onEditCellChangeCommitted = React.useCallback( + ({ id, update }: GridEditCellParams) => { + const field = Object.keys(update)[0]!; + const rowUpdate = { id }; + rowUpdate[field] = update[field].value; + + if (update.email) { + const newState = {}; + const componentProps = { + endAdornment: , + }; + newState[id] = {}; + newState[id][field] = { ...update.email, ...componentProps }; + setEditRowsModel((state) => ({ ...state, ...newState })); + setTimeout(() => { + apiRef.current.updateRows([rowUpdate]); + apiRef.current.setCellMode(id, field, 'view'); + }, 2000); + } else if (update.fullname && update.fullname.value) { + const [firstname, lastname] = update.fullname.value.toString().split(' '); + apiRef.current.updateRows([{ id, firstname, lastname }]); + apiRef.current.setCellMode(id, field, 'view'); + } else { + apiRef.current.updateRows([rowUpdate]); + apiRef.current.setCellMode(id, field, 'view'); + } + }, + [apiRef], + ); + + return ( + + Green cells are editable! Click + EDIT or Double click +
+ +
+
+ +
+
+ ); +} +export function EditRowsBasic() { + const apiRef = useGridApiRef(); + + const onCellDoubleClick = React.useCallback( + (params: GridCellParams) => { + if (params.isEditable) { + apiRef.current.setCellMode(params.row.id!.toString(), params.field, 'edit'); + } + }, + [apiRef], + ); + + return ( + + Double click to edit. +
+ +
+
+ ); +} +const singleData = { rows: [...baselineEditProps.rows], columns: [...baselineEditProps.columns] }; +singleData.rows.length = 1; +singleData.columns.length = 1; +singleData.columns[0].width = 200; + +export function SingleCellBasic() { + const apiRef = useGridApiRef(); + const onCellDoubleClick = React.useCallback( + (params: GridCellParams) => { + if (params.isEditable) { + apiRef.current.setCellMode(params.row.id!.toString(), params.field, 'edit'); + } + }, + [apiRef], + ); + + return ( + + Double click to edit. +
+ +
+
+ ); +} +export function CommodityEdit() { + const apiRef = useGridApiRef(); + const onCellDoubleClick = React.useCallback( + (params: GridCellParams) => { + apiRef.current.setCellMode(params.row.id!.toString(), params.field, 'edit'); + }, + [apiRef], + ); + + const { data } = useDemoData({ + dataSet: 'Commodity', + rowLength: 100000, + }); + + return ( + +
+ +
+
+ ); +}