Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Duplicate a record #2042

Merged
merged 29 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3872046
added checkbox and service for select the entities
Oct 18, 2023
eeb84ac
added checkbox for select entities
Oct 18, 2023
e597acb
added checkbox and button to create duplicate records
Oct 18, 2023
8ee0c74
created service for create copy of selected entities
Oct 19, 2023
eb9983b
#1362 - created services for copy of select entities
Oct 19, 2023
7f5b4aa
#1362 - created services for copy of select entities
Oct 19, 2023
89a40e3
updated for center
Oct 26, 2023
6144892
#1362 - updated code for DOB
Oct 28, 2023
2861fc2
updated test case
Oct 30, 2023
c2e1767
#2042 - updated test case for create duplicate
Oct 31, 2023
a87ab10
fixed linting
Oct 31, 2023
5b0bbfb
#1362 - updated test cases
Nov 1, 2023
52d5dc5
solved merge conflict
Nov 1, 2023
653f4af
updated test case for code climate
Nov 1, 2023
741692b
#1362 added flash message for select row
Nov 2, 2023
3d12984
fixed lint
Nov 2, 2023
52defe3
updated code for feedback
Nov 3, 2023
b1bb2a8
#1362 - fixed lint
Nov 3, 2023
afc5b93
updated for feedback
Nov 4, 2023
c387216
updated matTooltip
Nov 4, 2023
7832275
Update src/app/core/duplicate-records/duplicate-records.service.ts
sleidig Nov 6, 2023
b223d64
Update src/app/core/common-components/entity-subrecord/entity-subreco…
sleidig Nov 6, 2023
96046bc
simplify the output interface further
sleidig Nov 6, 2023
2e2421a
move into subfolder
sleidig Nov 6, 2023
487ad88
do not add "Copy of" to the id
sleidig Nov 6, 2023
e34fa87
feat(*): allow to export only displayed, filtered data (#2059)
brajesh-lab Nov 6, 2023
2159938
UI simplification
sleidig Nov 6, 2023
0cf20be
Merge remote-tracking branch 'origin/master' into duplicate-a-record
sleidig Nov 6, 2023
67fa871
keep selection across tab changes
sleidig Nov 6, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
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)"></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 +134,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[]>();
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"
(selectedRecordsChange)="selectedRows = $event"
[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 = [];
}
}
Loading