Pull request #186: Updates

Merge in RED/ui from updates to master

* commit 'b3ce5d7914cf0e9bed1bce12008cb52b829163ac':
  other deps updates
  nx updates
  update nx & eslint plugins
  update other dependencies
  update angular material and fix update errors
  migrate to ng12
  nx updates
  set max line length to 100 and set indent size for .json files to 2
This commit is contained in:
Timo Bejan 2021-05-18 14:06:27 +02:00
commit 61e454fa72
223 changed files with 10729 additions and 6760 deletions

View File

@ -8,7 +8,11 @@ indent_size = 4
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
ij_html_quote_style = double ij_html_quote_style = double
max_line_length = 100
[*.md] [*.md]
max_line_length = off max_line_length = off
trim_trailing_whitespace = false trim_trailing_whitespace = false
[{*.json, .prettierrc, .eslintrc}]
indent_size = 2

View File

@ -1,182 +1,205 @@
{ {
"root": true, "root": true,
"ignorePatterns": ["**/*"], "ignorePatterns": ["**/*"],
"plugins": ["@nrwl/nx"], "plugins": ["@nrwl/nx"],
"overrides": [ "overrides": [
{ {
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": { "rules": {
"@nrwl/nx/enforce-module-boundaries": [ "@nrwl/nx/enforce-module-boundaries": [
"error", "error",
{ {
"enforceBuildableLibDependency": true, "enforceBuildableLibDependency": true,
"allow": [], "allow": [
"depConstraints": [ "@redaction/red-ui-http",
{ "@redaction/red-cache",
"sourceTag": "*", "@services/**",
"onlyDependOnLibsWithTags": ["*"] "@components/**",
} "@guards/**",
] "@i18n/**",
} "@state/**",
] "@utils/**",
} "@models/**",
}, "@environments/**",
{ "@shared/**",
"files": ["*.ts", "*.tsx"], "@app-config/**",
"extends": ["plugin:@nrwl/nx/typescript"], "@upload-download/**"
"rules": {} ],
}, "depConstraints": [
{ {
"files": ["*.js", "*.jsx"], "sourceTag": "*",
"extends": ["plugin:@nrwl/nx/javascript"], "onlyDependOnLibsWithTags": ["*"]
"rules": {} }
}, ]
{ }
"files": ["*.ts"], ]
"rules": { }
"@angular-eslint/component-selector": [ },
"error", {
{ "files": ["*.ts", "*.tsx"],
"type": "element", "extends": ["plugin:@nrwl/nx/typescript"],
"prefix": "app", "rules": {}
"style": "kebab-case" },
} {
], "files": ["*.js", "*.jsx"],
"@angular-eslint/directive-selector": [ "extends": ["plugin:@nrwl/nx/javascript"],
"error", "rules": {}
{ },
"type": "attribute", {
"prefix": "app", "files": ["*.ts"],
"style": "camelCase" "rules": {
} "@angular-eslint/component-selector": [
], "error",
"@angular-eslint/no-conflicting-lifecycle": "error", {
"@angular-eslint/no-host-metadata-property": "error", "type": "element",
"@angular-eslint/no-input-rename": "error", "prefix": "app",
"@angular-eslint/no-inputs-metadata-property": "error", "style": "kebab-case"
"@angular-eslint/no-output-native": "error", }
"@angular-eslint/no-output-on-prefix": "error", ],
"@angular-eslint/no-output-rename": "error", "@angular-eslint/directive-selector": [
"@angular-eslint/no-outputs-metadata-property": "error", "error",
"@angular-eslint/use-lifecycle-interface": "error", {
"@angular-eslint/use-pipe-transform-interface": "error", "type": "attribute",
"@typescript-eslint/consistent-type-definitions": "error", "prefix": "app",
"@typescript-eslint/dot-notation": "off", "style": "camelCase"
"@typescript-eslint/no-explicit-any": "off", }
"@typescript-eslint/ban-types": "off", ],
"@typescript-eslint/explicit-member-accessibility": [ "@angular-eslint/no-conflicting-lifecycle": "error",
"warn", "@angular-eslint/no-host-metadata-property": "error",
{ "@angular-eslint/no-input-rename": "error",
"accessibility": "no-public" "@angular-eslint/no-inputs-metadata-property": "error",
} "@angular-eslint/no-output-native": "error",
], "@angular-eslint/no-output-on-prefix": "error",
"@typescript-eslint/member-ordering": "error", "@angular-eslint/no-output-rename": "error",
"@typescript-eslint/naming-convention": [ "@angular-eslint/no-outputs-metadata-property": "error",
"error", "@angular-eslint/use-lifecycle-interface": "error",
{ "@angular-eslint/use-pipe-transform-interface": "error",
"selector": "memberLike", "@typescript-eslint/consistent-type-definitions": "error",
"modifiers": ["readonly"], "@typescript-eslint/dot-notation": "off",
"format": ["UPPER_CASE", "camelCase"] "@typescript-eslint/no-explicit-any": "off",
}, "@typescript-eslint/ban-types": "off",
{ "@typescript-eslint/explicit-member-accessibility": [
"selector": "enumMember", "warn",
"format": ["UPPER_CASE"] {
}, "accessibility": "no-public"
{ }
"selector": "memberLike", ],
"modifiers": ["private"], "@typescript-eslint/member-ordering": "error",
"format": ["camelCase"], "@typescript-eslint/naming-convention": [
"leadingUnderscore": "require" "error",
}, {
{ "selector": "memberLike",
"selector": "memberLike", "modifiers": ["readonly"],
"modifiers": ["protected"], "format": ["UPPER_CASE", "camelCase"]
"format": ["camelCase"], },
"leadingUnderscore": "require" {
}, "selector": "enumMember",
{ "format": ["UPPER_CASE"]
"selector": "memberLike", },
"modifiers": ["private", "readonly"], {
"format": ["UPPER_CASE", "camelCase"], "selector": "memberLike",
"leadingUnderscore": "require" "modifiers": ["private"],
} "format": ["camelCase"],
], "leadingUnderscore": "require"
"@typescript-eslint/no-empty-function": "off", },
"@typescript-eslint/no-empty-interface": "error", {
"@typescript-eslint/no-inferrable-types": [ "selector": "memberLike",
"error", "modifiers": ["protected"],
{ "format": ["camelCase"],
"ignoreParameters": true "leadingUnderscore": "require"
} },
], {
"@typescript-eslint/no-misused-new": "error", "selector": "memberLike",
"@typescript-eslint/no-non-null-assertion": "error", "modifiers": ["private", "readonly"],
"@typescript-eslint/no-shadow": [ "format": ["UPPER_CASE", "camelCase"],
"error", "leadingUnderscore": "require"
{ }
"hoist": "all" ],
} "@typescript-eslint/no-empty-function": "off",
], "@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-unused-expressions": "error", "@typescript-eslint/no-inferrable-types": [
"@typescript-eslint/prefer-function-type": "error", "error",
"@typescript-eslint/unified-signatures": "error", {
"arrow-body-style": "error", "ignoreParameters": true
"constructor-super": "error", }
"eqeqeq": ["error", "smart"], ],
"guard-for-in": "error", "@typescript-eslint/no-misused-new": "error",
"id-blacklist": "off", "@typescript-eslint/no-non-null-assertion": "error",
"id-match": "off", "@typescript-eslint/no-shadow": [
"import/no-deprecated": "warn", "error",
"no-bitwise": "error", {
"no-caller": "error", "hoist": "all"
"no-console": [ }
"error", ],
{ "@typescript-eslint/no-unused-expressions": "error",
"allow": [ "@typescript-eslint/prefer-function-type": "error",
"log", "@typescript-eslint/unified-signatures": "error",
"warn", "arrow-body-style": "error",
"dir", "constructor-super": "error",
"timeLog", "eqeqeq": ["error", "smart"],
"assert", "guard-for-in": "error",
"clear", "id-blacklist": "off",
"count", "id-match": "off",
"countReset", "import/no-deprecated": "warn",
"group", "no-bitwise": "error",
"groupEnd", "no-caller": "error",
"table", "no-console": [
"dirxml", "error",
"error", {
"groupCollapsed", "allow": [
"Console", "log",
"profile", "warn",
"profileEnd", "dir",
"timeStamp", "timeLog",
"context" "assert",
] "clear",
} "count",
], "countReset",
"no-debugger": "error", "group",
"no-empty": "off", "groupEnd",
"no-eval": "error", "table",
"no-fallthrough": "error", "dirxml",
"no-new-wrappers": "error", "error",
"no-restricted-imports": ["error", "rxjs/Rx"], "groupCollapsed",
"no-throw-literal": "error", "Console",
"no-undef-init": "error", "profile",
"no-underscore-dangle": "off", "profileEnd",
"no-var": "error", "timeStamp",
"prefer-const": "error", "context"
"radix": "error" ]
}, }
"plugins": ["eslint-plugin-import", "@angular-eslint/eslint-plugin", "@typescript-eslint"] ],
}, "no-debugger": "error",
{ "no-empty": "off",
"files": ["*.html"], "no-eval": "error",
"rules": { "no-fallthrough": "error",
"@angular-eslint/template/banana-in-box": "error", "no-new-wrappers": "error",
"@angular-eslint/template/no-negated-async": "error" "no-restricted-imports": ["error", "rxjs/Rx"],
}, "no-throw-literal": "error",
"plugins": ["@angular-eslint/eslint-plugin-template"] "no-undef-init": "error",
} "no-underscore-dangle": "off",
] "no-var": "error",
"prefer-const": "error",
"radix": "error",
"max-len": [
"error",
{
"code": 100,
"tabWidth": 4,
"ignorePattern": "^import .*"
}
]
},
"plugins": ["eslint-plugin-import", "@angular-eslint/eslint-plugin", "@typescript-eslint"]
},
{
"files": ["*.html"],
"rules": {
"@angular-eslint/template/banana-in-box": "error",
"@angular-eslint/template/no-negated-async": "error",
"@angular-eslint/template/eqeqeq": "error"
},
"plugins": ["@angular-eslint/eslint-plugin-template"]
}
]
} }

View File

@ -2,3 +2,5 @@
/dist /dist
/coverage /coverage
/node_modules
/bamboo-specs

View File

@ -1,6 +1,6 @@
{ {
"useTabs": false, "useTabs": false,
"printWidth": 160, "printWidth": 100,
"tabWidth": 4, "tabWidth": 4,
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "none",
@ -10,6 +10,12 @@
"options": { "options": {
"parser": "angular" "parser": "angular"
} }
},
{
"files": ["*.json"],
"options": {
"tabWidth": 2
}
} }
] ]
} }

View File

@ -1,194 +1,200 @@
{ {
"version": 1, "$schema": "node_modules/@angular/cli/lib/config/schema.json",
"cli": { "version": 1,
"defaultCollection": "@nrwl/angular", "cli": {
"analytics": false, "defaultCollection": "@nrwl/angular",
"packageManager": "yarn" "analytics": false,
"packageManager": "yarn"
},
"defaultProject": "red-ui",
"schematics": {
"@nrwl/angular:application": {
"unitTestRunner": "jest",
"e2eTestRunner": "cypress"
}, },
"defaultProject": "red-ui", "@nrwl/angular:library": {
"schematics": { "unitTestRunner": "jest"
"@nrwl/angular:application": {
"unitTestRunner": "jest",
"e2eTestRunner": "cypress"
},
"@nrwl/angular:library": {
"unitTestRunner": "jest"
}
},
"projects": {
"red-ui": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "apps/red-ui",
"sourceRoot": "apps/red-ui/src",
"prefix": "redaction",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/apps/red-ui",
"index": "apps/red-ui/src/index.html",
"main": "apps/red-ui/src/main.ts",
"polyfills": "apps/red-ui/src/polyfills.ts",
"tsConfig": "apps/red-ui/tsconfig.app.json",
"aot": true,
"assets": [
"apps/red-ui/src/favicon.ico",
{
"glob": "**/*",
"input": "node_modules/@pdftron/webviewer/public/",
"output": "/assets/wv-resources/"
},
{
"glob": "**/*",
"input": "apps/red-ui/src/assets/",
"output": "/assets/"
},
{
"glob": "**/*",
"input": "node_modules/ace-builds/src-min/",
"output": "/assets/ace-builds"
},
"apps/red-ui/src/manifest.webmanifest"
],
"styles": ["apps/red-ui/src/styles.scss"],
"scripts": [
"node_modules/@pdftron/webviewer/webviewer.min.js",
"node_modules/ace-builds/src-min/ace.js",
"node_modules/ace-builds/src-min/mode-java.js",
"node_modules/ace-builds/src-min/theme-eclipse.js"
]
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "apps/red-ui/src/environments/environment.ts",
"with": "apps/red-ui/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
],
"serviceWorker": true,
"ngswConfigPath": "apps/red-ui/ngsw-config.json"
}
},
"outputs": ["{options.outputPath}"]
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "red-ui:build"
},
"configurations": {
"production": {
"browserTarget": "red-ui:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "red-ui:build"
}
},
"lint": {
"builder": "@nrwl/linter:eslint",
"options": {
"lintFilePatterns": ["apps/red-ui/src/**/*.ts", "apps/red-ui/src/**/*.html"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "apps/red-ui/jest.config.js",
"passWithNoTests": true
},
"outputs": ["coverage/apps/red-ui"]
}
}
},
"red-ui-http": {
"projectType": "library",
"root": "libs/red-ui-http",
"sourceRoot": "libs/red-ui-http/src",
"prefix": "redaction",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "libs/red-ui-http/tsconfig.lib.json",
"project": "libs/red-ui-http/ng-package.json"
}
},
"lint": {
"builder": "@nrwl/linter:eslint",
"options": {
"lintFilePatterns": ["libs/red-ui-http/src/**/*.ts", "libs/red-ui-http/src/**/*.html"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/red-ui-http/jest.config.js",
"passWithNoTests": true
},
"outputs": ["coverage/libs/red-ui-http"]
}
},
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
}
},
"red-cache": {
"projectType": "library",
"root": "libs/red-cache",
"sourceRoot": "libs/red-cache/src",
"prefix": "redaction",
"architect": {
"lint": {
"builder": "@nrwl/linter:eslint",
"options": {
"lintFilePatterns": ["libs/red-cache/src/**/*.ts", "libs/red-cache/src/**/*.html"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/red-cache/jest.config.js",
"passWithNoTests": true
},
"outputs": ["coverage/libs/red-cache"]
}
},
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
}
}
} }
},
"projects": {
"red-ui": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "apps/red-ui",
"sourceRoot": "apps/red-ui/src",
"prefix": "redaction",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/apps/red-ui",
"index": "apps/red-ui/src/index.html",
"main": "apps/red-ui/src/main.ts",
"polyfills": "apps/red-ui/src/polyfills.ts",
"tsConfig": "apps/red-ui/tsconfig.app.json",
"assets": [
"apps/red-ui/src/favicon.ico",
{
"glob": "**/*",
"input": "node_modules/@pdftron/webviewer/public/",
"output": "/assets/wv-resources/"
},
{
"glob": "**/*",
"input": "apps/red-ui/src/assets/",
"output": "/assets/"
},
{
"glob": "**/*",
"input": "node_modules/ace-builds/src-min/",
"output": "/assets/ace-builds"
},
"apps/red-ui/src/manifest.webmanifest"
],
"styles": ["apps/red-ui/src/styles.scss"],
"scripts": [
"node_modules/@pdftron/webviewer/webviewer.min.js",
"node_modules/ace-builds/src-min/ace.js",
"node_modules/ace-builds/src-min/mode-java.js",
"node_modules/ace-builds/src-min/theme-eclipse.js"
],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "apps/red-ui/src/environments/environment.ts",
"with": "apps/red-ui/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
],
"serviceWorker": true,
"ngswConfigPath": "apps/red-ui/ngsw-config.json"
}
},
"outputs": ["{options.outputPath}"]
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "red-ui:build"
},
"configurations": {
"production": {
"browserTarget": "red-ui:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "red-ui:build"
}
},
"lint": {
"builder": "@nrwl/linter:eslint",
"options": {
"lintFilePatterns": ["apps/red-ui/src/**/*.ts", "apps/red-ui/src/**/*.html"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "apps/red-ui/jest.config.js",
"passWithNoTests": true
},
"outputs": ["coverage/apps/red-ui"]
}
}
},
"red-ui-http": {
"projectType": "library",
"root": "libs/red-ui-http",
"sourceRoot": "libs/red-ui-http/src",
"prefix": "redaction",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "libs/red-ui-http/tsconfig.lib.json",
"project": "libs/red-ui-http/ng-package.json"
}
},
"lint": {
"builder": "@nrwl/linter:eslint",
"options": {
"lintFilePatterns": ["libs/red-ui-http/src/**/*.ts", "libs/red-ui-http/src/**/*.html"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/red-ui-http/jest.config.js",
"passWithNoTests": true
},
"outputs": ["coverage/libs/red-ui-http"]
}
},
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
}
},
"red-cache": {
"projectType": "library",
"root": "libs/red-cache",
"sourceRoot": "libs/red-cache/src",
"prefix": "redaction",
"architect": {
"lint": {
"builder": "@nrwl/linter:eslint",
"options": {
"lintFilePatterns": ["libs/red-cache/src/**/*.ts", "libs/red-cache/src/**/*.html"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/red-cache/jest.config.js",
"passWithNoTests": true
},
"outputs": ["coverage/libs/red-cache"]
}
},
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
}
}
}
} }

View File

@ -1,37 +1,40 @@
{ {
"extends": ["../../.eslintrc.json"], "extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"], "ignorePatterns": ["!**/*"],
"overrides": [ "overrides": [
{ {
"files": ["*.ts"], "files": ["*.ts"],
"extends": ["plugin:@nrwl/nx/angular", "plugin:@angular-eslint/template/process-inline-templates"], "extends": [
"parserOptions": { "plugin:@nrwl/nx/angular",
"project": ["apps/red-ui/tsconfig.*?.json"] "plugin:@angular-eslint/template/process-inline-templates"
}, ],
"rules": { "parserOptions": {
"@angular-eslint/directive-selector": [ "project": ["apps/red-ui/tsconfig.*?.json"]
"error", },
{ "rules": {
"type": "attribute", "@angular-eslint/directive-selector": [
"prefix": "redaction", "error",
"style": "camelCase" {
} "type": "attribute",
], "prefix": "redaction",
"@angular-eslint/component-selector": [ "style": "camelCase"
"error", }
{ ],
"type": "element", "@angular-eslint/component-selector": [
"prefix": "redaction", "error",
"style": "kebab-case" {
} "type": "element",
] "prefix": "redaction",
}, "style": "kebab-case"
"plugins": ["@angular-eslint/eslint-plugin", "@typescript-eslint"] }
}, ]
{ },
"files": ["*.html"], "plugins": ["@angular-eslint/eslint-plugin", "@typescript-eslint"]
"extends": ["plugin:@nrwl/nx/angular-template"], },
"rules": {} {
} "files": ["*.html"],
] "extends": ["plugin:@nrwl/nx/angular-template"],
"rules": {}
}
]
} }

View File

@ -4,7 +4,12 @@ module.exports = {
globals: { globals: {
'ts-jest': { 'ts-jest': {
stringifyContentPathRegex: '\\.(html|svg)$', stringifyContentPathRegex: '\\.(html|svg)$',
astTransformers: { before: ['jest-preset-angular/build/InlineFilesTransformer', 'jest-preset-angular/build/StripStylesTransformer'] }, astTransformers: {
before: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer'
]
},
tsconfig: '<rootDir>/tsconfig.spec.json' tsconfig: '<rootDir>/tsconfig.spec.json'
} }
}, },

View File

@ -1,21 +1,21 @@
{ {
"$schema": "../../node_modules/@angular/service-worker/config/schema.json", "$schema": "../../node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html", "index": "/index.html",
"assetGroups": [ "assetGroups": [
{ {
"name": "app", "name": "app",
"installMode": "prefetch", "installMode": "prefetch",
"resources": { "resources": {
"files": ["/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", "/*.js"] "files": ["/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", "/*.js"]
} }
}, },
{ {
"name": "assets", "name": "assets",
"installMode": "lazy", "installMode": "lazy",
"updateMode": "prefetch", "updateMode": "prefetch",
"resources": { "resources": {
"files": ["/assets/**", "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"] "files": ["/assets/**", "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"]
} }
} }
] ]
} }

View File

@ -43,7 +43,8 @@ const routes = [
{ {
path: 'main/projects', path: 'main/projects',
component: BaseScreenComponent, component: BaseScreenComponent,
loadChildren: () => import('./modules/projects/projects.module').then((m) => m.ProjectsModule) loadChildren: () =>
import('./modules/projects/projects.module').then((m) => m.ProjectsModule)
}, },
{ {
path: 'main/downloads', path: 'main/downloads',

View File

@ -1,2 +1,4 @@
<router-outlet></router-outlet> <router-outlet></router-outlet>
<redaction-full-page-loading-indicator [displayed]="appLoadStateService.loading | async"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator
[displayed]="appLoadStateService.loading | async"
></redaction-full-page-loading-indicator>

View File

@ -8,5 +8,8 @@ import { RouterHistoryService } from '@services/router-history.service';
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss']
}) })
export class AppComponent { export class AppComponent {
constructor(public appLoadStateService: AppLoadStateService, private readonly _routerHistoryService: RouterHistoryService) {} constructor(
public appLoadStateService: AppLoadStateService,
private readonly _routerHistoryService: RouterHistoryService
) {}
} }

View File

