mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
fix issue where the menu wouldn't update if an option was added after the initial render
This commit is contained in:

committed by
Marie Verdonck

parent
d2523ade9b
commit
2f946ba2a3
@@ -5,7 +5,7 @@
|
||||
<div class="sidebar-collapsible">
|
||||
<span class="section-header-text">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</li>
|
||||
|
@@ -25,7 +25,7 @@
|
||||
|
||||
<ng-container *ngFor="let section of (sections | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="sectionComponents.get(section.id); injector: sectionInjectors.get(section.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -49,4 +49,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { Component, Injector, OnInit } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { combineLatest as combineLatestObservable } from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { of } from 'rxjs/internal/observable/of';
|
||||
import { first, map, take } from 'rxjs/operators';
|
||||
import { first, map, take, tap, filter } from 'rxjs/operators';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||
import { ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||
@@ -99,7 +99,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
this.sidebarOpen = !collapsed;
|
||||
this.sidebarClosed = collapsed;
|
||||
});
|
||||
this.sidebarExpanded = combineLatestObservable(this.menuCollapsed, this.menuPreviewCollapsed)
|
||||
this.sidebarExpanded = observableCombineLatest(this.menuCollapsed, this.menuPreviewCollapsed)
|
||||
.pipe(
|
||||
map(([collapsed, previewCollapsed]) => (!collapsed || !previewCollapsed))
|
||||
);
|
||||
@@ -323,78 +323,81 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
* the export scripts exist and the current user is allowed to execute them
|
||||
*/
|
||||
createExportMenuSections() {
|
||||
const isAuthorized$: Observable<boolean> = this.authorizationService.isAuthorized(FeatureID.AdministratorOf);
|
||||
isAuthorized$.subscribe((authorized: boolean) => {
|
||||
if (authorized) {
|
||||
const metadataExportScriptExists$ = this.scriptDataService.scripWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME);
|
||||
metadataExportScriptExists$.subscribe((metadataExportScriptExists: boolean) => {
|
||||
const menuList = [
|
||||
/* Export */
|
||||
{
|
||||
id: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.export'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'sign-out-alt',
|
||||
index: 3
|
||||
},
|
||||
{
|
||||
id: 'export_community',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_community',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'export_collection',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_collection',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'export_item',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_item',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'export_metadata',
|
||||
parentID: 'export',
|
||||
active: true,
|
||||
visible: (authorized && metadataExportScriptExists),
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.export_metadata',
|
||||
function: () => {
|
||||
this.modalService.open(ExportMetadataSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true
|
||||
})));
|
||||
});
|
||||
}
|
||||
});
|
||||
const menuList = [
|
||||
/* Export */
|
||||
{
|
||||
id: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.export'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'sign-out-alt',
|
||||
index: 3,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
{
|
||||
id: 'export_community',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_community',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
{
|
||||
id: 'export_collection',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_collection',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
{
|
||||
id: 'export_item',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_item',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
||||
|
||||
observableCombineLatest(
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME)
|
||||
).pipe(
|
||||
filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists),
|
||||
take(1)
|
||||
).subscribe(() => {
|
||||
this.menuService.addSection(this.menuID, {
|
||||
id: 'export_metadata',
|
||||
parentID: 'export',
|
||||
active: true,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.export_metadata',
|
||||
function: () => {
|
||||
this.modalService.open(ExportMetadataSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -12,16 +12,16 @@
|
||||
(click)="toggleSection($event)">
|
||||
<span class="section-header-text">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</span>
|
||||
<i class="fas fa-chevron-right fa-pull-right"
|
||||
[@rotate]="(expanded | async) ? 'expanded' : 'collapsed'" [title]="('menu.section.toggle.' + section.id) | translate"></i>
|
||||
</a>
|
||||
<ul class="sidebar-sub-level-items list-unstyled" @slide *ngIf="(expanded | async)">
|
||||
<li *ngFor="let subSection of (subSections | async)">
|
||||
<li *ngFor="let subSection of (subSections$ | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(subSection.id); injector: itemInjectors.get(subSection.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</li>
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { getSucceededRemoteData } from '../../shared/operators';
|
||||
import {
|
||||
getSucceededRemoteData,
|
||||
getFirstSucceededRemoteDataPayload
|
||||
} from '../../shared/operators';
|
||||
import { DataService } from '../data.service';
|
||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
@@ -11,7 +14,7 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from '../default-change-analyzer.service';
|
||||
import { Script } from '../../../process-page/scripts/script.model';
|
||||
import { ProcessParameter } from '../../../process-page/processes/process-parameter.model';
|
||||
import { find, map, switchMap } from 'rxjs/operators';
|
||||
import { find, map, switchMap, filter } from 'rxjs/operators';
|
||||
import { URLCombiner } from '../../url-combiner/url-combiner';
|
||||
import { RemoteData } from '../remote-data';
|
||||
import { MultipartPostRequest, RestRequest } from '../request.models';
|
||||
@@ -20,6 +23,7 @@ import { Observable } from 'rxjs';
|
||||
import { RequestEntry } from '../request.reducer';
|
||||
import { dataService } from '../../cache/builders/build-decorators';
|
||||
import { SCRIPT } from '../../../process-page/scripts/script.resource-type';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
|
||||
@Injectable()
|
||||
@dataService(SCRIPT)
|
||||
@@ -65,11 +69,11 @@ export class ScriptDataService extends DataService<Script> {
|
||||
* Check whether a script with given name exist; user needs to be allowed to execute script for this to to not throw a 401 Unauthorized
|
||||
* @param scriptName script we want to check exists (and we can execute)
|
||||
*/
|
||||
public scripWithNameExistsAndCanExecute(scriptName: string): Observable<boolean> {
|
||||
public scriptWithNameExistsAndCanExecute(scriptName: string): Observable<boolean> {
|
||||
return this.findById(scriptName).pipe(
|
||||
getSucceededRemoteData(),
|
||||
map((scriptRD: RemoteData<Script>) => {
|
||||
return (scriptRD.hasSucceeded);
|
||||
find((rd: RemoteData<Script>) => hasValue(rd.payload) || hasValue(rd.error)),
|
||||
map((rd: RemoteData<Script>) => {
|
||||
return hasValue(rd.payload);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@@ -5,13 +5,13 @@
|
||||
id="browseDropdown" (click)="toggleSection($event)"
|
||||
data-toggle="dropdown">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</a>
|
||||
<ul @slide *ngIf="(active | async)" (click)="deactivateSection($event)"
|
||||
class="m-0 shadow-none border-top-0 dropdown-menu show">
|
||||
<ng-container *ngFor="let subSection of (subSections | async)">
|
||||
<ng-container *ngFor="let subSection of (subSections$ | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(subSection.id); injector: itemInjectors.get(subSection.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</li>
|
||||
</li>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<li class="nav-item">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||
</li>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</li>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<ul class="navbar-nav mr-auto shadow-none">
|
||||
<ng-container *ngFor="let section of (sections | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="sectionComponents.get(section.id); injector: sectionInjectors.get(section.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import { Component, Injector } from '@angular/core';
|
||||
import { Component, Injector, OnInit, OnDestroy } from '@angular/core';
|
||||
import { MenuService } from '../menu.service';
|
||||
import { MenuSection } from '../menu.reducer';
|
||||
import { getComponentForMenuItemType } from '../menu-item.decorator';
|
||||
import { MenuID, MenuItemType } from '../initial-menus-state';
|
||||
import { hasNoValue } from '../../empty.util';
|
||||
import { hasNoValue, hasValue } from '../../empty.util';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { MenuItemModel } from '../menu-item/models/menu-item.model';
|
||||
import { distinctUntilChanged } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
import { GenericConstructor } from '../../../core/shared/generic-constructor';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
/**
|
||||
* A basic implementation of a menu section's component
|
||||
@@ -16,7 +18,7 @@ import { GenericConstructor } from '../../../core/shared/generic-constructor';
|
||||
selector: 'ds-menu-section',
|
||||
template: ''
|
||||
})
|
||||
export class MenuSectionComponent {
|
||||
export class MenuSectionComponent implements OnInit, OnDestroy{
|
||||
|
||||
/**
|
||||
* Observable that emits whether or not this section is currently active
|
||||
@@ -28,20 +30,25 @@ export class MenuSectionComponent {
|
||||
*/
|
||||
menuID: MenuID;
|
||||
|
||||
/**
|
||||
* List of Injectors for each dynamically rendered menu item of this section
|
||||
*/
|
||||
itemInjectors: Map<string, Injector> = new Map<string, Injector>();
|
||||
|
||||
/**
|
||||
* List of child Components for each dynamically rendered menu item of this section
|
||||
*/
|
||||
itemComponents: Map<string, GenericConstructor<MenuSectionComponent>> = new Map<string, GenericConstructor<MenuSectionComponent>>();
|
||||
|
||||
/**
|
||||
* List of available subsections in this section
|
||||
*/
|
||||
subSections: Observable<MenuSection[]>;
|
||||
subSections$: Observable<MenuSection[]>;
|
||||
|
||||
/**
|
||||
* Map of components and injectors for each dynamically rendered menu section
|
||||
*/
|
||||
sectionMap$: BehaviorSubject<Map<string, {
|
||||
injector: Injector,
|
||||
component: GenericConstructor<MenuSectionComponent>
|
||||
}>> = new BehaviorSubject(new Map());
|
||||
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
subs: Subscription[] = [];
|
||||
|
||||
constructor(public section: MenuSection, protected menuService: MenuService, protected injector: Injector) {
|
||||
}
|
||||
@@ -85,15 +92,34 @@ export class MenuSectionComponent {
|
||||
* Method for initializing all injectors and component constructors for the menu items in this section
|
||||
*/
|
||||
private initializeInjectorData() {
|
||||
this.itemInjectors.set(this.section.id, this.getItemModelInjector(this.section.model));
|
||||
this.itemComponents.set(this.section.id, this.getMenuItemComponent(this.section.model));
|
||||
this.subSections = this.menuService.getSubSectionsByParentID(this.menuID, this.section.id);
|
||||
this.subSections.subscribe((sections: MenuSection[]) => {
|
||||
sections.forEach((section: MenuSection) => {
|
||||
this.itemInjectors.set(section.id, this.getItemModelInjector(section.model));
|
||||
this.itemComponents.set(section.id, this.getMenuItemComponent(section.model));
|
||||
this.updateSectionMap(
|
||||
this.section.id,
|
||||
this.getItemModelInjector(this.section.model),
|
||||
this.getMenuItemComponent(this.section.model)
|
||||
);
|
||||
this.subSections$ = this.menuService.getSubSectionsByParentID(this.menuID, this.section.id);
|
||||
this.subs.push(
|
||||
this.subSections$.pipe(
|
||||
// if you return an array from a switchMap it will emit each element as a separate event.
|
||||
// So this switchMap is equivalent to a subscribe with a forEach inside
|
||||
switchMap((sections: MenuSection[]) => sections)
|
||||
).subscribe((section: MenuSection) => {
|
||||
this.updateSectionMap(
|
||||
section.id,
|
||||
this.getItemModelInjector(section.model),
|
||||
this.getMenuItemComponent(section.model)
|
||||
)
|
||||
})
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sectionMap
|
||||
*/
|
||||
private updateSectionMap(id, injector, component) {
|
||||
const nextMap = this.sectionMap$.getValue();
|
||||
nextMap.set(id, { injector, component });
|
||||
this.sectionMap$.next(nextMap);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,4 +150,12 @@ export class MenuSectionComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from open subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subs
|
||||
.filter((subscription) => hasValue(subscription))
|
||||
.forEach((subscription) => subscription.unsubscribe());
|
||||
}
|
||||
}
|
||||
|
@@ -3,12 +3,14 @@ import { Observable } from 'rxjs/internal/Observable';
|
||||
import { MenuService } from './menu.service';
|
||||
import { MenuID } from './initial-menus-state';
|
||||
import { MenuSection } from './menu.reducer';
|
||||
import { distinctUntilChanged, first, map } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, first, map, tap, switchMap, take } from 'rxjs/operators';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import { hasValue } from '../empty.util';
|
||||
import { MenuSectionComponent } from './menu-section/menu-section.component';
|
||||
import { getComponentForMenu } from './menu-section.decorator';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { compareArraysUsingIds } from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||
|
||||
/**
|
||||
* A basic implementation of a MenuComponent
|
||||
@@ -44,14 +46,12 @@ export class MenuComponent implements OnInit, OnDestroy {
|
||||
sections: Observable<MenuSection[]>;
|
||||
|
||||
/**
|
||||
* List of Injectors for each dynamically rendered menu section
|
||||
* Map of components and injectors for each dynamically rendered menu section
|
||||
*/
|
||||
sectionInjectors: Map<string, Injector> = new Map<string, Injector>();
|
||||
|
||||
/**
|
||||
* List of child Components for each dynamically rendered menu section
|
||||
*/
|
||||
sectionComponents: Map<string, GenericConstructor<MenuSectionComponent>> = new Map<string, GenericConstructor<MenuSectionComponent>>();
|
||||
sectionMap$: BehaviorSubject<Map<string, {
|
||||
injector: Injector,
|
||||
component: GenericConstructor<MenuSectionComponent>
|
||||
}>> = new BehaviorSubject(new Map());
|
||||
|
||||
/**
|
||||
* Prevent unnecessary rerendering
|
||||
@@ -79,13 +79,25 @@ export class MenuComponent implements OnInit, OnDestroy {
|
||||
this.menuCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
||||
this.menuPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
||||
this.menuVisible = this.menuService.isMenuVisible(this.menuID);
|
||||
this.sections = this.menuService.getMenuTopSections(this.menuID).pipe(distinctUntilChanged((x, y) => JSON.stringify(x) === JSON.stringify(y)));
|
||||
this.subs.push(this.sections.subscribe((sections: MenuSection[]) => {
|
||||
sections.forEach((section: MenuSection) => {
|
||||
this.sectionInjectors.set(section.id, this.getSectionDataInjector(section));
|
||||
this.getSectionComponent(section).pipe(first()).subscribe((constr) => this.sectionComponents.set(section.id, constr));
|
||||
});
|
||||
}));
|
||||
this.sections = this.menuService.getMenuTopSections(this.menuID).pipe(distinctUntilChanged(compareArraysUsingIds()));
|
||||
this.subs.push(
|
||||
this.sections.pipe(
|
||||
// if you return an array from a switchMap it will emit each element as a separate event.
|
||||
// So this switchMap is equivalent to a subscribe with a forEach inside
|
||||
switchMap((sections: MenuSection[]) => sections),
|
||||
switchMap((section: MenuSection) => this.getSectionComponent(section).pipe(
|
||||
map((component: GenericConstructor<MenuSectionComponent>) => ({ section, component }))
|
||||
)),
|
||||
distinctUntilChanged((x,y) => x.section.id === y.section.id)
|
||||
).subscribe(({ section, component}) => {
|
||||
const nextMap = this.sectionMap$.getValue();
|
||||
nextMap.set(section.id, {
|
||||
injector: this.getSectionDataInjector(section),
|
||||
component
|
||||
});
|
||||
this.sectionMap$.next(nextMap);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user