diff --git a/.commit b/.commit index 0d3f5f6f0..f03d2007a 100644 --- a/.commit +++ b/.commit @@ -1 +1 @@ -a389dcd952875866288789039304ce0360b6d655 +d7376dc1bc7c9b8080948907c5bb14befb21039c diff --git a/README.md b/README.md index b2fd4b9e9..c770a7c04 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,16 @@ ## Change log +### August 12, 2024 +- 459770: Fixed an issue where the request parameter validation result was not working on first input. +- 462048: Fixed an issue with delegation search caching. +- 461659: Fixed an issue regarding delegation or ESet requests on the Request History page. +- 460151: Fixed an issue with the QueryWhereClause evaluation during the value check on updated parameter. +- 460511: Fixed an issue that causes the side navigation on New Request to show search results twice. +- 461414: Fixed the scrollability of the Operation Support Portal's Process view +- 462785: Fixed an UI issue regarding the alignment for the column "User account is disabled" and its data. + + ### July 30, 2024 The v92 branch has been updated with fixes for the following issues. diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk.component.html b/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk.component.html index ab2f7f6a4..34dd8394a 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk.component.html +++ b/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk.component.html @@ -1,66 +1,52 @@ - + {{ columnContainer?.display | translate }} - ({{ '#LDS#Table: {0}' | translate | ldsReplace : metadataProvider.tables[selectedTable.TableName]?.DisplaySingular || selectedTable.TableName }}) + ({{ '#LDS#Table: {0}' | translate | ldsReplace : metadataProvider.tables[selectedTable.TableName]?.DisplaySingular + || selectedTable.TableName }}) - - - -
-
-
{{ getDisplay(candidate) }}
-
- {{ candidate.displayLong }} + [required]="columnContainer.isValueRequired" /> + + + +
+
+
{{ getDisplay(candidate) }}
+
+ {{ candidate.displayLong }} +
-
- - {{ '#LDS#Loading...' | translate }} + + + {{ '#LDS#Loading...' | translate }}
- + [attr.data-imx-identifier]="'cdr-edit-fk-button-remove-assignment-' + columnContainer?.name">
-
- {{ '#LDS#The value entered in the {0} box could not be found. Please select a value from the list.' | translate | ldsReplace : (columnContainer?.display | translate) }} + {{ '#LDS#The value entered in the {0} box could not be found. Please select a value from the list.' | translate | + ldsReplace : (columnContainer?.display | translate) }} {{ '#LDS#This field is mandatory.' | translate }} @@ -68,18 +54,10 @@ - + - + - + \ No newline at end of file diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk.component.ts index da2851217..0a027be34 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk.component.ts @@ -111,7 +111,15 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI /** * A list of possible candidates, that can be selected. */ - public candidates: Candidate[]; + private _candidates: Candidate[]; + + public get candidates(): Candidate[] { + return this._candidates; + } + + public set candidates(value: Candidate[]) { + this._candidates = value; + } /** * Indicator that the component is loading data from the server. @@ -211,7 +219,7 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI public async onOpened(): Promise { // Use the stashed values if we already have a selected value this.parameters = this.savedParameters ?? { PageSize: this.pageSize, StartIndex: 0 }; - if ((this.savedCandidates?.length ?? 0) > 0) { + if (!!this.savedCandidates?.length) { this.candidates = this.savedCandidates; } else if (this.parameters.search || this.parameters.filter || this.control.value == null) { await this.updateCandidates({ search: undefined, filter: undefined, StartIndex: 0 }, false); @@ -485,10 +493,10 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI const multipleFkRelations = this.columnContainer.fkRelations && this.columnContainer.fkRelations.length > 1; const identityRelatedTable = this.selectedTable.TableName === 'Person'; - const newCandidates = candidateCollection.Entities.map((entityData) => { - let key: string = null; - let detailValue: string = entityData.LongDisplay; - const defaultEmailColumn = entityData.Columns['DefaultEmailAddress']; + const newCandidates = candidateCollection.Entities?.map((entityData) => { + let key: string = ''; + let detailValue: string = entityData.LongDisplay ?? ''; + const defaultEmailColumn = entityData.Columns?.['DefaultEmailAddress']; /** * If the candidates data relate to identities (fkRelation Person table) * then we want to use the email address for the detail line (displayLong) @@ -498,7 +506,7 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI } if (multipleFkRelations) { this.logger.trace(this, 'dynamic foreign key'); - const xObjectKeyColumn = entityData.Columns['XObjectKey']; + const xObjectKeyColumn = entityData.Columns?.['XObjectKey']; key = xObjectKeyColumn ? xObjectKeyColumn.Value : undefined; } else { this.logger.trace(this, 'foreign key'); @@ -510,7 +518,7 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI } else { this.logger.trace(this, 'Use the primary key'); const keys = entityData.Keys; - key = keys && keys.length ? keys[0] : undefined; + key = keys && keys.length ? keys[0] : ''; } } return { @@ -520,9 +528,11 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI }; }); if (concatCandidates) { - this.candidates.push(...newCandidates); + this.candidates.push(...(newCandidates || [])); + this.savedCandidates = this.candidates; } else { - this.candidates = newCandidates; + this.candidates = newCandidates || []; + this.savedCandidates = this.candidates; } } finally { this.loading = false; diff --git a/imxweb/projects/qbm/src/lib/cdr/editor-base.ts b/imxweb/projects/qbm/src/lib/cdr/editor-base.ts index ec30030e6..47e9483bc 100644 --- a/imxweb/projects/qbm/src/lib/cdr/editor-base.ts +++ b/imxweb/projects/qbm/src/lib/cdr/editor-base.ts @@ -24,7 +24,7 @@ * */ import { OnDestroy, Component, EventEmitter, ErrorHandler } from '@angular/core'; -import { AbstractControl, Validators } from '@angular/forms'; +import { AbstractControl, ValidatorFn, Validators } from '@angular/forms'; import { Subject, Subscription } from 'rxjs'; import { CdrEditor, ValueHasChangedEventArg } from './cdr-editor.interface'; @@ -73,7 +73,7 @@ export abstract class EditorBase implements CdrEditor, OnDestroy { * @ignore * Used for the template and displays the last server error, that occured while loading content. */ - public lastError: ServerError; + public lastError: ServerError | undefined; /** * The maximal length a string could have. @@ -99,21 +99,20 @@ export abstract class EditorBase implements CdrEditor, OnDestroy { * If an error occured, it returns its message */ public get validationErrorMessage(): string { - if (this.control.errors?.['generalError']) { - return this.lastError.toString(); - } - return undefined; + return this.lastError?.toString() || ''; } /** * Binds a column dependent reference to the component, by setting the control value and subscribing to the events, - * the CDR or the ColumnContainer emits + * the CDR or the ColumnContainer emits * @param cdref a column dependent reference */ public bind(cdref: ColumnDependentReference): void { if (cdref && cdref.column) { this.columnContainer.init(cdref); + this.control.addValidators(EditorBase.hasServerError(this)); + this.setControlValue(); this.subscribers.push(this.control.valueChanges.subscribe(async (value) => this.writeValue(value))); @@ -133,7 +132,7 @@ export abstract class EditorBase implements CdrEditor, OnDestroy { return; } - if (this.control.value !== this.columnContainer.value) { + if (!this.control.hasError('generalError') && this.control.value !== this.columnContainer.value) { this.logger.trace( this, `Control (${this.columnContainer.name}) set to new value:`, @@ -150,8 +149,16 @@ export abstract class EditorBase implements CdrEditor, OnDestroy { this.updateRequested.subscribe(() => { setTimeout(() => { try { - this.setControlValue(); - this.control.updateValueAndValidity({ onlySelf: true, emitEvent: false }); + if (!this.control.hasError('generalError') && this.control.value !== this.columnContainer.value) { + this.logger.trace( + this, + `Control (${this.columnContainer.name}) set to new value:`, + this.columnContainer.value, + this.control.value + ); + this.setControlValue(); + this.control.updateValueAndValidity({ onlySelf: true, emitEvent: false }); + } } finally { } this.valueHasChanged.emit({ value: this.control.value }); @@ -176,9 +183,9 @@ export abstract class EditorBase implements CdrEditor, OnDestroy { this.columnContainer.type !== ValType.Bool // because bool is always valid ) { this.logger.debug(this, `A value for column "${this.columnContainer.name}" is required`); - this.control.setValidators(Validators.required); + this.control.setValidators([Validators.required, EditorBase.hasServerError(this)]); } else { - this.control.setValidators(null); + this.control.setValidators(EditorBase.hasServerError(this)); } } @@ -187,11 +194,10 @@ export abstract class EditorBase implements CdrEditor, OnDestroy { * @param value the new value */ private async writeValue(value: any): Promise { - if (this.control.errors) { - this.logger.debug(this, 'writeValue - validation failed'); + if (this.control.errors && Object.keys(this.control.errors).some((elem) => elem !== 'generalError')) { + this.logger.debug(this, 'writeValue - client validation failed'); return; } - this.logger.debug(this, 'writeValue called with value', value); if (!this.columnContainer.canEdit || this.columnContainer.value === value) { @@ -203,10 +209,11 @@ export abstract class EditorBase implements CdrEditor, OnDestroy { try { this.logger.debug(this, 'writeValue - PutValue...'); await this.columnContainer.updateValue(value); + this.lastError = undefined; } catch (e) { this.lastError = e; this.logger.error(this, e); - this.control.setErrors({ generalError: true }); + this.control.updateValueAndValidity({ emitEvent: true }); } finally { this.isBusy = false; this.isWriting = false; @@ -218,4 +225,10 @@ export abstract class EditorBase implements CdrEditor, OnDestroy { this.valueHasChanged.emit({ value, forceEmit: true }); } + + private static hasServerError(base: any): ValidatorFn { + return (_: AbstractControl): { [key: string]: boolean } | null => { + return !base.lastError ? null : { generalError: true }; + }; + } } diff --git a/imxweb/projects/qbm/src/lib/cdr/entity-column-container.ts b/imxweb/projects/qbm/src/lib/cdr/entity-column-container.ts index ead85afc8..e279c5054 100644 --- a/imxweb/projects/qbm/src/lib/cdr/entity-column-container.ts +++ b/imxweb/projects/qbm/src/lib/cdr/entity-column-container.ts @@ -24,11 +24,11 @@ * */ +import { IForeignKeyInfo, IValueMetadata, ValType, ValueConstraint, ValueStruct } from 'imx-qbm-dbts'; +import { Subscription } from 'rxjs'; +import { ValueWrapper } from '../value-wrapper/value-wrapper'; import { ColumnDependentReference } from './column-dependent-reference.interface'; -import { ValueStruct, IForeignKeyInfo, ValType, ValueConstraint, IValueMetadata } from 'imx-qbm-dbts'; import { LimitedValuesContainer } from './limited-values-container'; -import { ValueWrapper } from '../value-wrapper/value-wrapper'; -import { Subscription } from 'rxjs'; /** * An implementation of the {@link ValueWrapper} interface. @@ -113,6 +113,10 @@ export class EntityColumnContainer implements ValueWrapper { return this.cdr?.hint; } + public get showDisplayValue(): boolean { + return !!this.metaData?.CanSee(); + } + /** * An container, that wraps limited value functionality */ @@ -141,7 +145,6 @@ export class EntityColumnContainer implements ValueWrapper { return new Subscription(() => subscription?.unsubscribe()); } - /** * Updates the value and puts it into the column * @param value The new value for the column diff --git a/imxweb/projects/qbm/src/lib/cdr/view-property/view-property.component.html b/imxweb/projects/qbm/src/lib/cdr/view-property/view-property.component.html index 463125a13..c287e3d5a 100644 --- a/imxweb/projects/qbm/src/lib/cdr/view-property/view-property.component.html +++ b/imxweb/projects/qbm/src/lib/cdr/view-property/view-property.component.html @@ -1,4 +1,4 @@
- {{ (columnContainer?.display | translate) + (columnContainer.isValueRequired ? '*' :'') }} - {{ displayedValue | translate }} + {{ (columnContainer?.display | translate) + (columnContainer.isValueRequired ? '*' : '') }} + {{ columnContainer.showDisplayValue ? (displayedValue | translate) : '' }}
diff --git a/imxweb/projects/qbm/src/lib/data-source-toolbar/data-source-toolbar.component.ts b/imxweb/projects/qbm/src/lib/data-source-toolbar/data-source-toolbar.component.ts index 8e2e629c6..bb8c9f943 100644 --- a/imxweb/projects/qbm/src/lib/data-source-toolbar/data-source-toolbar.component.ts +++ b/imxweb/projects/qbm/src/lib/data-source-toolbar/data-source-toolbar.component.ts @@ -1313,18 +1313,18 @@ export class DataSourceToolbarComponent implements OnChanges, OnInit, OnDestroy this.columnOptions?.resetView(); this.searchTerms = []; - if (this.settings.navigationState.search) { + if (this.settings.navigationState?.search) { this.searchControl.reset(null); delete this.settings.navigationState.search; } - if (this.settings.navigationState.OrderBy) { + if (this.settings.navigationState?.OrderBy) { delete this.settings.navigationState.OrderBy; this.clearSort(false); } if (this.settings?.groupData?.currentGrouping) { this.clearGroupedBy(false); } - delete this.settings.navigationState.filter; + delete this.settings.navigationState?.filter; if (this.filtersCurrentlyApplied) { this.clearFilters(false); } diff --git a/imxweb/projects/qbm/src/lib/sidenav-tree/sidenav-tree-dynamic-extension.ts b/imxweb/projects/qbm/src/lib/sidenav-tree/sidenav-tree-dynamic-extension.ts index 028b4676e..1abd43006 100644 --- a/imxweb/projects/qbm/src/lib/sidenav-tree/sidenav-tree-dynamic-extension.ts +++ b/imxweb/projects/qbm/src/lib/sidenav-tree/sidenav-tree-dynamic-extension.ts @@ -36,6 +36,7 @@ export class DynamicDataApiControls { getChildren: (node: T, onlyCheck?: boolean) => Promise; loadMore?: (root: T) => Promise; search?: (params: CollectionLoadParameters) => Promise<{ searchNodes: T[]; totalCount?: number; rootNode?: T }>; + abortSearch?: () => void; searchLoadMore?: (params: CollectionLoadParameters) => Promise; changeSelection?: (data: T[], selectedNode: T) => T[]; } @@ -87,11 +88,10 @@ export class DynamicDataSource { if (expandRoot) { this._treeControl.expand(this.rootNode); await this.toggleNode(this.rootNode, true); - this.dataChange.next(this.data); + this.nextData(this.data); } } finally { this.currentCount = this.data.length - 1; // Minus 1 to account for rootnode - this.initializingData = false; } } @@ -119,6 +119,9 @@ export class DynamicDataSource { } public async onSearch(): Promise { + if (!this._apiControls.search || !this._apiControls.changeSelection) { + throw Error('No remote search functionality has been defined.'); + } this.initializingData = true; this.dstSettings.navigationState.StartIndex = 0; @@ -130,32 +133,41 @@ export class DynamicDataSource { currentCount: this.currentCount, }; } - try { - if (!this.isSearch && this.cachedData) { - // If we are not searching, and there is cached data, load cache and remove old data - this.totalCount = this.cachedData.totalCount; - this.currentCount = this.cachedData.currentCount; - this.rootNode = this.cachedData.data?.[0]; - this.dataChange.next(this.cachedData.data); - this.cachedData = null; + if (!this.isSearch && !!this.cachedData) { + // If we are not searching, and there is cached data, load cache and remove old data + this.totalCount = this.cachedData.totalCount; + this.currentCount = this.cachedData.currentCount; + this.rootNode = this.cachedData.data?.[0]; + this.nextData(this.cachedData.data); + if (this._apiControls.abortSearch) this._apiControls?.abortSearch(); + this.cachedData = null; + } else { + // Proceed with normal search + const response = await this._apiControls.search(this.dstSettings.navigationState); + if (!response) { + // Here the search was aborted, so we return nothing + return; + } + this.totalCount = response?.totalCount; + const nodes = this._apiControls.changeSelection(response.searchNodes, this.selectedNode); + this.currentCount = nodes.length; + if (nodes.length > 0) { + this.rootNode = response.rootNode || this.rootNode; + this.nextData([this.rootNode, ...nodes]); + this._treeControl.expand(this.rootNode); } else { - // Proceed with normal search - const response = await this._apiControls.search(this.dstSettings.navigationState); - this.totalCount = response?.totalCount; - const nodes = this._apiControls.changeSelection(response.searchNodes, this.selectedNode); - this.currentCount = nodes.length; - if (nodes.length > 0) { - this.rootNode = response.rootNode || this.rootNode; - this.dataChange.next([this.rootNode, ...nodes]); - this._treeControl.expand(this.rootNode); - } else { - this.dataChange.next(nodes); - } + this.nextData(nodes); } - } finally { - this.initializingData = false; } } + /** + * Emits data and turns off the loading spinner + * @param data that will be emitted as the next data state + */ + private nextData(data: T[]): void { + this.dataChange.next(data); + this.initializingData = false; + } constructor(private _treeControl: FlatTreeControl, private _apiControls: DynamicDataApiControls) {} diff --git a/imxweb/projects/qbm/src/lib/sidenav-tree/sidenav-tree.component.html b/imxweb/projects/qbm/src/lib/sidenav-tree/sidenav-tree.component.html index 55c8f6e42..0c7ae3a93 100644 --- a/imxweb/projects/qbm/src/lib/sidenav-tree/sidenav-tree.component.html +++ b/imxweb/projects/qbm/src/lib/sidenav-tree/sidenav-tree.component.html @@ -11,7 +11,7 @@

{{ headerText }}