Merge branch 'master' into VM/NotificationsPreferences
This commit is contained in:
commit
7b5f5a296b
1
.gitignore
vendored
1
.gitignore
vendored
@ -45,3 +45,4 @@ version.properties
|
||||
paligo-styles/style.css*
|
||||
|
||||
migrations.json
|
||||
*.iml
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
{
|
||||
"cli": {
|
||||
"analytics": "d22ff5ae-c863-4253-83e3-0a969e4bb5fe"
|
||||
},
|
||||
"version": 1,
|
||||
"projects": {
|
||||
"common-ui": {
|
||||
|
||||
@ -25,6 +25,7 @@ export class LanguageService {
|
||||
} else {
|
||||
defaultLang = 'en';
|
||||
}
|
||||
console.log(defaultLang);
|
||||
document.documentElement.lang = defaultLang;
|
||||
this._translateService.setDefaultLang(defaultLang);
|
||||
this._translateService.use(defaultLang).toPromise().then();
|
||||
|
||||
@ -7,16 +7,16 @@
|
||||
<div class="dialog-content">
|
||||
<div class="iqser-input-group mb-14">
|
||||
<label translate="add-edit-dictionary.form.technical-name"></label>
|
||||
<div class="technical-name">{{ dictionary?.type || technicalName || '-' }}</div>
|
||||
<div class="technical-name">{{ dictionary?.type || (technicalName$ | async) || '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!!dictionary" class="iqser-input-group mb-14">
|
||||
<div *ngIf="(canEditLabel$ | async) === false" class="iqser-input-group mb-14">
|
||||
<label translate="add-edit-dictionary.form.name"></label>
|
||||
{{ dictionary.label }}
|
||||
</div>
|
||||
|
||||
<div class="first-row">
|
||||
<div *ngIf="!dictionary" class="iqser-input-group required">
|
||||
<div *ngIf="canEditLabel$ | async" class="iqser-input-group required">
|
||||
<label translate="add-edit-dictionary.form.name"></label>
|
||||
<input
|
||||
[placeholder]="'add-edit-dictionary.form.name-placeholder' | translate"
|
||||
@ -53,7 +53,7 @@
|
||||
[style.background]="form.get('hexColor').value"
|
||||
class="input-icon"
|
||||
>
|
||||
<mat-icon *ngIf="hasColor" svgIcon="red:color-picker"></mat-icon>
|
||||
<mat-icon *ngIf="hasColor$ | async" svgIcon="red:color-picker"></mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,39 +1,47 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BaseDialogComponent, Toaster } from '@iqser/common-ui';
|
||||
import { BaseDialogComponent, shareDistinctLast, Toaster } from '@iqser/common-ui';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { AppStateService } from '@state/app-state.service';
|
||||
import { toKebabCase } from '@utils/functions';
|
||||
import { DictionaryService } from '@shared/services/dictionary.service';
|
||||
import { Dictionary, IDictionary } from '@red/domain';
|
||||
import { UserService } from '@services/user.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-add-edit-dictionary-dialog',
|
||||
templateUrl: './add-edit-dictionary-dialog.component.html',
|
||||
styleUrls: ['./add-edit-dictionary-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
|
||||
form: FormGroup;
|
||||
readonly dictionary: Dictionary;
|
||||
technicalName = '';
|
||||
private readonly _dossierTemplateId: string;
|
||||
readonly form: FormGroup;
|
||||
readonly dictionary = this._data.dictionary;
|
||||
readonly canEditLabel$ = this._canEditLabel$;
|
||||
readonly technicalName$: Observable<string>;
|
||||
readonly dialogHeader = this._translateService.instant('add-edit-dictionary.title', {
|
||||
type: this._data.dictionary ? 'edit' : 'create',
|
||||
name: this._data.dictionary?.label,
|
||||
});
|
||||
readonly hasColor$: Observable<boolean>;
|
||||
private readonly _dossierTemplateId = this._data.dossierTemplateId;
|
||||
|
||||
constructor(
|
||||
private readonly _dictionaryService: DictionaryService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
readonly userService: UserService,
|
||||
private readonly _toaster: Toaster,
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _dictionaryService: DictionaryService,
|
||||
private readonly _dialogRef: MatDialogRef<AddEditDictionaryDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA)
|
||||
private readonly _data: { dictionary: Dictionary; dossierTemplateId: string },
|
||||
private readonly _data: { readonly dictionary: Dictionary; readonly dossierTemplateId: string },
|
||||
) {
|
||||
super();
|
||||
this.dictionary = _data.dictionary;
|
||||
this._dossierTemplateId = _data.dossierTemplateId;
|
||||
this.form = _formBuilder.group({
|
||||
label: [this.dictionary?.label, [Validators.required, Validators.minLength(3)]],
|
||||
description: [this.dictionary?.description],
|
||||
@ -43,21 +51,8 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
|
||||
addToDictionaryAction: [!!this.dictionary?.addToDictionaryAction],
|
||||
caseSensitive: [this.dictCaseSensitive],
|
||||
});
|
||||
this.form.get('label').valueChanges.subscribe(() => {
|
||||
this._updateTechnicalName();
|
||||
});
|
||||
}
|
||||
|
||||
get dialogHeader(): string {
|
||||
return this._translateService.instant('add-edit-dictionary.title', {
|
||||
type: this.dictionary ? 'edit' : 'create',
|
||||
name: this.dictionary?.label,
|
||||
});
|
||||
}
|
||||
|
||||
get hasColor(): boolean {
|
||||
const hexColorValue = this.form.get('hexColor').value;
|
||||
return !hexColorValue || hexColorValue?.length === 0;
|
||||
this.hasColor$ = this._colorEmpty$;
|
||||
this.technicalName$ = this.form.get('label').valueChanges.pipe(map(value => this._toTechnicalName(value)));
|
||||
}
|
||||
|
||||
get dictCaseSensitive(): boolean {
|
||||
@ -82,21 +77,34 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
private get _canEditLabel$() {
|
||||
return this.userService.currentUser$.pipe(
|
||||
map(user => user.isAdmin || !this._data.dictionary),
|
||||
shareDistinctLast(),
|
||||
);
|
||||
}
|
||||
|
||||
private get _colorEmpty$() {
|
||||
return this.form.get('hexColor').valueChanges.pipe(map((value: string) => !value || value?.length === 0));
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
const dictionary = this._formToObject();
|
||||
let observable: Observable<unknown>;
|
||||
const dossierTemplateId = this._data.dossierTemplateId;
|
||||
|
||||
if (this.dictionary) {
|
||||
// edit mode
|
||||
observable = this._dictionaryService.updateDictionary(dictionary, this._dossierTemplateId, dictionary.type);
|
||||
observable = this._dictionaryService.updateDictionary(dictionary, dossierTemplateId, dictionary.type);
|
||||
} else {
|
||||
// create mode
|
||||
observable = this._dictionaryService.addDictionary({ ...dictionary, dossierTemplateId: this._dossierTemplateId });
|
||||
observable = this._dictionaryService.addDictionary({ ...dictionary, dossierTemplateId });
|
||||
}
|
||||
|
||||
observable.subscribe(
|
||||
() => this._dialogRef.close(true),
|
||||
error => {
|
||||
return observable
|
||||
.toPromise()
|
||||
.then(() => this._dialogRef.close(true))
|
||||
.catch(error => {
|
||||
if (error.status === 409) {
|
||||
this._toaster.error(_('add-edit-dictionary.error.dictionary-already-exists'));
|
||||
} else if (error.status === 400) {
|
||||
@ -104,25 +112,23 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
|
||||
} else {
|
||||
this._toaster.error(_('add-edit-dictionary.error.generic'));
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _updateTechnicalName() {
|
||||
const displayName = this.form.get('label').value.trim();
|
||||
private _toTechnicalName(value: string) {
|
||||
const existingTechnicalNames = Object.keys(this._appStateService.dictionaryData[this._dossierTemplateId]);
|
||||
const baseTechnicalName: string = toKebabCase(displayName);
|
||||
const baseTechnicalName = toKebabCase(value.trim());
|
||||
let technicalName = baseTechnicalName;
|
||||
let suffix = 1;
|
||||
while (existingTechnicalNames.includes(technicalName)) {
|
||||
technicalName = [baseTechnicalName, suffix++].join('-');
|
||||
}
|
||||
this.technicalName = technicalName;
|
||||
return technicalName;
|
||||
}
|
||||
|
||||
private _formToObject(): IDictionary {
|
||||
return {
|
||||
type: this.dictionary?.type || this.technicalName,
|
||||
type: this.dictionary?.type || this._toTechnicalName(this.form.get('label').value),
|
||||
label: this.form.get('label').value,
|
||||
caseInsensitive: !this.form.get('caseSensitive').value,
|
||||
description: this.form.get('description').value,
|
||||
@ -130,7 +136,7 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
|
||||
hint: this.form.get('hint').value,
|
||||
rank: this.form.get('rank').value,
|
||||
addToDictionaryAction: this.form.get('addToDictionaryAction').value,
|
||||
dossierTemplateId: this._dossierTemplateId,
|
||||
dossierTemplateId: this._data.dossierTemplateId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { Component, EventEmitter, forwardRef, Injector, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||
import { Field } from '../file-attributes-csv-import-dialog.component';
|
||||
import { CircleButtonTypes, DefaultListingServices, ListingComponent, TableColumnConfig } from '@iqser/common-ui';
|
||||
import { fileAttributeTypesTranslations } from '../../../translations/file-attribute-types-translations';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { FileAttributeConfigTypes } from '@red/domain';
|
||||
import { FileAttributeConfigTypes, IField } from '@red/domain';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-active-fields-listing',
|
||||
@ -11,11 +10,11 @@ import { FileAttributeConfigTypes } from '@red/domain';
|
||||
styleUrls: ['./active-fields-listing.component.scss'],
|
||||
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => ActiveFieldsListingComponent) }],
|
||||
})
|
||||
export class ActiveFieldsListingComponent extends ListingComponent<Field> implements OnChanges {
|
||||
export class ActiveFieldsListingComponent extends ListingComponent<IField> implements OnChanges {
|
||||
readonly circleButtonTypes = CircleButtonTypes;
|
||||
readonly translations = fileAttributeTypesTranslations;
|
||||
readonly tableHeaderLabel = _('file-attributes-csv-import.table-header.title');
|
||||
readonly tableColumnConfigs: TableColumnConfig<Field>[] = [
|
||||
readonly tableColumnConfigs: TableColumnConfig<IField>[] = [
|
||||
{
|
||||
label: _('file-attributes-csv-import.table-col-names.name'),
|
||||
class: 'name',
|
||||
@ -40,10 +39,10 @@ export class ActiveFieldsListingComponent extends ListingComponent<Field> implem
|
||||
},
|
||||
];
|
||||
readonly typeOptions = Object.keys(FileAttributeConfigTypes);
|
||||
@Input() entities: Field[];
|
||||
@Output() readonly entitiesChange = new EventEmitter<Field[]>();
|
||||
@Input() entities: IField[];
|
||||
@Output() readonly entitiesChange = new EventEmitter<IField[]>();
|
||||
@Output() readonly setHoveredColumn = new EventEmitter<string>();
|
||||
@Output() readonly toggleFieldActive = new EventEmitter<Field>();
|
||||
@Output() readonly toggleFieldActive = new EventEmitter<IField>();
|
||||
|
||||
constructor(protected readonly _injector: Injector) {
|
||||
super(_injector);
|
||||
@ -68,7 +67,7 @@ export class ActiveFieldsListingComponent extends ListingComponent<Field> implem
|
||||
}
|
||||
}
|
||||
|
||||
togglePrimary(field: Field) {
|
||||
togglePrimary(field: IField) {
|
||||
if (field.primaryAttribute) {
|
||||
field.primaryAttribute = false;
|
||||
return;
|
||||
@ -80,6 +79,6 @@ export class ActiveFieldsListingComponent extends ListingComponent<Field> implem
|
||||
field.primaryAttribute = true;
|
||||
}
|
||||
|
||||
itemMouseEnterFn = (field: Field) => this.setHoveredColumn.emit(field.csvColumn);
|
||||
itemMouseEnterFn = (field: IField) => this.setHoveredColumn.emit(field.csvColumn);
|
||||
itemMouseLeaveFn = () => this.setHoveredColumn.emit();
|
||||
}
|
||||
|
||||
@ -1,33 +1,25 @@
|
||||
import { Component, Inject, Injector } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Injector } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import * as Papa from 'papaparse';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
import { DefaultListingServices, IListable, ListingComponent, TableColumnConfig, Toaster } from '@iqser/common-ui';
|
||||
import { DefaultListingServices, ListingComponent, TableColumnConfig, Toaster } from '@iqser/common-ui';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { FileAttributeConfig, FileAttributeConfigType, FileAttributeConfigTypes, IFileAttributesConfig } from '@red/domain';
|
||||
import { FileAttributeConfig, FileAttributeConfigTypes, IField, IFileAttributesConfig } from '@red/domain';
|
||||
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
|
||||
|
||||
export interface Field extends IListable {
|
||||
id: string;
|
||||
csvColumn: string;
|
||||
name: string;
|
||||
type: FileAttributeConfigType;
|
||||
readonly: boolean;
|
||||
primaryAttribute: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: './file-attributes-csv-import-dialog.component.html',
|
||||
styleUrls: ['./file-attributes-csv-import-dialog.component.scss'],
|
||||
providers: [...DefaultListingServices],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FileAttributesCsvImportDialogComponent extends ListingComponent<Field> {
|
||||
readonly tableColumnConfigs: TableColumnConfig<Field>[] = [];
|
||||
parseResult: { data: any[]; errors: any[]; meta: any; fields: Field[] };
|
||||
export class FileAttributesCsvImportDialogComponent extends ListingComponent<IField> {
|
||||
readonly tableColumnConfigs: TableColumnConfig<IField>[] = [];
|
||||
parseResult: { data: any[]; errors: any[]; meta: any; fields: IField[] };
|
||||
hoveredColumn: string;
|
||||
activeFields: Field[] = [];
|
||||
activeFields: IField[] = [];
|
||||
readonly baseConfigForm: FormGroup;
|
||||
isSearchOpen = false;
|
||||
previewExpanded = true;
|
||||
@ -62,6 +54,7 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<Fie
|
||||
}
|
||||
|
||||
get changedParseConfig(): boolean {
|
||||
console.log(this.baseConfigForm.invalid);
|
||||
return (
|
||||
this.initialParseConfig.delimiter !== this.baseConfigForm.get('delimiter').value ||
|
||||
this.initialParseConfig.encoding !== this.baseConfigForm.get('encoding').value
|
||||
@ -87,7 +80,7 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<Fie
|
||||
this.activeFields = [];
|
||||
|
||||
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) {
|
||||
entity.id = existing.id;
|
||||
entity.name = existing.label;
|
||||
@ -142,11 +135,11 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<Fie
|
||||
return 0;
|
||||
}
|
||||
|
||||
isActive(field: Field): boolean {
|
||||
isActive(field: IField): boolean {
|
||||
return !!this.activeFields.find(f => f.id === field.id);
|
||||
}
|
||||
|
||||
toggleFieldActive(field: Field) {
|
||||
toggleFieldActive(field: IField) {
|
||||
if (!this.isActive(field)) {
|
||||
this.activeFields = [...this.activeFields, { ...field, searchKey: field.csvColumn }];
|
||||
} else {
|
||||
@ -219,7 +212,7 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<Fie
|
||||
};
|
||||
}
|
||||
|
||||
private _buildAttribute(csvColumn: string): Field {
|
||||
private _buildAttribute(csvColumn: string): IField {
|
||||
const sample = this.getSample(csvColumn);
|
||||
const isNumber = sample && !isNaN(sample);
|
||||
return {
|
||||
|
||||
@ -21,9 +21,7 @@ function getKeycloakOptions(configService: ConfigService, baseUrl: string) {
|
||||
initOptions: {
|
||||
checkLoginIframe: false,
|
||||
onLoad: 'check-sso',
|
||||
silentCheckSsoRedirectUri: environment.production
|
||||
? window.location.origin + baseUrl + '/assets/oauth/silent-refresh.html'
|
||||
: null,
|
||||
silentCheckSsoRedirectUri: window.location.origin + baseUrl + '/assets/oauth/silent-refresh.html',
|
||||
flow: 'standard',
|
||||
},
|
||||
enableBearerInterceptor: true,
|
||||
|
||||
@ -4,7 +4,7 @@ import { Dossier, DossierAttributeWithValue, DossierTemplate } from '@red/domain
|
||||
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||
import { FilesService } from '../../../../../../services/entity-services/files.service';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||
|
||||
|
||||
@ -47,9 +47,7 @@
|
||||
{{ 'search-screen.missing' | translate }}:<span *ngFor="let term of unmatched"
|
||||
> <s>{{ term }}</s></span
|
||||
>. {{ 'search-screen.must-contain' | translate }}:
|
||||
<span
|
||||
(click)="$event.stopPropagation(); updateNavigation(search$.getValue().query, term)"
|
||||
*ngFor="let term of unmatched"
|
||||
<span (click)="$event.stopPropagation(); mustContain(term)" *ngFor="let term of unmatched"
|
||||
> <u>{{ term }}</u></span
|
||||
>
|
||||
</span>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { Component, forwardRef, Injector, OnDestroy } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnDestroy } from '@angular/core';
|
||||
import {
|
||||
DefaultListingServices,
|
||||
IListable,
|
||||
keyChecker,
|
||||
List,
|
||||
ListingComponent,
|
||||
@ -10,8 +9,8 @@ import {
|
||||
SearchPositions,
|
||||
TableColumnConfig,
|
||||
} from '@iqser/common-ui';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { debounceTime, map, skip, switchMap, tap } from 'rxjs/operators';
|
||||
import { merge, Observable } from 'rxjs';
|
||||
import { debounceTime, map, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { fileStatusTranslations } from '../../translations/file-status-translations';
|
||||
@ -19,44 +18,38 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { RouterHistoryService } from '@services/router-history.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { PlatformSearchService } from '../../shared/services/platform-search.service';
|
||||
import { IMatchedDocument, ISearchResponse } from '@red/domain';
|
||||
import { IMatchedDocument, ISearchInput, ISearchListItem, ISearchResponse } from '@red/domain';
|
||||
|
||||
interface ListItem extends IListable {
|
||||
readonly dossierId: string;
|
||||
readonly filename: string;
|
||||
readonly unmatched: List | null;
|
||||
readonly highlights: Record<string, List>;
|
||||
readonly routerLink: string;
|
||||
readonly status: string;
|
||||
readonly dossierName: string;
|
||||
readonly numberOfPages: number;
|
||||
}
|
||||
|
||||
interface SearchInput {
|
||||
readonly query: string;
|
||||
readonly dossierIds?: List;
|
||||
function toSearchInput(query: string, dossierIds: List | string): ISearchInput {
|
||||
return {
|
||||
query,
|
||||
dossierIds: dossierIds ? (typeof dossierIds === 'string' ? [dossierIds] : dossierIds) : [],
|
||||
};
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: './search-screen.component.html',
|
||||
styleUrls: ['./search-screen.component.scss'],
|
||||
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => SearchScreenComponent) }],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SearchScreenComponent extends ListingComponent<ListItem> implements OnDestroy {
|
||||
export class SearchScreenComponent extends ListingComponent<ISearchListItem> implements OnDestroy {
|
||||
readonly fileStatusTranslations = fileStatusTranslations;
|
||||
readonly searchPositions = SearchPositions;
|
||||
|
||||
readonly tableHeaderLabel = _('search-screen.table-header');
|
||||
readonly tableColumnConfigs: TableColumnConfig<ListItem>[] = [
|
||||
readonly tableColumnConfigs: TableColumnConfig<ISearchListItem>[] = [
|
||||
{ label: _('search-screen.cols.document'), width: '2fr' },
|
||||
{ label: _('search-screen.cols.status') },
|
||||
{ label: _('search-screen.cols.dossier') },
|
||||
{ label: _('search-screen.cols.pages'), width: 'auto' },
|
||||
];
|
||||
readonly search$ = new BehaviorSubject<SearchInput>(null);
|
||||
readonly searchResults$: Observable<ListItem[]> = this.search$.asObservable().pipe(
|
||||
|
||||
readonly searchResults$ = merge(this._searchChanged$, this._filtersChanged$).pipe(
|
||||
startWith(this._routeQuery),
|
||||
tap(() => this._loadingService.start()),
|
||||
switchMap(query => this._search(query)),
|
||||
tap(value => this.updateNavigation(value.query)),
|
||||
switchMap(query => this._platformSearchService.search(query)),
|
||||
map(searchResult => this._toMatchedDocuments(searchResult)),
|
||||
map(docs => this._toListItems(docs)),
|
||||
tap(result => this.entitiesService.setEntities(result)),
|
||||
@ -76,6 +69,7 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
|
||||
super(_injector);
|
||||
this.searchService.skip = true;
|
||||
|
||||
const dossierId = _activatedRoute.snapshot.queryParamMap.get('dossierId');
|
||||
this.filterService.addFilterGroups([
|
||||
{
|
||||
slug: 'dossiers',
|
||||
@ -87,57 +81,53 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
|
||||
new NestedFilter({
|
||||
id: dossier.id,
|
||||
label: dossier.dossierName,
|
||||
checked: dossier.id === dossierId,
|
||||
}),
|
||||
),
|
||||
checker: keyChecker('dossierId'),
|
||||
},
|
||||
]);
|
||||
|
||||
this.addSubscription = _activatedRoute.queryParamMap
|
||||
.pipe(map(value => ({ query: value.get('query'), dossierId: value.get('dossierId') })))
|
||||
.subscribe(mappedValue => this._updateValues(mappedValue));
|
||||
|
||||
this.addSubscription = this.searchService.valueChanges$.pipe(debounceTime(300)).subscribe(value => this.updateNavigation(value));
|
||||
|
||||
this.addSubscription = this.filterService.filterGroups$.pipe(skip(1)).subscribe(group => {
|
||||
const dossierIds = group[0].filters.filter(v => v.checked).map(v => v.id);
|
||||
this.search$.next({ query: this.searchService.searchValue, dossierIds: dossierIds });
|
||||
});
|
||||
}
|
||||
|
||||
updateNavigation(query: string, mustContain?: string): void {
|
||||
const newQuery = query?.replace(mustContain, `"${mustContain}"`);
|
||||
const queryParams = newQuery && newQuery !== '' ? { query: newQuery } : {};
|
||||
this._router.navigate([], { queryParams }).then();
|
||||
private get _searchChanged$(): Observable<ISearchInput> {
|
||||
return this.searchService.valueChanges$.pipe(
|
||||
debounceTime(300),
|
||||
map(value => ({ query: value, dossierIds: [] })),
|
||||
);
|
||||
}
|
||||
|
||||
private _search(searchInput: SearchInput): Observable<ISearchResponse> {
|
||||
return this._platformSearchService.search({
|
||||
dossierIds: [...searchInput.dossierIds],
|
||||
queryString: searchInput.query ?? '',
|
||||
page: 1,
|
||||
returnSections: false,
|
||||
pageSize: 300,
|
||||
});
|
||||
private get _filtersChanged$() {
|
||||
return this.filterService.filterGroups$.pipe(
|
||||
map(groups => groups[0].filters.filter(v => v.checked).map(v => v.id)),
|
||||
map(dossierIds => toSearchInput(this.searchService.searchValue, dossierIds)),
|
||||
);
|
||||
}
|
||||
|
||||
private _updateValues({ query, dossierId }: { readonly query: string; readonly dossierId: string }) {
|
||||
if (dossierId) {
|
||||
this.filterService.toggleFilter('dossiers', dossierId);
|
||||
}
|
||||
private get _routeQuery(): ISearchInput {
|
||||
const query = this._activatedRoute.snapshot.queryParamMap.get('query');
|
||||
const dossierId = this._activatedRoute.snapshot.queryParamMap.get('dossierId');
|
||||
this.searchService.searchValue = query;
|
||||
this.search$.next({ query, dossierIds: dossierId ? [dossierId] : [] });
|
||||
return { query, dossierIds: dossierId ? [dossierId] : [] };
|
||||
}
|
||||
|
||||
updateNavigation(query: string) {
|
||||
return this._router.navigate([], { queryParams: { query } });
|
||||
}
|
||||
|
||||
mustContain(value: string) {
|
||||
const newQuery = this.searchService.searchValue.replace(value, `"${value}"`);
|
||||
this.searchService.searchValue = newQuery ?? '';
|
||||
}
|
||||
|
||||
private _toMatchedDocuments({ matchedDocuments }: ISearchResponse): IMatchedDocument[] {
|
||||
return matchedDocuments.filter(doc => doc.score > 0 && doc.matchedTerms.length > 0);
|
||||
}
|
||||
|
||||
private _toListItems(matchedDocuments: IMatchedDocument[]): ListItem[] {
|
||||
private _toListItems(matchedDocuments: IMatchedDocument[]): ISearchListItem[] {
|
||||
return matchedDocuments.map(document => this._toListItem(document)).filter(value => value);
|
||||
}
|
||||
|
||||
private _toListItem({ dossierId, fileId, unmatchedTerms, highlights }: IMatchedDocument): ListItem {
|
||||
private _toListItem({ dossierId, fileId, unmatchedTerms, highlights, score }: IMatchedDocument): ISearchListItem {
|
||||
const file = this._dossiersService.find(dossierId, fileId);
|
||||
if (!file) {
|
||||
return undefined;
|
||||
@ -152,7 +142,7 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
|
||||
numberOfPages: file.numberOfPages,
|
||||
dossierName: this._dossiersService.find(dossierId).dossierName,
|
||||
filename: file.filename,
|
||||
searchKey: file.filename,
|
||||
searchKey: score.toString(),
|
||||
routerLink: `/main/dossiers/${dossierId}/file/${fileId}`,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { GenericService } from '@iqser/common-ui';
|
||||
import { ISearchRequest, ISearchResponse } from '@red/domain';
|
||||
import { ISearchInput, ISearchRequest, ISearchResponse } from '@red/domain';
|
||||
|
||||
@Injectable()
|
||||
export class PlatformSearchService extends GenericService<ISearchResponse> {
|
||||
@ -8,7 +8,13 @@ export class PlatformSearchService extends GenericService<ISearchResponse> {
|
||||
super(_injector, 'search');
|
||||
}
|
||||
|
||||
search(body: ISearchRequest) {
|
||||
return this._post(body);
|
||||
search({ dossierIds, query }: ISearchInput) {
|
||||
return this._post({
|
||||
dossierIds,
|
||||
queryString: query ?? '',
|
||||
page: 1,
|
||||
returnSections: false,
|
||||
pageSize: 300,
|
||||
} as ISearchRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +56,9 @@ const modules = [MatConfigModule, ScrollingModule, IconsModule, FormsModule, Rea
|
||||
{
|
||||
provide: MAT_DATE_FORMATS,
|
||||
useValue: {
|
||||
parse: {
|
||||
dateInput: 'DD/MM/YY',
|
||||
},
|
||||
display: {
|
||||
dateInput: 'DD/MM/YY',
|
||||
monthYearLabel: 'YYYY',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,4 @@
|
||||
server {
|
||||
|
||||
listen 8080;
|
||||
proxy_hide_header WWW-Authenticate;
|
||||
port_in_redirect off;
|
||||
@ -7,6 +6,8 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
# SSL stuff for cloudflare proxy-ing - ignores SSL certificate and uses SNI
|
||||
|
||||
add_header Content-Security-Policy "default-src 'self';";
|
||||
|
||||
proxy_ssl_verify off;
|
||||
proxy_read_timeout 1m;
|
||||
proxy_ssl_server_name on;
|
||||
|
||||
@ -46,5 +46,7 @@ RUN chmod g+r -R /usr/share/nginx/html
|
||||
## Change permissions to enable openShift functionality
|
||||
RUN chmod -R g+rwx /var/cache/nginx /var/run /var/log/nginx /usr/share /etc/nginx
|
||||
|
||||
USER 1001
|
||||
|
||||
COPY docker/red-ui/docker-entrypoint.sh /
|
||||
CMD ["/docker-entrypoint.sh"]
|
||||
|
||||
@ -4,7 +4,7 @@ import { IDictionary } from './dictionary';
|
||||
export class Dictionary implements IDictionary, IListable {
|
||||
readonly addToDictionaryAction: boolean;
|
||||
readonly caseInsensitive: boolean;
|
||||
readonly description?: string;
|
||||
readonly description: string;
|
||||
readonly dossierTemplateId?: string;
|
||||
entries: List;
|
||||
readonly hexColor?: string;
|
||||
@ -17,7 +17,7 @@ export class Dictionary implements IDictionary, IListable {
|
||||
constructor(dictionary: IDictionary, readonly virtual = false) {
|
||||
this.addToDictionaryAction = !!dictionary.addToDictionaryAction;
|
||||
this.caseInsensitive = !!dictionary.caseInsensitive;
|
||||
this.description = dictionary.description;
|
||||
this.description = dictionary.description ?? '';
|
||||
this.dossierTemplateId = dictionary.dossierTemplateId;
|
||||
this.entries = dictionary.entries ?? [];
|
||||
this.hexColor = dictionary.hexColor;
|
||||
|
||||
11
libs/red-domain/src/lib/file-attributes/field.ts
Normal file
11
libs/red-domain/src/lib/file-attributes/field.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { IListable } from '@iqser/common-ui';
|
||||
import { FileAttributeConfigType } from '@red/domain';
|
||||
|
||||
export interface IField extends IListable {
|
||||
id: string;
|
||||
csvColumn: string;
|
||||
name: string;
|
||||
type: FileAttributeConfigType;
|
||||
readonly: boolean;
|
||||
primaryAttribute: boolean;
|
||||
}
|
||||
@ -2,3 +2,4 @@ export * from './file-attribute-config';
|
||||
export * from './file-attribute-config.model';
|
||||
export * from './file-attributes';
|
||||
export * from './file-attributes-config';
|
||||
export * from './field';
|
||||
|
||||
@ -2,3 +2,5 @@ export * from './matched-document';
|
||||
export * from './matched-section';
|
||||
export * from './search.request';
|
||||
export * from './search.response';
|
||||
export * from './search-list-item';
|
||||
export * from './search-input';
|
||||
|
||||
6
libs/red-domain/src/lib/search/search-input.ts
Normal file
6
libs/red-domain/src/lib/search/search-input.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { List } from '@iqser/common-ui';
|
||||
|
||||
export interface ISearchInput {
|
||||
readonly query: string;
|
||||
readonly dossierIds?: List;
|
||||
}
|
||||
12
libs/red-domain/src/lib/search/search-list-item.ts
Normal file
12
libs/red-domain/src/lib/search/search-list-item.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { IListable, List } from '@iqser/common-ui';
|
||||
|
||||
export interface ISearchListItem extends IListable {
|
||||
readonly dossierId: string;
|
||||
readonly filename: string;
|
||||
readonly unmatched: List | null;
|
||||
readonly highlights: Record<string, List>;
|
||||
readonly routerLink: string;
|
||||
readonly status: string;
|
||||
readonly dossierName: string;
|
||||
readonly numberOfPages: number;
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "redaction",
|
||||
"version": "2.334.0",
|
||||
"version": "2.344.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@ -82,6 +82,7 @@
|
||||
"@types/node": "16.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "4.33.0",
|
||||
"@typescript-eslint/parser": "4.33.0",
|
||||
"axios": "^0.24.0",
|
||||
"cypress": "^6.9.1",
|
||||
"cypress-file-upload": "^5.0.8",
|
||||
"cypress-keycloak": "^1.7.0",
|
||||
|
||||
Binary file not shown.
@ -1,7 +0,0 @@
|
||||
# Run Example
|
||||
|
||||
node translate.js ./../../apps/red-ui/src/assets/i18n/en.json de AIzaSyC2fOUHLV6nhmCSwKcacaNqumn20k8Ic_M
|
||||
|
||||
# API KEY
|
||||
|
||||
AIzaSyC2fOUHLV6nhmCSwKcacaNqumn20k8Ic_M
|
||||
@ -1,133 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const moment = require('moment');
|
||||
const _ = require('lodash');
|
||||
const path = require('path');
|
||||
const agent = require('superagent-promise')(require('superagent'), Promise);
|
||||
const { translate } = require('google-translate-api-browser');
|
||||
let dicc = {};
|
||||
|
||||
//Lang Codes https://ctrlq.org/code/19899-google-translate-languages
|
||||
|
||||
if (process.argv.length >= 4) {
|
||||
//Args
|
||||
const inputFile = process.argv[2];
|
||||
const destinationCodes = process.argv[3].split(',');
|
||||
const apiKey = process.argv.length > 4 && process.argv[4];
|
||||
|
||||
const apiUrl = _.template('https://www.googleapis.com/language/translate/v2?key=<%= apiKey %>&q=<%= value %>&source=en&target=<%= languageKey %>');
|
||||
|
||||
const transformResponse = (res) => {
|
||||
return _.get(JSON.parse(res.text), ['data', 'translations', 0, 'translatedText'], '');
|
||||
};
|
||||
|
||||
const getCache = (languageKey) => {
|
||||
try {
|
||||
dicc[languageKey] = {};
|
||||
let fileContent = fs.readFileSync(`./translateCache-${languageKey}.txt`, 'utf-8').split('\n');
|
||||
fileContent.map((line) => {
|
||||
let cached = line.split('|');
|
||||
if (cached[0]) dicc[languageKey][cached[0]] = cached[1];
|
||||
});
|
||||
} catch (error) {}
|
||||
};
|
||||
const cachedIndex = (key, value, languageKey) => {
|
||||
const line = key + '|' + value + '\n';
|
||||
dicc[languageKey][key] = value;
|
||||
fs.appendFileSync(`./translateCache-${languageKey}.txt`, line);
|
||||
return value;
|
||||
};
|
||||
|
||||
function iterLeaves(value, keyChain, accumulator, languageKey) {
|
||||
accumulator = accumulator || {};
|
||||
keyChain = keyChain || [];
|
||||
if (_.isObject(value)) {
|
||||
return _.chain(value)
|
||||
.reduce((handlers, v, k) => {
|
||||
return handlers.concat(iterLeaves(v, keyChain.concat(k), accumulator, languageKey));
|
||||
}, [])
|
||||
.flattenDeep()
|
||||
.value();
|
||||
} else {
|
||||
if (typeof value !== 'string') return value;
|
||||
|
||||
return function () {
|
||||
if (!(value in dicc[languageKey])) {
|
||||
console.log(
|
||||
_.template('Translating <%= value %> to <%= languageKey %>')({
|
||||
value,
|
||||
languageKey
|
||||
})
|
||||
);
|
||||
|
||||
let prom;
|
||||
//Translates individual string to language code
|
||||
if (apiKey != '') {
|
||||
//using apiKey
|
||||
prom = agent(
|
||||
'GET',
|
||||
apiUrl({
|
||||
value: encodeURI(value),
|
||||
languageKey,
|
||||
apiKey
|
||||
})
|
||||
).then(transformResponse);
|
||||
} else {
|
||||
//using free api key
|
||||
prom = translate(value, { to: languageKey });
|
||||
}
|
||||
|
||||
return prom
|
||||
.then((res) => cachedIndex(value, res, languageKey))
|
||||
.catch((err) => console.log(err))
|
||||
.then((text) => {
|
||||
//Sets the value in the accumulator
|
||||
_.set(accumulator, keyChain, text);
|
||||
|
||||
//This needs to be returned to it's eventually written to json
|
||||
return accumulator;
|
||||
});
|
||||
} else {
|
||||
console.log(value + ' cached: ' + dicc[languageKey][value]);
|
||||
_.set(accumulator, keyChain, dicc[languageKey][value]);
|
||||
return accumulator;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(
|
||||
_.reduce(
|
||||
destinationCodes,
|
||||
(sum, languageKey) => {
|
||||
const fileName = _.template('<%= languageKey %>.json')({
|
||||
languageKey
|
||||
});
|
||||
|
||||
//read languageKey Cache.
|
||||
getCache(languageKey);
|
||||
|
||||
//Starts with the top level strings
|
||||
return sum.concat(
|
||||
_.reduce(
|
||||
iterLeaves(JSON.parse(fs.readFileSync(path.resolve(inputFile), 'utf-8')), undefined, undefined, languageKey),
|
||||
(promiseChain, fn) => {
|
||||
return promiseChain.then(fn);
|
||||
},
|
||||
Promise.resolve()
|
||||
)
|
||||
.then((payload) => {
|
||||
fs.writeFileSync('./../../apps/red-ui/src/assets/i18n/' + fileName, JSON.stringify(payload));
|
||||
})
|
||||
.then(_.partial(console.log, 'Successfully translated all nodes, file output at ' + fileName))
|
||||
);
|
||||
},
|
||||
[]
|
||||
)
|
||||
).then(() => {
|
||||
process.exit();
|
||||
});
|
||||
} else {
|
||||
console.error('You must provide an input json file and a comma-separated list of destination language codes.');
|
||||
}
|
||||
78
tools/auto-i18n/unflatten.js
Normal file
78
tools/auto-i18n/unflatten.js
Normal file
@ -0,0 +1,78 @@
|
||||
const fs = require('fs');
|
||||
const axios = require('axios');
|
||||
|
||||
function flatten(data) {
|
||||
const result = {};
|
||||
|
||||
function recurse(cur, prop) {
|
||||
if (Object(cur) !== cur) {
|
||||
result[prop] = cur;
|
||||
} else if (Array.isArray(cur)) {
|
||||
for (let i = 0, l = cur.length; i < l; i++) recurse(cur[i], prop + '[' + i + ']');
|
||||
if (l === 0) result[prop] = [];
|
||||
} else {
|
||||
let isEmpty = true;
|
||||
for (const p in cur) {
|
||||
isEmpty = false;
|
||||
recurse(cur[p], prop ? prop + '.' + p : p);
|
||||
}
|
||||
if (isEmpty && prop) result[prop] = {};
|
||||
}
|
||||
}
|
||||
|
||||
recurse(data, '');
|
||||
return result;
|
||||
}
|
||||
|
||||
function unflatten(data) {
|
||||
if (Object(data) !== data || Array.isArray(data)) return data;
|
||||
const regex = /\.?([^.\[\]]+)|\[(\d+)\]/g,
|
||||
resultholder = {};
|
||||
for (const p in data) {
|
||||
let cur = resultholder,
|
||||
prop = '',
|
||||
m;
|
||||
while ((m = regex.exec(p))) {
|
||||
cur = cur[prop] || (cur[prop] = m[2] ? [] : {});
|
||||
prop = m[2] || m[1];
|
||||
}
|
||||
cur[prop] = data[p];
|
||||
}
|
||||
return resultholder[''] || resultholder;
|
||||
}
|
||||
|
||||
async function execute() {
|
||||
// const flatGerman = JSON.parse(fs.readFileSync('de-flat.json', 'utf-8'));
|
||||
|
||||
const german = JSON.parse(fs.readFileSync('./../../apps/red-ui/src/assets/i18n/de.json', 'utf-8'));
|
||||
const flatGerman = flatten(german);
|
||||
|
||||
const english = JSON.parse(fs.readFileSync('./../../apps/red-ui/src/assets/i18n/en.json', 'utf-8'));
|
||||
|
||||
const flatEnglish = flatten(english);
|
||||
|
||||
const apiKey = 'AIzaSyBiqNTundSKFjAJnSb4wSVLDU6w0Kv651M';
|
||||
|
||||
for (const key of Object.keys(flatEnglish)) {
|
||||
if (!flatGerman[key]) {
|
||||
const value = flatEnglish[key];
|
||||
const apiUrl = `https://www.googleapis.com/language/translate/v2?key=${apiKey}&q=${value}&source=en&target=de`;
|
||||
|
||||
const response = await axios.get(apiUrl);
|
||||
let translated = flatEnglish[key];
|
||||
try {
|
||||
translated = response.data.data.translations[0].translatedText;
|
||||
} catch (e) {}
|
||||
console.log('missing: ' + key + ' -> ' + flatEnglish[key] + ' -> ' + translated);
|
||||
flatGerman[key] = translated;
|
||||
}
|
||||
}
|
||||
|
||||
const mergedGerman = { ...flatEnglish, ...flatGerman };
|
||||
|
||||
const finalGerman = unflatten(mergedGerman);
|
||||
|
||||
fs.writeFileSync('./../../apps/red-ui/src/assets/i18n/de.json', JSON.stringify(finalGerman));
|
||||
}
|
||||
|
||||
execute().then();
|
||||
12
yarn.lock
12
yarn.lock
@ -3982,6 +3982,13 @@ axios@^0.18.0:
|
||||
follow-redirects "1.5.10"
|
||||
is-buffer "^2.0.2"
|
||||
|
||||
axios@^0.24.0:
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"
|
||||
integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==
|
||||
dependencies:
|
||||
follow-redirects "^1.14.4"
|
||||
|
||||
axobject-query@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
|
||||
@ -7323,6 +7330,11 @@ follow-redirects@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
|
||||
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
|
||||
|
||||
follow-redirects@^1.14.4:
|
||||
version "1.14.5"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381"
|
||||
integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==
|
||||
|
||||
for-in@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user