From 72ef747bed152fb8c2fa2f08c2739817832219ac Mon Sep 17 00:00:00 2001
From: Ghislain B <gbeaulac@gmail.com>
Date: Wed, 28 Feb 2018 00:35:57 -0500
Subject: [PATCH] feat(grid): add inline editor undo command

---
 .../models/editCommand.interface.ts           | 15 ++++
 .../models/gridOption.interface.ts            | 27 ++++---
 .../src/aurelia-slickgrid/models/index.ts     |  2 +
 .../src/examples/slickgrid/example3.html      |  9 +++
 .../src/examples/slickgrid/example3.ts        | 37 +++++++---
 .../src/examples/slickgrid/example3.html      | 11 +++
 client-cli/src/examples/slickgrid/example3.js | 39 +++++++----
 .../src/examples/slickgrid/example3.html      | 11 +++
 .../src/examples/slickgrid/example3.ts        | 70 +++++++++++++++----
 9 files changed, 175 insertions(+), 46 deletions(-)
 create mode 100644 aurelia-slickgrid/src/aurelia-slickgrid/models/editCommand.interface.ts

diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/editCommand.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/editCommand.interface.ts
new file mode 100644
index 000000000..e656f44b9
--- /dev/null
+++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/editCommand.interface.ts
@@ -0,0 +1,15 @@
+import { Editor } from './editor.interface';
+
+export interface EditCommand {
+  row: number;
+  cell: number;
+  editor: Editor | any;
+  serializedValue: any;
+  prevSerializedValue: any;
+
+  /** Call to commit changes */
+  execute: () => void;
+
+  /** Call to rollback changes */
+  undo: () => void;
+}
diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/gridOption.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/gridOption.interface.ts
index 9cc331e18..54364690c 100644
--- a/aurelia-slickgrid/src/aurelia-slickgrid/models/gridOption.interface.ts
+++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/gridOption.interface.ts
@@ -1,13 +1,17 @@
-import { AutoResizeOption } from './autoResizeOption.interface';
-import { BackendEventChanged } from './backendEventChanged.interface';
-import { BackendServiceApi } from './backendServiceApi.interface';
-import { ColumnPicker } from './columnPicker.interface';
-import { CheckboxSelector } from './checkboxSelector.interface';
-import { ExportOption } from './exportOption.interface';
-import { GridMenu } from './gridMenu.interface';
-import { HeaderButton } from './headerButton.interface';
-import { HeaderMenu } from './headerMenu.interface';
-import { Pagination } from './pagination.interface';
+import {
+  AutoResizeOption,
+  BackendEventChanged,
+  BackendServiceApi,
+  Column,
+  ColumnPicker,
+  CheckboxSelector,
+  EditCommand,
+  ExportOption,
+  GridMenu,
+  HeaderButton,
+  HeaderMenu,
+  Pagination
+} from './../models/index';
 
 export interface GridOption {
   /** Defaults to false, which leads to load editor asynchronously (delayed) */
@@ -52,6 +56,9 @@ export interface GridOption {
   /** Defaults to false, when enabled will give the possibility to edit cell values with inline editors. */
   editable?: boolean;
 
+  /** option to intercept edit commands and implement undo support. */
+  editCommandHandler?: (item: any, column: Column, command: EditCommand) => void;
+
   /** Do we want to enable asynchronous (delayed) post rendering */
   enableAsyncPostRender?: boolean;
 
diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/index.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/index.ts
index d88025a69..37a2c6802 100644
--- a/aurelia-slickgrid/src/aurelia-slickgrid/models/index.ts
+++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/index.ts
@@ -9,9 +9,11 @@ export * from './checkboxSelector.interface';
 export * from './column.interface';
 export * from './columnFilter.interface';
 export * from './columnFilters.interface';
+export * from './columnPicker.interface';
 export * from './customGridMenu.interface';
 export * from './delimiterType.enum';
 export * from './editor.interface';
+export * from './editCommand.interface';
 export * from './exportOption.interface';
 export * from './fieldType.enum';
 export * from './fileType.enum';
diff --git a/aurelia-slickgrid/src/examples/slickgrid/example3.html b/aurelia-slickgrid/src/examples/slickgrid/example3.html
index fa08af263..5a36318dd 100644
--- a/aurelia-slickgrid/src/examples/slickgrid/example3.html
+++ b/aurelia-slickgrid/src/examples/slickgrid/example3.html
@@ -12,6 +12,12 @@ <h2>${title}</h2>
       <label class="radio-inline control-label" for="radioFalse">
         <input type="radio" name="inlineRadioOptions" id="radioFalse" value.bind="isAutoEdit" click.delegate="setAutoEdit(false)"> OFF (double-click)
       </label>
+      <span>
+        <button class="btn btn-default btn-sm" click.delegate="undo()">
+          <i class="fa fa-undo"></i>
+          Undo last edit
+        </button>
+      </span>
       <button class="btn btn-default btn-sm" click.delegate="switchLanguage()">Switch Language</button>
       <label>Locale</label>: ${selectedLanguage + '.json'}
     </span>
@@ -21,6 +27,9 @@ <h2>${title}</h2>
     <div class="alert alert-info" show.bind="updatedObject">
       <strong>Update Object:</strong> ${updatedObject | stringify}
     </div>
+    <div class="alert alert-warning" show.bind="alertWarning">
+      <strong></strong> ${alertWarning}
+    </div>
   </div>
 
   <div id="grid-container" class="col-sm-12">
diff --git a/aurelia-slickgrid/src/examples/slickgrid/example3.ts b/aurelia-slickgrid/src/examples/slickgrid/example3.ts
index ef97265ba..e0ce9f399 100644
--- a/aurelia-slickgrid/src/examples/slickgrid/example3.ts
+++ b/aurelia-slickgrid/src/examples/slickgrid/example3.ts
@@ -2,24 +2,29 @@ import { I18N } from 'aurelia-i18n';
 import { autoinject, bindable } from 'aurelia-framework';
 import { Column, Editors, FieldType, Formatters, GridExtraService, GridExtraUtils, GridOption, OnEventArgs, ResizerService } from '../../aurelia-slickgrid';
 
+// using external non-typed js libraries
+declare var Slick: any;
+
 @autoinject()
 export class Example3 {
   @bindable() gridObj: any;
   @bindable() dataview: any;
   title = 'Example 3: Editors';
   subTitle = `
-    Grid with Inline Editors and onCellClick actions (<a href="https://github.com/ghiscoding/aurelia-slickgrid/wiki/Editors" target="_blank">Wiki link</a>).
-    <ul>
-      <li>When using "enableCellNavigation: true", clicking on a cell will automatically make it active &amp; selected.</li>
-      <ul><li>If you don't want this behavior, then you should disable "enableCellNavigation"</li></ul>
-      <li>Inline Editors requires "enableCellNavigation: true" (not sure why though)</li>
-    </ul>
+  Grid with Inline Editors and onCellClick actions (<a href="https://github.com/ghiscoding/aurelia-slickgrid/wiki/Editors" target="_blank">Wiki link</a>).
+  <ul>
+  <li>When using "enableCellNavigation: true", clicking on a cell will automatically make it active &amp; selected.</li>
+  <ul><li>If you don't want this behavior, then you should disable "enableCellNavigation"</li></ul>
+  <li>Inline Editors requires "enableCellNavigation: true" (not sure why though)</li>
+  </ul>
   `;
+  private _commandQueue = [];
   gridOptions: GridOption;
   columnDefinitions: Column[];
   dataset: any[];
   updatedObject: any;
   isAutoEdit: boolean = true;
+  alertWarning: any;
   selectedLanguage: string;
 
   constructor(private gridExtraService: GridExtraService, private i18n: I18N, private resizer: ResizerService) {
@@ -51,7 +56,7 @@ export class Example3 {
         // use onCellClick OR grid.onClick.subscribe which you can see down below
         onCellClick: (args: OnEventArgs) => {
           console.log(args);
-          alert(`Editing: ${args.dataContext.title}`);
+          this.alertWarning = `Editing: ${args.dataContext.title}`;
           this.gridExtraService.highlightRow(args.row, 1500);
           this.gridExtraService.setSelectedRow(args.row);
         }
@@ -65,7 +70,7 @@ export class Example3 {
         /*
         onCellClick: (args: OnEventArgs) => {
           console.log(args);
-          alert(`Deleting: ${args.dataContext.title}`);
+          this.alertWarning = `Deleting: ${args.dataContext.title}`;
         }
         */
       },
@@ -85,7 +90,11 @@ export class Example3 {
         sidePadding: 15
       },
       editable: true,
-      enableCellNavigation: true
+      enableCellNavigation: true,
+      editCommandHandler: (item, column, editCommand) => {
+        this._commandQueue.push(editCommand);
+        editCommand.execute();
+      }
     };
   }
 
@@ -138,7 +147,7 @@ export class Example3 {
       const column = GridExtraUtils.getColumnDefinitionAndData(args);
       console.log('onClick', args, column);
       if (column.columnDef.id === 'edit') {
-        alert(`Call a modal window to edit: ${column.dataContext.title}`);
+        this.alertWarning = `open a modal window to edit: ${column.dataContext.title}`;
 
         // highlight the row, to customize the color, you can change the SASS variable $row-highlight-background-color
         this.gridExtraService.highlightRow(args.row, 1500);
@@ -165,4 +174,12 @@ export class Example3 {
     this.selectedLanguage = (this.selectedLanguage === 'en') ? 'fr' : 'en';
     this.i18n.setLocale(this.selectedLanguage);
   }
+
+  undo() {
+    const command = this._commandQueue.pop();
+    if (command && Slick.GlobalEditorLock.cancelCurrentEdit()) {
+      command.undo();
+      this.gridObj.gotoCell(command.row, command.cell, false);
+    }
+  }
 }
diff --git a/client-cli/src/examples/slickgrid/example3.html b/client-cli/src/examples/slickgrid/example3.html
index d208f70f9..5a36318dd 100644
--- a/client-cli/src/examples/slickgrid/example3.html
+++ b/client-cli/src/examples/slickgrid/example3.html
@@ -12,6 +12,14 @@ <h2>${title}</h2>
       <label class="radio-inline control-label" for="radioFalse">
         <input type="radio" name="inlineRadioOptions" id="radioFalse" value.bind="isAutoEdit" click.delegate="setAutoEdit(false)"> OFF (double-click)
       </label>
+      <span>
+        <button class="btn btn-default btn-sm" click.delegate="undo()">
+          <i class="fa fa-undo"></i>
+          Undo last edit
+        </button>
+      </span>
+      <button class="btn btn-default btn-sm" click.delegate="switchLanguage()">Switch Language</button>
+      <label>Locale</label>: ${selectedLanguage + '.json'}
     </span>
   </div>
 
@@ -19,6 +27,9 @@ <h2>${title}</h2>
     <div class="alert alert-info" show.bind="updatedObject">
       <strong>Update Object:</strong> ${updatedObject | stringify}
     </div>
+    <div class="alert alert-warning" show.bind="alertWarning">
+      <strong></strong> ${alertWarning}
+    </div>
   </div>
 
   <div id="grid-container" class="col-sm-12">
diff --git a/client-cli/src/examples/slickgrid/example3.js b/client-cli/src/examples/slickgrid/example3.js
index 6d83c7661..4909ad09e 100644
--- a/client-cli/src/examples/slickgrid/example3.js
+++ b/client-cli/src/examples/slickgrid/example3.js
@@ -7,18 +7,20 @@ export class Example3 {
   @bindable() dataview;
   title = 'Example 3: Editors';
   subTitle = `
-    Grid with Inline Editors and onCellClick actions (<a href="https://github.com/ghiscoding/aurelia-slickgrid/wiki/Editors" target="_blank">Wiki link</a>).
-    <ul>
-      <li>When using "enableCellNavigation: true", clicking on a cell will automatically make it active &amp; selected.
-      <ul><li>If you don't want this behavior, then you should disable "enableCellNavigation"</li></ul>
-      <li>Inline Editors requires "enableCellNavigation: true" (not sure why though)</li>
-    </ul>
+  Grid with Inline Editors and onCellClick actions (<a href="https://github.com/ghiscoding/aurelia-slickgrid/wiki/Editors" target="_blank">Wiki link</a>).
+  <ul>
+  <li>When using "enableCellNavigation: true", clicking on a cell will automatically make it active &amp; selected.
+  <ul><li>If you don't want this behavior, then you should disable "enableCellNavigation"</li></ul>
+  <li>Inline Editors requires "enableCellNavigation: true" (not sure why though)</li>
+  </ul>
   `;
   gridOptions;
   columnDefinitions;
+  commandQueue = [];
   dataset = [];
   updatedObject;
   isAutoEdit = true;
+  alertWarning;
   gridExtraService;
   resizer;
 
@@ -34,6 +36,13 @@ export class Example3 {
     this.getData();
   }
 
+  detached() {
+    // unsubscrible any Slick.Event you might have used
+    // a reminder again, these are SlickGrid Event, not Event Aggregator events
+    this.gridObj.onCellChange.unsubscribe();
+    this.gridObj.onClick.unsubscribe();
+  }
+
   /* Define grid Options and Columns */
   defineGrid() {
     this.columnDefinitions = [
@@ -45,7 +54,7 @@ export class Example3 {
         // use onCellClick OR grid.onClick.subscribe which you can see down below
         onCellClick: (args) => {
           console.log(args);
-          alert(`Editing: ${args.dataContext.title}`);
+          this.alertWarning = `Editing: ${args.dataContext.title}`;
           this.gridExtraService.highlightRow(args.row, 1500);
           this.gridExtraService.setSelectedRow(args.row);
         }
@@ -54,12 +63,14 @@ export class Example3 {
         id: 'delete', field: 'id',
         formatter: Formatters.deleteIcon,
         minWidth: 30,
-        maxWidth: 30,
+        maxWidth: 30
         // use onCellClick OR grid.onClick.subscribe which you can see down below
-        onCellClick: (args) => {
+        /*
+        onCellClick: (args: OnEventArgs) => {
           console.log(args);
-          alert(`Deleting: ${args.dataContext.title}`);
+          this.alertWarning = `Deleting: ${args.dataContext.title}`;
         }
+        */
       },
       { id: 'title', name: 'Title', field: 'title', sortable: true, type: FieldType.string, editor: Editors.longText, minWidth: 100 },
       { id: 'duration', name: 'Duration (days)', field: 'duration', sortable: true, type: FieldType.number, editor: Editors.text, minWidth: 100 },
@@ -77,7 +88,11 @@ export class Example3 {
         sidePadding: 15
       },
       editable: true,
-      enableCellNavigation: true
+      enableCellNavigation: true,
+      editCommandHandler: (item, column, editCommand) => {
+        this._commandQueue.push(editCommand);
+        editCommand.execute();
+      }
     };
   }
 
@@ -121,7 +136,7 @@ export class Example3 {
       const column = GridExtraUtils.getColumnDefinitionAndData(args);
       console.log('onClick', args, column);
       if (column.columnDef.id === 'edit') {
-        alert(`Call a modal window to edit: ${column.dataContext.title}`);
+        this.alertWarning = `open a modal window to edit: ${column.dataContext.title}`;
 
         // highlight the row, to customize the color, you can change the SASS variable $row-highlight-background-color
         this.gridExtraService.highlightRow(args.row, 1500);
diff --git a/doc/github-demo/src/examples/slickgrid/example3.html b/doc/github-demo/src/examples/slickgrid/example3.html
index d208f70f9..5a36318dd 100644
--- a/doc/github-demo/src/examples/slickgrid/example3.html
+++ b/doc/github-demo/src/examples/slickgrid/example3.html
@@ -12,6 +12,14 @@ <h2>${title}</h2>
       <label class="radio-inline control-label" for="radioFalse">
         <input type="radio" name="inlineRadioOptions" id="radioFalse" value.bind="isAutoEdit" click.delegate="setAutoEdit(false)"> OFF (double-click)
       </label>
+      <span>
+        <button class="btn btn-default btn-sm" click.delegate="undo()">
+          <i class="fa fa-undo"></i>
+          Undo last edit
+        </button>
+      </span>
+      <button class="btn btn-default btn-sm" click.delegate="switchLanguage()">Switch Language</button>
+      <label>Locale</label>: ${selectedLanguage + '.json'}
     </span>
   </div>
 
@@ -19,6 +27,9 @@ <h2>${title}</h2>
     <div class="alert alert-info" show.bind="updatedObject">
       <strong>Update Object:</strong> ${updatedObject | stringify}
     </div>
+    <div class="alert alert-warning" show.bind="alertWarning">
+      <strong></strong> ${alertWarning}
+    </div>
   </div>
 
   <div id="grid-container" class="col-sm-12">
diff --git a/doc/github-demo/src/examples/slickgrid/example3.ts b/doc/github-demo/src/examples/slickgrid/example3.ts
index 0037e62a9..fb9589469 100644
--- a/doc/github-demo/src/examples/slickgrid/example3.ts
+++ b/doc/github-demo/src/examples/slickgrid/example3.ts
@@ -1,29 +1,36 @@
+import { I18N } from 'aurelia-i18n';
 import { autoinject, bindable } from 'aurelia-framework';
 import { Column, Editors, FieldType, Formatters, GridExtraService, GridExtraUtils, GridOption, OnEventArgs, ResizerService } from 'aurelia-slickgrid';
 
+// using external non-typed js libraries
+declare var Slick: any;
+
 @autoinject()
 export class Example3 {
   @bindable() gridObj: any;
   @bindable() dataview: any;
   title = 'Example 3: Editors';
   subTitle = `
-    Grid with Inline Editors and onCellClick actions (<a href="https://github.com/ghiscoding/aurelia-slickgrid/wiki/Editors" target="_blank">Wiki link</a>).
-    <ul>
-      <li>When using "enableCellNavigation: true", clicking on a cell will automatically make it active &amp; selected.
-      <ul><li>If you don't want this behavior, then you should disable "enableCellNavigation"</li></ul>
-      <li>Inline Editors requires "enableCellNavigation: true" (not sure why though)</li>
-    </ul>
+  Grid with Inline Editors and onCellClick actions (<a href="https://github.com/ghiscoding/aurelia-slickgrid/wiki/Editors" target="_blank">Wiki link</a>).
+  <ul>
+  <li>When using "enableCellNavigation: true", clicking on a cell will automatically make it active &amp; selected.</li>
+  <ul><li>If you don't want this behavior, then you should disable "enableCellNavigation"</li></ul>
+  <li>Inline Editors requires "enableCellNavigation: true" (not sure why though)</li>
+  </ul>
   `;
-
+  private _commandQueue = [];
   gridOptions: GridOption;
   columnDefinitions: Column[];
   dataset: any[];
   updatedObject: any;
   isAutoEdit: boolean = true;
+  alertWarning: any;
+  selectedLanguage: string;
 
-  constructor(private gridExtraService: GridExtraService, private resizer: ResizerService) {
+  constructor(private gridExtraService: GridExtraService, private i18n: I18N, private resizer: ResizerService) {
     // define the grid options & columns and then create the grid itself
     this.defineGrid();
+    this.selectedLanguage = this.i18n.getLocale();
   }
 
   attached() {
@@ -31,6 +38,13 @@ export class Example3 {
     this.getData();
   }
 
+  detached() {
+    // unsubscrible any Slick.Event you might have used
+    // a reminder again, these are SlickGrid Event, not Event Aggregator events
+    this.gridObj.onCellChange.unsubscribe();
+    this.gridObj.onClick.unsubscribe();
+  }
+
   /* Define grid Options and Columns */
   defineGrid() {
     this.columnDefinitions = [
@@ -42,7 +56,7 @@ export class Example3 {
         // use onCellClick OR grid.onClick.subscribe which you can see down below
         onCellClick: (args: OnEventArgs) => {
           console.log(args);
-          alert(`Editing: ${args.dataContext.title}`);
+          this.alertWarning = `Editing: ${args.dataContext.title}`;
           this.gridExtraService.highlightRow(args.row, 1500);
           this.gridExtraService.setSelectedRow(args.row);
         }
@@ -53,15 +67,17 @@ export class Example3 {
         minWidth: 30,
         maxWidth: 30,
         // use onCellClick OR grid.onClick.subscribe which you can see down below
+        /*
         onCellClick: (args: OnEventArgs) => {
           console.log(args);
-          alert(`Deleting: ${args.dataContext.title}`);
+          this.alertWarning = `Deleting: ${args.dataContext.title}`;
         }
+        */
       },
       { id: 'title', name: 'Title', field: 'title', sortable: true, type: FieldType.string, editor: Editors.longText, minWidth: 100 },
       { id: 'duration', name: 'Duration (days)', field: 'duration', sortable: true, type: FieldType.number, editor: Editors.text, minWidth: 100 },
       { id: 'complete', name: '% Complete', field: 'percentComplete', formatter: Formatters.percentCompleteBar, type: FieldType.number, editor: Editors.integer, minWidth: 100 },
-      { id: 'start', name: 'Start', field: 'start', formatter: Formatters.dateIso, sortable: true, minWidth: 100, type: FieldType.date, editor: Editors.date },
+      { id: 'start', name: 'Start', field: 'start', formatter: Formatters.dateIso, sortable: true, minWidth: 100, type: FieldType.date, editor: Editors.date, params: { i18n: this.i18n } },
       { id: 'finish', name: 'Finish', field: 'finish', formatter: Formatters.dateIso, sortable: true, minWidth: 100, type: FieldType.date, editor: Editors.date },
       { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', formatter: Formatters.checkmark, type: FieldType.number, editor: Editors.checkbox, minWidth: 100 }
     ];
@@ -74,7 +90,11 @@ export class Example3 {
         sidePadding: 15
       },
       editable: true,
-      enableCellNavigation: true
+      enableCellNavigation: true,
+      editCommandHandler: (item, column, editCommand) => {
+        this._commandQueue.push(editCommand);
+        editCommand.execute();
+      }
     };
   }
 
@@ -84,7 +104,7 @@ export class Example3 {
 
   getData() {
     // mock a dataset
-    let mockedDataset = [];
+    const mockedDataset = [];
     for (let i = 0; i < 1000; i++) {
       const randomYear = 2000 + Math.floor(Math.random() * 10);
       const randomMonth = Math.floor(Math.random() * 11);
@@ -106,6 +126,15 @@ export class Example3 {
   }
 
   gridObjChanged(grid) {
+    grid.onBeforeEditCell.subscribe((e, args) => {
+      console.log('before edit', e);
+      e.stopImmediatePropagation();
+    });
+    grid.onBeforeCellEditorDestroy.subscribe((e, args) => {
+      console.log('before destroy');
+      e.stopPropagation();
+    });
+
     grid.onCellChange.subscribe((e, args) => {
       console.log('onCellChange', args);
       this.updatedObject = args.item;
@@ -118,7 +147,7 @@ export class Example3 {
       const column = GridExtraUtils.getColumnDefinitionAndData(args);
       console.log('onClick', args, column);
       if (column.columnDef.id === 'edit') {
-        alert(`Call a modal window to edit: ${column.dataContext.title}`);
+        this.alertWarning = `open a modal window to edit: ${column.dataContext.title}`;
 
         // highlight the row, to customize the color, you can change the SASS variable $row-highlight-background-color
         this.gridExtraService.highlightRow(args.row, 1500);
@@ -140,4 +169,17 @@ export class Example3 {
     this.gridObj.setOptions({ autoEdit: isAutoEdit });
     return true;
   }
+
+  switchLanguage() {
+    this.selectedLanguage = (this.selectedLanguage === 'en') ? 'fr' : 'en';
+    this.i18n.setLocale(this.selectedLanguage);
+  }
+
+  undo() {
+    const command = this._commandQueue.pop();
+    if (command && Slick.GlobalEditorLock.cancelCurrentEdit()) {
+      command.undo();
+      this.gridObj.gotoCell(command.row, command.cell, false);
+    }
+  }
 }