Skip to content

Commit

Permalink
feat(backend): add cancellable onBeforeSort & revert sort on error
Browse files Browse the repository at this point in the history
- in previous code, if an error happens on the backend server while querying, the sort icons would still be changed and we had no clue of the previous sort icons, this PR bring this functionality that if an error occurs it will rollback to previous sort icons.
- this PR also adds the option to use a new SlickGrid event `onBeforeSort` to optionally cancel the sort all together (for example we might want to cancel or not perform the sort if we have a cell edit that is still in process)
  • Loading branch information
ghiscoding committed Sep 2, 2021
1 parent aa9597a commit 958f823
Show file tree
Hide file tree
Showing 20 changed files with 396 additions and 134 deletions.
63 changes: 36 additions & 27 deletions examples/webpack-demo-vanilla-bundle/src/examples/example09.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,44 @@ <h3 class="title is-3">

<br />

<div>
<span>
<label>Programmatically go to first/last page:</label>
<button class="button is-small" data-test="goto-first-page" onclick.delegate="goToFirstPage()">
<i class="fa fa-caret-left fa-lg"></i>&lt;
</button>
<button class="button is-small" data-test="goto-last-page" onclick.delegate="goToLastPage()">
<i class="fa fa-caret-right fa-lg"></i>&gt;
</button>
</span>
<div class="columns">
<div class="column">
<span>
<label>Programmatically go to first/last page:</label>
<button class="button is-small" data-test="goto-first-page" onclick.delegate="goToFirstPage()">
<i class="fa fa-caret-left fa-lg"></i>&lt;
</button>
<button class="button is-small" data-test="goto-last-page" onclick.delegate="goToLastPage()">
<i class="fa fa-caret-right fa-lg"></i>&gt;
</button>
</span>

<span style="margin-left: 10px">
<label>OData Version: </label>
<span data-test="radioVersion">
<label class="radio-inline control-label" for="radio2">
<input type="radio" name="inlineRadioOptions" data-test="version2" id="radio2" checked value.bind="odataVersion"
onclick.delegate="setOdataVersion(2)"> 2
</label>
<label class="radio-inline control-label" for="radio4">
<input type="radio" name="inlineRadioOptions" data-test="version4" id="radio4" value.bind="odataVersion"
onclick.delegate="setOdataVersion(4)"> 4
</label>
<span style="margin-left: 10px">
<label>OData Version: </label>
<span data-test="radioVersion">
<label class="radio-inline control-label" for="radio2">
<input type="radio" name="inlineRadioOptions" data-test="version2" id="radio2" checked
value.bind="odataVersion"
onclick.delegate="setOdataVersion(2)"> 2
</label>
<label class="radio-inline control-label" for="radio4">
<input type="radio" name="inlineRadioOptions" data-test="version4" id="radio4" value.bind="odataVersion"
onclick.delegate="setOdataVersion(4)"> 4
</label>
</span>
</span>
</span>
<label class="checkbox-inline control-label" for="enableCount" style="margin-left: 20px">
<input type="checkbox" id="enableCount" data-test="enable-count" checked.bind="isCountEnabled"
onclick.delegate="changeCountEnableFlag()">
<span style="font-weight: bold">Enable Count</span> (add to OData query)
</label>
<label class="checkbox-inline control-label" for="enableCount" style="margin-left: 20px">
<input type="checkbox" id="enableCount" data-test="enable-count" checked.bind="isCountEnabled"
onclick.delegate="changeCountEnableFlag()">
<span style="font-weight: bold">Enable Count</span> (add to OData query)
</label>
</div>

<div class="column is-narrow" style="padding: 0">
<div class.bind="errorStatusClass" style="padding: 0.75rem 1.0rem" data-test="error-status">
<em><strong>Backend Error:</strong> <span innerhtml.bind="errorStatus"></span></em>
</div>
</div>
</div>

<div class="columns" style="margin-top: 5px">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.hidden {
display: none;
}
.visible {
display: inline-block;
}
54 changes: 48 additions & 6 deletions examples/webpack-demo-vanilla-bundle/src/examples/example09.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BindingEventService, Column, FieldType, Filters, GridOption, GridStateC
import { GridOdataService, OdataServiceApi, OdataOption } from '@slickgrid-universal/odata';
import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';
import { ExampleGridOptions } from './example-grid-options';
import './example09.scss';

const defaultPageSize = 20;

