[ACS-3742] Layout changes for workspace (#2980)

* Layout changes for workspace sidemenu

* added header and search layout changes

* implemented review comments and removed process related code

* Added expand and collapse functionality

* Modified the paths

* linting fixes

* use standard material settings icon

* use only specific modules needed for page layout

* use standard "menu" icon for now

* use standard avatar icon for now

* cleanup user profile menu item

* cleanup About component layout

* remove hardcoded settings route

* deprecate "headerImagePath"

* deprecate "headerTextColor" customisation

* deprecate "headerColor" customisation

* proper toggle of the side menu

* proper sidebar header implementation

* user profile basic cleanup

* minor fixes

* cleanup buttons

* remove old app layout and use ADF one

* remove old header component

* cleanup old layout module

* fix unit tests

* cleanup unit tests

* cleanup header actions module

* deprecate unused main-action component

* cleanup styles

* restore removed method

* cleanup search results toolbar

* restore expand menu functionality

* cleanup code, back buttons for about and profile

* restore original code

* proper collapse button

* remove unused i18n key

* styles cleanup

* cleanup sidebar

* cleanup user profile

* add safety checks for focus after close

* layout fixes

* update view profile unit tests

* code cleanup after reviews

* cleanup header actions

* fix menu population, user info

* improved upload and create actions

* remove useless tests

* fix folder rules tests

* search button workaround

* e2e: remove wait

* add create/upload tooltips

* e2e fix

* e2e fix

* e2e fix

* e2e fix

* e2e fix

* e2e fix

* e2e fix

* e2e fix

* e2e fix

* e2e fix

* e2e fix

* e2e fix

* try fix e2e

* update e2e extension configs

* try fix e2e

* try fix e2e

* fix eslint config

* try fix e2e

* move search button to extensions

* move upload and create to extensions

* remove header actions as no longer needed

* cleanup

* e2e fixes and cleanup for unwanted files

* linting fixes

* linting fixes

* added button type to support text buttons

* linting fixes

* added more unit tests to achieve code coverage requirement

* fixing code covergae for aca-content

* fixed code coverage for aca-shared

* linting fixes

* linting fixes

* cleanup

* version fix

---------

Co-authored-by: SheenaMalhotra182 <sheena.malhotra@globallogic.com>
Co-authored-by: Denys Vuika <denys.vuika@gmail.com>
Co-authored-by: SheenaMalhotra182 <sheena.malhotra@contractors.onbase.com>
This commit is contained in:
Yasa-Nataliya
2023-04-20 16:48:24 +05:30
committed by GitHub
parent d042b80386
commit 6fac964d94
140 changed files with 1663 additions and 2177 deletions

View File

@@ -42,7 +42,7 @@ import {
LibraryStatusColumnComponent,
TrashcanNameColumnComponent
} from '@alfresco/adf-content-services';
import { DocumentBasePageService, ExtensionsDataLoaderGuard, RouterExtensionService, SharedModule } from '@alfresco/aca-shared';
import { DocumentBasePageService, ExtensionsDataLoaderGuard, PageLayoutModule, RouterExtensionService, SharedModule } from '@alfresco/aca-shared';
import * as rules from '@alfresco/aca-shared/rules';
import { FilesComponent } from './components/files/files.component';
@@ -61,11 +61,9 @@ import { AppToolbarModule } from './components/toolbar/toolbar.module';
import { AppCreateMenuModule } from './components/create-menu/create-menu.module';
import { AppSidenavModule } from './components/sidenav/sidenav.module';
import { AppCommonModule } from './components/common/common.module';
import { AppLayoutModule } from './components/layout/layout.module';
import { AppSearchInputModule } from './components/search/search-input.module';
import { DocumentListCustomComponentsModule } from './components/dl-custom-components/document-list-custom-components.module';
import { AppSearchResultsModule } from './components/search/search-results.module';
import { AppHeaderModule } from './components/header/header.module';
import { AppNodeVersionModule } from './components/node-version/node-version.module';
import { FavoritesComponent } from './components/favorites/favorites.component';
import { RecentFilesComponent } from './components/recent-files/recent-files.component';
@@ -96,7 +94,6 @@ import { LocationLinkComponent } from './components/common/location-link/locatio
import { LogoutComponent } from './components/common/logout/logout.component';
import { ToggleSharedComponent } from './components/common/toggle-shared/toggle-shared.component';
import { CustomNameColumnComponent } from './components/dl-custom-components/name-column/name-column.component';
import { AppHeaderComponent } from './components/header/header.component';
import { CommentsTabComponent } from './components/info-drawer/comments-tab/comments-tab.component';
import { LibraryMetadataTabComponent } from './components/info-drawer/library-metadata-tab/library-metadata-tab.component';
import { MetadataTabComponent } from './components/info-drawer/metadata-tab/metadata-tab.component';
@@ -113,14 +110,14 @@ import { ViewNodeComponent } from './components/toolbar/view-node/view-node.comp
import { CONTENT_ROUTES } from './aca-content.routes';
import { RouterModule } from '@angular/router';
import { UploadFilesDialogComponent } from './components/upload-files-dialog/upload-files-dialog.component';
import { SidenavWrapperComponent } from './components/sidenav/sidenav-wrapper/sidenav-wrapper.component';
import { AppLayoutComponent } from './components/layout/app-layout/app-layout.component';
import { AppTrashcanModule } from './components/trashcan/trashcan.module';
import { AppSharedLinkViewModule } from './components/shared-link-view/shared-link-view.module';
import { AcaFolderRulesModule } from '@alfresco/aca-folder-rules';
import { TagsColumnComponent } from './components/dl-custom-components/tags-column/tags-column.component';
import { ContentManagementService } from './services/content-management.service';
import { UserInfoComponent } from './components/common/user-info/user-info.component';
import { SidenavComponent } from './components/sidenav/sidenav.component';
import { ContentManagementService } from './services/content-management.service';
import { ShellLayoutComponent, SHELL_NAVBAR_MIN_WIDTH } from '@alfresco/adf-core/shell';
registerLocaleData(localeFr);
registerLocaleData(localeDe);
@@ -153,7 +150,7 @@ registerLocaleData(localeSv);
MaterialModule,
AppStoreModule,
AppCommonModule,
AppLayoutModule,
PageLayoutModule,
DirectivesModule,
ContextMenuModule,
AppInfoDrawerModule,
@@ -163,7 +160,6 @@ registerLocaleData(localeSv);
DocumentListCustomComponentsModule,
AppSearchInputModule,
AppSearchResultsModule,
AppHeaderModule,
AppNodeVersionModule,
HammerModule,
ViewProfileModule,
@@ -194,7 +190,8 @@ registerLocaleData(localeSv);
name: 'app',
source: 'assets'
}
}
},
{ provide: SHELL_NAVBAR_MIN_WIDTH, useValue: 0 }
]
})
export class ContentServiceExtensionModule {
@@ -205,9 +202,8 @@ export class ContentServiceExtensionModule {
});
extensions.setComponents({
'app.layout.main': AppLayoutComponent,
'app.layout.header': AppHeaderComponent,
'app.layout.sidenav': SidenavWrapperComponent,
'app.layout.main': ShellLayoutComponent,
'app.layout.sidenav': SidenavComponent,
'app.shell.sibling': UploadFilesDialogComponent,
'app.components.tabs.metadata': MetadataTabComponent,
'app.components.tabs.library.metadata': LibraryMetadataTabComponent,
@@ -300,7 +296,10 @@ export class ContentServiceExtensionModule {
'repository.isQuickShareEnabled': rules.hasQuickShareEnabled,
'user.isAdmin': rules.isAdmin,
'app.canShowLogout': rules.canShowLogout,
'app.isContentServiceEnabled': rules.isContentServiceEnabled
'app.isContentServiceEnabled': rules.isContentServiceEnabled,
'app.isUploadSupported': rules.isUploadSupported,
'app.canCreateLibrary': rules.canCreateLibrary,
'app.isSearchSupported': rules.isSearchSupported
});
}
}

View File

