Skip to content
This repository was archived by the owner on Jun 1, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-transla
import { TranslateHttpLoader } from '@ngx-translate/http-loader';

import { AppComponent } from './app.component';
import { CustomActionFormatterComponent } from './examples/custom-actionFormatter.component';
import { CustomTitleFormatterComponent } from './examples/custom-titleFormatter.component';
import { EditorNgSelectComponent } from './examples/editor-ng-select.component';
import { FilterNgSelectComponent } from './examples/filter-ng-select.component';
Expand Down Expand Up @@ -77,6 +78,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj
@NgModule({
declarations: [
AppComponent,
CustomActionFormatterComponent,
CustomTitleFormatterComponent,
EditorNgSelectComponent,
FilterNgSelectComponent,
Expand Down Expand Up @@ -137,6 +139,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj
],
entryComponents: [
// dynamically created components
CustomActionFormatterComponent,
CustomTitleFormatterComponent,
EditorNgSelectComponent,
FilterNgSelectComponent,
Expand Down
15 changes: 15 additions & 0 deletions src/app/examples/custom-actionFormatter.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Component } from '@angular/core';

@Component({
template: `<div id="myDrop" class="dropdown" style="position:absolute; z-index:12000;">
<button class="btn btn-default btn-xs dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
Action
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li><a class="pointer" (click)="parent.deleteCell(row)">Delete Row</a></li>
</ul></div>`
})
export class CustomActionFormatterComponent {
parent: any;
}
1 change: 1 addition & 0 deletions src/app/examples/grid-angular.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ <h2>{{title}}</h2>
(sgOnCellChange)="onCellChanged($event.detail.eventData, $event.detail.args)"
(sgOnClick)="onCellClicked($event.detail.eventData, $event.detail.args)"
(sgOnValidationError)="onCellValidation($event.detail.eventData, $event.detail.args)"
(sgOnActiveCellChanged)="onActiveCellChanged($event.detail.eventData, $event.detail.args)"
[columnDefinitions]="columnDefinitions" [gridOptions]="gridOptions" [dataset]="dataset">
</angular-slickgrid>
</div>
Expand Down
56 changes: 54 additions & 2 deletions src/app/examples/grid-angular.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import {
Editors,
FieldType,
Filters,
Formatter,
Formatters,
GridOption,
OnEventArgs,
} from './../modules/angular-slickgrid';
import { EditorNgSelectComponent } from './editor-ng-select.component';
import { CustomActionFormatterComponent } from './custom-actionFormatter.component';
import { CustomAngularComponentEditor } from './custom-angularComponentEditor';
import { CustomAngularComponentFilter } from './custom-angularComponentFilter';
import { CustomTitleFormatterComponent } from './custom-titleFormatter.component';
Expand All @@ -23,6 +25,17 @@ declare var $: any;

const NB_ITEMS = 100;

const customActionFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any, grid: any) => {
// use the same button text "Action" as the "CustomActionFormatterComponent" button text
// we basically recreate a dropdown on top of this one here which is just an empty one to show something in the grid
return `<div id="myDrop-r${row}-c${cell}" class="dropdown">
<button class="btn btn-default btn-xs dropdown-toggle" type="button">
Action
<span class="caret"></span>
</button>
</div>`;
};

@Component({
templateUrl: './grid-angular.component.html',
styleUrls: ['./grid-angular.component.scss'],
Expand Down Expand Up @@ -218,7 +231,8 @@ export class GridAngularComponent implements OnInit {
editor: {
model: Editors.date
},
}
},
{ id: 'action', name: 'Action', field: 'id', formatter: customActionFormatter, width: 70 }
];

this.gridOptions = {
Expand Down Expand Up @@ -329,8 +343,46 @@ export class GridAngularComponent implements OnInit {
const componentOutput = this.angularUtilService.createAngularComponent(colDef.params.component);
Object.assign(componentOutput.componentRef.instance, { item: dataContext });

// use a delay to make sure Angular ran at least a full cycle and it finished rendering the Component
// use a delay to make sure Angular ran at least a full cycle and make sure it finished rendering the Component
setTimeout(() => $(cellNode).empty().html(componentOutput.domElement));
}
}

/* Create an Action Dropdown Menu */
deleteCell(rowNumber: number) {
const item = this.angularGrid.dataView.getItem(rowNumber);
this.angularGrid.gridService.deleteItemById(item.id);
}

onActiveCellChanged(event, args) {
if (args.cell !== 6) {
return; // don't do anything unless it's the Action column which is at position 6 in this grid
}

$('#myDrop').remove(); // make sure to remove previous Action dropdown, you don't want to have 100 after a 100 clicks...
const cell = args.cell;
const row = args.row;

// hide the dropdown we created as a Formatter, we'll redisplay it later
const cellPos = $(`#myDrop-r${row}-c${cell}`).offset();

const componentOutput = this.angularUtilService.createAngularComponent(CustomActionFormatterComponent);

// pass "this" and the row number to the Component instance (CustomActionFormatter) so that we can call "parent.deleteCell(row)" with (click)
Object.assign(componentOutput.componentRef.instance, { parent: this, row: args.row });

// use a delay to make sure Angular ran at least a full cycle and make sure it finished rendering the Component before using it
setTimeout(() => {
const elm = $(componentOutput.domElement);
elm.appendTo('body');
elm.css('position', 'absolute');
elm.css('top', cellPos.top + 5);
elm.css('left', cellPos.left);
$('#myDrop').addClass('open');

$('#myDrop').on('hidden.bs.dropdown', () => {
$(`#myDrop-r${row}-c${cell}`).show();
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -398,10 +398,17 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
if (backendApi) {
// internalPostProcess only works (for now) with a GraphQL Service, so make sure it is that type
if (backendApi && backendApi.service instanceof GraphqlService) {
backendApi.internalPostProcess = (processResult: any) => {
backendApi.internalPostProcess = (processResult: GraphqlResult) => {
const datasetName = (backendApi && backendApi.service && typeof backendApi.service.getDatasetName === 'function') ? backendApi.service.getDatasetName() : '';
if (processResult && processResult.data && processResult.data[datasetName]) {
this._dataset = processResult.data[datasetName].nodes;
if (processResult.data[datasetName].listSeparator) {
// if the "listSeparator" is available in the GraphQL result, we'll override the ExportOptions Delimiter with this new info
if (!this.gridOptions.exportOptions) {
this.gridOptions.exportOptions = {};
}
this.gridOptions.exportOptions.delimiterOverride = processResult.data[datasetName].listSeparator.toString();
}
this.refreshGridData(this._dataset, processResult.data[datasetName].totalCount);
} else {
this._dataset = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export interface ExportOption {
/** export delimiter, can be (comma, tab, ... or even custom string). */
delimiter?: DelimiterType | string;

/** Allows you to override for the export delimiter, useful when adding the "listSeparator" to the GraphQL query */
delimiterOverride?: DelimiterType | string;

/** Defaults to false, which leads to all Formatters of the grid being evaluated on export. You can also override a column by changing the propery on the column itself */
exportWithFormatter?: boolean;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { DelimiterType } from './delimiterType.enum';
import { Metrics } from './metrics.interface';
import { Statistic } from './statistic.interface';

export interface GraphqlResult {
data: {
[datasetName: string]: {
nodes: any[],
nodes: any[];
pageInfo: {
hasNextPage: boolean;
},
totalCount: number
};
listSeparator?: DelimiterType;
totalCount: number;
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@ import { GraphqlPaginationOption } from './graphqlPaginationOption.interface';

export interface GraphqlServiceOption extends BackendServiceOption {
/**
* When using Translation, we probably want to add locale in the query for the filterBy/orderBy to work
* ex.: users(first: 10, offset: 0, locale: "en-CA", filterBy: [{field: name, operator: EQ, value:"John"}]) {
* When using Translation, we probably want to add locale as a query parameter for the filterBy/orderBy to work
* ex.: users(first: 10, offset: 0, locale: "en-CA", filterBy: [{field: name, operator: EQ, value:"John"}]) { }
*/
addLocaleIntoQuery?: boolean;

/**
* Add the Current User List Separator to the result query (in English the separator is comma ",").
* This is useful to set the "delimiter" property when using Export CSV, for example French uses semicolon ";" as a delimiter/separator
*/
addListSeparator?: boolean;

/** What is the dataset, this is required for the GraphQL query to be built */
datasetName?: string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ describe('ExportService', () => {
describe('startDownloadFile call after all private methods ran ', () => {
let mockCollection: any[];

beforeEach(() => {
mockGridOptions.exportOptions = { delimiterOverride: '' };
});

it(`should have the Order exported correctly with multiple formatters which have 1 of them returning an object with a text property (instead of simple string)`, (done) => {
mockCollection = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }];
jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length);
Expand All @@ -274,6 +278,31 @@ describe('ExportService', () => {
});
});

it(`should have the Order exported correctly with multiple formatters and use a different delimiter when "delimiterOverride" is provided`, (done) => {
mockGridOptions.exportOptions = { delimiterOverride: DelimiterType.doubleSemicolon };
mockCollection = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }];
jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length);
jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]);
const spyOnAfter = jest.spyOn(service.onGridAfterExportToFile, 'next');
const spyUrlCreate = jest.spyOn(URL, 'createObjectURL');
const spyDownload = jest.spyOn(service, 'startDownloadFile');

const optionExpectation = { filename: 'export.csv', format: 'csv', useUtf8WithBom: false };
const contentExpectation =
`"User Id";;"FirstName";;"LastName";;"Position";;"Order"
="1E06";;"John";;"Z";;"SALES_REP";;"<b>10</b>"`;

service.init(gridStub, dataViewStub);
service.exportToFile(mockExportCsvOptions);

setTimeout(() => {
expect(spyOnAfter).toHaveBeenCalledWith(optionExpectation);
expect(spyUrlCreate).toHaveBeenCalledWith(mockCsvBlob);
expect(spyDownload).toHaveBeenCalledWith({ ...optionExpectation, content: removeMultipleSpaces(contentExpectation) });
done();
});
});

it(`should have the UserId escape with equal sign showing as prefix, to avoid Excel casting the value 1E06 to 1 exponential 6,
when "exportCsvForceToKeepAsString" is enable in its column definition`, (done) => {
mockCollection = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }];
Expand Down
Loading