@ -47,7 +47,14 @@ function cleanupBaseUrl(baseUrl: string) {
const screens = [BaseScreenComponent, DownloadsListScreenComponent, UserProfileScreenComponent]; const screens = [BaseScreenComponent, DownloadsListScreenComponent, UserProfileScreenComponent];
const components = [AppComponent, LogoComponent, AuthErrorComponent, ToastComponent, NotificationsComponent, ...screens]; const components = [
AppComponent,
LogoComponent,
AuthErrorComponent,
ToastComponent,
NotificationsComponent,
...screens
];
@NgModule({ @NgModule({
declarations: [...components], declarations: [...components],
@ -113,7 +120,11 @@ export class AppModule {
private _configureKeyCloakRouteHandling() { private _configureKeyCloakRouteHandling() {
this._route.queryParamMap.subscribe((queryParams) => { this._route.queryParamMap.subscribe((queryParams) => {
if (queryParams.has('code') || queryParams.has('state') || queryParams.has('session_state')) { if (
queryParams.has('code') ||
queryParams.has('state') ||
queryParams.has('session_state')
) {
this._router.navigate([], { this._router.navigate([], {
queryParams: { queryParams: {
state: null, state: null,

View File

@ -1,8 +1,15 @@
<section> <section>
<p *ngIf="!configuredAdminName && !configuredAdminUrl" [translate]="'auth-error.heading'" class="heading-xl"></p> <p
*ngIf="!configuredAdminName && !configuredAdminUrl"
[translate]="'auth-error.heading'"
class="heading-xl"
></p>
<p <p
*ngIf="configuredAdminName && configuredAdminUrl" *ngIf="configuredAdminName && configuredAdminUrl"
[innerHTML]="'auth-error.heading-with-name-and-link' | translate: { adminName: configuredAdminName, adminUrl: configuredAdminUrl }" [innerHTML]="
'auth-error.heading-with-name-and-link'
| translate: { adminName: configuredAdminName, adminUrl: configuredAdminUrl }
"
class="heading-xl" class="heading-xl"
></p> ></p>
<p <p

View File

@ -11,10 +11,15 @@ export class AuthErrorComponent implements OnInit {
configuredAdminName: string; configuredAdminName: string;
configuredAdminUrl: string; configuredAdminUrl: string;
constructor(private readonly _userService: UserService, private readonly _appConfigService: AppConfigService) {} constructor(
private readonly _userService: UserService,
private readonly _appConfigService: AppConfigService
) {}
ngOnInit(): void { ngOnInit(): void {
this.configuredAdminName = this._appConfigService.getConfig(AppConfigKey.ADMIN_CONTACT_NAME); this.configuredAdminName = this._appConfigService.getConfig(
AppConfigKey.ADMIN_CONTACT_NAME
);
this.configuredAdminUrl = this._appConfigService.getConfig(AppConfigKey.ADMIN_CONTACT_URL); this.configuredAdminUrl = this._appConfigService.getConfig(AppConfigKey.ADMIN_CONTACT_URL);
} }

View File

@ -6,20 +6,36 @@
<mat-icon svgIcon="red:menu"></mat-icon> <mat-icon svgIcon="red:menu"></mat-icon>
</button> </button>
<mat-menu #menuNav="matMenu"> <mat-menu #menuNav="matMenu">
<button mat-menu-item routerLink="/main/projects" translate="top-bar.navigation-items.projects"></button> <button
<button *ngIf="appStateService.activeProject" [routerLink]="'/main/projects/' + appStateService.activeProjectId" mat-menu-item> mat-menu-item
routerLink="/main/projects"
translate="top-bar.navigation-items.projects"
></button>
<button
*ngIf="appStateService.activeProject"
[routerLink]="'/main/projects/' + appStateService.activeProjectId"
mat-menu-item
>
{{ appStateService.activeProject.project.projectName }} {{ appStateService.activeProject.project.projectName }}
</button> </button>
<button <button
*ngIf="appStateService.activeFile" *ngIf="appStateService.activeFile"
[routerLink]="'/main/projects/' + appStateService.activeProjectId + '/file/' + appStateService.activeFile.fileId" [routerLink]="
'/main/projects/' +
appStateService.activeProjectId +
'/file/' +
appStateService.activeFile.fileId
"
mat-menu-item mat-menu-item
> >
{{ appStateService.activeFile.filename }} {{ appStateService.activeFile.filename }}
</button> </button>
</mat-menu> </mat-menu>
</div> </div>
<div *ngIf="permissionsService.isUser()" class="menu flex-2 visible-lg breadcrumbs-container"> <div
*ngIf="permissionsService.isUser()"
class="menu flex-2 visible-lg breadcrumbs-container"
>
<a <a
*ngIf="projectsView" *ngIf="projectsView"
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
@ -33,8 +49,15 @@
{{ 'top-bar.navigation-items.back' | translate }} {{ 'top-bar.navigation-items.back' | translate }}
</a> </a>
<ng-container *ngIf="projectsView"> <ng-container *ngIf="projectsView">
<mat-icon *ngIf="!appStateService.activeProject" class="primary" svgIcon="red:arrow-down"></mat-icon> <mat-icon
<mat-icon *ngIf="appStateService.activeProject" svgIcon="red:arrow-right"></mat-icon> *ngIf="!appStateService.activeProject"
class="primary"
svgIcon="red:arrow-down"
></mat-icon>
<mat-icon
*ngIf="appStateService.activeProject"
svgIcon="red:arrow-right"
></mat-icon>
<a <a
*ngIf="appStateService.activeProject" *ngIf="appStateService.activeProject"
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
@ -47,7 +70,12 @@
<mat-icon *ngIf="appStateService.activeFile" svgIcon="red:arrow-right"></mat-icon> <mat-icon *ngIf="appStateService.activeFile" svgIcon="red:arrow-right"></mat-icon>
<a <a
*ngIf="appStateService.activeFile" *ngIf="appStateService.activeFile"
[routerLink]="'/main/projects/' + appStateService.activeProjectId + '/file/' + appStateService.activeFile.fileId" [routerLink]="
'/main/projects/' +
appStateService.activeProjectId +
'/file/' +
appStateService.activeFile.fileId
"
class="breadcrumb" class="breadcrumb"
routerLinkActive="active" routerLinkActive="active"
> >
@ -60,13 +88,28 @@
<redaction-logo></redaction-logo> <redaction-logo></redaction-logo>
</redaction-hidden-action> </redaction-hidden-action>
<div class="app-name">{{ titleService.getTitle() }}</div> <div class="app-name">{{ titleService.getTitle() }}</div>
<span *ngIf="userPreferenceService.areDevFeaturesEnabled" class="dev-mode" translate="dev-mode"></span> <span
*ngIf="userPreferenceService.areDevFeaturesEnabled"
class="dev-mode"
translate="dev-mode"
></span>
</div> </div>
<div class="menu right flex-2"> <div class="menu right flex-2">
<redaction-notifications *ngIf="userPreferenceService.areDevFeaturesEnabled" class="mr-8"></redaction-notifications> <redaction-notifications
<redaction-user-button [matMenuTriggerFor]="userMenu" [showDot]="showPendingDownloadsDot" [user]="user"></redaction-user-button> *ngIf="userPreferenceService.areDevFeaturesEnabled"
class="mr-8"
></redaction-notifications>
<redaction-user-button
[matMenuTriggerFor]="userMenu"
[showDot]="showPendingDownloadsDot"
[user]="user"
></redaction-user-button>
<mat-menu #userMenu="matMenu" class="user-menu" xPosition="before"> <mat-menu #userMenu="matMenu" class="user-menu" xPosition="before">
<button [routerLink]="'/main/my-profile'" mat-menu-item translate="top-bar.navigation-items.my-account.children.my-profile"></button> <button
[routerLink]="'/main/my-profile'"
mat-menu-item
translate="top-bar.navigation-items.my-account.children.my-profile"
></button>
<button <button
(click)="appStateService.reset()" (click)="appStateService.reset()"
*ngIf="permissionsService.isManager() || permissionsService.isUserAdmin()" *ngIf="permissionsService.isManager() || permissionsService.isUserAdmin()"

View File

@ -14,26 +14,49 @@
<div class="content-container"> <div class="content-container">
<div class="header-item"> <div class="header-item">
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'downloads-list.table-header.title' | translate: { length: fileDownloadService.downloads.length } }} {{
'downloads-list.table-header.title'
| translate: { length: fileDownloadService.downloads.length }
}}
</span> </span>
</div> </div>
<div [class.no-data]="noData" class="table-header" redactionSyncWidth="table-item"> <div [class.no-data]="noData" class="table-header" redactionSyncWidth="table-item">
<redaction-table-col-name label="downloads-list.table-col-names.name"></redaction-table-col-name> <redaction-table-col-name
<redaction-table-col-name label="downloads-list.table-col-names.size"></redaction-table-col-name> label="downloads-list.table-col-names.name"
<redaction-table-col-name label="downloads-list.table-col-names.date"></redaction-table-col-name> ></redaction-table-col-name>
<redaction-table-col-name label="downloads-list.table-col-names.status"></redaction-table-col-name> <redaction-table-col-name
label="downloads-list.table-col-names.size"
></redaction-table-col-name>
<redaction-table-col-name
label="downloads-list.table-col-names.date"
></redaction-table-col-name>
<redaction-table-col-name
label="downloads-list.table-col-names.status"
></redaction-table-col-name>
<div></div> <div></div>
<div class="scrollbar-placeholder"></div> <div class="scrollbar-placeholder"></div>
</div> </div>
<redaction-empty-state *ngIf="noData" icon="red:download" screen="downloads-list"></redaction-empty-state> <redaction-empty-state
*ngIf="noData"
icon="red:download"
screen="downloads-list"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines --> <!-- Table lines -->
<div *cdkVirtualFor="let download of fileDownloadService.downloads" class="table-item"> <div
*cdkVirtualFor="let download of fileDownloadService.downloads"
class="table-item"
>
<div> <div>
<div [class.no-bold]="download.lastDownload" class="table-item-title heading">{{ download.filename }}</div> <div
[class.no-bold]="download.lastDownload"
class="table-item-title heading"
>
{{ download.filename }}
</div>
</div> </div>
<div> <div>
<div class="small-label"> <div class="small-label">
@ -61,7 +84,12 @@
> >
</redaction-circle-button> </redaction-circle-button>
<redaction-circle-button (action)="deleteItem(download)" icon="red:trash" tooltip="downloads-list.actions.delete" type="dark-bg"> <redaction-circle-button
(action)="deleteItem(download)"
icon="red:trash"
tooltip="downloads-list.actions.delete"
type="dark-bg"
>
</redaction-circle-button> </redaction-circle-button>
<mat-spinner *ngIf="download.inProgress" diameter="15"></mat-spinner> <mat-spinner *ngIf="download.inProgress" diameter="15"></mat-spinner>

View File

@ -9,7 +9,10 @@ import { DownloadControllerService } from '@redaction/red-ui-http';
styleUrls: ['./downloads-list-screen.component.scss'] styleUrls: ['./downloads-list-screen.component.scss']
}) })
export class DownloadsListScreenComponent implements OnInit { export class DownloadsListScreenComponent implements OnInit {
constructor(readonly fileDownloadService: FileDownloadService, private readonly _downloadControllerService: DownloadControllerService) {} constructor(
readonly fileDownloadService: FileDownloadService,
private readonly _downloadControllerService: DownloadControllerService
) {}
get noData(): boolean { get noData(): boolean {
return this.fileDownloadService.downloads.length === 0; return this.fileDownloadService.downloads.length === 0;

View File

@ -1,5 +1,14 @@
<redaction-circle-button [matMenuTriggerFor]="overlay" [showDot]="hasUnread" icon="red:notification"></redaction-circle-button> <redaction-circle-button
<mat-menu #overlay="matMenu" backdropClass="notifications-backdrop" class="notifications-menu" xPosition="before"> [matMenuTriggerFor]="overlay"
[showDot]="hasUnread"
icon="red:notification"
></redaction-circle-button>
<mat-menu
#overlay="matMenu"
backdropClass="notifications-backdrop"
class="notifications-menu"
xPosition="before"
>
<div *ngFor="let group of groupedNotifications | sortBy: 'desc':'dateString'"> <div *ngFor="let group of groupedNotifications | sortBy: 'desc':'dateString'">
<div class="all-caps-label">{{ day(group) }}</div> <div class="all-caps-label">{{ day(group) }}</div>
<div <div
@ -15,7 +24,10 @@
</div> </div>
<div <div
(click)="toggleRead(notification, $event)" (click)="toggleRead(notification, $event)"
[matTooltip]="(notification.read ? 'notifications.mark-unread' : 'notifications.mark-read') | translate" [matTooltip]="
(notification.read ? 'notifications.mark-unread' : 'notifications.mark-read')
| translate
"
class="dot" class="dot"
matTooltipPosition="before" matTooltipPosition="before"
></div> ></div>

View File

@ -2,8 +2,20 @@
<div *ngIf="title" [attr.aria-label]="title" [class]="options.titleClass"> <div *ngIf="title" [attr.aria-label]="title" [class]="options.titleClass">
{{ title }} {{ title }}
</div> </div>
<div *ngIf="message && options.enableHtml" [class]="options.messageClass" [innerHTML]="message" aria-live="polite" role="alert"></div> <div
<div *ngIf="message && !options.enableHtml" [attr.aria-label]="message" [class]="options.messageClass" aria-live="polite" role="alert"> *ngIf="message && options.enableHtml"
[class]="options.messageClass"
[innerHTML]="message"
aria-live="polite"
role="alert"
></div>
<div
*ngIf="message && !options.enableHtml"
[attr.aria-label]="message"
[class]="options.messageClass"
aria-live="polite"
role="alert"
>
{{ message }} {{ message }}
</div> </div>

View File

@ -7,7 +7,10 @@ import { Toast, ToastPackage, ToastrService } from 'ngx-toastr';
styleUrls: ['./toast.component.scss'] styleUrls: ['./toast.component.scss']
}) })
export class ToastComponent extends Toast { export class ToastComponent extends Toast {
constructor(protected readonly _toastrService: ToastrService, readonly toastPackage: ToastPackage) { constructor(
protected readonly _toastrService: ToastrService,
readonly toastPackage: ToastPackage
) {
super(_toastrService, toastPackage); super(_toastrService, toastPackage);
} }

View File

@ -23,17 +23,29 @@
<input formControlName="lastName" name="lastName" type="text" /> <input formControlName="lastName" name="lastName" type="text" />
</div> </div>
<div class="red-input-group"> <div class="red-input-group">
<label [translate]="'top-bar.navigation-items.my-account.children.language.label'"></label> <label
[translate]="
'top-bar.navigation-items.my-account.children.language.label'
"
></label>
<mat-select formControlName="language"> <mat-select formControlName="language">
<mat-option *ngFor="let language of languages" [value]="language"> <mat-option *ngFor="let language of languages" [value]="language">
{{ 'top-bar.navigation-items.my-account.children.language.' + language | translate }} {{
'top-bar.navigation-items.my-account.children.language.' +
language | translate
}}
</mat-option> </mat-option>
</mat-select> </mat-select>
</div> </div>
</div> </div>
</div> </div>
<div class="dialog-actions"> <div class="dialog-actions">
<button [disabled]="formGroup.invalid || !(profileChanged || languageChanged)" color="primary" mat-flat-button type="submit"> <button
[disabled]="formGroup.invalid || !(profileChanged || languageChanged)"
color="primary"
mat-flat-button
type="submit"
>
{{ 'user-profile.actions.save' | translate }} {{ 'user-profile.actions.save' | translate }}
</button> </button>
</div> </div>
@ -42,4 +54,6 @@
</div> </div>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator
[displayed]="!viewReady"
></redaction-full-page-loading-indicator>

View File

@ -30,8 +30,10 @@ export class PendingChangesGuard implements CanDeactivate<ComponentCanDeactivate
// if there are no pending changes, just allow deactivation; else confirm first // if there are no pending changes, just allow deactivation; else confirm first
return !component.hasChanges return !component.hasChanges
? true ? true
: // NOTE: this warning message will only be shown when navigating elsewhere within your angular app; : // NOTE: this warning message will only be shown when navigating elsewhere
// when navigating away from your angular app, the browser will show a generic warning message // within your angular app;
// when navigating away from your angular app,
// the browser will show a generic warning message
// see http://stackoverflow.com/a/42207299/7307355 // see http://stackoverflow.com/a/42207299/7307355
confirm(this._translateService.instant('pending-changes-guard')); confirm(this._translateService.instant('pending-changes-guard'));
} }

View File

@ -1,4 +1,10 @@
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; import {
ActivatedRouteSnapshot,
CanActivate,
Router,
RouterStateSnapshot,
UrlTree
} from '@angular/router';
import { Injectable, Injector } from '@angular/core'; import { Injectable, Injector } from '@angular/core';
import { from, of } from 'rxjs'; import { from, of } from 'rxjs';
import { AppLoadStateService } from '@services/app-load-state.service'; import { AppLoadStateService } from '@services/app-load-state.service';
@ -7,7 +13,11 @@ import { AppLoadStateService } from '@services/app-load-state.service';
providedIn: 'root' providedIn: 'root'
}) })
export class CompositeRouteGuard implements CanActivate { export class CompositeRouteGuard implements CanActivate {
constructor(protected readonly _router: Router, protected readonly _injector: Injector, private readonly _appLoadStateService: AppLoadStateService) {} constructor(
protected readonly _router: Router,
protected readonly _injector: Injector,
private readonly _appLoadStateService: AppLoadStateService
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> { async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
this._appLoadStateService.pushLoadingEvent(true); this._appLoadStateService.pushLoadingEvent(true);
@ -21,7 +31,10 @@ export class CompositeRouteGuard implements CanActivate {
if (canActivateResult instanceof Promise) { if (canActivateResult instanceof Promise) {
canActivateResult = from(canActivateResult); canActivateResult = from(canActivateResult);
} }
if (typeof canActivateResult === 'boolean' || canActivateResult instanceof UrlTree) { if (
typeof canActivateResult === 'boolean' ||
canActivateResult instanceof UrlTree
) {
canActivateResult = of(canActivateResult); canActivateResult = of(canActivateResult);
} }

View File

@ -30,22 +30,34 @@ export class AnnotationPermissions {
const permissions: AnnotationPermissions = new AnnotationPermissions(); const permissions: AnnotationPermissions = new AnnotationPermissions();
permissions.canUndo = permissions.canUndo =
annotation.isUndoableSuperType && (annotation.userId === user.id || (annotation.userId && isApprover && !annotation.isSuggestion)); annotation.isUndoableSuperType &&
(annotation.userId === user.id ||
(annotation.userId && isApprover && !annotation.isSuggestion));
permissions.canForceRedaction = annotation.isSkipped && !permissions.canUndo; permissions.canForceRedaction = annotation.isSkipped && !permissions.canUndo;
permissions.canAcceptRecommendation = annotation.isRecommendation; permissions.canAcceptRecommendation = annotation.isRecommendation;
permissions.canMarkAsFalsePositive = annotation.canBeMarkedAsFalsePositive && !annotation.force; permissions.canMarkAsFalsePositive =
permissions.canMarkTextOnlyAsFalsePositive = annotation.canBeMarkedAsFalsePositiveWithTextOnly && !annotation.force; annotation.canBeMarkedAsFalsePositive && !annotation.force;
permissions.canMarkTextOnlyAsFalsePositive =
annotation.canBeMarkedAsFalsePositiveWithTextOnly && !annotation.force;
permissions.canRemoveOrSuggestToRemoveOnlyHere = annotation.isRedacted && !annotation.force; permissions.canRemoveOrSuggestToRemoveOnlyHere = annotation.isRedacted && !annotation.force;
permissions.canRemoveOrSuggestToRemoveFromDictionary = permissions.canRemoveOrSuggestToRemoveFromDictionary =
annotation.isRedacted && !annotation.isManualRedaction && annotation.isModifyDictionary && !annotation.force; annotation.isRedacted &&
!annotation.isManualRedaction &&
annotation.isModifyDictionary &&
!annotation.force;
permissions.canAcceptSuggestion = isApprover && (annotation.isSuggestion || annotation.isDeclinedSuggestion); permissions.canAcceptSuggestion =
isApprover && (annotation.isSuggestion || annotation.isDeclinedSuggestion);
permissions.canRejectSuggestion = permissions.canRejectSuggestion =
isApprover && (annotation.isSuggestion || (annotation.isReadyForAnalysis && !permissions.canUndo && annotation.superType !== 'pending-analysis')); isApprover &&
(annotation.isSuggestion ||
(annotation.isReadyForAnalysis &&
!permissions.canUndo &&
annotation.superType !== 'pending-analysis'));
return permissions; return permissions;
} }

View File

@ -79,15 +79,28 @@ export class AnnotationWrapper {
} }
get canBeMarkedAsFalsePositive() { get canBeMarkedAsFalsePositive() {
return (this.isRecommendation || this.superType === 'redaction') && (this.hasTextAfter || this.hasTextBefore) && !this.isImage; return (
(this.isRecommendation || this.superType === 'redaction') &&
(this.hasTextAfter || this.hasTextBefore) &&
!this.isImage
);
} }
get canBeMarkedAsFalsePositiveWithTextOnly() { get canBeMarkedAsFalsePositiveWithTextOnly() {
return !this.canBeMarkedAsFalsePositive && (this.isRecommendation || this.superType === 'redaction') && !this.isImage; return (
!this.canBeMarkedAsFalsePositive &&
(this.isRecommendation || this.superType === 'redaction') &&
!this.isImage
);
} }
get isSuperTypeBasedColor() { get isSuperTypeBasedColor() {
return this.isSkipped || this.isSuggestion || this.isReadyForAnalysis || this.isDeclinedSuggestion; return (
this.isSkipped ||
this.isSuggestion ||
this.isReadyForAnalysis ||
this.isDeclinedSuggestion
);
} }
get isSkipped() { get isSkipped() {
@ -104,7 +117,10 @@ export class AnnotationWrapper {
get isFalsePositive() { get isFalsePositive() {
return ( return (
this.dictionary?.toLowerCase() === 'false_positive' && (this.superType === 'skipped' || this.superType === 'hint' || this.superType === 'redaction') this.dictionary?.toLowerCase() === 'false_positive' &&
(this.superType === 'skipped' ||
this.superType === 'hint' ||
this.superType === 'redaction')
); );
} }
@ -142,11 +158,18 @@ export class AnnotationWrapper {
} }
get isSuggestionAdd() { get isSuggestionAdd() {
return this.superType === 'suggestion-add' || this.superType === 'suggestion-add-dictionary' || this.superType === 'suggestion-force-redaction'; return (
this.superType === 'suggestion-add' ||
this.superType === 'suggestion-add-dictionary' ||
this.superType === 'suggestion-force-redaction'
);
} }
get isSuggestionRemove() { get isSuggestionRemove() {
return this.superType === 'suggestion-remove' || this.superType === 'suggestion-remove-dictionary'; return (
this.superType === 'suggestion-remove' ||
this.superType === 'suggestion-remove-dictionary'
);
} }
get isModifyDictionary() { get isModifyDictionary() {
@ -154,7 +177,10 @@ export class AnnotationWrapper {
} }
get isConvertedRecommendation() { get isConvertedRecommendation() {
return this.isRecommendation && (this.superType === 'suggestion-add-dictionary' || this.superType === 'add-dictionary'); return (
this.isRecommendation &&
(this.superType === 'suggestion-add-dictionary' || this.superType === 'add-dictionary')
);
} }
get isRecommendation() { get isRecommendation() {
@ -203,13 +229,21 @@ export class AnnotationWrapper {
return annotationWrapper; return annotationWrapper;
} }
private static _handleRecommendations(annotationWrapper: AnnotationWrapper, redactionLogEntry: RedactionLogEntryWrapper) { private static _handleRecommendations(
annotationWrapper: AnnotationWrapper,
redactionLogEntry: RedactionLogEntryWrapper
) {
if (annotationWrapper.superType === 'recommendation') { if (annotationWrapper.superType === 'recommendation') {
annotationWrapper.recommendationType = redactionLogEntry.type.substr('recommendation_'.length); annotationWrapper.recommendationType = redactionLogEntry.type.substr(
'recommendation_'.length
);
} }
} }
private static _setSuperType(annotationWrapper: AnnotationWrapper, redactionLogEntryWrapper: RedactionLogEntryWrapper) { private static _setSuperType(
annotationWrapper: AnnotationWrapper,
redactionLogEntryWrapper: RedactionLogEntryWrapper
) {
if (redactionLogEntryWrapper.recommendation) { if (redactionLogEntryWrapper.recommendation) {
if (redactionLogEntryWrapper.redacted) { if (redactionLogEntryWrapper.redacted) {
annotationWrapper.superType = 'recommendation'; annotationWrapper.superType = 'recommendation';
@ -235,23 +269,40 @@ export class AnnotationWrapper {
} else { } else {
if (redactionLogEntryWrapper.redacted) { if (redactionLogEntryWrapper.redacted) {
if (redactionLogEntryWrapper.force) { if (redactionLogEntryWrapper.force) {
annotationWrapper.superType = redactionLogEntryWrapper.status === 'REQUESTED' ? 'suggestion-add' : 'redaction'; annotationWrapper.superType =
redactionLogEntryWrapper.status === 'REQUESTED'
? 'suggestion-add'
: 'redaction';
} else if (redactionLogEntryWrapper.type === 'manual') { } else if (redactionLogEntryWrapper.type === 'manual') {
annotationWrapper.superType = redactionLogEntryWrapper.status === 'REQUESTED' ? 'suggestion-add' : 'manual-redaction'; annotationWrapper.superType =
redactionLogEntryWrapper.status === 'REQUESTED'
? 'suggestion-add'
: 'manual-redaction';
} else { } else {
if (redactionLogEntryWrapper.status === 'REQUESTED') { if (redactionLogEntryWrapper.status === 'REQUESTED') {
if (redactionLogEntryWrapper.dictionaryEntry) { if (redactionLogEntryWrapper.dictionaryEntry) {
annotationWrapper.superType = annotationWrapper.superType =
redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'suggestion-add-dictionary' : 'suggestion-remove-dictionary'; redactionLogEntryWrapper.manualRedactionType === 'ADD'
? 'suggestion-add-dictionary'
: 'suggestion-remove-dictionary';
} else { } else {
annotationWrapper.superType = redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'suggestion-add' : 'suggestion-remove'; annotationWrapper.superType =
redactionLogEntryWrapper.manualRedactionType === 'ADD'
? 'suggestion-add'
: 'suggestion-remove';
} }
} }
if (redactionLogEntryWrapper.status === 'APPROVED') { if (redactionLogEntryWrapper.status === 'APPROVED') {
if (redactionLogEntryWrapper.dictionaryEntry) { if (redactionLogEntryWrapper.dictionaryEntry) {
annotationWrapper.superType = redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'add-dictionary' : 'remove-dictionary'; annotationWrapper.superType =
redactionLogEntryWrapper.manualRedactionType === 'ADD'
? 'add-dictionary'
: 'remove-dictionary';
} else { } else {
annotationWrapper.superType = redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'manual-redaction' : 'remove-only-here'; annotationWrapper.superType =
redactionLogEntryWrapper.manualRedactionType === 'ADD'
? 'manual-redaction'
: 'remove-only-here';
} }
} }
} }
@ -259,14 +310,24 @@ export class AnnotationWrapper {
} }
if (!annotationWrapper.superType) { if (!annotationWrapper.superType) {
annotationWrapper.superType = annotationWrapper.redaction ? 'redaction' : annotationWrapper.hint ? 'hint' : 'skipped'; annotationWrapper.superType = annotationWrapper.redaction
? 'redaction'
: annotationWrapper.hint
? 'hint'
: 'skipped';
} }
} }
private static _handleSkippedState(annotationWrapper: AnnotationWrapper, redactionLogEntryWrapper: RedactionLogEntryWrapper) { private static _handleSkippedState(
annotationWrapper: AnnotationWrapper,
redactionLogEntryWrapper: RedactionLogEntryWrapper
) {
if (annotationWrapper.superType === 'skipped') { if (annotationWrapper.superType === 'skipped') {
if (!annotationWrapper.userId) { if (!annotationWrapper.userId) {
if (redactionLogEntryWrapper.manualRedactionType === 'REMOVE' || redactionLogEntryWrapper.manualRedactionType === 'UNDO') { if (
redactionLogEntryWrapper.manualRedactionType === 'REMOVE' ||
redactionLogEntryWrapper.manualRedactionType === 'UNDO'
) {
annotationWrapper.superType = 'pending-analysis'; annotationWrapper.superType = 'pending-analysis';
return; return;
} }
@ -283,7 +344,10 @@ export class AnnotationWrapper {
} }
} }
private static _createContent(annotationWrapper: AnnotationWrapper, entry: RedactionLogEntryWrapper) { private static _createContent(
annotationWrapper: AnnotationWrapper,
entry: RedactionLogEntryWrapper
) {
let content = ''; let content = '';
if (entry.matchedRule) { if (entry.matchedRule) {
content += 'Rule ' + entry.matchedRule + ' matched \n\n'; content += 'Rule ' + entry.matchedRule + ' matched \n\n';

View File

@ -33,7 +33,12 @@ export class FileDataModel {
return this.redactionLog.redactionLogEntry; return this.redactionLog.redactionLogEntry;
} }
getAnnotations(dictionaryData: { [p: string]: TypeValue }, currentUser: UserWrapper, viewMode: ViewMode, areDevFeaturesEnabled: boolean): AnnotationData { getAnnotations(
dictionaryData: { [p: string]: TypeValue },
currentUser: UserWrapper,
viewMode: ViewMode,
areDevFeaturesEnabled: boolean
): AnnotationData {
const entries: RedactionLogEntryWrapper[] = this._convertData(dictionaryData); const entries: RedactionLogEntryWrapper[] = this._convertData(dictionaryData);
let allAnnotations = entries.map((entry) => AnnotationWrapper.fromData(entry)); let allAnnotations = entries.map((entry) => AnnotationWrapper.fromData(entry));
@ -70,11 +75,14 @@ export class FileDataModel {
return; return;
} }
const redactionLogEntryWrapper: RedactionLogEntryWrapper = { actionPendingReanalysis: false }; const redactionLogEntryWrapper: RedactionLogEntryWrapper = {
actionPendingReanalysis: false
};
Object.assign(redactionLogEntryWrapper, changeLogEntry); Object.assign(redactionLogEntryWrapper, changeLogEntry);
redactionLogEntryWrapper.comments = this.manualRedactions.comments[redactionLogEntryWrapper.id]; redactionLogEntryWrapper.comments =
this.manualRedactions.comments[redactionLogEntryWrapper.id];
redactionLogEntryWrapper.isChangeLogEntry = true; redactionLogEntryWrapper.isChangeLogEntry = true;
redactionLogEntryWrapper.changeLogType = changeLogEntry.changeType; redactionLogEntryWrapper.changeLogType = changeLogEntry.changeType;
redactionLogEntryWrapper.id = 'changed-log-removed-' + redactionLogEntryWrapper.id; redactionLogEntryWrapper.id = 'changed-log-removed-' + redactionLogEntryWrapper.id;
@ -88,12 +96,17 @@ export class FileDataModel {
return; return;
} }
const existingChangeLogEntry = this.redactionChangeLog?.redactionLogEntry?.find((rle) => rle.id === redactionLogEntry.id); const existingChangeLogEntry = this.redactionChangeLog?.redactionLogEntry?.find(
(rle) => rle.id === redactionLogEntry.id
);
// copy the redactionLog Entry // copy the redactionLog Entry
const redactionLogEntryWrapper: RedactionLogEntryWrapper = { actionPendingReanalysis: false }; const redactionLogEntryWrapper: RedactionLogEntryWrapper = {
actionPendingReanalysis: false
};
Object.assign(redactionLogEntryWrapper, redactionLogEntry); Object.assign(redactionLogEntryWrapper, redactionLogEntry);
redactionLogEntryWrapper.comments = this.manualRedactions.comments[redactionLogEntryWrapper.id]; redactionLogEntryWrapper.comments =
this.manualRedactions.comments[redactionLogEntryWrapper.id];
redactionLogEntryWrapper.isChangeLogEntry = !!existingChangeLogEntry; redactionLogEntryWrapper.isChangeLogEntry = !!existingChangeLogEntry;
redactionLogEntryWrapper.changeLogType = 'ADDED'; redactionLogEntryWrapper.changeLogType = 'ADDED';
result.push(redactionLogEntryWrapper); result.push(redactionLogEntryWrapper);
@ -117,7 +130,10 @@ export class FileDataModel {
relevantRedactionLogEntry.force = true; relevantRedactionLogEntry.force = true;
// if statuses differ // if statuses differ
if (!forceRedaction.processedDate || forceRedaction.status !== relevantRedactionLogEntry.status) { if (
!forceRedaction.processedDate ||
forceRedaction.status !== relevantRedactionLogEntry.status
) {
relevantRedactionLogEntry.actionPendingReanalysis = true; relevantRedactionLogEntry.actionPendingReanalysis = true;
relevantRedactionLogEntry.status = forceRedaction.status; relevantRedactionLogEntry.status = forceRedaction.status;
} }
@ -152,7 +168,8 @@ export class FileDataModel {
relevantRedactionLogEntry.status = manual.status; relevantRedactionLogEntry.status = manual.status;
} }
} else { } else {
// dictionary modifying requests that have been processed already updated the dictionary and should not be drawn // dictionary modifying requests that have been processed already updated
// the dictionary and should not be drawn
if (manual.addToDictionary && this._hasAlreadyBeenProcessed(manual)) { if (manual.addToDictionary && this._hasAlreadyBeenProcessed(manual)) {
return; return;
} }
@ -175,9 +192,11 @@ export class FileDataModel {
redactionLogEntryWrapper.hint = dictionary.hint; redactionLogEntryWrapper.hint = dictionary.hint;
redactionLogEntryWrapper.manualRedactionType = 'ADD'; redactionLogEntryWrapper.manualRedactionType = 'ADD';
redactionLogEntryWrapper.manual = true; redactionLogEntryWrapper.manual = true;
redactionLogEntryWrapper.comments = this.manualRedactions.comments[redactionLogEntryWrapper.id]; redactionLogEntryWrapper.comments =
this.manualRedactions.comments[redactionLogEntryWrapper.id];
if (markedAsReasonRedactionLogEntry) { if (markedAsReasonRedactionLogEntry) {
// cleanup reason if the reason is another annotationId - it is not needed for drawing // cleanup reason if the reason is another annotationId
// it is not needed for drawing
redactionLogEntryWrapper.reason = null; redactionLogEntryWrapper.reason = null;
} }
@ -212,14 +231,18 @@ export class FileDataModel {
result.forEach((redactionLogEntry) => { result.forEach((redactionLogEntry) => {
if (redactionLogEntry.manual) { if (redactionLogEntry.manual) {
if (redactionLogEntry.manualRedactionType === 'ADD') { if (redactionLogEntry.manualRedactionType === 'ADD') {
const foundManualEntry = this.manualRedactions.entriesToAdd.find((me) => me.id === redactionLogEntry.id); const foundManualEntry = this.manualRedactions.entriesToAdd.find(
(me) => me.id === redactionLogEntry.id
);
// ADD has been undone - not yet processed // ADD has been undone - not yet processed
if (!foundManualEntry) { if (!foundManualEntry) {
redactionLogEntry.hidden = true; redactionLogEntry.hidden = true;
} }
} }
if (redactionLogEntry.manualRedactionType === 'REMOVE') { if (redactionLogEntry.manualRedactionType === 'REMOVE') {
const foundManualEntry = this.manualRedactions.idsToRemove.find((me) => me.id === redactionLogEntry.id); const foundManualEntry = this.manualRedactions.idsToRemove.find(
(me) => me.id === redactionLogEntry.id
);
// REMOVE has been undone - not yet processed // REMOVE has been undone - not yet processed
if (!foundManualEntry) { if (!foundManualEntry) {
redactionLogEntry.manual = false; redactionLogEntry.manual = false;
@ -236,6 +259,9 @@ export class FileDataModel {
} }
private _hasAlreadyBeenProcessed(entry: ManualRedactionEntry | IdRemoval): boolean { private _hasAlreadyBeenProcessed(entry: ManualRedactionEntry | IdRemoval): boolean {
return !entry.processedDate ? false : new Date(entry.processedDate).getTime() < new Date(this.fileStatus.lastProcessed).getTime(); return !entry.processedDate
? false
: new Date(entry.processedDate).getTime() <
new Date(this.fileStatus.lastProcessed).getTime();
} }
} }

View File

@ -5,11 +5,18 @@ export class FileStatusWrapper {
primaryAttribute: string; primaryAttribute: string;
searchField: string; searchField: string;
constructor(public fileStatus: FileStatus, public reviewerName: string, public ruleSetId: string, fileAttributesConfig?: FileAttributesConfig) { constructor(
public fileStatus: FileStatus,
public reviewerName: string,
public ruleSetId: string,
fileAttributesConfig?: FileAttributesConfig
) {
this.searchField = fileStatus.filename; this.searchField = fileStatus.filename;
if (fileAttributesConfig) { if (fileAttributesConfig) {
const primary = fileAttributesConfig.fileAttributeConfigs?.find((c) => c.primaryAttribute); const primary = fileAttributesConfig.fileAttributeConfigs?.find(
(c) => c.primaryAttribute
);
if (primary && fileStatus.fileAttributes?.attributeIdToValue) { if (primary && fileStatus.fileAttributes?.attributeIdToValue) {
this.primaryAttribute = fileStatus.fileAttributes?.attributeIdToValue[primary.id]; this.primaryAttribute = fileStatus.fileAttributes?.attributeIdToValue[primary.id];
this.searchField += ' ' + this.primaryAttribute; this.searchField += ' ' + this.primaryAttribute;
@ -125,7 +132,9 @@ export class FileStatusWrapper {
} }
get status() { get status() {
return this.fileStatus.status === 'REPROCESS' || this.fileStatus.status === 'FULLREPROCESS' ? 'PROCESSING' : this.fileStatus.status; return this.fileStatus.status === 'REPROCESS' || this.fileStatus.status === 'FULLREPROCESS'
? 'PROCESSING'
: this.fileStatus.status;
} }
get numberOfPages() { get numberOfPages() {
@ -195,6 +204,8 @@ export class FileStatusWrapper {
get newestDate() { get newestDate() {
const uploadedDate = new Date(this.fileStatus.lastUploaded); const uploadedDate = new Date(this.fileStatus.lastUploaded);
const updatedDate = new Date(this.fileStatus.lastUpdated); const updatedDate = new Date(this.fileStatus.lastUpdated);
return updatedDate && updatedDate.getTime() > uploadedDate.getTime() ? updatedDate : uploadedDate; return updatedDate && updatedDate.getTime() > uploadedDate.getTime()
? updatedDate
: uploadedDate;
} }
} }

View File

@ -5,9 +5,16 @@ export class ManualAnnotationResponse {
annotationId; annotationId;
commentId; commentId;
constructor(public manualRedactionEntryWrapper: ManualRedactionEntryWrapper, public manualAddResponse: ManualAddResponse) { constructor(
this.annotationId = manualAddResponse?.annotationId ? manualAddResponse.annotationId : new Date().getTime(); public manualRedactionEntryWrapper: ManualRedactionEntryWrapper,
this.commentId = manualAddResponse?.commentId ? manualAddResponse.commentId : new Date().getTime(); public manualAddResponse: ManualAddResponse
) {
this.annotationId = manualAddResponse?.annotationId
? manualAddResponse.annotationId
: new Date().getTime();
this.commentId = manualAddResponse?.commentId
? manualAddResponse.commentId
: new Date().getTime();
} }
get dictionary() { get dictionary() {

View File

@ -77,6 +77,13 @@ const components = [
@NgModule({ @NgModule({
declarations: [...components], declarations: [...components],
providers: [AdminDialogService], providers: [AdminDialogService],
imports: [CommonModule, SharedModule, AdminRoutingModule, AceEditorModule, NgxChartsModule, ColorPickerModule] imports: [
CommonModule,
SharedModule,
AdminRoutingModule,
AceEditorModule,
NgxChartsModule,
ColorPickerModule
]
}) })
export class AdminModule {} export class AdminModule {}

View File

@ -1,5 +1,10 @@
<div class="menu flex-2 visible-lg breadcrumbs-container"> <div class="menu flex-2 visible-lg breadcrumbs-container">
<a *ngIf="root || !!appStateService.activeRuleSet" [routerLink]="'/main/admin/project-templates'" class="breadcrumb" translate="project-templates"></a> <a
*ngIf="root || !!appStateService.activeRuleSet"
[routerLink]="'/main/admin/project-templates'"
class="breadcrumb"
translate="project-templates"
></a>
<ng-container *ngIf="appStateService.activeRuleSet"> <ng-container *ngIf="appStateService.activeRuleSet">
<mat-icon svgIcon="red:arrow-right"></mat-icon> <mat-icon svgIcon="red:arrow-right"></mat-icon>
@ -15,7 +20,12 @@
<ng-container *ngIf="appStateService.activeDictionary"> <ng-container *ngIf="appStateService.activeDictionary">
<mat-icon svgIcon="red:arrow-right"></mat-icon> <mat-icon svgIcon="red:arrow-right"></mat-icon>
<a <a
[routerLink]="'/main/admin/project-templates/' + appStateService.activeRuleSetId + '/dictionaries/' + appStateService.activeDictionaryType" [routerLink]="
'/main/admin/project-templates/' +
appStateService.activeRuleSetId +
'/dictionaries/' +
appStateService.activeDictionaryType
"
class="breadcrumb ml-0" class="breadcrumb ml-0"
routerLinkActive="active" routerLinkActive="active"
> >

View File

@ -1,8 +1,24 @@
import { Component, ContentChild, EventEmitter, HostListener, Input, Output, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core'; import {
Component,
ContentChild,
EventEmitter,
HostListener,
Input,
Output,
TemplateRef,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { curveLinear } from 'd3-shape'; import { curveLinear } from 'd3-shape';
import { scaleBand, scaleLinear, scalePoint, scaleTime } from 'd3-scale'; import { scaleBand, scaleLinear, scalePoint, scaleTime } from 'd3-scale';
import { BaseChartComponent, calculateViewDimensions, ColorHelper, LineSeriesComponent, ViewDimensions } from '@swimlane/ngx-charts'; import {
BaseChartComponent,
calculateViewDimensions,
ColorHelper,
LineSeriesComponent,
ViewDimensions
} from '@swimlane/ngx-charts';
@Component({ @Component({
// eslint-disable-next-line @angular-eslint/component-selector // eslint-disable-next-line @angular-eslint/component-selector
@ -261,7 +277,9 @@ export class ComboChartComponent extends BaseChartComponent {
if (this.bandwidth === undefined) { if (this.bandwidth === undefined) {
this.bandwidth = width - this.barPadding; this.bandwidth = width - this.barPadding;
} }
const offset = Math.floor((width + this.barPadding - (this.bandwidth + this.barPadding) * domain.length) / 2); const offset = Math.floor(
(width + this.barPadding - (this.bandwidth + this.barPadding) * domain.length) / 2
);
if (this.scaleType === 'time') { if (this.scaleType === 'time') {
scale = scaleTime().range([0, width]).domain(domain); scale = scaleTime().range([0, width]).domain(domain);
@ -326,7 +344,12 @@ export class ComboChartComponent extends BaseChartComponent {
domain = this.yDomain; domain = this.yDomain;
} }
this.colors = new ColorHelper(this.scheme, this.schemeType, domain, this.customColors); this.colors = new ColorHelper(this.scheme, this.schemeType, domain, this.customColors);
this.colorsLine = new ColorHelper(this.colorSchemeLine, this.schemeType, domain, this.customColors); this.colorsLine = new ColorHelper(
this.colorSchemeLine,
this.schemeType,
domain,
this.customColors
);
} }
getLegendOptions() { getLegendOptions() {
@ -364,7 +387,9 @@ export class ComboChartComponent extends BaseChartComponent {
} }
onActivate(item) { onActivate(item) {
const idx = this.activeEntries.findIndex((d) => d.name === item.name && d.value === item.value && d.series === item.series); const idx = this.activeEntries.findIndex(
(d) => d.name === item.name && d.value === item.value && d.series === item.series
);
if (idx > -1) { if (idx > -1) {
return; return;
} }
@ -374,7 +399,9 @@ export class ComboChartComponent extends BaseChartComponent {
} }
onDeactivate(item) { onDeactivate(item) {
const idx = this.activeEntries.findIndex((d) => d.name === item.name && d.value === item.value && d.series === item.series); const idx = this.activeEntries.findIndex(
(d) => d.name === item.name && d.value === item.value && d.series === item.series
);
this.activeEntries.splice(idx, 1); this.activeEntries.splice(idx, 1);
this.activeEntries = [...this.activeEntries]; this.activeEntries = [...this.activeEntries];

View File

@ -1,4 +1,11 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
Output
} from '@angular/core';
import { animate, style, transition, trigger } from '@angular/animations'; import { animate, style, transition, trigger } from '@angular/animations';
import { formatLabel } from '@swimlane/ngx-charts'; import { formatLabel } from '@swimlane/ngx-charts';
@ -151,7 +158,10 @@ export class ComboSeriesVerticalComponent implements OnChanges {
bar.gradientStops = this.colors.getLinearGradientStops(value); bar.gradientStops = this.colors.getLinearGradientStops(value);
} else { } else {
bar.color = this.colors.getColor(bar.offset1); bar.color = this.colors.getColor(bar.offset1);
bar.gradientStops = this.colors.getLinearGradientStops(bar.offset1, bar.offset0); bar.gradientStops = this.colors.getLinearGradientStops(
bar.offset1,
bar.offset0
);
} }
} }
@ -164,7 +174,9 @@ export class ComboSeriesVerticalComponent implements OnChanges {
const lineValue = this.seriesLine[0].series[index].value; const lineValue = this.seriesLine[0].series[index].value;
bar.tooltipText = ` bar.tooltipText = `
<span class='tooltip-label'>${tooltipLabel}</span> <span class='tooltip-label'>${tooltipLabel}</span>
<span class='tooltip-val'> Y1 - ${value.toLocaleString()} Y2 - ${lineValue.toLocaleString()}%</span> <span class='tooltip-val'>
Y1 - ${value.toLocaleString()} Y2 - ${lineValue.toLocaleString()}%
</span>
`; `;
return bar; return bar;
@ -177,7 +189,9 @@ export class ComboSeriesVerticalComponent implements OnChanges {
isActive(entry): boolean { isActive(entry): boolean {
if (!this.activeEntries) return false; if (!this.activeEntries) return false;
const item = this.activeEntries.find((d) => entry.name === d.name && entry.series === d.series); const item = this.activeEntries.find(
(d) => entry.name === d.name && entry.series === d.series
);
return item !== undefined; return item !== undefined;
} }

View File

@ -11,7 +11,15 @@ import { AppStateService } from '@state/app-state.service';
export class SideNavComponent { export class SideNavComponent {
@Input() type: 'settings' | 'project-templates'; @Input() type: 'settings' | 'project-templates';
items: { [key: string]: { screen: string; onlyDevMode?: boolean; onlyAdmin?: boolean; userManagerOnly?: boolean; label?: string }[] } = { items: {
[key: string]: {
screen: string;
onlyDevMode?: boolean;
onlyAdmin?: boolean;
userManagerOnly?: boolean;
label?: string;
}[];
} = {
settings: [ settings: [
{ screen: 'project-templates', onlyAdmin: true }, { screen: 'project-templates', onlyAdmin: true },
{ screen: 'digital-signature', onlyAdmin: true }, { screen: 'digital-signature', onlyAdmin: true },

View File

@ -1,5 +1,10 @@
<div class="collapsed-wrapper"> <div class="collapsed-wrapper">
<redaction-circle-button (action)="toggleCollapse.emit()" icon="red:expand" tooltip="user-stats.expand" tooltipPosition="before"></redaction-circle-button> <redaction-circle-button
(action)="toggleCollapse.emit()"
icon="red:expand"
tooltip="user-stats.expand"
tooltipPosition="before"
></redaction-circle-button>
<div class="all-caps-label" translate="user-stats.title"></div> <div class="all-caps-label" translate="user-stats.title"></div>
</div> </div>

View File

@ -1,6 +1,9 @@
<section class="dialog"> <section class="dialog">
<div class="dialog-header heading-l"> <div class="dialog-header heading-l">
{{ (dictionary ? 'add-edit-dictionary.title.edit' : 'add-edit-dictionary.title.new') | translate: { name: dictionary?.type | humanize } }} {{
(dictionary ? 'add-edit-dictionary.title.edit' : 'add-edit-dictionary.title.new')
| translate: { name: dictionary?.type | humanize }
}}
</div> </div>
<form (submit)="saveDictionary()" [formGroup]="dictionaryForm"> <form (submit)="saveDictionary()" [formGroup]="dictionaryForm">
@ -15,13 +18,23 @@
<div class="first-row"> <div class="first-row">
<div *ngIf="!dictionary" class="red-input-group required"> <div *ngIf="!dictionary" class="red-input-group required">
<label translate="add-edit-dictionary.form.name"></label> <label translate="add-edit-dictionary.form.name"></label>
<input formControlName="type" name="type" placeholder="{{ 'add-edit-dictionary.form.name-placeholder' | translate }}" type="text" /> <input
formControlName="type"
name="type"
placeholder="{{ 'add-edit-dictionary.form.name-placeholder' | translate }}"
type="text"
/>
<span class="hint" translate="add-edit-dictionary.form.name-hint"></span> <span class="hint" translate="add-edit-dictionary.form.name-hint"></span>
</div> </div>
<div class="red-input-group required w-75"> <div class="red-input-group required w-75">
<label translate="add-edit-dictionary.form.rank"></label> <label translate="add-edit-dictionary.form.rank"></label>
<input formControlName="rank" name="rank" placeholder="{{ 'add-edit-dictionary.form.rank-placeholder' | translate }}" type="number" /> <input
formControlName="rank"
name="rank"
placeholder="{{ 'add-edit-dictionary.form.rank-placeholder' | translate }}"
type="number"
/>
</div> </div>
<div class="red-input-group required"> <div class="red-input-group required">
@ -41,7 +54,10 @@
class="input-icon" class="input-icon"
> >
<mat-icon <mat-icon
*ngIf="!dictionaryForm.get('hexColor').value || dictionaryForm.get('hexColor').value?.length === 0" *ngIf="
!dictionaryForm.get('hexColor').value ||
dictionaryForm.get('hexColor').value?.length === 0
"
svgIcon="red:color-picker" svgIcon="red:color-picker"
></mat-icon> ></mat-icon>
</div> </div>
@ -53,7 +69,9 @@
<textarea <textarea
formControlName="description" formControlName="description"
name="description" name="description"
placeholder="{{ 'add-edit-dictionary.form.description-placeholder' | translate }}" placeholder="{{
'add-edit-dictionary.form.description-placeholder' | translate
}}"
redactionHasScrollbar redactionHasScrollbar
rows="4" rows="4"
type="text" type="text"
@ -62,8 +80,12 @@
<div class="red-input-group slider-row"> <div class="red-input-group slider-row">
<mat-button-toggle-group appearance="legacy" formControlName="hint" name="hint"> <mat-button-toggle-group appearance="legacy" formControlName="hint" name="hint">
<mat-button-toggle [value]="false"> {{ 'add-edit-dictionary.form.redaction' | translate }}</mat-button-toggle> <mat-button-toggle [value]="false">
<mat-button-toggle [value]="true"> {{ 'add-edit-dictionary.form.hint' | translate }}</mat-button-toggle> {{ 'add-edit-dictionary.form.redaction' | translate }}</mat-button-toggle
>
<mat-button-toggle [value]="true">
{{ 'add-edit-dictionary.form.hint' | translate }}</mat-button-toggle
>
</mat-button-toggle-group> </mat-button-toggle-group>
</div> </div>
@ -74,18 +96,31 @@
</div> </div>
<div class="red-input-group"> <div class="red-input-group">
<mat-checkbox color="primary" formControlName="addToDictionaryAction" name="addToDictionaryAction"> <mat-checkbox
color="primary"
formControlName="addToDictionaryAction"
name="addToDictionaryAction"
>
{{ 'add-edit-dictionary.form.add-to-dictionary-action' | translate }} {{ 'add-edit-dictionary.form.add-to-dictionary-action' | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>
<div class="dialog-actions"> <div class="dialog-actions">
<button [disabled]="dictionaryForm.invalid || !changed" color="primary" mat-flat-button type="submit"> <button
[disabled]="dictionaryForm.invalid || !changed"
color="primary"
mat-flat-button
type="submit"
>
{{ 'add-edit-dictionary.save' | translate }} {{ 'add-edit-dictionary.save' | translate }}
</button> </button>
</div> </div>
</form> </form>
<redaction-circle-button class="dialog-close" icon="red:close" mat-dialog-close></redaction-circle-button> <redaction-circle-button
class="dialog-close"
icon="red:close"
mat-dialog-close
></redaction-circle-button>
</section> </section>

View File

@ -65,7 +65,11 @@ export class AddEditDictionaryDialogComponent {
let observable: Observable<any>; let observable: Observable<any>;
if (this.dictionary) { if (this.dictionary) {
// edit mode // edit mode
observable = this._dictionaryControllerService.updateType(typeValue, typeValue.type, this._ruleSetId); observable = this._dictionaryControllerService.updateType(
typeValue,
typeValue.type,
this._ruleSetId
);
} else { } else {
// create mode // create mode
typeValue.ruleSetId = this._ruleSetId; typeValue.ruleSetId = this._ruleSetId;
@ -89,7 +93,11 @@ export class AddEditDictionaryDialogComponent {
} }
private _notifyError(message: string) { private _notifyError(message: string) {
this._notificationService.showToastNotification(this._translateService.instant(message), null, NotificationType.ERROR); this._notificationService.showToastNotification(
this._translateService.instant(message),
null,
NotificationType.ERROR
);
} }
private _formToObject(): TypeValue { private _formToObject(): TypeValue {

View File

@ -1,13 +1,23 @@
<section class="dialog"> <section class="dialog">
<div class="dialog-header heading-l"> <div class="dialog-header heading-l">
{{ (fileAttribute ? 'add-edit-file-attribute.title.edit' : 'add-edit-file-attribute.title.new') | translate: { name: fileAttribute?.label } }} {{
(fileAttribute
? 'add-edit-file-attribute.title.edit'
: 'add-edit-file-attribute.title.new'
) | translate: { name: fileAttribute?.label }
}}
</div> </div>
<form (submit)="saveFileAttribute()" [formGroup]="fileAttributeForm"> <form (submit)="saveFileAttribute()" [formGroup]="fileAttributeForm">
<div class="dialog-content"> <div class="dialog-content">
<div class="red-input-group required w-300"> <div class="red-input-group required w-300">
<label translate="add-edit-file-attribute.form.name"></label> <label translate="add-edit-file-attribute.form.name"></label>
<input formControlName="label" name="label" placeholder="{{ 'add-edit-file-attribute.form.name-placeholder' | translate }}" type="text" /> <input
formControlName="label"
name="label"
placeholder="{{ 'add-edit-file-attribute.form.name-placeholder' | translate }}"
type="text"
/>
</div> </div>
<div class="red-input-group required w-300"> <div class="red-input-group required w-300">
@ -15,7 +25,9 @@
<input <input
formControlName="csvColumnHeader" formControlName="csvColumnHeader"
name="csvColumnHeader" name="csvColumnHeader"
placeholder="{{ 'add-edit-file-attribute.form.column-header-placeholder' | translate }}" placeholder="{{
'add-edit-file-attribute.form.column-header-placeholder' | translate
}}"
type="text" type="text"
/> />
</div> </div>
@ -31,22 +43,37 @@
<div class="options-wrapper"> <div class="options-wrapper">
<div class="red-input-group"> <div class="red-input-group">
<mat-slide-toggle color="primary" formControlName="readonly">{{ 'add-edit-file-attribute.form.read-only' | translate }}</mat-slide-toggle> <mat-slide-toggle color="primary" formControlName="readonly">{{
'add-edit-file-attribute.form.read-only' | translate
}}</mat-slide-toggle>
</div> </div>
<div class="red-input-group mt-0"> <div class="red-input-group mt-0">
<mat-checkbox color="primary" formControlName="primaryAttribute" name="primaryAttribute"> <mat-checkbox
color="primary"
formControlName="primaryAttribute"
name="primaryAttribute"
>
{{ 'add-edit-file-attribute.form.primary' | translate }} {{ 'add-edit-file-attribute.form.primary' | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>
</div> </div>
<div class="dialog-actions"> <div class="dialog-actions">
<button [disabled]="fileAttributeForm.invalid || !changed" color="primary" mat-flat-button type="submit"> <button
[disabled]="fileAttributeForm.invalid || !changed"
color="primary"
mat-flat-button
type="submit"
>
{{ 'add-edit-file-attribute.save' | translate }} {{ 'add-edit-file-attribute.save' | translate }}
</button> </button>
</div> </div>
</form> </form>
<redaction-circle-button class="dialog-close" icon="red:close" mat-dialog-close></redaction-circle-button> <redaction-circle-button
class="dialog-close"
icon="red:close"
mat-dialog-close
></redaction-circle-button>
</section> </section>

View File

@ -13,13 +13,18 @@ export class AddEditFileAttributeDialogComponent {
fileAttributeForm: FormGroup; fileAttributeForm: FormGroup;
fileAttribute: FileAttributeConfig; fileAttribute: FileAttributeConfig;
ruleSetId: string; ruleSetId: string;
readonly typeOptions = [FileAttributeConfig.TypeEnum.TEXT, FileAttributeConfig.TypeEnum.NUMBER, FileAttributeConfig.TypeEnum.DATE]; readonly typeOptions = [
FileAttributeConfig.TypeEnum.TEXT,
FileAttributeConfig.TypeEnum.NUMBER,
FileAttributeConfig.TypeEnum.DATE
];
constructor( constructor(
private readonly _appStateService: AppStateService, private readonly _appStateService: AppStateService,
private readonly _formBuilder: FormBuilder, private readonly _formBuilder: FormBuilder,
public dialogRef: MatDialogRef<AddEditFileAttributeDialogComponent>, public dialogRef: MatDialogRef<AddEditFileAttributeDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { fileAttribute: FileAttributeConfig; ruleSetId: string } @Inject(MAT_DIALOG_DATA)
public data: { fileAttribute: FileAttributeConfig; ruleSetId: string }
) { ) {
this.fileAttribute = data.fileAttribute; this.fileAttribute = data.fileAttribute;
this.ruleSetId = data.ruleSetId; this.ruleSetId = data.ruleSetId;
@ -27,7 +32,10 @@ export class AddEditFileAttributeDialogComponent {
this.fileAttributeForm = this._formBuilder.group({ this.fileAttributeForm = this._formBuilder.group({
label: [this.fileAttribute?.label, Validators.required], label: [this.fileAttribute?.label, Validators.required],
csvColumnHeader: [this.fileAttribute?.csvColumnHeader, Validators.required], csvColumnHeader: [this.fileAttribute?.csvColumnHeader, Validators.required],
type: [this.fileAttribute?.type || FileAttributeConfig.TypeEnum.TEXT, Validators.required], type: [
this.fileAttribute?.type || FileAttributeConfig.TypeEnum.TEXT,
Validators.required
],
readonly: [this.fileAttribute ? !this.fileAttribute.editable : false], readonly: [this.fileAttribute ? !this.fileAttribute.editable : false],
primaryAttribute: [this.fileAttribute?.primaryAttribute] primaryAttribute: [this.fileAttribute?.primaryAttribute]
}); });

View File

@ -1,13 +1,25 @@
<section class="dialog"> <section class="dialog">
<div class="dialog-header heading-l"> <div class="dialog-header heading-l">
{{ (ruleSet ? 'add-edit-project-template.title.edit' : 'add-edit-project-template.title.new') | translate: { name: ruleSet?.name } }} {{
(ruleSet
? 'add-edit-project-template.title.edit'
: 'add-edit-project-template.title.new'
) | translate: { name: ruleSet?.name }
}}
</div> </div>
<form (submit)="saveRuleSet()" [formGroup]="ruleSetForm"> <form (submit)="saveRuleSet()" [formGroup]="ruleSetForm">
<div class="dialog-content"> <div class="dialog-content">
<div class="red-input-group required w-300"> <div class="red-input-group required w-300">
<label translate="add-edit-project-template.form.name"></label> <label translate="add-edit-project-template.form.name"></label>
<input formControlName="name" name="name" placeholder="{{ 'add-edit-project-template.form.name-placeholder' | translate }}" type="text" /> <input
formControlName="name"
name="name"
placeholder="{{
'add-edit-project-template.form.name-placeholder' | translate
}}"
type="text"
/>
</div> </div>
<div class="red-input-group w-400"> <div class="red-input-group w-400">
@ -15,7 +27,9 @@
<textarea <textarea
formControlName="description" formControlName="description"
name="description" name="description"
placeholder="{{ 'add-edit-project-template.form.description-placeholder' | translate }}" placeholder="{{
'add-edit-project-template.form.description-placeholder' | translate
}}"
rows="4" rows="4"
type="text" type="text"
></textarea> ></textarea>
@ -23,11 +37,21 @@
<div class="validity"> <div class="validity">
<div> <div>
<mat-checkbox (change)="hasValidFrom = !hasValidFrom" [checked]="hasValidFrom" class="filter-menu-checkbox" color="primary"> <mat-checkbox
(change)="hasValidFrom = !hasValidFrom"
[checked]="hasValidFrom"
class="filter-menu-checkbox"
color="primary"
>
{{ 'add-edit-project-template.form.valid-from' | translate }} {{ 'add-edit-project-template.form.valid-from' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox (change)="hasValidTo = !hasValidTo" [checked]="hasValidTo" class="filter-menu-checkbox" color="primary"> <mat-checkbox
(change)="hasValidTo = !hasValidTo"
[checked]="hasValidTo"
class="filter-menu-checkbox"
color="primary"
>
{{ 'add-edit-project-template.form.valid-to' | translate }} {{ 'add-edit-project-template.form.valid-to' | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
@ -35,7 +59,11 @@
<div> <div>
<div class="red-input-group datepicker-wrapper"> <div class="red-input-group datepicker-wrapper">
<ng-container *ngIf="hasValidFrom"> <ng-container *ngIf="hasValidFrom">
<input [matDatepicker]="fromPicker" formControlName="validFrom" placeholder="dd/mm/yy" /> <input
[matDatepicker]="fromPicker"
formControlName="validFrom"
placeholder="dd/mm/yy"
/>
<mat-datepicker-toggle [for]="fromPicker" matSuffix> <mat-datepicker-toggle [for]="fromPicker" matSuffix>
<mat-icon matDatepickerToggleIcon svgIcon="red:calendar"></mat-icon> <mat-icon matDatepickerToggleIcon svgIcon="red:calendar"></mat-icon>
</mat-datepicker-toggle> </mat-datepicker-toggle>
@ -45,7 +73,11 @@
<div class="red-input-group datepicker-wrapper"> <div class="red-input-group datepicker-wrapper">
<ng-container *ngIf="hasValidTo"> <ng-container *ngIf="hasValidTo">
<input [matDatepicker]="toPicker" formControlName="validTo" placeholder="dd/mm/yy" /> <input
[matDatepicker]="toPicker"
formControlName="validTo"
placeholder="dd/mm/yy"
/>
<mat-datepicker-toggle [for]="toPicker" matSuffix> <mat-datepicker-toggle [for]="toPicker" matSuffix>
<mat-icon matDatepickerToggleIcon svgIcon="red:calendar"></mat-icon> <mat-icon matDatepickerToggleIcon svgIcon="red:calendar"></mat-icon>
</mat-datepicker-toggle> </mat-datepicker-toggle>
@ -58,14 +90,25 @@
<p class="download-includes">{{ 'download-includes' | translate }}</p> <p class="download-includes">{{ 'download-includes' | translate }}</p>
<div class="space-between"> <div class="space-between">
<redaction-select <redaction-select
[label]="'report-type.label' | translate: { length: this.ruleSetForm.controls['reportTypes'].value.length }" [label]="
'report-type.label'
| translate
: { length: this.ruleSetForm.controls['reportTypes'].value.length }
"
[options]="reportTypesEnum" [options]="reportTypesEnum"
[translatePrefix]="'report-type.'" [translatePrefix]="'report-type.'"
class="w-410" class="w-410"
formControlName="reportTypes" formControlName="reportTypes"
></redaction-select> ></redaction-select>
<redaction-select <redaction-select
[label]="'download-type.label' | translate: { length: this.ruleSetForm.controls['downloadFileTypes'].value.length }" [label]="
'download-type.label'
| translate
: {
length: this.ruleSetForm.controls['downloadFileTypes'].value
.length
}
"
[options]="downloadTypesEnum" [options]="downloadTypesEnum"
[translatePrefix]="'download-type.'" [translatePrefix]="'download-type.'"
class="w-410" class="w-410"
@ -75,11 +118,20 @@
</div> </div>
<div class="dialog-actions"> <div class="dialog-actions">
<button [disabled]="ruleSetForm.invalid || !changed" color="primary" mat-flat-button type="submit"> <button
[disabled]="ruleSetForm.invalid || !changed"
color="primary"
mat-flat-button
type="submit"
>
{{ 'add-edit-project-template.save' | translate }} {{ 'add-edit-project-template.save' | translate }}
</button> </button>
</div> </div>
</form> </form>
<redaction-circle-button class="dialog-close" icon="red:close" mat-dialog-close></redaction-circle-button> <redaction-circle-button
class="dialog-close"
icon="red:close"
mat-dialog-close
></redaction-circle-button>
</section> </section>

View File

@ -32,13 +32,25 @@ export class AddEditRuleSetDialogComponent {
this.ruleSetForm = this._formBuilder.group({ this.ruleSetForm = this._formBuilder.group({
name: [this.ruleSet?.name, Validators.required], name: [this.ruleSet?.name, Validators.required],
description: [this.ruleSet?.description], description: [this.ruleSet?.description],
validFrom: [this.ruleSet?.validFrom ? moment(this.ruleSet?.validFrom) : null, this._requiredIfValidator(() => this.hasValidFrom)], validFrom: [
validTo: [this.ruleSet?.validTo ? moment(this.ruleSet?.validTo) : null, this._requiredIfValidator(() => this.hasValidTo)], this.ruleSet?.validFrom ? moment(this.ruleSet?.validFrom) : null,
downloadFileTypes: [this.ruleSet ? this.ruleSet.downloadFileTypes : ['PREVIEW', 'REDACTED']], this._requiredIfValidator(() => this.hasValidFrom)
],
validTo: [
this.ruleSet?.validTo ? moment(this.ruleSet?.validTo) : null,
this._requiredIfValidator(() => this.hasValidTo)
],
downloadFileTypes: [
this.ruleSet ? this.ruleSet.downloadFileTypes : ['PREVIEW', 'REDACTED']
],
reportTypes: [ reportTypes: [
this.ruleSet this.ruleSet
? this.ruleSet.reportTypes ? this.ruleSet.reportTypes
: ['WORD_SINGLE_FILE_APPENDIX_A1_TEMPLATE', 'WORD_SINGLE_FILE_APPENDIX_A2_TEMPLATE', 'EXCEL_MULTI_FILE'], : [
'WORD_SINGLE_FILE_APPENDIX_A1_TEMPLATE',
'WORD_SINGLE_FILE_APPENDIX_A2_TEMPLATE',
'EXCEL_MULTI_FILE'
],
Validators.required Validators.required
] ]
}); });
@ -61,14 +73,24 @@ export class AddEditRuleSetDialogComponent {
if (this.hasValidFrom !== !!this.ruleSet.validFrom) { if (this.hasValidFrom !== !!this.ruleSet.validFrom) {
return true; return true;
} }
if (this.hasValidFrom && !moment(this.ruleSet.validFrom).isSame(moment(this.ruleSetForm.get('validFrom').value))) { if (
this.hasValidFrom &&
!moment(this.ruleSet.validFrom).isSame(
moment(this.ruleSetForm.get('validFrom').value)
)
) {
return true; return true;
} }
} else if (key === 'validTo') { } else if (key === 'validTo') {
if (this.hasValidTo !== !!this.ruleSet.validTo) { if (this.hasValidTo !== !!this.ruleSet.validTo) {
return true; return true;
} }
if (this.hasValidTo && !moment(this.ruleSet.validTo).isSame(moment(this.ruleSetForm.get('validTo').value))) { if (
this.hasValidTo &&
!moment(this.ruleSet.validTo).isSame(
moment(this.ruleSetForm.get('validTo').value)
)
) {
return true; return true;
} }
} else if (this.ruleSet[key] !== this.ruleSetForm.get(key).value) { } else if (this.ruleSet[key] !== this.ruleSetForm.get(key).value) {
@ -93,7 +115,16 @@ export class AddEditRuleSetDialogComponent {
} }
private _applyValidityIntervalConstraints(value): boolean { private _applyValidityIntervalConstraints(value): boolean {
if (applyIntervalConstraints(value, this._previousValidFrom, this._previousValidTo, this.ruleSetForm, 'validFrom', 'validTo')) { if (
applyIntervalConstraints(
value,
this._previousValidFrom,
this._previousValidTo,
this.ruleSetForm,
'validFrom',
'validTo'
)
) {
return true; return true;
} }

View File

@ -28,7 +28,11 @@
<div class="red-input-group"> <div class="red-input-group">
<label translate="add-edit-user.form.role"></label> <label translate="add-edit-user.form.role"></label>
<div class="roles-wrapper"> <div class="roles-wrapper">
<mat-checkbox *ngFor="let role of ROLES" [formControlName]="role" color="primary"> <mat-checkbox
*ngFor="let role of ROLES"
[formControlName]="role"
color="primary"
>
{{ 'roles.' + role | translate }} {{ 'roles.' + role | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
@ -36,15 +40,37 @@
</div> </div>
<div class="dialog-actions"> <div class="dialog-actions">
<button [disabled]="userForm.invalid || !changed" color="primary" mat-flat-button type="submit"> <button
{{ (user ? 'add-edit-user.actions.save-changes' : 'add-edit-user.actions.save') | translate }} [disabled]="userForm.invalid || !changed"
color="primary"
mat-flat-button
type="submit"
>
{{
(user ? 'add-edit-user.actions.save-changes' : 'add-edit-user.actions.save')
| translate
}}
</button> </button>
<redaction-icon-button (action)="delete()" *ngIf="user" icon="red:trash" text="add-edit-user.actions.delete" type="show-bg"></redaction-icon-button> <redaction-icon-button
(action)="delete()"
*ngIf="user"
icon="red:trash"
text="add-edit-user.actions.delete"
type="show-bg"
></redaction-icon-button>
<div class="all-caps-label cancel" mat-dialog-close translate="add-edit-user.actions.cancel"></div> <div
class="all-caps-label cancel"
mat-dialog-close
translate="add-edit-user.actions.cancel"
></div>
</div> </div>
</form> </form>
<redaction-circle-button class="dialog-close" icon="red:close" mat-dialog-close></redaction-circle-button> <redaction-circle-button
class="dialog-close"
icon="red:close"
mat-dialog-close
></redaction-circle-button>
</section> </section>

View File

@ -29,7 +29,10 @@ export class AddEditUserDialogComponent {
disabled: disabled:
this.user && this.user &&
Object.keys(this._ROLE_REQUIREMENTS).reduce( Object.keys(this._ROLE_REQUIREMENTS).reduce(
(value, key) => value || (role === this._ROLE_REQUIREMENTS[key] && this.user.roles.indexOf(key) !== -1), (value, key) =>
value ||
(role === this._ROLE_REQUIREMENTS[key] &&
this.user.roles.indexOf(key) !== -1),
false false
) )
} }
@ -40,7 +43,10 @@ export class AddEditUserDialogComponent {
this.userForm = this._formBuilder.group({ this.userForm = this._formBuilder.group({
firstName: [this.user?.firstName, Validators.required], firstName: [this.user?.firstName, Validators.required],
lastName: [this.user?.lastName, Validators.required], lastName: [this.user?.lastName, Validators.required],
email: [{ value: this.user?.email, disabled: !!user }, [Validators.required, Validators.email]], email: [
{ value: this.user?.email, disabled: !!user },
[Validators.required, Validators.email]
],
// password: [this.user?.password, Validators.required], // password: [this.user?.password, Validators.required],
...rolesControls ...rolesControls
}); });

View File

@ -1,6 +1,9 @@
<section class="dialog"> <section class="dialog">
<div class="dialog-header heading-l"> <div class="dialog-header heading-l">
{{ 'confirm-delete-file-attribute.title.' + type | translate: { name: fileAttribute?.label } }} {{
'confirm-delete-file-attribute.title.' + type
| translate: { name: fileAttribute?.label }
}}
</div> </div>
<div *ngIf="showToast" class="inline-dialog-toast toast-error"> <div *ngIf="showToast" class="inline-dialog-toast toast-error">
@ -27,7 +30,15 @@
<button (click)="deleteFileAttribute()" color="primary" mat-flat-button> <button (click)="deleteFileAttribute()" color="primary" mat-flat-button>
{{ 'confirm-delete-file-attribute.delete.' + type | translate }} {{ 'confirm-delete-file-attribute.delete.' + type | translate }}
</button> </button>
<div (click)="cancel()" [translate]="'confirm-delete-file-attribute.cancel.' + type" class="all-caps-label cancel"></div> <div
(click)="cancel()"
[translate]="'confirm-delete-file-attribute.cancel.' + type"
class="all-caps-label cancel"
></div>
</div> </div>
<redaction-circle-button class="dialog-close" icon="red:close" mat-dialog-close></redaction-circle-button> <redaction-circle-button
class="dialog-close"
icon="red:close"
mat-dialog-close
></redaction-circle-button>
</section> </section>

View File

@ -17,7 +17,10 @@
[class.error]="!checkbox.value && showToast" [class.error]="!checkbox.value && showToast"
color="primary" color="primary"
> >
{{ 'confirm-delete-users.' + checkbox.label | translate: { projectsCount: projectsCount } }} {{
'confirm-delete-users.' + checkbox.label
| translate: { projectsCount: projectsCount }
}}
</mat-checkbox> </mat-checkbox>
</div> </div>
@ -25,7 +28,15 @@
<button (click)="deleteUser()" color="primary" mat-flat-button> <button (click)="deleteUser()" color="primary" mat-flat-button>
{{ 'confirm-delete-users.delete.' + type | translate }} {{ 'confirm-delete-users.delete.' + type | translate }}
</button> </button>
<div (click)="cancel()" [translate]="'confirm-delete-users.cancel.' + type" class="all-caps-label cancel"></div> <div
(click)="cancel()"
[translate]="'confirm-delete-users.cancel.' + type"
class="all-caps-label cancel"
></div>
</div> </div>
<redaction-circle-button class="dialog-close" icon="red:close" mat-dialog-close></redaction-circle-button> <redaction-circle-button
class="dialog-close"
icon="red:close"
mat-dialog-close
></redaction-circle-button>
</section> </section>

View File

@ -1,5 +1,8 @@
<section class="dialog"> <section class="dialog">
<div [translate]="'default-colors-screen.types.' + this.colorKey" class="dialog-header heading-l"></div> <div
[translate]="'default-colors-screen.types.' + this.colorKey"
class="dialog-header heading-l"
></div>
<form (submit)="saveColors()" [formGroup]="colorForm"> <form (submit)="saveColors()" [formGroup]="colorForm">
<div class="dialog-content"> <div class="dialog-content">
@ -19,17 +22,32 @@
[style.background]="colorForm.get('color').value" [style.background]="colorForm.get('color').value"
class="input-icon" class="input-icon"
> >
<mat-icon *ngIf="!colorForm.get('color').value || colorForm.get('color').value?.length === 0" svgIcon="red:color-picker"></mat-icon> <mat-icon
*ngIf="
!colorForm.get('color').value ||
colorForm.get('color').value?.length === 0
"
svgIcon="red:color-picker"
></mat-icon>
</div> </div>
</div> </div>
</div> </div>
<div class="dialog-actions"> <div class="dialog-actions">
<button [disabled]="colorForm.invalid || !changed" color="primary" mat-flat-button type="submit"> <button
[disabled]="colorForm.invalid || !changed"
color="primary"
mat-flat-button
type="submit"
>
{{ 'edit-color-dialog.save' | translate }} {{ 'edit-color-dialog.save' | translate }}
</button> </button>
</div> </div>
</form> </form>
<redaction-circle-button class="dialog-close" icon="red:close" mat-dialog-close></redaction-circle-button> <redaction-circle-button
class="dialog-close"
icon="red:close"
mat-dialog-close
></redaction-circle-button>
</section> </section>

View File

@ -23,7 +23,8 @@ export class EditColorDialogComponent {
private readonly _notificationService: NotificationService, private readonly _notificationService: NotificationService,
private readonly _translateService: TranslateService, private readonly _translateService: TranslateService,
public dialogRef: MatDialogRef<EditColorDialogComponent>, public dialogRef: MatDialogRef<EditColorDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { colors: Colors; colorKey: string; ruleSetId: string } @Inject(MAT_DIALOG_DATA)
public data: { colors: Colors; colorKey: string; ruleSetId: string }
) { ) {
this.colors = data.colors; this.colors = data.colors;
this.colorKey = data.colorKey; this.colorKey = data.colorKey;
@ -50,11 +51,17 @@ export class EditColorDialogComponent {
this.dialogRef.close(true); this.dialogRef.close(true);
this._notificationService.showToastNotification( this._notificationService.showToastNotification(
this._translateService.instant('edit-color-dialog.success', { this._translateService.instant('edit-color-dialog.success', {
color: this._translateService.instant('default-colors-screen.types.' + this.colorKey) color: this._translateService.instant(
'default-colors-screen.types.' + this.colorKey
)
}) })
); );
} catch (e) { } catch (e) {
this._notificationService.showToastNotification(this._translateService.instant('edit-color-dialog.error'), null, NotificationType.ERROR); this._notificationService.showToastNotification(
this._translateService.instant('edit-color-dialog.error'),
null,
NotificationType.ERROR
);
} }
} }
} }

View File

@ -7,7 +7,10 @@
></redaction-round-checkbox> ></redaction-round-checkbox>
</div> </div>
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'file-attributes-csv-import.table-header.title' | translate: { length: allEntities.length } }} {{
'file-attributes-csv-import.table-header.title'
| translate: { length: allEntities.length }
}}
</span> </span>
<ng-container *ngIf="areSomeEntitiesSelected"> <ng-container *ngIf="areSomeEntitiesSelected">
@ -29,7 +32,10 @@
<div class="separator"></div> <div class="separator"></div>
<redaction-chevron-button [matMenuTriggerFor]="typeMenu" text="file-attributes-csv-import.table-header.actions.type"></redaction-chevron-button> <redaction-chevron-button
[matMenuTriggerFor]="typeMenu"
text="file-attributes-csv-import.table-header.actions.type"
></redaction-chevron-button>
<mat-menu #readOnlyMenu="matMenu" class="no-padding-bottom"> <mat-menu #readOnlyMenu="matMenu" class="no-padding-bottom">
<button <button
@ -45,7 +51,11 @@
</mat-menu> </mat-menu>
<mat-menu #typeMenu="matMenu" class="no-padding-bottom"> <mat-menu #typeMenu="matMenu" class="no-padding-bottom">
<button (click)="setAttributeForSelection('type', type)" *ngFor="let type of typeOptions" mat-menu-item> <button
(click)="setAttributeForSelection('type', type)"
*ngFor="let type of typeOptions"
mat-menu-item
>
{{ 'file-attribute-types.' + type | translate }} {{ 'file-attribute-types.' + type | translate }}
</button> </button>
</mat-menu> </mat-menu>
@ -55,9 +65,14 @@
<div class="table-header" redactionSyncWidth="table-item"> <div class="table-header" redactionSyncWidth="table-item">
<div class="select-oval-placeholder"></div> <div class="select-oval-placeholder"></div>
<redaction-table-col-name class="name" label="file-attributes-csv-import.table-col-names.name"></redaction-table-col-name> <redaction-table-col-name
class="name"
label="file-attributes-csv-import.table-col-names.name"
></redaction-table-col-name>
<redaction-table-col-name label="file-attributes-csv-import.table-col-names.type"></redaction-table-col-name> <redaction-table-col-name
label="file-attributes-csv-import.table-col-names.type"
></redaction-table-col-name>
<redaction-table-col-name <redaction-table-col-name
class="flex-center" class="flex-center"
@ -76,7 +91,11 @@
<div class="scrollbar-placeholder"></div> <div class="scrollbar-placeholder"></div>
</div> </div>
<redaction-empty-state *ngIf="!allEntities.length" icon="red:attribute" screen="file-attributes-csv-import"></redaction-empty-state> <redaction-empty-state
*ngIf="!allEntities.length"
icon="red:attribute"
screen="file-attributes-csv-import"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="50" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="50" redactionHasScrollbar>
<!-- Table lines --> <!-- Table lines -->
@ -93,7 +112,10 @@
<div *ngIf="!field.editingName"> <div *ngIf="!field.editingName">
{{ field.name }} {{ field.name }}
</div> </div>
<form (submit)="field.editingName = false; field.name = field.temporaryName" *ngIf="field.editingName"> <form
(submit)="field.editingName = false; field.name = field.temporaryName"
*ngIf="field.editingName"
>
<div class="red-input-group w-200"> <div class="red-input-group w-200">
<input [(ngModel)]="field.temporaryName" name="name" /> <input [(ngModel)]="field.temporaryName" name="name" />
</div> </div>
@ -139,7 +161,10 @@
<mat-slide-toggle [(ngModel)]="field.readonly" color="primary"></mat-slide-toggle> <mat-slide-toggle [(ngModel)]="field.readonly" color="primary"></mat-slide-toggle>
</div> </div>
<div class="center"> <div class="center">
<redaction-round-checkbox (click)="togglePrimary(field)" [active]="field.primaryAttribute"></redaction-round-checkbox> <redaction-round-checkbox
(click)="togglePrimary(field)"
[active]="field.primaryAttribute"
></redaction-round-checkbox>
</div> </div>
<div class="actions-container"> <div class="actions-container">
<div class="action-buttons"> <div class="action-buttons">

View File

@ -1,4 +1,12 @@
import { Component, EventEmitter, Injector, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import {
Component,
EventEmitter,
Injector,
Input,
OnChanges,
Output,
SimpleChanges
} from '@angular/core';
import { BaseListingComponent } from '@shared/base/base-listing.component'; import { BaseListingComponent } from '@shared/base/base-listing.component';
import { Field } from '../file-attributes-csv-import-dialog.component'; import { Field } from '../file-attributes-csv-import-dialog.component';
import { FileAttributeConfig } from '@redaction/red-ui-http'; import { FileAttributeConfig } from '@redaction/red-ui-http';
@ -14,7 +22,11 @@ export class ActiveFieldsListingComponent extends BaseListingComponent<Field> im
@Output() setHoveredColumn = new EventEmitter<string>(); @Output() setHoveredColumn = new EventEmitter<string>();
@Output() toggleFieldActive = new EventEmitter<Field>(); @Output() toggleFieldActive = new EventEmitter<Field>();
readonly typeOptions = [FileAttributeConfig.TypeEnum.TEXT, FileAttributeConfig.TypeEnum.NUMBER, FileAttributeConfig.TypeEnum.DATE]; readonly typeOptions = [
FileAttributeConfig.TypeEnum.TEXT,
FileAttributeConfig.TypeEnum.NUMBER,
FileAttributeConfig.TypeEnum.DATE
];
protected readonly _selectionKey = 'csvColumn'; protected readonly _selectionKey = 'csvColumn';
@ -30,7 +42,9 @@ export class ActiveFieldsListingComponent extends BaseListingComponent<Field> im
} }
deactivateSelection() { deactivateSelection() {
this.allEntities.filter((field) => this.isEntitySelected(field)).forEach((field) => (field.primaryAttribute = false)); this.allEntities
.filter((field) => this.isEntitySelected(field))
.forEach((field) => (field.primaryAttribute = false));
this.allEntities = [...this.allEntities.filter((field) => !this.isEntitySelected(field))]; this.allEntities = [...this.allEntities.filter((field) => !this.isEntitySelected(field))];
this.allEntitiesChange.emit(this.allEntities); this.allEntitiesChange.emit(this.allEntities);
this.selectedEntitiesIds = []; this.selectedEntitiesIds = [];

View File

@ -4,25 +4,37 @@
<div class="dialog-content"> <div class="dialog-content">
<div class="sub-header"> <div class="sub-header">
<div class="left"> <div class="left">
<div class="info"><span translate="file-attributes-csv-import.file"> </span> {{ csvFile.name }}</div> <div class="info">
<span translate="file-attributes-csv-import.file"> </span> {{ csvFile.name }}
</div>
<div class="large-label"> <div class="large-label">
{{ 'file-attributes-csv-import.total-rows' | translate: { rows: parseResult?.data?.length } }} {{
'file-attributes-csv-import.total-rows'
| translate: { rows: parseResult?.data?.length }
}}
</div> </div>
</div> </div>
<div class="right"> <div class="right">
<form (submit)="changedParseConfig && readFile()" [formGroup]="baseConfigForm"> <form (submit)="changedParseConfig && readFile()" [formGroup]="baseConfigForm">
<div class="red-input-group required w-250"> <div class="red-input-group required w-250">
<mat-form-field floatLabel="always"> <mat-form-field floatLabel="always">
<mat-label>{{ 'file-attributes-csv-import.key-column' | translate }}</mat-label> <mat-label>{{
'file-attributes-csv-import.key-column' | translate
}}</mat-label>
<input <input
[matAutocomplete]="auto" [matAutocomplete]="auto"
[placeholder]="'file-attributes-csv-import.key-column-placeholder' | translate" [placeholder]="
'file-attributes-csv-import.key-column-placeholder' | translate
"
formControlName="filenameMappingColumnHeaderName" formControlName="filenameMappingColumnHeaderName"
matInput matInput
type="text" type="text"
/> />
<mat-autocomplete #auto="matAutocomplete" autoActiveFirstOption> <mat-autocomplete #auto="matAutocomplete" autoActiveFirstOption>
<mat-option *ngFor="let field of filteredKeyOptions | async" [value]="field"> <mat-option
*ngFor="let field of filteredKeyOptions | async"
[value]="field"
>
{{ field }} {{ field }}
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
@ -32,7 +44,9 @@
<div class="red-input-group required w-110"> <div class="red-input-group required w-110">
<label translate="file-attributes-csv-import.delimiter"></label> <label translate="file-attributes-csv-import.delimiter"></label>
<input <input
[placeholder]="'file-attributes-csv-import.delimiter-placeholder' | translate" [placeholder]="
'file-attributes-csv-import.delimiter-placeholder' | translate
"
formControlName="delimiter" formControlName="delimiter"
name="delimiter" name="delimiter"
type="text" type="text"
@ -42,7 +56,9 @@
<div class="red-input-group required w-160"> <div class="red-input-group required w-160">
<label translate="file-attributes-csv-import.encoding"></label> <label translate="file-attributes-csv-import.encoding"></label>
<input <input
[placeholder]="'file-attributes-csv-import.encoding-placeholder' | translate" [placeholder]="
'file-attributes-csv-import.encoding-placeholder' | translate
"
formControlName="encoding" formControlName="encoding"
name="encoding" name="encoding"
type="text" type="text"
@ -62,9 +78,13 @@
<div class="csv-part-header"> <div class="csv-part-header">
<div> <div>
<span class="all-caps-label">{{ <span class="all-caps-label">{{
'file-attributes-csv-import.available' | translate: { value: parseResult?.meta?.fields.length } 'file-attributes-csv-import.available'
| translate: { value: parseResult?.meta?.fields.length }
}}</span>
<span class="all-caps-label">{{
'file-attributes-csv-import.selected'
| translate: { value: activeFields.length }
}}</span> }}</span>
<span class="all-caps-label">{{ 'file-attributes-csv-import.selected' | translate: { value: activeFields.length } }}</span>
</div> </div>
<div class="actions"> <div class="actions">
<redaction-circle-button <redaction-circle-button
@ -106,17 +126,29 @@
{{ field.csvColumn }} {{ field.csvColumn }}
</div> </div>
<div class="secondary"> <div class="secondary">
<div class="entry-count small-label">{{ getEntries(field.csvColumn) }} entries</div> <div class="entry-count small-label">
<div class="sample small-label">Sample: {{ getSample(field.csvColumn) }}</div> {{ getEntries(field.csvColumn) }} entries
</div>
<div class="sample small-label">
Sample: {{ getSample(field.csvColumn) }}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div (mouseenter)="keepPreview = true" (mouseleave)="keepPreview = false; setHoveredColumn()" [class.collapsed]="!previewExpanded" class="center"> <div
(mouseenter)="keepPreview = true"
(mouseleave)="keepPreview = false; setHoveredColumn()"
[class.collapsed]="!previewExpanded"
class="center"
>
<div class="csv-part-header"> <div class="csv-part-header">
<div class="all-caps-label"> <div class="all-caps-label">
{{ 'file-attributes-csv-import.csv-column' + (previewExpanded ? '' : '-preview') | translate }} {{
'file-attributes-csv-import.csv-column' +
(previewExpanded ? '' : '-preview') | translate
}}
</div> </div>
<redaction-circle-button <redaction-circle-button
(click)="previewExpanded = !previewExpanded" (click)="previewExpanded = !previewExpanded"
@ -124,9 +156,16 @@
></redaction-circle-button> ></redaction-circle-button>
</div> </div>
<div [class.hidden]="!previewExpanded" class="csv-part-content"> <div [class.hidden]="!previewExpanded" class="csv-part-content">
<div *ngIf="!hoveredColumn" class="no-column-data" translate="file-attributes-csv-import.no-hovered-column"></div> <div
*ngIf="!hoveredColumn"
class="no-column-data"
translate="file-attributes-csv-import.no-hovered-column"
></div>
<div *ngIf="hoveredColumn && !columnSample.length" class="no-column-data"> <div *ngIf="hoveredColumn && !columnSample.length" class="no-column-data">
{{ 'file-attributes-csv-import.no-sample-data-for' | translate: { column: hoveredColumn } }} {{
'file-attributes-csv-import.no-sample-data-for'
| translate: { column: hoveredColumn }
}}
</div> </div>
<div *ngFor="let row of columnSample"> <div *ngFor="let row of columnSample">
{{ row }} {{ row }}
@ -144,12 +183,23 @@
</div> </div>
<div class="dialog-actions"> <div class="dialog-actions">
<button (click)="save()" [disabled]="changedParseConfig || baseConfigForm.invalid" color="primary" mat-flat-button> <button
(click)="save()"
[disabled]="changedParseConfig || baseConfigForm.invalid"
color="primary"
mat-flat-button
>
{{ 'file-attributes-csv-import.save.label' | translate }} {{ 'file-attributes-csv-import.save.label' | translate }}
</button> </button>
<div (click)="dialogRef.close()" class="all-caps-label cancel">{{ 'file-attributes-csv-import.cancel' | translate }}</div> <div (click)="dialogRef.close()" class="all-caps-label cancel">
{{ 'file-attributes-csv-import.cancel' | translate }}
</div>
</div> </div>
<redaction-circle-button class="dialog-close" icon="red:close" mat-dialog-close></redaction-circle-button> <redaction-circle-button
class="dialog-close"
icon="red:close"
mat-dialog-close
></redaction-circle-button>
</section> </section>

View File

@ -3,7 +3,11 @@ import { AbstractControl, FormGroup, ValidatorFn, Validators } from '@angular/fo
import { AppStateService } from '@state/app-state.service'; import { AppStateService } from '@state/app-state.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import * as Papa from 'papaparse'; import * as Papa from 'papaparse';
import { FileAttributeConfig, FileAttributesConfig, FileAttributesControllerService } from '@redaction/red-ui-http'; import {
FileAttributeConfig,
FileAttributesConfig,
FileAttributesControllerService
} from '@redaction/red-ui-http';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators'; import { map, startWith } from 'rxjs/operators';
@ -40,7 +44,8 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
keepPreview = false; keepPreview = false;
columnSample = []; columnSample = [];
initialParseConfig: { delimiter?: string; encoding?: string } = {}; initialParseConfig: { delimiter?: string; encoding?: string } = {};
@ViewChild(CdkVirtualScrollViewport, { static: false }) cdkVirtualScrollViewport: CdkVirtualScrollViewport; @ViewChild(CdkVirtualScrollViewport, { static: false })
cdkVirtualScrollViewport: CdkVirtualScrollViewport;
protected readonly _searchKey = 'csvColumn'; protected readonly _searchKey = 'csvColumn';
constructor( constructor(
@ -50,14 +55,18 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
private readonly _notificationService: NotificationService, private readonly _notificationService: NotificationService,
public dialogRef: MatDialogRef<FileAttributesCsvImportDialogComponent>, public dialogRef: MatDialogRef<FileAttributesCsvImportDialogComponent>,
protected readonly _injector: Injector, protected readonly _injector: Injector,
@Inject(MAT_DIALOG_DATA) public data: { csv: File; ruleSetId: string; existingConfiguration: FileAttributesConfig } @Inject(MAT_DIALOG_DATA)
public data: { csv: File; ruleSetId: string; existingConfiguration: FileAttributesConfig }
) { ) {
super(_injector); super(_injector);
this.csvFile = data.csv; this.csvFile = data.csv;
this.ruleSetId = data.ruleSetId; this.ruleSetId = data.ruleSetId;
this.baseConfigForm = this._formBuilder.group({ this.baseConfigForm = this._formBuilder.group({
filenameMappingColumnHeaderName: ['', [Validators.required, this._autocompleteStringValidator()]], filenameMappingColumnHeaderName: [
'',
[Validators.required, this._autocompleteStringValidator()]
],
delimiter: [undefined, Validators.required], delimiter: [undefined, Validators.required],
encoding: ['UTF-8', Validators.required] encoding: ['UTF-8', Validators.required]
}); });
@ -87,12 +96,16 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
this.parseResult.meta.fields = Object.keys(this.parseResult.data[0]); this.parseResult.meta.fields = Object.keys(this.parseResult.data[0]);
} }
this.allEntities = this.parseResult.meta.fields.map((field) => this._buildAttribute(field)); this.allEntities = this.parseResult.meta.fields.map((field) =>
this._buildAttribute(field)
);
this.displayedEntities = [...this.allEntities]; this.displayedEntities = [...this.allEntities];
this.activeFields = []; this.activeFields = [];
for (const entity of this.allEntities) { for (const entity of this.allEntities) {
const existing = this.data.existingConfiguration.fileAttributeConfigs.find((a) => a.csvColumnHeader === entity.csvColumn); const existing = this.data.existingConfiguration.fileAttributeConfigs.find(
(a) => a.csvColumnHeader === entity.csvColumn
);
if (existing) { if (existing) {
entity.id = existing.id; entity.id = existing.id;
entity.name = existing.label; entity.name = existing.label;
@ -104,18 +117,35 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
} }
} }
this.filteredKeyOptions = this.baseConfigForm.get('filenameMappingColumnHeaderName').valueChanges.pipe( this.filteredKeyOptions = this.baseConfigForm
startWith(this.baseConfigForm.get('filenameMappingColumnHeaderName').value as string), .get('filenameMappingColumnHeaderName')
map((value: string) => .valueChanges.pipe(
this.allEntities.filter((field) => field.csvColumn.toLowerCase().indexOf(value.toLowerCase()) !== -1).map((field) => field.csvColumn) startWith(
) this.baseConfigForm.get('filenameMappingColumnHeaderName').value as string
); ),
map((value: string) =>
this.allEntities
.filter(
(field) =>
field.csvColumn.toLowerCase().indexOf(value.toLowerCase()) !==
-1
)
.map((field) => field.csvColumn)
)
);
if ( if (
this.data.existingConfiguration && this.data.existingConfiguration &&
this.allEntities.find((entity) => entity.csvColumn === this.data.existingConfiguration.filenameMappingColumnHeaderName) this.allEntities.find(
(entity) =>
entity.csvColumn ===
this.data.existingConfiguration.filenameMappingColumnHeaderName
)
) { ) {
this.baseConfigForm.patchValue({ filenameMappingColumnHeaderName: this.data.existingConfiguration.filenameMappingColumnHeaderName }); this.baseConfigForm.patchValue({
filenameMappingColumnHeaderName:
this.data.existingConfiguration.filenameMappingColumnHeaderName
});
} }
this.initialParseConfig = { this.initialParseConfig = {
@ -169,14 +199,17 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
const newPrimary = !!this.activeFields.find((attr) => attr.primaryAttribute); const newPrimary = !!this.activeFields.find((attr) => attr.primaryAttribute);
if (newPrimary) { if (newPrimary) {
this.data.existingConfiguration.fileAttributeConfigs.forEach((attr) => (attr.primaryAttribute = false)); this.data.existingConfiguration.fileAttributeConfigs.forEach(
(attr) => (attr.primaryAttribute = false)
);
} }
const fileAttributes = { const fileAttributes = {
...this.baseConfigForm.getRawValue(), ...this.baseConfigForm.getRawValue(),
fileAttributeConfigs: [ fileAttributeConfigs: [
...this.data.existingConfiguration.fileAttributeConfigs.filter( ...this.data.existingConfiguration.fileAttributeConfigs.filter(
(a) => !this.allEntities.find((entity) => entity.csvColumn === a.csvColumnHeader) (a) =>
!this.allEntities.find((entity) => entity.csvColumn === a.csvColumnHeader)
), ),
...this.activeFields.map((field) => ({ ...this.activeFields.map((field) => ({
id: field.id, id: field.id,
@ -190,9 +223,13 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
}; };
try { try {
await this._fileAttributesControllerService.setFileAttributesConfig(fileAttributes, this.ruleSetId).toPromise(); await this._fileAttributesControllerService
.setFileAttributesConfig(fileAttributes, this.ruleSetId)
.toPromise();
this._notificationService.showToastNotification( this._notificationService.showToastNotification(
this._translateService.instant('file-attributes-csv-import.save.success', { count: this.activeFields.length }), this._translateService.instant('file-attributes-csv-import.save.success', {
count: this.activeFields.length
}),
null, null,
NotificationType.SUCCESS NotificationType.SUCCESS
); );
@ -217,7 +254,9 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
if (!column) { if (!column) {
this.columnSample = []; this.columnSample = [];
} else { } else {
this.columnSample = this.parseResult.data.filter((row) => !!row[column]).map((row) => row[column]); this.columnSample = this.parseResult.data
.filter((row) => !!row[column])
.map((row) => row[column]);
} }
}, 0); }, 0);
} }
@ -238,7 +277,9 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
csvColumn, csvColumn,
name: csvColumn, name: csvColumn,
temporaryName: csvColumn, temporaryName: csvColumn,
type: isNumber ? FileAttributeConfig.TypeEnum.NUMBER : FileAttributeConfig.TypeEnum.TEXT, type: isNumber
? FileAttributeConfig.TypeEnum.NUMBER
: FileAttributeConfig.TypeEnum.TEXT,
readonly: false, readonly: false,
primaryAttribute: false primaryAttribute: false
}; };

View File

@ -5,7 +5,12 @@
<div class="dialog-content"> <div class="dialog-content">
<div class="red-input-group required w-300"> <div class="red-input-group required w-300">
<label translate="smtp-auth-config.form.username"></label> <label translate="smtp-auth-config.form.username"></label>
<input formControlName="user" name="user" placeholder="{{ 'smtp-auth-config.form.username-placeholder' | translate }}" type="text" /> <input
formControlName="user"
name="user"
placeholder="{{ 'smtp-auth-config.form.username-placeholder' | translate }}"
type="text"
/>
</div> </div>
<div class="red-input-group required w-300"> <div class="red-input-group required w-300">
@ -18,9 +23,17 @@
<button [disabled]="authForm.invalid" color="primary" mat-flat-button type="submit"> <button [disabled]="authForm.invalid" color="primary" mat-flat-button type="submit">
{{ 'smtp-auth-config.actions.save' | translate }} {{ 'smtp-auth-config.actions.save' | translate }}
</button> </button>
<div class="all-caps-label cancel" mat-dialog-close translate="smtp-auth-config.actions.cancel"></div> <div
class="all-caps-label cancel"
mat-dialog-close
translate="smtp-auth-config.actions.cancel"
></div>
</div> </div>
</form> </form>
<redaction-circle-button class="dialog-close" icon="red:close" mat-dialog-close></redaction-circle-button> <redaction-circle-button
class="dialog-close"
icon="red:close"
mat-dialog-close
></redaction-circle-button>
</section> </section>

View File

@ -22,7 +22,10 @@
<div class="content-container"> <div class="content-container">
<div class="header-item"> <div class="header-item">
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'audit-screen.table-header.title' | translate: { length: logs?.totalHits || 0 } }} {{
'audit-screen.table-header.title'
| translate: { length: logs?.totalHits || 0 }
}}
</span> </span>
<div class="actions-wrapper"> <div class="actions-wrapper">
<redaction-pagination <redaction-pagination
@ -34,7 +37,10 @@
<div class="red-input-group w-150 mr-20"> <div class="red-input-group w-150 mr-20">
<mat-form-field class="no-label"> <mat-form-field class="no-label">
<mat-select formControlName="category"> <mat-select formControlName="category">
<mat-option *ngFor="let category of categories" [value]="category"> <mat-option
*ngFor="let category of categories"
[value]="category"
>
{{ category | translate }} {{ category | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
@ -50,7 +56,10 @@
[withName]="true" [withName]="true"
size="small" size="small"
></redaction-initials-avatar> ></redaction-initials-avatar>
<div *ngIf="filterForm.get('userId').value === ALL_USERS" [translate]="ALL_USERS"></div> <div
*ngIf="filterForm.get('userId').value === ALL_USERS"
[translate]="ALL_USERS"
></div>
</mat-select-trigger> </mat-select-trigger>
<mat-option *ngFor="let userId of userIds" [value]="userId"> <mat-option *ngFor="let userId of userIds" [value]="userId">
<redaction-initials-avatar <redaction-initials-avatar
@ -59,16 +68,26 @@
[withName]="true" [withName]="true"
size="small" size="small"
></redaction-initials-avatar> ></redaction-initials-avatar>
<div *ngIf="userId === ALL_USERS" [translate]="ALL_USERS"></div> <div
*ngIf="userId === ALL_USERS"
[translate]="ALL_USERS"
></div>
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="separator">·</div> <div class="separator">·</div>
<div class="red-input-group datepicker-wrapper mr-20"> <div class="red-input-group datepicker-wrapper mr-20">
<input [matDatepicker]="fromPicker" formControlName="from" placeholder="dd/mm/yy" /> <input
[matDatepicker]="fromPicker"
formControlName="from"
placeholder="dd/mm/yy"
/>
<mat-datepicker-toggle [for]="fromPicker" matSuffix> <mat-datepicker-toggle [for]="fromPicker" matSuffix>
<mat-icon matDatepickerToggleIcon svgIcon="red:calendar"></mat-icon> <mat-icon
matDatepickerToggleIcon
svgIcon="red:calendar"
></mat-icon>
</mat-datepicker-toggle> </mat-datepicker-toggle>
<mat-datepicker #fromPicker></mat-datepicker> <mat-datepicker #fromPicker></mat-datepicker>
</div> </div>
@ -76,9 +95,16 @@
<div class="mr-20" translate="audit-screen.to"></div> <div class="mr-20" translate="audit-screen.to"></div>
<div class="red-input-group datepicker-wrapper"> <div class="red-input-group datepicker-wrapper">
<input [matDatepicker]="toPicker" formControlName="to" placeholder="dd/mm/yy" /> <input
[matDatepicker]="toPicker"
formControlName="to"
placeholder="dd/mm/yy"
/>
<mat-datepicker-toggle [for]="toPicker" matSuffix> <mat-datepicker-toggle [for]="toPicker" matSuffix>
<mat-icon matDatepickerToggleIcon svgIcon="red:calendar"></mat-icon> <mat-icon
matDatepickerToggleIcon
svgIcon="red:calendar"
></mat-icon>
</mat-datepicker-toggle> </mat-datepicker-toggle>
<mat-datepicker #toPicker></mat-datepicker> <mat-datepicker #toPicker></mat-datepicker>
</div> </div>
@ -87,14 +113,31 @@
</div> </div>
<div class="table-header" redactionSyncWidth="table-item"> <div class="table-header" redactionSyncWidth="table-item">
<redaction-table-col-name column="message" label="audit-screen.table-col-names.message"></redaction-table-col-name> <redaction-table-col-name
<redaction-table-col-name column="date" label="audit-screen.table-col-names.date"></redaction-table-col-name> column="message"
<redaction-table-col-name class="user-column" column="user" label="audit-screen.table-col-names.user"></redaction-table-col-name> label="audit-screen.table-col-names.message"
<redaction-table-col-name column="category" label="audit-screen.table-col-names.category"></redaction-table-col-name> ></redaction-table-col-name>
<redaction-table-col-name
column="date"
label="audit-screen.table-col-names.date"
></redaction-table-col-name>
<redaction-table-col-name
class="user-column"
column="user"
label="audit-screen.table-col-names.user"
></redaction-table-col-name>
<redaction-table-col-name
column="category"
label="audit-screen.table-col-names.category"
></redaction-table-col-name>
<div class="scrollbar-placeholder"></div> <div class="scrollbar-placeholder"></div>
</div> </div>
<redaction-empty-state *ngIf="!logs?.totalHits" icon="red:document" screen="audit-screen"></redaction-empty-state> <redaction-empty-state
*ngIf="!logs?.totalHits"
icon="red:document"
screen="audit-screen"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div *cdkVirtualFor="let log of logs?.data" class="table-item"> <div *cdkVirtualFor="let log of logs?.data" class="table-item">
@ -105,7 +148,11 @@
{{ log.recordDate | date: 'd MMM. yyyy, hh:mm a' }} {{ log.recordDate | date: 'd MMM. yyyy, hh:mm a' }}
</div> </div>
<div class="user-column"> <div class="user-column">
<redaction-initials-avatar [userId]="log.userId" [withName]="true" size="small"></redaction-initials-avatar> <redaction-initials-avatar
[userId]="log.userId"
[withName]="true"
size="small"
></redaction-initials-avatar>
</div> </div>
<div> <div>
{{ log.category }} {{ log.category }}
@ -118,4 +165,6 @@
</div> </div>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator
[displayed]="!viewReady"
></redaction-full-page-loading-indicator>

View File

@ -61,7 +61,16 @@ export class AuditScreenComponent {
} }
private _updateDateFilters(value): boolean { private _updateDateFilters(value): boolean {
if (applyIntervalConstraints(value, this._previousFrom, this._previousTo, this.filterForm, 'from', 'to')) { if (
applyIntervalConstraints(
value,
this._previousFrom,
this._previousTo,
this.filterForm,
'from',
'to'
)
) {
return true; return true;
} }

View File

@ -5,7 +5,12 @@
<div class="flex-1 actions"> <div class="flex-1 actions">
<redaction-rule-set-actions></redaction-rule-set-actions> <redaction-rule-set-actions></redaction-rule-set-actions>
<redaction-circle-button [routerLink]="['../..']" icon="red:close" tooltip="common.close" tooltipPosition="below"></redaction-circle-button> <redaction-circle-button
[routerLink]="['../..']"
icon="red:close"
tooltip="common.close"
tooltipPosition="below"
></redaction-circle-button>
</div> </div>
</div> </div>
@ -17,7 +22,10 @@
<div class="content-container"> <div class="content-container">
<div class="header-item"> <div class="header-item">
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'default-colors-screen.table-header.title' | translate: { length: allEntities.length } }} {{
'default-colors-screen.table-header.title'
| translate: { length: allEntities.length }
}}
</span> </span>
</div> </div>
@ -30,7 +38,10 @@
label="default-colors-screen.table-col-names.key" label="default-colors-screen.table-col-names.key"
></redaction-table-col-name> ></redaction-table-col-name>
<redaction-table-col-name class="flex-center" label="default-colors-screen.table-col-names.color"></redaction-table-col-name> <redaction-table-col-name
class="flex-center"
label="default-colors-screen.table-col-names.color"
></redaction-table-col-name>
<div></div> <div></div>
<div class="scrollbar-placeholder"></div> <div class="scrollbar-placeholder"></div>
@ -38,13 +49,24 @@
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines --> <!-- Table lines -->
<div *cdkVirtualFor="let color of allEntities | sortBy: sortingOption.order:sortingOption.column" class="table-item"> <div
*cdkVirtualFor="
let color of allEntities | sortBy: sortingOption.order:sortingOption.column
"
class="table-item"
>
<div> <div>
<div [translate]="'default-colors-screen.types.' + color.key" class="table-item-title heading"></div> <div
[translate]="'default-colors-screen.types.' + color.key"
class="table-item-title heading"
></div>
</div> </div>
<div class="color-wrapper"> <div class="color-wrapper">
<div [ngStyle]="{ 'background-color': color.value }" class="color-square"></div> <div
[ngStyle]="{ 'background-color': color.value }"
class="color-square"
></div>
</div> </div>
<div class="actions-container"> <div class="actions-container">
@ -66,4 +88,6 @@
</div> </div>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator
[displayed]="!viewReady"
></redaction-full-page-loading-indicator>

View File

@ -11,7 +11,10 @@ import { BaseListingComponent } from '@shared/base/base-listing.component';
templateUrl: './default-colors-screen.component.html', templateUrl: './default-colors-screen.component.html',
styleUrls: ['./default-colors-screen.component.scss'] styleUrls: ['./default-colors-screen.component.scss']
}) })
export class DefaultColorsScreenComponent extends BaseListingComponent<{ key: string; value: string }> { export class DefaultColorsScreenComponent extends BaseListingComponent<{
key: string;
value: string;
}> {
viewReady = false; viewReady = false;
protected readonly _sortKey = 'default-colors'; protected readonly _sortKey = 'default-colors';
private _colorsObj: Colors; private _colorsObj: Colors;
@ -35,9 +38,14 @@ export class DefaultColorsScreenComponent extends BaseListingComponent<{ key: st
openEditColorDialog($event: any, color: { key: string; value: string }) { openEditColorDialog($event: any, color: { key: string; value: string }) {
$event.stopPropagation(); $event.stopPropagation();
this._dialogService.openEditColorsDialog(this._colorsObj, color.key, this._appStateService.activeRuleSetId, async () => { this._dialogService.openEditColorsDialog(
this._loadColors(); this._colorsObj,
}); color.key,
this._appStateService.activeRuleSetId,
async () => {
this._loadColors();
}
);
} }
private _loadColors() { private _loadColors() {

View File

@ -5,7 +5,12 @@
<div class="flex-1 actions"> <div class="flex-1 actions">
<redaction-rule-set-actions></redaction-rule-set-actions> <redaction-rule-set-actions></redaction-rule-set-actions>
<redaction-circle-button [routerLink]="['../..']" icon="red:close" tooltip="common.close" tooltipPosition="below"></redaction-circle-button> <redaction-circle-button
[routerLink]="['../..']"
icon="red:close"
tooltip="common.close"
tooltipPosition="below"
></redaction-circle-button>
</div> </div>
</div> </div>
@ -25,11 +30,17 @@
</div> </div>
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'dictionary-listing.table-header.title' | translate: { length: displayedEntities.length } }} {{
'dictionary-listing.table-header.title'
| translate: { length: displayedEntities.length }
}}
</span> </span>
<div class="attributes-actions-container"> <div class="attributes-actions-container">
<redaction-search-input [form]="searchForm" [placeholder]="'dictionary-listing.search'"></redaction-search-input> <redaction-search-input
[form]="searchForm"
[placeholder]="'dictionary-listing.search'"
></redaction-search-input>
<div class="actions"> <div class="actions">
<redaction-icon-button <redaction-icon-button
(action)="openAddEditDictionaryDialog()" (action)="openAddEditDictionaryDialog()"
@ -42,7 +53,11 @@
</div> </div>
</div> </div>
<div [class.no-data]="!allEntities.length" class="table-header" redactionSyncWidth="table-item"> <div
[class.no-data]="!allEntities.length"
class="table-header"
redactionSyncWidth="table-item"
>
<div class="select-oval-placeholder"></div> <div class="select-oval-placeholder"></div>
<redaction-table-col-name <redaction-table-col-name
@ -62,7 +77,10 @@
label="dictionary-listing.table-col-names.order-of-importance" label="dictionary-listing.table-col-names.order-of-importance"
></redaction-table-col-name> ></redaction-table-col-name>
<redaction-table-col-name class="flex-center" label="dictionary-listing.table-col-names.hint-redaction"></redaction-table-col-name> <redaction-table-col-name
class="flex-center"
label="dictionary-listing.table-col-names.hint-redaction"
></redaction-table-col-name>
<div></div> <div></div>
<div class="scrollbar-placeholder"></div> <div class="scrollbar-placeholder"></div>
</div> </div>
@ -75,20 +93,32 @@
screen="dictionary-listing" screen="dictionary-listing"
></redaction-empty-state> ></redaction-empty-state>
<redaction-empty-state *ngIf="allEntities.length && !displayedEntities.length" screen="dictionary-listing" type="no-match"></redaction-empty-state> <redaction-empty-state
*ngIf="allEntities.length && !displayedEntities.length"
screen="dictionary-listing"
type="no-match"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div <div
*cdkVirtualFor="let dict of displayedEntities | sortBy: sortingOption.order:sortingOption.column" *cdkVirtualFor="
let dict of displayedEntities
| sortBy: sortingOption.order:sortingOption.column
"
[routerLink]="[dict.type]" [routerLink]="[dict.type]"
class="table-item pointer" class="table-item pointer"
> >
<div (click)="toggleEntitySelected($event, dict)" class="selection-column"> <div (click)="toggleEntitySelected($event, dict)" class="selection-column">
<redaction-round-checkbox [active]="isEntitySelected(dict)"></redaction-round-checkbox> <redaction-round-checkbox
[active]="isEntitySelected(dict)"
></redaction-round-checkbox>
</div> </div>
<div> <div>
<div [ngStyle]="{ 'background-color': dict.hexColor }" class="color-square"></div> <div
[ngStyle]="{ 'background-color': dict.hexColor }"
class="color-square"
></div>
<div class="dict-name"> <div class="dict-name">
<div class="table-item-title heading"> <div class="table-item-title heading">
{{ dict.label }} {{ dict.label }}
@ -111,7 +141,10 @@
</div> </div>
<div class="center"> <div class="center">
<redaction-annotation-icon [dictType]="dict" [type]="dict.hint ? 'circle' : 'square'"></redaction-annotation-icon> <redaction-annotation-icon
[dictType]="dict"
[type]="dict.hint ? 'circle' : 'square'"
></redaction-annotation-icon>
</div> </div>
<div class="actions-container"> <div class="actions-container">
@ -154,4 +187,6 @@
</div> </div>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator
[displayed]="!viewReady"
></redaction-full-page-loading-indicator>

View File

@ -14,7 +14,10 @@ import { BaseListingComponent } from '@shared/base/base-listing.component';
templateUrl: './dictionary-listing-screen.component.html', templateUrl: './dictionary-listing-screen.component.html',
styleUrls: ['./dictionary-listing-screen.component.scss'] styleUrls: ['./dictionary-listing-screen.component.scss']
}) })
export class DictionaryListingScreenComponent extends BaseListingComponent<TypeValue> implements OnInit { export class DictionaryListingScreenComponent
extends BaseListingComponent<TypeValue>
implements OnInit
{
viewReady = false; viewReady = false;
chartData: DoughnutChartConfig[] = []; chartData: DoughnutChartConfig[] = [];
protected readonly _searchKey = 'label'; protected readonly _searchKey = 'label';
@ -39,38 +42,50 @@ export class DictionaryListingScreenComponent extends BaseListingComponent<TypeV
openAddEditDictionaryDialog($event?: MouseEvent, dict?: TypeValue) { openAddEditDictionaryDialog($event?: MouseEvent, dict?: TypeValue) {
$event?.stopPropagation(); $event?.stopPropagation();
this._dialogService.openAddEditDictionaryDialog(dict, this._appStateService.activeRuleSetId, async (newDictionary) => { this._dialogService.openAddEditDictionaryDialog(
if (newDictionary) { dict,
await this._appStateService.loadDictionaryData(); this._appStateService.activeRuleSetId,
this._loadDictionaryData(); async (newDictionary) => {
if (newDictionary) {
await this._appStateService.loadDictionaryData();
this._loadDictionaryData();
}
} }
}); );
} }
openDeleteDictionaryDialog($event: any, dict: TypeValue) { openDeleteDictionaryDialog($event: any, dict: TypeValue) {
this._dialogService.openDeleteDictionaryDialog($event, dict, this._appStateService.activeRuleSetId, async () => { this._dialogService.openDeleteDictionaryDialog(
await this._appStateService.loadDictionaryData(); $event,
this._loadDictionaryData(); dict,
}); this._appStateService.activeRuleSetId,
async () => {
await this._appStateService.loadDictionaryData();
this._loadDictionaryData();
}
);
} }
private _loadDictionaryData() { private _loadDictionaryData() {
const appStateDictionaryData = this._appStateService.dictionaryData[this._appStateService.activeRuleSetId]; const appStateDictionaryData =
this._appStateService.dictionaryData[this._appStateService.activeRuleSetId];
this.allEntities = Object.keys(appStateDictionaryData) this.allEntities = Object.keys(appStateDictionaryData)
.map((key) => appStateDictionaryData[key]) .map((key) => appStateDictionaryData[key])
.filter((d) => !d.virtual); .filter((d) => !d.virtual);
this.displayedEntities = [...this.allEntities]; this.displayedEntities = [...this.allEntities];
const dataObs = this.allEntities.map((dict) => const dataObs = this.allEntities.map((dict) =>
this._dictionaryControllerService.getDictionaryForType(this._appStateService.activeRuleSetId, dict.type).pipe( this._dictionaryControllerService
tap((values) => { .getDictionaryForType(this._appStateService.activeRuleSetId, dict.type)
dict.entries = values.entries ? values.entries : []; .pipe(
}), tap((values) => {
catchError(() => { dict.entries = values.entries ? values.entries : [];
console.log('error'); }),
dict.entries = []; catchError(() => {
return of({}); console.log('error');
}) dict.entries = [];
) return of({});
})
)
); );
forkJoin(dataObs) forkJoin(dataObs)
.pipe(defaultIfEmpty(null)) .pipe(defaultIfEmpty(null))

View File

@ -38,7 +38,14 @@
tooltipPosition="below" tooltipPosition="below"
></redaction-circle-button> ></redaction-circle-button>
<input #fileInput (change)="upload($event)" accept="text/plain" class="file-upload-input" hidden type="file" /> <input
#fileInput
(change)="upload($event)"
accept="text/plain"
class="file-upload-input"
hidden
type="file"
/>
<redaction-circle-button <redaction-circle-button
[routerLink]="['..']" [routerLink]="['..']"
@ -93,11 +100,16 @@
</div> </div>
<div *ngIf="!!dictionary.description" class="pb-32 mt-20"> <div *ngIf="!!dictionary.description" class="pb-32 mt-20">
<div class="heading" translate="dictionary-overview.dictionary-details.description"></div> <div
class="heading"
translate="dictionary-overview.dictionary-details.description"
></div>
<div class="mt-8">{{ dictionary.description }}</div> <div class="mt-8">{{ dictionary.description }}</div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="processing"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator
[displayed]="processing"
></redaction-full-page-loading-indicator>

View File

@ -21,7 +21,8 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
processing = false; processing = false;
entries: string[] = []; entries: string[] = [];
@ViewChild('dictionaryManager', { static: false }) private _dictionaryManager: DictionaryManagerComponent; @ViewChild('dictionaryManager', { static: false })
private _dictionaryManager: DictionaryManagerComponent;
@ViewChild('fileInput') private _fileInput: ElementRef; @ViewChild('fileInput') private _fileInput: ElementRef;
constructor( constructor(
@ -37,7 +38,10 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
private readonly _formBuilder: FormBuilder private readonly _formBuilder: FormBuilder
) { ) {
super(_translateService); super(_translateService);
this._appStateService.activateDictionary(this._activatedRoute.snapshot.params.type, this._activatedRoute.snapshot.params.ruleSetId); this._appStateService.activateDictionary(
this._activatedRoute.snapshot.params.type,
this._activatedRoute.snapshot.params.ruleSetId
);
} }
ngOnInit(): void { ngOnInit(): void {
@ -54,16 +58,25 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
openEditDictionaryDialog($event: any) { openEditDictionaryDialog($event: any) {
$event.stopPropagation(); $event.stopPropagation();
this._dialogService.openAddEditDictionaryDialog(this.dictionary, this.dictionary.ruleSetId, async () => { this._dialogService.openAddEditDictionaryDialog(
await this._appStateService.loadDictionaryData(); this.dictionary,
}); this.dictionary.ruleSetId,
async () => {
await this._appStateService.loadDictionaryData();
}
);
} }
openDeleteDictionaryDialog($event: any) { openDeleteDictionaryDialog($event: any) {
this._dialogService.openDeleteDictionaryDialog($event, this.dictionary, this.dictionary.ruleSetId, async () => { this._dialogService.openDeleteDictionaryDialog(
await this._appStateService.loadDictionaryData(); $event,
this._router.navigate(['..']); this.dictionary,
}); this.dictionary.ruleSetId,
async () => {
await this._appStateService.loadDictionaryData();
this._router.navigate(['..']);
}
);
} }
download(): void { download(): void {
@ -89,28 +102,40 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
saveEntries(entries: string[]) { saveEntries(entries: string[]) {
this.processing = true; this.processing = true;
this._dictionarySaveService.saveEntries(entries, this.entries, this.dictionary.ruleSetId, this.dictionary.type, null).subscribe( this._dictionarySaveService
() => { .saveEntries(
this.processing = false; entries,
this._loadEntries(); this.entries,
}, this.dictionary.ruleSetId,
() => { this.dictionary.type,
this.processing = false; null
} )
); .subscribe(
() => {
this.processing = false;
this._loadEntries();
},
() => {
this.processing = false;
}
);
} }
private _loadEntries() { private _loadEntries() {
this.processing = true; this.processing = true;
this._dictionaryControllerService.getDictionaryForType(this.dictionary.ruleSetId, this.dictionary.type).subscribe( this._dictionaryControllerService
(data) => { .getDictionaryForType(this.dictionary.ruleSetId, this.dictionary.type)
this.processing = false; .subscribe(
this.entries = data.entries.sort((str1, str2) => str1.localeCompare(str2, undefined, { sensitivity: 'accent' })); (data) => {
}, this.processing = false;
() => { this.entries = data.entries.sort((str1, str2) =>
this.processing = false; str1.localeCompare(str2, undefined, { sensitivity: 'accent' })
this.entries = []; );
} },
); () => {
this.processing = false;
this.entries = [];
}
);
} }
} }

View File

@ -22,8 +22,19 @@
<div class="red-content-inner"> <div class="red-content-inner">
<div class="content-container"> <div class="content-container">
<div class="content-container-content"> <div class="content-container-content">
<form (keyup)="formChanged()" *ngIf="digitalSignatureForm" [formGroup]="digitalSignatureForm" autocomplete="off"> <form
<input #fileInput (change)="fileChanged($event, fileInput)" class="file-upload-input" hidden type="file" /> (keyup)="formChanged()"
*ngIf="digitalSignatureForm"
[formGroup]="digitalSignatureForm"
autocomplete="off"
>
<input
#fileInput
(change)="fileChanged($event, fileInput)"
class="file-upload-input"
hidden
type="file"
/>
<redaction-empty-state <redaction-empty-state
(action)="fileInput.click()" (action)="fileInput.click()"
@ -32,34 +43,66 @@
screen="digital-signature-screen" screen="digital-signature-screen"
></redaction-empty-state> ></redaction-empty-state>
<div [class.hidden]="!hasDigitalSignatureSet" class="red-input-group required w-300"> <div
<label translate="digital-signature-screen.certificate-name.label"></label> [class.hidden]="!hasDigitalSignatureSet"
class="red-input-group required w-300"
>
<label
translate="digital-signature-screen.certificate-name.label"
></label>
<input <input
[placeholder]="'digital-signature-screen.certificate-name.placeholder' | translate" [placeholder]="
'digital-signature-screen.certificate-name.placeholder'
| translate
"
formControlName="certificateName" formControlName="certificateName"
name="certificateName" name="certificateName"
/> />
</div> </div>
<div *ngIf="!digitalSignatureExists" [class.hidden]="!hasDigitalSignatureSet" class="red-input-group required w-300"> <div
*ngIf="!digitalSignatureExists"
[class.hidden]="!hasDigitalSignatureSet"
class="red-input-group required w-300"
>
<label translate="digital-signature-screen.password.label"></label> <label translate="digital-signature-screen.password.label"></label>
<input [placeholder]="'digital-signature-screen.password.placeholder' | translate" formControlName="keySecret" name="keySecret" /> <input
[placeholder]="
'digital-signature-screen.password.placeholder' | translate
"
formControlName="keySecret"
name="keySecret"
/>
</div> </div>
<div [class.hidden]="!hasDigitalSignatureSet" class="red-input-group w-300"> <div [class.hidden]="!hasDigitalSignatureSet" class="red-input-group w-300">
<label translate="digital-signature-screen.reason.label"></label> <label translate="digital-signature-screen.reason.label"></label>
<input [placeholder]="'digital-signature-screen.reason.placeholder' | translate" formControlName="reason" name="reason" /> <input
[placeholder]="
'digital-signature-screen.reason.placeholder' | translate
"
formControlName="reason"
name="reason"
/>
</div> </div>
<div [class.hidden]="!hasDigitalSignatureSet" class="red-input-group w-300"> <div [class.hidden]="!hasDigitalSignatureSet" class="red-input-group w-300">
<label translate="digital-signature-screen.location.label"></label> <label translate="digital-signature-screen.location.label"></label>
<input [placeholder]="'digital-signature-screen.location.placeholder' | translate" formControlName="location" name="location" /> <input
[placeholder]="
'digital-signature-screen.location.placeholder' | translate
"
formControlName="location"
name="location"
/>
</div> </div>
<div [class.hidden]="!hasDigitalSignatureSet" class="red-input-group w-300"> <div [class.hidden]="!hasDigitalSignatureSet" class="red-input-group w-300">
<label translate="digital-signature-screen.contact-info.label"></label> <label translate="digital-signature-screen.contact-info.label"></label>
<input <input
[placeholder]="'digital-signature-screen.contact-info.placeholder' | translate" [placeholder]="
'digital-signature-screen.contact-info.placeholder' | translate
"
formControlName="contactInfo" formControlName="contactInfo"
name="contactInfo" name="contactInfo"
/> />
@ -96,4 +139,6 @@
</div> </div>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator
[displayed]="!viewReady"
></redaction-full-page-loading-indicator>

View File

@ -29,7 +29,10 @@ export class DigitalSignatureScreenComponent {
} }
get hasDigitalSignatureSet() { get hasDigitalSignatureSet() {
return this.digitalSignatureExists || !!this.digitalSignatureForm.get('base64EncodedPrivateKey').value; return (
this.digitalSignatureExists ||
!!this.digitalSignatureForm.get('base64EncodedPrivateKey').value
);
} }
saveDigitalSignature() { saveDigitalSignature() {
@ -55,13 +58,17 @@ export class DigitalSignatureScreenComponent {
(error) => { (error) => {
if (error.status === 400) { if (error.status === 400) {
this._notificationService.showToastNotification( this._notificationService.showToastNotification(
this._translateService.instant('digital-signature-screen.action.certificate-not-valid-error'), this._translateService.instant(
'digital-signature-screen.action.certificate-not-valid-error'
),
null, null,
NotificationType.ERROR NotificationType.ERROR
); );
} else { } else {
this._notificationService.showToastNotification( this._notificationService.showToastNotification(
this._translateService.instant('digital-signature-screen.action.save-error'), this._translateService.instant(
'digital-signature-screen.action.save-error'
),
null, null,
NotificationType.ERROR NotificationType.ERROR
); );
@ -75,7 +82,9 @@ export class DigitalSignatureScreenComponent {
() => { () => {
this.loadDigitalSignatureAndInitializeForm(); this.loadDigitalSignatureAndInitializeForm();
this._notificationService.showToastNotification( this._notificationService.showToastNotification(
this._translateService.instant('digital-signature-screen.action.delete-success'), this._translateService.instant(
'digital-signature-screen.action.delete-success'
),
null, null,
NotificationType.SUCCESS NotificationType.SUCCESS
); );
@ -130,9 +139,13 @@ export class DigitalSignatureScreenComponent {
certificateName: [this.digitalSignature.certificateName, Validators.required], certificateName: [this.digitalSignature.certificateName, Validators.required],
contactInfo: this.digitalSignature.contactInfo, contactInfo: this.digitalSignature.contactInfo,
location: this.digitalSignature.location, location: this.digitalSignature.location,
keySecret: this.digitalSignatureExists ? null : [this.digitalSignature.password, Validators.required], keySecret: this.digitalSignatureExists
? null
: [this.digitalSignature.password, Validators.required],
reason: this.digitalSignature.reason, reason: this.digitalSignature.reason,
base64EncodedPrivateKey: this.digitalSignatureExists ? null : [this.digitalSignature.base64EncodedPrivateKey, Validators.required] base64EncodedPrivateKey: this.digitalSignatureExists
? null
: [this.digitalSignature.base64EncodedPrivateKey, Validators.required]
}); });
} }
} }

View File

@ -3,7 +3,12 @@
<redaction-admin-breadcrumbs class="flex-1"></redaction-admin-breadcrumbs> <redaction-admin-breadcrumbs class="flex-1"></redaction-admin-breadcrumbs>
<div class="actions flex-1"> <div class="actions flex-1">
<redaction-circle-button [routerLink]="['../..']" icon="red:close" tooltip="common.close" tooltipPosition="below"></redaction-circle-button> <redaction-circle-button
[routerLink]="['../..']"
icon="red:close"
tooltip="common.close"
tooltipPosition="below"
></redaction-circle-button>
</div> </div>
</div> </div>
@ -23,7 +28,10 @@
</div> </div>
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'file-attributes-listing.table-header.title' | translate: { length: displayedEntities.length } }} {{
'file-attributes-listing.table-header.title'
| translate: { length: displayedEntities.length }
}}
</span> </span>
<redaction-circle-button <redaction-circle-button
@ -38,8 +46,17 @@
<mat-spinner *ngIf="loading" diameter="15"></mat-spinner> <mat-spinner *ngIf="loading" diameter="15"></mat-spinner>
<div class="attributes-actions-container"> <div class="attributes-actions-container">
<redaction-search-input [form]="searchForm" [placeholder]="'file-attributes-listing.search'"></redaction-search-input> <redaction-search-input
<input #fileInput (change)="importCSV($event.target['files'])" accept=".csv" class="csv-input" type="file" /> [form]="searchForm"
[placeholder]="'file-attributes-listing.search'"
></redaction-search-input>
<input
#fileInput
(change)="importCSV($event.target['files'])"
accept=".csv"
class="csv-input"
type="file"
/>
<redaction-circle-button <redaction-circle-button
(action)="fileInput.click()" (action)="fileInput.click()"
@ -58,7 +75,11 @@
</div> </div>
</div> </div>
<div [class.no-data]="!allEntities.length" class="table-header" redactionSyncWidth="table-item"> <div
[class.no-data]="!allEntities.length"
class="table-header"
redactionSyncWidth="table-item"
>
<div class="select-oval-placeholder"></div> <div class="select-oval-placeholder"></div>
<redaction-table-col-name <redaction-table-col-name
@ -86,7 +107,9 @@
label="file-attributes-listing.table-col-names.read-only" label="file-attributes-listing.table-col-names.read-only"
></redaction-table-col-name> ></redaction-table-col-name>
<redaction-table-col-name label="file-attributes-listing.table-col-names.csv-column"></redaction-table-col-name> <redaction-table-col-name
label="file-attributes-listing.table-col-names.csv-column"
></redaction-table-col-name>
<redaction-table-col-name <redaction-table-col-name
class="flex-center" class="flex-center"
@ -100,7 +123,11 @@
<div class="scrollbar-placeholder"></div> <div class="scrollbar-placeholder"></div>
</div> </div>
<redaction-empty-state *ngIf="!allEntities.length" icon="red:attribute" screen="file-attributes-listing"></redaction-empty-state> <redaction-empty-state
*ngIf="!allEntities.length"
icon="red:attribute"
screen="file-attributes-listing"
></redaction-empty-state>
<redaction-empty-state <redaction-empty-state
*ngIf="allEntities.length && !displayedEntities.length" *ngIf="allEntities.length && !displayedEntities.length"
@ -110,16 +137,27 @@
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines --> <!-- Table lines -->
<div *cdkVirtualFor="let attribute of displayedEntities | sortBy: sortingOption.order:sortingOption.column" class="table-item"> <div
*cdkVirtualFor="
let attribute of displayedEntities
| sortBy: sortingOption.order:sortingOption.column
"
class="table-item"
>
<div (click)="toggleEntitySelected($event, attribute)" class="selection-column"> <div (click)="toggleEntitySelected($event, attribute)" class="selection-column">
<redaction-round-checkbox [active]="isEntitySelected(attribute)"></redaction-round-checkbox> <redaction-round-checkbox
[active]="isEntitySelected(attribute)"
></redaction-round-checkbox>
</div> </div>
<div class="label"> <div class="label">
<span>{{ attribute.label }}</span> <span>{{ attribute.label }}</span>
</div> </div>
<div [translate]="'file-attribute-types.' + attribute.type" class="small-label"></div> <div
[translate]="'file-attribute-types.' + attribute.type"
class="small-label"
></div>
<div class="center read-only"> <div class="center read-only">
<mat-icon <mat-icon
@ -133,7 +171,11 @@
{{ attribute.csvColumnHeader }} {{ attribute.csvColumnHeader }}
</div> </div>
<div class="center"> <div class="center">
<redaction-round-checkbox *ngIf="attribute.primaryAttribute" [active]="true" [size]="18"></redaction-round-checkbox> <redaction-round-checkbox
*ngIf="attribute.primaryAttribute"
[active]="true"
[size]="18"
></redaction-round-checkbox>
</div> </div>
<div class="actions-container"> <div class="actions-container">
<div class="action-buttons"> <div class="action-buttons">
@ -162,4 +204,6 @@
</div> </div>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator
[displayed]="!viewReady"
></redaction-full-page-loading-indicator>

View File

@ -1,6 +1,10 @@
import { Component, ElementRef, Injector, OnInit, ViewChild } from '@angular/core'; import { Component, ElementRef, Injector, OnInit, ViewChild } from '@angular/core';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { FileAttributeConfig, FileAttributesConfig, FileAttributesControllerService } from '@redaction/red-ui-http'; import {
FileAttributeConfig,
FileAttributesConfig,
FileAttributesControllerService
} from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service'; import { AppStateService } from '@state/app-state.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { AdminDialogService } from '../../services/admin-dialog.service'; import { AdminDialogService } from '../../services/admin-dialog.service';
@ -11,7 +15,10 @@ import { BaseListingComponent } from '@shared/base/base-listing.component';
templateUrl: './file-attributes-listing-screen.component.html', templateUrl: './file-attributes-listing-screen.component.html',
styleUrls: ['./file-attributes-listing-screen.component.scss'] styleUrls: ['./file-attributes-listing-screen.component.scss']
}) })
export class FileAttributesListingScreenComponent extends BaseListingComponent<FileAttributeConfig> implements OnInit { export class FileAttributesListingScreenComponent
extends BaseListingComponent<FileAttributeConfig>
implements OnInit
{
viewReady = false; viewReady = false;
loading = false; loading = false;
protected readonly _searchKey = 'label'; protected readonly _searchKey = 'label';
@ -39,38 +46,65 @@ export class FileAttributesListingScreenComponent extends BaseListingComponent<F
openAddEditAttributeDialog($event: MouseEvent, fileAttribute?: FileAttributeConfig) { openAddEditAttributeDialog($event: MouseEvent, fileAttribute?: FileAttributeConfig) {
$event.stopPropagation(); $event.stopPropagation();
this._dialogService.openAddEditFileAttributeDialog(fileAttribute, this._appStateService.activeRuleSetId, async (newValue: FileAttributeConfig) => { this._dialogService.openAddEditFileAttributeDialog(
this.loading = true; fileAttribute,
await this._fileAttributesService.setFileAttributesConfiguration(newValue, this._appStateService.activeRuleSetId).toPromise(); this._appStateService.activeRuleSetId,
await this._loadData(); async (newValue: FileAttributeConfig) => {
}); this.loading = true;
await this._fileAttributesService
.setFileAttributesConfiguration(newValue, this._appStateService.activeRuleSetId)
.toPromise();
await this._loadData();
}
);
} }
openConfirmDeleteAttributeDialog($event: MouseEvent, fileAttribute?: FileAttributeConfig) { openConfirmDeleteAttributeDialog($event: MouseEvent, fileAttribute?: FileAttributeConfig) {
$event.stopPropagation(); $event.stopPropagation();
this._dialogService.openConfirmDeleteFileAttributeDialog(fileAttribute, this._appStateService.activeRuleSetId, async () => { this._dialogService.openConfirmDeleteFileAttributeDialog(
this.loading = true; fileAttribute,
if (fileAttribute) { this._appStateService.activeRuleSetId,
await this._fileAttributesService.deleteFileAttribute(this._appStateService.activeRuleSetId, fileAttribute.id).toPromise(); async () => {
} else { this.loading = true;
await this._fileAttributesService.deleteFileAttributes(this.selectedEntitiesIds, this._appStateService.activeRuleSetId).toPromise(); if (fileAttribute) {
await this._fileAttributesService
.deleteFileAttribute(
this._appStateService.activeRuleSetId,
fileAttribute.id
)
.toPromise();
} else {
await this._fileAttributesService
.deleteFileAttributes(
this.selectedEntitiesIds,
this._appStateService.activeRuleSetId
)
.toPromise();
}
await this._loadData();
} }
await this._loadData(); );
});
} }
importCSV(files: FileList | File[]) { importCSV(files: FileList | File[]) {
const csvFile = files[0]; const csvFile = files[0];
this._fileInput.nativeElement.value = null; this._fileInput.nativeElement.value = null;
this._dialogService.openImportFileAttributeCSVDialog(csvFile, this._appStateService.activeRuleSetId, this._existingConfiguration, async () => { this._dialogService.openImportFileAttributeCSVDialog(
await this._loadData(); csvFile,
}); this._appStateService.activeRuleSetId,
this._existingConfiguration,
async () => {
await this._loadData();
}
);
} }
private async _loadData() { private async _loadData() {
try { try {
const response = await this._fileAttributesService.getFileAttributesConfiguration(this._appStateService.activeRuleSetId).toPromise(); const response = await this._fileAttributesService
.getFileAttributesConfiguration(this._appStateService.activeRuleSetId)
.toPromise();
this._existingConfiguration = response; this._existingConfiguration = response;
this.allEntities = response?.fileAttributeConfigs || []; this.allEntities = response?.fileAttributeConfigs || [];
} catch (e) { } catch (e) {

View File

@ -8,7 +8,12 @@
<div class="breadcrumb" translate="license-information"></div> <div class="breadcrumb" translate="license-information"></div>
<div class="actions"> <div class="actions">
<button (click)="sendMail()" color="primary" mat-flat-button translate="license-info-screen.email-report"></button> <button
(click)="sendMail()"
color="primary"
mat-flat-button
translate="license-info-screen.email-report"
></button>
<redaction-circle-button <redaction-circle-button
*ngIf="permissionsService.isUser()" *ngIf="permissionsService.isUser()"
class="ml-6" class="ml-6"
@ -40,7 +45,12 @@
<div class="row"> <div class="row">
<div translate="license-info-screen.copyright-claim-title"></div> <div translate="license-info-screen.copyright-claim-title"></div>
<div>{{ 'license-info-screen.copyright-claim-text' | translate: { currentYear: currentYear } }}</div> <div>
{{
'license-info-screen.copyright-claim-text'
| translate: { currentYear: currentYear }
}}
</div>
</div> </div>
<div class="row"> <div class="row">
@ -54,7 +64,10 @@
<!-- <div>Future feature: we will provide that with the /info endpoint</div>--> <!-- <div>Future feature: we will provide that with the /info endpoint</div>-->
<!-- </div>--> <!-- </div>-->
<div class="section-title all-caps-label" translate="license-info-screen.licensing-details"></div> <div
class="section-title all-caps-label"
translate="license-info-screen.licensing-details"
></div>
<div class="row"> <div class="row">
<div translate="license-info-screen.licensed-to"></div> <div translate="license-info-screen.licensed-to"></div>
@ -63,7 +76,10 @@
<div class="row"> <div class="row">
<div translate="license-info-screen.licensing-period"></div> <div translate="license-info-screen.licensing-period"></div>
<div>{{ appConfigService.getConfig('LICENSE_START', '-') }} / {{ appConfigService.getConfig('LICENSE_END', '-') }}</div> <div>
{{ appConfigService.getConfig('LICENSE_START', '-') }} /
{{ appConfigService.getConfig('LICENSE_END', '-') }}
</div>
</div> </div>
<div class="row"> <div class="row">
@ -76,16 +92,28 @@
<div>{{ currentInfo.numberOfAnalyzedPages }}</div> <div>{{ currentInfo.numberOfAnalyzedPages }}</div>
</div> </div>
<div class="section-title all-caps-label" translate="license-info-screen.usage-details"></div> <div
class="section-title all-caps-label"
translate="license-info-screen.usage-details"
></div>
<div class="row"> <div class="row">
<div>{{ 'license-info-screen.total-analyzed' | translate: { date: totalInfo.startDate | date: 'longDate' } }}</div> <div>
{{
'license-info-screen.total-analyzed'
| translate: { date: totalInfo.startDate | date: 'longDate' }
}}
</div>
<div>{{ totalInfo.numberOfAnalyzedPages }}</div> <div>{{ totalInfo.numberOfAnalyzedPages }}</div>
</div> </div>
<div class="row"> <div class="row">
<div translate="license-info-screen.current-analyzed"></div> <div translate="license-info-screen.current-analyzed"></div>
<div>{{ currentInfo.numberOfAnalyzedPages }} ({{ analysisPercentageOfLicense | number: '1.0-2' }}%)</div> <div>
{{ currentInfo.numberOfAnalyzedPages }} ({{
analysisPercentageOfLicense | number: '1.0-2'
}}%)
</div>
</div> </div>
<div *ngIf="!!unlicensedInfo" class="row"> <div *ngIf="!!unlicensedInfo" class="row">
@ -123,4 +151,6 @@
</div> </div>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator
[displayed]="!viewReady"
></redaction-full-page-loading-indicator>

View File

@ -54,31 +54,47 @@ export class LicenseInformationScreenComponent implements OnInit {
startDate: startDate.toDate(), startDate: startDate.toDate(),
endDate: endDate.toDate() endDate: endDate.toDate()
}; };
const promises = [this._licenseReportController.licenseReport(currentConfig).toPromise(), this._licenseReportController.licenseReport({}).toPromise()]; const promises = [
this._licenseReportController.licenseReport(currentConfig).toPromise(),
this._licenseReportController.licenseReport({}).toPromise()
];
if (endDate.isBefore(moment())) { if (endDate.isBefore(moment())) {
const unlicensedConfig = { const unlicensedConfig = {
startDate: endDate.toDate() startDate: endDate.toDate()
}; };
promises.push(this._licenseReportController.licenseReport(unlicensedConfig).toPromise()); promises.push(
this._licenseReportController.licenseReport(unlicensedConfig).toPromise()
);
} }
Promise.all(promises).then((reports) => { Promise.all(promises).then((reports) => {
[this.currentInfo, this.totalInfo, this.unlicensedInfo] = reports; [this.currentInfo, this.totalInfo, this.unlicensedInfo] = reports;
this.viewReady = true; this.viewReady = true;
this.analysisPercentageOfLicense = this.analysisPercentageOfLicense =
this.totalLicensedNumberOfPages > 0 ? (this.currentInfo.numberOfAnalyzedPages / this.totalLicensedNumberOfPages) * 100 : 100; this.totalLicensedNumberOfPages > 0
? (this.currentInfo.numberOfAnalyzedPages / this.totalLicensedNumberOfPages) *
100
: 100;
}); });
} }
sendMail(): void { sendMail(): void {
const licenseCustomer = this.appConfigService.getConfig('LICENSE_CUSTOMER'); const licenseCustomer = this.appConfigService.getConfig('LICENSE_CUSTOMER');
const subject = this._translateService.instant('license-info-screen.email.title', { licenseCustomer }); const subject = this._translateService.instant('license-info-screen.email.title', {
licenseCustomer
});
const body = [ const body = [
this._translateService.instant('license-info-screen.email.body.analyzed', { pages: this.currentInfo.numberOfAnalyzedPages }), this._translateService.instant('license-info-screen.email.body.analyzed', {
this._translateService.instant('license-info-screen.email.body.licensed', { pages: this.totalLicensedNumberOfPages }) pages: this.currentInfo.numberOfAnalyzedPages
}),
this._translateService.instant('license-info-screen.email.body.licensed', {
pages: this.totalLicensedNumberOfPages
})
].join('%0D%0A'); ].join('%0D%0A');
window.location.href = `mailto:${this.appConfigService.getConfig('LICENSE_EMAIL')}?subject=${subject}&body=${body}`; window.location.href = `mailto:${this.appConfigService.getConfig(
'LICENSE_EMAIL'
)}?subject=${subject}&body=${body}`;
} }
private async _setMonthlyStats(startDate: moment.Moment, endDate: moment.Moment) { private async _setMonthlyStats(startDate: moment.Moment, endDate: moment.Moment) {

View File

@ -28,15 +28,24 @@
</div> </div>
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'project-templates-listing.table-header.title' | translate: { length: displayedEntities.length } }} {{
'project-templates-listing.table-header.title'
| translate: { length: displayedEntities.length }
}}
</span> </span>
<div class="actions flex-1"> <div class="actions flex-1">
<redaction-search-input [form]="searchForm" [placeholder]="'project-templates-listing.search'"></redaction-search-input> <redaction-search-input
[form]="searchForm"
[placeholder]="'project-templates-listing.search'"
></redaction-search-input>
<redaction-icon-button <redaction-icon-button
(action)="openAddRuleSetDialog()" (action)="openAddRuleSetDialog()"
*ngIf="permissionsService.isAdmin() && userPreferenceService.areDevFeaturesEnabled" *ngIf="
permissionsService.isAdmin() &&
userPreferenceService.areDevFeaturesEnabled
"
icon="red:plus" icon="red:plus"
text="project-templates-listing.add-new" text="project-templates-listing.add-new"
type="primary" type="primary"
@ -44,7 +53,11 @@
</div> </div>
</div> </div>
<div [class.no-data]="!allEntities.length" class="table-header" redactionSyncWidth="table-item"> <div
[class.no-data]="!allEntities.length"
class="table-header"
redactionSyncWidth="table-item"
>
<div class="select-oval-placeholder"></div> <div class="select-oval-placeholder"></div>
<redaction-table-col-name <redaction-table-col-name
@ -54,7 +67,10 @@
column="name" column="name"
label="project-templates-listing.table-col-names.name" label="project-templates-listing.table-col-names.name"
></redaction-table-col-name> ></redaction-table-col-name>
<redaction-table-col-name class="user-column" label="project-templates-listing.table-col-names.created-by"></redaction-table-col-name> <redaction-table-col-name
class="user-column"
label="project-templates-listing.table-col-names.created-by"
></redaction-table-col-name>
<redaction-table-col-name <redaction-table-col-name
(toggleSort)="toggleSort($event)" (toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption" [activeSortingOption]="sortingOption"
@ -72,7 +88,11 @@
<div class="scrollbar-placeholder"></div> <div class="scrollbar-placeholder"></div>
</div> </div>
<redaction-empty-state *ngIf="!allEntities.length" icon="red:template" screen="project-templates-listing"></redaction-empty-state> <redaction-empty-state
*ngIf="!allEntities.length"
icon="red:template"
screen="project-templates-listing"
></redaction-empty-state>
<redaction-empty-state <redaction-empty-state
*ngIf="allEntities.length && !displayedEntities.length" *ngIf="allEntities.length && !displayedEntities.length"
@ -82,12 +102,20 @@
<cdk-virtual-scroll-viewport [itemSize]="100" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="100" redactionHasScrollbar>
<div <div
*cdkVirtualFor="let ruleSet of displayedEntities | sortBy: sortingOption.order:sortingOption.column" *cdkVirtualFor="
let ruleSet of displayedEntities
| sortBy: sortingOption.order:sortingOption.column
"
[routerLink]="[ruleSet.ruleSetId, 'dictionaries']" [routerLink]="[ruleSet.ruleSetId, 'dictionaries']"
class="table-item pointer" class="table-item pointer"
> >
<div (click)="toggleEntitySelected($event, ruleSet)" class="selection-column"> <div
<redaction-round-checkbox [active]="isEntitySelected(ruleSet)"></redaction-round-checkbox> (click)="toggleEntitySelected($event, ruleSet)"
class="selection-column"
>
<redaction-round-checkbox
[active]="isEntitySelected(ruleSet)"
></redaction-round-checkbox>
</div> </div>
<div> <div>
@ -97,13 +125,19 @@
<div class="small-label stats-subtitle"> <div class="small-label stats-subtitle">
<div> <div>
<mat-icon svgIcon="red:dictionary"></mat-icon> <mat-icon svgIcon="red:dictionary"></mat-icon>
{{ 'project-templates-listing.dictionaries' | translate: { length: ruleSet.dictionariesCount } }} {{
'project-templates-listing.dictionaries'
| translate: { length: ruleSet.dictionariesCount }
}}
</div> </div>
</div> </div>
</div> </div>
<div class="user-column"> <div class="user-column">
<redaction-initials-avatar [userId]="ruleSet.createdBy" [withName]="true"></redaction-initials-avatar> <redaction-initials-avatar
[userId]="ruleSet.createdBy"
[withName]="true"
></redaction-initials-avatar>
</div> </div>
<div class="small-label"> <div class="small-label">
{{ ruleSet.dateAdded | date: 'd MMM. yyyy' }} {{ ruleSet.dateAdded | date: 'd MMM. yyyy' }}

View File

@ -11,7 +11,10 @@ import { RuleSetModel } from '@redaction/red-ui-http';
templateUrl: './rule-sets-listing-screen.component.html', templateUrl: './rule-sets-listing-screen.component.html',
styleUrls: ['./rule-sets-listing-screen.component.scss'] styleUrls: ['./rule-sets-listing-screen.component.scss']
}) })
export class RuleSetsListingScreenComponent extends BaseListingComponent<RuleSetModel> implements OnInit { export class RuleSetsListingScreenComponent
extends BaseListingComponent<RuleSetModel>
implements OnInit
{
protected readonly _searchKey = 'name'; protected readonly _searchKey = 'name';
protected readonly _selectionKey = 'ruleSetId'; protected readonly _selectionKey = 'ruleSetId';
protected readonly _sortKey = 'rule-sets-listing'; protected readonly _sortKey = 'rule-sets-listing';

View File

@ -5,7 +5,12 @@
<div class="flex-1 actions"> <div class="flex-1 actions">
<redaction-rule-set-actions></redaction-rule-set-actions> <redaction-rule-set-actions></redaction-rule-set-actions>
<redaction-circle-button [routerLink]="['../..']" icon="red:close" tooltip="common.close" tooltipPosition="below"></redaction-circle-button> <redaction-circle-button
[routerLink]="['../..']"
icon="red:close"
tooltip="common.close"
tooltipPosition="below"
></redaction-circle-button>
</div> </div>
</div> </div>
@ -29,10 +34,21 @@
</ace-editor> </ace-editor>
</div> </div>
<div *ngIf="hasChanges && permissionsService.isAdmin()" class="changes-box"> <div *ngIf="hasChanges && permissionsService.isAdmin()" class="changes-box">
<redaction-icon-button (action)="save()" icon="red:check" text="rules-screen.save-changes" type="primary"></redaction-icon-button> <redaction-icon-button
<div (click)="revert()" class="all-caps-label cancel" translate="rules-screen.revert-changes"></div> (action)="save()"
icon="red:check"
text="rules-screen.save-changes"
type="primary"
></redaction-icon-button>
<div
(click)="revert()"
class="all-caps-label cancel"
translate="rules-screen.revert-changes"
></div>
</div> </div>
</div> </div>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="processing"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator
[displayed]="processing"
></redaction-full-page-loading-indicator>

View File

@ -69,7 +69,12 @@ export class RulesScreenComponent extends ComponentHasChanges {
const entry = this.currentLines[i]; const entry = this.currentLines[i];
if (entry?.trim().length > 0) { if (entry?.trim().length > 0) {
// only mark non-empty lines // only mark non-empty lines
this.activeEditMarkers.push(this.editorComponent.getEditor().getSession().addMarker(new range(i, 0, i, 1), 'changed-row-marker', 'fullLine')); this.activeEditMarkers.push(
this.editorComponent
.getEditor()
.getSession()
.addMarker(new range(i, 0, i, 1), 'changed-row-marker', 'fullLine')
);
} }
} }
} }
@ -92,7 +97,11 @@ export class RulesScreenComponent extends ComponentHasChanges {
}, },
() => { () => {
this.processing = false; this.processing = false;
this._notificationService.showToastNotification(this._translateService.instant('rules-screen.error.generic'), null, NotificationType.ERROR); this._notificationService.showToastNotification(
this._translateService.instant('rules-screen.error.generic'),
null,
NotificationType.ERROR
);
} }
); );
} }

View File

@ -34,7 +34,9 @@
<input <input
formControlName="host" formControlName="host"
name="host" name="host"
placeholder="{{ 'smtp-config-screen.form.host-placeholder' | translate }}" placeholder="{{
'smtp-config-screen.form.host-placeholder' | translate
}}"
type="text" type="text"
/> />
</div> </div>
@ -44,7 +46,9 @@
<input <input
formControlName="port" formControlName="port"
name="port" name="port"
placeholder="{{ 'smtp-config-screen.form.port-placeholder' | translate }}" placeholder="{{
'smtp-config-screen.form.port-placeholder' | translate
}}"
type="number" type="number"
/> />
</div> </div>
@ -54,20 +58,30 @@
<input <input
formControlName="from" formControlName="from"
name="from" name="from"
placeholder="{{ 'smtp-config-screen.form.from-placeholder' | translate }}" placeholder="{{
'smtp-config-screen.form.from-placeholder' | translate
}}"
type="email" type="email"
/> />
</div> </div>
<div class="red-input-group"> <div class="red-input-group">
<label translate="smtp-config-screen.form.from-display-name"></label> <label
translate="smtp-config-screen.form.from-display-name"
></label>
<input <input
formControlName="fromDisplayName" formControlName="fromDisplayName"
name="fromDisplayName" name="fromDisplayName"
placeholder="{{ 'smtp-config-screen.form.from-display-name-placeholder' | translate }}" placeholder="{{
'smtp-config-screen.form.from-display-name-placeholder'
| translate
}}"
type="text" type="text"
/> />
<span class="hint" translate="smtp-config-screen.form.from-display-name-hint"></span> <span
class="hint"
translate="smtp-config-screen.form.from-display-name-hint"
></span>
</div> </div>
<div class="red-input-group"> <div class="red-input-group">
@ -75,44 +89,69 @@
<input <input
formControlName="replyTo" formControlName="replyTo"
name="replyTo" name="replyTo"
placeholder="{{ 'smtp-config-screen.form.reply-to-placeholder' | translate }}" placeholder="{{
'smtp-config-screen.form.reply-to-placeholder'
| translate
}}"
type="text" type="text"
/> />
</div> </div>
<div class="red-input-group"> <div class="red-input-group">
<label translate="smtp-config-screen.form.reply-to-display-name"></label> <label
translate="smtp-config-screen.form.reply-to-display-name"
></label>
<input <input
formControlName="replyToDisplayName" formControlName="replyToDisplayName"
name="replyToDisplayName" name="replyToDisplayName"
placeholder="{{ 'smtp-config-screen.form.reply-to-display-name-placeholder' | translate }}" placeholder="{{
'smtp-config-screen.form.reply-to-display-name-placeholder'
| translate
}}"
type="text" type="text"
/> />
</div> </div>
<div class="red-input-group"> <div class="red-input-group">
<label translate="smtp-config-screen.form.envelope-from"></label> <label
translate="smtp-config-screen.form.envelope-from"
></label>
<input <input
formControlName="envelopeFrom" formControlName="envelopeFrom"
name="envelopeFrom" name="envelopeFrom"
placeholder="{{ 'smtp-config-screen.form.envelope-from-placeholder' | translate }}" placeholder="{{
'smtp-config-screen.form.envelope-from-placeholder'
| translate
}}"
type="text" type="text"
/> />
<span class="hint" translate="smtp-config-screen.form.envelope-from-hint"></span> <span
class="hint"
translate="smtp-config-screen.form.envelope-from-hint"
></span>
</div> </div>
</div> </div>
<div> <div>
<div class="red-input-group"> <div class="red-input-group">
<label translate="smtp-config-screen.form.ssl"></label> <label translate="smtp-config-screen.form.ssl"></label>
<mat-slide-toggle color="primary" formControlName="ssl"></mat-slide-toggle> <mat-slide-toggle
color="primary"
formControlName="ssl"
></mat-slide-toggle>
</div> </div>
<div class="red-input-group"> <div class="red-input-group">
<label translate="smtp-config-screen.form.starttls"></label> <label translate="smtp-config-screen.form.starttls"></label>
<mat-slide-toggle color="primary" formControlName="starttls"></mat-slide-toggle> <mat-slide-toggle
color="primary"
formControlName="starttls"
></mat-slide-toggle>
</div> </div>
<div class="red-input-group"> <div class="red-input-group">
<label translate="smtp-config-screen.form.auth"></label> <label translate="smtp-config-screen.form.auth"></label>
<mat-slide-toggle color="primary" formControlName="auth"></mat-slide-toggle> <mat-slide-toggle
color="primary"
formControlName="auth"
></mat-slide-toggle>
</div> </div>
<div <div
(click)="openAuthConfigDialog(true)" (click)="openAuthConfigDialog(true)"
@ -123,7 +162,12 @@
</div> </div>
</div> </div>
<div class="dialog-actions"> <div class="dialog-actions">
<button [disabled]="configForm.invalid || !changed" color="primary" mat-flat-button type="submit"> <button
[disabled]="configForm.invalid || !changed"
color="primary"
mat-flat-button
type="submit"
>
{{ 'smtp-config-screen.actions.save' | translate }} {{ 'smtp-config-screen.actions.save' | translate }}
</button> </button>
@ -141,4 +185,6 @@
</div> </div>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator
[displayed]="!viewReady"
></redaction-full-page-loading-indicator>

View File

@ -65,32 +65,43 @@ export class SmtpConfigScreenComponent implements OnInit {
async save() { async save() {
this.viewReady = false; this.viewReady = false;
await this._smtpConfigService.updateSMTPConfiguration(this.configForm.getRawValue()).toPromise(); await this._smtpConfigService
.updateSMTPConfiguration(this.configForm.getRawValue())
.toPromise();
this._initialValue = this.configForm.getRawValue(); this._initialValue = this.configForm.getRawValue();
this.viewReady = true; this.viewReady = true;
} }
openAuthConfigDialog(skipDisableOnCancel?: boolean) { openAuthConfigDialog(skipDisableOnCancel?: boolean) {
this._dialogService.openSMTPAuthConfigDialog(this.configForm.getRawValue(), (authConfig) => { this._dialogService.openSMTPAuthConfigDialog(
if (authConfig) { this.configForm.getRawValue(),
this.configForm.patchValue(authConfig); (authConfig) => {
} else if (!skipDisableOnCancel) { if (authConfig) {
this.configForm.patchValue({ auth: false }, { emitEvent: false }); this.configForm.patchValue(authConfig);
} else if (!skipDisableOnCancel) {
this.configForm.patchValue({ auth: false }, { emitEvent: false });
}
} }
}); );
} }
async testConnection() { async testConnection() {
this.viewReady = false; this.viewReady = false;
try { try {
await this._smtpConfigService.testSMTPConfiguration(this.configForm.getRawValue()).toPromise(); await this._smtpConfigService
.testSMTPConfiguration(this.configForm.getRawValue())
.toPromise();
this._notificationService.showToastNotification( this._notificationService.showToastNotification(
this._translateService.instant('smtp-config-screen.test.success'), this._translateService.instant('smtp-config-screen.test.success'),
undefined, undefined,
NotificationType.SUCCESS NotificationType.SUCCESS
); );
} catch (e) { } catch (e) {
this._notificationService.showToastNotification(this._translateService.instant('smtp-config-screen.test.error'), undefined, NotificationType.ERROR); this._notificationService.showToastNotification(
this._translateService.instant('smtp-config-screen.test.error'),
undefined,
NotificationType.ERROR
);
} finally { } finally {
this.viewReady = true; this.viewReady = true;
} }
@ -98,7 +109,9 @@ export class SmtpConfigScreenComponent implements OnInit {
private async _loadData() { private async _loadData() {
try { try {
this._initialValue = await this._smtpConfigService.getCurrentSMTPConfiguration().toPromise(); this._initialValue = await this._smtpConfigService
.getCurrentSMTPConfiguration()
.toPromise();
this.configForm.patchValue(this._initialValue, { emitEvent: false }); this.configForm.patchValue(this._initialValue, { emitEvent: false });
} catch (e) { } catch (e) {
} finally { } finally {

View File

@ -8,7 +8,10 @@
<div class="breadcrumb" translate="user-management"></div> <div class="breadcrumb" translate="user-management"></div>
<div class="actions"> <div class="actions">
<redaction-search-input [form]="searchForm" [placeholder]="'user-listing.search'"></redaction-search-input> <redaction-search-input
[form]="searchForm"
[placeholder]="'user-listing.search'"
></redaction-search-input>
<redaction-icon-button <redaction-icon-button
(action)="openAddEditUserDialog($event)" (action)="openAddEditUserDialog($event)"
*ngIf="permissionsService.isUserAdmin()" *ngIf="permissionsService.isUserAdmin()"
@ -39,14 +42,21 @@
</div> </div>
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'user-listing.table-header.title' | translate: { length: displayedEntities.length } }} {{
'user-listing.table-header.title'
| translate: { length: displayedEntities.length }
}}
</span> </span>
<ng-container *ngIf="areSomeEntitiesSelected && !loading"> <ng-container *ngIf="areSomeEntitiesSelected && !loading">
<redaction-circle-button <redaction-circle-button
(action)="bulkDelete()" (action)="bulkDelete()"
[disabled]="!canDeleteSelected" [disabled]="!canDeleteSelected"
[tooltip]="canDeleteSelected ? 'user-listing.bulk.delete' : 'user-listing.bulk.delete-disabled'" [tooltip]="
canDeleteSelected
? 'user-listing.bulk.delete'
: 'user-listing.bulk.delete-disabled'
"
icon="red:trash" icon="red:trash"
tooltipPosition="after" tooltipPosition="after"
type="dark-bg" type="dark-bg"
@ -59,33 +69,56 @@
<div class="table-header" redactionSyncWidth="table-item"> <div class="table-header" redactionSyncWidth="table-item">
<div class="select-oval-placeholder"></div> <div class="select-oval-placeholder"></div>
<redaction-table-col-name label="user-listing.table-col-names.name"></redaction-table-col-name> <redaction-table-col-name
label="user-listing.table-col-names.name"
></redaction-table-col-name>
<redaction-table-col-name label="user-listing.table-col-names.email"></redaction-table-col-name> <redaction-table-col-name
label="user-listing.table-col-names.email"
></redaction-table-col-name>
<redaction-table-col-name class="flex-center" label="user-listing.table-col-names.active"></redaction-table-col-name> <redaction-table-col-name
class="flex-center"
label="user-listing.table-col-names.active"
></redaction-table-col-name>
<redaction-table-col-name label="user-listing.table-col-names.roles"></redaction-table-col-name> <redaction-table-col-name
label="user-listing.table-col-names.roles"
></redaction-table-col-name>
<div></div> <div></div>
<div class="scrollbar-placeholder"></div> <div class="scrollbar-placeholder"></div>
</div> </div>
<redaction-empty-state *ngIf="!displayedEntities.length" screen="user-listing" type="no-match"></redaction-empty-state> <redaction-empty-state
*ngIf="!displayedEntities.length"
screen="user-listing"
type="no-match"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines --> <!-- Table lines -->
<div *cdkVirtualFor="let user of displayedEntities" class="table-item"> <div *cdkVirtualFor="let user of displayedEntities" class="table-item">
<div (click)="toggleEntitySelected($event, user)" class="selection-column"> <div (click)="toggleEntitySelected($event, user)" class="selection-column">
<redaction-round-checkbox [active]="isEntitySelected(user)"></redaction-round-checkbox> <redaction-round-checkbox
[active]="isEntitySelected(user)"
></redaction-round-checkbox>
</div> </div>
<div> <div>
<redaction-initials-avatar [showYou]="true" [user]="user" [withName]="true"></redaction-initials-avatar> <redaction-initials-avatar
[showYou]="true"
[user]="user"
[withName]="true"
></redaction-initials-avatar>
</div> </div>
<div class="small-label">{{ user.email || '-' }}</div> <div class="small-label">{{ user.email || '-' }}</div>
<div class="center"> <div class="center">
<mat-slide-toggle (toggleChange)="toggleActive(user)" [checked]="userService.isActive(user)" color="primary"></mat-slide-toggle> <mat-slide-toggle
(toggleChange)="toggleActive(user)"
[checked]="userService.isActive(user)"
color="primary"
></mat-slide-toggle>
</div> </div>
<div class="small-label">{{ getDisplayRoles(user) }}</div> <div class="small-label">{{ getDisplayRoles(user) }}</div>
<div class="actions-container"> <div class="actions-container">
@ -113,10 +146,15 @@
</div> </div>
<div [class.collapsed]="collapsedDetails" class="right-container" redactionHasScrollbar> <div [class.collapsed]="collapsedDetails" class="right-container" redactionHasScrollbar>
<redaction-users-stats (toggleCollapse)="toggleCollapsedDetails()" [chartData]="chartData"></redaction-users-stats> <redaction-users-stats
(toggleCollapse)="toggleCollapsedDetails()"
[chartData]="chartData"
></redaction-users-stats>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator
[displayed]="!viewReady"
></redaction-full-page-loading-indicator>

View File

@ -50,7 +50,9 @@ export class UserListingScreenComponent extends BaseListingComponent<User> imple
if (result.action === 'CREATE') { if (result.action === 'CREATE') {
await this._userControllerService.createUser(result.user).toPromise(); await this._userControllerService.createUser(result.user).toPromise();
} else if (result.action === 'UPDATE') { } else if (result.action === 'UPDATE') {
await this._userControllerService.updateProfile(result.user, user.userId).toPromise(); await this._userControllerService
.updateProfile(result.user, user.userId)
.toPromise();
} }
await this._loadData(); await this._loadData();
} }
@ -67,7 +69,10 @@ export class UserListingScreenComponent extends BaseListingComponent<User> imple
} }
getDisplayRoles(user: User) { getDisplayRoles(user: User) {
return user.roles.map((role) => this._translateService.instant('roles.' + role)).join(', ') || this._translateService.instant('roles.NO_ROLE'); return (
user.roles.map((role) => this._translateService.instant('roles.' + role)).join(', ') ||
this._translateService.instant('roles.NO_ROLE')
);
} }
async toggleActive(user: User) { async toggleActive(user: User) {
@ -101,32 +106,46 @@ export class UserListingScreenComponent extends BaseListingComponent<User> imple
this.chartData = this._translateChartService.translateRoles( this.chartData = this._translateChartService.translateRoles(
[ [
{ {
value: this.allEntities.filter((user) => !this.userService.isActive(user)).length, value: this.allEntities.filter((user) => !this.userService.isActive(user))
.length,
color: 'INACTIVE', color: 'INACTIVE',
label: 'INACTIVE' label: 'INACTIVE'
}, },
{ {
value: this.allEntities.filter((user) => user.roles.length === 1 && user.roles[0] === 'RED_USER').length, value: this.allEntities.filter(
(user) => user.roles.length === 1 && user.roles[0] === 'RED_USER'
).length,
color: 'REGULAR', color: 'REGULAR',
label: 'REGULAR' label: 'REGULAR'
}, },
{ {
value: this.allEntities.filter((user) => this.userService.isManager(user) && !this.userService.isAdmin(user)).length, value: this.allEntities.filter(
(user) =>
this.userService.isManager(user) && !this.userService.isAdmin(user)
).length,
color: 'MANAGER', color: 'MANAGER',
label: 'RED_MANAGER' label: 'RED_MANAGER'
}, },
{ {
value: this.allEntities.filter((user) => this.userService.isManager(user) && this.userService.isAdmin(user)).length, value: this.allEntities.filter(
(user) => this.userService.isManager(user) && this.userService.isAdmin(user)
).length,
color: 'MANAGER_ADMIN', color: 'MANAGER_ADMIN',
label: 'MANAGER_ADMIN' label: 'MANAGER_ADMIN'
}, },
{ {
value: this.allEntities.filter((user) => this.userService.isUserAdmin(user) && !this.userService.isAdmin(user)).length, value: this.allEntities.filter(
(user) =>
this.userService.isUserAdmin(user) && !this.userService.isAdmin(user)
).length,
color: 'USER_ADMIN', color: 'USER_ADMIN',
label: 'RED_USER_ADMIN' label: 'RED_USER_ADMIN'
}, },
{ {
value: this.allEntities.filter((user) => this.userService.isAdmin(user) && !this.userService.isManager(user)).length, value: this.allEntities.filter(
(user) =>
this.userService.isAdmin(user) && !this.userService.isManager(user)
).length,
color: 'ADMIN', color: 'ADMIN',
label: 'RED_ADMIN' label: 'RED_ADMIN'
} }

View File

@ -3,7 +3,12 @@
<redaction-admin-breadcrumbs class="flex-1"></redaction-admin-breadcrumbs> <redaction-admin-breadcrumbs class="flex-1"></redaction-admin-breadcrumbs>
<div class="actions flex-1"> <div class="actions flex-1">
<redaction-circle-button [routerLink]="['../..']" icon="red:close" tooltip="common.close" tooltipPosition="below"></redaction-circle-button> <redaction-circle-button
[routerLink]="['../..']"
icon="red:close"
tooltip="common.close"
tooltipPosition="below"
></redaction-circle-button>
</div> </div>
</div> </div>
@ -22,7 +27,11 @@
text="watermark-screen.action.save" text="watermark-screen.action.save"
type="primary" type="primary"
></redaction-icon-button> ></redaction-icon-button>
<div (click)="revert()" class="all-caps-label cancel" translate="watermark-screen.action.revert"></div> <div
(click)="revert()"
class="all-caps-label cancel"
translate="watermark-screen.action.revert"
></div>
</div> </div>
</div> </div>
@ -43,7 +52,10 @@
</div> </div>
<div class="red-input-group"> <div class="red-input-group">
<label class="all-caps-label mb-8" translate="watermark-screen.form.orientation"></label> <label
class="all-caps-label mb-8"
translate="watermark-screen.form.orientation"
></label>
<div class="square-options"> <div class="square-options">
<div <div
(click)="setValue('orientation', option)" (click)="setValue('orientation', option)"
@ -58,17 +70,34 @@
</div> </div>
<div class="red-input-group"> <div class="red-input-group">
<label class="all-caps-label" translate="watermark-screen.form.font-size"></label> <label
<mat-slider (change)="configChanged()" color="primary" formControlName="fontSize" max="50" min="5"></mat-slider> class="all-caps-label"
translate="watermark-screen.form.font-size"
></label>
<mat-slider
(change)="configChanged()"
color="primary"
formControlName="fontSize"
max="50"
min="5"
></mat-slider>
</div> </div>
<div class="red-input-group"> <div class="red-input-group">
<label class="all-caps-label" translate="watermark-screen.form.opacity"></label> <label class="all-caps-label" translate="watermark-screen.form.opacity"></label>
<mat-slider (change)="configChanged()" color="primary" formControlName="opacity" min="1"></mat-slider> <mat-slider
(change)="configChanged()"
color="primary"
formControlName="opacity"
min="1"
></mat-slider>
</div> </div>
<div class="red-input-group w-150"> <div class="red-input-group w-150">
<label class="all-caps-label mb-5" translate="watermark-screen.form.color"></label> <label
class="all-caps-label mb-5"
translate="watermark-screen.form.color"
></label>
<input <input
class="hex-color-input" class="hex-color-input"
formControlName="hexColor" formControlName="hexColor"
@ -88,14 +117,20 @@
class="input-icon" class="input-icon"
> >
<mat-icon <mat-icon
*ngIf="!configForm.get('hexColor')?.value || configForm.get('hexColor').value?.length === 0" *ngIf="
!configForm.get('hexColor')?.value ||
configForm.get('hexColor').value?.length === 0
"
svgIcon="red:color-picker" svgIcon="red:color-picker"
></mat-icon> ></mat-icon>
</div> </div>
</div> </div>
<div class="red-input-group"> <div class="red-input-group">
<label class="all-caps-label mb-8" translate="watermark-screen.form.font-type"></label> <label
class="all-caps-label mb-8"
translate="watermark-screen.form.font-type"
></label>
<div class="square-options"> <div class="square-options">
<div <div
(click)="setValue('fontType', option.value)" (click)="setValue('fontType', option.value)"
@ -119,4 +154,6 @@
</div> </div>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator> <redaction-full-page-loading-indicator
[displayed]="!viewReady"
></redaction-full-page-loading-indicator>

View File

@ -78,20 +78,33 @@ export class WatermarkScreenComponent implements OnInit {
}; };
const observable = watermark.text const observable = watermark.text
? this._watermarkControllerService.saveWatermark(watermark, this.appStateService.activeRuleSetId) ? this._watermarkControllerService.saveWatermark(
: this._watermarkControllerService.deleteWatermark(this.appStateService.activeRuleSetId); watermark,
this.appStateService.activeRuleSetId
)
: this._watermarkControllerService.deleteWatermark(
this.appStateService.activeRuleSetId
);
observable.subscribe( observable.subscribe(
() => { () => {
this._loadWatermark(); this._loadWatermark();
this._notificationService.showToastNotification( this._notificationService.showToastNotification(
this._translateService.instant(watermark.text ? 'watermark-screen.action.change-success' : 'watermark-screen.action.delete-success'), this._translateService.instant(
watermark.text
? 'watermark-screen.action.change-success'
: 'watermark-screen.action.delete-success'
),
null, null,
NotificationType.SUCCESS NotificationType.SUCCESS
); );
}, },
() => { () => {
this._notificationService.showToastNotification(this._translateService.instant('watermark-screen.action.error'), null, NotificationType.ERROR); this._notificationService.showToastNotification(
this._translateService.instant('watermark-screen.action.error'),
null,
NotificationType.ERROR
);
} }
); );
} }
@ -111,18 +124,20 @@ export class WatermarkScreenComponent implements OnInit {
} }
private _loadWatermark() { private _loadWatermark() {
this._watermarkControllerService.getWatermark(this.appStateService.activeRuleSetId).subscribe( this._watermarkControllerService
(watermark) => { .getWatermark(this.appStateService.activeRuleSetId)
this._watermark = watermark; .subscribe(
this.configForm.setValue({ ...this._watermark }); (watermark) => {
this._loadViewer(); this._watermark = watermark;
}, this.configForm.setValue({ ...this._watermark });
() => { this._loadViewer();
this._watermark = DEFAULT_WATERMARK; },
this.configForm.setValue({ ...this._watermark }); () => {
this._loadViewer(); this._watermark = DEFAULT_WATERMARK;
} this.configForm.setValue({ ...this._watermark });
); this._loadViewer();
}
);
} }
private _loadViewer() { private _loadViewer() {
@ -177,14 +192,17 @@ export class WatermarkScreenComponent implements OnInit {
const text = this.configForm.get('text').value || ''; const text = this.configForm.get('text').value || '';
const fontSize = this.configForm.get('fontSize').value; const fontSize = this.configForm.get('fontSize').value;
const fontType = this.configForm.get('fontType').value; const fontType = this.configForm.get('fontType').value;
const orientation: WatermarkModel.WatermarkOrientationEnum = this.configForm.get('orientation').value; const orientation: WatermarkModel.WatermarkOrientationEnum =
this.configForm.get('orientation').value;
const opacity = this.configForm.get('opacity').value; const opacity = this.configForm.get('opacity').value;
const color = this.configForm.get('hexColor').value; const color = this.configForm.get('hexColor').value;
const rgbColor = hexToRgb(color); const rgbColor = hexToRgb(color);
const stamper = await pdfNet.Stamper.create(3, fontSize, 0); const stamper = await pdfNet.Stamper.create(3, fontSize, 0);
await stamper.setFontColor(await pdfNet.ColorPt.init(rgbColor.r / 255, rgbColor.g / 255, rgbColor.b / 255)); await stamper.setFontColor(
await pdfNet.ColorPt.init(rgbColor.r / 255, rgbColor.g / 255, rgbColor.b / 255)
);
await stamper.setOpacity(opacity / 100); await stamper.setOpacity(opacity / 100);
switch (orientation) { switch (orientation) {
@ -200,7 +218,10 @@ export class WatermarkScreenComponent implements OnInit {
await stamper.setRotation(-45); await stamper.setRotation(-45);
} }
const font = await pdfNet.Font.createAndEmbed(document, this._convertFont(fontType)); const font = await pdfNet.Font.createAndEmbed(
document,
this._convertFont(fontType)
);
await stamper.setFont(font); await stamper.setFont(font);
await stamper.setTextAlignment(0); await stamper.setTextAlignment(0);
await stamper.stampText(document, text, pageSet); await stamper.stampText(document, text, pageSet);
@ -216,11 +237,26 @@ export class WatermarkScreenComponent implements OnInit {
private _initForm() { private _initForm() {
this.configForm = this._formBuilder.group({ this.configForm = this._formBuilder.group({
text: [{ value: null, disabled: !this.permissionsService.isAdmin() }], text: [{ value: null, disabled: !this.permissionsService.isAdmin() }],
hexColor: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required], hexColor: [
opacity: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required], { value: null, disabled: !this.permissionsService.isAdmin() },
fontSize: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required], Validators.required
fontType: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required], ],
orientation: [{ value: null, disabled: !this.permissionsService.isAdmin() }, Validators.required] opacity: [
{ value: null, disabled: !this.permissionsService.isAdmin() },
Validators.required
],
fontSize: [
{ value: null, disabled: !this.permissionsService.isAdmin() },
Validators.required
],
fontType: [
{ value: null, disabled: !this.permissionsService.isAdmin() },
Validators.required
],
orientation: [
{ value: null, disabled: !this.permissionsService.isAdmin() },
Validators.required
]
}); });
} }

View File

@ -53,19 +53,30 @@ export class AdminDialogService {
private readonly _manualRedactionControllerService: ManualRedactionControllerService private readonly _manualRedactionControllerService: ManualRedactionControllerService
) {} ) {}
openDeleteDictionaryDialog($event: MouseEvent, dictionary: TypeValue, ruleSetId: string, cb?: Function): MatDialogRef<ConfirmationDialogComponent> { openDeleteDictionaryDialog(
$event: MouseEvent,
dictionary: TypeValue,
ruleSetId: string,
cb?: Function
): MatDialogRef<ConfirmationDialogComponent> {
$event.stopPropagation(); $event.stopPropagation();
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig); const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
ref.afterClosed().subscribe(async (result) => { ref.afterClosed().subscribe(async (result) => {
if (result) { if (result) {
await this._dictionaryControllerService.deleteType(dictionary.type, ruleSetId).toPromise(); await this._dictionaryControllerService
.deleteType(dictionary.type, ruleSetId)
.toPromise();
if (cb) cb(); if (cb) cb();
} }
}); });
return ref; return ref;
} }
openDeleteRuleSetDialog($event: MouseEvent, ruleSet: RuleSetModel, cb?: Function): MatDialogRef<ConfirmationDialogComponent> { openDeleteRuleSetDialog(
$event: MouseEvent,
ruleSet: RuleSetModel,
cb?: Function
): MatDialogRef<ConfirmationDialogComponent> {
$event.stopPropagation(); $event.stopPropagation();
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig); const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
ref.afterClosed().subscribe(async (result) => { ref.afterClosed().subscribe(async (result) => {
@ -77,7 +88,11 @@ export class AdminDialogService {
return ref; return ref;
} }
openAddEditDictionaryDialog(dictionary: TypeValue, ruleSetId: string, cb?: Function): MatDialogRef<AddEditDictionaryDialogComponent> { openAddEditDictionaryDialog(
dictionary: TypeValue,
ruleSetId: string,
cb?: Function
): MatDialogRef<AddEditDictionaryDialogComponent> {
const ref = this._dialog.open(AddEditDictionaryDialogComponent, { const ref = this._dialog.open(AddEditDictionaryDialogComponent, {
...dialogConfig, ...dialogConfig,
data: { dictionary, ruleSetId }, data: { dictionary, ruleSetId },
@ -93,7 +108,12 @@ export class AdminDialogService {
return ref; return ref;
} }
openEditColorsDialog(colors: Colors, colorKey: string, ruleSetId: string, cb?: Function): MatDialogRef<EditColorDialogComponent> { openEditColorsDialog(
colors: Colors,
colorKey: string,
ruleSetId: string,
cb?: Function
): MatDialogRef<EditColorDialogComponent> {
const ref = this._dialog.open(EditColorDialogComponent, { const ref = this._dialog.open(EditColorDialogComponent, {
...dialogConfig, ...dialogConfig,
data: { colors, colorKey, ruleSetId }, data: { colors, colorKey, ruleSetId },
@ -109,7 +129,10 @@ export class AdminDialogService {
return ref; return ref;
} }
openAddEditRuleSetDialog(ruleSet: RuleSetModel, cb?: Function): MatDialogRef<AddEditRuleSetDialogComponent> { openAddEditRuleSetDialog(
ruleSet: RuleSetModel,
cb?: Function
): MatDialogRef<AddEditRuleSetDialogComponent> {
const ref = this._dialog.open(AddEditRuleSetDialogComponent, { const ref = this._dialog.open(AddEditRuleSetDialogComponent, {
...dialogConfig, ...dialogConfig,
width: '900px', width: '900px',
@ -146,7 +169,11 @@ export class AdminDialogService {
return ref; return ref;
} }
openAddEditFileAttributeDialog(fileAttribute: FileAttributeConfig, ruleSetId: string, cb?: Function): MatDialogRef<AddEditFileAttributeDialogComponent> { openAddEditFileAttributeDialog(
fileAttribute: FileAttributeConfig,
ruleSetId: string,
cb?: Function
): MatDialogRef<AddEditFileAttributeDialogComponent> {
const ref = this._dialog.open(AddEditFileAttributeDialogComponent, { const ref = this._dialog.open(AddEditFileAttributeDialogComponent, {
...dialogConfig, ...dialogConfig,
data: { fileAttribute, ruleSetId }, data: { fileAttribute, ruleSetId },
@ -182,7 +209,10 @@ export class AdminDialogService {
return ref; return ref;
} }
openSMTPAuthConfigDialog(smtpConfig: SMTPConfigurationModel, cb?: Function): MatDialogRef<SmtpAuthDialogComponent> { openSMTPAuthConfigDialog(
smtpConfig: SMTPConfigurationModel,
cb?: Function
): MatDialogRef<SmtpAuthDialogComponent> {
const ref = this._dialog.open(SmtpAuthDialogComponent, { const ref = this._dialog.open(SmtpAuthDialogComponent, {
...dialogConfig, ...dialogConfig,
data: smtpConfig, data: smtpConfig,
@ -214,7 +244,10 @@ export class AdminDialogService {
return ref; return ref;
} }
openConfirmDeleteUsersDialog(users: User[], cb?: Function): MatDialogRef<ConfirmDeleteUsersDialogComponent> { openConfirmDeleteUsersDialog(
users: User[],
cb?: Function
): MatDialogRef<ConfirmDeleteUsersDialogComponent> {
const ref = this._dialog.open(ConfirmDeleteUsersDialogComponent, { const ref = this._dialog.open(ConfirmDeleteUsersDialogComponent, {
...dialogConfig, ...dialogConfig,
data: users, data: users,

View File

@ -3,7 +3,9 @@ import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { version } from '../../../../../../package.json'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import packageInfo from '../../../../../../package.json';
import { CacheApiService, wipeCaches } from '@redaction/red-cache'; import { CacheApiService, wipeCaches } from '@redaction/red-cache';
export enum AppConfigKey { export enum AppConfigKey {
@ -34,23 +36,37 @@ export enum AppConfigKey {
export class AppConfigService { export class AppConfigService {
private _config: { [key in AppConfigKey]?: any } = {}; private _config: { [key in AppConfigKey]?: any } = {};
constructor(private readonly _httpClient: HttpClient, private readonly _cacheApiService: CacheApiService, private readonly _titleService: Title) {} constructor(
private readonly _httpClient: HttpClient,
private readonly _cacheApiService: CacheApiService,
private readonly _titleService: Title
) {}
loadAppConfig(): Observable<any> { loadAppConfig(): Observable<any> {
this._cacheApiService.getCachedValue(AppConfigKey.FRONTEND_APP_VERSION).then(async (lastVersion) => { this._cacheApiService
console.log('[REDACTION] Last app version: ', lastVersion, ' current version ', version); .getCachedValue(AppConfigKey.FRONTEND_APP_VERSION)
if (lastVersion !== version) { .then(async (lastVersion) => {
console.log('[REDACTION] Version-missmatch - wiping caches!'); console.log(
await wipeCaches(); '[REDACTION] Last app version: ',
} lastVersion,
await this._cacheApiService.cacheValue(AppConfigKey.FRONTEND_APP_VERSION, version); ' current version ',
}); this.version
);
if (lastVersion !== this.version) {
console.log('[REDACTION] Version-missmatch - wiping caches!');
await wipeCaches();
}
await this._cacheApiService.cacheValue(
AppConfigKey.FRONTEND_APP_VERSION,
this.version
);
});
return this._httpClient.get<any>('/assets/config/config.json').pipe( return this._httpClient.get<any>('/assets/config/config.json').pipe(
tap((config) => { tap((config) => {
console.log('[REDACTION] Started with config: ', config); console.log('[REDACTION] Started with config: ', config);
this._config = config; this._config = config;
this._config[AppConfigKey.FRONTEND_APP_VERSION] = version; this._config[AppConfigKey.FRONTEND_APP_VERSION] = this.version;
this._titleService.setTitle(this.getConfig(AppConfigKey.APP_NAME, 'DDA-R')); this._titleService.setTitle(this.getConfig(AppConfigKey.APP_NAME, 'DDA-R'));
}) })
); );
@ -59,4 +75,8 @@ export class AppConfigService {
getConfig(key: AppConfigKey | string, defaultValue?: any) { getConfig(key: AppConfigKey | string, defaultValue?: any) {
return this._config[key] ? this._config[key] : defaultValue; return this._config[key] ? this._config[key] : defaultValue;
} }
get version(): string {
return packageInfo.version;
}
} }

View File

@ -7,7 +7,11 @@ import { KeycloakAngularModule, KeycloakOptions, KeycloakService } from 'keycloa
import { AppConfigKey, AppConfigService } from '@app-config/app-config.service'; import { AppConfigKey, AppConfigService } from '@app-config/app-config.service';
import { BASE_HREF } from '../../tokens'; import { BASE_HREF } from '../../tokens';
export function keycloakInitializer(keycloak: KeycloakService, appConfigService: AppConfigService, baseUrl) { export function keycloakInitializer(
keycloak: KeycloakService,
appConfigService: AppConfigService,
baseUrl
) {
return () => return () =>
appConfigService appConfigService
.loadAppConfig() .loadAppConfig()
@ -26,12 +30,15 @@ export function keycloakInitializer(keycloak: KeycloakService, appConfigService:
initOptions: { initOptions: {
checkLoginIframe: false, checkLoginIframe: false,
onLoad: 'check-sso', onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + baseUrl + '/assets/oauth/silent-refresh.html', silentCheckSsoRedirectUri:
window.location.origin + baseUrl + '/assets/oauth/silent-refresh.html',
flow: 'standard' flow: 'standard'
}, },
enableBearerInterceptor: true enableBearerInterceptor: true
}; };
return keycloak.init(options).then(() => configureAutomaticRedirectToLoginScreen(keycloak)); return keycloak
.init(options)
.then(() => configureAutomaticRedirectToLoginScreen(keycloak));
}); });
} }

View File

@ -8,7 +8,11 @@ import { Observable } from 'rxjs';
providedIn: 'root' providedIn: 'root'
}) })
export class RedRoleGuard implements CanActivate { export class RedRoleGuard implements CanActivate {
constructor(protected readonly _router: Router, private readonly _appLoadStateService: AppLoadStateService, private readonly _userService: UserService) {} constructor(
protected readonly _router: Router,
private readonly _appLoadStateService: AppLoadStateService,
private readonly _userService: UserService
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return new Observable((obs) => { return new Observable((obs) => {
@ -20,7 +24,11 @@ export class RedRoleGuard implements CanActivate {
} else { } else {
// we have at least 1 RED Role -> if it's not user he must be admin // we have at least 1 RED Role -> if it's not user he must be admin
if (this._userService.user.isUserAdmin && !this._userService.user.isAdmin && !state.url.startsWith('/main/admin/users')) { if (
this._userService.user.isUserAdmin &&
!this._userService.user.isAdmin &&
!state.url.startsWith('/main/admin/users')
) {
this._router.navigate(['/main/admin/users']); this._router.navigate(['/main/admin/users']);
obs.next(false); obs.next(false);
obs.complete(); obs.complete();

View File

@ -9,7 +9,10 @@ import { DomSanitizer } from '@angular/platform-browser';
exports: [MatIconModule] exports: [MatIconModule]
}) })
export class IconsModule { export class IconsModule {
constructor(private readonly _iconRegistry: MatIconRegistry, private readonly _sanitizer: DomSanitizer) { constructor(
private readonly _iconRegistry: MatIconRegistry,
private readonly _sanitizer: DomSanitizer
) {
const icons = [ const icons = [
'add', 'add',
'analyse', 'analyse',
@ -84,7 +87,11 @@ export class IconsModule {
]; ];
for (const icon of icons) { for (const icon of icons) {
_iconRegistry.addSvgIconInNamespace('red', icon, _sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/general/${icon}.svg`)); _iconRegistry.addSvgIconInNamespace(
'red',
icon,
_sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/general/${icon}.svg`)
);
} }
} }
} }

View File

@ -10,7 +10,13 @@
</redaction-circle-button> </redaction-circle-button>
<redaction-circle-button <redaction-circle-button
(action)="annotationActionsService.convertRecommendationToAnnotation($event, [annotation], annotationsChanged)" (action)="
annotationActionsService.convertRecommendationToAnnotation(
$event,
[annotation],
annotationsChanged
)
"
*ngIf="annotationPermissions.canAcceptRecommendation" *ngIf="annotationPermissions.canAcceptRecommendation"
icon="red:check" icon="red:check"
tooltip="annotation-actions.accept-recommendation.label" tooltip="annotation-actions.accept-recommendation.label"
@ -20,8 +26,13 @@
</redaction-circle-button> </redaction-circle-button>
<redaction-circle-button <redaction-circle-button
(action)="annotationActionsService.markAsFalsePositive($event, [annotation], annotationsChanged)" (action)="
*ngIf="annotationPermissions.canMarkTextOnlyAsFalsePositive && !annotationPermissions.canPerformMultipleRemoveActions" annotationActionsService.markAsFalsePositive($event, [annotation], annotationsChanged)
"
*ngIf="
annotationPermissions.canMarkTextOnlyAsFalsePositive &&
!annotationPermissions.canPerformMultipleRemoveActions
"
icon="red:thumb-down" icon="red:thumb-down"
tooltip="annotation-actions.remove-annotation.false-positive" tooltip="annotation-actions.remove-annotation.false-positive"
tooltipPosition="before" tooltipPosition="before"
@ -30,7 +41,9 @@
</redaction-circle-button> </redaction-circle-button>
<redaction-circle-button <redaction-circle-button
(action)="annotationActionsService.acceptSuggestion($event, [annotation], annotationsChanged)" (action)="
annotationActionsService.acceptSuggestion($event, [annotation], annotationsChanged)
"
*ngIf="annotationPermissions.canAcceptSuggestion" *ngIf="annotationPermissions.canAcceptSuggestion"
icon="red:check" icon="red:check"
tooltip="annotation-actions.accept-suggestion.label" tooltip="annotation-actions.accept-suggestion.label"
@ -40,7 +53,9 @@
</redaction-circle-button> </redaction-circle-button>
<redaction-circle-button <redaction-circle-button
(action)="annotationActionsService.undoDirectAction($event, [annotation], annotationsChanged)" (action)="
annotationActionsService.undoDirectAction($event, [annotation], annotationsChanged)
"
*ngIf="annotationPermissions.canUndo" *ngIf="annotationPermissions.canUndo"
icon="red:undo" icon="red:undo"
tooltip="annotation-actions.undo" tooltip="annotation-actions.undo"
@ -70,7 +85,9 @@
</redaction-circle-button> </redaction-circle-button>
<redaction-circle-button <redaction-circle-button
(action)="annotationActionsService.rejectSuggestion($event, [annotation], annotationsChanged)" (action)="
annotationActionsService.rejectSuggestion($event, [annotation], annotationsChanged)
"
*ngIf="annotationPermissions.canRejectSuggestion" *ngIf="annotationPermissions.canRejectSuggestion"
icon="red:close" icon="red:close"
tooltip="annotation-actions.reject-suggestion" tooltip="annotation-actions.reject-suggestion"

View File

@ -1,6 +1,9 @@
<redaction-circle-button <redaction-circle-button
(action)="suggestRemoveAnnotations($event, false)" (action)="suggestRemoveAnnotations($event, false)"
*ngIf="permissions.canRemoveOrSuggestToRemoveOnlyHere && permissions.canNotPerformMultipleRemoveActions" *ngIf="
permissions.canRemoveOrSuggestToRemoveOnlyHere &&
permissions.canNotPerformMultipleRemoveActions
"
[tooltipPosition]="tooltipPosition" [tooltipPosition]="tooltipPosition"
[type]="btnType" [type]="btnType"
icon="red:trash" icon="red:trash"
@ -21,16 +24,36 @@
</redaction-circle-button> </redaction-circle-button>
<mat-menu #menu="matMenu" (closed)="onMenuClosed()" xPosition="before"> <mat-menu #menu="matMenu" (closed)="onMenuClosed()" xPosition="before">
<div (click)="suggestRemoveAnnotations($event, true)" *ngIf="permissions.canRemoveOrSuggestToRemoveFromDictionary" mat-menu-item> <div
<redaction-annotation-icon [color]="dictionaryColor" [label]="'S'" [type]="'rhombus'"></redaction-annotation-icon> (click)="suggestRemoveAnnotations($event, true)"
*ngIf="permissions.canRemoveOrSuggestToRemoveFromDictionary"
mat-menu-item
>
<redaction-annotation-icon
[color]="dictionaryColor"
[label]="'S'"
[type]="'rhombus'"
></redaction-annotation-icon>
<div [translate]="'annotation-actions.remove-annotation.remove-from-dict'"></div> <div [translate]="'annotation-actions.remove-annotation.remove-from-dict'"></div>
</div> </div>
<div (click)="suggestRemoveAnnotations($event, false)" *ngIf="permissions.canRemoveOrSuggestToRemoveOnlyHere" mat-menu-item> <div
<redaction-annotation-icon [color]="suggestionColor" [label]="'S'" [type]="'rhombus'"></redaction-annotation-icon> (click)="suggestRemoveAnnotations($event, false)"
*ngIf="permissions.canRemoveOrSuggestToRemoveOnlyHere"
mat-menu-item
>
<redaction-annotation-icon
[color]="suggestionColor"
[label]="'S'"
[type]="'rhombus'"
></redaction-annotation-icon>
<div translate="annotation-actions.remove-annotation.only-here"></div> <div translate="annotation-actions.remove-annotation.only-here"></div>
</div> </div>
<div (click)="markAsFalsePositive($event)" *ngIf="permissions.canMarkAsFalsePositive" mat-menu-item> <div
(click)="markAsFalsePositive($event)"
*ngIf="permissions.canMarkAsFalsePositive"
mat-menu-item
>
<mat-icon class="false-positive-icon" svgIcon="red:thumb-down"></mat-icon> <mat-icon class="false-positive-icon" svgIcon="red:thumb-down"></mat-icon>
<div translate="annotation-actions.remove-annotation.false-positive"></div> <div translate="annotation-actions.remove-annotation.false-positive"></div>
</div> </div>

View File

@ -65,20 +65,44 @@ export class AnnotationRemoveActionsComponent {
suggestRemoveAnnotations($event, removeFromDict: boolean) { suggestRemoveAnnotations($event, removeFromDict: boolean) {
$event.stopPropagation(); $event.stopPropagation();
this._annotationActionsService.suggestRemoveAnnotation($event, this.annotations, removeFromDict, this.annotationsChanged); this._annotationActionsService.suggestRemoveAnnotation(
$event,
this.annotations,
removeFromDict,
this.annotationsChanged
);
} }
markAsFalsePositive($event) { markAsFalsePositive($event) {
this._annotationActionsService.markAsFalsePositive($event, this.annotations, this.annotationsChanged); this._annotationActionsService.markAsFalsePositive(
$event,
this.annotations,
this.annotationsChanged
);
} }
private _setPermissions() { private _setPermissions() {
this.permissions = { this.permissions = {
canRemoveOrSuggestToRemoveOnlyHere: this._annotationsPermissions(['canRemoveOrSuggestToRemoveOnlyHere'], true), canRemoveOrSuggestToRemoveOnlyHere: this._annotationsPermissions(
canPerformMultipleRemoveActions: this._annotationsPermissions(['canPerformMultipleRemoveActions'], true), ['canRemoveOrSuggestToRemoveOnlyHere'],
canNotPerformMultipleRemoveActions: this._annotationsPermissions(['canPerformMultipleRemoveActions'], false), true
canRemoveOrSuggestToRemoveFromDictionary: this._annotationsPermissions(['canRemoveOrSuggestToRemoveFromDictionary'], true), ),
canMarkAsFalsePositive: this._annotationsPermissions(['canMarkAsFalsePositive', 'canMarkTextOnlyAsFalsePositive'], true) canPerformMultipleRemoveActions: this._annotationsPermissions(
['canPerformMultipleRemoveActions'],
true
),
canNotPerformMultipleRemoveActions: this._annotationsPermissions(
['canPerformMultipleRemoveActions'],
false
),
canRemoveOrSuggestToRemoveFromDictionary: this._annotationsPermissions(
['canRemoveOrSuggestToRemoveFromDictionary'],
true
),
canMarkAsFalsePositive: this._annotationsPermissions(
['canMarkAsFalsePositive', 'canMarkTextOnlyAsFalsePositive'],
true
)
}; };
} }
@ -89,7 +113,10 @@ export class AnnotationRemoveActionsComponent {
this._permissionsService.currentUser, this._permissionsService.currentUser,
annotation annotation
); );
const hasAtLeastOnePermission = keys.reduce((acc, key) => acc || annotationPermissions[key] === expectedValue, false); const hasAtLeastOnePermission = keys.reduce(
(acc, key) => acc || annotationPermissions[key] === expectedValue,
false
);
return prevValue && hasAtLeastOnePermission; return prevValue && hasAtLeastOnePermission;
}, true); }, true);
} }

View File

@ -7,9 +7,21 @@
type="dark-bg" type="dark-bg"
></redaction-circle-button> ></redaction-circle-button>
<redaction-circle-button (action)="assign()" *ngIf="canAssign" icon="red:assign" [tooltip]="assignTooltip" type="dark-bg"></redaction-circle-button> <redaction-circle-button
(action)="assign()"
*ngIf="canAssign"
icon="red:assign"
[tooltip]="assignTooltip"
type="dark-bg"
></redaction-circle-button>
<redaction-circle-button (action)="assignToMe()" *ngIf="canAssignToSelf" icon="red:assign-me" tooltip="project-overview.assign-me" type="dark-bg"> <redaction-circle-button
(action)="assignToMe()"
*ngIf="canAssignToSelf"
icon="red:assign-me"
tooltip="project-overview.assign-me"
type="dark-bg"
>
</redaction-circle-button> </redaction-circle-button>
<redaction-circle-button <redaction-circle-button
@ -21,10 +33,19 @@
> >
</redaction-circle-button> </redaction-circle-button>
<redaction-circle-button (action)="setToUnderReview()" *ngIf="canSetToUnderReview" icon="red:undo" tooltip="project-overview.under-review" type="dark-bg"> <redaction-circle-button
(action)="setToUnderReview()"
*ngIf="canSetToUnderReview"
icon="red:undo"
tooltip="project-overview.under-review"
type="dark-bg"
>
</redaction-circle-button> </redaction-circle-button>
<redaction-file-download-btn [file]="selectedFiles" [project]="project"></redaction-file-download-btn> <redaction-file-download-btn
[file]="selectedFiles"
[project]="project"
></redaction-file-download-btn>
<!-- Approved--> <!-- Approved-->
<redaction-circle-button <redaction-circle-button
@ -38,10 +59,22 @@
</redaction-circle-button> </redaction-circle-button>
<!-- Back to approval --> <!-- Back to approval -->
<redaction-circle-button (action)="setToUnderApproval()" *ngIf="canUndoApproval" icon="red:undo" tooltip="project-overview.under-approval" type="dark-bg"> <redaction-circle-button
(action)="setToUnderApproval()"
*ngIf="canUndoApproval"
icon="red:undo"
tooltip="project-overview.under-approval"
type="dark-bg"
>
</redaction-circle-button> </redaction-circle-button>
<redaction-circle-button (action)="ocr()" *ngIf="canOcr" icon="red:ocr" tooltip="project-overview.ocr-file" type="dark-bg"></redaction-circle-button> <redaction-circle-button
(action)="ocr()"
*ngIf="canOcr"
icon="red:ocr"
tooltip="project-overview.ocr-file"
type="dark-bg"
></redaction-circle-button>
<redaction-circle-button <redaction-circle-button
(action)="reanalyse()" (action)="reanalyse()"

View File

@ -1,7 +1,10 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
import { AppStateService } from '@state/app-state.service'; import { AppStateService } from '@state/app-state.service';
import { UserService } from '@services/user.service'; import { UserService } from '@services/user.service';
import { FileManagementControllerService, ReanalysisControllerService } from '@redaction/red-ui-http'; import {
FileManagementControllerService,
ReanalysisControllerService
} from '@redaction/red-ui-http';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { FileStatusWrapper } from '@models/file/file-status.wrapper'; import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { FileActionService } from '../../services/file-action.service'; import { FileActionService } from '../../services/file-action.service';
@ -39,11 +42,19 @@ export class ProjectOverviewBulkActionsComponent {
} }
get selectedFiles(): FileStatusWrapper[] { get selectedFiles(): FileStatusWrapper[] {
return this.selectedFileIds.map((fileId) => this._appStateService.getFileById(this._appStateService.activeProject.project.projectId, fileId)); return this.selectedFileIds.map((fileId) =>
this._appStateService.getFileById(
this._appStateService.activeProject.project.projectId,
fileId
)
);
} }
get areAllFilesSelected() { get areAllFilesSelected() {
return this._appStateService.activeProject.files.length !== 0 && this.selectedFileIds.length === this._appStateService.activeProject.files.length; return (
this._appStateService.activeProject.files.length !== 0 &&
this.selectedFileIds.length === this._appStateService.activeProject.files.length
);
} }
get areSomeFilesSelected() { get areSomeFilesSelected() {
@ -53,8 +64,14 @@ export class ProjectOverviewBulkActionsComponent {
get allSelectedFilesCanBeAssignedIntoSameState() { get allSelectedFilesCanBeAssignedIntoSameState() {
if (this.areSomeFilesSelected) { if (this.areSomeFilesSelected) {
const selectedFiles = this.selectedFiles; const selectedFiles = this.selectedFiles;
const allFilesAreUnderReviewOrUnassigned = selectedFiles.reduce((acc, file) => acc && (file.isUnderReview || file.isUnassigned), true); const allFilesAreUnderReviewOrUnassigned = selectedFiles.reduce(
const allFilesAreUnderApproval = selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true); (acc, file) => acc && (file.isUnderReview || file.isUnassigned),
true
);
const allFilesAreUnderApproval = selectedFiles.reduce(
(acc, file) => acc && file.isUnderApproval,
true
);
return allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval; return allFilesAreUnderReviewOrUnassigned || allFilesAreUnderApproval;
} }
return false; return false;
@ -63,27 +80,42 @@ export class ProjectOverviewBulkActionsComponent {
get canAssignToSelf() { get canAssignToSelf() {
return ( return (
this.allSelectedFilesCanBeAssignedIntoSameState && this.allSelectedFilesCanBeAssignedIntoSameState &&
this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canAssignToSelf(file), true) this.selectedFiles.reduce(
(acc, file) => acc && this._permissionsService.canAssignToSelf(file),
true
)
); );
} }
get canAssign() { get canAssign() {
return ( return (
this.allSelectedFilesCanBeAssignedIntoSameState && this.allSelectedFilesCanBeAssignedIntoSameState &&
this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canAssignUser(file), true) this.selectedFiles.reduce(
(acc, file) => acc && this._permissionsService.canAssignUser(file),
true
)
); );
} }
get canDelete() { get canDelete() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canDeleteFile(file), true); return this.selectedFiles.reduce(
(acc, file) => acc && this._permissionsService.canDeleteFile(file),
true
);
} }
get canReanalyse() { get canReanalyse() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canReanalyseFile(file), true); return this.selectedFiles.reduce(
(acc, file) => acc && this._permissionsService.canReanalyseFile(file),
true
);
} }
get canOcr() { get canOcr() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canOcrFile(file), true); return this.selectedFiles.reduce(
(acc, file) => acc && this._permissionsService.canOcrFile(file),
true
);
} }
get fileStatuses() { get fileStatuses() {
@ -92,47 +124,74 @@ export class ProjectOverviewBulkActionsComponent {
// Under review // Under review
get canSetToUnderReview() { get canSetToUnderReview() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canSetUnderReview(file), true); return this.selectedFiles.reduce(
(acc, file) => acc && this._permissionsService.canSetUnderReview(file),
true
);
} }
// Under approval // Under approval
get canSetToUnderApproval() { get canSetToUnderApproval() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canSetUnderApproval(file), true); return this.selectedFiles.reduce(
(acc, file) => acc && this._permissionsService.canSetUnderApproval(file),
true
);
} }
// Approve // Approve
get isReadyForApproval() { get isReadyForApproval() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.isReadyForApproval(file), true); return this.selectedFiles.reduce(
(acc, file) => acc && this._permissionsService.isReadyForApproval(file),
true
);
} }
get canApprove() { get canApprove() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canApprove(file), true); return this.selectedFiles.reduce(
(acc, file) => acc && this._permissionsService.canApprove(file),
true
);
} }
// Undo approval // Undo approval
get canUndoApproval() { get canUndoApproval() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canUndoApproval(file), true); return this.selectedFiles.reduce(
(acc, file) => acc && this._permissionsService.canUndoApproval(file),
true
);
} }
get assignTooltip() { get assignTooltip() {
const allFilesAreUnderApproval = this.selectedFiles.reduce((acc, file) => acc && file.isUnderApproval, true); const allFilesAreUnderApproval = this.selectedFiles.reduce(
return allFilesAreUnderApproval ? 'project-overview.assign-approver' : 'project-overview.assign-reviewer'; (acc, file) => acc && file.isUnderApproval,
true
);
return allFilesAreUnderApproval
? 'project-overview.assign-approver'
: 'project-overview.assign-reviewer';
} }
delete() { delete() {
this.loading = true; this.loading = true;
this._dialogService.openDeleteFilesDialog(null, this._appStateService.activeProjectId, this.selectedFileIds, () => { this._dialogService.openDeleteFilesDialog(
this.reload.emit(); null,
this.loading = false; this._appStateService.activeProjectId,
this.selectedFileIds.splice(0, this.selectedFileIds.length); this.selectedFileIds,
}); () => {
this.reload.emit();
this.loading = false;
this.selectedFileIds.splice(0, this.selectedFileIds.length);
}
);
} }
setToUnderApproval() { setToUnderApproval() {
// If more than 1 approver - show dialog and ask who to assign // If more than 1 approver - show dialog and ask who to assign
if (this._appStateService.activeProject.approverIds.length > 1) { if (this._appStateService.activeProject.approverIds.length > 1) {
this.loading = true; this.loading = true;
const files = this.selectedFileIds.map((fileId) => this._appStateService.getFileById(this._appStateService.activeProjectId, fileId)); const files = this.selectedFileIds.map((fileId) =>
this._appStateService.getFileById(this._appStateService.activeProjectId, fileId)
);
this._dialogService.openAssignFileToUserDialog( this._dialogService.openAssignFileToUserDialog(
files, files,
@ -144,13 +203,25 @@ export class ProjectOverviewBulkActionsComponent {
true true
); );
} else { } else {
this._performBulkAction(this._fileActionService.setFileUnderApproval(this.selectedFiles, this._appStateService.activeProject.approverIds[0])); this._performBulkAction(
this._fileActionService.setFileUnderApproval(
this.selectedFiles,
this._appStateService.activeProject.approverIds[0]
)
);
} }
} }
async reanalyse() { async reanalyse() {
const fileIds = this.selectedFiles.filter((file) => this._permissionsService.fileRequiresReanalysis(file)).map((file) => file.fileId); const fileIds = this.selectedFiles
this._performBulkAction(this._reanalysisControllerService.reanalyzeFilesForProject(fileIds, this._appStateService.activeProject.projectId)); .filter((file) => this._permissionsService.fileRequiresReanalysis(file))
.map((file) => file.fileId);
this._performBulkAction(
this._reanalysisControllerService.reanalyzeFilesForProject(
fileIds,
this._appStateService.activeProject.projectId
)
);
} }
ocr() { ocr() {
@ -175,7 +246,9 @@ export class ProjectOverviewBulkActionsComponent {
assign() { assign() {
this.loading = true; this.loading = true;
const files = this.selectedFileIds.map((fileId) => this._appStateService.getFileById(this._appStateService.activeProjectId, fileId)); const files = this.selectedFileIds.map((fileId) =>
this._appStateService.getFileById(this._appStateService.activeProjectId, fileId)
);
const mode = files[0].isUnderApproval ? 'approver' : 'reviewer'; const mode = files[0].isUnderApproval ? 'approver' : 'reviewer';

View File

@ -1,10 +1,20 @@
<div class="wrapper"> <div class="wrapper">
<ng-container *ngIf="expanded"> <ng-container *ngIf="expanded">
<div *ngFor="let comment of annotation.comments; let idx = index" class="comment"> <div *ngFor="let comment of annotation.comments; let idx = index" class="comment">
<div [class.comment-owner]="isCommentOwner(comment)" [class.red]="isCommentOwner(comment)" class="comment-icon"> <div
<mat-icon [svgIcon]="isCommentOwner(comment) ? 'red:comment-fill' : 'red:comment'"></mat-icon> [class.comment-owner]="isCommentOwner(comment)"
[class.red]="isCommentOwner(comment)"
class="comment-icon"
>
<mat-icon
[svgIcon]="isCommentOwner(comment) ? 'red:comment-fill' : 'red:comment'"
></mat-icon>
</div> </div>
<div (click)="deleteComment(comment)" [class.comment-owner]="isCommentOwner(comment)" class="trash-icon red"> <div
(click)="deleteComment(comment)"
[class.comment-owner]="isCommentOwner(comment)"
class="trash-icon red"
>
<mat-icon svgIcon="red:trash"></mat-icon> <mat-icon svgIcon="red:trash"></mat-icon>
</div> </div>
@ -20,9 +30,14 @@
{{ {{
expanded expanded
? translateService.instant('comments.hide-comments') ? translateService.instant('comments.hide-comments')
: translateService.instant(annotation.comments.length === 1 ? 'comments.comment' : 'comments.comments', { : translateService.instant(
count: annotation.comments.length annotation.comments.length === 1
}) ? 'comments.comment'
: 'comments.comments',
{
count: annotation.comments.length
}
)
}} }}
</div> </div>
<div <div
@ -32,14 +47,33 @@
></div> ></div>
</div> </div>
<form (submit)="addComment()" *ngIf="addingComment && permissionsService.canAddComment()" [formGroup]="commentForm"> <form
(submit)="addComment()"
*ngIf="addingComment && permissionsService.canAddComment()"
[formGroup]="commentForm"
>
<div class="red-input-group"> <div class="red-input-group">
<input [placeholder]="translateService.instant('comments.add-comment')" class="w-full" formControlName="comment" name="comment" type="text" /> <input
[placeholder]="translateService.instant('comments.add-comment')"
class="w-full"
formControlName="comment"
name="comment"
type="text"
/>
</div> </div>
</form> </form>
<div *ngIf="addingComment" class="comment-actions-container"> <div *ngIf="addingComment" class="comment-actions-container">
<redaction-circle-button (action)="addComment()" [disabled]="!commentForm.value.comment" icon="red:check" type="primary"></redaction-circle-button> <redaction-circle-button
<div (click)="toggleAddingComment($event)" class="all-caps-label cancel" translate="comments.cancel"></div> (action)="addComment()"
[disabled]="!commentForm.value.comment"
icon="red:check"
type="primary"
></redaction-circle-button>
<div
(click)="toggleAddingComment($event)"
class="all-caps-label cancel"
translate="comments.cancel"
></div>
</div> </div>
</div> </div>

View File

@ -58,25 +58,29 @@ export class CommentsComponent {
addComment(): void { addComment(): void {
const value = this.commentForm.value.comment; const value = this.commentForm.value.comment;
if (value) { if (value) {
this._manualAnnotationService.addComment(value, this.annotation.id).subscribe((commentResponse) => { this._manualAnnotationService
this.annotation.comments.push({ .addComment(value, this.annotation.id)
text: value, .subscribe((commentResponse) => {
id: commentResponse.commentId, this.annotation.comments.push({
user: this._userService.userId text: value,
id: commentResponse.commentId,
user: this._userService.userId
});
}); });
});
this.commentForm.reset(); this.commentForm.reset();
this.toggleAddingComment(); this.toggleAddingComment();
} }
} }
deleteComment(comment: Comment): void { deleteComment(comment: Comment): void {
this._manualAnnotationService.deleteComment(comment.id, this.annotation.id).subscribe(() => { this._manualAnnotationService
this.annotation.comments.splice(this.annotation.comments.indexOf(comment), 1); .deleteComment(comment.id, this.annotation.id)
if (!this.annotation.comments.length) { .subscribe(() => {
this.expanded = false; this.annotation.comments.splice(this.annotation.comments.indexOf(comment), 1);
} if (!this.annotation.comments.length) {
}); this.expanded = false;
}
});
} }
isCommentOwner(comment: Comment): boolean { isCommentOwner(comment: Comment): boolean {

View File

@ -26,19 +26,31 @@
<div class="section small-label stats-subtitle"> <div class="section small-label stats-subtitle">
<div> <div>
<mat-icon svgIcon="red:folder"></mat-icon> <mat-icon svgIcon="red:folder"></mat-icon>
<span>{{ 'file-preview.tabs.document-info.details.project' | translate: { projectName: project.name } }}</span> <span>{{
'file-preview.tabs.document-info.details.project'
| translate: { projectName: project.name }
}}</span>
</div> </div>
<div> <div>
<mat-icon svgIcon="red:document"></mat-icon> <mat-icon svgIcon="red:document"></mat-icon>
<span>{{ 'file-preview.tabs.document-info.details.pages' | translate: { pages: file.numberOfPages } }}</span> <span>{{
'file-preview.tabs.document-info.details.pages'
| translate: { pages: file.numberOfPages }
}}</span>
</div> </div>
<div> <div>
<mat-icon svgIcon="red:calendar"></mat-icon> <mat-icon svgIcon="red:calendar"></mat-icon>
<span>{{ 'file-preview.tabs.document-info.details.created-on' | translate: { date: file.added | date: 'mediumDate' } }}</span> <span>{{
'file-preview.tabs.document-info.details.created-on'
| translate: { date: file.added | date: 'mediumDate' }
}}</span>
</div> </div>
<div *ngIf="project.project.dueDate"> <div *ngIf="project.project.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon> <mat-icon svgIcon="red:lightning"></mat-icon>
<span>{{ 'file-preview.tabs.document-info.details.due' | translate: { date: project.project.dueDate | date: 'mediumDate' } }}</span> <span>{{
'file-preview.tabs.document-info.details.due'
| translate: { date: project.project.dueDate | date: 'mediumDate' }
}}</span>
</div> </div>
<div> <div>
<mat-icon svgIcon="red:template"></mat-icon> <mat-icon svgIcon="red:template"></mat-icon>

View File

@ -14,7 +14,10 @@ export class DocumentInfoComponent {
fileAttributesConfig: FileAttributesConfig; fileAttributesConfig: FileAttributesConfig;
constructor(private readonly _appStateService: AppStateService, private readonly _dialogService: ProjectsDialogService) { constructor(
private readonly _appStateService: AppStateService,
private readonly _dialogService: ProjectsDialogService
) {
this.fileAttributesConfig = this._appStateService.activeFileAttributesConfig; this.fileAttributesConfig = this._appStateService.activeFileAttributesConfig;
} }

View File

@ -94,7 +94,11 @@
*ngIf="permissionsService.isReadyForApproval(fileStatus)" *ngIf="permissionsService.isReadyForApproval(fileStatus)"
[disabled]="!permissionsService.canApprove(fileStatus)" [disabled]="!permissionsService.canApprove(fileStatus)"
[tooltipPosition]="tooltipPosition" [tooltipPosition]="tooltipPosition"
[tooltip]="permissionsService.canApprove(fileStatus) ? 'project-overview.approve' : 'project-overview.approve-disabled'" [tooltip]="
permissionsService.canApprove(fileStatus)
? 'project-overview.approve'
: 'project-overview.approve-disabled'
"
[type]="buttonType" [type]="buttonType"
icon="red:approved" icon="red:approved"
> >

View File

@ -38,7 +38,9 @@ export class FileActionsComponent implements OnInit {
return 'file-preview.toggle-analysis.only-managers'; return 'file-preview.toggle-analysis.only-managers';
} }
return this.fileStatus?.isExcluded ? 'file-preview.toggle-analysis.enable' : 'file-preview.toggle-analysis.disable'; return this.fileStatus?.isExcluded
? 'file-preview.toggle-analysis.enable'
: 'file-preview.toggle-analysis.disable';
} }
get canAssignToSelf() { get canAssignToSelf() {
@ -86,7 +88,9 @@ export class FileActionsComponent implements OnInit {
} }
get assignTooltip() { get assignTooltip() {
return this.fileStatus.isUnderApproval ? 'project-overview.assign-approver' : 'project-overview.assign-reviewer'; return this.fileStatus.isUnderApproval
? 'project-overview.assign-approver'
: 'project-overview.assign-reviewer';
} }
ngOnInit(): void { ngOnInit(): void {
@ -108,9 +112,14 @@ export class FileActionsComponent implements OnInit {
} }
openDeleteFileDialog($event: MouseEvent) { openDeleteFileDialog($event: MouseEvent) {
this._dialogService.openDeleteFilesDialog($event, this.fileStatus.projectId, [this.fileStatus.fileId], () => { this._dialogService.openDeleteFilesDialog(
this.actionPerformed.emit('delete'); $event,
}); this.fileStatus.projectId,
[this.fileStatus.fileId],
() => {
this.actionPerformed.emit('delete');
}
);
} }
assign($event: MouseEvent) { assign($event: MouseEvent) {
@ -137,7 +146,11 @@ export class FileActionsComponent implements OnInit {
setFileUnderApproval($event: MouseEvent) { setFileUnderApproval($event: MouseEvent) {
$event.stopPropagation(); $event.stopPropagation();
if (this.appStateService.activeProject.approverIds.length > 1) { if (this.appStateService.activeProject.approverIds.length > 1) {
this._fileActionService.assignProjectApprover(this.fileStatus, () => this.actionPerformed.emit('assign-reviewer'), true); this._fileActionService.assignProjectApprover(
this.fileStatus,
() => this.actionPerformed.emit('assign-reviewer'),
true
);
} else { } else {
this._fileActionService.setFileUnderApproval(this.fileStatus).subscribe(() => { this._fileActionService.setFileUnderApproval(this.fileStatus).subscribe(() => {
this.reloadProjects('set-under-approval'); this.reloadProjects('set-under-approval');
@ -161,7 +174,11 @@ export class FileActionsComponent implements OnInit {
setFileUnderReview($event: MouseEvent, ignoreDialogChanges = false) { setFileUnderReview($event: MouseEvent, ignoreDialogChanges = false) {
$event.stopPropagation(); $event.stopPropagation();
this._fileActionService.assignProjectReviewer(this.fileStatus, () => this.actionPerformed.emit('assign-reviewer'), ignoreDialogChanges); this._fileActionService.assignProjectReviewer(
this.fileStatus,
() => this.actionPerformed.emit('assign-reviewer'),
ignoreDialogChanges
);
} }
reloadProjects(action: string) { reloadProjects(action: string) {
@ -174,6 +191,8 @@ export class FileActionsComponent implements OnInit {
$event.stopPropagation(); $event.stopPropagation();
await this._fileActionService.toggleAnalysis(this.fileStatus).toPromise(); await this._fileActionService.toggleAnalysis(this.fileStatus).toPromise();
await this.appStateService.getFiles(); await this.appStateService.getFiles();
this.actionPerformed.emit(this.fileStatus?.isExcluded ? 'enable-analysis' : 'disable-analysis'); this.actionPerformed.emit(
this.fileStatus?.isExcluded ? 'enable-analysis' : 'disable-analysis'
);
} }
} }

View File

@ -33,7 +33,11 @@
tooltipPosition="above" tooltipPosition="above"
></redaction-annotation-remove-actions> ></redaction-annotation-remove-actions>
</div> </div>
<redaction-circle-button (action)="multiSelectActive = false" icon="red:close" type="primary"></redaction-circle-button> <redaction-circle-button
(action)="multiSelectActive = false"
icon="red:close"
type="primary"
></redaction-circle-button>
</div> </div>
<div [class.multi-select-active]="multiSelectActive" class="annotations-wrapper"> <div [class.multi-select-active]="multiSelectActive" class="annotations-wrapper">
<div <div
@ -78,13 +82,24 @@
<div style="overflow: hidden; width: 100%"> <div style="overflow: hidden; width: 100%">
<div attr.anotation-page-header="{{ activeViewerPage }}" class="page-separator"> <div attr.anotation-page-header="{{ activeViewerPage }}" class="page-separator">
<span *ngIf="!!activeViewerPage" class="all-caps-label" <span *ngIf="!!activeViewerPage" class="all-caps-label"
><span translate="page"></span> {{ activeViewerPage }} - {{ displayedAnnotations[activeViewerPage]?.annotations?.length || 0 }} ><span translate="page"></span> {{ activeViewerPage }} -
<span [translate]="displayedAnnotations[activeViewerPage]?.annotations?.length === 1 ? 'annotation' : 'annotations'"></span {{ displayedAnnotations[activeViewerPage]?.annotations?.length || 0 }}
<span
[translate]="
displayedAnnotations[activeViewerPage]?.annotations?.length === 1
? 'annotation'
: 'annotations'
"
></span
></span> ></span>
<div *ngIf="multiSelectActive"> <div *ngIf="multiSelectActive">
<div (click)="selectAllOnActivePage()" class="all-caps-label primary pointer">All</div> <div (click)="selectAllOnActivePage()" class="all-caps-label primary pointer">
<div (click)="deselectAllOnActivePage()" class="all-caps-label primary pointer">None</div> All
</div>
<div (click)="deselectAllOnActivePage()" class="all-caps-label primary pointer">
None
</div>
</div> </div>
</div> </div>
@ -98,7 +113,12 @@
tabindex="1" tabindex="1"
> >
<ng-container *ngIf="!displayedAnnotations[activeViewerPage]"> <ng-container *ngIf="!displayedAnnotations[activeViewerPage]">
<redaction-empty-state [horizontalPadding]="24" [verticalPadding]="40" icon="red:document" screen="file-preview"></redaction-empty-state> <redaction-empty-state
[horizontalPadding]="24"
[verticalPadding]="40"
icon="red:document"
screen="file-preview"
></redaction-empty-state>
<div class="no-annotations-buttons-container mt-32"> <div class="no-annotations-buttons-container mt-32">
<redaction-icon-button <redaction-icon-button
(action)="jumpToPreviousWithAnnotations()" (action)="jumpToPreviousWithAnnotations()"
@ -109,7 +129,9 @@
></redaction-icon-button> ></redaction-icon-button>
<redaction-icon-button <redaction-icon-button
(action)="jumpToNextWithAnnotations()" (action)="jumpToNextWithAnnotations()"
[disabled]="activeViewerPage >= displayedPages[displayedPages.length - 1]" [disabled]="
activeViewerPage >= displayedPages[displayedPages.length - 1]
"
class="mt-8" class="mt-8"
icon="red:nav-next" icon="red:nav-next"
text="file-preview.tabs.annotations.jump-to-next" text="file-preview.tabs.annotations.jump-to-next"
@ -129,21 +151,32 @@
> >
<div class="active-bar-marker"></div> <div class="active-bar-marker"></div>
<div [class.removed]="annotation.isChangeLogRemoved" class="annotation"> <div [class.removed]="annotation.isChangeLogRemoved" class="annotation">
<redaction-hidden-action (action)="logAnnotation(annotation)" [requiredClicks]="2"> <redaction-hidden-action
(action)="logAnnotation(annotation)"
[requiredClicks]="2"
>
<div class="details"> <div class="details">
<redaction-type-annotation-icon [annotation]="annotation"></redaction-type-annotation-icon> <redaction-type-annotation-icon
[annotation]="annotation"
></redaction-type-annotation-icon>
<div class="flex-1"> <div class="flex-1">
<div> <div>
<strong>{{ annotation.typeLabel | translate }}</strong> <strong>{{ annotation.typeLabel | translate }}</strong>
</div> </div>
<div *ngIf="annotation.dictionary && annotation.dictionary !== 'manual'"> <div
*ngIf="
annotation.dictionary &&
annotation.dictionary !== 'manual'
"
>
<strong <strong
><span>{{ annotation.descriptor | translate }}</span ><span>{{ annotation.descriptor | translate }}</span
>: </strong >: </strong
>{{ annotation.dictionary | humanize: false }} >{{ annotation.dictionary | humanize: false }}
</div> </div>
<div *ngIf="annotation.content && !annotation.isHint"> <div *ngIf="annotation.content && !annotation.isHint">
<strong><span translate="content"></span>: </strong>{{ annotation.content }} <strong><span translate="content"></span>: </strong
>{{ annotation.content }}
</div> </div>
{{ annotation.id }} {{ annotation.id }}
</div> </div>
@ -155,7 +188,9 @@
</ng-container> </ng-container>
<div class="active-icon-marker-container"> <div class="active-icon-marker-container">
<redaction-round-checkbox <redaction-round-checkbox
*ngIf="multiSelectActive && annotationIsSelected(annotation)" *ngIf="
multiSelectActive && annotationIsSelected(annotation)
"
[active]="true" [active]="true"
></redaction-round-checkbox> ></redaction-round-checkbox>
</div> </div>
@ -170,16 +205,25 @@
</div> </div>
<ng-template #annotationFilterTemplate let-filter="filter"> <ng-template #annotationFilterTemplate let-filter="filter">
<redaction-type-filter *ngIf="_(filter).topLevelFilter" [filter]="filter"></redaction-type-filter> <redaction-type-filter
*ngIf="_(filter).topLevelFilter"
[filter]="filter"
></redaction-type-filter>
<ng-container *ngIf="!_(filter).topLevelFilter"> <ng-container *ngIf="!_(filter).topLevelFilter">
<redaction-dictionary-annotation-icon [dictionaryKey]="filter.key"></redaction-dictionary-annotation-icon> <redaction-dictionary-annotation-icon
[dictionaryKey]="filter.key"
></redaction-dictionary-annotation-icon>
{{ filter.key | humanize: false }} {{ filter.key | humanize: false }}
</ng-container> </ng-container>
</ng-template> </ng-template>
<ng-template #annotationFilterActionTemplate let-filter="filter"> <ng-template #annotationFilterActionTemplate let-filter="filter">
<ng-container *ngIf="filter.key === 'skipped'"> <ng-container *ngIf="filter.key === 'skipped'">
<redaction-circle-button (action)="toggleSkipped.emit($event)" [icon]="hideSkipped ? 'red:visibility-off' : 'red:visibility'" type="dark-bg"> <redaction-circle-button
(action)="toggleSkipped.emit($event)"
[icon]="hideSkipped ? 'red:visibility-off' : 'red:visibility'"
type="dark-bg"
>
</redaction-circle-button> </redaction-circle-button>
</ng-container> </ng-container>
</ng-template> </ng-template>

View File

@ -1,4 +1,14 @@
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, Output, TemplateRef, ViewChild } from '@angular/core'; import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
HostListener,
Input,
Output,
TemplateRef,
ViewChild
} from '@angular/core';
import { FilterModel } from '@shared/components/filter/model/filter.model'; import { FilterModel } from '@shared/components/filter/model/filter.model';
import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { AnnotationProcessingService } from '../../services/annotation-processing.service'; import { AnnotationProcessingService } from '../../services/annotation-processing.service';
@ -27,7 +37,9 @@ export class FileWorkloadComponent {
@Input() hideSkipped: boolean; @Input() hideSkipped: boolean;
@Input() annotationActionsTemplate: TemplateRef<any>; @Input() annotationActionsTemplate: TemplateRef<any>;
@Output() shouldDeselectAnnotationsOnPageChangeChange = new EventEmitter<boolean>(); @Output() shouldDeselectAnnotationsOnPageChangeChange = new EventEmitter<boolean>();
@Output() selectAnnotations = new EventEmitter<AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean }>(); @Output() selectAnnotations = new EventEmitter<
AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean }
>();
@Output() deselectAnnotations = new EventEmitter<AnnotationWrapper[]>(); @Output() deselectAnnotations = new EventEmitter<AnnotationWrapper[]>();
@Output() selectPage = new EventEmitter<number>(); @Output() selectPage = new EventEmitter<number>();
@Output() toggleSkipped = new EventEmitter<any>(); @Output() toggleSkipped = new EventEmitter<any>();
@ -37,7 +49,10 @@ export class FileWorkloadComponent {
@ViewChild('annotationsElement') private _annotationsElement: ElementRef; @ViewChild('annotationsElement') private _annotationsElement: ElementRef;
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef; @ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
constructor(private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _annotationProcessingService: AnnotationProcessingService) {} constructor(
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _annotationProcessingService: AnnotationProcessingService
) {}
private _annotations: AnnotationWrapper[]; private _annotations: AnnotationWrapper[];
@ -66,7 +81,10 @@ export class FileWorkloadComponent {
return this.selectedAnnotations?.length ? this.selectedAnnotations[0] : null; return this.selectedAnnotations?.length ? this.selectedAnnotations[0] : null;
} }
private static _scrollToFirstElement(elements: HTMLElement[], mode: 'always' | 'if-needed' = 'if-needed') { private static _scrollToFirstElement(
elements: HTMLElement[],
mode: 'always' | 'if-needed' = 'if-needed'
) {
if (elements.length > 0) { if (elements.length > 0) {
scrollIntoView(elements[0], { scrollIntoView(elements[0], {
behavior: 'smooth', behavior: 'smooth',
@ -86,7 +104,9 @@ export class FileWorkloadComponent {
} }
pageHasSelection(page: number) { pageHasSelection(page: number) {
return this.multiSelectActive && !!this.selectedAnnotations?.find((a) => a.pageNumber === page); return (
this.multiSelectActive && !!this.selectedAnnotations?.find((a) => a.pageNumber === page)
);
} }
selectAllOnActivePage() { selectAllOnActivePage() {
@ -101,7 +121,11 @@ export class FileWorkloadComponent {
@debounce(0) @debounce(0)
filtersChanged(filters: { primary: FilterModel[]; secondary?: FilterModel[] }) { filtersChanged(filters: { primary: FilterModel[]; secondary?: FilterModel[] }) {
this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this._annotations, filters.primary, filters.secondary); this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(
this._annotations,
filters.primary,
filters.secondary
);
this.displayedPages = Object.keys(this.displayedAnnotations).map((key) => Number(key)); this.displayedPages = Object.keys(this.displayedAnnotations).map((key) => Number(key));
this._changeDetectorRef.markForCheck(); this._changeDetectorRef.markForCheck();
} }
@ -114,13 +138,20 @@ export class FileWorkloadComponent {
if (($event.ctrlKey || $event.metaKey) && this.selectedAnnotations.length > 0) { if (($event.ctrlKey || $event.metaKey) && this.selectedAnnotations.length > 0) {
this.multiSelectActive = true; this.multiSelectActive = true;
} }
this.selectAnnotations.emit({ annotations: [annotation], multiSelect: this.multiSelectActive }); this.selectAnnotations.emit({
annotations: [annotation],
multiSelect: this.multiSelectActive
});
} }
} }
@HostListener('window:keyup', ['$event']) @HostListener('window:keyup', ['$event'])
handleKeyEvent($event: KeyboardEvent) { handleKeyEvent($event: KeyboardEvent) {
if (!ALL_HOTKEY_ARRAY.includes($event.key) || this.dialogRef?.getState() === MatDialogState.OPEN || ($event.target as any).localName === 'input') { if (
!ALL_HOTKEY_ARRAY.includes($event.key) ||
this.dialogRef?.getState() === MatDialogState.OPEN ||
($event.target as any).localName === 'input'
) {
return; return;
} }
@ -131,7 +162,8 @@ export class FileWorkloadComponent {
if ($event.key === 'ArrowRight') { if ($event.key === 'ArrowRight') {
this.pagesPanelActive = false; this.pagesPanelActive = false;
// if we activated annotationsPanel - select first annotation from this page in case there is no // if we activated annotationsPanel -
// select first annotation from this page in case there is no
// selected annotation on this page and not in multi select mode // selected annotation on this page and not in multi select mode
if (!this.pagesPanelActive && !this.multiSelectActive) { if (!this.pagesPanelActive && !this.multiSelectActive) {
this._selectFirstAnnotationOnCurrentPageIfNecessary(); this._selectFirstAnnotationOnCurrentPageIfNecessary();
@ -140,7 +172,8 @@ export class FileWorkloadComponent {
} }
if (!this.pagesPanelActive) { if (!this.pagesPanelActive) {
// Disable annotation navigation in multi select mode => TODO: maybe implement selection on enter? // Disable annotation navigation in multi select mode
// => TODO: maybe implement selection on enter?
if (!this.multiSelectActive) { if (!this.multiSelectActive) {
this._navigateAnnotations($event); this._navigateAnnotations($event);
} }
@ -159,7 +192,9 @@ export class FileWorkloadComponent {
} }
scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed') { scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed') {
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`); const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(
`div[anotation-page-header="${page}"]`
);
FileWorkloadComponent._scrollToFirstElement(elements, mode); FileWorkloadComponent._scrollToFirstElement(elements, mode);
} }
@ -168,13 +203,18 @@ export class FileWorkloadComponent {
if (!this.selectedAnnotations || this.selectedAnnotations.length === 0) { if (!this.selectedAnnotations || this.selectedAnnotations.length === 0) {
return; return;
} }
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[annotation-id="${this._firstSelectedAnnotation?.id}"].active`); const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(
`div[annotation-id="${this._firstSelectedAnnotation?.id}"].active`
);
FileWorkloadComponent._scrollToFirstElement(elements); FileWorkloadComponent._scrollToFirstElement(elements);
} }
scrollQuickNavigation() { scrollQuickNavigation() {
let quickNavPageIndex = this.displayedPages.findIndex((p) => p >= this.activeViewerPage); let quickNavPageIndex = this.displayedPages.findIndex((p) => p >= this.activeViewerPage);
if (quickNavPageIndex === -1 || this.displayedPages[quickNavPageIndex] !== this.activeViewerPage) { if (
quickNavPageIndex === -1 ||
this.displayedPages[quickNavPageIndex] !== this.activeViewerPage
) {
quickNavPageIndex = Math.max(0, quickNavPageIndex - 1); quickNavPageIndex = Math.max(0, quickNavPageIndex - 1);
} }
this._scrollQuickNavigationToPage(this.displayedPages[quickNavPageIndex]); this._scrollQuickNavigationToPage(this.displayedPages[quickNavPageIndex]);
@ -194,7 +234,10 @@ export class FileWorkloadComponent {
} }
preventKeyDefault($event: KeyboardEvent) { preventKeyDefault($event: KeyboardEvent) {
if (COMMAND_KEY_ARRAY.includes($event.key) && !(($event.target as any).localName === 'input')) { if (
COMMAND_KEY_ARRAY.includes($event.key) &&
!(($event.target as any).localName === 'input')
) {
$event.preventDefault(); $event.preventDefault();
} }
} }
@ -213,39 +256,53 @@ export class FileWorkloadComponent {
private _selectFirstAnnotationOnCurrentPageIfNecessary() { private _selectFirstAnnotationOnCurrentPageIfNecessary() {
if ( if (
(!this._firstSelectedAnnotation || this.activeViewerPage !== this._firstSelectedAnnotation.pageNumber) && (!this._firstSelectedAnnotation ||
this.activeViewerPage !== this._firstSelectedAnnotation.pageNumber) &&
this.displayedPages.indexOf(this.activeViewerPage) >= 0 this.displayedPages.indexOf(this.activeViewerPage) >= 0
) { ) {
this.selectAnnotations.emit([this.displayedAnnotations[this.activeViewerPage].annotations[0]]); this.selectAnnotations.emit([
this.displayedAnnotations[this.activeViewerPage].annotations[0]
]);
} }
} }
private _navigateAnnotations($event: KeyboardEvent) { private _navigateAnnotations($event: KeyboardEvent) {
if (!this._firstSelectedAnnotation || this.activeViewerPage !== this._firstSelectedAnnotation.pageNumber) { if (
!this._firstSelectedAnnotation ||
this.activeViewerPage !== this._firstSelectedAnnotation.pageNumber
) {
const pageIdx = this.displayedPages.indexOf(this.activeViewerPage); const pageIdx = this.displayedPages.indexOf(this.activeViewerPage);
if (pageIdx !== -1) { if (pageIdx !== -1) {
// Displayed page has annotations // Displayed page has annotations
this.selectAnnotations.emit([this.displayedAnnotations[this.activeViewerPage].annotations[0]]); this.selectAnnotations.emit([
this.displayedAnnotations[this.activeViewerPage].annotations[0]
]);
} else { } else {
// Displayed page doesn't have annotations // Displayed page doesn't have annotations
if ($event.key === 'ArrowDown') { if ($event.key === 'ArrowDown') {
const nextPage = this._nextPageWithAnnotations(); const nextPage = this._nextPageWithAnnotations();
this.shouldDeselectAnnotationsOnPageChange = false; this.shouldDeselectAnnotationsOnPageChange = false;
this.shouldDeselectAnnotationsOnPageChangeChange.emit(false); this.shouldDeselectAnnotationsOnPageChangeChange.emit(false);
this.selectAnnotations.emit([this.displayedAnnotations[nextPage].annotations[0]]); this.selectAnnotations.emit([
this.displayedAnnotations[nextPage].annotations[0]
]);
} else { } else {
const prevPage = this._prevPageWithAnnotations(); const prevPage = this._prevPageWithAnnotations();
this.shouldDeselectAnnotationsOnPageChange = false; this.shouldDeselectAnnotationsOnPageChange = false;
this.shouldDeselectAnnotationsOnPageChangeChange.emit(false); this.shouldDeselectAnnotationsOnPageChangeChange.emit(false);
const prevPageAnnotations = this.displayedAnnotations[prevPage].annotations; const prevPageAnnotations = this.displayedAnnotations[prevPage].annotations;
this.selectAnnotations.emit([prevPageAnnotations[prevPageAnnotations.length - 1]]); this.selectAnnotations.emit([
prevPageAnnotations[prevPageAnnotations.length - 1]
]);
} }
} }
} else { } else {
const page = this._firstSelectedAnnotation.pageNumber; const page = this._firstSelectedAnnotation.pageNumber;
const pageIdx = this.displayedPages.indexOf(page); const pageIdx = this.displayedPages.indexOf(page);
const annotationsOnPage = this.displayedAnnotations[page].annotations; const annotationsOnPage = this.displayedAnnotations[page].annotations;
const idx = annotationsOnPage.findIndex((a) => a.id === this._firstSelectedAnnotation.id); const idx = annotationsOnPage.findIndex(
(a) => a.id === this._firstSelectedAnnotation.id
);
if ($event.key === 'ArrowDown') { if ($event.key === 'ArrowDown') {
if (idx + 1 !== annotationsOnPage.length) { if (idx + 1 !== annotationsOnPage.length) {
@ -253,7 +310,8 @@ export class FileWorkloadComponent {
this.selectAnnotations.emit([annotationsOnPage[idx + 1]]); this.selectAnnotations.emit([annotationsOnPage[idx + 1]]);
} else if (pageIdx + 1 < this.displayedPages.length) { } else if (pageIdx + 1 < this.displayedPages.length) {
// If not last page // If not last page
const nextPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx + 1]].annotations; const nextPageAnnotations =
this.displayedAnnotations[this.displayedPages[pageIdx + 1]].annotations;
this.shouldDeselectAnnotationsOnPageChange = false; this.shouldDeselectAnnotationsOnPageChange = false;
this.shouldDeselectAnnotationsOnPageChangeChange.emit(false); this.shouldDeselectAnnotationsOnPageChangeChange.emit(false);
this.selectAnnotations.emit([nextPageAnnotations[0]]); this.selectAnnotations.emit([nextPageAnnotations[0]]);
@ -264,10 +322,13 @@ export class FileWorkloadComponent {
this.selectAnnotations.emit([annotationsOnPage[idx - 1]]); this.selectAnnotations.emit([annotationsOnPage[idx - 1]]);
} else if (pageIdx) { } else if (pageIdx) {
// If not first page // If not first page
const prevPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx - 1]].annotations; const prevPageAnnotations =
this.displayedAnnotations[this.displayedPages[pageIdx - 1]].annotations;
this.shouldDeselectAnnotationsOnPageChange = false; this.shouldDeselectAnnotationsOnPageChange = false;
this.shouldDeselectAnnotationsOnPageChangeChange.emit(false); this.shouldDeselectAnnotationsOnPageChangeChange.emit(false);
this.selectAnnotations.emit([prevPageAnnotations[prevPageAnnotations.length - 1]]); this.selectAnnotations.emit([
prevPageAnnotations[prevPageAnnotations.length - 1]
]);
} }
} }
} }
@ -329,7 +390,9 @@ export class FileWorkloadComponent {
} }
private _scrollQuickNavigationToPage(page: number) { private _scrollQuickNavigationToPage(page: number) {
const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`); const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(
`#quick-nav-page-${page}`
);
FileWorkloadComponent._scrollToFirstElement(elements); FileWorkloadComponent._scrollToFirstElement(elements);
} }
} }

View File

@ -1,9 +1,39 @@
<div class="needs-work"> <div class="needs-work">
<redaction-annotation-icon *ngIf="reanalysisRequired()" [color]="analysisColor" label="A" type="square"></redaction-annotation-icon> <redaction-annotation-icon
<redaction-annotation-icon *ngIf="hasUpdates" [color]="updatedColor" label="U" type="square"></redaction-annotation-icon> *ngIf="reanalysisRequired()"
<redaction-annotation-icon *ngIf="needsWorkInput.hasRedactions" [color]="redactionColor" label="R" type="square"></redaction-annotation-icon> [color]="analysisColor"
<redaction-annotation-icon *ngIf="hasImages" [color]="imageColor" label="I" type="square"></redaction-annotation-icon> label="A"
<redaction-annotation-icon *ngIf="needsWorkInput.hintsOnly" [color]="hintColor" label="H" type="circle"></redaction-annotation-icon> type="square"
<redaction-annotation-icon *ngIf="needsWorkInput.hasRequests" [color]="suggestionColor" label="S" type="rhombus"></redaction-annotation-icon> ></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="hasUpdates"
[color]="updatedColor"
label="U"
type="square"
></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="needsWorkInput.hasRedactions"
[color]="redactionColor"
label="R"
type="square"
></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="hasImages"
[color]="imageColor"
label="I"
type="square"
></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="needsWorkInput.hintsOnly"
[color]="hintColor"
label="H"
type="circle"
></redaction-annotation-icon>
<redaction-annotation-icon
*ngIf="needsWorkInput.hasRequests"
[color]="suggestionColor"
label="S"
type="rhombus"
></redaction-annotation-icon>
<mat-icon *ngIf="hasAnnotationComments" svgIcon="red:comment"></mat-icon> <mat-icon *ngIf="hasAnnotationComments" svgIcon="red:comment"></mat-icon>
</div> </div>

View File

@ -1,4 +1,4 @@
@import '/apps/red-ui/src/assets/styles/red-variables'; @import 'apps/red-ui/src/assets/styles/red-variables';
.needs-work { .needs-work {
display: flex; display: flex;

View File

@ -12,7 +12,10 @@ import { ProjectWrapper } from '@state/model/project.wrapper';
export class NeedsWorkBadgeComponent { export class NeedsWorkBadgeComponent {
@Input() needsWorkInput: FileStatusWrapper | ProjectWrapper; @Input() needsWorkInput: FileStatusWrapper | ProjectWrapper;
constructor(private readonly _appStateService: AppStateService, private readonly _permissionsService: PermissionsService) {} constructor(
private readonly _appStateService: AppStateService,
private readonly _permissionsService: PermissionsService
) {}
get suggestionColor() { get suggestionColor() {
return this._getDictionaryColor('suggestion'); return this._getDictionaryColor('suggestion');
@ -47,7 +50,10 @@ export class NeedsWorkBadgeComponent {
} }
get hasAnnotationComments(): boolean { get hasAnnotationComments(): boolean {
return this.needsWorkInput instanceof FileStatusWrapper && (<any>this.needsWorkInput).hasAnnotationComments; return (
this.needsWorkInput instanceof FileStatusWrapper &&
(<any>this.needsWorkInput).hasAnnotationComments
);
} }
reanalysisRequired() { reanalysisRequired() {

View File

@ -1,4 +1,13 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import {
Component,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges
} from '@angular/core';
import { ViewedPages, ViewedPagesControllerService } from '@redaction/red-ui-http'; import { ViewedPages, ViewedPagesControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service'; import { AppStateService } from '@state/app-state.service';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
@ -95,15 +104,25 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy {
private _markPageRead() { private _markPageRead() {
this._viewedPagesControllerService this._viewedPagesControllerService
.addPage({ page: this.number }, this._appStateService.activeProjectId, this._appStateService.activeFileId) .addPage(
{ page: this.number },
this._appStateService.activeProjectId,
this._appStateService.activeFileId
)
.subscribe(() => { .subscribe(() => {
this.viewedPages?.pages?.push(this.number); this.viewedPages?.pages?.push(this.number);
}); });
} }
private _markPageUnread() { private _markPageUnread() {
this._viewedPagesControllerService.removePage(this._appStateService.activeProjectId, this._appStateService.activeFileId, this.number).subscribe(() => { this._viewedPagesControllerService
this.viewedPages?.pages?.splice(this.viewedPages?.pages?.indexOf(this.number), 1); .removePage(
}); this._appStateService.activeProjectId,
this._appStateService.activeFileId,
this.number
)
.subscribe(() => {
this.viewedPages?.pages?.splice(this.viewedPages?.pages?.indexOf(this.number), 1);
});
} }
} }

View File

@ -1,4 +1,17 @@
import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, NgZone, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; import {
AfterViewInit,
Component,
ElementRef,
EventEmitter,
Inject,
Input,
NgZone,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild
} from '@angular/core';
import { ManualRedactionEntry, Rectangle } from '@redaction/red-ui-http'; import { ManualRedactionEntry, Rectangle } from '@redaction/red-ui-http';
import WebViewer, { Annotations, Tools, WebViewerInstance } from '@pdftron/webviewer'; import WebViewer, { Annotations, Tools, WebViewerInstance } from '@pdftron/webviewer';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -72,7 +85,9 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
this.instance.annotManager.deselectAllAnnotations(); this.instance.annotManager.deselectAllAnnotations();
} }
selectAnnotations($event: AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean }) { selectAnnotations(
$event: AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean }
) {
let annotations: AnnotationWrapper[]; let annotations: AnnotationWrapper[];
let multiSelect: boolean; let multiSelect: boolean;
if ($event instanceof Array) { if ($event instanceof Array) {
@ -87,14 +102,18 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
this.deselectAllAnnotations(); this.deselectAllAnnotations();
} }
const annotationsFromViewer = annotations.map((ann) => this.instance.annotManager.getAnnotationById(ann.id)); const annotationsFromViewer = annotations.map((ann) =>
this.instance.annotManager.getAnnotationById(ann.id)
);
this.instance.annotManager.selectAnnotations(annotationsFromViewer); this.instance.annotManager.selectAnnotations(annotationsFromViewer);
this.navigateToPage(annotations[0].pageNumber); this.navigateToPage(annotations[0].pageNumber);
this.instance.annotManager.jumpToAnnotation(annotationsFromViewer[0]); this.instance.annotManager.jumpToAnnotation(annotationsFromViewer[0]);
} }
deselectAnnotations(annotations: AnnotationWrapper[]) { deselectAnnotations(annotations: AnnotationWrapper[]) {
this.instance.annotManager.deselectAnnotations(annotations.map((ann) => this.instance.annotManager.getAnnotationById(ann.id))); this.instance.annotManager.deselectAnnotations(
annotations.map((ann) => this.instance.annotManager.getAnnotationById(ann.id))
);
} }
navigateToPage(pageNumber: number) { navigateToPage(pageNumber: number) {
@ -107,7 +126,9 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
setInitialViewerState() { setInitialViewerState() {
// viewer init // viewer init
this.instance.setFitMode('FitPage'); this.instance.setFitMode('FitPage');
const instanceDisplayMode = this.instance.docViewer.getDisplayModeManager().getDisplayMode(); const instanceDisplayMode = this.instance.docViewer
.getDisplayModeManager()
.getDisplayMode();
instanceDisplayMode.mode = 'Single'; instanceDisplayMode.mode = 'Single';
this.instance.docViewer.getDisplayModeManager().setDisplayMode(instanceDisplayMode); this.instance.docViewer.getDisplayModeManager().setDisplayMode(instanceDisplayMode);
} }
@ -127,20 +148,28 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
this._configureTextPopup(); this._configureTextPopup();
instance.annotManager.on('annotationSelected', (annotations, action) => { instance.annotManager.on('annotationSelected', (annotations, action) => {
this.annotationSelected.emit(instance.annotManager.getSelectedAnnotations().map((ann) => ann.Id)); this.annotationSelected.emit(
instance.annotManager.getSelectedAnnotations().map((ann) => ann.Id)
);
if (action === 'deselected') { if (action === 'deselected') {
this._toggleRectangleAnnotationAction(true); this._toggleRectangleAnnotationAction(true);
} else { } else {
this._configureAnnotationSpecificActions(annotations); this._configureAnnotationSpecificActions(annotations);
this._toggleRectangleAnnotationAction(annotations.length === 1 && annotations[0].ReadOnly); this._toggleRectangleAnnotationAction(
annotations.length === 1 && annotations[0].ReadOnly
);
// this.annotationSelected.emit(annotations.map((a) => a.Id)); // this.annotationSelected.emit(annotations.map((a) => a.Id));
} }
}); });
instance.annotManager.on('annotationChanged', (annotations) => { instance.annotManager.on('annotationChanged', (annotations) => {
// when a rectangle is drawn, it returns one annotation with tool name 'AnnotationCreateRectangle; // when a rectangle is drawn,
// it returns one annotation with tool name 'AnnotationCreateRectangle;
// this will auto select rectangle after drawing // this will auto select rectangle after drawing
if (annotations.length === 1 && annotations[0].ToolName === 'AnnotationCreateRectangle') { if (
annotations.length === 1 &&
annotations[0].ToolName === 'AnnotationCreateRectangle'
) {
instance.annotManager.selectAnnotations(annotations); instance.annotManager.selectAnnotations(annotations);
} }
}); });
@ -183,7 +212,9 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
instance.iframeWindow.addEventListener('visibilityChanged', (event: any) => { instance.iframeWindow.addEventListener('visibilityChanged', (event: any) => {
if (event.detail.element === 'searchPanel') { if (event.detail.element === 'searchPanel') {
const inputElement = instance.iframeWindow.document.getElementById('SearchPanel__input') as HTMLInputElement; const inputElement = instance.iframeWindow.document.getElementById(
'SearchPanel__input'
) as HTMLInputElement;
setTimeout(() => { setTimeout(() => {
inputElement.value = ''; inputElement.value = '';
}, 0); }, 0);
@ -253,43 +284,58 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
private _configureAnnotationSpecificActions(viewerAnnotations: Annotations.Annotation[]) { private _configureAnnotationSpecificActions(viewerAnnotations: Annotations.Annotation[]) {
console.log('configure actions', viewerAnnotations); console.log('configure actions', viewerAnnotations);
if (this.canPerformActions) { if (!this.canPerformActions) {
const annotationWrappers = viewerAnnotations.map((va) => this.annotations.find((a) => a.id === va.Id)).filter((va) => !!va); return;
this.instance.annotationPopup.update([]);
if (annotationWrappers.length === 0) {
this._configureRectangleAnnotationPopup();
return;
}
// Add hide action as last item
const allAnnotationsHaveImageAction = annotationWrappers.reduce((acc, next) => acc && next.isImage, true);
if (allAnnotationsHaveImageAction) {
const allAreVisible = viewerAnnotations.reduce((acc, next) => next.isVisible() && acc, true);
this.instance.annotationPopup.add([
{
type: 'actionButton',
img: allAreVisible
? this._convertPath('/assets/icons/general/visibility-off.svg')
: this._convertPath('/assets/icons/general/visibility.svg'),
title: this._translateService.instant('annotation-actions.hide'),
onClick: () => {
this._ngZone.run(() => {
if (allAreVisible) {
this.instance.annotManager.hideAnnotations(viewerAnnotations);
} else {
this.instance.annotManager.showAnnotations(viewerAnnotations);
}
this.instance.annotManager.deselectAllAnnotations();
});
}
}
]);
}
this.instance.annotationPopup.add(this._annotationActionsService.getViewerAvailableActions(annotationWrappers, this.annotationsChanged));
} }
const annotationWrappers = viewerAnnotations
.map((va) => this.annotations.find((a) => a.id === va.Id))
.filter((va) => !!va);
this.instance.annotationPopup.update([]);
if (annotationWrappers.length === 0) {
this._configureRectangleAnnotationPopup();
return;
}
// Add hide action as last item
const allAnnotationsHaveImageAction = annotationWrappers.reduce(
(acc, next) => acc && next.isImage,
true
);
if (allAnnotationsHaveImageAction) {
const allAreVisible = viewerAnnotations.reduce(
(acc, next) => next.isVisible() && acc,
true
);
this.instance.annotationPopup.add([
{
type: 'actionButton',
img: allAreVisible
? this._convertPath('/assets/icons/general/visibility-off.svg')
: this._convertPath('/assets/icons/general/visibility.svg'),
title: this._translateService.instant('annotation-actions.hide'),
onClick: () => {
this._ngZone.run(() => {
if (allAreVisible) {
this.instance.annotManager.hideAnnotations(viewerAnnotations);
} else {
this.instance.annotManager.showAnnotations(viewerAnnotations);
}
this.instance.annotManager.deselectAllAnnotations();
});
}
}
]);
}
this.instance.annotationPopup.add(
this._annotationActionsService.getViewerAvailableActions(
annotationWrappers,
this.annotationsChanged
)
);
} }
private _configureRectangleAnnotationPopup() { private _configureRectangleAnnotationPopup() {
@ -297,12 +343,17 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
type: 'actionButton', type: 'actionButton',
dataElement: 'add-rectangle', dataElement: 'add-rectangle',
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'),
title: this._translateService.instant(this._manualAnnotationService.getTitle('REDACTION')), title: this._translateService.instant(
this._manualAnnotationService.getTitle('REDACTION')
),
onClick: () => { onClick: () => {
const selectedAnnotations = this.instance.annotManager.getSelectedAnnotations(); const selectedAnnotations = this.instance.annotManager.getSelectedAnnotations();
const activeAnnotation = selectedAnnotations[0]; const activeAnnotation = selectedAnnotations[0];
const activePage = selectedAnnotations[0].getPageNumber(); const activePage = selectedAnnotations[0].getPageNumber();
const quad = this._annotationDrawService.annotationToQuads(activeAnnotation, this.instance); const quad = this._annotationDrawService.annotationToQuads(
activeAnnotation,
this.instance
);
const quadsObject = {}; const quadsObject = {};
quadsObject[activePage] = [quad]; quadsObject[activePage] = [quad];
const mre = this._getManualRedactionEntry(quadsObject, 'Rectangle'); const mre = this._getManualRedactionEntry(quadsObject, 'Rectangle');
@ -311,7 +362,15 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
this.instance.disableElements(['shapeToolGroupButton']); this.instance.disableElements(['shapeToolGroupButton']);
this.instance.enableElements(['shapeToolGroupButton']); this.instance.enableElements(['shapeToolGroupButton']);
// dispatch event // dispatch event
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper([quad], mre, 'REDACTION', 'RECTANGLE', activeAnnotation.Id)); this.manualAnnotationRequested.emit(
new ManualRedactionEntryWrapper(
[quad],
mre,
'REDACTION',
'RECTANGLE',
activeAnnotation.Id
)
);
} }
}); });
} }
@ -344,12 +403,20 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
type: 'actionButton', type: 'actionButton',
dataElement: 'add-false-positive', dataElement: 'add-false-positive',
img: this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg'), img: this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg'),
title: this._translateService.instant(this._manualAnnotationService.getTitle('FALSE_POSITIVE')), title: this._translateService.instant(
this._manualAnnotationService.getTitle('FALSE_POSITIVE')
),
onClick: () => { onClick: () => {
const selectedQuads = this.instance.docViewer.getSelectedTextQuads(); const selectedQuads = this.instance.docViewer.getSelectedTextQuads();
const text = this.instance.docViewer.getSelectedText(); const text = this.instance.docViewer.getSelectedText();
const mre = this._getManualRedactionEntry(selectedQuads, text, true); const mre = this._getManualRedactionEntry(selectedQuads, text, true);
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(this.instance.docViewer.getSelectedTextQuads(), mre, 'FALSE_POSITIVE')); this.manualAnnotationRequested.emit(
new ManualRedactionEntryWrapper(
this.instance.docViewer.getSelectedTextQuads(),
mre,
'FALSE_POSITIVE'
)
);
} }
}); });
} }
@ -358,12 +425,20 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
type: 'actionButton', type: 'actionButton',
dataElement: 'add-dictionary', dataElement: 'add-dictionary',
img: this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'), img: this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'),
title: this._translateService.instant(this._manualAnnotationService.getTitle('DICTIONARY')), title: this._translateService.instant(
this._manualAnnotationService.getTitle('DICTIONARY')
),
onClick: () => { onClick: () => {
const selectedQuads = this.instance.docViewer.getSelectedTextQuads(); const selectedQuads = this.instance.docViewer.getSelectedTextQuads();
const text = this.instance.docViewer.getSelectedText(); const text = this.instance.docViewer.getSelectedText();
const mre = this._getManualRedactionEntry(selectedQuads, text, true); const mre = this._getManualRedactionEntry(selectedQuads, text, true);
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(this.instance.docViewer.getSelectedTextQuads(), mre, 'DICTIONARY')); this.manualAnnotationRequested.emit(
new ManualRedactionEntryWrapper(
this.instance.docViewer.getSelectedTextQuads(),
mre,
'DICTIONARY'
)
);
} }
}); });
@ -371,12 +446,20 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
type: 'actionButton', type: 'actionButton',
dataElement: 'add-redaction', dataElement: 'add-redaction',
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'),
title: this._translateService.instant(this._manualAnnotationService.getTitle('REDACTION')), title: this._translateService.instant(
this._manualAnnotationService.getTitle('REDACTION')
),
onClick: () => { onClick: () => {
const selectedQuads = this.instance.docViewer.getSelectedTextQuads(); const selectedQuads = this.instance.docViewer.getSelectedTextQuads();
const text = this.instance.docViewer.getSelectedText(); const text = this.instance.docViewer.getSelectedText();
const mre = this._getManualRedactionEntry(selectedQuads, text, true); const mre = this._getManualRedactionEntry(selectedQuads, text, true);
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(this.instance.docViewer.getSelectedTextQuads(), mre, 'REDACTION')); this.manualAnnotationRequested.emit(
new ManualRedactionEntryWrapper(
this.instance.docViewer.getSelectedTextQuads(),
mre,
'REDACTION'
)
);
} }
}); });
this._handleCustomActions(); this._handleCustomActions();
@ -386,7 +469,13 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
this.instance.setToolMode('AnnotationEdit'); this.instance.setToolMode('AnnotationEdit');
if (this.canPerformActions) { if (this.canPerformActions) {
this.instance.enableTools(['AnnotationCreateRectangle']); this.instance.enableTools(['AnnotationCreateRectangle']);
this.instance.enableElements(['add-redaction', 'add-rectangle', 'add-false-positive', 'shapeToolGroupButton', 'annotationPopup']); this.instance.enableElements([
'add-redaction',
'add-rectangle',
'add-false-positive',
'shapeToolGroupButton',
'annotationPopup'
]);
if (this._selectedText.length > 2) { if (this._selectedText.length > 2) {
this.instance.enableElements(['add-dictionary', 'add-false-positive']); this.instance.enableElements(['add-dictionary', 'add-false-positive']);
} }
@ -403,13 +492,19 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
} }
} }
private _getManualRedactionEntry(quads: any, text: string, convertQuads: boolean = false): ManualRedactionEntry { private _getManualRedactionEntry(
quads: any,
text: string,
convertQuads: boolean = false
): ManualRedactionEntry {
text = text.replace(/-\n/gi, ''); text = text.replace(/-\n/gi, '');
const entry: ManualRedactionEntry = { positions: [] }; const entry: ManualRedactionEntry = { positions: [] };
for (const key of Object.keys(quads)) { for (const key of Object.keys(quads)) {
for (const quad of quads[key]) { for (const quad of quads[key]) {
const page = parseInt(key, 10); const page = parseInt(key, 10);
entry.positions.push(this._toPosition(page, convertQuads ? this._translateQuads(page, quad) : quad)); entry.positions.push(
this._toPosition(page, convertQuads ? this._translateQuads(page, quad) : quad)
);
} }
} }
entry.value = text; entry.value = text;

View File

@ -53,7 +53,11 @@
</div> </div>
<div *ngIf="hasFiles" class="mt-24 legend pb-32"> <div *ngIf="hasFiles" class="mt-24 legend pb-32">
<div (click)="toggleFilter('needsWorkFilters', filter.key)" *ngFor="let filter of filters.needsWorkFilters" [class.active]="filter.checked"> <div
(click)="toggleFilter('needsWorkFilters', filter.key)"
*ngFor="let filter of filters.needsWorkFilters"
[class.active]="filter.checked"
>
<redaction-type-filter [filter]="filter"></redaction-type-filter> <redaction-type-filter [filter]="filter"></redaction-type-filter>
</div> </div>
</div> </div>
@ -61,37 +65,63 @@
<div [class.mt-24]="!hasFiles" class="pb-32 small-label stats-subtitle"> <div [class.mt-24]="!hasFiles" class="pb-32 small-label stats-subtitle">
<div> <div>
<mat-icon svgIcon="red:document"></mat-icon> <mat-icon svgIcon="red:document"></mat-icon>
<span>{{ 'project-overview.project-details.stats.documents' | translate: { count: appStateService.activeProject.files.length } }}</span> <span>{{
'project-overview.project-details.stats.documents'
| translate: { count: appStateService.activeProject.files.length }
}}</span>
</div> </div>
<div> <div>
<mat-icon svgIcon="red:user"></mat-icon> <mat-icon svgIcon="red:user"></mat-icon>
<span>{{ 'project-overview.project-details.stats.people' | translate: { count: appStateService.activeProject.memberCount } }}</span> <span>{{
'project-overview.project-details.stats.people'
| translate: { count: appStateService.activeProject.memberCount }
}}</span>
</div> </div>
<div> <div>
<mat-icon svgIcon="red:pages"></mat-icon> <mat-icon svgIcon="red:pages"></mat-icon>
<span>{{ <span>{{
'project-overview.project-details.stats.analysed-pages' | translate: { count: appStateService.activeProject.totalNumberOfPages | number } 'project-overview.project-details.stats.analysed-pages'
| translate
: { count: appStateService.activeProject.totalNumberOfPages | number }
}}</span> }}</span>
</div> </div>
<div> <div>
<mat-icon svgIcon="red:calendar"></mat-icon> <mat-icon svgIcon="red:calendar"></mat-icon>
<span <span
>{{ >{{
'project-overview.project-details.stats.created-on' | translate: { date: appStateService.activeProject.project.date | date: 'd MMM. yyyy' } 'project-overview.project-details.stats.created-on'
| translate
: {
date:
appStateService.activeProject.project.date
| date: 'd MMM. yyyy'
}
}} }}
</span> </span>
</div> </div>
<div *ngIf="appStateService.activeProject.project.dueDate"> <div *ngIf="appStateService.activeProject.project.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon> <mat-icon svgIcon="red:lightning"></mat-icon>
<span>{{ <span>{{
'project-overview.project-details.stats.due-date' | translate: { date: appStateService.activeProject.project.dueDate | date: 'd MMM. yyyy' } 'project-overview.project-details.stats.due-date'
| translate
: {
date:
appStateService.activeProject.project.dueDate
| date: 'd MMM. yyyy'
}
}}</span> }}</span>
</div> </div>
<div> <div>
<mat-icon svgIcon="red:template"></mat-icon> <mat-icon svgIcon="red:template"></mat-icon>
<span>{{ appStateService.getRuleSetById(appStateService.activeProject.ruleSetId)?.name }} </span> <span
>{{ appStateService.getRuleSetById(appStateService.activeProject.ruleSetId)?.name }}
</span>
</div> </div>
<div *ngIf="appStateService.activeProject.type" class="pointer" (click)="openDossierDictionaryDialog.emit()"> <div
*ngIf="appStateService.activeProject.type"
class="pointer"
(click)="openDossierDictionaryDialog.emit()"
>
<mat-icon svgIcon="red:dictionary"></mat-icon> <mat-icon svgIcon="red:dictionary"></mat-icon>
<span>{{ 'project-overview.project-details.dictionary' | translate }} </span> <span>{{ 'project-overview.project-details.dictionary' | translate }} </span>
</div> </div>

View File

@ -51,10 +51,17 @@ export class ProjectDetailsComponent implements OnInit {
const groups = groupBy(this.appStateService.activeProject?.files, 'status'); const groups = groupBy(this.appStateService.activeProject?.files, 'status');
this.documentsChartData = []; this.documentsChartData = [];
for (const key of Object.keys(groups)) { for (const key of Object.keys(groups)) {
this.documentsChartData.push({ value: groups[key].length, color: key, label: key, key: key }); this.documentsChartData.push({
value: groups[key].length,
color: key,
label: key,
key: key
});
} }
this.documentsChartData.sort((a, b) => StatusSorter[a.key] - StatusSorter[b.key]); this.documentsChartData.sort((a, b) => StatusSorter[a.key] - StatusSorter[b.key]);
this.documentsChartData = this.translateChartService.translateStatus(this.documentsChartData); this.documentsChartData = this.translateChartService.translateStatus(
this.documentsChartData
);
this._changeDetectorRef.detectChanges(); this._changeDetectorRef.detectChanges();
} }
} }

Some files were not shown because too many files have changed in this diff Show More