Search archived WIP

This commit is contained in:
Adina Țeudan 2022-03-03 22:53:23 +02:00
parent e4df3bb8b0
commit 1533bbfc95
24 changed files with 130 additions and 58 deletions

View File

@ -67,6 +67,16 @@ const routes: Routes = [
routeGuards: [AuthGuard, RedRoleGuard],
},
},
{
path: 'main/search',
component: BaseScreenComponent,
loadChildren: () => import('./modules/search/search.module').then(m => m.SearchModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, DossiersGuard],
requiredRoles: ['RED_USER', 'RED_MANAGER'],
},
},
{
path: '**',
redirectTo: 'main/dossiers',

View File

@ -57,7 +57,12 @@ export class BaseScreenComponent {
action: (query): void => this._searchThisDossier(query),
},
{
text: this._translateService.instant('search.entire-platform'),
text: this._translateService.instant('search.active-dossiers'),
icon: 'red:enter',
action: (query): void => this._search(query, [], true),
},
{
text: this._translateService.instant('search.all-dossiers'),
icon: 'red:enter',
action: (query): void => this._search(query, []),
},
@ -85,7 +90,7 @@ export class BaseScreenComponent {
return true;
}
const isDossierOverview = routerLink.includes('dossiers') && routerLink.length === 3;
const isDossierOverview = (routerLink.includes('dossiers') || routerLink.includes('archive')) && routerLink.length === 3;
return !isDossierOverview;
}
@ -93,9 +98,9 @@ export class BaseScreenComponent {
return item.name;
}
private _search(query: string, dossierIds: string[]) {
const queryParams = { query, dossierIds: dossierIds.join(',') };
this._router.navigate(['main/dossiers/search'], { queryParams }).then();
private _search(query: string, dossierIds: string[], onlyActive = false) {
const queryParams = { query, dossierIds: dossierIds.join(','), onlyActive };
this._router.navigate(['main/search'], { queryParams }).then();
}
private _searchThisDossier(query: string) {

View File

@ -1,16 +1,29 @@
import { Injectable, Injector, ProviderToken } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
import { firstValueFrom } from 'rxjs';
import { firstValueFrom, forkJoin } from 'rxjs';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { ActiveDossiersService } from '@services/entity-services/active-dossiers.service';
import { ArchivedDossiersService } from '@services/entity-services/archived-dossiers.service';
import { take } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class DossiersGuard implements CanActivate {
constructor(private readonly _injector: Injector) {}
constructor(
private readonly _injector: Injector,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _archivedDossiersService: ArchivedDossiersService,
) {}
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
const token: ProviderToken<DossiersService> = route.data.dossiersService;
const dossiersService: DossiersService = this._injector.get<DossiersService>(token);
await firstValueFrom(dossiersService.loadAll());
if (token) {
const dossiersService: DossiersService = this._injector.get<DossiersService>(token);
await firstValueFrom(dossiersService.loadAll());
} else {
const services = [this._archivedDossiersService, this._activeDossiersService];
const loading$ = forkJoin(services.map(service => service.loadAll().pipe(take(1))));
await firstValueFrom(loading$);
}
return true;
}
}

View File

@ -22,7 +22,7 @@ import { distinctUntilChanged, map } from 'rxjs/operators';
import { DossiersDialogService } from '../../../services/dossiers-dialog.service';
import { FilesService } from '@services/entity-services/files.service';
import { FileManagementService } from '@services/entity-services/file-management.service';
import { workflowFileStatusTranslations } from '../../../translations/file-status-translations';
import { workflowFileStatusTranslations } from '../../../../../translations/file-status-translations';
import { PermissionsService } from '@services/permissions.service';
import { UserService } from '@services/user.service';

View File

@ -1,6 +1,5 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SearchScreenComponent } from './screens/search-screen/search-screen.component';
import { FilePreviewGuard } from '@guards/file-preview.guard';
import { DossierFilesGuard } from '@guards/dossier-files-guard';
import { CompositeRouteGuard } from '@iqser/common-ui';
@ -9,10 +8,6 @@ import { DOSSIER_ID, FILE_ID } from '@utils/constants';
import { ACTIVE_DOSSIERS_SERVICE } from '../../tokens';
const routes: Routes = [
{
path: 'search',
component: SearchScreenComponent,
},
{
path: `:${DOSSIER_ID}`,
canActivate: [CompositeRouteGuard],

View File

@ -8,15 +8,12 @@ import { DossiersRoutingModule } from './dossiers-routing.module';
import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module';
import { ChangeLegalBasisDialogComponent } from './dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component';
import { RecategorizeImageDialogComponent } from './dialogs/recategorize-image-dialog/recategorize-image-dialog.component';
import { SearchScreenComponent } from './screens/search-screen/search-screen.component';
import { OverlayModule } from '@angular/cdk/overlay';
import { SharedDossiersModule } from './shared/shared-dossiers.module';
import { ResizeAnnotationDialogComponent } from './dialogs/resize-annotation-dialog/resize-annotation-dialog.component';
import { HighlightActionDialogComponent } from './screens/file-preview-screen/dialogs/highlight-action-dialog/highlight-action-dialog.component';
import { ColorPickerModule } from 'ngx-color-picker';
const screens = [SearchScreenComponent];
const dialogs = [
ManualAnnotationDialogComponent,
ForceAnnotationDialogComponent,
@ -27,10 +24,8 @@ const dialogs = [
HighlightActionDialogComponent,
];
const components = [...screens, ...dialogs];
@NgModule({
declarations: [...components],
declarations: [...dialogs],
imports: [
CommonModule,
SharedModule,

View File

@ -3,7 +3,7 @@ import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/si
import { TranslateChartService } from '@services/translate-chart.service';
import { UserService } from '@services/user.service';
import { FilterService, ProgressBarConfigModel, shareLast, Toaster } from '@iqser/common-ui';
import { workflowFileStatusTranslations } from '../../../../translations/file-status-translations';
import { workflowFileStatusTranslations } from '../../../../../../translations/file-status-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { Dossier, DossierAttributeWithValue, DossierStats, IDossierRequest, StatusSorter, User } from '@red/domain';
import { ActivatedRoute } from '@angular/router';

View File

@ -12,7 +12,7 @@ import {
WorkflowConfig,
} from '@iqser/common-ui';
import { File, IFileAttributeConfig, StatusSorter, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain';
import { workflowFileStatusTranslations } from '../../translations/file-status-translations';
import { workflowFileStatusTranslations } from '../../../../translations/file-status-translations';
import { PermissionsService } from '@services/permissions.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core';

View File

@ -4,7 +4,7 @@ import { FilterService, mapEach } from '@iqser/common-ui';
import { ActiveDossiersService } from '@services/entity-services/active-dossiers.service';
import { combineLatest, Observable } from 'rxjs';
import { DossierStats, FileCountPerWorkflowStatus, StatusSorter } from '@red/domain';
import { workflowFileStatusTranslations } from '../../../../translations/file-status-translations';
import { workflowFileStatusTranslations } from '../../../../../../translations/file-status-translations';
import { TranslateChartService } from '@services/translate-chart.service';
import { filter, map, switchMap } from 'rxjs/operators';
import { DossierStatsService } from '@services/entity-services/dossier-stats.service';

View File

@ -5,7 +5,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core';
import { UserPreferenceService } from '@services/user-preference.service';
import { UserService } from '@services/user.service';
import { workflowFileStatusTranslations } from '../../translations/file-status-translations';
import { workflowFileStatusTranslations } from '../../../../translations/file-status-translations';
import { dossierMemberChecker, dossierStateChecker, dossierTemplateChecker, RedactionFilterSorter } from '@utils/index';
import { workloadTranslations } from '../../translations/workload-translations';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';

View File

@ -3,7 +3,7 @@ import { Dossier, File, StatusBarConfigs, User } from '@red/domain';
import { List, LoadingService, Toaster } from '@iqser/common-ui';
import { PermissionsService } from '@services/permissions.service';
import { FileAssignService } from '../../../../shared/services/file-assign.service';
import { workflowFileStatusTranslations } from '../../../../translations/file-status-translations';
import { workflowFileStatusTranslations } from '../../../../../../translations/file-status-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { UserService } from '@services/user.service';
import { FilesService } from '@services/entity-services/files.service';

View File

@ -74,8 +74,11 @@
></iqser-status-bar>
</div>
<div class="cell small-label">
{{ item.dossierName }}
<div class="cell small-label stats-subtitle">
<div>
<mat-icon *ngIf="item.dossierStatus === 'ARCHIVED'" svgIcon="red:archive"></mat-icon>
{{ item.dossierName }}
</div>
</div>
<div class="cell small-label stats-subtitle">

View File

@ -14,13 +14,14 @@ import { combineLatest, 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 { workflowFileStatusTranslations } from '../../translations/file-status-translations';
import { workflowFileStatusTranslations } from '../../../translations/file-status-translations';
import { TranslateService } from '@ngx-translate/core';
import { RouterHistoryService } from '@services/router-history.service';
import { ActiveDossiersService } from '@services/entity-services/active-dossiers.service';
import { Dossier, IMatchedDocument, ISearchListItem, ISearchResponse } from '@red/domain';
import { Dossier, DossierStatuses, IMatchedDocument, ISearchListItem, ISearchResponse } from '@red/domain';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { PlatformSearchService } from '@services/entity-services/platform-search.service';
import { ArchivedDossiersService } from '@services/entity-services/archived-dossiers.service';
@Component({
templateUrl: './search-screen.component.html',
@ -43,8 +44,14 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
readonly searchResults$ = combineLatest([this._queryChanged, this._filtersChanged$]).pipe(
tap(() => this._loadingService.start()),
tap(([query, dossierIds]) => this._updateNavigation(query, dossierIds)),
switchMap(([query, dossierIds]) => this._platformSearchService.search({ query, dossierIds })),
tap(([query, [dossierIds, onlyActive]]) => this._updateNavigation(query, dossierIds, onlyActive)),
switchMap(([query, [dossierIds, onlyActive]]) =>
this._platformSearchService.search({
query,
dossierIds,
dossierStatus: onlyActive ? [] : [DossierStatuses.ACTIVE, DossierStatuses.ARCHIVED],
}),
),
map(searchResult => this._toMatchedDocuments(searchResult)),
map(docs => this._toListItems(docs)),
tap(result => this.entitiesService.setEntities(result)),
@ -57,6 +64,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
private readonly _activatedRoute: ActivatedRoute,
private readonly _loadingService: LoadingService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _archivedDossiersService: ArchivedDossiersService,
readonly routerHistoryService: RouterHistoryService,
private readonly _translateService: TranslateService,
private readonly _filesMapService: FilesMapService,
@ -71,12 +79,13 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
const checked = dossierIds.includes(id);
return new NestedFilter({ id, label: dossierName, checked });
};
const allDossiers = [...this._activeDossiersService.all, ...this._archivedDossiersService.all];
const dossierNameFilter: IFilterGroup = {
slug: 'dossiers',
label: this._translateService.instant('search-screen.filters.by-dossier'),
filterceptionPlaceholder: this._translateService.instant('search-screen.filters.search-placeholder'),
icon: 'red:folder',
filters: this._activeDossiersService.all.map(dossierToFilter),
filters: allDossiers.map(dossierToFilter),
checker: keyChecker('dossierId'),
};
this.filterService.addFilterGroups([dossierNameFilter]);
@ -86,6 +95,10 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
return this._activatedRoute.snapshot.queryParamMap.get('dossierIds').split(',');
}
private get _routeOnlyActive(): boolean {
return this._activatedRoute.snapshot.queryParamMap.get('onlyActive') === 'true';
}
private get _routeQuery(): string {
return this._activatedRoute.snapshot.queryParamMap.get('query');
}
@ -98,10 +111,14 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
);
}
private get _filtersChanged$(): Observable<string[]> {
private get _filtersChanged$(): Observable<[string[], boolean]> {
return this.filterService.filterGroups$.pipe(
map(groups => groups[0].filters.filter(v => v.checked).map(v => v.id)),
startWith(this._routeDossierIds),
map(groups => {
const dossierIds: string[] = groups[0].filters.filter(v => v.checked).map(v => v.id);
// TODO: Only active filter
return [dossierIds, this._routeOnlyActive];
}),
startWith<[string[], boolean]>([this._routeDossierIds, this._routeOnlyActive]),
);
}
@ -110,8 +127,8 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
this.searchService.searchValue = newQuery ?? '';
}
private _updateNavigation(query?: string, dossierIds?: string[]): Promise<boolean> {
const queryParams = { query, dossierIds: dossierIds.join(',') };
private _updateNavigation(query?: string, dossierIds?: string[], onlyActive?: boolean): Promise<boolean> {
const queryParams = { query, dossierIds: dossierIds.join(','), onlyActive };
return this._router.navigate([], { queryParams, replaceUrl: true });
}
@ -123,24 +140,29 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
return matchedDocuments.map(document => this._toListItem(document)).filter(value => value);
}
private _toListItem({ dossierId, fileId, unmatchedTerms, highlights, score }: IMatchedDocument): ISearchListItem {
private _toListItem({ dossierId, fileId, unmatchedTerms, highlights, score, dossierStatus }: IMatchedDocument): ISearchListItem {
const file = this._filesMapService.get(dossierId, fileId);
if (!file) {
return undefined;
}
const dossier = (dossierStatus === DossierStatuses.ARCHIVED ? this._archivedDossiersService : this._activeDossiersService).find(
dossierId,
);
return {
id: fileId,
dossierId,
dossierStatus,
unmatched: unmatchedTerms || null,
highlights,
status: file.workflowStatus,
assignee: file.assignee,
numberOfPages: file.numberOfPages,
dossierName: this._activeDossiersService.find(dossierId).dossierName,
dossierName: dossier.dossierName,
filename: file.filename,
searchKey: score.toString(),
routerLink: `/main/dossiers/${dossierId}/file/${fileId}`,
routerLink: file.routerLink,
};
}
}

View File

@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SearchScreenComponent } from './search-screen/search-screen.component';
import { RouterModule } from '@angular/router';
import { CommonUiModule } from '@iqser/common-ui';
import { SharedModule } from '../shared/shared.module';
const routes = [{ path: '', component: SearchScreenComponent }];
@NgModule({
declarations: [SearchScreenComponent],
imports: [CommonModule, RouterModule.forChild(routes), CommonUiModule, SharedModule],
})
export class SearchModule {}

View File

@ -6,6 +6,8 @@ import { mapTo, switchMap } from 'rxjs/operators';
import { ActiveDossiersService } from './active-dossiers.service';
import { FilesMapService } from './files-map.service';
import { FilesService } from './files.service';
import { DossiersService } from './dossiers.service';
import { ArchivedDossiersService } from './archived-dossiers.service';
@Injectable({ providedIn: 'root' })
export class PlatformSearchService extends GenericService<ISearchResponse> {
@ -13,12 +15,13 @@ export class PlatformSearchService extends GenericService<ISearchResponse> {
protected readonly _injector: Injector,
private readonly _filesService: FilesService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _archivedDossiersService: ArchivedDossiersService,
private readonly _filesMapService: FilesMapService,
) {
super(_injector, 'search-v2');
}
search({ dossierIds, query }: ISearchInput): Observable<ISearchResponse> {
search({ dossierIds, query, dossierStatus }: ISearchInput): Observable<ISearchResponse> {
if (!query) {
return of({
matchedDocuments: [],
@ -32,19 +35,22 @@ export class PlatformSearchService extends GenericService<ISearchResponse> {
page: 0,
returnSections: false,
pageSize: 300,
dossierStatus,
};
return this._post(body).pipe(switchMap(searchValue => this._loadMissingFiles$(searchValue)));
}
private _loadMissingFiles$(searchResponse: ISearchResponse): Observable<ISearchResponse> {
const documentsOfActiveDossiers = searchResponse.matchedDocuments.filter(document =>
this._activeDossiersService.has(document.dossierId),
);
private _dossiersWithMissingFiles(searchResponse: ISearchResponse, service: DossiersService): Dossier[] {
const documentsOfType = searchResponse.matchedDocuments.filter(document => service.has(document.dossierId));
const fileNotLoaded = ({ dossierId, fileId }: IMatchedDocument) => !this._filesMapService.get(dossierId, fileId);
const dossiersWithNotLoadedFiles = documentsOfActiveDossiers.filter(fileNotLoaded).map(document => document.dossierId);
const dossiersWithNotLoadedFiles = documentsOfType.filter(fileNotLoaded).map(document => document.dossierId);
return Array.from(new Set(dossiersWithNotLoadedFiles)).map(dossierId => service.find(dossierId));
}
const dossiers = Array.from(new Set(dossiersWithNotLoadedFiles)).map(dossierId => this._activeDossiersService.find(dossierId));
private _loadMissingFiles$(searchResponse: ISearchResponse): Observable<ISearchResponse> {
const services = [this._activeDossiersService, this._archivedDossiersService];
const dossiers = services.map(service => this._dossiersWithMissingFiles(searchResponse, service)).flat();
return dossiers.length ? this._loadFilesFor$(dossiers).pipe(mapTo(searchResponse)) : of(searchResponse);
}

View File

@ -10,7 +10,7 @@ export class RouterHistoryService {
constructor(private readonly _router: Router) {
this._router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe((event: NavigationEnd) => {
if (event.url.startsWith('/main/dossiers') && !event.url.includes('/search')) {
if (event.url.startsWith('/main/dossiers')) {
this._lastDossiersScreen = event.url;
}
});

View File

@ -1587,7 +1587,7 @@
"table-header": "{length} {length, plural, one{Suchergebnis} other{Suchergebnisse}}"
},
"search": {
"entire-platform": "ganze Plattform",
"active-dossiers": "ganze Plattform",
"placeholder": "Nach Dokumenten oder Dokumenteninhalt suchen",
"this-dossier": "in diesem Dossier"
},

View File

@ -1763,7 +1763,8 @@
"table-header": "{length} search {length, plural, one{result} other{results}}"
},
"search": {
"entire-platform": "across all dossiers",
"active-dossiers": "documents in active dossiers",
"all-dossiers": "all documents",
"placeholder": "Search documents...",
"this-dossier": "in this dossier"
},

View File

@ -1,10 +1,12 @@
import { IMatchedSection } from './matched-section';
import { List } from '@iqser/common-ui';
import { DossierStatus } from '../dossiers';
export interface IMatchedDocument {
containsAllMatchedSections?: boolean;
dossierId?: string;
dossierTemplateId?: string;
dossierStatus?: DossierStatus;
fileId?: string;
highlights?: { [key: string]: List };
matchedSections?: List<IMatchedSection>;

View File

@ -1,6 +1,8 @@
import { List } from '@iqser/common-ui';
import { DossierStatus } from '../dossiers';
export interface ISearchInput {
readonly query: string;
readonly dossierIds?: List;
readonly dossierStatus?: List<DossierStatus>;
}

View File

@ -1,7 +1,9 @@
import { IListable, List } from '@iqser/common-ui';
import { DossierStatus } from '../dossiers';
export interface ISearchListItem extends IListable {
readonly dossierId: string;
readonly dossierStatus: DossierStatus;
readonly filename: string;
readonly assignee: string;
readonly unmatched: List | null;

View File

@ -1,11 +1,13 @@
import { List } from '@iqser/common-ui';
import { DossierStatus } from '../dossiers';
export interface ISearchRequest {
dossierIds?: List;
dossierTemplateIds?: List;
fileId?: string;
page?: number;
pageSize?: number;
queryString?: string;
returnSections?: boolean;
readonly dossierIds?: List;
readonly dossierTemplateIds?: List;
readonly dossierStatus?: List<DossierStatus>;
readonly fileId?: string;
readonly page?: number;
readonly pageSize?: number;
readonly queryString?: string;
readonly returnSections?: boolean;
}