From 757a828483619d3abfc93c5cf1b6d2264946469c Mon Sep 17 00:00:00 2001 From: Wayne Date: Sun, 28 Jul 2024 16:46:38 +0900 Subject: [PATCH] Fix time leave input --- .../TimeLeaveModal/TimeLeaveInputTable.tsx | 209 ++++++++++++------ .../TimesheetTable/TaskGroupSelect.tsx | 2 +- 2 files changed, 142 insertions(+), 69 deletions(-) diff --git a/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx b/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx index e623112..7876a16 100644 --- a/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx +++ b/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx @@ -6,7 +6,6 @@ import { GridCellParams, GridColDef, GridEditInputCell, - GridEventListener, GridRenderEditCellParams, GridRowId, GridRowModel, @@ -67,6 +66,22 @@ export type TimeLeaveRow = Partial< } >; +class ProcessRowUpdateError extends Error { + public readonly rowId: GridRowId; + public readonly errors: TimeEntryError | LeaveEntryError | undefined; + constructor( + rowId: GridRowId, + message?: string, + errors?: TimeEntryError | LeaveEntryError, + ) { + super(message); + this.rowId = rowId; + this.errors = errors; + + Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); + } +} + const TimeLeaveInputTable: React.FC = ({ day, allProjects, @@ -133,38 +148,32 @@ const TimeLeaveInputTable: React.FC = ({ }, []); const validateRow = useCallback( - (id: GridRowId) => { - const row = apiRef.current.getRowWithUpdatedValues( - id, - "", - ) as TimeLeaveRow; - - // Test for warnings + (row: TimeLeaveRow) => { if (row.type === "timeEntry") { - const error = validateTimeEntry(row, isHoliday); - let _isPlanned; - if ( - row.projectId && - row.taskGroupId && - milestonesByProject[row.projectId] - ) { - const milestone = - milestonesByProject[row.projectId][row.taskGroupId] || {}; - const { startDate, endDate } = milestone; - // Check if the current day is between the start and end date inclusively - _isPlanned = dayjs(day).isBetween(startDate, endDate, "day", "[]"); - } - apiRef.current.updateRows([{ id, _error: error, _isPlanned }]); - return !error; - } else if (row.type === "leaveEntry") { - const error = validateLeaveEntry(row, isHoliday); - apiRef.current.updateRows([{ id, _error: error }]); - return !error; + return validateTimeEntry(row, isHoliday); } else { - return false; + return validateLeaveEntry(row, isHoliday); } }, - [apiRef, day, isHoliday, milestonesByProject], + [isHoliday], + ); + + const verifyIsPlanned = useCallback( + (row: TimeLeaveRow) => { + if ( + row.type === "timeEntry" && + row.projectId && + row.taskGroupId && + milestonesByProject[row.projectId] + ) { + const milestone = + milestonesByProject[row.projectId][row.taskGroupId] || {}; + const { startDate, endDate } = milestone; + // Check if the current day is between the start and end date inclusively + return dayjs(day).isBetween(startDate, endDate, "day", "[]"); + } + }, + [day, milestonesByProject], ); const handleCancel = useCallback( @@ -176,6 +185,14 @@ const TimeLeaveInputTable: React.FC = ({ const editedRow = entries.find((entry) => entry.id === id); if (editedRow?._isNew) { setEntries((es) => es.filter((e) => e.id !== id)); + } else { + setEntries((es) => + es.map((e) => + e.id === id + ? { ...e, _error: undefined, _isPlanned: undefined } + : e, + ), + ); } }, [entries], @@ -190,30 +207,60 @@ const TimeLeaveInputTable: React.FC = ({ const handleSave = useCallback( (id: GridRowId) => () => { - if (validateRow(id)) { - setRowModesModel((model) => ({ - ...model, - [id]: { mode: GridRowModes.View }, - })); - } + setRowModesModel((model) => ({ + ...model, + [id]: { mode: GridRowModes.View }, + })); }, - [validateRow], + [], ); - const handleEditStop = useCallback>( - (params, event) => { - if (!validateRow(params.id)) { - event.defaultMuiPrevented = true; + const processRowUpdate = useCallback( + (newRow: GridRowModel) => { + const errors = validateRow(newRow); + if (errors) { + throw new ProcessRowUpdateError(newRow.id!, "validation error", errors); } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { _isNew, _error, _isPlanned, ...updatedRow } = newRow; + const newIsPlanned = verifyIsPlanned(updatedRow); + + const rowToSave = { + ...updatedRow, + ...(updatedRow.type === "timeEntry" + ? { + leaveTypeId: undefined, + } + : { + projectId: undefined, + taskGroupId: undefined, + taskId: undefined, + }), + _isPlanned: newIsPlanned, + } satisfies TimeLeaveRow; + setEntries((es) => + es.map((e) => (e.id === rowToSave.id ? rowToSave : e)), + ); + return rowToSave; }, - [validateRow], + [validateRow, verifyIsPlanned], ); - const processRowUpdate = useCallback((newRow: GridRowModel) => { - const updatedRow = { ...newRow, _isNew: false }; - setEntries((es) => es.map((e) => (e.id === newRow.id ? updatedRow : e))); - return updatedRow; - }, []); + const onProcessRowUpdateError = useCallback( + (updateError: ProcessRowUpdateError) => { + const errors = updateError.errors; + const rowId = updateError.rowId; + + apiRef.current.updateRows([ + { + id: rowId, + _error: errors, + }, + ]); + }, + [apiRef], + ); const columns = useMemo( () => [ @@ -249,6 +296,14 @@ const TimeLeaveInputTable: React.FC = ({ ]; }, }, + { + field: "projectId", + editable: true, + }, + { + field: "leaveTypeId", + editable: true, + }, { field: "type", headerName: t("Project or Leave"), @@ -293,23 +348,30 @@ const TimeLeaveInputTable: React.FC = ({ value: isLeave ? "leaveEntry" : "timeEntry", }); - params.api.updateRows([ - { - id: params.id, - ...(isLeave - ? { - type: "leaveEntry", - leaveTypeId: projectOrLeaveId, - projectId: undefined, - } - : { - type: "timeEntry", - projectId: projectOrLeaveId, - leaveTypeId: undefined, - }), - _error: undefined, - }, - ]); + await params.api.setEditCellValue({ + id: params.id, + field: isLeave ? "leaveTypeId" : "projectId", + value: projectOrLeaveId, + }); + + await params.api.setEditCellValue({ + id: params.id, + field: isLeave ? "projectId" : "leaveTypeId", + value: undefined, + }); + + await params.api.setEditCellValue({ + id: params.id, + field: "taskGroupId", + value: undefined, + }); + + await params.api.setEditCellValue({ + id: params.id, + field: "taskId", + value: undefined, + }); + params.api.setCellFocus( params.id, isLeave || !projectOrLeaveId ? "inputHours" : "taskGroupId", @@ -332,12 +394,17 @@ const TimeLeaveInputTable: React.FC = ({ projectId={params.row.projectId} value={params.value} taskGroupsByProject={taskGroupsByProject} - onTaskGroupSelect={(taskGroupId) => { - params.api.setEditCellValue({ + onTaskGroupSelect={async (taskGroupId) => { + await params.api.setEditCellValue({ id: params.id, field: params.field, value: taskGroupId, }); + await params.api.setEditCellValue({ + id: params.id, + field: "taskId", + value: undefined, + }); params.api.setCellFocus(params.id, "taskId"); }} /> @@ -346,12 +413,17 @@ const TimeLeaveInputTable: React.FC = ({ return ( { - params.api.setEditCellValue({ + onTaskGroupSelect={async (taskGroupId) => { + await params.api.setEditCellValue({ id: params.id, field: params.field, value: taskGroupId, }); + await params.api.setEditCellValue({ + id: params.id, + field: "taskId", + value: undefined, + }); params.api.setCellFocus(params.id, "taskId"); }} taskGroups={taskGroupsWithoutProject} @@ -614,11 +686,12 @@ const TimeLeaveInputTable: React.FC = ({ }} disableColumnMenu editMode="row" + columnVisibilityModel={{ projectId: false, leaveTypeId: false }} rows={entries} rowModesModel={rowModesModel} onRowModesModelChange={setRowModesModel} - onRowEditStop={handleEditStop} processRowUpdate={processRowUpdate} + onProcessRowUpdateError={onProcessRowUpdateError} columns={columns} getCellClassName={(params: GridCellParams) => { let classname = ""; diff --git a/src/components/TimesheetTable/TaskGroupSelect.tsx b/src/components/TimesheetTable/TaskGroupSelect.tsx index df77154..93c63f8 100644 --- a/src/components/TimesheetTable/TaskGroupSelect.tsx +++ b/src/components/TimesheetTable/TaskGroupSelect.tsx @@ -12,7 +12,7 @@ interface Props { }; projectId: number | undefined; value: number | undefined; - onTaskGroupSelect: (taskGroupId: number | string) => void; + onTaskGroupSelect: (taskGroupId: number | string) => Promise | void; error?: boolean; }