import {ChangeDetectorRef, Component, OnDestroy, Renderer2} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; import {Observable, Subscription} from 'rxjs'; import {Document, FieldDocument, FieldGeometry} from 'idai-components-2'; import {Loading} from '../widgets/loading'; import {RoutingService} from '../routing-service'; import {DoceditLauncher} from './service/docedit-launcher'; import {M} from '../messages/m'; import {MoveModalComponent} from './move-modal.component'; import {AngularUtility} from '../../angular/angular-utility'; import {ResourceDeletion} from './deletion/resource-deletion'; import {Category} from '../../core/configuration/model/category'; import {TabManager} from '../../core/tabs/tab-manager'; import {ResourcesViewMode, ViewFacade} from '../../core/resources/view/view-facade'; import {NavigationService} from '../../core/resources/navigation/navigation-service'; import {MenuContext, MenuService} from '../menu-service'; import {Messages} from '../messages/messages'; import {ProjectCategories} from '../../core/configuration/project-categories'; import {ProjectConfiguration} from '../../core/configuration/project-configuration'; import {NavigationPath} from '../../core/resources/view/state/navigation-path'; export type PopoverMenu = 'none'|'info'|'children'; @Component({ templateUrl: './resources.html' }) /** * @author Sebastian Cuy * @author Daniel de Oliveira * @author Jan G. Wieners * @author Thomas Kleinke */ export class ResourcesComponent implements OnDestroy { public activePopoverMenu: PopoverMenu = 'none'; public filterOptions: Array = []; public additionalSelectedDocuments: Array = []; private clickEventObservers: Array = []; private deselectionSubscription: Subscription; private populateDocumentsSubscription: Subscription; private changedDocumentFromRemoteSubscription: Subscription; constructor(route: ActivatedRoute, public viewFacade: ViewFacade, private routingService: RoutingService, private doceditLauncher: DoceditLauncher, private renderer: Renderer2, private messages: Messages, private loading: Loading, private changeDetectorRef: ChangeDetectorRef, private modalService: NgbModal, private resourceDeletion: ResourceDeletion, private tabManager: TabManager, private navigationService: NavigationService, private projectConfiguration: ProjectConfiguration, private menuService: MenuService) { routingService.routeParams(route).subscribe(async (params: any) => { this.quitGeometryEditing(); if (params['id']) { await this.selectDocumentFromParams(params['id'], params['menu'], params['group']); } }); this.initializeClickEventListener(); this.initializeSubscriptions(); this.viewFacade.navigationPathNotifications().subscribe((_: any) => { this.quitGeometryEditing(); }); this.viewFacade.rebuildNavigationPath(); } public isCurrentMode = (mode: string) => (this.viewFacade.getMode() === mode); public getQueryString = () => this.viewFacade.getSearchString(); public getCategoryFilters = () => this.viewFacade.getFilterCategories(); public isInExtendedSearchMode = () => this.viewFacade.isInExtendedSearchMode(); public isReady = () => this.viewFacade.isReady(); public isInTypesManagement = () => this.viewFacade.isInTypesManagement(); ngOnDestroy() { if (this.deselectionSubscription) this.deselectionSubscription.unsubscribe(); if (this.populateDocumentsSubscription) this.populateDocumentsSubscription.unsubscribe(); if (this.changedDocumentFromRemoteSubscription) { this.changedDocumentFromRemoteSubscription.unsubscribe(); } } public listenToClickEvents(): Observable { return Observable.create((observer: any) => { this.clickEventObservers.push(observer); }); } public async setQueryString(q: string) { this.viewFacade.setLimitSearchResults(true); await this.viewFacade.setSearchString(q); }; public async setCategoryFilters(categories: string[]|undefined) { this.viewFacade.setLimitSearchResults(true); await this.viewFacade.setFilterCategories(categories ? categories : []); }; private updateFilterOptions() { if (this.viewFacade.isInOverview()) { this.filterOptions = this.viewFacade.isInExtendedSearchMode() ? ProjectCategories.getConcreteFieldCategories(this.projectConfiguration.getCategoryTreelist()) .filter(category => !category.parentCategory) : ProjectCategories.getOverviewToplevelCategories(this.projectConfiguration.getCategoryTreelist()); } else if (this.viewFacade.isInTypesManagement()) { this.filterOptions = ProjectCategories.getTypeCategories(this.projectConfiguration.getCategoryTreelist()); } else { this.filterOptions = this.projectConfiguration.getAllowedRelationDomainCategories( 'isRecordedIn', (this.viewFacade.getCurrentOperation() as FieldDocument).resource.category ); } } public async startEditNewDocument(newDocument: FieldDocument, geometryType: string) { if (geometryType === 'none') { await this.editDocument(newDocument); } else { newDocument.resource['geometry'] = { type: geometryType }; this.viewFacade.addNewDocument(newDocument); this.startGeometryEditing(); this.viewFacade.setMode('map'); } } public editDocument(document: Document|undefined, activeGroup?: string): Promise { if (!document) throw 'Called edit document with undefined document'; this.quitGeometryEditing(document); return this.doceditLauncher.editDocument(document, activeGroup); } public async moveDocuments(documents: Array) { this.quitGeometryEditing(); this.menuService.setContext(MenuContext.MODAL); const modalRef: NgbModalRef = this.modalService.open(MoveModalComponent, { keyboard: false }); modalRef.componentInstance.initialize(documents); try { await modalRef.result; await this.viewFacade.deselect(); await this.viewFacade.rebuildNavigationPath(); await this.routingService.jumpToResource(documents[0]); } catch (msgWithParams) { if (Array.isArray(msgWithParams)) this.messages.add(msgWithParams); // Otherwise, the move modal has been canceled } this.menuService.setContext(MenuContext.DEFAULT); } public async deleteDocument(documents: Array) { this.quitGeometryEditing(); this.menuService.setContext(MenuContext.MODAL); try { await this.resourceDeletion.delete(documents); await this.viewFacade.deselect(); for (let document of documents) { await this.tabManager.closeTab('resources', document.resource.id); this.viewFacade.removeView(document.resource.id); } await this.viewFacade.rebuildNavigationPath(); await this.viewFacade.populateDocumentList(); } catch (msgWithParams) { if (Array.isArray(msgWithParams)) this.messages.add(msgWithParams); // Otherwise, the delete modal has been canceled. } this.menuService.setContext(MenuContext.DEFAULT); } public createGeometry(geometryType: string) { (this.viewFacade.getSelectedDocument() as any).resource['geometry'] = { type: geometryType }; this.startGeometryEditing(); } public async switchMode(mode: ResourcesViewMode) { if (!this.isReady()) return; this.loading.start(); await AngularUtility.refresh(); // This is so that new elements are properly included and sorted when coming back to list if (this.viewFacade.getMode() === 'list' && mode !== 'list') { await this.viewFacade.populateDocumentList(); } this.viewFacade.deselect(); this.viewFacade.setMode(mode); this.loading.stop(); } public isSearchResultsInfoVisible(): boolean { return this.viewFacade.getDocuments() !== undefined && this.viewFacade.isInExtendedSearchMode() && this.viewFacade.isReady(); } public isDocumentLimitExceeded(): boolean { const documents: Array = this.viewFacade.getDocuments(); return documents && documents.length > 0 && this.viewFacade.getTotalDocumentCount() > documents.length; } public async select(document: FieldDocument) { this.quitGeometryEditing(); if (!document) { this.viewFacade.deselect(); } else { await this.viewFacade.setSelectedDocument(document.resource.id, false); } } public isSelected(document: FieldDocument) { if (!this.viewFacade.getSelectedDocument()) return false; return (this.viewFacade.getSelectedDocument() as FieldDocument).resource.id === document.resource.id; } public async togglePopoverMenu(popoverMenu: PopoverMenu, document: FieldDocument) { if (this.isPopoverMenuOpened(popoverMenu, document) || popoverMenu === 'none') { this.closePopover(); } else { await this.openPopoverMenu(popoverMenu, document); } } public isPopoverMenuOpened(popoverMenu?: PopoverMenu, document?: FieldDocument): boolean { return this.viewFacade.getSelectedDocument() !== undefined && ((!popoverMenu && this.activePopoverMenu !== 'none') || this.activePopoverMenu === popoverMenu) && (!document || this.isSelected(document)); } public closePopover() { this.activePopoverMenu = 'none'; }; public async navigatePopoverMenus(direction: 'previous'|'next') { const selectedDocument: FieldDocument|undefined = this.viewFacade.getSelectedDocument(); if (!selectedDocument) return; const availablePopoverMenus: string[] = this.getAvailablePopoverMenus(selectedDocument); let index: number = availablePopoverMenus.indexOf(this.activePopoverMenu) + (direction === 'next' ? 1 : -1); if (index < 0) index = availablePopoverMenus.length - 1; if (index >= availablePopoverMenus.length) index = 0; await this.openPopoverMenu(availablePopoverMenus[index] as PopoverMenu, selectedDocument); } public async removeLimit() { this.viewFacade.setLimitSearchResults(false); await this.viewFacade.populateDocumentList(); } public toggleAdditionalSelected(document: FieldDocument, allowDeselection: boolean) { if (this.additionalSelectedDocuments.includes(document)) { if (!allowDeselection) return; this.additionalSelectedDocuments = this.additionalSelectedDocuments.filter(doc => doc !== document); } else { this.additionalSelectedDocuments.push(document); this.additionalSelectedDocuments = this.additionalSelectedDocuments.slice(); } } private async selectDocumentFromParams(id: string, menu: string, group: string|undefined) { if (this.viewFacade.getMode() === 'types') { await this.viewFacade.moveInto(id, false, true); } else { await this.viewFacade.setSelectedDocument(id); } try { if (menu === 'edit') { await this.editDocument( this.viewFacade.getMode() === 'types' ? NavigationPath.getSelectedSegment(this.viewFacade.getNavigationPath())?.document : this.viewFacade.getSelectedDocument(), group ); } else { await this.viewFacade.setActiveDocumentViewTab(group) } } catch (e) { this.messages.add([M.DATASTORE_ERROR_NOT_FOUND]); } } public quitGeometryEditing(document: Document|undefined = this.viewFacade.getSelectedDocument()) { if (this.menuService.getContext() !== MenuContext.GEOMETRY_EDIT) return; if (document && document.resource.geometry && !document.resource.geometry.coordinates) { delete document.resource.geometry; } this.menuService.setContext(MenuContext.DEFAULT); } private initializeClickEventListener() { this.renderer.listen('document', 'click', (event: any) => this.clickEventObservers.forEach(observer => observer.next(event))); } private initializeSubscriptions() { this.deselectionSubscription = this.viewFacade.deselectionNotifications().subscribe(deselectedDocument => { this.quitGeometryEditing(deselectedDocument); }); this.populateDocumentsSubscription = this.viewFacade.populateDocumentsNotifications().subscribe(() => { this.changeDetectorRef.detectChanges(); this.updateFilterOptions(); }); this.changedDocumentFromRemoteSubscription = this.viewFacade.documentChangedFromRemoteNotifications().subscribe(() => { this.changeDetectorRef.detectChanges(); }); } private async openPopoverMenu(popoverMenu: PopoverMenu, document: FieldDocument) { this.activePopoverMenu = popoverMenu; if (!this.isSelected(document)) await this.select(document); } private getAvailablePopoverMenus(document: FieldDocument): string[] { const availablePopoverMenus: string[] = ['none', 'info']; if (this.navigationService.shouldShowArrowBottomRight(document)) { availablePopoverMenus.push('children'); } return availablePopoverMenus; } private startGeometryEditing() { this.menuService.setContext(MenuContext.GEOMETRY_EDIT); } }