Merge branch 'master' into VM/NotificationsPreferences

This commit is contained in:
Valentin 2021-11-09 22:26:11 +02:00
commit 7b5f5a296b
28 changed files with 1379 additions and 940 deletions

1
.gitignore vendored
View File

@ -45,3 +45,4 @@ version.properties
paligo-styles/style.css*
migrations.json
*.iml

View File

@ -1,4 +1,7 @@
{
"cli": {
"analytics": "d22ff5ae-c863-4253-83e3-0a969e4bb5fe"
},
"version": 1,
"projects": {
"common-ui": {

View File

@ -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();

View File

@ -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>

View File

@ -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,
};
}
}

View File

@ -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();
}

View File

@ -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 {

View File

@ -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,

View File

@ -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';

View File

@ -47,9 +47,7 @@
{{ 'search-screen.missing' | translate }}:<span *ngFor="let term of unmatched"
>&nbsp;<s>{{ term }}</s></span
>.&nbsp;{{ '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"
>&nbsp;<u>{{ term }}</u></span
>
</span>

View File

@ -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}`,
};
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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;

View File

@ -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"]

View File

@ -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;

View 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;
}

View File

@ -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';

View File

@ -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';

View File

@ -0,0 +1,6 @@
import { List } from '@iqser/common-ui';
export interface ISearchInput {
readonly query: string;
readonly dossierIds?: List;
}

View 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;
}

View File

@ -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.

View File

@ -1,7 +0,0 @@
# Run Example
node translate.js ./../../apps/red-ui/src/assets/i18n/en.json de AIzaSyC2fOUHLV6nhmCSwKcacaNqumn20k8Ic_M
# API KEY
AIzaSyC2fOUHLV6nhmCSwKcacaNqumn20k8Ic_M

View File

@ -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.');
}

View 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();

View File

@ -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"