Expand All @@ -16,11 +17,14 @@ export class Example09 {
odataVersion = 2;
odataQuery = '';
processing = false;
errorStatus = '';
errorStatusClass = 'hidden';
status = '';
statusClass = 'is-success';

constructor() {
this._bindingEventService = new BindingEventService();
this.resetAllStatus();
}

attached() {
Expand All @@ -31,13 +35,27 @@ export class Example09 {
// this._bindingEventService.bind(gridContainerElm, 'onbeforeexporttoexcel', () => console.log('onBeforeExportToExcel'));
// this._bindingEventService.bind(gridContainerElm, 'onafterexporttoexcel', () => console.log('onAfterExportToExcel'));
this.sgb = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, []);

// you can optionally cancel the sort for whatever reason
// this._bindingEventService.bind(gridContainerElm, 'onbeforesort', (e) => {
// e.preventDefault();
// return false;
// });
}

dispose() {
if (this.sgb) {
this.sgb?.dispose();
}
this._bindingEventService.unbindAll();
this.resetAllStatus();
}

resetAllStatus() {
this.status = '';
this.errorStatus = '';
this.statusClass = 'is-success';
this.errorStatusClass = 'hidden';
}

initializeGrid() {
Expand All @@ -57,7 +75,7 @@ export class Example09 {
collection: [{ value: '', label: '' }, { value: 'male', label: 'male' }, { value: 'female', label: 'female' }]
}
},
{ id: 'company', name: 'Company', field: 'company' },
{ id: 'company', name: 'Company', field: 'company', sortable: true },
];

this.gridOptions = {
Expand Down Expand Up @@ -98,21 +116,35 @@ export class Example09 {
enableCount: this.isCountEnabled, // add the count in the OData query, which will return a property named "odata.count" (v2) or "@odata.count" (v4)
version: this.odataVersion // defaults to 2, the query string is slightly different between OData 2 and 4
},
preProcess: () => this.displaySpinner(true),
onError: () => {
this.errorStatusClass = 'visible notification is-light is-danger is-small is-narrow';
this.displaySpinner(false, true);
},
preProcess: () => {
this.errorStatus = '';
this.errorStatusClass = 'hidden';
this.displaySpinner(true);
},
process: (query) => this.getCustomerApiCall(query),
postProcess: (response) => {
this.metrics = response.metrics;
this.displaySpinner(false);
this.getCustomerCallback(response);
}
},
} as OdataServiceApi
};
}

