diff --git a/.commit b/.commit index d57de0d14..548d1eea8 100644 --- a/.commit +++ b/.commit @@ -1 +1 @@ -f441fc015e629f5e8987c3163af6a85af6293484 +d0e5493b4e0f1ce5d3d1a9a84202243999a67791 diff --git a/README.md b/README.md index 05950b409..68c1d9778 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ ## Change log +### October 20, 2023 + +- The repository has been updated with the source code for the Identity Manager 9.2 release in the `v92` branch. + For information about new features and enhancements in this version, please refer to the Identity Manager 9.2 Release Notes. + ### October 15, 2022 - There is a new application in the workspace called `custom-app`. This application is a template that provides the basic building blocks (such as Material integration, session handling, login, and the API client configuration) can be used as a starting point for building new applications. See [`readme.md`](./imxweb/projects/custom-app/readme.md) for more information. @@ -34,6 +39,8 @@ This repository contains the source code for the HTML5 applications contained in It is a monorepo containing the Angular [workspace](https://angular.io/guide/workspace-config), which consists of apps and [libraries](https://angular.io/guide/libraries). +We strongly recommend to read the [HTML Development Guide](https://support.oneidentity.com/technical-documents/identity-manager/9.2/html5-development-guide) before starting to work with the code in this repository. + By forking this repository, you may create customized versions of the projects and add them to your Identity Manager deployment. ## Workspace overview @@ -60,9 +67,9 @@ Each Angular library and app belongs to a folder in the `projects` directory. Th |`olg`|Angular plugin library|`qbm`, `qer`| |`pol`|Angular plugin library|`qbm`, `qer`| -Each Angular library belongs to the Identity Manager module of the same name. +Each Angular library belongs to the Identity Manager module of the same name. You do not need to build Angular libraries for modules that are not part of your Identity Manager installation. -A (non-plugin) library acts like a regular compile-time dependency. A _plugin_ library is loaded dynamically at runtime, as determined by the plugins' `imx-plugin-config.json` files. +A (non-plugin) library acts like a regular compile-time dependency. A _plugin_ library is loaded dynamically at runtime, as determined by each plugin's `imx-plugin-config.json` file. For more information about each project, see the `readme.md` files in each project's folder. @@ -76,9 +83,9 @@ For more information about each project, see the `readme.md` files in each proje |`qer-app-pwdportal`|Password Reset Portal|Angular app|`qbm`, `qer`| |`custom-app`|Template for custom applications|Angular app|`qbm`| -## Installing node.js +## Installing Node.js -Verify that you have installed the correct `node.js` version for your branch. The version used by the CI build is defined in the [`.github/workflows/npm-build.yml`](.github/workflows/npm-build.yml) file in the `node-version` property. Other versions of `node.js`, including newer versions, are not guaranteed to be compatible with other Angular versions. +Verify that you have installed a compatible `node.js` version for your branch. The version used by the CI build is defined in the [`.github/workflows/npm-build.yml`](.github/workflows/npm-build.yml) file in the `node-version` property. Other versions of `node.js`, including newer versions, are not guaranteed to be compatible with other Angular versions. Please see the [version compatibility table](https://angular.io/guide/versions) on the official Angular site. ## Building @@ -107,7 +114,7 @@ imxclient.exe run-apiserver -B The web apps will connect to the API Server using the URL defined in the application's `environment.ts` file. The default setting is `http://localhost:8182` which is the default URL that a local API Server will run on. -## More information +## Getting started Please refer to the [HTML Development Guide](https://support.oneidentity.com/technical-documents/identity-manager/8.2.1/html5-development-guide#TOPIC-1801966) for step-by-step instructions on getting started. @@ -115,15 +122,23 @@ Please refer to the [HTML Development Guide](https://support.oneidentity.com/tec The following table shows the branches in this repository corresponding to each product version. -|Branch|Product version|`node.js` version| +|Branch|Product version|Angular version| |-|-|-| -|`v91`|Identity Manager 9.1.x|14| -|`v90`|Identity Manager 9.0|14| -|`v82`|Identity Manager 8.2.x|14| +|`v92`|Identity Manager 9.2.x|14| +|`v91`|Identity Manager 9.1.x|13| +|`v90`|Identity Manager 9.0|13| +|`v82`|Identity Manager 8.2.x|11| |`master`|The `master` branch does not correspond to a supported version of Identity Manager. Do not use this branch for development purposes.|| +Please also see the [version compatibility table](https://angular.io/guide/versions). + We plan to push updates for each minor and major product release, allowing developers to track source code changes from one version to the next. Occasionally we may also publish important bug fixes. +## Step-by-step guides + +- [Theming guide](./imxweb/custom-theme/readme.md) +- [CDR guide](./imxweb/projects/qbm/src/lib/cdr/Readme.md) + ## Contributing We welcome and appreciate contributions. Here's how you can open a pull request to submit code changes. diff --git a/imxweb/custom-theme/readme.md b/imxweb/custom-theme/readme.md index 01f1a28ee..8f40f57f8 100644 --- a/imxweb/custom-theme/readme.md +++ b/imxweb/custom-theme/readme.md @@ -1 +1,41 @@ -Run "npm run build" to compile the SASS file to a CSS file. \ No newline at end of file +# Theming the application + +The HTML5 applications support the integration of **custom themes**. Before proceeding, we recommend that you read the [Angular Material Theming](https://material.angular.io/guide/theming) guide. + +## Compiling a custom theme + +- Change the variables in the file [custom-theme.scss](./custom-theme.scss) as required: + - `$font-family` + - `$primary` + - `$accent` + - `$warn` +- Rename the `.custom-theme` class to your theme's name. (e.g. `.space-theme`) +- In a terminal, change to the `imxweb/custom-theme` folder and run the `npm run build` command +- Take the `custom-theme.css` file and create a .zip file. The naming convention is to use `Html_.zip` (for example `Html_space-theme.zip`). +- Copy the .zip file to the `imxweb` folder. +- Create a folder inside the `imxweb` folder with the name of your .zip file (e.g. `Html_space-theme`). +- Create a `imx-theme-config.json` file inside in this folder. Use this text as a content template, filling in the correct values for your theme. + - `Name`: a unique name and identifier of the theme + - `DisplayName`: a user friendly name for display purposes + - `Class`: the CSS class identifier which is used for theming (e.g. `eui-light-theme` in default) + - `Urls`: a list of all relevant files for this theme (also pictures, icons or other resources which are referenced if required) + +``` json +{ + "Themes": [ + { + "Name": "space-theme", + "Display Name": "Space Theme", + "Class": "space-theme", + "Urls": [ + "../space-theme/custom-theme.css" + ] + } + ] +} +``` +- Upload the .zip file and the `imx-theme-config.json` file with Software Loader like you would with an Angular plugin. +- Restart your API server. +- Login to Web Portal > Click on your username > Select "User Interface Settings" > Change the application's theme to your custom theme. + +*Note*: Multiple theme definition files are possible. Multiple themes can also be declared inside one theme `imx-theme-config.json` file, however every theme needs to be provided as single .zip file. diff --git a/imxweb/imx-modules/imx-api-aad.tgz b/imxweb/imx-modules/imx-api-aad.tgz index 7904a9781..9d483e657 100644 Binary files a/imxweb/imx-modules/imx-api-aad.tgz and b/imxweb/imx-modules/imx-api-aad.tgz differ diff --git a/imxweb/imx-modules/imx-api-aob.tgz b/imxweb/imx-modules/imx-api-aob.tgz index ca93932e5..b455b0306 100644 Binary files a/imxweb/imx-modules/imx-api-aob.tgz and b/imxweb/imx-modules/imx-api-aob.tgz differ diff --git a/imxweb/imx-modules/imx-api-apc.tgz b/imxweb/imx-modules/imx-api-apc.tgz index 33df9d866..b5606c19c 100644 Binary files a/imxweb/imx-modules/imx-api-apc.tgz and b/imxweb/imx-modules/imx-api-apc.tgz differ diff --git a/imxweb/imx-modules/imx-api-att.tgz b/imxweb/imx-modules/imx-api-att.tgz index 79222141f..684976b2a 100644 Binary files a/imxweb/imx-modules/imx-api-att.tgz and b/imxweb/imx-modules/imx-api-att.tgz differ diff --git a/imxweb/imx-modules/imx-api-cpl.tgz b/imxweb/imx-modules/imx-api-cpl.tgz index 84d3f2299..99c4ba84f 100644 Binary files a/imxweb/imx-modules/imx-api-cpl.tgz and b/imxweb/imx-modules/imx-api-cpl.tgz differ diff --git a/imxweb/imx-modules/imx-api-dpr.tgz b/imxweb/imx-modules/imx-api-dpr.tgz index a733c2766..1e8b251fe 100644 Binary files a/imxweb/imx-modules/imx-api-dpr.tgz and b/imxweb/imx-modules/imx-api-dpr.tgz differ diff --git a/imxweb/imx-modules/imx-api-hds.tgz b/imxweb/imx-modules/imx-api-hds.tgz index 8c659a962..efdaf3428 100644 Binary files a/imxweb/imx-modules/imx-api-hds.tgz and b/imxweb/imx-modules/imx-api-hds.tgz differ diff --git a/imxweb/imx-modules/imx-api-o3e.tgz b/imxweb/imx-modules/imx-api-o3e.tgz index 5f553a65f..8d2f7de80 100644 Binary files a/imxweb/imx-modules/imx-api-o3e.tgz and b/imxweb/imx-modules/imx-api-o3e.tgz differ diff --git a/imxweb/imx-modules/imx-api-o3t.tgz b/imxweb/imx-modules/imx-api-o3t.tgz index 78bb4304d..9457e536f 100644 Binary files a/imxweb/imx-modules/imx-api-o3t.tgz and b/imxweb/imx-modules/imx-api-o3t.tgz differ diff --git a/imxweb/imx-modules/imx-api-olg.tgz b/imxweb/imx-modules/imx-api-olg.tgz index 870667f84..002bb8fac 100644 Binary files a/imxweb/imx-modules/imx-api-olg.tgz and b/imxweb/imx-modules/imx-api-olg.tgz differ diff --git a/imxweb/imx-modules/imx-api-pol.tgz b/imxweb/imx-modules/imx-api-pol.tgz index 0e13167fc..4ae641c57 100644 Binary files a/imxweb/imx-modules/imx-api-pol.tgz and b/imxweb/imx-modules/imx-api-pol.tgz differ diff --git a/imxweb/imx-modules/imx-api-qbm.tgz b/imxweb/imx-modules/imx-api-qbm.tgz index eadb5832e..2a8f70543 100644 Binary files a/imxweb/imx-modules/imx-api-qbm.tgz and b/imxweb/imx-modules/imx-api-qbm.tgz differ diff --git a/imxweb/imx-modules/imx-api-qer.tgz b/imxweb/imx-modules/imx-api-qer.tgz index 5be15f0f6..4fbedf08b 100644 Binary files a/imxweb/imx-modules/imx-api-qer.tgz and b/imxweb/imx-modules/imx-api-qer.tgz differ diff --git a/imxweb/imx-modules/imx-api-rmb.tgz b/imxweb/imx-modules/imx-api-rmb.tgz index bfc8dedef..771224aa1 100644 Binary files a/imxweb/imx-modules/imx-api-rmb.tgz and b/imxweb/imx-modules/imx-api-rmb.tgz differ diff --git a/imxweb/imx-modules/imx-api-rms.tgz b/imxweb/imx-modules/imx-api-rms.tgz index bcbc23e51..26d3d739a 100644 Binary files a/imxweb/imx-modules/imx-api-rms.tgz and b/imxweb/imx-modules/imx-api-rms.tgz differ diff --git a/imxweb/imx-modules/imx-api-rps.tgz b/imxweb/imx-modules/imx-api-rps.tgz index dafe69269..2273bfb92 100644 Binary files a/imxweb/imx-modules/imx-api-rps.tgz and b/imxweb/imx-modules/imx-api-rps.tgz differ diff --git a/imxweb/imx-modules/imx-api-sac.tgz b/imxweb/imx-modules/imx-api-sac.tgz index 0c4d34fd6..16d79de4f 100644 Binary files a/imxweb/imx-modules/imx-api-sac.tgz and b/imxweb/imx-modules/imx-api-sac.tgz differ diff --git a/imxweb/imx-modules/imx-api-tsb.tgz b/imxweb/imx-modules/imx-api-tsb.tgz index cc2fc9f2e..45e4b34df 100644 Binary files a/imxweb/imx-modules/imx-api-tsb.tgz and b/imxweb/imx-modules/imx-api-tsb.tgz differ diff --git a/imxweb/imx-modules/imx-api-uci.tgz b/imxweb/imx-modules/imx-api-uci.tgz index 85123afd8..d5874cc1a 100644 Binary files a/imxweb/imx-modules/imx-api-uci.tgz and b/imxweb/imx-modules/imx-api-uci.tgz differ diff --git a/imxweb/imx-modules/imx-api.tgz b/imxweb/imx-modules/imx-api.tgz index 5d7c89a09..1bc15d74d 100644 Binary files a/imxweb/imx-modules/imx-api.tgz and b/imxweb/imx-modules/imx-api.tgz differ diff --git a/imxweb/imx-modules/imx-qbm-dbts.tgz b/imxweb/imx-modules/imx-qbm-dbts.tgz index ba846c411..7b370162e 100644 Binary files a/imxweb/imx-modules/imx-qbm-dbts.tgz and b/imxweb/imx-modules/imx-qbm-dbts.tgz differ diff --git a/imxweb/projects/att/src/lib/policies/attestation-cases/attestation-cases.component.html b/imxweb/projects/att/src/lib/policies/attestation-cases/attestation-cases.component.html index 996f2946a..f45be6adb 100644 --- a/imxweb/projects/att/src/lib/policies/attestation-cases/attestation-cases.component.html +++ b/imxweb/projects/att/src/lib/policies/attestation-cases/attestation-cases.component.html @@ -28,7 +28,7 @@ - + diff --git a/imxweb/projects/att/src/lib/policies/attestation-cases/attestation-cases.component.ts b/imxweb/projects/att/src/lib/policies/attestation-cases/attestation-cases.component.ts index f0005e6a7..df7dd56c2 100644 --- a/imxweb/projects/att/src/lib/policies/attestation-cases/attestation-cases.component.ts +++ b/imxweb/projects/att/src/lib/policies/attestation-cases/attestation-cases.component.ts @@ -30,7 +30,7 @@ import { EuiLoadingService, EuiSidesheetRef, EUI_SIDESHEET_DATA } from '@element import { TranslateService } from '@ngx-translate/core'; import { PortalAttestationFilterMatchingobjects } from 'imx-api-att'; -import { CollectionLoadParameters, IClientProperty, DisplayColumns, ValType, EntitySchema, DataModel } from 'imx-qbm-dbts'; +import { CollectionLoadParameters, DisplayColumns, ValType, EntitySchema } from 'imx-qbm-dbts'; import { ClassloggerService, ClientPropertyForTableColumns, ConfirmationService, DataSourceToolbarSettings, LdsReplacePipe, SettingsService, SnackBarService } from 'qbm'; import { PolicyService } from '../policy.service'; import { AttestationCasesComponentParameter } from './attestation-cases-component-parameter.interface'; @@ -50,7 +50,6 @@ export class AttestationCasesComponent implements OnInit { private navigationState: CollectionLoadParameters; private displayedColumns: ClientPropertyForTableColumns[]; - private dataModel: DataModel; private threshold = -1; constructor( @@ -86,7 +85,6 @@ export class AttestationCasesComponent implements OnInit { let overlayRef: OverlayRef; setTimeout(() => (overlayRef = this.busyService.show())); try { - this.dataModel = await this.policyService.getDataModel(); this.threshold = await this.policyService.getCasesThreshold(); } finally { setTimeout(async () => { @@ -161,8 +159,7 @@ export class AttestationCasesComponent implements OnInit { displayedColumns: this.displayedColumns, dataSource: datasource, entitySchema: this.entitySchemaPolicy, - navigationState: this.navigationState, - dataModel: this.dataModel + navigationState: this.navigationState }; this.logger.debug(this, 'matching objects table navigated to', this.navigationState); diff --git a/imxweb/projects/cpl/src/lib/cpl-config.module.ts b/imxweb/projects/cpl/src/lib/cpl-config.module.ts index f559f5237..f0202e3a7 100644 --- a/imxweb/projects/cpl/src/lib/cpl-config.module.ts +++ b/imxweb/projects/cpl/src/lib/cpl-config.module.ts @@ -48,6 +48,7 @@ import { RulesViolationsModule } from './rules-violations/rules-violations.modul import { RulesViolationsComponent } from './rules-violations/rules-violations.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ComplianceRulesGuardService } from './guards/compliance-rules-guard.service'; +import { RuleViolationsGuardService } from './guards/rule-violations-guard.service'; import { MatCardModule } from '@angular/material/card'; import { RequestModule} from './request/request.module'; const routes: Routes = [ @@ -63,7 +64,7 @@ const routes: Routes = [ { path: 'compliance/rulesviolations/approve', component: RulesViolationsComponent, - canActivate: [RouteGuardService, ComplianceRulesGuardService], + canActivate: [RouteGuardService, RuleViolationsGuardService], resolve: [RouteGuardService], data:{ contextId: HELP_CONTEXTUAL.ComplianceRulesViolationsApprove diff --git a/imxweb/projects/cpl/src/lib/dashboard-plugin/dashboard-plugin.component.html b/imxweb/projects/cpl/src/lib/dashboard-plugin/dashboard-plugin.component.html index e85fb9b3f..00dbfd4ae 100644 --- a/imxweb/projects/cpl/src/lib/dashboard-plugin/dashboard-plugin.component.html +++ b/imxweb/projects/cpl/src/lib/dashboard-plugin/dashboard-plugin.component.html @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/imxweb/projects/cpl/src/lib/dashboard-plugin/dashboard-plugin.component.ts b/imxweb/projects/cpl/src/lib/dashboard-plugin/dashboard-plugin.component.ts index e773b2292..9610327ad 100644 --- a/imxweb/projects/cpl/src/lib/dashboard-plugin/dashboard-plugin.component.ts +++ b/imxweb/projects/cpl/src/lib/dashboard-plugin/dashboard-plugin.component.ts @@ -28,6 +28,7 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { DashboardService, PendingItemsType, UserModelService } from 'qer'; +import { CplPermissionsService } from '../rules/admin/cpl-permissions.service'; @Component({ templateUrl: './dashboard-plugin.component.html' @@ -36,9 +37,12 @@ export class DashboardPluginComponent implements OnInit { public pendingItems: PendingItemsType; + public isExceptionAdmin = false; + constructor( public readonly router: Router, private readonly dashboardService: DashboardService, + private readonly permissionService: CplPermissionsService, private readonly userModelSvc: UserModelService ) { } @@ -47,7 +51,10 @@ export class DashboardPluginComponent implements OnInit { const busy = this.dashboardService.beginBusy(); try { - this.pendingItems = await this.userModelSvc.getPendingItems(); + this.isExceptionAdmin = await this.permissionService.isExceptionAdmin(); + if (this.isExceptionAdmin) { + this.pendingItems = await this.userModelSvc.getPendingItems(); + } } finally { busy.endBusy(); } diff --git a/imxweb/projects/cpl/src/lib/guards/compliance-rules-guard.service.ts b/imxweb/projects/cpl/src/lib/guards/compliance-rules-guard.service.ts index 1baea725c..8a1d054f7 100644 --- a/imxweb/projects/cpl/src/lib/guards/compliance-rules-guard.service.ts +++ b/imxweb/projects/cpl/src/lib/guards/compliance-rules-guard.service.ts @@ -28,7 +28,6 @@ import { Injectable } from '@angular/core'; import { CanActivate, Router } from '@angular/router'; import { AppConfigService } from 'qbm'; -import { QerPermissionsService } from 'qer'; import { CplPermissionsService } from '../rules/admin/cpl-permissions.service'; @Injectable({ @@ -37,7 +36,6 @@ import { CplPermissionsService } from '../rules/admin/cpl-permissions.service'; export class ComplianceRulesGuardService implements CanActivate { constructor( private readonly permissionService: CplPermissionsService, - private readonly permissionsQer: QerPermissionsService, private readonly appConfig: AppConfigService, private readonly router: Router ) { } diff --git a/imxweb/projects/cpl/src/lib/guards/rule-violations-guard.service.ts b/imxweb/projects/cpl/src/lib/guards/rule-violations-guard.service.ts new file mode 100644 index 000000000..fdb529509 --- /dev/null +++ b/imxweb/projects/cpl/src/lib/guards/rule-violations-guard.service.ts @@ -0,0 +1,51 @@ +/* + * ONE IDENTITY LLC. PROPRIETARY INFORMATION + * + * This software is confidential. One Identity, LLC. or one of its affiliates or + * subsidiaries, has supplied this software to you under terms of a + * license agreement, nondisclosure agreement or both. + * + * You may not copy, disclose, or use this software except in accordance with + * those terms. + * + * + * Copyright 2023 One Identity LLC. + * ALL RIGHTS RESERVED. + * + * ONE IDENTITY LLC. MAKES NO REPRESENTATIONS OR + * WARRANTIES ABOUT THE SUITABILITY OF THE SOFTWARE, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, OR + * NON-INFRINGEMENT. ONE IDENTITY LLC. SHALL NOT BE + * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING + * THIS SOFTWARE OR ITS DERIVATIVES. + * + */ + +import { Injectable } from '@angular/core'; +import { CanActivate, Router } from '@angular/router'; + +import { AppConfigService } from 'qbm'; +import { CplPermissionsService } from '../rules/admin/cpl-permissions.service'; + +@Injectable({ + providedIn: 'root', +}) +export class RuleViolationsGuardService implements CanActivate { + constructor( + private readonly permissionService: CplPermissionsService, + private readonly appConfig: AppConfigService, + private readonly router: Router + ) { } + + public async canActivate(): Promise { + const isExceptionAdmin = await this.permissionService.isExceptionAdmin(); + if (!isExceptionAdmin) { + this.router.navigate([this.appConfig.Config.routeConfig.start], { queryParams: {} }); + return false; + } + return isExceptionAdmin; + } +} diff --git a/imxweb/projects/cpl/src/lib/rules-violations/rules-violations.component.ts b/imxweb/projects/cpl/src/lib/rules-violations/rules-violations.component.ts index 93c681864..0a09fd6d1 100644 --- a/imxweb/projects/cpl/src/lib/rules-violations/rules-violations.component.ts +++ b/imxweb/projects/cpl/src/lib/rules-violations/rules-violations.component.ts @@ -30,7 +30,6 @@ import { TranslateService } from '@ngx-translate/core'; import { CollectionLoadParameters, EntitySchema, TypedEntity, ValType } from 'imx-qbm-dbts'; import { ActivatedRoute, Params } from '@angular/router'; -import { OverlayRef } from '@angular/cdk/overlay'; import { ClassloggerService, diff --git a/imxweb/projects/cpl/src/lib/rules-violations/rules-violations.module.ts b/imxweb/projects/cpl/src/lib/rules-violations/rules-violations.module.ts index 22844d0bd..7e61d8925 100644 --- a/imxweb/projects/cpl/src/lib/rules-violations/rules-violations.module.ts +++ b/imxweb/projects/cpl/src/lib/rules-violations/rules-violations.module.ts @@ -28,7 +28,6 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; -import { RouterModule, Routes } from '@angular/router'; import { EuiCoreModule, EuiMaterialModule } from '@elemental-ui/core'; import { TranslateModule } from '@ngx-translate/core'; @@ -41,7 +40,6 @@ import { ExtModule, MenuItem, MenuService, - RouteGuardService, SelectedElementsModule } from 'qbm'; import { RulesViolationsComponent } from './rules-violations.component'; @@ -52,10 +50,11 @@ import { RulesViolationsMultiActionComponent } from './rules-violations-action/r import { RulesViolationsSingleActionComponent } from './rules-violations-action/rules-violations-single-action/rules-violations-single-action.component'; import { ResolveComponent } from './resolve/resolve.component'; import { MatStepperModule } from '@angular/material/stepper'; -import { isRuleStatistics } from '../rules/admin/permissions-helper'; +import { isExceptionAdmin } from '../rules/admin/permissions-helper'; import { MitigatingControlsPersonComponent } from './mitigating-controls-person/mitigating-controls-person.component'; import { MatExpansionModule } from '@angular/material/expansion'; import { MitigatingControlContainerModule } from '../mitigating-control-container/mitigating-control-container.module'; +import { ProjectConfig } from 'imx-api-qer'; @NgModule({ @@ -102,11 +101,11 @@ export class RulesViolationsModule { private setupMenu(): void { this.menuService.addMenuFactories( - (preProps: string[], features: string[]) => { + (preProps: string[], features: string[], projectConfig: ProjectConfig, groups: string[]) => { const items: MenuItem[] = []; - if (preProps.includes('ITSHOP') && isRuleStatistics(features)) { + if (preProps.includes('ITSHOP') && isExceptionAdmin(groups)) { items.push( { id: 'CPL_Compliance_RulesViolations', diff --git a/imxweb/projects/cpl/src/lib/rules/admin/cpl-permissions.service.ts b/imxweb/projects/cpl/src/lib/rules/admin/cpl-permissions.service.ts index ed4073e6c..821b8e1a0 100644 --- a/imxweb/projects/cpl/src/lib/rules/admin/cpl-permissions.service.ts +++ b/imxweb/projects/cpl/src/lib/rules/admin/cpl-permissions.service.ts @@ -27,7 +27,7 @@ import { Injectable } from '@angular/core'; import { UserModelService } from 'qer'; -import { isRuleStatistics } from './permissions-helper'; +import { isExceptionAdmin, isRuleStatistics } from './permissions-helper'; @Injectable({ providedIn: 'root' @@ -35,6 +35,10 @@ import { isRuleStatistics } from './permissions-helper'; export class CplPermissionsService { constructor(private readonly userService: UserModelService) { } + public async isExceptionAdmin(): Promise { + return isExceptionAdmin((await this.userService.getGroups()).map(group => group.Name)); + } + public async isRuleStatistics(): Promise { return isRuleStatistics((await this.userService.getFeatures()).Features); } diff --git a/imxweb/projects/cpl/src/lib/rules/admin/permissions-helper.ts b/imxweb/projects/cpl/src/lib/rules/admin/permissions-helper.ts index ec6f5ac9b..5e7a288fc 100644 --- a/imxweb/projects/cpl/src/lib/rules/admin/permissions-helper.ts +++ b/imxweb/projects/cpl/src/lib/rules/admin/permissions-helper.ts @@ -25,6 +25,10 @@ */ +export function isExceptionAdmin(groups: string[]): boolean { + return groups.find(item => item === 'vi_4_RULEADMIN_EXCEPTION') != null; +} + export function isRuleStatistics(features: string[]): boolean { return features.find(item => item === 'Portal_UI_RuleStatistics') != null; } diff --git a/imxweb/projects/qbm/src/lib/admin/admin.module.ts b/imxweb/projects/qbm/src/lib/admin/admin.module.ts index f26692de7..90f7bb6b8 100644 --- a/imxweb/projects/qbm/src/lib/admin/admin.module.ts +++ b/imxweb/projects/qbm/src/lib/admin/admin.module.ts @@ -64,6 +64,8 @@ import { SwaggerComponent } from './swagger/swagger.component'; import { CacheComponent } from './cache.component'; import { PluginsComponent } from './plugins.component'; import { InfoModalDialogModule } from './../info-modal-dialog/info-modal-dialog.module'; +import { SqlWizardApiService } from '../sqlwizard/sqlwizard-api.service'; +import { QbmSqlWizardService } from '../base/qbm-sqlwizard.service'; @NgModule({ imports: [ @@ -88,9 +90,16 @@ import { InfoModalDialogModule } from './../info-modal-dialog/info-modal-dialog. TranslateModule, DateModule, ScrollingModule, - InfoModalDialogModule + InfoModalDialogModule, + ], + providers: [ + ConfigService, + StatusService, + { + provide: SqlWizardApiService, + useClass: QbmSqlWizardService, + }, ], - providers: [ConfigService, StatusService], declarations: [ AddConfigSidesheetComponent, ApplyConfigSidesheetComponent, @@ -110,4 +119,4 @@ import { InfoModalDialogModule } from './../info-modal-dialog/info-modal-dialog. SwaggerComponent, ], }) -export class AdminModule { } +export class AdminModule {} diff --git a/imxweb/projects/qbm/src/lib/admin/logs.component.ts b/imxweb/projects/qbm/src/lib/admin/logs.component.ts index 6cc7664e2..f8151f618 100644 --- a/imxweb/projects/qbm/src/lib/admin/logs.component.ts +++ b/imxweb/projects/qbm/src/lib/admin/logs.component.ts @@ -43,11 +43,10 @@ import { MatPaginator, PageEvent } from '@angular/material/paginator'; import moment from 'moment-timezone'; import { DataSourceToolbarFilter } from '../data-source-toolbar/data-source-toolbar-filters.interface'; - @Component({ selector: 'imx-logs', templateUrl: './logs.component.html', - styleUrls: ['./logs.component.scss'] + styleUrls: ['./logs.component.scss'], }) export class LogsComponent implements OnInit { private stream: EventSource; @@ -66,18 +65,19 @@ export class LogsComponent implements OnInit { public liveLogsOnPage: ApiLogEntry[] = []; public liveTotalCount: number; public pageSize: number = 20; - public searchText: string = ""; + public searchText: string = ''; public searchBoxText: string = '#LDS#Search'; public isRegexChecked: boolean = false; public isRegexValid: boolean = true; public logDownloads: EuiDownloadOptions[] = []; - @ViewChild("dstSession") dstSession: DataSourceToolbarComponent; - @ViewChild("dstLive") dstLive: DataSourceToolbarComponent; - @ViewChild("sessionPaginator") sessionPaginator: MatPaginator; - @ViewChild("livePaginator") livePaginator: MatPaginator; + @ViewChild('dstSession') dstSession: DataSourceToolbarComponent; + @ViewChild('dstLive') dstLive: DataSourceToolbarComponent; + @ViewChild('sessionPaginator') sessionPaginator: MatPaginator; + @ViewChild('livePaginator') livePaginator: MatPaginator; - constructor( private readonly appConfigService: AppConfigService, + constructor( + private readonly appConfigService: AppConfigService, private readonly sidesheet: EuiSidesheetService, private readonly busyService: EuiLoadingService, private readonly translateService: TranslateService, @@ -85,11 +85,9 @@ export class LogsComponent implements OnInit { private logger: ClassloggerService, private elementalUiConfigService: ElementalUiConfigService, private translator: TranslateService - ) { } - + ) {} - - public async ngOnInit(): Promise { + public async ngOnInit(): Promise { const overlayRef = this.busyService.show(); try { @@ -134,16 +132,14 @@ export class LogsComponent implements OnInit { Logs: { ColumnName: 'Logs', Type: ValType.String, - } - } + }, + }, }, navigationState: { StartIndex: 0, - PageSize: 20 + PageSize: 20, }, - filters: [ - timeFilter - ], + filters: [timeFilter], }; this.dstSettingsLive = JSON.parse(JSON.stringify(this.dstSettingsSession)); @@ -172,7 +168,6 @@ export class LogsComponent implements OnInit { this.stream.onerror = (err) => { this.logger.error('An error occured in data stream:', err); }; - } finally { setTimeout(() => this.busyService.hide(overlayRef)); } @@ -182,7 +177,7 @@ export class LogsComponent implements OnInit { const overlayRef = this.busyService.show(); try { return await this.appConfigService.client.admin_systeminfo_log_session_get(); - } finally{ + } finally { setTimeout(() => this.busyService.hide(overlayRef)); } } @@ -197,13 +192,16 @@ export class LogsComponent implements OnInit { try { this.logFiles = await this.appConfigService.client.admin_systeminfo_logs_get(); - this.logFiles.forEach(log => { - var dir = log.Path.split("\\"); - const url = this.appConfigService.BaseUrl + new MethodDefinition(new V2ApiClientMethodFactory().admin_systeminfo_log_get(dir[0], dir[1])).path; + this.logFiles.forEach((log) => { + var dir = log.Path.split('\\'); + const url = + this.appConfigService.BaseUrl + + new MethodDefinition(new V2ApiClientMethodFactory().admin_systeminfo_log_get(dir[0], dir[1])).path; this.logDownloads.push({ ...this.elementalUiConfigService.Config.downloadOptions, - url: url, - fileName: log.File + fileMimeType: '', + url, + fileName: log.File, }); }); } finally { @@ -213,22 +211,27 @@ export class LogsComponent implements OnInit { } public onSessionLogSearch(keywords: string): void { - const sessionKeywords: ({IsRegex?: boolean} & FilterData)[] = this.dstSettingsSession.navigationState.filter?.filter(filter => filter.Type === 1); + const sessionKeywords: ({ IsRegex?: boolean } & FilterData)[] = this.dstSettingsSession.navigationState.filter?.filter( + (filter) => filter.Type === 1 + ); this.sessionLogsFiltered = this.sessionLogs; //Handles keyword filters if (sessionKeywords && sessionKeywords.length > 0) { - this.sessionLogsFiltered = this.sessionLogsFiltered.filter(log => - sessionKeywords.every(keyword => keyword.IsRegex - ? this.regexTest(keyword.Value1.toLowerCase(), log.Message?.toLowerCase()) - : log.Message?.toLowerCase().includes(keyword.Value1.toLowerCase()) + this.sessionLogsFiltered = this.sessionLogsFiltered.filter((log) => + sessionKeywords.every((keyword) => + keyword.IsRegex + ? this.regexTest(keyword.Value1.toLowerCase(), log.Message?.toLowerCase()) + : log.Message?.toLowerCase().includes(keyword.Value1.toLowerCase()) ) ); } //Handles time filters if (this.dstSettingsSession.navigationState.TimeFilter) { - this.sessionLogsFiltered = this.sessionLogsFiltered.filter(log => moment(log.TimeStamp).isAfter(this.dstSettingsSession.navigationState.TimeFilter)); + this.sessionLogsFiltered = this.sessionLogsFiltered.filter((log) => + moment(log.TimeStamp).isAfter(this.dstSettingsSession.navigationState.TimeFilter) + ); } if (this.isRegexChecked) { @@ -239,8 +242,8 @@ export class LogsComponent implements OnInit { } } else { this.sessionLogsFiltered = keywords - ? this.sessionLogsFiltered.filter(log => log.Message?.toLowerCase().includes(keywords?.toLowerCase())) - : this.sessionLogsFiltered; + ? this.sessionLogsFiltered.filter((log) => log.Message?.toLowerCase().includes(keywords?.toLowerCase())) + : this.sessionLogsFiltered; } this.sessionTotalCount = this.sessionLogsFiltered.length; @@ -249,22 +252,27 @@ export class LogsComponent implements OnInit { } public onLiveLogSearch(keywords: string): void { - const liveKeywords: ({IsRegex?: boolean} & FilterData)[] = this.dstSettingsLive.navigationState.filter?.filter(filter => filter.Type === 1); + const liveKeywords: ({ IsRegex?: boolean } & FilterData)[] = this.dstSettingsLive.navigationState.filter?.filter( + (filter) => filter.Type === 1 + ); this.liveLogsFiltered = this.liveLogs; //Handles keyword filters if (liveKeywords && liveKeywords.length > 0) { - this.liveLogsFiltered = this.liveLogsFiltered.filter(log => - liveKeywords.every(keyword => keyword.IsRegex - ? this.regexTest(keyword.Value1.toLowerCase(), log.Message?.toLowerCase()) - : log.Message?.toLowerCase().includes(keyword.Value1.toLowerCase()) + this.liveLogsFiltered = this.liveLogsFiltered.filter((log) => + liveKeywords.every((keyword) => + keyword.IsRegex + ? this.regexTest(keyword.Value1.toLowerCase(), log.Message?.toLowerCase()) + : log.Message?.toLowerCase().includes(keyword.Value1.toLowerCase()) ) ); } //Handles time filters if (this.dstSettingsLive.navigationState.TimeFilter) { - this.liveLogsFiltered = this.liveLogsFiltered.filter(log => moment(log.TimeStamp).isAfter(this.dstSettingsLive.navigationState.TimeFilter)); + this.liveLogsFiltered = this.liveLogsFiltered.filter((log) => + moment(log.TimeStamp).isAfter(this.dstSettingsLive.navigationState.TimeFilter) + ); } if (this.isRegexChecked) { @@ -275,8 +283,8 @@ export class LogsComponent implements OnInit { } } else { this.liveLogsFiltered = keywords - ? this.liveLogsFiltered.filter(log => log.Message?.toLowerCase().includes(keywords?.toLowerCase())) - : this.liveLogsFiltered; + ? this.liveLogsFiltered.filter((log) => log.Message?.toLowerCase().includes(keywords?.toLowerCase())) + : this.liveLogsFiltered; } this.liveTotalCount = this.liveLogsFiltered.length; @@ -286,30 +294,25 @@ export class LogsComponent implements OnInit { private regexTest(keywords: string, message: string) { try { - return new RegExp(keywords, "gi").test(message); - } catch (e) { - } + return new RegExp(keywords, 'gi').test(message); + } catch (e) {} } public onRegexSessionLogSearch(keywords: string): void { this.sessionLogsFiltered = keywords - ? this.sessionLogsFiltered.filter(log => this.regexTest(keywords, log.Message)) - : this.sessionLogsFiltered; + ? this.sessionLogsFiltered.filter((log) => this.regexTest(keywords, log.Message)) + : this.sessionLogsFiltered; } public onRegexLiveLogSearch(keywords: string): void { - this.liveLogsFiltered = keywords - ? this.liveLogsFiltered.filter(log => this.regexTest(keywords, log.Message)) - : this.liveLogsFiltered; + this.liveLogsFiltered = keywords ? this.liveLogsFiltered.filter((log) => this.regexTest(keywords, log.Message)) : this.liveLogsFiltered; } public onRegexToggle(event: MatSlideToggleChange): void { - this.searchBoxText = event.checked - ? '#LDS#Search using regular expressions' - : '#LDS#Search'; + this.searchBoxText = event.checked ? '#LDS#Search using regular expressions' : '#LDS#Search'; - if (this.currentTab === 0) this.onSessionLogSearch(this.dstSession.settings.navigationState.search); - if (this.currentTab === 1) this.onLiveLogSearch(this.dstLive.settings.navigationState.search); + if (this.currentTab === 0) this.onSessionLogSearch(this.dstSession.settings.navigationState.search); + if (this.currentTab === 1) this.onLiveLogSearch(this.dstLive.settings.navigationState.search); } public validateRegex(keywords: string): boolean { @@ -353,31 +356,35 @@ export class LogsComponent implements OnInit { this.setLivePage(); } - public getListIcon(changeType:string): string - { + public getListIcon(changeType: string): string { let icon = 'clock'; - switch(changeType) - { - case 'Error': icon = 'error'; break; - case 'Warn': icon = 'warning'; break; + switch (changeType) { + case 'Error': + icon = 'error'; + break; + case 'Warn': + icon = 'warning'; + break; } return icon; } - public getIconColor(changeType:string): string - { + public getIconColor(changeType: string): string { let color = 'imx-primary-icon'; - switch(changeType) - { - case 'Error': color = 'imx-error-icon'; break; - case 'Warn': color = 'imx-warning-icon'; break; + switch (changeType) { + case 'Error': + color = 'imx-error-icon'; + break; + case 'Warn': + color = 'imx-warning-icon'; + break; } return color; } public async openLogSideSheet(log: ApiLogEntry): Promise { let overlayRef: OverlayRef; - setTimeout(() => overlayRef = this.busyService.show()); + setTimeout(() => (overlayRef = this.busyService.show())); let config: EuiSidesheetConfig; try { config = { @@ -386,17 +393,15 @@ export class LogsComponent implements OnInit { padding: '0', width: 'max(600px, 60%)', testId: 'log-details-sidesheet', - data: log + data: log, }; } finally { - setTimeout(() => this.busyService.hide(overlayRef)); + setTimeout(() => this.busyService.hide(overlayRef)); } this.sidesheet.open(LogDetailsSidesheetComponent, config); } public ngOnDestroy(): void { - if (this.stream) - this.stream.close(); + if (this.stream) this.stream.close(); } - } diff --git a/imxweb/projects/qbm/src/lib/admin/swagger/swagger.component.ts b/imxweb/projects/qbm/src/lib/admin/swagger/swagger.component.ts index 1a2a39bd5..52c99da71 100644 --- a/imxweb/projects/qbm/src/lib/admin/swagger/swagger.component.ts +++ b/imxweb/projects/qbm/src/lib/admin/swagger/swagger.component.ts @@ -53,6 +53,7 @@ export class SwaggerComponent implements AfterViewInit { if (token) { req.headers['X-XSRF-TOKEN'] = token; } + return req; }, }); } diff --git a/imxweb/projects/qbm/src/lib/base/qbm-sqlwizard.service.ts b/imxweb/projects/qbm/src/lib/base/qbm-sqlwizard.service.ts new file mode 100644 index 000000000..6f8015d14 --- /dev/null +++ b/imxweb/projects/qbm/src/lib/base/qbm-sqlwizard.service.ts @@ -0,0 +1,47 @@ +/* + * ONE IDENTITY LLC. PROPRIETARY INFORMATION + * + * This software is confidential. One Identity, LLC. or one of its affiliates or + * subsidiaries, has supplied this software to you under terms of a + * license agreement, nondisclosure agreement or both. + * + * You may not copy, disclose, or use this software except in accordance with + * those terms. + * + * + * Copyright 2023 One Identity LLC. + * ALL RIGHTS RESERVED. + * + * ONE IDENTITY LLC. MAKES NO REPRESENTATIONS OR + * WARRANTIES ABOUT THE SUITABILITY OF THE SOFTWARE, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, OR + * NON-INFRINGEMENT. ONE IDENTITY LLC. SHALL NOT BE + * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING + * THIS SOFTWARE OR ITS DERIVATIVES. + * + */ + +import { Injectable } from '@angular/core'; +import { FilterProperty, CollectionLoadParameters, EntityCollectionData } from 'imx-qbm-dbts'; +import { SqlWizardApiService } from '../sqlwizard/sqlwizard-api.service'; + +@Injectable({ + providedIn: 'root', +}) +export class QbmSqlWizardService extends SqlWizardApiService { + public implemented: boolean = false; + + getFilterProperties(table: string): Promise { + return new Promise((resolve) => resolve([])); + } + getCandidates(parentTable: string, options?: CollectionLoadParameters): Promise { + return new Promise((resolve) => resolve({ TotalCount: 0 })); + } + + constructor() { + super(); + } +} diff --git a/imxweb/projects/qbm/src/lib/bulk-property-editor/bulk-item/bulk-item.component.ts b/imxweb/projects/qbm/src/lib/bulk-property-editor/bulk-item/bulk-item.component.ts index 9175b2fb1..7aa88e36d 100644 --- a/imxweb/projects/qbm/src/lib/bulk-property-editor/bulk-item/bulk-item.component.ts +++ b/imxweb/projects/qbm/src/lib/bulk-property-editor/bulk-item/bulk-item.component.ts @@ -24,20 +24,25 @@ * */ -import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output, QueryList, ViewChild, ViewChildren } from '@angular/core'; import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { MatExpansionPanel } from '@angular/material/expansion'; import { BulkItem, BulkItemStatus } from './bulk-item'; import { BulkItemIcon } from './bulk-item-icon'; +import { CdrEditorComponent } from '../../cdr/cdr-editor/cdr-editor.component'; +import { isEqual } from 'lodash'; +import { EntityWriteDataSingle } from 'imx-qbm-dbts'; @Component({ selector: 'imx-bulk-item', templateUrl: './bulk-item.component.html', - styleUrls: ['./bulk-item.component.scss'] + styleUrls: ['./bulk-item.component.scss'], }) export class BulkItemComponent implements OnInit { - public get icon(): BulkItemIcon { return this.icons[this.bulkItem?.status]; } + public get icon(): BulkItemIcon { + return this.icons[this.bulkItem?.status]; + } public iconStyle = {}; @@ -56,20 +61,22 @@ export class BulkItemComponent implements OnInit { public readonly formGroup = new UntypedFormGroup({}); + private diffData: EntityWriteDataSingle; private readonly icons: { [key: number]: BulkItemIcon } = {}; + @ViewChildren(CdrEditorComponent) private cdrEditors: QueryList; constructor() { this.icons[BulkItemStatus.unknown] = { name: 'help', - color: 'grey' + color: 'grey', }; this.icons[BulkItemStatus.skipped] = { name: 'delete', - color: 'grey' + color: 'grey', }; this.icons[BulkItemStatus.saved] = { name: 'check', - color: 'green' + color: 'green', }; } public ngOnInit(): void { @@ -80,8 +87,8 @@ export class BulkItemComponent implements OnInit { } this.setIconStyle(); - this.bulkItem.readonly = this.bulkItem.properties.every(p => p.isReadOnly()); - this.bulkItem.mandatory = this.bulkItem.properties.some(p => p.column.GetMetadata().GetMinLength() > 0); + this.bulkItem.readonly = this.bulkItem.properties.every((p) => p.isReadOnly()); + this.bulkItem.mandatory = this.bulkItem.properties.some((p) => p.column.GetMetadata().GetMinLength() > 0); }); } @@ -114,13 +121,20 @@ export class BulkItemComponent implements OnInit { this.statusUnknown.emit(this.bulkItem); } + const areEqual = isEqual(this.diffData, this.bulkItem.entity.GetEntity().GetDiffData()); + this.diffData = { ...this.bulkItem.entity.GetEntity().GetDiffData() }; + + if (!areEqual) { + this.cdrEditors.forEach((item) => { + item.update(); + }); + } + this.validate(); } public addControl(name: string, control: AbstractControl): void { - setTimeout(() => - this.formGroup.addControl(name, control) - ); + setTimeout(() => this.formGroup.addControl(name, control)); } private validate(): void { @@ -130,7 +144,7 @@ export class BulkItemComponent implements OnInit { private setIconStyle(): void { let visible = false; - this.bulkItem.properties.every(item => { + this.bulkItem.properties.every((item) => { if (item.column.GetMetadata().GetMinLength() > 0) { visible = true; return; @@ -141,6 +155,6 @@ export class BulkItemComponent implements OnInit { visible = true; } - visible ? this.iconStyle = {visibility: 'visible'} : this.iconStyle = {visibility: 'hidden'}; + visible ? (this.iconStyle = { visibility: 'visible' }) : (this.iconStyle = { visibility: 'hidden' }); } } diff --git a/imxweb/projects/qbm/src/lib/cdr/Readme.md b/imxweb/projects/qbm/src/lib/cdr/Readme.md index 9c8817d09..9cdfffead 100644 --- a/imxweb/projects/qbm/src/lib/cdr/Readme.md +++ b/imxweb/projects/qbm/src/lib/cdr/Readme.md @@ -55,11 +55,11 @@ Nevertheless within the CDR module there are two plugins that supply some genera If you want to make use of these two provider services you have to register them at the CdrRegistryService, e.g. during your app's initialization like this: ```typescript -export function initApp(registry: CdrRegistryService, resolver: ComponentFactoryResolver) { +export function initApp(registry: CdrRegistryService) { return () => new Promise(async (resolve: any) => { - registry.register(new DefaultCdrEditorProvider(resolver)); - registry.register(new FkCdrEditorProvider(resolver)); + registry.register(new DefaultCdrEditorProvider()); + registry.register(new FkCdrEditorProvider()); resolve(); }); } diff --git a/imxweb/projects/qbm/src/lib/cdr/date-range/date-range.component.html b/imxweb/projects/qbm/src/lib/cdr/date-range/date-range.component.html index 46b988155..4fa842234 100644 --- a/imxweb/projects/qbm/src/lib/cdr/date-range/date-range.component.html +++ b/imxweb/projects/qbm/src/lib/cdr/date-range/date-range.component.html @@ -1,4 +1,4 @@ - + {{ columnContainer?.display | translate }} {{'#LDS#Use dynamic time frame' | translate}} @@ -34,3 +34,14 @@ + + + + + + + + diff --git a/imxweb/projects/qbm/src/lib/cdr/default-cdr-editor-provider.spec.ts b/imxweb/projects/qbm/src/lib/cdr/default-cdr-editor-provider.spec.ts index b529e46c4..6fa2dbf68 100644 --- a/imxweb/projects/qbm/src/lib/cdr/default-cdr-editor-provider.spec.ts +++ b/imxweb/projects/qbm/src/lib/cdr/default-cdr-editor-provider.spec.ts @@ -24,7 +24,7 @@ * */ -import { ComponentFactoryResolver, ViewContainerRef, ComponentRef } from '@angular/core'; +import { ViewContainerRef, ComponentRef } from '@angular/core'; import * as TypeMoq from 'typemoq'; import { DefaultCdrEditorProvider } from './default-cdr-editor-provider'; @@ -42,10 +42,8 @@ import { ViewPropertyDefaultComponent } from './view-property-default/view-prope import { clearStylesFromDOM } from '../testing/clear-styles.spec'; describe('DefaultCdrEditorProvider', () => { - let factoryResolverMock: TypeMoq.IMock; - beforeEach(() => { - factoryResolverMock = TypeMoq.Mock.ofType(); + }); afterAll(() => { @@ -53,7 +51,7 @@ describe('DefaultCdrEditorProvider', () => { }); it('should create an instance', () => { - expect(new DefaultCdrEditorProvider(factoryResolverMock.object)).toBeDefined(); + expect(new DefaultCdrEditorProvider()).toBeDefined(); }); it('should create EditBooleanComponent for simple bool cdr', () => { @@ -160,21 +158,17 @@ function testCreateEditor(TCtor: new (...args: any[]) => T, const editorMock = TypeMoq.Mock.ofType(); const parentMock = TypeMoq.Mock.ofType(); const childMock = createComponentMock(editorMock.object); - const factoryResolverMock = TypeMoq.Mock.ofType(); parentMock.setup( p => p.createComponent(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => childMock.object); - factoryResolverMock.setup(r => r.resolveComponentFactory(TCtor)).returns(() => null); // Act - const provider = new DefaultCdrEditorProvider(factoryResolverMock.object); + const provider = new DefaultCdrEditorProvider(); const editor = provider.createEditor(parentMock.object, cdrMock.object); // Assert expect(editor === childMock.object).toBeTruthy(); editorMock.verify(e => e.bind(cdrMock.object), TypeMoq.Times.once()); editorMock.verify(e => e.bind(TypeMoq.It.isAny()), TypeMoq.Times.once()); - factoryResolverMock.verify(e => e.resolveComponentFactory(TCtor), TypeMoq.Times.once()); - factoryResolverMock.verify(e => e.resolveComponentFactory(TypeMoq.It.isAny()), TypeMoq.Times.once()); } export function createComponentMock(instance: T): TypeMoq.IMock> { diff --git a/imxweb/projects/qbm/src/lib/cdr/default-cdr-editor-provider.ts b/imxweb/projects/qbm/src/lib/cdr/default-cdr-editor-provider.ts index 2e3a26e0a..82c1cefe7 100644 --- a/imxweb/projects/qbm/src/lib/cdr/default-cdr-editor-provider.ts +++ b/imxweb/projects/qbm/src/lib/cdr/default-cdr-editor-provider.ts @@ -25,7 +25,7 @@ */ import { CdrEditorProvider } from './cdr-editor-provider.interface'; -import { ViewContainerRef, ComponentRef, ComponentFactoryResolver } from '@angular/core'; +import { ViewContainerRef, ComponentRef, Type } from '@angular/core'; import { ColumnDependentReference } from './column-dependent-reference.interface'; import { CdrEditor } from './cdr-editor.interface'; import { ValType, IValueMetadata } from 'imx-qbm-dbts'; @@ -38,13 +38,12 @@ import { EditMultilineComponent } from './edit-multiline/edit-multiline.componen import { EditImageComponent } from './edit-image/edit-image.component'; import { EditDateComponent } from './edit-date/edit-date.component'; import { EditRiskIndexComponent } from './edit-risk-index/edit-risk-index.component'; -import { ViewPropertyDefaultComponent } from './view-property-default/view-property-default.component'; import { DateRangeComponent } from './date-range/date-range.component'; import { EditUrlComponent } from './edit-url/edit-url.component'; export class DefaultCdrEditorProvider implements CdrEditorProvider { - constructor(private componentFactoryResolver: ComponentFactoryResolver) { } + constructor() { } public createEditor(parent: ViewContainerRef, cdref: ColumnDependentReference): ComponentRef { const meta = cdref.column.GetMetadata(); @@ -53,16 +52,15 @@ export class DefaultCdrEditorProvider implements CdrEditorProvider { const range = meta.IsRange(); const limitedValues = this.isLimitedValues(meta); const schemaKey = meta.GetSchemaKey(); - const isRiskIndexColumn = ['RiskIndex', 'RiskLevel'].includes(schemaKey.substr(schemaKey.lastIndexOf('.') + 1)) + const isRiskIndexColumn = ['RiskIndex', 'RiskLevel'].includes(schemaKey.substring(schemaKey.lastIndexOf('.') + 1)) || schemaKey == 'QERRiskIndex.Weight'; const type = meta.GetType(); - const isReadonly = cdref.isReadOnly() || !meta.CanEdit(); if (type === ValType.Binary) { return this.createBound(EditImageComponent, parent, cdref); } - if (!multiLine && !multiValue && !range && !limitedValues && !isRiskIndexColumn && !isReadonly) { + if (!multiLine && !multiValue && !range && !limitedValues && !isRiskIndexColumn) { switch (type) { case ValType.Bool: return this.createBound(EditBooleanComponent, parent, cdref); @@ -76,8 +74,7 @@ export class DefaultCdrEditorProvider implements CdrEditorProvider { case ValType.Date: return this.createBound(EditDateComponent, parent, cdref); } - } else if (isReadonly) { - return this.createBound(ViewPropertyDefaultComponent, parent, cdref); + } else if (limitedValues) { return multiValue ? this.createBound(EditMultiLimitedValueComponent, parent, cdref) @@ -101,9 +98,9 @@ export class DefaultCdrEditorProvider implements CdrEditorProvider { return null; } - private createBound(tCtor: new (...args: any[]) => T, parent: ViewContainerRef, cdref: ColumnDependentReference) + private createBound(editor: Type, parent: ViewContainerRef, cdref: ColumnDependentReference) : ComponentRef { - const result = parent.createComponent(this.componentFactoryResolver.resolveComponentFactory(tCtor)); + const result = parent.createComponent(editor); result.instance.bind(cdref); return result; } diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-binary/edit-binary.component.html b/imxweb/projects/qbm/src/lib/cdr/edit-binary/edit-binary.component.html index 14d624eb3..da31437a4 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-binary/edit-binary.component.html +++ b/imxweb/projects/qbm/src/lib/cdr/edit-binary/edit-binary.component.html @@ -1,4 +1,13 @@ - + {{ columnContainer?.display | translate }} + + + + + + diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-boolean/edit-boolean.component.html b/imxweb/projects/qbm/src/lib/cdr/edit-boolean/edit-boolean.component.html index 0d020a940..9080b4daa 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-boolean/edit-boolean.component.html +++ b/imxweb/projects/qbm/src/lib/cdr/edit-boolean/edit-boolean.component.html @@ -1,4 +1,12 @@ - + {{ columnContainer?.display | translate }} + + + + + diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-date/edit-date.component.html b/imxweb/projects/qbm/src/lib/cdr/edit-date/edit-date.component.html index 7f9f0bbc9..482c41131 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-date/edit-date.component.html +++ b/imxweb/projects/qbm/src/lib/cdr/edit-date/edit-date.component.html @@ -1,11 +1,21 @@ - + + + + + + + diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-date/edit-date.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-date/edit-date.component.ts index 718d7f02c..1412c67f8 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-date/edit-date.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-date/edit-date.component.ts @@ -26,7 +26,7 @@ import { Component, ErrorHandler, EventEmitter, OnDestroy } from '@angular/core'; import { UntypedFormControl } from '@angular/forms'; -import { Subscription } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { CdrEditor, ValueHasChangedEventArg } from '../cdr-editor.interface'; import { ColumnDependentReference } from '../column-dependent-reference.interface'; import moment from 'moment-timezone'; @@ -42,7 +42,7 @@ import { DateFormat } from 'imx-qbm-dbts'; @Component({ selector: 'imx-edit-date', templateUrl: './edit-date.component.html', - styleUrls: ['./edit-date.component.scss'] + styleUrls: ['./edit-date.component.scss'], }) export class EditDateComponent implements CdrEditor, OnDestroy { public readonly control = new UntypedFormControl(undefined, { updateOn: 'blur' }); @@ -51,26 +51,24 @@ export class EditDateComponent implements CdrEditor, OnDestroy { public readonly valueHasChanged = new EventEmitter(); + public readonly updateRequested = new Subject(); + public isBusy = false; private readonly subscribers: Subscription[] = []; private isWriting = false; - public get withTime(): boolean { // try to get the date format detail from metadata; defaulting to DateTime. const dateFormat = this.columnContainer.metaData?.GetDateFormat() ?? DateFormat.DateTime; - return (dateFormat === DateFormat.DateTime) || (dateFormat === DateFormat.UtcDateTime); + return dateFormat === DateFormat.DateTime || dateFormat === DateFormat.UtcDateTime; } - public constructor( - private readonly errorHandler: ErrorHandler, - private logger: ClassloggerService, - ) { } + public constructor(private readonly errorHandler: ErrorHandler, private logger: ClassloggerService) {} public ngOnDestroy(): void { - this.subscribers.forEach(s => s.unsubscribe()); + this.subscribers.forEach((s) => s.unsubscribe()); } /** @@ -91,22 +89,46 @@ export class EditDateComponent implements CdrEditor, OnDestroy { ); } - this.subscribers.push(this.control.valueChanges.subscribe(async value => - this.writeValue(this.control.value) - )); + this.subscribers.push(this.control.valueChanges.subscribe(async () => this.writeValue(this.control.value))); // bind to entity change event - this.subscribers.push(this.columnContainer.subscribe(() => { - if (!this.isWriting) { - this.logger.trace(this, 'Control set to new value'); - this.resetControlValue(); - this.valueHasChanged.emit({value: this.control.value}); - } - })); - - if (this.columnContainer.isValueRequired && this.columnContainer.canEdit) { - this.control.addValidators((control) => (control.value == undefined || control.value.length === 0 ? { required: true } : null)); - } + this.subscribers.push( + this.columnContainer.subscribe(() => { + if (!this.isWriting) { + this.logger.trace(this, 'Control set to new value'); + this.resetControlValue(); + this.valueHasChanged.emit({ value: this.control.value }); + } + }) + ); + + this.setValidators(); + + this.subscribers.push( + this.updateRequested.subscribe(() => { + setTimeout(() => { + try { + this.setValidators(); + this.control.updateValueAndValidity({ onlySelf: true, emitEvent: false }); + this.resetControlValue(); + } finally { + } + this.valueHasChanged.emit({ value: this.control.value }); + }); + }) + ); + } + } + + /** + * Sets Validators.required, if the control is mandatory, else it's set to null. + * @ignore used internally + */ + private setValidators() { + if (this.columnContainer.isValueRequired && this.columnContainer.canEdit) { + this.control.addValidators((control) => (control.value == undefined || control.value.length === 0 ? { required: true } : null)); + } else { + this.control.setValidators(null); } } @@ -117,7 +139,7 @@ export class EditDateComponent implements CdrEditor, OnDestroy { private updateControlValue(value: Moment): void { if (this.control.value !== value) { - this.control.setValue(value, {emitEvent: false}); + this.control.setValue(value, { emitEvent: false }); } } @@ -153,9 +175,6 @@ export class EditDateComponent implements CdrEditor, OnDestroy { this.resetControlValue(); } - this.valueHasChanged.emit({value: this.columnContainer.value, forceEmit: true}); + this.valueHasChanged.emit({ value: this.columnContainer.value, forceEmit: true }); } - } - - diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-default/edit-default.component.html b/imxweb/projects/qbm/src/lib/cdr/edit-default/edit-default.component.html index 611bc4ae4..b21f28bd8 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-default/edit-default.component.html +++ b/imxweb/projects/qbm/src/lib/cdr/edit-default/edit-default.component.html @@ -1,4 +1,4 @@ - + {{ columnContainer?.display | translate }} @@ -15,3 +15,13 @@ {{validationErrorMessage}} + + + + + + + diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk-multi.component.html b/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk-multi.component.html index db987328a..c4bdba47f 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk-multi.component.html +++ b/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk-multi.component.html @@ -1,4 +1,4 @@ - + {{ columnContainer?.display | translate }} + + + + + + diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk-multi.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk-multi.component.ts index 10e19efbd..7c1101ccd 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk-multi.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk-multi.component.ts @@ -110,11 +110,7 @@ export class EditFkMultiComponent implements CdrEditor, OnInit, OnDestroy { if (cdref.minlengthSubject) { this.subscribers.push( cdref.minlengthSubject.subscribe((elem) => { - if (this.columnContainer.isValueRequired && this.columnContainer.canEdit) { - this.control.setValidators((control) => (control.value == null || control.value.length === 0 ? { required: true } : null)); - } else { - this.control.setValidators(null); - } + this.setValidators(); }) ); } @@ -152,6 +148,7 @@ export class EditFkMultiComponent implements CdrEditor, OnInit, OnDestroy { }; const candidateCollection = await this.columnContainer.fkRelations[0]?.Get({ PageSize: -1 }); this.isHierarchical = candidateCollection?.Hierarchy != null; + this.setValidators(); this.control.setValue(await this.multiValueToDisplay(this.currentValueStruct), { emitEvent: false }); this.valueHasChanged.emit({ value: this.currentValueStruct }); } finally { @@ -166,6 +163,18 @@ export class EditFkMultiComponent implements CdrEditor, OnInit, OnDestroy { } } + /** + * Sets Validators.required, if the control is mandatory, else it's set to null. + * @ignore used internally + */ + private setValidators():void{ + if (this.columnContainer.isValueRequired && this.columnContainer.canEdit) { + this.control.setValidators((control) => (control.value == null || control.value.length === 0 ? { required: true } : null)); + } else { + this.control.setValidators(null); + } + } + /** * @ignore * Opens a dialog for selecting fk objects 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 20a267fa1..9e0239e10 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,28 +1,39 @@ - + {{ 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 }}) - - + [required]="columnContainer.isValueRequired" + /> + - + [attr.data-imx-identifier]="'cdr-edit-fk-mat-option-assign-candidate-' + columnContainer?.name" + >
{{ getDisplay(candidate) }}
- {{ candidate.displayLong }}
+ {{ candidate.displayLong }} +
@@ -30,29 +41,46 @@
- + [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 }}
- + + [attr.data-imx-identifier]="'cdr-edit-fk-no-candidates-' + columnContainer?.name" + > + + 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 5897a884b..04a442562 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 @@ -320,6 +320,7 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI try { this.setControlValue(); await this.initCandidates(); + this.control.updateValueAndValidity({ onlySelf: true, emitEvent: false }); } finally { this.loading = false; } diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-image/edit-image.component.html b/imxweb/projects/qbm/src/lib/cdr/edit-image/edit-image.component.html index 410da5dcd..cbbe32b1a 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-image/edit-image.component.html +++ b/imxweb/projects/qbm/src/lib/cdr/edit-image/edit-image.component.html @@ -10,5 +10,5 @@ - + diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-image/edit-image.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-image/edit-image.component.ts index 784416ee5..0b6e993bb 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-image/edit-image.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-image/edit-image.component.ts @@ -26,7 +26,7 @@ import { Component, ElementRef, EventEmitter, OnDestroy, ViewChild } from '@angular/core'; import { UntypedFormControl, Validators } from '@angular/forms'; -import { Subscription } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { ColumnDependentReference } from '../column-dependent-reference.interface'; import { CdrEditor, ValueHasChangedEventArg } from '../cdr-editor.interface'; @@ -41,16 +41,17 @@ import { FileSelectorService } from '../../file-selector/file-selector.service'; @Component({ selector: 'imx-edit-image', templateUrl: './edit-image.component.html', - styleUrls: ['./edit-image.component.scss'] + styleUrls: ['./edit-image.component.scss'], }) export class EditImageComponent implements CdrEditor, OnDestroy { - @ViewChild('file') public fileInput: ElementRef; public get fileFormatHint(): string { return this.fileFormatError ? '#LDS#Please select an image in PNG format.' : undefined; } + public readonly updateRequested = new Subject(); + public readonly control = new UntypedFormControl(undefined); public readonly columnContainer = new EntityColumnContainer(); @@ -69,17 +70,13 @@ export class EditImageComponent implements CdrEditor, OnDestroy { private readonly fileSelector: FileSelectorService ) { this.subscriptions.push( - this.fileSelector.fileFormatError.subscribe(() => - this.fileFormatError = true - ), - this.fileSelector.fileSelected.subscribe(filepath => - this.writeValue(this.imageProvider.getImageData(filepath)) - ) + this.fileSelector.fileFormatError.subscribe(() => (this.fileFormatError = true)), + this.fileSelector.fileSelected.subscribe((filepath) => this.writeValue(this.imageProvider.getImageData(filepath))) ); } public ngOnDestroy(): void { - this.subscriptions.forEach(s => s.unsubscribe()); + this.subscriptions.forEach((s) => s.unsubscribe()); } /** @@ -97,22 +94,39 @@ export class EditImageComponent implements CdrEditor, OnDestroy { if (cdref.minlengthSubject) { this.subscriptions.push( cdref.minlengthSubject.subscribe(() => { - if (this.columnContainer.isValueRequired && this.columnContainer.canEdit) { - this.control.setValidators(Validators.required); - } else{ - this.control.setValidators(null); - } + this.setValidators(); }) ); } - this.subscriptions.push(this.columnContainer.subscribe(() => { - if (this.isWriting) { return; } - if (this.control.value !== this.columnContainer.value) { - this.logger.trace(this, 'Control set to new value'); - this.control.setValue(this.columnContainer.value, { emitEvent: false }); - } - this.valueHasChanged.emit({value: this.control.value}); - })); + this.subscriptions.push( + this.columnContainer.subscribe(() => { + if (this.isWriting) { + return; + } + if (this.control.value !== this.columnContainer.value) { + this.logger.trace(this, 'Control set to new value'); + this.control.setValue(this.columnContainer.value, { emitEvent: false }); + } + this.valueHasChanged.emit({ value: this.control.value }); + }) + ); + + this.subscriptions.push( + this.updateRequested.subscribe(() => { + setTimeout(() => { + try { + if (this.control.value !== this.columnContainer.value) { + this.logger.trace(this, 'Control set to new value'); + this.control.setValue(this.columnContainer.value, { emitEvent: false }); + } + this.valueHasChanged.emit({ value: this.control.value }); + this.setValidators(); + this.control.updateValueAndValidity({ onlySelf: true, emitEvent: false }); + } finally { + } + }); + }) + ); } } @@ -136,6 +150,18 @@ export class EditImageComponent implements CdrEditor, OnDestroy { await this.writeValue(undefined); } + /** + * Sets Validators.required, if the control is mandatory, else it's set to null. + * @ignore used internally + */ + private setValidators() { + if (this.columnContainer.isValueRequired && this.columnContainer.canEdit) { + this.control.setValidators(Validators.required); + } else { + this.control.setValidators(null); + } + } + /** * updates the value for the CDR * @param value the new image url @@ -164,7 +190,7 @@ export class EditImageComponent implements CdrEditor, OnDestroy { this.control.setValue(this.columnContainer.value, { emitEvent: false }); this.logger.debug(this, 'form control value is set to', this.control.value); } - this.valueHasChanged.emit({value: this.control.value, forceEmit: true}); + this.valueHasChanged.emit({ value: this.control.value, forceEmit: true }); } this.control.markAsDirty(); diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-limited-value/edit-limited-value.component.html b/imxweb/projects/qbm/src/lib/cdr/edit-limited-value/edit-limited-value.component.html index 6cc4332b3..01fdcf038 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-limited-value/edit-limited-value.component.html +++ b/imxweb/projects/qbm/src/lib/cdr/edit-limited-value/edit-limited-value.component.html @@ -1,35 +1,30 @@ - + + + {{ columnContainer?.display | translate }} + + + {{ columnContainer?.displayValue }} + + {{ limitedValue.Description }} + + +
+ + +
+ + {{ '#LDS#This field is mandatory.' | translate }} + +
- - - - {{ columnContainer?.display | translate }} - - - - - - - {{ columnContainer?.display | translate }} - - - {{ columnContainer?.displayValue }} - - {{ limitedValue.Description }} - - -
- - -
- - {{ '#LDS#This field is mandatory.' | translate }} - -
+ + diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-multi-limited-value/edit-multi-limited-value.component.html b/imxweb/projects/qbm/src/lib/cdr/edit-multi-limited-value/edit-multi-limited-value.component.html index ed96b7e33..0fa8cb2b3 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-multi-limited-value/edit-multi-limited-value.component.html +++ b/imxweb/projects/qbm/src/lib/cdr/edit-multi-limited-value/edit-multi-limited-value.component.html @@ -1,4 +1,4 @@ - + {{ columnContainer?.display | translate }}
@@ -10,3 +10,11 @@ {{ '#LDS#This field is mandatory.' | translate }} + + + + + diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-multi-limited-value/edit-multi-limited-value.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-multi-limited-value/edit-multi-limited-value.component.ts index c7a07a78a..1ecb4cf47 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-multi-limited-value/edit-multi-limited-value.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-multi-limited-value/edit-multi-limited-value.component.ts @@ -92,6 +92,7 @@ export class EditMultiLimitedValueComponent implements CdrEditor, OnDestroy { this.updateRequested.subscribe(() => setTimeout(() => { this.initValues(); + this.control.updateValueAndValidity({ onlySelf: true, emitEvent: false }); }) ) ); diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-multi-value/edit-multi-value.component.html b/imxweb/projects/qbm/src/lib/cdr/edit-multi-value/edit-multi-value.component.html index 5dd00abf8..b3cb3e413 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-multi-value/edit-multi-value.component.html +++ b/imxweb/projects/qbm/src/lib/cdr/edit-multi-value/edit-multi-value.component.html @@ -1,7 +1,16 @@ - + {{ columnContainer?.display | translate }} {{ '#LDS#This field is mandatory.' | translate }} + + + + + + diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-multiline/edit-multiline.component.html b/imxweb/projects/qbm/src/lib/cdr/edit-multiline/edit-multiline.component.html index eaf6eb559..835321c87 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-multiline/edit-multiline.component.html +++ b/imxweb/projects/qbm/src/lib/cdr/edit-multiline/edit-multiline.component.html @@ -1,4 +1,4 @@ - + {{ columnContainer.display | translate }}