diff --git a/.commit b/.commit index 1f3a8e7c6..d6edabbfe 100644 --- a/.commit +++ b/.commit @@ -1 +1 @@ -47d44c74207f3d773d1ffa718dc102fa887f1cab +f1d5948d892d7b711ece214c0813f59849a9676b diff --git a/.sync-history b/.sync-history index a7f049007..e69de29bb 100644 --- a/.sync-history +++ b/.sync-history @@ -1 +0,0 @@ -41487b63c 2024-02-29 Merged PR 65170: #446439 - Develop new request url params \ No newline at end of file diff --git a/README.md b/README.md index e3d4fbc0a..01e5c1e5d 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,42 @@ # Identity Manager HTML5 applications ## Change log + +### May 6, 2024 + +We are integrating [Compodoc](https://compodoc.app/) to provide Angular documentation for our components. Please see the [Documentation](#documentation) section for more information on how to generate documentation. + +The package versions have been updated to 9.2.1. + +The `v92` branch has been updated with fixes for the following issues. + +**Important**: Integrating these changes will require a server-side hotfix for the issue 454356 to be installed. + +- 454675: Fixed an issue where sometimes a blank screen was displayed instead of the login page +- 446439: Fixed the preselection of a child service category on the new request page +- 440745: Fixed the preset of the search term on the new request page +- 454356: Data Explorer identities needs to load interactive entities. +- 453350/447417/453346: Fixed issues where the filter wizard could enter an inconsistent state +- 453132: Fixed an issue in the attestation policy editor where the "objects matching the condition" were not displayed hierarchically +- 438231: Added documentation of some basic components +- 450070: Fixed an issue where switching to another page while loading a page caused an error +- 453932: Fixed an issue where approving multiple requests in bulk could cause an error +- 439918: Fixed some issues in the product bundles component +- 450675: Fixed an issue where some help text was shown in an incorrect language +- 452886: Fixed an issue in the attestation policy editor +- 449616: Fixed an issue where reports could not be saved in format other then PDF +- 450404: Fixed an issue in the overview of attestation case approvers. +- 442324: Duplicate (additional) "Target system" columns in the Data Explorer +- 448531: Fixed a scrolling issue on the organizational chart view +- 449891: Fixed the inherited group membership pagination +- 448406: Fixed an issue where the list of entitlements assigned to a role was incomplete +- 418493: Fixed an issue in the overview of request approvers +- 447713: Fixed a styling issue with the login button +- 449124: Fixed the declaration of the metadata service +- 447723: Fixed an issue with additional columns in a grouped table view +- 442324: Fixed the duplicate "Target system" columns in the Data Explorer +- 447039: Fixed an issue where the FK picker could enter an inconsistent state + ### March 11, 2024 The v92 branch has been updated with fixes for the following issues. diff --git a/imxweb/.vscode/settings.json b/imxweb/.vscode/settings.json index e309221cd..4aad9f8c8 100644 --- a/imxweb/.vscode/settings.json +++ b/imxweb/.vscode/settings.json @@ -1,5 +1,8 @@ { "explorer.compactFolders": false, "editor.tabSize": 2, - "git.ignoreLimitWarning": true + "git.ignoreLimitWarning": true, + "cSpell.words": [ + "requestable" + ] } \ No newline at end of file diff --git a/imxweb/compodoc/assets/images/dashboard/1-dashboard.png b/imxweb/compodoc/assets/images/dashboard/1-dashboard.png new file mode 100644 index 000000000..edf1cfc10 Binary files /dev/null and b/imxweb/compodoc/assets/images/dashboard/1-dashboard.png differ diff --git a/imxweb/compodoc/assets/images/dashboard/2-block-identity-component.png b/imxweb/compodoc/assets/images/dashboard/2-block-identity-component.png new file mode 100644 index 000000000..b13885428 Binary files /dev/null and b/imxweb/compodoc/assets/images/dashboard/2-block-identity-component.png differ diff --git a/imxweb/compodoc/assets/images/dashboard/3-example-template-code.png b/imxweb/compodoc/assets/images/dashboard/3-example-template-code.png new file mode 100644 index 000000000..8c3e2c37c Binary files /dev/null and b/imxweb/compodoc/assets/images/dashboard/3-example-template-code.png differ diff --git a/imxweb/compodoc/assets/images/dashboard/4-example-component-code.png b/imxweb/compodoc/assets/images/dashboard/4-example-component-code.png new file mode 100644 index 000000000..a44f720ac Binary files /dev/null and b/imxweb/compodoc/assets/images/dashboard/4-example-component-code.png differ diff --git a/imxweb/compodoc/assets/images/dashboard/5-block-identity-button.png b/imxweb/compodoc/assets/images/dashboard/5-block-identity-button.png new file mode 100644 index 000000000..8acfc5764 Binary files /dev/null and b/imxweb/compodoc/assets/images/dashboard/5-block-identity-button.png differ diff --git a/imxweb/compodoc/assets/images/data_table/1-overall-structure.png b/imxweb/compodoc/assets/images/data_table/1-overall-structure.png new file mode 100644 index 000000000..75393aaad Binary files /dev/null and b/imxweb/compodoc/assets/images/data_table/1-overall-structure.png differ diff --git a/imxweb/compodoc/assets/images/data_table/2-first-version.png b/imxweb/compodoc/assets/images/data_table/2-first-version.png new file mode 100644 index 000000000..f8daef573 Binary files /dev/null and b/imxweb/compodoc/assets/images/data_table/2-first-version.png differ diff --git a/imxweb/compodoc/assets/images/data_table/3-table-with-search-enabled.png b/imxweb/compodoc/assets/images/data_table/3-table-with-search-enabled.png new file mode 100644 index 000000000..3759f2f26 Binary files /dev/null and b/imxweb/compodoc/assets/images/data_table/3-table-with-search-enabled.png differ diff --git a/imxweb/compodoc/assets/images/data_table/4-table-with-search.png b/imxweb/compodoc/assets/images/data_table/4-table-with-search.png new file mode 100644 index 000000000..6a09aad1f Binary files /dev/null and b/imxweb/compodoc/assets/images/data_table/4-table-with-search.png differ diff --git a/imxweb/compodoc/assets/images/menu/1-menu-bar.png b/imxweb/compodoc/assets/images/menu/1-menu-bar.png new file mode 100644 index 000000000..51181e40a Binary files /dev/null and b/imxweb/compodoc/assets/images/menu/1-menu-bar.png differ diff --git a/imxweb/compodoc/assets/images/menu/2-example-folder.png b/imxweb/compodoc/assets/images/menu/2-example-folder.png new file mode 100644 index 000000000..671e583f9 Binary files /dev/null and b/imxweb/compodoc/assets/images/menu/2-example-folder.png differ diff --git a/imxweb/compodoc/assets/images/menu/3-sub-item-menu.png b/imxweb/compodoc/assets/images/menu/3-sub-item-menu.png new file mode 100644 index 000000000..a857698c4 Binary files /dev/null and b/imxweb/compodoc/assets/images/menu/3-sub-item-menu.png differ diff --git a/imxweb/compodoc/samples/data_table.md b/imxweb/compodoc/samples/data_table.md new file mode 100644 index 000000000..f202a0893 --- /dev/null +++ b/imxweb/compodoc/samples/data_table.md @@ -0,0 +1,261 @@ +# Working with data tables + +A frequently recurring task is the representation of data in tables. The IMX QBM library offers components that facilitate the visualization of data and take the special IMX data structure into account. + +Data tables offer a lot of configuration possibilities. We will present the most important ones in this sample. + +The two most important modules in this context are "data-source-toolbar" (projects\qbm\src\lib\data-source-toolbar) and "data-table" (projects\qbm\src\lib\data-table). + +The basic structure consists of 3 elements, the data source toolbar, the actual table and a paginator. + +![overall Structure](../../assets/images/data_table/1-overall-structure.png) + +In addition to the ability to search, filter, etc, the Data Source Toolbar contains a Data Source component that is used by the Data Table and the Paginator to display data and move within the data set. Think of the DST (Data Source Toolbar) as a link between the Data Table and the Paginator. + + +The "Hello World" version of the Data Table component looks like as follow. + +> Code +``` html +

{{ '#LDS#Identities' | translate }}

+ + + + + + + +``` + +> Code +``` ts + +@Component({ + selector: 'imx-select-identity', + templateUrl: './select-identity.component.html', + styleUrls: ['./select-identity.component.scss'], +}) +export class SelectIdentityComponent implements OnInit { + public dstSettings: DataSourceToolbarSettings; + public readonly schema: EntitySchema; + public readonly DisplayColumns = DisplayColumns; + public navigationState: CollectionLoadParameters = { PageSize: 20 }; + + private displayedColumns: IClientProperty[] = []; + + constructor(private readonly qerApiClient: QerApiService) { + this.schema = this.qerApiClient.typedClient.PortalPersonAll.GetSchema(); + this.displayedColumns = [ + this.schema.Columns[DisplayColumns.DISPLAY_PROPERTYNAME], + this.schema.Columns.DefaultEmailAddress + ]; + } + + public async ngOnInit(): Promise { + await this.navigate(); + } + + public async onNavigationStateChanged(newState?: CollectionLoadParameters): Promise { + if (newState) { + this.navigationState = newState; + } + await this.navigate(); + } + + private async navigate(): Promise { + const data = await this.qerApiClient.typedClient.PortalPersonAll.Get(this.navigationState); + + this.dstSettings = { + displayedColumns: this.displayedColumns, + dataSource: data, + entitySchema: this.schema, + navigationState: this.navigationState, + }; + } +} + +``` + +The minimum set of properties that must be set are "EntitySchema", "DisplayColumns" and "CollectionLoadParameters". + +Three places in the .ts Datei are worth to be highlighted. + +> Code +``` ts + this.displayedColumns = [ + this.schema.Columns[DisplayColumns.DISPLAY_PROPERTYNAME], + this.schema.Columns.DefaultEmailAddress + ]; +``` + +"displayedColumns" defines which columns the table should display. + +> Code +``` ts + public async onNavigationStateChanged(newState?: CollectionLoadParameters): Promise { + if (newState) { + this.navigationState = newState; + } + await this.navigate(); + } +``` + +This event handler is called every time the state of the data changes, e.g. when the user navigates to the next page. + +> Code +``` ts + private async navigate(): Promise { + const data = await this.qerApiClient.typedClient.PortalPersonAll.Get(this.navigationState); + + this.dstSettings = { + displayedColumns: this.displayedColumns, + dataSource: data, + entitySchema: this.schema, + navigationState: this.navigationState, + }; + } +``` +The "navigate()" method retrievs data from the API server. The actual request is made by calling the API client ("this.qerApiClient.typedClient.PortalPersonAll.Get(this.navigationState)"). The concept of API clients is described in a separate sample. + +The first version of the component looks like this. + +![First Version](../../assets/images/data_table/2-first-version.png) + +## Designing the table + +The table above shows two fields that are rendered automatically. It is also possible to design columns manually. Whether the table is rendered automatically or manually is controlled by the "mode" input field. + +> Code +``` html + + +``` + +"mode" can take two values: "auto" and "manual". + +To display the previous table in manual mode, we need to add the two columns to the html template. + +> Code +``` html + + + +
{{ item.GetEntity().GetDisplay() }}
+
+
+ + +
{{ item.DefaultEmailAddress.Column.GetDisplayValue() }}
+
+
+
+``` +In the next step we will add a new column to the table containing a button and slightly modify the first column. + +The first column currently shows the default display of the object. We want to add a second row that indicates whether the person is a primary identity or not. + +> Code +``` html + + + +
{{ item.GetEntity().GetDisplay() }}
+
{{ item.IdentityType.Column.GetDisplayValue() }}
+
+
+ + +
{{ item.DefaultEmailAddress.Column.GetDisplayValue() }}
+
+
+
+``` + +Next we will add a third column that will contain a button. To display data of an object the table uses the "" tag. To display other types of elements, such as buttons, we use the "" tag. +Before we can display the button, we need to add the new synthetic column to the columns to be displayed. This is done in the *.ts file. + +> Code +``` ts + this.displayedColumns = [ + this.schema.Columns[DisplayColumns.DISPLAY_PROPERTYNAME], + this.schema.Columns.DefaultEmailAddress, + { + ColumnName: 'viewDetailsButton', + Type: ValType.String + } + ]; +``` + + +> Code +``` html + + + +
{{ item.GetEntity().GetDisplay() }}
+
{{ item.IdentityType.Column.GetDisplayValue() }}
+
+
+ + +
{{ item.DefaultEmailAddress.Column.GetDisplayValue() }}
+
+
+ + + + + +
+``` + +The extended component now looks like this. + +![Version with search enabled](../../assets/images/data_table/4-table-with-search.png) + +## Adding search + +Enabling search is pretty straightforward. +To do this, you must first enable the "search" option and secondly implement a method that processes the output of the "search" output parameter. + +The following code snippets shows these changes. + +> Code +``` html + +``` + +> Code +``` ts + public async onSearch(keywords: string): Promise { + this.navigationState.StartIndex = 0; + this.navigationState.search = keywords; + await this.navigate(); + } +``` + + +![Version with search enabled](../../assets/images/data_table/3-table-with-search-enabled.png) + + + + + diff --git a/imxweb/compodoc/samples/menu.md b/imxweb/compodoc/samples/menu.md new file mode 100644 index 000000000..bd441ac5c --- /dev/null +++ b/imxweb/compodoc/samples/menu.md @@ -0,0 +1,121 @@ +# Adding a menu to the Portal + +In the previous example, we added a tile component to the dashboard to block a user's account. + +In this example, we want to add a new item to the portal main menu to achieve the same functionality. + +The main menu (projects\qbm\src\lib\menu) is a central component, just like the dashboard. + +![Main menu](../../assets/images/menu/1-menu-bar.png) + +New menus and menu items are dynamically added to the main menu via a plugin system. + +Before we implement the menu, we have to add a route that navigates to the (currently empty) component where you can select the identity you want to block. + +We will name the new component SelectIdentityComponent. + +![Select Identity Component](../../assets/images/menu/2-example-folder.png) + +First we add a new entry to the routing table. + +> Code + +``` ts +const routes: Routes = [ + : + { + path: 'selectidentity', + component: SelectIdentityComponent + } +]; +``` + +Now we can add the new menu with the associated route. Again, as in the previous example, this can be done in the init service (init-service.ts). In the code snippet below, only the part where the menu is added is shown, the rest is hidden. + +> Code + +``` ts +: + +@Injectable({ providedIn: 'root' }) +export class InitService { + : + + public onInit(routes: Route[]): void { + this.addRoutes(routes); + : + } + + private addRoutes(routes: Route[]): void { + const config = this.router.config; + routes.forEach((route) => { + config.unshift(route); + }); + this.router.resetConfig(config); + } + + private setupMenu(): void { + this.menuService.addMenuFactories( + : + (preProps: string[], __: string[]) => { + return { + id: 'ROOT_SAMPLES', + title: '#LDS#Samples' + items: [ + { + id: 'SAMPLE_SELECT_IDENTITY', + route: 'selectidentity', + title: '#LDS#Select Identity' + }, + ], + }; + ); + } +} +``` + +You can add menus and menu items via the menu service (projects\qbm\src\lib\menu). The structure of the menu and the menu items is defined by the menu-item.interface.ts file. The most important properties are "id" and "title". If you add a menu item, the "route" property specifies the route of the component to be displayed. + +Here is an extract of the file. + +> Code + +``` ts +import { ProjectConfig } from 'imx-api-qbm'; +import { NavigationCommandsMenuItem } from './navigation-commands-menu-item.interface'; + +/** Represents a single menu item. */ +export interface MenuItem { + /** Unique identifier for the menu item. */ + readonly id?: string; + + /** Display name. */ + readonly title: string; + + /** Returns a descriptive text, intended for tooltips. */ + readonly description?: string; + + /** Property for simple navigation. */ + readonly route?: string; + + /** Property for sorting the items. */ + readonly sorting?: string; + + /** Property for complex navigation, including outlets etc. */ + navigationCommands?: NavigationCommandsMenuItem; + + /** Called when the menu item is clicked. */ + readonly trigger?: () => void; + + /** Submenu items. */ + items?: MenuItem[]; + +} + +export type MenuFactory = (preProps: string[], groups: string[], projectConfig: ProjectConfig) => MenuItem; + +``` + +The final result looks like this. + +![Final Result](../../assets/images/menu/3-sub-item-menu.png) \ No newline at end of file diff --git a/imxweb/compodoc/samples/samples.md b/imxweb/compodoc/samples/samples.md new file mode 100644 index 000000000..4400068fc --- /dev/null +++ b/imxweb/compodoc/samples/samples.md @@ -0,0 +1,6 @@ +# SDK Samples + +Currently, the following examples are available: +- [Adding a tile component to the dashboard](sdk-samples/adding-a-tile-component-to-the-dashboard.html) +- [Adding a menu to the Portal](sdk-samples/adding-a-menu-to-the-portal.html) +- [Working with data tables](sdk-samples/working-with-data-tables.html) diff --git a/imxweb/compodoc/samples/tiles.md b/imxweb/compodoc/samples/tiles.md new file mode 100644 index 000000000..a63df6f71 --- /dev/null +++ b/imxweb/compodoc/samples/tiles.md @@ -0,0 +1,119 @@ +# Adding a tile component to the dashboard + +The IMX Portal landing page consists of the IMX Dashboard component, which can be found in the QER library (projects\qer\src\lib\wport\start). + +It is composed of 3 sections with different tiles. + +![Dashboard](../../assets/images/dashboard/1-dashboard.png) + + +Tiles can be added dynamically to the dashboard. The following example gives an overview of the different types of tiles and demonstrates how to add a new element - the blue bordered tile - to the dashboard. + + +## Dashboard tiles + +The Tiles modules (projects\qbm\src\lib\tile) and (projects\qer\src\lib\tiles) offer different base components : + +- TileComponent (QBM) +- BadgeTileComponent (QER) +- IconTileComponent (QER) +- NotificationTileComponent (QER) + + +These components are variations of the same concept. In the further course we will implement a new tile based on the IconTileComponent. + + +## Implementing the "Block Identity" Tile + +What is the fictitious but realistic scenario we will implement? +There is a security breach and an administrator wants to block the account of the affected identity. The implementation of this scenario will span several examples. Here we will first create the tile that can trigger that process. + +### Creating the "Block Identity" Component + +First we need to create the "Block Identity" component. We assume that the reader has a basic knowledge of the Angular framework and knows how to create components, services, etc. + +The component consists of 3 files, but we will not pay further attention to the stylesheet. + +![Block Identity Component](../../assets/images/dashboard/2-block-identity-component.png) + +### The anatomy of the "Block Identity" component + +Basically, the component consists of 2 parts, a Typescript file and the corresponding HTML template. + + +The HTML template is based on the previously mentioned IconTileComponent component. + +> Code + +``` html + + + + + +``` + +The IconTileComponent expects some input fields like "caption", "image" or "subtitle". Which tiles components expect which inputs can be found in the Tiles Module. + +> NOTE + +> IMX components and applications are based on the One Identity Elemental UI framework, which in turn extends Angular Material. The "eui-icon" tag is such an Elemental UI component (https://elemental.dev.oneidentity.com/) + +The corresponding *.ts component is not very exciting. On the other hand it sets the "Description" property/input used in the template and implements the (dummy) "block()" method. + +> Code +``` ts +import { Component } from '@angular/core'; + +@Component({ + selector: 'imx-block-identity', + templateUrl: './block-identity.component.html', + styleUrls: ['./block-identity.component.scss'] +}) +export class BlockIdentityComponent { + public description = 'Blocks an identity and marks the identity as security risk.'; + + constructor() { } + + public block(): void { + alert('Block Tile Clicked'); + } + +} +``` + +That's all we need at this time for the "Block Identity" component. + +## Wiring it up +The next step is to include the component into the dashboard. +To do this, we must make it available to the web application, in this case the portal. +This is done in the init service of the corresponding library (don't worry, there will be more samples on the topic). + +Here is the relevant section of the service. + +> Code + +``` ts +@Injectable({ providedIn: 'root' }) +export class InitService { + public onInit(routes: Route[]): void { + this.extService.register('Dashboard-MediumTiles', { + instance: BlockIdentityComponent, + }); + } +} +``` + +The final result looks like this. + + +![block-identity.component.ts](../../assets/images/dashboard/5-block-identity-button.png) + + + + + diff --git a/imxweb/custom-theme/package.json b/imxweb/custom-theme/package.json index f4bfb5e0c..e7f5dfe70 100644 --- a/imxweb/custom-theme/package.json +++ b/imxweb/custom-theme/package.json @@ -1,6 +1,6 @@ { "name": "custom-theme", - "version": "9.2.0", + "version": "9.2.1", "private": true, "scripts": { "build": "sass custom-theme.scss:custom-theme.css --load-path=..\\node_modules" diff --git a/imxweb/imx-modules/imx-api-aad.tgz b/imxweb/imx-modules/imx-api-aad.tgz index a642a0ab1..b9da9f74a 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 c56353b6c..c47c2f7cd 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 68124e7f8..353e32491 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 4f3caf4b4..05e83b24d 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 4a0677067..ccdd8c8bb 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 bfd20e551..66fa0a585 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 bba3ce95d..7fdbb7540 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 6e2a5935c..99a49756b 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 3a0ddb76c..3198ddd2c 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 db36d77cd..9fbfea458 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 30a794265..bdd35084c 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 d97914308..46b5dbf33 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 114a4d653..e49d40b66 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 2bdbb5a43..c7b37acab 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 e214f843e..9a025df64 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 e9b966344..1ca610ba9 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 6041a60e3..e6d9c8a8d 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 4cb4cfef1..36900ff1a 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 a4bb73659..51648c9cb 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 78e874d66..b6a37ec44 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 e55521c45..af7251247 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/package-lock.json b/imxweb/package-lock.json index 411f3c3ab..850f8f221 100644 --- a/imxweb/package-lock.json +++ b/imxweb/package-lock.json @@ -1,6 +1,6 @@ { "name": "imxweb", - "version": "9.2.0", + "version": "9.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/imxweb/package.json b/imxweb/package.json index f5e916d4e..70d61de05 100644 --- a/imxweb/package.json +++ b/imxweb/package.json @@ -1,6 +1,6 @@ { "name": "imxweb", - "version": "9.2.0", + "version": "9.2.1", "scripts": { "ng": "ng", "start": "ng serve --configuration development", diff --git a/imxweb/projects/aad/.compodocrc.json b/imxweb/projects/aad/.compodocrc.json index 887a5929c..c4e734351 100644 --- a/imxweb/projects/aad/.compodocrc.json +++ b/imxweb/projects/aad/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - AAD Library", - "output": "../../documentation/aad", + "output": "../../documentation/v92/aad", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/aad/package.json b/imxweb/projects/aad/package.json index d0291f568..df0bfba60 100644 --- a/imxweb/projects/aad/package.json +++ b/imxweb/projects/aad/package.json @@ -1,6 +1,6 @@ { "name": "aad", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-aad" diff --git a/imxweb/projects/aob/.compodocrc.json b/imxweb/projects/aob/.compodocrc.json index 6967afd81..6bac08861 100644 --- a/imxweb/projects/aob/.compodocrc.json +++ b/imxweb/projects/aob/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - AOB Library", - "output": "../../documentation/aob", + "output": "../../documentation/v92/aob", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/aob/package.json b/imxweb/projects/aob/package.json index e19f2b74b..5c8ce28bf 100644 --- a/imxweb/projects/aob/package.json +++ b/imxweb/projects/aob/package.json @@ -1,6 +1,6 @@ { "name": "aob", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-aob" diff --git a/imxweb/projects/aob/src/lib/entitlements/entitlement-add/entitlements-add.component.ts b/imxweb/projects/aob/src/lib/entitlements/entitlement-add/entitlements-add.component.ts index 3f8b567af..d683dde62 100644 --- a/imxweb/projects/aob/src/lib/entitlements/entitlement-add/entitlements-add.component.ts +++ b/imxweb/projects/aob/src/lib/entitlements/entitlement-add/entitlements-add.component.ts @@ -37,7 +37,7 @@ import { StorageService, MetadataService, DataSourceToolbarFilter, - CdrFactoryService + CdrFactoryService, } from 'qbm'; import { CollectionLoadParameters, IClientProperty, EntitySchema, DisplayColumns, TypedEntity, FilterData } from 'imx-qbm-dbts'; import { EntitlementsService } from '../entitlements.service'; @@ -53,7 +53,7 @@ const helperAlertKey = `${HELPER_ALERT_KEY_PREFIX}_addNewEntitlements`; @Component({ selector: 'imx-entitlements-add', templateUrl: './entitlements-add.component.html', - styleUrls: ['./entitlements-add.component.scss'] + styleUrls: ['./entitlements-add.component.scss'], }) export class EntitlementsAddComponent implements OnInit { public readonly EntitlementsType = EntitlementsType; // Enables use of this Enum in Angular Templates. @@ -70,7 +70,9 @@ export class EntitlementsAddComponent implements OnInit { public selections: TypedEntity[]; public readonly browserCulture: string; public entitlementSourceTypes: EntitlementSourceType[] = []; - public get showHelperAlert(): boolean { return !this.storageService.isHelperAlertDismissed(helperAlertKey); } + public get showHelperAlert(): boolean { + return !this.storageService.isHelperAlertDismissed(helperAlertKey); + } private filters: DataSourceToolbarFilter[]; @@ -83,7 +85,8 @@ export class EntitlementsAddComponent implements OnInit { private readonly storageService: StorageService, private readonly metaData: MetadataService, private readonly sidesheet: EuiSidesheetService, - private readonly translateService: TranslateService) { + private readonly translateService: TranslateService, + ) { this.selectedSourceType = data.defaultType; this.isSystemRolesEnabled = data.isSystemRolesEnabled; @@ -91,8 +94,7 @@ export class EntitlementsAddComponent implements OnInit { } public async ngOnInit(): Promise { - - await this.metaData.update(['ESet', 'UNSGroup', 'QERResource', 'RPSReport', 'TSBAccountDef']); + await this.metaData.updateNonExisting(['ESet', 'UNSGroup', 'QERResource', 'RPSReport', 'TSBAccountDef']); this.entitlementSourceTypes = [ { entitlementsType: EntitlementsType.UnsGroup, display: this.metaData.tables.UNSGroup.Display }, @@ -112,7 +114,6 @@ export class EntitlementsAddComponent implements OnInit { this.storageService.storeHelperAlertDismissal(helperAlertKey); } - /** * Call to toggle the view and show entitlements candidates or roles candidates. */ @@ -129,15 +130,17 @@ export class EntitlementsAddComponent implements OnInit { } public async onCreateRole(): Promise { - const entitlementSystemRoleInput: EntitlementSystemRoleInput - = await this.sidesheet.open(SystemRoleConfigComponent, { + const entitlementSystemRoleInput: EntitlementSystemRoleInput = await this.sidesheet + .open(SystemRoleConfigComponent, { // TODO: make LDS Heading title: await this.translateService.get('#LDS#Create new system role').toPromise(), padding: '0px', width: 'max(500px, 50%)', testId: 'create-role-sidesheet', - data: { uid: this.data.uidApplication, createOnly: true } - }).afterClosed().toPromise(); + data: { uid: this.data.uidApplication, createOnly: true }, + }) + .afterClosed() + .toPromise(); if (!entitlementSystemRoleInput) { this.logger.debug(this, 'role dialog canceled'); @@ -151,21 +154,24 @@ export class EntitlementsAddComponent implements OnInit { } public async onAddToRole(): Promise { - const entitlementSystemRoleInput: EntitlementSystemRoleInput - = await this.sidesheet.open(SystemRoleConfigComponent, { + const entitlementSystemRoleInput: EntitlementSystemRoleInput = await this.sidesheet + .open(SystemRoleConfigComponent, { title: await this.translateService.get('#LDS#Heading Merge Application Entitlements into System Role').toPromise(), padding: '0px', width: 'max(500px, 50%)', testId: 'add-to-existing-role-sidesheet', - data: { uid: this.data.uidApplication, createOnly: false } - }).afterClosed().toPromise(); + data: { uid: this.data.uidApplication, createOnly: false }, + }) + .afterClosed() + .toPromise(); if (!entitlementSystemRoleInput) { this.logger.debug(this, 'role dialog canceled'); return; } - const keys = this.selections.map((elem: TypedEntity) => CdrFactoryService.tryGetColumn(elem.GetEntity(), 'XObjectKey')?.GetValue()) + const keys = this.selections + .map((elem: TypedEntity) => CdrFactoryService.tryGetColumn(elem.GetEntity(), 'XObjectKey')?.GetValue()) .filter((elem: string) => elem != null); if (keys.length < this.selections.length) { @@ -207,7 +213,7 @@ export class EntitlementsAddComponent implements OnInit { this.selectedSourceType = type; this.entitySchema = this.entitlementsProvider.candidateSchema(type); let overlayRef: OverlayRef; - setTimeout(() => overlayRef = this.busyService.show()); + setTimeout(() => (overlayRef = this.busyService.show())); try { this.filters = (await this.entitlementsProvider.getDataModel(type))?.Filters; } finally { @@ -223,7 +229,7 @@ export class EntitlementsAddComponent implements OnInit { private async updateCandidates(): Promise { let overlayRef: OverlayRef; - setTimeout(() => overlayRef = this.busyService.show()); + setTimeout(() => (overlayRef = this.busyService.show())); try { this.logger.debug(this, 'Get candidates from the server...'); const data = await this.entitlementsProvider.getCandidates(this.selectedSourceType, this.navigationState); @@ -234,16 +240,17 @@ export class EntitlementsAddComponent implements OnInit { entitySchema: this.entitySchema, navigationState: this.navigationState, filters: this.filters, - filterTree: this.selectedSourceType === EntitlementsType.UnsGroup ? - { - filterMethode: async (parentkey) => { - return this.entitlementsProvider.getEntitlementsFilterTree({ - parentkey, - }); - }, - multiSelect: false, - } - : undefined + filterTree: + this.selectedSourceType === EntitlementsType.UnsGroup + ? { + filterMethode: async (parentkey) => { + return this.entitlementsProvider.getEntitlementsFilterTree({ + parentkey, + }); + }, + multiSelect: false, + } + : undefined, }; if (data == null) { @@ -254,21 +261,22 @@ export class EntitlementsAddComponent implements OnInit { } } - private getDisplayedColumnsForEntitlement( - entitySchema: EntitySchema, - type: EntitlementsType): IClientProperty[] { - if (!entitySchema) { return []; } + private getDisplayedColumnsForEntitlement(entitySchema: EntitySchema, type: EntitlementsType): IClientProperty[] { + if (!entitySchema) { + return []; + } switch (type) { case EntitlementsType.UnsGroup: return [ entitySchema.Columns[DisplayColumns.DISPLAY_PROPERTYNAME], entitySchema.Columns.CanonicalName, - entitySchema.Columns.UID_UNSRoot + entitySchema.Columns.UID_UNSRoot, ]; default: return [entitySchema.Columns[DisplayColumns.DISPLAY_PROPERTYNAME]]; } } - public LdsInfoAlert = '#LDS#Here you can assign application entitlements to the application. Once assigned, you can publish these application entitlements so that they can be requested.'; + public LdsInfoAlert = + '#LDS#Here you can assign application entitlements to the application. Once assigned, you can publish these application entitlements so that they can be requested.'; } diff --git a/imxweb/projects/aob/src/lib/entitlements/entitlement-edit-auto-add/entitlement-edit-auto-add.component.html b/imxweb/projects/aob/src/lib/entitlements/entitlement-edit-auto-add/entitlement-edit-auto-add.component.html index 317f5c16a..2d6a5d5a6 100644 --- a/imxweb/projects/aob/src/lib/entitlements/entitlement-edit-auto-add/entitlement-edit-auto-add.component.html +++ b/imxweb/projects/aob/src/lib/entitlements/entitlement-edit-auto-add/entitlement-edit-auto-add.component.html @@ -1,6 +1,6 @@
- +
diff --git a/imxweb/projects/aob/src/lib/entitlements/entitlement-edit-auto-add/entitlement-edit-auto-add.component.ts b/imxweb/projects/aob/src/lib/entitlements/entitlement-edit-auto-add/entitlement-edit-auto-add.component.ts index 9054ec639..b942b2618 100644 --- a/imxweb/projects/aob/src/lib/entitlements/entitlement-edit-auto-add/entitlement-edit-auto-add.component.ts +++ b/imxweb/projects/aob/src/lib/entitlements/entitlement-edit-auto-add/entitlement-edit-auto-add.component.ts @@ -49,8 +49,11 @@ export class EntitlementEditAutoAddComponent implements OnDestroy { public sqlExpression: SqlWizardExpression; - public checkChanges(): void { - this.exprHasntChanged = _.isEqual(this.data.sqlExpression, this.sqlExpression); + public checkChanges(expression: SqlExpression): void { + this.exprHasntChanged = _.isEqual(expression, this.sqlExpression.Expression); + if (!this.exprHasntChanged) { + this.sqlExpression.Expression = expression; + } } public get controlsInvalid(): boolean { @@ -99,7 +102,8 @@ export class EntitlementEditAutoAddComponent implements OnDestroy { } const entitlementToAdd = this.entitlementToAddWrapperService.buildTypedEntities(elements); - const saveChanges: { save: boolean; map: boolean } = await this.sidesheet.open(MappedEntitlementsPreviewComponent, { + const saveChanges: { save: boolean; map: boolean } = await this.sidesheet + .open(MappedEntitlementsPreviewComponent, { title: await this.translateService.get('#LDS#Heading View Matching Application Entitlements').toPromise(), subTitle: this.data.application.GetEntity().GetDisplay(), padding: '0px', diff --git a/imxweb/projects/aob/src/lib/entitlements/entitlements.component.ts b/imxweb/projects/aob/src/lib/entitlements/entitlements.component.ts index 3cedef830..3321f6199 100644 --- a/imxweb/projects/aob/src/lib/entitlements/entitlements.component.ts +++ b/imxweb/projects/aob/src/lib/entitlements/entitlements.component.ts @@ -135,7 +135,7 @@ export class EntitlementsComponent implements OnChanges { private readonly settingsService: SettingsService, private readonly systemInfo: SystemInfoService, private readonly metadata: MetadataService, - private readonly autoAddService: EntitlementEditAutoAddService + private readonly autoAddService: EntitlementEditAutoAddService, ) { this.entitySchema = entitlementsProvider.entitlementSchema; this.displayedColumns = [ @@ -309,7 +309,7 @@ export class EntitlementsComponent implements OnChanges { try { this.logger.debug( this, - `Publishing entitlement(s)/role(s) ${publishData.date && publishData.publishFuture ? `on ${publishData.date}` : 'now'}` + `Publishing entitlement(s)/role(s) ${publishData.date && publishData.publishFuture ? `on ${publishData.date}` : 'now'}`, ); const publishCount = await this.entitlementsProvider.publish(selectedEntitlements, publishData); @@ -477,7 +477,7 @@ export class EntitlementsComponent implements OnChanges { return; } - this.unassignedDisabled = selection.some(elem => elem.IsDynamic.value === true); + this.unassignedDisabled = selection.some((elem) => elem.IsDynamic.value === true); let foundPublished = false; let foundUnpublished = false; @@ -556,7 +556,7 @@ export class EntitlementsComponent implements OnChanges { if (success) { this.snackbar.open( { key: '#LDS#The application entitlements have been successfully merged into the system role and added to application.' }, - '#LDS#Close' + '#LDS#Close', ); await this.getData(); } @@ -598,7 +598,7 @@ export class EntitlementsComponent implements OnChanges { } this.updatedTableNames.push(...newTableNamesForUpdate); this.logger.trace(this, 'following items are added to the list of table names', newTableNamesForUpdate); - return this.metadata.update([...new Set(newTableNamesForUpdate)]); + return this.metadata.updateNonExisting([...new Set(newTableNamesForUpdate)]); } private async createEntitlementWrapper(entitlement: PortalEntitlement): Promise { diff --git a/imxweb/projects/apc/.compodocrc.json b/imxweb/projects/apc/.compodocrc.json index 73d69ccf8..442e29f03 100644 --- a/imxweb/projects/apc/.compodocrc.json +++ b/imxweb/projects/apc/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - APC Library", - "output": "../../documentation/apc", + "output": "../../documentation/v92/apc", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/apc/package.json b/imxweb/projects/apc/package.json index e9bae9c71..344a4c47d 100644 --- a/imxweb/projects/apc/package.json +++ b/imxweb/projects/apc/package.json @@ -1,6 +1,6 @@ { "name": "apc", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-apc" diff --git a/imxweb/projects/apc/src/lib/software/software.component.ts b/imxweb/projects/apc/src/lib/software/software.component.ts index 908501532..cb659972d 100644 --- a/imxweb/projects/apc/src/lib/software/software.component.ts +++ b/imxweb/projects/apc/src/lib/software/software.component.ts @@ -29,7 +29,14 @@ import { EuiSidesheetService } from '@elemental-ui/core'; import { TranslateService } from '@ngx-translate/core'; import { CollectionLoadParameters, DataModel, DisplayColumns, EntitySchema, IClientProperty, TypedEntity } from 'imx-qbm-dbts'; -import { BusyService, DataSourceToolbarSettings, HelpContextualValues, LdsReplacePipe, MetadataService, SideNavigationComponent } from 'qbm'; +import { + BusyService, + DataSourceToolbarSettings, + HelpContextualValues, + LdsReplacePipe, + MetadataService, + SideNavigationComponent, +} from 'qbm'; import { SoftwareSidesheetComponent } from './software-sidesheet/software-sidesheet.component'; import { SoftwareService } from './software.service'; @@ -56,14 +63,14 @@ export class SoftwareComponent implements OnInit, SideNavigationComponent { private readonly metadata: MetadataService, private readonly sidesheet: EuiSidesheetService, private readonly ldsReplace: LdsReplacePipe, - private readonly translate: TranslateService + private readonly translate: TranslateService, ) {} public async ngOnInit(): Promise { const isBusy = this.busyService.beginBusy(); try { this.entitySchema = this.resourceProvider.getSchema(false); - await this.metadata.update(['Application']); + await this.metadata.updateNonExisting(['Application']); this.dataModel = await this.resourceProvider.getDataModel(undefined); } finally { isBusy.endBusy(); diff --git a/imxweb/projects/att/.compodocrc.json b/imxweb/projects/att/.compodocrc.json index 1996edd69..5a25aeb97 100644 --- a/imxweb/projects/att/.compodocrc.json +++ b/imxweb/projects/att/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - ATT Library", - "output": "../../documentation/att", + "output": "../../documentation/v92att", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/att/package.json b/imxweb/projects/att/package.json index 25c4431e5..b6ccecc96 100644 --- a/imxweb/projects/att/package.json +++ b/imxweb/projects/att/package.json @@ -1,6 +1,6 @@ { "name": "att", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-att" diff --git a/imxweb/projects/att/src/lib/decision/attestation-case.component.ts b/imxweb/projects/att/src/lib/decision/attestation-case.component.ts index 9aa42e029..6b10c4673 100644 --- a/imxweb/projects/att/src/lib/decision/attestation-case.component.ts +++ b/imxweb/projects/att/src/lib/decision/attestation-case.component.ts @@ -109,7 +109,7 @@ export class AttestationCaseComponent implements OnDestroy, OnInit { private readonly systemInfoService: SystemInfoService, private readonly logger: ClassloggerService, private readonly metadataService: MetadataService, - authentication: AuthenticationService + authentication: AuthenticationService, ) { this.case = data.case; this.approvers = data.approvers; @@ -236,13 +236,13 @@ export class AttestationCaseComponent implements OnDestroy, OnInit { this.data.case.data?.RelatedObjects.map(async (relatedObject) => { const objectType = DbObjectKey.FromXml(relatedObject.ObjectKey); if (!this.metadataService.tables[objectType.TableName]) { - await this.metadataService.update([objectType.TableName]); + await this.metadataService.updateNonExisting([objectType.TableName]); } return { ObjectKey: relatedObject.ObjectKey, Display: `${relatedObject.Display} - ${this.metadataService.tables[objectType.TableName].DisplaySingular}`, }; - }) + }), )) || []; } diff --git a/imxweb/projects/att/src/lib/policies/attestation-cases/attestation-cases-tree-database.service.ts b/imxweb/projects/att/src/lib/policies/attestation-cases/attestation-cases-tree-database.service.ts new file mode 100644 index 000000000..47d64bbb7 --- /dev/null +++ b/imxweb/projects/att/src/lib/policies/attestation-cases/attestation-cases-tree-database.service.ts @@ -0,0 +1,122 @@ +/* + * 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 { CollectionLoadParameters, EntityData, EntitySchema, HierarchyData, IEntity, TypedEntityBuilder, ValType } from 'imx-qbm-dbts'; +import { BusyService, SettingsService, TreeDatabase, TreeNodeResultParameter } from 'qbm'; +import { PolicyService } from '../policy.service'; +import { AttestationCasesComponentParameter } from './attestation-cases-component-parameter.interface'; +import { PortalAttestationFilterMatchingobjects } from 'imx-api-att'; + +export class AttestationCasesTreeDatabaseService extends TreeDatabase { + private entitySchema: EntitySchema; + private builder: TypedEntityBuilder; + + constructor( + private readonly policyService: PolicyService, + private readonly settingsService: SettingsService, + private readonly data: AttestationCasesComponentParameter, + public busyService: BusyService, + ) { + super(); + this.canSearch = false; + this.builder = new TypedEntityBuilder(PortalAttestationFilterMatchingobjects); + } + + public async getData( + showLoading: boolean, + parameter: CollectionLoadParameters = { ParentKey: '' /* first level */ } + ): Promise { + if (this.data == null) { + return { entities: [], canLoadMore: false, totalCount: 0 }; + } + + const isBusy = showLoading ? this.busyService.beginBusy() : undefined; + + try { + const navigationState = { + ...parameter, + ...{ + PageSize: this.settingsService.DefaultPageSize, + StartIndex: parameter.StartIndex ? parameter.StartIndex : 0, + }, + }; + + const data = await this.policyService.getObjectsForFilterUntyped( + this.data.uidobject, + this.data.uidPickCategory, + { Elements: this.data.filter, ConcatenationType: this.data.concat }, + navigationState + ); + + if (data) { + const nodeEntities = await Promise.all( + data.Entities.map(async (elem): Promise => { + return (await this.buildEntityWithHasChildren(elem, data.Hierarchy))?.GetEntity(); + }) + ); + this.dataChanged.emit(nodeEntities.filter((elem) => elem != null)); + return { + entities: nodeEntities.filter((elem) => elem != null), + canLoadMore: navigationState.StartIndex + navigationState.PageSize < data.TotalCount, + totalCount: data.TotalCount, + }; + } + return { entities: [], canLoadMore: false, totalCount: 0 }; + } finally { + isBusy?.endBusy(); + } + } + + public async prepare(entitySchema: EntitySchema, withReload: boolean): Promise { + this.entitySchema = entitySchema; + if (withReload) { + this.reloadData(); + } + } + + /** adds a hasChildren column to the entity */ + private async buildEntityWithHasChildren(entityData: EntityData, data: HierarchyData): Promise { + if (!this.entitySchema) { + return undefined; + } + const entity = this.builder.buildReadWriteEntity({ entitySchema: this.entitySchema, entityData }); + entity.GetEntity().AddColumns([ + { + Type: ValType.Bool, + IsMultiValued: true, + ColumnName: 'HasChildren', + MinLen: 0, + Display: '', + }, + ]); + await entity + .GetEntity() + .GetColumn('HasChildren') + .PutValue(data ? data.EntitiesWithHierarchy.some((elem) => entityData.Keys.some((key) => key === elem)) : false); + + return entity; + } +} 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 ce9a97de2..818858588 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 @@ -17,7 +17,8 @@

{{ '#LDS#A sample is assigned to this attestation policy. You can start the attestation only for all objects in the sample.' | translate }}

{{ '#LDS#Note: After you have started an attestation, it may take some time until the associated attestation cases are created.' | translate }}

- -
- - + + + +
{{ data.GetEntity().GetDisplay() }}
+
+ {{ data.GetEntity().GetDisplayLong() }} +
+
+
+ + + + + +
+ + -
+
+ + + + + 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 df7dd56c2..63bab7993 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 @@ -31,9 +31,19 @@ import { TranslateService } from '@ngx-translate/core'; import { PortalAttestationFilterMatchingobjects } from 'imx-api-att'; import { CollectionLoadParameters, DisplayColumns, ValType, EntitySchema } from 'imx-qbm-dbts'; -import { ClassloggerService, ClientPropertyForTableColumns, ConfirmationService, DataSourceToolbarSettings, LdsReplacePipe, SettingsService, SnackBarService } from 'qbm'; +import { + BusyService, + ClassloggerService, + ClientPropertyForTableColumns, + ConfirmationService, + DataSourceToolbarSettings, + LdsReplacePipe, + SettingsService, + SnackBarService, +} from 'qbm'; import { PolicyService } from '../policy.service'; import { AttestationCasesComponentParameter } from './attestation-cases-component-parameter.interface'; +import { AttestationCasesTreeDatabaseService } from './attestation-cases-tree-database.service'; @Component({ templateUrl: './attestation-cases.component.html', @@ -47,22 +57,26 @@ export class AttestationCasesComponent implements OnInit { public deactivatedChecked = false; public selectedItems: PortalAttestationFilterMatchingobjects[] = []; + public treeDatabase: AttestationCasesTreeDatabaseService; + public entitySchema = PortalAttestationFilterMatchingobjects.GetEntitySchema(); private navigationState: CollectionLoadParameters; private displayedColumns: ClientPropertyForTableColumns[]; private threshold = -1; + public hierarchical: boolean; + private busyService = new BusyService(); constructor( public readonly sidesheetRef: EuiSidesheetRef, @Inject(EUI_SIDESHEET_DATA) public readonly data: AttestationCasesComponentParameter, private readonly policyService: PolicyService, - private readonly busyService: EuiLoadingService, + private readonly busyServiceEui: EuiLoadingService, private readonly snackbar: SnackBarService, private readonly confirmationService: ConfirmationService, - settingsService: SettingsService, + private readonly settingsService: SettingsService, private readonly translate: TranslateService, private readonly ldsReplace: LdsReplacePipe, - private readonly logger: ClassloggerService + private readonly logger: ClassloggerService, ) { this.navigationState = { PageSize: settingsService.DefaultPageSize, StartIndex: 0, ParentKey: '' }; this.entitySchemaPolicy = policyService.AttestationMatchingObjectsSchema; @@ -72,7 +86,7 @@ export class AttestationCasesComponent implements OnInit { this.displayedColumns.push({ ColumnName: 'runMethod', Type: ValType.String, - untranslatedDisplay: '#LDS#Actions' + untranslatedDisplay: '#LDS#Actions', }); } } @@ -83,16 +97,28 @@ export class AttestationCasesComponent implements OnInit { public async ngOnInit(): Promise { let overlayRef: OverlayRef; - setTimeout(() => (overlayRef = this.busyService.show())); + setTimeout(() => (overlayRef = this.busyServiceEui.show())); try { this.threshold = await this.policyService.getCasesThreshold(); + this.hierarchical = + ( + await this.policyService.getObjectsForFilterUntyped( + this.data.uidobject, + this.data.uidPickCategory, + { Elements: this.data.filter, ConcatenationType: this.data.concat }, + { PageSize: -1 } + ) + ).Hierarchy != null; + + this.treeDatabase = new AttestationCasesTreeDatabaseService(this.policyService, this.settingsService, this.data, this.busyService); + await this.treeDatabase.prepare(this.entitySchema, false); } finally { setTimeout(async () => { - this.busyService.hide(overlayRef); + this.busyServiceEui.hide(overlayRef); }); } - return this.navigate(); + return this.hierarchical ? this.navigateTree() : this.navigate(); } public get hasSampleData(): boolean { @@ -113,7 +139,7 @@ export class AttestationCasesComponent implements OnInit { if (count <= this.threshold || (await this.confirmCreation())) { let overlayRef: OverlayRef; - setTimeout(() => (overlayRef = this.busyService.show())); + setTimeout(() => (overlayRef = this.busyServiceEui.show())); try { await this.policyService.createAttestationRun( @@ -136,19 +162,23 @@ export class AttestationCasesComponent implements OnInit { ); } finally { setTimeout(async () => { - this.busyService.hide(overlayRef); + this.busyServiceEui.hide(overlayRef); this.sidesheetRef.close(true); }); } } } + private async navigateTree(): Promise { + await this.treeDatabase.prepare(this.entitySchema, true); + } + private async navigate(): Promise { let overlayRef: OverlayRef; - setTimeout(() => (overlayRef = this.busyService.show())); + setTimeout(() => (overlayRef = this.busyServiceEui.show())); try { - const datasource = await this.policyService.getObjectsForFilter( + const dataSource = await this.policyService.getObjectsForFilter( this.data.uidobject, this.data.uidPickCategory, { Elements: this.data.filter, ConcatenationType: this.data.concat }, @@ -157,21 +187,23 @@ export class AttestationCasesComponent implements OnInit { this.dstSettings = { displayedColumns: this.displayedColumns, - dataSource: datasource, + dataSource, entitySchema: this.entitySchemaPolicy, - navigationState: this.navigationState + navigationState: this.navigationState, }; this.logger.debug(this, 'matching objects table navigated to', this.navigationState); } finally { - setTimeout(() => this.busyService.hide(overlayRef)); + setTimeout(() => this.busyServiceEui.hide(overlayRef)); } } private async confirmCreation(): Promise { const message = this.ldsReplace.transform( await this.translate - .get('#LDS#You have selected more than {0} objects. Attestation of the selected objects may take some time and generate notifications to many approvers. Are you sure you want to start the attestation for the selected objects?') + .get( + '#LDS#You have selected more than {0} objects. Attestation of the selected objects may take some time and generate notifications to many approvers. Are you sure you want to start the attestation for the selected objects?' + ) .toPromise(), this.threshold ); diff --git a/imxweb/projects/att/src/lib/policies/editors/edit-generic.component.ts b/imxweb/projects/att/src/lib/policies/editors/edit-generic.component.ts index 61a8e5809..8e32b99c8 100644 --- a/imxweb/projects/att/src/lib/policies/editors/edit-generic.component.ts +++ b/imxweb/projects/att/src/lib/policies/editors/edit-generic.component.ts @@ -43,17 +43,21 @@ export class EditGenericComponent implements OnChanges { @Output() public valueChanged = new EventEmitter(); - constructor(private readonly logger: ClassloggerService, private readonly metaData: MetadataService) {} + constructor( + private readonly logger: ClassloggerService, + private readonly metaData: MetadataService, + ) {} - public async ngOnChanges(): Promise{ + public async ngOnChanges(): Promise { const tableName = this.filterElementModel.getTableName(); if (tableName == null) { this.cdr = new BaseCdr(this.filterElementModel.columnForFilter); } else { - await this.metaData.update([this.filterElementModel.getTableName()]); - this.cdr = new BaseCdr(this.filterElementModel.columnForFilter, - this.metaData.tables[this.filterElementModel.getTableName()] - .Columns[this.filterElementModel.getColumnName()].Display); + await this.metaData.updateNonExisting([this.filterElementModel.getTableName()]); + this.cdr = new BaseCdr( + this.filterElementModel.columnForFilter, + this.metaData.tables[this.filterElementModel.getTableName()].Columns[this.filterElementModel.getColumnName()].Display, + ); } } diff --git a/imxweb/projects/att/src/lib/policies/policy.module.ts b/imxweb/projects/att/src/lib/policies/policy.module.ts index c2017a070..877695653 100644 --- a/imxweb/projects/att/src/lib/policies/policy.module.ts +++ b/imxweb/projects/att/src/lib/policies/policy.module.ts @@ -40,7 +40,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { LdsReplaceModule, DataSourceToolbarModule, DataTableModule, CdrModule, ClassloggerModule, HelpContextualModule } from 'qbm'; +import { LdsReplaceModule, DataSourceToolbarModule, DataTableModule, CdrModule, ClassloggerModule, HelpContextualModule, DataTreeWrapperModule } from 'qbm'; import { UserModule, StatisticsModule } from 'qer'; import { EditNameComponent } from './editors/edit-name.component'; import { EditGenericComponent } from './editors/edit-generic.component'; @@ -88,6 +88,7 @@ import { AttestationRunsModule } from '../runs/attestation-runs.module'; LdsReplaceModule, StatisticsModule, HelpContextualModule, + DataTreeWrapperModule, ], declarations: [ EditGenericComponent, diff --git a/imxweb/projects/att/src/lib/policies/policy.service.ts b/imxweb/projects/att/src/lib/policies/policy.service.ts index 3c9493316..e6a3e8118 100644 --- a/imxweb/projects/att/src/lib/policies/policy.service.ts +++ b/imxweb/projects/att/src/lib/policies/policy.service.ts @@ -49,6 +49,7 @@ import { GroupInfoData, MethodDefinition, MethodDescriptor, + TypedEntityBuilder, } from 'imx-qbm-dbts'; import { AppConfigService, ClassloggerService, DataSourceToolbarExportMethod, ElementalUiConfigService } from 'qbm'; import { ApiService } from '../api.service'; @@ -96,13 +97,13 @@ export class PolicyService { getMethod: (withProperties: string, PageSize?: number) => { let method: MethodDescriptor; if (PageSize) { - method = this.apiClientMethodFactory.portal_attestation_policy_get({...parameters, withProperties, PageSize, StartIndex: 0}) + method = this.apiClientMethodFactory.portal_attestation_policy_get({ ...parameters, withProperties, PageSize, StartIndex: 0 }); } else { - method = this.apiClientMethodFactory.portal_attestation_policy_get({...parameters, withProperties}) + method = this.apiClientMethodFactory.portal_attestation_policy_get({ ...parameters, withProperties }); } return new MethodDefinition(method); - } - } + }, + }; } public async getPolicyEditInteractive(uid: string): Promise> { @@ -137,7 +138,21 @@ export class PolicyService { policyfilter: PolicyFilter, parameters: CollectionLoadParameters ): Promise> { - return this.api.typedClient.PortalAttestationFilterMatchingobjects.Get(uidAttestatation, { + const data = await this.getObjectsForFilterUntyped(uidAttestatation, uidPickCategory, policyfilter, parameters); + + return new TypedEntityBuilder(PortalAttestationFilterMatchingobjects).buildReadWriteEntities( + data, + PortalAttestationFilterMatchingobjects.GetEntitySchema() + ); + } + + public async getObjectsForFilterUntyped( + uidAttestatation: string, + uidPickCategory: string, + policyfilter: PolicyFilter, + parameters: CollectionLoadParameters + ): Promise { + return this.api.client.portal_attestation_filter_matchingobjects_get(uidAttestatation, { uidpickcategory: uidPickCategory, ...parameters, ...{ policyfilter }, @@ -174,14 +189,14 @@ export class PolicyService { } public canSeeAttestations(preProps: string[], features: string[]): boolean { - return (preProps.includes('ATTESTATION') && features.some((elem) => elem === 'Portal_UI_PolicyAdmin')); + return preProps.includes('ATTESTATION') && features.some((elem) => elem === 'Portal_UI_PolicyAdmin'); } public async getGroupInfo(parameters: { by?: string; def?: string } & CollectionLoadParameters = {}): Promise { const { withProperties, OrderBy, search, ...params } = parameters; const test = await this.api.client.portal_attestation_policy_group_get({ - ...params, + ...params, withcount: true, }); return test; @@ -243,9 +258,9 @@ export class PolicyService { // build a new title (shorten it, if maxlength is exceeded) let newTitle = `${reference.Ident_AttestationPolicy.value} (${await this.translator.get('#LDS#New').toPromise()})`; - const max=entity.Ident_AttestationPolicy.GetMetadata().GetMaxLength(); - if ( max < newTitle.length) { - newTitle = newTitle.substring(0,max); + const max = entity.Ident_AttestationPolicy.GetMetadata().GetMaxLength(); + if (max < newTitle.length) { + newTitle = newTitle.substring(0, max); } entity.Ident_AttestationPolicy.value = newTitle; diff --git a/imxweb/projects/cpl/.compodocrc.json b/imxweb/projects/cpl/.compodocrc.json index 2aae0ab13..a54cb8453 100644 --- a/imxweb/projects/cpl/.compodocrc.json +++ b/imxweb/projects/cpl/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - CPL Library", - "output": "../../documentation/cpl", + "output": "../../documentation/v92/cpl", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/cpl/package.json b/imxweb/projects/cpl/package.json index 3f49fe113..22d4e9b12 100644 --- a/imxweb/projects/cpl/package.json +++ b/imxweb/projects/cpl/package.json @@ -1,6 +1,6 @@ { "name": "cpl", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-cpl" diff --git a/imxweb/projects/cpl/src/lib/identity-rule-violations/identity-rule-violations.component.ts b/imxweb/projects/cpl/src/lib/identity-rule-violations/identity-rule-violations.component.ts index 4f8fb1ed7..9d0fb7764 100644 --- a/imxweb/projects/cpl/src/lib/identity-rule-violations/identity-rule-violations.component.ts +++ b/imxweb/projects/cpl/src/lib/identity-rule-violations/identity-rule-violations.component.ts @@ -62,7 +62,7 @@ export class IdentityRuleViolationsComponent implements OnInit { private readonly settingService: SettingsService, private readonly sidesheet: EuiSidesheetService, private readonly translate: TranslateService, - dataProvider: DynamicTabDataProviderDirective + dataProvider: DynamicTabDataProviderDirective, ) { this.referrer = dataProvider.data; this.entitySchema = this.roleMembershipsService.nonComplianceSchema; @@ -79,7 +79,7 @@ export class IdentityRuleViolationsComponent implements OnInit { public async ngOnInit(): Promise { const overlay = this.busyService.show(); try { - await this.metadataService.update([this.referrer.objecttable]); + await this.metadataService.updateNonExisting([this.referrer.objecttable]); } finally { this.busyService.hide(overlay); } diff --git a/imxweb/projects/cpl/src/lib/request/compliance-violation-details/compliance-violation-details.component.ts b/imxweb/projects/cpl/src/lib/request/compliance-violation-details/compliance-violation-details.component.ts index 559b82f54..fb6a1726d 100644 --- a/imxweb/projects/cpl/src/lib/request/compliance-violation-details/compliance-violation-details.component.ts +++ b/imxweb/projects/cpl/src/lib/request/compliance-violation-details/compliance-violation-details.component.ts @@ -65,7 +65,7 @@ export class ComplianceViolationDetailsComponent implements OnInit { private readonly sidesheets: EuiSidesheetService, private readonly translate: TranslateService, private readonly systemInfoService: SystemInfoService, - @Inject(EUI_SIDESHEET_DATA) public data?: ICartItemCheck | any + @Inject(EUI_SIDESHEET_DATA) public data?: ICartItemCheck | any, ) {} public async ngOnInit(): Promise { @@ -113,7 +113,7 @@ export class ComplianceViolationDetailsComponent implements OnInit { if (sourecedRules.length > 0) { for (const source of sourecedRules) { - await this.metaData.update(source.Sources.map((item) => DbObjectKey.FromXml(item.ObjectKeyEntitlement).TableName)); + await this.metaData.updateNonExisting(source.Sources.map((item) => DbObjectKey.FromXml(item.ObjectKeyEntitlement).TableName)); } } } @@ -165,7 +165,7 @@ export class ComplianceViolationDetailsComponent implements OnInit { private async buildCdrForViolations(rule: PortalRules, detail: ComplianceViolation): Promise { const tableName = DbObjectKey.FromXml(detail.ObjectKeyElement).TableName; - await this.metaData.update([tableName]); + await this.metaData.updateNonExisting([tableName]); const displayTitle = this.metaData.tables[tableName]?.DisplaySingular; //Build common elments @@ -184,7 +184,7 @@ export class ComplianceViolationDetailsComponent implements OnInit { cdrColumns.push( rule ? rule.GetEntity().GetColumn('RiskIndex') - : this.buildColumn('RiskIndex', this.schema.Columns['RiskIndex'].Display, detail.RiskIndex, ValType.Double) + : this.buildColumn('RiskIndex', this.schema.Columns['RiskIndex'].Display, detail.RiskIndex, ValType.Double), ); if ( @@ -195,7 +195,7 @@ export class ComplianceViolationDetailsComponent implements OnInit { } else { if (detail.RiskIndex !== detail.RiskIndexReduced) { cdrColumns.push( - this.buildColumn('RiskIndexReduced', this.schema.Columns['RiskIndexReduced'].Display, detail.RiskIndexReduced, ValType.Double) + this.buildColumn('RiskIndexReduced', this.schema.Columns['RiskIndexReduced'].Display, detail.RiskIndexReduced, ValType.Double), ); } } @@ -204,7 +204,7 @@ export class ComplianceViolationDetailsComponent implements OnInit { let sources: ColumnDependentReference[] | undefined; if (detail.Sources?.length > 0) { sources = detail.Sources.map( - (source, index) => new BaseReadonlyCdr(this.buildColumn(`source ${index}`, this.getDisplayForSource(source), source.Display)) + (source, index) => new BaseReadonlyCdr(this.buildColumn(`source ${index}`, this.getDisplayForSource(source), source.Display)), ); } @@ -219,7 +219,7 @@ export class ComplianceViolationDetailsComponent implements OnInit { Display: display, }, undefined, - { Value: value } + { Value: value }, ); } diff --git a/imxweb/projects/cpl/src/lib/request/request-rule-violation.ts b/imxweb/projects/cpl/src/lib/request/request-rule-violation.ts index 7ead5dc50..781d5990e 100644 --- a/imxweb/projects/cpl/src/lib/request/request-rule-violation.ts +++ b/imxweb/projects/cpl/src/lib/request/request-rule-violation.ts @@ -40,7 +40,7 @@ export class RequestRuleViolation implements IExtension { public set inputData(dstSettings: DataSourceToolbarSettings) { this.dstSettings = dstSettings; - if (this.dstSettings.extendedData) { + if (this.dstSettings?.extendedData) { for (let i = 0; i < this.dstSettings.dataSource.Data.length; i++) { const item = this.dstSettings.dataSource.Data[i] as ItshopRequest; item.complianceRuleViolation = item.pwoData.WorkflowHistory.Entities.filter((wh: EntityData) => diff --git a/imxweb/projects/cpl/src/lib/role-compliance-violations/role-compliance-violations.component.ts b/imxweb/projects/cpl/src/lib/role-compliance-violations/role-compliance-violations.component.ts index e23fdd739..dadc4ea2c 100644 --- a/imxweb/projects/cpl/src/lib/role-compliance-violations/role-compliance-violations.component.ts +++ b/imxweb/projects/cpl/src/lib/role-compliance-violations/role-compliance-violations.component.ts @@ -33,10 +33,9 @@ import { RoleComplianceViolationsService } from './role-compliance-violations.se @Component({ templateUrl: './role-compliance-violations.component.html', - styleUrls: ['./role-compliance-violations.component.scss'] + styleUrls: ['./role-compliance-violations.component.scss'], }) export class RoleComplianceViolationsComponent implements OnInit { - public tablename: string; public uidRole: string; public dstSettings: DataSourceToolbarSettings; @@ -50,45 +49,49 @@ export class RoleComplianceViolationsComponent implements OnInit { dataProvider: DynamicTabDataProviderDirective, private readonly entityService: RoleComplianceViolationsWrapperService, private readonly roleComplianceViolationService: RoleComplianceViolationsService, - private readonly metaDataService: MetadataService + private readonly metaDataService: MetadataService, ) { this.tablename = dataProvider.data.tablename; this.uidRole = dataProvider.data.entity.GetKeys()[0]; this.entitySchema = this.entityService.roleComplianceEntitySchema; - this.displayedColumns = [ - this.entitySchema.Columns.RuleName, - this.entitySchema.Columns.ObjectDisplay - ]; + this.displayedColumns = [this.entitySchema.Columns.RuleName, this.entitySchema.Columns.ObjectDisplay]; // tslint:disable:max-line-length switch ((this.tablename ?? '').toLowerCase()) { - case 'aerole' : - this.keyDescription = '#LDS#Here you can get an overview of all entitlements assigned to this application role that may violate a compliance rule.'; + case 'aerole': + this.keyDescription = + '#LDS#Here you can get an overview of all entitlements assigned to this application role that may violate a compliance rule.'; break; - case 'department' : - this.keyDescription = '#LDS#Here you can get an overview of all entitlements assigned to this department that may violate a compliance rule.'; + case 'department': + this.keyDescription = + '#LDS#Here you can get an overview of all entitlements assigned to this department that may violate a compliance rule.'; break; - case 'locality' : - this.keyDescription = '#LDS#Here you can get an overview of all entitlements assigned to this location that may violate a compliance rule.'; + case 'locality': + this.keyDescription = + '#LDS#Here you can get an overview of all entitlements assigned to this location that may violate a compliance rule.'; break; - case 'profitcenter' : - this.keyDescription = '#LDS#Here you can get an overview of all entitlements assigned to this cost center that may violate a compliance rule.'; + case 'profitcenter': + this.keyDescription = + '#LDS#Here you can get an overview of all entitlements assigned to this cost center that may violate a compliance rule.'; break; - case 'eset' : - this.keyDescription = '#LDS#Here you can get an overview of all entitlements assigned to this system role that may violate a compliance rule.'; + case 'eset': + this.keyDescription = + '#LDS#Here you can get an overview of all entitlements assigned to this system role that may violate a compliance rule.'; break; - case 'org' : - this.keyDescription = '#LDS#Here you can get an overview of all entitlements assigned to this business role that may violate a compliance rule.'; + case 'org': + this.keyDescription = + '#LDS#Here you can get an overview of all entitlements assigned to this business role that may violate a compliance rule.'; break; - default : - this.keyDescription = '#LDS#Here you can get an overview of all entitlements assigned to this object that may violate a compliance rule.'; + default: + this.keyDescription = + '#LDS#Here you can get an overview of all entitlements assigned to this object that may violate a compliance rule.'; break; } // tslint:enable:max-line-length } public async ngOnInit(): Promise { - await this.metaDataService.update([this.tablename]); + await this.metaDataService.updateNonExisting([this.tablename]); await this.getData(); } @@ -100,7 +103,7 @@ export class RoleComplianceViolationsComponent implements OnInit { displayedColumns: this.displayedColumns, dataSource: this.entityService.build(data.Violations), entitySchema: this.entityService.roleComplianceEntitySchema, - navigationState: {} + navigationState: {}, }; } finally { this.roleComplianceViolationService.handleCloseLoader(); diff --git a/imxweb/projects/custom-app/package.json b/imxweb/projects/custom-app/package.json index 5e65970ad..8e65fa6ec 100644 --- a/imxweb/projects/custom-app/package.json +++ b/imxweb/projects/custom-app/package.json @@ -1,5 +1,5 @@ { "name": "custom-app", - "version": "9.2.0", + "version": "9.2.1", "private": true } \ No newline at end of file diff --git a/imxweb/projects/dpr/.compodocrc.json b/imxweb/projects/dpr/.compodocrc.json index 3489de978..f76fe1406 100644 --- a/imxweb/projects/dpr/.compodocrc.json +++ b/imxweb/projects/dpr/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - DPR Library", - "output": "../../documentation/dpr", + "output": "../../documentation/v92/dpr", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/dpr/package.json b/imxweb/projects/dpr/package.json index 9ceafc3a1..e33682b8d 100644 --- a/imxweb/projects/dpr/package.json +++ b/imxweb/projects/dpr/package.json @@ -1,6 +1,6 @@ { "name": "dpr", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api" diff --git a/imxweb/projects/hds/.compodocrc.json b/imxweb/projects/hds/.compodocrc.json index a118e3df3..c22076fc2 100644 --- a/imxweb/projects/hds/.compodocrc.json +++ b/imxweb/projects/hds/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - hds Library", - "output": "../../documentation/hds", + "output": "../../documentation/v92/hds", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/hds/package.json b/imxweb/projects/hds/package.json index 90c5e7b5e..8a684144f 100644 --- a/imxweb/projects/hds/package.json +++ b/imxweb/projects/hds/package.json @@ -1,6 +1,6 @@ { "name": "hds", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-hds" diff --git a/imxweb/projects/o3t/.compodocrc.json b/imxweb/projects/o3t/.compodocrc.json index 596270c01..2c79baa1c 100644 --- a/imxweb/projects/o3t/.compodocrc.json +++ b/imxweb/projects/o3t/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - O3T Library", - "output": "../../documentation/o3t", + "output": "../../documentation/v92/o3t", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/o3t/package.json b/imxweb/projects/o3t/package.json index 1e5786eb2..717908d70 100644 --- a/imxweb/projects/o3t/package.json +++ b/imxweb/projects/o3t/package.json @@ -1,6 +1,6 @@ { "name": "o3t", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-o3t" diff --git a/imxweb/projects/olg/.compodocrc.json b/imxweb/projects/olg/.compodocrc.json index 48c60c76c..9919bd6cc 100644 --- a/imxweb/projects/olg/.compodocrc.json +++ b/imxweb/projects/olg/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - olg Library", - "output": "../../documentation/olg", + "output": "../../documentation/v92/olg", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/olg/package.json b/imxweb/projects/olg/package.json index fc5885be7..020943ccb 100644 --- a/imxweb/projects/olg/package.json +++ b/imxweb/projects/olg/package.json @@ -1,6 +1,6 @@ { "name": "olg", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-olg" diff --git a/imxweb/projects/pol/.compodocrc.json b/imxweb/projects/pol/.compodocrc.json index 87cc00850..574c63fac 100644 --- a/imxweb/projects/pol/.compodocrc.json +++ b/imxweb/projects/pol/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - POL Library", - "output": "../../documentation/pol", + "output": "../../documentation/v92/pol", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/pol/package.json b/imxweb/projects/pol/package.json index e10c6a50b..026ece565 100644 --- a/imxweb/projects/pol/package.json +++ b/imxweb/projects/pol/package.json @@ -1,6 +1,6 @@ { "name": "pol", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-pol" diff --git a/imxweb/projects/qbm-app-landingpage/.compodocrc.json b/imxweb/projects/qbm-app-landingpage/.compodocrc.json index d38f723e0..162019b61 100644 --- a/imxweb/projects/qbm-app-landingpage/.compodocrc.json +++ b/imxweb/projects/qbm-app-landingpage/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - QBM Landing Page", - "output": "../../documentation/qbm-app-landingpage", + "output": "../../documentation/v92/qbm-app-landingpage", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/qbm-app-landingpage/package.json b/imxweb/projects/qbm-app-landingpage/package.json index 2018188cf..f5397e23a 100644 --- a/imxweb/projects/qbm-app-landingpage/package.json +++ b/imxweb/projects/qbm-app-landingpage/package.json @@ -1,6 +1,6 @@ { "name": "qbm-app-landingpage", - "version": "9.2.0", + "version": "9.2.1", "private": true, "peerDependencies": { diff --git a/imxweb/projects/qbm/.compodocrc.json b/imxweb/projects/qbm/.compodocrc.json index 8081a3b1d..60cb1367c 100644 --- a/imxweb/projects/qbm/.compodocrc.json +++ b/imxweb/projects/qbm/.compodocrc.json @@ -1,11 +1,14 @@ { "name": "IMX Web - QBM Library", - "output": "../../documentation/qbm", + "output": "../../documentation/v92/qbm", "assetsFolder": "../../compodoc/assets", "theme": "material", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true, + "includes": "./additionalDocumentation", + "includesName": "Additional documentation" } diff --git a/imxweb/projects/qbm/README.md b/imxweb/projects/qbm/README.md new file mode 100644 index 000000000..9cc173179 --- /dev/null +++ b/imxweb/projects/qbm/README.md @@ -0,0 +1,22 @@ +# Configuration module + +This library contains all basic components, that are used in all other projects as well. + +That includes, but is not limited to, our table structure, column dependent references (CDR) and the sql wizard. + +For a more detailed view of the API structure, you can got to the following pages: + +- [Administration](additional-documentation/library-overview/administration.html) +- [Properties of an Object](additional-documentation/library-overview/property-handling.html) +- [Data Sets](additional-documentation/library-overview/data-sets.html) +- [Messages and Translation](additional-documentation/library-overview/messages.html) +- [Other Reusable Components and Services](additional-documentation/library-overview/other-reusable-components.html) + + +## Helpful topics +- [Theming the application](additional-documentation/theming-the-application.html) +- [Samples](additional-documentation/sdk-samples.html) + - [Adding a tile component to the dashboard](additional-documentation/sdk-samples/adding-a-tile-component-to-the-dashboard.html) + - [Adding a menu to the Portal](additional-documentation/sdk-samples/adding-a-menu-to-the-portal.html) + - [Working with data tables](additional-documentation/sdk-samples/working-with-data-tables.html) + \ No newline at end of file diff --git a/imxweb/projects/qbm/additionalDocumentation/admin.md b/imxweb/projects/qbm/additionalDocumentation/admin.md new file mode 100644 index 000000000..c4ff53076 --- /dev/null +++ b/imxweb/projects/qbm/additionalDocumentation/admin.md @@ -0,0 +1,20 @@ +# Administration + +The Administration Portal consists of the following main components. + +- [`DashboardComponent`](../../components/DashboardComponent.html) defines the main page. +- [`StatusComponent`](../../components/StatusComponent.html) shows the list of available applications. +- [`ConfigComponent`](../../components/ConfigComponent.html) exposes the API configuration layer. +- [`PackagesComponent`](../../components/PackagesComponent.html) shows the list of available packages. +- [`PluginsComponent`](../../components/PluginsComponent.html) shows the list of loaded API plugins. +- [`CacheComponent`](../../components/CacheComponent.html) shows the cache status. +- [`LogsComponent`](../../components/LogsComponent.html) provides access to the server logs. +- [`SwaggerComponent`](../../components/SwaggerComponent.html) wraps the API documentation component. + +## Authentication + +In this section you will find other component, that are used during authentication. + +The login itself is handled by the [`AuthenticationService`](../../injectables/AuthenticationService.html). The UI component is provided by the [`LoginComponent`](../../components/LoginComponent.html). + +To check whether the user is currently logged in, there is a special [route guard](../../guards/AuthenticationGuardService.html) to redirect the user to the login component if a login is requred. An older way doing this is using the [`imx_SessionService`](../../injectables/imx_SessionService.html). \ No newline at end of file diff --git a/imxweb/projects/qbm/additionalDocumentation/datasets.md b/imxweb/projects/qbm/additionalDocumentation/datasets.md new file mode 100644 index 000000000..2e5306ed1 --- /dev/null +++ b/imxweb/projects/qbm/additionalDocumentation/datasets.md @@ -0,0 +1,67 @@ +# Data Sets + +In this section you can find components that are designed to display data sets. These cover both flat lists and hierarchical data structures. + +## 1. Data Source Toolbar +The [`DataSourceToolbarComponent`](../../components/DataSourceToolbarComponent.html) is used to handle the interaction between the data source and the actual view. +It is defined in the [`DataSourceToolbarModule`](../../modules/DataSourceToolbarModule.html), together with the other components that are used inside the toolbar. + +The following features are available: +- Filtering +- Searching +- Grouping +- Sorting +- Export and configuration of the view + +The definition of values and functions for these features should be defined using the [`DataSourceToolbarSettings`](../../interfaces/DataSourceToolbarSettings.html) interface. + + +## 2. Paginator +The [`DataSourcePaginatorComponent`](../../components/DataSourcePaginatorComponent.html) is used to handle pagination of the data. This is done by updating the navigation state of its linked [`DataSourceToolbarComponent`](../../components/DataSourceToolbarComponent.html). The usage of a paginator is optional. + +## 3. View Components + +### 3.1. Data Table +The [`DataTableComponent`](../../components/DataTableComponent.html) is the most commonly used view component for our portals. + +It renders an Angular Material table with columns that can be defined by using other components. These are defined in the [`DataTableModule`](../../modules/DataTableModule.html), as well as the data table itself. + +### 3.2. Data Tiles + +The [`DataTilesComponent`](../../components/DataTilesComponent.html) can be used to render a tile view which displays a [`DataTileComponent`](../../components/DataTileComponent.html) for each element in the data source. Both components are part of the [`DataTileModule`](../../modules/DataTilesModule.html), but only the [`DataTilesComponent`](../../components/DataTilesComponent.html) is exported. + +### 3.3. Data Tree +The [`DataTreeComponent`](../../components/DataTreeComponent.html) can be used to display hierarchical data. Other than the table, it uses a special data source, that can be defined by extending the abstract [`TreeDatabase`](../../classes/TreeDatabase.html) class. The data tree is part of the [`DataTreeModule`](../../modules/DataTreeModule.html), together with other components that can be used with the data tree like the [`DataTreeSearchResultsComponent`](../../components/DataTreeSearchResultsComponent.html). This component shows a flat view of an entry subset, because some parameter like a filter or a search string will flatten the result. + +If you prefer to use a data tree with a data source toolbar functionality, there is a [`DataTreeWrapperComponent`](../../components/DataTreeWrapperComponent.html) defined in the [`DataTreeWrapperModule`](../../modules/DataTreeWrapperModule.html). This component combines a data tree with a data source toolbar so that the user can search and filter the tree. + +### 3.5. Other + +There are some other components that can be used to display a list of elements. However, these components are not compatible with the data source toolbar. + +**Select Component** + +The [`SelectComponent`](../../components/SelectComponent.html) can be used to show a list of entries using an auto complete control. It is defined in the [`SelectModule`](../../modules/SelectModule.html). + +**Ordered List Component** + +The [`OrderedListComponent`](../../components/OrderedListComponent.html) can be used to display a simple list of `` entries. It is defined in the [`OrderedListModule`](../../modules/OrderedListModule.html). + + +## 4. Other Components + +Here are some components that are connected to listings, but do not really fit into the other categories. + +### 4.1 Selected Elements +Because all view component can contain a multi-select feature, it is possible to select some items. If you like to check, which elements are selected across pages, you can use a [`SelectedElementsComponent`](../../) that shows, how many elements are selected. This component is clickable, which opens a side sheet with all elements inside a table. + +### 4.2 Foreign Key Picker +There are two foreign key picker dialogs available. + +**4.2.1 Normal Picker** + +The classic data picker is defined inside the [`FkAdvancedPickerComponent`](../../components/FkAdvancedPickerComponent.html). It contains a [`FkSelectorComponent`](../../components/FkSelectorComponent.html) that could be used on other components as well. + +**4.2.2 Hierarchical Picker** + +The hierarchical picker is defined in the [`FkHierarchicalDialogComponent`](../../components/FkHierarchicalDialogComponent.html) and displays the data inside a searchable tree. \ No newline at end of file diff --git a/imxweb/projects/qbm/additionalDocumentation/messages.md b/imxweb/projects/qbm/additionalDocumentation/messages.md new file mode 100644 index 000000000..7caefc1f2 --- /dev/null +++ b/imxweb/projects/qbm/additionalDocumentation/messages.md @@ -0,0 +1,48 @@ +# Messages and Translation +This section describes components that are used to display messages in different languages to the user. There are the following components and services available. + +## 1. Messages + +### 1.1. User Messages +User messages are displayed using the [`UserMessageComponent`](../../components/UserMessageComponent.html), which renders/uses an Elemental UI alert component. + +### 1.2. Message Dialog +The message dialog is defined in the [`MessageDialogComponent`](../../components/MessageDialogComponent.html). \ +It can display the same information the normal [`UserMessageComponent`](../../components/UserMessageComponent.html) is showing, but it uses an Angular Material Dialog instead. + +[`MessageDialogComponent`](../../components/MessageDialogComponent.html) is part of the [`QbmModule`](../../modules/QbmModule.html). + +### 1.3. Confirmation Dialog +The confirmation dialog is opened using the [`ConfirmationService`](../../injectables/ConfirmationService.html). \ +It allows the user to acknowledge the information it is presented. + +### 1.4. Snackbar +A possible way to display a small notification is by using an Angular Snackbar component. To open a pre defined snackbar, you can use the [`SnackBarService`](../../injectables/SnackBarService.html). +It is part of the [`QbmModule`](../../modules/QbmModule.html) as well. + +### 1.5. Logging +There is a special logger, that can be used, to send messages to the command line. + +The code is defined in the [ClassLoggerService](../../injectables/ClassloggerService.html). + +### 1.6 Error Handling +Error handling is handled by our own `ErrorHandler`. + +It is defined in the [`GlobalErrorHandler`](../../injectables/GlobalErrorHandler.html). + +--- + +## 2. Translation +Our libraries are using translatable keys, identifiable by the prefix `#LDS#`. The services need to translate these keys into localized text at runtime are described below. + +### 2.1. Translation +This uses the [`ImxTranslationProviderService`](../../injectables/ImxTranslationProviderService.html) to initialize the translation information. You can use this service for translation purposes as well, but it is recommended to use the `TranslateService` from `@ngx-translate`, which is used in this service anyway. + +### 2.2. LDS Replace +The [`LdsReplaceModule`](../../modules/LdsReplaceModule.html) contains a pipe which can be used to replace placeholder inside LDS key translations. It is defined in the [`LdsReplacePipe`](../../injectables/LdsReplacePipe.html) class. + +### 2.3. Parameterized Text +The [`ParameterizedTextModule`](../../modules/ParameterizedTextModule.html) provides components and services to display a parameterized text that emphasizes it's parameters. The UI is defined in the [`ParameterizedTextComponent`](../../components/ParameterizedTextComponent.html). + +### 2.4. Translation Editor +The [`TranslationEditorComponent`](../../) is declared in the [`QbmModule`](../../modules/QbmModule.html). It can be used to add translations to an LDS key. \ No newline at end of file diff --git a/imxweb/projects/qbm/additionalDocumentation/other.md b/imxweb/projects/qbm/additionalDocumentation/other.md new file mode 100644 index 000000000..1105fbc56 --- /dev/null +++ b/imxweb/projects/qbm/additionalDocumentation/other.md @@ -0,0 +1,53 @@ +# Other Reusable Components and Services + +In this section you can find components that are intended to be reused in other libraries and applications, and not covered by the other sections. + +## 1. Bulk Editor + +The [`BulkPropertyEditorComponent`](../../components/BulkPropertyEditorComponent.html) is used to edit the same list properties for multiple objects. Each [`BulkItemComponent`](../../components/BulkItemComponent.html) has its own list of [`CdrEditorComponents`](../../components/CdrEditorComponent.html). + +## 2. Busy Indicator +The [BusyIndicatorComponent](../../components/BusyIndicatorComponent.html) can be used to display a loading spinner and a predefined text. A good solution is to use it together with a [`BusyService`](../../injectables/BusyService.html) that indicates if something has started / ended loading. + +## 3. Charts +In our web, charts are rendered using the `ElementalUiChartComponent`. There are some configurations defined by related classes. +- [`LineChartOptions`](../../classes/LineChartOptions.html) that defines a line chart definition. +- [`SeriesInformation`](../../classes/SeriesInformation.html), that defines one series in a line chart. +- [`XAxisInformation`](../../classes/XAxisInformation.html) and [`YAxisInformation`](classes/YAxisInformation.html) that defines the information for the line axis. + +There is also a [`ChartTileComponent`](../../components/ChartTileComponent.html) that can be used to render a chart as part of a tile. + +## 4. Custom Theme +The [`CustomThemeService`](../../injectables/CustomThemeService.html) can be used to load all defined themes. It is also used to add the themes to the `` part of the page. + +## 5. Dynamic Tabs +Some tabs in side sheets have to be added dynamically because they are part of a dynamic module. The item is defined by a new [`TabItem`](../../interfaces/TabItem.html) and can be added to a tab control using a [`DynamicTabDataProviderDirective`](../../directives/DynamicTabDataProviderDirective.html). + +## 6. Extensions +Sometimes it is necessary to add a component, that is not defined as part of a static module, but is part of a dynamic one. To display this component, a [`ExtComponent`](../../components/ExtComponent.html) can be used. It is part of the [`ExtModule`](../../modules/ExtModule.html), together with the needed service and a directive. + +## 7. File Selector +If the user should be able to upload files, there is a [FileSelectorService](../../injectables/FileSelectorService.html) for that. + +## 8. Hyper View +A hyper view is a linked graph that visualizes the relationships of an object to other objects. The [`HyperviewComponent`](../../components/HyperviewComponent.html) and its parts are all part of the [`HyperviewModule`](../../modules/HyperViewModule.html). + +## 9. Masthead +In the masthead, information like the company logo, a help icon or the user menu can be displayed. It is defined inside the [`MastHeadComponent`](../../components/MastHeadComponent.html), that is part of the ['MastHeadModule'](../../modules/MastHeadModule.html). + +The [`MastHeadComponent`](../../components/MastHeadComponent.html) is also responsible for the menu line underneath it. The menu is rendered using an `ElementalUiTopNavigationComponent`. The menu items can be defined by using the [`MenuService`](../../injectables/MenuService.html). + +## 10. Object History +The [`ObjectHistoryComponent`](../../components/ObjectHistoryComponent.html) can be used to show the course of an object. It can be display as a table or by using a [`TimelineComponent`](../../components/TimelineComponent.html). + +## 11. Side Navigation +The [`SideNavigationViewComponent`](../../components/SideNavigationViewComponent.html) is used to display a vertical menu that can be part of a page or an other component. It is collapsible for smaller screens. + +Another representation of a side navigation is the [`SidenavTreeComponent`](../../components/SidenavTreeComponent.html) which uses a tree view inside a collapsible panel. + + The [`SideNavigationViewComponent`](../../components/SideNavigationViewComponent.html) is part of the [`SideNavigationViewModule`](../../modules/SideNavigationViewModule.html) while the [`SidenavTreeComponent`](../../components/SidenavTreeComponent.html) is part of the [`SidenavTreeModule`](../../modules/SidenavTreeModule.html). + +## 12. SQL Wizard +The [`SqlWizardComponent`](../../components/SqlWizardComponent.html) can be used to filter the result of a larger amount of entries. It generates a filter expression that can be used as part of a normal API call. The SQL wizard is defined in the [`SqlWizardModule`](../../modules/SqlWizardModule.html) together with the sub components, that are used to build the filter. + +To use a [`SqlWizardComponent`](../../components/SqlWizardComponent.html) as part of the table filter in a custom project, the abstract class [`SqlWizardApiService`](../../classes/SqlWizardApiService.html) has to be extended and added as a provider. \ No newline at end of file diff --git a/imxweb/projects/qbm/additionalDocumentation/overview.md b/imxweb/projects/qbm/additionalDocumentation/overview.md new file mode 100644 index 000000000..9560d0e45 --- /dev/null +++ b/imxweb/projects/qbm/additionalDocumentation/overview.md @@ -0,0 +1,7 @@ +# Library Overview + +- [Administration](library-overview/administration.html) +- [Properties of an Object](library-overview/property-handling.html) +- [Data Sets](library-overview/data-sets.html) +- [Messages and Translation](library-overview/messages.html) +- [Other Reusable Components and Services](library-overview/other-reusable-components.html) diff --git a/imxweb/projects/qbm/additionalDocumentation/properties.md b/imxweb/projects/qbm/additionalDocumentation/properties.md new file mode 100644 index 000000000..76db2d7a5 --- /dev/null +++ b/imxweb/projects/qbm/additionalDocumentation/properties.md @@ -0,0 +1,30 @@ +# Properties of an Object + +In this section you will find useful components that can be used to edit properties of an entity (`IEntity`). + +The main way of doing this by using **C**olumn **D**ependent **R**eferences. They are all listed in the `cdr` folder of this project. + +Normally column dependent references are represented in templates with the `` tag. \ +More information about this component can be found [here](../../components/CdrEditorComponent.html). + +Another way is to use the [`EntityColumnEditorComponent`](../../components/EntityColumnEditorComponent.html) that wraps the editor. + +The correct definition of an editor is determined by the information provided by the column dependent reference. \ +For this it is necessary to register the components. Our predefined components are registered in the [`DefaultCdrEditorProvider`](../../classes/DefaultCdrEditorProvider.html) and the [`FkCdrEditorProvider`](../../classes/FkCdrEditorProvider.html). + +The following property types have predefined editors, that can be displayed as read only as well: +* [boolean](../../components/EditBooleanComponent.html) +* [date](../../components/EditDateComponent.html) +* [date range](../../components/DateRangeComponent.html) +* [foreign-key definition](../../components/EditFkComponent.html) +* [image](../../components/EditImageComponent.html) +* [limited value](../../components/EditLimitedValueComponent.html) +* [multi foreign-key definition](../../components/EditFkMultiComponent.html) +* [multi-limited value](../../components/EditMultiLimitedValueComponent.html) +* [multi-line string](../../components/EditMultilineComponent.html) +* [multi value](../../components/EditMultiValueComponent.html) +* [number](../../components/EditNumberComponent.html) +* [risk index](../../components/EditRiskIndexComponent.html) +* [simple string](../../components/EditDefaultComponent.html) +* [url](../../components/EditUrlComponent.html) + diff --git a/imxweb/projects/qbm/additionalDocumentation/summary.json b/imxweb/projects/qbm/additionalDocumentation/summary.json new file mode 100644 index 000000000..d357d2e81 --- /dev/null +++ b/imxweb/projects/qbm/additionalDocumentation/summary.json @@ -0,0 +1,50 @@ +[ + { + "title": "Library Overview", + "file": "overview.md", + "children": [ + { + "title": "Administration", + "file": "admin.md" + }, + { + "title": "Property Handling", + "file": "properties.md" + }, + { + "title": "Data Sets", + "file": "datasets.md" + }, + { + "title": "Messages", + "file": "messages.md" + }, + { + "title": "Other reusable components", + "file": "other.md" + } + ] + }, + { + "title": "Theming the application", + "file": "../../../custom-theme/readme.md" + }, + { + "title": "SDK samples", + "file": "../../../compodoc/samples/samples.md", + "children": [ + { + "title": "Adding a tile component to the dashboard", + "file": "../../../compodoc/samples/tiles.md" + }, + { + "title": "Adding a menu to the Portal", + "file": "../../../compodoc/samples/menu.md" + }, + { + "title": "Working with data tables", + "file": "../../../compodoc/samples/data_table.md" + } + ] + } +] diff --git a/imxweb/projects/qbm/package.json b/imxweb/projects/qbm/package.json index b57840fde..b7dcf1abb 100644 --- a/imxweb/projects/qbm/package.json +++ b/imxweb/projects/qbm/package.json @@ -1,6 +1,6 @@ { "name": "qbm", - "version": "9.2.0", + "version": "9.2.1", "private": true, "dependencies": { diff --git a/imxweb/projects/qbm/src/lib/base/metadata.service.ts b/imxweb/projects/qbm/src/lib/base/metadata.service.ts index 72a9845b5..4e5cb4a93 100644 --- a/imxweb/projects/qbm/src/lib/base/metadata.service.ts +++ b/imxweb/projects/qbm/src/lib/base/metadata.service.ts @@ -30,7 +30,9 @@ import { TranslateService } from '@ngx-translate/core'; import { MetaTableData } from 'imx-api-qbm'; import { imx_SessionService } from '../session/imx-session.service'; -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class MetadataService { public readonly tables: { [id: string]: MetaTableData } = {}; @@ -41,8 +43,8 @@ export class MetadataService { constructor( private sessionService: imx_SessionService, - private readonly translateService: TranslateService - ) { } + private readonly translateService: TranslateService, + ) {} /** * Updates meta data for the tables of the provided table names that are not already present in the tables map @@ -50,7 +52,7 @@ export class MetadataService { */ public async updateNonExisting(tableNames: string[]): Promise { // Use a Set to obtain unique values - const uniqueSet = Array.from(new Set(tableNames.filter(tableName => this.tables[tableName] == null))); + const uniqueSet = Array.from(new Set(tableNames.filter((tableName) => this.tables[tableName] == null))); return this.update(uniqueSet); } @@ -60,7 +62,9 @@ export class MetadataService { */ public async update(tableNames: string[]): Promise { for (const tableName of tableNames) { - this.tables[tableName] = await this.sessionService.Client.imx_metadata_table_get(tableName, { cultureName: this.translateService.currentLang }); + this.tables[tableName] = await this.sessionService.Client.imx_metadata_table_get(tableName, { + cultureName: this.translateService.currentLang, + }); } } @@ -70,9 +74,9 @@ export class MetadataService { */ public async GetTableMetadata(table: string): Promise { if (this.tableMetadata[table] == null) { - this.tableMetadata[ - table - ] = await this.sessionService.Client.imx_metadata_table_get(table, { cultureName: this.translateService.currentLang }); + this.tableMetadata[table] = await this.sessionService.Client.imx_metadata_table_get(table, { + cultureName: this.translateService.currentLang, + }); } return this.tableMetadata[table]; diff --git a/imxweb/projects/qbm/src/lib/cdr/base-cdr-editor-provider.ts b/imxweb/projects/qbm/src/lib/cdr/base-cdr-editor-provider.ts index 60cfe7b58..8f8b41674 100644 --- a/imxweb/projects/qbm/src/lib/cdr/base-cdr-editor-provider.ts +++ b/imxweb/projects/qbm/src/lib/cdr/base-cdr-editor-provider.ts @@ -29,25 +29,45 @@ import { CdrEditorProvider } from './cdr-editor-provider.interface'; import { ViewContainerRef, ComponentRef, ComponentFactoryResolver } from '@angular/core'; import { ColumnDependentReference } from './column-dependent-reference.interface'; +/** + * A base implementation of the interface {@link CdrEditorProvider}. + * It can create an editor, according to a given {@link ColumnDependentReference | column dependent reference}. + * To extend this class you only need to implement the 'accept' method + */ export abstract class BaseCdrEditorProvider implements CdrEditorProvider { + protected abstract tCtor: new (...args: any[]) => T; + constructor(private componentFactoryResolver: ComponentFactoryResolver) {} - protected abstract tCtor: new (...args: any[]) => T; - constructor(private componentFactoryResolver: ComponentFactoryResolver) {} + /** + * Creates an editor, depending on the given CDR and renders it to the UI + * @param parent The view container used for rendering the editor into. + * @param cdref A column dependent reference that contains the data for the editor. + * @returns An instance of {@link CdrEditor}, that can be used for editing data. + */ + public createEditor(parent: ViewContainerRef, cdref: ColumnDependentReference): ComponentRef { + if (!this.accept(cdref)) { + return null; + } - public createEditor(parent: ViewContainerRef, cdref: ColumnDependentReference): ComponentRef { - if (!this.accept(cdref)) { - return null; - } + const view = parent.createComponent(this.componentFactoryResolver.resolveComponentFactory(this.tCtor)); + this.configure(view.instance, cdref); - const view = parent.createComponent(this.componentFactoryResolver.resolveComponentFactory(this.tCtor)); - this.configure(view.instance, cdref); + return view; + } - return view; - } + /** + * A method, that is used to check, whether a CDR is suitable for this provider or not. + * @param cdref A column dependent reference that contains the data for the editor. + * @returns True, if the CDR fits the criteria of the provider, otherwise false. + */ + protected abstract accept(cdref: ColumnDependentReference): boolean; - protected abstract accept(cdref: ColumnDependentReference): boolean; - - protected configure(editor: T, cdref: ColumnDependentReference): void { - editor.bind(cdref); - } + /** + * Binds an editor to a CDR. + * @param editor The editor, that is needed for the process. + * @param cdref The CDR, that handles the data. + */ + protected configure(editor: T, cdref: ColumnDependentReference): void { + editor.bind(cdref); + } } diff --git a/imxweb/projects/qbm/src/lib/cdr/base-cdr.ts b/imxweb/projects/qbm/src/lib/cdr/base-cdr.ts index 4c1b07a7b..bb7ca0153 100644 --- a/imxweb/projects/qbm/src/lib/cdr/base-cdr.ts +++ b/imxweb/projects/qbm/src/lib/cdr/base-cdr.ts @@ -30,15 +30,35 @@ import { ColumnDependentReference } from './column-dependent-reference.interface import { Subject } from 'rxjs'; /** - * Generic implementation of a ColumnDependentReference. + * Generic implementation of a {@link ColumnDependentReference | column dependent reference}. + * @examples + * const value = new BaseCdr(columnToUse, optionalDisplay, renderedReadonlyOrNot); // Build a CDR with an optional display and a give readOnly state + * const value = new BaseCdr(columnToUse, optionalDisplay); // Build a CDR with an optional display + * const value = new BaseCdr(columnToUse, undefined, renderedReadonlyOrNot ); // Build a CDR with a give readOnly state */ export class BaseCdr implements ColumnDependentReference { + + /** + * A small hint, that is displayed on a hint icon + */ public hint: string; + + /** + * A minimal size of the content, that differs from the minLength of the column. + */ public minLength?: number; + + /** + * A subject to listen for changes of the minLength + */ public minlengthSubject = new Subject(); constructor(public readonly column: IEntityColumn, public readonly display?: string, public readonly isReadOnlyColumn?: boolean) {} + /** + * Checks, whether a CDR should be rendered as read-only + * @returns True, if the CDR needs to be show as 'read-only', otherwise false. + */ public isReadOnly(): boolean { if (this.isReadOnlyColumn !== undefined) { return this.column == null || this.isReadOnlyColumn || !this.column.GetMetadata().CanEdit(); @@ -47,6 +67,10 @@ export class BaseCdr implements ColumnDependentReference { } } + /** + * Can be used to update the minimal size, that controls the mandatory state of the CDR. + * It calls the minLengthSubject, too. + * */ public updateMinLength(value: number): void { this.minLength = value; this.minlengthSubject.next(value); diff --git a/imxweb/projects/qbm/src/lib/cdr/cdr-editor-provider-registry.interface.ts b/imxweb/projects/qbm/src/lib/cdr/cdr-editor-provider-registry.interface.ts index 82c3b0cc9..16382bd15 100644 --- a/imxweb/projects/qbm/src/lib/cdr/cdr-editor-provider-registry.interface.ts +++ b/imxweb/projects/qbm/src/lib/cdr/cdr-editor-provider-registry.interface.ts @@ -38,7 +38,7 @@ export interface CdrEditorProviderRegistry extends CdrEditorProvider { /** * Registers an editor provider for column dependent references. - * @param provider The editor provider to register + * @param provider The editor provider to register. */ register(provider: CdrEditorProvider): void; } diff --git a/imxweb/projects/qbm/src/lib/cdr/cdr-editor-provider.interface.ts b/imxweb/projects/qbm/src/lib/cdr/cdr-editor-provider.interface.ts index 056f6b913..a2510e7e6 100644 --- a/imxweb/projects/qbm/src/lib/cdr/cdr-editor-provider.interface.ts +++ b/imxweb/projects/qbm/src/lib/cdr/cdr-editor-provider.interface.ts @@ -29,8 +29,8 @@ import { CdrEditor } from './cdr-editor.interface'; import { ViewContainerRef, ComponentRef } from '@angular/core'; /** - * Defines a class that can provide an UI component to display and/or edit a column dependent reference, - * namely a cdr "editor". + * Defines an interface that can provide an UI component to display and/or edit a column dependent reference, + * namely a CDR "editor". */ export interface CdrEditorProvider { diff --git a/imxweb/projects/qbm/src/lib/cdr/cdr-editor.interface.ts b/imxweb/projects/qbm/src/lib/cdr/cdr-editor.interface.ts index cc9f0aabe..20381aa4a 100644 --- a/imxweb/projects/qbm/src/lib/cdr/cdr-editor.interface.ts +++ b/imxweb/projects/qbm/src/lib/cdr/cdr-editor.interface.ts @@ -30,17 +30,17 @@ import { Subject } from 'rxjs'; import { ColumnDependentReference } from './column-dependent-reference.interface'; /** - * Interface for the argument, that it emitted in the CDR editor + * Interface for the argument, that it emitted in the CDR editor. */ export interface ValueHasChangedEventArg { /** - * The new value of the editor + * The new value of the editor. */ value: any; /** * A flag to show whether the emitting of a follow up event should be forced - * (evaluated by {@link CdrEditorComponent|CdrEditorComponent}) + * (evaluated by {@link CdrEditorComponent|CdrEditorComponent}). */ forceEmit?: boolean; } @@ -49,31 +49,30 @@ export interface ValueHasChangedEventArg { * Interface for an editor of a column dependent reference. */ export interface CdrEditor { - /** - * The abstract control associated with the editor. - */ - control: AbstractControl; + /** + * The abstract control associated with the editor. + */ + control: AbstractControl; - /** - * An event, that is emmited, if the value of the cdr has changed. - */ - valueHasChanged?: EventEmitter; + /** + * An event, that is emitted, if the value of the cdr has changed. + */ + valueHasChanged?: EventEmitter; - /** - * An event, that is emmited, if the value of the cdr is pending. - */ - pendingChanged?: EventEmitter; + /** + * An event, that is emitted, if the value of the cdr is pending. + */ + pendingChanged?: EventEmitter; - /** - * Binds a column dependent reference to the editor. - * - * @param cdref The column dependent reference. - */ - bind(cdref: ColumnDependentReference): void; - - /** - * A subject, that can be called, if the control needs to be updated. - */ - updateRequested?: Subject; + /** + * Binds a column dependent reference to the editor. + * + * @param cdref The column dependent reference. + */ + bind(cdref: ColumnDependentReference): void; + /** + * A subject, that can be called, if the control needs to be updated. + */ + updateRequested?: Subject; } diff --git a/imxweb/projects/qbm/src/lib/cdr/cdr-editor/cdr-editor.component.ts b/imxweb/projects/qbm/src/lib/cdr/cdr-editor/cdr-editor.component.ts index 1f05dcdd8..f6979e0aa 100644 --- a/imxweb/projects/qbm/src/lib/cdr/cdr-editor/cdr-editor.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/cdr-editor/cdr-editor.component.ts @@ -36,6 +36,23 @@ import { CdrEditor } from '../cdr-editor.interface'; * This component provides an {@link CdrEditor|editor} for a {@link ColumnDependentReference|column dependent reference}. * * In order to determine the appropriate editor it uses a {@link CdrRegistryService|registry}. + * @example + * A simple form that uses cdr to display the properties of a column. + * + *
+ * + * + *
(); + + /** + * This is emitted, after the value changes inside the editor. + */ @Output() public readonly valueChange = new EventEmitter(); + + /** + * This is emitted, after the read-only state changes inside the editor. + */ @Output() public readonly readOnlyChanged = new EventEmitter(); @Output() public readonly pendingChanged = new EventEmitter(); + /** + * @ignore only used internally + * The container, the component is rendered in. + */ @ViewChild('viewcontainer', { read: ViewContainerRef, static: true }) private viewContainerRef: ViewContainerRef; - // stores if the cdr is readonly, because otherwise you're unable to check if the value has changed + // stores if the cdr is readonly, because otherwise you're unable to check if the value has changed. private isReadonly: boolean; constructor(private registry: CdrRegistryService, private logger: ClassloggerService, private readonly elementRef: ElementRef) {} + /** + * Updates the component, if the cdr input is set to a new value. + * + * It creates a new editor, subscribes to its valueHasChanged event and emits the controlCreated event afterwards. + * @param changes Changes, that were triggering the changes hook of the component. + */ public ngOnChanges(changes: SimpleChanges): void { if (changes['cdr'] && changes['cdr'].currentValue) { this.viewContainerRef.clear(); @@ -98,16 +136,26 @@ export class CdrEditorComponent implements OnChanges { } } + /** + * Gets the description of the cdr. + * That can be the hint provided in the CDRs creation or the description of the column. + */ public get description() { // Preferably use CDR-level hint; if none is defined: use the metadata description field. return this.cdr?.hint || this.cdr?.column.GetMetadata().GetDescription(); } + /** + * Gets the display of the column. + */ public get infotitle() { return this.cdr.column.GetMetadata().GetDisplay(); } - public update(){ + /** + * Forces the underlying CdrEditor to update by triggering the editors 'updateRequested' subject. + */ + public update() { this.editor?.updateRequested?.next(); } } diff --git a/imxweb/projects/qbm/src/lib/cdr/cdr-factory.service.ts b/imxweb/projects/qbm/src/lib/cdr/cdr-factory.service.ts index c51e4ed3c..33a3c6b26 100644 --- a/imxweb/projects/qbm/src/lib/cdr/cdr-factory.service.ts +++ b/imxweb/projects/qbm/src/lib/cdr/cdr-factory.service.ts @@ -37,10 +37,10 @@ export class CdrFactoryService { /** * Builds an array of column dependent references, depending on the columns provided. * If the column does not exists, it is left out of the array. - * @param entity the complete entity - * @param columnNames the list of columns, a CDR is needed for - * @param readOnly if true, readonly CDR will be build otherwise normal base CDR - * @returns a list of column depedent references + * @param entity The complete entity that provides the columns. + * @param columnNames The list of columns, a CDR is needed for. + * @param readOnly If true, readonly CDR will be build otherwise normal base CDR. + * @returns A list of column dependent references. */ public buildCdrFromColumnList(entity: IEntity, columnNames: string[], readOnly: boolean = false): ColumnDependentReference[] { return columnNames.map((column) => this.buildCdr(entity, column, readOnly)).filter((cdr) => cdr != null); @@ -49,11 +49,11 @@ export class CdrFactoryService { /** * Builds an array of column dependent references, depending on the columns provided. * If the column does not exists, it is left out of the array. - * You are able to add a list of columnnames, that should be readonly - * @param entity the complete entity - * @param columnNames the list of columns, a CDR is needed for - * @param readOnlyColumns a list of column names, that needed readonly CDR - * @returns a list of column depedent references + * You are able to add a list of column names, that should be readonly + * @param entity The complete entity that provides the columns. + * @param columnNames The list of columns, a CDR is needed for. + * @param readOnlyColumns A list of column names, that needed readonly CDR. + * @returns A list of column dependent references. */ public buildCdrFromColumnListAdvanced( entity: IEntity, @@ -65,22 +65,22 @@ export class CdrFactoryService { /** * Builds a single CDR for a specific column of the entity provided - * @param entity the complete entity - * @param columnName the name of the column, that should be used by the CDR - * @param readOnly if true, a read only CDR will be build otherwise a normal base CDR - * @returns the columm dependent reference or null, if the column is not defined in the entity + * @param entity The complete entity that provides the columns. + * @param columnName The name of the column, that should be used by the CDR. + * @param readOnly If true, a read-only CDR will be build otherwise a normal base CDR. + * @returns The column dependent reference or null, if the column is not defined in the entity. */ - public buildCdr(entity: IEntity, columnName: string, readOnly: boolean = false, columnDisplay?: string ): ColumnDependentReference { + public buildCdr(entity: IEntity, columnName: string, readOnly: boolean = false, columnDisplay?: string): ColumnDependentReference { const column = CdrFactoryService.tryGetColumn(entity, columnName); return column == null ? null : readOnly ? new BaseReadonlyCdr(column, columnDisplay) : new BaseCdr(column, columnDisplay); } /** - * - * @param entity the complete entity - * @param columnName the name of the column - * @returns null, if the entity doesn't have a column with the given name otherwise the column is returned + * Trys to get a specific entity column by a given column name. + * @param entity The complete entity, the column is part of. + * @param columnName The name of the column, that should be provided. + * @returns Null, if the entity doesn't have a column with the given name otherwise the column is returned. */ public static tryGetColumn(entity: IEntity, columnName: string): IEntityColumn { try { diff --git a/imxweb/projects/qbm/src/lib/cdr/cdr-registry.service.ts b/imxweb/projects/qbm/src/lib/cdr/cdr-registry.service.ts index eb19a0ce5..f48a54bf0 100644 --- a/imxweb/projects/qbm/src/lib/cdr/cdr-registry.service.ts +++ b/imxweb/projects/qbm/src/lib/cdr/cdr-registry.service.ts @@ -56,9 +56,9 @@ export class CdrRegistryService implements CdrEditorProviderRegistry { /** * Creates a new registry service for column dependent reference editor providers. - * @param componentFactoryResolver The resolver required for resolving the factory capable of creating an editor component + * @param componentFactoryResolver The resolver required for resolving the factory capable of creating an editor component. * @param errorHandler Required error handler to handle errors. - * @param logger The logger used for logging messages + * @param logger The logger used for logging messages. * @throws {Error} Throws an error if the given error handler is null or undefined. */ constructor(private componentFactoryResolver: ComponentFactoryResolver, @@ -67,7 +67,7 @@ export class CdrRegistryService implements CdrEditorProviderRegistry { /** * Registers an editor provider for column dependent references. * - * @param provider The editor provider to register + * @param provider The editor provider to register. * @throws {Error} Throws an error if the given provider is null or undefined. * @throws {Error} Throws an error if the given provider has already been registered. */ diff --git a/imxweb/projects/qbm/src/lib/cdr/cdr-sidesheet/cdr-sidesheet.component.ts b/imxweb/projects/qbm/src/lib/cdr/cdr-sidesheet/cdr-sidesheet.component.ts index cb4161039..8c9ab6c3a 100644 --- a/imxweb/projects/qbm/src/lib/cdr/cdr-sidesheet/cdr-sidesheet.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/cdr-sidesheet/cdr-sidesheet.component.ts @@ -30,16 +30,36 @@ import { EuiSidesheetRef, EUI_SIDESHEET_DATA } from '@elemental-ui/core'; import { CdrSidesheetConfig } from './cdr-sidesheet-config'; +/** + * Provides a side sheet, that displays a form with {@link CdrEditor | cdr editors}. + * + * Writeable properties can be edited. + */ @Component({ templateUrl: './cdr-sidesheet.component.html' }) export class CdrSidesheetComponent { + + /** + * The form, that stores the editors. + */ public readonly cdrFormGroup = new UntypedFormGroup({}); + + /** + * Creates an instance of this component. + * @param config Information about the cdr and the caption of the 'close' button. + * @param sidesheetRef a reference of the sidesheet. + */ constructor( @Inject(EUI_SIDESHEET_DATA) public readonly config: CdrSidesheetConfig, public readonly sidesheetRef: EuiSidesheetRef ) {} + /** + * Adds the form control of an editor to the form group. + * @param name The name of the control. + * @param control The form control that should be added. + */ public addFormControl(name: string, control: UntypedFormControl): void { this.cdrFormGroup.addControl(name, control); } diff --git a/imxweb/projects/qbm/src/lib/cdr/date-range/date-range.component.ts b/imxweb/projects/qbm/src/lib/cdr/date-range/date-range.component.ts index f9e306a52..a8abef75d 100644 --- a/imxweb/projects/qbm/src/lib/cdr/date-range/date-range.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/date-range/date-range.component.ts @@ -36,28 +36,64 @@ import { EntityColumnContainer } from '../entity-column-container'; import { EuiSelectOption } from '@elemental-ui/core'; import { ImxTranslationProviderService } from '../../translation/imx-translation-provider.service'; +/** + * Provides a {@link CdrEditor | CDR editor} for editing / viewing date range columns. + * + * The user can choose between these two options: + * It displays either two {@link DateComponent | date components} or a dynamic time frame like 'two weeks ago'. + * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. + */ @Component({ selector: 'imx-date-range', templateUrl: './date-range.component.html', - styleUrls: ['./date-range.component.scss'] + styleUrls: ['./date-range.component.scss'], }) export class DateRangeComponent implements CdrEditor, OnDestroy { + /** + * The form control associated with the editor. + */ public readonly control = new UntypedFormControl(undefined, { updateOn: 'blur' }); + /** + * The form control associated with the 'from' part of the range. + */ public readonly dateFrom = new UntypedFormControl(undefined, { updateOn: 'blur' }); + /** + * The form control associated with the 'until' part of the range. + */ public readonly dateUntil = new UntypedFormControl(undefined, { updateOn: 'blur' }); + /** + * The form control associated with a dynamic date range. + */ public readonly dynamicDateControl = new UntypedFormControl(undefined, { updateOn: 'blur' }); + /** + * The container that wraps the column functionality. + */ public readonly columnContainer = new EntityColumnContainer(); + /** + * Event that is emitted, after a value has been changed. + */ public readonly valueHasChanged = new EventEmitter(); + /** + * Indicator that the component is loading data from the server. + */ public isLoading = false; + /** + * @ignore only used in templates + * Indicator, if a dynamic time frame is used. + */ public dateRangeTypeDynamic = false; + /** + * @ignore only used in templates + * The options available for a dynamic date range. + */ public dynamicDateRangeOptions: EuiSelectOption[] = []; private readonly subscribers: Subscription[] = []; @@ -68,20 +104,21 @@ export class DateRangeComponent implements CdrEditor, OnDestroy { private required = false; - public constructor( - private readonly errorHandler: ErrorHandler, - private translateProviderService: ImxTranslationProviderService, - ) { - this.translateProviderService.GetCultures().then(() => this.updateOptions()) + public constructor(private readonly errorHandler: ErrorHandler, private translateProviderService: ImxTranslationProviderService) { + this.translateProviderService.GetCultures().then(() => this.updateOptions()); } + /** + * Unsubscribes all events, after the 'OnDestroy' hook is triggered. + */ public ngOnDestroy(): void { - this.subscribers.forEach(s => s.unsubscribe()); + this.subscribers.forEach((s) => s.unsubscribe()); } /** - * Binds a column dependent reference to the component - * @param cdref a column dependent reference + * Binds a column dependent reference to the component. + * Subscribes to subjects from the column dependent reference and its container. + * @param cdref a column dependent reference. */ public bind(cdref: ColumnDependentReference): void { if (cdref && cdref.column) { @@ -135,7 +172,7 @@ export class DateRangeComponent implements CdrEditor, OnDestroy { this.subscribers.push( this.dynamicDateControl.valueChanges.subscribe((value: string) => { - if(!!value){ + if (!!value) { this.writeValue(value); } }) @@ -144,11 +181,14 @@ export class DateRangeComponent implements CdrEditor, OnDestroy { } /** - * updates the value for the CDR - * @param value the new value + * Updates the value for the column dependent reference and writes it back to the columm. + * @param value The date range, that should be written to the column. */ private async writeValue(value: { from: Date; until: Date } | string): Promise { - if ((this.required && this.dateRangeTypeDynamic && this.dynamicDateControl.errors) || (this.required && !this.dateRangeTypeDynamic && (this.dateFrom.errors || this.dateUntil.errors))) { + if ( + (this.required && this.dateRangeTypeDynamic && this.dynamicDateControl.errors) || + (this.required && !this.dateRangeTypeDynamic && (this.dateFrom.errors || this.dateUntil.errors)) + ) { return; } let convertedValue: string; @@ -187,7 +227,7 @@ export class DateRangeComponent implements CdrEditor, OnDestroy { if (!!value && value in this.dateRangeTypeEnum) { this.dateRangeTypeDynamic = true; this.dynamicDateControl.setValue(value, { emitEvent: true }); - if(this.required){ + if (this.required) { this.dynamicDateControl.setValidators(Validators.required); } this.dateFrom.reset(); @@ -202,7 +242,7 @@ export class DateRangeComponent implements CdrEditor, OnDestroy { const until = valueRange.result.End ? moment(valueRange.result.End) : null; this.dateFrom.setValue(from, { emitEvent: true }); this.dateUntil.setValue(until, { emitEvent: true }); - if(this.required){ + if (this.required) { this.dateFrom.setValidators(Validators.required); this.dateUntil.setValidators(Validators.required); } @@ -211,7 +251,7 @@ export class DateRangeComponent implements CdrEditor, OnDestroy { } } - private updateOptions():void{ + private updateOptions(): void { DateRangeTypeLabels.forEach((label, index) => { this.dynamicDateRangeOptions.push({ display: this.translateProviderService.GetTranslation({ UidColumn: 'RESOURCES', Key: label }), 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 82c1cefe7..956f6d2e1 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 @@ -41,73 +41,102 @@ import { EditRiskIndexComponent } from './edit-risk-index/edit-risk-index.compon import { DateRangeComponent } from './date-range/date-range.component'; import { EditUrlComponent } from './edit-url/edit-url.component'; +/** + * A default provider for creating {@link CdrEditor | CDR editors}. + * It creates an editor for basic types such as string, decimal, double etc. + * as well as some more complex types like date, multi value or a risk index. + * + * Depending on the type, a different {@link CdrEditor | CDR editor} is used. + * (e.g. bool typed column => {@link EditBooleanComponent}) + */ export class DefaultCdrEditorProvider implements CdrEditorProvider { + constructor() {} - constructor() { } - - public createEditor(parent: ViewContainerRef, cdref: ColumnDependentReference): ComponentRef { - const meta = cdref.column.GetMetadata(); - const multiLine = meta.IsMultiLine(); - const multiValue = meta.IsMultiValue(); - const range = meta.IsRange(); - const limitedValues = this.isLimitedValues(meta); - const schemaKey = meta.GetSchemaKey(); - const isRiskIndexColumn = ['RiskIndex', 'RiskLevel'].includes(schemaKey.substring(schemaKey.lastIndexOf('.') + 1)) - || schemaKey == 'QERRiskIndex.Weight'; - const type = meta.GetType(); - - if (type === ValType.Binary) { - return this.createBound(EditImageComponent, parent, cdref); - } - - if (!multiLine && !multiValue && !range && !limitedValues && !isRiskIndexColumn) { - switch (type) { - - case ValType.Bool: return this.createBound(EditBooleanComponent, parent, cdref); + /** + * Creates an editor, depending on the given CDR and binds the column to the newly created component. + * The actual instance of the editor is determined by the data type of the column and some additional information. + * (e.g. bool typed column => {@link EditBooleanComponent}) + * (e.g. string typed column with IsMultiLIne => {@link EditMultilineComponent}) + * @param parent The view container used for rendering the editor into. + * @param cdref A column dependent reference that contains the data for the editor. + * @returns An instance of {@link CdrEditor}, that can be used or editing data. + */ + public createEditor(parent: ViewContainerRef, cdref: ColumnDependentReference): ComponentRef { + const meta = cdref.column.GetMetadata(); + const multiLine = meta.IsMultiLine(); + const multiValue = meta.IsMultiValue(); + const range = meta.IsRange(); + const limitedValues = this.isLimitedValues(meta); + const schemaKey = meta.GetSchemaKey(); + const isRiskIndexColumn = + ['RiskIndex', 'RiskLevel'].includes(schemaKey.substring(schemaKey.lastIndexOf('.') + 1)) || schemaKey == 'QERRiskIndex.Weight'; + const type = meta.GetType(); - case ValType.Byte: - case ValType.Decimal: - case ValType.Double: - case ValType.Int: - case ValType.Long: - case ValType.Short: return this.createBound(EditNumberComponent, parent, cdref); + if (type === ValType.Binary) { + return this.createBound(EditImageComponent, parent, cdref); + } - case ValType.Date: return this.createBound(EditDateComponent, parent, cdref); - } - - } else if (limitedValues) { - return multiValue - ? this.createBound(EditMultiLimitedValueComponent, parent, cdref) - : this.createBound(EditLimitedValueComponent, parent, cdref); - } else if (multiValue) { - return this.createBound(EditMultiValueComponent, parent, cdref); - } else if (multiLine) { - return this.createBound(EditMultilineComponent, parent, cdref); - } else if (isRiskIndexColumn) { - return this.createBound(EditRiskIndexComponent, parent, cdref); - } + if (!multiLine && !multiValue && !range && !limitedValues && !isRiskIndexColumn) { + switch (type) { + case ValType.Bool: + return this.createBound(EditBooleanComponent, parent, cdref); - if (range && type === ValType.Date) { - return this.createBound(DateRangeComponent, parent, cdref); - } + case ValType.Byte: + case ValType.Decimal: + case ValType.Double: + case ValType.Int: + case ValType.Long: + case ValType.Short: + return this.createBound(EditNumberComponent, parent, cdref); - if (meta.isUrl) { - return this.createBound(EditUrlComponent, parent, cdref); - } + case ValType.Date: + return this.createBound(EditDateComponent, parent, cdref); + } + } else if (limitedValues) { + return multiValue + ? this.createBound(EditMultiLimitedValueComponent, parent, cdref) + : this.createBound(EditLimitedValueComponent, parent, cdref); + } else if (multiValue) { + return this.createBound(EditMultiValueComponent, parent, cdref); + } else if (multiLine) { + return this.createBound(EditMultilineComponent, parent, cdref); + } else if (isRiskIndexColumn) { + return this.createBound(EditRiskIndexComponent, parent, cdref); + } - return null; + if (range && type === ValType.Date) { + return this.createBound(DateRangeComponent, parent, cdref); } - private createBound(editor: Type, parent: ViewContainerRef, cdref: ColumnDependentReference) - : ComponentRef { - const result = parent.createComponent(editor); - result.instance.bind(cdref); - return result; + if (meta.isUrl) { + return this.createBound(EditUrlComponent, parent, cdref); } - private isLimitedValues(meta: IValueMetadata): boolean { - const limitedValues = meta.GetLimitedValues(); + return null; + } - return limitedValues != null && limitedValues.length > 0; - } + /** + * @ignore only used internally. + * Creates the component and binds its value. + */ + private createBound( + editor: Type, + parent: ViewContainerRef, + cdref: ColumnDependentReference + ): ComponentRef { + const result = parent.createComponent(editor); + result.instance.bind(cdref); + return result; + } + + /** + * @ignore only ised internally + * Checks, if there are limited values defined for the column. + * @returns True, if there are limited values available, otherwise false. + */ + private isLimitedValues(meta: IValueMetadata): boolean { + const limitedValues = meta.GetLimitedValues(); + + return limitedValues != null && limitedValues.length > 0; + } } diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-binary/edit-binary.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-binary/edit-binary.component.ts index 259536284..4a33a8754 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-binary/edit-binary.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-binary/edit-binary.component.ts @@ -33,27 +33,36 @@ import { CdrEditor } from '../cdr-editor.interface'; import { EntityColumnContainer } from '../entity-column-container'; /** - * A component for viewing / editing binary columns + * Provides a {@link CdrEditor | CDR editor} for editing / viewing binary value columns. + * + * It renders an input field for adding data. + * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. */ @Component({ selector: 'imx-edit-binary', templateUrl: './edit-binary.component.html', - styleUrls: ['./edit-binary.component.scss'] + styleUrls: ['./edit-binary.component.scss'], }) export class EditBinaryComponent implements CdrEditor { + /** + * The form control associated with the editor. + */ public readonly control = new UntypedFormControl({ value: undefined, disabled: true }); // TODO: TFS 806165 "imx-web: Editor für Binärdaten verbessern" umsetzen + /** + * The container that wraps the column functionality. + */ public readonly columnContainer = new EntityColumnContainer(); private message: string; constructor(translationProvider: TranslateService) { - translationProvider.get('#LDS#This property cannot be displayed.').subscribe(value => this.message = value); + translationProvider.get('#LDS#This property cannot be displayed.').subscribe((value) => (this.message = value)); } /** - * Binds a column dependent reference to the component + * Binds a column dependent reference to the component. * @param cdref a column dependent reference */ public bind(cdref: ColumnDependentReference): void { diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-boolean/edit-boolean.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-boolean/edit-boolean.component.ts index 5520b4509..1d6202d9c 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-boolean/edit-boolean.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-boolean/edit-boolean.component.ts @@ -30,13 +30,19 @@ import { UntypedFormControl } from '@angular/forms'; import { EditorBase } from '../editor-base'; /** - * A component for viewing / editing boolean columns + * Provides a {@link CdrEditor | CDR editor} for editing / viewing boolean value columns, by extending {@link EditorBase}. + * + * It renders a checkbox for updating the value. + * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. */ @Component({ selector: 'imx-checkbox-editor', templateUrl: './edit-boolean.component.html', - styleUrls: ['./edit-boolean.component.scss'] + styleUrls: ['./edit-boolean.component.scss'], }) export class EditBooleanComponent extends EditorBase { + /** + * The form control associated with the editor. + */ public readonly control = new UntypedFormControl(); } 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 1412c67f8..52fc4d146 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 @@ -37,7 +37,10 @@ import { ClassloggerService } from '../../classlogger/classlogger.service'; import { DateFormat } from 'imx-qbm-dbts'; /** - * A component for viewing / editing date columns + * Provides a {@link CdrEditor | CDR editor} for editing / viewing date value columns + * + * It uses a {@link DateComponent | date component} for editing the value. + * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. */ @Component({ selector: 'imx-edit-date', @@ -45,19 +48,37 @@ import { DateFormat } from 'imx-qbm-dbts'; styleUrls: ['./edit-date.component.scss'], }) export class EditDateComponent implements CdrEditor, OnDestroy { + /** + * The form control associated with the editor. + */ public readonly control = new UntypedFormControl(undefined, { updateOn: 'blur' }); + /** + * The container that wraps the column functionality. + */ public readonly columnContainer = new EntityColumnContainer(); + /** + * Event that is emitted, after a value has been changed. + */ public readonly valueHasChanged = new EventEmitter(); + /** + * A subject for triggering an update of the editor. + */ public readonly updateRequested = new Subject(); + /** + * Indicator that the component is loading data from the server. + */ public isBusy = false; private readonly subscribers: Subscription[] = []; private isWriting = false; + /** + * Determines, if a time control should be added. + */ public get withTime(): boolean { // try to get the date format detail from metadata; defaulting to DateTime. const dateFormat = this.columnContainer.metaData?.GetDateFormat() ?? DateFormat.DateTime; @@ -67,12 +88,16 @@ export class EditDateComponent implements CdrEditor, OnDestroy { public constructor(private readonly errorHandler: ErrorHandler, private logger: ClassloggerService) {} + /** + * Unsubscribes all events, after the 'OnDestroy' hook is triggered. + */ public ngOnDestroy(): void { this.subscribers.forEach((s) => s.unsubscribe()); } /** - * Binds a column dependent reference to the component + * Binds a column dependent reference to the component. + * Subscribes to subjects from the column dependent reference and its container. * @param cdref a column dependent reference */ public bind(cdref: ColumnDependentReference): void { @@ -144,8 +169,8 @@ export class EditDateComponent implements CdrEditor, OnDestroy { } /** - * updates the value for the CDR - * @param value the new value + * Updates the value for the CDR. + * @param value The Moment object, that is used as the new value for the control. */ private async writeValue(value: Moment): Promise { if (this.control.errors) { diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-default/edit-default.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-default/edit-default.component.ts index d548642e9..0e5e390d6 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-default/edit-default.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-default/edit-default.component.ts @@ -30,7 +30,10 @@ import { UntypedFormControl } from '@angular/forms'; import { EditorBase } from '../editor-base'; /** - * The fallback component, used for viewing / editing a column dependent reference + * Provides a fallback component for editing / viewing a column dependent reference, by extending {@link EditorBase}. + * + * It changes its value by using an input field. + * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. */ @Component({ selector: 'imx-edit-default', 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 7c1101ccd..afc34ecfd 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 @@ -41,6 +41,12 @@ import { LdsReplacePipe } from '../../lds-replace/lds-replace.pipe'; import { MultiValueService } from '../../multi-value/multi-value.service'; import { FkHierarchicalDialogComponent } from '../../fk-hierarchical-dialog/fk-hierarchical-dialog.component'; +/** + * Provides a {@link CdrEditor | CDR editor} for editing / viewing multi foreign key value columns. + * + * Its value is changed by clicking on the 'select' / 'change' button. Then a side sheet is opened for selecting multiple values. + * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. + */ @Component({ selector: 'imx-edit-fk-multi', templateUrl: './edit-fk-multi.component.html', @@ -62,10 +68,10 @@ export class EditFkMultiComponent implements CdrEditor, OnInit, OnDestroy { private readonly subscribers: Subscription[] = []; /** - * Creates a new EditFkMultiComponent for column dependent reference with a foreign key relation. + * Creates a new EditFkMultiComponent. * @param logger Log service. - * @param sidesheet Dialog to open the pickerdialog for selecting objects. - */ + * @param sidesheet Side sheet, that opens the picker dialog for selecting objects. + */ constructor( private readonly logger: ClassloggerService, private readonly sidesheet: EuiSidesheetService, @@ -167,17 +173,17 @@ 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); - } + 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 + * Opens a side sheet for selecting fk objects. */ public async editAssignment(): Promise { const dialogRef = this.sidesheet.open(this.isHierarchical ? FkHierarchicalDialogComponent : FkAdvancedPickerComponent, { @@ -223,8 +229,8 @@ export class EditFkMultiComponent implements CdrEditor, OnInit, OnDestroy { } /** - * updates the value for the CDR - * @param value the new value + * Updates the value for the CDR. + * @param value The new value struct, that is used for the new value of the component. */ private async writeValue(value: ValueStruct): Promise { this.logger.debug(this, 'writeValue - called with', value); diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk.component.spec.ts b/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk.component.spec.ts index 43916ca3b..eca50406e 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk.component.spec.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-fk/edit-fk.component.spec.ts @@ -42,34 +42,31 @@ import { LdsReplacePipe } from '../../lds-replace/lds-replace.pipe'; import { ViewPropertyComponent } from '../view-property/view-property.component'; import { ChangeDetectorRef } from '@angular/core'; -function createColumnStub(value: ValueStruct, canEdit = true, candidateCollections?: EntityCollectionData[], minLength = 0): IEntityColumn { - const getFki = c => ({ Get: _ => Promise.resolve(c) } as IForeignKeyInfo); - - return new EntityColumnStub( - value.DataValue, - value.DisplayValue, - { - GetFkRelations: () => ( - candidateCollections ? - candidateCollections.map(c => getFki(c)) - : [getFki({ Entities: [] })] - ) as ReadonlyArray, - CanEdit: () => canEdit, - GetLimitedValues: () => undefined, - GetMinLength: () => minLength, - GetDisplay: () => 'display' - } as IValueMetadata - ); +function createColumnStub( + value: ValueStruct, + canEdit = true, + candidateCollections?: EntityCollectionData[], + minLength = 0, +): IEntityColumn { + const getFki = (c) => ({ Get: (_) => Promise.resolve(c) }) as IForeignKeyInfo; + + return new EntityColumnStub(value.DataValue, value.DisplayValue, { + GetFkRelations: () => + (candidateCollections ? candidateCollections.map((c) => getFki(c)) : [getFki({ Entities: [] })]) as ReadonlyArray, + CanEdit: () => canEdit, + GetLimitedValues: () => undefined, + GetMinLength: () => minLength, + GetDisplay: () => 'display', + } as IValueMetadata); } - describe('EditFkComponent', () => { let component: EditFkComponent; let fixture: MockedComponentFixture; const metadataServiceStub = { - update: jasmine.createSpy('update'), - tables:[] + updateNonExisting: jasmine.createSpy('updateNonExisting'), + tables: [], } as any; const detectorstub = { @@ -82,16 +79,16 @@ describe('EditFkComponent', () => { .keep(LdsReplacePipe) .mock(ViewPropertyComponent) .mock(EuiSidesheetService) - .mock(MetadataService,metadataServiceStub) - .mock(ChangeDetectorRef,detectorstub) - .keep(TranslateService) + .mock(MetadataService, metadataServiceStub) + .mock(ChangeDetectorRef, detectorstub) + .keep(TranslateService); }); beforeEach(() => { fixture = MockRender(EditFkComponent); component = fixture.point.componentInstance; QbmDefaultMocks.sidesheetServiceStub.open.calls.reset(); - metadataServiceStub.update.calls.reset(); + metadataServiceStub.updateNonExisting.calls.reset(); }); afterAll(() => { @@ -105,29 +102,30 @@ describe('EditFkComponent', () => { [ { input: { isReadonly: false }, expected: { canEdit: true } }, { input: { isReadonly: true }, expected: { canEdit: false } }, - ].forEach(testcase => - it('should bind the object to this component' + testcase.input.isReadonly, () => { - // Arrange - const metadataMinLength = 5; - const columnStub = new EntityColumnStub('value', 'display', { - CanEdit: () => !testcase.input.isReadonly, - GetMinLength: () => metadataMinLength, - GetFkRelations: () => [{} as IForeignKeyInfo] as ReadonlyArray, - GetLimitedValues: () => undefined - } as IValueMetadata); - - // Act - component.bind({ - column: columnStub, - isReadOnly: () => testcase.input.isReadonly - }); - - // Assert - expect(component.columnContainer.displayValue).toBe(columnStub.GetDisplayValue()); - expect(component.columnContainer.value).toEqual(columnStub.GetValue()); - expect(component.columnContainer.canEdit).toEqual(testcase.expected.canEdit, 'canEdit'); - expect(metadataServiceStub.update).toHaveBeenCalled(); - })); + ].forEach((testcase) => + it('should bind the object to this component' + testcase.input.isReadonly, () => { + // Arrange + const metadataMinLength = 5; + const columnStub = new EntityColumnStub('value', 'display', { + CanEdit: () => !testcase.input.isReadonly, + GetMinLength: () => metadataMinLength, + GetFkRelations: () => [{} as IForeignKeyInfo] as ReadonlyArray, + GetLimitedValues: () => undefined, + } as IValueMetadata); + + // Act + component.bind({ + column: columnStub, + isReadOnly: () => testcase.input.isReadonly, + }); + + // Assert + expect(component.columnContainer.displayValue).toBe(columnStub.GetDisplayValue()); + expect(component.columnContainer.value).toEqual(columnStub.GetValue()); + expect(component.columnContainer.canEdit).toEqual(testcase.expected.canEdit, 'canEdit'); + expect(metadataServiceStub.updateNonExisting).toHaveBeenCalled(); + }), + ); it('is readonly when the cdr is missing', () => { // Act/Assert @@ -136,75 +134,76 @@ describe('EditFkComponent', () => { [ { - valueStructs: [{ - DataValue: 'val1', - DisplayValue: 'displayValue' - }], - canEdit: true + valueStructs: [ + { + DataValue: 'val1', + DisplayValue: 'displayValue', + }, + ], + canEdit: true, }, { valueStructs: [], - canEdit: true + canEdit: true, }, { valueStructs: [], - canEdit: false - } - ].forEach(testcase => - it('should change the assignment', fakeAsync(() => { - const fakeDelay = 1000; - const start = { - DataValue: 'val0', - DisplayValue: 'display0' - }; - const column = createColumnStub(start, testcase.canEdit); - component.bind({ - column, - isReadOnly: () => false - }); + canEdit: false, + }, + ].forEach((testcase) => + it('should change the assignment', fakeAsync(() => { + const fakeDelay = 1000; + const start = { + DataValue: 'val0', + DisplayValue: 'display0', + }; + const column = createColumnStub(start, testcase.canEdit); + component.bind({ + column, + isReadOnly: () => false, + }); - tick(fakeDelay); + tick(fakeDelay); - component.editAssignment(); + component.editAssignment(); - tick(fakeDelay); + tick(fakeDelay); - QbmDefaultMocks.afterClosedSubject.subscribe(_ => - expect(QbmDefaultMocks.sidesheetServiceStub.open).toHaveBeenCalled() - ); + QbmDefaultMocks.afterClosedSubject.subscribe((_) => expect(QbmDefaultMocks.sidesheetServiceStub.open).toHaveBeenCalled()); - QbmDefaultMocks.afterClosedSubject.next({ table: {}, candidates: testcase.valueStructs }); + QbmDefaultMocks.afterClosedSubject.next({ table: {}, candidates: testcase.valueStructs }); - tick(fakeDelay); + tick(fakeDelay); - discardPeriodicTasks(); + discardPeriodicTasks(); - if (testcase.canEdit) { - if (testcase.valueStructs && testcase.valueStructs.length > 0) { - expect(component.columnContainer.displayValue).toBe(testcase.valueStructs[0].DisplayValue); - expect(component.control.value).toEqual(testcase.valueStructs[0]); - expect(component.columnContainer.value).toEqual(testcase.valueStructs[0].DataValue); + if (testcase.canEdit) { + if (testcase.valueStructs && testcase.valueStructs.length > 0) { + expect(component.columnContainer.displayValue).toBe(testcase.valueStructs[0].DisplayValue); + expect(component.control.value).toEqual(testcase.valueStructs[0]); + expect(component.columnContainer.value).toEqual(testcase.valueStructs[0].DataValue); + } else { + expect(component.columnContainer.displayValue).toBeUndefined(); + expect(component.control.value).toBeUndefined(); + expect(component.columnContainer.value).toBeUndefined(); + } } else { - expect(component.columnContainer.displayValue).toBeUndefined(); - expect(component.control.value).toBeUndefined(); - expect(component.columnContainer.value).toBeUndefined(); + expect(component.columnContainer.displayValue).toBe(start.DisplayValue); + expect(component.control.value).toEqual(start); + expect(component.columnContainer.value).toEqual(start.DataValue); } - } else { - expect(component.columnContainer.displayValue).toBe(start.DisplayValue); - expect(component.control.value).toEqual(start); - expect(component.columnContainer.value).toEqual(start.DataValue); - } - }))); + })), + ); it('should revert to previous value if leaving autocomplete', () => { const start = { DataValue: 'val0', - DisplayValue: 'display0' + DisplayValue: 'display0', }; const column = createColumnStub(start); component.bind({ column, - isReadOnly: () => false + isReadOnly: () => false, }); component.control.setValue('some string', { emitEvent: false }); @@ -217,12 +216,12 @@ describe('EditFkComponent', () => { it('should remove the assignment', async () => { const start = { DataValue: 'val0', - DisplayValue: 'display0' + DisplayValue: 'display0', }; const column = createColumnStub(start); component.bind({ column, - isReadOnly: () => false + isReadOnly: () => false, }); await component.removeAssignment(); @@ -235,12 +234,12 @@ describe('EditFkComponent', () => { it('should update the entity upon autocomplete option selection', async () => { const start = { DataValue: 'val0', - DisplayValue: 'display0' + DisplayValue: 'display0', }; const column = createColumnStub(start); component.bind({ column, - isReadOnly: () => false + isReadOnly: () => false, }); const value = { DataValue: 'val1', DisplayValue: 'display1' }; @@ -269,30 +268,30 @@ describe('EditFkComponent', () => { }, { DataValue: 'val1', - DisplayValue: 'display1' - } + DisplayValue: 'display1', + }, ]; const candidateCollection = { - Entities: mockValues.map(e => ({ + Entities: mockValues.map((e) => ({ Display: e.DisplayValue, - Columns: { XObjectKey: { Value: createKey(e.DataValue) } } + Columns: { XObjectKey: { Value: createKey(e.DataValue) } }, })), - TotalCount: mockValues.length + TotalCount: mockValues.length, }; const column = createColumnStub( { DataValue: createKey(mockValues[0].DataValue), - DisplayValue: mockValues[0].DisplayValue + DisplayValue: mockValues[0].DisplayValue, }, true, - [candidateCollection, { Entities: [], TotalCount: 0 }] + [candidateCollection, { Entities: [], TotalCount: 0 }], ); component.bind({ column, - isReadOnly: () => false + isReadOnly: () => false, }); - spyOn((component as any).changeDetectorRef , 'detectChanges'); + spyOn((component as any).changeDetectorRef, 'detectChanges'); await component.ngOnInit(); expect(component.candidates[0].DataValue).toEqual(candidateCollection.Entities[0].Columns.XObjectKey.Value); @@ -302,27 +301,27 @@ describe('EditFkComponent', () => { [ { description: '= 0', minLength: 0, expectedError: false }, - { description: '> 0', minLength: 1, expectedError: true } - ].forEach(testcase => - it('should set error.required to ' + testcase.expectedError + - ' if minLength ' + testcase.description + ' and value is not set', () => { - const start = { - DataValue: null - }; - const column = createColumnStub(start, true, undefined, testcase.minLength); - component.bind({ - column, - isReadOnly: () => false - }); - - component.control.markAsTouched(); - component.control.updateValueAndValidity({ onlySelf: true, emitEvent: false }); - - expect(component.control.value).toBeUndefined(); - if (testcase.expectedError) { - expect(component.control.errors.required).toBeTruthy(); - } else { - expect(component.control.errors).toBeNull(); - } - })); + { description: '> 0', minLength: 1, expectedError: true }, + ].forEach((testcase) => + it('should set error.required to ' + testcase.expectedError + ' if minLength ' + testcase.description + ' and value is not set', () => { + const start = { + DataValue: null, + }; + const column = createColumnStub(start, true, undefined, testcase.minLength); + component.bind({ + column, + isReadOnly: () => false, + }); + + component.control.markAsTouched(); + component.control.updateValueAndValidity({ onlySelf: true, emitEvent: false }); + + expect(component.control.value).toBeUndefined(); + if (testcase.expectedError) { + expect(component.control.errors.required).toBeTruthy(); + } else { + expect(component.control.errors).toBeNull(); + } + }), + ); }); 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 fbc5acc08..8300a645b 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 @@ -54,12 +54,17 @@ import { ForeignKeySelection } from '../../fk-advanced-picker/foreign-key-select import { Candidate } from '../../fk-advanced-picker/candidate.interface'; import { MetadataService } from '../../base/metadata.service'; import { FkHierarchicalDialogComponent } from '../../fk-hierarchical-dialog/fk-hierarchical-dialog.component'; -import { LdsReplacePipe } from '../../lds-replace/lds-replace.pipe'; /** - * A component for viewing / editing foreign key relations + * Provides a {@link CdrEditor | CDR editor} for editing / viewing foreign key value columns. + * + * There are two methods for selecting values available: + *
    + *
  1. using an auto complete control - this is used for a flat list, containing values from a single table.
  2. + *
  3. by using a 'select' / 'change' button - this is used by hierarchical listings or elements from multiple tables.
  4. + *
+ * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. */ -// tslint:disable-next-line: max-classes-per-file @Component({ selector: 'imx-edit-fk', templateUrl: './edit-fk.component.html', @@ -67,10 +72,17 @@ import { LdsReplacePipe } from '../../lds-replace/lds-replace.pipe'; changeDetection: ChangeDetectionStrategy.OnPush, }) /** - * A component for viewing / editing foreign key relations + * A component for viewing / editing foreign key relations. */ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnInit { + /** + * A subject for triggering an update of the editor. + */ public readonly updateRequested = new Subject(); + + /** + * Indicator that the component is loading data from the server, or has a candidate list. + */ public get hasCandidatesOrIsLoading(): boolean { return ( this.candidatesTotalCount > 0 || @@ -83,15 +95,50 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI ); } + /** + * The form control associated with the editor. + */ public readonly control = new UntypedFormControl(undefined); + + /** + * The container that wraps the column functionality. + */ public readonly columnContainer = new EntityColumnContainer(); + + /** + * @ignore Only used in template. + */ public readonly pageSize = 20; + + /** + * A list of possible candidates, that can be selected. + */ public candidates: Candidate[]; + + /** + * Indicator that the component is loading data from the server. + */ public loading = false; + + /** + * The table, the user is currently selecting items from. + * It is possible to choose elements from different tables at the same time. + */ public selectedTable: IForeignKeyInfo; + + /** + * Indicator, whether the candidate data is hierarchical or not. + */ public isHierarchical: boolean; + + /** + * The number of possible candidates + */ public candidatesTotalCount: number; + /** + * Event that is emitted, after a value has been changed. + */ public readonly valueHasChanged = new EventEmitter(); private parameters: CollectionLoadParameters = { PageSize: this.pageSize, StartIndex: 0 }; @@ -102,19 +149,18 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI @ViewChild('viewport') private viewport: CdkVirtualScrollViewport; /** - * Creates a new EditFkComponent for column dependent reference with a foreign key relation. - * @param logger Log service. - * @param sidesheet Dialog to open the pickerdialog for selecting an object. - * @param metadataProvider Service providing table meta data + * Creates a new EditFkComponent. + * @param logger The log service, that is used for logging. + * @param sidesheet Side sheet, that opens the picker dialog for selecting an object. + * @param metadataProvider Service providing table meta data. */ constructor( private readonly logger: ClassloggerService, private readonly sidesheet: EuiSidesheetService, private readonly changeDetectorRef: ChangeDetectorRef, private readonly translator: TranslateService, - private readonly ldsReplace: LdsReplacePipe, public readonly metadataProvider: MetadataService, - private readonly errorHandler: ErrorHandler + private readonly errorHandler: ErrorHandler, ) { this.subscribers.push( this.control.valueChanges.pipe(debounceTime(500)).subscribe(async (keyword) => { @@ -124,15 +170,21 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI } return this.search(keyword); - }) + }), ); } + /** + * Initializes the candidate list, after the 'OnInit' hook is triggered. + */ public async ngOnInit(): Promise { return this.initCandidates(); - // Muss leider immer gemacht werden, damit klar ist, ob es sich um eine hierarchische Ansicht handelt oder nicht + // Unfortunately this is mandatory, to decide, if the component is hierarchical or not } + /** + * Initializes the viewport for dynamic scrolling, after the 'AfterViewInit' hook is triggered. + */ public async ngAfterViewInit(): Promise { if (this.columnContainer && this.columnContainer.canEdit && this.viewport) { // Give a debounce to the stream so we don't get multiple calls and lose data @@ -151,30 +203,36 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI this.viewport.checkViewportSize(); this.changeDetectorRef.detectChanges(); } - }) + }), ); } } + /** + * Unsubscribes all events, after the 'OnDestroy' hook is triggered. + */ public ngOnDestroy(): void { this.subscribers.forEach((s) => s.unsubscribe()); } + /** + * Reinitialize the candidate list, if the input is focused. + */ public async inputFocus(): Promise { if (!this.candidates?.length && !this.loading) { await this.initCandidates(); } } + /** + * Handles the control value and displays it, when the auto complete control is opened. + */ public async onOpened(): Promise { - if (this.control.value) { - // 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) { - this.candidates = this.savedCandidates; - } + // 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) { + this.candidates = this.savedCandidates; } else if (this.parameters.search || this.parameters.filter || this.control.value == null) { - // If we don't have a chosen value, then we have residual values, reset them and update await this.updateCandidates({ search: undefined, filter: undefined, StartIndex: 0 }, false); } @@ -185,17 +243,29 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI } } + /** + * @ignore Only used in template. + * Gets the display of a candidate. + * @param candidate The candidate object. + * @returns The display of the candidate object. + */ public getDisplay(candidate: Candidate): string { return candidate ? candidate.DisplayValue : undefined; } + /** + * Writes the value, if a new one is selected in the auto complete control. + * @param event The MatAutocompleteSelectedEvent, that was triggered. + */ public async optionSelected(event: MatAutocompleteSelectedEvent): Promise { - // Save these parameters for later use, set start index back to zero - this.savedParameters = this.parameters; - this.savedCandidates = this.candidates; return this.writeValue(event.option.value); } + /** + * Removes all the assignments and writes the 'empty' value to the column. + * Afterward it resets all request parameter and updates the candidate list. + * @param event The event, that was emitted. + */ public async removeAssignment(event?: Event): Promise { if (event) { event.stopPropagation(); @@ -219,16 +289,24 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI */ } + /** + * Is called, after the auto complete closes and writes the value to the column. + * @param event The event, that was emitted. + */ public close(event?: any): void { if (this.control.value == null || typeof this.control.value === 'string') { this.logger.debug(this, 'autoCompleteClose no match - reset to previous value', event); this.control.setValue(this.getValueStruct(), { emitEvent: false }); } + // Save these parameters for later use, set start index back to zero + this.savedParameters = this.parameters; + this.savedCandidates = this.candidates; } /** - * @ignore - * Opens a dialog for selecting an object + * Opens a dialog for selecting an object. + * This is used, if the data is hierarchical or multiple tabes are available. + * @param event The event, that was emitted. */ public async editAssignment(event?: Event): Promise { if (event) { @@ -269,7 +347,8 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI } /** - * Binds a column dependent reference to the component + * Binds a column dependent reference to the component. + * Subscribes to subjects from the column dependent reference and its container. * @param cdref a column dependent reference */ public bind(cdref: ColumnDependentReference): void { @@ -282,7 +361,7 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI cdref.minlengthSubject.subscribe((elem) => { this.setControlValue(); this.changeDetectorRef.detectChanges(); - }) + }), ); } @@ -300,7 +379,7 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI this, `Control (${this.columnContainer.name}) set to new value:`, this.columnContainer.value, - this.control.value + this.control.value, ); this.candidates = []; this.setControlValue(); @@ -310,7 +389,7 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI } } this.valueHasChanged.emit({ value: this.control.value }); - }) + }), ); this.subscribers.push( @@ -326,7 +405,7 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI } this.valueHasChanged.emit({ value: this.control.value }); }); - }) + }), ); this.logger.trace(this, 'Control initialized', this.control.value); } else { @@ -345,7 +424,7 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI } this.selectedTable = table || fkRelations[0]; - this.metadataProvider.update(fkRelations.map((fkr) => fkr.TableName)); + this.metadataProvider.updateNonExisting(fkRelations.map((fkr) => fkr.TableName)); } this.control.setValue(this.getValueStruct(), { emitEvent: false }); @@ -377,8 +456,8 @@ export class EditFkComponent implements CdrEditor, AfterViewInit, OnDestroy, OnI } /** - * updates the value for the CDR - * @param value the new value + * Updates the value for the CDR. + * @param value The new value struct, that should be used as the new control value. */ private async writeValue(value: ValueStruct): Promise { this.logger.debug(this, 'writeValue called with value', value); 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 0b6e993bb..9d515ffa0 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 @@ -36,7 +36,10 @@ import { Base64ImageService } from '../../images/base64-image.service'; import { FileSelectorService } from '../../file-selector/file-selector.service'; /** - * A component for viewing / editing binary columns with image data + * Provides a {@link CdrEditor | CDR editor} for editing / viewing image data columns. + * + * To change its value, it uses an {@link ImageSelectComponent | image select component}. + * When set to read-only, it uses an {@link ImageViewComponent | image view component} to display the content. */ @Component({ selector: 'imx-edit-image', @@ -44,19 +47,41 @@ import { FileSelectorService } from '../../file-selector/file-selector.service'; styleUrls: ['./edit-image.component.scss'], }) export class EditImageComponent implements CdrEditor, OnDestroy { + /** + * @ignore only to access the file input from the template. + */ @ViewChild('file') public fileInput: ElementRef; + /** + * Gets a small hint, if the file format is not supported. + */ public get fileFormatHint(): string { return this.fileFormatError ? '#LDS#Please select an image in PNG format.' : undefined; } + /** + * A subject for triggering an update of the editor. + */ public readonly updateRequested = new Subject(); + /** + * The form control associated with the editor. + */ public readonly control = new UntypedFormControl(undefined); + /** + * The container that wraps the column functionality. + */ public readonly columnContainer = new EntityColumnContainer(); + + /** + * Event that is emitted, after a value has been changed. + */ public readonly valueHasChanged = new EventEmitter(); + /** + * Indicator that the component is loading data from the server. + */ public isLoading = false; private fileFormatError = false; @@ -75,13 +100,17 @@ export class EditImageComponent implements CdrEditor, OnDestroy { ); } + /** + * Unsubscribes all events, after the 'OnDestroy' hook is triggered. + */ public ngOnDestroy(): void { this.subscriptions.forEach((s) => s.unsubscribe()); } /** - * Binds a column dependent reference to the component - * @param cdref a column dependent reference + * Binds a column dependent reference to the component. + * Subscribes to subjects from the column dependent reference and its container. + * @param cdref a column dependent reference. */ public bind(cdref: ColumnDependentReference): void { if (cdref && cdref.column) { @@ -130,17 +159,24 @@ export class EditImageComponent implements CdrEditor, OnDestroy { } } + /** + * Resets the file format error. + */ public resetFileFormatErrorState(): void { this.fileFormatError = false; } + /** + * Emits a list of files to the {@link FileSelectorService | file selector service}. + * @param files A list of files to emit as *.png. + */ // TODO: Check Upgrade public emitFiles(files: FileList): void { this.fileSelector.emitFiles(files, 'image/png'); } /** - * removes the current image + * Removes the current image and writes the 'empty' value to the column. */ public async remove(): Promise { this.fileInput.nativeElement.value = ''; @@ -163,8 +199,8 @@ export class EditImageComponent implements CdrEditor, OnDestroy { } /** - * updates the value for the CDR - * @param value the new image url + * Updates the value for the CDR. + * @param value The new image url, that will be used as the new value. */ private async writeValue(value: string): Promise { this.logger.debug(this, 'writeValue called with value', value); diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-limited-value/edit-limited-value.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-limited-value/edit-limited-value.component.ts index ab261d358..f99f13e2e 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-limited-value/edit-limited-value.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-limited-value/edit-limited-value.component.ts @@ -31,7 +31,10 @@ import { EditorBase } from '../editor-base'; import { ClassloggerService } from '../../classlogger/classlogger.service'; /** - * A component for viewing / editing limited value columns + * Provides a {@link CdrEditor | CDR editor} for editing / viewing limited value columns. + * + * To change the value it uses an Angular Material select component. + * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. */ @Component({ selector: 'imx-edit-limited-value', @@ -39,13 +42,19 @@ import { ClassloggerService } from '../../classlogger/classlogger.service'; styleUrls: ['./edit-limited-value.component.scss'], }) export class EditLimitedValueComponent extends EditorBase { + /** + * The form control associated with the editor. + */ public readonly control = new UntypedFormControl(undefined, { updateOn: 'blur' }); constructor(logger: ClassloggerService) { super(logger); } - public removeAssignment(evt: Event){ + /** + * Removes the current assignment and writes the 'empty' value to the form control. + */ + public removeAssignment(evt: Event) { evt.stopPropagation(); this.control.setValue(''); this.control.markAsDirty(); 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 cd9cfbb24..111716158 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 @@ -37,7 +37,10 @@ import { EntityColumnContainer } from '../entity-column-container'; import { MultiValueService } from '../../multi-value/multi-value.service'; /** - * A component for viewing / editing multi limited value columns + * Provides a {@link CdrEditor | CDR editor} for editing / viewing multi limited value columns + * + * To change the value it uses a list of check boxes with one box per possible value. + * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. */ @Component({ selector: 'imx-edit-multi-limited-value', @@ -47,10 +50,20 @@ import { MultiValueService } from '../../multi-value/multi-value.service'; export class EditMultiLimitedValueComponent implements CdrEditor, OnDestroy { public readonly updateRequested = new Subject(); + /** + * The form control associated with the editor. + */ // TODO: Check Upgrade public control = new UntypedFormArray([]); + /** + * The container that wraps the column functionality. + */ public readonly columnContainer = new EntityColumnContainer(); + + /** + * Event that is emitted, after a value has been changed. + */ public readonly valueHasChanged = new EventEmitter(); public readonly pendingChanged = new EventEmitter(); @@ -63,12 +76,16 @@ export class EditMultiLimitedValueComponent implements CdrEditor, OnDestroy { private readonly multiValueProvider: MultiValueService ) {} + /** + * Unsubscribes all events, after the 'OnDestroy' hook is triggered. + */ public ngOnDestroy(): void { this.subscriptions.forEach((s) => s.unsubscribe()); } /** - * Binds a column dependent reference to the component + * Binds a column dependent reference to the component. + * Subscribes to subjects from the column dependent reference and its container. * @param cdref a column dependent reference */ public bind(cdref: ColumnDependentReference): void { @@ -120,6 +137,9 @@ export class EditMultiLimitedValueComponent implements CdrEditor, OnDestroy { } } + /** + * Initializes possible values and marks all selected ones. + */ public initValues(): void { if (this.control.controls?.length > 0) { return; @@ -136,8 +156,8 @@ export class EditMultiLimitedValueComponent implements CdrEditor, OnDestroy { } /** - * updates the value for the CDR - * @param values the new values + * Updates the value for the CDR. + * @param values The boolean values, that will be used as new values. */ private async writeValue(values: boolean[]): Promise { this.logger.debug(this, 'writeValue called with value', values); diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-multi-value/edit-multi-value.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-multi-value/edit-multi-value.component.ts index bab611d98..b9c967d42 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-multi-value/edit-multi-value.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-multi-value/edit-multi-value.component.ts @@ -35,7 +35,10 @@ import { CdrEditor, ValueHasChangedEventArg } from '../cdr-editor.interface'; import { MultiValueService } from '../../multi-value/multi-value.service'; /** - * A component for viewing / editing multi value columns + * Provides a {@link CdrEditor | CDR editor} for editing / viewing multi valued columns. + * + * To change its value, it uses a text area. Each line represents a part of the multi value. + * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. */ @Component({ selector: 'imx-edit-multi-value', @@ -43,10 +46,19 @@ import { MultiValueService } from '../../multi-value/multi-value.service'; styleUrls: ['./edit-multi-value.component.scss'], }) export class EditMultiValueComponent implements CdrEditor, OnDestroy { + /** + * The form control associated with the editor. + */ public readonly control = new UntypedFormControl(undefined, { updateOn: 'blur' }); + /** + * The container that wraps the column functionality. + */ public readonly columnContainer = new EntityColumnContainer(); + /** + * Event that is emitted, after a value has been changed. + */ public readonly valueHasChanged = new EventEmitter(); private readonly subscribers: Subscription[] = []; @@ -54,12 +66,16 @@ export class EditMultiValueComponent implements CdrEditor, OnDestroy { constructor(private readonly logger: ClassloggerService, private readonly multiValueProvider: MultiValueService) {} + /** + * Unsubscribes all events, after the 'OnDestroy' hook is triggered. + */ public ngOnDestroy(): void { this.subscribers.forEach((subscriber) => subscriber.unsubscribe()); } /** - * Binds a column dependent reference to the component + * Binds a column dependent reference to the component. + * Subscribes to subjects from the column dependent reference and its container. * @param cdref a column dependent reference */ public bind(cdref: ColumnDependentReference): void { @@ -103,8 +119,8 @@ export class EditMultiValueComponent implements CdrEditor, OnDestroy { } /** - * updates the value for the CDR - * @param values the new value + * Updates the value for the CDR. + * @param values The values, that will be used as a new value. */ private async writeValue(value: string): Promise { this.logger.debug(this, 'writeValue called with value', value); diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-multiline/edit-multiline.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-multiline/edit-multiline.component.ts index 2d00770d3..e5b3d8157 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-multiline/edit-multiline.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-multiline/edit-multiline.component.ts @@ -30,13 +30,19 @@ import { UntypedFormControl } from '@angular/forms'; import { EditorBase } from '../editor-base'; /** - * A component for viewing / editing multilined string columns + * Provides a {@link CdrEditor | CDR editor} for editing / viewing multiline string columns + * + * To change the value, it uses a text area. + * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. */ @Component({ selector: 'imx-edit-multiline', templateUrl: './edit-multiline.component.html', - styleUrls: ['./edit-multiline.component.scss'] + styleUrls: ['./edit-multiline.component.scss'], }) export class EditMultilineComponent extends EditorBase { + /** + * The form control associated with the editor. + */ public readonly control = new UntypedFormControl(undefined, { updateOn: 'blur' }); } diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-number/edit-number.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-number/edit-number.component.ts index 4b2fa8ab8..728721d4d 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-number/edit-number.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-number/edit-number.component.ts @@ -34,27 +34,35 @@ import { NumberError } from './number-error.interface'; import { NumberValidatorService } from './number-validator.service'; /** - * A component for viewing / editing number columns + * Provides a {@link CdrEditor | CDR editor} for editing / viewing number value columns. + * + * To change the value, it uses an input field, that is typed as 'number'. + * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. */ @Component({ selector: 'imx-edit-number', templateUrl: './edit-number.component.html', - styleUrls: ['./edit-number.component.scss'] + styleUrls: ['./edit-number.component.scss'], }) export class EditNumberComponent extends EditorBase implements AfterViewInit { + /** + * The form control associated with the editor. + */ public readonly control = new UntypedFormControl(undefined, { updateOn: 'blur' }); - constructor( - logger: ClassloggerService, - private readonly numberValidator: NumberValidatorService, - ) { + constructor(logger: ClassloggerService, private readonly numberValidator: NumberValidatorService) { super(logger); } + /** + * Sets the validators according to the data type, after the 'AfterViewInit' hook is triggered + */ public ngAfterViewInit(): void { - if (this.columnContainer.type === ValType.Int || + if ( + this.columnContainer.type === ValType.Int || this.columnContainer.type === ValType.Long || - this.columnContainer.type === ValType.Short) { + this.columnContainer.type === ValType.Short + ) { this.control.setValidators(this.control.validator ? [this.control.validator, this.checkIntegerValue()] : this.checkIntegerValue()); } } diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-number/number-error.interface.ts b/imxweb/projects/qbm/src/lib/cdr/edit-number/number-error.interface.ts index 99bb608c9..21494121d 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-number/number-error.interface.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-number/number-error.interface.ts @@ -24,8 +24,22 @@ * */ +/** + * Represents an error state, that can be caused by a number value. + */ export interface NumberError { + /** + * Indicates, that the number is not a number at all. + */ invalidInteger?: boolean; + + /** + * Indicates, that the value is out of the lower bound. + */ rangeMin?: boolean; + + /** + * Indicates, that the value is out of the upper bound. + */ rangeMax?: boolean; } diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-number/number-validator.service.ts b/imxweb/projects/qbm/src/lib/cdr/edit-number/number-validator.service.ts index 48be63c34..0d1ef64d4 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-number/number-validator.service.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-number/number-validator.service.ts @@ -29,12 +29,25 @@ import { Injectable } from '@angular/core'; import { ValueConstraint } from 'imx-qbm-dbts'; import { NumberError } from './number-error.interface'; +/** + * A service for providing a number validation. + */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class NumberValidatorService { private readonly regexPatternInteger = /^[-+]?\d+$/; + /** + * Validates the value by checking two things: + *
    + *
      The value is a valid number.
    + *
      The value is inside the range, given by a {@link ValueConstraint}.
    + *
+ * @param value The value that needs to be checked. + * @param range The {@link ValueConstraint}, that determines the bounds. + * @returns + */ public validate(value: any, range: ValueConstraint): NumberError | null { if (value == null) { return null; @@ -45,11 +58,11 @@ export class NumberValidatorService { } if (range) { - if (range.MinValue != null && (value < range.MinValue)) { + if (range.MinValue != null && value < range.MinValue) { return { rangeMin: true }; } - if (range.MaxValue != null && (value > range.MaxValue)) { + if (range.MaxValue != null && value > range.MaxValue) { return { rangeMax: true }; } } diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-risk-index/edit-risk-index.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-risk-index/edit-risk-index.component.ts index 4fad91b54..a542e93ee 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-risk-index/edit-risk-index.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-risk-index/edit-risk-index.component.ts @@ -28,15 +28,33 @@ import { Component } from '@angular/core'; import { UntypedFormControl } from '@angular/forms'; import { EditorBase } from '../editor-base'; +/** + * Provides a {@link CdrEditor | CDR editor} for editing / viewing risk index columns. + * + * To change the value, it uses an Angular Material slider, that ranges between 0 and 1. + * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. + */ @Component({ selector: 'imx-edit-risk-index', templateUrl: './edit-risk-index.component.html', - styleUrls: ['./edit-risk-index.component.scss'] + styleUrls: ['./edit-risk-index.component.scss'], }) export class EditRiskIndexComponent extends EditorBase { + /** + * The form control associated with the editor. + */ public readonly control = new UntypedFormControl(undefined, { updateOn: 'blur' }); + + /** + * @ignore Only used in template. + */ public sliderFocused = false; + /** + * Converts a number value to a string in the current language. + * @param value The number value, that should be formatted. + * @returns A local representation of the number value. + */ public formatLabel(value: number): string { return value.toLocaleString(); } diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-url/edit-url.component.ts b/imxweb/projects/qbm/src/lib/cdr/edit-url/edit-url.component.ts index 76fc9b1c9..7cecba97a 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-url/edit-url.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-url/edit-url.component.ts @@ -33,26 +33,53 @@ import { CdrEditor, ValueHasChangedEventArg } from '../cdr-editor.interface'; import { ColumnDependentReference } from '../column-dependent-reference.interface'; import { EntityColumnContainer } from '../entity-column-container'; +/** + * Provides a {@link CdrEditor | CDR editor} for editing / viewing url columns. + * + * To change its value, it uses the input type 'url'. + * When set to read-only, it uses a {@link ViewPropertyComponent | view property component} to display the content. + */ @Component({ selector: 'imx-edit-url', templateUrl: './edit-url.component.html', styleUrls: ['./edit-url.component.scss'], }) export class EditUrlComponent implements CdrEditor, OnDestroy { + /** + * The form control associated with the editor. + */ public readonly control = new UntypedFormControl('', { updateOn: 'blur' }); + /** + * The container that wraps the column functionality. + */ public readonly columnContainer = new EntityColumnContainer(); + /** + * Event that is emitted, after a value has been changed. + */ public readonly valueHasChanged = new EventEmitter(); private readonly subscribers: Subscription[] = []; private isWriting = false; + /** * + * Creates a new EditUrlComponent + * @param urlValidator The {@link UrlValidatorService} used for validation. + */ constructor(private readonly urlValidator: UrlValidatorService) {} + /** + * Unsubscribes all events, after the 'OnDestroy' hook is triggered. + */ public ngOnDestroy(): void { this.subscribers.forEach((s) => s.unsubscribe()); } + /** + * Binds a column dependent reference to the component. + * Subscribes to subjects from the column dependent reference and its container. + * @param cdref a column dependent reference + */ public bind(cdref: ColumnDependentReference): void { if (cdref && cdref.column) { this.columnContainer.init(cdref); diff --git a/imxweb/projects/qbm/src/lib/cdr/edit-url/url-validator.service.ts b/imxweb/projects/qbm/src/lib/cdr/edit-url/url-validator.service.ts index 841d82703..b57933f4f 100644 --- a/imxweb/projects/qbm/src/lib/cdr/edit-url/url-validator.service.ts +++ b/imxweb/projects/qbm/src/lib/cdr/edit-url/url-validator.service.ts @@ -27,10 +27,21 @@ import { Injectable } from '@angular/core'; import { ValidatorFn, Validators } from '@angular/forms'; +/** + * A service for providing an url validation. + */ @Injectable({ providedIn: 'root' }) export class UrlValidatorService { + + /** + * Validates, if the given string uses the right pattern. + * @example + * Valid urls could be: + * 'https://localhost:8182' + * 'http://www.google.com' + */ public readonly validators: ReadonlyArray = [ Validators.pattern(new RegExp('^(http|https)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?')) ]; diff --git a/imxweb/projects/qbm/src/lib/cdr/editor-base.ts b/imxweb/projects/qbm/src/lib/cdr/editor-base.ts index 4f3f2bfbf..32ba85103 100644 --- a/imxweb/projects/qbm/src/lib/cdr/editor-base.ts +++ b/imxweb/projects/qbm/src/lib/cdr/editor-base.ts @@ -34,6 +34,12 @@ import { EntityColumnContainer } from './entity-column-container'; import { ServerError } from '../base/server-error'; import { ValType } from 'imx-qbm-dbts'; +/** + * A base class for CDR editors, that handles simple dataTypes like string, boolean or integer. + * To extend the component, the AbstractControl 'control' has to be implemented, as well as a template. + * The component itself has no template attached. + * For more complex editors, like our {@link EditFkComponent | FK editor} it might be more sufficient, to implement CdrEditor itself + */ @Component({ template: '' }) export abstract class EditorBase implements CdrEditor, OnDestroy { /** @@ -41,14 +47,38 @@ export abstract class EditorBase implements CdrEditor, OnDestroy { */ public abstract readonly control: AbstractControl; + /** + * The {@link EntityColumnContainer | entity column container} that handles column <-> editor communication. + */ public readonly columnContainer = new EntityColumnContainer(); + /** + * Event, that is emitted, if the value of the component is changed. + */ public readonly valueHasChanged = new EventEmitter(); + /** + * A subject, that is used to signal changes in the column. + * Mainly used to signal that the editor needs to be updated. + */ public readonly updateRequested = new Subject(); + /** + * @ignore + * used for the template to signal, that the component is loading content from the server. + */ public isBusy = false; + + /** + * @ignore + * Used for the template and displays the last server error, that occured while loading content. + */ public lastError: ServerError; + + /** + * The maximal length a string could have. + * The value depends on the meta data of the column. + */ public get maxlength(): number | undefined { return this.columnContainer?.metaData?.GetMaxLength(); } @@ -58,18 +88,26 @@ export abstract class EditorBase implements CdrEditor, OnDestroy { public constructor(protected readonly logger: ClassloggerService, protected readonly errorHandler?: ErrorHandler) {} + /** + * Unsubscribes all events, as soon as the component is destroyed. + */ public ngOnDestroy(): void { this.subscribers.forEach((s) => s.unsubscribe()); } + /** + * If an error occured, it returns its message + */ public get validationErrorMessage(): string { if (this.control.errors?.['generalError']) { return this.lastError.toString(); } + return undefined; } /** - * Binds a column dependent reference to the component + * Binds a column dependent reference to the component, by setting the control value and subscribing to the events, + * the CDR or the ColumnContainer emits * @param cdref a column dependent reference */ public bind(cdref: ColumnDependentReference): void { @@ -127,6 +165,9 @@ export abstract class EditorBase implements CdrEditor, OnDestroy { } } + /** + * Updates the value of the form control as well as its validators. + */ private setControlValue(): void { this.control.setValue(this.columnContainer.value, { emitEvent: false }); if ( @@ -142,7 +183,7 @@ export abstract class EditorBase implements CdrEditor, OnDestroy { } /** - * updates the value for the CDR + * Updates the value for the CDR and writes them back to the column. * @param value the new value */ private async writeValue(value: any): Promise { 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 9ea7fb696..ead85afc8 100644 --- a/imxweb/projects/qbm/src/lib/cdr/entity-column-container.ts +++ b/imxweb/projects/qbm/src/lib/cdr/entity-column-container.ts @@ -30,85 +30,135 @@ import { LimitedValuesContainer } from './limited-values-container'; import { ValueWrapper } from '../value-wrapper/value-wrapper'; import { Subscription } from 'rxjs'; +/** + * An implementation of the {@link ValueWrapper} interface. + * It provides information, that at stored in the cdr and its column. + */ export class EntityColumnContainer implements ValueWrapper { - /** - * Gives the info whether the column can be edited or not - */ - public get canEdit(): boolean { - return this.cdr && this.cdr.column && !this.cdr.isReadOnly() && this.cdr.column.GetMetadata().CanEdit(); - } - - /** - * The value of the column - */ - public get value(): T { - return this.cdr && this.cdr.column ? this.cdr.column.GetValue() : undefined; - } - - /** - * The display value of the column - */ - public get displayValue(): string { - return this.cdr && this.cdr.column ? this.cdr.column.GetDisplayValue() : undefined; - } - - public get fkRelations(): ReadonlyArray { - return this.cdr && this.cdr.column ? this.cdr.column.GetMetadata().GetFkRelations() : undefined; - } - - public get display(): string { - return this.cdr && (this.cdr.display || (this.cdr.column ? this.cdr.column.GetMetadata().GetDisplay() : undefined)); - } - - public get isValueRequired(): boolean { - return this.cdr && (this.cdr.minLength > 0 || this.cdr.column && this.cdr.column.GetMetadata().GetMinLength() > 0); - } - - public get name(): string { - return this.cdr && this.cdr.column ? this.cdr.column.ColumnName : undefined; - } - - public get type(): ValType { - return this.cdr && this.cdr.column ? this.cdr.column.GetType() : undefined; + /** + * Gives the info whether the column can be edited or not. + */ + public get canEdit(): boolean { + return this.cdr && this.cdr.column && !this.cdr.isReadOnly() && this.cdr.column.GetMetadata().CanEdit(); + } + + /** + * The value of the column. + */ + public get value(): T { + return this.cdr && this.cdr.column ? this.cdr.column.GetValue() : undefined; + } + + /** + * The display value of the column. + */ + public get displayValue(): string { + return this.cdr && this.cdr.column ? this.cdr.column.GetDisplayValue() : undefined; + } + + /** + * A read-only list of {@link IForeignKeyInfo | FK informations} + */ + public get fkRelations(): ReadonlyArray { + return this.cdr && this.cdr.column ? this.cdr.column.GetMetadata().GetFkRelations() : undefined; + } + + /** + * The display, that is provided by the cdr. + * If the CDR itself doesn't contain a display, the display given by the column is used. + */ + public get display(): string { + return this.cdr && (this.cdr.display || (this.cdr.column ? this.cdr.column.GetMetadata().GetDisplay() : undefined)); + } + + /** + * The information, whether a value is mandatory or not. + */ + public get isValueRequired(): boolean { + return this.cdr && (this.cdr.minLength > 0 || (this.cdr.column && this.cdr.column.GetMetadata().GetMinLength() > 0)); + } + + /** + * The name of the column. + */ + public get name(): string { + return this.cdr && this.cdr.column ? this.cdr.column.ColumnName : undefined; + } + + /** + * The value type of the column, like bool, number, string, etc. + */ + public get type(): ValType { + return this.cdr && this.cdr.column ? this.cdr.column.GetType() : undefined; + } + + /** + * The information of a min value, a max value or limited values used by the given column. + */ + public get valueConstraint(): ValueConstraint { + return this.cdr && (this.cdr.valueConstraint || (this.cdr.column ? this.cdr.column.GetMetadata().valueConstraint : undefined)); + } + + /** + * The meta data provided for the given column, like minLength, display, isMultiLine, etc. + */ + public get metaData(): IValueMetadata { + return this.cdr && this.cdr.column ? this.cdr.column.GetMetadata() : undefined; + } + + /** + * An additinal hint provided by the given CDR. + */ + public get hint(): string { + return this.cdr?.hint; + } + + /** + * An container, that wraps limited value functionality + */ + public limitedValuesContainer: LimitedValuesContainer; + + private cdr: ColumnDependentReference; + + /** + * Sets the CDR property and initalizes the LimitedValueContainer + * @param cdr The CDR used by the container. + */ + public init(cdr: ColumnDependentReference): void { + this.cdr = cdr; + this.limitedValuesContainer = new LimitedValuesContainer(cdr.column.GetMetadata()); + } + + /** + * Subscribes a listener to the column.ColumnChanged event and returns a subscription + * @param listener The listener to subscribe to column.ColumnChanged event. + * @returns A subscription of the given listener + */ + public subscribe(listener: () => void): Subscription { + // subscribe to entity event + const subscription = this.cdr.column.ColumnChanged?.subscribe(listener); + // wrap in a rxjs subscription + return new Subscription(() => subscription?.unsubscribe()); + } + + + /** + * Updates the value and puts it into the column + * @param value The new value for the column + */ + public async updateValue(value: T): Promise { + if (this.cdr && this.cdr.column) { + return this.cdr.column.PutValue(value); } - - public get valueConstraint(): ValueConstraint { - return this.cdr && (this.cdr.valueConstraint || (this.cdr.column ? this.cdr.column.GetMetadata().valueConstraint : undefined)); - } - - public get metaData(): IValueMetadata { - return this.cdr && this.cdr.column ? this.cdr.column.GetMetadata() : undefined; - } - - public get hint(): string { - return this.cdr?.hint; - } - - public limitedValuesContainer: LimitedValuesContainer; - - private cdr: ColumnDependentReference; - - public init(cdr: ColumnDependentReference): void { - this.cdr = cdr; - this.limitedValuesContainer = new LimitedValuesContainer(cdr.column.GetMetadata()); - } - - public subscribe(listener: () => void): Subscription { - // subscribe to entity event - const subscription = this.cdr.column.ColumnChanged?.subscribe(listener); - // wrap in a rxjs subscription - return new Subscription(() => subscription?.unsubscribe()); - } - - public async updateValue(value: T): Promise { - if (this.cdr && this.cdr.column) { - return this.cdr.column.PutValue(value); - } - } - - public async updateValueStruct(value: ValueStruct): Promise { - if (this.cdr && this.cdr.column) { - return this.cdr.column.PutValueStruct(value); - } + } + + /** + * Updates column value and column display in one call + * @param value The value struct, that should be used + */ + public async updateValueStruct(value: ValueStruct): Promise { + if (this.cdr && this.cdr.column) { + return this.cdr.column.PutValueStruct(value); } + } } diff --git a/imxweb/projects/qbm/src/lib/cdr/entity-column-editor/entity-column-editor.component.ts b/imxweb/projects/qbm/src/lib/cdr/entity-column-editor/entity-column-editor.component.ts index 5cc28d1ac..c1c0bcafa 100644 --- a/imxweb/projects/qbm/src/lib/cdr/entity-column-editor/entity-column-editor.component.ts +++ b/imxweb/projects/qbm/src/lib/cdr/entity-column-editor/entity-column-editor.component.ts @@ -32,32 +32,61 @@ import { ColumnDependentReference } from '../column-dependent-reference.interfac import { CdrEditorComponent } from '../cdr-editor/cdr-editor.component'; +/** + * Provides a column editor component, that wraps around a {@link CdrEditor | column dependent reference editor}. + */ @Component({ selector: 'imx-entity-column-editor', templateUrl: './entity-column-editor.component.html', - styleUrls: ['./entity-column-editor.component.scss'] + styleUrls: ['./entity-column-editor.component.scss'], }) export class EntityColumnEditorComponent implements OnChanges { + /** + * @ignore only used in template. + * The column dependent reference used by the editor. + */ public cdr: ColumnDependentReference; + /** + * An entity column, that should be edited with a {@link CdrEditor | column dependent reference editor}. + */ @Input() public column: IEntityColumn; + + /** + * Indicator, whether the control should be displayed as read-only. + */ @Input() public readonly: boolean; - @Output() public controlCreated = new EventEmitter<{ name: string; control: AbstractControl; }>(); + /** + * This is emitted, after the control is created properly. + * Only after this, the component can be added to a formGroup or FormArray. + */ + @Output() public controlCreated = new EventEmitter<{ name: string; control: AbstractControl }>(); + /** + * @ignore Used in template only. + * The cdr editor component. + */ @ViewChild(CdrEditorComponent) editor: CdrEditorComponent; + /** + * Updates the cdr, if the 'OnChanges' hook is called. + * @param changes The changes that are applied (only column and readonly is evaluated). + */ public ngOnChanges(changes: SimpleChanges): void { if (changes['column'] || changes['readonly']) { - this.cdr = this.column ? - { - column: this.column, - isReadOnly: () => this.readonly || !this.column.GetMetadata().CanEdit(), - } + this.cdr = this.column + ? { + column: this.column, + isReadOnly: () => this.readonly || !this.column.GetMetadata().CanEdit(), + } : undefined; } } + /** + * Calls the update method from the {@link CdrEditor | column dependent reference editor}. + */ public update(): void { this.editor?.update(); } diff --git a/imxweb/projects/qbm/src/lib/cdr/fk-cdr-editor-provider.ts b/imxweb/projects/qbm/src/lib/cdr/fk-cdr-editor-provider.ts index 246d2adb9..0d5659432 100644 --- a/imxweb/projects/qbm/src/lib/cdr/fk-cdr-editor-provider.ts +++ b/imxweb/projects/qbm/src/lib/cdr/fk-cdr-editor-provider.ts @@ -32,9 +32,21 @@ import { CdrEditor } from './cdr-editor.interface'; import { EditFkComponent } from './edit-fk/edit-fk.component'; import { EditFkMultiComponent } from './edit-fk/edit-fk-multi.component'; +/** + * A special provider for foreign key columns. + * The provider determines, if the column has foreign key information attached. + * If that's the case, it creates an CdrEditor for a single or a multi foreign key. + * If no such information is given, it returns null + * */ export class FkCdrEditorProvider implements CdrEditorProvider { constructor() {} + /** + * Creates a CDR editor for single and multi foreign key columns and binds it to the column + * @param parent The view container used for rendering the editor into. + * @param cdref A column dependent reference that contains the data for the editor. + * @returns An instance of {@link CdrEditor}, that can be used or editing data, or null, if no foreign key information is given. + */ public createEditor(parent: ViewContainerRef, cdref: ColumnDependentReference): ComponentRef { if (this.hasFkRelations(cdref)) { return cdref.column.GetMetadata().IsMultiValue() @@ -45,8 +57,12 @@ export class FkCdrEditorProvider implements CdrEditorProvider { return null; } + /** + * @ignore only used internally. + * Creates the component and binds its value. + */ private createBound( - type: Type, + type: Type, parent: ViewContainerRef, cdref: ColumnDependentReference ): ComponentRef { @@ -55,6 +71,11 @@ export class FkCdrEditorProvider implements CdrEditorProvider { return result; } + /** + * Determines, if there are fk relations present or not. + * @param cdref The column dependent reference, that needs to be checked + * @returns + */ private hasFkRelations(cdref: ColumnDependentReference): boolean { const fkRelations = cdref.column.GetMetadata().GetFkRelations(); if (fkRelations == null) { diff --git a/imxweb/projects/qbm/src/lib/cdr/limited-values-container.ts b/imxweb/projects/qbm/src/lib/cdr/limited-values-container.ts index c2b80fbd1..d15b22501 100644 --- a/imxweb/projects/qbm/src/lib/cdr/limited-values-container.ts +++ b/imxweb/projects/qbm/src/lib/cdr/limited-values-container.ts @@ -26,33 +26,47 @@ import { LimitedValueData, IValueMetadata, ValType } from 'imx-qbm-dbts'; +/** + * A wrapper, that encapsules limited value property functions. + */ export class LimitedValuesContainer { - public get values(): ReadonlyArray { - return this.metadata ? this.metadata.GetLimitedValues() : undefined; - } + /** + * A read-only list of a possible limited values. + */ + public get values(): ReadonlyArray { + return this.metadata ? this.metadata.GetLimitedValues() : undefined; + } - constructor(private metadata: IValueMetadata) { } + constructor(private metadata: IValueMetadata) {} - /** - * Determines, whether the limited value collection allows a null option - */ - public hasNullOption(): boolean { - return this.metadata.GetMinLength() === 0 && !this.contains(this.getNullValue()); - } + /** + * Determines, whether the limited value collection allows a null option. + */ + public hasNullOption(): boolean { + return this.metadata.GetMinLength() === 0 && !this.contains(this.getNullValue()); + } - /** - * Determines, whether the value is part of the limited value range or not - */ - public isNotInLimitedValueRange(value: string | number): boolean { - return !((value || '') === (this.getNullValue() || '')) && !this.contains(value); - } + /** + * Determines, whether the value is part of the limited value range or not. + */ + public isNotInLimitedValueRange(value: string | number): boolean { + return !((value || '') === (this.getNullValue() || '')) && !this.contains(value); + } - // gets the value representing "null" - private getNullValue(): string { - return this.metadata.GetType() === ValType.String ? null : '0'; - } + /** + * Gets the value representing 'null'. + * @returns the value that is used as 'null'. + */ + private getNullValue(): string { + return this.metadata.GetType() === ValType.String ? null : '0'; + } - private contains(value: string | number): boolean { - return this.values && this.values.filter(v => `${v.Value}` === `${value}`).length > 0; - } + /** + * Checks, if a value is part of the limited value list + * @param value The value to be checked. + * @returns + */ + private contains(value: string | number): boolean { + return this.values && this.values.filter((v) => `${v.Value}` === `${value}`).length > 0; + } } diff --git a/imxweb/projects/qbm/src/lib/connection/connection.component.ts b/imxweb/projects/qbm/src/lib/connection/connection.component.ts index 8df94b852..672b8e233 100644 --- a/imxweb/projects/qbm/src/lib/connection/connection.component.ts +++ b/imxweb/projects/qbm/src/lib/connection/connection.component.ts @@ -89,8 +89,8 @@ export class ConnectionComponent implements OnInit, OnDestroy { } /** - * Creates cdr list based on the system users - * @returns List of read-only cdrs + * Creates CDR list based on the system users + * @returns List of read-only CDR */ private createCdrList(): BaseReadonlyCdr[] { const cdrList: BaseReadonlyCdr[] = []; diff --git a/imxweb/projects/qbm/src/lib/data-source-toolbar/additional-infos/additional-infos.component.ts b/imxweb/projects/qbm/src/lib/data-source-toolbar/additional-infos/additional-infos.component.ts index 04bb97dd7..e658f8673 100644 --- a/imxweb/projects/qbm/src/lib/data-source-toolbar/additional-infos/additional-infos.component.ts +++ b/imxweb/projects/qbm/src/lib/data-source-toolbar/additional-infos/additional-infos.component.ts @@ -34,14 +34,14 @@ import { ClientPropertyForTableColumns } from '../client-property-for-table-colu @Component({ selector: 'imx-additional-infos', templateUrl: './additional-infos.component.html', - styleUrls: ['./additional-infos.component.scss'] + styleUrls: ['./additional-infos.component.scss'], }) export class AdditionalInfosComponent implements OnInit { - public possibleProperties: IClientProperty[]; public infoText = '#LDS#Select the columns you want to add.'; - public infoTextLong = '#LDS#Here you can add additional columns to your table. Additionally, you can change the order using drag and drop. Move the mouse pointer over the shaded area and drag the element to the desired location.'; + public infoTextLong = + '#LDS#Here you can add additional columns to your table. Additionally, you can change the order using drag and drop. Move the mouse pointer over the shaded area and drag the element to the desired location.'; public get result(): any { return { all: this.data.preselectedProperties, optionals: this.optionals }; @@ -52,22 +52,24 @@ export class AdditionalInfosComponent implements OnInit { } constructor( - @Inject(MAT_DIALOG_DATA) public readonly data: { - dataModel: DataModel, - entitySchema: EntitySchema, - displayedColumns: ClientPropertyForTableColumns[], - additionalPropertyNames: ClientPropertyForTableColumns[], - preselectedProperties: ClientPropertyForTableColumns[], - additionalColumns: ClientPropertyForTableColumns[] - type: 'list' | 'columns' + @Inject(MAT_DIALOG_DATA) + public readonly data: { + dataModel: DataModel; + entitySchema: EntitySchema; + displayedColumns: ClientPropertyForTableColumns[]; + additionalPropertyNames: ClientPropertyForTableColumns[]; + preselectedProperties: ClientPropertyForTableColumns[]; + additionalColumns: ClientPropertyForTableColumns[]; + type: 'list' | 'columns'; }, - public dialogRef: MatDialogRef) { - } + public dialogRef: MatDialogRef + ) {} public ngOnInit(): void { - this.possibleProperties = this.data.additionalPropertyNames.map(elem => elem) - .concat(this.data.displayedColumns) - .sort((elem1, elem2) => AdditionalInfosComponent.compareNames(elem1, elem2)); + const possiblePropertiesWithDuplicates = this.data.additionalPropertyNames.concat(this.data.displayedColumns); + this.possibleProperties = possiblePropertiesWithDuplicates + .filter((element, index) => possiblePropertiesWithDuplicates.findIndex((prop) => prop.ColumnName === element.ColumnName) === index) + .sort((prop1, prop2) => AdditionalInfosComponent.compareNames(prop1, prop2)); } public drop(event: CdkDragDrop): void { @@ -75,28 +77,28 @@ export class AdditionalInfosComponent implements OnInit { } public remove(property: IClientProperty): void { - const position = this.data.preselectedProperties.findIndex(elem => elem.ColumnName === property.ColumnName); + const position = this.data.preselectedProperties.findIndex((elem) => elem.ColumnName === property.ColumnName); this.data.preselectedProperties.splice(position, 1); } public isChecked(property: IClientProperty): boolean { - return this.data.preselectedProperties.find(elem => elem.ColumnName === property.ColumnName) != null; + return this.data.preselectedProperties.find((elem) => elem.ColumnName === property.ColumnName) != null; } public updateSelected(event: MatSelectionListChange): void { if (event.options[0].selected) { // add new columns before first item with afterAdditionals = true or at the end - let index = this.data.preselectedProperties.findIndex(elem => elem.afterAdditionals === true); + let index = this.data.preselectedProperties.findIndex((elem) => elem.afterAdditionals === true); this.data.preselectedProperties.splice(index === -1 ? this.data.preselectedProperties.length : index, 0, event.options[0].value); - index = this.optionals.findIndex(elem => elem.afterAdditionals === true); + index = this.optionals.findIndex((elem) => elem.afterAdditionals === true); this.optionals.splice(index === -1 ? this.optionals.length : index, 0, event.options[0].value); } else { // Find item and remove it - let position = this.data.preselectedProperties.findIndex(elem => elem.ColumnName === event.options[0].value.ColumnName); + let position = this.data.preselectedProperties.findIndex((elem) => elem.ColumnName === event.options[0].value.ColumnName); this.data.preselectedProperties.splice(position, 1); - position = this.optionals.findIndex(elem => elem.ColumnName === event.options[0].value.ColumnName); + position = this.optionals.findIndex((elem) => elem.ColumnName === event.options[0].value.ColumnName); this.optionals.splice(position, 1); } } @@ -114,5 +116,4 @@ export class AdditionalInfosComponent implements OnInit { } return column1.ColumnName?.localeCompare(column2.ColumnName); } - } diff --git a/imxweb/projects/qbm/src/lib/data-source-toolbar/column-options.ts b/imxweb/projects/qbm/src/lib/data-source-toolbar/column-options.ts index 88a0f302e..7d30c019a 100644 --- a/imxweb/projects/qbm/src/lib/data-source-toolbar/column-options.ts +++ b/imxweb/projects/qbm/src/lib/data-source-toolbar/column-options.ts @@ -40,7 +40,6 @@ export interface ShownClientPropertiesArg { needsReload: boolean; } export class ColumnOptions { - /** * A list of client properties, that will be shown in the DataTable */ @@ -82,8 +81,11 @@ export class ColumnOptions { // Additional columns are set to null if we are using a config so that we can still edit the columns public get additionalColumns(): IClientProperty[] { - return this.currentViewSettings?.AdditionalTableColumns?.map(elem => - ColumnOptions.getClientProperty(ColumnOptions.findKey(elem, this.entitySchema), this.dataModel, this.entitySchema)) ?? [] + return ( + this.currentViewSettings?.AdditionalTableColumns?.map((elem) => + ColumnOptions.getClientProperty(ColumnOptions.findKey(elem, this.entitySchema), this.dataModel, this.entitySchema) + ) ?? [] + ); } // We are default if we have not injected a viewconfig @@ -97,33 +99,38 @@ export class ColumnOptions { private logger: ClassloggerService; // getter for settings - private get dataModel(): DataModel { return this.settings.dataModel; } + private get dataModel(): DataModel { + return this.settings.dataModel; + } private entitySchema: EntitySchema; - private get displayedColumns(): IClientProperty[] { return this.settings.displayedColumns; } + private get displayedColumns(): IClientProperty[] { + return this.settings.displayedColumns; + } private originalEntitySchema; - constructor( - public settings: DataSourceToolbarSettings, - injector: Injector, - public viewConfig?: DSTViewConfig, - ) { + constructor(public settings: DataSourceToolbarSettings, injector: Injector, public viewConfig?: DSTViewConfig) { // Use the injected viewConfig if available - this.currentViewSettings = viewConfig ?? this.dataModel.Configurations?. - find(elem => elem.Id === this.dataModel.DefaultConfigId); + this.currentViewSettings = viewConfig ?? this.dataModel.Configurations?.find((elem) => elem.Id === this.dataModel.DefaultConfigId); if (this.currentViewSettings) { // Clean up settings, if there are null or empty columnsnames attached - if (this.currentViewSettings.AdditionalListColumns - && this.currentViewSettings.AdditionalListColumns.some(elem => elem == null || elem === '')) { - (this.currentViewSettings.AdditionalListColumns as any) = - this.currentViewSettings.AdditionalListColumns.filter(elem => elem != null && elem !== ''); + if ( + this.currentViewSettings.AdditionalListColumns && + this.currentViewSettings.AdditionalListColumns.some((elem) => elem == null || elem === '') + ) { + (this.currentViewSettings.AdditionalListColumns as any) = this.currentViewSettings.AdditionalListColumns.filter( + (elem) => elem != null && elem !== '' + ); } - if (this.currentViewSettings.AdditionalTableColumns - && this.currentViewSettings.AdditionalTableColumns.some(elem => elem == null || elem === '')) { - (this.currentViewSettings.AdditionalTableColumns as any) = - this.currentViewSettings.AdditionalTableColumns.filter(elem => elem != null && elem !== ''); + if ( + this.currentViewSettings.AdditionalTableColumns && + this.currentViewSettings.AdditionalTableColumns.some((elem) => elem == null || elem === '') + ) { + (this.currentViewSettings.AdditionalTableColumns as any) = this.currentViewSettings.AdditionalTableColumns.filter( + (elem) => elem != null && elem !== '' + ); } } @@ -137,10 +144,7 @@ export class ColumnOptions { public getPropertiesForNavigation(): string[] { return this.shownClientProperties - .filter( - (elem) => - this.displayedColumns.findIndex((disp) => disp.ColumnName === elem.ColumnName) === -1 - ) + .filter((elem) => this.displayedColumns.findIndex((disp) => disp.ColumnName === elem.ColumnName) === -1) .map((elem) => elem.ColumnName); } @@ -150,12 +154,10 @@ export class ColumnOptions { public async updateAdditional(): Promise { // Don't force additional columns to be unselectable unless it was the default const additional = this.isDefaultConfig ? this.additionalColumns : []; - const displayedColumns = [ - ...this.displayedColumns, - ...additional]; + const displayedColumns = [...this.displayedColumns, ...additional]; this.logger.trace(this, 'unchangeable columns', displayedColumns); - const result: { all: IClientProperty[], optionals: IClientProperty[] } - = await this.dialog.open(AdditionalInfosComponent, { + const result: { all: IClientProperty[]; optionals: IClientProperty[] } = await this.dialog + .open(AdditionalInfosComponent, { width: 'min(1200px,70%)', autoFocus: false, height: 'min(700px,70%)', @@ -165,17 +167,22 @@ export class ColumnOptions { displayedColumns, additionalPropertyNames: this.optionalColumns, additionalColumns: this.additionalColumns, - preselectedProperties: [...this.shownClientProperties] + preselectedProperties: [...this.shownClientProperties], }, - panelClass: 'imx-toolbar-dialog' - }).afterClosed().toPromise(); + panelClass: 'imx-toolbar-dialog', + }) + .afterClosed() + .toPromise(); if (result) { - if (JSON.stringify(this.shownClientProperties) === JSON.stringify(result.all)) { return; } + if (JSON.stringify(this.shownClientProperties) === JSON.stringify(result.all)) { + return; + } this.shownClientProperties = result.all; - const needsReload = result.optionals.length > this.selectedOptionals.length - || result.optionals.some(res => this.selectedOptionals.find(sel => sel.ColumnName === res.ColumnName) == null); + const needsReload = + result.optionals.length > this.selectedOptionals.length || + result.optionals.some((res) => this.selectedOptionals.find((sel) => sel.ColumnName === res.ColumnName) == null); this.selectedOptionals = result.optionals; this.logger.trace(this, 'new displayed columns', result.all, 'new optional columns', result.optionals, 'needs reload', needsReload); @@ -187,20 +194,20 @@ export class ColumnOptions { * resets the view by removing all optional columns and restoring the initial order */ public resetView(): void { - if (this.currentViewSettings == null) { return; } + if (this.currentViewSettings == null) { + return; + } // We will reset by grabbing the default Id - this.currentViewSettings = this.dataModel.Configurations?. - find(elem => elem.Id === 'Default'); + this.currentViewSettings = this.dataModel.Configurations?.find((elem) => elem.Id === 'Default'); const addition = this.additionalColumns; this.selectedOptionals = []; this.shownClientProperties = [...this.displayedColumns]; - const index = this.shownClientProperties.findIndex(elem => elem.afterAdditionals); + const index = this.shownClientProperties.findIndex((elem) => elem.afterAdditionals); this.shownClientProperties.splice(index === -1 ? this.shownClientProperties.length : index, 0, ...addition); - this.shownColumnsSelectionChanged.emit({ properties: this.shownClientProperties, needsReload: false }); this.logger.trace(this, 'shown client properties resetted to', this.shownClientProperties); } @@ -223,7 +230,7 @@ export class ColumnOptions { if (elements.length > 0) { this.logger.trace(this, 'properties, that have to be updated', elements); // hack for adding the new columns to to entitySchema - elements.forEach(element => { + elements.forEach((element) => { const key = ColumnOptions.findKey(element, this.entitySchema); (this.entitySchema.Columns[key] as any) = ColumnOptions.getClientProperty(key, this.dataModel, this.entitySchema); }); @@ -249,24 +256,27 @@ export class ColumnOptions { } private initOptionalColumns(): void { - const optional = this.dataModel.Properties?.filter(elem => elem.IsAdditionalColumn).map(elem => elem.Property); + const optional = this.dataModel.Properties?.filter((elem) => elem.IsAdditionalColumn).map((elem) => elem.Property); // Check if this isAdditional or if its already in the additionalColumns, both are needed to not lose the option from config selection this.optionalColumns = optional?.filter((value, index, categoryArray) => { - const isAdditional = this.isAdditional(value.ColumnName) || (this.additionalColumns.find(ele => ele.ColumnName.toLocaleLowerCase() == value.ColumnName.toLocaleLowerCase()) != null); + const isAdditional = + this.isAdditional(value.ColumnName) || + this.additionalColumns.find((ele) => ele.ColumnName.toLocaleLowerCase() == value.ColumnName.toLocaleLowerCase()) != null; const indexMatch = categoryArray.indexOf(value) === index; return isAdditional && indexMatch; - }) - + }); this.logger.trace(this, 'optional columns', this.optionalColumns); } private initShownClientProperties(): void { - const current = this.currentViewSettings?.AdditionalTableColumns?. - map(elem => ColumnOptions.getClientProperty(elem, this.dataModel)) ?? []; + const current = + this.currentViewSettings?.AdditionalTableColumns?.filter((element) => + this.displayedColumns.every((elem) => elem.ColumnName !== element) + ).map((elem) => ColumnOptions.getClientProperty(elem, this.dataModel)) ?? []; this.shownClientProperties = [...this.displayedColumns]; - const index = this.shownClientProperties.findIndex(elem => elem.afterAdditionals); + const index = this.shownClientProperties.findIndex((elem) => elem.afterAdditionals); this.shownClientProperties.splice(index === -1 ? this.shownClientProperties.length : index, 0, ...current, ...this.selectedOptionals); this.logger.trace(this, 'shown client properties initialized with', this.shownClientProperties); @@ -275,19 +285,20 @@ export class ColumnOptions { private initAdditionalListElements(): void { const lists = this.currentViewSettings?.AdditionalListColumns; if (lists?.length > 0) { - this.additionalListElements = lists.map(elem => - ColumnOptions.getClientProperty(ColumnOptions.findKey(elem, this.entitySchema), this.dataModel, this.entitySchema)); + this.additionalListElements = lists.map((elem) => + ColumnOptions.getClientProperty(ColumnOptions.findKey(elem, this.entitySchema), this.dataModel, this.entitySchema) + ); this.additionalListElementsChanged.emit(this.additionalListElements); this.logger.trace(this, 'additional list elements from viewSettings', this.additionalListElements); } } private isAdditional(key: string): boolean { - return this.displayedColumns.find(elem => elem.ColumnName.toLocaleLowerCase() === key.toLocaleLowerCase()) == null - && - this.currentViewSettings?.AdditionalListColumns?.find(elem => elem.toLocaleLowerCase() === key.toLocaleLowerCase()) == null - && - this.currentViewSettings?.AdditionalTableColumns?.find(elem => elem.toLocaleLowerCase() === key.toLocaleLowerCase()) == null + return ( + this.displayedColumns.find((elem) => elem.ColumnName.toLocaleLowerCase() === key.toLocaleLowerCase()) == null && + this.currentViewSettings?.AdditionalListColumns?.find((elem) => elem.toLocaleLowerCase() === key.toLocaleLowerCase()) == null && + this.currentViewSettings?.AdditionalTableColumns?.find((elem) => elem.toLocaleLowerCase() === key.toLocaleLowerCase()) == null + ); } public static getClientProperty(name: string, dataModel: DataModel, entitySchema?: EntitySchema): IClientProperty { @@ -297,8 +308,9 @@ export class ColumnOptions { property = key != null ? entitySchema.Columns[key] : null; } if (property == null) { - property = dataModel?.Properties?. - find(elem => elem?.Property?.ColumnName?.toLocaleLowerCase() === name?.toLocaleLowerCase())?.Property; + property = dataModel?.Properties?.find( + (elem) => elem?.Property?.ColumnName?.toLocaleLowerCase() === name?.toLocaleLowerCase() + )?.Property; } if (property == null) { property = { ColumnName: name, Type: ValType.String }; @@ -306,8 +318,8 @@ export class ColumnOptions { return property; } - private static findKey(key: string, schema: EntitySchema): string { - const keyVariant = Object.keys(schema.Columns).find(elem => elem.toLocaleLowerCase() === key.toLocaleLowerCase()); + public static findKey(key: string, schema: EntitySchema): string { + const keyVariant = Object.keys(schema.Columns).find((elem) => elem.toLocaleLowerCase() === key.toLocaleLowerCase()); return keyVariant ?? key; } } 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 c208e8f4f..e65d86055 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 @@ -446,7 +446,7 @@ export class DataSourceToolbarComponent implements OnChanges, OnInit, OnDestroy } if (config?.IsReadOnly && currentDefault) { - // We need to update the last config to be not-default, we cannot update the chosen as it is read only + // We need to update the last config to be not-default, we cannot update the chosen as it is read-sonly //TODO: We do nothing if the chosen is readonly and the last was also readonly, there is no API for this currentDefault?.IsReadOnly ? null : this.updateConfig.emit(currentDefault); } @@ -888,7 +888,7 @@ export class DataSourceToolbarComponent implements OnChanges, OnInit, OnDestroy } this.filterType = this.filterTreeItems?.Description; - this.columnForTree = this.filterTreeItems?.Elements.length > 0 ? this.filterTreeItems?.Elements[0]?.Filter?.ColumnName : ''; + this.columnForTree = this.filterTreeItems?.Elements?.length > 0 ? this.filterTreeItems?.Elements[0]?.Filter?.ColumnName : ''; this.internalDataSource = new MatTableDataSource(this.settings.dataSource?.Data); if (this.isDataSourceLocal && (this.searchCurrenltyApplied || this.filtersCurrentlyApplied)) { // We need to apply a filter still over the local data since it was skipped earlier. Do so now. diff --git a/imxweb/projects/qbm/src/lib/data-source-toolbar/filter-wizard/filter-wizard.component.html b/imxweb/projects/qbm/src/lib/data-source-toolbar/filter-wizard/filter-wizard.component.html index 5a46cf50f..8aefb41fe 100644 --- a/imxweb/projects/qbm/src/lib/data-source-toolbar/filter-wizard/filter-wizard.component.html +++ b/imxweb/projects/qbm/src/lib/data-source-toolbar/filter-wizard/filter-wizard.component.html @@ -36,7 +36,7 @@ - + diff --git a/imxweb/projects/qbm/src/lib/data-source-toolbar/filter-wizard/filter-wizard.component.ts b/imxweb/projects/qbm/src/lib/data-source-toolbar/filter-wizard/filter-wizard.component.ts index 8882d6f2c..a03113dea 100644 --- a/imxweb/projects/qbm/src/lib/data-source-toolbar/filter-wizard/filter-wizard.component.ts +++ b/imxweb/projects/qbm/src/lib/data-source-toolbar/filter-wizard/filter-wizard.component.ts @@ -120,8 +120,11 @@ export class FilterWizardComponent implements OnDestroy { this.subscriptions.forEach((s) => s.unsubscribe()); } - public checkChanges(): void { - this.expressionDirty = !_.isEqual(this.sqlExpression?.Expression, this.lastGoodExpression); + public checkChanges(expression: SqlExpression): void { + this.expressionDirty = !_.isEqual(expression, this.lastGoodExpression); + if (this.expressionDirty) { + this.sqlExpression.Expression = expression; + } this.expressionInvalid = !_.isEqual(this.sqlExpression?.Expression, this.emptyExpression?.Expression) && isExpressionInvalid(this.sqlExpression); } diff --git a/imxweb/projects/qbm/src/lib/data-table/data-table-column.component.html b/imxweb/projects/qbm/src/lib/data-table/data-table-column.component.html index 82aaefc5e..80c0a93a1 100644 --- a/imxweb/projects/qbm/src/lib/data-table/data-table-column.component.html +++ b/imxweb/projects/qbm/src/lib/data-table/data-table-column.component.html @@ -7,7 +7,7 @@ {{ translateProvider.GetColumnDisplay(entityColumn?.ColumnName, entitySchema) }} - + diff --git a/imxweb/projects/qbm/src/lib/data-table/data-table.component.html b/imxweb/projects/qbm/src/lib/data-table/data-table.component.html index 26f56049c..b88c365b2 100644 --- a/imxweb/projects/qbm/src/lib/data-table/data-table.component.html +++ b/imxweb/projects/qbm/src/lib/data-table/data-table.component.html @@ -103,7 +103,7 @@ - {{ translateProvider.GetColumnDisplay(column.ColumnName, entitySchema) }} + {{ column.Display }} - {{ translateProvider.GetColumnDisplay(column.ColumnName, entitySchema) }} + {{ column.Display }} diff --git a/imxweb/projects/qbm/src/lib/data-table/data-table.component.ts b/imxweb/projects/qbm/src/lib/data-table/data-table.component.ts index 519ec8b63..384333e04 100644 --- a/imxweb/projects/qbm/src/lib/data-table/data-table.component.ts +++ b/imxweb/projects/qbm/src/lib/data-table/data-table.component.ts @@ -57,6 +57,7 @@ import { GroupPaginatorInformation } from './group-paginator/group-paginator.com import { EuiLoadingService } from '@elemental-ui/core'; import { OverlayRef } from '@angular/cdk/overlay'; import { debounce } from 'lodash'; +import { ColumnOptions } from '../data-source-toolbar/column-options'; /** * A data table component with a detail view specialized on typed entities. @@ -649,12 +650,19 @@ export class DataTableComponent implements OnInit, OnChanges, AfterViewInit, // TODO: hier die additional columns berücksichtigen? if (this.settings && this.settings.entitySchema) { this.entitySchema = this.settings.entitySchema; - this.manualColumns.forEach((item: DataTableColumnComponent) => { + //update schema with additionals + this.parentAdditionals.concat(this.additional).forEach((element) => { + const key = ColumnOptions.findKey(element.ColumnName, this.entitySchema); + (this.entitySchema.Columns[key] as any) = element; + }); + this.manualColumns?.forEach((item: DataTableColumnComponent) => { item.entitySchema = this.entitySchema; }); } if (this.settings && this.settings.dataSource) { + //Apply schema to elements + this.settings.dataSource.Data.forEach((elem) => elem.GetEntity().ApplySchema(this.entitySchema)); this.dataSource = new MatTableDataSource(this.settings.dataSource.Data); } @@ -682,7 +690,7 @@ export class DataTableComponent implements OnInit, OnChanges, AfterViewInit, this.columnDefs.forEach((colDef) => this.table.removeColumnDef(colDef)); } - if ((this.dst.dataSourceChanged || this.dst.shownColumnsSelectionChanged) ) { + if (this.dst.dataSourceChanged || this.dst.shownColumnsSelectionChanged) { this.displayedColumns = []; this.additional = this.dst == null || this.dst.additionalColumns?.length === 0 ? this.parentAdditionals : this.dst.additionalColumns; // filter additionals for columns, that are already set in the DataSourceToolbarSettings diff --git a/imxweb/projects/qbm/src/lib/data-tree/data-tree-search-results/data-tree-search-results.component.html b/imxweb/projects/qbm/src/lib/data-tree/data-tree-search-results/data-tree-search-results.component.html index 8acd04a92..6cf093f2b 100644 --- a/imxweb/projects/qbm/src/lib/data-tree/data-tree-search-results/data-tree-search-results.component.html +++ b/imxweb/projects/qbm/src/lib/data-tree/data-tree-search-results/data-tree-search-results.component.html @@ -18,7 +18,7 @@ - diff --git a/imxweb/projects/qbm/src/lib/data-tree/data-tree-search-results/data-tree-search-results.component.ts b/imxweb/projects/qbm/src/lib/data-tree/data-tree-search-results/data-tree-search-results.component.ts index d73358eda..83d282595 100644 --- a/imxweb/projects/qbm/src/lib/data-tree/data-tree-search-results/data-tree-search-results.component.ts +++ b/imxweb/projects/qbm/src/lib/data-tree/data-tree-search-results/data-tree-search-results.component.ts @@ -31,6 +31,7 @@ import { PageEvent } from '@angular/material/paginator'; import { CollectionLoadParameters, IEntity } from 'imx-qbm-dbts'; import { ClassloggerService } from '../../classlogger/classlogger.service'; import { TreeDatabase } from '../tree-database'; +import { SettingsService} from '../../settings/settings-service'; @Component({ selector: 'imx-data-tree-search-results', @@ -78,7 +79,7 @@ export class DataTreeSearchResultsComponent implements OnChanges { public paginatorLength: number; /** @ignore the pagesiuze for the paginator */ - public paginatorPageSize = 25; + public paginatorPageSize = this.settings.DefaultPageSize; public loading = true; public selectedEntity: IEntity; @@ -92,7 +93,7 @@ export class DataTreeSearchResultsComponent implements OnChanges { /** event, that fires, after the checked nodes list has been updated */ @Output() public checkedNodesChanged = new EventEmitter(); - constructor(private readonly logger: ClassloggerService) { } + constructor(private readonly logger: ClassloggerService, private readonly settings: SettingsService) {} public async ngOnChanges(changes: SimpleChanges): Promise { if (changes['navigationState']) { diff --git a/imxweb/projects/qbm/src/lib/data-tree/data-tree.component.ts b/imxweb/projects/qbm/src/lib/data-tree/data-tree.component.ts index b9f44521f..b011e1874 100644 --- a/imxweb/projects/qbm/src/lib/data-tree/data-tree.component.ts +++ b/imxweb/projects/qbm/src/lib/data-tree/data-tree.component.ts @@ -233,7 +233,7 @@ export class DataTreeComponent implements OnChanges, OnDestroy { this.subscriptions.push( sidesheetRef.afterClosed().subscribe((result) => { - this.logger.log(this, 'The sidesheet was closed', result); + this.logger.log(this, 'The side sheet was closed', result); }) ); } diff --git a/imxweb/projects/qbm/src/lib/entity/typed-entity-candidate-sidesheet/typed-entity-candidate-sidesheet.component.ts b/imxweb/projects/qbm/src/lib/entity/typed-entity-candidate-sidesheet/typed-entity-candidate-sidesheet.component.ts index e0a575612..7eba9ec06 100644 --- a/imxweb/projects/qbm/src/lib/entity/typed-entity-candidate-sidesheet/typed-entity-candidate-sidesheet.component.ts +++ b/imxweb/projects/qbm/src/lib/entity/typed-entity-candidate-sidesheet/typed-entity-candidate-sidesheet.component.ts @@ -27,14 +27,7 @@ import { Component, Inject, OnInit } from '@angular/core'; import { EUI_SIDESHEET_DATA } from '@elemental-ui/core'; import { TranslateService } from '@ngx-translate/core'; -import { - DataModelFilterOption, - DbObjectKey, - DisplayColumns, - EntitySchema, - IClientProperty, - TypedEntity -} from 'imx-qbm-dbts'; +import { DataModelFilterOption, DbObjectKey, DisplayColumns, EntitySchema, IClientProperty, TypedEntity } from 'imx-qbm-dbts'; import { MetadataService } from '../../base/metadata.service'; import { CdrFactoryService } from '../../cdr/cdr-factory.service'; import { DataSourceToolbarFilter } from '../../data-source-toolbar/data-source-toolbar-filters.interface'; @@ -45,10 +38,9 @@ import { TypedEntityTableFilter } from './typed-entity-table-filter.interface'; @Component({ templateUrl: './typed-entity-candidate-sidesheet.component.html', - styleUrls: ['./typed-entity-candidate-sidesheet.component.scss'] + styleUrls: ['./typed-entity-candidate-sidesheet.component.scss'], }) export class TypedEntityCandidateSidesheetComponent implements OnInit { - public readonly DisplayColumns = DisplayColumns; // Enables use of this static class in Angular Templates. public dstSettings: DataSourceToolbarSettings; public readonly entitySchemaCandidates: EntitySchema; @@ -59,43 +51,43 @@ export class TypedEntityCandidateSidesheetComponent implements OnInit { private readonly sortedEntities: TypedEntity[]; private searchedEntities: TypedEntity[]; - private filters: DataSourceToolbarFilter[] + private filters: DataSourceToolbarFilter[]; constructor( @Inject(EUI_SIDESHEET_DATA) public readonly data: TypedEntityCandidateSidesheetParameter, private translate: TranslateService, - private metaData: MetadataService + private metaData: MetadataService, ) { - this.entitySchemaCandidates = CandidateEntity.GetEntitySchema(); - this.displayedColumns = [ - this.entitySchemaCandidates.Columns[DisplayColumns.DISPLAY_PROPERTYNAME] - ]; - this.sortedEntities = data.entities?.sort( - (a, b) => a.GetEntity().GetDisplay().localeCompare(b.GetEntity().GetDisplay())); - + this.displayedColumns = [this.entitySchemaCandidates.Columns[DisplayColumns.DISPLAY_PROPERTYNAME]]; + this.sortedEntities = data.entities?.sort((a, b) => a.GetEntity().GetDisplay().localeCompare(b.GetEntity().GetDisplay())); } public async ngOnInit(): Promise { this.searchedEntities = [...this.sortedEntities]; if (this.data.tables && this.data.tables.length > 1) { - this.metaData.update(this.data.tables); + this.metaData.updateNonExisting(this.data.tables); } - this.filters = this.data.tables && this.data.tables.length > 1 ? [{ - Name: 'table', - Description: await this.translate.get('#LDS#Type').toPromise(), - Options: this.getOptionsForFilter() - }] : []; + this.filters = + this.data.tables && this.data.tables.length > 1 + ? [ + { + Name: 'table', + Description: await this.translate.get('#LDS#Type').toPromise(), + Options: this.getOptionsForFilter(), + }, + ] + : []; this.navigate(this.navigationState); - } public search(key: string): void { if (key === '') { this.searchedEntities = [...this.sortedEntities]; } else { - this.searchedEntities = this.sortedEntities.filter(elem => - elem.GetEntity().GetDisplay().toLocaleLowerCase().includes(key.toLocaleLowerCase())); + this.searchedEntities = this.sortedEntities.filter((elem) => + elem.GetEntity().GetDisplay().toLocaleLowerCase().includes(key.toLocaleLowerCase()), + ); } this.navigate({ StartIndex: 0, search: key }); } @@ -103,7 +95,9 @@ export class TypedEntityCandidateSidesheetComponent implements OnInit { public navigate(source: TypedEntityTableFilter): void { this.navigationState = { ...this.navigationState, ...source }; const possible = source.table - ? this.searchedEntities.filter(elem => CdrFactoryService.tryGetColumn(elem.GetEntity(), 'XObjectKey')?.GetValue()?.includes(source.table)) + ? this.searchedEntities.filter( + (elem) => CdrFactoryService.tryGetColumn(elem.GetEntity(), 'XObjectKey')?.GetValue()?.includes(source.table), + ) : this.searchedEntities; const data = possible.slice(this.navigationState.StartIndex, this.navigationState.StartIndex + this.navigationState.PageSize); @@ -111,16 +105,18 @@ export class TypedEntityCandidateSidesheetComponent implements OnInit { displayedColumns: this.displayedColumns, dataSource: { Data: data, - totalCount: possible.length + totalCount: possible.length, }, filters: this.filters, entitySchema: this.entitySchemaCandidates, - navigationState: this.navigationState + navigationState: this.navigationState, }; } public getTable(entity: TypedEntity): string { - if (!this.data.tables || this.data.tables.length <= 1) { return ''; } + if (!this.data.tables || this.data.tables.length <= 1) { + return ''; + } const column = CdrFactoryService.tryGetColumn(entity.GetEntity(), 'XObjectKey'); if (!column) { return ''; @@ -131,9 +127,11 @@ export class TypedEntityCandidateSidesheetComponent implements OnInit { private getOptionsForFilter(): DataModelFilterOption[] { return this.data.tables - .map(elem => ({ Value: elem, Display: this.metaData.tables[elem]?.DisplaySingular })) - .filter(elem => - this.data.entities.some(entity => CdrFactoryService.tryGetColumn(entity.GetEntity(), 'XObjectKey')?.GetValue().includes(elem.Value) - )); + .map((elem) => ({ Value: elem, Display: this.metaData.tables[elem]?.DisplaySingular })) + .filter((elem) => + this.data.entities.some( + (entity) => CdrFactoryService.tryGetColumn(entity.GetEntity(), 'XObjectKey')?.GetValue().includes(elem.Value), + ), + ); } } diff --git a/imxweb/projects/qbm/src/lib/fk-advanced-picker/fk-selector.component.ts b/imxweb/projects/qbm/src/lib/fk-advanced-picker/fk-selector.component.ts index 9216a73c3..c064a2e17 100644 --- a/imxweb/projects/qbm/src/lib/fk-advanced-picker/fk-selector.component.ts +++ b/imxweb/projects/qbm/src/lib/fk-advanced-picker/fk-selector.component.ts @@ -29,7 +29,8 @@ import { Component, OnInit, ViewChild, Input, Output, EventEmitter } from '@angu import { TypedEntityBuilder, CollectionLoadParameters, - DisplayColumns, ValType, + DisplayColumns, + ValType, TypedEntity, IForeignKeyInfo, FilterType, @@ -37,7 +38,7 @@ import { DbObjectKey, DataModelFilter, FilterData, - DataModel + DataModel, } from 'imx-qbm-dbts'; import { ClassloggerService } from '../classlogger/classlogger.service'; import { MetadataService } from '../base/metadata.service'; @@ -52,7 +53,7 @@ import { BusyService } from '../base/busy.service'; @Component({ selector: 'imx-fk-selector', templateUrl: './fk-selector.component.html', - styleUrls: ['./fk-selector.component.scss'] + styleUrls: ['./fk-selector.component.scss'], }) export class FkSelectorComponent implements OnInit { public settings: DataSourceToolbarSettings; @@ -78,21 +79,21 @@ export class FkSelectorComponent implements OnInit { constructor( public readonly metadataProvider: MetadataService, private readonly settingsService: SettingsService, - private readonly logger: ClassloggerService) { - } + private readonly logger: ClassloggerService, + ) {} public async ngOnInit(): Promise { const isBusy = this.busyService.beginBusy(); if (this.data.fkRelations && this.data.fkRelations.length > 0) { this.logger.trace(this, 'Pre-select the first candidate table'); - this.selectedTable = this.data.fkRelations.find(fkr => fkr.TableName === this.data.selectedTableName) || this.data.fkRelations[0]; + this.selectedTable = this.data.fkRelations.find((fkr) => fkr.TableName === this.data.selectedTableName) || this.data.fkRelations[0]; this.dataModel = await this.selectedTable.GetDataModel(); this.filters = this.dataModel.Filters; } if (this.data.fkRelations && this.data.fkRelations.length > 0) { - await this.metadataProvider.update(this.data.fkRelations.map(fkr => fkr.TableName)); + await this.metadataProvider.updateNonExisting(this.data.fkRelations.map((fkr) => fkr.TableName)); } await this.loadTableData(); await this.getPreselectedEntities(); @@ -101,7 +102,7 @@ export class FkSelectorComponent implements OnInit { } this.logger.debug(this, 'Pre selected elements', this.selectedCandidates.length); - isBusy.endBusy(); + isBusy.endBusy(); } public search(keywords: string): void { @@ -110,7 +111,7 @@ export class FkSelectorComponent implements OnInit { } public amIDisabled(item: TypedEntity): boolean { - return this.data.disabledIds?.find( x => x === item.GetEntity().GetKeys()[0]) ? true : false; + return this.data.disabledIds?.find((x) => x === item.GetEntity().GetKeys()[0]) ? true : false; } /** @@ -121,7 +122,7 @@ export class FkSelectorComponent implements OnInit { return; } - return this.selectedCandidates[0] === item ? {'imx-selected-row': true} : {}; + return this.selectedCandidates[0] === item ? { 'imx-selected-row': true } : {}; } /** @@ -170,9 +171,10 @@ export class FkSelectorComponent implements OnInit { if (this.selectedTable) { const isBusy = this.busyService.beginBusy(); try { - let navigationState = this.settings && this.settings.navigationState ? - this.settings.navigationState : - { PageSize: this.settingsService.DefaultPageSize, StartIndex: 0 }; + let navigationState = + this.settings && this.settings.navigationState + ? this.settings.navigationState + : { PageSize: this.settingsService.DefaultPageSize, StartIndex: 0 }; if (newState) { navigationState = { ...navigationState, ...newState }; @@ -185,17 +187,14 @@ export class FkSelectorComponent implements OnInit { displayedColumns.push({ Type: ValType.String, ColumnName: 'Select', - untranslatedDisplay: '#LDS#Selection' - }); + untranslatedDisplay: '#LDS#Selection', + }); } displayedColumns.push(DisplayColumns.DISPLAY_PROPERTY); this.settings = { - dataSource: this.builder.buildReadWriteEntities( - await this.selectedTable.Get(navigationState), - this.entitySchema - ), + dataSource: this.builder.buildReadWriteEntities(await this.selectedTable.Get(navigationState), this.entitySchema), displayedColumns, entitySchema: this.entitySchema, filters: this.filters, @@ -203,8 +202,8 @@ export class FkSelectorComponent implements OnInit { navigationState, filterTree: { multiSelect: true, - filterMethode: async (parentKey) => this.selectedTable.GetFilterTree(parentKey) - } + filterMethode: async (parentKey) => this.selectedTable.GetFilterTree(parentKey), + }, }; } finally { isBusy.endBusy(); @@ -229,7 +228,7 @@ export class FkSelectorComponent implements OnInit { let table: IForeignKeyInfo; if (this.data.fkRelations.length > 1) { const tableName = DbObjectKey.FromXml(key).TableName; - table = this.data.fkRelations.find(fkr => fkr.TableName === tableName); + table = this.data.fkRelations.find((fkr) => fkr.TableName === tableName); } table = table || this.data.fkRelations[0]; @@ -240,16 +239,13 @@ export class FkSelectorComponent implements OnInit { ColumnName: table.ColumnName, Type: FilterType.Compare, CompareOp: CompareOperator.Equal, - Value1: key - } - ] + Value1: key, + }, + ], }; this.logger.debug(this, 'Getting preselected entity with navigation state', navigationState); - const result = this.builder.buildReadWriteEntities( - await table.Get(navigationState), - this.entitySchema - ); + const result = this.builder.buildReadWriteEntities(await table.Get(navigationState), this.entitySchema); if (result.Data.length) { preselectedTemp.push(result.Data[0]); @@ -264,4 +260,3 @@ export class FkSelectorComponent implements OnInit { } } } - diff --git a/imxweb/projects/qbm/src/lib/login/login.component.html b/imxweb/projects/qbm/src/lib/login/login.component.html index b0f5262a7..78c5d4e4f 100644 --- a/imxweb/projects/qbm/src/lib/login/login.component.html +++ b/imxweb/projects/qbm/src/lib/login/login.component.html @@ -4,35 +4,29 @@
- +
-

{{ product.name }} -
+

+ {{ product.name }} +
{{ title }}

- +
- -
@@ -41,21 +35,27 @@

{{ product.name }}
- - + + {{ authConfig.display | translate }}
-
- +
+
@@ -65,6 +65,4 @@

{{ product.name }}

-
{{ product.name }} -
{{ product.copyright }} -
+
{{ product.name }}
{{ product.copyright }}
diff --git a/imxweb/projects/qbm/src/lib/object-history/object-history.service.ts b/imxweb/projects/qbm/src/lib/object-history/object-history.service.ts index 2ea4dc010..233ad5522 100644 --- a/imxweb/projects/qbm/src/lib/object-history/object-history.service.ts +++ b/imxweb/projects/qbm/src/lib/object-history/object-history.service.ts @@ -28,7 +28,7 @@ import { Injectable } from '@angular/core'; import { HistoryComparisonData } from 'imx-api-qbm'; import { IStateOverviewItem, ObjectHistoryEvent } from 'imx-qbm-dbts'; import { ObjectHistoryApiService } from './object-history-api.service'; -import { MetadataService } from '../base/metadata.service' +import { MetadataService } from '../base/metadata.service'; export interface ObjectHistoryParameters { table: string; @@ -36,20 +36,20 @@ export interface ObjectHistoryParameters { } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ObjectHistoryService { private dataCached: ObjectHistoryEvent[]; - constructor(private readonly apiService: ObjectHistoryApiService, private metadataService: MetadataService) {} + constructor( + private readonly apiService: ObjectHistoryApiService, + private metadataService: MetadataService, + ) {} public async get(parameters: ObjectHistoryParameters, fetchRemote: boolean = true): Promise { if (fetchRemote || this.dataCached == null) { - this.dataCached = (await this.apiService.getHistoryData( - parameters.table, - parameters.uid - )) - .map(x => x.Events) + this.dataCached = (await this.apiService.getHistoryData(parameters.table, parameters.uid)) + .map((x) => x.Events) .reduce((a, b) => a.concat(b)); } @@ -58,7 +58,7 @@ export class ObjectHistoryService { public async getStateOverviewItems(table: string, uid: string): Promise { let stateOverviewItems = (await this.apiService.getHistoryData(table, uid)) - .map(x => x.StateOverviewItems) + .map((x) => x.StateOverviewItems) .reduce((a, b) => a.concat(b)); return stateOverviewItems; } @@ -66,13 +66,9 @@ export class ObjectHistoryService { public async getHistoryComparisonData(table: string, uid: string, options?: { CompareDate?: Date }): Promise { let historyComparisonData = await this.apiService.getHistoryComparisonData(table, uid, options); // Update tableName with translated display name - for await (const item of historyComparisonData){ - if(!!this.metadataService.tables[item.TableName]?.Display){ - item.TableName = this.metadataService.tables[item.TableName].Display; - }else{ - await this.metadataService.update([item.TableName]); - item.TableName = this.metadataService.tables[item.TableName].Display - } + for await (const item of historyComparisonData) { + await this.metadataService.updateNonExisting([item.TableName]); + item.TableName = this.metadataService.tables[item.TableName].Display; } return historyComparisonData; } diff --git a/imxweb/projects/qbm/src/lib/selected-elements/selected-elements-dialog/selected-elements-dialog.component.ts b/imxweb/projects/qbm/src/lib/selected-elements/selected-elements-dialog/selected-elements-dialog.component.ts index 7ec8e091c..d22f4a4d1 100644 --- a/imxweb/projects/qbm/src/lib/selected-elements/selected-elements-dialog/selected-elements-dialog.component.ts +++ b/imxweb/projects/qbm/src/lib/selected-elements/selected-elements-dialog/selected-elements-dialog.component.ts @@ -26,14 +26,7 @@ import { Component, Inject, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { - DataModelFilterOption, - DbObjectKey, - DisplayColumns, - EntitySchema, - IClientProperty, - TypedEntity -} from 'imx-qbm-dbts'; +import { DataModelFilterOption, DbObjectKey, DisplayColumns, EntitySchema, IClientProperty, TypedEntity } from 'imx-qbm-dbts'; import { MetadataService } from '../../base/metadata.service'; import { CdrFactoryService } from '../../cdr/cdr-factory.service'; import { DataSourceToolbarFilter } from '../../data-source-toolbar/data-source-toolbar-filters.interface'; @@ -47,8 +40,8 @@ import { MAT_DIALOG_DATA } from '@angular/material/dialog'; * A component, that can be shown inside a MatDialogComponent. It contains a table of entities with their display value as a column * If the entities, that are displayed, originated from different tables, the table name is displayed as a column as well * To use this, some {@link SelectedElementsDialogParameter | dialog parameter } has to be injected - * - * Example: + * + * Example: * this.dialog.open(SelectedElementsDialog, { * data: { entities: [array of entities], tables: [array of table names], header: 'translated header text' }, * }); @@ -71,17 +64,23 @@ export class SelectedElementsDialog implements OnInit { constructor( @Inject(MAT_DIALOG_DATA) public readonly data: SelectedElementsDialogParameter, private translate: TranslateService, - private metaData: MetadataService + private metaData: MetadataService, ) { this.entitySchemaCandidates = CandidateEntity.GetEntitySchema(); this.displayedColumns = [this.entitySchemaCandidates.Columns[DisplayColumns.DISPLAY_PROPERTYNAME]]; - this.sortedEntities = data.entities?.sort((a, b) => a.GetEntity().GetDisplay()?.localeCompare(b.GetEntity()?.GetDisplay())); + this.sortedEntities = data.entities?.sort( + (a, b) => + a + .GetEntity() + .GetDisplay() + ?.localeCompare(b.GetEntity()?.GetDisplay()), + ); } public async ngOnInit(): Promise { this.searchedEntities = [...this.sortedEntities]; if (this.data.tables && this.data.tables.length > 1) { - this.metaData.update(this.data.tables); + this.metaData.updateNonExisting(this.data.tables); } this.filters = this.data.tables && this.data.tables.length > 1 @@ -105,7 +104,7 @@ export class SelectedElementsDialog implements OnInit { this.searchedEntities = [...this.sortedEntities]; } else { this.searchedEntities = this.sortedEntities.filter((elem) => - elem.GetEntity().GetDisplay().toLocaleLowerCase().includes(key.toLocaleLowerCase()) + elem.GetEntity().GetDisplay().toLocaleLowerCase().includes(key.toLocaleLowerCase()), ); } this.navigate({ StartIndex: 0, search: key }); @@ -118,8 +117,8 @@ export class SelectedElementsDialog implements OnInit { public navigate(source: TypedEntityTableFilter): void { this.navigationState = { ...this.navigationState, ...source }; const possible = source.table - ? this.searchedEntities.filter((elem) => - CdrFactoryService.tryGetColumn(elem.GetEntity(), 'XObjectKey')?.GetValue()?.includes(source.table) + ? this.searchedEntities.filter( + (elem) => CdrFactoryService.tryGetColumn(elem.GetEntity(), 'XObjectKey')?.GetValue()?.includes(source.table), ) : this.searchedEntities; @@ -157,9 +156,9 @@ export class SelectedElementsDialog implements OnInit { return this.data.tables .map((elem) => ({ Value: elem, Display: this.metaData.tables[elem]?.DisplaySingular })) .filter((elem) => - this.data.entities.some((entity) => - CdrFactoryService.tryGetColumn(entity.GetEntity(), 'XObjectKey')?.GetValue().includes(elem.Value) - ) + this.data.entities.some( + (entity) => CdrFactoryService.tryGetColumn(entity.GetEntity(), 'XObjectKey')?.GetValue().includes(elem.Value), + ), ); } } diff --git a/imxweb/projects/qbm/src/lib/sqlwizard/single-value.component.html b/imxweb/projects/qbm/src/lib/sqlwizard/single-value.component.html index 594c5835a..e228a9498 100644 --- a/imxweb/projects/qbm/src/lib/sqlwizard/single-value.component.html +++ b/imxweb/projects/qbm/src/lib/sqlwizard/single-value.component.html @@ -22,13 +22,15 @@
{{ '#LDS#Value' | translate }} - + + {{ '#LDS#WD_InputInvalidInteger' | translate | ldsReplace : integerFormControl.value }}
{{ '#LDS#Value' | translate }} - + + {{ '#LDS#WD_InputInvalidFloat' | translate | ldsReplace : doubleFormControl.value }}
diff --git a/imxweb/projects/qbm/src/lib/sqlwizard/single-value.component.ts b/imxweb/projects/qbm/src/lib/sqlwizard/single-value.component.ts index 6dc1935e3..9cf470f36 100644 --- a/imxweb/projects/qbm/src/lib/sqlwizard/single-value.component.ts +++ b/imxweb/projects/qbm/src/lib/sqlwizard/single-value.component.ts @@ -39,6 +39,7 @@ import { BaseCdr } from '../cdr/base-cdr'; import { EntityService } from '../entity/entity.service'; import { SqlNodeView } from './SqlNodeView'; import { SqlWizardApiService } from './sqlwizard-api.service'; +import { FormControl, Validators } from '@angular/forms'; @Component({ selector: 'imx-sqlwizard-singlevalue', @@ -83,7 +84,7 @@ export class SingleValueComponent implements OnInit, OnDestroy { } set displayValue(val) { - if(!this.expr.Data?.DisplayValues){ + if (!this.expr.Data?.DisplayValues) { return; } if (this.mode == 'array' && this.expr.Data.DisplayValues) { @@ -101,8 +102,9 @@ export class SingleValueComponent implements OnInit, OnDestroy { public ValType = _valType; public ColumnType = SqlColumnTypes; - public cdr: BaseCdr; + public doubleFormControl = new FormControl(null, Validators.pattern(/^[+-]?\d+(\.\d+)?$/)); + public integerFormControl = new FormControl(null, Validators.pattern(/^[+-]?\d+$/)); private _selectedTable: SqlTable; private _fkRelation: MetaTableRelationData = { @@ -117,10 +119,10 @@ export class SingleValueComponent implements OnInit, OnDestroy { getDataModel: async () => ({}), }; - constructor(private readonly entityService: EntityService, private readonly sqlWizardApi: SqlWizardApiService) {} - private subscriptions: Subscription[] = []; + constructor(private readonly entityService: EntityService, private readonly sqlWizardApi: SqlWizardApiService) {} + public ngOnInit(): void { this.subscriptions.push( this.expr.columnChanged.subscribe((_) => { @@ -129,6 +131,11 @@ export class SingleValueComponent implements OnInit, OnDestroy { ); this.buildCdr(); + this.onFormValueChanges(); + } + + public ngOnDestroy(): void { + for (var s of this.subscriptions) s.unsubscribe(); } public emitChanges(): void { @@ -149,12 +156,18 @@ export class SingleValueComponent implements OnInit, OnDestroy { FkRelation: this._fkRelation, }; - if (this.expr.Property.Type === ValType.Bool) this.value = false; + if (this.expr.Property.Type === ValType.Bool && this.expr.Data.Value === undefined ) this.value = false; const column = this.entityService.createLocalEntityColumn(property, [this._fkProviderItem], { Value: this.value, DisplayValue: this.displayValue, }); + if (this.expr.Property.Type === ValType.Double) { + this.doubleFormControl.setValue(column.GetValue()); + } + if (this.expr.Property.Type === ValType.Int) { + this.integerFormControl.setValue(column.GetValue()); + } // when the CDR value changes, write back to the SQL wizard data structure column.ColumnChanged.subscribe(() => { @@ -167,7 +180,27 @@ export class SingleValueComponent implements OnInit, OnDestroy { this.cdr = new BaseCdr(column, '#LDS#Value'); } - public ngOnDestroy(): void { - for (var s of this.subscriptions) s.unsubscribe(); + private onFormValueChanges(): void { + this.subscriptions.push( + this.doubleFormControl.valueChanges.subscribe((value) => { + if (!this.doubleFormControl.hasError('pattern')) { + this.value = value; + this.emitChanges(); + } else { + this.value = {}; + } + }) + ); + this.subscriptions.push( + this.integerFormControl.valueChanges.subscribe((value) => { + if (!this.integerFormControl.hasError('pattern')) { + this.value = value; + this.emitChanges(); + } else { + this.value = {}; + } + }) + ); } } + diff --git a/imxweb/projects/qbm/src/lib/sqlwizard/sqlwizard.component.ts b/imxweb/projects/qbm/src/lib/sqlwizard/sqlwizard.component.ts index f65f2f930..84e4c86f1 100644 --- a/imxweb/projects/qbm/src/lib/sqlwizard/sqlwizard.component.ts +++ b/imxweb/projects/qbm/src/lib/sqlwizard/sqlwizard.component.ts @@ -24,7 +24,19 @@ * */ -import { AfterViewInit, Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + QueryList, + SimpleChanges, + ViewChildren, +} from '@angular/core'; import { SqlViewSettings } from './SqlNodeView'; import { LogOp as _logOp, SqlExpression } from 'imx-qbm-dbts'; import { SqlWizardApiService } from './sqlwizard-api.service'; @@ -55,7 +67,7 @@ export class SqlWizardComponent implements OnInit, OnChanges, AfterViewInit { /** Alternate API service to use. */ @Input() public apiService: SqlWizardApiService; - @Output() public change = new EventEmitter(); + @Output() public change = new EventEmitter(); @ViewChildren('expressionItem') public expressionList: QueryList>; @@ -69,14 +81,14 @@ export class SqlWizardComponent implements OnInit, OnChanges, AfterViewInit { } public ngAfterViewInit(): void { - setTimeout( () => { + setTimeout(() => { this.expressionList.changes.subscribe(() => { if (this.newExpressionAdded) { this.expressionList?.last?.nativeElement.scrollIntoView(true); } this.newExpressionAdded = false; - }) + }); }); } @@ -103,13 +115,15 @@ export class SqlWizardComponent implements OnInit, OnChanges, AfterViewInit { } public async emitChanges(): Promise { - this.change.emit(); await this.addEmptyExpression(); + this.change.emit(this.viewSettings.root.Data); } public onOperatorChanged(event: MatButtonToggleChange): void { - (event.value as string).toLowerCase() === 'and' ? (this.expression.LogOperator = this.LogOp.AND) : (this.expression.LogOperator = this.LogOp.OR); - this.change.emit(); + (event.value as string).toLowerCase() === 'and' + ? (this.expression.LogOperator = this.LogOp.AND) + : (this.expression.LogOperator = this.LogOp.OR); + this.change.emit(this.viewSettings.root.Data); } public logOpText(): string { diff --git a/imxweb/projects/qbm/src/lib/sqlwizard/sqlwizard.module.ts b/imxweb/projects/qbm/src/lib/sqlwizard/sqlwizard.module.ts index edb5417a6..0c8d8bd13 100644 --- a/imxweb/projects/qbm/src/lib/sqlwizard/sqlwizard.module.ts +++ b/imxweb/projects/qbm/src/lib/sqlwizard/sqlwizard.module.ts @@ -50,45 +50,41 @@ import { DatePickerComponent } from './date-picker.component'; import { MatRadioModule } from '@angular/material/radio'; import { SingleValueComponent } from './single-value.component'; import { CdrModule } from '../cdr/cdr.module'; +import { LdsReplaceModule } from '../lds-replace/lds-replace.module'; @NgModule({ - imports: [ - CommonModule, - CdrModule, - EuiCoreModule, - EuiMaterialModule, - FormsModule, - TranslateModule, - ReactiveFormsModule, - MatButtonModule, - MatDatepickerModule, - MatSelectModule, - MatCheckboxModule, - MatInputModule, - MatListModule, - MatFormFieldModule, - MatOptionModule, - MatRadioModule, - MatSlideToggleModule, - MatButtonToggleModule, - ], - declarations: [ - SqlWizardComponent, - ColumnSelectionComponent, - DatePickerComponent, - SimpleExpressionComponent, - SingleExpressionComponent, - SingleValueComponent, - TableSelectionComponent, - WhereClauseExpressionComponent, - ], - providers: [ - SqlWizardService - ], - exports: [ - SqlWizardComponent, - ] + imports: [ + CommonModule, + CdrModule, + EuiCoreModule, + EuiMaterialModule, + FormsModule, + TranslateModule, + LdsReplaceModule, + ReactiveFormsModule, + MatButtonModule, + MatDatepickerModule, + MatSelectModule, + MatCheckboxModule, + MatInputModule, + MatListModule, + MatFormFieldModule, + MatOptionModule, + MatRadioModule, + MatSlideToggleModule, + MatButtonToggleModule, + ], + declarations: [ + SqlWizardComponent, + ColumnSelectionComponent, + DatePickerComponent, + SimpleExpressionComponent, + SingleExpressionComponent, + SingleValueComponent, + TableSelectionComponent, + WhereClauseExpressionComponent, + ], + providers: [SqlWizardService], + exports: [SqlWizardComponent], }) -export class SqlWizardModule { - -} +export class SqlWizardModule {} diff --git a/imxweb/projects/qbm/src/lib/translation/imx-translation-provider.service.ts b/imxweb/projects/qbm/src/lib/translation/imx-translation-provider.service.ts index 899ea10a9..a734a42f7 100644 --- a/imxweb/projects/qbm/src/lib/translation/imx-translation-provider.service.ts +++ b/imxweb/projects/qbm/src/lib/translation/imx-translation-provider.service.ts @@ -60,12 +60,17 @@ export class ImxTranslationProviderService implements ITranslationProvider { } public async init(culture: string = this.translateService.getBrowserCultureLang(), cultureFormat: string = this.translateService.getBrowserCultureLang()): Promise { + //Use more specific culture, if de is provided (used for help documents) + if(culture == 'de') { + culture = 'de-DE'; + } const defaultLang = this.translateService.getDefaultLang(); // Get filtered cultures that are available to frontends and set to english if culture (browser language) is not supported const cultures = await this.appConfig.client.imx_multilanguage_uicultures_get({filter: [{ColumnName: 'Ident_DialogCulture', Value1: culture}]}); if(cultures.TotalCount === 0){ culture = 'en-US'; } + if (defaultLang == null || defaultLang !== culture) { this.translateService.setDefaultLang(culture); } diff --git a/imxweb/projects/qer-app-operationssupport/.compodocrc.json b/imxweb/projects/qer-app-operationssupport/.compodocrc.json index 204b886af..4c9cd3fc1 100644 --- a/imxweb/projects/qer-app-operationssupport/.compodocrc.json +++ b/imxweb/projects/qer-app-operationssupport/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - QER Operations Support", - "output": "../../documentation/qer-app-operationssupport", + "output": "../../documentation/v92/qer-app-operationssupport", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/qer-app-operationssupport/README.md b/imxweb/projects/qer-app-operationssupport/README.md new file mode 100644 index 000000000..740fc70ea --- /dev/null +++ b/imxweb/projects/qer-app-operationssupport/README.md @@ -0,0 +1,4 @@ +# Operations Support Portal + +The _Operations Support Portal_ is a web application to monitor the Identity Manager system state and troubleshoot queue processing. + diff --git a/imxweb/projects/qer-app-operationssupport/package.json b/imxweb/projects/qer-app-operationssupport/package.json index 73bcd51fb..4cfb5b054 100644 --- a/imxweb/projects/qer-app-operationssupport/package.json +++ b/imxweb/projects/qer-app-operationssupport/package.json @@ -1,6 +1,6 @@ { "name": "qer-app-operationssupport", - "version": "9.2.0", + "version": "9.2.1", "private": true, "description": "One Identity Manager Operations Support Web Portal", "peerDependencies": { diff --git a/imxweb/projects/qer-app-operationssupport/src/app/app.component.ts b/imxweb/projects/qer-app-operationssupport/src/app/app.component.ts index f2a081e7c..8a49b6ba1 100644 --- a/imxweb/projects/qer-app-operationssupport/src/app/app.component.ts +++ b/imxweb/projects/qer-app-operationssupport/src/app/app.component.ts @@ -26,17 +26,21 @@ import { OverlayRef } from '@angular/cdk/overlay'; import { Component, OnInit, OnDestroy, ErrorHandler } from '@angular/core'; -import { Router, RouterEvent, NavigationStart, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router'; +import { Router, RouterEvent, NavigationStart, NavigationEnd, NavigationError, NavigationCancel, EventType } from '@angular/router'; import { EuiLoadingService, EuiTheme, EuiThemeService, EuiTopNavigationItem } from '@elemental-ui/core'; import { Subscription } from 'rxjs'; -import { MenuItem, AuthenticationService, ISessionState, MenuService, SettingsService, imx_SessionService, SplashService, ImxTranslationProviderService } from 'qbm'; import { - FeatureConfigService, - OpSupportUserService, - QerApiService, - SettingsComponent -} from 'qer'; + MenuItem, + AuthenticationService, + ISessionState, + MenuService, + SettingsService, + imx_SessionService, + SplashService, + ImxTranslationProviderService, +} from 'qbm'; +import { FeatureConfigService, OpSupportUserService, QerApiService, SettingsComponent } from 'qer'; import { FeatureConfig, ProfileSettings } from 'imx-api-qer'; import { isOutstandingManager } from './permissions/permissions-helper'; import { MatDialog } from '@angular/material/dialog'; @@ -53,7 +57,7 @@ export class AppComponent implements OnInit, OnDestroy { public hideMenu = false; public hideUserMessage = false; public showPageContent = true; - + private routerStatus: EventType; private readonly subscriptions: Subscription[] = []; constructor( @@ -79,7 +83,7 @@ export class AppComponent implements OnInit, OnDestroy { // Needs to close here when there is an error on sessionState splash.close(); } else { - if (sessionState.IsLoggedOut) { + if (sessionState.IsLoggedOut && this.routerStatus !== EventType.NavigationEnd) { this.showPageContent = false; } } @@ -90,7 +94,7 @@ export class AppComponent implements OnInit, OnDestroy { // Set session culture if isUseProfileLangChecked is true, set browser culture otherwise if (isUseProfileLangChecked) { await this.translationProvider.init(sessionState.culture, sessionState.cultureFormat); - }else{ + } else { const browserCulture = this.translateService.getBrowserCultureLang(); await this.translationProvider.init(browserCulture); } @@ -104,7 +108,11 @@ export class AppComponent implements OnInit, OnDestroy { settings.DefaultPageSize = conf.DefaultPageSize; const groupInfo = await userModelService.getGroups(); - this.menuItems = await this.menuService.getMenuItems([], groupInfo.map(group => group.Name), true); + this.menuItems = await this.menuService.getMenuItems( + [], + groupInfo.map((group) => group.Name), + true + ); this.applyProfileSettings(); } @@ -129,28 +137,26 @@ export class AppComponent implements OnInit, OnDestroy { } public async openSettingsDialog(): Promise { - this.dialog.open(SettingsComponent,{minWidth: '600px'}); + this.dialog.open(SettingsComponent, { minWidth: '600px' }); } private setupRouter(): void { let overlayRef: OverlayRef; this.router.events.subscribe((event: RouterEvent) => { - switch (true) { - case event instanceof NavigationStart: - this.hideUserMessage = true; - - if (this.isLoggedIn && event.url === '/') { - // show the splash screen, when the user logs out! - this.splash.init({ applicationName: 'Operations Support Web Portal' }); - } - break; - case event instanceof NavigationEnd: - case event instanceof NavigationCancel: - case event instanceof NavigationError: - this.hideUserMessage = false; - this.hideMenu = event.url === '/'; - this.showPageContent = true; + if (event instanceof NavigationStart) { + this.routerStatus = event.type; + this.hideUserMessage = true; + if (this.isLoggedIn && event.url === '/') { + // show the splash screen, when the user logs out! + this.splash.init({ applicationName: 'Operations Support Web Portal' }); + } + } + if (event instanceof NavigationEnd || event instanceof NavigationCancel || event instanceof NavigationError) { + this.hideUserMessage = false; + this.routerStatus = event.type; + this.hideMenu = event.url === '/'; + this.showPageContent = true; } }); } @@ -226,7 +232,7 @@ export class AppComponent implements OnInit, OnDestroy { id: 'OpsWeb_DbQueue_DbQueue', title: '#LDS#Menu Entry DBQueue', route: 'DbQueue', - } + }, ], }; return menu; @@ -288,9 +294,9 @@ export class AppComponent implements OnInit, OnDestroy { { id: 'OpsWeb_System_DataChanges', title: '#LDS#Menu Entry Operation history', - route: 'DataChanges' - } - ] + route: 'DataChanges', + }, + ], }; if (featureConfig?.EnableSystemStatus) { @@ -307,15 +313,13 @@ export class AppComponent implements OnInit, OnDestroy { return null; } - private async applyProfileSettings() - { + private async applyProfileSettings() { try { let profileSettings: ProfileSettings = await this.qerClient.client.opsupport_profile_get(); if (profileSettings?.PreferredAppThemes) { this.themeService.setTheme(profileSettings.PreferredAppThemes); } - } - catch (error) { + } catch (error) { this.errorHandler.handleError(error); } } diff --git a/imxweb/projects/qer-app-portal/.compodocrc.json b/imxweb/projects/qer-app-portal/.compodocrc.json index 686712572..f06b44253 100644 --- a/imxweb/projects/qer-app-portal/.compodocrc.json +++ b/imxweb/projects/qer-app-portal/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - QER Portal", - "output": "../../documentation/qer-app-portal", + "output": "../../documentation/v92/qer-app-portal", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/qer-app-portal/README.md b/imxweb/projects/qer-app-portal/README.md new file mode 100644 index 000000000..5e027eea0 --- /dev/null +++ b/imxweb/projects/qer-app-portal/README.md @@ -0,0 +1,37 @@ +# One Identity Manager Web portal + +This is the main application of the product. It contains features for managers, auditors, normal users and so on. It can be categorized into the following parts. + +For a more detailed description, please read the Web Portal Documentation. + +## 1. IT-Shop +The IT-Shop allows the user to request products. This requests will be decided through configurable workflows, that can be combinations of automated processes and user approvals. + +## 2. Data Management +The main part of data management is handled through the _Data Explorer_ and the _My Responsibilities_ view, which look quite similar. The _Data Explorer_ is used by administrators or auditors, the _My Responsibilities_ by managers. The objects that can be handled are determined by the dynamic One Identity Manager modules the company has installed. +Under the menu item _Statistics_ managers and administrators can look into the statistics. + +## 3. Attestation +This concerns everything attestation related. Users can manage attestation policies, handle attestation runs and decide the attestation of objects. Attestation is a dynamic library, which needs the corresponding One Identity Manager module to be installed. + +## 4. Setup +Under the menu item _Setup_ users can manage shops, service items, service categories or reports. + +The following libraries are available. Which libraries will be used, depends on the configuration of the installation. + +|Library name | Description| +|---|---| +| qbm | This is the base library. It contains all the base components, like the data table and the column dependent references. | +| qer | This is the Identity Management base library. It contains the basic components for identity management, like the Data Explorer.| +| aob | This is the applications library. It contains the components, that are needed to assign entitlements to application bundles. | +| apc | This is a library, that is linked into the Data Explorer. It is used to manage software, that is assigned to users.| +| att | This is the attestation library. It contains everything, that is attestation related, like attesting objects, manage attestation runs, etc.| +| cpl | This is the compliance library. It is used to handle compliance violations and manage compliance rules. | +| hds | This is the help desk library. It contains components to resolve help desk tickets or create new ones.| +| olg | This is the One Login library. It handles the one login multi factor authentication process.| +| pol | This is the policy library. It is used to handle policy violations and manage company policies.| +| rmb | This is the business role library. It is linked into the Data Explorer and is used to manage business roles. | +| rms | This is the system role library. It is linked into the Data Explorer and is used to manage system roles.| +| rps | This is the report library. It contains components to manage reports and assign them to other users. It handles report subscriptions as well.| +| sac | This is the SAP R/3 Compliance Add-on Module. It adds some SAP features to the request process. | +| tsb | This is the target system library. It is linked into the Data Explorer and is used to manage system entitlements and user accounts from different target systems. | diff --git a/imxweb/projects/qer-app-portal/package.json b/imxweb/projects/qer-app-portal/package.json index 7fb9297e1..cd2818945 100644 --- a/imxweb/projects/qer-app-portal/package.json +++ b/imxweb/projects/qer-app-portal/package.json @@ -1,6 +1,6 @@ { "name": "qer-app-portal", - "version": "9.2.0", + "version": "9.2.1", "private": true, "peerDependencies": { } diff --git a/imxweb/projects/qer-app-portal/src/app/app.component.ts b/imxweb/projects/qer-app-portal/src/app/app.component.ts index 97fca5a69..6e5a18f4f 100644 --- a/imxweb/projects/qer-app-portal/src/app/app.component.ts +++ b/imxweb/projects/qer-app-portal/src/app/app.component.ts @@ -24,10 +24,18 @@ * */ import { Component, Inject, ErrorHandler, OnDestroy, OnInit } from '@angular/core'; -import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, RouterEvent } from '@angular/router'; +import { EventType, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, RouterEvent } from '@angular/router'; import { Subscription } from 'rxjs'; -import { AuthenticationService, IeWarningService, ImxTranslationProviderService, ISessionState, MenuService, SplashService, SystemInfoService } from 'qbm'; +import { + AuthenticationService, + IeWarningService, + ImxTranslationProviderService, + ISessionState, + MenuService, + SplashService, + SystemInfoService, +} from 'qbm'; import { ProjectConfigurationService, UserModelService, SettingsComponent, QerApiService } from 'qer'; @@ -50,7 +58,7 @@ export class AppComponent implements OnInit, OnDestroy { public hideMenu = false; public hideUserMessage = false; public showPageContent = true; - + private routerStatus: EventType; private readonly subscriptions: Subscription[] = []; constructor( @@ -76,7 +84,7 @@ export class AppComponent implements OnInit, OnDestroy { // Needs to close here when there is an error on sessionState splash.close(); } else { - if (sessionState.IsLoggedOut) { + if (sessionState.IsLoggedOut && this.routerStatus !== EventType.NavigationEnd) { this.showPageContent = false; } } @@ -95,18 +103,12 @@ export class AppComponent implements OnInit, OnDestroy { // Set session culture if isUseProfileLangChecked is true, set browser culture otherwise if (isUseProfileLangChecked) { await this.translationProvider.init(sessionState.culture, sessionState.cultureFormat); - }else{ + } else { const browserCulture = this.translateService.getBrowserCultureLang(); await this.translationProvider.init(browserCulture); } - this.menuItems = await menuService.getMenuItems( - systemInfo.PreProps, - features, - true, - config, - groups - ); + this.menuItems = await menuService.getMenuItems(systemInfo.PreProps, features, true, config, groups); ieWarningService.showIe11Banner(); @@ -153,7 +155,7 @@ export class AppComponent implements OnInit, OnDestroy { } public async openSettingsDialog(): Promise { - this.dialog.open(SettingsComponent,{minWidth: '600px'}); + this.dialog.open(SettingsComponent, { minWidth: '600px' }); } public async goToMyProcesses(): Promise { @@ -168,6 +170,7 @@ export class AppComponent implements OnInit, OnDestroy { this.router.events.subscribe((event: RouterEvent) => { if (event instanceof NavigationStart) { this.hideUserMessage = true; + this.routerStatus = event.type; if (this.isLoggedIn && event.url === '/') { // show the splash screen, when the user logs out! this.splash.init({ applicationName: 'One Identity Manager Portal' }); @@ -176,29 +179,30 @@ export class AppComponent implements OnInit, OnDestroy { if (event instanceof NavigationCancel) { this.hideUserMessage = false; + this.routerStatus = event.type; } if (event instanceof NavigationEnd) { this.hideUserMessage = false; this.hideMenu = event.url === '/'; this.showPageContent = true; + this.routerStatus = event.type; } if (event instanceof NavigationError) { this.hideUserMessage = false; + this.routerStatus = event.type; } }); } - private async applyProfileSettings() - { + private async applyProfileSettings() { try { let profileSettings: ProfileSettings = await this.qerClient.client.portal_profile_get(); if (profileSettings?.PreferredAppThemes) { this.themeService.setTheme(profileSettings.PreferredAppThemes); } - } - catch (error) { + } catch (error) { this.errorHandler.handleError(error); } } diff --git a/imxweb/projects/qer-app-pwdportal/package.json b/imxweb/projects/qer-app-pwdportal/package.json index ed62d113f..e98f32e1e 100644 --- a/imxweb/projects/qer-app-pwdportal/package.json +++ b/imxweb/projects/qer-app-pwdportal/package.json @@ -1,5 +1,5 @@ { "name": "qer-app-pwdportal", - "version": "9.2.0", + "version": "9.2.1", "private": true } \ No newline at end of file diff --git a/imxweb/projects/qer-app-pwdportal/src/app/app.component.ts b/imxweb/projects/qer-app-pwdportal/src/app/app.component.ts index a2033ca77..1b1a537b4 100644 --- a/imxweb/projects/qer-app-pwdportal/src/app/app.component.ts +++ b/imxweb/projects/qer-app-pwdportal/src/app/app.component.ts @@ -25,7 +25,7 @@ */ import { Component, ErrorHandler, OnDestroy, OnInit } from '@angular/core'; -import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, RouterEvent } from '@angular/router'; +import { EventType, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, RouterEvent } from '@angular/router'; import { Subscription } from 'rxjs'; import { AppConfigService, AuthenticationService, ISessionState, ImxTranslationProviderService, SplashService } from 'qbm'; @@ -45,7 +45,7 @@ export class AppComponent implements OnInit, OnDestroy { public isLoggedIn = false; public hideUserMessage = false; public showPageContent = true; - + private routerStatus: EventType; private readonly subscriptions: Subscription[] = []; constructor( @@ -66,7 +66,7 @@ export class AppComponent implements OnInit, OnDestroy { // Needs to close here when there is an error on sessionState this.splash.close(); } else { - if (sessionState.IsLoggedOut && !this.isOnUserActivation()) { + if (sessionState.IsLoggedOut && !this.isOnUserActivation() && this.routerStatus !== EventType.NavigationEnd) { this.showPageContent = false; } } @@ -77,7 +77,7 @@ export class AppComponent implements OnInit, OnDestroy { // Set session culture if isUseProfileLangChecked is true, set browser culture otherwise if (isUseProfileLangChecked) { await this.translationProvider.init(sessionState.culture, sessionState.cultureFormat); - }else{ + } else { const browserCulture = this.translateService.getBrowserCultureLang(); await this.translationProvider.init(browserCulture); } @@ -117,6 +117,7 @@ export class AppComponent implements OnInit, OnDestroy { this.router.events.subscribe((event: RouterEvent) => { if (event instanceof NavigationStart) { this.hideUserMessage = true; + this.routerStatus = event.type; if (this.isLoggedIn) { if (event.url === '/') { // show the splash screen, when the user logs out! @@ -130,15 +131,18 @@ export class AppComponent implements OnInit, OnDestroy { if (event instanceof NavigationCancel) { this.hideUserMessage = false; + this.routerStatus = event.type; } if (event instanceof NavigationEnd) { this.hideUserMessage = false; this.showPageContent = true; + this.routerStatus = event.type; } if (event instanceof NavigationError) { this.hideUserMessage = false; + this.routerStatus = event.type; } }); } diff --git a/imxweb/projects/qer/.compodocrc.json b/imxweb/projects/qer/.compodocrc.json index b34f59eff..bc4741cb2 100644 --- a/imxweb/projects/qer/.compodocrc.json +++ b/imxweb/projects/qer/.compodocrc.json @@ -1,11 +1,14 @@ { "name": "IMX Web - QER Library", - "output": "../../documentation/qer", + "output": "../../documentation/v92/qer", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true, + "includes": "./additionalDocumentation", + "includesName": "Library overview" } diff --git a/imxweb/projects/qer/README.md b/imxweb/projects/qer/README.md new file mode 100644 index 000000000..b480bf293 --- /dev/null +++ b/imxweb/projects/qer/README.md @@ -0,0 +1,11 @@ +# Identity management base module + +This library contains the basic components for identity management. +That includes, but is not limited to, role management, the IT shop and identity administration. + +See the following pages for more details on the code structure. + +- [Identity Management](additional-documentation/identity-management.html) +- [Role management](additional-documentation/role-management.html) +- [IT shop](additional-documentation/it-shop.html) +- [Other components and services](additional-documentation/other-reusable-components.html) \ No newline at end of file diff --git a/imxweb/projects/qer/additionalDocumentation/identity.md b/imxweb/projects/qer/additionalDocumentation/identity.md new file mode 100644 index 000000000..a60f30552 --- /dev/null +++ b/imxweb/projects/qer/additionalDocumentation/identity.md @@ -0,0 +1,35 @@ +# Identity Management + +This section lists components that support identity management use-cases. + +## 1. Data Explorer 'Identities' section + +The identities section of the data explorer is defined in the [`DataExplorerIdentitiesComponent`](../components/DataExplorerIdentitiesComponent.html) and can be used in two different contexts: +1. as an identity administrator +2. as a manager + +The manager view is filtered by identities, that are reporting to the user, while the admin view shows all identities in the system. + +Clicking on an identity opens a side sheet that displays more information about the identity. + +This library defines the following sub components that are part of this side sheet: +- a tab control with the following sub components: + - [`ObjectHyperviewComponent`](../components/ObjectHyperviewComponent.html): displays a hyper view for the identity. + - [`OrgChartComponent`](../components/OrgChartComponent.html): displays an organizational chart for the identity. + - [`ObjectHistoryComponent`](../../qbm/components/ObjectHistoryComponent.html): displays the history of the identity object. This is defined in QBM. +- [`AssignmentsComponent`](../components/AssignmentsComponent.html): A component, that shows the memberships of the selected entity. The component looks quite similar to the data explorer control. The registration of membership tabs is quite similar, too. + +- Additionally, it is possible to register other tabs using the [`ExtService`](../../qbm/injectables/ExtService.html), which is part of the QBM library. + + +## 2. Address Book +The [`AddressbookComponent`](../components/AddressbookComponent.html) lists all identities from the `Person` database table. It is a read-only view that opens a read-only sidesheet as well. + +## 3. Profile +The [`ProfileComponent`](../components/ProfileComponent.html) provides access to the current user's main data. Additionally the user can configure e-mail subscriptions ([`MailSubscriptionsComponent`](../components/MailSubscriptionsComponent.html)), manage +security keys ([`SecurityKeysComponent`](../components/SecurityKeysComponent.html)) and edit the Q&A profile ([`PasswordQueryComponent`](../components/PasswordQueryComponent.html)). + +To extend the profile with more tab pages, register them with the [`ExtService`](../../qbm/injectables/ExtService.html), which is part of the QBM library. + +## 4. User Model +The user model is defined in the [`UserModelService`](../injectables/UserModelService.html). It contains functions to fetch the current session's configuration, including information on available program features, pending items and direct reports. diff --git a/imxweb/projects/qer/additionalDocumentation/it-shop.md b/imxweb/projects/qer/additionalDocumentation/it-shop.md new file mode 100644 index 000000000..dc0e24674 --- /dev/null +++ b/imxweb/projects/qer/additionalDocumentation/it-shop.md @@ -0,0 +1,53 @@ +# IT Shop + +## 1. New Request +The starting point of the request life cycle is defined by the [`NewRequestComponent`](../components/NewRequestComponent.html). This component consists of several tab page:. + +1. [`NewRequestProductComponent`](../components/NewRequestProductComponent.html): The main page shows the service items organized by service category. +2. [`NewRequestPeerGroupComponent`](../components/NewRequestPeerGroupComponent.html): In this tab, the user can select recommended service items and organizational structures. +3. [`NewRequestReferenceUserComponent`](../components/NewRequestReferenceUserComponent.html): The user can select service items and organizational structures by reference user here. +4. [`NewRequestProductBundleComponent`](../components/NewRequestProductBundleComponent.html): The user can create requests based on a product bundle. + +## 2. Parameter Editor + +Service items can define request properties. These are handled using the [`ParameterDataService`](../injectables/ParameterDataService.html), which converts the data given by the server to editable parameter columns, that can be edited using the [`CartItemEditComponent`](../components/CartItemEditComponent.html). + +## 3. Shopping Cart +The entry point into the shopping cart is defined by the [`ShoppingCartComponent`](../components/ShoppingCartComponent.html). It contains a table that lists the cart items in the cart. The items can be edited, deleted and checked for validity. All sub components and services are part of the [`ShoppingCartModule`](../modules/ShoppingCartModule.html). +Items in the cart can be moved to the Saved-for-later list and vice versa, this is handled by the [`ShoppingCartForLaterComponent`](../components/ShoppingCartForLaterComponent.html). + +## 4. Pending Requests + +The next step in the workflow is the approval of items. The entry point for this is the [`ApprovalsComponent`](../components/ApprovalsComponent.html), which lists all the requests currently approvable by the user. + +This also contains a sub component [`InquiriesComponent`](../components/InquiriesComponent.html) which lists inquiries for the user. + +All the sub components are part of the [`ApprovalsModule`](../modules/ApprovalsModule.html) which includes the components on the corresponding side sheet. + +## 5. Request History + +All requests visible for the current user are displayed by the [`RequestHistoryComponent`](../components/RequestHistoryComponent.html). The actions that the user can perform on these requests are implemented in the [`RequestActionService`](../injectables/RequestActionService.html). + +## 6. Archived Requests + +Archived requests are requests that have been offloaded to the history database. These can be viewed using the [`ArchivedRequestsComponent`](../components/ArchivedRequestsComponent.html). + +## 7. Editors for Items + +### 7.1. Product Bundles + +Product bundles can be edited using the [`ItshopPatternComponent`](../components/ItshopPatternComponent.html), defined in the [`ItshopPatternModule`](../modules/ItshopPatternModule.html). + +### 7.2. Service Categories + +The [`ServiceCategoriesModule`](../modules/ServiceCategoriesModule.html) contains the [`ServiceCategoriesComponent`](../components/ServiceCategoriesComponent.html). This component uses a [`DataTreeWrapperComponent`](../../qbm/components/DataTreeWrapperComponent.html) to show the service category structure of the IT shop. + +### 7.3. Service Items + +The service item functionality is part of the [`ServiceItemsEditModule`](../modules/ServiceItemsEditModule.html). The entry point for this component is the ['ServiceItemsEditComponent'](../components/ServiceItemsEditComponent.html) which lists all available service items. Clicking on an item opens a [`ServiceItemsEditSidesheetComponent`](../components/ServiceItemsEditSidesheetComponent.html). + +### 7.4. Workflow Editor + +The workflow editor is part of the [`ApprovalWorkFlowModule`](../modules/ApprovalWorkFlowModule.html). The entry point is the [`ApprovalWorkflowHomeComponent`](../components/ApprovalWorkflowHomeComponent.html). + +The editing functionality is implemented in the [`ApprovalWorkflowEditComponent`](../components/ApprovalWorkflowEditComponent.html), \ No newline at end of file diff --git a/imxweb/projects/qer/additionalDocumentation/other.md b/imxweb/projects/qer/additionalDocumentation/other.md new file mode 100644 index 000000000..73d3cdc80 --- /dev/null +++ b/imxweb/projects/qer/additionalDocumentation/other.md @@ -0,0 +1,33 @@ +# Other components and services + +This page describes the most important components in the `qer` library. + +## 1. Data Explorer \ My Responsibilities + +The [`DataExplorerViewComponent`](../components/DataExplorerViewComponent.html) and the [`MyResponsibilitiesViewComponent`](../components/MyResponsibilitiesViewComponent.html) are used to display the main component for the _Data administration > _Data Explorer_ and the _Responsibilities_ > _My Responsibilities_ pages. It shows a menu item for each type registered in the [`DataExplorerRegistryService`](../injectables/DataExplorerRegistryService.html) or the [`MyResponsibilitiesRegistryService`](../injectables/MyResponsibilitiesRegistryService.html) respectively. + +## 2. Delegation + +The [`DelegationComponent`](../components/DelegationComponent.html) implements the delegation functionality. The component uses a `MatStepper` to navigate through the steps. + +## 3. Related Applications + +The [`RelatedApplicationsComponent`](../components/RelatedApplicationsComponent.html) adds links to other sites to the navigation. These links are configured in the database table `RelatedApplication`. + +## 4. Risk Index + +The [`RiskConfigComponent`](../components/RiskConfigComponent.html) displays a table of all risk index functions. They can be edited by using the [`RiskConfigSidesheetComponent`](../components/RiskConfigSidesheetComponent.html). + +## 5. Source Detective +The [`SourceDetectiveComponent`](../components/SourceDetectiveComponent.html) shows the assignment analysis of an object. It contains a tree with branches for every assignment. + +## 6. Statistics +The [`StatisticsModule`](../modules/StatisticsModule.html) contains the components for the Statistics view. The entry point is the [`StatisticsHomePageComponent`](../components/StatisticsHomePageComponent.html). Statistics are organized into *areas* which are displayed in a navigation tree. The user can view the statistics for each area. Clicking on a statistic opens a [`ChartsSidesheetComponent`](../components/ChartsSidesheetComponent.html) or a [`HeatmapSidesheetComponent`](../components/HeatmapSidesheetComponent.html) for a heatmap statistic. + +## 7. Terms of Use + +The [`TermsOfUseListComponent`](../components/TermsOfUseListComponent.html) handles the user flow to accept the terms of use for a service item. It also includes step-up 2FA if configured. + +## 8. User Process + +The [`UserProcessComponent`](../components/UserProcessComponent.html) displays a list of all the processes associated with the current user. diff --git a/imxweb/projects/qer/additionalDocumentation/roles.md b/imxweb/projects/qer/additionalDocumentation/roles.md new file mode 100644 index 000000000..25633ef1b --- /dev/null +++ b/imxweb/projects/qer/additionalDocumentation/roles.md @@ -0,0 +1,20 @@ +# Role Management + +The [role management module](../modules/RoleManangementModule.html) extends the Data Explorer with components for the following object types. + +- departments +- cost centers +- locations +- application roles +- resources +- multi requestable / unsubscribeable resources +- multi request resources +- assignment resources + +The UI uses the [`RolesOverviewComponent`](../components/RolesOverviewComponent.html) for roles and the [`ResourcesComponent`](../components/ResourcesComponent.html) for resources. + +Each role can be edited inside a [`RoleDetailComponent`](../components/RoleDetailComponent.html) or [`ResourceSidesheetComponent`](../components/ResourceSidesheetComponent.html). + +## Extending Data Explorer + +It is possible to define plugins to extend the view for more role types. A [`RoleObjectInfo`](../interfaces/RoleObjectInfo.html) object needs to be registered on a [`RoleService`](../injectables/RoleService.html) that can be injected to the constructor of a plugin. diff --git a/imxweb/projects/qer/additionalDocumentation/summary.json b/imxweb/projects/qer/additionalDocumentation/summary.json new file mode 100644 index 000000000..3de719004 --- /dev/null +++ b/imxweb/projects/qer/additionalDocumentation/summary.json @@ -0,0 +1,18 @@ +[ + { + "title": "Identity Management", + "file": "identity.md" + }, + { + "title": "Role Management", + "file": "roles.md" + }, + { + "title": "IT Shop", + "file": "it-shop.md" + }, + { + "title": "Other Components and Services", + "file": "other.md" + } +] diff --git a/imxweb/projects/qer/package.json b/imxweb/projects/qer/package.json index 93b42620e..831c0fbdb 100644 --- a/imxweb/projects/qer/package.json +++ b/imxweb/projects/qer/package.json @@ -1,6 +1,6 @@ { "name": "qer", - "version": "9.2.0", + "version": "9.2.1", "private": true, "dependencies": { diff --git a/imxweb/projects/qer/src/lib/identities/identities.service.ts b/imxweb/projects/qer/src/lib/identities/identities.service.ts index 2e3597da0..c782f995b 100644 --- a/imxweb/projects/qer/src/lib/identities/identities.service.ts +++ b/imxweb/projects/qer/src/lib/identities/identities.service.ts @@ -190,18 +190,8 @@ export class IdentitiesService { * @returns Details of admin person. */ public async getAdminPerson(id: string): Promise { - const getPersonNavigationState: CollectionLoadParameters = { - filter: [ - { - ColumnName: 'UID_Person', - Type: FilterType.Compare, - CompareOp: CompareOperator.Equal, - Value1: id, - }, - ], - }; this.logger.debug(this, `Retrieving admin person with Id ${id}`); - return (await this.qerClient.typedClient.PortalAdminPerson.Get(getPersonNavigationState)).Data[0]; + return (await this.qerClient.typedClient.PortalAdminPersonInteractive.Get_byid(id)).Data[0]; } public async getPersonInteractive(uid: string): Promise> { diff --git a/imxweb/projects/qer/src/lib/identities/identity-sidesheet/identity-role-memberships/identity-role-memberships.component.ts b/imxweb/projects/qer/src/lib/identities/identity-sidesheet/identity-role-memberships/identity-role-memberships.component.ts index 0ea5f69eb..991140679 100644 --- a/imxweb/projects/qer/src/lib/identities/identity-sidesheet/identity-role-memberships/identity-role-memberships.component.ts +++ b/imxweb/projects/qer/src/lib/identities/identity-sidesheet/identity-role-memberships/identity-role-memberships.component.ts @@ -65,7 +65,7 @@ export class IdentityRoleMembershipsComponent implements OnInit { private readonly settingService: SettingsService, private readonly sidesheet: EuiSidesheetService, private readonly translate: TranslateService, - dataProvider: DynamicTabDataProviderDirective + dataProvider: DynamicTabDataProviderDirective, ) { this.referrer = dataProvider.data; this.entitySchema = this.roleMembershipsService.getSchema(this.referrer.tablename); @@ -85,7 +85,7 @@ export class IdentityRoleMembershipsComponent implements OnInit { public async ngOnInit(): Promise { const isBusy = this.busyService.beginBusy(); try { - await this.metadataService.update([this.referrer.tablename]); + await this.metadataService.updateNonExisting([this.referrer.tablename]); } finally { isBusy.endBusy(); } diff --git a/imxweb/projects/qer/src/lib/identities/identity-sidesheet/identity-sidesheet.component.scss b/imxweb/projects/qer/src/lib/identities/identity-sidesheet/identity-sidesheet.component.scss index d909a7f16..ece80de4a 100644 --- a/imxweb/projects/qer/src/lib/identities/identity-sidesheet/identity-sidesheet.component.scss +++ b/imxweb/projects/qer/src/lib/identities/identity-sidesheet/identity-sidesheet.component.scss @@ -40,9 +40,10 @@ display: flex; flex-direction: column; flex: 1 1 auto; - overflow: hidden; + overflow: auto; } + .imx-account-ext { padding: 30px 30px 0px 30px; } diff --git a/imxweb/projects/qer/src/lib/itshop/request-info/approver-container.spec.ts b/imxweb/projects/qer/src/lib/itshop/request-info/approver-container.spec.ts index 54216c144..78ad9b628 100644 --- a/imxweb/projects/qer/src/lib/itshop/request-info/approver-container.spec.ts +++ b/imxweb/projects/qer/src/lib/itshop/request-info/approver-container.spec.ts @@ -63,7 +63,8 @@ describe('ApproverContainer', () => { UID_PersonHead: { Value: uidPersonHead, DisplayValue: uidPersonHead, - } + }, + Decision: {Value: ''} } } } diff --git a/imxweb/projects/qer/src/lib/itshop/request-info/approver-container.ts b/imxweb/projects/qer/src/lib/itshop/request-info/approver-container.ts index 42a5c6387..f36954ceb 100644 --- a/imxweb/projects/qer/src/lib/itshop/request-info/approver-container.ts +++ b/imxweb/projects/qer/src/lib/itshop/request-info/approver-container.ts @@ -129,6 +129,7 @@ export class ApproverContainer { ? [] : this.request.pwoData.WorkflowData.Entities.filter( (data) => + !this.isDecided(data,this.request.pwoData.WorkflowData.Entities.filter((elem) => elem.Columns.Decision?.Value !== '')) && data.Columns.UID_PersonHead.Value && currentSteps.some((step) => data.Columns.UID_QERWorkingStep.Value === step.uidWorkingStep) && this.request.approvers.includes(data.Columns.UID_PersonHead.Value) @@ -152,6 +153,17 @@ export class ApproverContainer { } } + /* + * Checks, if a workflowData item is already decided (by any person in the same sub step) + */ + private isDecided(data: EntityData, decidedEntries:EntityData[]): boolean { + return decidedEntries.some( + (elem) => + elem.Columns.LevelNumber.Value === data.Columns.LevelNumber.Value && + elem.Columns.SubLevelNumber.Value === data.Columns.SubLevelNumber.Value + ); + } + private buildOrderedWorkingSteps(): OrderedWorkingStep[] { if (this.request == null || this.request.pwoData == null || this.request.pwoData.WorkflowSteps == null) { return []; diff --git a/imxweb/projects/qer/src/lib/itshopapprove/approvals-table.component.ts b/imxweb/projects/qer/src/lib/itshopapprove/approvals-table.component.ts index 2a15fd46b..dca15aaa9 100644 --- a/imxweb/projects/qer/src/lib/itshopapprove/approvals-table.component.ts +++ b/imxweb/projects/qer/src/lib/itshopapprove/approvals-table.component.ts @@ -216,6 +216,7 @@ export class ApprovalsTableComponent implements OnInit, OnDestroy { } public ngOnDestroy(): void { + this.approvalsService.abortCall(); // Set service value back to false since the toggle value is stored there this.approvalsService.isChiefApproval = false; this.subscriptions.forEach((s) => s.unsubscribe()); @@ -264,8 +265,8 @@ export class ApprovalsTableComponent implements OnInit, OnDestroy { const isBusy = this.busyService.beginBusy(); try { - this.approvalsCollection = await this.approvalsService.get(this.navigationState); - this.hasData = this.approvalsCollection.totalCount > 0 || (this.navigationState.search ?? '') !== ''; + this.approvalsCollection = await this.approvalsService.get(this.navigationState, {signal: this.approvalsService.abortController.signal}); + this.hasData = this.approvalsCollection?.totalCount > 0 || (this.navigationState.search ?? '') !== ''; this.updateTable(); if (this.extensions && this.extensions[0]) { diff --git a/imxweb/projects/qer/src/lib/itshopapprove/approvals.service.ts b/imxweb/projects/qer/src/lib/itshopapprove/approvals.service.ts index 3bc854e9e..9350f3edd 100644 --- a/imxweb/projects/qer/src/lib/itshopapprove/approvals.service.ts +++ b/imxweb/projects/qer/src/lib/itshopapprove/approvals.service.ts @@ -26,7 +26,7 @@ import { Injectable } from '@angular/core'; -import { ExtendedTypedEntityCollection, EntitySchema, DataModel, MethodDescriptor, EntityCollectionData, MethodDefinition } from 'imx-qbm-dbts'; +import { ExtendedTypedEntityCollection, EntitySchema, DataModel, MethodDescriptor, EntityCollectionData, MethodDefinition, ApiRequestOptions } from 'imx-qbm-dbts'; import { PortalItshopApproveRequests, OtherApproverInput, @@ -47,6 +47,8 @@ import { DataSourceToolbarExportMethod } from 'qbm'; @Injectable() export class ApprovalsService { + public abortController = new AbortController(); + constructor(private readonly apiService: QerApiService, private readonly itshopRequest: ItshopRequestService) {} public get PortalItshopApproveRequestsSchema(): EntitySchema { @@ -61,12 +63,21 @@ export class ApprovalsService { this.itshopRequest.isChiefApproval = val; } - public async get(parameters: ApprovalsLoadParameters): Promise> { + public abortCall(): void { + this.abortController.abort(); + this.abortController = new AbortController(); + } + + public async get( + parameters: ApprovalsLoadParameters, + requestOpts?: ApiRequestOptions + ): Promise> { const collection = await this.apiService.typedClient.PortalItshopApproveRequests.Get({ Escalation: this.isChiefApproval, ...parameters, - }); - return { + },requestOpts); + + return collection == null ? undefined: { tableName: collection.tableName, totalCount: collection.totalCount, Data: collection.Data.map((element, index) => @@ -82,13 +93,13 @@ export class ApprovalsService { getMethod: (withProperties: string, PageSize?: number) => { let method: MethodDescriptor; if (PageSize) { - method = factory.portal_itshop_approve_requests_get({...parameters, withProperties, PageSize, StartIndex: 0}) + method = factory.portal_itshop_approve_requests_get({ ...parameters, withProperties, PageSize, StartIndex: 0 }); } else { - method = factory.portal_itshop_approve_requests_get({...parameters, withProperties}) + method = factory.portal_itshop_approve_requests_get({ ...parameters, withProperties }); } return new MethodDefinition(method); - } - } + }, + }; } public async getApprovalDataModel(): Promise { diff --git a/imxweb/projects/qer/src/lib/itshopapprove/workflow-action/workflow-multi-action/workflow-multi-action.component.ts b/imxweb/projects/qer/src/lib/itshopapprove/workflow-action/workflow-multi-action/workflow-multi-action.component.ts index a3db2753a..fe8ae6fb0 100644 --- a/imxweb/projects/qer/src/lib/itshopapprove/workflow-action/workflow-multi-action/workflow-multi-action.component.ts +++ b/imxweb/projects/qer/src/lib/itshopapprove/workflow-action/workflow-multi-action/workflow-multi-action.component.ts @@ -91,7 +91,10 @@ export class WorkflowMultiActionComponent implements OnInit { } } - bulkItem.properties.unshift(this.stepService.getCurrentStepCdr(item, item.pwoData,'#LDS#Current approval step')); + const step = this.stepService.getCurrentStepCdr(item, item.pwoData, '#LDS#Current approval step'); + if (step != null) { + bulkItem.properties.unshift(step); + } item.parameterColumns.forEach((column) => bulkItem.properties.push(this.data.approve ? new BaseCdr(column) : new BaseReadonlyCdr(column)) diff --git a/imxweb/projects/qer/src/lib/new-request/new-request-header/new-request-header-toolbar/new-request-header-toolbar.component.html b/imxweb/projects/qer/src/lib/new-request/new-request-header/new-request-header-toolbar/new-request-header-toolbar.component.html index af0db075b..a485cfc02 100644 --- a/imxweb/projects/qer/src/lib/new-request/new-request-header/new-request-header-toolbar/new-request-header-toolbar.component.html +++ b/imxweb/projects/qer/src/lib/new-request/new-request-header/new-request-header-toolbar/new-request-header-toolbar.component.html @@ -4,21 +4,21 @@ #LDS#Root category (all products)
+(null); + + public keywords: string = ''; //#endregion //#region Navigation State diff --git a/imxweb/projects/qer/src/lib/new-request/new-request-product-bundle/product-bundle-items/product-bundle-items.component.ts b/imxweb/projects/qer/src/lib/new-request/new-request-product-bundle/product-bundle-items/product-bundle-items.component.ts index 6876fbe5d..3c8952bea 100644 --- a/imxweb/projects/qer/src/lib/new-request/new-request-product-bundle/product-bundle-items/product-bundle-items.component.ts +++ b/imxweb/projects/qer/src/lib/new-request/new-request-product-bundle/product-bundle-items/product-bundle-items.component.ts @@ -75,7 +75,7 @@ export class ProductBundleItemsComponent implements OnInit, OnDestroy { this.orchestration.abortCall(); let parameters: CollectionLoadParameters = { - ...this.navigationState, + ...this.orchestration.dstSettingsProductBundles.navigationState, }; return from( @@ -103,7 +103,7 @@ export class ProductBundleItemsComponent implements OnInit, OnDestroy { this.entitySchema.Columns.Description, ]; this.dstWrapper = new DataSourceWrapper( - (state) => this.patternItemService.getPatternItemList(this.selectedProductBundle, this.navigationState), + (state, requestOpts) => this.patternItemService.getPatternItemList(this.selectedProductBundle, state, requestOpts), this.displayedColumns, this.entitySchema ); @@ -177,7 +177,7 @@ export class ProductBundleItemsComponent implements OnInit, OnDestroy { this.subscriptions.forEach((subscription) => subscription.unsubscribe()); } - public async getData(parameter?: CollectionLoadParameters): Promise { + public async getData(): Promise { if (!this.selectedProductBundle) { this.orchestration.disableSearch = true; return; @@ -186,22 +186,7 @@ export class ProductBundleItemsComponent implements OnInit, OnDestroy { const busy = this.myBusyService.beginBusy(); try { this.orchestration.abortCall(); - const filteredState: CollectionLoadParameters = { - filter: [ - { - ColumnName: 'UID_ShoppingCartPattern', - Type: FilterType.Compare, - CompareOp: CompareOperator.Equal, - Value1: this.selectedProductBundle.GetEntity().GetKeys()[0], - }, - ], - }; - - const parameters = { - ...parameter, - ...filteredState, - }; - this.dstSettings = await this.dstWrapper.getDstSettings(parameters, { signal: this.orchestration.abortController.signal }); + this.dstSettings = await this.dstWrapper.getDstSettings(this.navigationState, { signal: this.orchestration.abortController.signal }); this.orchestration.dstSettingsProductBundles = this.dstSettings; } finally { busy.endBusy(); diff --git a/imxweb/projects/qer/src/lib/new-request/new-request-product/new-request-product.component.ts b/imxweb/projects/qer/src/lib/new-request/new-request-product/new-request-product.component.ts index aac6b307f..a83079edd 100644 --- a/imxweb/projects/qer/src/lib/new-request/new-request-product/new-request-product.component.ts +++ b/imxweb/projects/qer/src/lib/new-request/new-request-product/new-request-product.component.ts @@ -26,6 +26,7 @@ import { FlatTreeControl } from '@angular/cdk/tree'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; import { PortalServicecategories, PortalShopServiceitems } from 'imx-api-qer'; import { CollectionLoadParameters, CompareOperator, DisplayColumns, IClientProperty, IWriteValue, MultiValue } from 'imx-qbm-dbts'; import { @@ -38,6 +39,8 @@ import { ClassloggerService, } from 'qbm'; import { Subscription } from 'rxjs/internal/Subscription'; +import { first, skip } from 'rxjs/operators'; + import { NewRequestOrchestrationService } from '../new-request-orchestration.service'; import { NewRequestCategoryApiService } from './new-request-category-api.service'; import { NewRequestProductApiService } from './new-request-product-api.service'; @@ -47,8 +50,6 @@ import { SelectedProductSource } from '../new-request-selected-products/selected import { ProductDetailsService } from './product-details-sidesheet/product-details.service'; import { NewRequestSelectionService } from '../new-request-selection.service'; import { CurrentProductSource } from '../current-product-source'; -import { ActivatedRoute } from '@angular/router'; -import { skip } from 'rxjs/operators'; export interface NewRequestCategoryNode { isSelected?: boolean; @@ -89,7 +90,7 @@ export class NewRequestProductComponent implements OnInit, OnDestroy { public categoryTreeControl = new FlatTreeControl( (leaf) => leaf.level, - (leaf) => leaf.entity.HasChildren.Column.GetValue(), + (leaf) => leaf.entity.HasChildren.Column.GetValue() ); public apiControls: DynamicDataApiControls = { setup: async () => { @@ -108,6 +109,7 @@ export class NewRequestProductComponent implements OnInit, OnDestroy { Value1: this.selectedServiceCategoryUID, }, ]; + userParams.ParentKey = undefined; } const servicecategories = await this.categoryApi.get(userParams); this.serviceCategoriesTotalCount = servicecategories?.totalCount; @@ -247,7 +249,7 @@ export class NewRequestProductComponent implements OnInit, OnDestroy { private readonly productDetailsService: ProductDetailsService, private readonly cd: ChangeDetectorRef, private readonly busyService: BusyService, - private readonly route: ActivatedRoute, + private readonly route: ActivatedRoute ) { this.orchestration.selectedView = SelectedProductSource.AllProducts; this.orchestration.searchApi$.next(this.searchApi); @@ -280,10 +282,10 @@ export class NewRequestProductComponent implements OnInit, OnDestroy { this.orchestration.dstSettingsAllProducts = this.dstSettings; } this.busy.endBusy(true); - }), + }) ); } - }), + }) ); this.subscriptions.push( @@ -291,7 +293,7 @@ export class NewRequestProductComponent implements OnInit, OnDestroy { this.productNavigationState = navigation; this.updateDisplayedColumns(this.displayedProductColumns); await this.getProductData(true); - }), + }) ); this.subscriptions.push( @@ -300,13 +302,13 @@ export class NewRequestProductComponent implements OnInit, OnDestroy { this.includeChildCategories = includeChilds; await this.getProductData(); } - }), + }) ); this.subscriptions.push( this.selectionService.selectedProducts$.subscribe(() => { this.orchestration.preselectBySource(SelectedProductSource.AllProducts, this.dst); - }), + }) ); this.subscriptions.push(this.selectionService.selectedProductsCleared$.subscribe(() => this.dst?.clearSelection())); @@ -319,6 +321,13 @@ export class NewRequestProductComponent implements OnInit, OnDestroy { this.orchestration.selectedCategory = null; this.updateDisplayedColumns(this.displayedProductColumns); + const queryParams = await this.route.queryParams.pipe(first()).toPromise(); + const productSearchString = queryParams['ProductSearchString']; + if (productSearchString?.length > 0) { + // the user can pass product search string by URL parameter -> load the data with this search string + this.setProductSearchString(productSearchString); + } + this.selectedServiceCategoryUID = this.route.snapshot.queryParams['serviceCategory']; this.selectedServiceItemUID = this.route.snapshot.queryParams['serviceItem']; let firstIteration = true; @@ -334,7 +343,7 @@ export class NewRequestProductComponent implements OnInit, OnDestroy { this.dynamicDataSource.setup(true); this.getProductData(true); } - }), + }) ); } @@ -477,4 +486,10 @@ export class NewRequestProductComponent implements OnInit, OnDestroy { this.resetSidenav = true; } } + + private async setProductSearchString(productSearchString: string): Promise { + this.orchestration.keywords = productSearchString; + this.productNavigationState.search = productSearchString; + await this.getProductData(); + } } diff --git a/imxweb/projects/qer/src/lib/new-request/new-request-product/product-entitlements/product-entitlements.component.html b/imxweb/projects/qer/src/lib/new-request/new-request-product/product-entitlements/product-entitlements.component.html index eb286e093..f861d15d7 100644 --- a/imxweb/projects/qer/src/lib/new-request/new-request-product/product-entitlements/product-entitlements.component.html +++ b/imxweb/projects/qer/src/lib/new-request/new-request-product/product-entitlements/product-entitlements.component.html @@ -1,5 +1,5 @@ +[options]="[]" (navigationStateChanged)="navigate($event)"> this.busy.hide(overlayRef)); } - await this.metadataSvc.update(this.passwordItems.map((elem) => elem.tableName)); + await this.metadataSvc.updateNonExisting(this.passwordItems.map((elem) => elem.tableName)); for (const p of this.passwordItems) { p.tableDisplay = this.metadataSvc.tables[p.tableName].DisplaySingular; } diff --git a/imxweb/projects/qer/src/lib/pattern-item-list/pattern-item.service.ts b/imxweb/projects/qer/src/lib/pattern-item-list/pattern-item.service.ts index 04c2736ff..dba4d85cf 100644 --- a/imxweb/projects/qer/src/lib/pattern-item-list/pattern-item.service.ts +++ b/imxweb/projects/qer/src/lib/pattern-item-list/pattern-item.service.ts @@ -40,6 +40,7 @@ import { EntityData, EntitySchema, ExtendedTypedEntityCollection, + FilterData, FilterType, ValueStruct, } from 'imx-qbm-dbts'; @@ -98,19 +99,25 @@ export class PatternItemService { requestOpts?: ApiRequestOptions, getAllItems?: boolean, ): Promise> { - let params: CollectionLoadParameters = { - ...parameters, - ...{ - filter: [ - { - ColumnName: 'UID_ShoppingCartPattern', - Type: FilterType.Compare, - CompareOp: CompareOperator.Equal, - Value1: patternRequestable.UID_ShoppingCartPattern.value, - }, - ], - }, - }; + + var shoppingCartPatternFilter = { + ColumnName: 'UID_ShoppingCartPattern', + Type: FilterType.Compare, + CompareOp: CompareOperator.Equal, + Value1: patternRequestable.UID_ShoppingCartPattern.value, + } + + const index = this.findFilterIndex(parameters.filter, 'UID_ShoppingCartPattern'); + if (index >= 0) { + parameters.filter[index] = shoppingCartPatternFilter; + } else { + if(!parameters.filter){ + parameters.filter = []; + } + parameters.filter.push(shoppingCartPatternFilter); + } + + let params = parameters; if (getAllItems) { let getAllItemsParams: CollectionLoadParameters = { @@ -124,7 +131,6 @@ export class PatternItemService { ...params, ...{ PageSize: totalCount - }, }; } @@ -170,4 +176,19 @@ export class PatternItemService { return onlySelected ? selectedItems : allItems; } + + + /** + * @ignore Used internally + * Attempts to find an existing filter matching the given column name. + * Returns the index or -1 if no match was found + */ + private findFilterIndex(filter: FilterData[], filterName: string): number { + let index: number; + if (!filter) { + return -1; + } + + return filter.map(f => f.ColumnName).indexOf(filterName); + } } diff --git a/imxweb/projects/qer/src/lib/product-selection/product-selection.component.ts b/imxweb/projects/qer/src/lib/product-selection/product-selection.component.ts index dffc3f7df..ccf4311ab 100644 --- a/imxweb/projects/qer/src/lib/product-selection/product-selection.component.ts +++ b/imxweb/projects/qer/src/lib/product-selection/product-selection.component.ts @@ -191,18 +191,14 @@ export class ProductSelectionComponent implements OnInit, OnDestroy { this.canRequestForSomebodyElse = (await this.userModelSvc.getUserConfig()).CanRequestForSomebodyElse; // TODO activatedRoute parameters may change, must subscribe to changes - - this.uidaccproduct = this.activatedRoute.snapshot.paramMap.get('UID_AccProduct'); + + this.uidaccproduct = this.activatedRoute.snapshot.queryParams.UID_AccProduct; if (this.uidaccproduct) { // TODO load all according to this.categoryModel.SelectedCategory } - this.searchString = this.activatedRoute.snapshot.paramMap.get('ProductSearchString'); - - if (this.searchString) { - /* user can pass product search string by URL parameter -> load the data with this search string - */ - } + // the user can pass product search string by URL parameter -> load the data with this search string + this.searchString = this.activatedRoute.snapshot.queryParams.ProductSearchString; // preset recipient to the current user await this.recipients.Column.PutValueStruct({ diff --git a/imxweb/projects/qer/src/lib/resources/resources.component.ts b/imxweb/projects/qer/src/lib/resources/resources.component.ts index 2325a473e..6010c46e7 100644 --- a/imxweb/projects/qer/src/lib/resources/resources.component.ts +++ b/imxweb/projects/qer/src/lib/resources/resources.component.ts @@ -41,12 +41,20 @@ import { IEntityColumn, TypedEntity, } from 'imx-qbm-dbts'; -import { BusyService, DataSourceToolbarSettings, DataSourceToolbarViewConfig,SideNavigationComponent, LdsReplacePipe, MetadataService, ClassloggerService, HelpContextualValues } from 'qbm'; +import { + BusyService, + DataSourceToolbarSettings, + DataSourceToolbarViewConfig, + SideNavigationComponent, + LdsReplacePipe, + MetadataService, + ClassloggerService, + HelpContextualValues, +} from 'qbm'; import { ResourceSidesheetComponent } from './resource-sidesheet/resource-sidesheet.component'; import { ResourcesService } from './resources.service'; import { ViewConfigService } from '../view-config/view-config.service'; - @Component({ templateUrl: './resources.component.html', styleUrls: ['./resources.component.scss'], @@ -87,7 +95,7 @@ export class ResourcesComponent implements OnInit, SideNavigationComponent { this.tablename = this.data?.TableName ?? this.route.snapshot?.url[this.route.snapshot?.url.length - 1]?.path; try { - await this.metadata.update([this.tablename]); + await this.metadata.updateNonExisting([this.tablename]); } catch (error) { this.logger.error(this, error); } @@ -175,7 +183,7 @@ export class ResourcesComponent implements OnInit, SideNavigationComponent { private async navigate(): Promise { const isBusy = this.busyService.beginBusy(); const exportMethod = this.resourceProvider.getExportMethod(this.tablename, this.isAdmin, this.navigationState); - exportMethod.initialColumns = this.displayColumns.map(col => col.ColumnName); + exportMethod.initialColumns = this.displayColumns.map((col) => col.ColumnName); try { this.dstSettings = { dataSource: await this.resourceProvider.get(this.tablename, this.isAdmin, this.navigationState), @@ -185,7 +193,7 @@ export class ResourcesComponent implements OnInit, SideNavigationComponent { filters: this.dataModel.Filters, dataModel: this.dataModel, viewConfig: this.viewConfig, - exportMethod + exportMethod, }; } finally { isBusy.endBusy(); diff --git a/imxweb/projects/qer/src/lib/role-management/role-memberships/dynamic-role.component.html b/imxweb/projects/qer/src/lib/role-management/role-memberships/dynamic-role.component.html index 42e35ef25..680bcf91d 100644 --- a/imxweb/projects/qer/src/lib/role-management/role-memberships/dynamic-role.component.html +++ b/imxweb/projects/qer/src/lib/role-management/role-memberships/dynamic-role.component.html @@ -27,7 +27,7 @@
- + diff --git a/imxweb/projects/qer/src/lib/role-management/role-memberships/dynamic-role.component.ts b/imxweb/projects/qer/src/lib/role-management/role-memberships/dynamic-role.component.ts index 2427ce5f4..e8f5b77c1 100644 --- a/imxweb/projects/qer/src/lib/role-management/role-memberships/dynamic-role.component.ts +++ b/imxweb/projects/qer/src/lib/role-management/role-memberships/dynamic-role.component.ts @@ -77,8 +77,8 @@ export class DynamicRoleComponent implements OnInit { public async ngOnInit(): Promise { this.canEdit = this.roleService.canEdit; - if(!this.canEdit) { - this.LdsNoDynamicRole = '#LDS#Currently, no identities automatically become members through a dynamic role.' + if (!this.canEdit) { + this.LdsNoDynamicRole = '#LDS#Currently, no identities automatically become members through a dynamic role.'; } await this.loadDynamicRole(); this.resetState(); @@ -104,9 +104,7 @@ export class DynamicRoleComponent implements OnInit { } await this.dynamicGroup.GetEntity().Commit(true); } else { - const e = >( - this.dataManagementService.entityInteractive - ); + const e = >this.dataManagementService.entityInteractive; e.extendedData = { NewDynamicRole: this.sqlExpression.Expression }; await e.GetEntity().Commit(true); this.uidDynamicGroup = e.GetEntity().GetColumn('UID_DynamicGroup').GetValue(); @@ -162,9 +160,10 @@ export class DynamicRoleComponent implements OnInit { } } - public checkChanges(): void { - this.exprHasntChanged = _.isEqual(this.sqlExpression?.Expression, this.lastSavedExpression); + public checkChanges(expression: SqlExpression): void { + this.exprHasntChanged = _.isEqual(expression, this.lastSavedExpression); if (!this.exprHasntChanged) { + this.sqlExpression.Expression = expression; this.dataManagementService.autoMembershipDirty(true); } else if (this.cdrsHaventChanged && this.exprHasntChanged) { this.dataManagementService.autoMembershipDirty(false); @@ -211,13 +210,12 @@ export class DynamicRoleComponent implements OnInit { this.sqlExpression.Expression.LogOperator = LogOp.AND; } - this.cdrList = this.canEdit ? [ - new BaseCdr(this.dynamicGroup.UID_DialogSchedule.Column), - new BaseCdr(this.dynamicGroup.IsCalculateImmediately.Column), - ] : [ - new BaseReadonlyCdr(this.dynamicGroup.UID_DialogSchedule.Column), - new BaseReadonlyCdr(this.dynamicGroup.IsCalculateImmediately.Column), - ]; + this.cdrList = this.canEdit + ? [new BaseCdr(this.dynamicGroup.UID_DialogSchedule.Column), new BaseCdr(this.dynamicGroup.IsCalculateImmediately.Column)] + : [ + new BaseReadonlyCdr(this.dynamicGroup.UID_DialogSchedule.Column), + new BaseReadonlyCdr(this.dynamicGroup.IsCalculateImmediately.Column), + ]; } } finally { this.busy = false; @@ -225,7 +223,9 @@ export class DynamicRoleComponent implements OnInit { } private hasValuesSet(sqlExpression: SqlExpression, checkCurrent: boolean = false): boolean { - const current = !checkCurrent || (sqlExpression.Value != null && (Object.keys(sqlExpression.Value).length > 0 || typeof sqlExpression.Value === 'boolean')); + const current = + !checkCurrent || + (sqlExpression.Value != null && (Object.keys(sqlExpression.Value).length > 0 || typeof sqlExpression.Value === 'boolean')); if (sqlExpression.Expressions?.length > 0) { return current && sqlExpression.Expressions.every((elem) => this.hasValuesSet(elem, true)); diff --git a/imxweb/projects/qer/src/lib/role-management/roles-overview/roles-overview.component.ts b/imxweb/projects/qer/src/lib/role-management/roles-overview/roles-overview.component.ts index 7f67c06af..a3b80e323 100644 --- a/imxweb/projects/qer/src/lib/role-management/roles-overview/roles-overview.component.ts +++ b/imxweb/projects/qer/src/lib/role-management/roles-overview/roles-overview.component.ts @@ -114,7 +114,7 @@ export class RolesOverviewComponent implements OnInit, OnDestroy, SideNavigation private readonly translate: TranslateService, private readonly permission: QerPermissionsService, private readonly errorService: ErrorService, - private readonly userModelService: UserModelService + private readonly userModelService: UserModelService, ) {} public ngOnDestroy(): void { @@ -127,7 +127,7 @@ export class RolesOverviewComponent implements OnInit, OnDestroy, SideNavigation this.canCreateAeRole = (await this.userModelService.getUserConfig()).CanCreateAERole; try { - await this.metadataProvider.update([this.ownershipInfo.TableName]); + await this.metadataProvider.updateNonExisting([this.ownershipInfo.TableName]); this.isStructureAdmin = await this.permission.isStructAdmin(); } catch (error) { this.navigateToStartPage(error); @@ -157,7 +157,8 @@ export class RolesOverviewComponent implements OnInit, OnDestroy, SideNavigation this.hasHierarchy = (await this.roleService.getEntitiesForTree(this.ownershipInfo.TableName, { PageSize: -1 }))?.Hierarchy != null; this.useTree = this.isAdmin && this.hasHierarchy; this.canCreate = - ((this.isAdmin && this.isStructureAdmin) || !this.isAdmin) && this.roleService.canCreate(this.ownershipInfo.TableName, this.isAdmin, this.canCreateAeRole); + ((this.isAdmin && this.isStructureAdmin) || !this.isAdmin) && + this.roleService.canCreate(this.ownershipInfo.TableName, this.isAdmin, this.canCreateAeRole); this.navigationState = this.useTree ? { diff --git a/imxweb/projects/qer/src/lib/shopping-cart/cart-items.service.ts b/imxweb/projects/qer/src/lib/shopping-cart/cart-items.service.ts index 4b07648cf..5df949ab5 100644 --- a/imxweb/projects/qer/src/lib/shopping-cart/cart-items.service.ts +++ b/imxweb/projects/qer/src/lib/shopping-cart/cart-items.service.ts @@ -28,13 +28,7 @@ import { Injectable, ErrorHandler } from '@angular/core'; import { EuiLoadingService } from '@elemental-ui/core'; import { FilterData, ExtendedTypedEntityCollection, CompareOperator, FilterType, EntitySchema, TypedEntity } from 'imx-qbm-dbts'; -import { - CartCheckResult, - CheckMode, - PortalCartitem, - RequestableProductForPerson, - CartItemDataRead, -} from 'imx-api-qer'; +import { CartCheckResult, CheckMode, PortalCartitem, RequestableProductForPerson, CartItemDataRead } from 'imx-api-qer'; import { BulkItemStatus, ClassloggerService } from 'qbm'; import { QerApiService } from '../qer-api-client.service'; import { ItemEditService } from '../product-selection/service-item-edit/item-edit.service'; @@ -88,7 +82,7 @@ export class CartItemsService { cartItem.UID_PersonOrdered.value = requestableServiceItemForPerson.UidPerson; cartItem.UID_ITShopOrg.value = requestableServiceItemForPerson.UidITShopOrg; if (requestableServiceItemForPerson?.UidPatternItem?.length > 0) { - cartItem.UID_PatternItem.value = requestableServiceItemForPerson.UidPatternItem + cartItem.UID_PatternItem.value = requestableServiceItemForPerson.UidPatternItem; } if (parentCartUid) { cartItem.UID_ShoppingCartItemParent.value = parentCartUid; @@ -191,7 +185,10 @@ export class CartItemsService { } } - public async getInteractiveCartitem(entityReference?: string, callbackOnChange?: () => void): Promise> { + public async getInteractiveCartitem( + entityReference?: string, + callbackOnChange?: () => void + ): Promise> { return this.cartItemInteractive.getExtendedEntity(entityReference, callbackOnChange); } @@ -237,13 +234,13 @@ export class CartItemsService { for (const item of results.bulkItems) { try { const found = cartItems.find((x) => x.typedEntity.GetEntity().GetKeys()[0] === item.entity.GetEntity().GetKeys()[0]); - if (item.status === BulkItemStatus.saved) { + if (item.status === BulkItemStatus.saved && results.submit) { await this.save(found); - this.logger.debug(this, `${found.typedEntity.GetEntity().GetDisplay} saved`); + this.logger.debug(this, `${found.typedEntity.GetEntity().GetDisplay()} saved`); } else { await this.removeItems([found.typedEntity]); result = result - 1; - this.logger.debug(this, `${found.typedEntity.GetEntity().GetDisplay} removed`); + this.logger.debug(this, `${found.typedEntity.GetEntity().GetDisplay()} removed`); } } catch (e) { this.logger.error(this, e.message); @@ -262,7 +259,6 @@ export class CartItemsService { setTimeout(() => this.busyIndicator.hide()); } - return result; } } diff --git a/imxweb/projects/qer/src/lib/sourcedetective/sourcedetective.component.ts b/imxweb/projects/qer/src/lib/sourcedetective/sourcedetective.component.ts index abead8d19..8b7f5ab42 100644 --- a/imxweb/projects/qer/src/lib/sourcedetective/sourcedetective.component.ts +++ b/imxweb/projects/qer/src/lib/sourcedetective/sourcedetective.component.ts @@ -43,208 +43,212 @@ import { ItshopRequestService } from '../itshop/itshop-request.service'; import { SourceDetectiveType } from './sourcedetective-type.enum'; type SourceNodeEnriched = SourceNode & { - Description: ParameterizedText; - TextTokens?: TextToken[]; - ObjectTypeDisplay: string; - Children: SourceNodeEnriched[]; - Level: number; + Description: ParameterizedText; + TextTokens?: TextToken[]; + ObjectTypeDisplay: string; + Children: SourceNodeEnriched[]; + Level: number; }; @Component({ - templateUrl: './sourcedetective.component.html', - styleUrls: ['./sourcedetective.component.scss'], - selector: 'imx-source-detective' + templateUrl: './sourcedetective.component.html', + styleUrls: ['./sourcedetective.component.scss'], + selector: 'imx-source-detective', }) export class SourceDetectiveComponent implements OnInit, OnChanges, OnDestroy { - - public treeControl = new FlatTreeControl( - node => node.Level, node => this.hasChild(node)); - - public treeFlattener = new MatTreeFlattener(x => x, - node => node.Level, node => this.hasChild(node), node => node.Children); - - public dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); - - public dataReady: boolean; - public busy = false; - - @Input() public UID_Person: string; - @Input() public TableName: string; - @Input() public UID: string; - @Input() public Type: SourceDetectiveType; - - public SourceDetectiveType = SourceDetectiveType; - private itShopConfig: ITShopConfig; - private currentUserId: string; - - private readonly subscriptions: Subscription[] = []; - - constructor( - private readonly apiClient: QerApiService, - private readonly metadata: MetadataService, - private readonly loader: EuiLoadingService, - public readonly ldsReplace: LdsReplacePipe, - private readonly translationProvider: ImxTranslationProviderService, - private readonly translator: TranslateService, - private readonly sidesheet: EuiSidesheetService, - private readonly projectConfig: ProjectConfigurationService, - private readonly itshopRequestService: ItshopRequestService, - authentication: AuthenticationService - ) { - this.subscriptions.push(authentication.onSessionResponse.subscribe(state => this.currentUserId = state.UserUid)); - } - - public hasChild = (node: SourceNodeEnriched) => !!node.Children && node.Children.length > 0; - - public async ngOnInit(): Promise { - this.busy = true; - try { - this.itShopConfig = (await this.projectConfig.getConfig()).ITShopConfig; - } finally { - this.busy = false; - } - this.reload(); - } - - public ngOnChanges(changes: SimpleChanges): void { - if (changes.UID || changes.TableName || changes.UID_Person) { - this.reload(); - } - } - - public ngOnDestroy(): void { - this.subscriptions.forEach(s => s.unsubscribe()); + public treeControl = new FlatTreeControl( + (node) => node.Level, + (node) => this.hasChild(node), + ); + + public treeFlattener = new MatTreeFlattener( + (x) => x, + (node) => node.Level, + (node) => this.hasChild(node), + (node) => node.Children, + ); + + public dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + + public dataReady: boolean; + public busy = false; + + @Input() public UID_Person: string; + @Input() public TableName: string; + @Input() public UID: string; + @Input() public Type: SourceDetectiveType; + + public SourceDetectiveType = SourceDetectiveType; + private itShopConfig: ITShopConfig; + private currentUserId: string; + + private readonly subscriptions: Subscription[] = []; + + constructor( + private readonly apiClient: QerApiService, + private readonly metadata: MetadataService, + private readonly loader: EuiLoadingService, + public readonly ldsReplace: LdsReplacePipe, + private readonly translationProvider: ImxTranslationProviderService, + private readonly translator: TranslateService, + private readonly sidesheet: EuiSidesheetService, + private readonly projectConfig: ProjectConfigurationService, + private readonly itshopRequestService: ItshopRequestService, + authentication: AuthenticationService, + ) { + this.subscriptions.push(authentication.onSessionResponse.subscribe((state) => (this.currentUserId = state.UserUid))); + } + + public hasChild = (node: SourceNodeEnriched) => !!node.Children && node.Children.length > 0; + + public async ngOnInit(): Promise { + this.busy = true; + try { + this.itShopConfig = (await this.projectConfig.getConfig()).ITShopConfig; + } finally { + this.busy = false; } + this.reload(); + } - public async openRequestDetail(node: SourceNodeEnriched): Promise { - const uidPwo = DbObjectKey.FromXml(node.ObjectKey).Keys[0]; - - let data: RequestDetailParameter; - const overlay = this.loader.show(); - try { - const collection = await this.apiClient.typedClient.PortalItshopRequests.Get({ - uidpwo: uidPwo - }); - if (collection.Data.length < 0) { - throw new Error(await this.translator.get("#LDS#The request could not be found. You may not have permission to view this request.").toPromise()) - } - const pwoEntity = collection.Data[0]; - - const requestData = new ItshopRequestData({ ...collection.extendedData, index: 0 }); - const parameterColumns = this.itshopRequestService.createParameterColumns( - pwoEntity.GetEntity(), - requestData.parameters - ); - const request = new ItshopRequest(pwoEntity.GetEntity(), requestData.pwoData, parameterColumns, this.currentUserId); - - data = { - isReadOnly: true, - personWantsOrg: request, - itShopConfig: this.itShopConfig, - userUid: this.currentUserId - }; - } finally { - this.loader.hide(overlay); - } - - if (data) { - // We need to find the node with the object display that matches the assignment Display - const parameterizedText = data.personWantsOrg.GetEntity().GetColumn('Assignment').GetDisplayValue(); - const parentNode = this.dataSource.data.find(item => item.Description.value === parameterizedText); - - this.sidesheet.open(RequestDetailComponent, { - title: await this.translator.get('#LDS#Heading View Request Details').toPromise(), - subTitle: parentNode && parentNode.TextTokens ? parentNode.TextTokens.map(token => token.value).join('') : undefined, - testId: 'sourcedetective-request-details-sidesheet', - padding: '0px', - width: 'max(700px, 40%)', - data - }); - } + public ngOnChanges(changes: SimpleChanges): void { + if (changes.UID || changes.TableName || changes.UID_Person) { + this.reload(); } - - public grabText(node: SourceNodeEnriched, textTokens: TextToken[]): void { - // Set the parameter-filled text tokens back into the node - node.TextTokens = textTokens; + } + + public ngOnDestroy(): void { + this.subscriptions.forEach((s) => s.unsubscribe()); + } + + public async openRequestDetail(node: SourceNodeEnriched): Promise { + const uidPwo = DbObjectKey.FromXml(node.ObjectKey).Keys[0]; + + let data: RequestDetailParameter; + const overlay = this.loader.show(); + try { + const collection = await this.apiClient.typedClient.PortalItshopRequests.Get({ + uidpwo: uidPwo, + }); + if (collection.Data.length < 0) { + throw new Error( + await this.translator.get('#LDS#The request could not be found. You may not have permission to view this request.').toPromise(), + ); + } + const pwoEntity = collection.Data[0]; + + const requestData = new ItshopRequestData({ ...collection.extendedData, index: 0 }); + const parameterColumns = this.itshopRequestService.createParameterColumns(pwoEntity.GetEntity(), requestData.parameters); + const request = new ItshopRequest(pwoEntity.GetEntity(), requestData.pwoData, parameterColumns, this.currentUserId); + + data = { + isReadOnly: true, + personWantsOrg: request, + itShopConfig: this.itShopConfig, + userUid: this.currentUserId, + }; + } finally { + this.loader.hide(overlay); } - private isDirectAssignment(node: SourceNode): boolean { - // nodes that cannot be analyzed further - return !node.ObjectKey; + if (data) { + // We need to find the node with the object display that matches the assignment Display + const parameterizedText = data.personWantsOrg.GetEntity().GetColumn('Assignment').GetDisplayValue(); + const parentNode = this.dataSource.data.find((item) => item.Description.value === parameterizedText); + + this.sidesheet.open(RequestDetailComponent, { + title: await this.translator.get('#LDS#Heading View Request Details').toPromise(), + subTitle: parentNode && parentNode.TextTokens ? parentNode.TextTokens.map((token) => token.value).join('') : undefined, + testId: 'sourcedetective-request-details-sidesheet', + padding: '0px', + width: 'max(700px, 40%)', + data, + }); } + } - private isByDynamicGroup(node: SourceNode): boolean { - return node.ObjectType === 'DynamicGroup'; - } + public grabText(node: SourceNodeEnriched, textTokens: TextToken[]): void { + // Set the parameter-filled text tokens back into the node + node.TextTokens = textTokens; + } - private async getObjectTypeDisplay(node: SourceNode): Promise { - if (!node.ObjectType) { return ''; } - await this.metadata.update([node.ObjectType]); + private isDirectAssignment(node: SourceNode): boolean { + // nodes that cannot be analyzed further + return !node.ObjectKey; + } - return this.metadata.tables[node.ObjectType].DisplaySingular; - } + private isByDynamicGroup(node: SourceNode): boolean { + return node.ObjectType === 'DynamicGroup'; + } - private async reload(): Promise { - if (this.UID && this.TableName && this.UID_Person) { - this.dataReady = false; - this.busy = true; - - try { - const data = await this.apiClient.client.portal_detective_get(this.UID_Person, this.TableName, this.UID); - this.dataSource.data = await this.Enrich(data); - } finally { - this.busy = false; - this.dataReady = true; - } - } + private async getObjectTypeDisplay(node: SourceNode): Promise { + if (!node.ObjectType) { + return ''; } - - private async Enrich(nodes: SourceNode[], level: number = 0): Promise { - const result: SourceNodeEnriched[] = []; - for (const node of nodes) { - const children = node.Children ? await this.Enrich(node.Children, level + 1) : []; - result.push({ - ...node, - Level: level, - Description: await this.getParametrizedText(node), - ObjectTypeDisplay: await this.getObjectTypeDisplay(node), - Children: children - }); - } - return result; + await this.metadata.updateNonExisting([node.ObjectType]); + + return this.metadata.tables[node.ObjectType].DisplaySingular; + } + + private async reload(): Promise { + if (this.UID && this.TableName && this.UID_Person) { + this.dataReady = false; + this.busy = true; + + try { + const data = await this.apiClient.client.portal_detective_get(this.UID_Person, this.TableName, this.UID); + this.dataSource.data = await this.Enrich(data); + } finally { + this.busy = false; + this.dataReady = true; + } } - - private async getParametrizedText(node: SourceNode): Promise { - return { - value: await this.GetDescription(node), - marker: { start: '"%', end: '%"' }, - getParameterValue: (columnName: string) => node.ObjectDisplayParameters[columnName] - }; + } + + private async Enrich(nodes: SourceNode[], level: number = 0): Promise { + const result: SourceNodeEnriched[] = []; + for (const node of nodes) { + const children = node.Children ? await this.Enrich(node.Children, level + 1) : []; + result.push({ + ...node, + Level: level, + Description: await this.getParametrizedText(node), + ObjectTypeDisplay: await this.getObjectTypeDisplay(node), + Children: children, + }); } - - private async GetDescription(node: SourceNode): Promise { - const tableName = node.ObjectKey ? DbObjectKey.FromXml(node.ObjectKey).TableName : null; - if (this.isDirectAssignment(node)) { - if ('Person' === tableName) { - return this.translationProvider.Translate('#LDS#The identity is directly assigned to this object.').toPromise(); - } - return this.translationProvider.Translate('#LDS#The entitlement is directly assigned to this object.').toPromise(); - } - else if (this.isByDynamicGroup(node)) { - return this.translationProvider.Translate('#LDS#The identity is a member of the dynamic role.').toPromise(); - } - else if (['Org', 'Locality', 'ProfitCenter', 'Department', 'AERole'].includes(tableName)) { - return this.translationProvider.Translate({ - key: '#LDS#Primary assignment: {0} {1}', - parameters: [await this.getObjectTypeDisplay(node), node.ObjectDisplay] - }).toPromise(); - } - else if ('Person' === tableName) { - return this.translationProvider.Translate('#LDS#The identity is a primary member of this role.').toPromise(); - } - else if ('PersonWantsOrg' === tableName) { - return this.translationProvider.Translate('#LDS#The assignment was made by a request.').toPromise(); - } - return node.ObjectDisplay; + return result; + } + + private async getParametrizedText(node: SourceNode): Promise { + return { + value: await this.GetDescription(node), + marker: { start: '"%', end: '%"' }, + getParameterValue: (columnName: string) => node.ObjectDisplayParameters[columnName], + }; + } + + private async GetDescription(node: SourceNode): Promise { + const tableName = node.ObjectKey ? DbObjectKey.FromXml(node.ObjectKey).TableName : null; + if (this.isDirectAssignment(node)) { + if ('Person' === tableName) { + return this.translationProvider.Translate('#LDS#The identity is directly assigned to this object.').toPromise(); + } + return this.translationProvider.Translate('#LDS#The entitlement is directly assigned to this object.').toPromise(); + } else if (this.isByDynamicGroup(node)) { + return this.translationProvider.Translate('#LDS#The identity is a member of the dynamic role.').toPromise(); + } else if (['Org', 'Locality', 'ProfitCenter', 'Department', 'AERole'].includes(tableName)) { + return this.translationProvider + .Translate({ + key: '#LDS#Primary assignment: {0} {1}', + parameters: [await this.getObjectTypeDisplay(node), node.ObjectDisplay], + }) + .toPromise(); + } else if ('Person' === tableName) { + return this.translationProvider.Translate('#LDS#The identity is a primary member of this role.').toPromise(); + } else if ('PersonWantsOrg' === tableName) { + return this.translationProvider.Translate('#LDS#The assignment was made by a request.').toPromise(); } + return node.ObjectDisplay; + } } diff --git a/imxweb/projects/rmb/.compodocrc.json b/imxweb/projects/rmb/.compodocrc.json index 26d4ada03..865166461 100644 --- a/imxweb/projects/rmb/.compodocrc.json +++ b/imxweb/projects/rmb/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - RMB Library", - "output": "../../documentation/rmb", + "output": "../../documentation/v92/rmb", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/rmb/package.json b/imxweb/projects/rmb/package.json index e1a38012d..57b92335c 100644 --- a/imxweb/projects/rmb/package.json +++ b/imxweb/projects/rmb/package.json @@ -1,6 +1,6 @@ { "name": "rmb", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-rmb" diff --git a/imxweb/projects/rms/.compodocrc.json b/imxweb/projects/rms/.compodocrc.json index ec6cd0f3a..adfe1cd53 100644 --- a/imxweb/projects/rms/.compodocrc.json +++ b/imxweb/projects/rms/.compodocrc.json @@ -5,7 +5,8 @@ "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/rms/package.json b/imxweb/projects/rms/package.json index 99747c5f0..5fd9bf564 100644 --- a/imxweb/projects/rms/package.json +++ b/imxweb/projects/rms/package.json @@ -1,6 +1,6 @@ { "name": "rms", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-rms" diff --git a/imxweb/projects/rps/.compodocrc.json b/imxweb/projects/rps/.compodocrc.json index 88a41fcbe..3515829fd 100644 --- a/imxweb/projects/rps/.compodocrc.json +++ b/imxweb/projects/rps/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - RPS Library", - "output": "../../documentation/rps", + "output": "../../documentation/v92/rps", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/rps/package.json b/imxweb/projects/rps/package.json index 96fb62617..aef856677 100644 --- a/imxweb/projects/rps/package.json +++ b/imxweb/projects/rps/package.json @@ -1,6 +1,6 @@ { "name": "rps", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-rps" diff --git a/imxweb/projects/rps/src/lib/reports/edit-report-sidesheet/edit-report-sidesheet.component.html b/imxweb/projects/rps/src/lib/reports/edit-report-sidesheet/edit-report-sidesheet.component.html index bb40e4605..3ebb14392 100644 --- a/imxweb/projects/rps/src/lib/reports/edit-report-sidesheet/edit-report-sidesheet.component.html +++ b/imxweb/projects/rps/src/lib/reports/edit-report-sidesheet/edit-report-sidesheet.component.html @@ -22,7 +22,7 @@ [colored]="true" type="info"> {{ ldsAllRowsInfoText | translate }} - + diff --git a/imxweb/projects/rps/src/lib/reports/edit-report-sidesheet/edit-report-sidesheet.component.ts b/imxweb/projects/rps/src/lib/reports/edit-report-sidesheet/edit-report-sidesheet.component.ts index 38c0409fc..7f7929157 100644 --- a/imxweb/projects/rps/src/lib/reports/edit-report-sidesheet/edit-report-sidesheet.component.ts +++ b/imxweb/projects/rps/src/lib/reports/edit-report-sidesheet/edit-report-sidesheet.component.ts @@ -88,8 +88,11 @@ export class EditReportSidesheetComponent implements OnInit, OnDestroy { public lastSavedExpression: SqlExpression; public exprHasntChanged = true; - public checkChanges(): void { - this.exprHasntChanged = _.isEqual(this.sqlExpression?.Expression, this.lastSavedExpression); + public checkChanges(expression: SqlExpression): void { + this.exprHasntChanged = _.isEqual(expression, this.lastSavedExpression); + if (!this.exprHasntChanged) { + this.sqlExpression.Expression = expression; + } } @ViewChild(SqlWizardComponent) private sqlwizard: SqlWizardComponent; @@ -143,7 +146,9 @@ export class EditReportSidesheetComponent implements OnInit, OnDestroy { // is it a list report? this.cdrList.push(await this.buildTableCdr()); } - this.cdrList.push(this.data.isReadonly ? new BaseReadonlyCdr(this.report.AvailableTo.Column): new BaseCdr(this.report.AvailableTo.Column)); + this.cdrList.push( + this.data.isReadonly ? new BaseReadonlyCdr(this.report.AvailableTo.Column) : new BaseCdr(this.report.AvailableTo.Column) + ); } public addCdr(control: AbstractControl): void { diff --git a/imxweb/projects/rps/src/lib/subscriptions/report-view-config/report-view-config.component.html b/imxweb/projects/rps/src/lib/subscriptions/report-view-config/report-view-config.component.html index e935fb693..ff638ff7d 100644 --- a/imxweb/projects/rps/src/lib/subscriptions/report-view-config/report-view-config.component.html +++ b/imxweb/projects/rps/src/lib/subscriptions/report-view-config/report-view-config.component.html @@ -20,7 +20,7 @@ diff --git a/imxweb/projects/sac/.compodocrc.json b/imxweb/projects/sac/.compodocrc.json index 37bd25197..a6c3a50eb 100644 --- a/imxweb/projects/sac/.compodocrc.json +++ b/imxweb/projects/sac/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - SAC Library", - "output": "../../documentation/sac", + "output": "../../documentation/v92/sac", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/sac/package.json b/imxweb/projects/sac/package.json index e1a050f62..61cfeef62 100644 --- a/imxweb/projects/sac/package.json +++ b/imxweb/projects/sac/package.json @@ -1,6 +1,6 @@ { "name": "sac", - "version": "9.2.0", + "version": "9.2.1", "private": true, "peerDependencies": { diff --git a/imxweb/projects/tsb/.compodocrc.json b/imxweb/projects/tsb/.compodocrc.json index 98de95d9a..103e340ed 100644 --- a/imxweb/projects/tsb/.compodocrc.json +++ b/imxweb/projects/tsb/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - TSB Library", - "output": "../../documentation/tsb", + "output": "../../documentation/v92/tsb", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/tsb/package.json b/imxweb/projects/tsb/package.json index 1d02e58fa..cb36ff59f 100644 --- a/imxweb/projects/tsb/package.json +++ b/imxweb/projects/tsb/package.json @@ -1,6 +1,6 @@ { "name": "tsb", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-tsb" diff --git a/imxweb/projects/tsb/src/lib/groups/group-sidesheet/group-members/group-members.component.ts b/imxweb/projects/tsb/src/lib/groups/group-sidesheet/group-members/group-members.component.ts index cb9a86736..d38a33033 100644 --- a/imxweb/projects/tsb/src/lib/groups/group-sidesheet/group-members/group-members.component.ts +++ b/imxweb/projects/tsb/src/lib/groups/group-sidesheet/group-members/group-members.component.ts @@ -31,18 +31,8 @@ import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { EuiLoadingService, EuiSidesheetService } from '@elemental-ui/core'; import { TranslateService } from '@ngx-translate/core'; -import { - PortalTargetsystemUnsDirectmembers, - PortalTargetsystemUnsGroupServiceitem, - PortalTargetsystemUnsNestedmembers -} from 'imx-api-tsb'; -import { - CollectionLoadParameters, - EntitySchema, - TypedEntity, - TypedEntityCollectionData, - ValType -} from 'imx-qbm-dbts'; +import { PortalTargetsystemUnsDirectmembers, PortalTargetsystemUnsGroupServiceitem, PortalTargetsystemUnsNestedmembers } from 'imx-api-tsb'; +import { CollectionLoadParameters, EntitySchema, TypedEntity, TypedEntityCollectionData, ValType } from 'imx-qbm-dbts'; import { ConfirmationService, DataSourceToolbarSettings, @@ -50,7 +40,7 @@ import { FkAdvancedPickerComponent, ClientPropertyForTableColumns, SettingsService, - SnackBarService + SnackBarService, } from 'qbm'; import { SourceDetectiveSidesheetComponent, SourceDetectiveSidesheetData, SourceDetectiveType } from 'qer'; import { DbObjectKeyBase } from '../../../target-system/db-object-key-wrapper.interface'; @@ -60,10 +50,9 @@ import { NewMembershipService } from './new-membership/new-membership.service'; @Component({ selector: 'imx-group-members', templateUrl: './group-members.component.html', - styleUrls: ['./group-members.component.scss'] + styleUrls: ['./group-members.component.scss'], }) export class GroupMembersComponent implements OnInit { - @Input() public groupDirectMembershipData: TypedEntityCollectionData; @Input() public groupNestedMembershipData: TypedEntityCollectionData; @Input() public unsGroupDbObjectKey: DbObjectKeyBase; @@ -90,10 +79,11 @@ export class GroupMembersComponent implements OnInit { public readonly itemStatus = { enabled: (item: PortalTargetsystemUnsDirectmembers): boolean => { - return !item.IsFromDynamic?.value - && ((item.UID_PersonWantsOrg.value !== '' && item.IsRequestCancellable.value) - || item.XOrigin.value === 1); - } + return ( + !item.IsFromDynamic?.value && + ((item.UID_PersonWantsOrg.value !== '' && item.IsRequestCancellable.value) || item.XOrigin.value === 1) + ); + }, }; private displayedColumns: ClientPropertyForTableColumns[] = []; @@ -111,7 +101,7 @@ export class GroupMembersComponent implements OnInit { private readonly confirmationService: ConfirmationService, private readonly membershipService: NewMembershipService, private readonly translate: TranslateService, - private readonly settingsService: SettingsService, + private readonly settingsService: SettingsService ) { this.entitySchemaGroupDirectMemberships = groupsService.UnsGroupDirectMembersSchema; this.entitySchemaGroupNestedMemberships = groupsService.UnsGroupNestedMembersSchema; @@ -135,12 +125,12 @@ export class GroupMembersComponent implements OnInit { this.entitySchemaGroupDirectMemberships.Columns.XDateInserted, this.entitySchemaGroupDirectMemberships.Columns.OrderState, this.entitySchemaGroupDirectMemberships.Columns.ValidUntil, - this.entitySchemaGroupDirectMemberships.Columns.XMarkedForDeletion + this.entitySchemaGroupDirectMemberships.Columns.XMarkedForDeletion, ]; this.nestedDisplayColumns = [ this.entitySchemaGroupNestedMemberships.Columns.UID_Person, this.entitySchemaGroupNestedMemberships.Columns.UID_UNSGroupChild, - this.entitySchemaGroupNestedMemberships.Columns.XMarkedForDeletion + this.entitySchemaGroupNestedMemberships.Columns.XMarkedForDeletion, ]; this.groupId = this.unsGroupDbObjectKey.Keys[0]; @@ -157,16 +147,23 @@ export class GroupMembersComponent implements OnInit { } public canUnsubscribeSelected(): boolean { - return this.selectedItems != null - && this.selectedItemsCount > 0 - && this.selectedItems.every(elem => elem.GetEntity().GetColumn('UID_PersonWantsOrg').GetValue() !== '' - && elem.GetEntity().GetColumn('IsRequestCancellable').GetValue()); + return ( + this.selectedItems != null && + this.selectedItemsCount > 0 && + this.selectedItems.every( + (elem) => + elem.GetEntity().GetColumn('UID_PersonWantsOrg').GetValue() !== '' && + elem.GetEntity().GetColumn('IsRequestCancellable').GetValue() + ) + ); } public canDeleteSelected(): boolean { - return this.selectedItems != null - && this.selectedItemsCount > 0 - && this.selectedItems.every(elem => elem.GetEntity().GetColumn('XOrigin').GetValue() === 1); + return ( + this.selectedItems != null && + this.selectedItemsCount > 0 && + this.selectedItems.every((elem) => elem.GetEntity().GetColumn('XOrigin').GetValue() === 1) + ); } public async onToggleChanged(change: MatButtonToggleChange): Promise { @@ -174,8 +171,7 @@ export class GroupMembersComponent implements OnInit { this.selectedItems = []; if (this.selectedMembershipView === 'direct') { return this.onDirectNavigationStateChanged({ PageSize: this.settingsService.DefaultPageSize, StartIndex: 0 }); - } - else { + } else { return this.onNestedNavigationStateChanged({ PageSize: this.settingsService.DefaultPageSize, StartIndex: 0 }); } } @@ -195,16 +191,19 @@ export class GroupMembersComponent implements OnInit { } public async deleteMembers(): Promise { - if (await this.confirmationService.confirm({ - Title: '#LDS#Heading Remove Memberships', - Message: '#LDS#The removal of memberships may take some time. The displayed data may differ from the actual state. Are you sure you want to remove the selected memberships?', - identifier: 'group-members-confirm-delete-memberships' - })) { + if ( + await this.confirmationService.confirm({ + Title: '#LDS#Heading Remove Memberships', + Message: + '#LDS#The removal of memberships may take some time. The displayed data may differ from the actual state. Are you sure you want to remove the selected memberships?', + identifier: 'group-members-confirm-delete-memberships', + }) + ) { this.handleOpenLoader(); try { await this.groupsService.deleteGroupMembers( this.unsGroupDbObjectKey, - this.selectedItems.map(i => i.GetEntity().GetColumn('UID_UNSAccount').GetValue()) + this.selectedItems.map((i) => i.GetEntity().GetColumn('UID_UNSAccount').GetValue()) ); this.membersTable.clearSelection(); this.snackBarService.open({ key: '#LDS#The memberships have been successfully removed.' }, '#LDS#Close'); @@ -225,13 +224,13 @@ export class GroupMembersComponent implements OnInit { testId: 'systementitlements-reqeust-memberships', data: { fkRelations: this.membershipService.getFKRelation(), - isMultiValue: true - } + isMultiValue: true, + }, }); const result = await sidesheetRef.afterClosed().toPromise(); - if (result && result.candidates.length > 0 && await this.membershipService.requestMembership(result.candidates, serviceItem)) { + if (result && result.candidates.length > 0 && (await this.membershipService.requestMembership(result.candidates, serviceItem))) { this.snackBarService.open({ key: '#LDS#The memberships for "{0}" have been successfully added to the shopping cart.', parameters: [serviceItem.GetEntity().GetDisplay()], @@ -244,23 +243,26 @@ export class GroupMembersComponent implements OnInit { } public async unsubscribeMembership(): Promise { - // if there is at least 1 item, that is not unsubscribable, show a warning instead - const notSubscribable = this.selectedItems.some(entity => entity.GetEntity().GetColumn('IsRequestCancellable').GetValue() === false); + const notSubscribable = this.selectedItems.some((entity) => entity.GetEntity().GetColumn('IsRequestCancellable').GetValue() === false); if (notSubscribable) { this.showUnsubscribeWarning = true; this.membersTable.clearSelection(); return; } - if (await this.confirmationService.confirm({ - Title: '#LDS#Heading Unsubscribe Memberships', - Message: '#LDS#Are you sure you want to unsubscribe the selected memberships?', - identifier: 'group-members-confirm-unsubscribe-membership' - })) { + if ( + await this.confirmationService.confirm({ + Title: '#LDS#Heading Unsubscribe Memberships', + Message: '#LDS#Are you sure you want to unsubscribe the selected memberships?', + identifier: 'group-members-confirm-unsubscribe-membership', + }) + ) { this.handleOpenLoader(); try { - await Promise.all(this.selectedItems.map((entity => this.membershipService.unsubscribeMembership(entity)))); - this.snackBarService.open({ key: '#LDS#The memberships have been successfully unsubscribed. It may take some time for the changes to take effect. The displayed data may differ from the actual state.' }); + await Promise.all(this.selectedItems.map((entity) => this.membershipService.unsubscribeMembership(entity))); + this.snackBarService.open({ + key: '#LDS#The memberships have been successfully unsubscribed. It may take some time for the changes to take effect. The displayed data may differ from the actual state.', + }); } finally { this.handleCloseLoader(); this.membersTable.clearSelection(); @@ -269,16 +271,14 @@ export class GroupMembersComponent implements OnInit { } } - public async onShowDetails(item: PortalTargetsystemUnsDirectmembers): Promise { - const uidPerson = item.UID_Person.value; const data: SourceDetectiveSidesheetData = { UID_Person: uidPerson, Type: SourceDetectiveType.MembershipOfSystemEntitlement, UID: this.unsGroupDbObjectKey.Keys[0], - TableName: this.unsGroupDbObjectKey.TableName + TableName: this.unsGroupDbObjectKey.TableName, }; this.sidesheet.open(SourceDetectiveSidesheetComponent, { title: await this.translate.get('#LDS#Heading View Assignment Analysis').toPromise(), @@ -299,7 +299,7 @@ export class GroupMembersComponent implements OnInit { displayedColumns: this.displayedColumns, dataSource: this.groupDirectMembershipData, entitySchema: this.entitySchemaGroupDirectMemberships, - navigationState: this.navigationState + navigationState: this.navigationState, }; } finally { this.handleCloseLoader(); @@ -310,12 +310,12 @@ export class GroupMembersComponent implements OnInit { this.showUnsubscribeWarning = false; this.handleOpenLoader(); try { - this.groupNestedMembershipData = await this.groupsService.getGroupNestedMembers(this.groupId, this.navigationState); + this.groupNestedMembershipData = await this.groupsService.getGroupNestedMembers(this.groupId, this.nestedNavigationState); this.dstNestedGroupSettings = { displayedColumns: this.nestedDisplayColumns, dataSource: this.groupNestedMembershipData, entitySchema: this.entitySchemaGroupNestedMemberships, - navigationState: this.nestedNavigationState + navigationState: this.nestedNavigationState, }; } finally { this.handleCloseLoader(); @@ -337,13 +337,15 @@ export class GroupMembersComponent implements OnInit { } } - public LdsNotUnsubscribableHint = "#LDS#There is at least one membership you cannot unsubscribe. You can only unsubscribe memberships you have requested."; + public LdsNotUnsubscribableHint = + '#LDS#There is at least one membership you cannot unsubscribe. You can only unsubscribe memberships you have requested.'; - public LdsDirectlyAssignedHint = "#LDS#Here you can get an overview of members assigned to the system entitlement itself."; + public LdsDirectlyAssignedHint = '#LDS#Here you can get an overview of members assigned to the system entitlement itself.'; - public LdsIndirectlyAssignedHint = "#LDS#Here you can get an overview of members not assigned to the system entitlement itself, but to a child system entitlement."; + public LdsIndirectlyAssignedHint = + '#LDS#Here you can get an overview of members not assigned to the system entitlement itself, but to a child system entitlement.'; - public LdsDirectlyAssigned = "#LDS#Direct memberships"; + public LdsDirectlyAssigned = '#LDS#Direct memberships'; - public LdsIndirectlyAssigned = "#LDS#Inherited memberships"; + public LdsIndirectlyAssigned = '#LDS#Inherited memberships'; } diff --git a/imxweb/projects/uci/.compodocrc.json b/imxweb/projects/uci/.compodocrc.json index 8b426e6d7..299ddc43c 100644 --- a/imxweb/projects/uci/.compodocrc.json +++ b/imxweb/projects/uci/.compodocrc.json @@ -1,11 +1,12 @@ { "name": "IMX Web - UCI Library", - "output": "../../documentation/tsb", + "output": "../../documentation/v92/tsb", "theme": "material", "assetsFolder": "../../compodoc/assets", "customLogo": "../../compodoc/assets/images/oneidentity-logo.png", "customFavicon": "../../compodoc/assets/images/favicon.ico", - "serve": true, - "watch": true, - "coverageTest": 0 + "serve": false, + "watch": false, + "coverageTest": 0, + "disableCoverage": true } diff --git a/imxweb/projects/uci/package.json b/imxweb/projects/uci/package.json index 4b987196f..07b309c06 100644 --- a/imxweb/projects/uci/package.json +++ b/imxweb/projects/uci/package.json @@ -1,6 +1,6 @@ { "name": "uci", - "version": "9.2.0", + "version": "9.2.1", "private": true, "bundledDependencies": [ "imx-api-uci"