displaySpinner(isProcessing) {
displaySpinner(isProcessing, isError?: boolean) {
this.processing = isProcessing;
this.status = (isProcessing) ? 'loading...' : 'finished!!';
this.statusClass = (isProcessing) ? 'notification is-light is-warning' : 'notification is-light is-success';

if (isError) {
this.status = 'ERROR!!!';
this.statusClass = 'notification is-light is-danger';
} else {
this.status = (isProcessing) ? 'loading...' : 'finished!!';
this.statusClass = (isProcessing) ? 'notification is-light is-warning' : 'notification is-light is-success';
}
}

getCustomerCallback(data) {
Expand Down Expand Up @@ -143,6 +175,9 @@ export class Example09 {
* in your case the getCustomer() should be a WebAPI function returning a Promise
*/
getCustomerDataApiMock(query): Promise<any> {
this.errorStatus = '';
this.errorStatusClass = 'hidden';

// the mock is returning a Promise, just like a WebAPI typically does
return new Promise((resolve) => {
const queryParams = query.toLowerCase().split('&');
Expand Down Expand Up @@ -194,6 +229,13 @@ export class Example09 {
}
}

// simular a backend error when trying to sort on the "Company" field
if (orderBy.includes('company')) {
const errorMsg = 'Cannot sort by the field "Company"';
this.errorStatus = errorMsg;
throw new Error(errorMsg);
}

const sort = orderBy.includes('asc')
? 'ASC'
: orderBy.includes('desc')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$autocomplete-max-height: 250px !default;
$autocomplete-max-height: 250px;
$control-height: 2.4em;

@import '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-salesforce.scss';
Expand Down
62 changes: 35 additions & 27 deletions examples/webpack-demo-vanilla-bundle/src/examples/example15.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,43 @@ <h3 class="title is-3">

<br />

<div>
<span>
<label>Programmatically go to first/last page:</label>
<button class="button is-small" data-test="goto-first-page" onclick.delegate="goToFirstPage()">
<i class="fa fa-caret-left fa-lg"></i>&lt;
</button>
<button class="button is-small" data-test="goto-last-page" onclick.delegate="goToLastPage()">
<i class="fa fa-caret-right fa-lg"></i>&gt;
</button>
</span>
<div class="columns">
<div class="column">
<span>
<label>Programmatically go to first/last page:</label>
<button class="button is-small" data-test="goto-first-page" onclick.delegate="goToFirstPage()">
<i class="fa fa-caret-left fa-lg"></i>&lt;
</button>
<button class="button is-small" data-test="goto-last-page" onclick.delegate="goToLastPage()">
<i class="fa fa-caret-right fa-lg"></i>&gt;
</button>
</span>

<span style="margin-left: 10px">
<label>OData Version: </label>
<span data-test="radioVersion">
<label class="radio-inline control-label" for="radio2">
<input type="radio" name="inlineRadioOptions" data-test="version2" id="radio2" checked value.bind="odataVersion"
onclick.delegate="setOdataVersion(2)"> 2
</label>
<label class="radio-inline control-label" for="radio4">
<input type="radio" name="inlineRadioOptions" data-test="version4" id="radio4" value.bind="odataVersion"
onclick.delegate="setOdataVersion(4)"> 4
</label>
<span style="margin-left: 10px">
<label>OData Version: </label>
<span data-test="radioVersion">
<label class="radio-inline control-label" for="radio2">
<input type="radio" name="inlineRadioOptions" data-test="version2" id="radio2" checked
value.bind="odataVersion"
onclick.delegate="setOdataVersion(2)"> 2
</label>
<label class="radio-inline control-label" for="radio4">
<input type="radio" name="inlineRadioOptions" data-test="version4" id="radio4" value.bind="odataVersion"
onclick.delegate="setOdataVersion(4)"> 4
</label>
</span>
</span>
</span>
<label class="checkbox-inline control-label" for="enableCount" style="margin-left: 20px">
<input type="checkbox" id="enableCount" data-test="enable-count" checked.bind="isCountEnabled"
onclick.delegate="changeCountEnableFlag()">
<span style="font-weight: bold">Enable Count</span> (add to OData query)
</label>
<label class="checkbox-inline control-label" for="enableCount" style="margin-left: 20px">
<input type="checkbox" id="enableCount" data-test="enable-count" checked.bind="isCountEnabled"
onclick.delegate="changeCountEnableFlag()">
<span style="font-weight: bold">Enable Count</span> (add to OData query)
</label>
</div>
<div class="column is-narrow" style="padding: 0">
<div class.bind="errorStatusClass" style="padding: 0.75rem 1.0rem" data-test="error-status">
<em><strong>Backend Error:</strong> <span innerhtml.bind="errorStatus"></span></em>
</div>
</div>
</div>

<div class="columns" style="margin-top: 5px">
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
@import '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-salesforce.scss';
@import '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-salesforce.scss';

.hidden {
display: none;
}
.visible {
display: inline-block;
}
36 changes: 33 additions & 3 deletions examples/webpack-demo-vanilla-bundle/src/examples/example15.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ export class Example15 {
odataVersion = 2;
odataQuery = '';
processing = false;
errorStatus = '';
errorStatusClass = '';
status = '';
statusClass = 'is-success';
isOtherGenderAdded = false;
genderCollection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];

constructor() {
this._bindingEventService = new BindingEventService();
this.resetAllStatus();
}

attached() {
Expand All @@ -45,6 +48,14 @@ export class Example15 {
this.sgb?.dispose();
}
this._bindingEventService.unbindAll();
this.resetAllStatus();
}

resetAllStatus() {
this.status = '';
this.errorStatus = '';
this.statusClass = 'is-success';
this.errorStatusClass = 'hidden';
}

initializeGrid() {
Expand Down Expand Up @@ -72,7 +83,7 @@ export class Example15 {
}
}
},
{ id: 'company', name: 'Company', field: 'company' },
{ id: 'company', name: 'Company', field: 'company', sortable: true },
];

this.gridOptions = {
Expand Down Expand Up @@ -118,7 +129,15 @@ export class Example15 {
enableCount: this.isCountEnabled, // add the count in the OData query, which will return a property named "odata.count" (v2) or "@odata.count" (v4)
version: this.odataVersion // defaults to 2, the query string is slightly different between OData 2 and 4
},
preProcess: () => this.displaySpinner(true),
onError: () => {
this.errorStatusClass = 'visible notification is-light is-danger is-small is-narrow';
this.displaySpinner(false, true);
},
preProcess: () => {
this.errorStatus = '';
this.errorStatusClass = 'hidden';
this.displaySpinner(true);
},
process: (query) => this.getCustomerApiCall(query),
postProcess: (response) => {
this.metrics = response.metrics;
Expand Down Expand Up @@ -160,10 +179,14 @@ export class Example15 {
this.isOtherGenderAdded = true;
}

displaySpinner(isProcessing) {
displaySpinner(isProcessing, isError?: boolean) {
this.processing = isProcessing;
this.status = (isProcessing) ? 'loading...' : 'finished!!';
this.statusClass = (isProcessing) ? 'notification is-light is-warning' : 'notification is-light is-success';
if (isError) {
this.status = 'ERROR!!!';
this.statusClass = 'notification is-light is-danger';
}
}

getCustomerCallback(data) {
Expand Down Expand Up @@ -245,6 +268,13 @@ export class Example15 {
}
}

// simular a backend error when trying to sort on the "Company" field
if (orderBy.includes('company')) {
const errorMsg = 'Cannot sort by the field "Company"';
this.errorStatus = errorMsg;
throw new Error(errorMsg);
}

const sort = orderBy.includes('asc')
? 'ASC'
: orderBy.includes('desc')
Expand Down
3 changes: 2 additions & 1 deletion packages/common/src/extensions/headerMenuExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
HeaderMenu,
MenuCommandItem,
MenuCommandItemCallbackArgs,
MultiColumnSort,
SlickEventHandler,
SlickHeaderMenu,
SlickNamespace,
Expand Down Expand Up @@ -429,7 +430,7 @@ export class HeaderMenuExtension implements Extension {
emitterType = EmitterType.local;
} else {
// when using customDataView, we will simply send it as a onSort event with notify
args.grid.onSort.notify(tmpSortedColumns);
args.grid.onSort.notify(tmpSortedColumns as unknown as MultiColumnSort);
}

// update the sharedService.slickGrid sortColumns array which will at the same add the visual sort icon(s) on the UI
Expand Down
Loading

0 comments on commit 958f823

Please sign in to comment.