├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── dev ├── Component.ts ├── control │ ├── CustomizationPanel.generated.ts │ ├── CustomizationPanel.ts │ ├── EntityFilterPanel.generated.ts │ ├── EntityFilterPanel.ts │ ├── QuickFilter.generated.ts │ ├── QuickFilter.ts │ ├── SideFilterPanel.generated.ts │ ├── SideFilterPanel.ts │ ├── ToggleableSideContent.generated.ts │ ├── ToggleableSideContent.ts │ └── ToggleableSideContentRenderer.ts ├── controller │ ├── BaseController.ts │ ├── Entity.controller.ts │ └── MainPage.controller.ts ├── css │ ├── addFilterPopover.less │ ├── base.less │ ├── customizationPanel.less │ ├── entityFilterPanel.less │ ├── filterPopover.less │ ├── main.less │ ├── quickFilter.less │ ├── sideFilterPanel.less │ └── toggleableSideContent.less ├── element │ ├── ColumnItem.ts │ └── FilterItem.ts ├── fragment │ ├── AddFilterPopover.fragment.xml │ ├── EntitySettingsDialog.fragment.xml │ └── MainTableItemsTemplate.fragment.xml ├── helper │ ├── EntityTableSettings.ts │ ├── FormatUtil.ts │ ├── I18nUtil.ts │ ├── ResizeAdapter.ts │ ├── filter │ │ ├── AddQuickFilterPopover.ts │ │ ├── FilterCreator.ts │ │ └── FilterOperatorConfigurations.ts │ ├── systemUtil.ts │ ├── valuehelp │ │ ├── ValueHelpDialog.ts │ │ ├── ValueHelpFactory.ts │ │ ├── ValueHelpModel.ts │ │ └── ValueHelpUtil.ts │ └── variants │ │ └── SmartVariantManagementConnector.ts ├── i18n │ ├── i18n.properties │ ├── i18n_de.properties │ └── i18n_en.properties ├── img │ └── favicon.png ├── localService │ ├── MockServer.ts │ └── mockdata │ │ ├── datapreview.json │ │ ├── dbentities.json │ │ ├── entityMetadata.json │ │ ├── entityvariants.json │ │ ├── valueHelpData.json │ │ └── valueHelpMetadata.json ├── manifest.json ├── model │ ├── AjaxJSONModel.ts │ ├── AjaxListBinding.ts │ ├── Entity.ts │ ├── TypeFactory.ts │ ├── TypeUtil.ts │ ├── formatter.ts │ ├── globalConsts.ts │ ├── models.ts │ └── types.ts ├── service │ ├── EntityService.ts │ ├── ValueHelpService.ts │ └── ajaxUtil.ts ├── state │ ├── BaseState.ts │ ├── EntityState.ts │ └── StateRegistry.ts ├── test │ ├── changes_loader.js │ ├── fakeLRep.json │ ├── flpSandbox.html │ └── flpSandboxMockServer.html └── view │ ├── App.view.xml │ ├── Entity.view.xml │ └── MainPage.view.xml ├── img └── entity_page_sample.png ├── package.json ├── tsconfig.json ├── types ├── global.d.ts ├── sap.ui.comp.d.ts ├── sap.ui.core.d.ts └── sap.ui.table.d.ts ├── ui5-dist.yaml ├── ui5-mock.yaml └── ui5.yaml /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["**/*.d.ts"], 3 | "presets": ["transform-ui5", "@babel/preset-typescript", "@babel/preset-env"], 4 | "plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-nullish-coalescing-operator"] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules/ 3 | dist/ 4 | doc/ 5 | reports/ 6 | changes_preview.js 7 | lib/ 8 | webapp/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 11 4 | }, 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "es6": true 9 | }, 10 | "globals": { 11 | "sap": true 12 | }, 13 | "extends": [ 14 | "prettier", 15 | "eslint:recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:@typescript-eslint/eslint-recommended" 18 | ], 19 | "plugins": ["prettier", "@typescript-eslint/eslint-plugin"], 20 | "parser": "@typescript-eslint/parser", 21 | "rules": { 22 | "@typescript-eslint/no-explicit-any": "off", 23 | "@typescript-eslint/ban-types": "off", 24 | "@typescript-eslint/ban-ts-comment": "off", 25 | "no-prototype-builtins": "off", 26 | "prettier/prettier": ["error"] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | .temp 14 | 15 | # Stores environment variables 16 | .env 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # ignore output folders 62 | dist/ 63 | doc/ 64 | reports/ 65 | webapp/ 66 | 67 | # ignore settings of IDE's 68 | .idea 69 | .vscode 70 | .project 71 | 72 | *.zip 73 | 74 | package-lock.json 75 | 76 | # section for Web IDE 77 | UIAdaptation_index.html 78 | changes_preview.js 79 | sap-ui-cachebuster-info.json 80 | extended_runnable_file.html 81 | .*/extended_runnable_file.html 82 | mock_preview_sapui5.html 83 | .*/mock_preview_sapui5.html 84 | fioriHtmlRunner.html 85 | .*/fioriHtmlRunner.html 86 | visual_ext_index.html 87 | /webapp/visual_ext_index.html 88 | .project 89 | UIAdaptation_index.html 90 | AppVariant_index.html 91 | AppVariantPreviewPayload.zip 92 | mergedManifestDescriptor.json 93 | APIExternalProducer.js 94 | .*/APIExternalProducer.js 95 | mta_archives/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "printWidth": 120, 4 | "semi": true, 5 | "trailingComma": "none", 6 | "arrowParens": "avoid", 7 | "singleQuote": false, 8 | "endOfLine": "lf", 9 | "overrides": [ 10 | { 11 | "files": ["package.json", "ui5*.yaml", ".eslint*"], 12 | "options": { 13 | "tabWidth": 2 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ludwig Stockbauer-Muhr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quick-data-reporter 2 | 3 | UI5 App for analyzing content of ABAP Database tables/Database views or CDS Views 4 | 5 | ![Entity Page for Table DD03L](img/entity_page_sample.png) 6 | 7 | ## Installation 8 | 9 | ```sh 10 | npm i 11 | ``` 12 | 13 | Install UI5 tooling globally 14 | 15 | ```sh 16 | npm i -g @ui5/cli 17 | ``` 18 | 19 | ## Development Server 20 | 21 | ### `.env` File Template 22 | 23 | ```env 24 | # Proxy to REST service (SICF path /sap/zqdrtrest) 25 | HTTP_PROXY_AUTH_USER= 26 | HTTP_PROXY_AUTH_PASS= 27 | HTTP_PROXY_TARGET= 28 | 29 | # Deployment on OnPrem System 30 | UI5_TASK_NWABAP_DEPLOYER__USER= 31 | UI5_TASK_NWABAP_DEPLOYER__PASSWORD= 32 | UI5_TASK_NWABAP_DEPLOYER__SERVER= 33 | UI5_TASK_NWABAP_DEPLOYER__CLIENT= 34 | ``` 35 | 36 | ### Script commands 37 | 38 | - With real data 39 | 40 | ```sh 41 | npm start 42 | ``` 43 | 44 | - With mock data 45 | 46 | ```sh 47 | npm run start:mock 48 | ``` 49 | 50 | ## Deployment 51 | 52 | ```sh 53 | npm run deploy 54 | ``` 55 | 56 | ## Dependencies 57 | 58 | If the app is running in production mode the following [abapGit](https://github.com/abapGit/abapGit) Repositories are required to be installed on the ABAP system 59 | 60 | - [abap-qdrt](https://github.com/DevEpos/abap-qdrt) 61 | -------------------------------------------------------------------------------- /dev/Component.ts: -------------------------------------------------------------------------------- 1 | import UIComponent from "sap/ui/core/UIComponent"; 2 | import models from "com/devepos/qdrt/model/models"; 3 | import ResourceBundle from "sap/base/i18n/ResourceBundle"; 4 | import ResourceModel from "sap/ui/model/resource/ResourceModel"; 5 | import { setResourceBundle } from "./helper/I18nUtil"; 6 | 7 | /** 8 | * Component for the Quick Data Reporter 9 | * @namespace com.devepos.qdrt 10 | */ 11 | export default class QdrtComponent extends UIComponent { 12 | _bundle: ResourceBundle; 13 | metadata = { 14 | manifest: "json" 15 | }; 16 | 17 | init(): void { 18 | // call the base component's init function 19 | // eslint-disable-next-line prefer-rest-params 20 | super.init.apply(this, arguments as any); 21 | // set the device model 22 | this.setModel(models.createDeviceModel(), "device"); 23 | // create the views based on the url/hash 24 | this.getRouter().initialize(); 25 | // globally register the resource bundle of the application 26 | setResourceBundle(this.getResourceBundle()); 27 | } 28 | 29 | /** 30 | * Returns the i18n bundle 31 | * @returns the i18n resource bundle 32 | */ 33 | getResourceBundle(): ResourceBundle { 34 | if (!this._bundle) { 35 | this._bundle = (this.getModel("i18n") as ResourceModel)?.getResourceBundle() as ResourceBundle; 36 | } 37 | return this._bundle; 38 | } 39 | 40 | destroy(supressInvalidate?: boolean): void { 41 | super.destroy.apply(this, [supressInvalidate]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /dev/control/CustomizationPanel.generated.ts: -------------------------------------------------------------------------------- 1 | import Control, { $ControlSettings } from "sap/ui/core/Control"; 2 | import { URI } from "sap/ui/core/library"; 3 | 4 | declare module "./CustomizationPanel" { 5 | interface $CustomizationPanelSetting extends $ControlSettings { 6 | title?: string; 7 | icon?: URI; 8 | content?: Control[]; 9 | } 10 | 11 | export default interface CustomizationPanel { 12 | getTitle(): string; 13 | setTitle(title: string): this; 14 | getIcon(): URI; 15 | setIcon(icon: URI): this; 16 | getContent(): Control[]; 17 | setContent(content: Control[]): this; 18 | addContent(content: Control): this; 19 | removeAllContent(): this; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /dev/control/CustomizationPanel.ts: -------------------------------------------------------------------------------- 1 | import FlexBox from "sap/m/FlexBox"; 2 | import { FlexAlignItems, FlexJustifyContent } from "sap/m/library"; 3 | import Control from "sap/ui/core/Control"; 4 | import Icon from "sap/ui/core/Icon"; 5 | import Title from "sap/m/Title"; 6 | import RenderManager from "sap/ui/core/RenderManager"; 7 | 8 | /** 9 | * Panel for customization like Column settings, Filter criteria, etc. 10 | * @namespace com.devepos.qdrt.control 11 | */ 12 | export default class CustomizationPanel extends Control { 13 | metadata = { 14 | properties: { 15 | title: { type: "String", group: "Misc" }, 16 | icon: { type: "sap.ui.core.URI", group: "Appearance" } 17 | }, 18 | defaultAggregation: "content", 19 | aggregations: { 20 | content: { type: "sap.ui.core.Control", multiple: true, singularName: "content" } 21 | }, 22 | events: {} 23 | }; 24 | renderer = { 25 | apiVersion: 2, 26 | 27 | /** 28 | * Renders the HTML for the given control, using the provided 29 | * {@link sap.ui.core.RenderManager}. 30 | * 31 | * @param rm the RenderManager that can be used for writing to the Render-Output-Buffer 32 | * @param panel the side customziation panel to render 33 | */ 34 | render(rm: RenderManager, panel: CustomizationPanel): void { 35 | // create a wrapper element with some styling 36 | rm.openStart("div", panel); 37 | rm.class("deveposQdrt-CustomizationPanel"); 38 | rm.openEnd(); 39 | 40 | // render the header 41 | rm.openStart("header", panel.getId() + "-header"); 42 | rm.openEnd(); 43 | rm.renderControl(panel.getHeader()); 44 | rm.close("header"); 45 | 46 | const contentItems = panel.getContent(); 47 | if (contentItems?.length > 0) { 48 | rm.openStart("div", panel.getId() + "-content"); 49 | rm.openEnd(); 50 | // render content aggregation 51 | for (const contentItem of contentItems) { 52 | if (contentItem.getVisible()) { 53 | rm.renderControl(contentItem); 54 | } 55 | } 56 | rm.close("div"); 57 | } 58 | rm.close("div"); 59 | } 60 | }; 61 | private _icon: Icon; 62 | private _titleControl: Title; 63 | private _headerBox: FlexBox; 64 | 65 | constructor(idOrSettings?: string | $CustomizationPanelSetting); 66 | constructor(id?: string, settings?: $CustomizationPanelSetting); 67 | constructor(id?: string, settings?: $CustomizationPanelSetting) { 68 | super(id, settings); 69 | } 70 | 71 | getHeader(): FlexBox { 72 | if (this._headerBox) { 73 | // update the icon 74 | this._getIconControl(); 75 | } else { 76 | this._headerBox = new FlexBox({ 77 | alignItems: FlexAlignItems.Center, 78 | justifyContent: FlexJustifyContent.Start, 79 | items: [this._getIconControl(), this._getTitleControl()] 80 | }); 81 | } 82 | return this._headerBox; 83 | } 84 | 85 | exit(): void { 86 | // it is enough to dispose of the header box as the icon and title are aggregations 87 | // of the box 88 | if (this._headerBox) { 89 | this._headerBox.destroy(); 90 | } 91 | } 92 | /** 93 | * Lazily creates the icon control 94 | * @returns the control for the icon URI 95 | */ 96 | private _getIconControl(): Icon { 97 | if (this._icon) { 98 | this._icon.setSrc(this.getIcon()); 99 | } else { 100 | this._icon = new Icon({ 101 | src: this.getIcon() 102 | }); 103 | } 104 | return this._icon; 105 | } 106 | 107 | /** 108 | * Lazily creates the icon control 109 | * @returns the title control to hold the title 110 | */ 111 | private _getTitleControl(): Title { 112 | if (this._titleControl) { 113 | this._titleControl.setText(this.getTitle()); 114 | } else { 115 | this._titleControl = new Title({ 116 | text: this.getTitle(), 117 | titleStyle: "H4" 118 | }); 119 | this._titleControl.addStyleClass("deveposQdrt-CustomizationPanel__HeaderTitle"); 120 | } 121 | return this._titleControl; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /dev/control/EntityFilterPanel.generated.ts: -------------------------------------------------------------------------------- 1 | import { $ControlSettings } from "sap/ui/core/Control"; 2 | import SideFilterPanel from "./SideFilterPanel"; 3 | 4 | declare module "./EntityFilterPanel" { 5 | interface $EntityFilterPanelSettings extends $ControlSettings { 6 | filterPanel: SideFilterPanel; 7 | parameterPanal: SideFilterPanel; 8 | } 9 | 10 | export default interface EntityFilterPanel { 11 | getFilterPanel(): SideFilterPanel; 12 | getParameterPanel(): SideFilterPanel; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /dev/control/EntityFilterPanel.ts: -------------------------------------------------------------------------------- 1 | import ResizeAdapter from "../helper/ResizeAdapter"; 2 | 3 | import Control from "sap/ui/core/Control"; 4 | import RenderManager from "sap/ui/core/RenderManager"; 5 | 6 | /** 7 | * Entity filter panel which holds a panel for the field filters 8 | * and an optional panel for parameters (e.g. from a CDS view) 9 | * 10 | * @namespace com.devepos.qdrt.control 11 | */ 12 | export default class EntityFilterPanel extends Control { 13 | metadata = { 14 | properties: {}, 15 | aggregations: { 16 | filterPanel: { 17 | type: "com.devepos.qdrt.control.SideFilterPanel", 18 | multiple: false, 19 | singularName: "filterPanel" 20 | }, 21 | parameterPanel: { 22 | type: "com.devepos.qdrt.control.SideFilterPanel", 23 | multiple: false, 24 | singularName: "parameterPanel" 25 | } 26 | } 27 | }; 28 | renderer = { 29 | apiVersion: 2, 30 | 31 | /** 32 | * Renders the HTML for the given control, using the provided 33 | * {@link sap.ui.core.RenderManager}. 34 | * 35 | * @param rm the RenderManager that can be used for writing to the Render-Output-Buffer 36 | * @param panel the side customziation panel to render 37 | */ 38 | render(rm: RenderManager, panel: EntityFilterPanel): void { 39 | // create a wrapper element with some styling 40 | rm.openStart("div", panel); 41 | rm.class("deveposQdrt-EntityFilterPanel"); 42 | rm.openEnd(); 43 | 44 | const parameterPanel = panel.getParameterPanel(); 45 | const filterPanel = panel.getFilterPanel(); 46 | if (parameterPanel && parameterPanel.getVisible()) { 47 | rm.renderControl(parameterPanel); 48 | } 49 | if (filterPanel) { 50 | rm.renderControl(filterPanel); 51 | } 52 | rm.close("div"); 53 | } 54 | }; 55 | private _resizeAdapter: ResizeAdapter; 56 | 57 | constructor(idOrSettings?: string | $EntityFilterPanelSettings); 58 | constructor(id?: string, settings?: $EntityFilterPanelSettings); 59 | constructor(id?: string, settings?: $EntityFilterPanelSettings) { 60 | super(id, settings); 61 | } 62 | 63 | onBeforeRendering(): void { 64 | if (!this._resizeAdapter && this.getParameterPanel()) { 65 | this._resizeAdapter = new ResizeAdapter(this.getParameterPanel(), this._onParamPanelResize.bind(this)); 66 | } 67 | } 68 | onAfterRendering(): void { 69 | if (this._resizeAdapter && !this._resizeAdapter.isResizeInitialized()) { 70 | this._resizeAdapter.initializeResize(); 71 | } 72 | } 73 | exit(): void { 74 | if (this._resizeAdapter) { 75 | this._resizeAdapter.destroy(); 76 | } 77 | } 78 | private _onParamPanelResize() { 79 | const domRef = this.getFilterPanel()?.getDomRef() as HTMLElement; 80 | if (!domRef) { 81 | return; 82 | } 83 | const oldCSSHeight = domRef.style.height; 84 | 85 | const newCSSHeight = `calc(${this.getFilterPanel().getHeight()} - ${domRef.offsetTop}px)`; 86 | 87 | if (newCSSHeight !== oldCSSHeight) { 88 | domRef.style.height = newCSSHeight; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /dev/control/QuickFilter.generated.ts: -------------------------------------------------------------------------------- 1 | import { $ControlSettings } from "sap/ui/core/Control"; 2 | import { FieldFilter, FieldMetadata, ValueHelpType } from "../model/types"; 3 | 4 | declare module "./QuickFilter" { 5 | interface $QuickFilterSettings extends $ControlSettings { 6 | /** 7 | * The name of the column 8 | */ 9 | columnName?: string; 10 | /** 11 | * Label of the column 12 | */ 13 | label?: string; 14 | /** 15 | * Data type of the column 16 | */ 17 | type?: string; 18 | /** 19 | * Holds the actual entered value/ranges of the filter 20 | */ 21 | filterData?: string; 22 | /** 23 | * Gets fired if value help request is triggered on filter control 24 | */ 25 | valueHelpRequest?: Function; 26 | /** 27 | * Event handler for the remove event. 28 | */ 29 | remove?: Function; 30 | /** 31 | * Controls whether only a single value can be entered in the filter 32 | */ 33 | singleValueOnly?: boolean; 34 | /** 35 | * Controls whether the filter can be deleted 36 | */ 37 | deletable?: boolean; 38 | /** 39 | * Controls whether a value is required 40 | */ 41 | required?: boolean; 42 | /** 43 | * Flag whether or not there is value help defined for the column 44 | */ 45 | hasValueHelp?: boolean; 46 | /** 47 | * The type of the value help of the column if one is defined 48 | */ 49 | valueHelpType?: ValueHelpType; 50 | /** 51 | * Metadata of the reference field which provides additional information 52 | */ 53 | referenceFieldMetadata?: FieldMetadata | string; 54 | } 55 | 56 | export default interface QuickFilter { 57 | // properties 58 | getType(): string; 59 | setType(type: string): this; 60 | getLabel(): string; 61 | setLabel(label: string): this; 62 | getColumnName(): string; 63 | setColumnName(columnName: string): this; 64 | getFilterData(): FieldFilter; 65 | setFilterData(filterData: FieldFilter): this; 66 | getSingleValueOnly(): boolean; 67 | setSingleValueOnly(singleValueOnly: boolean): this; 68 | getDeletable(): boolean; 69 | setDeletable(deletable: boolean): this; 70 | getRequired(): boolean; 71 | setRequired(required: boolean): this; 72 | getHasValueHelp(): boolean; 73 | setHasValueHelp(hasValueHelp: boolean): this; 74 | getValueHelpType(): ValueHelpType; 75 | setValueHelpType(valueHelpType: ValueHelpType): this; 76 | getReferenceFieldMetadata(): FieldMetadata; 77 | setReferenceFieldMetadata(referenceFieldMetadata: FieldMetadata): this; 78 | // Events 79 | attachValueHelpRequest(data: object, callback: Function, listener?: object): this; 80 | attachValueHelpRequest(callback: Function, listener?: object): this; 81 | detachValueHelpRequest(callback: Function, listener?: object): this; 82 | fireValueHelpRequest(): this; 83 | attachRemove(data: object, callback: Function, listener?: object): this; 84 | attachRemove(callback: Function, listener?: object): this; 85 | detachRemove(callback: Function, listener?: object): this; 86 | fireRemove(): this; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /dev/control/SideFilterPanel.generated.ts: -------------------------------------------------------------------------------- 1 | import { $ControlSettings } from "sap/ui/core/Control"; 2 | import { TableFilters } from "../model/Entity"; 3 | import { FieldMetadata } from "../model/types"; 4 | 5 | declare module "./SideFilterPanel" { 6 | interface $SideFilterPanelSettings extends $ControlSettings { 7 | availableFilterMetadata?: object; 8 | visibleFilters?: object; 9 | filterCategory?: string; 10 | } 11 | 12 | export default interface SideFilterPanel { 13 | getAvailableFilterMetadata(): FieldMetadata[]; 14 | setAvailableFilterMetadata(availableFilterMetadata: FieldMetadata[]): this; 15 | getVisibleFilters(): TableFilters; 16 | setVisibleFilters(visibleFilters: TableFilters): this; 17 | getFilterCategory(): FilterCategory; 18 | setFilterCategory(filterCategory: FilterCategory): this; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /dev/control/ToggleableSideContent.generated.ts: -------------------------------------------------------------------------------- 1 | import Control, { $ControlSettings } from "sap/ui/core/Control"; 2 | import { CSSSize } from "sap/ui/core/library"; 3 | import { SideContentPosition } from "sap/ui/layout/library"; 4 | 5 | declare module "./ToggleableSideContent" { 6 | interface $ToggleableSideContentSettings extends $ControlSettings { 7 | sideContentVisible?: boolean; 8 | sideContentWidth?: CSSSize; 9 | sideContentPosition?: SideContentPosition; 10 | content?: Control; 11 | sideContent?: Control[]; 12 | } 13 | 14 | export default interface ToggleableSideContent { 15 | getSideContentVisible(): boolean; 16 | setSideContentVisible(sideContentVisible: boolean): this; 17 | getSideContentWidth(): CSSSize; 18 | setSideContentWidth(sideContentWidth: CSSSize): this; 19 | getSideContentPosition(): SideContentPosition; 20 | setSideContentPosition(sideContentPosition: SideContentPosition): this; 21 | getContent(): Control; 22 | setContent(content: Control): this; 23 | getSideContent(): Control[]; 24 | removeAllSideContent(): Control[]; 25 | destroySideContent(): this; 26 | indexOfSideContent(sideContent: Control): number; 27 | addSideContent(sideContent: Control): this; 28 | insertSideContent(sideContent: Control, index: number): this; 29 | removeSideContent(sideContent: number | string | Control): Control; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dev/control/ToggleableSideContent.ts: -------------------------------------------------------------------------------- 1 | import Control from "sap/ui/core/Control"; 2 | 3 | /** 4 | * Control with a main part and a toggleable side content. 5 | * 6 | * @namespace com.devepos.qdrt.control 7 | */ 8 | export default class ToggleableSideContent extends Control { 9 | metadata = { 10 | properties: { 11 | /** 12 | * Defines if the side content view will be shown or not 13 | */ 14 | sideContentVisible: { type: "boolean", group: "Misc", defaultValue: true }, 15 | /** 16 | * Width of the side content 17 | */ 18 | sideContentWidth: { type: "sap.ui.core.CSSSize", group: "Appearance", defaultValue: "420px" }, 19 | /** 20 | * Position of the side content. The default position is on the right side 21 | */ 22 | sideContentPosition: { 23 | type: "sap.ui.layout.SideContentPosition", 24 | group: "Appearance", 25 | defaultValue: "End" 26 | } 27 | }, 28 | defaultAggregation: "content", 29 | aggregations: { 30 | /** 31 | * Main content which normally should hold a table control like 32 | * {@link sap.m.Table} 33 | */ 34 | content: { type: "sap.ui.core.Control", multiple: false, singularName: "content" }, 35 | /** 36 | * The side control 37 | */ 38 | sideContent: { type: "sap.ui.core.Control", multiple: true, singularName: "sideContent" } 39 | }, 40 | events: {} 41 | }; 42 | 43 | constructor(idOrSettings?: string | $ToggleableSideContentSettings); 44 | constructor(id?: string, settings?: $ToggleableSideContentSettings); 45 | constructor(id?: string, settings?: $ToggleableSideContentSettings) { 46 | super(id, settings); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /dev/control/ToggleableSideContentRenderer.ts: -------------------------------------------------------------------------------- 1 | import RenderManager from "sap/ui/core/RenderManager"; 2 | import { SideContentPosition } from "sap/ui/layout/library"; 3 | import ToggleableSideContent from "./ToggleableSideContent"; 4 | 5 | /** 6 | * Renderer for the {@link com.devepos.qdrt.control.ToggleableSideContent} control 7 | */ 8 | class ToggleableSideContentRenderer { 9 | apiVersion = 2; 10 | 11 | /** 12 | * Renders the HTML for the given control, using the provided 13 | * {@link sap.ui.core.RenderManager}. 14 | * 15 | * @param rm the RenderManager that can be used for writing to the Render-Output-Buffer 16 | * @param toggleableSideContent the toggleable side content 17 | */ 18 | render(rm: RenderManager, toggleableSideContent: ToggleableSideContent) { 19 | // open main control 20 | rm.openStart("div", toggleableSideContent); 21 | rm.class("deveposQdrt-ToggleableSideContent"); 22 | rm.openEnd(); 23 | 24 | let sideContentWidth; 25 | let contentWidth = ""; 26 | let sideContentPosition = toggleableSideContent.getSideContentPosition(); 27 | if (!sideContentPosition) { 28 | sideContentPosition = SideContentPosition.End; 29 | } 30 | const sideContentVisible = toggleableSideContent.getSideContentVisible(); 31 | 32 | if (!sideContentVisible) { 33 | contentWidth = "100%"; 34 | } else { 35 | // calculate width of content and side filter 36 | sideContentWidth = toggleableSideContent.getSideContentWidth(); 37 | if (!sideContentWidth) { 38 | // reset to default size 39 | sideContentWidth = "450px"; 40 | } 41 | 42 | if (sideContentWidth.endsWith("%")) { 43 | const filterWidthNumeric = sideContentWidth.match(/(\d+)%/)[1]; 44 | contentWidth = `${100 - (filterWidthNumeric as unknown as number)}%`; 45 | } else { 46 | contentWidth = `calc(100% - ${sideContentWidth})`; 47 | } 48 | } 49 | 50 | if (sideContentPosition === SideContentPosition.Begin) { 51 | this._renderSideContent(rm, toggleableSideContent, sideContentWidth, sideContentVisible); 52 | this._renderMain(rm, toggleableSideContent, contentWidth); 53 | } else { 54 | this._renderMain(rm, toggleableSideContent, contentWidth); 55 | this._renderSideContent(rm, toggleableSideContent, sideContentWidth, sideContentVisible); 56 | } 57 | 58 | // close main control 59 | rm.close("div"); 60 | } 61 | 62 | /** 63 | * Renders the main content 64 | */ 65 | private _renderMain(rm: RenderManager, toggleableSideContent: ToggleableSideContent, width: string) { 66 | // open main content 67 | rm.openStart("section", toggleableSideContent.getId() + "-content"); 68 | rm.class("deveposQdrt-ToggleableSideContent__Content"); 69 | rm.style("width", width); 70 | rm.openEnd(); 71 | 72 | rm.renderControl(toggleableSideContent.getContent()); 73 | 74 | // close main content 75 | rm.close("section"); 76 | } 77 | 78 | /** 79 | * Renders the side content 80 | */ 81 | private _renderSideContent( 82 | rm: RenderManager, 83 | toggleableSideContent: ToggleableSideContent, 84 | width: string, 85 | visible: boolean 86 | ) { 87 | if (!visible) { 88 | return; 89 | } 90 | // open side filter 91 | rm.openStart("section", toggleableSideContent.getId() + "-sideContent"); 92 | rm.class("deveposQdrt-ToggleableSideContent__SideContent"); 93 | rm.style("width", width); 94 | rm.openEnd(); 95 | 96 | const sideContentControls = toggleableSideContent.getSideContent(); 97 | for (const sideContentControl of sideContentControls) { 98 | rm.renderControl(sideContentControl); 99 | } 100 | 101 | // close side filter 102 | rm.close("section"); 103 | } 104 | } 105 | 106 | export default new ToggleableSideContentRenderer(); 107 | -------------------------------------------------------------------------------- /dev/controller/BaseController.ts: -------------------------------------------------------------------------------- 1 | import ResourceBundle from "sap/base/i18n/ResourceBundle"; 2 | import Router from "sap/ui/core/routing/Router"; 3 | import Controller from "sap/ui/core/mvc/Controller"; 4 | import History from "sap/ui/core/routing/History"; 5 | import Model from "sap/ui/model/Model"; 6 | import QdrtComponent from "../Component"; 7 | 8 | /** 9 | * Base controller for all view controllers 10 | * @alias com.devepos.qdrt.controller.BaseController 11 | * @namespace com.devepos.qdrt.controller 12 | */ 13 | export default class BaseController extends Controller { 14 | protected router: Router; 15 | onInit(): void { 16 | this.router = this.getRouter() as Router; 17 | } 18 | getOwnerComponent(): QdrtComponent { 19 | return super.getOwnerComponent() as QdrtComponent; 20 | } 21 | getRouter(): Router { 22 | return this.getOwnerComponent().getRouter(); 23 | } 24 | /** 25 | * Convenience method for getting the view model by name in every controller of the application. 26 | * @param name the model name 27 | * @returns the model instance 28 | */ 29 | getModel(name: string): Model { 30 | return this.getView().getModel(name); 31 | } 32 | 33 | /** 34 | * Convenience method for setting the view model in every controller of the application. 35 | * @param model the model instance 36 | * @param name the model name 37 | */ 38 | setModel(model: Model, name: string): void { 39 | this.getView().setModel(model, name); 40 | } 41 | 42 | /** 43 | * Convenience method for getting the resource bundle. 44 | * @returns the resourceModel of the component 45 | */ 46 | getResourceBundle(): ResourceBundle { 47 | return this.getOwnerComponent().getResourceBundle(); 48 | } 49 | 50 | /** 51 | * Event handler for navigating back. 52 | * It there is a history entry we go one step back in the browser history 53 | * If not, it will replace the current entry of the browser history with the main route. 54 | */ 55 | onNavBack(): void { 56 | const previousHash = History.getInstance().getPreviousHash(); 57 | 58 | if (previousHash !== undefined) { 59 | history.go(-1); 60 | } else { 61 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 62 | // @ts-ignore - navTo method has only 3 parameters on lower versions 63 | this.router.navTo("main", {}, true); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /dev/controller/MainPage.controller.ts: -------------------------------------------------------------------------------- 1 | import models from "../model/models"; 2 | import BaseController from "./BaseController"; 3 | import EntityService from "../service/EntityService"; 4 | import { DbEntity, EntitySearchScope, EntityType, PagingParams } from "../model/types"; 5 | import SmartVariantManagementConnector from "../helper/variants/SmartVariantManagementConnector"; 6 | import { entityTypeIconFormatter, entityTypeTooltipFormatter } from "../model/formatter"; 7 | import AjaxJSONModel from "../model/AjaxJSONModel"; 8 | 9 | import JSONModel from "sap/ui/model/json/JSONModel"; 10 | import Event from "sap/ui/base/Event"; 11 | import Table from "sap/m/Table"; 12 | import Control from "sap/ui/core/Control"; 13 | import SmartVariantManagementUi2 from "sap/ui/comp/smartvariants/SmartVariantManagementUi2"; 14 | import FilterBar from "sap/ui/comp/filterbar/FilterBar"; 15 | import Log from "sap/base/Log"; 16 | import formatMessage from "sap/base/strings/formatMessage"; 17 | import Binding from "sap/ui/model/Binding"; 18 | import Fragment from "sap/ui/core/Fragment"; 19 | 20 | const FOUND_ENTITIES_PATH = "/foundEntities"; 21 | 22 | type ViewModelType = { 23 | nameFilter: string; 24 | descriptionFilter: string; 25 | selectedEntityType: EntityType; 26 | selectedSearchScope: EntitySearchScope; 27 | }; 28 | 29 | /** 30 | * Main Page controller 31 | * 32 | * @namespace com.devepos.qdrt.controller 33 | */ 34 | export default class MainPageController extends BaseController { 35 | entityTypeIconFormatter = entityTypeIconFormatter; 36 | entityTypeTooltipFormatter = entityTypeTooltipFormatter; 37 | formatMessage = formatMessage; 38 | private _entitiesTable: Table; 39 | private _entityService: EntityService; 40 | private _viewModel: JSONModel; 41 | private _dataModel: AjaxJSONModel; 42 | private _viewModelData: ViewModelType; 43 | private _variantMgmnt: SmartVariantManagementUi2; 44 | 45 | onInit(): void { 46 | super.onInit(); 47 | this._entityService = new EntityService(); 48 | this._viewModelData = { 49 | nameFilter: "", 50 | descriptionFilter: "", 51 | selectedEntityType: EntityType.All, 52 | selectedSearchScope: EntitySearchScope.All 53 | }; 54 | this._viewModel = models.createViewModel(this._viewModelData); 55 | this.getView().setModel(this._viewModel, "ui"); 56 | 57 | this._dataModel = new AjaxJSONModel({ foundEntities: [] }); 58 | this._dataModel.setDataProvider(FOUND_ENTITIES_PATH, { 59 | getData: (startIndex, length, determineLength) => { 60 | const pagingParams = { 61 | $top: length, 62 | $skip: startIndex 63 | } as PagingParams; 64 | if (determineLength) { 65 | pagingParams.$count = true; 66 | } 67 | return this._entityService.findEntities( 68 | this._viewModelData.nameFilter, 69 | this._viewModelData.descriptionFilter, 70 | this._viewModelData.selectedEntityType, 71 | this._viewModelData.selectedSearchScope, 72 | pagingParams 73 | ); 74 | } 75 | }); 76 | this._entitiesTable = this.getView().byId("foundEntitiesTable") as Table; 77 | this.getView().setModel(this._dataModel); 78 | 79 | this._variantMgmnt = this.byId("variantManagement") as SmartVariantManagementUi2; 80 | new SmartVariantManagementConnector(this.byId("filterbar") as FilterBar, this._variantMgmnt).connectFilterBar(); 81 | } 82 | 83 | _onEntityNavPress(event: Event): void { 84 | const selectedEntity = this._dataModel.getObject((event.getSource() as Control).getBindingContext().getPath()); 85 | this._navToEntity(selectedEntity); 86 | } 87 | 88 | /** 89 | * Set variant to modified if a filter changes 90 | */ 91 | _onFilterChange(): void { 92 | this._variantMgmnt?.currentVariantSetModified(true); 93 | } 94 | 95 | async _onToggleFavorite(event: Event): Promise { 96 | const selectedPath = (event.getSource() as Control)?.getBindingContext()?.getPath(); 97 | const selectedEntity = this._dataModel.getObject(selectedPath) as DbEntity; 98 | if (selectedEntity) { 99 | try { 100 | if (selectedEntity?.isFavorite) { 101 | await this._entityService.deleteFavorite(selectedEntity.name, selectedEntity.type); 102 | selectedEntity.isFavorite = false; 103 | } else { 104 | await this._entityService.createFavorite(selectedEntity.name, selectedEntity.type); 105 | selectedEntity.isFavorite = true; 106 | } 107 | this._dataModel.updateBindings(false); 108 | } catch (reqError) { 109 | Log.error(`Error during favorite handling`, (reqError as any).error?.message || reqError); 110 | } 111 | } 112 | } 113 | 114 | async _onSearch(): Promise { 115 | const itemsBinding = this._entitiesTable.getBinding("items") as Binding; 116 | if (!itemsBinding) { 117 | const colItemTemplate = await Fragment.load({ 118 | id: this.getView().getId(), 119 | name: "com.devepos.qdrt.fragment.MainTableItemsTemplate", 120 | controller: this 121 | }); 122 | this.getView().addDependent(colItemTemplate); 123 | this._entitiesTable.bindItems({ 124 | path: FOUND_ENTITIES_PATH, 125 | template: colItemTemplate 126 | }); 127 | } else { 128 | this._dataModel.refreshListPath(FOUND_ENTITIES_PATH); 129 | } 130 | } 131 | private _navToEntity(entity: DbEntity) { 132 | if (entity) { 133 | this.router.navTo("entity", { 134 | type: encodeURIComponent(entity.type), 135 | name: encodeURIComponent(entity.name) 136 | }); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /dev/css/addFilterPopover.less: -------------------------------------------------------------------------------- 1 | .sapUiSizeCompact { 2 | & .deveposQdrt-AddFilterPopover__Scroller { 3 | height: calc(100% - 2rem) !important; 4 | } 5 | & .deveposQdrt-AddFilterPopover__Scroller--multiSelect { 6 | height: calc(100% - 4rem) !important; 7 | } 8 | } 9 | 10 | .sapUiSizeCozy { 11 | & .deveposQdrt-AddFilterPopover__Scroller { 12 | height: calc(100% - 2.8rem) !important; 13 | } 14 | & .deveposQdrt-AddFilterPopover__Scroller--multiSelect { 15 | height: calc(100% - 5.6rem) !important; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /dev/css/base.less: -------------------------------------------------------------------------------- 1 | /** Custom border colors **/ 2 | .quartzDarkBorderColor() { 3 | border-color: #495767; 4 | } 5 | 6 | .quartzLightBorderColor() { 7 | border-color: #d9d9d9; 8 | } 9 | 10 | .belizeBorderColor() { 11 | border-color: #cccccc; 12 | } 13 | 14 | .belizePlusBorderColor() { 15 | border-color: #cccccc; 16 | } -------------------------------------------------------------------------------- /dev/css/customizationPanel.less: -------------------------------------------------------------------------------- 1 | .deveposQdrt-CustomizationPanel { 2 | height: 100%; 3 | 4 | .sapUiTheme-sap_fiori_3_dark & { 5 | border-left: 2px solid #495767; 6 | } 7 | 8 | .sapUiTheme-sap_fiori_3_hcb & { 9 | border-left: 2px solid white; 10 | } 11 | 12 | .sapUiTheme-sap_fiori_3_hcw & { 13 | border-left: 2px solid black; 14 | } 15 | 16 | .sapUiTheme-sap_fiori_3 &, 17 | .sapUiTheme-sap_belize_plus &, 18 | .sapUiTheme-sap_belize & { 19 | border-left: 2px solid #c3c3c3; 20 | box-shadow: -1px 0 8px 1px rgb(175, 175, 175); 21 | } 22 | 23 | > header { 24 | padding: 0.6rem 0.75rem 0.4rem 0.75rem; 25 | margin-bottom: 0.5rem; 26 | 27 | .sapUiTheme-sap_fiori_3 &, 28 | .sapUiTheme-sap_fiori_3_dark &, 29 | .sapUiTheme-sap_belize_plus &, 30 | .sapUiTheme-sap_belize & { 31 | background: rgb(59, 112, 165); 32 | box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.3); 33 | color: white; 34 | } 35 | 36 | .sapUiTheme-sap_fiori_3_dark & { 37 | box-shadow: none; 38 | } 39 | 40 | .sapUiTheme-sap_fiori_3_hcb & { 41 | border-bottom: 2px solid white; 42 | } 43 | .sapUiTheme-sap_fiori_3_hcw & { 44 | border-bottom: 2px solid black; 45 | } 46 | 47 | & .deveposQdrt-CustomizationPanel__HeaderTitle { 48 | padding-left: 0.5rem; 49 | text-shadow: none; 50 | 51 | .sapUiTheme-sap_fiori_3 &, 52 | .sapUiTheme-sap_belize_plus &, 53 | .sapUiTheme-sap_fiori_3_dark &, 54 | .sapUiTheme-sap_belize & { 55 | color: white; 56 | } 57 | } 58 | } 59 | 60 | > div { 61 | height: 100%; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /dev/css/entityFilterPanel.less: -------------------------------------------------------------------------------- 1 | .deveposQdrt-EntityFilterPanel { 2 | height: 100%; 3 | } -------------------------------------------------------------------------------- /dev/css/filterPopover.less: -------------------------------------------------------------------------------- 1 | .deveposQdrt-EntityFilterPopover__ListItem { 2 | padding: 0 1rem; 3 | 4 | .sapUiSizeCompact & { 5 | height: 2rem; 6 | } 7 | .sapUiSizeCozy & { 8 | height: 2.75rem; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /dev/css/main.less: -------------------------------------------------------------------------------- 1 | @import "./base.less"; 2 | @import "./toggleableSideContent.less"; 3 | @import "./quickFilter.less"; 4 | @import "./entityFilterPanel.less"; 5 | @import "./filterPopover.less"; 6 | @import "./SideFilterPanel.less"; 7 | @import "./addFilterPopover.less"; 8 | @import "./customizationPanel.less"; -------------------------------------------------------------------------------- /dev/css/quickFilter.less: -------------------------------------------------------------------------------- 1 | .deveposQdrt-QuickFilter { 2 | width: 93%; 3 | padding: 0.4rem; 4 | border-radius: 4px; 5 | // the border color will be applied in the control 6 | border: 1px solid; 7 | margin: 0.2rem; 8 | } 9 | -------------------------------------------------------------------------------- /dev/css/sideFilterPanel.less: -------------------------------------------------------------------------------- 1 | .deveposQdrt-SideFilterPanel--reducedHeight { 2 | height: 40% !important; 3 | } 4 | .deveposQdrt-SideFilterPanel__Container { 5 | padding-bottom: 2rem; 6 | } 7 | -------------------------------------------------------------------------------- /dev/css/toggleableSideContent.less: -------------------------------------------------------------------------------- 1 | .deveposQdrt-ToggleableSideContent { 2 | height: 100%; 3 | width: 100%; 4 | position: relative; 5 | display: block; 6 | overflow: hidden; 7 | white-space: nowrap; 8 | 9 | & > .deveposQdrt-ToggleableSideContent__Content, 10 | & > .deveposQdrt-ToggleableSideContent__SideContent { 11 | white-space: normal; 12 | display: inline-block; 13 | vertical-align: top; 14 | height: 100%; 15 | } 16 | 17 | & > .deveposQdrt-ToggleableSideContent__Content { 18 | } 19 | 20 | & > .deveposQdrt-ToggleableSideContent__SideContent { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /dev/element/ColumnItem.ts: -------------------------------------------------------------------------------- 1 | import Item from "sap/ui/core/Item"; 2 | 3 | /** 4 | * Item which describes a column configuration 5 | * @namespace com.devepos.qdrt.element 6 | */ 7 | export default class ColumnItem extends Item { 8 | metadata = { 9 | properties: {} 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /dev/element/FilterItem.ts: -------------------------------------------------------------------------------- 1 | import Item from "sap/ui/core/Item"; 2 | 3 | /** 4 | * Holds configuration for a DB entity filter 5 | * 6 | * @namespace com.devepos.qdrt.type 7 | */ 8 | export default class FilterItem extends Item { 9 | metadata = { 10 | properties: {} 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /dev/fragment/AddFilterPopover.fragment.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 9 | 10 | 11 | 12 | <ToolbarSpacer /> 13 | <Button press="onSelectAll" 14 | visible="{/multiSelect}" 15 | type="Transparent" 16 | tooltip="{i18n>entity_addFilterPopover_selectAll_button_tooltip}" 17 | icon="sap-icon://multiselect-all"/> 18 | <ToggleButton pressed="{/multiSelect}" 19 | icon="sap-icon://multi-select" 20 | tooltip="{i18n>entity_addFilterPopover_multiSelect_toggleButton_tooltip}" 21 | type="Transparent"/> 22 | </content> 23 | </OverflowToolbar> 24 | </customHeader> 25 | <content> 26 | <SearchField placeholder="{i18n>entity_addFilterPopover_searchPrompt}" 27 | liveChange="onSearchPromptLiveChange"/> 28 | <OverflowToolbar id="infoToolbar" 29 | class="sapMTB-Info-CTX" 30 | visible="{= ${/selectedItemCount} > 0}"> 31 | <content> 32 | <Label text="{ 33 | parts: [ 34 | 'i18n>entity_addFilterPopover_selectedFieldsText', 35 | '/selectedItemCount' 36 | ], 37 | formatter: '.formatMessage' 38 | }"/> 39 | </content> 40 | </OverflowToolbar> 41 | <ScrollContainer id="listScroller" 42 | vertical="true" 43 | horizontal="false" 44 | class="deveposQdrt-AddFilterPopover__Scroller"> 45 | <List id="fieldsList" 46 | items="{/fields}" 47 | includeItemInSelection="true" 48 | mode="{= ${/multiSelect} ? 'MultiSelect' : 'None' }" 49 | selectionChange="onFieldSelectionChange"> 50 | <items> 51 | <CustomListItem selected="{selected}" 52 | class="deveposQdrt-EntityFilterPopover__ListItem" 53 | tooltip="{tooltip}" 54 | type="Active" 55 | press="onFieldPress"> 56 | <FlexBox justifyContent="SpaceBetween"> 57 | <Text text="{label}" /> 58 | <core:Icon src="sap-icon://value-help" 59 | class="sapUiSmallMarginBegin" 60 | tooltip="{i18n>entity_addFilterPopover_hasValueHelpIcon_tooltip}" 61 | visible="{hasValueHelp}" /> 62 | </FlexBox> 63 | </CustomListItem> 64 | </items> 65 | </List> 66 | </ScrollContainer> 67 | </content> 68 | <endButton> 69 | <Button text="{i18n>entity_addFilterPopover_acceptButton_label}" 70 | visible="{/multiSelect}" 71 | enabled="{/hasSelectedItems}" 72 | type="Accept" 73 | press="onAcceptSelection"/> 74 | </endButton> 75 | </ResponsivePopover> 76 | </core:FragmentDefinition> -------------------------------------------------------------------------------- /dev/fragment/EntitySettingsDialog.fragment.xml: -------------------------------------------------------------------------------- 1 | <core:FragmentDefinition xmlns="sap.m" 2 | xmlns:core="sap.ui.core"> 3 | <P13nDialog showReset="true" 4 | ok="onOK" 5 | title="{i18n>entity_settingsDialog_title}" 6 | cancel="onCancel" 7 | reset="onReset" 8 | showResetEnabled="true" 9 | class="sapUiSizeCompact"> 10 | <panels> 11 | <P13nColumnsPanel id="columnsPanel" 12 | changeColumnsItems="onChangeColumnsItems" 13 | items="{/allColumnMetadata}" 14 | columnsItems="{/columnsItems}"> 15 | <items> 16 | <P13nItem columnKey="{name}" 17 | text="{description}" 18 | tooltip="{tooltip}"/> 19 | </items> 20 | <columnsItems> 21 | <P13nColumnsItem columnKey="{fieldName}" 22 | index="{index}" 23 | visible="{visible}"/> 24 | </columnsItems> 25 | </P13nColumnsPanel> 26 | <P13nSortPanel id="sortPanel" 27 | items="{/allColumnMetadata}" 28 | sortItems="{/sortCond}" 29 | addSortItem="onSortItemUpdate" 30 | updateSortItem="onSortItemUpdate" 31 | removeSortItem="onSortItemUpdate"> 32 | <items> 33 | <P13nItem columnKey="{name}" 34 | text="{description}" 35 | tooltip="{tooltip}"/> 36 | </items> 37 | <sortItems> 38 | <P13nSortItem columnKey="{fieldName}" 39 | operation="{sortDirection}"></P13nSortItem> 40 | </sortItems> 41 | </P13nSortPanel> 42 | <P13nGroupPanel id="groupPanel" 43 | groupItems="{/aggregationCond}" 44 | addGroupItem="onGroupItemUpdate" 45 | updateGroupItem="onGroupItemUpdate" 46 | removeGroupItem="onGroupItemUpdate" 47 | items="{/columnMetadata}"> 48 | <items> 49 | <P13nItem columnKey="{name}" 50 | text="{description}" 51 | tooltip="{tooltip}"/> 52 | </items> 53 | <groupItems> 54 | <P13nGroupItem columnKey="{fieldName}" 55 | showIfGrouped="{showIfGrouped}"></P13nGroupItem> 56 | </groupItems> 57 | </P13nGroupPanel> 58 | </panels> 59 | </P13nDialog> 60 | </core:FragmentDefinition> -------------------------------------------------------------------------------- /dev/fragment/MainTableItemsTemplate.fragment.xml: -------------------------------------------------------------------------------- 1 | <core:FragmentDefinition xmlns="sap.m" 2 | xmlns:core="sap.ui.core"> 3 | <ColumnListItem type="Active" 4 | press="_onEntityNavPress"> 5 | <cells> 6 | <core:Icon src="{path: 'type', formatter: '.entityTypeIconFormatter'}" 7 | tooltip="{path: 'type', formatter: '.entityTypeTooltipFormatter'}" /> 8 | <core:Icon src="{= ${isFavorite} ? 'sap-icon://favorite' : 'sap-icon://unfavorite'}" 9 | tooltip="{= ${isFavorite} ? ${i18n>dbEntities_table_act_unmarkAsFavorite} : ${i18n>dbEntities_table_act_markAsFavorite}}" 10 | class="sapUICompVarMngmtFavColor" 11 | press="_onToggleFavorite"/> 12 | <ObjectIdentifier title="{name}" 13 | text="{description}"/> 14 | <Text text="{packageName}" /> 15 | </cells> 16 | </ColumnListItem> 17 | </core:FragmentDefinition> -------------------------------------------------------------------------------- /dev/helper/FormatUtil.ts: -------------------------------------------------------------------------------- 1 | import { FieldMetadata } from "../model/types"; 2 | 3 | /** 4 | * Utility concerning formatting like e.g. getting the width for a 5 | * table column via metadata 6 | */ 7 | export default class FormatUtil { 8 | /** 9 | * Returns the width from the metadata attributes. min-width if there is no width specified 10 | * 11 | * @param fieldMeta Field metadata for the table field 12 | * @param maxWidth The max width (optional, default 30) 13 | * @param minWidth The min width (optional, default 3) 14 | * @returns width of the filter field in em 15 | */ 16 | static getWidth(fieldMeta: FieldMetadata, maxWidth = 30, minWidth = 3): string { 17 | let width = fieldMeta.maxLength; 18 | 19 | if (fieldMeta.type === "DateTime" || fieldMeta.type === "DateTimeOffset") { 20 | width = 12; 21 | } else if (fieldMeta.type === "Date") { 22 | // Force set the width to 9em for date fields 23 | width = 9; 24 | } else if (width) { 25 | // // Use Max width for description&Id and descriptionOnly use-case to accommodate description texts better on the UI 26 | // if ( 27 | // fieldMeta.type === "Edm.String" && 28 | // fieldMeta.description && 29 | // fieldMeta.displayBehaviour && 30 | // (fieldMeta.displayBehaviour === "descriptionAndId" || fieldMeta.displayBehaviour === "descriptionOnly") 31 | // ) { 32 | // width = "Max"; 33 | // } 34 | 35 | // // Use max width if "Max" is set in the metadata or above 36 | // if (width === "Max") { 37 | // width = maxWidth + ""; 38 | // } 39 | // Add additional .75 em (~12px) to avoid showing ellipsis in some cases! 40 | width += 0.75; 41 | // use a max initial width of 30em (default) 42 | if (width > maxWidth) { 43 | width = maxWidth; 44 | } else if (width < minWidth) { 45 | // use a min width of 3em (default) 46 | width = minWidth; 47 | } 48 | } 49 | if (!width) { 50 | // For Boolean fields - Use min width as the fallabck, in case no width could be derived. 51 | if (fieldMeta.type === "Boolean") { 52 | width = minWidth; 53 | } else { 54 | // use the max width as the fallback width of the column, if no width can be derived 55 | width = maxWidth; 56 | } 57 | } 58 | return width + "em"; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /dev/helper/I18nUtil.ts: -------------------------------------------------------------------------------- 1 | import ResourceBundle from "sap/base/i18n/ResourceBundle"; 2 | 3 | let resourceBundle: ResourceBundle; 4 | 5 | /** 6 | * Sets the resource bundle of the application 7 | * @param bundle the bundle with the application i18n texts 8 | */ 9 | export function setResourceBundle(bundle: ResourceBundle): void { 10 | resourceBundle = bundle; 11 | } 12 | 13 | /** 14 | * Utility object to retrieve translatable texts of the application. 15 | * This object is only to be used outside controller classes, as they 16 | * have access to the app resource bundle via the component 17 | */ 18 | export default { 19 | /** 20 | * Retrieves a translatable text from the currently set resource bundle 21 | * @param textId the id of the translatable text 22 | * @param args optional array with placeholder arguments 23 | * @returns the translated text in the current language 24 | */ 25 | getText(textId: string, args?: any[]): string { 26 | return resourceBundle?.getText(textId, args); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /dev/helper/ResizeAdapter.ts: -------------------------------------------------------------------------------- 1 | import Control from "sap/ui/core/Control"; 2 | import ResizeHandler from "sap/ui/core/ResizeHandler"; 3 | 4 | /** 5 | * Helper class for connecting a scroll container 6 | * @namespace com.devepos.qdrt.helper 7 | */ 8 | export default class ResizeAdapter { 9 | private _resizeCallback: Function; 10 | private _resizeHandlerId: string; 11 | private _liveChangeTimer: number; 12 | private _onAfterRenderingFirstTimeExecuted: boolean; 13 | 14 | /** 15 | * Creates new Resize adapter instance 16 | * @param listenerControl the control to check for size updates 17 | * @param resizeCallback the function to be called if resize was detected 18 | */ 19 | constructor(listenerControl: Control, resizeCallback: Function) { 20 | this._resizeCallback = resizeCallback; 21 | this._resizeHandlerId = ResizeHandler.register(listenerControl, resizeCallback); 22 | } 23 | destroy(): void { 24 | ResizeHandler.deregister(this._resizeHandlerId); 25 | // ResizeHandler.deregister(this._resizeListener2); 26 | window.clearTimeout(this._liveChangeTimer); 27 | } 28 | isResizeInitialized(): boolean { 29 | return this._onAfterRenderingFirstTimeExecuted; 30 | } 31 | initializeResize(): void { 32 | // adapt scroll-container very first time to the right size of the browser 33 | if (!this._onAfterRenderingFirstTimeExecuted) { 34 | this._onAfterRenderingFirstTimeExecuted = true; 35 | 36 | window.clearTimeout(this._liveChangeTimer); 37 | this._liveChangeTimer = window.setTimeout(() => { 38 | this._resizeCallback(); 39 | }, 0); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /dev/helper/filter/AddQuickFilterPopover.ts: -------------------------------------------------------------------------------- 1 | import { FieldMetadata } from "../../model/types"; 2 | 3 | import ResponsivePopover from "sap/m/ResponsivePopover"; 4 | import Control from "sap/ui/core/Control"; 5 | import JSONModel from "sap/ui/model/json/JSONModel"; 6 | import Fragment from "sap/ui/core/Fragment"; 7 | import Event from "sap/ui/base/Event"; 8 | import List from "sap/m/List"; 9 | import formatMessage from "sap/base/strings/formatMessage"; 10 | import ListBinding from "sap/ui/model/ListBinding"; 11 | import Filter from "sap/ui/model/Filter"; 12 | import ScrollContainer from "sap/m/ScrollContainer"; 13 | import FilterOperator from "sap/ui/model/FilterOperator"; 14 | 15 | export interface SelectedField { 16 | name: string; 17 | label: string; 18 | tooltip: string; 19 | fieldMetadata: FieldMetadata; 20 | } 21 | 22 | interface FieldConfig extends SelectedField { 23 | selected: boolean; 24 | hasValueHelp: boolean; 25 | } 26 | 27 | class PopoverModel { 28 | fields: FieldConfig[] = []; 29 | multiSelect = false; 30 | get hasSelectedItems(): boolean { 31 | return this.fields?.some(f => f.selected); 32 | } 33 | get selectedItemCount(): int { 34 | let selectedItemCount = 0; 35 | for (const field of this.fields) { 36 | if (field.selected) { 37 | selectedItemCount++; 38 | } 39 | } 40 | return selectedItemCount; 41 | } 42 | get selectedFields(): SelectedField[] { 43 | return this.fields.filter(f => f.selected); 44 | } 45 | } 46 | 47 | const FRAGMENT_ID = "addFiltersPopover"; 48 | 49 | const CSS_CLASS_SCROLLER_SINGLE_SELECT = "deveposQdrt-AddFilterPopover__Scroller"; 50 | const CSS_CLASS_SCROLLER_MULTI_SELECT = "deveposQdrt-AddFilterPopover__Scroller--multiSelect"; 51 | 52 | /** 53 | * Popover to add new filters to side filter bar 54 | */ 55 | export default class AddQuickFiltersPopover { 56 | formatMessage = formatMessage; 57 | private _modelData: PopoverModel; 58 | private _popover: ResponsivePopover; 59 | private _model: JSONModel; 60 | private _searchTimer: number; 61 | private _fieldList: List; 62 | private _popoverPromise: { resolve: (selectedFields: SelectedField[]) => void }; 63 | private _scroller: ScrollContainer; 64 | 65 | constructor(filterMetadata: FieldMetadata[]) { 66 | this._createModel(filterMetadata); 67 | } 68 | /** 69 | * Shows popover to allow user the selection of one or several fields of 70 | * the current entity 71 | * @param sourceButton the button the popover should be opened by 72 | * @returns Promise with selected field(s) 73 | */ 74 | async showPopover(sourceButton: Control): Promise<SelectedField[]> { 75 | const popover = await this._createPopover(); 76 | popover.setModel(this._model); 77 | sourceButton.addDependent(popover); 78 | this._fieldList = Fragment.byId(FRAGMENT_ID, "fieldsList") as List; 79 | this._scroller = Fragment.byId(FRAGMENT_ID, "listScroller") as ScrollContainer; 80 | return new Promise(resolve => { 81 | this._popoverPromise = { resolve }; 82 | popover.openBy(sourceButton); 83 | }); 84 | } 85 | 86 | onSearchPromptLiveChange(evt: Event): void { 87 | if (this._searchTimer) { 88 | clearTimeout(this._searchTimer); 89 | } 90 | const query = evt.getParameter("newValue"); 91 | this._searchTimer = setTimeout(() => { 92 | const fieldsBinding = this._fieldList.getBinding("items") as ListBinding<any>; 93 | if (query) { 94 | fieldsBinding.filter(new Filter("label", FilterOperator.Contains, query)); 95 | } else { 96 | fieldsBinding.filter([]); 97 | } 98 | }, 300); 99 | } 100 | onFieldPress(evt: Event): void { 101 | const selectedFieldConfig = (evt.getSource() as Control)?.getBindingContext()?.getObject() as FieldConfig; 102 | this._popover.close(); 103 | this._popoverPromise.resolve([selectedFieldConfig]); 104 | } 105 | onSelectAll(): void { 106 | for (const field of this._modelData.fields) { 107 | field.selected = true; 108 | } 109 | this._model.updateBindings(false); 110 | } 111 | onAcceptSelection(): void { 112 | this._popover.close(); 113 | this._popoverPromise.resolve(this._modelData.selectedFields); 114 | } 115 | onAfterClose(): void { 116 | this._popover?.destroy(); 117 | } 118 | private _createModel(filterMetadata: FieldMetadata[]): void { 119 | this._modelData = new PopoverModel(); 120 | for (const filterMeta of filterMetadata) { 121 | this._modelData.fields.push({ 122 | name: filterMeta.name, 123 | label: 124 | filterMeta.description === filterMeta.name 125 | ? filterMeta.name 126 | : `${filterMeta.description} (${filterMeta.name})`, 127 | tooltip: filterMeta.tooltip, 128 | fieldMetadata: filterMeta, 129 | selected: false, 130 | hasValueHelp: !!filterMeta.valueHelpType 131 | }); 132 | } 133 | this._model = new JSONModel(this._modelData); 134 | /* recalculate the scroller height if the info toolbar becomes 135 | * visible. This happens if at least one field is selected 136 | */ 137 | this._model.attachPropertyChange( 138 | null, 139 | (evt: Event) => { 140 | const path = evt.getParameter("path"); 141 | if (path === "selected") { 142 | if (this._modelData.hasSelectedItems) { 143 | this._scroller.removeStyleClass(CSS_CLASS_SCROLLER_SINGLE_SELECT); 144 | this._scroller.addStyleClass(CSS_CLASS_SCROLLER_MULTI_SELECT); 145 | } else { 146 | this._scroller.removeStyleClass(CSS_CLASS_SCROLLER_MULTI_SELECT); 147 | this._scroller.addStyleClass(CSS_CLASS_SCROLLER_SINGLE_SELECT); 148 | } 149 | } 150 | }, 151 | this 152 | ); 153 | } 154 | private async _createPopover(): Promise<ResponsivePopover> { 155 | this._popover = await Fragment.load({ 156 | id: FRAGMENT_ID, 157 | name: "com.devepos.qdrt.fragment.AddFilterPopover", 158 | controller: this 159 | }); 160 | return this._popover; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /dev/helper/filter/FilterCreator.ts: -------------------------------------------------------------------------------- 1 | import { DisplayFormat, FieldMetadata, FilterCond, FilterOperator } from "../../model/types"; 2 | import FilterOperatorConfigurations from "./FilterOperatorConfigurations"; 3 | 4 | import Token from "sap/m/Token"; 5 | import P13nConditionPanel from "sap/m/P13nConditionPanel"; 6 | import Log from "sap/base/Log"; 7 | 8 | /** 9 | * Creator for filter conditions / tokens 10 | */ 11 | export default class FilterCreator { 12 | private _value: string; 13 | private _fieldName: string; 14 | private _fieldMetadata: FieldMetadata; 15 | 16 | constructor(fieldName: string, fieldMetadata: FieldMetadata) { 17 | this._fieldName = fieldName; 18 | this._fieldMetadata = fieldMetadata; 19 | } 20 | 21 | /** 22 | * Sets the value to be used for filter/token creation 23 | * @param value the new value 24 | */ 25 | setValue(value: string): void { 26 | this._value = value; 27 | } 28 | 29 | /** 30 | * Creates a filter condition from the given value. 31 | * The value can contain special prefix/suffix - like "<=, <, >, >=, !()" which determines the 32 | * filter operation 33 | * @returns the created filter condition 34 | */ 35 | createFilter(): FilterCond { 36 | const filterCond = { 37 | keyField: this._fieldName, 38 | exclude: false 39 | } as FilterCond; 40 | let filterOpMatch: RegExpMatchArray; 41 | let value1: string; 42 | let value2: string; 43 | 44 | if (!this._value || this._value === "") { 45 | return null; 46 | } 47 | 48 | const validFilterConditions = FilterOperatorConfigurations.getOperatorsForType(this._fieldMetadata.type); 49 | const isExcludingPattern = this._value.startsWith("!("); 50 | 51 | for (const filterOperation of validFilterConditions) { 52 | filterOpMatch = filterOperation.getMatches(this._value); 53 | if (filterOpMatch) { 54 | filterCond.operation = filterOperation.operatorKey; 55 | filterCond.exclude = isExcludingPattern; 56 | this._adjustOperation(filterCond); 57 | 58 | if (filterOpMatch.length > 1) { 59 | value1 = filterOpMatch[1]; 60 | 61 | if (filterOpMatch.length > 2) { 62 | value2 = filterOpMatch[2]; 63 | } 64 | } 65 | break; 66 | } 67 | } 68 | if (!filterCond.operation) { 69 | Log.error(`No appropriate filter operator for value "${this._value}" found`); 70 | return null; 71 | } 72 | 73 | if (filterCond.operation !== FilterOperator.Empty) { 74 | filterCond.value1 = this._validate(value1); 75 | filterCond.value2 = this._validate(value2); 76 | } 77 | 78 | return filterCond; 79 | } 80 | 81 | /** 82 | * Creates a token for the passed filter condition 83 | * @param filterCond the filter condition as base for the token 84 | * @param existingTokens any existing tokens in the multi input control 85 | * @param tokenKey optional key for a token - overrides token determination from existingTokens array 86 | * @returns the created token 87 | */ 88 | createToken(filterCond: FilterCond, existingTokens?: Token[], tokenKey?: string): Token { 89 | // without filter condition no token creation is possible 90 | if (!filterCond || (!filterCond.value1 && filterCond.operation !== FilterOperator.Empty)) { 91 | return null; 92 | } 93 | const token = new Token({ 94 | key: tokenKey || this._getTokenKey(existingTokens), 95 | text: this._getTokenText(filterCond) 96 | }); 97 | 98 | if (existingTokens?.length) { 99 | if (existingTokens.find(t => t.getText() === token.getText())) { 100 | return null; 101 | } 102 | } 103 | 104 | const { value1, value2, exclude, operation, keyField } = filterCond; 105 | 106 | token.data("range", { 107 | value1, 108 | value2, 109 | exclude, 110 | operation, 111 | keyField, 112 | __quickFilter: true 113 | }); 114 | 115 | return token; 116 | } 117 | 118 | private _adjustOperation(filterCond: FilterCond) { 119 | if (filterCond.operation === FilterOperator.NotEmpty) { 120 | filterCond.exclude = true; 121 | filterCond.operation = FilterOperator.Empty; 122 | } else if (filterCond.operation === FilterOperator.NE) { 123 | filterCond.exclude = true; 124 | filterCond.operation = FilterOperator.EQ; 125 | } else if (filterCond.operation === FilterOperator.Auto) { 126 | filterCond.operation = FilterOperator.EQ; 127 | } 128 | } 129 | 130 | private _getTokenKey(existingTokens?: Token[]): string { 131 | if (!existingTokens || existingTokens.length === 0) { 132 | return "range_0"; 133 | } else { 134 | const keys = existingTokens.map(t => t.getKey()); 135 | let rangeIndex = 0; 136 | let key = ""; 137 | do { 138 | key = `range_${rangeIndex++}`; 139 | } while (keys.includes(key)); 140 | return key; 141 | } 142 | } 143 | 144 | private _getTokenText(filterCond: FilterCond): string { 145 | return P13nConditionPanel.getFormatedConditionText( 146 | filterCond.operation, 147 | this._formatValue(filterCond.value1), 148 | this._formatValue(filterCond.value2), 149 | filterCond.exclude 150 | ); 151 | } 152 | 153 | /** 154 | * Validates and returns the parsed value 155 | * @param value value to validate 156 | * @returns the parsed value adhereing to the internal representation of the type 157 | * @throws {sap.ui.model.ParseException|sap.ui.model.ValidateException} 158 | */ 159 | private _validate(value: string): string { 160 | if (!value || value === "") { 161 | return null; 162 | } 163 | if (this._fieldMetadata.displayFormat === DisplayFormat.UpperCase) { 164 | value = value.toUpperCase(); 165 | } 166 | 167 | const type = this._fieldMetadata.typeInstance; 168 | const parsedValue = type.parseValue(value, "string"); 169 | type.validateValue(parsedValue) as any; 170 | return parsedValue; 171 | } 172 | 173 | private _formatValue(value: string): string { 174 | if (!value || value === "") { 175 | return null; 176 | } 177 | return this._fieldMetadata.typeInstance.formatValue(value, "string"); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /dev/helper/filter/FilterOperatorConfigurations.ts: -------------------------------------------------------------------------------- 1 | import { FilterOperator } from "../../model/types"; 2 | import TypeUtil from "../../model/TypeUtil"; 3 | 4 | export type FilterOperatorRegExp = { re: RegExp; template: string }; 5 | 6 | class FilterOperatorConfig { 7 | operatorKey: string; 8 | private _include: FilterOperatorRegExp; 9 | private _exclude?: FilterOperatorRegExp; 10 | 11 | constructor(operatorKey: string, include: FilterOperatorRegExp, exclude?: FilterOperatorRegExp) { 12 | this.operatorKey = operatorKey; 13 | this._include = include; 14 | this._exclude = exclude; 15 | } 16 | 17 | getMatches(value: string): RegExpMatchArray { 18 | if (value?.startsWith("!(")) { 19 | return this._exclude?.re.exec(value); 20 | } else { 21 | return this._include.re.exec(value); 22 | } 23 | } 24 | } 25 | 26 | const allConditionsMap: { [operator: string]: FilterOperatorConfig } = { 27 | [FilterOperator.EQ]: new FilterOperatorConfig( 28 | FilterOperator.EQ, 29 | { 30 | re: /^=(.+)$/, 31 | template: "=$0" 32 | }, 33 | { 34 | re: /^!\(=(.+)\)$/, 35 | template: "!(=$0)" 36 | } 37 | ), 38 | [FilterOperator.GE]: new FilterOperatorConfig( 39 | FilterOperator.GE, 40 | { 41 | re: /^>=(.+)$/, 42 | template: ">=$0" 43 | }, 44 | { 45 | re: /^!\(>=(.+)\)$/, 46 | template: "!(>=$0)" 47 | } 48 | ), 49 | [FilterOperator.GT]: new FilterOperatorConfig( 50 | FilterOperator.GT, 51 | { 52 | re: /^>(.+)$/, 53 | template: ">$0" 54 | }, 55 | { 56 | re: /^!\(>(.+)\)$/, 57 | template: "!(>$0)" 58 | } 59 | ), 60 | [FilterOperator.LE]: new FilterOperatorConfig( 61 | FilterOperator.LE, 62 | { 63 | re: /^<=(.+)$/, 64 | template: "<=$0" 65 | }, 66 | { 67 | re: /^!\(<=(.+)\)$/, 68 | template: "!(<=$0)" 69 | } 70 | ), 71 | // needs to be earlier than LT, otherwise <empty> will be matched by LT 72 | [FilterOperator.Empty]: new FilterOperatorConfig( 73 | FilterOperator.Empty, 74 | { 75 | re: /^<empty>$/i, 76 | template: "<empty>" 77 | }, 78 | { 79 | re: /^!\(<empty>(.+)\)$/, 80 | template: "!(<empty>)" 81 | } 82 | ), 83 | [FilterOperator.LT]: new FilterOperatorConfig( 84 | FilterOperator.LT, 85 | { 86 | re: /^<(.+)$/, 87 | template: "<$0" 88 | }, 89 | { 90 | re: /^!\(<(.+)\)$/, 91 | template: "!(<$0)" 92 | } 93 | ), 94 | [FilterOperator.BT]: new FilterOperatorConfig( 95 | FilterOperator.BT, 96 | { 97 | re: /^(.+)\.{3}(.+)$/, 98 | template: "$0..$1" 99 | }, 100 | { 101 | re: /^!\((.+)\.{3}(.+)\)$/, 102 | template: "!($0..$1)" 103 | } 104 | ), 105 | [FilterOperator.Contains]: new FilterOperatorConfig( 106 | FilterOperator.Contains, 107 | { 108 | re: /^\*(.+)\*$/, 109 | template: "*$0*" 110 | }, 111 | { 112 | re: /^!\(\*(.+)\*\)$/, 113 | template: "!(*$0*)" 114 | } 115 | ), 116 | [FilterOperator.StartsWith]: new FilterOperatorConfig( 117 | FilterOperator.StartsWith, 118 | { 119 | re: /^([^\\*].*)\*$/, 120 | template: "$0*" 121 | }, 122 | { 123 | re: /^!\(([^\\*].*)\*\)$/, 124 | template: "!($0*)" 125 | } 126 | ), 127 | [FilterOperator.EndsWith]: new FilterOperatorConfig( 128 | FilterOperator.EndsWith, 129 | { 130 | re: /^\*(.*[^\\*])$/, 131 | template: "*$0" 132 | }, 133 | { 134 | re: /^!\(\*(.*[^\\*])\)$/, 135 | template: "!(*$0)" 136 | } 137 | ), 138 | // has to be the last entry, because its regex is the most wide 139 | [FilterOperator.Auto]: new FilterOperatorConfig(FilterOperator.Auto, { 140 | re: /^([^!\\(\\)\\*<>].*[^*]?)$/, 141 | template: "$0" 142 | }) 143 | }; 144 | 145 | /** 146 | * Map of possible operators by data type 147 | */ 148 | const typeFilterOperationMap: Record<string, string[]> = { 149 | Date: [ 150 | FilterOperator.EQ, 151 | FilterOperator.GE, 152 | FilterOperator.GT, 153 | FilterOperator.LE, 154 | FilterOperator.Empty, 155 | FilterOperator.LT, 156 | FilterOperator.BT, 157 | FilterOperator.Auto 158 | ], 159 | Time: [ 160 | FilterOperator.EQ, 161 | FilterOperator.GE, 162 | FilterOperator.GT, 163 | FilterOperator.LE, 164 | FilterOperator.LT, 165 | FilterOperator.BT, 166 | FilterOperator.Auto 167 | ], 168 | Numeric: [ 169 | FilterOperator.EQ, 170 | FilterOperator.BT, 171 | FilterOperator.LT, 172 | FilterOperator.LE, 173 | FilterOperator.GT, 174 | FilterOperator.GE, 175 | FilterOperator.Auto 176 | ] 177 | }; 178 | 179 | export default class FilterOperatorConfigurations implements Iterable<FilterOperatorConfig> { 180 | private _operatorKeys: string[]; 181 | /** 182 | * Retrieves conditions object for a given type 183 | * @param type type name (e.g. String, Time, ...) 184 | * @returns conditions object for a given type 185 | */ 186 | static getOperatorsForType(type: string, onlyExclude?: boolean): FilterOperatorConfigurations { 187 | let operators: string[]; 188 | if (TypeUtil.isNumeric(type)) { 189 | operators = typeFilterOperationMap.Numeric.slice(); 190 | } else { 191 | operators = typeFilterOperationMap[type]?.slice() ?? Object.keys(allConditionsMap); 192 | } 193 | if (onlyExclude) { 194 | operators = operators.filter(op => op !== FilterOperator.Auto); 195 | } 196 | return new FilterOperatorConfigurations(operators); 197 | } 198 | 199 | constructor(operatorKeys: string[]) { 200 | this._operatorKeys = operatorKeys; 201 | } 202 | 203 | [Symbol.iterator](): Iterator<FilterOperatorConfig> { 204 | let index = 0; 205 | 206 | return { 207 | next: () => { 208 | if (index < this._operatorKeys.length) { 209 | return { value: allConditionsMap[this._operatorKeys[index++]], done: false }; 210 | } else { 211 | return { done: true, value: null }; 212 | } 213 | } 214 | }; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /dev/helper/systemUtil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility for retrieving ABAP system values 3 | */ 4 | export default { 5 | /** 6 | * Retrieves the current client for Backend calls 7 | * @returns the found client 8 | */ 9 | getCurrentClient(): string { 10 | // @ts-ignore 11 | let sClient = sap?.ushell?.Container.getLogonSystem?.().getClient(); 12 | if (!sClient) { 13 | // retrieve client from current url 14 | const aClientMatchResult = window.location.href.match(/(?<=sap-client=)(\d{3})/); 15 | if (aClientMatchResult && aClientMatchResult.length === 2) { 16 | sClient = aClientMatchResult[1]; 17 | } 18 | } 19 | return sClient; 20 | }, 21 | 22 | /** 23 | * Retrieves the current language for backend calls 24 | * @returns the current language 25 | */ 26 | getCurrentLanguage(): string { 27 | return sap.ui.getCore().getConfiguration().getLanguage(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /dev/helper/valuehelp/ValueHelpFactory.ts: -------------------------------------------------------------------------------- 1 | import { FilterCond, ValueHelpMetadata, ValueHelpType } from "../../model/types"; 2 | import ValueHelpDialog from "./ValueHelpDialog"; 3 | 4 | import Input from "sap/m/Input"; 5 | import Token from "sap/m/Token"; 6 | 7 | /** 8 | * Factory for creating value helps 9 | */ 10 | export default class ValueHelpFactory { 11 | private static _instance: ValueHelpFactory; 12 | private constructor() { 13 | // singleton constructor 14 | } 15 | 16 | /** 17 | * Retrieves an instance of the factory 18 | * @returns instance of the factory 19 | */ 20 | static getInstance(): ValueHelpFactory { 21 | if (!ValueHelpFactory._instance) { 22 | ValueHelpFactory._instance = new ValueHelpFactory(); 23 | } 24 | return ValueHelpFactory._instance; 25 | } 26 | 27 | /** 28 | * Creates a new instance of a value help dialog 29 | * @param metadata metadata information of a value help 30 | * @param inputField reference to the input field on which value help is called 31 | * @param multipleSelection whether or not multiple selection is allowed 32 | * @param initialFilters optional array of initial filter conditions 33 | * @param initialTokens optional array of conditions as tokens 34 | * @returns the created value help dialog reference 35 | */ 36 | createValueHelpDialog( 37 | metadata: ValueHelpMetadata, 38 | inputField?: Input, 39 | multipleSelection?: boolean, 40 | initialFilters?: FilterCond[], 41 | initialTokens?: Token[] 42 | ): ValueHelpDialog { 43 | let supportRangesOnly = false; 44 | 45 | if (!metadata?.type) { 46 | supportRangesOnly = true; 47 | } 48 | 49 | const vhDialog = new ValueHelpDialog({ 50 | inputField: inputField, 51 | loadDataAtOpen: metadata.type === ValueHelpType.DomainFixValues, 52 | valueHelpMetadata: metadata, 53 | multipleSelection: multipleSelection, 54 | supportRanges: multipleSelection, 55 | initialFilters: initialFilters, 56 | initialTokens: initialTokens, 57 | supportRangesOnly: supportRangesOnly 58 | }); 59 | 60 | return vhDialog; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /dev/helper/valuehelp/ValueHelpModel.ts: -------------------------------------------------------------------------------- 1 | import ValueHelpService from "../../service/ValueHelpService"; 2 | import { ValueHelpMetadata } from "../../model/types"; 3 | import models from "../../model/models"; 4 | import { SimpleBindingParams } from "../../model/types"; 5 | 6 | import JSONModel from "sap/ui/model/json/JSONModel"; 7 | import { AggregationBindingInfo } from "sap/ui/base/ManagedObject"; 8 | 9 | /** 10 | * Model for ValueHelpDialog. It handles the necessary service 11 | * calls for retrieving the value help data 12 | */ 13 | export default class ValueHelpModel { 14 | private _vhMetadata: ValueHelpMetadata; 15 | private _model = models.createViewModel([]); 16 | private _service: ValueHelpService = new ValueHelpService(); 17 | 18 | constructor(vhMetadata: ValueHelpMetadata) { 19 | this.setVhMetadata(vhMetadata); 20 | } 21 | setVhMetadata(vhMetadata: ValueHelpMetadata): void { 22 | this._vhMetadata = vhMetadata; 23 | this._model?.setProperty("/", null); 24 | } 25 | getModel(): JSONModel { 26 | return this._model; 27 | } 28 | /** 29 | * Returns binding info for VH result 30 | * @returns binding info for VH result 31 | */ 32 | getVhResultBindingInfo(): AggregationBindingInfo { 33 | return { 34 | path: "/" 35 | }; 36 | } 37 | async fetchData(params?: SimpleBindingParams): Promise<void> { 38 | const valueHelpData = await this._service.retrieveValueHelpData({ 39 | type: this._vhMetadata.type, 40 | valueHelpName: this._vhMetadata.valueHelpName, 41 | sourceTab: this._vhMetadata.sourceTab, 42 | sourceField: this._vhMetadata.sourceField, 43 | filters: params?.filters, 44 | maxRows: 200 45 | }); 46 | this._model.setProperty("/", valueHelpData); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /dev/helper/valuehelp/ValueHelpUtil.ts: -------------------------------------------------------------------------------- 1 | import { FieldMetadata, ValueHelpField, ValueHelpMetadata, ValueHelpType } from "../../model/types"; 2 | import I18nUtil from "../I18nUtil"; 3 | 4 | /** 5 | * Utility object for value helps 6 | */ 7 | export default class ValueHelpUtil { 8 | /** 9 | * Retrieves metadata information to call a ranges only value help 10 | * @param fieldMeta metadata of field where no explicit value help is defined 11 | */ 12 | static getNoVhMetadata(fieldMeta: FieldMetadata): ValueHelpMetadata { 13 | return { 14 | fields: [{ ...fieldMeta }], 15 | outputFields: [fieldMeta.name], 16 | tokenKeyField: fieldMeta.name 17 | } as ValueHelpMetadata; 18 | } 19 | /** 20 | * Returns the metadata of a field which has a value help of 21 | * type {@link ValueHelpType.DomainFixValues} 22 | * 23 | * @param fieldMeta metadata of field which has domain fix values value help 24 | * @returns metadata for the domain fix value help 25 | */ 26 | static getDomainFixValuesVhMetadata(fieldMeta: FieldMetadata): ValueHelpMetadata { 27 | return { 28 | valueHelpName: fieldMeta.rollname, 29 | type: ValueHelpType.DomainFixValues, 30 | targetField: fieldMeta.name, 31 | tokenKeyField: "fixValue", 32 | tokenDescriptionField: "description", 33 | fields: [ 34 | Object.assign(new ValueHelpField(), { 35 | maxLength: fieldMeta.maxLength, 36 | displayFormat: fieldMeta.displayFormat, 37 | type: fieldMeta.type, 38 | name: "fixValue", 39 | description: fieldMeta.description 40 | } as ValueHelpField), 41 | Object.assign(new ValueHelpField(), { 42 | maxLength: 40, 43 | type: "String", 44 | name: "description", 45 | description: I18nUtil.getText("vhDialog_domainSH_descriptionCol_text") 46 | } as ValueHelpField) 47 | ], 48 | outputFields: ["fixValue", "description"] 49 | }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /dev/helper/variants/SmartVariantManagementConnector.ts: -------------------------------------------------------------------------------- 1 | import Log from "sap/base/Log"; 2 | import Input from "sap/m/Input"; 3 | import Select from "sap/m/Select"; 4 | import FilterBar from "sap/ui/comp/filterbar/FilterBar"; 5 | import PersonalizableInfo from "sap/ui/comp/smartvariants/PersonalizableInfo"; 6 | import SmartVariantManagementUi2 from "sap/ui/comp/smartvariants/SmartVariantManagementUi2"; 7 | import Control from "sap/ui/core/Control"; 8 | 9 | interface FilterWithValues { 10 | name: string; 11 | values: string[]; 12 | } 13 | 14 | type FilterGroupItemEntries = Record<string, Control>; 15 | 16 | /** 17 | * Connector for attaching a {@link SmartVariantManagementUi2} control to 18 | * a {@link FilterBar} control. 19 | * 20 | * @nonui5 21 | */ 22 | export default class SmartVariantManagementConnector { 23 | private _filterBar: FilterBar; 24 | private _smartVariantManagement: SmartVariantManagementUi2; 25 | 26 | constructor(filterBar: FilterBar, smartVariantManagement: SmartVariantManagementUi2) { 27 | this._filterBar = filterBar; 28 | this._smartVariantManagement = smartVariantManagement; 29 | } 30 | 31 | /** 32 | * Connects the filterbar to the variantsmanagement 33 | */ 34 | connectFilterBar(): void { 35 | const persInfo = new PersonalizableInfo({ 36 | type: "filterBar", 37 | keyName: "persistencyKey" 38 | }); 39 | persInfo.setControl(this._filterBar); 40 | 41 | this._filterBar.registerFetchData(this._fetchDataCallback.bind(this)); 42 | this._filterBar.registerApplyData(this._applyDataCallback.bind(this)); 43 | this._filterBar.attachAfterVariantLoad(this._onAfterVariantLoad.bind(this)); 44 | 45 | this._smartVariantManagement.addPersonalizableControl(persInfo); 46 | this._smartVariantManagement.initialise(); 47 | } 48 | 49 | private _fetchDataCallback(version: any) { 50 | const filters: FilterWithValues[] = []; 51 | for (const filterGroupItem of this._filterBar.getFilterGroupItems()) { 52 | const filter = <FilterWithValues>{ 53 | name: filterGroupItem.getName(), 54 | values: [] 55 | }; 56 | // somehow the return value of .getControl() is void and not sap.ui.core.Control 57 | const control = filterGroupItem.getControl() as unknown as Control; 58 | if (control.isA("sap.m.Input")) { 59 | filter.values.push((control as Input).getValue()); 60 | } else if (control.isA("sap.m.Select")) { 61 | filter.values.push((control as Select).getSelectedKey()); 62 | } 63 | filters.push(filter); 64 | } 65 | return JSON.stringify(filters); 66 | } 67 | 68 | private _applyDataCallback(variantDataJson: string, version: string) { 69 | if (variantDataJson && variantDataJson !== "") { 70 | try { 71 | const filters = JSON.parse(variantDataJson) as FilterWithValues[]; 72 | const filterGroupItems: FilterGroupItemEntries = {}; 73 | this._filterBar 74 | .getFilterGroupItems() 75 | .forEach(fgi => (filterGroupItems[fgi.getName()] = fgi.getControl() as unknown as Control)); 76 | // apply found filters to filterbar 77 | for (const filter of filters) { 78 | if (filter?.values?.length <= 0) { 79 | continue; 80 | } 81 | const filterControl = filterGroupItems[filter.name]; 82 | if (!filterControl) { 83 | continue; 84 | } 85 | if (filterControl.isA("sap.m.Input")) { 86 | (filterControl as Input).setValue(filter.values[0]); 87 | } else if (filterControl.isA("sap.m.Select")) { 88 | (filterControl as Select).setSelectedKey(filter.values[0]); 89 | } 90 | } 91 | } catch (ex) { 92 | Log.error("Error during parsing filters from variant"); 93 | } 94 | } 95 | } 96 | 97 | private _onAfterVariantLoad() { 98 | this._filterBar.fireSearch(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /dev/i18n/i18n.properties: -------------------------------------------------------------------------------- 1 | #XTIT: Application name 2 | title=Quick Data Reporter 3 | 4 | #YDES: Application description 5 | appDescription=App Description 6 | 7 | # Tool Header 8 | #******************************************************** 9 | 10 | theme_switcher_tooltip=Switch Theme 11 | 12 | # Main Page 13 | #******************************************************** 14 | 15 | mainToolbar_settings=Settings 16 | 17 | dbEntities_filterbar_entityNameFilter_label=Entity Name 18 | dbEntities_filterbar_entityDescrFilter_label=Entity Description 19 | dbEntities_filterbar_scopeFilter_label=Scope 20 | dbEntities_filterbar_scopeFilter_all=All 21 | dbEntities_filterbar_scopeFilter_favs=Favorites 22 | dbEntities_filterbar_entityTypeFilter_label=Entity type 23 | dbEntities_filterbar_entityTypeFilter_all=All 24 | 25 | dbEntities_table_title=Database Entities ({0}) 26 | dbEntities_table_filterPrompt=Search for Table/View, CDS View 27 | 28 | dbEntities_table_col_type=Type 29 | dbEntities_table_col_favorite=Fav 30 | dbEntities_table_col_ttip_favorite=Favorite 31 | dbEntities_table_col_name=Name 32 | dbEntities_table_col_description=Description 33 | dbEntities_table_col_package=Package 34 | 35 | dbEntities_table_act_markAsFavorite=Mark as favorite 36 | dbEntities_table_act_unmarkAsFavorite=Delete favorite 37 | 38 | # Entity page 39 | #******************************************************** 40 | 41 | entity_not_exists_msg=There is no Entity with the name {0} 42 | 43 | entity_table_tb_maxRows=Max Rows 44 | entity_table_title=Data ({0}) 45 | entity_table_tb_refresh=Refresh 46 | 47 | entity_table_aggrCount_col_title=Aggr. Count 48 | entity_table_aggrCount_col_tooltip=Aggregation Count 49 | 50 | entity_sideFilterPanel_title=Filters 51 | entity_sideFilterPanel_newFilter=Add Filter 52 | entity_sideFilterPanel_hideEmpty_tooltip=Hide Filters without value 53 | entity_sideFilterPanel_deleteAllFilters=Delete all Filters 54 | entity_sideFilterPanel_clearFilterValues=Clear all filters 55 | 56 | entity_sideParamPanel_title=Parameters 57 | 58 | entity_customizationPanel_filter_title=Filter Criteria 59 | 60 | entity_quickFilter_delete=Delete 61 | 62 | entity_field_technicalName=Technical 63 | entity_field_description=Description 64 | 65 | # Add Filter Popover 66 | #******************************************************** 67 | 68 | entity_addFilterPopover_title=Choose field(s) to Add 69 | entity_addFilterPopover_searchPrompt=Enter field name 70 | entity_addFilterPopover_selectedFieldsText=Selected Fields: {0} 71 | entity_addFilterPopover_acceptButton_label=Accept 72 | entity_addFilterPopover_multiSelect_toggleButton_tooltip=Toggle Multi Selection 73 | entity_addFilterPopover_hasValueHelpIcon_tooltip=Has Value Help 74 | entity_addFilterPopover_selectAll_button_tooltip=Select all 75 | 76 | # General Message Texts 77 | #******************************************************** 78 | 79 | message_title_confirm=Confirm 80 | 81 | # General Texts/Tooltips needed 82 | #******************************************************** 83 | 84 | dbEntity_type_cds=CDS View 85 | dbEntity_type_table=Database Table 86 | dbEntity_type_view=Database View 87 | 88 | booleanType_true=True 89 | booleanType_false=Falsee 90 | booleanType_yes=Yes 91 | booleanType_no=No 92 | 93 | validation_msg_invalidKey=Please select a valid entry! 94 | 95 | # Settings dialog 96 | #******************************************************** 97 | 98 | entity_settingsDialog_title=Entity settings 99 | 100 | # Value Help Dialog 101 | #******************************************************** 102 | 103 | vhDialog_searchField_placeholder=Search 104 | vhDialog_domainSH_descriptionCol_text=Description 105 | vhDialog_collectiveSH_popover_title=Select search help -------------------------------------------------------------------------------- /dev/i18n/i18n_de.properties: -------------------------------------------------------------------------------- 1 | #XTIT: Application name 2 | title=Quick Data Reporter 3 | 4 | #YDES: Application description 5 | appDescription=App Description 6 | 7 | # Tool Header 8 | #******************************************************** 9 | 10 | theme_switcher_tooltip=Theme wechseln 11 | 12 | # Main Page 13 | #******************************************************** 14 | 15 | mainToolbar_settings=Einstellungen 16 | 17 | dbEntities_filterbar_entityNameFilter_label=Entit\u00E4tsname 18 | dbEntities_filterbar_entityDescrFilter_label=Entit\u00E4tsbeschreibung 19 | dbEntities_filterbar_scopeFilter_label=Suchbereich 20 | dbEntities_filterbar_scopeFilter_all=Alle 21 | dbEntities_filterbar_scopeFilter_favs=Favoriten 22 | dbEntities_filterbar_entityTypeFilter_label=Art der Entit\u00E4t 23 | dbEntities_filterbar_entityTypeFilter_all=Alle 24 | 25 | dbEntities_table_title=Entit\u00E4ten ({0}) 26 | dbEntities_table_filterPrompt=Suche nach Tabelle/View, CDS View 27 | 28 | dbEntities_table_col_type=Typ 29 | dbEntities_table_col_favorite=Fav 30 | dbEntities_table_col_ttip_favorite=Favorit 31 | dbEntities_table_col_name=Name 32 | dbEntities_table_col_description=Beschreibung 33 | dbEntities_table_col_package=Paket 34 | 35 | dbEntities_table_act_markAsFavorite=Als Favorit markieren 36 | dbEntities_table_act_unmarkAsFavorite=Favorit l\u00F6schen 37 | 38 | # Entity page 39 | #******************************************************** 40 | 41 | entity_not_exists_msg=Die Entit\u00E4t mit dem Namen {0} existiert nicht 42 | 43 | entity_table_tb_maxRows=Max Zeilen 44 | entity_table_title=Daten ({0}) 45 | entity_table_tb_refresh=Aktualisieren 46 | 47 | entity_table_aggrCount_col_title=Gruppenanzahl 48 | entity_table_aggrCount_col_tooltip=Gruppenanzahl 49 | 50 | entity_sideFilterPanel_title=Filter 51 | entity_sideFilterPanel_newFilter=Filter hinzuf\u00fcgen 52 | entity_sideFilterPanel_hideEmpty_tooltip=Filter ohne Wert ausblenden 53 | entity_sideFilterPanel_deleteAllFilters=Alle Filter l\u00f6schen 54 | entity_sideFilterPanel_clearFilterValues=Alle Filter zur\u00FCcksetzen 55 | 56 | entity_sideParamPanel_title=Parameter 57 | 58 | entity_customizationPanel_filter_title=Filterkriterien 59 | 60 | entity_quickFilter_delete=L\u00f6schen 61 | 62 | entity_field_technicalName=Technisch 63 | entity_field_description=Beschreibung 64 | 65 | # Add Filter Popover 66 | #******************************************************** 67 | 68 | entity_addFilterPopover_title=Felder hinzuf\u00FCgen 69 | entity_addFilterPopover_searchPrompt=Feldname eingeben 70 | entity_addFilterPopover_selectedFieldsText=Selektierte Felder: {0} 71 | entity_addFilterPopover_acceptButton_label=Best\u00E4tigen 72 | entity_addFilterPopover_multiSelect_toggleButton_tooltip=Mehrfachauswahl aktivieren 73 | entity_addFilterPopover_hasValueHelpIcon_tooltip=Suchhilfe vorhanden 74 | entity_addFilterPopover_selectAll_button_tooltip=Alle selektieren 75 | 76 | # General Message Texts 77 | #******************************************************** 78 | 79 | message_title_confirm=Best\u00E4tigen 80 | 81 | # General Texts/Tooltips needed 82 | #******************************************************** 83 | 84 | dbEntity_type_cds=CDS View 85 | dbEntity_type_table=Datenbanktabelle 86 | dbEntity_type_view=Datenbankview 87 | 88 | booleanType_true=Wahr 89 | booleanType_false=Falsch 90 | booleanType_yes=Ja 91 | booleanType_no=Nein 92 | 93 | validation_msg_invalidKey=Bitte selektieren Sie einen g\u00FCltigen Eintrag! 94 | 95 | # Settings dialog 96 | #******************************************************** 97 | 98 | entity_settingsDialog_title=Entit\u00E4tseinstellungen 99 | 100 | # Value Help Dialog 101 | #******************************************************** 102 | 103 | vhDialog_searchField_placeholder=Suchen 104 | vhDialog_domainSH_descriptionCol_text=Beschreibung 105 | vhDialog_collectiveSH_popover_title=Suchhilfe ausw\u00E4hlen -------------------------------------------------------------------------------- /dev/i18n/i18n_en.properties: -------------------------------------------------------------------------------- 1 | #XTIT: Application name 2 | title=Quick Data Reporter 3 | 4 | #YDES: Application description 5 | appDescription=App Description 6 | 7 | # Tool Header 8 | #******************************************************** 9 | 10 | theme_switcher_tooltip=Switch Theme 11 | 12 | # Main Page 13 | #******************************************************** 14 | 15 | mainToolbar_settings=Settings 16 | 17 | dbEntities_filterbar_entityNameFilter_label=Entity Name 18 | dbEntities_filterbar_entityDescrFilter_label=Entity Description 19 | dbEntities_filterbar_scopeFilter_label=Scope 20 | dbEntities_filterbar_scopeFilter_all=All 21 | dbEntities_filterbar_scopeFilter_favs=Favorites 22 | dbEntities_filterbar_entityTypeFilter_label=Entity type 23 | dbEntities_filterbar_entityTypeFilter_all=All 24 | 25 | dbEntities_table_title=Database Entities ({0}) 26 | dbEntities_table_filterPrompt=Search for Table/View, CDS View 27 | 28 | dbEntities_table_col_type=Type 29 | dbEntities_table_col_favorite=Fav 30 | dbEntities_table_col_ttip_favorite=Favorite 31 | dbEntities_table_col_name=Name 32 | dbEntities_table_col_description=Description 33 | dbEntities_table_col_package=Package 34 | 35 | dbEntities_table_act_markAsFavorite=Mark as favorite 36 | dbEntities_table_act_unmarkAsFavorite=Delete favorite 37 | 38 | # Entity page 39 | #******************************************************** 40 | 41 | entity_not_exists_msg=There is no Entity with the name {0} 42 | 43 | entity_table_tb_maxRows=Max Rows 44 | entity_table_title=Data ({0}) 45 | entity_table_tb_refresh=Refresh 46 | 47 | entity_table_aggrCount_col_title=Aggr. Count 48 | entity_table_aggrCount_col_tooltip=Aggregation Count 49 | 50 | entity_sideFilterPanel_title=Filters 51 | entity_sideFilterPanel_newFilter=Add Filter 52 | entity_sideFilterPanel_hideEmpty_tooltip=Hide Filters without value 53 | entity_sideFilterPanel_deleteAllFilters=Delete all Filters 54 | entity_sideFilterPanel_clearFilterValues=Clear all filters 55 | 56 | entity_sideParamPanel_title=Parameters 57 | 58 | entity_customizationPanel_filter_title=Filter Criteria 59 | 60 | entity_quickFilter_delete=Delete 61 | 62 | entity_field_technicalName=Technical 63 | entity_field_description=Description 64 | 65 | # Add Filter Popover 66 | #******************************************************** 67 | 68 | entity_addFilterPopover_title=Choose field(s) to Add 69 | entity_addFilterPopover_searchPrompt=Enter field name 70 | entity_addFilterPopover_selectedFieldsText=Selected Fields: {0} 71 | entity_addFilterPopover_acceptButton_label=Accept 72 | entity_addFilterPopover_multiSelect_toggleButton_tooltip=Toggle Multi Selection 73 | entity_addFilterPopover_hasValueHelpIcon_tooltip=Has Value Help 74 | entity_addFilterPopover_selectAll_button_tooltip=Select all 75 | 76 | # General Message Texts 77 | #******************************************************** 78 | 79 | message_title_confirm=Confirm 80 | 81 | # General Texts/Tooltips needed 82 | #******************************************************** 83 | 84 | dbEntity_type_cds=CDS View 85 | dbEntity_type_table=Database Table 86 | dbEntity_type_view=Database View 87 | 88 | booleanType_true=True 89 | booleanType_false=Falsee 90 | booleanType_yes=Yes 91 | booleanType_no=No 92 | 93 | validation_msg_invalidKey=Please select a valid entry! 94 | 95 | # Settings dialog 96 | #******************************************************** 97 | 98 | entity_settingsDialog_title=Entity settings 99 | 100 | # Value Help Dialog 101 | #******************************************************** 102 | 103 | vhDialog_searchField_placeholder=Search 104 | vhDialog_domainSH_descriptionCol_text=Description 105 | vhDialog_collectiveSH_popover_title=Select search help -------------------------------------------------------------------------------- /dev/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevEpos/quick-data-reporter/67811c2b53e8c8f2e7ebd18da70b6ee466c602c9/dev/img/favicon.png -------------------------------------------------------------------------------- /dev/localService/MockServer.ts: -------------------------------------------------------------------------------- 1 | import ajaxUtil from "../service/ajaxUtil"; 2 | import { ValueHelpRequest } from "../model/types"; 3 | 4 | import _MockServer from "sap/ui/core/util/MockServer"; 5 | import Log from "sap/base/Log"; 6 | import UriParameters from "sap/base/util/UriParameters"; 7 | import DateFormat from "sap/ui/core/format/DateFormat"; 8 | import CalendarType from "sap/ui/core/CalendarType"; 9 | import { SinonFakeXMLHttpRequest } from "sinon"; 10 | 11 | const APP_MODULE_PATH = "com/devepos/qdrt/"; 12 | const JSON_FILES_MODULE_PATH = APP_MODULE_PATH + "localService/mockdata/"; 13 | const MOCK_SERVER_URL = "/sap/zqdrtrest/"; 14 | 15 | /** 16 | * @namespace com.devepos.qdrt.localService 17 | */ 18 | export default class MockServer { 19 | private _mockServer: _MockServer; 20 | private _mockData: Record<string, unknown> = {}; 21 | private _dateFormatter: DateFormat; 22 | private _randomSeed: Record<string, number> = {}; 23 | private _dateTimeFormatter: DateFormat; 24 | private _timeFormatter: DateFormat; 25 | /** 26 | * Initializes the mock server. 27 | * You can configure the delay with the URL parameter "serverDelay". 28 | * The local mock data in this folder is returned instead of the real data for testing. 29 | * @public 30 | * 31 | */ 32 | constructor() { 33 | const uriParameters = new UriParameters(window.location.href); 34 | this._mockServer = new _MockServer({ 35 | rootUri: MOCK_SERVER_URL 36 | }); 37 | this._dateFormatter = DateFormat.getDateInstance({ 38 | calendarType: CalendarType.Gregorian, 39 | pattern: "yyyy-MM-dd", 40 | strictParsing: true, 41 | UTC: true 42 | }); 43 | this._dateTimeFormatter = DateFormat.getDateTimeInstance({ 44 | calendarType: CalendarType.Gregorian, 45 | pattern: "yyyyMMddHHmmss", 46 | strictParsing: true, 47 | UTC: false 48 | }); 49 | this._timeFormatter = DateFormat.getTimeInstance({ 50 | calendarType: CalendarType.Gregorian, 51 | pattern: "HH:mm:ss", 52 | strictParsing: true, 53 | UTC: false 54 | }); 55 | this._randomSeed = {}; 56 | // configure mock server with a delay of 1s 57 | _MockServer.config({ 58 | autoRespond: true, 59 | autoRespondAfter: (uriParameters.get("serverDelay") as unknown as number) || 1000 60 | }); 61 | 62 | this._mockServer.setRequests([ 63 | { 64 | method: "HEAD", 65 | path: /.*/, 66 | response: (xhr: SinonFakeXMLHttpRequest) => { 67 | xhr.respond(200, { "X-CSRF-Token": "Dummy" }, null); 68 | } 69 | }, 70 | { 71 | method: "GET", 72 | path: /entities[^/]*/, 73 | response: (xhr: SinonFakeXMLHttpRequest) => { 74 | this._getMockdata(xhr, "dbentities"); 75 | } 76 | }, 77 | { 78 | method: "POST", 79 | path: /entities\/(.*)\/(.*)\/queryResult.*/, 80 | response: (xhr: SinonFakeXMLHttpRequest) => { 81 | this._getMockdata(xhr, "datapreview"); 82 | } 83 | }, 84 | { 85 | method: "GET", 86 | path: /entities\/(.*)\/(.*)\/metadata.*/, 87 | response: (xhr: SinonFakeXMLHttpRequest) => { 88 | this._getMockdata(xhr, "entityMetadata"); 89 | } 90 | }, 91 | { 92 | method: "GET", 93 | path: /entities\/(.*)\/(.*)\/variants.*/, 94 | response: (xhr: SinonFakeXMLHttpRequest) => { 95 | this._getMockdata(xhr, "entityvariants"); 96 | } 97 | }, 98 | { 99 | method: "GET", 100 | path: /entities\/(.*)\/(.*)\/vhMetadata.*/, 101 | response: (xhr: SinonFakeXMLHttpRequest) => { 102 | const response = this._getCachedMockdata("valueHelpMetadata"); 103 | if (!response) { 104 | xhr.respond(204, {}, ""); 105 | return; 106 | } 107 | const uriParams = new UriParameters(xhr.url); 108 | xhr.respond(200, {}, JSON.stringify(response[uriParams.get("field")])); 109 | } 110 | }, 111 | { 112 | method: "POST", 113 | path: /valueHelpData.*/, 114 | response: (xhr: SinonFakeXMLHttpRequest) => { 115 | const response = this._getCachedMockdata("valueHelpData"); 116 | if (!response) { 117 | xhr.respond(204, {}, ""); 118 | return; 119 | } 120 | if (xhr.requestBody) { 121 | try { 122 | const payload = JSON.parse(xhr.requestBody) as ValueHelpRequest; 123 | if (payload?.valueHelpName) { 124 | xhr.respond(200, {}, JSON.stringify(response[payload.valueHelpName])); 125 | } 126 | } catch (parseError) { 127 | xhr.respond(500, {}, "Parsing error of request"); 128 | return; 129 | } 130 | } 131 | } 132 | } 133 | ]); 134 | this._mockServer.start(); 135 | 136 | Log.info("Running the app with mock data"); 137 | } 138 | 139 | /** 140 | * Returns the mockserver of the app, should be used in integration tests 141 | * @public 142 | * @returns {sap.ui.core.util.MockServer} Mockserver instance 143 | */ 144 | getMockServer(): _MockServer { 145 | return this._mockServer; 146 | } 147 | 148 | private _getCachedMockdata(jsonFileName: string) { 149 | if (this._mockData[jsonFileName]) { 150 | return this._mockData[jsonFileName]; 151 | } else { 152 | const jsonResponse = this._getJSONContent(jsonFileName); 153 | if (jsonResponse?.status === 200) { 154 | this._mockData[jsonFileName] = jsonResponse?.data || {}; 155 | } else { 156 | this._mockData[jsonFileName] = null; 157 | } 158 | return jsonResponse?.data; 159 | } 160 | } 161 | 162 | private _getJSONContent(jsonFileName: string) { 163 | const localUri = sap.ui.require.toUrl(JSON_FILES_MODULE_PATH + jsonFileName + ".json"); 164 | return ajaxUtil.sendSync(localUri); 165 | } 166 | 167 | private _getMockdata(xhr: SinonFakeXMLHttpRequest, jsonFileName: string) { 168 | try { 169 | const json = this._getCachedMockdata(jsonFileName); 170 | if (json) { 171 | xhr.respond(200, {}, JSON.stringify(json)); 172 | } else { 173 | xhr.respond(204, {}, ""); 174 | } 175 | } catch (errorStatus) { 176 | xhr.respond(500, {}, ""); 177 | } 178 | } 179 | 180 | private _getPseudoRandomNumber(type: string): number { 181 | if (!this._randomSeed) { 182 | this._randomSeed = {}; 183 | } 184 | // eslint-disable-next-line no-prototype-builtins 185 | if (!this._randomSeed.hasOwnProperty(type)) { 186 | this._randomSeed[type] = 0; 187 | } 188 | this._randomSeed[type] = ((this._randomSeed[type] + 11) * 25214903917) % 281474976710655; 189 | return this._randomSeed[type] / 281474976710655; 190 | } 191 | 192 | private _generatePropertyValue(propertyName: string, type: string, index: int) { 193 | if (!index) { 194 | index = Math.floor(this._getPseudoRandomNumber("String") * 10000) + 101; 195 | } 196 | let date; 197 | switch (type) { 198 | case "String": 199 | return propertyName + " " + index; 200 | case "DateTime": 201 | date = new Date(); 202 | date.setFullYear(2000 + Math.floor(this._getPseudoRandomNumber("DateTime") * 20)); 203 | date.setDate(Math.floor(this._getPseudoRandomNumber("DateTime") * 30)); 204 | date.setMonth(Math.floor(this._getPseudoRandomNumber("DateTime") * 12)); 205 | date.setMilliseconds(0); 206 | return this._dateTimeFormatter.format(date); 207 | case "Date": 208 | date = new Date(); 209 | date.setFullYear(2000 + Math.floor(this._getPseudoRandomNumber("DateTime") * 20)); 210 | date.setDate(Math.floor(this._getPseudoRandomNumber("DateTime") * 30)); 211 | date.setMonth(Math.floor(this._getPseudoRandomNumber("DateTime") * 12)); 212 | date.setMilliseconds(0); 213 | return this._dateFormatter.format(date); 214 | case "Int": 215 | case "Int16": 216 | case "Int32": 217 | case "Int64": 218 | return Math.floor(this._getPseudoRandomNumber("Int") * 10000); 219 | case "Decimal": 220 | return Math.floor(this._getPseudoRandomNumber("Decimal") * 1000000) / 100; 221 | case "Boolean": 222 | return this._getPseudoRandomNumber("Boolean") < 0.5; 223 | case "Byte": 224 | return Math.floor(this._getPseudoRandomNumber("Byte") * 10); 225 | case "Double": 226 | return this._getPseudoRandomNumber("Double") * 10; 227 | case "Single": 228 | return this._getPseudoRandomNumber("Single") * 1000000000; 229 | case "SByte": 230 | return Math.floor(this._getPseudoRandomNumber("SByte") * 10); 231 | case "Time": 232 | return ( 233 | `${Math.floor(this._getPseudoRandomNumber("Time") * 23)}:` + 234 | `${Math.floor(this._getPseudoRandomNumber("Time") * 59)}:` + 235 | `${Math.floor(this._getPseudoRandomNumber("Time") * 59)}` 236 | ); 237 | case "Guid": 238 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => { 239 | const r = (this._getPseudoRandomNumber("Guid") * 16) | 0; 240 | const v = c === "x" ? r : (r & 0x3) | 0x8; 241 | return v.toString(16); 242 | }); 243 | case "Binary": 244 | /*eslint-disable */ 245 | const nMask = Math.floor(-2147483648 + this._getPseudoRandomNumber("Binary") * 4294967295); 246 | let sMask = ""; 247 | for ( 248 | var nFlag = 0, nShifted = nMask; 249 | nFlag < 32; 250 | nFlag++, sMask += String(nShifted >>> 31), nShifted <<= 1 251 | ); 252 | /*eslint-enable */ 253 | return sMask; 254 | case "DateTimeOffset": 255 | date = new Date(); 256 | date.setFullYear(2000 + Math.floor(this._getPseudoRandomNumber("DateTimeOffset") * 20)); 257 | date.setDate(Math.floor(this._getPseudoRandomNumber("DateTimeOffset") * 30)); 258 | date.setMonth(Math.floor(this._getPseudoRandomNumber("DateTimeOffset") * 12)); 259 | date.setMilliseconds(0); 260 | return "/Date(" + date.getTime() + "+0000)/"; 261 | default: 262 | return propertyName + " " + index; 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /dev/localService/mockdata/dbentities.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "T", 4 | "name": "SEOCLASSDF", 5 | "description": "Definition of Class/Interface", 6 | "packageName": "SEO" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /dev/localService/mockdata/entityvariants.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "MY_DEFAULT", 4 | "text": "My Default", 5 | "author": "STOCKBAL", 6 | "executeOnSelection": false, 7 | "favorite": true, 8 | "global": false, 9 | "labelReadOnly": true, 10 | "readOnly": false, 11 | "dataString": "{\"columnItems\":[],\"sortItems\":[],\"aggregationItems\":[],\"filterItems\":[]}" 12 | }, 13 | { 14 | "key": "TEST_123", 15 | "text": "Test 123", 16 | "author": "STOCKBAL", 17 | "executeOnSelection": false, 18 | "favorite": false, 19 | "global": true, 20 | "labelReadOnly": false, 21 | "readOnly": false, 22 | "dataString": "{\"columnItems\":[{\"fieldName\":\"clsname\",\"index\":0},{\"fieldName\":\"category\",\"index\":1},{\"fieldName\":\"author\",\"index\":2}],\"sortItems\":[],\"aggregationItems\":[], \"filterItems\":[]}" 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /dev/localService/mockdata/valueHelpData.json: -------------------------------------------------------------------------------- 1 | { 2 | "USER_ADDR": [ 3 | { 4 | "bname": "BWDEVELOPER", 5 | "mc_namelas": "JANE", 6 | "mc_namefir": "DOE", 7 | "mc_name1": "ITELO2", 8 | "company": "ITELO2" 9 | }, 10 | { "bname": "DDIC" }, 11 | { "bname": "DEVELOPER", "mc_namelas": "STOCKBAL", "mc_name1": "ITELO1", "company": "ITELO1" }, 12 | { "bname": "SAP*" }, 13 | { "bname": "SDMI_DLRYYAU", "idadtype": 1 }, 14 | { "bname": "TAGTEST", "mc_namelas": "TAGTEST", "mc_name1": "ITELO1", "company": "ITELO1" } 15 | ], 16 | "SEOVERSION": [ 17 | { "fixValue": "0", "description": "Inactive" }, 18 | { "fixValue": "1", "description": "Active" } 19 | ], 20 | "SEOCATEGRY": [ 21 | { "fixValue": "00", "description": "General object type" }, 22 | { "fixValue": "01", "description": "Exit class" }, 23 | { "fixValue": "10", "description": "Persistent class" }, 24 | { "fixValue": "11", "description": "Factory for persistent class" }, 25 | { "fixValue": "12", "description": "Status class for persistent class" }, 26 | { "fixValue": "20", "description": "View class" }, 27 | { "fixValue": "30", "description": "Proxy class for remote interface" }, 28 | { "fixValue": "40", "description": "Exception class" }, 29 | { "fixValue": "50", "description": "Business class" }, 30 | { "fixValue": "51", "description": "Business class interface for static components" }, 31 | { "fixValue": "52", "description": "Business class interface for instance-dependent components" }, 32 | { "fixValue": "60", "description": "BSP application class" }, 33 | { "fixValue": "70", "description": "Basis class for BSP element handlers" }, 34 | { "fixValue": "80", "description": "Web Dynpro runtime object" }, 35 | { "fixValue": "90", "description": "ESI: Provider interface (generated)" }, 36 | { "fixValue": "45", "description": "Area class (shared objects)" }, 37 | { "fixValue": "05", "description": "Test class (ABAP Unit)" }, 38 | { "fixValue": "65", "description": "Interface for parameter types for database procedure proxies" }, 39 | { "fixValue": "06", "description": "Behavior (Class ... for Behavior of ...)" } 40 | ], 41 | "SEOEXPOSE": [ 42 | { "fixValue": "0", "description": "Private" }, 43 | { "fixValue": "1", "description": "Protected" }, 44 | { "fixValue": "2", "description": "Public" } 45 | ], 46 | "SEOSTATE": [ 47 | { "fixValue": "0", "description": "Only modeled" }, 48 | { "fixValue": "1", "description": "Implemented" }, 49 | { "fixValue": "2", "description": "Implemented *** obsolete ***" } 50 | ], 51 | "SEORELSTAT": [ 52 | { "fixValue": "0", "description": "Not released" }, 53 | { "fixValue": "1", "description": "Released internally" } 54 | ], 55 | "UCCHECK": [ 56 | { "fixValue": "2", "description": "ABAP for key users" }, 57 | { "fixValue": "3", "description": "Static ABAP with limited object use (obsolete)" }, 58 | { "fixValue": "4", "description": "Standard ABAP with limited object use (obsolete)" }, 59 | { "fixValue": "5", "description": "ABAP for SAP Cloud Platform" }, 60 | { "fixValue": "X", "description": "Standard ABAP (Unicode)" }, 61 | { "fixValue": "", "description": "Non-Unicode ABAP (obsolete)" } 62 | ], 63 | "RDIR_RSTAT": [ 64 | { "fixValue": "P", "description": "SAP Standard Production Program" }, 65 | { "fixValue": "K", "description": "Customer Production Program" }, 66 | { "fixValue": "S", "description": "System Program" }, 67 | { "fixValue": "T", "description": "Test Program" }, 68 | { "fixValue": "", "description": "Unclassified" } 69 | ], 70 | "SEOBCCAT": [ 71 | { "fixValue": "00", "description": "Organizational Unit" }, 72 | { "fixValue": "01", "description": "Application Business Class" }, 73 | { "fixValue": "02", "description": "Business Class for APIs" } 74 | ], 75 | "SEOPROXY": [ 76 | { "fixValue": "", "description": "No proxy class" }, 77 | { "fixValue": "X", "description": "Proxy class" }, 78 | { "fixValue": "M", "description": "MDRS proxy class" } 79 | ], 80 | "SAUNIT_D_ALLOWED_RT_DURATION": [ 81 | { "fixValue": "12", "description": "Short" }, 82 | { "fixValue": "24", "description": "Medium" }, 83 | { "fixValue": "36", "description": "Long" } 84 | ], 85 | "SAUNIT_D_ALLOWED_RISK_LEVEL": [ 86 | { "fixValue": "11", "description": "Harmless" }, 87 | { "fixValue": "22", "description": "Dangerous" }, 88 | { "fixValue": "33", "description": "Critical" } 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /dev/localService/mockdata/valueHelpMetadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "targetField": "author", 4 | "tokenKeyField": "bname", 5 | "valueHelpName": "USER_ADDR", 6 | "type": "ElementaryDDICSearchHelp", 7 | "filterFields": [ 8 | "bname", 9 | "mc_namelas", 10 | "mc_namefir", 11 | "department", 12 | "building", 13 | "roomnumber", 14 | "tel_extens", 15 | "kostl", 16 | "inhouse_ml", 17 | "company", 18 | "mc_name1", 19 | "mc_city1", 20 | "idadtype" 21 | ], 22 | "outputFields": [ 23 | "bname", 24 | "mc_namelas", 25 | "mc_namefir", 26 | "department", 27 | "building", 28 | "roomnumber", 29 | "tel_extens", 30 | "kostl", 31 | "inhouse_ml", 32 | "company", 33 | "mc_name1", 34 | "mc_city1", 35 | "idadtype" 36 | ], 37 | "fields": [ 38 | { 39 | "name": "bname", 40 | "type": "String", 41 | "maxLength": 12, 42 | "fieldText": "User Name in User Master Record", 43 | "shortDescription": "User", 44 | "mediumDescription": "User", 45 | "longDescription": "User", 46 | "displayFormat": "UpperCase" 47 | }, 48 | { 49 | "name": "mc_namelas", 50 | "type": "String", 51 | "maxLength": 25, 52 | "fieldText": "Last Name in Uppercase for Search Help", 53 | "shortDescription": "Last name", 54 | "mediumDescription": "Last name", 55 | "longDescription": "Last name", 56 | "displayFormat": "UpperCase" 57 | }, 58 | { 59 | "name": "mc_namefir", 60 | "type": "String", 61 | "maxLength": 25, 62 | "fieldText": "First Name in Uppercase for Search Help", 63 | "shortDescription": "First name", 64 | "mediumDescription": "First name", 65 | "longDescription": "First name", 66 | "displayFormat": "UpperCase" 67 | }, 68 | { 69 | "name": "department", 70 | "type": "String", 71 | "maxLength": 40, 72 | "fieldText": "Department", 73 | "shortDescription": "Department", 74 | "mediumDescription": "Department", 75 | "longDescription": "Department" 76 | }, 77 | { 78 | "name": "building", 79 | "type": "String", 80 | "maxLength": 10, 81 | "fieldText": "Building (number or code)", 82 | "shortDescription": "Buildings", 83 | "mediumDescription": "Building code", 84 | "longDescription": "Building code" 85 | }, 86 | { 87 | "name": "roomnumber", 88 | "type": "String", 89 | "maxLength": 10, 90 | "fieldText": "Room or Apartment Number", 91 | "shortDescription": "Room", 92 | "mediumDescription": "Room Number", 93 | "longDescription": "Room Number" 94 | }, 95 | { 96 | "name": "tel_extens", 97 | "type": "String", 98 | "maxLength": 20, 99 | "fieldText": "First Telephone No.: Extension", 100 | "shortDescription": "Extension", 101 | "mediumDescription": "Extension", 102 | "longDescription": "Extension", 103 | "displayFormat": "UpperCase" 104 | }, 105 | { 106 | "name": "kostl", 107 | "type": "String", 108 | "maxLength": 8, 109 | "fieldText": "Cost center", 110 | "shortDescription": "Cost ctr", 111 | "mediumDescription": "Cost center", 112 | "longDescription": "Cost center", 113 | "displayFormat": "UpperCase" 114 | }, 115 | { 116 | "name": "inhouse_ml", 117 | "type": "String", 118 | "maxLength": 10, 119 | "fieldText": "Internal Mail Postal Code", 120 | "shortDescription": "Internal", 121 | "mediumDescription": "Internal mail", 122 | "longDescription": "Internal mail" 123 | }, 124 | { 125 | "name": "mc_name1", 126 | "type": "String", 127 | "maxLength": 25, 128 | "fieldText": "Name (Field NAME1) in Uppercase for Search Help", 129 | "shortDescription": "Name", 130 | "mediumDescription": "Name", 131 | "longDescription": "Company name", 132 | "displayFormat": "UpperCase" 133 | }, 134 | { 135 | "name": "mc_city1", 136 | "type": "String", 137 | "maxLength": 25, 138 | "fieldText": "City name in Uppercase for Search Help", 139 | "shortDescription": "City", 140 | "mediumDescription": "City", 141 | "longDescription": "City", 142 | "displayFormat": "UpperCase" 143 | }, 144 | { 145 | "name": "company", 146 | "type": "String", 147 | "maxLength": 42, 148 | "fieldText": "Company address, cross-system key", 149 | "shortDescription": "Company", 150 | "mediumDescription": "Company", 151 | "longDescription": "Company", 152 | "displayFormat": "UpperCase" 153 | }, 154 | { 155 | "name": "idadtype", 156 | "type": "String", 157 | "maxLength": 2, 158 | "fieldText": "Address Type of the Identity", 159 | "shortDescription": "ID Type", 160 | "mediumDescription": "Identity Add. Type", 161 | "longDescription": "Identity Add. Type" 162 | } 163 | ] 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /dev/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "sap.app": { 3 | "id": "com.devepos.qdrt", 4 | "type": "application", 5 | "i18n": "i18n/i18n.properties", 6 | "dataSources": {}, 7 | "ach": "", 8 | "title": "{{title}}", 9 | "description": "{{appDescription}}", 10 | "applicationVersion": { 11 | "version": "${version}" 12 | } 13 | }, 14 | "sap.ui5": { 15 | "config": { 16 | "fullWidth": true 17 | }, 18 | "dependencies": { 19 | "minUI5Version": "1.71.28", 20 | "libs": { 21 | "sap.ui.core": {}, 22 | "sap.m": {}, 23 | "sap.ui.comp": {} 24 | } 25 | }, 26 | "contentDensities": { "compact": true, "cozy": true }, 27 | "rootView": { 28 | "viewName": "com.devepos.qdrt.view.App", 29 | "type": "XML", 30 | "id": "idAppControl" 31 | }, 32 | "models": { 33 | "i18n": { 34 | "type": "sap.ui.model.resource.ResourceModel", 35 | "uri": "i18n/i18n.properties" 36 | }, 37 | "@i18n": { 38 | "type": "sap.ui.model.resource.ResourceModel", 39 | "uri": "i18n/i18n.properties" 40 | } 41 | }, 42 | "resources": { 43 | "css": [{ "uri": "css/main.css" }] 44 | }, 45 | "routing": { 46 | "config": { 47 | "routerClass": "sap.m.routing.Router", 48 | "viewType": "XML", 49 | "viewPath": "com.devepos.qdrt.view", 50 | "controlId": "idAppControl", 51 | "controlAggregation": "pages", 52 | "transition": "slide" 53 | }, 54 | "routes": [ 55 | { 56 | "pattern": "", 57 | "name": "main", 58 | "target": "main" 59 | }, 60 | { 61 | "pattern": "entities/{type}/{name}", 62 | "name": "entity", 63 | "target": "entity" 64 | } 65 | ], 66 | "targets": { 67 | "main": { 68 | "viewName": "MainPage" 69 | }, 70 | "entity": { 71 | "viewName": "Entity" 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /dev/model/AjaxJSONModel.ts: -------------------------------------------------------------------------------- 1 | import AjaxListBinding from "./AjaxListBinding"; 2 | 3 | import Log from "sap/base/Log"; 4 | import Binding from "sap/ui/model/Binding"; 5 | import Context from "sap/ui/model/Context"; 6 | import Filter from "sap/ui/model/Filter"; 7 | import JSONModel from "sap/ui/model/json/JSONModel"; 8 | import ListBinding from "sap/ui/model/ListBinding"; 9 | import Model from "sap/ui/model/Model"; 10 | import Sorter from "sap/ui/model/Sorter"; 11 | import MessageBox from "sap/m/MessageBox"; 12 | 13 | export interface DataResult { 14 | results: object | []; 15 | count?: number; 16 | } 17 | 18 | export interface AjaxDataProvider { 19 | /** 20 | * Fetches data 21 | * @param startIndex starting index for data retrieval 22 | * @param length number of entries to be retrieved 23 | * @param determineLength flag to indicate that the 24 | * @returns promise with result of request 25 | */ 26 | getData(startIndex: number, length: number, determineLength?: boolean): Promise<DataResult>; 27 | } 28 | 29 | type AjaxPathSettings = { 30 | path: string; 31 | dataProvider: AjaxDataProvider; 32 | }; 33 | 34 | /** 35 | * Model which enriches the standard JSONModel with optional asynchronous 36 | * data loading for specified paths in the model 37 | * 38 | * @namespace com.devepos.qdrt.model 39 | */ 40 | export default class AjaxJSONModel extends JSONModel { 41 | private _ajaxListPaths: Record<string, AjaxPathSettings>; 42 | /** 43 | * Constructor for new AjaxJSONModel 44 | * @param data Either the URL where to load the JSON from or a JS object 45 | * @param observe Whether to observe the JSON data for property changes (experimental) 46 | */ 47 | constructor(data?: object | string, observe?: boolean) { 48 | super(data, observe); 49 | this._ajaxListPaths = {}; 50 | } 51 | 52 | /** 53 | * Configures a given path in this model as an Ajax Path 54 | * @param path absolute Binding path 55 | * @param dataProvider Data Provider which will be used to retrieve data 56 | */ 57 | setDataProvider(path: string, dataProvider: AjaxDataProvider): void { 58 | this._ajaxListPaths[path] = { 59 | path, 60 | dataProvider 61 | }; 62 | } 63 | 64 | getProperty(path: string, context?: Context): any { 65 | const resolvedPath = this.resolve(path, context); 66 | if (path?.endsWith("$count")) { 67 | const parentObjPath = resolvedPath.substring(0, resolvedPath.indexOf("$count")); 68 | const parentObj = this.getObject(parentObjPath); 69 | return parentObj?.$count ?? 0; 70 | } else { 71 | return super.getProperty(path, context); 72 | } 73 | } 74 | 75 | /** 76 | * Resets the data at the given path and refreshes 77 | * all the binding 78 | * @param path a resolved path 79 | * @param forceRefresh if <code>true</code> the binding refresh will be forced 80 | */ 81 | refreshListPath(path: string, forceRefresh?: boolean): void { 82 | if (!path) { 83 | return; 84 | } 85 | if (path === "/") { 86 | this.oData = []; // can it also be an associative array? (i.e. {}) 87 | } else { 88 | // the path should already be resolved 89 | const lastSlashIndex = path.lastIndexOf("/"); 90 | const objectPath = path.substring(0, lastSlashIndex || 1); 91 | const propertyPath = path.substring(lastSlashIndex + 1); 92 | if (objectPath) { 93 | const object = this.getObject(objectPath); 94 | object[propertyPath] = []; 95 | } 96 | } 97 | 98 | // Refresh the binding that match the given path 99 | const bindings = (this.getBindings() as Binding<Model>[]) || []; 100 | for (const binding of bindings) { 101 | let bindingPath: string = binding.getPath(); 102 | if (binding.isRelative()) { 103 | bindingPath = this.resolve(binding.getPath(), binding.getContext()); 104 | } 105 | if (bindingPath === path) { 106 | binding.refresh(forceRefresh); 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * Requests data from the server. The data provider to be used for the service call 113 | * will be determined through the given path. {@link AjaxDataProvider} 114 | * @param path path to object in the model 115 | * @param startIndex starting index for data request 116 | * @param length number of entries to read 117 | * @param determineLength if <code>true</code> the maximum available length should be determined 118 | * @returns promise with data response 119 | */ 120 | async requestData( 121 | path: string, 122 | startIndex: number, 123 | length: number, 124 | determineLength: boolean, 125 | successHandler: (data: DataResult) => void 126 | ): Promise<void> { 127 | const dataProvider = this._ajaxListPaths[path]?.dataProvider; 128 | if (!dataProvider) { 129 | throw new Error(`No Data Provider registered at path ${path}`); 130 | } 131 | try { 132 | const result = await dataProvider.getData(startIndex, length, determineLength); 133 | this._importData(path, result, startIndex); 134 | if (successHandler) { 135 | successHandler(result); 136 | } 137 | this.checkUpdate(false, false); 138 | } catch (reqError) { 139 | const errorMessage = (reqError as any).error?.message; 140 | Log.error(`Error occurred during data fetch at path ${path}`, errorMessage); 141 | // temporary solution to display the error to the user 142 | if (errorMessage) { 143 | MessageBox.error(errorMessage); 144 | } 145 | throw reqError; 146 | } 147 | } 148 | 149 | /** 150 | * Creates a new list binding to this model. 151 | * 152 | * @param path Binding path, either absolute or relative to a given <code>context</code> 153 | * @param context Binding context referring to this model 154 | * @param sorters Initial sort order 155 | * @param filters Predifined filters 156 | * @param additionalParams additional binding parameters specific to this model 157 | * @returns the created List Binding 158 | */ 159 | bindList( 160 | path: string, 161 | context?: Context, 162 | sorters?: Sorter | Sorter[], 163 | filters?: Filter | Filter[], 164 | additionalParams?: object 165 | ): ListBinding<JSONModel> { 166 | const absolutePath = this.resolve(path, context); 167 | if (this._ajaxListPaths.hasOwnProperty(absolutePath)) { 168 | return new AjaxListBinding(this, path, context); 169 | } 170 | // TODO: check the path if an {@see AjaxListBinding} is necessary 171 | return super.bindList(path, context, sorters, filters, additionalParams); 172 | } 173 | 174 | private _importData(path: string, data: DataResult, startIndex: number) { 175 | if (!data?.results || !Array.isArray(data.results)) { 176 | return; 177 | } 178 | const list = this.getObject(path); 179 | if (!list) { 180 | Log.error(`No array defined at path '${path}'`); 181 | return; 182 | } 183 | 184 | if (data.count) { 185 | (list as any).$count = data.count; 186 | } 187 | 188 | for (let i = 0; i < data.results.length; i++) { 189 | list[startIndex++] = data.results[i]; 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /dev/model/Entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DataRow, 3 | EntityType, 4 | EntityMetadata, 5 | SortCond, 6 | ColumnConfig, 7 | AggregationCond, 8 | FieldMetadata, 9 | EntityVariant, 10 | FieldFilter, 11 | FilterCond 12 | } from "./types"; 13 | 14 | /** 15 | * Describes an entity which is configurable 16 | */ 17 | export interface ConfigurableEntity { 18 | /** 19 | * Current sort items 20 | */ 21 | sortCond: SortCond[]; 22 | /** 23 | * Current column configuration 24 | */ 25 | columnsItems: ColumnConfig[]; 26 | /** 27 | * Current group items 28 | */ 29 | aggregationCond: AggregationCond[]; 30 | } 31 | 32 | export type TableFilters = Record<string, FieldFilter>; 33 | 34 | /** 35 | * Describes an entity 36 | */ 37 | export default class Entity implements ConfigurableEntity { 38 | /** 39 | * The name of an entity 40 | */ 41 | name: string; 42 | /** 43 | * Type of an entity 44 | */ 45 | type: EntityType; 46 | /** 47 | * Metadata of an entity 48 | */ 49 | metadata: EntityMetadata = { entity: {}, fields: [] }; 50 | /** 51 | * List of variants of the entity 52 | */ 53 | variants?: EntityVariant[] = []; 54 | /** 55 | * Data rows of an entity 56 | */ 57 | rows: DataRow[] = []; 58 | /** 59 | * CDS View parameters 60 | */ 61 | parameters?: TableFilters = {}; 62 | /** 63 | * All visible filters with their set filter values 64 | */ 65 | filters: TableFilters = {}; 66 | sortCond: SortCond[] = []; 67 | columnsItems: ColumnConfig[] = []; 68 | aggregationCond: AggregationCond[] = []; 69 | maxRows = 200; 70 | /** 71 | * Returns all visible columns 72 | */ 73 | get visibleFieldMetadata(): FieldMetadata[] { 74 | const visibleColKeys = this.columnsItems.filter(col => col.visible).map(col => col.fieldName); 75 | return visibleColKeys.map(visibleColKey => 76 | this.metadata.fields.find(colMeta => colMeta.name === visibleColKey) 77 | ); 78 | } 79 | getFilledFilters(): FieldFilter[] { 80 | const filters = [] as FieldFilter[]; 81 | for (const filterName of Object.keys(this.filters)) { 82 | const filter = this.filters[filterName]; 83 | if (filter.value || filter?.items?.length > 0 || filter?.ranges?.length > 0) { 84 | /* 85 | * in case of field name with a namespace the filtername must be decoded before it is 86 | * sent to the backend 87 | */ 88 | const reducedFilter = { fieldName: decodeURIComponent(filterName) } as FieldFilter; 89 | if (filter.value) { 90 | reducedFilter.value = encodeURIComponent(filter.value); 91 | } 92 | if (filter.ranges?.length > 0) { 93 | reducedFilter.ranges = []; 94 | 95 | for (const range of filter.ranges) { 96 | const reducedRange = { operation: range.operation } as FilterCond; 97 | if (range.value1) { 98 | reducedRange.value1 = encodeURIComponent(range.value1); 99 | } 100 | if (range.value2) { 101 | reducedRange.value2 = encodeURIComponent(range.value2); 102 | } 103 | if (range.exclude) { 104 | reducedRange.exclude = true; 105 | } 106 | reducedFilter.ranges.push(reducedRange); 107 | } 108 | } 109 | if (filter.items?.length > 0) { 110 | reducedFilter.items = filter.items.map(item => { 111 | return { 112 | key: encodeURIComponent(item.key) 113 | }; 114 | }); 115 | } 116 | 117 | filters.push(reducedFilter); 118 | } 119 | } 120 | 121 | return filters; 122 | } 123 | getOutputFields(): ColumnConfig[] { 124 | return this.columnsItems 125 | .filter(col => col.visible) 126 | .map(col => { 127 | return { fieldName: col.fieldName }; 128 | }); 129 | } 130 | getParameters(): FieldFilter[] { 131 | const params = [] as FieldFilter[]; 132 | for (const paramName of Object.keys(this.parameters)) { 133 | const param = this.parameters[paramName]; 134 | if (param.value) { 135 | params.push({ fieldName: paramName, value: encodeURIComponent(param.value) }); 136 | } 137 | } 138 | return params; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /dev/model/TypeFactory.ts: -------------------------------------------------------------------------------- 1 | import Boolean from "sap/ui/model/odata/type/Boolean"; 2 | import Byte from "sap/ui/model/odata/type/Byte"; 3 | import DateTime from "sap/ui/model/odata/type/DateTime"; 4 | import Date1 from "sap/ui/model/odata/type/Date"; 5 | import DateTimeOffset from "sap/ui/model/odata/type/DateTimeOffset"; 6 | import Decimal from "sap/ui/model/odata/type/Decimal"; 7 | import Double from "sap/ui/model/odata/type/Double"; 8 | import Single from "sap/ui/model/odata/type/Single"; 9 | import Guid from "sap/ui/model/odata/type/Guid"; 10 | import Int16 from "sap/ui/model/odata/type/Int16"; 11 | import Int32 from "sap/ui/model/odata/type/Int32"; 12 | import Int64 from "sap/ui/model/odata/type/Int64"; 13 | import SByte from "sap/ui/model/odata/type/SByte"; 14 | import String from "sap/ui/model/odata/type/String"; 15 | import TimeOfDay from "sap/ui/model/odata/type/TimeOfDay"; 16 | import ODataType from "sap/ui/model/odata/type/ODataType"; 17 | 18 | const typeMap: Record<string, typeof ODataType> = { 19 | Boolean, 20 | Byte, 21 | Date: Date1, 22 | DateTime, 23 | DateTimeOffset, 24 | Decimal, 25 | Double, 26 | Single, 27 | Float: Single, 28 | Guid, 29 | Int16, 30 | Int32, 31 | Int64, 32 | SByte, 33 | String, 34 | Time: TimeOfDay 35 | }; 36 | 37 | /** 38 | * Factory for creating type instances 39 | */ 40 | export default class TypeFactory { 41 | /** 42 | * Creates new type instance of for the given type 43 | * @param name the name of a service type 44 | * @param formatOptions optional object with formatting options 45 | * @param constraints optional object with constraints, e.g. "maxLength" 46 | * @returns the created type instance or <code>null</code> if none could be determined 47 | */ 48 | static getType(name: string, formatOptions?: object, constraints?: object): ODataType { 49 | let type: ODataType = null; 50 | 51 | const typeConstructor = typeMap[name]; 52 | if (typeConstructor) { 53 | type = new typeConstructor(formatOptions, constraints); 54 | } 55 | return type; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /dev/model/TypeUtil.ts: -------------------------------------------------------------------------------- 1 | const numericTypes = ["Byte", "Single", "Double", "Int16", "Int32", "Int64", "SByte", "Decimal"]; 2 | 3 | /** 4 | * Factory for creating type instances 5 | */ 6 | export default class TypeUtil { 7 | /** 8 | * Returns a generic type for a given type 9 | * @param typeName name of a type (e.g. String) 10 | */ 11 | static generalizeType(typeName: string): string { 12 | if (numericTypes.includes(typeName)) { 13 | return "Numeric"; 14 | } else { 15 | return typeName; 16 | } 17 | } 18 | 19 | /** 20 | * Returns <code>true</code> if the given type is a numeric type 21 | * @param typeName the name of a type 22 | * @returns <code>true</code> if the given type is a numeric type 23 | */ 24 | static isNumeric(typeName: string): boolean { 25 | return numericTypes.includes(typeName); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /dev/model/formatter.ts: -------------------------------------------------------------------------------- 1 | import { EntityType } from "./types"; 2 | import I18nUtil from "../helper/I18nUtil"; 3 | 4 | /** 5 | * Returns the icon string for the given entity type 6 | * @param type the type of an entity 7 | * @returns the corresponding type for an entity type 8 | */ 9 | export function entityTypeIconFormatter(type: EntityType): string { 10 | switch (type) { 11 | case EntityType.CdsView: 12 | return "sap-icon://customer-view"; 13 | case EntityType.Table: 14 | return "sap-icon://grid"; 15 | case EntityType.View: 16 | return "sap-icon://table-view"; 17 | default: 18 | return null; // an empty string will raise an error in an Icon control 19 | } 20 | } 21 | /** 22 | * Returns the tooltip for the given entity type 23 | * @param type the type of an entity 24 | * @returns the corresponding tooltip for an entity type 25 | */ 26 | export function entityTypeTooltipFormatter(type: EntityType): string { 27 | switch (type) { 28 | case EntityType.CdsView: 29 | return I18nUtil.getText("dbEntity_type_cds"); 30 | case EntityType.Table: 31 | return I18nUtil.getText("dbEntity_type_table"); 32 | case EntityType.View: 33 | return I18nUtil.getText("dbEntity_type_view"); 34 | default: 35 | return ""; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /dev/model/globalConsts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The default value of visible columns of an entity 3 | */ 4 | export const DEFAULT_VISIBLE_COL_COUNT = 50; 5 | /** 6 | * Constants for special field names 7 | */ 8 | export const SpecialFieldNames = { 9 | /** 10 | * Name of column during active grouping 11 | */ 12 | groupCountCol: "$groupCount" 13 | }; 14 | -------------------------------------------------------------------------------- /dev/model/models.ts: -------------------------------------------------------------------------------- 1 | import JSONModel from "sap/ui/model/json/JSONModel"; 2 | import Device from "sap/ui/Device"; 3 | 4 | export default { 5 | /** 6 | * Creates json model with device information 7 | * @returns the device model 8 | * @public 9 | */ 10 | createDeviceModel(): JSONModel { 11 | const oModel = new JSONModel(Device); 12 | oModel.setDefaultBindingMode("OneWay"); 13 | return oModel; 14 | }, 15 | /** 16 | * Creates new json view model 17 | * @param data the data for the model 18 | * @param observeChanges if <code>true</code> all property changes will trigger an automatic binding update 19 | * @returns the created JSON model 20 | * @public 21 | */ 22 | createViewModel(data?: object, observeChanges?: boolean): JSONModel { 23 | return new JSONModel(data, observeChanges); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /dev/model/types.ts: -------------------------------------------------------------------------------- 1 | import I18nUtil from "../helper/I18nUtil"; 2 | import TypeFactory from "./TypeFactory"; 3 | 4 | import ODataType from "sap/ui/model/odata/type/ODataType"; 5 | 6 | export enum EntityType { 7 | All = "all", 8 | CdsView = "C", 9 | Table = "T", 10 | View = "V" 11 | } 12 | 13 | export enum FieldType { 14 | Normal = "normal", 15 | Parameter = "param" 16 | } 17 | 18 | export enum EntitySearchScope { 19 | All = "all", 20 | Favorite = "favorites" 21 | } 22 | 23 | export enum DisplayFormat { 24 | UpperCase = "UpperCase" 25 | } 26 | 27 | /** 28 | * Operators for the Filter. 29 | */ 30 | export enum FilterOperator { 31 | /** 32 | * Will result in Operator EQ 33 | */ 34 | Auto = "Auto", 35 | Contains = "Contains", 36 | StartsWith = "StartsWith", 37 | EndsWith = "EndsWith", 38 | BT = "BT", 39 | EQ = "EQ", 40 | GE = "GE", 41 | GT = "GT", 42 | LE = "LE", 43 | LT = "LT", 44 | NE = "NE", 45 | Empty = "Empty", 46 | NotEmpty = "NotEmpty" 47 | } 48 | 49 | export enum ValueHelpType { 50 | DomainFixValues = "DomainFixValues", 51 | DDICSearchHelp = "DDICSearchHelp", 52 | ElementaryDDICSearchHelp = "ElementaryDDICSearchHelp", 53 | CollectiveDDICSearchHelp = "CollectiveDDICSearchHelp", 54 | CheckTable = "CheckTable", 55 | /** 56 | * For future implementations 57 | */ 58 | CdsAnnotation = "CdsAnnotation" 59 | } 60 | 61 | export interface SearchResult<T> { 62 | results: T[]; 63 | count?: number; 64 | } 65 | 66 | export interface FieldFilter { 67 | fieldName?: string; 68 | value?: string; 69 | ranges?: FilterCond[]; 70 | items?: FilterItem[]; 71 | } 72 | 73 | export interface FilterItem { 74 | key: string; 75 | text?: string; 76 | } 77 | 78 | export interface FilterCond { 79 | keyField?: string; 80 | operation: string; 81 | value1?: any; 82 | value2?: any; 83 | exclude?: boolean; 84 | } 85 | 86 | export interface SortCond { 87 | fieldName: string; 88 | sortDirection?: "Ascending" | "Descending"; 89 | } 90 | 91 | export interface AggregationCond { 92 | fieldName: string; 93 | operation?: string; 94 | showIfGrouped?: boolean; 95 | } 96 | 97 | export interface ColumnConfig { 98 | fieldName: string; 99 | index?: number; 100 | visible?: boolean; 101 | } 102 | 103 | export interface DbEntity { 104 | type: EntityType; 105 | name: string; 106 | description?: string; 107 | packageName?: string; 108 | isFavorite?: boolean; 109 | } 110 | 111 | export interface EntityVariantData { 112 | columnItems?: ColumnConfig[]; 113 | sortCond?: SortCond[]; 114 | aggregationCond?: AggregationCond[]; 115 | filterCond?: FilterCond[]; 116 | } 117 | 118 | export type SimpleBindingParams = { 119 | filters?: FilterCond[]; 120 | parameters?: object; 121 | }; 122 | 123 | export interface EntityVariant { 124 | key: string; 125 | text: string; 126 | author: string; 127 | executeOnSelection?: boolean; 128 | favorite?: boolean; 129 | global?: boolean; 130 | labelReadOnly?: boolean; 131 | readOnly?: boolean; 132 | dataString: string; 133 | data: EntityVariantData; 134 | } 135 | 136 | export interface EntityInfo { 137 | name?: string; 138 | rawName?: string; 139 | description?: string; 140 | } 141 | 142 | export interface EntityMetadata { 143 | entity: EntityInfo; 144 | parameters?: FieldMetadata[]; 145 | fields?: FieldMetadata[]; 146 | } 147 | 148 | /** 149 | * Declares properties that are needed for paging 150 | */ 151 | export interface PagingParams { 152 | $top: number; 153 | $count?: boolean; 154 | $skip?: number; 155 | } 156 | 157 | export class FieldMetadata { 158 | private _typeInstance: ODataType; 159 | private _tooltip: string; 160 | /** 161 | * The name of the column 162 | */ 163 | name: string; 164 | /** 165 | * Indicates if the field is a key field 166 | */ 167 | isKey?: boolean; 168 | /** 169 | * The data type of the column 170 | */ 171 | type: string; // use enum 172 | /** 173 | * The max number of characters/digits this column can hold 174 | */ 175 | maxLength?: number; 176 | /** 177 | * Number of digits a numerical type can hold including decimal places 178 | */ 179 | precision?: number; 180 | /** 181 | * Maximum number of digits right next to the decimal point 182 | */ 183 | scale?: number; 184 | /** 185 | * The data element assigned as column type 186 | */ 187 | rollname?: string; 188 | /** 189 | * The short description for the column 190 | */ 191 | shortDescription?: string; 192 | /** 193 | * The medium description for the column 194 | */ 195 | mediumDescription?: string; 196 | /** 197 | * The long description for the column 198 | */ 199 | longDescription?: string; 200 | /** 201 | * The tooltip for the column 202 | */ 203 | get tooltip(): string { 204 | if (!this._tooltip) { 205 | const description = this.fieldText || (this.description && this.description !== this.name) || "-"; 206 | this._tooltip = 207 | `${I18nUtil.getText("entity_field_description")}: ${description}\n` + 208 | `${I18nUtil.getText("entity_field_technicalName")}: ${this.name}`; 209 | } 210 | return this._tooltip; 211 | } 212 | /** 213 | * The DDIC description for the column 214 | */ 215 | fieldText?: string; 216 | /** 217 | * The description for the ui. 218 | */ 219 | description?: string; 220 | /** 221 | * Indicates if the field is filterable 222 | */ 223 | filterable?: boolean; 224 | /** 225 | * Indicates if the field is sortable 226 | */ 227 | sortable?: boolean; 228 | /** 229 | * Indicates if the field is a technical field and should not be displayed 230 | */ 231 | technical?: boolean; 232 | /** 233 | * The display format to be used for the field 234 | */ 235 | displayFormat?: string; 236 | /** 237 | * Indicates if there is a value help for the field available 238 | */ 239 | hasValueHelp?: boolean; 240 | /** 241 | * The type of the defined value help if the property "hasValueHelp" is true 242 | */ 243 | valueHelpType?: ValueHelpType; 244 | 245 | /** 246 | * Retrieves the type instance derived from the metadata of the field 247 | */ 248 | get typeInstance(): ODataType { 249 | if (!this._typeInstance) { 250 | const formatOptions: { displayFormat?: String } = {}; 251 | const constraints: { precision?: number; scale?: number; maxLength?: number } = {}; 252 | if (this.displayFormat) { 253 | formatOptions.displayFormat = this.displayFormat; 254 | } 255 | if (this.maxLength) { 256 | constraints.maxLength = this.maxLength; 257 | } 258 | if (this.precision || this.scale) { 259 | constraints.precision = this.precision; 260 | constraints.scale = this.scale; 261 | } 262 | this._typeInstance = TypeFactory.getType(this.type, formatOptions, constraints); 263 | } 264 | return this._typeInstance; 265 | } 266 | } 267 | 268 | export type DataRow = Record<string, unknown>; 269 | 270 | /** 271 | * Information about field in a Value Help dialog 272 | */ 273 | export class ValueHelpField extends FieldMetadata { 274 | isDescription?: boolean; 275 | visible?: boolean; 276 | /** 277 | * Width in CSS style, e.g. 9em 278 | */ 279 | width?: string; 280 | } 281 | 282 | interface ValueHelpInfo { 283 | /** 284 | * The name of the value help. Depending on the type of the value help 285 | * this can be either a domain, a check table or the name of DDIC search help 286 | */ 287 | valueHelpName: string; 288 | description?: string; 289 | /** 290 | * The type of the value help 291 | */ 292 | type: ValueHelpType; 293 | /** 294 | * The source table where the value help was detected 295 | */ 296 | sourceTab?: string; 297 | /** 298 | * The source field where the value help was detected 299 | */ 300 | sourceField?: string; 301 | } 302 | 303 | /** 304 | * Metadata of a value help for a field in an entity 305 | */ 306 | export interface ValueHelpMetadata extends ValueHelpInfo { 307 | /** 308 | * The targeted field of a DB entity 309 | */ 310 | targetField?: string; 311 | /** 312 | * Identifier of field that is to be used as the token key 313 | */ 314 | tokenKeyField?: string; 315 | /** 316 | * Identifier of field that is to be used as the token description 317 | */ 318 | tokenDescriptionField?: string; 319 | /** 320 | * Field metadata for Table/Filters in value help dialog 321 | */ 322 | fields?: ValueHelpField[]; 323 | /** 324 | * Ids of filter fields 325 | */ 326 | filterFields?: string[]; 327 | /** 328 | * Ids of output fields 329 | */ 330 | outputFields?: string[]; 331 | /** 332 | * Included value helps 333 | */ 334 | includedValueHelps?: ValueHelpMetadata[]; 335 | } 336 | 337 | /** 338 | * Describes a request to retrieve data via value help 339 | */ 340 | export interface ValueHelpRequest extends ValueHelpInfo { 341 | /** 342 | * Optional array of filter conditions 343 | */ 344 | filters?: FilterCond[]; 345 | /** 346 | * Optional array of sort items 347 | */ 348 | sortCond?: SortCond[]; 349 | /** 350 | * The maximum number of rows to retrieve 351 | */ 352 | maxRows?: number; 353 | } 354 | 355 | export interface QuerySettings { 356 | maxRows?: number; 357 | offset?: number; 358 | determineMaxRows?: boolean; 359 | reducedMemory?: boolean; 360 | } 361 | 362 | export interface OutputField extends ColumnConfig { 363 | function?: "MIN" | "MAX" | "AVG" | "SUM"; 364 | } 365 | 366 | export interface AggregationExpression { 367 | fieldName: string; 368 | } 369 | 370 | export interface HavingExpression { 371 | fieldName: string; 372 | function: "COUNT" | "MIN" | "MAX" | "AVG" | "SUM"; 373 | operation: string; 374 | value1: any; 375 | value2?: any; 376 | } 377 | 378 | export interface AggregationConfig { 379 | aggregationExpressions: AggregationExpression[]; 380 | havingExpressions?: HavingExpression[]; 381 | } 382 | 383 | export interface QueryRequest { 384 | settings?: QuerySettings; 385 | outputFields?: ColumnConfig[]; 386 | sortFields?: SortCond[]; 387 | filters?: FieldFilter[]; 388 | parameters?: FieldFilter[]; 389 | aggregations?: AggregationConfig; 390 | } 391 | -------------------------------------------------------------------------------- /dev/service/EntityService.ts: -------------------------------------------------------------------------------- 1 | import ajaxUtil from "./ajaxUtil"; 2 | import { 3 | DbEntity, 4 | EntityMetadata, 5 | EntityVariant, 6 | EntityType, 7 | ValueHelpMetadata, 8 | FieldMetadata, 9 | QueryRequest as QueryRequestData, 10 | EntitySearchScope, 11 | FieldType, 12 | ValueHelpType, 13 | PagingParams, 14 | SearchResult, 15 | DataRow 16 | } from "../model/types"; 17 | 18 | const BASE_SRV_URL = "/sap/zqdrtrest/entities"; 19 | const SUB_ENTITY_SRV_URL = `${BASE_SRV_URL}/{type}/{name}`; 20 | 21 | interface EntitiesSearchReqParams extends PagingParams { 22 | name?: string; 23 | description?: string; 24 | entityType?: EntityType; 25 | scope?: EntitySearchScope; 26 | } 27 | 28 | /** 29 | * Service to get meta data information about database entities 30 | */ 31 | export default class EntityService { 32 | /** 33 | * Retrieves metadata for entity 34 | * @param type type of the entity 35 | * @param name the name of the entity 36 | * @returns Promise with object from the response 37 | */ 38 | async getMetadata(type: string, name: string): Promise<EntityMetadata> { 39 | const response = await ajaxUtil.send( 40 | `${SUB_ENTITY_SRV_URL.replace("{type}", type).replace("{name}", encodeURIComponent(name))}/metadata`, 41 | { 42 | method: "GET" 43 | } 44 | ); 45 | if (response?.data?.fields) { 46 | const metadata: EntityMetadata = { 47 | entity: response.data.entity, 48 | fields: (response.data.fields as Record<string, any>[]).map(f => Object.assign(new FieldMetadata(), f)) 49 | }; 50 | 51 | if (response.data.parameters) { 52 | metadata.parameters = (response.data.parameters as Record<string, any>[]).map(f => 53 | Object.assign(new FieldMetadata(), f) 54 | ); 55 | } 56 | 57 | return metadata; 58 | } else { 59 | return null; 60 | } 61 | } 62 | 63 | /** 64 | * Retrieves entity data 65 | * @param type type of the entity 66 | * @param entity the name of the entity 67 | * @param queryRequestData the data to be passed in the data request 68 | * @returns the object from the response if response was ok 69 | */ 70 | async getEntityData(type: string, entity: string, queryRequest?: QueryRequestData): Promise<SearchResult<DataRow>> { 71 | const csrfToken = await ajaxUtil.fetchCSRF(); 72 | const response = await ajaxUtil.send( 73 | `${SUB_ENTITY_SRV_URL.replace("{type}", type).replace("{name}", encodeURIComponent(entity))}/queryResult`, 74 | { 75 | method: "POST", 76 | csrfToken, 77 | data: queryRequest ? JSON.stringify(queryRequest) : undefined 78 | } 79 | ); 80 | return response?.data; 81 | } 82 | 83 | /** 84 | * Retrieves value help metadata for a field in a DB entity 85 | * 86 | * @param entityName the name of an entity (DB table/DB view/CDS view) 87 | * @param entityType the type of the entity 88 | * @param valueHelpType the type of the value help for the entity field 89 | * @param field the name of the field for which the value help metatdata should be retrieved 90 | * @param fieldType the type of the field 91 | * @returns promise with metadata result of the found valuehelp 92 | */ 93 | async getValueHelpMetadataForField( 94 | entityName: string, 95 | entityType: EntityType, 96 | valueHelpType: ValueHelpType, 97 | field: string, 98 | fieldType: FieldType 99 | ): Promise<ValueHelpMetadata> { 100 | const response = await ajaxUtil.send( 101 | `${SUB_ENTITY_SRV_URL.replace("{type}", entityType).replace( 102 | "{name}", 103 | encodeURIComponent(entityName) 104 | )}/vhMetadata`, 105 | { 106 | method: "GET", 107 | data: { valueHelpType, field, fieldType } 108 | } 109 | ); 110 | return response?.data; 111 | } 112 | 113 | /** 114 | * Retrieves variants for entity 115 | * @param type type of the entity 116 | * @param name the name of the entity 117 | * @returns Promise of entity variants 118 | */ 119 | async getVariants(type: string, name: string): Promise<EntityVariant[]> { 120 | const response = await ajaxUtil.send( 121 | `${SUB_ENTITY_SRV_URL.replace("{type}", type).replace("{name}", encodeURIComponent(name))}/variants`, 122 | { 123 | method: "GET" 124 | } 125 | ); 126 | return response?.data; 127 | } 128 | 129 | /** 130 | * Searches for DB entities 131 | * @param nameFilter name filter value to search entities 132 | * @param descriptionFilter description filter value 133 | * @param entityType the type of entities to be searched 134 | * @param scope search scope for the db entities 135 | * @returns Promise with response result 136 | */ 137 | async findEntities( 138 | nameFilter: string, 139 | descriptionFilter?: string, 140 | entityType?: EntityType, 141 | scope?: EntitySearchScope, 142 | paging?: PagingParams 143 | ): Promise<SearchResult<DbEntity>> { 144 | const reqParams = { 145 | $top: paging?.$top || 200 146 | } as EntitiesSearchReqParams; 147 | if (paging?.$count) { 148 | reqParams.$count = true; 149 | } 150 | if (paging?.$skip) { 151 | reqParams.$skip = paging.$skip; 152 | } 153 | if (nameFilter) { 154 | reqParams.name = nameFilter; 155 | } 156 | if (descriptionFilter) { 157 | reqParams.description = descriptionFilter; 158 | } 159 | if (entityType && entityType !== EntityType.All) { 160 | reqParams.entityType = entityType; 161 | } 162 | if (scope && scope !== EntitySearchScope.All) { 163 | reqParams.scope = scope; 164 | } 165 | const response = await ajaxUtil.send(BASE_SRV_URL, { 166 | method: "GET", 167 | data: reqParams 168 | }); 169 | return response?.data; 170 | } 171 | 172 | /** 173 | * Marks the given entity as favorite for the current user 174 | * @param entityName the name of an entity 175 | * @param entityType the type of an entity 176 | */ 177 | async createFavorite(entityName: string, entityType?: EntityType): Promise<void> { 178 | const csrfToken = await ajaxUtil.fetchCSRF(); 179 | ajaxUtil.send( 180 | `${SUB_ENTITY_SRV_URL.replace("{type}", entityType).replace( 181 | "{name}", 182 | encodeURIComponent(entityName) 183 | )}/favorite`, 184 | { 185 | method: "POST", 186 | csrfToken 187 | } 188 | ); 189 | } 190 | 191 | /** 192 | * Delete the given entity favorite for the current user 193 | * @param entityName the name of an entity 194 | * @param entityType the type of an entity 195 | */ 196 | async deleteFavorite(entityName: string, entityType?: EntityType): Promise<void> { 197 | const csrfToken = await ajaxUtil.fetchCSRF(); 198 | ajaxUtil.send( 199 | `${SUB_ENTITY_SRV_URL.replace("{type}", entityType).replace( 200 | "{name}", 201 | encodeURIComponent(entityName) 202 | )}/favorite`, 203 | { 204 | method: "DELETE", 205 | csrfToken 206 | } 207 | ); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /dev/service/ValueHelpService.ts: -------------------------------------------------------------------------------- 1 | import { DataRow, SearchResult, ValueHelpMetadata, ValueHelpRequest, ValueHelpType } from "../model/types"; 2 | import ajaxUtil from "./ajaxUtil"; 3 | 4 | const BASE_URL = `/sap/zqdrtrest/valueHelpData`; 5 | /** 6 | * Service to retrieve information about value helps 7 | */ 8 | export default class ValueHelpService { 9 | /** 10 | * Retrieves value help metadata for a field in a DB entity 11 | * 12 | * @param valueHelpRequest the request information to fetch value help data 13 | * @returns promise with metadata result of the found valuehelp 14 | */ 15 | async retrieveValueHelpData(valueHelpRequest: ValueHelpRequest): Promise<SearchResult<DataRow>> { 16 | const csrfToken = await ajaxUtil.fetchCSRF(); 17 | const response = await ajaxUtil.send(`${BASE_URL}`, { 18 | method: "POST", 19 | data: JSON.stringify(valueHelpRequest), 20 | csrfToken 21 | }); 22 | return response?.data; 23 | } 24 | 25 | /** 26 | * Retrieves metadata of a named value help 27 | * 28 | * @param name the name of the value help 29 | * @param valueHelpType the type of the value help 30 | * @returns promise with metadata result of the found valuehelp 31 | */ 32 | async getValueHelpMetadata(name: string, valueHelpType: ValueHelpType): Promise<ValueHelpMetadata> { 33 | const response = await ajaxUtil.send(`/sap/zqdrtrest/vh/${valueHelpType}/${encodeURIComponent(name)}`, { 34 | method: "GET" 35 | }); 36 | return response?.data; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /dev/service/ajaxUtil.ts: -------------------------------------------------------------------------------- 1 | import SystemUtil from "../helper/systemUtil"; 2 | 3 | import jQuery from "sap/ui/thirdparty/jquery"; 4 | 5 | const SAP_LANGUAGE_PARAM = "sap-language"; 6 | const SAP_CLIENT_PARAM = "sap-client"; 7 | /** 8 | * CSRF Token Header 9 | */ 10 | const CSRF_TOKEN_HEADER = "X-CSRF-Token"; 11 | 12 | /** 13 | * Response from AJAX request 14 | */ 15 | export type AjaxResponse = { 16 | data?: any; 17 | status?: int; 18 | request: JQuery.jqXHR<any>; 19 | }; 20 | 21 | export type RequestOptions = { 22 | method: "GET" | "POST" | "PUT" | "HEAD" | "DELETE"; 23 | headers?: Record<string, string>; 24 | data?: object | string; 25 | dataType?: string; 26 | csrfToken?: string; 27 | username?: string; 28 | password?: string; 29 | }; 30 | 31 | const defaultReqOptions: RequestOptions = { 32 | method: "GET", 33 | dataType: "json", 34 | headers: {} 35 | }; 36 | 37 | /** 38 | * Adds SAP Query parameters to url 39 | * @param urlString url string 40 | * @returns url string with SAP query parameters 41 | */ 42 | function addSapQueryParamsToUrl(urlString = ""): string { 43 | const url = urlString.startsWith("http") ? new URL(urlString) : new URL(`http:/${urlString}`); 44 | if (!url.searchParams.has(SAP_LANGUAGE_PARAM)) { 45 | const language = SystemUtil.getCurrentLanguage(); 46 | if (language && language !== "") { 47 | url.searchParams.set(SAP_LANGUAGE_PARAM, language); 48 | } 49 | } 50 | if (!url.searchParams.has(SAP_CLIENT_PARAM)) { 51 | const client = SystemUtil.getCurrentClient(); 52 | if (client && client !== "") { 53 | url.searchParams.set(SAP_CLIENT_PARAM, client); 54 | } 55 | } 56 | 57 | return url.toString().slice(6); // remove starting 'http:/' part 58 | } 59 | 60 | class AjaxUtil { 61 | private _csrfToken: string = undefined; 62 | 63 | /** 64 | * Promisfied AJAX call 65 | * @param url request url 66 | * @param options options for the request 67 | * @param addSapQueryParams if <code>true</code> the sap query parameters 68 | * 'language' and 'client' will be added to the request url 69 | * @returns promise to ajax request 70 | * @public 71 | */ 72 | send(url: string, options = defaultReqOptions, addSapQueryParams = true): Promise<AjaxResponse> { 73 | if (addSapQueryParams) { 74 | url = addSapQueryParamsToUrl(url); 75 | } 76 | const headers = options?.headers ?? {}; 77 | this._addCSRFToRequestData(headers, options?.csrfToken); 78 | return new Promise((fnResolve, fnReject) => { 79 | jQuery.ajax({ 80 | url: url, 81 | headers: headers, 82 | method: options.method, 83 | username: options.username, 84 | dataType: options.dataType ?? "json", 85 | password: options.password, 86 | data: options.data, 87 | success: (data, status, jqXHR) => { 88 | fnResolve({ data, status: jqXHR.status, request: jqXHR }); 89 | }, 90 | error: (jqXHR, status, error) => { 91 | fnReject({ status: jqXHR.status, error: jqXHR.responseJSON || jqXHR.responseText || error }); 92 | } 93 | }); 94 | }); 95 | } 96 | 97 | /** 98 | * Fetches Data synchronously 99 | * @param url url for the request 100 | * @param options = defaultReqOptions, 101 | * @param addSapQueryParams if <code>true</code> the sap query parameters 102 | * 'language' and 'client' will be added to the request url 103 | * @returns the result of synchronous request 104 | */ 105 | sendSync(url: string, options = defaultReqOptions, addSapQueryParams = true): AjaxResponse { 106 | let response; 107 | if (addSapQueryParams) { 108 | url = addSapQueryParamsToUrl(url); 109 | } 110 | const headers = options?.headers ?? {}; 111 | this._addCSRFToRequestData(headers, options?.csrfToken); 112 | jQuery.ajax({ 113 | url: url, 114 | headers: headers, 115 | method: options.method, 116 | username: options.username, 117 | dataType: options.dataType ?? "json", 118 | password: options.password, 119 | data: options.data, 120 | async: false, 121 | success: (data, statusText, jqXHR) => { 122 | response = { data, status: jqXHR.status }; 123 | }, 124 | error: (jqXHR, statusText, error) => { 125 | response = { status: jqXHR.status, error: jqXHR.responseJSON || jqXHR.responseText || error }; 126 | } 127 | }); 128 | 129 | return response; 130 | } 131 | 132 | /** 133 | * Fetches CSRF token 134 | * @param invalidate if <code>true</code> the token will be fetched again from the backend 135 | * @returns the value of the CSRF-Token 136 | * @public 137 | */ 138 | async fetchCSRF(invalidate?: boolean): Promise<string> { 139 | if (invalidate) { 140 | this._csrfToken = ""; 141 | } 142 | if (this._csrfToken) { 143 | return this._csrfToken; 144 | } 145 | const result = await this.send("/sap/zqdrtrest/", { 146 | method: "HEAD", 147 | headers: { 148 | [CSRF_TOKEN_HEADER]: "Fetch", 149 | accept: "*/*" 150 | } 151 | }); 152 | this._csrfToken = result?.request?.getResponseHeader(CSRF_TOKEN_HEADER); 153 | return this._csrfToken; 154 | } 155 | 156 | private _addCSRFToRequestData(headers: Record<string, unknown>, csrfToken: string): void { 157 | if (!headers[CSRF_TOKEN_HEADER] && csrfToken) { 158 | headers[CSRF_TOKEN_HEADER] = csrfToken; 159 | } 160 | } 161 | } 162 | 163 | export default new AjaxUtil(); 164 | -------------------------------------------------------------------------------- /dev/state/BaseState.ts: -------------------------------------------------------------------------------- 1 | import AjaxJSONModel from "../model/AjaxJSONModel"; 2 | 3 | /** 4 | * Read only data of the state 5 | */ 6 | export type ReadOnlyStateData<T> = { +readonly [P in keyof T]: T[P] }; 7 | 8 | /** 9 | * Base for all state classes 10 | */ 11 | export default class BaseState<T> { 12 | protected data: T; 13 | protected noModelUpdates: boolean; 14 | private _model: AjaxJSONModel; 15 | private _observeModelChanges: boolean; 16 | 17 | constructor(stateData: T, observeModelChanges?: boolean) { 18 | this._observeModelChanges = observeModelChanges; 19 | this.setStateData(stateData); 20 | } 21 | 22 | turnOffModelUpdate(): void { 23 | this.noModelUpdates = true; 24 | } 25 | 26 | turnOnModelUpdate(): void { 27 | this.noModelUpdates = false; 28 | } 29 | 30 | /** 31 | * Returns the current state data as readonly 32 | * @returns the current state data 33 | */ 34 | getData(): ReadOnlyStateData<T> { 35 | return this.data; 36 | } 37 | 38 | /** 39 | * Returns the model of the state 40 | * @returns the model of the state 41 | */ 42 | getModel(): AjaxJSONModel { 43 | if (!this._model) { 44 | this._model = new AjaxJSONModel(this.data as any, this._observeModelChanges); 45 | } 46 | return this._model; 47 | } 48 | 49 | /** 50 | * Updates the model 51 | * @param forceUpdate whether or not a model update should be forced 52 | */ 53 | updateModel(forceUpdate?: boolean): void { 54 | if (this.noModelUpdates) { 55 | return; 56 | } 57 | if (this._model) { 58 | this._model.updateBindings(forceUpdate); 59 | this.data = this._model.getData(); 60 | } 61 | } 62 | 63 | /** 64 | * Complete update of the state object 65 | * @param stateData the new data for the object 66 | */ 67 | protected setStateData(stateData: T): void { 68 | this.data = stateData; 69 | if (this._model) { 70 | this._model.setData(this.data as any); 71 | } else { 72 | this.getModel(); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /dev/state/EntityState.ts: -------------------------------------------------------------------------------- 1 | import Log from "sap/base/Log"; 2 | 3 | import ValueHelpUtil from "../helper/valuehelp/ValueHelpUtil"; 4 | import { DataResult } from "../model/AjaxJSONModel"; 5 | import Entity, { ConfigurableEntity, TableFilters } from "../model/Entity"; 6 | import { DEFAULT_VISIBLE_COL_COUNT } from "../model/globalConsts"; 7 | import { 8 | EntityType, 9 | EntityMetadata, 10 | ColumnConfig, 11 | ValueHelpMetadata, 12 | ValueHelpType, 13 | FieldMetadata, 14 | QueryRequest, 15 | FieldType 16 | } from "../model/types"; 17 | import EntityService from "../service/EntityService"; 18 | import BaseState from "./BaseState"; 19 | 20 | /** 21 | * State for the current visible entity 22 | * @nonui5 23 | */ 24 | export default class EntityState extends BaseState<Entity> { 25 | private _entityService: EntityService; 26 | private _valueHelpMetadataMap: Record<string, ValueHelpMetadata> = {}; 27 | 28 | constructor() { 29 | super(new Entity()); 30 | this._entityService = new EntityService(); 31 | this.getModel().setDataProvider("/rows", { 32 | getData: this._loadData.bind(this) 33 | }); 34 | } 35 | exists(): boolean { 36 | return this.data.metadata?.fields?.length > 0; 37 | } 38 | setConfiguration(newSettings: ConfigurableEntity): void { 39 | this.data.columnsItems = newSettings?.columnsItems; 40 | this.data.aggregationCond = newSettings?.aggregationCond; 41 | this.data.sortCond = newSettings?.sortCond; 42 | this.updateModel(); 43 | } 44 | setColumnsItems(columnsItems: ColumnConfig[]): void { 45 | this.data.columnsItems = columnsItems; 46 | this.updateModel(); 47 | } 48 | getCurrentConfiguration(): ConfigurableEntity { 49 | return this.data; 50 | } 51 | setEntityInfo(name: string, type: EntityType): void { 52 | this.data.name = name; 53 | this.data.type = type; 54 | this.updateModel(); 55 | } 56 | setMetadata(metadata: EntityMetadata): void { 57 | this.data.metadata = metadata; 58 | this.updateModel(); 59 | } 60 | setFilters(filters: TableFilters): void { 61 | this.data.filters = filters; 62 | this.updateModel(); 63 | } 64 | setParameters(parameters: TableFilters): void { 65 | this.data.parameters = parameters; 66 | this.updateModel(); 67 | } 68 | /** 69 | * DataProvider for fetching query results 70 | */ 71 | private async _loadData(startIndex: number, length: number, determineLength?: boolean): Promise<DataResult> { 72 | const queryRequest = { 73 | settings: { 74 | maxRows: length, 75 | determineMaxRows: determineLength, 76 | offset: startIndex 77 | }, 78 | parameters: this.data.getParameters(), 79 | filters: this.data.getFilledFilters(), 80 | sortFields: this.data.sortCond, 81 | outputFields: this.data.getOutputFields(), 82 | aggregations: { 83 | aggregationExpressions: this.data.aggregationCond 84 | } 85 | } as QueryRequest; 86 | return this._entityService.getEntityData(this.data.type, this.data.name, queryRequest); 87 | } 88 | async loadVariants(): Promise<void> { 89 | try { 90 | const variantData = await this._entityService.getVariants(this.data.type, this.data.name); 91 | this.data.variants = [...variantData] || []; 92 | this.updateModel(); 93 | } catch (reqError) { 94 | const errorMessage = (reqError as any).error?.message; 95 | Log.error( 96 | `Variants for entity with type: ${this.data.type}, name: ${this.data.name} could not be loaded`, 97 | errorMessage 98 | ); 99 | } 100 | } 101 | async loadMetadata(): Promise<EntityMetadata> { 102 | const getDescription = (fieldMeta: FieldMetadata): string => { 103 | if (fieldMeta.mediumDescription) { 104 | return fieldMeta.mediumDescription; 105 | } else if (fieldMeta.shortDescription) { 106 | return fieldMeta.shortDescription; 107 | } else if (fieldMeta.longDescription) { 108 | if (fieldMeta.longDescription.length <= 25) { 109 | return fieldMeta.longDescription; 110 | } else { 111 | return `${fieldMeta.longDescription.substring(0, 22)}...`; 112 | } 113 | } else if (fieldMeta.fieldText) { 114 | if (fieldMeta.fieldText.length <= 25) { 115 | return fieldMeta.fieldText; 116 | } else { 117 | return `${fieldMeta.fieldText.substring(0, 22)}...`; 118 | } 119 | } else { 120 | return fieldMeta.name; 121 | } 122 | }; 123 | try { 124 | const entityMetadata = await this._entityService.getMetadata(this.data.type, this.data.name); 125 | this.noModelUpdates = true; 126 | if (entityMetadata?.fields) { 127 | for (let i = 0; i < entityMetadata.fields.length; i++) { 128 | const fieldMeta = entityMetadata.fields[i]; 129 | fieldMeta.description = getDescription(fieldMeta); 130 | if (i < DEFAULT_VISIBLE_COL_COUNT) { 131 | this.data.columnsItems.push({ 132 | fieldName: fieldMeta.name, 133 | visible: true, 134 | index: i 135 | }); 136 | } 137 | } 138 | } 139 | if (entityMetadata?.parameters) { 140 | const initialParamFilters = {} as TableFilters; 141 | for (let i = 0; i < entityMetadata.parameters.length; i++) { 142 | const fieldMeta = entityMetadata.parameters[i]; 143 | fieldMeta.description = getDescription(fieldMeta); 144 | 145 | // create filter entry for parameter as all parameters are mandatory at this time 146 | initialParamFilters[fieldMeta.name] = {}; 147 | } 148 | this.data.parameters = initialParamFilters; 149 | } 150 | this.setMetadata({ 151 | entity: entityMetadata.entity, 152 | fields: entityMetadata?.fields || [], 153 | parameters: entityMetadata?.parameters || [] 154 | }); 155 | this.noModelUpdates = false; 156 | this.updateModel(); 157 | } catch (reqError) { 158 | const errorMessage = (reqError as any).error?.message; 159 | this.reset(); 160 | Log.error( 161 | `Metadata for entity with type: ${this.data.type}, name: ${this.data.name} could not be determined`, 162 | errorMessage 163 | ); 164 | } 165 | return this.data.metadata; 166 | } 167 | /** 168 | * Retrieves value help metadata for the given field 169 | * @param fieldName metadata information of a field 170 | * @param isParam flag to indicate if field is a parameter 171 | * @returns promise with value help meta data 172 | */ 173 | async getFieldValueHelpInfo(fieldName: string, isParam?: boolean): Promise<ValueHelpMetadata> { 174 | const mappedFieldName = isParam ? `@param:${fieldName}` : fieldName; 175 | if (!this._valueHelpMetadataMap.hasOwnProperty(mappedFieldName)) { 176 | this._valueHelpMetadataMap[mappedFieldName] = await this._determineValueHelpInfo(fieldName, isParam); 177 | } 178 | return this._valueHelpMetadataMap[mappedFieldName]; 179 | } 180 | reset(): void { 181 | this._valueHelpMetadataMap = {}; 182 | this.setStateData(new Entity()); 183 | this.updateModel(true); 184 | } 185 | 186 | private async _determineValueHelpInfo(fieldName: string, isParam: boolean): Promise<ValueHelpMetadata> { 187 | const source = isParam ? "parameters" : "fields"; 188 | const fieldMeta = this.data.metadata[source].find(f => f.name === fieldName); 189 | 190 | switch (fieldMeta.valueHelpType) { 191 | case ValueHelpType.CheckTable: 192 | case ValueHelpType.DDICSearchHelp: 193 | case ValueHelpType.ElementaryDDICSearchHelp: 194 | case ValueHelpType.CollectiveDDICSearchHelp: 195 | case ValueHelpType.CdsAnnotation: 196 | try { 197 | const vhMetadataForField = await this._entityService.getValueHelpMetadataForField( 198 | this.data.name, 199 | this.data.type, 200 | fieldMeta.valueHelpType, 201 | fieldName, 202 | isParam ? FieldType.Parameter : FieldType.Normal 203 | ); 204 | if (fieldMeta.valueHelpType === ValueHelpType.DDICSearchHelp) { 205 | fieldMeta.valueHelpType = vhMetadataForField.type; 206 | } 207 | return vhMetadataForField; 208 | } catch (error) { 209 | Log.error( 210 | `Valuehelp metadata at column '${fieldMeta.name}' with type '${fieldMeta.valueHelpType}' could not be determined` 211 | ); 212 | return ValueHelpUtil.getNoVhMetadata(fieldMeta); 213 | } 214 | 215 | case ValueHelpType.DomainFixValues: 216 | return ValueHelpUtil.getDomainFixValuesVhMetadata(fieldMeta); 217 | 218 | default: 219 | return ValueHelpUtil.getNoVhMetadata(fieldMeta); 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /dev/state/StateRegistry.ts: -------------------------------------------------------------------------------- 1 | import EntityState from "./EntityState"; 2 | 3 | /** 4 | * Keeps singleton instances of states 5 | */ 6 | export default class StateRegistry { 7 | private static _entityState: EntityState; 8 | /** 9 | * Singleton 10 | */ 11 | // eslint-disable-next-line 12 | private constructor() {} 13 | /** 14 | * Retrieves the current entity state 15 | * @returns the current entity state 16 | */ 17 | static getEntityState(): EntityState { 18 | if (!StateRegistry._entityState) { 19 | StateRegistry._entityState = new EntityState(); 20 | } 21 | return StateRegistry._entityState; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /dev/test/changes_loader.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable strict */ 2 | /* eslint-disable no-unused-vars */ 3 | /* eslint-disable no-console */ 4 | /* eslint-disable no-undef */ 5 | //This file used only for loading the changes in the preview and not required to be checked in. 6 | //Loads and extends the openui5 FileListBaseConnector 7 | 8 | //For UI5 version >= 1.80, the location of the FileListBaseConnector is different 9 | const connectorPath = 10 | parseFloat(sap.ui.version) >= 1.8 11 | ? "sap/ui/fl/write/api/connectors/FileListBaseConnector" 12 | : "sap/ui/fl/initial/api/connectors/FileListBaseConnector"; 13 | 14 | sap.ui.define(["sap/base/util/merge", connectorPath], function (merge, FileListBaseConnector) { 15 | var aPromises = []; 16 | var trustedHosts = [/^localhost$/, /^.*.applicationstudio.cloud.sap$/]; 17 | var url = new URL(window.location.toString()); 18 | var isValidHost = trustedHosts.some(host => { 19 | return host.test(url.hostname); 20 | }); 21 | return merge({}, FileListBaseConnector, { 22 | getFileList: function () { 23 | return new Promise(function (resolve, reject) { 24 | // If no changes found, maybe because the app was executed without doing a build. 25 | // Check for changes folder and load the changes, if any. 26 | if (!isValidHost) { 27 | reject(console.log("cannot load flex changes: invalid host")); 28 | } 29 | $.ajax({ 30 | url: url.origin + "/changes/", 31 | type: "GET", 32 | cache: false 33 | }) 34 | .then(function (sChangesFolderContent) { 35 | var regex = /(\/changes\/[^"]*\.change)/g; 36 | var result = regex.exec(sChangesFolderContent); 37 | var aChanges = []; 38 | while (result !== null) { 39 | aChanges.push(result[1]); 40 | result = regex.exec(sChangesFolderContent); 41 | } 42 | resolve(aChanges); 43 | }) 44 | .fail(function (obj) { 45 | // No changes folder, then just resolve 46 | resolve(); 47 | }); 48 | }); 49 | } 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /dev/test/fakeLRep.json: -------------------------------------------------------------------------------- 1 | { 2 | "changes": [], 3 | "settings": { 4 | "isKeyUser": true, 5 | "isAtoAvailable": false, 6 | "isProductiveSystem": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /dev/test/flpSandbox.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <!-- Copyright (c) 2015 SAP AG, All Rights Reserved --> 4 | <head> 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 6 | <meta charset="UTF-8" /> 7 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 8 | <title>{{appTitle}} 9 | 10 | 25 | 56 | 57 | 58 | 59 | 60 | 75 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /dev/test/flpSandboxMockServer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{appTitle}} 9 | 10 | 25 | 56 | 57 | 58 | 59 | 60 | 75 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /dev/view/App.view.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /dev/view/Entity.view.xml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 22 | 24 | <Title text="({/metadata/entity/description})" 25 | visible="{= !!${/metadata/entity/description}}" /> 26 | </FlexBox> 27 | </f:heading> 28 | <f:actions> 29 | <ToggleButton icon="sap-icon://filter" 30 | pressed="{ui>/sideContentVisible}" 31 | type="Transparent" /> 32 | </f:actions> 33 | </f:DynamicPageTitle> 34 | </f:title> 35 | 36 | <f:content> 37 | <app:ToggleableSideContent sideContentVisible="{ui>/sideContentVisible}" 38 | sideContentPosition="End"> 39 | <app:content> 40 | <Panel height="100%"> 41 | <table:Table id="queryResultTable" 42 | columnMove="onColumnMove" 43 | enableBusyIndicator="true" 44 | beforeOpenContextMenu="onCellContextMenu" 45 | visibleRowCountMode="Auto"> 46 | <table:extension> 47 | <OverflowToolbar class="sapMTBHeader-CTX"> 48 | <Title text="{ 49 | parts: [ 50 | {path: 'i18n>entity_table_title'}, 51 | {path: '/rows/$count', type: 'sap.ui.model.odata.type.Int32'}], 52 | formatter: '.formatMessage' 53 | }" 54 | titleStyle="H4"/> 55 | <ToolbarSeparator /> 56 | <vm:VariantManagement id="variantManagement" 57 | useFavorites="true" 58 | showShare="true" 59 | showSetAsDefault="true" 60 | showExecuteOnSelection="true" 61 | standardItemText="Standard" 62 | variantItems="{/variants}"> 63 | <vm:variantItems> 64 | <vm:VariantItem author="{author}" 65 | key="{key}" 66 | text="{text}" 67 | executeOnSelection="{executeOnSelection}" 68 | favorite="{favorite}" 69 | global="{global}" 70 | labelReadOnly="{labelReadOnly}" 71 | readOnly="{readOnly}"/> 72 | </vm:variantItems> 73 | </vm:VariantManagement> 74 | <ToolbarSpacer /> 75 | <Label text="{i18n>entity_table_tb_maxRows}" 76 | labelFor="maxRowsInput"/> 77 | <Input value="{/maxRows}" 78 | id="maxRowsInput" 79 | submit="onUpdateData" 80 | width="5rem"> 81 | </Input> 82 | <ToolbarSeparator /> 83 | <Button type="Transparent" 84 | press="onTableSettings" 85 | icon="sap-icon://action-settings"></Button> 86 | <Button text="{i18n>entity_table_tb_refresh}" 87 | icon="sap-icon://refresh" 88 | type="Emphasized" 89 | press="onUpdateData" /> 90 | </OverflowToolbar> 91 | </table:extension> 92 | <table:contextMenu> 93 | <Menu /> 94 | </table:contextMenu> 95 | </table:Table> 96 | </Panel> 97 | </app:content> 98 | <app:sideContent> 99 | <app:CustomizationPanel title="{i18n>entity_customizationPanel_filter_title}" 100 | icon="sap-icon://filter"> 101 | <app:EntityFilterPanel> 102 | <app:parameterPanel> 103 | <app:SideFilterPanel headerText="{i18n>entity_sideParamPanel_title}" 104 | visible="{= ${/metadata/parameters/length} > 0}" 105 | filterCategory="parameters" 106 | expandable="true" 107 | expanded="true" 108 | availableFilterMetadata="{/metadata/parameters}" 109 | visibleFilters="{/parameters}" /> 110 | </app:parameterPanel> 111 | <app:filterPanel> 112 | <app:SideFilterPanel headerText="{i18n>entity_sideFilterPanel_title}" 113 | availableFilterMetadata="{/metadata/fields}" 114 | visibleFilters="{/filters}"/> 115 | </app:filterPanel> 116 | </app:EntityFilterPanel> 117 | </app:CustomizationPanel> 118 | </app:sideContent> 119 | </app:ToggleableSideContent> 120 | </f:content> 121 | </f:DynamicPage> 122 | </mvc:View> -------------------------------------------------------------------------------- /dev/view/MainPage.view.xml: -------------------------------------------------------------------------------- 1 | <mvc:View controllerName="com.devepos.qdrt.controller.MainPage" 2 | xmlns:mvc="sap.ui.core.mvc" 3 | xmlns:c="sap.ui.core" 4 | xmlns:vm="sap.ui.comp.variants" 5 | xmlns:svm="sap.ui.comp.smartvariants" 6 | xmlns:fb="sap.ui.comp.filterbar" 7 | xmlns="sap.m" 8 | xmlns:f="sap.f" 9 | xmlns:layout="sap.ui.layout" 10 | height="100%"> 11 | <f:DynamicPage class="mainpage" 12 | toggleHeaderOnTitleClick="true"> 13 | <f:title> 14 | <f:DynamicPageTitle> 15 | <f:heading> 16 | <svm:SmartVariantManagementUi2 id="variantManagement" /> 17 | </f:heading> 18 | </f:DynamicPageTitle> 19 | </f:title> 20 | <f:header> 21 | <f:DynamicPageHeader pinnable="true"> 22 | <f:content> 23 | <fb:FilterBar id="filterbar" 24 | showFilterConfiguration="false" 25 | search="_onSearch" 26 | useToolbar="false" 27 | persistencyKey="DevEposQdrt_MainPage_PersCont" 28 | showGoOnFB="true"> 29 | <fb:filterGroupItems> 30 | <fb:FilterGroupItem name="scopeSelection" 31 | groupName="_MISC" 32 | visibleInFilterBar="true" 33 | label="{i18n>dbEntities_filterbar_scopeFilter_label}"> 34 | <fb:control> 35 | <Select selectedKey="{ui>/selectedSearchScope}" 36 | change="_onFilterChange"> 37 | <items> 38 | <c:Item key="all" 39 | text="{i18n>dbEntities_filterbar_scopeFilter_all}" /> 40 | <c:Item key="favorites" 41 | text="{i18n>dbEntities_filterbar_scopeFilter_favs}" /> 42 | </items> 43 | </Select> 44 | </fb:control> 45 | </fb:FilterGroupItem> 46 | <fb:FilterGroupItem name="typeSelection" 47 | groupName="_MISC" 48 | visibleInFilterBar="true" 49 | label="{i18n>dbEntities_filterbar_entityTypeFilter_label}"> 50 | <fb:control> 51 | <Select selectedKey="{ui>/selectedEntityType}" 52 | change="_onFilterChange"> 53 | <items> 54 | <c:Item key="all" 55 | text="{i18n>dbEntities_filterbar_entityTypeFilter_all}" /> 56 | <c:Item key="T" 57 | text="{i18n>dbEntity_type_table}" /> 58 | <c:Item key="V" 59 | text="{i18n>dbEntity_type_view}" /> 60 | <c:Item key="C" 61 | text="{i18n>dbEntity_type_cds}" /> 62 | </items> 63 | </Select> 64 | </fb:control> 65 | </fb:FilterGroupItem> 66 | <fb:FilterGroupItem name="nameFilter" 67 | groupName="_MISC" 68 | visibleInFilterBar="true" 69 | label="{i18n>dbEntities_filterbar_entityNameFilter_label}"> 70 | <fb:control> 71 | <Input value="{ui>/nameFilter}" 72 | maxLength="30" 73 | submit="_onSearch" 74 | change="_onFilterChange"/> 75 | </fb:control> 76 | </fb:FilterGroupItem> 77 | <fb:FilterGroupItem name="desscriptionFilter" 78 | groupName="_MISC" 79 | visibleInFilterBar="true" 80 | label="{i18n>dbEntities_filterbar_entityDescrFilter_label}"> 81 | <fb:control> 82 | <Input value="{ui>/descriptionFilter}" 83 | maxLength="40" 84 | submit="_onSearch" 85 | change="_onFilterChange"/> 86 | </fb:control> 87 | </fb:FilterGroupItem> 88 | </fb:filterGroupItems> 89 | </fb:FilterBar> 90 | </f:content> 91 | </f:DynamicPageHeader> 92 | </f:header> 93 | <f:content> 94 | <Table id="foundEntitiesTable" 95 | width="100%" 96 | growing="true" 97 | growingThreshold="25" 98 | growingScrollToLoad="true" 99 | sticky="ColumnHeaders,HeaderToolbar"> 100 | <headerToolbar> 101 | <Toolbar > 102 | <Title text="{ 103 | parts: [ 104 | {path: 'i18n>dbEntities_table_title'}, 105 | {path: '/foundEntities/$count', type: 'sap.ui.model.odata.type.Int32'}], 106 | formatter: '.formatMessage' 107 | }" /> 108 | <ToolbarSpacer /> 109 | </Toolbar> 110 | </headerToolbar> 111 | <columns> 112 | <Column width="4rem"> 113 | <Text text="{i18n>dbEntities_table_col_type}" /> 114 | </Column> 115 | <Column width="3.5rem"> 116 | <Text text="{i18n>dbEntities_table_col_favorite}" 117 | tooltip="{i18n>dbEntities_table_col_ttip_favorite}"/> 118 | </Column> 119 | <Column> 120 | <Text text="{i18n>dbEntities_table_col_name}" /> 121 | </Column> 122 | <Column> 123 | <Text text="{i18n>dbEntities_table_col_package}" /> 124 | </Column> 125 | </columns> 126 | </Table> 127 | </f:content> 128 | </f:DynamicPage> 129 | </mvc:View> 130 | -------------------------------------------------------------------------------- /img/entity_page_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevEpos/quick-data-reporter/67811c2b53e8c8f2e7ebd18da70b6ee466c602c9/img/entity_page_sample.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quick-data-reporter", 3 | "version": "0.12.0", 4 | "description": "Quick Data Reporter", 5 | "private": true, 6 | "author": { 7 | "email": "stocki87@gmail.com", 8 | "name": "stockbal" 9 | }, 10 | "scripts": { 11 | "lint": "eslint --ext .ts,.js dev", 12 | "ts-typecheck": "tsc --noEmit", 13 | "build:ts": "babel dev --out-dir webapp --source-maps true --extensions \".ts,.js\" --copy-files", 14 | "compile:less": "less-watch-compiler dev/css webapp/css main.less --run-once", 15 | "watch:ts": "babel dev --out-dir webapp --source-maps inline --extensions \".ts,.js\" --copy-files --watch", 16 | "watch:less": "less-watch-compiler dev/css webapp/css main.less", 17 | "deploy:ui5": "ui5 build --include-task=generateManifestBundle generateCachebusterInfo --clean-dest", 18 | "build:ui5": "ui5 build --include-task=generateManifestBundle generateCachebusterInfo --exclude-task=ui5-task-nwabap-deployer --clean-dest", 19 | "start:ui5": "ui5 serve --port 1081 --open \"test/flpSandbox.html?sap-language=EN#masterDetail-display\"", 20 | "start:ui5_mock": "ui5 serve --config ui5-mock.yaml --port 1081 --open \"test/flpSandboxMockServer.html?sap-language=EN#masterDetail-display\"", 21 | "start:ui5_dist": "ui5 serve --config ui5-dist.yaml --port 1081 --open \"test/flpSandbox.html?sap-language=EN#masterDetail-display\"", 22 | "build": "npm-run-all clean ts-typecheck build:ts compile:less build:ui5", 23 | "clean": "rimraf webapp dist", 24 | "deploy": "npm-run-all clean ts-typecheck build:ts compile:less deploy:ui5", 25 | "start": "npm-run-all clean build:ts --parallel watch:ts watch:less start:ui5", 26 | "start:mock": "npm-run-all clean build:ts --parallel watch:ts watch:less start:ui5_mock" 27 | }, 28 | "browserslist": [ 29 | "> 1%", 30 | "last 2 versions", 31 | "not dead", 32 | "not ie 11" 33 | ], 34 | "devDependencies": { 35 | "@babel/cli": "^7.13.16", 36 | "@babel/core": "^7.14.2", 37 | "@babel/eslint-parser": "^7.14.2", 38 | "@babel/plugin-proposal-nullish-coalescing-operator": "7.14.5", 39 | "@babel/preset-env": "^7.14.2", 40 | "@babel/preset-typescript": "7.14.5", 41 | "@sapui5/ts-types-esm": "1.91.0", 42 | "@types/jquery": "3.5.5", 43 | "@types/sinon": "10.0.2", 44 | "@typescript-eslint/eslint-plugin": "^4.28.0", 45 | "@typescript-eslint/parser": "^4.28.0", 46 | "babel-preset-transform-ui5": "^7.0.3", 47 | "dotenv": "^9.0.2", 48 | "eslint": "^7.26.0", 49 | "eslint-config-prettier": "^8.3.0", 50 | "eslint-plugin-prettier": "^3.4.0", 51 | "less-watch-compiler": "^1.15.2", 52 | "npm-run-all": "^4.1.5", 53 | "prettier": "^2.3.0", 54 | "rimraf": "^3.0.2", 55 | "typescript": "4.3.4", 56 | "ui5-middleware-livereload": "^0.5.4", 57 | "ui5-middleware-route-proxy": "1.0.9", 58 | "ui5-middleware-servestatic": "^0.3.4", 59 | "ui5-task-nwabap-deployer": "1.0.15" 60 | }, 61 | "ui5": { 62 | "dependencies": [ 63 | "ui5-middleware-livereload", 64 | "ui5-middleware-route-proxy", 65 | "ui5-middleware-servestatic", 66 | "ui5-task-nwabap-deployer" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "es2015", 5 | "lib": ["DOM", "ES2016"], 6 | "experimentalDecorators": true, 7 | "removeComments": false, 8 | "skipLibCheck": true, 9 | "strictNullChecks": false, 10 | "strictPropertyInitialization": false, 11 | "preserveConstEnums": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": false, 14 | "allowJs": true, 15 | "strict": true, 16 | "outDir": "./tsc_out", 17 | "baseUrl": "./", 18 | "rootDir": "./dev", 19 | "paths": { 20 | "com/devepos/qdrt/*": ["./dev/*"], 21 | "sap/ui/thirdparty/jquery": ["node_modules/@types/jquery/index.d.ts"] 22 | }, 23 | "types": ["@sapui5/ts-types-esm/types"] 24 | }, 25 | "include": ["./dev/**/*", "types"] 26 | } 27 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | // Define any types that should be globally available 2 | interface ResizableControl { 3 | setHeight?(height: string): void; 4 | getHeight?(): string; 5 | setWidth?(width: string): void; 6 | getWidth?(): string; 7 | } 8 | -------------------------------------------------------------------------------- /types/sap.ui.comp.d.ts: -------------------------------------------------------------------------------- 1 | // Expose some needed properties/methods that are not included 2 | // in the public interface 3 | 4 | declare module "sap/ui/comp/valuehelpdialog/ValueHelpDialog" { 5 | import Text from "sap/m/Text"; 6 | import Button from "sap/m/Button"; 7 | 8 | export default interface ValueHelpDialog { 9 | oSelectionTitle: Text; 10 | oSelectionButton: Button; 11 | resetTableState(); 12 | /** 13 | * Adds or removes a token 14 | * @private 15 | */ 16 | _addRemoveTokenByKey(key: string, row: any, add: boolean): void; 17 | /** 18 | * Rotates the icon for the collective value help popover 19 | * @private 20 | */ 21 | _rotateSelectionButtonIcon(flag: boolean); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /types/sap.ui.core.d.ts: -------------------------------------------------------------------------------- 1 | // Expose some needed properties/methods that are not included 2 | // in the public interface 3 | 4 | import Context from "sap/ui/model/Context"; 5 | import Model from "sap/ui/model/Model"; 6 | import Binding from "sap/ui/model/Binding"; 7 | 8 | declare module "sap/ui/model/Model" { 9 | export default interface Model { 10 | iSizeLimit: number; 11 | mContexts: Record<string, Context>; 12 | getContext(path: string): Context; 13 | getBindings(): Binding[]; 14 | } 15 | } 16 | 17 | declare module "sap/ui/model/Binding" { 18 | export default interface Binding<T extends Model> { 19 | oModel: T; 20 | sPath: string; 21 | oContext: Context; 22 | bSuspended: boolean; 23 | /** 24 | * @private 25 | */ 26 | _fireChange(parameters: object): void; 27 | /** 28 | * @private 29 | */ 30 | _fireRefresh(parameters: object): void; 31 | /** 32 | * @private 33 | */ 34 | getContext(): Context; 35 | } 36 | } 37 | 38 | declare module "sap/ui/model/ListBinding" { 39 | export default interface ListBinding<T extends Model> extends Binding<T> { 40 | /** 41 | * @private 42 | */ 43 | getContextData(context: Context): any; 44 | } 45 | } 46 | 47 | declare module "sap/ui/model/json/JSONModel" { 48 | export default interface JSONModel { 49 | oData: object; 50 | bObserve: boolean; 51 | checkUpdate(forceUpdate?: boolean, async?: boolean): void; 52 | resolve(path: string, context?: object): string; 53 | _getObject(path: string, context?: object): any; 54 | observeData(): void; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /types/sap.ui.table.d.ts: -------------------------------------------------------------------------------- 1 | declare module "sap/ui/table/Table" { 2 | export default interface Table { 3 | /** 4 | * Internal method to enable a timout during moving the scrollbar 5 | * in the table 6 | * @private 7 | */ 8 | _setLargeDataScrolling(enable: boolean): boolean; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ui5-dist.yaml: -------------------------------------------------------------------------------- 1 | specVersion: "2.3" 2 | metadata: 3 | name: quick-data-reporter 4 | type: application 5 | resources: 6 | configuration: 7 | propertiesFileSourceEncoding: UTF-8 8 | paths: 9 | webapp: dist 10 | server: 11 | customMiddleware: 12 | - name: ui5-middleware-servestatic 13 | afterMiddleware: compression 14 | mountPath: /resources 15 | configuration: 16 | rootPath: ${env.SAPUI5_RESOURCES} 17 | - name: ui5-middleware-servestatic 18 | afterMiddleware: compression 19 | mountPath: /test-resources 20 | configuration: 21 | rootPath: ${env.SAPUI5_TEST_RESOURCES} 22 | # proxy to backend service 23 | - name: ui5-middleware-route-proxy 24 | afterMiddleware: compression 25 | configuration: 26 | /sap/zqdrtrest: 27 | target: PROXY_TARGET 28 | auth: 29 | user: PROXY_AUTH_USER 30 | pass: PROXY_AUTH_PASS 31 | -------------------------------------------------------------------------------- /ui5-mock.yaml: -------------------------------------------------------------------------------- 1 | specVersion: "2.3" 2 | metadata: 3 | name: quick-data-reporter 4 | type: application 5 | framework: 6 | name: SAPUI5 7 | version: 1.84.1 8 | libraries: 9 | - name: sap.ui.core 10 | - name: sap.ui.comp 11 | - name: sap.m 12 | - name: sap.ui.layout 13 | - name: sap.uxap 14 | - name: sap.tnt 15 | - name: sap.f 16 | - name: sap.ushell 17 | - name: sap.suite.ui.generic.template 18 | - name: sap.ui.generic.app 19 | - name: themelib_sap_belize 20 | - name: themelib_sap_fiori_3 21 | resources: 22 | configuration: 23 | propertiesFileSourceEncoding: UTF-8 24 | server: 25 | customMiddleware: 26 | - name: ui5-middleware-livereload 27 | afterMiddleware: compression 28 | configuration: 29 | debug: true 30 | ext: "xml,json,properties" 31 | port: 35729 32 | path: "webapp" 33 | -------------------------------------------------------------------------------- /ui5.yaml: -------------------------------------------------------------------------------- 1 | specVersion: "2.3" 2 | metadata: 3 | name: quick-data-reporter 4 | type: application 5 | framework: 6 | name: SAPUI5 7 | version: 1.84.1 8 | libraries: 9 | - name: sap.ui.core 10 | - name: sap.ui.comp 11 | - name: sap.m 12 | - name: sap.ui.layout 13 | - name: sap.uxap 14 | - name: sap.tnt 15 | - name: sap.f 16 | - name: sap.ushell 17 | - name: sap.suite.ui.generic.template 18 | - name: sap.ui.generic.app 19 | - name: themelib_sap_belize 20 | - name: themelib_sap_fiori_3 21 | resources: 22 | configuration: 23 | propertiesFileSourceEncoding: UTF-8 24 | builder: 25 | resources: 26 | excludes: 27 | - "/test/**" 28 | - "/localService/**" 29 | - "**/.eslintrc" 30 | customTasks: 31 | - name: ui5-task-nwabap-deployer 32 | afterTask: generateCachebusterInfo 33 | configuration: 34 | resources: 35 | pattern: "**/*.*" 36 | ui5: 37 | language: EN 38 | package: $QDRT_UI5 39 | bspContainer: ZDEVEPOS_QDRT 40 | bspContainerText: Quick Data Reporter 41 | calculateApplicationIndex: true 42 | server: 43 | customMiddleware: 44 | # proxy to backend service 45 | - name: ui5-middleware-route-proxy 46 | afterMiddleware: compression 47 | configuration: 48 | /sap/zqdrtrest: 49 | target: PROXY_TARGET 50 | auth: 51 | user: PROXY_AUTH_USER 52 | pass: PROXY_AUTH_PASS 53 | - name: ui5-middleware-livereload 54 | afterMiddleware: compression 55 | configuration: 56 | debug: true 57 | ext: "xml,json,properties" 58 | port: 35729 59 | path: "webapp" 60 | --------------------------------------------------------------------------------