Merged cdl7 into CST-12043-primary-bitstream-flag

This commit is contained in:
Giuseppe Digilio
2023-10-27 15:44:00 +00:00
46 changed files with 411 additions and 93 deletions

View File

@@ -12,8 +12,7 @@ import { Router } from '@angular/router';
* Represents a non-expandable section in the admin sidebar
*/
@Component({
/* eslint-disable @angular-eslint/component-selector */
selector: 'li[ds-admin-sidebar-section]',
selector: 'ds-admin-sidebar-section',
templateUrl: './admin-sidebar-section.component.html',
styleUrls: ['./admin-sidebar-section.component.scss'],

View File

@@ -26,10 +26,10 @@
</div>
</li>
<ng-container *ngFor="let section of (sections | async)">
<li *ngFor="let section of (sections | async)">
<ng-container
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
</ng-container>
</li>
</ul>
</div>
<div class="navbar-nav">

View File

@@ -15,8 +15,7 @@ import { Router } from '@angular/router';
* Represents a expandable section in the sidebar
*/
@Component({
/* eslint-disable @angular-eslint/component-selector */
selector: 'li[ds-expandable-admin-sidebar-section]',
selector: 'ds-expandable-admin-sidebar-section',
templateUrl: './expandable-admin-sidebar-section.component.html',
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
animations: [rotate, slide, bgColor]

View File

@@ -7,6 +7,7 @@ import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
import { RouteEffects } from './services/route.effects';
import { RouterEffects } from './router/router.effects';
import { MenuEffects } from '../shared/menu/menu.effects';
export const coreEffects = [
RequestEffects,
@@ -18,4 +19,5 @@ export const coreEffects = [
ObjectUpdatesEffects,
RouteEffects,
RouterEffects,
MenuEffects,
];

View File

@@ -661,7 +661,9 @@ describe('RequestService', () => {
spyOn(service, 'getByHref').and.returnValue(observableOf(staleRE));
spyOn(store, 'dispatch');
service.setStaleByHref(href).subscribe(() => {
expect(store.dispatch).toHaveBeenCalledWith(new RequestStaleAction(uuid));
const requestStaleAction = new RequestStaleAction(uuid);
requestStaleAction.lastUpdated = jasmine.any(Number) as any;
expect(store.dispatch).toHaveBeenCalledWith(requestStaleAction);
done();
});
});

View File

@@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ElementRef } from '@angular/core';
import { ContextHelpService } from '../../shared/context-help.service';
import { Observable } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
/**
@@ -15,12 +15,23 @@ import { map } from 'rxjs/operators';
export class ContextHelpToggleComponent implements OnInit {
buttonVisible$: Observable<boolean>;
subscriptions: Subscription[] = [];
constructor(
private contextHelpService: ContextHelpService,
) { }
protected elRef: ElementRef,
protected contextHelpService: ContextHelpService,
) {
}
ngOnInit(): void {
this.buttonVisible$ = this.contextHelpService.tooltipCount$().pipe(map(x => x > 0));
this.subscriptions.push(this.buttonVisible$.subscribe((showContextHelpToggle: boolean) => {
if (showContextHelpToggle) {
this.elRef.nativeElement.classList.remove('d-none');
} else {
this.elRef.nativeElement.classList.add('d-none');
}
}));
}
onClick() {

View File

@@ -7,12 +7,12 @@
<nav role="navigation" [attr.aria-label]="'nav.user.description' | translate" class="navbar navbar-light navbar-expand-md flex-shrink-0 px-0">
<ds-themed-search-navbar></ds-themed-search-navbar>
<ds-lang-switch></ds-lang-switch>
<ds-themed-lang-switch></ds-themed-lang-switch>
<ds-context-help-toggle></ds-context-help-toggle>
<ds-themed-auth-nav-menu></ds-themed-auth-nav-menu>
<ds-impersonate-navbar></ds-impersonate-navbar>
<div class="pl-2">
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
<div *ngIf="isXsOrSm$ | async" class="pl-2">
<button class="navbar-toggler px-0" type="button" (click)="toggleNavbar()"
aria-controls="collapsingNav"
aria-expanded="false" [attr.aria-label]="'nav.toggle' | translate">
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>

View File

@@ -20,3 +20,8 @@
}
}
.navbar {
display: flex;
gap: calc(var(--bs-spacer) / 3);
align-items: center;
}

View File

@@ -10,6 +10,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { MenuService } from '../shared/menu/menu.service';
import { MenuServiceStub } from '../shared/testing/menu-service.stub';
import { HostWindowService } from '../shared/host-window.service';
import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub';
let comp: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
@@ -26,6 +28,7 @@ describe('HeaderComponent', () => {
ReactiveFormsModule],
declarations: [HeaderComponent],
providers: [
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: MenuService, useValue: menuService }
],
schemas: [NO_ERRORS_SCHEMA]
@@ -40,7 +43,7 @@ describe('HeaderComponent', () => {
fixture = TestBed.createComponent(HeaderComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
});
describe('when the toggle button is clicked', () => {

View File

@@ -1,7 +1,8 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { MenuService } from '../shared/menu/menu.service';
import { MenuID } from '../shared/menu/menu-id.model';
import { HostWindowService } from '../shared/host-window.service';
/**
* Represents the header with the logo and simple navigation
@@ -11,20 +12,25 @@ import { MenuID } from '../shared/menu/menu-id.model';
styleUrls: ['header.component.scss'],
templateUrl: 'header.component.html',
})
export class HeaderComponent {
export class HeaderComponent implements OnInit {
/**
* Whether user is authenticated.
* @type {Observable<string>}
*/
public isAuthenticated: Observable<boolean>;
public showAuth = false;
public isXsOrSm$: Observable<boolean>;
menuID = MenuID.PUBLIC;
constructor(
private menuService: MenuService
protected menuService: MenuService,
protected windowService: HostWindowService,
) {
}
ngOnInit(): void {
this.isXsOrSm$ = this.windowService.isXsOrSm();
}
public toggleNavbar(): void {
this.menuService.toggleMenu(this.menuID);
}

View File

@@ -14,9 +14,9 @@
</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)">
<li *ngFor="let subSection of (subSections$ | async)">
<ng-container
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
</ng-container>
</li>
</ul>
</div>

View File

@@ -4,7 +4,6 @@ import { MenuService } from '../../shared/menu/menu.service';
import { slide } from '../../shared/animations/slide';
import { first } from 'rxjs/operators';
import { HostWindowService } from '../../shared/host-window.service';
import { rendersSectionForMenu } from '../../shared/menu/menu-section.decorator';
import { MenuID } from '../../shared/menu/menu-id.model';
/**
@@ -16,7 +15,6 @@ import { MenuID } from '../../shared/menu/menu-id.model';
styleUrls: ['./expandable-navbar-section.component.scss'],
animations: [slide]
})
@rendersSectionForMenu(MenuID.PUBLIC, true)
export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements OnInit {
/**
* This section resides in the Public Navbar

View File

@@ -8,8 +8,7 @@ import { MenuID } from '../../shared/menu/menu-id.model';
* Themed wrapper for ExpandableNavbarSectionComponent
*/
@Component({
/* eslint-disable @angular-eslint/component-selector */
selector: 'li[ds-themed-expandable-navbar-section]',
selector: 'ds-themed-expandable-navbar-section',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})

View File

@@ -8,8 +8,7 @@ import { MenuID } from '../../shared/menu/menu-id.model';
* Represents a non-expandable section in the navbar
*/
@Component({
/* eslint-disable @angular-eslint/component-selector */
selector: 'li[ds-navbar-section]',
selector: 'ds-navbar-section',
templateUrl: './navbar-section.component.html',
styleUrls: ['./navbar-section.component.scss']
})

View File

@@ -8,9 +8,9 @@
<li *ngIf="(isXsOrSm$ | async) && (isAuthenticated$ | async)">
<ds-user-menu [inExpandableNavbar]="true"></ds-user-menu>
</li>
<ng-container *ngFor="let section of (sections | async)">
<li *ngFor="let section of (sections | async)">
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
</ng-container>
</li>
</ul>
</div>
</div>

View File

@@ -13,7 +13,7 @@
<ng-content></ng-content>
<div class="d-flex flex-row-reverse">
<button (click)="submit()"
[disabled]="!message || message.length === 0 || !subject || subject.length === 0"
[disabled]="!subject || subject.length === 0"
class="btn btn-primary"
title="{{'grant-deny-request-copy.email.send' | translate }}">
<i class="fas fa-envelope"></i> {{'grant-deny-request-copy.email.send' | translate }}

View File

@@ -1,9 +1,12 @@
<div id="search-navbar-container" [title]="'nav.search' | translate" (dsClickOutside)="collapse()">
<div class="d-inline-block position-relative">
<form [formGroup]="searchForm" (ngSubmit)="onSubmit(searchForm.value)" autocomplete="on">
<form [formGroup]="searchForm" (ngSubmit)="onSubmit(searchForm.value)" autocomplete="on" class="d-flex">
<input #searchInput [@toggleAnimation]="isExpanded" [attr.aria-label]="('nav.search' | translate)" name="query"
formControlName="query" type="text" placeholder="{{searchExpanded ? ('nav.search' | translate) : ''}}"
class="d-inline-block bg-transparent position-absolute form-control dropdown-menu-right p-1" [attr.data-test]="'header-search-box' | dsBrowserOnly">
class="bg-transparent position-absolute form-control dropdown-menu-right pl-1 pr-4"
[class.display]="searchExpanded ? 'inline-block' : 'none'"
[tabIndex]="searchExpanded ? 0 : -1"
[attr.data-test]="'header-search-box' | dsBrowserOnly">
<button class="submit-icon btn btn-link btn-link-inline" [attr.aria-label]="'nav.search.button' | translate" type="button" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" [attr.data-test]="'header-search-icon' | dsBrowserOnly">
<em class="fas fa-search fa-lg fa-fw"></em>
</button>

View File

@@ -12,6 +12,7 @@ input[type="text"] {
cursor: pointer;
position: sticky;
top: 0;
border: 0 !important;
color: var(--ds-header-icon-color);
&:hover, &:focus {

View File

@@ -55,7 +55,7 @@ export const slideSidebarPadding = trigger('slideSidebarPadding', [
export const expandSearchInput = trigger('toggleAnimation', [
state('collapsed', style({
width: '30px',
width: '0',
opacity: '0'
})),
state('expanded', style({

View File

@@ -2,7 +2,7 @@
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"
(click)="$event.stopPropagation();">
<div ngbDropdown #loginDrop display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="javascript:void(0);" class="dropdownLogin px-1" [attr.aria-label]="'nav.login' |translate"
<a href="javascript:void(0);" class="dropdownLogin px-0.5" [attr.aria-label]="'nav.login' |translate"
(click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly"
ngbDropdownToggle>{{ 'nav.login' | translate }}</a>
<div class="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu
@@ -13,7 +13,7 @@
</div>
</li>
<li *ngIf="!(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
<a routerLink="/login" routerLinkActive="active" class="loginLink px-1">
<a routerLink="/login" routerLinkActive="active" class="loginLink px-0.5">
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
</a>
</li>

View File

@@ -13,7 +13,6 @@ import { hasValue } from '../../../empty.util';
* Represents an expandable section in the dso edit menus
*/
@Component({
/* tslint:disable:component-selector */
selector: 'ds-dso-edit-menu-expandable-section',
templateUrl: './dso-edit-menu-expandable-section.component.html',
styleUrls: ['./dso-edit-menu-expandable-section.component.scss'],

View File

@@ -10,7 +10,6 @@ import { MenuSection } from '../../../menu/menu-section.model';
* Represents a non-expandable section in the dso edit menus
*/
@Component({
/* tslint:disable:component-selector */
selector: 'ds-dso-edit-menu-section',
templateUrl: './dso-edit-menu-section.component.html',
styleUrls: ['./dso-edit-menu-section.component.scss']

View File

@@ -1,4 +1,4 @@
<ul class="navbar-nav" *ngIf="(isAuthenticated$ | async) && isImpersonating">
<ul class="navbar-nav" *ngIf="isImpersonating$ | async">
<li class="nav-item">
<button class="btn btn-sm btn-dark" ngbTooltip="{{'nav.stop-impersonating' | translate}}" (click)="stopImpersonating()">
<i class="fa fa-user-secret"></i>

View File

@@ -14,6 +14,7 @@ import { authReducer } from '../../core/auth/auth.reducer';
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
import { EPersonMock } from '../testing/eperson.mock';
import { AppState, storeModuleConfig } from '../../app.reducer';
import { of as observableOf } from 'rxjs';
describe('ImpersonateNavbarComponent', () => {
let component: ImpersonateNavbarComponent;
@@ -65,7 +66,7 @@ describe('ImpersonateNavbarComponent', () => {
describe('when the user is impersonating another user', () => {
beforeEach(() => {
component.isImpersonating = true;
component.isImpersonating$ = observableOf(true);
fixture.detectChanges();
});

View File

@@ -1,8 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ElementRef } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { AppState } from '../../app.reducer';
import { AuthService } from '../../core/auth/auth.service';
import { Observable } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { isAuthenticated } from '../../core/auth/selectors';
@Component({
@@ -13,24 +14,32 @@ import { isAuthenticated } from '../../core/auth/selectors';
* Navbar component for actions to take concerning impersonating users
*/
export class ImpersonateNavbarComponent implements OnInit {
/**
* Whether or not the user is authenticated.
* @type {Observable<string>}
*/
isAuthenticated$: Observable<boolean>;
/**
* Is the user currently impersonating another user?
*/
isImpersonating: boolean;
isImpersonating$: Observable<boolean>;
constructor(private store: Store<AppState>,
private authService: AuthService) {
subscriptions: Subscription[] = [];
constructor(
protected elRef: ElementRef,
protected store: Store<AppState>,
protected authService: AuthService,
) {
}
ngOnInit(): void {
this.isAuthenticated$ = this.store.pipe(select(isAuthenticated));
this.isImpersonating = this.authService.isImpersonating();
this.isImpersonating$ = this.store.pipe(select(isAuthenticated)).pipe(
map((isUserAuthenticated: boolean) => isUserAuthenticated && this.authService.isImpersonating()),
);
this.subscriptions.push(this.isImpersonating$.subscribe((isImpersonating: boolean) => {
if (isImpersonating) {
this.elRef.nativeElement.classList.remove('d-none');
} else {
this.elRef.nativeElement.classList.add('d-none');
}
}));
}
/**

View File

@@ -1,7 +1,7 @@
<div ngbDropdown class="navbar-nav" *ngIf="moreThanOneLanguage" display="dynamic" placement="bottom-right">
<a href="javascript:void(0);" role="button"
[attr.aria-label]="'nav.language' |translate"
[title]="'nav.language' | translate" class="px-1"
[title]="'nav.language' | translate"
(click)="$event.preventDefault()" data-toggle="dropdown" ngbDropdownToggle
tabindex="0">
<i class="fas fa-globe-asia fa-lg fa-fw"></i>

View File

@@ -1,7 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ElementRef } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { LangConfig } from '../../../config/lang-config.interface';
import { environment } from '../../../environments/environment';
import { LocaleService } from '../../core/locale/locale.service';
@@ -25,6 +23,7 @@ export class LangSwitchComponent implements OnInit {
moreThanOneLanguage: boolean;
constructor(
public el: ElementRef,
public translate: TranslateService,
private localeService: LocaleService
) {
@@ -33,6 +32,9 @@ export class LangSwitchComponent implements OnInit {
ngOnInit(): void {
this.activeLangs = environment.languages.filter((MyLangConfig) => MyLangConfig.active === true);
this.moreThanOneLanguage = (this.activeLangs.length > 1);
if (!this.moreThanOneLanguage) {
this.el.nativeElement.parentElement.classList.add('d-none');
}
}
/**

View File

@@ -0,0 +1,27 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../theme-support/themed.component';
import { LangSwitchComponent } from './lang-switch.component';
/**
* Themed wrapper for {@link LangSwitchComponent}
*/
@Component({
selector: 'ds-themed-lang-switch',
styleUrls: [],
templateUrl: '../theme-support/themed.component.html',
})
export class ThemedLangSwitchComponent extends ThemedComponent<LangSwitchComponent> {
protected getComponentName(): string {
return 'LangSwitchComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/shared/lang-switch/lang-switch.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./lang-switch.component`);
}
}

View File

@@ -18,6 +18,7 @@ export const MenuActionTypes = {
EXPAND_MENU: type('dspace/menu/EXPAND_MENU'),
SHOW_MENU: type('dspace/menu/SHOW_MENU'),
HIDE_MENU: type('dspace/menu/HIDE_MENU'),
REINIT_MENUS: type('dspace/menu/REINIT_MENUS'),
COLLAPSE_MENU_PREVIEW: type('dspace/menu/COLLAPSE_MENU_PREVIEW'),
EXPAND_MENU_PREVIEW: type('dspace/menu/EXPAND_MENU_PREVIEW'),
ADD_SECTION: type('dspace/menu-section/ADD_SECTION'),
@@ -115,6 +116,13 @@ export class ExpandMenuPreviewAction implements Action {
}
}
/**
* Action used to re-initialise the menus
*/
export class ReinitMenuAction implements Action {
type = MenuActionTypes.REINIT_MENUS;
}
// MENU SECTION ACTIONS
/**
* Action used to perform state changes for a section of a certain menu
@@ -224,4 +232,5 @@ export type MenuAction =
| DeactivateMenuSectionAction
| ToggleActiveMenuSectionAction
| CollapseMenuPreviewAction
| ExpandMenuPreviewAction;
| ExpandMenuPreviewAction
| ReinitMenuAction;

View File

@@ -0,0 +1,41 @@
import { Observable } from 'rxjs';
import { TestBed, waitForAsync } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { cold, hot } from 'jasmine-marbles';
import { MenuEffects } from './menu.effects';
import { ReinitMenuAction } from './menu.actions';
import { StoreAction, StoreActionTypes } from '../../store.actions';
describe('MenuEffects', () => {
let menuEffects: MenuEffects;
let actions: Observable<any>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
providers: [
MenuEffects,
provideMockActions(() => actions),
],
});
}));
beforeEach(() => {
menuEffects = TestBed.inject(MenuEffects);
});
describe('reinitDSOMenus', () => {
describe('When a REHYDRATE action is triggered', () => {
let action;
beforeEach(() => {
action = new StoreAction(StoreActionTypes.REHYDRATE, null);
});
it('should return a ReinitMenuAction', () => {
actions = hot('--a-', {a: action});
const expected = cold('--b-', {b: new ReinitMenuAction});
expect(menuEffects.reinitDSOMenus).toBeObservable(expected);
});
});
});
});

View File

@@ -0,0 +1,23 @@
import { map } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { StoreActionTypes } from '../../store.actions';
import { ReinitMenuAction } from './menu.actions';
@Injectable()
export class MenuEffects {
/**
* When the store is rehydrated in the browser, re-initialise the menus to ensure
* the menus with functions are loaded correctly.
*/
reinitDSOMenus = createEffect(() => this.actions$
.pipe(ofType(StoreActionTypes.REHYDRATE),
map(() => new ReinitMenuAction())
));
constructor(private actions$: Actions) {
}
}

View File

@@ -9,7 +9,7 @@ import {
ExpandMenuAction,
ExpandMenuPreviewAction,
HideMenuAction,
HideMenuSectionAction,
HideMenuSectionAction, ReinitMenuAction,
RemoveMenuSectionAction,
ShowMenuAction,
ShowMenuSectionAction,
@@ -317,6 +317,17 @@ describe('menusReducer', () => {
// is mutated, and any uncaught exception will cause the test to fail
});
it('should reset the menu state to the initial state when performing the REINIT_MENUS action without affecting the previous state', () => {
dummyState[MenuID.ADMIN].visible = true;
const state = dummyState;
deepFreeze([state]);
const action = new ReinitMenuAction();
const menusState = menusReducer(state, action);
expect(menusState).toEqual(initialMenusState);
});
it('should set add a new section for the correct menu in response to the ADD_SECTION action', () => {
const state = dummyState;
const action = new AddMenuSectionAction(menuID, visibleSection1);

View File

@@ -26,36 +26,39 @@ import { MenuID } from './menu-id.model';
* @returns {MenusState} The new, reducer MenusState
*/
export function menusReducer(state: MenusState = initialMenusState, action: MenuAction): MenusState {
const menuState: MenuState = state[action.menuID];
switch (action.type) {
case MenuActionTypes.COLLAPSE_MENU: {
const newMenuState = Object.assign({}, menuState, { collapsed: true });
const newMenuState = Object.assign({}, state[action.menuID], { collapsed: true });
return Object.assign({}, state, { [action.menuID]: newMenuState });
}
case MenuActionTypes.EXPAND_MENU: {
const newMenuState = Object.assign({}, menuState, { collapsed: false });
const newMenuState = Object.assign({}, state[action.menuID], { collapsed: false });
return Object.assign({}, state, { [action.menuID]: newMenuState });
}
case MenuActionTypes.COLLAPSE_MENU_PREVIEW: {
const newMenuState = Object.assign({}, menuState, { previewCollapsed: true });
const newMenuState = Object.assign({}, state[action.menuID], { previewCollapsed: true });
return Object.assign({}, state, { [action.menuID]: newMenuState });
}
case MenuActionTypes.EXPAND_MENU_PREVIEW: {
const newMenuState = Object.assign({}, menuState, { previewCollapsed: false });
const newMenuState = Object.assign({}, state[action.menuID], { previewCollapsed: false });
return Object.assign({}, state, { [action.menuID]: newMenuState });
}
case MenuActionTypes.TOGGLE_MENU: {
const menuState = state[action.menuID];
const newMenuState = Object.assign({}, menuState, { collapsed: !menuState.collapsed });
return Object.assign({}, state, { [action.menuID]: newMenuState });
}
case MenuActionTypes.SHOW_MENU: {
const newMenuState = Object.assign({}, menuState, { visible: true });
const newMenuState = Object.assign({}, state[action.menuID], { visible: true });
return Object.assign({}, state, { [action.menuID]: newMenuState });
}
case MenuActionTypes.HIDE_MENU: {
const newMenuState = Object.assign({}, menuState, { visible: false });
const newMenuState = Object.assign({}, state[action.menuID], { visible: false });
return Object.assign({}, state, { [action.menuID]: newMenuState });
}
case MenuActionTypes.REINIT_MENUS: {
return Object.assign({}, initialMenusState);
}
case MenuActionTypes.ADD_SECTION: {
return addSection(state, action as AddMenuSectionAction);
}
@@ -230,7 +233,7 @@ function toggleActiveSection(state: MenusState, action: ToggleActiveMenuSectionA
* @param {MenuSection} section The section that will be added or replaced in the state
* @returns {MenusState} The new reduced state
*/
function putSectionState(state: MenusState, action: MenuAction, section: MenuSection): MenusState {
function putSectionState(state: MenusState, action: MenuSectionAction, section: MenuSection): MenusState {
const menuState: MenuState = state[action.menuID];
const newSections = Object.assign({}, menuState.sections, {
[section.id]: section

View File

@@ -41,6 +41,7 @@ describe('MenuService', () => {
let routeDataMenuSection: MenuSection;
let routeDataMenuSectionResolved: MenuSection;
let routeDataMenuChildSection: MenuSection;
let routeDataMenuOverwrittenChildSection: MenuSection;
let toBeRemovedMenuSection: MenuSection;
let alreadyPresentMenuSection: MenuSection;
let route;
@@ -127,6 +128,17 @@ describe('MenuService', () => {
link: ''
} as LinkMenuItemModel
};
routeDataMenuOverwrittenChildSection = {
id: 'mockChildSection',
parentID: 'mockSection',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.mockChildOverwrittenSection',
link: ''
} as LinkMenuItemModel
};
toBeRemovedMenuSection = {
id: 'toBeRemovedSection',
active: false,
@@ -167,7 +179,17 @@ describe('MenuService', () => {
[MenuID.PUBLIC]: routeDataMenuChildSection
}
}
},
firstChild: {
snapshot: {
data: {
menu: {
[MenuID.PUBLIC]: routeDataMenuOverwrittenChildSection
}
}
}
}
}
}
};
@@ -541,7 +563,7 @@ describe('MenuService', () => {
});
describe('buildRouteMenuSections', () => {
it('should add and remove menu sections depending on the current route', () => {
it('should add and remove menu sections depending on the current route and overwrite menu sections when they have the same ID with the child route version', () => {
spyOn(service, 'addSection');
spyOn(service, 'removeSection');
@@ -550,7 +572,8 @@ describe('MenuService', () => {
service.buildRouteMenuSections(MenuID.PUBLIC);
expect(service.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuSectionResolved);
expect(service.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuChildSection);
expect(service.addSection).not.toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuChildSection);
expect(service.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, routeDataMenuOverwrittenChildSection);
expect(service.addSection).not.toHaveBeenCalledWith(MenuID.PUBLIC, alreadyPresentMenuSection);
expect(service.removeSection).toHaveBeenCalledWith(MenuID.PUBLIC, toBeRemovedMenuSection.id);
});

View File

@@ -399,7 +399,8 @@ export class MenuService {
}
if (!last) {
return [...menuSections, ...this.resolveRouteMenuSections(route.firstChild, menuID)];
const childMenuSections = this.resolveRouteMenuSections(route.firstChild, menuID);
return [...menuSections.filter(menu => !(childMenuSections).map(childMenu => childMenu.id).includes(menu.id)), ...childMenuSections];
} else {
return [...menuSections];
}

View File

@@ -281,6 +281,7 @@ import {
} from '../item-page/simple/field-components/specific-field/title/themed-item-page-field.component';
import { BitstreamListItemComponent } from './object-list/bitstream-list-item/bitstream-list-item.component';
import { NgxPaginationModule } from 'ngx-pagination';
import { ThemedLangSwitchComponent } from './lang-switch/themed-lang-switch.component';
const MODULES = [
CommonModule,
@@ -332,6 +333,7 @@ const COMPONENTS = [
DsSelectComponent,
ErrorComponent,
LangSwitchComponent,
ThemedLangSwitchComponent,
LoadingComponent,
ThemedLoadingComponent,
LogInComponent,

View File

@@ -267,3 +267,29 @@ ul.dso-edit-menu-dropdown > li .nav-item.nav-link {
.table td {
vertical-align: middle;
}
.pt-0\.5 {
padding-top: 0.125rem !important;
}
.pr-0\.5 {
padding-right: 0.125rem !important;
}
.pb-0\.5 {
padding-bottom: 0.125rem !important;
}
.pl-0\.5 {
padding-left: 0.125rem !important;
}
.px-0\.5 {
padding-left: 0.125rem !important;
padding-right: 0.125rem !important;
}
.py-0\.5 {
padding-top: 0.125rem !important;
padding-bottom: 0.125rem !important;
}

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { LangSwitchComponent as BaseComponent } from '../../../../../app/shared/lang-switch/lang-switch.component';
@Component({
selector: 'ds-lang-switch',
// styleUrls: ['./lang-switch.component.scss'],
styleUrls: ['../../../../../app/shared/lang-switch/lang-switch.component.scss'],
// templateUrl: './lang-switch.component.html',
templateUrl: '../../../../../app/shared/lang-switch/lang-switch.component.html',
})
export class LangSwitchComponent extends BaseComponent {
}

View File

@@ -54,6 +54,7 @@ import {
ItemSearchResultListElementComponent
} from './app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component';
import { TopLevelCommunityListComponent } from './app/home-page/top-level-community-list/top-level-community-list.component';
import { LangSwitchComponent } from './app/shared/lang-switch/lang-switch.component';
/**
@@ -91,6 +92,7 @@ const DECLARATIONS = [
EditCollectionSelectorComponent,
EditCommunitySelectorComponent,
EditItemSelectorComponent,
LangSwitchComponent,
];
@NgModule({

View File

@@ -5,9 +5,9 @@
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/>
</a>
</div>
<div class="d-flex flex-grow-1 ml-auto justify-content-end align-items-center">
<div class="navbar-buttons d-flex flex-grow-1 ml-auto justify-content-end align-items-center">
<ds-themed-search-navbar></ds-themed-search-navbar>
<ds-lang-switch></ds-lang-switch>
<ds-themed-lang-switch></ds-themed-lang-switch>
<ds-context-help-toggle></ds-context-help-toggle>
<ds-themed-auth-nav-menu></ds-themed-auth-nav-menu>
<ds-impersonate-navbar></ds-impersonate-navbar>

View File

@@ -24,3 +24,9 @@
color: var(--ds-header-icon-color-hover);
}
}
.navbar-buttons {
display: flex;
gap: calc(var(--bs-spacer) / 3);
align-items: center;
}

View File

@@ -10,15 +10,17 @@
<li *ngIf="(isXsOrSm$ | async) && (isAuthenticated$ | async)">
<ds-user-menu [inExpandableNavbar]="true"></ds-user-menu>
</li>
<ng-container *ngFor="let section of (sections | async)">
<li *ngFor="let section of (sections | async)">
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
</ng-container>
</li>
</ul>
</div>
<ds-search-navbar class="navbar-collapsed"></ds-search-navbar>
<ds-lang-switch class="navbar-collapsed"></ds-lang-switch>
<ds-context-help-toggle class="navbar-collapsed"></ds-context-help-toggle>
<ds-themed-auth-nav-menu class="navbar-collapsed"></ds-themed-auth-nav-menu>
<ds-impersonate-navbar class="navbar-collapsed"></ds-impersonate-navbar>
<div class="navbar-buttons">
<ds-themed-search-navbar class="navbar-collapsed"></ds-themed-search-navbar>
<ds-themed-lang-switch class="navbar-collapsed"></ds-themed-lang-switch>
<ds-context-help-toggle class="navbar-collapsed"></ds-context-help-toggle>
<ds-themed-auth-nav-menu class="navbar-collapsed"></ds-themed-auth-nav-menu>
<ds-impersonate-navbar class="navbar-collapsed"></ds-impersonate-navbar>
</div>
</div>
</nav>

View File

@@ -54,3 +54,9 @@ a.navbar-brand img {
color: var(--ds-navbar-link-color-hover);
}
}
.navbar-buttons {
display: flex;
gap: calc(var(--bs-spacer) / 3);
align-items: center;
}

119
yarn.lock
View File

@@ -451,13 +451,21 @@
resolved "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz"
integrity sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.21.4":
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6":
version "7.21.4"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz"
integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==
dependencies:
"@babel/highlight" "^7.18.6"
"@babel/code-frame@^7.22.13":
version "7.22.13"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
dependencies:
"@babel/highlight" "^7.22.13"
chalk "^2.4.2"
"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.4":
version "7.21.4"
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.4.tgz"
@@ -535,7 +543,7 @@
"@jridgewell/gen-mapping" "^0.3.2"
jsesc "^2.5.1"
"@babel/generator@^7.18.9", "@babel/generator@^7.19.3", "@babel/generator@^7.21.4":
"@babel/generator@^7.18.9", "@babel/generator@^7.19.3":
version "7.21.4"
resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz"
integrity sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==
@@ -545,6 +553,16 @@
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/generator@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420"
integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==
dependencies:
"@babel/types" "^7.23.0"
"@jridgewell/gen-mapping" "^0.3.2"
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/helper-annotate-as-pure@7.18.6", "@babel/helper-annotate-as-pure@^7.18.6":
version "7.18.6"
resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz"
@@ -610,6 +628,11 @@
resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz"
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
"@babel/helper-environment-visitor@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
"@babel/helper-explode-assignable-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz"
@@ -625,6 +648,14 @@
"@babel/template" "^7.20.7"
"@babel/types" "^7.21.0"
"@babel/helper-function-name@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
dependencies:
"@babel/template" "^7.22.15"
"@babel/types" "^7.23.0"
"@babel/helper-hoist-variables@^7.18.6":
version "7.18.6"
resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz"
@@ -632,6 +663,13 @@
dependencies:
"@babel/types" "^7.18.6"
"@babel/helper-hoist-variables@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb"
integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==
dependencies:
"@babel/types" "^7.22.5"
"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0":
version "7.21.0"
resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz"
@@ -715,16 +753,33 @@
dependencies:
"@babel/types" "^7.18.6"
"@babel/helper-split-export-declaration@^7.22.6":
version "7.22.6"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c"
integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==
dependencies:
"@babel/types" "^7.22.5"
"@babel/helper-string-parser@^7.19.4":
version "7.19.4"
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz"
integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==
"@babel/helper-string-parser@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1":
version "7.19.1"
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz"
integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
"@babel/helper-validator-identifier@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
"@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.21.0":
version "7.21.0"
resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz"
@@ -758,11 +813,25 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@babel/parser@^7.10.3", "@babel/parser@^7.14.7", "@babel/parser@^7.18.9", "@babel/parser@^7.19.3", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4":
"@babel/highlight@^7.22.13":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54"
integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==
dependencies:
"@babel/helper-validator-identifier" "^7.22.20"
chalk "^2.4.2"
js-tokens "^4.0.0"
"@babel/parser@^7.10.3", "@babel/parser@^7.14.7", "@babel/parser@^7.18.9", "@babel/parser@^7.19.3", "@babel/parser@^7.20.7":
version "7.21.4"
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz"
integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==
"@babel/parser@^7.22.15", "@babel/parser@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719"
integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz"
@@ -1403,19 +1472,28 @@
"@babel/parser" "^7.20.7"
"@babel/types" "^7.20.7"
"@babel/traverse@^7.10.3", "@babel/traverse@^7.18.9", "@babel/traverse@^7.19.3", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2":
version "7.21.4"
resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz"
integrity sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==
"@babel/template@^7.22.15":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
dependencies:
"@babel/code-frame" "^7.21.4"
"@babel/generator" "^7.21.4"
"@babel/helper-environment-visitor" "^7.18.9"
"@babel/helper-function-name" "^7.21.0"
"@babel/helper-hoist-variables" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6"
"@babel/parser" "^7.21.4"
"@babel/types" "^7.21.4"
"@babel/code-frame" "^7.22.13"
"@babel/parser" "^7.22.15"
"@babel/types" "^7.22.15"
"@babel/traverse@^7.10.3", "@babel/traverse@^7.18.9", "@babel/traverse@^7.19.3", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2":
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
dependencies:
"@babel/code-frame" "^7.22.13"
"@babel/generator" "^7.23.0"
"@babel/helper-environment-visitor" "^7.22.20"
"@babel/helper-function-name" "^7.23.0"
"@babel/helper-hoist-variables" "^7.22.5"
"@babel/helper-split-export-declaration" "^7.22.6"
"@babel/parser" "^7.23.0"
"@babel/types" "^7.23.0"
debug "^4.1.0"
globals "^11.1.0"
@@ -1428,6 +1506,15 @@
"@babel/helper-validator-identifier" "^7.19.1"
to-fast-properties "^2.0.0"
"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb"
integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==
dependencies:
"@babel/helper-string-parser" "^7.22.5"
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
"@colors/colors@1.5.0":
version "1.5.0"
resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz"
@@ -3849,7 +3936,7 @@ chalk@^1.1.1:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1:
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==