@@ -18,6 +18,8 @@ import { renderFormattedPayloadDate } from "@plane/utils";
1818import { TimeLineTypeContext } from "@/components/gantt-chart/contexts" ;
1919import { GanttChartRoot } from "@/components/gantt-chart/root" ;
2020import { GanttColorBy } from "@/plane-web/components/gantt-chart/color-by" ;
21+ import { GanttUndoButton } from "@/plane-web/components/gantt-chart/undo-button" ;
22+ import { ganttUndo } from "@/plane-web/store/gantt-undo" ;
2123import { IssueGanttSidebar } from "@/components/gantt-chart/sidebar/issues/sidebar" ;
2224// hooks
2325import { useIssues } from "@/hooks/store/use-issues" ;
@@ -85,12 +87,48 @@ export const BaseGanttRoot = observer(function BaseGanttRoot(props: IBaseGanttRo
8587 const updateIssueBlockStructure = async ( issue : TIssue , data : IBlockUpdateData ) => {
8688 if ( ! workspaceSlug ) return ;
8789
90+ // record the pre-change dates so Ctrl+Z / Undo can revert a bar drag or resize
91+ if ( data . start_date !== undefined || data . target_date !== undefined ) {
92+ ganttUndo . push ( {
93+ projectId : issue . project_id ,
94+ issueId : issue . id ,
95+ prev : { start_date : issue . start_date , target_date : issue . target_date } ,
96+ } ) ;
97+ }
98+
8899 const payload : any = { ...data } ;
89100 if ( data . sort_order ) payload . sort_order = data . sort_order . newSortOrder ;
90101
91102 updateIssue && ( await updateIssue ( issue . project_id , issue . id , payload ) ) ;
92103 } ;
93104
105+ // revert the last recorded bar date edit
106+ const handleGanttUndo = useCallback ( async ( ) => {
107+ const entry = ganttUndo . pop ( ) ;
108+ if ( ! entry || ! updateIssue ) return ;
109+ await updateIssue ( entry . projectId , entry . issueId , entry . prev ) ;
110+ } , [ updateIssue ] ) ;
111+
112+ // Ctrl/Cmd+Z reverts the last bar date edit (ignored while typing in a field)
113+ useEffect ( ( ) => {
114+ const onKey = ( e : KeyboardEvent ) => {
115+ if ( ! ( ( e . ctrlKey || e . metaKey ) && ! e . shiftKey && e . key . toLowerCase ( ) === "z" ) ) return ;
116+ const el = document . activeElement as HTMLElement | null ;
117+ if ( el ?. tagName === "INPUT" || el ?. tagName === "TEXTAREA" || el ?. isContentEditable ) return ;
118+ if ( ! ganttUndo . canUndo ) return ;
119+ e . preventDefault ( ) ;
120+ void handleGanttUndo ( ) ;
121+ } ;
122+ window . addEventListener ( "keydown" , onKey ) ;
123+ return ( ) => window . removeEventListener ( "keydown" , onKey ) ;
124+ } , [ handleGanttUndo ] ) ;
125+
126+ // undo history is scoped to the current project
127+ useEffect ( ( ) => {
128+ ganttUndo . clear ( ) ;
129+ return ( ) => ganttUndo . clear ( ) ;
130+ } , [ projectId ] ) ;
131+
94132 const isAllowed = allowPermissions ( [ EUserPermissions . ADMIN , EUserPermissions . MEMBER ] , EUserPermissionsLevel . PROJECT ) ;
95133 const updateBlockDates = useCallback (
96134 (
@@ -129,8 +167,9 @@ export const BaseGanttRoot = observer(function BaseGanttRoot(props: IBaseGanttRo
129167 < IssueLayoutHOC layout = { EIssueLayoutTypes . GANTT } >
130168 < TimeLineTypeContext . Provider value = { GANTT_TIMELINE_TYPE . ISSUE } >
131169 < div className = "relative h-full w-full" >
132- < div className = "absolute left-3 top-1.5 z-20" >
170+ < div className = "absolute left-3 top-1.5 z-20 flex items-center gap-2 " >
133171 < GanttColorBy />
172+ < GanttUndoButton onUndo = { handleGanttUndo } />
134173 </ div >
135174 < GanttChartRoot
136175 border = { false }
0 commit comments