Skip to content

Commit

Permalink
feat(*): duplicate records from list views (#2042)
Browse files Browse the repository at this point in the history
closes #1362

Co-authored-by: Sebastian <sebastian@aam-digital.com>
  • Loading branch information
brajesh-lab and sleidig authored Nov 6, 2023
1 parent 106e71d commit c8b8c18
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
matSort
class="full-width table"
>
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef style="width: 0"></th>
<td mat-cell *matCellDef="let row">
<mat-checkbox
(change)="selectRow(row, $event)"
[checked]="selectedRecords.includes(row.record)"
></mat-checkbox>
</td>
</ng-container>

<ng-container *ngFor="let col of filteredColumns" [matColumnDef]="col.id">
<th
mat-header-cell
Expand Down Expand Up @@ -127,11 +137,13 @@
</div>
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
<tr
mat-header-row
*matHeaderRowDef="columnsToDisplay.concat('select')"
></tr>
<tr
mat-row
*matRowDef="let row; columns: columnsToDisplay"
*matRowDef="let row; columns: columnsToDisplay.concat('select')"
[style.background-color]="getBackgroundColor?.(row.record)"
class="table-row"
></tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { DisableEntityOperationDirective } from "../../../permissions/permission-directive/disable-entity-operation.directive";
import { Angulartics2Module } from "angulartics2";
import { ListPaginatorComponent } from "../list-paginator/list-paginator.component";
import {
MatCheckboxChange,
MatCheckboxModule,
} from "@angular/material/checkbox";
import { MatSlideToggleModule } from "@angular/material/slide-toggle";

export interface TableRow<T extends Entity> {
Expand Down Expand Up @@ -88,6 +92,7 @@ export interface TableRow<T extends Entity> {
DisableEntityOperationDirective,
Angulartics2Module,
ListPaginatorComponent,
MatCheckboxModule,
MatSlideToggleModule,
],
standalone: true,
Expand All @@ -96,6 +101,10 @@ export class EntitySubrecordComponent<T extends Entity> implements OnChanges {
@Input() isLoading: boolean;
@Input() clickMode: "popup" | "navigate" | "none" = "popup";

/** outputs an event containing an array of currently selected records (checkmarked by the user) */
@Output() selectedRecordsChange: EventEmitter<T[]> = new EventEmitter<T[]>();
@Input() selectedRecords: T[] = [];

@Input() showInactive = false;
@Output() showInactiveChange = new EventEmitter<boolean>();

Expand Down Expand Up @@ -485,6 +494,19 @@ export class EntitySubrecordComponent<T extends Entity> implements OnChanges {
return this.screenWidthObserver.currentScreenSize() >= numericValue;
}

selectRow(row: TableRow<T>, event: MatCheckboxChange) {
if (event.checked) {
this.selectedRecords.push(row.record);
} else {
const index = this.selectedRecords.indexOf(row.record);
if (index > -1) {
this.selectedRecords.splice(index, 1);
}
}

this.selectedRecordsChange.emit(this.selectedRecords);
}

filterActiveInactive() {
if (this.showInactive) {
// @ts-ignore type has issues with getters
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { TestBed } from "@angular/core/testing";
import { DuplicateRecordService } from "./duplicate-records.service";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import {
DatabaseEntity,
entityRegistry,
EntityRegistry,
} from "../../entity/database-entity.decorator";
import { Database } from "../../database/database";
import { SessionService } from "../../session/session-service/session.service";
import { Entity } from "../../entity/model/entity";
import { DatabaseField } from "../../entity/database-field.decorator";
import { CoreModule } from "../../core.module";
import { ComponentRegistry } from "../../../dynamic-components";
import { UpdateMetadata } from "../../entity/model/update-metadata";
import { MatDialog } from "@angular/material/dialog";
import { MatSnackBar } from "@angular/material/snack-bar";
import { FileService } from "../../../features/file/file.service";

describe("DuplicateRecordsService", () => {
let service: DuplicateRecordService;
let entityMapperService: EntityMapperService;

@DatabaseEntity("DuplicateTestEntity")
class DuplicateTestEntity extends Entity {
static toStringAttributes = ["name"];
@DatabaseField() name: String;
@DatabaseField() boolProperty: boolean;
@DatabaseField() created: UpdateMetadata;
@DatabaseField() updated: UpdateMetadata;
@DatabaseField() inactive: boolean;
}

beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreModule],
providers: [
DuplicateRecordService,
Database,
EntityMapperService,
SessionService,
{ provide: EntityRegistry, useValue: entityRegistry },
{ provide: MatDialog, useValue: {} },
{ provide: MatSnackBar, useValue: {} },
{ provide: FileService, useValue: {} },
ComponentRegistry,
],
});
service = TestBed.inject(DuplicateRecordService);
entityMapperService = TestBed.inject(EntityMapperService);
});

it("should be created", () => {
expect(service).toBeTruthy();
});

it("should transform data correctly", () => {
const duplicateTest = new DuplicateTestEntity();
duplicateTest.name = "TestName";
duplicateTest.boolProperty = true;

const originalData = [duplicateTest];
const transformedData = service.clone(originalData);

expect(transformedData[0]).toBeInstanceOf(Entity);
expect(transformedData[0]._id).toBeDefined();
expect(transformedData[0]._id).not.toBe(duplicateTest["_id"]);
expect(transformedData[0].name).toMatch(/^Copy of /);
expect(transformedData[0].boolProperty).toBe(true);
});

it("should save duplicate record", async () => {
const duplicateTestEntity = new DuplicateTestEntity();
duplicateTestEntity.name = "TestName";
duplicateTestEntity.boolProperty = true;
duplicateTestEntity.inactive = false;

const originalData = [duplicateTestEntity];
const cloneSpy = spyOn(service, "clone").and.callThrough();
const saveAllSpy = spyOn(entityMapperService, "saveAll");

await service.duplicateRecord(originalData);

expect(cloneSpy).toHaveBeenCalledWith(originalData);
expect(saveAllSpy).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Injectable } from "@angular/core";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import { EntityRegistry } from "../../entity/database-entity.decorator";
import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
import { Entity } from "../../entity/model/entity";

@Injectable({
providedIn: "root",
})
export class DuplicateRecordService {
get: jasmine.Spy<jasmine.Func>;
constructor(
private entitymapperservice: EntityMapperService,
private entityTypes: EntityRegistry,
private entityService: EntitySchemaService,
) {}

async duplicateRecord(sourceData: Entity[]) {
const duplicateData = this.clone(sourceData);
return await this.entitymapperservice.saveAll(duplicateData);
}

clone(sourceData: Entity[]): any {
const duplicateData = [];

sourceData.map((item: Entity) => {
const entityConstructor = item.getConstructor();
const keys = [...entityConstructor.schema.keys()].filter(
(key) => key !== "_id" && key !== "_rev",
);
const dbEntity = this.entityService.transformEntityToDatabaseFormat(item);
const entityformat = this.entityService.transformDatabaseToEntityFormat(
dbEntity,
entityConstructor.schema,
);
const entity = new entityConstructor();
const nameAttribute = entityConstructor.toStringAttributes[0];
for (const key of keys) {
if (nameAttribute === key && nameAttribute !== "entityId") {
entityformat[key] = `Copy of ${entityformat[key]}`;
}
entity[key] = entityformat[key];
}
duplicateData.push(entity);
});
return duplicateData;
}
}
17 changes: 17 additions & 0 deletions src/app/core/entity-list/entity-list/entity-list.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ <h2>{{ listName }}</h2>
[isLoading]="isLoading"
[filter]="filterObj"
[defaultSort]="listConfig?.defaultSort"
[(selectedRecords)]="selectedRows"
[showInactive]="showInactive"
(filteredRecordsChange)="filteredData = $event"
></app-entity-subrecord>
Expand Down Expand Up @@ -221,5 +222,21 @@ <h2>{{ listName }}</h2>
<span i18n> Import from file </span>
</button>

<button
mat-menu-item
(click)="duplicateRecords()"
[disabled]="selectedRows.length === 0"
matTooltip="Please select rows"
[matTooltipDisabled]="selectedRows.length > 0"
i18n-matTooltip
>
<fa-icon
class="color-accent standard-icon-with-text"
aria-label="duplicates record"
icon="copy"
></fa-icon>
<span i18n> Duplicate selected records </span>
</button>

<ng-content select="[mat-menu-item]"></ng-content>
</mat-menu>
11 changes: 11 additions & 0 deletions src/app/core/entity-list/entity-list/entity-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import { TabStateModule } from "../../../utils/tab-state/tab-state.module";
import { ViewTitleComponent } from "../../common-components/view-title/view-title.component";
import { ExportDataDirective } from "../../export/export-data-directive/export-data.directive";
import { DisableEntityOperationDirective } from "../../permissions/permission-directive/disable-entity-operation.directive";
import { DuplicateRecordService } from "../duplicate-records/duplicate-records.service";
import { MatTooltipModule } from "@angular/material/tooltip";

/**
* This component allows to create a full-blown table with pagination, filtering, searching and grouping.
Expand All @@ -58,6 +60,7 @@ import { DisableEntityOperationDirective } from "../../permissions/permission-di
selector: "app-entity-list",
templateUrl: "./entity-list.component.html",
styleUrls: ["./entity-list.component.scss"],
providers: [DuplicateRecordService],
imports: [
NgIf,
NgStyle,
Expand All @@ -78,6 +81,7 @@ import { DisableEntityOperationDirective } from "../../permissions/permission-di
ExportDataDirective,
DisableEntityOperationDirective,
RouterLink,
MatTooltipModule,
],
standalone: true,
})
Expand All @@ -98,6 +102,7 @@ export class EntityListComponent<T extends Entity>

@Output() elementClick = new EventEmitter<T>();
@Output() addNewClick = new EventEmitter();
selectedRows: T[] = [];

@ViewChild(EntitySubrecordComponent) entityTable: EntitySubrecordComponent<T>;

Expand Down Expand Up @@ -148,6 +153,7 @@ export class EntityListComponent<T extends Entity>
private entityMapperService: EntityMapperService,
private entities: EntityRegistry,
private dialog: MatDialog,
private duplicateRecord: DuplicateRecordService,
) {
if (this.activatedRoute.component === EntityListComponent) {
// the component is used for a route and not inside a template
Expand Down Expand Up @@ -305,4 +311,9 @@ export class EntityListComponent<T extends Entity>
}
this.addNewClick.emit();
}

duplicateRecords() {
this.duplicateRecord.duplicateRecord(this.selectedRows);
this.selectedRows = [];
}
}

0 comments on commit c8b8c18

Please sign in to comment.