Skip to content

Commit

Permalink
Merge pull request #279 from Service-Soft/fix-unsaved-changes-guard-w…
Browse files Browse the repository at this point in the history
…ith-dialogs

fixed unsaved changes guard, refactored table and form
  • Loading branch information
tim-fabian authored Jun 10, 2024
2 parents a233b61 + 157a92e commit 7244ec8
Show file tree
Hide file tree
Showing 78 changed files with 1,547 additions and 1,186 deletions.
27 changes: 24 additions & 3 deletions api-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
/* eslint-disable jsdoc/require-jsdoc */
import { DateFilterFn } from '@angular/material/datepicker';

import { TestEntityWithoutCustomPropertiesInterface } from './projects/ngx-material-entity/src/mocks/test-entity.interface';
import { HasManyEntity, TestEntityWithoutCustomPropertiesInterface } from './projects/ngx-material-entity/src/mocks/test-entity.interface';

function getDatesBetween(
startDate: Date,
endDate: Date,
filter?: DateFilterFn<Date>
): Date[] {

const DAY_IN_MS: number = 1000 * 60 * 60 * 24;
const res: Date[] = [];
while (
Expand Down Expand Up @@ -208,7 +207,18 @@ const testEntityData: TestEntityWithoutCustomPropertiesInterface = {
],
referencesManyIds: ['1'],
randomValue: '42',
notDecoratedValue: '42'
notDecoratedValue: '42',
referencesOneId: '1',
hasManyValues: [
{
id: '1',
stringValue: 'test string value #1'
},
{
id: '2',
stringValue: 'test string value #2'
}
]
};

interface Address {
Expand Down Expand Up @@ -281,11 +291,22 @@ const addressData: Address = {

export interface ApiData {
testEntities: TestEntityWithoutCustomPropertiesInterface[],
hasManyEntities: HasManyEntity[],
persons: Person[],
addresses: Address[]
}
export const apiData: ApiData = {
testEntities: [testEntityData],
hasManyEntities: [
{
id: '1',
stringValue: 'test string value #1'
},
{
id: '2',
stringValue: 'test string value #2'
}
],
persons: [
personData,
personData,
Expand Down
Binary file added cypress/downloads/downloads.html
Binary file not shown.
3 changes: 3 additions & 0 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ Cypress.Commands.add(
cy.get('input[type=file]').eq(i).selectFile('@testImage', { force: true });
}

cy.getInputByLabel('References One Value').click();
cy.get('mat-option').contains('Referenced Entity #1').click();

cy.getInputByLabel('Select').click();
cy.get('mat-option').contains('#1: String Value').click();
cy.get('button').filter((i, elt) => elt.innerText === 'Add').eq(4).click();
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "ngx-material-entity",
"version": "0.0.0",
"scripts": {
"ci": "run-s test lint e2e:run",
"start": "npm run stack",
"stack": "run-s clear:dist stack:only",
"showcase": "ng serve --host 0.0.0.0 ngx-material-entity-showcase",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { Entity, NgxMatEntityBaseDisplayColumnValueComponent } from 'ngx-materia
templateUrl: './pdf-download-display-value.component.html',
styleUrls: ['./pdf-download-display-value.component.scss']
})
// eslint-disable-next-line typescript/no-explicit-any
export class PdfDownloadDisplayValueComponent extends NgxMatEntityBaseDisplayColumnValueComponent<any> {

export class PdfDownloadDisplayValueComponent<T extends Entity> extends NgxMatEntityBaseDisplayColumnValueComponent<T> {
logToConsole(): void {
console.log('Clicked on the pdf column of the entity', (this.entity as Entity).id);
console.log('Clicked on the pdf column of the entity', this.entity.id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export class Person extends Entity {
@hasMany({
tableData: {
baseData: {
title: 'Addresses',
title: 'Has Many Addresses',
displayColumns: [
{
displayName: 'Street',
Expand All @@ -209,7 +209,7 @@ export class Person extends Entity {
createData: {}
},
RelatedEntityServiceClass: PersonService,
displayName: 'Addresses',
displayName: 'Has Many Addresses',
position: {
tab: 2,
tabName: 'Addresses'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { customTableData, customTableDataReadOnly, defaultTableData, defaultTabl
import { TestEntity } from '../../../../../ngx-material-entity/src/mocks/test-entity.mock';

@Component({

selector: 'app-showcase-table',
templateUrl: './showcase-table.component.html',
styleUrls: ['./showcase-table.component.scss'],
Expand Down
35 changes: 32 additions & 3 deletions projects/ngx-material-entity-showcase/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
import { provideHttpClient } from '@angular/common/http';
import { enableProdMode } from '@angular/core';
import { HttpClient, provideHttpClient } from '@angular/common/http';
import { EnvironmentInjector, Injectable, enableProdMode } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatDateFormats } from '@angular/material/core';
import { MomentDateAdapter } from '@angular/material-moment-adapter';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter } from '@angular/router';
import { NGX_GLOBAL_DEFAULT_VALUES, NgxGlobalDefaultValues } from 'ngx-material-entity';
import { EntityService, NGX_GLOBAL_DEFAULT_VALUES, NgxGlobalDefaultValues } from 'ngx-material-entity';

import { AppComponent } from './app/app.component';
import { routes } from './app/routes';
import { environment } from './environments/environment';
import { HasManyEntityService as ImportedHasManyEntityService } from '../../ngx-material-entity/src/mocks/has-many-entity.service.mock';
import { HasManyEntity, TestEntityWithoutCustomProperties } from '../../ngx-material-entity/src/mocks/test-entity.interface';
import { TestEntityService as ImportedTestEntityService } from '../../ngx-material-entity/src/mocks/test-entity.service.mock';

@Injectable({ providedIn: 'root' })
export class TestEntityService extends EntityService<TestEntityWithoutCustomProperties> {
baseUrl: string = 'http://localhost:3000/testEntities';

constructor(http: HttpClient, injector: EnvironmentInjector) {
super(http, injector);
}
}

@Injectable({ providedIn: 'root' })
export class HasManyEntityService extends EntityService<HasManyEntity> {
baseUrl: string = 'http://localhost:3000/hasManyEntities';

constructor(http: HttpClient, injector: EnvironmentInjector) {
super(http, injector);
}
}

const DateFormats: MatDateFormats = {
parse: {
Expand Down Expand Up @@ -51,6 +72,14 @@ bootstrapApplication(
{
provide: NGX_GLOBAL_DEFAULT_VALUES,
useValue: NgxEntityDefaults
},
{
provide: ImportedHasManyEntityService,
useExisting: HasManyEntityService
},
{
provide: ImportedTestEntityService,
useExisting: TestEntityService
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion projects/ngx-material-entity/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ngx-material-entity",
"version": "18.0.1",
"version": "18.1.0",
"license": "MIT",
"keywords": [
"angular",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
.checkbox-wrapper {
min-height: 50px;
display: flex;

>mat-checkbox{
align-self: center;
}
}
mat-dialog-actions {
justify-content: space-between;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NgFor, NgIf } from '@angular/common';
import { CommonModule } from '@angular/common';
import { Component, Inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
Expand All @@ -17,11 +17,10 @@ import { NGX_COMPLETE_GLOBAL_DEFAULT_VALUES, NgxGlobalDefaultValues } from '../.
@Component({
selector: 'ngx-mat-entity-confirm-dialog',
templateUrl: './confirm-dialog.component.html',
styleUrls: ['./confirm-dialog.component.scss'],
styleUrls: ['./confirm-dialog.component.scss', '../../scss/dialog-styles.scss'],
standalone: true,
imports: [
NgIf,
NgFor,
CommonModule,
MatDialogModule,
FormsModule,
MatCheckboxModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Location, NgFor, NgIf } from '@angular/common';
import { CommonModule, Location } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, ElementRef, EnvironmentInjector, HostListener, Inject, InjectionToken, OnInit, Renderer2, runInInjectionContext } from '@angular/core';
import { MatBadgeModule } from '@angular/material/badge';
Expand All @@ -11,7 +11,7 @@ import { PageCreateDataBuilder, PageCreateDataInternal } from './page-create-dat
import { BaseEntityType, EntityClassNewable } from '../../classes/entity.model';
import { PropertyDecoratorConfigInternal } from '../../decorators/base/property-decorator-internal.data';
import { LodashUtilities } from '../../encapsulation/lodash.utilities';
import { getValidationErrorsTooltipContent } from '../../functions/get-validation-errors-tooltip-content.function.ts';
import { getValidationErrorsTooltipContent } from '../../functions/get-validation-errors-tooltip-content.function';
import { NGX_COMPLETE_GLOBAL_DEFAULT_VALUES, NgxGlobalDefaultValues } from '../../global-configuration-values';
import { EntityService } from '../../services/entity.service';
import { UnsavedChangesPage } from '../../services/unsaved-changes.guard';
Expand Down Expand Up @@ -81,8 +81,7 @@ export const NGX_CREATE_DATA: InjectionToken<PageCreateData<any>> = new Injectio
styleUrls: ['./create-page.component.scss'],
standalone: true,
imports: [
NgIf,
NgFor,
CommonModule,
MatButtonModule,
MatProgressSpinnerModule,
MatBadgeModule,
Expand Down Expand Up @@ -211,7 +210,7 @@ export class NgxMatEntityCreatePageComponent<EntityType extends BaseEntityType<E
*/
async checkEntity(): Promise<void> {
await this.checkIsEntityValid();
this.isEntityDirty = await EntityUtilities.isDirty(this.entity, this.entityPriorChanges, this.http);
this.isEntityDirty = await EntityUtilities.isDirty(this.entity, this.entityPriorChanges, this.http, this.injector);
}

private async checkIsEntityValid(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { defaultDynamicStyleClasses } from '../../functions/default-style-classes.function';
import { defaultTrue } from '../../functions/default-true.function';
import { getConfigValue } from '../../functions/get-config-value.function';
import { NgxGlobalDefaultValues } from '../../global-configuration-values';
import { DisplayColumn, DynamicStyleClasses } from '../table/table-data';
import { defaultSearchFunction } from '../table/table-data.builder';

/**
* Configuration for the custom table.
*/
export interface CustomTableConfiguration<T = unknown> {
/**
* Configuration for css classes that should be applied to table rows based on a condition.
* This could be used to eg. Set the background color to green when an item has the status completed etc.
* INFO: You need to use ng-deep or apply the styling in the styles.scss.
* @default () => []
*/
dynamicRowStyleClasses?: DynamicStyleClasses<T>,
/**
* The columns to display inside the custom table.
*/
displayColumns: DisplayColumn<T>[],
/**
* Whether or not a loading spinner should be shown.
* Only works when you provide the "loading" input to the custom table.
* @default true
*/
displayLoadingSpinner?: boolean,
/**
* Whether or not the select column should be enabled.
* @default true
*/
withSelection: boolean,
/**
* A function that resolves a row value to a string that can be searched for.
* By default this uses a built in search filter.
*/
searchStringForRow?: (value: T) => string,
/**
* Whether or not the user is allowed to click on entries in the custom table.
* @default () => true
*/
allowClick?: (value: T) => boolean,
/**
* An error message to show when the table contains no entries.
* Also applies styling to the table.
*/
emptyErrorMessage?: string,
/**
* Resolves the given id for an entity.
* This is used for the referencesMany input,
* where you want to show values of the entity in the table rows and not the id.
*/
resolveToReferencedEntity?: (id: unknown) => unknown
}

/**
* Internal configuration for the custom table.
* Provides default values.
*/
export class InternalCustomTableConfiguration<
T = unknown
>implements CustomTableConfiguration<T> {
// eslint-disable-next-line jsdoc/require-jsdoc
dynamicRowStyleClasses: DynamicStyleClasses<T>;
// eslint-disable-next-line jsdoc/require-jsdoc
displayColumns: DisplayColumn<T>[];
// eslint-disable-next-line jsdoc/require-jsdoc
displayLoadingSpinner: boolean;
// eslint-disable-next-line jsdoc/require-jsdoc
withSelection: boolean;
// eslint-disable-next-line jsdoc/require-jsdoc
searchStringForRow: (value: T) => string;
// eslint-disable-next-line jsdoc/require-jsdoc
allowClick: (value: T) => boolean;
// eslint-disable-next-line jsdoc/require-jsdoc
emptyErrorMessage: string;
// eslint-disable-next-line jsdoc/require-jsdoc
resolveToReferencedEntity?: (id: unknown) => unknown;

constructor(globalConfig: NgxGlobalDefaultValues, configuration: CustomTableConfiguration<T>) {
this.dynamicRowStyleClasses = configuration.dynamicRowStyleClasses ?? defaultDynamicStyleClasses;
this.displayColumns = configuration.displayColumns;
this.displayLoadingSpinner = configuration.displayLoadingSpinner ?? true;
this.withSelection = configuration.withSelection;
this.searchStringForRow = configuration.searchStringForRow ?? defaultSearchFunction;
this.allowClick = configuration.allowClick ?? defaultTrue;
this.emptyErrorMessage = getConfigValue(globalConfig.emptyArrayErrorMessage, configuration.emptyErrorMessage);
this.resolveToReferencedEntity = configuration.resolveToReferencedEntity;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!-- eslint-disable angular/no-call-expression -->
<mat-table matSort [dataSource]="dataSource" [class.table-with-error-slot]="shouldShowMissingError">
<!-- select Column -->
<ng-container matColumnDef="select">
<mat-header-cell *matHeaderCellDef>
<mat-checkbox [checked]="selection.hasValue() && SelectionUtilities.isAllSelected(selection, dataSource)"
[indeterminate]="selection.hasValue() && !SelectionUtilities.isAllSelected(selection, dataSource)"
(change)="masterToggle($event)">
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let value" [dynamicStyleClasses]="internalConfiguration.dynamicRowStyleClasses" [value]="value">
<mat-checkbox [checked]="selection.isSelected(value)"
(click)="$event.stopPropagation()"
(change)="toggle(value, $event)">
</mat-checkbox>
</mat-cell>
</ng-container>

@for (dCol of internalConfiguration.displayColumns; track $index) {
<ng-container [matColumnDef]="dCol.displayName">
<mat-header-cell *matHeaderCellDef mat-sort-header>
{{dCol.displayName}}
</mat-header-cell>
<mat-cell *matCellDef="let value" [class.enabled]="!dCol.disableClick && internalConfiguration.allowClick(value)"
[dynamicStyleClasses]="internalConfiguration.dynamicRowStyleClasses" [value]="value"
(click)="clickCell(value, dCol)"
>
@if (dCol.Component) {
<display-column-value [entity]="value" [ComponentClass]="dCol.Component"></display-column-value>
}
@else {
{{getDisplayColumnValue(value, dCol)}}
}
</mat-cell>
</ng-container>
}

<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>

@if (internalConfiguration.displayLoadingSpinner && isLoading) {
<mat-spinner>
</mat-spinner>
}
@else if (shouldShowMissingError && !dataSource.data.length) {
<div class="table-error">{{internalConfiguration.emptyErrorMessage}}</div>
}

<mat-paginator [length]="dataSource.filteredData.length" [pageIndex]="0" [pageSize]="10" [pageSizeOptions]="[5, 10, 25, 50]"></mat-paginator>
Loading

0 comments on commit 7244ec8

Please sign in to comment.