@@ -22,7 +22,6 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { AppLayoutComponent } from './components/layout/app-layout/app-layout.component';
import { FilesComponent } from './components/files/files.component';
import { LibrariesComponent } from './components/libraries/libraries.component';
import { FavoriteLibrariesComponent } from './components/favorite-libraries/favorite-libraries.component';
@@ -40,6 +39,7 @@ import { ViewProfileRuleGuard } from './components/view-profile/view-profile.gua
import { Route } from '@angular/router';
import { SharedLinkViewComponent } from './components/shared-link-view/shared-link-view.component';
import { TrashcanComponent } from './components/trashcan/trashcan.component';
import { ShellLayoutComponent } from '@alfresco/adf-core/shell';
export const CONTENT_ROUTES: ExtensionRoute[] = [
{
@@ -56,7 +56,7 @@ export const CONTENT_ROUTES: ExtensionRoute[] = [
},
{
path: 'view',
component: AppLayoutComponent,
component: ShellLayoutComponent,
children: [
{
path: ':nodeId',

View File

@@ -33,9 +33,10 @@ import { LanguagePickerComponent } from './language-picker/language-picker.compo
import { LogoutComponent } from './logout/logout.component';
import { ContentModule } from '@alfresco/adf-content-services';
import { UserInfoComponent } from './user-info/user-info.component';
import { RouterModule } from '@angular/router';
@NgModule({
imports: [CommonModule, CoreModule.forChild(), ContentModule.forChild(), ExtensionsModule, GenericErrorModule],
imports: [CommonModule, CoreModule.forChild(), ContentModule.forChild(), ExtensionsModule, GenericErrorModule, RouterModule],
declarations: [LocationLinkComponent, ToggleSharedComponent, LanguagePickerComponent, LogoutComponent, UserInfoComponent],
exports: [
ExtensionsModule,

View File

@@ -1,14 +1,4 @@
<ng-container>
<adf-content-user-info
*ngIf="mode === userInfoMode.CONTENT || mode === userInfoMode.CONTENT_SSO"
[ecmUser]="ecmUser$ | async"
[identityUser]="identityUser$ | async"
[isLoggedIn]="isLoggedIn"
[mode]="mode"
></adf-content-user-info>
<adf-identity-user-info
*ngIf="mode === userInfoMode.SSO"
[identityUser]="identityUser$ | async"
[isLoggedIn]="isLoggedIn"
></adf-identity-user-info>
</ng-container>
<button mat-menu-item [routerLink]="['/profile']" title="{{'APP.TOOLTIPS.MY_PROFILE' | translate}}">
<mat-icon>account_circle</mat-icon>
<span>{{ (displayName$ | async) }}</span>
</button>

View File

@@ -0,0 +1,121 @@
/*!
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AuthenticationService } from '@alfresco/adf-core';
import { PeopleContentService } from '@alfresco/adf-content-services';
import { UserInfoComponent } from './user-info.component';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { of } from 'rxjs';
describe('UserInfoComponent', () => {
let component: UserInfoComponent;
let fixture: ComponentFixture<UserInfoComponent>;
let authServiceStub: Partial<AuthenticationService>;
let peopleContentServiceStub: Partial<PeopleContentService>;
let identityUserServiceStub: Partial<PeopleContentService>;
beforeEach(() => {
authServiceStub = {
isOauth: () => true,
isECMProvider: () => true,
isEcmLoggedIn: () => true,
isKerberosEnabled: () => false,
isLoggedIn: () => true
};
peopleContentServiceStub = {
getCurrentUserInfo: () =>
of({
firstName: 'John',
email: 'john@example.com',
id: 'johnDoe1',
enabled: true,
company: {
organization: 'ABC Organization',
address1: 'XYZ Road',
address2: 'Ohio',
address3: 'Westlake',
postcode: '44145',
telephone: '456876',
fax: '323984',
email: 'contact.us@abc.com'
},
isAdmin: () => true
})
};
identityUserServiceStub = {
getCurrentUserInfo: () =>
of({
firstName: 'John',
email: 'john@example.com',
id: 'johnDoe1',
enabled: true,
company: {
organization: 'ABC Organization',
address1: 'XYZ Road',
address2: 'Ohio',
address3: 'Westlake',
postcode: '44145',
telephone: '456876',
fax: '323984',
email: 'contact.us@abc.com'
},
isAdmin: () => true
})
};
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [UserInfoComponent],
providers: [
{ provide: AuthenticationService, useValue: authServiceStub },
{ provide: PeopleContentService, useValue: peopleContentServiceStub },
{ provide: identityUserServiceStub, useValue: identityUserServiceStub }
]
}).compileComponents();
fixture = TestBed.createComponent(UserInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should check if user is logged in', async () => {
const loggedIn = component.isLoggedIn;
expect(loggedIn).toBeTrue();
});
it('should parse display name without email', async () => {
const model = { firstName: 'John' };
const displayName = component['parseDisplayName'](model);
expect(displayName).toBe('John');
});
it('should parse display name with email', async () => {
const model = { firstName: 'John', email: 'john@example.com' };
const displayName = component['parseDisplayName'](model);
expect(displayName).toBe('John (john@example.com)');
});
});

View File

@@ -22,21 +22,18 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { IdentityUserModel, IdentityUserService, AuthenticationService, UserInfoMode } from '@alfresco/adf-core';
import { IdentityUserService, AuthenticationService } from '@alfresco/adf-core';
import { Component, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
import { EcmUserModel, PeopleContentService } from '@alfresco/adf-content-services';
import { PeopleContentService } from '@alfresco/adf-content-services';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-user-info',
templateUrl: './user-info.component.html'
})
export class UserInfoComponent implements OnInit {
mode: UserInfoMode;
ecmUser$: Observable<EcmUserModel>;
identityUser$: Observable<IdentityUserModel>;
selectedIndex: number;
userInfoMode = UserInfoMode;
displayName$: Observable<string>;
constructor(
private peopleContentService: PeopleContentService,
@@ -51,15 +48,12 @@ export class UserInfoComponent implements OnInit {
getUserInfo() {
if (this.authService.isOauth()) {
this.loadIdentityUserInfo();
this.mode = UserInfoMode.SSO;
if (this.authService.isECMProvider() && this.authService.isEcmLoggedIn()) {
this.mode = UserInfoMode.CONTENT_SSO;
this.loadEcmUserInfo();
}
} else if (this.isEcmLoggedIn()) {
this.loadEcmUserInfo();
this.mode = UserInfoMode.CONTENT;
}
}
@@ -71,11 +65,21 @@ export class UserInfoComponent implements OnInit {
}
private loadEcmUserInfo(): void {
this.ecmUser$ = this.peopleContentService.getCurrentUserInfo();
this.displayName$ = this.peopleContentService.getCurrentUserInfo().pipe(map((model) => this.parseDisplayName(model)));
}
private loadIdentityUserInfo() {
this.identityUser$ = of(this.identityUserService.getCurrentUserInfo());
this.displayName$ = of(this.identityUserService.getCurrentUserInfo()).pipe(map((model) => this.parseDisplayName(model)));
}
private parseDisplayName(model: { firstName?: string; email?: string }): string {
let result = model.firstName;
if (model.email) {
result = `${model.firstName} (${model.email})`;
}
return result;
}
private isEcmLoggedIn() {

View File

@@ -1,19 +0,0 @@
<adf-layout-header
[logo]="logo$ | async"
[redirectUrl]="landingPage"
[tooltip]="appName$ | async"
[color]="headerColor$ | async"
[title]="appName$ | async"
(clicked)="onToggleSidenav($event)"
[expandedSidenav]="isSidenavExpanded"
>
<div class="adf-toolbar--spacer adf-toolbar-divider"></div>
<aca-search-input *ngIf="isContentServiceEnabled()"></aca-search-input>
<adf-toolbar-divider></adf-toolbar-divider>
<ng-container *ngFor="let actionRef of actions; trackBy: trackByActionId">
<aca-toolbar-action [actionRef]="actionRef"> </aca-toolbar-action>
</ng-container>
</adf-layout-header>

View File

@@ -1,21 +0,0 @@
.app-header {
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.02), 0 6px 10px 0 rgba(0, 0, 0, 0.014), 0 1px 18px 0 rgba(0, 0, 0, 0.012);
z-index: 2;
adf-layout-header .mat-toolbar-single-row {
color: var(--theme-header-text-color) !important;
}
.mat-toolbar {
background-image: var(--header-background-image) !important;
background-repeat: no-repeat !important;
.aca-current-user {
color: var(--theme-foreground-text-color) !important;
}
.adf-toolbar-divider div {
background-color: var(--theme-foreground-text-color) !important;
}
}
}

View File

@@ -1,133 +0,0 @@
/*!
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { AppHeaderComponent } from './header.component';
import { AppState } from '@alfresco/aca-shared/store';
import { of } from 'rxjs';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { ContentActionRef } from '@alfresco/adf-extensions';
import { Store } from '@ngrx/store';
import { AppTestingModule } from '../../testing/app-testing.module';
import { AppExtensionService, SharedToolbarModule } from '@alfresco/aca-shared';
import { CoreModule, SidenavLayoutComponent } from '@alfresco/adf-core';
import { AppSearchInputModule } from '../search/search-input.module';
import { By } from '@angular/platform-browser';
describe('AppHeaderComponent', () => {
let component: AppHeaderComponent;
let fixture: ComponentFixture<AppHeaderComponent>;
const actions = [
{ id: 'action-1', type: 'button' },
{ id: 'action-2', type: 'button' }
] as Array<ContentActionRef>;
const store = {
select: jasmine.createSpy('select'),
dispatch: () => {}
} as any;
const appExtensionService = {
getHeaderActions: () => of(actions)
} as any;
const app = {
headerColor: 'some-color',
headerTextColor: 'text-color',
appName: 'name',
logoPath: 'some/path'
} as AppState;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, CoreModule.forChild(), AppSearchInputModule, SharedToolbarModule],
declarations: [AppHeaderComponent],
providers: [
{
provide: AppExtensionService,
useValue: appExtensionService
},
{
provide: Store,
useValue: store
}
]
});
store.select.and.callFake((memoizeFn) => of(memoizeFn({ app })));
fixture = TestBed.createComponent(AppHeaderComponent);
component = fixture.componentInstance;
});
it('should set header color, header text color, name and logo', fakeAsync(() => {
component.appName$.subscribe((val) => expect(val).toBe(app.appName));
component.logo$.subscribe((val) => expect(val).toBe(app.logoPath));
component.headerColor$.subscribe((val) => expect(val).toBe(app.headerColor));
component.headerTextColor$.subscribe((val) => expect(val).toBe(app.headerTextColor));
}));
it('should get header actions', fakeAsync(() => {
component.ngOnInit();
tick();
expect(component.actions).toEqual(actions);
}));
it('should minimize sidenav on toggle sidenav click', () => {
const layout = TestBed.createComponent(SidenavLayoutComponent);
const mockData: any = { layout: layout.componentInstance, isMenuMinimized: true };
component.data = mockData;
const toggleMenuSpy = spyOn(component.data.layout, 'toggleMenu');
component.onToggleSidenav(true);
expect(toggleMenuSpy).toHaveBeenCalled();
expect(component.isSidenavExpanded).toBe(false);
});
describe('Search input', () => {
beforeEach(() => {
localStorage.clear();
});
afterEach(() => {
localStorage.clear();
});
it('should search be present when contentService is enabled', () => {
fixture.detectChanges();
const searchInput = fixture.debugElement.query(By.css('.aca-search-input'));
expect(searchInput).not.toBeNull();
});
it('should search not be present when contentService is disabled', () => {
localStorage.setItem('contentService', 'false');
fixture.detectChanges();
const searchInput = fixture.debugElement.query(By.css('.aca-search-input'));
expect(searchInput).toBeNull();
});
});
});

View File

@@ -1,45 +0,0 @@
<adf-upload-drag-area [rootFolderId]="currentFolderId" [disabled]="!canUpload">
<adf-sidenav-layout
#layout
[sidenavMin]="70"
[sidenavMax]="320"
[stepOver]="600"
[hideSidenav]="hideSidenav"
[expandedSidenav]="expandedSidenav"
(expanded)="onExpanded($event)"
>
<adf-sidenav-layout-header>
<ng-template let-isMenuMinimized="isMenuMinimized">
<app-header
*ngIf="!hideSidenav"
role="heading"
aria-level="1"
(toggleClicked)="layout.toggleMenu()"
[data]="{ layout }"
[expandedSidenav]="!isMenuMinimized()">
</app-header>
</ng-template>
</adf-sidenav-layout-header>
<adf-sidenav-layout-navigation>
<ng-template let-isMenuMinimized="isMenuMinimized">
<app-sidenav
[mode]="isMenuMinimized() ? 'collapsed' : 'expanded'"
[attr.data-automation-id]="isMenuMinimized() ? 'collapsed' : 'expanded'"
(swipeleft)="hideMenu($event)"
>
</app-sidenav>
</ng-template>
</adf-sidenav-layout-navigation>
<adf-sidenav-layout-content>
<ng-template>
<router-outlet></router-outlet>
</ng-template>
</adf-sidenav-layout-content>
</adf-sidenav-layout>
<adf-file-uploading-dialog *ngIf="showFileUploadingDialog" position="left"></adf-file-uploading-dialog>
</adf-upload-drag-area>
<router-outlet name="viewer"></router-outlet>

View File

@@ -1,34 +0,0 @@
.app-layout {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
overflow: hidden;
min-height: 0;
router-outlet[name='viewer'] + * {
width: 100%;
height: 100%;
z-index: 999;
position: absolute;
top: 0;
right: 0;
background-color: white;
}
adf-file-uploading-dialog {
z-index: 1000;
}
}
@media screen and (max-width: 599px) {
.adf-app-title {
display: none;
}
}
@media screen and (max-width: 719px) {
.adf-app-logo {
display: none;
}
}

View File

@@ -1,220 +0,0 @@
/*!
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { AppConfigService, UserPreferencesService } from '@alfresco/adf-core';
import { FileModel, UploadService } from '@alfresco/adf-content-services';
import { AppLayoutComponent } from './app-layout.component';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { Store } from '@ngrx/store';
import { AppStore, SetSelectedNodesAction, ResetSelectionAction } from '@alfresco/aca-shared/store';
import { Router, NavigationStart } from '@angular/router';
import { of, Subject } from 'rxjs';
import { By } from '@angular/platform-browser';
class MockRouter {
private url = 'some-url';
private subject = new Subject();
events = this.subject.asObservable();
routerState = { snapshot: { url: this.url } };
navigateByUrl(url: string) {
const navigationStart = new NavigationStart(0, url);
this.subject.next(navigationStart);
}
}
describe('AppLayoutComponent', () => {
let fixture: ComponentFixture<AppLayoutComponent>;
let component: AppLayoutComponent;
let appConfig: AppConfigService;
let userPreference: UserPreferencesService;
let store: Store<AppStore>;
let router: Router;
let uploadService: UploadService;
let fakeFileList: FileModel[];
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
providers: [
Store,
{
provide: Router,
useClass: MockRouter
}
],
declarations: [AppLayoutComponent],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(AppLayoutComponent);
component = fixture.componentInstance;
appConfig = TestBed.inject(AppConfigService);
store = TestBed.inject(Store);
router = TestBed.inject(Router);
userPreference = TestBed.inject(UserPreferencesService);
fakeFileList = [new FileModel(new File([], 'fakeFile'))];
uploadService = TestBed.inject(UploadService);
});
beforeEach(() => {
appConfig.config.languages = [];
appConfig.config.locale = 'en';
});
describe('sidenav state', () => {
it('should get state from configuration', () => {
appConfig.config.sideNav = {
expandedSidenav: false,
preserveState: false
};
fixture.detectChanges();
expect(component.expandedSidenav).toBe(false);
});
it('should resolve state to true is no configuration', () => {
appConfig.config.sidenav = {};
fixture.detectChanges();
expect(component.expandedSidenav).toBe(true);
});
it('should get state from user settings as true', () => {
appConfig.config.sideNav = {
expandedSidenav: false,
preserveState: true
};
spyOn(userPreference, 'get').and.callFake((key) => {
if (key === 'expandedSidenav') {
return 'true';
}
return 'false';
});
fixture.detectChanges();
expect(component.expandedSidenav).toBe(true);
});
it('should get state from user settings as false', () => {
appConfig.config.sidenav = {
expandedSidenav: false,
preserveState: true
};
spyOn(userPreference, 'get').and.callFake((key) => {
if (key === 'expandedSidenav') {
return 'false';
}
return 'true';
});
fixture.detectChanges();
expect(component.expandedSidenav).toBe(false);
});
});
it('should reset selection before navigation', () => {
const selection: any[] = [{ entry: { id: 'nodeId', name: 'name' } }];
spyOn(store, 'dispatch').and.stub();
fixture.detectChanges();
store.dispatch(new SetSelectedNodesAction(selection));
router.navigateByUrl('somewhere/over/the/rainbow');
fixture.detectChanges();
expect(store.dispatch['calls'].mostRecent().args).toEqual([new ResetSelectionAction()]);
});
it('should close menu on mobile screen size', () => {
component.minimizeSidenav = false;
component.layout.container = {
isMobileScreenSize: true,
toggleMenu: () => {}
};
spyOn(component.layout.container, 'toggleMenu');
fixture.detectChanges();
component.hideMenu({ preventDefault: () => {} } as any);
expect(component.layout.container.toggleMenu).toHaveBeenCalled();
});
it('should close menu on mobile screen size also when minimizeSidenav true', () => {
fixture.detectChanges();
component.minimizeSidenav = true;
component.layout.container = {
isMobileScreenSize: true,
toggleMenu: () => {}
};
spyOn(component.layout.container, 'toggleMenu');
fixture.detectChanges();
component.hideMenu({ preventDefault: () => {} } as any);
expect(component.layout.container.toggleMenu).toHaveBeenCalled();
});
describe('File Uploading Dialog', () => {
it('should the uploading file dialog be visible on the left when the showFileUploadingDialog is true', async () => {
fixture.detectChanges();
await fixture.whenStable();
uploadService.addToQueue(...fakeFileList);
fixture.detectChanges();
await fixture.whenStable();
const fileUploadingDialog = fixture.debugElement.query(By.css('adf-file-uploading-dialog'));
expect(fileUploadingDialog.attributes['position']).toEqual('left');
expect(component.showFileUploadingDialog).toEqual(true);
expect(fileUploadingDialog).not.toEqual(null);
});
it('should the uploading file dialog not be visible when the showFileUploadingDialog is false', async () => {
spyOn(store, 'select').and.returnValue(of(false));
fixture.detectChanges();
await fixture.whenStable();
uploadService.addToQueue(...fakeFileList);
fixture.detectChanges();
await fixture.whenStable();
const fileUploadingDialog = fixture.debugElement.query(By.css('adf-file-uploading-dialog'));
expect(component.showFileUploadingDialog).toEqual(false);
expect(fileUploadingDialog).toEqual(null);
});
});
});

View File

@@ -1,176 +0,0 @@
/*!
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { AppConfigService, SidenavLayoutComponent, UserPreferencesService } from '@alfresco/adf-core';
import { Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { NavigationEnd, Router, NavigationStart } from '@angular/router';
import { Store } from '@ngrx/store';
import { Subject, Observable } from 'rxjs';
import { filter, takeUntil, map, withLatestFrom, delay } from 'rxjs/operators';
import { NodePermissionService } from '@alfresco/aca-shared';
import { BreakpointObserver } from '@angular/cdk/layout';
import { AppStore, getCurrentFolder, getFileUploadingDialog, ResetSelectionAction } from '@alfresco/aca-shared/store';
import { Directionality } from '@angular/cdk/bidi';
@Component({
selector: 'app-layout',
templateUrl: './app-layout.component.html',
styleUrls: ['./app-layout.component.scss'],
encapsulation: ViewEncapsulation.None,
host: { class: 'app-layout' }
})
export class AppLayoutComponent implements OnInit, OnDestroy {
@ViewChild('layout', { static: true })
layout: SidenavLayoutComponent;
onDestroy$: Subject<boolean> = new Subject<boolean>();
isSmallScreen$: Observable<boolean>;
expandedSidenav: boolean;
currentFolderId: string;
canUpload = false;
minimizeSidenav = false;
hideSidenav = false;
direction: Directionality;
showFileUploadingDialog: boolean;
private minimizeConditions: string[] = ['search'];
private hideConditions: string[] = ['/preview/'];
constructor(
protected store: Store<AppStore>,
private permission: NodePermissionService,
private router: Router,
private userPreferenceService: UserPreferencesService,
private appConfigService: AppConfigService,
private breakpointObserver: BreakpointObserver
) {}
ngOnInit() {
this.isSmallScreen$ = this.breakpointObserver.observe(['(max-width: 600px)']).pipe(map((result) => result.matches));
this.hideSidenav = this.hideConditions.some((el) => this.router.routerState.snapshot.url.includes(el));
this.minimizeSidenav = this.minimizeConditions.some((el) => this.router.routerState.snapshot.url.includes(el));
if (!this.minimizeSidenav) {
this.expandedSidenav = this.getSidenavState();
} else {
this.expandedSidenav = false;
}
this.store
.select(getCurrentFolder)
.pipe(takeUntil(this.onDestroy$))
.subscribe((node) => {
this.currentFolderId = node ? node.id : null;
this.canUpload = node && this.permission.check(node, ['create']);
});
this.router.events
.pipe(
withLatestFrom(this.isSmallScreen$),
filter(([event, isSmallScreen]) => isSmallScreen && event instanceof NavigationEnd),
takeUntil(this.onDestroy$)
)
.subscribe(() => {
this.layout.container.sidenav.close();
});
this.router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
takeUntil(this.onDestroy$)
)
.subscribe((event: NavigationEnd) => {
this.minimizeSidenav = this.minimizeConditions.some((el) => event.urlAfterRedirects.includes(el));
this.hideSidenav = this.hideConditions.some((el) => event.urlAfterRedirects.includes(el));
this.updateState();
});
this.router.events
.pipe(
filter((event) => event instanceof NavigationStart),
takeUntil(this.onDestroy$)
)
.subscribe(() => {
this.store.dispatch(new ResetSelectionAction());
});
this.store
.select(getFileUploadingDialog)
.pipe(delay(0), takeUntil(this.onDestroy$))
.subscribe((fileUploadingDialog: boolean) => {
this.showFileUploadingDialog = fileUploadingDialog;
});
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
hideMenu(event: Event) {
if (this.layout.container.isMobileScreenSize) {
event.preventDefault();
this.layout.container.toggleMenu();
}
}
private updateState() {
if (this.minimizeSidenav && !this.layout.isMenuMinimized) {
this.layout.isMenuMinimized = true;
if (!this.layout.container.isMobileScreenSize) {
this.layout.container.toggleMenu();
}
}
if (!this.minimizeSidenav) {
if (this.getSidenavState() && this.layout.isMenuMinimized) {
this.layout.isMenuMinimized = false;
this.layout.container.toggleMenu();
}
}
}
onExpanded(state: boolean) {
if (!this.minimizeSidenav && this.appConfigService.get('sideNav.preserveState')) {
this.userPreferenceService.set('expandedSidenav', state);
}
}
private getSidenavState(): boolean {
const expand = this.appConfigService.get<boolean>('sideNav.expandedSidenav', true);
const preserveState = this.appConfigService.get<boolean>('sideNav.preserveState', true);
if (preserveState) {
return this.userPreferenceService.get('expandedSidenav', expand.toString()) === 'true';
}
return expand;
}
}

View File

@@ -1,52 +0,0 @@
/*!
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from '@alfresco/adf-core';
import { AppLayoutComponent } from './app-layout/app-layout.component';
import { ContentModule } from '@alfresco/adf-content-services';
import { RouterModule } from '@angular/router';
import { AppSidenavModule } from '../sidenav/sidenav.module';
import { AppCommonModule } from '../common/common.module';
import { AppHeaderModule } from '../header/header.module';
import { HttpClientModule } from '@angular/common/http';
import { PageLayoutModule } from '@alfresco/aca-shared';
@NgModule({
imports: [
CommonModule,
RouterModule,
CoreModule.forChild(),
ContentModule.forChild(),
AppCommonModule,
AppSidenavModule,
AppHeaderModule,
HttpClientModule,
PageLayoutModule
],
declarations: [AppLayoutComponent],
exports: [AppLayoutComponent, PageLayoutModule]
})
export class AppLayoutModule {}

View File

@@ -1,30 +0,0 @@
<ng-container *ngIf="(mainAction$ | async) as action">
<ng-container [ngSwitch]="action.type">
<ng-container *ngSwitchCase="actionTypes.button">
<button
*ngIf="expanded"
mat-stroked-button
[id]="action.id"
(click)="runAction(action.actions.click)"
[disabled]="action.disabled"
class="app-main-action-button"
data-automation-id="app-main-action-button"
>
{{action.title | translate}}
</button>
<button
*ngIf="expanded === false"
mat-icon-button
[id]="action.id"
(click)="runAction(action.actions.click)"
[disabled]="action.disabled"
data-automation-id="app-main-action-icon"
title="{{ action.title| translate }}"
>
<mat-icon class="main-action-menu-icon">{{action.icon}}</mat-icon>
</button>
</ng-container>
</ng-container>
</ng-container>

View File

@@ -1,10 +0,0 @@
.app-main-action-button {
width: 100%;
border-radius: 4px;
background-color: var(--theme-accent-color);
color: var(--theme-accent-color-default-contrast);
}
.main-action-menu-icon {
color: var(--theme-accent-color);
}

View File

@@ -1,156 +0,0 @@
/*!
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MainActionComponent } from './main-action.component';
import { TranslationService, TranslationMock } from '@alfresco/adf-core';
import { AppExtensionService } from '@alfresco/aca-shared';
import { of } from 'rxjs';
import { ACTION_CLICK, ACTION_TITLE, getContentActionRef } from '../../testing/content-action-ref';
import { AppExtensionServiceMock } from '../../testing/app-extension-service-mock';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { TranslateModule } from '@ngx-translate/core';
describe('MainActionComponent', () => {
let mainActionComponent: MainActionComponent;
const buttonQuery = '[data-automation-id="app-main-action-button"]';
const iconQuery = '[data-automation-id="app-main-action-icon"]';
let fixture: ComponentFixture<MainActionComponent>;
let appExtensionService: AppExtensionServiceMock;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [CommonModule, MatButtonModule, TranslateModule.forRoot()],
providers: [
{ provide: TranslationService, useClass: TranslationMock },
{ provide: AppExtensionService, useClass: AppExtensionServiceMock }
]
}).compileComponents();
appExtensionService = TestBed.inject(AppExtensionService);
fixture = TestBed.createComponent(MainActionComponent);
mainActionComponent = fixture.componentInstance;
});
describe('component is in expanded mode', () => {
beforeEach(async () => {
mainActionComponent.expanded = true;
fixture.detectChanges();
});
it('should display button if main action is configured', () => {
const buttonMainAction = fixture.debugElement.nativeElement.querySelector(buttonQuery);
const iconMainAction = fixture.debugElement.nativeElement.querySelector(iconQuery);
expect(iconMainAction).toBeFalsy();
expect(buttonMainAction.textContent.trim()).toBe(ACTION_TITLE);
});
it('should not display button if main action is not configured', () => {
spyOn(appExtensionService, 'getMainAction').and.returnValue(of(undefined));
mainActionComponent.ngOnInit();
fixture.detectChanges();
const button = fixture.debugElement.nativeElement.querySelector(buttonQuery);
expect(button).toBeFalsy();
});
it('should call extension action', () => {
const runExtensionActionSpy = spyOn(appExtensionService, 'runActionById');
const button = fixture.debugElement.nativeElement.querySelector(buttonQuery);
button.click();
expect(runExtensionActionSpy).toHaveBeenCalledWith(ACTION_CLICK);
});
it('should not call button if main action is disabled', () => {
const disabledMainActionRef = getContentActionRef();
disabledMainActionRef.disabled = true;
spyOn(appExtensionService, 'getMainAction').and.returnValue(of(disabledMainActionRef));
const runAction = spyOn(mainActionComponent, 'runAction');
mainActionComponent.ngOnInit();
fixture.detectChanges();
const button = fixture.debugElement.nativeElement.querySelector(buttonQuery);
button.click();
expect(runAction).not.toHaveBeenCalled();
});
});
describe('component is displayed as icon', () => {
beforeEach(async () => {
mainActionComponent.expanded = false;
fixture.detectChanges();
});
it('should display icon if main action is configured', () => {
const buttonMainAction = fixture.debugElement.nativeElement.querySelector(buttonQuery);
const iconMainAction = fixture.debugElement.nativeElement.querySelector(iconQuery);
expect(buttonMainAction).toBeFalsy();
expect(iconMainAction).toBeTruthy();
});
it('should not display icon if main action is not configured', () => {
spyOn(appExtensionService, 'getMainAction').and.returnValue(of(undefined));
mainActionComponent.ngOnInit();
fixture.detectChanges();
const mainAction = fixture.debugElement.nativeElement.querySelector(iconQuery);
expect(mainAction).toBeFalsy();
});
it('should call extension action', () => {
const runExtensionActionSpy = spyOn(appExtensionService, 'runActionById');
const mainAction = fixture.debugElement.nativeElement.querySelector(iconQuery);
mainAction.click();
expect(runExtensionActionSpy).toHaveBeenCalledWith(ACTION_CLICK);
});
it('should not call icon if main action is disabled', () => {
const disabledMainActionRef = getContentActionRef();
disabledMainActionRef.disabled = true;
spyOn(appExtensionService, 'getMainAction').and.returnValue(of(disabledMainActionRef));
const runAction = spyOn(mainActionComponent, 'runAction');
mainActionComponent.ngOnInit();
fixture.detectChanges();
const button = fixture.debugElement.nativeElement.querySelector(iconQuery);
button.click();
expect(runAction).not.toHaveBeenCalled();
});
});
});

View File

@@ -1,58 +0,0 @@
/*!
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { AppExtensionService } from '@alfresco/aca-shared';
import { ContentActionRef, ContentActionType } from '@alfresco/adf-extensions';
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-main-action',
templateUrl: './main-action.component.html',
styleUrls: ['./main-action.component.scss']
})
export class MainActionComponent implements OnInit, OnDestroy {
@Input() expanded: boolean;
mainAction$: Observable<ContentActionRef>;
actionTypes = ContentActionType;
private onDestroy$ = new Subject<boolean>();
constructor(private extensions: AppExtensionService) {}
ngOnDestroy(): void {
this.onDestroy$.next(true);
}
ngOnInit(): void {
this.mainAction$ = this.extensions.getMainAction().pipe(takeUntil(this.onDestroy$));
}
runAction(action: string): void {
this.extensions.runActionById(action);
}
}

View File

@@ -1,37 +0,0 @@
/*!
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { TranslateModule } from '@ngx-translate/core';
import { MainActionComponent } from './main-action.component';
import { MatIconModule } from '@angular/material/icon';
@NgModule({
imports: [CommonModule, MatButtonModule, MatIconModule, TranslateModule.forChild()],
exports: [MainActionComponent],
declarations: [MainActionComponent]
})
export class MainActionModule {}

View File

@@ -22,6 +22,7 @@ $top-margin: 12px;
font-size: 16px;
padding-left: 15px;
box-sizing: border-box;
margin-bottom: 12px !important;
.mat-form-field {
font-size: 16px;

View File

@@ -1,9 +1,9 @@
<div
class="app-search-container searchMenuTrigger"
<div class="app-search-container searchMenuTrigger"
[matMenuTriggerFor]="searchOptionsMenu"
(menuOpened)="onMenuOpened()"
(menuClosed)="syncInputValues()"
>
<button mat-icon-button class="app-search-button" (click)="searchByOption()" [title]="'SEARCH.BUTTON.TOOLTIP' | translate">
<mat-icon [attr.aria-label]="'SEARCH.BUTTON.ARIA-LABEL' | translate">search</mat-icon>
</button>
@@ -20,6 +20,10 @@
<div matSuffix class="app-suffix-search-icon-wrapper">
<mat-icon>arrow_drop_down</mat-icon>
</div>
<button mat-icon-button matSuffix class="app-suffix-search-icon-wrapper app-close-icon" (click)="exitSearch()">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
</div>

View File

@@ -1,5 +1,5 @@
$search-width: 594px;
$search-height: 40px;
$search-height: 32px;
$search-background: #f5f6f5;
$search-border-radius: 4px;
$top-margin: 12px;
@@ -9,8 +9,26 @@ $top-margin: 12px;
width: 100%;
max-width: $search-width;
height: $search-height + $top-margin;
margin: 0 !important;
.app-search-button {
width: 32px;
height: 32px;
margin-left: 0;
padding-left: 0;
margin-top: -4px;
}
.app-input-form-field {
.app-close-icon {
height: 6px;
.mat-icon {
font-size: 18px !important;
line-height: 28px;
}
}
.mat-input-element {
caret-color: var(--theme-text-color);
@@ -52,6 +70,8 @@ mat-checkbox {
background-color: $search-background;
border-radius: $search-border-radius;
height: $search-height;
margin-bottom: 0 !important;
padding-bottom: 26px !important;
}
.app-search-control {

View File

@@ -31,12 +31,14 @@ import { SearchByTermAction, SearchActionTypes, SnackbarErrorAction, SnackbarAct
import { AppHookService } from '@alfresco/aca-shared';
import { map } from 'rxjs/operators';
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { SearchNavigationService } from '../search-navigation.service';
describe('SearchInputComponent', () => {
let fixture: ComponentFixture<SearchInputComponent>;
let component: SearchInputComponent;
let actions$: Actions;
let appHookService: AppHookService;
let searchInputService: SearchNavigationService;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -49,12 +51,20 @@ describe('SearchInputComponent', () => {
actions$ = TestBed.inject(Actions);
fixture = TestBed.createComponent(SearchInputComponent);
appHookService = TestBed.inject(AppHookService);
searchInputService = TestBed.inject(SearchNavigationService);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should change flag on library400Error event', () => {
afterEach(() => {
fixture.destroy();
});
it('should change flag on library400Error event', async () => {
fixture.detectChanges();
await fixture.whenStable();
expect(component.has400LibraryError).toBe(false);
appHookService.library400Error.next();
expect(component.has400LibraryError).toBe(true);
@@ -64,9 +74,13 @@ describe('SearchInputComponent', () => {
expect(component.hasLibraryConstraint()).toBe(false);
});
it('should have library constraint on 400 error received', () => {
it('should have library constraint on 400 error received', async () => {
fixture.detectChanges();
await fixture.whenStable();
const libItem = component.searchOptions.find((item) => item.key.toLowerCase().indexOf('libraries') > 0);
libItem.value = true;
appHookService.library400Error.next();
expect(component.hasLibraryConstraint()).toBe(true);
@@ -192,4 +206,23 @@ describe('SearchInputComponent', () => {
expect(component.isContentChecked()).toBe(true);
});
});
describe('exitSearch()', () => {
it('should exit search on click of close icon', async () => {
spyOn(component, 'exitSearch').and.callThrough();
spyOn(searchInputService, 'navigateBack').and.callThrough();
fixture.detectChanges();
await fixture.whenStable();
const closeIcon = fixture.debugElement.nativeElement.querySelector('.app-close-icon');
closeIcon.click();
fixture.detectChanges();
await fixture.whenStable();
expect(component.exitSearch).toHaveBeenCalled();
expect(searchInputService.navigateBack).toHaveBeenCalledWith();
});
});
});

View File

@@ -33,6 +33,7 @@ import { Store } from '@ngrx/store';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { SearchInputControlComponent } from '../search-input-control/search-input-control.component';
import { SearchNavigationService } from '../search-navigation.service';
import { SearchLibrariesQueryBuilderService } from '../search-libraries-results/search-libraries-query-builder.service';
@Component({
@@ -84,7 +85,8 @@ export class SearchInputComponent implements OnInit, OnDestroy {
private config: AppConfigService,
private router: Router,
private store: Store<AppStore>,
private appHookService: AppHookService
private appHookService: AppHookService,
public searchInputService: SearchNavigationService
) {
this.searchOnChange = this.config.get<boolean>('search.aca:triggeredOnChange', true);
}
@@ -106,6 +108,10 @@ export class SearchInputComponent implements OnInit, OnDestroy {
});
}
exitSearch() {
this.searchInputService.navigateBack();
}
showInputValue() {
this.has400LibraryError = false;
this.searchedWord = this.getUrlSearchTerm();
@@ -122,7 +128,9 @@ export class SearchInputComponent implements OnInit, OnDestroy {
}
onMenuOpened() {
this.searchInputControl.searchInput.nativeElement.focus();
if (this.searchInputControl) {
this.searchInputControl.searchInput?.nativeElement?.focus();
}
}
/**
@@ -139,7 +147,10 @@ export class SearchInputComponent implements OnInit, OnDestroy {
} else {
this.store.dispatch(new SnackbarErrorAction('APP.BROWSE.SEARCH.EMPTY_SEARCH'));
}
this.trigger.closeMenu();
if (this.trigger) {
this.trigger.closeMenu();
}
}
onSearchChange(searchTerm: string) {

View File

@@ -0,0 +1,65 @@
/*!
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { TestBed } from '@angular/core/testing';
import { CoreModule } from '@alfresco/adf-core';
import { TranslateModule } from '@ngx-translate/core';
import { SearchNavigationService } from './search-navigation.service';
import { Router } from '@angular/router';
describe('SearchNavigationService', () => {
let service: SearchNavigationService;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), CoreModule.forRoot()]
});
service = TestBed.inject(SearchNavigationService);
router = TestBed.inject(Router);
});
it('should not navigate to saved route when exitSearch function is called if saved route is null', () => {
const routerNavigate = spyOn(router, 'navigate');
service.saveRoute('');
service.navigateBack();
expect(routerNavigate).not.toHaveBeenCalledWith([service.previousRoute]);
});
it('should navigate to saved route when exitSearch function is called', () => {
const routerNavigate = spyOn(router, 'navigate');
service.saveRoute('/personal-files');
service.navigateBack();
expect(routerNavigate).toHaveBeenCalledWith([service.previousRoute]);
});
it('should navigate to Search when navigateToSearch function is called', () => {
const routerNavigate = spyOn(router, 'navigate');
service.navigateToSearch();
expect(routerNavigate).toHaveBeenCalledWith(['/search']);
});
});

View File

@@ -22,16 +22,33 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from '@alfresco/adf-core';
import { AppHeaderComponent } from './header.component';
import { AppSearchInputModule } from '../search/search-input.module';
import { AppToolbarModule } from '../toolbar/toolbar.module';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
@NgModule({
imports: [CommonModule, CoreModule.forChild(), AppSearchInputModule, AppToolbarModule],
declarations: [AppHeaderComponent],
exports: [AppHeaderComponent]
@Injectable({
providedIn: 'root'
})
export class AppHeaderModule {}
export class SearchNavigationService {
private _previousRoute = '';
get previousRoute(): string {
return this._previousRoute;
}
constructor(private router: Router) {}
saveRoute(route: string): void {
this._previousRoute = route;
}
navigateBack(): void {
if (this.previousRoute) {
this.router.navigate([this.previousRoute]);
}
}
navigateToSearch(): void {
this.saveRoute(this.router.url);
this.router.navigate(['/search']);
}
}

View File

@@ -26,7 +26,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from '@alfresco/adf-core';
import { ContentModule } from '@alfresco/adf-content-services';
import { LockedByModule } from '@alfresco/aca-shared';
import { LockedByModule, PageLayoutModule } from '@alfresco/aca-shared';
import { SearchResultsComponent } from './search-results/search-results.component';
import { SearchResultsRowComponent } from './search-results-row/search-results-row.component';
import { SearchLibrariesResultsComponent } from './search-libraries-results/search-libraries-results.component';
@@ -34,10 +34,10 @@ import { AppInfoDrawerModule } from '../info-drawer/info.drawer.module';
import { AppToolbarModule } from '../toolbar/toolbar.module';
import { AppCommonModule } from '../common/common.module';
import { DirectivesModule } from '../../directives/directives.module';
import { AppLayoutModule } from '../layout/layout.module';
import { ContextMenuModule } from '../context-menu/context-menu.module';
import { SearchActionMenuComponent } from './search-action-menu/search-action-menu.component';
import { DocumentListCustomComponentsModule } from '../dl-custom-components/document-list-custom-components.module';
import { AppSearchInputModule } from './search-input.module';
@NgModule({
imports: [
@@ -48,10 +48,11 @@ import { DocumentListCustomComponentsModule } from '../dl-custom-components/docu
AppInfoDrawerModule,
AppToolbarModule,
DirectivesModule,
AppLayoutModule,
PageLayoutModule,
ContextMenuModule,
LockedByModule,
DocumentListCustomComponentsModule
DocumentListCustomComponentsModule,
AppSearchInputModule
],
declarations: [SearchResultsComponent, SearchLibrariesResultsComponent, SearchResultsRowComponent, SearchActionMenuComponent],
exports: [SearchResultsComponent, SearchLibrariesResultsComponent, SearchResultsRowComponent, SearchActionMenuComponent]

View File

@@ -1,6 +1,7 @@
<aca-page-layout>
<aca-page-layout-header>
<adf-breadcrumb root="APP.BROWSE.SEARCH.TITLE"> </adf-breadcrumb>
<aca-search-input></aca-search-input>
<div class="adf-toolbar--spacer adf-toolbar-divider"></div>
<adf-toolbar class="adf-toolbar--inline">
<ng-container *ngFor="let entry of actions; trackBy: trackByActionId">
<aca-toolbar-action [actionRef]="entry"></aca-toolbar-action>

View File

@@ -24,7 +24,6 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { SearchResultsComponent } from './search-results.component';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { AppSearchResultsModule } from '../search-results.module';
import { AppConfigService, CoreModule, TranslationService } from '@alfresco/adf-core';
import { Store } from '@ngrx/store';
@@ -33,7 +32,9 @@ import { Pagination, SearchRequest } from '@alfresco/js-api';
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { BehaviorSubject } from 'rxjs';
import { BehaviorSubject, Subject } from 'rxjs';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { AppService } from '@alfresco/aca-shared';
describe('SearchComponent', () => {
let component: SearchResultsComponent;
@@ -49,8 +50,15 @@ describe('SearchComponent', () => {
beforeEach(() => {
params = new BehaviorSubject({ q: 'TYPE: "cm:folder" AND %28=cm: name: email OR cm: name: budget%29' });
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), CoreModule.forRoot(), AppTestingModule, AppSearchResultsModule],
imports: [TranslateModule.forRoot(), AppTestingModule, CoreModule.forRoot(), AppSearchResultsModule],
providers: [
{
provide: AppService,
useValue: {
appNavNarMode$: new BehaviorSubject('expanded'),
toggleAppNavBar$: new Subject()
}
},
{
provide: ActivatedRoute,
useValue: {

View File

@@ -27,9 +27,6 @@
<mat-panel-title>
<div class="item">
<button
[ngClass]="{
'action-button--active': acaExpansionPanel.hasActiveLinks()
}"
[attr.aria-label]="item.title | translate"
[id]="item.id"
[attr.title]="item.description | translate"

View File

@@ -0,0 +1,20 @@
<div class="sidenav-header">
<div class="sidenav-header-title">
<div class="sidenav-header-title-logo"
(click)="toggleNavBar.emit()"
(keypress)="toggleNavBar.emit()">
<img
src="{{ logo$ | async }}"
title="{{'APP.TOOLTIPS.COLLAPSE_NAVIGATION' | translate}}"
alt="{{ 'CORE.HEADER.LOGO_ARIA' | translate }}" />
</div>
<div class="sidenav-header-title-text" [routerLink]="landingPage">
{{ appName$ | async | translate }}
</div>
<ng-container *ngFor="let actionRef of actions; trackBy: trackByActionId">
<aca-toolbar-action [actionRef]="actionRef"></aca-toolbar-action>
</ng-container>
</div>
</div>

View File

@@ -22,55 +22,36 @@
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component, ViewEncapsulation, Output, EventEmitter, OnInit, Input, OnDestroy } from '@angular/core';
import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable, Subject } from 'rxjs';
import { AppStore, getAppName, getLogoPath } from '@alfresco/aca-shared/store';
import { AppConfigService } from '@alfresco/adf-core';
import { ContentActionRef } from '@alfresco/adf-extensions';
import { AppStore, getHeaderColor, getAppName, getLogoPath, getHeaderImagePath, getHeaderTextColor } from '@alfresco/aca-shared/store';
import { AppExtensionService } from '@alfresco/aca-shared';
import { takeUntil } from 'rxjs/operators';
import { AppConfigService, SidenavLayoutComponent } from '@alfresco/adf-core';
import { isContentServiceEnabled } from '@alfresco/aca-shared/rules';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
selector: 'app-sidenav-header',
templateUrl: `./sidenav-header.component.html`,
encapsulation: ViewEncapsulation.None,
host: { class: 'app-header' }
host: { class: 'app-sidenav-header' }
})
export class AppHeaderComponent implements OnInit, OnDestroy {
private onDestroy$: Subject<boolean> = new Subject<boolean>();
@Output()
toggleClicked = new EventEmitter();
@Input() expandedSidenav = true;
@Input() data: { layout?: SidenavLayoutComponent; isMenuMinimized?: boolean } = {};
get isSidenavExpanded(): boolean {
return !this.data.isMenuMinimized ?? this.expandedSidenav;
}
export class SidenavHeaderComponent implements OnInit, OnDestroy {
private onDestroy$ = new Subject<boolean>();
appName$: Observable<string>;
headerColor$: Observable<any>;
headerTextColor$: Observable<string>;
logo$: Observable<string>;
landingPage: string;
actions: Array<ContentActionRef> = [];
constructor(public store: Store<AppStore>, private appExtensions: AppExtensionService, private appConfigService: AppConfigService) {
this.headerColor$ = store.select(getHeaderColor);
this.headerTextColor$ = store.select(getHeaderTextColor);
@Output()
toggleNavBar = new EventEmitter();
constructor(public store: Store<AppStore>, private appConfigService: AppConfigService, private appExtensions: AppExtensionService) {
this.appName$ = store.select(getAppName);
this.logo$ = store.select(getLogoPath);
this.landingPage = this.appConfigService.get('landingPage', '/personal-files');
store.select(getHeaderImagePath).subscribe((path) => {
document.body.style.setProperty('--header-background-image', `url('${path}')`);
});
}
ngOnInit() {
@@ -80,18 +61,6 @@ export class AppHeaderComponent implements OnInit, OnDestroy {
.subscribe((actions) => {
this.actions = actions;
});
this.headerTextColor$.subscribe((color) => {
document.documentElement.style.setProperty('--adf-header-text-color', color);
});
}
onToggleSidenav(_event: boolean): void {
this.data.layout.toggleMenu();
}
isContentServiceEnabled(): boolean {
return isContentServiceEnabled();
}
ngOnDestroy() {

View File

@@ -1,3 +0,0 @@
<app-sidenav
[mode]="data.mode"
></app-sidenav>

View File

@@ -1,36 +1,16 @@
<div class="sidenav">
<ng-container [ngSwitch]="mode">
<div class="section action-menu" [ngClass]="'section--' + mode">
<app-main-action [expanded]="mode === 'expanded'"></app-main-action>
<app-create-menu [expanded]="mode === 'expanded'"></app-create-menu>
</div>
<app-sidenav-header (toggleNavBar)="toggleClick()"></app-sidenav-header>
<div class="section-sub-actions">
<div *ngFor="let group of groups; trackBy: trackByGroupId" class="section" [ngClass]="'section--' + mode">
<ng-container *ngSwitchCase="'expanded'">
<div *ngFor="let group of groups; trackBy: trackByGroupId" class="section">
<mat-list-item *ngFor="let item of group.items; trackBy: trackByLinkId">
<ng-container *ngIf="!item.component">
<app-expand-menu [item]="item"></app-expand-menu>
</ng-container>
<ng-container *ngIf="item.component">
<adf-dynamic-component [data]="{ item: item, state: 'expanded' }" [id]="item.component"></adf-dynamic-component>
</ng-container>
</mat-list-item>
</ng-container>
<ng-container *ngSwitchCase="'collapsed'">
<div class="list-item" *ngFor="let item of group.items; trackBy: trackByLinkId">
<ng-container *ngIf="!item.component">
<app-button-menu [item]="item"></app-button-menu>
</ng-container>
<ng-container *ngIf="item.component">
<adf-dynamic-component [data]="{ item: item, state: 'collapsed' }" [id]="item.component"> </adf-dynamic-component>
</ng-container>
</div>
</ng-container>
</div>
</div>
</ng-container>
</div>

View File

@@ -1,151 +1,129 @@
.app-sidenav {
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
overflow-y: hidden;
}
.sidenav {
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
background-color: var(--theme-background-color);
overflow-y: hidden;
background: var(--theme-sidenav-background-color);
.action-menu {
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
}
&-header {
padding: 32px 0;
.section.action-menu {
padding: 8px 14px;
position: sticky;
}
&-title {
display: flex;
flex-direction: row;
align-items: center;
height: 32px;
padding: 0 24px;
.section-sub-actions {
overflow-y: auto;
}
&-logo {
img {
cursor: pointer;
height: 28px;
vertical-align: middle;
}
}
.section {
padding: 8px 6px;
border-bottom: 1px solid var(--theme-divider-color);
&-text {
flex: 1;
color: var(--theme-selected-text-color);
padding-left: 32px;
letter-spacing: 0.25px;
font-style: normal;
font-weight: 400;
font-size: var(--theme-body-1-font-size);
cursor: pointer;
}
}
}
.section:last-child {
border-bottom: 0;
}
.section--collapsed {
display: flex;
flex-direction: column;
align-items: center;
}
.section-sub-actions {
overflow-y: auto;
.list-item {
padding: 12px 0;
display: flex;
align-items: center;
height: 24px;
}
.mat-expansion-panel {
width: 100%;
background-color: unset;
box-shadow: none;
border-radius: 0;
.menu {
display: flex;
flex: 1;
flex-direction: row;
}
&-header {
height: 32px;
padding: 0 32px 0 0;
display: flex;
align-items: center;
border: none;
}
&-header:hover {
background: var(--theme-hover-background-color);
}
.full-width {
display: flex;
width: 100%;
}
&-header-title {
display: flex;
flex-direction: row;
align-items: center;
}
.action-button--active {
color: var(--theme-primary-color) !important;
}
&-body {
padding: 0 0 16px;
font-size: var(--theme-body-1-font-size);
.action-button {
color: var(--theme-text-color);
}
.mat-button {
line-height: 32px;
}
}
.action-button .action-button__label {
margin: 0 8px !important;
}
.mat-expansion-indicator {
display: flex;
align-content: center;
}
.item {
padding: 12px 0;
flex-direction: row;
display: flex;
align-items: center;
text-decoration: none;
height: 24px;
width: 100%;
user-select: none;
}
.mat-expansion-indicator::after {
transform: rotate(226deg);
}
}
.app-item,
.app-item .item {
display: flex;
flex: 1;
flex-direction: row;
}
.item {
flex-direction: row;
display: flex;
align-items: center;
text-decoration: none;
width: 100%;
user-select: none;
.item:hover .action-button__label {
color: var(--theme-primary-color);
}
&:hover .action-button__label {
color: var(--theme-selected-text-color);
}
}
.mat-expansion-panel-header {
padding: 0 8px 0 0 !important;
display: flex;
align-items: center;
font-size: 14px !important;
}
.action-button {
color: var(--theme-action-button-text-color);
height: 32px;
padding: 0 24px;
border-radius: 0;
}
.mat-expansion-panel {
width: 100%;
background-color: unset;
box-shadow: none !important;
}
.full-width {
display: flex;
width: 100%;
}
.mat-expansion-panel:not(.mat-expanded) .mat-expansion-panel-header:not([aria-disabled='true']):hover {
background: none !important;
}
.action-button--active {
color: var(--theme-selected-text-color) !important;
background: var(--theme-selected-background-color);
}
.mat-expansion-indicator {
display: flex;
align-content: center;
}
.mat-expansion-panel-body {
padding-bottom: 0;
}
.mat-expansion-panel-header-title {
display: flex;
flex-direction: row;
align-items: center;
}
}
.aca-menu-panel {
.action-button--active {
color: var(--theme-accent-color) !important;
}
.action-button {
color: var(--theme-primary-color);
}
.action-button:hover {
color: var(--theme-accent-color);
}
}
[dir='rtl'] .sidenav {
/* stylelint-disable-next-line no-descending-specificity */
.mat-expansion-panel-header {
padding: 0 0 0 8px !important;
.action-panel-header {
color: var(--theme-action-button-text-color);
padding: 0 24px;
&__label {
font-size: var(--theme-caption-font-size);
}
}
}
}

View File

@@ -26,7 +26,8 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { SidenavComponent } from './sidenav.component';
import { AppTestingModule } from '../../testing/app-testing.module';
import { AppExtensionService } from '@alfresco/aca-shared';
import { AppExtensionService, AppService } from '@alfresco/aca-shared';
import { BehaviorSubject, Subject } from 'rxjs';
describe('SidenavComponent', () => {
let fixture: ComponentFixture<SidenavComponent>;
@@ -37,6 +38,15 @@ describe('SidenavComponent', () => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [SidenavComponent],
providers: [
{
provide: AppService,
useValue: {
appNavNarMode$: new BehaviorSubject('expanded'),
toggleAppNavBar$: new Subject()
}
}
],
schemas: [NO_ERRORS_SCHEMA]
});

View File

@@ -28,7 +28,8 @@ import { Store } from '@ngrx/store';
import { AppStore, getSideNavState } from '@alfresco/aca-shared/store';
import { Subject } from 'rxjs';
import { takeUntil, distinctUntilChanged, debounceTime } from 'rxjs/operators';
import { AppExtensionService } from '@alfresco/aca-shared';
import { AppExtensionService, AppService } from '@alfresco/aca-shared';
import { SidenavLayoutComponent } from '@alfresco/adf-core';
@Component({
selector: 'app-sidenav',
@@ -39,12 +40,15 @@ import { AppExtensionService } from '@alfresco/aca-shared';
})
export class SidenavComponent implements OnInit, OnDestroy {
@Input()
mode: 'collapsed' | 'expanded' = 'expanded';
data: {
layout?: SidenavLayoutComponent;
mode?: 'collapsed' | 'expanded';
} = {};
groups: Array<NavBarGroupRef> = [];
private onDestroy$ = new Subject<boolean>();
constructor(private store: Store<AppStore>, private extensions: AppExtensionService) {}
constructor(private store: Store<AppStore>, private extensions: AppExtensionService, private appService: AppService) {}
ngOnInit() {
this.store
@@ -53,6 +57,9 @@ export class SidenavComponent implements OnInit, OnDestroy {
.subscribe(() => {
this.groups = this.extensions.getApplicationNavigation(this.extensions.navbar);
});
this.appService.appNavNarMode$.next(this.data.mode);
this.appService.toggleAppNavBar$.pipe(takeUntil(this.onDestroy$)).subscribe(() => this.toggleNavBar());
}
trackByGroupId(_: number, obj: NavBarGroupRef): string {
@@ -63,6 +70,15 @@ export class SidenavComponent implements OnInit, OnDestroy {
return obj.id;
}
toggleClick() {
this.toggleNavBar();
}
private toggleNavBar() {
this.data.layout.toggleMenu();
this.appService.appNavNarMode$.next(this.data.layout.isMenuMinimized ? 'collapsed' : 'expanded');
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();

View File

@@ -24,11 +24,9 @@
import { NgModule } from '@angular/core';
import { AppCreateMenuModule } from '../create-menu/create-menu.module';
import { CommonModule } from '@angular/common';
import { CoreModule } from '@alfresco/adf-core';
import { RouterModule } from '@angular/router';
import { ExtensionsModule } from '@alfresco/adf-extensions';
import { CoreExtensionsModule } from '../../extensions/core.extensions.module';
import { ExpansionPanelDirective } from './directives/expansion-panel.directive';
import { MenuPanelDirective } from './directives/menu-panel.directive';
import { SidenavComponent } from './sidenav.component';
@@ -36,19 +34,11 @@ import { ActiveLinkDirective } from './directives/active-link.directive';
import { ExpandMenuComponent } from './components/expand-menu.component';
import { ButtonMenuComponent } from './components/button-menu.component';
import { ActionDirective } from './directives/action.directive';
import { MainActionModule } from '../main-action/main-action.module';
import { SidenavWrapperComponent } from './sidenav-wrapper/sidenav-wrapper.component';
import { SidenavHeaderComponent } from './components/sidenav-header.component';
import { SharedToolbarModule } from '@alfresco/aca-shared';
@NgModule({
imports: [
CommonModule,
CoreModule.forChild(),
CoreExtensionsModule.forChild(),
ExtensionsModule.forChild(),
RouterModule,
AppCreateMenuModule,
MainActionModule
],
imports: [CoreModule.forChild(), ExtensionsModule.forChild(), RouterModule, AppCreateMenuModule, SharedToolbarModule],
declarations: [
MenuPanelDirective,
ExpansionPanelDirective,
@@ -57,7 +47,7 @@ import { SidenavWrapperComponent } from './sidenav-wrapper/sidenav-wrapper.compo
ExpandMenuComponent,
ButtonMenuComponent,
SidenavComponent,
SidenavWrapperComponent
SidenavHeaderComponent
],
exports: [
MenuPanelDirective,

View File

@@ -31,7 +31,8 @@ import { AppCommonModule } from '../common/common.module';
import { AppToolbarModule } from '../toolbar/toolbar.module';
import { DirectivesModule } from '../../directives/directives.module';
import { ContextMenuModule } from '../context-menu/context-menu.module';
import { AppLayoutModule } from '../layout/layout.module';
import { AppSearchInputModule } from '../search/search-input.module';
import { PageLayoutModule } from '@alfresco/aca-shared';
@NgModule({
imports: [
@@ -42,7 +43,8 @@ import { AppLayoutModule } from '../layout/layout.module';
AppCommonModule,
AppToolbarModule,
ContextMenuModule,
AppLayoutModule
PageLayoutModule,
AppSearchInputModule
],
declarations: [TrashcanComponent],
exports: [TrashcanComponent]

View File

@@ -139,4 +139,4 @@
</div>
</div>
</div>
</div>
</div>

View File

@@ -185,4 +185,4 @@ app-view-profile {
padding-top: 2rem;
padding-left: .5rem;
}
}
}

View File

@@ -1089,7 +1089,7 @@ export class ContentManagementService {
private focusAfterClose(focusedElementSelector: string): void {
if (focusedElementSelector) {
document.querySelector<HTMLElement>(focusedElementSelector).focus();
document.querySelector<HTMLElement>(focusedElementSelector)?.focus();
}
}
}

View File

@@ -700,7 +700,7 @@ export class NodeActionsService {
private focusAfterClose(focusedElementSelector: string): void {
if (focusedElementSelector) {
document.querySelector<HTMLElement>(focusedElementSelector).focus();
document.querySelector<HTMLElement>(focusedElementSelector)?.focus();
}
}
}

View File

@@ -164,7 +164,7 @@ export class DownloadEffects {
private focusAfterClose(focusedElementSelector: string): void {
if (focusedElementSelector) {
document.querySelector<HTMLElement>(focusedElementSelector).focus();
document.querySelector<HTMLElement>(focusedElementSelector)?.focus();
}
}
}

View File

@@ -28,7 +28,7 @@ import { SearchEffects } from './search.effects';
import { EffectsModule } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { Router } from '@angular/router';
import { SearchOptionIds, SearchByTermAction } from '@alfresco/aca-shared/store';
import { SearchOptionIds, SearchByTermAction, SearchAction } from '@alfresco/aca-shared/store';
describe('SearchEffects', () => {
let store: Store<any>;
@@ -77,4 +77,15 @@ describe('SearchEffects', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith('/search;q=%2528test%2529');
}));
});
describe('search$', () => {
it('should navigate to search when the toolbar search icon is clicked', fakeAsync(() => {
const routerNavigate = spyOn(router, 'navigate');
store.dispatch(new SearchAction());
tick();
expect(routerNavigate).toHaveBeenCalledWith(['/search']);
}));
});
});

View File

@@ -25,12 +25,24 @@
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';
import { SearchActionTypes, SearchByTermAction, SearchOptionIds } from '@alfresco/aca-shared/store';
import { SearchAction, SearchActionTypes, SearchByTermAction, SearchOptionIds } from '@alfresco/aca-shared/store';
import { Router } from '@angular/router';
import { SearchNavigationService } from '../../components/search/search-navigation.service';
@Injectable()
export class SearchEffects {
constructor(private actions$: Actions, private router: Router) {}
constructor(private actions$: Actions, private router: Router, private searchNavigationService: SearchNavigationService) {}
search$ = createEffect(
() =>
this.actions$.pipe(
ofType<SearchAction>(SearchActionTypes.Search),
map(() => {
this.searchNavigationService.navigateToSearch();
})
),
{ dispatch: false }
);
searchByTerm$ = createEffect(
() =>

View File

@@ -26,10 +26,7 @@ import { AppState, AppStore } from '@alfresco/aca-shared/store';
export const INITIAL_APP_STATE: AppState = {
appName: 'Alfresco Content Application',
headerColor: '#ffffff',
headerTextColor: '#000000',
logoPath: 'assets/images/alfresco-logo-white.svg',
headerImagePath: 'assets/images/mastHead-bg-shapesPattern.svg',
customCssPath: '',
webFontPath: '',
sharedUrl: '',

View File

@@ -34,7 +34,6 @@ import {
SetRepositoryInfoAction,
SetInfoDrawerStateAction,
SetInfoDrawerMetadataAspectAction,
SetHeaderColorAction,
SetCurrentNodeVersionAction,
SetFileUploadingDialogAction,
SetInfoDrawerPreviewStateAction,
@@ -50,12 +49,6 @@ export function appReducer(state: AppState = INITIAL_APP_STATE, action: Action):
case AppActionTypes.SetInitialState:
newState = Object.assign({}, (action as SetInitialStateAction).payload);
break;
case AppActionTypes.SetHeaderColor:
newState = {
...state,
headerColor: (action as SetHeaderColorAction).color
};
break;
case NodeActionTypes.SetSelection:
newState = updateSelectedNodes(state, action as SetSelectedNodesAction);
break;

View File

@@ -48,6 +48,6 @@ ng-component {
adf-layout-container,
aca-search-results,
ng-component {
height: 80vh;
height: 100vh;
}
}

View File

@@ -31,6 +31,11 @@ $datetimepicker-selected-date-background: #2254b2;
$datetimepicker-cell-background-color: #fff;
$datetimepicker-cell-focus-border-color: #1f74db;
$datetimepicker-cell-focus-background-color: rgba(33, 33, 33, 0.12);
$sidenav-background-color: #f8f8f8;
$selected-text-color: #212121;
$selected-background-color: rgba(31, 116, 219, 0.24);
$action-button-text-color: rgba(33, 35, 40, 0.7);
$page-layout-header-background-color: #fff;
// CSS Variables
$defaults: (
@@ -44,7 +49,6 @@ $defaults: (
--theme-title-color: mat.get-color-from-palette($foreground, text, 0.87),
--theme-text-disabled-color: mat.get-color-from-palette($foreground, text, 0.38),
--theme-border-color: mat.get-color-from-palette($foreground, text, 0.07),
--header-background-image: url('/assets/images/mastHead-bg-shapesPattern.svg'),
--theme-card-background-color: mat.get-color-from-palette($background, card),
--theme-foreground-text-color: mat.get-color-from-palette($foreground, text, 0.72),
--theme-foreground-text-bold-color: mat.get-color-from-palette($foreground, text, 0.87),
@@ -68,7 +72,14 @@ $defaults: (
--theme-datetimepicker-selected-date-background: $datetimepicker-selected-date-background,
--theme-datetimepicker-cell-background: $datetimepicker-cell-background-color,
--theme-datetimepicker-cell-focus-border: $datetimepicker-cell-focus-border-color,
--theme-datetimepicker-cell-focus-background: $datetimepicker-cell-focus-background-color
--theme-datetimepicker-cell-focus-background: $datetimepicker-cell-focus-background-color,
--theme-sidenav-background-color: $sidenav-background-color,
--theme-selected-text-color: $selected-text-color,
--theme-selected-background-color: $selected-background-color,
--theme-hover-background-color: $grey-text-background,
--theme-action-button-text-color: $action-button-text-color,
--theme-header-border-color: $grey-background,
--theme-page-layout-header-background-color: $page-layout-header-background-color,
);
// propagates SCSS variables into the CSS variables scope