RED-9321: refactored trash and search modules.
This commit is contained in:
parent
0fe7584f4b
commit
c4912ffd8f
@ -147,7 +147,7 @@ const mainRoutes: IqserRoutes = [
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
loadChildren: () => import('./modules/search/search.module').then(m => m.SearchModule),
|
||||
loadComponent: () => import('./modules/search/search-screen/search-screen.component'),
|
||||
canActivate: [CompositeRouteGuard, IqserPermissionsGuard, loadAllDossiersGuard()],
|
||||
data: {
|
||||
routeGuards: [IqserAuthGuard, RedRoleGuard],
|
||||
@ -159,7 +159,7 @@ const mainRoutes: IqserRoutes = [
|
||||
},
|
||||
{
|
||||
path: 'trash',
|
||||
loadChildren: () => import('./modules/trash/trash.module').then(m => m.TrashModule),
|
||||
loadChildren: () => import('./modules/trash/trash.routes'),
|
||||
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
|
||||
data: {
|
||||
routeGuards: [IqserAuthGuard, RedRoleGuard, TrashGuard],
|
||||
|
||||
@ -3,11 +3,31 @@ import { workflowFileStatusTranslations } from '@translations/file-status-transl
|
||||
import { ISearchListItem } from '@red/domain';
|
||||
import { escapeHtml } from '@iqser/common-ui/lib/utils';
|
||||
import { getDossierRouterLink } from '@utils/router-links';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { NgForOf, NgIf } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { StopPropagationDirective } from '@iqser/common-ui';
|
||||
import { InitialsAvatarComponent } from '@common-ui/users';
|
||||
import { StatusBarComponent } from '@common-ui/shared';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-search-item-template',
|
||||
templateUrl: './search-item-template.component.html',
|
||||
styleUrls: ['./search-item-template.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatTooltip,
|
||||
NgIf,
|
||||
TranslateModule,
|
||||
NgForOf,
|
||||
StopPropagationDirective,
|
||||
InitialsAvatarComponent,
|
||||
StatusBarComponent,
|
||||
MatIcon,
|
||||
RouterLink,
|
||||
],
|
||||
})
|
||||
export class SearchItemTemplateComponent implements OnChanges {
|
||||
@Input() item: ISearchListItem;
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core';
|
||||
import { ListingComponent, listingProvidersFactory, LoadingService, SearchPositions, TableColumnConfig } from '@iqser/common-ui';
|
||||
import {
|
||||
IqserListingModule,
|
||||
ListingComponent,
|
||||
listingProvidersFactory,
|
||||
LoadingService,
|
||||
SearchPositions,
|
||||
TableColumnConfig,
|
||||
} from '@iqser/common-ui';
|
||||
import { combineLatest, Observable, of } 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 { TranslateService } from '@ngx-translate/core';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { RouterHistoryService } from '@services/router-history.service';
|
||||
import {
|
||||
Dossier,
|
||||
@ -25,14 +32,18 @@ import { DossierTemplatesService } from '@services/dossier-templates/dossier-tem
|
||||
import { UserService } from '@users/user.service';
|
||||
import { IFilterGroup, keyChecker, NestedFilter } from '@iqser/common-ui/lib/filtering';
|
||||
import { SortingOrders } from '@iqser/common-ui/lib/sorting';
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { SearchItemTemplateComponent } from '../search-item-template/search-item-template.component';
|
||||
|
||||
@Component({
|
||||
templateUrl: './search-screen.component.html',
|
||||
styleUrls: ['./search-screen.component.scss'],
|
||||
providers: listingProvidersFactory(SearchScreenComponent),
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [IqserListingModule, TranslateModule, AsyncPipe, NgIf, SearchItemTemplateComponent],
|
||||
})
|
||||
export class SearchScreenComponent extends ListingComponent<ISearchListItem> implements OnDestroy {
|
||||
export default class SearchScreenComponent extends ListingComponent<ISearchListItem> implements OnDestroy {
|
||||
readonly searchPositions = SearchPositions;
|
||||
|
||||
readonly tableHeaderLabel = _('search-screen.table-header');
|
||||
@ -46,7 +57,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
|
||||
readonly searchResults$ = new Observable<ISearchListItem[]>();
|
||||
|
||||
readonly dossierTemplates$ = this._dossierTemplateService.loadAll().pipe(tap(templates => this._addTemplateFilter(templates)));
|
||||
readonly dossierTemplates$ = this._dossierTemplateService.loadAll().pipe(tap(templates => this.#addTemplateFilter(templates)));
|
||||
|
||||
constructor(
|
||||
private readonly _router: Router,
|
||||
@ -65,10 +76,10 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
if (!Object.keys(this._activatedRoute.snapshot.queryParams).length) {
|
||||
this._router.navigate(['main']).then();
|
||||
}
|
||||
this.searchResults$ = combineLatest([this._queryChanged$, this._filtersChanged$]).pipe(
|
||||
this.searchResults$ = combineLatest([this.#queryChanged$, this.#filtersChanged$]).pipe(
|
||||
tap(() => this._loadingService.start()),
|
||||
tap(([query, [dossierIds, workflowStatus, assignee, dossierTemplateIds, onlyActive]]) =>
|
||||
this._updateNavigation(query, dossierIds, workflowStatus, assignee, dossierTemplateIds, onlyActive),
|
||||
this.#updateNavigation(query, dossierIds, workflowStatus, assignee, dossierTemplateIds, onlyActive),
|
||||
),
|
||||
switchMap(([query, [dossierIds, workflowStatus, assignee, dossierTemplateIds, onlyActive]]) =>
|
||||
this._platformSearchService.search({
|
||||
@ -81,43 +92,43 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
includeArchivedDossiers: !onlyActive,
|
||||
}),
|
||||
),
|
||||
map(searchResult => this._toMatchedDocuments(searchResult)),
|
||||
map(docs => this._toListItems(docs)),
|
||||
map(searchResult => this.#toMatchedDocuments(searchResult)),
|
||||
map(docs => this.#toListItems(docs)),
|
||||
tap(result => this.entitiesService.setEntities(result)),
|
||||
tap(() => this._loadingService.stop()),
|
||||
);
|
||||
this.searchService.skip = true;
|
||||
this.sortingService.setSortingOption({ column: 'searchKey', order: SortingOrders.desc });
|
||||
this._initFilters();
|
||||
this.#initFilters();
|
||||
}
|
||||
|
||||
private get _routeDossierIds(): string[] {
|
||||
get #routeDossierIds(): string[] {
|
||||
return this._activatedRoute.snapshot.queryParamMap.get('dossierIds').split(',');
|
||||
}
|
||||
|
||||
private get _routeDossierTemplateIds(): string[] {
|
||||
get #routeDossierTemplateIds(): string[] {
|
||||
return this._activatedRoute.snapshot.queryParamMap.get('dossierTemplateIds')?.split(',');
|
||||
}
|
||||
|
||||
private get _routeStatus(): WorkflowFileStatus {
|
||||
get #routeStatus(): WorkflowFileStatus {
|
||||
return this._activatedRoute.snapshot.queryParamMap.get('status') as WorkflowFileStatus;
|
||||
}
|
||||
|
||||
private get _routeAssignee(): string {
|
||||
get #routeAssignee(): string {
|
||||
return this._activatedRoute.snapshot.queryParamMap.get('assignee');
|
||||
}
|
||||
|
||||
private get _routeOnlyActive(): boolean {
|
||||
get #routeOnlyActive(): boolean {
|
||||
return this._activatedRoute.snapshot.queryParamMap.get('onlyActive') === 'true';
|
||||
}
|
||||
|
||||
private get _routeQuery(): string {
|
||||
get #routeQuery(): string {
|
||||
return this._activatedRoute.snapshot.queryParamMap.get('query');
|
||||
}
|
||||
|
||||
private get _queryChanged$(): Observable<string> {
|
||||
get #queryChanged$(): Observable<string> {
|
||||
return this.searchService.valueChanges$.pipe(
|
||||
startWith(this._routeQuery),
|
||||
startWith(this.#routeQuery),
|
||||
tap(query => (this.searchService.searchValue = query)),
|
||||
debounceTime(300),
|
||||
);
|
||||
@ -127,7 +138,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
return this._featuresService.isEnabled(DOSSIERS_ARCHIVE);
|
||||
}
|
||||
|
||||
private get _filtersChanged$(): Observable<[string[], WorkflowFileStatus, string, string[], boolean]> {
|
||||
get #filtersChanged$(): Observable<[string[], WorkflowFileStatus, string, string[], boolean]> {
|
||||
const onlyActiveDossiers$ = this.#enabledArchive
|
||||
? this.filterService.getSingleFilter('onlyActiveDossiers').pipe(map(f => !!f.checked))
|
||||
: of(true);
|
||||
@ -141,11 +152,11 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
return [dossierIds, workflowStatus, assignee, dossierTemplateIds, onlyActive];
|
||||
}),
|
||||
startWith<[string[], WorkflowFileStatus, string, string[], boolean]>([
|
||||
this._routeDossierIds,
|
||||
this._routeStatus,
|
||||
this._routeAssignee,
|
||||
this._routeDossierTemplateIds,
|
||||
this._routeOnlyActive,
|
||||
this.#routeDossierIds,
|
||||
this.#routeStatus,
|
||||
this.#routeAssignee,
|
||||
this.#routeDossierTemplateIds,
|
||||
this.#routeOnlyActive,
|
||||
]),
|
||||
);
|
||||
}
|
||||
@ -155,8 +166,8 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
this.searchService.searchValue = newQuery ?? '';
|
||||
}
|
||||
|
||||
private _initFilters() {
|
||||
const dossierIds = this._routeDossierIds;
|
||||
#initFilters() {
|
||||
const dossierIds = this.#routeDossierIds;
|
||||
const dossierToFilter = ({ dossierName, id }: Dossier) => {
|
||||
const checked = dossierIds.includes(id);
|
||||
return new NestedFilter({ id, label: dossierName, checked });
|
||||
@ -170,7 +181,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
checker: keyChecker('dossierId'),
|
||||
};
|
||||
|
||||
const status = this._routeStatus;
|
||||
const status = this.#routeStatus;
|
||||
const statusToFilter = (workflowStatus: WorkflowFileStatus) => {
|
||||
const checked = status === workflowStatus;
|
||||
return new NestedFilter({
|
||||
@ -188,7 +199,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
checker: keyChecker('status'),
|
||||
};
|
||||
|
||||
const assignee = this._routeAssignee;
|
||||
const assignee = this.#routeAssignee;
|
||||
const assigneeToFilter = (userId: string) => {
|
||||
const checked = assignee === userId;
|
||||
return new NestedFilter({ id: userId, label: this._userService.getName(userId), checked });
|
||||
@ -215,12 +226,12 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
this.filterService.addFilterGroups([dossierNameFilter, workflowStatusFilter, assigneeFilter]);
|
||||
const onlyActiveLabel = this._translateService.instant('search-screen.filters.only-active');
|
||||
if (this.#enabledArchive) {
|
||||
this.filterService.addSingleFilter({ id: 'onlyActiveDossiers', label: onlyActiveLabel, checked: this._routeOnlyActive });
|
||||
this.filterService.addSingleFilter({ id: 'onlyActiveDossiers', label: onlyActiveLabel, checked: this.#routeOnlyActive });
|
||||
}
|
||||
}
|
||||
|
||||
private _addTemplateFilter(templates: DossierTemplate[]) {
|
||||
const templatesIds = this._routeDossierTemplateIds;
|
||||
#addTemplateFilter(templates: DossierTemplate[]) {
|
||||
const templatesIds = this.#routeDossierTemplateIds;
|
||||
const templateToFilter = ({ name, id }: DossierTemplate) => {
|
||||
const checked = templatesIds?.includes(id);
|
||||
return new NestedFilter({ id, label: name, checked });
|
||||
@ -236,7 +247,7 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
this.filterService.addFilterGroups([templateNameFilter]);
|
||||
}
|
||||
|
||||
private _updateNavigation(
|
||||
#updateNavigation(
|
||||
query?: string,
|
||||
dossierIds?: string[],
|
||||
workflowStatus?: WorkflowFileStatus,
|
||||
@ -255,15 +266,15 @@ export class SearchScreenComponent extends ListingComponent<ISearchListItem> imp
|
||||
return this._router.navigate([], { queryParams, replaceUrl: true });
|
||||
}
|
||||
|
||||
private _toMatchedDocuments({ matchedDocuments }: ISearchResponse): IMatchedDocument[] {
|
||||
#toMatchedDocuments({ matchedDocuments }: ISearchResponse): IMatchedDocument[] {
|
||||
return matchedDocuments.filter(doc => doc.score > 0 && doc.matchedTerms.length > 0);
|
||||
}
|
||||
|
||||
private _toListItems(matchedDocuments: IMatchedDocument[]): ISearchListItem[] {
|
||||
return matchedDocuments.map(document => this._toListItem(document)).filter(value => value);
|
||||
#toListItems(matchedDocuments: IMatchedDocument[]): ISearchListItem[] {
|
||||
return matchedDocuments.map(document => this.#toListItem(document)).filter(value => value);
|
||||
}
|
||||
|
||||
private _toListItem({
|
||||
#toListItem({
|
||||
dossierId,
|
||||
fileId,
|
||||
unmatchedTerms,
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SearchScreenComponent } from './search-screen/search-screen.component';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IqserListingModule, StopPropagationDirective } from '@iqser/common-ui';
|
||||
import { SharedModule } from '@shared/shared.module';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SearchItemTemplateComponent } from './search-item-template/search-item-template.component';
|
||||
import { InitialsAvatarComponent, IqserUsersModule } from '@iqser/common-ui/lib/users';
|
||||
import { StatusBarComponent } from '@iqser/common-ui/lib/shared';
|
||||
|
||||
const routes = [{ path: '', component: SearchScreenComponent }];
|
||||
|
||||
@NgModule({
|
||||
declarations: [SearchScreenComponent, SearchItemTemplateComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharedModule,
|
||||
IqserUsersModule,
|
||||
TranslateModule,
|
||||
IqserListingModule,
|
||||
StatusBarComponent,
|
||||
StopPropagationDirective,
|
||||
InitialsAvatarComponent,
|
||||
],
|
||||
})
|
||||
export class SearchModule {}
|
||||
@ -1,6 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { ListingComponent, listingProvidersFactory, LoadingService, TableColumnConfig } from '@iqser/common-ui';
|
||||
import {
|
||||
CircleButtonComponent,
|
||||
IqserListingModule,
|
||||
ListingComponent,
|
||||
listingProvidersFactory,
|
||||
LoadingService,
|
||||
TableColumnConfig,
|
||||
} from '@iqser/common-ui';
|
||||
import { SortingOrders } from '@iqser/common-ui/lib/sorting';
|
||||
import { TrashItem } from '@red/domain';
|
||||
import { TrashService } from '@services/entity-services/trash.service';
|
||||
@ -8,6 +15,9 @@ import { RouterHistoryService } from '@services/router-history.service';
|
||||
import { firstValueFrom, Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { TrashDialogService } from '../services/trash-dialog.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { TrashTableItemComponent } from './trash-table-item/trash-table-item.component';
|
||||
|
||||
@Component({
|
||||
templateUrl: './trash-screen.component.html',
|
||||
@ -16,11 +26,13 @@ import { TrashDialogService } from '../services/trash-dialog.service';
|
||||
entitiesService: TrashService,
|
||||
component: TrashScreenComponent,
|
||||
}),
|
||||
standalone: true,
|
||||
imports: [IqserListingModule, TranslateModule, CircleButtonComponent, NgIf, TrashTableItemComponent, AsyncPipe],
|
||||
})
|
||||
export class TrashScreenComponent extends ListingComponent<TrashItem> implements OnInit {
|
||||
readonly tableHeaderLabel = _('trash.table-header.title');
|
||||
readonly canRestoreSelected$ = this._canRestoreSelected$;
|
||||
readonly canHardDeleteSelected$ = this._canHardDeleteSelected$;
|
||||
readonly canRestoreSelected$ = this.#canRestoreSelected$;
|
||||
readonly canHardDeleteSelected$ = this.#canHardDeleteSelected$;
|
||||
readonly tableColumnConfigs: TableColumnConfig<TrashItem>[] = [
|
||||
{ label: _('trash.table-col-names.name'), sortByKey: 'name' },
|
||||
{ label: _('trash.table-col-names.owner'), class: 'user-column', sortByKey: 'ownerName' },
|
||||
@ -43,14 +55,14 @@ export class TrashScreenComponent extends ListingComponent<TrashItem> implements
|
||||
});
|
||||
}
|
||||
|
||||
private get _canRestoreSelected$(): Observable<boolean> {
|
||||
get #canRestoreSelected$(): Observable<boolean> {
|
||||
return this.listingService.selectedEntities$.pipe(
|
||||
map(entities => entities.length && !entities.find(dossier => !(dossier.canRestore && dossier.hasRestoreRights))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
private get _canHardDeleteSelected$(): Observable<boolean> {
|
||||
get #canHardDeleteSelected$(): Observable<boolean> {
|
||||
return this.listingService.selectedEntities$.pipe(
|
||||
map(entities => entities.length && !entities.find(dossier => !dossier.hasHardDeleteRights)),
|
||||
distinctUntilChanged(),
|
||||
|
||||
@ -2,15 +2,34 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Out
|
||||
import { Dossier, DossierStats, TrashFile, TrashItem, User } from '@red/domain';
|
||||
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
|
||||
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
|
||||
import { PartialDossier } from '@shared/components/dossier-name-column/dossier-name-column.component';
|
||||
import { DossierNameColumnComponent, PartialDossier } from '@shared/components/dossier-name-column/dossier-name-column.component';
|
||||
import { Observable } from 'rxjs';
|
||||
import { getCurrentUser } from '@common-ui/users';
|
||||
import { getCurrentUser, InitialsAvatarComponent } from '@common-ui/users';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { FileNameColumnComponent } from '@shared/components/file-name-column/file-name-column.component';
|
||||
import { AsyncPipe, DatePipe, NgIf } from '@angular/common';
|
||||
import { CircleButtonComponent } from '@iqser/common-ui';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-trash-table-item [item]',
|
||||
templateUrl: './trash-table-item.component.html',
|
||||
styleUrls: ['./trash-table-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatIcon,
|
||||
FileNameColumnComponent,
|
||||
DossierNameColumnComponent,
|
||||
NgIf,
|
||||
InitialsAvatarComponent,
|
||||
CircleButtonComponent,
|
||||
AsyncPipe,
|
||||
RouterLink,
|
||||
DatePipe,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class TrashTableItemComponent implements OnChanges {
|
||||
@Input() item: TrashItem;
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TrashScreenComponent } from './trash-screen/trash-screen.component';
|
||||
import { CircleButtonComponent, IqserListingModule } from '@iqser/common-ui';
|
||||
import { TrashTableItemComponent } from './trash-screen/trash-table-item/trash-table-item.component';
|
||||
import { SharedModule } from '@shared/shared.module';
|
||||
import { TrashDialogService } from './services/trash-dialog.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { InitialsAvatarComponent, IqserUsersModule } from '@iqser/common-ui/lib/users';
|
||||
import { FileNameColumnComponent } from '@shared/components/file-name-column/file-name-column.component';
|
||||
import { DossierNameColumnComponent } from '@shared/components/dossier-name-column/dossier-name-column.component';
|
||||
|
||||
const routes = [{ path: '', component: TrashScreenComponent }];
|
||||
|
||||
@NgModule({
|
||||
declarations: [TrashScreenComponent, TrashTableItemComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharedModule,
|
||||
IqserUsersModule,
|
||||
TranslateModule,
|
||||
IqserListingModule,
|
||||
CircleButtonComponent,
|
||||
FileNameColumnComponent,
|
||||
InitialsAvatarComponent,
|
||||
DossierNameColumnComponent,
|
||||
],
|
||||
providers: [TrashDialogService],
|
||||
})
|
||||
export class TrashModule {}
|
||||
5
apps/red-ui/src/app/modules/trash/trash.routes.ts
Normal file
5
apps/red-ui/src/app/modules/trash/trash.routes.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { TrashScreenComponent } from './trash-screen/trash-screen.component';
|
||||
import { TrashDialogService } from './services/trash-dialog.service';
|
||||
import { IqserRoutes } from '@iqser/common-ui';
|
||||
|
||||
export default [{ path: '', component: TrashScreenComponent, providers: [TrashDialogService] }] satisfies IqserRoutes;
|
||||
Loading…
x
Reference in New Issue
Block a user