[ACA-1443] prettier formatting and checks (#629)

* intergrate prettier

* update settings

* integrate with travis

* unified formatting across all files
This commit is contained in:
Denys Vuika
2018-09-13 16:47:55 +01:00
committed by GitHub
parent 06402a9c72
commit 883a1971c5
163 changed files with 13571 additions and 12512 deletions

View File

@@ -4,7 +4,7 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
indent_style = space indent_style = space
indent_size = 4 indent_size = 2
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true

2
.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules
src/assets/i18n

3
.prettierrc Normal file
View File

@@ -0,0 +1,3 @@
{
"singleQuote": true
}

View File

@@ -9,7 +9,7 @@ addons:
language: node_js language: node_js
node_js: node_js:
- "8" - '8'
before_script: before_script:
# Disable services enabled by default # Disable services enabled by default
@@ -24,7 +24,10 @@ before_install:
jobs: jobs:
include: include:
- stage: test - stage: test
script: npm run lint && npm run spellcheck script:
- npm run lint
- npm run spellcheck
- npm run format:check
- stage: test - stage: test
script: script:
- npm run test:ci - npm run test:ci

View File

@@ -1,4 +1,5 @@
{ {
"javascript.preferences.quoteStyle": "single", "javascript.preferences.quoteStyle": "single",
"typescript.preferences.quoteStyle": "single" "typescript.preferences.quoteStyle": "single",
"editor.formatOnSave": true
} }

6
package-lock.json generated
View File

@@ -11444,6 +11444,12 @@
"integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
"dev": true "dev": true
}, },
"prettier": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.14.2.tgz",
"integrity": "sha512-McHPg0n1pIke+A/4VcaS2en+pTNjy4xF+Uuq86u/5dyDO59/TtFZtQ708QIRkEZ3qwKz3GVkVa6mpxK/CpB8Rg==",
"dev": true
},
"pretty-error": { "pretty-error": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz",

View File

@@ -22,7 +22,8 @@
"stop:docker": "docker-compose stop", "stop:docker": "docker-compose stop",
"e2e:docker": "npm run start:docker && npm run e2e && npm run stop:docker", "e2e:docker": "npm run start:docker && npm run e2e && npm run stop:docker",
"spellcheck": "cspell 'src/**/*.ts' 'e2e/**/*.ts' 'projects/**/*.ts'", "spellcheck": "cspell 'src/**/*.ts' 'e2e/**/*.ts' 'projects/**/*.ts'",
"inspect.bundle": "ng build app --prod --stats-json && npx webpack-bundle-analyzer dist/app/stats.json" "inspect.bundle": "ng build app --prod --stats-json && npx webpack-bundle-analyzer dist/app/stats.json",
"format:check": "prettier --list-different \"src/{app,environments}/**/*{.ts,.js,.json,.css,.scss}\""
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
@@ -89,6 +90,7 @@
"karma-jasmine": "~1.1.0", "karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2", "karma-jasmine-html-reporter": "^0.2.2",
"ng-packagr": "^4.1.1", "ng-packagr": "^4.1.1",
"prettier": "^1.14.2",
"protractor": "^5.4.0", "protractor": "^5.4.0",
"rimraf": "2.6.2", "rimraf": "2.6.2",
"selenium-webdriver": "4.0.0-alpha.1", "selenium-webdriver": "4.0.0-alpha.1",

View File

@@ -24,112 +24,119 @@
*/ */
import { import {
AlfrescoApiService, AlfrescoApiService,
AppConfigService, AppConfigService,
AuthenticationService, AuthenticationService,
FileUploadErrorEvent, FileUploadErrorEvent,
PageTitleService, PageTitleService,
UploadService UploadService
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppExtensionService } from './extensions/extension.service'; import { AppExtensionService } from './extensions/extension.service';
import { import {
SetLanguagePickerAction, SetLanguagePickerAction,
SnackbarErrorAction, SnackbarErrorAction,
SetCurrentUrlAction, SetCurrentUrlAction,
SetInitialStateAction SetInitialStateAction
} from './store/actions'; } from './store/actions';
import { AppStore, AppState, INITIAL_APP_STATE } from './store/states/app.state'; import {
AppStore,
AppState,
INITIAL_APP_STATE
} from './store/states/app.state';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import { MatDialog } from '@angular/material'; import { MatDialog } from '@angular/material';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss']
}) })
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private pageTitle: PageTitleService, private pageTitle: PageTitleService,
private store: Store<AppStore>, private store: Store<AppStore>,
private config: AppConfigService, private config: AppConfigService,
private alfrescoApiService: AlfrescoApiService, private alfrescoApiService: AlfrescoApiService,
private authenticationService: AuthenticationService, private authenticationService: AuthenticationService,
private uploadService: UploadService, private uploadService: UploadService,
private extensions: AppExtensionService, private extensions: AppExtensionService,
private dialogRef: MatDialog private dialogRef: MatDialog
) {} ) {}
ngOnInit() { ngOnInit() {
this.alfrescoApiService.getInstance().on('error', error => { this.alfrescoApiService.getInstance().on('error', error => {
if (error.status === 401) { if (error.status === 401) {
if (!this.authenticationService.isLoggedIn()) { if (!this.authenticationService.isLoggedIn()) {
this.authenticationService.setRedirect({ provider: 'ECM', url: this.router.url }); this.authenticationService.setRedirect({
this.router.navigate(['/login']); provider: 'ECM',
url: this.router.url
});
this.router.navigate(['/login']);
this.dialogRef.closeAll(); this.dialogRef.closeAll();
} }
} }
}); });
this.loadAppSettings(); this.loadAppSettings();
const { router, pageTitle, route } = this; const { router, pageTitle, route } = this;
router.events router.events
.pipe(filter(event => event instanceof NavigationEnd)) .pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => { .subscribe(() => {
let currentRoute = route.root; let currentRoute = route.root;
while (currentRoute.firstChild) { while (currentRoute.firstChild) {
currentRoute = currentRoute.firstChild; currentRoute = currentRoute.firstChild;
}
const snapshot: any = currentRoute.snapshot || {};
const data: any = snapshot.data || {};
pageTitle.setTitle(data.title || '');
this.store.dispatch(new SetCurrentUrlAction(router.url));
});
this.router.config.unshift(...this.extensions.getApplicationRoutes());
this.uploadService.fileUploadError.subscribe(error =>
this.onFileUploadedError(error)
);
}
private loadAppSettings() {
const languagePicker = this.config.get<boolean>('languagePicker');
this.store.dispatch(new SetLanguagePickerAction(languagePicker));
const state: AppState = {
... INITIAL_APP_STATE,
appName: this.config.get<string>('application.name'),
headerColor: this.config.get<string>('headerColor'),
logoPath: this.config.get<string>('application.logo'),
sharedUrl: this.config.get<string>('ecmHost') + '/#/preview/s/'
};
this.store.dispatch(new SetInitialStateAction(state));
}
onFileUploadedError(error: FileUploadErrorEvent) {
let message = 'APP.MESSAGES.UPLOAD.ERROR.GENERIC';
if (error.error.status === 409) {
message = 'APP.MESSAGES.UPLOAD.ERROR.CONFLICT';
} }
if (error.error.status === 500) { const snapshot: any = currentRoute.snapshot || {};
message = 'APP.MESSAGES.UPLOAD.ERROR.500'; const data: any = snapshot.data || {};
}
this.store.dispatch(new SnackbarErrorAction(message)); pageTitle.setTitle(data.title || '');
this.store.dispatch(new SetCurrentUrlAction(router.url));
});
this.router.config.unshift(...this.extensions.getApplicationRoutes());
this.uploadService.fileUploadError.subscribe(error =>
this.onFileUploadedError(error)
);
}
private loadAppSettings() {
const languagePicker = this.config.get<boolean>('languagePicker');
this.store.dispatch(new SetLanguagePickerAction(languagePicker));
const state: AppState = {
...INITIAL_APP_STATE,
appName: this.config.get<string>('application.name'),
headerColor: this.config.get<string>('headerColor'),
logoPath: this.config.get<string>('application.logo'),
sharedUrl: this.config.get<string>('ecmHost') + '/#/preview/s/'
};
this.store.dispatch(new SetInitialStateAction(state));
}
onFileUploadedError(error: FileUploadErrorEvent) {
let message = 'APP.MESSAGES.UPLOAD.ERROR.GENERIC';
if (error.error.status === 409) {
message = 'APP.MESSAGES.UPLOAD.ERROR.CONFLICT';
} }
if (error.error.status === 500) {
message = 'APP.MESSAGES.UPLOAD.ERROR.500';
}
this.store.dispatch(new SnackbarErrorAction(message));
}
} }

View File

@@ -28,7 +28,12 @@ import { NgModule } from '@angular/core';
import { RouterModule, RouteReuseStrategy } from '@angular/router'; import { RouterModule, RouteReuseStrategy } from '@angular/router';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TRANSLATION_PROVIDER, CoreModule, AppConfigService, DebugAppConfigService } from '@alfresco/adf-core'; import {
TRANSLATION_PROVIDER,
CoreModule,
AppConfigService,
DebugAppConfigService
} from '@alfresco/adf-core';
import { ContentModule } from '@alfresco/adf-content-services'; import { ContentModule } from '@alfresco/adf-content-services';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
@@ -75,76 +80,76 @@ import { ExtensionsModule } from '@alfresco/adf-extensions';
import { AppToolbarModule } from './components/toolbar/toolbar.module'; import { AppToolbarModule } from './components/toolbar/toolbar.module';
@NgModule({ @NgModule({
imports: [ imports: [
BrowserModule, BrowserModule,
BrowserAnimationsModule, BrowserAnimationsModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterModule.forRoot(APP_ROUTES, { RouterModule.forRoot(APP_ROUTES, {
useHash: true, useHash: true,
enableTracing: false // enable for debug only enableTracing: false // enable for debug only
}), }),
MaterialModule, MaterialModule,
CoreModule.forRoot(), CoreModule.forRoot(),
ContentModule.forRoot(), ContentModule.forRoot(),
AppStoreModule, AppStoreModule,
CoreExtensionsModule.forRoot(), CoreExtensionsModule.forRoot(),
ExtensionsModule.forRoot(), ExtensionsModule.forRoot(),
AppExtensionsModule, AppExtensionsModule,
DirectivesModule, DirectivesModule,
ContextMenuModule.forRoot(), ContextMenuModule.forRoot(),
AppInfoDrawerModule, AppInfoDrawerModule,
AppToolbarModule AppToolbarModule
], ],
declarations: [ declarations: [
AppComponent, AppComponent,
GenericErrorComponent, GenericErrorComponent,
LoginComponent, LoginComponent,
LayoutComponent, LayoutComponent,
SidenavViewsManagerDirective, SidenavViewsManagerDirective,
CurrentUserComponent, CurrentUserComponent,
SearchInputComponent, SearchInputComponent,
SearchInputControlComponent, SearchInputControlComponent,
SidenavComponent, SidenavComponent,
FilesComponent, FilesComponent,
FavoritesComponent, FavoritesComponent,
LibrariesComponent, LibrariesComponent,
RecentFilesComponent, RecentFilesComponent,
SharedFilesComponent, SharedFilesComponent,
TrashcanComponent, TrashcanComponent,
LocationLinkComponent, LocationLinkComponent,
SearchResultsRowComponent, SearchResultsRowComponent,
NodeVersionsDialogComponent, NodeVersionsDialogComponent,
LibraryDialogComponent, LibraryDialogComponent,
NodePermissionsDialogComponent, NodePermissionsDialogComponent,
PermissionsManagerComponent, PermissionsManagerComponent,
SearchResultsComponent, SearchResultsComponent,
SharedLinkViewComponent SharedLinkViewComponent
], ],
providers: [ providers: [
{ provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy }, { provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy },
{ provide: AppConfigService, useClass: DebugAppConfigService }, { provide: AppConfigService, useClass: DebugAppConfigService },
{ {
provide: TRANSLATION_PROVIDER, provide: TRANSLATION_PROVIDER,
multi: true, multi: true,
useValue: { useValue: {
name: 'app', name: 'app',
source: 'assets' source: 'assets'
} }
}, },
ContentManagementService, ContentManagementService,
NodeActionsService, NodeActionsService,
NodePermissionService, NodePermissionService,
ProfileResolver, ProfileResolver,
ExperimentalGuard, ExperimentalGuard,
ContentApiService ContentApiService
], ],
entryComponents: [ entryComponents: [
LibraryDialogComponent, LibraryDialogComponent,
NodeVersionsDialogComponent, NodeVersionsDialogComponent,
NodePermissionsDialogComponent NodePermissionsDialogComponent
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { } export class AppModule {}

View File

@@ -24,103 +24,101 @@
*/ */
import { import {
RouteReuseStrategy, RouteReuseStrategy,
DetachedRouteHandle, DetachedRouteHandle,
ActivatedRouteSnapshot ActivatedRouteSnapshot
} from '@angular/router'; } from '@angular/router';
interface RouteData { interface RouteData {
reuse: boolean; reuse: boolean;
} }
interface RouteInfo { interface RouteInfo {
handle: DetachedRouteHandle; handle: DetachedRouteHandle;
data: RouteData; data: RouteData;
} }
export class AppRouteReuseStrategy implements RouteReuseStrategy { export class AppRouteReuseStrategy implements RouteReuseStrategy {
private routeCache = new Map<string, RouteInfo>(); private routeCache = new Map<string, RouteInfo>();
shouldReuseRoute( shouldReuseRoute(
future: ActivatedRouteSnapshot, future: ActivatedRouteSnapshot,
curr: ActivatedRouteSnapshot curr: ActivatedRouteSnapshot
): boolean { ): boolean {
const ret = future.routeConfig === curr.routeConfig; const ret = future.routeConfig === curr.routeConfig;
if (ret) { if (ret) {
this.addRedirectsRecursively(future); // update redirects this.addRedirectsRecursively(future); // update redirects
}
return ret;
}
shouldDetach(route: ActivatedRouteSnapshot): boolean {
const data = this.getRouteData(route);
return data && data.reuse;
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
const url = this.getFullRouteUrl(route);
const data = this.getRouteData(route);
this.routeCache.set(url, { handle, data });
this.addRedirectsRecursively(route);
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
const url = this.getFullRouteUrl(route);
return this.routeCache.has(url);
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
const url = this.getFullRouteUrl(route);
const data = this.getRouteData(route);
return data && data.reuse && this.routeCache.has(url)
? this.routeCache.get(url).handle
: null;
}
private addRedirectsRecursively(route: ActivatedRouteSnapshot): void {
const config = route.routeConfig;
if (config) {
if (!config.loadChildren) {
const routeFirstChild = route.firstChild;
const routeFirstChildUrl = routeFirstChild
? this.getRouteUrlPaths(routeFirstChild).join('/')
: '';
const childConfigs = config.children;
if (childConfigs) {
const childConfigWithRedirect = childConfigs.find(
c => c.path === '' && !!c.redirectTo
);
if (childConfigWithRedirect) {
childConfigWithRedirect.redirectTo = routeFirstChildUrl;
}
} }
return ret; }
route.children.forEach(childRoute =>
this.addRedirectsRecursively(childRoute)
);
} }
}
shouldDetach(route: ActivatedRouteSnapshot): boolean { private getFullRouteUrl(route: ActivatedRouteSnapshot): string {
const data = this.getRouteData(route); return this.getFullRouteUrlPaths(route)
return data && data.reuse; .filter(Boolean)
} .join('/');
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { private getFullRouteUrlPaths(route: ActivatedRouteSnapshot): string[] {
const url = this.getFullRouteUrl(route); const paths = this.getRouteUrlPaths(route);
const data = this.getRouteData(route); return route.parent
this.routeCache.set(url, { handle, data }); ? [...this.getFullRouteUrlPaths(route.parent), ...paths]
this.addRedirectsRecursively(route); : paths;
} }
shouldAttach(route: ActivatedRouteSnapshot): boolean { private getRouteUrlPaths(route: ActivatedRouteSnapshot): string[] {
const url = this.getFullRouteUrl(route); return route.url.map(urlSegment => urlSegment.path);
return this.routeCache.has(url); }
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { private getRouteData(route: ActivatedRouteSnapshot): RouteData {
const url = this.getFullRouteUrl(route); return route.routeConfig && (route.routeConfig.data as RouteData);
const data = this.getRouteData(route); }
return data && data.reuse && this.routeCache.has(url)
? this.routeCache.get(url).handle
: null;
}
private addRedirectsRecursively(route: ActivatedRouteSnapshot): void {
const config = route.routeConfig;
if (config) {
if (!config.loadChildren) {
const routeFirstChild = route.firstChild;
const routeFirstChildUrl = routeFirstChild
? this.getRouteUrlPaths(routeFirstChild).join('/')
: '';
const childConfigs = config.children;
if (childConfigs) {
const childConfigWithRedirect = childConfigs.find(
c => c.path === '' && !!c.redirectTo
);
if (childConfigWithRedirect) {
childConfigWithRedirect.redirectTo = routeFirstChildUrl;
}
}
}
route.children.forEach(childRoute =>
this.addRedirectsRecursively(childRoute)
);
}
}
private getFullRouteUrl(route: ActivatedRouteSnapshot): string {
return this.getFullRouteUrlPaths(route)
.filter(Boolean)
.join('/');
}
private getFullRouteUrlPaths(route: ActivatedRouteSnapshot): string[] {
const paths = this.getRouteUrlPaths(route);
return route.parent
? [...this.getFullRouteUrlPaths(route.parent), ...paths]
: paths;
}
private getRouteUrlPaths(route: ActivatedRouteSnapshot): string[] {
return route.url.map(urlSegment => urlSegment.path);
}
private getRouteData(route: ActivatedRouteSnapshot): RouteData {
return (
route.routeConfig && (route.routeConfig.data as RouteData)
);
}
} }

View File

@@ -43,224 +43,233 @@ import { SearchResultsComponent } from './components/search/search-results/searc
import { ProfileResolver } from './services/profile.resolver'; import { ProfileResolver } from './services/profile.resolver';
export const APP_ROUTES: Routes = [ export const APP_ROUTES: Routes = [
{ {
path: 'login', path: 'login',
component: LoginComponent, component: LoginComponent,
data: { data: {
title: 'APP.SIGN_IN' title: 'APP.SIGN_IN'
}
},
{
path: 'settings',
loadChildren: 'src/app/components/settings/settings.module#AppSettingsModule',
data: {
title: 'Settings'
}
},
{
path: 'preview/s/:id',
component: SharedLinkViewComponent,
data: {
title: 'APP.PREVIEW.TITLE',
}
},
{
path: '',
component: LayoutComponent,
resolve: { profile: ProfileResolver },
children: [
{
path: '',
redirectTo: `/personal-files`,
pathMatch: 'full'
},
{
path: 'favorites',
data: {
sortingPreferenceKey: 'favorites'
},
children: [
{
path: '',
component: FavoritesComponent,
data: {
title: 'APP.BROWSE.FAVORITES.TITLE'
}
},
{
path: 'preview/:nodeId',
loadChildren: 'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'favorites'
}
}
]
},
{
path: 'libraries',
data: {
sortingPreferenceKey: 'libraries'
},
children: [{
path: '',
component: LibrariesComponent,
data: {
title: 'APP.BROWSE.LIBRARIES.TITLE'
}
}, {
path: ':folderId',
component: FilesComponent,
data: {
title: 'APP.BROWSE.LIBRARIES.TITLE',
sortingPreferenceKey: 'libraries-files'
}
},
{
path: ':folderId/preview/:nodeId',
loadChildren: 'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'libraries'
}
}
]
},
{
path: 'personal-files',
data: {
sortingPreferenceKey: 'personal-files'
},
children: [
{
path: '',
component: FilesComponent,
data: {
title: 'APP.BROWSE.PERSONAL.TITLE',
defaultNodeId: '-my-'
}
},
{
path: ':folderId',
component: FilesComponent,
data: {
title: 'APP.BROWSE.PERSONAL.TITLE'
}
},
{
path: 'preview/:nodeId',
loadChildren: 'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'personal-files'
}
},
{
path: ':folderId/preview/:nodeId',
loadChildren: 'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'personal-files'
}
}
]
},
{
path: 'recent-files',
data: {
sortingPreferenceKey: 'recent-files'
},
children: [
{
path: '',
component: RecentFilesComponent,
data: {
title: 'APP.BROWSE.RECENT.TITLE'
}
},
{
path: 'preview/:nodeId',
loadChildren: 'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'recent-files'
}
}
]
},
{
path: 'shared',
data: {
sortingPreferenceKey: 'shared-files'
},
children: [
{
path: '',
component: SharedFilesComponent,
data: {
title: 'APP.BROWSE.SHARED.TITLE'
}
},
{
path: 'preview/:nodeId',
loadChildren: 'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'shared'
}
}
]
},
{
path: 'trashcan',
component: TrashcanComponent,
data: {
title: 'APP.BROWSE.TRASHCAN.TITLE',
sortingPreferenceKey: 'trashcan'
}
},
{
path: 'about',
loadChildren: 'src/app/components/about/about.module#AboutModule',
data: {
title: 'APP.BROWSE.ABOUT.TITLE'
}
},
{
path: 'search',
children: [
{
path: '',
component: SearchResultsComponent,
data: {
title: 'APP.BROWSE.SEARCH.TITLE',
reuse: true
}
},
{
path: 'preview/:nodeId',
loadChildren: 'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'search'
}
}
]
},
{
path: '**',
component: GenericErrorComponent
}
],
canActivateChild: [ AuthGuardEcm ],
canActivate: [ AuthGuardEcm ]
} }
},
{
path: 'settings',
loadChildren:
'src/app/components/settings/settings.module#AppSettingsModule',
data: {
title: 'Settings'
}
},
{
path: 'preview/s/:id',
component: SharedLinkViewComponent,
data: {
title: 'APP.PREVIEW.TITLE'
}
},
{
path: '',
component: LayoutComponent,
resolve: { profile: ProfileResolver },
children: [
{
path: '',
redirectTo: `/personal-files`,
pathMatch: 'full'
},
{
path: 'favorites',
data: {
sortingPreferenceKey: 'favorites'
},
children: [
{
path: '',
component: FavoritesComponent,
data: {
title: 'APP.BROWSE.FAVORITES.TITLE'
}
},
{
path: 'preview/:nodeId',
loadChildren:
'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'favorites'
}
}
]
},
{
path: 'libraries',
data: {
sortingPreferenceKey: 'libraries'
},
children: [
{
path: '',
component: LibrariesComponent,
data: {
title: 'APP.BROWSE.LIBRARIES.TITLE'
}
},
{
path: ':folderId',
component: FilesComponent,
data: {
title: 'APP.BROWSE.LIBRARIES.TITLE',
sortingPreferenceKey: 'libraries-files'
}
},
{
path: ':folderId/preview/:nodeId',
loadChildren:
'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'libraries'
}
}
]
},
{
path: 'personal-files',
data: {
sortingPreferenceKey: 'personal-files'
},
children: [
{
path: '',
component: FilesComponent,
data: {
title: 'APP.BROWSE.PERSONAL.TITLE',
defaultNodeId: '-my-'
}
},
{
path: ':folderId',
component: FilesComponent,
data: {
title: 'APP.BROWSE.PERSONAL.TITLE'
}
},
{
path: 'preview/:nodeId',
loadChildren:
'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'personal-files'
}
},
{
path: ':folderId/preview/:nodeId',
loadChildren:
'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'personal-files'
}
}
]
},
{
path: 'recent-files',
data: {
sortingPreferenceKey: 'recent-files'
},
children: [
{
path: '',
component: RecentFilesComponent,
data: {
title: 'APP.BROWSE.RECENT.TITLE'
}
},
{
path: 'preview/:nodeId',
loadChildren:
'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'recent-files'
}
}
]
},
{
path: 'shared',
data: {
sortingPreferenceKey: 'shared-files'
},
children: [
{
path: '',
component: SharedFilesComponent,
data: {
title: 'APP.BROWSE.SHARED.TITLE'
}
},
{
path: 'preview/:nodeId',
loadChildren:
'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'shared'
}
}
]
},
{
path: 'trashcan',
component: TrashcanComponent,
data: {
title: 'APP.BROWSE.TRASHCAN.TITLE',
sortingPreferenceKey: 'trashcan'
}
},
{
path: 'about',
loadChildren: 'src/app/components/about/about.module#AboutModule',
data: {
title: 'APP.BROWSE.ABOUT.TITLE'
}
},
{
path: 'search',
children: [
{
path: '',
component: SearchResultsComponent,
data: {
title: 'APP.BROWSE.SEARCH.TITLE',
reuse: true
}
},
{
path: 'preview/:nodeId',
loadChildren:
'src/app/components/preview/preview.module#PreviewModule',
data: {
title: 'APP.PREVIEW.TITLE',
navigateMultiple: true,
navigateSource: 'search'
}
}
]
},
{
path: '**',
component: GenericErrorComponent
}
],
canActivateChild: [AuthGuardEcm],
canActivate: [AuthGuardEcm]
}
]; ];

View File

@@ -1,39 +1,39 @@
@mixin aca-about-component-theme($theme) { @mixin aca-about-component-theme($theme) {
$foreground: map-get($theme, foreground); $foreground: map-get($theme, foreground);
article { article {
color: mat-color($foreground, text, 0.54); color: mat-color($foreground, text, 0.54);
} }
article:first-of-type { article:first-of-type {
padding-bottom: 0; padding-bottom: 0;
} }
article:last-of-type { article:last-of-type {
margin-bottom: 50px; margin-bottom: 50px;
} }
header { header {
line-height: 24px; line-height: 24px;
font-size: 14px; font-size: 14px;
font-weight: 800; font-weight: 800;
letter-spacing: -0.2px; letter-spacing: -0.2px;
} }
a { a {
text-decoration: none; text-decoration: none;
color: mat-color($foreground, text, 0.87); color: mat-color($foreground, text, 0.87);
} }
.padding { .padding {
padding: 25px; padding: 25px;
} }
.padding-top-bottom { .padding-top-bottom {
padding: 25px 0 25px 0; padding: 25px 0 25px 0;
} }
.padding-left-right { .padding-left-right {
padding: 0 25px 0 25px; padding: 0 25px 0 25px;
} }
} }

View File

@@ -25,83 +25,164 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ObjectDataTableAdapter } from '@alfresco/adf-core'; import { ObjectDataTableAdapter } from '@alfresco/adf-core';
import { ContentApiService } from '../../services/content-api.service'; import { ContentApiService } from '../../services/content-api.service';
import { RepositoryInfo } from 'alfresco-js-api'; import { RepositoryInfo } from 'alfresco-js-api';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@Component({ @Component({
selector: 'app-about', selector: 'app-about',
templateUrl: './about.component.html' templateUrl: './about.component.html'
}) })
export class AboutComponent implements OnInit { export class AboutComponent implements OnInit {
repository: RepositoryInfo; repository: RepositoryInfo;
data: ObjectDataTableAdapter; data: ObjectDataTableAdapter;
status: ObjectDataTableAdapter; status: ObjectDataTableAdapter;
license: ObjectDataTableAdapter; license: ObjectDataTableAdapter;
modules: ObjectDataTableAdapter; modules: ObjectDataTableAdapter;
githubUrlCommitAlpha = 'https://github.com/Alfresco/alfresco-content-app/commits'; githubUrlCommitAlpha =
releaseVersion = ''; 'https://github.com/Alfresco/alfresco-content-app/commits';
releaseVersion = '';
constructor( constructor(
private contentApi: ContentApiService, private contentApi: ContentApiService,
private http: HttpClient private http: HttpClient
) {} ) {}
ngOnInit() { ngOnInit() {
this.contentApi.getRepositoryInformation() this.contentApi
.pipe(map(node => node.entry.repository)) .getRepositoryInformation()
.subscribe(repository => { .pipe(map(node => node.entry.repository))
this.repository = repository; .subscribe(repository => {
this.repository = repository;
this.modules = new ObjectDataTableAdapter(repository.modules, [ this.modules = new ObjectDataTableAdapter(repository.modules, [
{type: 'text', key: 'id', title: 'ID', sortable: true}, { type: 'text', key: 'id', title: 'ID', sortable: true },
{type: 'text', key: 'title', title: 'Title', sortable: true}, { type: 'text', key: 'title', title: 'Title', sortable: true },
{type: 'text', key: 'version', title: 'Description', sortable: true}, {
{type: 'date', key: 'installDate', title: 'Install Date', sortable: true}, type: 'text',
{type: 'text', key: 'installState', title: 'Install State', sortable: true}, key: 'version',
{type: 'text', key: 'versionMin', title: 'Version Minor', sortable: true}, title: 'Description',
{type: 'text', key: 'versionMax', title: 'Version Max', sortable: true} sortable: true
]); },
{
type: 'date',
key: 'installDate',
title: 'Install Date',
sortable: true
},
{
type: 'text',
key: 'installState',
title: 'Install State',
sortable: true
},
{
type: 'text',
key: 'versionMin',
title: 'Version Minor',
sortable: true
},
{
type: 'text',
key: 'versionMax',
title: 'Version Max',
sortable: true
}
]);
this.status = new ObjectDataTableAdapter([repository.status], [ this.status = new ObjectDataTableAdapter(
{type: 'text', key: 'isReadOnly', title: 'Read Only', sortable: true}, [repository.status],
{type: 'text', key: 'isAuditEnabled', title: 'Audit Enable', sortable: true}, [
{type: 'text', key: 'isQuickShareEnabled', title: 'Quick Shared Enable', sortable: true}, {
{type: 'text', key: 'isThumbnailGenerationEnabled', title: 'Thumbnail Generation', sortable: true} type: 'text',
]); key: 'isReadOnly',
title: 'Read Only',
sortable: true
if (repository.license) { },
this.license = new ObjectDataTableAdapter([repository.license], [ {
{type: 'date', key: 'issuedAt', title: 'Issued At', sortable: true}, type: 'text',
{type: 'date', key: 'expiresAt', title: 'Expires At', sortable: true}, key: 'isAuditEnabled',
{type: 'text', key: 'remainingDays', title: 'Remaining Days', sortable: true}, title: 'Audit Enable',
{type: 'text', key: 'holder', title: 'Holder', sortable: true}, sortable: true
{type: 'text', key: 'mode', title: 'Type', sortable: true}, },
{type: 'text', key: 'isClusterEnabled', title: 'Cluster Enabled', sortable: true}, {
{type: 'text', key: 'isCryptodocEnabled', title: 'Cryptodoc Enable', sortable: true} type: 'text',
]); key: 'isQuickShareEnabled',
title: 'Quick Shared Enable',
sortable: true
},
{
type: 'text',
key: 'isThumbnailGenerationEnabled',
title: 'Thumbnail Generation',
sortable: true
} }
}); ]
);
this.http.get('/versions.json') if (repository.license) {
.subscribe((response: any) => { this.license = new ObjectDataTableAdapter(
const regexp = new RegExp('^(@alfresco|alfresco-)'); [repository.license],
[
{
type: 'date',
key: 'issuedAt',
title: 'Issued At',
sortable: true
},
{
type: 'date',
key: 'expiresAt',
title: 'Expires At',
sortable: true
},
{
type: 'text',
key: 'remainingDays',
title: 'Remaining Days',
sortable: true
},
{ type: 'text', key: 'holder', title: 'Holder', sortable: true },
{ type: 'text', key: 'mode', title: 'Type', sortable: true },
{
type: 'text',
key: 'isClusterEnabled',
title: 'Cluster Enabled',
sortable: true
},
{
type: 'text',
key: 'isCryptodocEnabled',
title: 'Cryptodoc Enable',
sortable: true
}
]
);
}
});
const alfrescoPackagesTableRepresentation = Object.keys(response.dependencies) this.http.get('/versions.json').subscribe((response: any) => {
.filter((val) => regexp.test(val)) const regexp = new RegExp('^(@alfresco|alfresco-)');
.map((val) => ({
name: val,
version: response.dependencies[val].version
}));
this.data = new ObjectDataTableAdapter(alfrescoPackagesTableRepresentation, [ const alfrescoPackagesTableRepresentation = Object.keys(
{type: 'text', key: 'name', title: 'Name', sortable: true}, response.dependencies
{type: 'text', key: 'version', title: 'Version', sortable: true} )
]); .filter(val => regexp.test(val))
.map(val => ({
name: val,
version: response.dependencies[val].version
}));
this.releaseVersion = response.version; this.data = new ObjectDataTableAdapter(
}); alfrescoPackagesTableRepresentation,
} [
{ type: 'text', key: 'name', title: 'Name', sortable: true },
{ type: 'text', key: 'version', title: 'Version', sortable: true }
]
);
this.releaseVersion = response.version;
});
}
} }

View File

@@ -30,19 +30,14 @@ import { CommonModule } from '@angular/common';
import { CoreModule } from '@alfresco/adf-core'; import { CoreModule } from '@alfresco/adf-core';
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: AboutComponent component: AboutComponent
} }
]; ];
@NgModule({ @NgModule({
imports: [ imports: [CommonModule, CoreModule.forChild(), RouterModule.forChild(routes)],
CommonModule, declarations: [AboutComponent]
CoreModule.forChild(),
RouterModule.forChild(routes)
],
declarations: [AboutComponent]
}) })
export class AboutModule { export class AboutModule {}
}

View File

@@ -24,29 +24,45 @@
*/ */
import { import {
state, state,
style, style,
animate, animate,
transition, transition,
query, query,
group, group,
sequence sequence
} from '@angular/animations'; } from '@angular/animations';
export const contextMenuAnimation = [ export const contextMenuAnimation = [
state('void', style({ state(
opacity: 0, 'void',
transform: 'scale(0.01, 0.01)' style({
})), opacity: 0,
transition('void => *', sequence([ transform: 'scale(0.01, 0.01)'
query('.mat-menu-content', style({ opacity: 0 })), })
animate('100ms linear', style({ opacity: 1, transform: 'scale(1, 0.5)' })), ),
group([ transition(
query('.mat-menu-content', animate('400ms cubic-bezier(0.55, 0, 0.55, 0.2)', 'void => *',
style({ opacity: 1 }) sequence([
)), query('.mat-menu-content', style({ opacity: 0 })),
animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({ transform: 'scale(1, 1)' })), animate(
]) '100ms linear',
])), style({ opacity: 1, transform: 'scale(1, 0.5)' })
transition('* => void', animate('150ms 50ms linear', style({ opacity: 0 }))) ),
group([
query(
'.mat-menu-content',
animate(
'400ms cubic-bezier(0.55, 0, 0.55, 0.2)',
style({ opacity: 1 })
)
),
animate(
'300ms cubic-bezier(0.25, 0.8, 0.25, 1)',
style({ transform: 'scale(1, 1)' })
)
])
])
),
transition('* => void', animate('150ms 50ms linear', style({ opacity: 0 })))
]; ];

View File

@@ -27,25 +27,25 @@ import { Directive, ElementRef, OnDestroy } from '@angular/core';
import { FocusableOption, FocusMonitor, FocusOrigin } from '@angular/cdk/a11y'; import { FocusableOption, FocusMonitor, FocusOrigin } from '@angular/cdk/a11y';
@Directive({ @Directive({
selector: '[acaContextMenuItem]', selector: '[acaContextMenuItem]'
}) })
export class ContextMenuItemDirective implements OnDestroy, FocusableOption { export class ContextMenuItemDirective implements OnDestroy, FocusableOption {
constructor( constructor(
private elementRef: ElementRef, private elementRef: ElementRef,
private focusMonitor: FocusMonitor) { private focusMonitor: FocusMonitor
) {
focusMonitor.monitor(this.getHostElement(), false);
}
focusMonitor.monitor(this.getHostElement(), false); ngOnDestroy() {
} this.focusMonitor.stopMonitoring(this.getHostElement());
}
ngOnDestroy() { focus(origin: FocusOrigin = 'keyboard'): void {
this.focusMonitor.stopMonitoring(this.getHostElement()); this.focusMonitor.focusVia(this.getHostElement(), origin);
} }
focus(origin: FocusOrigin = 'keyboard'): void { private getHostElement(): HTMLElement {
this.focusMonitor.focusVia(this.getHostElement(), origin); return this.elementRef.nativeElement;
} }
private getHostElement(): HTMLElement {
return this.elementRef.nativeElement;
}
} }

View File

@@ -1,28 +1,34 @@
import { Directive, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'; import {
Directive,
Output,
EventEmitter,
OnInit,
OnDestroy
} from '@angular/core';
import { fromEvent, Subscription } from 'rxjs'; import { fromEvent, Subscription } from 'rxjs';
import { delay } from 'rxjs/operators'; import { delay } from 'rxjs/operators';
@Directive({ @Directive({
selector: '[acaContextMenuOutsideEvent]' selector: '[acaContextMenuOutsideEvent]'
}) })
export class OutsideEventDirective implements OnInit, OnDestroy { export class OutsideEventDirective implements OnInit, OnDestroy {
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
@Output() clickOutside: EventEmitter<null> = new EventEmitter(); @Output()
clickOutside: EventEmitter<null> = new EventEmitter();
constructor() {} constructor() {}
ngOnInit() { ngOnInit() {
this.subscriptions = this.subscriptions.concat([ this.subscriptions = this.subscriptions.concat([
fromEvent(document.body, 'click') fromEvent(document.body, 'click')
.pipe(delay(1)) .pipe(delay(1))
.subscribe(() => this.clickOutside.next()) .subscribe(() => this.clickOutside.next())
]); ]);
} }
ngOnDestroy() { ngOnDestroy() {
this.subscriptions.forEach(subscription => subscription.unsubscribe()); this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.subscriptions = []; this.subscriptions = [];
} }
} }

View File

@@ -26,10 +26,9 @@
import { OverlayRef } from '@angular/cdk/overlay'; import { OverlayRef } from '@angular/cdk/overlay';
export class ContextMenuOverlayRef { export class ContextMenuOverlayRef {
constructor(private overlayRef: OverlayRef) {}
constructor(private overlayRef: OverlayRef) { } close(): void {
this.overlayRef.dispose();
close(): void { }
this.overlayRef.dispose();
}
} }

View File

@@ -1,19 +1,19 @@
.aca-context-menu { .aca-context-menu {
&__more-actions::after { &__more-actions::after {
margin-left: 34px; margin-left: 34px;
width: 0; width: 0;
height: 0; height: 0;
border-style: solid; border-style: solid;
border-width: 5px 0 5px 5px; border-width: 5px 0 5px 5px;
content: ''; content: '';
display: inline-block; display: inline-block;
} }
&__separator { &__separator {
display: block; display: block;
margin: 0; margin: 0;
padding: 0; padding: 0;
border-top-width: 1px; border-top-width: 1px;
border-top-style: solid; border-top-style: solid;
} }
} }

View File

@@ -1,17 +1,17 @@
@mixin aca-context-menu-theme($theme) { @mixin aca-context-menu-theme($theme) {
$foreground: map-get($theme, foreground); $foreground: map-get($theme, foreground);
$primary: map-get($theme, primary); $primary: map-get($theme, primary);
.aca-context-menu { .aca-context-menu {
@include angular-material-theme($theme); @include angular-material-theme($theme);
&__separator { &__separator {
border-top-color: mat-color($foreground, divider); border-top-color: mat-color($foreground, divider);
}
&__more-actions::after {
border-color: transparent;
border-left-color: mat-color($primary);
}
} }
&__more-actions::after {
border-color: transparent;
border-left-color: mat-color($primary);
}
}
} }

View File

@@ -24,8 +24,14 @@
*/ */
import { import {
Component, ViewEncapsulation, OnInit, OnDestroy, HostListener, Component,
ViewChildren, QueryList, AfterViewInit ViewEncapsulation,
OnInit,
OnDestroy,
HostListener,
ViewChildren,
QueryList,
AfterViewInit
} from '@angular/core'; } from '@angular/core';
import { trigger } from '@angular/animations'; import { trigger } from '@angular/animations';
import { FocusKeyManager } from '@angular/cdk/a11y'; import { FocusKeyManager } from '@angular/cdk/a11y';
@@ -44,99 +50,99 @@ import { contextMenuAnimation } from './animations';
import { ContextMenuItemDirective } from './context-menu-item.directive'; import { ContextMenuItemDirective } from './context-menu-item.directive';
@Component({ @Component({
selector: 'aca-context-menu', selector: 'aca-context-menu',
templateUrl: './context-menu.component.html', templateUrl: './context-menu.component.html',
styleUrls: [ styleUrls: [
'./context-menu.component.scss', './context-menu.component.scss',
'./context-menu.component.theme.scss' './context-menu.component.theme.scss'
], ],
host: { host: {
role: 'menu', role: 'menu',
class: 'aca-context-menu' class: 'aca-context-menu'
}, },
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
animations: [ animations: [trigger('panelAnimation', contextMenuAnimation)]
trigger('panelAnimation', contextMenuAnimation)
]
}) })
export class ContextMenuComponent implements OnInit, OnDestroy, AfterViewInit { export class ContextMenuComponent implements OnInit, OnDestroy, AfterViewInit {
private onDestroy$: Subject<boolean> = new Subject<boolean>(); private onDestroy$: Subject<boolean> = new Subject<boolean>();
private selection: SelectionState; private selection: SelectionState;
private _keyManager: FocusKeyManager<ContextMenuItemDirective>; private _keyManager: FocusKeyManager<ContextMenuItemDirective>;
actions: Array<ContentActionRef> = []; actions: Array<ContentActionRef> = [];
@ViewChildren(ContextMenuItemDirective) @ViewChildren(ContextMenuItemDirective)
private contextMenuItems: QueryList<ContextMenuItemDirective>; private contextMenuItems: QueryList<ContextMenuItemDirective>;
@HostListener('contextmenu', ['$event']) @HostListener('contextmenu', ['$event'])
handleContextMenu(event: MouseEvent) { handleContextMenu(event: MouseEvent) {
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
if (this.contextMenuOverlayRef) { if (this.contextMenuOverlayRef) {
this.contextMenuOverlayRef.close();
}
}
}
@HostListener('document:keydown.Escape', ['$event'])
handleKeydownEscape(event: KeyboardEvent) {
if (event) {
if (this.contextMenuOverlayRef) {
this.contextMenuOverlayRef.close();
}
}
}
@HostListener('document:keydown', ['$event'])
handleKeydownEvent(event: KeyboardEvent) {
if (event) {
const keyCode = event.keyCode;
if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) {
this._keyManager.onKeydown(event);
}
}
}
constructor(
private contextMenuOverlayRef: ContextMenuOverlayRef,
private extensions: AppExtensionService,
private store: Store<AppStore>,
) { }
onClickOutsideEvent() {
if (this.contextMenuOverlayRef) {
this.contextMenuOverlayRef.close();
}
}
runAction(actionId: string) {
const context = {
selection: this.selection
};
this.extensions.runActionById(actionId, context);
this.contextMenuOverlayRef.close(); this.contextMenuOverlayRef.close();
}
} }
}
ngOnDestroy() { @HostListener('document:keydown.Escape', ['$event'])
this.onDestroy$.next(true); handleKeydownEscape(event: KeyboardEvent) {
this.onDestroy$.complete(); if (event) {
if (this.contextMenuOverlayRef) {
this.contextMenuOverlayRef.close();
}
} }
}
ngOnInit() { @HostListener('document:keydown', ['$event'])
this.store handleKeydownEvent(event: KeyboardEvent) {
.select(appSelection) if (event) {
.pipe(takeUntil(this.onDestroy$)) const keyCode = event.keyCode;
.subscribe(selection => { if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) {
if (selection.count) { this._keyManager.onKeydown(event);
this.selection = selection; }
this.actions = this.extensions.getAllowedContextMenuActions();
}
});
} }
}
ngAfterViewInit() { constructor(
this._keyManager = new FocusKeyManager<ContextMenuItemDirective>(this.contextMenuItems); private contextMenuOverlayRef: ContextMenuOverlayRef,
this._keyManager.setFirstItemActive(); private extensions: AppExtensionService,
private store: Store<AppStore>
) {}
onClickOutsideEvent() {
if (this.contextMenuOverlayRef) {
this.contextMenuOverlayRef.close();
} }
}
runAction(actionId: string) {
const context = {
selection: this.selection
};
this.extensions.runActionById(actionId, context);
this.contextMenuOverlayRef.close();
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
ngOnInit() {
this.store
.select(appSelection)
.pipe(takeUntil(this.onDestroy$))
.subscribe(selection => {
if (selection.count) {
this.selection = selection;
this.actions = this.extensions.getAllowedContextMenuActions();
}
});
}
ngAfterViewInit() {
this._keyManager = new FocusKeyManager<ContextMenuItemDirective>(
this.contextMenuItems
);
this._keyManager.setFirstItemActive();
}
} }

View File

@@ -34,106 +34,112 @@ import { DataRow } from '@alfresco/adf-core';
import { MinimalNodeEntity } from 'alfresco-js-api'; import { MinimalNodeEntity } from 'alfresco-js-api';
@Directive({ @Directive({
selector: '[acaContextActions]' selector: '[acaContextActions]'
}) })
export class ContextActionsDirective { export class ContextActionsDirective {
private overlayRef: ContextMenuOverlayRef = null; private overlayRef: ContextMenuOverlayRef = null;
// tslint:disable-next-line:no-input-rename // tslint:disable-next-line:no-input-rename
@Input('acaContextEnable') enabled = true; @Input('acaContextEnable')
enabled = true;
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
onResize(event) { onResize(event) {
if (event && this.overlayRef) { if (event && this.overlayRef) {
this.clearSelection(); this.clearSelection();
this.overlayRef.close(); this.overlayRef.close();
} }
}
@HostListener('contextmenu', ['$event'])
onContextMenuEvent(event: MouseEvent) {
if (event) {
event.preventDefault();
if (this.enabled) {
this.execute(event);
}
}
}
constructor(
private documentList: DocumentListComponent,
private store: Store<AppStore>,
private contextMenuService: ContextMenuService
) {}
private execute(event: MouseEvent) {
const selected = this.getSelectedRow(event);
if (selected) {
if (!this.isInSelection(selected)) {
this.clearSelection();
this.documentList.dataTable.selectRow(selected, true);
this.documentList.selection.push((<any>selected).node);
this.updateSelection();
}
this.render(event);
}
}
private render(event: MouseEvent) {
if (this.overlayRef) {
this.overlayRef.close();
} }
@HostListener('contextmenu', ['$event']) this.overlayRef = this.contextMenuService.open({
onContextMenuEvent(event: MouseEvent) { source: event,
if (event) { hasBackdrop: false,
event.preventDefault(); backdropClass: 'cdk-overlay-transparent-backdrop',
panelClass: 'cdk-overlay-pane'
});
}
if (this.enabled) { private updateSelection() {
this.execute(event); this.store.dispatch(
} new SetSelectedNodesAction(this.documentList.selection)
} );
}
private isInSelection(row: DataRow): MinimalNodeEntity {
return this.documentList.selection.find(
selected => row.getValue('name') === selected.entry.name
);
}
private getSelectedRow(event): DataRow {
const rowElement = this.findAncestor(
<HTMLElement>event.target,
'adf-datatable-row'
);
if (!rowElement) {
return null;
} }
constructor( const rowName = rowElement
private documentList: DocumentListComponent, .querySelector('.adf-data-table-cell--text .adf-datatable-cell')
private store: Store<AppStore>, .textContent.trim();
private contextMenuService: ContextMenuService
) { }
private execute(event: MouseEvent) { return this.documentList.data
const selected = this.getSelectedRow(event); .getRows()
.find((row: DataRow) => row.getValue('name') === rowName);
}
if (selected) { private clearSelection() {
if (!this.isInSelection(selected)) { this.documentList.data.getRows().map((row: DataRow) => {
this.clearSelection(); return this.documentList.dataTable.selectRow(row, false);
});
this.documentList.dataTable.selectRow(selected, true); this.documentList.selection = [];
this.documentList.selection.push((<any>selected).node); }
this.updateSelection(); private findAncestor(el: Element, className: string): Element {
} // tslint:disable-next-line:curly
while ((el = el.parentElement) && !el.classList.contains(className));
this.render(event); return el;
} }
}
private render(event: MouseEvent) {
if (this.overlayRef) {
this.overlayRef.close();
}
this.overlayRef = this.contextMenuService.open({
source: event,
hasBackdrop: false,
backdropClass: 'cdk-overlay-transparent-backdrop',
panelClass: 'cdk-overlay-pane',
});
}
private updateSelection() {
this.store.dispatch(
new SetSelectedNodesAction(this.documentList.selection)
);
}
private isInSelection(row: DataRow): MinimalNodeEntity {
return this.documentList.selection.find((selected) =>
row.getValue('name') === selected.entry.name);
}
private getSelectedRow(event): DataRow {
const rowElement = this.findAncestor(<HTMLElement>event.target, 'adf-datatable-row');
if (!rowElement) {
return null;
}
const rowName = rowElement.querySelector('.adf-data-table-cell--text .adf-datatable-cell')
.textContent
.trim();
return this.documentList.data.getRows()
.find((row: DataRow) => row.getValue('name') === rowName);
}
private clearSelection() {
this.documentList.data.getRows().map((row: DataRow) => {
return this.documentList.dataTable.selectRow(row, false);
});
this.documentList.selection = [];
}
private findAncestor (el: Element, className: string): Element {
// tslint:disable-next-line:curly
while ((el = el.parentElement) && !el.classList.contains(className));
return el;
}
} }

View File

@@ -24,7 +24,12 @@
*/ */
import { NgModule, ModuleWithProviders } from '@angular/core'; import { NgModule, ModuleWithProviders } from '@angular/core';
import { MatMenuModule, MatListModule, MatIconModule, MatButtonModule } from '@angular/material'; import {
MatMenuModule,
MatListModule,
MatIconModule,
MatButtonModule
} from '@angular/material';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { CoreModule } from '@alfresco/adf-core'; import { CoreModule } from '@alfresco/adf-core';
import { CoreExtensionsModule } from '../../extensions/core.extensions.module'; import { CoreExtensionsModule } from '../../extensions/core.extensions.module';
@@ -37,44 +42,40 @@ import { ExtensionsModule } from '@alfresco/adf-extensions';
import { OutsideEventDirective } from './context-menu-outside-event.directive'; import { OutsideEventDirective } from './context-menu-outside-event.directive';
@NgModule({ @NgModule({
imports: [ imports: [
MatMenuModule, MatMenuModule,
MatListModule, MatListModule,
MatIconModule, MatIconModule,
MatButtonModule, MatButtonModule,
BrowserModule, BrowserModule,
CoreExtensionsModule.forChild(), CoreExtensionsModule.forChild(),
CoreModule.forChild(), CoreModule.forChild(),
ExtensionsModule.forChild() ExtensionsModule.forChild()
], ],
declarations: [ declarations: [
ContextActionsDirective, ContextActionsDirective,
ContextMenuComponent, ContextMenuComponent,
ContextMenuItemDirective, ContextMenuItemDirective,
OutsideEventDirective OutsideEventDirective
], ],
exports: [ exports: [
OutsideEventDirective, OutsideEventDirective,
ContextActionsDirective, ContextActionsDirective,
ContextMenuComponent ContextMenuComponent
], ],
entryComponents: [ entryComponents: [ContextMenuComponent]
ContextMenuComponent
]
}) })
export class ContextMenuModule { export class ContextMenuModule {
static forRoot(): ModuleWithProviders { static forRoot(): ModuleWithProviders {
return { return {
ngModule: ContextMenuModule, ngModule: ContextMenuModule,
providers: [ providers: [ContextMenuService]
ContextMenuService };
] }
};
}
static forChild(): ModuleWithProviders { static forChild(): ModuleWithProviders {
return { return {
ngModule: ContextMenuModule ngModule: ContextMenuModule
}; };
} }
} }

View File

@@ -7,96 +7,115 @@ import { ContextmenuOverlayConfig } from './interfaces';
@Injectable() @Injectable()
export class ContextMenuService { export class ContextMenuService {
constructor( constructor(private injector: Injector, private overlay: Overlay) {}
private injector: Injector,
private overlay: Overlay) { }
open(config: ContextmenuOverlayConfig) { open(config: ContextmenuOverlayConfig) {
const overlay = this.createOverlay(config);
const overlay = this.createOverlay(config); const overlayRef = new ContextMenuOverlayRef(overlay);
const overlayRef = new ContextMenuOverlayRef(overlay); this.attachDialogContainer(overlay, config, overlayRef);
this.attachDialogContainer(overlay, config, overlayRef); overlay.backdropClick().subscribe(() => overlayRef.close());
overlay.backdropClick().subscribe(() => overlayRef.close()); // prevent native contextmenu on overlay element if config.hasBackdrop is true
if (config.hasBackdrop) {
// prevent native contextmenu on overlay element if config.hasBackdrop is true (<any>overlay)._backdropElement.addEventListener(
if (config.hasBackdrop) { 'contextmenu',
(<any>overlay)._backdropElement () => {
.addEventListener('contextmenu', () => { event.preventDefault();
event.preventDefault(); (<any>overlay)._backdropClick.next(null);
(<any>overlay)._backdropClick.next(null); },
}, true); true
} );
return overlayRef;
} }
private createOverlay(config: ContextmenuOverlayConfig) { return overlayRef;
const overlayConfig = this.getOverlayConfig(config); }
return this.overlay.create(overlayConfig);
}
private attachDialogContainer(overlay: OverlayRef, config: ContextmenuOverlayConfig, contextmenuOverlayRef: ContextMenuOverlayRef) { private createOverlay(config: ContextmenuOverlayConfig) {
const injector = this.createInjector(config, contextmenuOverlayRef); const overlayConfig = this.getOverlayConfig(config);
return this.overlay.create(overlayConfig);
}
const containerPortal = new ComponentPortal(ContextMenuComponent, null, injector); private attachDialogContainer(
const containerRef: ComponentRef<ContextMenuComponent> = overlay.attach(containerPortal); overlay: OverlayRef,
config: ContextmenuOverlayConfig,
contextmenuOverlayRef: ContextMenuOverlayRef
) {
const injector = this.createInjector(config, contextmenuOverlayRef);
return containerRef.instance; const containerPortal = new ComponentPortal(
} ContextMenuComponent,
null,
injector
);
const containerRef: ComponentRef<ContextMenuComponent> = overlay.attach(
containerPortal
);
private createInjector(config: ContextmenuOverlayConfig, contextmenuOverlayRef: ContextMenuOverlayRef): PortalInjector { return containerRef.instance;
const injectionTokens = new WeakMap(); }
injectionTokens.set(ContextMenuOverlayRef, contextmenuOverlayRef); private createInjector(
config: ContextmenuOverlayConfig,
contextmenuOverlayRef: ContextMenuOverlayRef
): PortalInjector {
const injectionTokens = new WeakMap();
return new PortalInjector(this.injector, injectionTokens); injectionTokens.set(ContextMenuOverlayRef, contextmenuOverlayRef);
}
private getOverlayConfig(config: ContextmenuOverlayConfig): OverlayConfig { return new PortalInjector(this.injector, injectionTokens);
const fakeElement: any = { }
getBoundingClientRect: (): ClientRect => ({
bottom: config.source.clientY,
height: 0,
left: config.source.clientX,
right: config.source.clientX,
top: config.source.clientY,
width: 0
})
};
const positionStrategy = this.overlay.position() private getOverlayConfig(config: ContextmenuOverlayConfig): OverlayConfig {
.connectedTo( const fakeElement: any = {
new ElementRef(fakeElement), getBoundingClientRect: (): ClientRect => ({
{ originX: 'start', originY: 'bottom' }, bottom: config.source.clientY,
{ overlayX: 'start', overlayY: 'top' }) height: 0,
.withFallbackPosition( left: config.source.clientX,
{ originX: 'start', originY: 'top' }, right: config.source.clientX,
{ overlayX: 'start', overlayY: 'bottom' }) top: config.source.clientY,
.withFallbackPosition( width: 0
{ originX: 'end', originY: 'top' }, })
{ overlayX: 'start', overlayY: 'top' }) };
.withFallbackPosition(
{ originX: 'start', originY: 'top' },
{ overlayX: 'end', overlayY: 'top' })
.withFallbackPosition(
{ originX: 'end', originY: 'center' },
{ overlayX: 'start', overlayY: 'center' })
.withFallbackPosition(
{ originX: 'start', originY: 'center' },
{ overlayX: 'end', overlayY: 'center' }
);
const overlayConfig = new OverlayConfig({ const positionStrategy = this.overlay
hasBackdrop: config.hasBackdrop, .position()
backdropClass: config.backdropClass, .connectedTo(
panelClass: config.panelClass, new ElementRef(fakeElement),
scrollStrategy: this.overlay.scrollStrategies.close(), { originX: 'start', originY: 'bottom' },
positionStrategy { overlayX: 'start', overlayY: 'top' }
}); )
.withFallbackPosition(
{ originX: 'start', originY: 'top' },
{ overlayX: 'start', overlayY: 'bottom' }
)
.withFallbackPosition(
{ originX: 'end', originY: 'top' },
{ overlayX: 'start', overlayY: 'top' }
)
.withFallbackPosition(
{ originX: 'start', originY: 'top' },
{ overlayX: 'end', overlayY: 'top' }
)
.withFallbackPosition(
{ originX: 'end', originY: 'center' },
{ overlayX: 'start', overlayY: 'center' }
)
.withFallbackPosition(
{ originX: 'start', originY: 'center' },
{ overlayX: 'end', overlayY: 'center' }
);
return overlayConfig; const overlayConfig = new OverlayConfig({
} hasBackdrop: config.hasBackdrop,
backdropClass: config.backdropClass,
panelClass: config.panelClass,
scrollStrategy: this.overlay.scrollStrategies.close(),
positionStrategy
});
return overlayConfig;
}
} }

View File

@@ -24,9 +24,9 @@
*/ */
export interface ContextmenuOverlayConfig { export interface ContextmenuOverlayConfig {
panelClass?: string; panelClass?: string;
hasBackdrop?: boolean; hasBackdrop?: boolean;
backdropClass?: string; backdropClass?: string;
source?: MouseEvent; source?: MouseEvent;
data?: any; data?: any;
} }

View File

@@ -1,44 +1,44 @@
@mixin aca-current-user-theme($theme) { @mixin aca-current-user-theme($theme) {
$background: map-get($theme, background); $background: map-get($theme, background);
$am-avatar-size: 35px; $am-avatar-size: 35px;
.aca-current-user { .aca-current-user {
position: relative; position: relative;
color: mat-color($background, card); color: mat-color($background, card);
line-height: 20px; line-height: 20px;
.am-avatar { .am-avatar {
margin-left: 9px; margin-left: 9px;
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
width: $am-avatar-size; width: $am-avatar-size;
height: $am-avatar-size; height: $am-avatar-size;
line-height: $am-avatar-size; line-height: $am-avatar-size;
font-size: 1em; font-size: 1em;
text-align: center; text-align: center;
color: inherit; color: inherit;
border-radius: 100%; border-radius: 100%;
background-color: mat-color($background, card, .15); background-color: mat-color($background, card, 0.15);
}
.current-user__full-name {
width: 100px;
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display:inline-block;
height: 18px;
font-size: 14px;
line-height: 1.43;
letter-spacing: -0.3px;
cursor: default;
}
@media screen and ($mat-small) {
.current-user__full-name {
display: none;
}
}
} }
.current-user__full-name {
width: 100px;
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
height: 18px;
font-size: 14px;
line-height: 1.43;
letter-spacing: -0.3px;
cursor: default;
}
@media screen and ($mat-small) {
.current-user__full-name {
display: none;
}
}
}
} }

View File

@@ -26,27 +26,30 @@
import { Component, ViewEncapsulation } from '@angular/core'; import { Component, ViewEncapsulation } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { selectUser, appLanguagePicker } from '../../store/selectors/app.selectors'; import {
selectUser,
appLanguagePicker
} from '../../store/selectors/app.selectors';
import { AppStore } from '../../store/states'; import { AppStore } from '../../store/states';
import { ProfileState } from '@alfresco/adf-extensions'; import { ProfileState } from '@alfresco/adf-extensions';
import { SetSelectedNodesAction } from '../../store/actions'; import { SetSelectedNodesAction } from '../../store/actions';
@Component({ @Component({
selector: 'aca-current-user', selector: 'aca-current-user',
templateUrl: './current-user.component.html', templateUrl: './current-user.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { class: 'aca-current-user' } host: { class: 'aca-current-user' }
}) })
export class CurrentUserComponent { export class CurrentUserComponent {
profile$: Observable<ProfileState>; profile$: Observable<ProfileState>;
languagePicker$: Observable<boolean>; languagePicker$: Observable<boolean>;
constructor(private store: Store<AppStore>) { constructor(private store: Store<AppStore>) {
this.profile$ = this.store.select(selectUser); this.profile$ = this.store.select(selectUser);
this.languagePicker$ = store.select(appLanguagePicker); this.languagePicker$ = store.select(appLanguagePicker);
} }
onLogoutEvent() { onLogoutEvent() {
this.store.dispatch(new SetSelectedNodesAction([])); this.store.dispatch(new SetSelectedNodesAction([]));
} }
} }

View File

@@ -27,9 +27,12 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TestBed, ComponentFixture } from '@angular/core/testing'; import { TestBed, ComponentFixture } from '@angular/core/testing';
import { import {
AlfrescoApiService, AlfrescoApiService,
TimeAgoPipe, NodeNameTooltipPipe, TimeAgoPipe,
NodeFavoriteDirective, DataTableComponent, AppConfigPipe NodeNameTooltipPipe,
NodeFavoriteDirective,
DataTableComponent,
AppConfigPipe
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services'; import { DocumentListComponent } from '@alfresco/adf-content-services';
import { ContentManagementService } from '../../services/content-management.service'; import { ContentManagementService } from '../../services/content-management.service';
@@ -40,136 +43,144 @@ import { ContentApiService } from '../../services/content-api.service';
import { ExperimentalDirective } from '../../directives/experimental.directive'; import { ExperimentalDirective } from '../../directives/experimental.directive';
describe('FavoritesComponent', () => { describe('FavoritesComponent', () => {
let fixture: ComponentFixture<FavoritesComponent>; let fixture: ComponentFixture<FavoritesComponent>;
let component: FavoritesComponent; let component: FavoritesComponent;
let alfrescoApi: AlfrescoApiService; let alfrescoApi: AlfrescoApiService;
let contentService: ContentManagementService; let contentService: ContentManagementService;
let contentApi: ContentApiService; let contentApi: ContentApiService;
let router: Router; let router: Router;
let page; let page;
let node; let node;
beforeEach(() => {
page = {
list: {
entries: [
{ entry: { id: 1, target: { file: {} } } },
{ entry: { id: 2, target: { folder: {} } } }
],
pagination: { data: 'data' }
}
};
node = <any>{
id: 'folder-node',
isFolder: true,
isFile: false,
path: {
elements: []
}
};
});
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [
DataTableComponent,
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DocumentListComponent,
FavoritesComponent,
AppConfigPipe,
ExperimentalDirective
],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(FavoritesComponent);
component = fixture.componentInstance;
alfrescoApi = TestBed.get(AlfrescoApiService);
alfrescoApi.reset();
spyOn(alfrescoApi.favoritesApi, 'getFavorites').and.returnValue(
Promise.resolve(page)
);
contentApi = TestBed.get(ContentApiService);
contentService = TestBed.get(ContentManagementService);
router = TestBed.get(Router);
});
describe('Events', () => {
beforeEach(() => { beforeEach(() => {
page = { spyOn(component, 'reload');
list: { fixture.detectChanges();
entries: [
{ entry: { id: 1, target: { file: {} } } },
{ entry: { id: 2, target: { folder: {} } } }
],
pagination: { data: 'data'}
}
};
node = <any> {
id: 'folder-node',
isFolder: true,
isFile: false,
path: {
elements: []
}
};
}); });
it('should refresh on editing folder event', () => {
contentService.folderEdited.next(null);
expect(component.reload).toHaveBeenCalled();
});
it('should refresh on move node event', () => {
contentService.nodesMoved.next(null);
expect(component.reload).toHaveBeenCalled();
});
it('should refresh on node deleted event', () => {
contentService.nodesDeleted.next(null);
expect(component.reload).toHaveBeenCalled();
});
it('should refresh on node restore event', () => {
contentService.nodesRestored.next(null);
expect(component.reload).toHaveBeenCalled();
});
});
describe('Node navigation', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
imports: [ AppTestingModule ], spyOn(router, 'navigate');
declarations: [ fixture.detectChanges();
DataTableComponent,
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DocumentListComponent,
FavoritesComponent,
AppConfigPipe,
ExperimentalDirective
],
schemas: [ NO_ERRORS_SCHEMA ]
});
fixture = TestBed.createComponent(FavoritesComponent);
component = fixture.componentInstance;
alfrescoApi = TestBed.get(AlfrescoApiService);
alfrescoApi.reset();
spyOn(alfrescoApi.favoritesApi, 'getFavorites').and.returnValue(Promise.resolve(page));
contentApi = TestBed.get(ContentApiService);
contentService = TestBed.get(ContentManagementService);
router = TestBed.get(Router);
}); });
describe('Events', () => { it('navigates to `/libraries` if node path has `Sites`', () => {
beforeEach(() => { node.path.elements = [{ name: 'Sites' }];
spyOn(component, 'reload');
fixture.detectChanges();
});
it('should refresh on editing folder event', () => { component.navigate(node);
contentService.folderEdited.next(null);
expect(component.reload).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalledWith([
}); '/libraries',
'folder-node'
it('should refresh on move node event', () => { ]);
contentService.nodesMoved.next(null);
expect(component.reload).toHaveBeenCalled();
});
it('should refresh on node deleted event', () => {
contentService.nodesDeleted.next(null);
expect(component.reload).toHaveBeenCalled();
});
it('should refresh on node restore event', () => {
contentService.nodesRestored.next(null);
expect(component.reload).toHaveBeenCalled();
});
}); });
describe('Node navigation', () => { it('navigates to `/personal-files` if node path has no `Sites`', () => {
beforeEach(() => { node.path.elements = [{ name: 'something else' }];
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node}));
spyOn(router, 'navigate');
fixture.detectChanges();
});
it('navigates to `/libraries` if node path has `Sites`', () => { component.navigate(node);
node.path.elements = [{ name: 'Sites' }];
component.navigate(node); expect(router.navigate).toHaveBeenCalledWith([
'/personal-files',
expect(router.navigate).toHaveBeenCalledWith([ '/libraries', 'folder-node' ]); 'folder-node'
}); ]);
it('navigates to `/personal-files` if node path has no `Sites`', () => {
node.path.elements = [{ name: 'something else' }];
component.navigate(node);
expect(router.navigate).toHaveBeenCalledWith([ '/personal-files', 'folder-node' ]);
});
it('does not navigate when node is not folder', () => {
node.isFolder = false;
component.navigate(node);
expect(router.navigate).not.toHaveBeenCalled();
});
}); });
describe('refresh', () => { it('does not navigate when node is not folder', () => {
it('should call document list reload', () => { node.isFolder = false;
spyOn(component.documentList, 'reload');
fixture.detectChanges();
component.reload(); component.navigate(node);
expect(component.documentList.reload).toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled();
});
}); });
});
describe('refresh', () => {
it('should call document list reload', () => {
spyOn(component.documentList, 'reload');
fixture.detectChanges();
component.reload();
expect(component.documentList.reload).toHaveBeenCalled();
});
});
}); });

View File

@@ -28,10 +28,10 @@ import { Router } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { import {
MinimalNodeEntity, MinimalNodeEntity,
MinimalNodeEntryEntity, MinimalNodeEntryEntity,
PathElementEntity, PathElementEntity,
PathInfo PathInfo
} from 'alfresco-js-api'; } from 'alfresco-js-api';
import { ContentManagementService } from '../../services/content-management.service'; import { ContentManagementService } from '../../services/content-management.service';
import { AppStore } from '../../store/states/app.state'; import { AppStore } from '../../store/states/app.state';
@@ -41,76 +41,71 @@ import { AppExtensionService } from '../../extensions/extension.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@Component({ @Component({
templateUrl: './favorites.component.html' templateUrl: './favorites.component.html'
}) })
export class FavoritesComponent extends PageComponent implements OnInit { export class FavoritesComponent extends PageComponent implements OnInit {
isSmallScreen = false; isSmallScreen = false;
constructor( constructor(
private router: Router, private router: Router,
store: Store<AppStore>, store: Store<AppStore>,
extensions: AppExtensionService, extensions: AppExtensionService,
private contentApi: ContentApiService, private contentApi: ContentApiService,
content: ContentManagementService, content: ContentManagementService,
private breakpointObserver: BreakpointObserver private breakpointObserver: BreakpointObserver
) { ) {
super(store, extensions, content); super(store, extensions, content);
}
ngOnInit() {
super.ngOnInit();
this.subscriptions = this.subscriptions.concat([
this.content.nodesDeleted.subscribe(() => this.reload()),
this.content.nodesRestored.subscribe(() => this.reload()),
this.content.folderEdited.subscribe(() => this.reload()),
this.content.nodesMoved.subscribe(() => this.reload()),
this.content.favoriteRemoved.subscribe(() => this.reload()),
this.content.favoriteToggle.subscribe(() => this.reload()),
this.breakpointObserver
.observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])
.subscribe(result => {
this.isSmallScreen = result.matches;
})
]);
}
navigate(favorite: MinimalNodeEntryEntity) {
const { isFolder, id } = favorite;
// TODO: rework as it will fail on non-English setups
const isSitePath = (path: PathInfo): boolean => {
return path.elements.some(
({ name }: PathElementEntity) => name === 'Sites'
);
};
if (isFolder) {
this.contentApi
.getNode(id)
.pipe(map(node => node.entry))
.subscribe(({ path }: MinimalNodeEntryEntity) => {
const routeUrl = isSitePath(path) ? '/libraries' : '/personal-files';
this.router.navigate([routeUrl, id]);
});
} }
}
ngOnInit() { onNodeDoubleClick(node: MinimalNodeEntity) {
super.ngOnInit(); if (node && node.entry) {
if (node.entry.isFolder) {
this.navigate(node.entry);
}
this.subscriptions = this.subscriptions.concat([ if (node.entry.isFile) {
this.content.nodesDeleted.subscribe(() => this.reload()), this.showPreview(node);
this.content.nodesRestored.subscribe(() => this.reload()), }
this.content.folderEdited.subscribe(() => this.reload()),
this.content.nodesMoved.subscribe(() => this.reload()),
this.content.favoriteRemoved.subscribe(() => this.reload()),
this.content.favoriteToggle.subscribe(() => this.reload()),
this.breakpointObserver
.observe([
Breakpoints.HandsetPortrait,
Breakpoints.HandsetLandscape
])
.subscribe(result => {
this.isSmallScreen = result.matches;
})
]);
}
navigate(favorite: MinimalNodeEntryEntity) {
const { isFolder, id } = favorite;
// TODO: rework as it will fail on non-English setups
const isSitePath = (path: PathInfo): boolean => {
return path.elements.some(
({ name }: PathElementEntity) => name === 'Sites'
);
};
if (isFolder) {
this.contentApi
.getNode(id)
.pipe(map(node => node.entry))
.subscribe(({ path }: MinimalNodeEntryEntity) => {
const routeUrl = isSitePath(path)
? '/libraries'
: '/personal-files';
this.router.navigate([routeUrl, id]);
});
}
}
onNodeDoubleClick(node: MinimalNodeEntity) {
if (node && node.entry) {
if (node.entry.isFolder) {
this.navigate(node.entry);
}
if (node.entry.isFile) {
this.showPreview(node);
}
}
} }
}
} }

View File

@@ -23,14 +23,22 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { TestBed, fakeAsync, tick, ComponentFixture } from '@angular/core/testing'; import {
TestBed,
fakeAsync,
tick,
ComponentFixture
} from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { import {
TimeAgoPipe, NodeNameTooltipPipe, FileSizePipe, NodeFavoriteDirective, TimeAgoPipe,
DataTableComponent, NodeNameTooltipPipe,
UploadService, FileSizePipe,
AppConfigPipe NodeFavoriteDirective,
DataTableComponent,
UploadService,
AppConfigPipe
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services'; import { DocumentListComponent } from '@alfresco/adf-content-services';
import { ContentManagementService } from '../../services/content-management.service'; import { ContentManagementService } from '../../services/content-management.service';
@@ -42,270 +50,278 @@ import { ExperimentalDirective } from '../../directives/experimental.directive';
import { of, throwError } from 'rxjs'; import { of, throwError } from 'rxjs';
describe('FilesComponent', () => { describe('FilesComponent', () => {
let node; let node;
let fixture: ComponentFixture<FilesComponent>; let fixture: ComponentFixture<FilesComponent>;
let component: FilesComponent; let component: FilesComponent;
let contentManagementService: ContentManagementService; let contentManagementService: ContentManagementService;
let uploadService: UploadService; let uploadService: UploadService;
let router: Router; let router: Router;
let nodeActionsService: NodeActionsService; let nodeActionsService: NodeActionsService;
let contentApi: ContentApiService; let contentApi: ContentApiService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [
FilesComponent,
DataTableComponent,
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DocumentListComponent,
FileSizePipe,
AppConfigPipe,
ExperimentalDirective
],
providers: [
{
provide: ActivatedRoute,
useValue: {
snapshot: { data: { preferencePrefix: 'prefix' } },
params: of({ folderId: 'someId' })
}
}
],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(FilesComponent);
component = fixture.componentInstance;
contentManagementService = TestBed.get(ContentManagementService);
uploadService = TestBed.get(UploadService);
router = TestBed.get(Router);
nodeActionsService = TestBed.get(NodeActionsService);
contentApi = TestBed.get(ContentApiService);
});
beforeEach(() => {
node = { id: 'node-id', isFolder: true };
spyOn(component.documentList, 'loadFolder').and.callFake(() => {});
});
describe('Current page is valid', () => {
it('should be a valid current page', fakeAsync(() => {
spyOn(contentApi, 'getNode').and.returnValue(throwError(null));
component.ngOnInit();
fixture.detectChanges();
tick();
expect(component.isValidPath).toBe(false);
}));
it('should set current page as invalid path', fakeAsync(() => {
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
component.ngOnInit();
tick();
fixture.detectChanges();
expect(component.isValidPath).toBe(true);
}));
});
describe('OnInit', () => {
it('should set current node', () => {
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
fixture.detectChanges();
expect(component.node).toBe(node);
});
it('if should navigate to parent if node is not a folder', () => {
node.isFolder = false;
node.parentId = 'parent-id';
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
spyOn(router, 'navigate');
fixture.detectChanges();
expect(router.navigate['calls'].argsFor(0)[0]).toEqual([
'/personal-files',
'parent-id'
]);
});
});
describe('refresh on events', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
imports: [ AppTestingModule ], spyOn(component.documentList, 'reload');
declarations: [
FilesComponent,
DataTableComponent,
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DocumentListComponent,
FileSizePipe,
AppConfigPipe,
ExperimentalDirective
],
providers: [
{ provide: ActivatedRoute, useValue: {
snapshot: { data: { preferencePrefix: 'prefix' } },
params: of({ folderId: 'someId' })
} }
],
schemas: [ NO_ERRORS_SCHEMA ]
});
fixture = TestBed.createComponent(FilesComponent); fixture.detectChanges();
component = fixture.componentInstance;
contentManagementService = TestBed.get(ContentManagementService);
uploadService = TestBed.get(UploadService);
router = TestBed.get(Router);
nodeActionsService = TestBed.get(NodeActionsService);
contentApi = TestBed.get(ContentApiService);
}); });
it('should call refresh onContentCopied event if parent is the same', () => {
const nodes = [
{ entry: { parentId: '1' } },
{ entry: { parentId: '2' } }
];
component.node = { id: '1' };
nodeActionsService.contentCopied.next(nodes);
expect(component.documentList.reload).toHaveBeenCalled();
});
it('should not call refresh onContentCopied event when parent mismatch', () => {
const nodes = [
{ entry: { parentId: '1' } },
{ entry: { parentId: '2' } }
];
component.node = { id: '3' };
nodeActionsService.contentCopied.next(nodes);
expect(component.documentList.reload).not.toHaveBeenCalled();
});
it('should call refresh onCreateFolder event', () => {
contentManagementService.folderCreated.next();
expect(component.documentList.reload).toHaveBeenCalled();
});
it('should call refresh editFolder event', () => {
contentManagementService.folderEdited.next();
expect(component.documentList.reload).toHaveBeenCalled();
});
it('should call refresh deleteNode event', () => {
contentManagementService.nodesDeleted.next();
expect(component.documentList.reload).toHaveBeenCalled();
});
it('should call refresh moveNode event', () => {
contentManagementService.nodesMoved.next();
expect(component.documentList.reload).toHaveBeenCalled();
});
it('should call refresh restoreNode event', () => {
contentManagementService.nodesRestored.next();
expect(component.documentList.reload).toHaveBeenCalled();
});
it('should call refresh on fileUploadComplete event if parent node match', fakeAsync(() => {
const file = { file: { options: { parentId: 'parentId' } } };
component.node = { id: 'parentId' };
uploadService.fileUploadComplete.next(<any>file);
tick(500);
expect(component.documentList.reload).toHaveBeenCalled();
}));
it('should not call refresh on fileUploadComplete event if parent mismatch', fakeAsync(() => {
const file = { file: { options: { parentId: 'otherId' } } };
component.node = { id: 'parentId' };
uploadService.fileUploadComplete.next(<any>file);
tick(500);
expect(component.documentList.reload).not.toHaveBeenCalled();
}));
it('should call refresh on fileUploadDeleted event if parent node match', fakeAsync(() => {
const file = { file: { options: { parentId: 'parentId' } } };
component.node = { id: 'parentId' };
uploadService.fileUploadDeleted.next(<any>file);
tick(500);
expect(component.documentList.reload).toHaveBeenCalled();
}));
it('should not call refresh on fileUploadDeleted event if parent mismatch', fakeAsync(() => {
const file: any = { file: { options: { parentId: 'otherId' } } };
component.node = { id: 'parentId' };
uploadService.fileUploadDeleted.next(file);
tick(500);
expect(component.documentList.reload).not.toHaveBeenCalled();
}));
});
describe('onBreadcrumbNavigate()', () => {
beforeEach(() => { beforeEach(() => {
node = { id: 'node-id', isFolder: true }; spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
spyOn(component.documentList, 'loadFolder').and.callFake(() => {}); fixture.detectChanges();
}); });
describe('Current page is valid', () => { it('should navigates to node id', () => {
it('should be a valid current page', fakeAsync(() => { const routeData: any = { id: 'some-where-over-the-rainbow' };
spyOn(contentApi, 'getNode').and.returnValue(throwError(null)); spyOn(component, 'navigate');
component.ngOnInit(); component.onBreadcrumbNavigate(routeData);
fixture.detectChanges();
tick();
expect(component.isValidPath).toBe(false); expect(component.navigate).toHaveBeenCalledWith(routeData.id);
})); });
});
it('should set current page as invalid path', fakeAsync(() => { describe('Node navigation', () => {
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node })); beforeEach(() => {
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
spyOn(router, 'navigate');
component.ngOnInit(); fixture.detectChanges();
tick();
fixture.detectChanges();
expect(component.isValidPath).toBe(true);
}));
}); });
describe('OnInit', () => { it('should navigates to node when id provided', () => {
it('should set current node', () => { component.navigate(node.id);
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
fixture.detectChanges();
expect(component.node).toBe(node);
});
it('if should navigate to parent if node is not a folder', () => { expect(router.navigate).toHaveBeenCalledWith(
node.isFolder = false; ['./', node.id],
node.parentId = 'parent-id'; jasmine.any(Object)
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node })); );
spyOn(router, 'navigate');
fixture.detectChanges();
expect(router.navigate['calls'].argsFor(0)[0]).toEqual(['/personal-files', 'parent-id']);
});
}); });
describe('refresh on events', () => { it('should navigates to home when id not provided', () => {
beforeEach(() => { component.navigate();
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
spyOn(component.documentList, 'reload');
fixture.detectChanges(); expect(router.navigate).toHaveBeenCalledWith(['./'], jasmine.any(Object));
});
it('should call refresh onContentCopied event if parent is the same', () => {
const nodes = [
{ entry: { parentId: '1' } },
{ entry: { parentId: '2' } }
];
component.node = { id: '1' };
nodeActionsService.contentCopied.next(nodes);
expect(component.documentList.reload).toHaveBeenCalled();
});
it('should not call refresh onContentCopied event when parent mismatch', () => {
const nodes = [
{ entry: { parentId: '1' } },
{ entry: { parentId: '2' } }
];
component.node = { id: '3' };
nodeActionsService.contentCopied.next(nodes);
expect(component.documentList.reload).not.toHaveBeenCalled();
});
it('should call refresh onCreateFolder event', () => {
contentManagementService.folderCreated.next();
expect(component.documentList.reload).toHaveBeenCalled();
});
it('should call refresh editFolder event', () => {
contentManagementService.folderEdited.next();
expect(component.documentList.reload).toHaveBeenCalled();
});
it('should call refresh deleteNode event', () => {
contentManagementService.nodesDeleted.next();
expect(component.documentList.reload).toHaveBeenCalled();
});
it('should call refresh moveNode event', () => {
contentManagementService.nodesMoved.next();
expect(component.documentList.reload).toHaveBeenCalled();
});
it('should call refresh restoreNode event', () => {
contentManagementService.nodesRestored.next();
expect(component.documentList.reload).toHaveBeenCalled();
});
it('should call refresh on fileUploadComplete event if parent node match', fakeAsync(() => {
const file = { file: { options: { parentId: 'parentId' } } };
component.node = { id: 'parentId' };
uploadService.fileUploadComplete.next(<any>file);
tick(500);
expect(component.documentList.reload).toHaveBeenCalled();
}));
it('should not call refresh on fileUploadComplete event if parent mismatch', fakeAsync(() => {
const file = { file: { options: { parentId: 'otherId' } } };
component.node = { id: 'parentId' };
uploadService.fileUploadComplete.next(<any>file);
tick(500);
expect(component.documentList.reload).not.toHaveBeenCalled();
}));
it('should call refresh on fileUploadDeleted event if parent node match', fakeAsync(() => {
const file = { file: { options: { parentId: 'parentId' } } };
component.node = { id: 'parentId' };
uploadService.fileUploadDeleted.next(<any>file);
tick(500);
expect(component.documentList.reload).toHaveBeenCalled();
}));
it('should not call refresh on fileUploadDeleted event if parent mismatch', fakeAsync(() => {
const file: any = { file: { options: { parentId: 'otherId' } } };
component.node = { id: 'parentId' };
uploadService.fileUploadDeleted.next(file);
tick(500);
expect(component.documentList.reload).not.toHaveBeenCalled();
}));
}); });
it('should navigate home if node is root', () => {
component.node = {
path: {
elements: [{ id: 'node-id' }]
}
};
describe('onBreadcrumbNavigate()', () => { component.navigate(node.id);
beforeEach(() => {
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
fixture.detectChanges();
});
it('should navigates to node id', () => { expect(router.navigate).toHaveBeenCalledWith(['./'], jasmine.any(Object));
const routeData: any = { id: 'some-where-over-the-rainbow' }; });
spyOn(component, 'navigate'); });
component.onBreadcrumbNavigate(routeData); describe('isSiteContainer', () => {
it('should return false if node has no aspectNames', () => {
const mock = { aspectNames: [] };
expect(component.navigate).toHaveBeenCalledWith(routeData.id); expect(component.isSiteContainer(mock)).toBe(false);
});
}); });
describe('Node navigation', () => { it('should return false if node is not site container', () => {
beforeEach(() => { const mock = { aspectNames: ['something-else'] };
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: node }));
spyOn(router, 'navigate');
fixture.detectChanges(); expect(component.isSiteContainer(mock)).toBe(false);
});
it('should navigates to node when id provided', () => {
component.navigate(node.id);
expect(router.navigate).toHaveBeenCalledWith(['./', node.id], jasmine.any(Object));
});
it('should navigates to home when id not provided', () => {
component.navigate();
expect(router.navigate).toHaveBeenCalledWith(['./'], jasmine.any(Object));
});
it('should navigate home if node is root', () => {
component.node = {
path: {
elements: [ {id: 'node-id'} ]
}
};
component.navigate(node.id);
expect(router.navigate).toHaveBeenCalledWith(['./'], jasmine.any(Object));
});
}); });
describe('isSiteContainer', () => { it('should return true if node is a site container', () => {
it('should return false if node has no aspectNames', () => { const mock = { aspectNames: ['st:siteContainer'] };
const mock = { aspectNames: [] };
expect(component.isSiteContainer(mock)).toBe(false); expect(component.isSiteContainer(mock)).toBe(true);
});
it('should return false if node is not site container', () => {
const mock = { aspectNames: ['something-else'] };
expect(component.isSiteContainer(mock)).toBe(false);
});
it('should return true if node is a site container', () => {
const mock = { aspectNames: [ 'st:siteContainer' ] };
expect(component.isSiteContainer(mock)).toBe(true);
});
}); });
});
}); });

View File

@@ -27,7 +27,12 @@ import { FileUploadEvent, UploadService } from '@alfresco/adf-core';
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router'; import { ActivatedRoute, Params, Router } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { MinimalNodeEntity, MinimalNodeEntryEntity, PathElement, PathElementEntity } from 'alfresco-js-api'; import {
MinimalNodeEntity,
MinimalNodeEntryEntity,
PathElement,
PathElementEntity
} from 'alfresco-js-api';
import { ContentManagementService } from '../../services/content-management.service'; import { ContentManagementService } from '../../services/content-management.service';
import { NodeActionsService } from '../../services/node-actions.service'; import { NodeActionsService } from '../../services/node-actions.service';
import { AppStore } from '../../store/states/app.state'; import { AppStore } from '../../store/states/app.state';
@@ -39,238 +44,256 @@ import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { debounceTime } from 'rxjs/operators'; import { debounceTime } from 'rxjs/operators';
@Component({ @Component({
templateUrl: './files.component.html' templateUrl: './files.component.html'
}) })
export class FilesComponent extends PageComponent implements OnInit, OnDestroy { export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
isValidPath = true;
isSmallScreen = false;
isValidPath = true; private nodePath: PathElement[];
isSmallScreen = false;
private nodePath: PathElement[]; constructor(
private router: Router,
private route: ActivatedRoute,
private contentApi: ContentApiService,
store: Store<AppStore>,
private nodeActionsService: NodeActionsService,
private uploadService: UploadService,
content: ContentManagementService,
extensions: AppExtensionService,
private breakpointObserver: BreakpointObserver
) {
super(store, extensions, content);
}
constructor(private router: Router, ngOnInit() {
private route: ActivatedRoute, super.ngOnInit();
private contentApi: ContentApiService,
store: Store<AppStore>,
private nodeActionsService: NodeActionsService,
private uploadService: UploadService,
content: ContentManagementService,
extensions: AppExtensionService,
private breakpointObserver: BreakpointObserver) {
super(store, extensions, content);
}
ngOnInit() { const { route, content, nodeActionsService, uploadService } = this;
super.ngOnInit(); const { data } = route.snapshot;
const { route, content, nodeActionsService, uploadService } = this; this.title = data.title;
const { data } = route.snapshot;
this.title = data.title; route.params.subscribe(({ folderId }: Params) => {
const nodeId = folderId || data.defaultNodeId;
route.params.subscribe(({ folderId }: Params) => { this.contentApi.getNode(nodeId).subscribe(
const nodeId = folderId || data.defaultNodeId; node => {
this.isValidPath = true;
this.contentApi if (node.entry && node.entry.isFolder) {
.getNode(nodeId) this.updateCurrentNode(node.entry);
.subscribe( } else {
node => { this.router.navigate(['/personal-files', node.entry.parentId], {
this.isValidPath = true; replaceUrl: true
if (node.entry && node.entry.isFolder) {
this.updateCurrentNode(node.entry);
} else {
this.router.navigate(
['/personal-files', node.entry.parentId],
{ replaceUrl: true }
);
}
},
() => this.isValidPath = false
);
});
this.subscriptions = this.subscriptions.concat([
nodeActionsService.contentCopied.subscribe((nodes) => this.onContentCopied(nodes)),
content.folderCreated.subscribe(() => this.documentList.reload()),
content.folderEdited.subscribe(() => this.documentList.reload()),
content.nodesDeleted.subscribe(() => this.documentList.reload()),
content.nodesMoved.subscribe(() => this.documentList.reload()),
content.nodesRestored.subscribe(() => this.documentList.reload()),
uploadService.fileUploadComplete.pipe(debounceTime(300)).subscribe(file => this.onFileUploadedEvent(file)),
uploadService.fileUploadDeleted.pipe(debounceTime(300)).subscribe((file) => this.onFileUploadedEvent(file)),
this.breakpointObserver
.observe([
Breakpoints.HandsetPortrait,
Breakpoints.HandsetLandscape
])
.subscribe(result => {
this.isSmallScreen = result.matches;
})
]);
}
ngOnDestroy() {
super.ngOnDestroy();
this.store.dispatch(new SetCurrentFolderAction(null));
}
navigate(nodeId: string = null) {
const commands = [ './' ];
if (nodeId && !this.isRootNode(nodeId)) {
commands.push(nodeId);
}
this.router.navigate(commands, {
relativeTo: this.route.parent
});
}
navigateTo(node: MinimalNodeEntity) {
if (node && node.entry) {
const { id, isFolder } = node.entry;
if (isFolder) {
this.navigate(id);
return;
}
if (PageComponent.isLockedNode(node.entry)) {
event.preventDefault();
return;
}
this.showPreview(node);
}
}
onBreadcrumbNavigate(route: PathElementEntity) {
// todo: review this approach once 5.2.3 is out
if (this.nodePath && this.nodePath.length > 2) {
if (this.nodePath[1].name === 'Sites' && this.nodePath[2].id === route.id) {
return this.navigate(this.nodePath[3].id);
}
}
this.navigate(route.id);
}
onFileUploadedEvent(event: FileUploadEvent) {
const node: MinimalNodeEntity = event.file.data;
// check root and child nodes
if (node && node.entry && node.entry.parentId === this.getParentNodeId()) {
this.documentList.reload();
return;
}
// check the child nodes to show dropped folder
if (event && event.file.options.parentId === this.getParentNodeId()) {
this.displayFolderParent(event.file.options.path, 0);
return;
}
if (event && event.file.options.parentId) {
if (this.nodePath) {
const correspondingNodePath = this.nodePath.find(pathItem => pathItem.id === event.file.options.parentId);
// check if the current folder has the 'trigger-upload-folder' as one of its parents
if (correspondingNodePath) {
const correspondingIndex = this.nodePath.length - this.nodePath.indexOf(correspondingNodePath);
this.displayFolderParent(event.file.options.path, correspondingIndex);
}
}
}
}
displayFolderParent(filePath = '', index) {
const parentName = filePath.split('/')[index];
const currentFoldersDisplayed: any = this.documentList.data.getRows() || [];
const alreadyDisplayedParentFolder = currentFoldersDisplayed.find(
row => row.node.entry.isFolder && row.node.entry.name === parentName);
if (alreadyDisplayedParentFolder) {
return;
}
this.documentList.reload();
}
onContentCopied(nodes: MinimalNodeEntity[]) {
const newNode = nodes
.find((node) => {
return node && node.entry && node.entry.parentId === this.getParentNodeId();
}); });
if (newNode) { }
this.documentList.reload(); },
} () => (this.isValidPath = false)
);
});
this.subscriptions = this.subscriptions.concat([
nodeActionsService.contentCopied.subscribe(nodes =>
this.onContentCopied(nodes)
),
content.folderCreated.subscribe(() => this.documentList.reload()),
content.folderEdited.subscribe(() => this.documentList.reload()),
content.nodesDeleted.subscribe(() => this.documentList.reload()),
content.nodesMoved.subscribe(() => this.documentList.reload()),
content.nodesRestored.subscribe(() => this.documentList.reload()),
uploadService.fileUploadComplete
.pipe(debounceTime(300))
.subscribe(file => this.onFileUploadedEvent(file)),
uploadService.fileUploadDeleted
.pipe(debounceTime(300))
.subscribe(file => this.onFileUploadedEvent(file)),
this.breakpointObserver
.observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])
.subscribe(result => {
this.isSmallScreen = result.matches;
})
]);
}
ngOnDestroy() {
super.ngOnDestroy();
this.store.dispatch(new SetCurrentFolderAction(null));
}
navigate(nodeId: string = null) {
const commands = ['./'];
if (nodeId && !this.isRootNode(nodeId)) {
commands.push(nodeId);
} }
this.router.navigate(commands, {
relativeTo: this.route.parent
});
}
navigateTo(node: MinimalNodeEntity) {
if (node && node.entry) {
const { id, isFolder } = node.entry;
if (isFolder) {
this.navigate(id);
return;
}
if (PageComponent.isLockedNode(node.entry)) {
event.preventDefault();
return;
}
this.showPreview(node);
}
}
onBreadcrumbNavigate(route: PathElementEntity) {
// todo: review this approach once 5.2.3 is out // todo: review this approach once 5.2.3 is out
private async updateCurrentNode(node: MinimalNodeEntryEntity) { if (this.nodePath && this.nodePath.length > 2) {
this.nodePath = null; if (
this.nodePath[1].name === 'Sites' &&
this.nodePath[2].id === route.id
) {
return this.navigate(this.nodePath[3].id);
}
}
this.navigate(route.id);
}
if (node && node.path && node.path.elements) { onFileUploadedEvent(event: FileUploadEvent) {
const elements = node.path.elements; const node: MinimalNodeEntity = event.file.data;
this.nodePath = elements.map(pathElement => { // check root and child nodes
return Object.assign({}, pathElement); if (node && node.entry && node.entry.parentId === this.getParentNodeId()) {
}); this.documentList.reload();
return;
if (elements.length > 1) {
if (elements[1].name === 'User Homes') {
elements.splice(0, 2);
} else if (elements[1].name === 'Sites') {
await this.normalizeSitePath(node);
}
}
}
this.node = node;
this.store.dispatch(new SetCurrentFolderAction(node));
} }
// todo: review this approach once 5.2.3 is out // check the child nodes to show dropped folder
private async normalizeSitePath(node: MinimalNodeEntryEntity) { if (event && event.file.options.parentId === this.getParentNodeId()) {
const elements = node.path.elements; this.displayFolderParent(event.file.options.path, 0);
return;
// remove 'Sites'
elements.splice(1, 1);
if (this.isSiteContainer(node)) {
// rename 'documentLibrary' entry to the target site display name
// clicking on the breadcrumb entry loads the site content
const parentNode = await this.contentApi.getNodeInfo(node.parentId).toPromise();
node.name = parentNode.properties['cm:title'] || parentNode.name;
// remove the site entry
elements.splice(1, 1);
} else {
// remove 'documentLibrary' in the middle of the path
const docLib = elements.findIndex(el => el.name === 'documentLibrary');
if (docLib > -1) {
const siteFragment = elements[docLib - 1];
const siteNode = await this.contentApi.getNodeInfo(siteFragment.id).toPromise();
// apply Site Name to the parent fragment
siteFragment.name = siteNode.properties['cm:title'] || siteNode.name;
elements.splice(docLib, 1);
}
}
} }
isSiteContainer(node: MinimalNodeEntryEntity): boolean { if (event && event.file.options.parentId) {
if (node && node.aspectNames && node.aspectNames.length > 0) { if (this.nodePath) {
return node.aspectNames.indexOf('st:siteContainer') >= 0; const correspondingNodePath = this.nodePath.find(
pathItem => pathItem.id === event.file.options.parentId
);
// check if the current folder has the 'trigger-upload-folder' as one of its parents
if (correspondingNodePath) {
const correspondingIndex =
this.nodePath.length - this.nodePath.indexOf(correspondingNodePath);
this.displayFolderParent(event.file.options.path, correspondingIndex);
} }
return false; }
}
}
displayFolderParent(filePath = '', index) {
const parentName = filePath.split('/')[index];
const currentFoldersDisplayed: any = this.documentList.data.getRows() || [];
const alreadyDisplayedParentFolder = currentFoldersDisplayed.find(
row => row.node.entry.isFolder && row.node.entry.name === parentName
);
if (alreadyDisplayedParentFolder) {
return;
}
this.documentList.reload();
}
onContentCopied(nodes: MinimalNodeEntity[]) {
const newNode = nodes.find(node => {
return (
node && node.entry && node.entry.parentId === this.getParentNodeId()
);
});
if (newNode) {
this.documentList.reload();
}
}
// todo: review this approach once 5.2.3 is out
private async updateCurrentNode(node: MinimalNodeEntryEntity) {
this.nodePath = null;
if (node && node.path && node.path.elements) {
const elements = node.path.elements;
this.nodePath = elements.map(pathElement => {
return Object.assign({}, pathElement);
});
if (elements.length > 1) {
if (elements[1].name === 'User Homes') {
elements.splice(0, 2);
} else if (elements[1].name === 'Sites') {
await this.normalizeSitePath(node);
}
}
} }
isRootNode(nodeId: string): boolean { this.node = node;
if (this.node && this.node.path && this.node.path.elements && this.node.path.elements.length > 0) { this.store.dispatch(new SetCurrentFolderAction(node));
return this.node.path.elements[0].id === nodeId; }
}
return false; // todo: review this approach once 5.2.3 is out
private async normalizeSitePath(node: MinimalNodeEntryEntity) {
const elements = node.path.elements;
// remove 'Sites'
elements.splice(1, 1);
if (this.isSiteContainer(node)) {
// rename 'documentLibrary' entry to the target site display name
// clicking on the breadcrumb entry loads the site content
const parentNode = await this.contentApi
.getNodeInfo(node.parentId)
.toPromise();
node.name = parentNode.properties['cm:title'] || parentNode.name;
// remove the site entry
elements.splice(1, 1);
} else {
// remove 'documentLibrary' in the middle of the path
const docLib = elements.findIndex(el => el.name === 'documentLibrary');
if (docLib > -1) {
const siteFragment = elements[docLib - 1];
const siteNode = await this.contentApi
.getNodeInfo(siteFragment.id)
.toPromise();
// apply Site Name to the parent fragment
siteFragment.name = siteNode.properties['cm:title'] || siteNode.name;
elements.splice(docLib, 1);
}
} }
}
isSiteContainer(node: MinimalNodeEntryEntity): boolean {
if (node && node.aspectNames && node.aspectNames.length > 0) {
return node.aspectNames.indexOf('st:siteContainer') >= 0;
}
return false;
}
isRootNode(nodeId: string): boolean {
if (
this.node &&
this.node.path &&
this.node.path.elements &&
this.node.path.elements.length > 0
) {
return this.node.path.elements[0].id === nodeId;
}
return false;
}
} }

View File

@@ -1,27 +1,27 @@
@mixin aca-generic-error-theme($theme) { @mixin aca-generic-error-theme($theme) {
$warn: map-get($theme, warn); $warn: map-get($theme, warn);
$foreground: map-get($theme, foreground); $foreground: map-get($theme, foreground);
.aca-generic-error { .aca-generic-error {
color: mat-color($foreground, text, 0.54); color: mat-color($foreground, text, 0.54);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
&__title { &__title {
font-size: 16px; font-size: 16px;
}
mat-icon {
color: mat-color($warn);
direction: rtl;
font-size: 52px;
height: 52px;
width: 52px;
}
} }
mat-icon {
color: mat-color($warn);
direction: rtl;
font-size: 52px;
height: 52px;
width: 52px;
}
}
} }

View File

@@ -23,14 +23,17 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Component, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core'; import {
Component,
ViewEncapsulation,
ChangeDetectionStrategy
} from '@angular/core';
@Component({ @Component({
selector: 'aca-generic-error', selector: 'aca-generic-error',
templateUrl: './generic-error.component.html', templateUrl: './generic-error.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'aca-generic-error' } host: { class: 'aca-generic-error' }
}) })
export class GenericErrorComponent {} export class GenericErrorComponent {}

View File

@@ -28,18 +28,18 @@ import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { NodePermissionService } from '../../../services/node-permission.service'; import { NodePermissionService } from '../../../services/node-permission.service';
@Component({ @Component({
selector: 'app-comments-tab', selector: 'app-comments-tab',
template: ` template: `
<adf-comments [readOnly]="!canUpdateNode" [nodeId]="node?.id"></adf-comments> <adf-comments [readOnly]="!canUpdateNode" [nodeId]="node?.id"></adf-comments>
` `
}) })
export class CommentsTabComponent { export class CommentsTabComponent {
@Input() @Input()
node: MinimalNodeEntryEntity; node: MinimalNodeEntryEntity;
constructor(private permission: NodePermissionService) {} constructor(private permission: NodePermissionService) {}
get canUpdateNode() { get canUpdateNode() {
return this.node && this.permission.check(this.node, ['update']); return this.node && this.permission.check(this.node, ['update']);
} }
} }

View File

@@ -30,71 +30,73 @@ import { AppExtensionService } from '../../extensions/extension.service';
import { SidebarTabRef } from '@alfresco/adf-extensions'; import { SidebarTabRef } from '@alfresco/adf-extensions';
@Component({ @Component({
selector: 'aca-info-drawer', selector: 'aca-info-drawer',
templateUrl: './info-drawer.component.html' templateUrl: './info-drawer.component.html'
}) })
export class InfoDrawerComponent implements OnChanges, OnInit { export class InfoDrawerComponent implements OnChanges, OnInit {
@Input() nodeId: string; @Input()
@Input() node: MinimalNodeEntity; nodeId: string;
@Input()
node: MinimalNodeEntity;
isLoading = false; isLoading = false;
displayNode: MinimalNodeEntryEntity; displayNode: MinimalNodeEntryEntity;
tabs: Array<SidebarTabRef> = []; tabs: Array<SidebarTabRef> = [];
constructor( constructor(
private contentApi: ContentApiService, private contentApi: ContentApiService,
private extensions: AppExtensionService private extensions: AppExtensionService
) {} ) {}
ngOnInit() { ngOnInit() {
this.tabs = this.extensions.getSidebarTabs(); this.tabs = this.extensions.getSidebarTabs();
} }
ngOnChanges() { ngOnChanges() {
if (this.node) { if (this.node) {
const entry = this.node.entry; const entry = this.node.entry;
if (entry.nodeId) { if (entry.nodeId) {
this.loadNodeInfo(entry.nodeId); this.loadNodeInfo(entry.nodeId);
} else if ((<any>entry).guid) { } else if ((<any>entry).guid) {
// workaround for Favorite files // workaround for Favorite files
this.loadNodeInfo(entry.id); this.loadNodeInfo(entry.id);
} else { } else {
// workaround Recent // workaround Recent
if (this.isTypeImage(entry) && !this.hasAspectNames(entry)) { if (this.isTypeImage(entry) && !this.hasAspectNames(entry)) {
this.loadNodeInfo(this.node.entry.id); this.loadNodeInfo(this.node.entry.id);
} else { } else {
this.setDisplayNode(this.node.entry); this.setDisplayNode(this.node.entry);
}
}
} }
}
} }
}
private hasAspectNames(entry: MinimalNodeEntryEntity): boolean { private hasAspectNames(entry: MinimalNodeEntryEntity): boolean {
return entry.aspectNames && entry.aspectNames.includes('exif:exif'); return entry.aspectNames && entry.aspectNames.includes('exif:exif');
}
private isTypeImage(entry: MinimalNodeEntryEntity): boolean {
if (entry && entry.content && entry.content.mimeType) {
return entry.content.mimeType.includes('image/');
} }
return false;
}
private isTypeImage(entry: MinimalNodeEntryEntity): boolean { private loadNodeInfo(nodeId: string) {
if (entry && entry.content && entry.content.mimeType) { if (nodeId) {
return entry.content.mimeType.includes('image/'); this.isLoading = true;
}
return false; this.contentApi.getNodeInfo(nodeId).subscribe(
entity => {
this.setDisplayNode(entity);
this.isLoading = false;
},
() => (this.isLoading = false)
);
} }
}
private loadNodeInfo(nodeId: string) { private setDisplayNode(node: MinimalNodeEntryEntity) {
if (nodeId) { this.displayNode = node;
this.isLoading = true; }
this.contentApi.getNodeInfo(nodeId).subscribe(
entity => {
this.setDisplayNode(entity);
this.isLoading = false;
},
() => this.isLoading = false
);
}
}
private setDisplayNode(node: MinimalNodeEntryEntity) {
this.displayNode = node;
}
} }

View File

@@ -24,8 +24,8 @@
*/ */
import { import {
ContentMetadataModule, ContentMetadataModule,
VersionManagerModule VersionManagerModule
} from '@alfresco/adf-content-services'; } from '@alfresco/adf-content-services';
import { CoreModule } from '@alfresco/adf-core'; import { CoreModule } from '@alfresco/adf-core';
import { ExtensionsModule } from '@alfresco/adf-extensions'; import { ExtensionsModule } from '@alfresco/adf-extensions';
@@ -39,26 +39,26 @@ import { MetadataTabComponent } from './metadata-tab/metadata-tab.component';
import { VersionsTabComponent } from './versions-tab/versions-tab.component'; import { VersionsTabComponent } from './versions-tab/versions-tab.component';
export function components() { export function components() {
return [ return [
InfoDrawerComponent, InfoDrawerComponent,
MetadataTabComponent, MetadataTabComponent,
CommentsTabComponent, CommentsTabComponent,
VersionsTabComponent VersionsTabComponent
]; ];
} }
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
MaterialModule, MaterialModule,
CoreModule.forChild(), CoreModule.forChild(),
ExtensionsModule.forChild(), ExtensionsModule.forChild(),
ContentMetadataModule, ContentMetadataModule,
VersionManagerModule, VersionManagerModule,
DirectivesModule DirectivesModule
], ],
declarations: [...components()], declarations: [...components()],
exports: [...components()], exports: [...components()],
entryComponents: [...components()] entryComponents: [...components()]
}) })
export class AppInfoDrawerModule {} export class AppInfoDrawerModule {}

View File

@@ -28,25 +28,25 @@ import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { NodePermissionService } from '../../../services/node-permission.service'; import { NodePermissionService } from '../../../services/node-permission.service';
@Component({ @Component({
selector: 'app-metadata-tab', selector: 'app-metadata-tab',
template: ` template: `
<adf-content-metadata-card <adf-content-metadata-card
[readOnly]="!canUpdateNode" [readOnly]="!canUpdateNode"
[displayEmpty]="canUpdateNode" [displayEmpty]="canUpdateNode"
[preset]="'custom'" [preset]="'custom'"
[node]="node"> [node]="node">
</adf-content-metadata-card> </adf-content-metadata-card>
`, `,
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { 'class': 'app-metadata-tab' } host: { class: 'app-metadata-tab' }
}) })
export class MetadataTabComponent { export class MetadataTabComponent {
@Input() @Input()
node: MinimalNodeEntryEntity; node: MinimalNodeEntryEntity;
constructor(private permission: NodePermissionService) {} constructor(private permission: NodePermissionService) {}
get canUpdateNode() { get canUpdateNode() {
return this.node && this.permission.check(this.node, ['update']); return this.node && this.permission.check(this.node, ['update']);
} }
} }

View File

@@ -27,8 +27,8 @@ import { Component, Input, OnChanges, OnInit } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api'; import { MinimalNodeEntryEntity } from 'alfresco-js-api';
@Component({ @Component({
selector: 'app-versions-tab', selector: 'app-versions-tab',
template: ` template: `
<ng-container *ngIf="isFileSelected;else empty"> <ng-container *ngIf="isFileSelected;else empty">
<adf-version-manager <adf-version-manager
[showComments]="'adf-version-manager.allowComments' | adfAppConfig:true" [showComments]="'adf-version-manager.allowComments' | adfAppConfig:true"
@@ -46,25 +46,25 @@ import { MinimalNodeEntryEntity } from 'alfresco-js-api';
` `
}) })
export class VersionsTabComponent implements OnInit, OnChanges { export class VersionsTabComponent implements OnInit, OnChanges {
@Input() @Input()
node: MinimalNodeEntryEntity; node: MinimalNodeEntryEntity;
isFileSelected = false; isFileSelected = false;
ngOnInit() { ngOnInit() {
this.updateState(); this.updateState();
} }
ngOnChanges() { ngOnChanges() {
this.updateState(); this.updateState();
} }
private updateState() { private updateState() {
if (this.node && this.node.nodeId) { if (this.node && this.node.nodeId) {
// workaround for shared files type. // workaround for shared files type.
this.isFileSelected = true; this.isFileSelected = true;
} else { } else {
this.isFileSelected = this.node.isFile; this.isFileSelected = this.node.isFile;
}
} }
}
} }

View File

@@ -1,16 +1,16 @@
:host { :host {
display: flex; display: flex;
flex: 1; flex: 1;
} }
@media screen and (max-width: 599px) { @media screen and (max-width: 599px) {
.adf-app-title { .adf-app-title {
display: none; display: none;
} }
} }
@media screen and (max-width: 719px) { @media screen and (max-width: 719px) {
.adf-app-logo { .adf-app-logo {
display: none; display: none;
} }
} }

View File

@@ -31,85 +31,82 @@ import { SidenavViewsManagerDirective } from './sidenav-views-manager.directive'
import { AppTestingModule } from '../../testing/app-testing.module'; import { AppTestingModule } from '../../testing/app-testing.module';
describe('LayoutComponent', () => { describe('LayoutComponent', () => {
let fixture: ComponentFixture<LayoutComponent>; let fixture: ComponentFixture<LayoutComponent>;
let component: LayoutComponent; let component: LayoutComponent;
let appConfig: AppConfigService; let appConfig: AppConfigService;
let userPreference: UserPreferencesService; let userPreference: UserPreferencesService;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ AppTestingModule ], imports: [AppTestingModule],
declarations: [ declarations: [LayoutComponent, SidenavViewsManagerDirective],
LayoutComponent, schemas: [NO_ERRORS_SCHEMA]
SidenavViewsManagerDirective
],
schemas: [ NO_ERRORS_SCHEMA ]
});
fixture = TestBed.createComponent(LayoutComponent);
component = fixture.componentInstance;
appConfig = TestBed.get(AppConfigService);
userPreference = TestBed.get(UserPreferencesService);
}); });
describe('sidenav state', () => { fixture = TestBed.createComponent(LayoutComponent);
it('should get state from configuration', () => { component = fixture.componentInstance;
appConfig.config = { appConfig = TestBed.get(AppConfigService);
sideNav: { userPreference = TestBed.get(UserPreferencesService);
expandedSidenav: false, });
preserveState: false
}
};
fixture.detectChanges(); describe('sidenav state', () => {
it('should get state from configuration', () => {
appConfig.config = {
sideNav: {
expandedSidenav: false,
preserveState: false
}
};
expect(component.expandedSidenav).toBe(false); fixture.detectChanges();
});
it('should resolve state to true is no configuration', () => { expect(component.expandedSidenav).toBe(false);
appConfig.config = {};
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';
}
});
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';
}
});
fixture.detectChanges();
expect(component.expandedSidenav).toBe(false);
});
}); });
});
it('should resolve state to true is no configuration', () => {
appConfig.config = {};
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';
}
});
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';
}
});
fixture.detectChanges();
expect(component.expandedSidenav).toBe(false);
});
});
});

View File

@@ -24,11 +24,11 @@
*/ */
import { import {
Component, Component,
OnInit, OnInit,
OnDestroy, OnDestroy,
ViewChild, ViewChild,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { MinimalNodeEntryEntity } from 'alfresco-js-api'; import { MinimalNodeEntryEntity } from 'alfresco-js-api';
@@ -37,76 +37,72 @@ import { SidenavViewsManagerDirective } from './sidenav-views-manager.directive'
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states'; import { AppStore } from '../../store/states';
import { import {
currentFolder, currentFolder,
selectAppName, selectAppName,
selectHeaderColor, selectHeaderColor,
selectLogoPath selectLogoPath
} from '../../store/selectors/app.selectors'; } from '../../store/selectors/app.selectors';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
@Component({ @Component({
selector: 'app-layout', selector: 'app-layout',
templateUrl: './layout.component.html', templateUrl: './layout.component.html',
styleUrls: ['./layout.component.scss'], styleUrls: ['./layout.component.scss'],
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { class: 'app-layout' } host: { class: 'app-layout' }
}) })
export class LayoutComponent implements OnInit, OnDestroy { export class LayoutComponent implements OnInit, OnDestroy {
@ViewChild(SidenavViewsManagerDirective) @ViewChild(SidenavViewsManagerDirective)
manager: SidenavViewsManagerDirective; manager: SidenavViewsManagerDirective;
onDestroy$: Subject<boolean> = new Subject<boolean>(); onDestroy$: Subject<boolean> = new Subject<boolean>();
expandedSidenav: boolean; expandedSidenav: boolean;
node: MinimalNodeEntryEntity; node: MinimalNodeEntryEntity;
canUpload = false; canUpload = false;
appName$: Observable<string>; appName$: Observable<string>;
headerColor$: Observable<string>; headerColor$: Observable<string>;
logo$: Observable<string>; logo$: Observable<string>;
isSmallScreen = false; isSmallScreen = false;
constructor( constructor(
protected store: Store<AppStore>, protected store: Store<AppStore>,
private permission: NodePermissionService, private permission: NodePermissionService,
private breakpointObserver: BreakpointObserver private breakpointObserver: BreakpointObserver
) { ) {
this.headerColor$ = store.select(selectHeaderColor); this.headerColor$ = store.select(selectHeaderColor);
this.appName$ = store.select(selectAppName); this.appName$ = store.select(selectAppName);
this.logo$ = store.select(selectLogoPath); this.logo$ = store.select(selectLogoPath);
}
ngOnInit() {
if (!this.manager.minimizeSidenav) {
this.expandedSidenav = this.manager.sidenavState;
} else {
this.expandedSidenav = false;
} }
ngOnInit() { this.manager.run(true);
if (!this.manager.minimizeSidenav) {
this.expandedSidenav = this.manager.sidenavState;
} else {
this.expandedSidenav = false;
}
this.manager.run(true); this.store
.select(currentFolder)
.pipe(takeUntil(this.onDestroy$))
.subscribe(node => {
this.node = node;
this.canUpload = node && this.permission.check(node, ['create']);
});
this.store this.breakpointObserver
.select(currentFolder) .observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])
.pipe(takeUntil(this.onDestroy$)) .subscribe(result => {
.subscribe(node => { this.isSmallScreen = result.matches;
this.node = node; });
this.canUpload = }
node && this.permission.check(node, ['create']);
});
this.breakpointObserver ngOnDestroy() {
.observe([ this.onDestroy$.next(true);
Breakpoints.HandsetPortrait, this.onDestroy$.complete();
Breakpoints.HandsetLandscape }
])
.subscribe(result => {
this.isSmallScreen = result.matches;
});
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
} }

View File

@@ -1,93 +1,92 @@
import { Directive, ContentChild } from '@angular/core'; import { Directive, ContentChild } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router'; import { Router, NavigationEnd } from '@angular/router';
import { import {
UserPreferencesService, UserPreferencesService,
AppConfigService, AppConfigService,
SidenavLayoutComponent SidenavLayoutComponent
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
@Directive({ @Directive({
selector: '[acaSidenavManager]', selector: '[acaSidenavManager]',
exportAs: 'acaSidenavManager' exportAs: 'acaSidenavManager'
}) })
export class SidenavViewsManagerDirective { export class SidenavViewsManagerDirective {
@ContentChild(SidenavLayoutComponent) sidenavLayout: SidenavLayoutComponent; @ContentChild(SidenavLayoutComponent)
sidenavLayout: SidenavLayoutComponent;
minimizeSidenav = false; minimizeSidenav = false;
hideSidenav = false; hideSidenav = false;
private _run = false; private _run = false;
private minimizeConditions: string[] = ['search']; private minimizeConditions: string[] = ['search'];
private hideConditions: string[] = ['preview']; private hideConditions: string[] = ['preview'];
constructor( constructor(
private router: Router, private router: Router,
private userPreferenceService: UserPreferencesService, private userPreferenceService: UserPreferencesService,
private appConfigService: AppConfigService private appConfigService: AppConfigService
) {
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: any) => {
this.minimizeSidenav = this.minimizeConditions.some(el =>
event.urlAfterRedirects.includes(el)
);
this.hideSidenav = this.hideConditions.some(el =>
event.urlAfterRedirects.includes(el)
);
if (this._run) {
this.manageSidenavState();
}
});
}
run(shouldRun) {
this._run = shouldRun;
}
manageSidenavState() {
if (this.minimizeSidenav && !this.sidenavLayout.isMenuMinimized) {
this.sidenavLayout.isMenuMinimized = true;
this.sidenavLayout.container.toggleMenu();
}
if (!this.minimizeSidenav) {
if (this.sidenavState && this.sidenavLayout.isMenuMinimized) {
this.sidenavLayout.isMenuMinimized = false;
this.sidenavLayout.container.toggleMenu();
}
}
}
setState(state) {
if (
!this.minimizeSidenav &&
this.appConfigService.get('sideNav.preserveState')
) { ) {
this.router.events this.userPreferenceService.set('expandedSidenav', state);
.pipe(filter(event => event instanceof NavigationEnd)) }
.subscribe((event: any) => { }
this.minimizeSidenav = this.minimizeConditions.some(el =>
event.urlAfterRedirects.includes(el)
);
this.hideSidenav = this.hideConditions.some(el =>
event.urlAfterRedirects.includes(el)
);
if (this._run) { get sidenavState(): boolean {
this.manageSidenavState(); 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'
);
} }
run(shouldRun) { return expand;
this._run = shouldRun; }
}
manageSidenavState() {
if (this.minimizeSidenav && !this.sidenavLayout.isMenuMinimized) {
this.sidenavLayout.isMenuMinimized = true;
this.sidenavLayout.container.toggleMenu();
}
if (!this.minimizeSidenav) {
if (this.sidenavState && this.sidenavLayout.isMenuMinimized) {
this.sidenavLayout.isMenuMinimized = false;
this.sidenavLayout.container.toggleMenu();
}
}
}
setState(state) {
if (
!this.minimizeSidenav &&
this.appConfigService.get('sideNav.preserveState')
) {
this.userPreferenceService.set('expandedSidenav', state);
}
}
get sidenavState(): 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

@@ -28,8 +28,12 @@ import { of } from 'rxjs';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { import {
AlfrescoApiService, AlfrescoApiService,
TimeAgoPipe, NodeNameTooltipPipe, NodeFavoriteDirective, DataTableComponent, AppConfigPipe TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DataTableComponent,
AppConfigPipe
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services'; import { DocumentListComponent } from '@alfresco/adf-content-services';
import { ShareDataTableAdapter } from '@alfresco/adf-content-services'; import { ShareDataTableAdapter } from '@alfresco/adf-content-services';
@@ -39,149 +43,157 @@ import { ContentApiService } from '../../services/content-api.service';
import { ExperimentalDirective } from '../../directives/experimental.directive'; import { ExperimentalDirective } from '../../directives/experimental.directive';
describe('LibrariesComponent', () => { describe('LibrariesComponent', () => {
let fixture: ComponentFixture<LibrariesComponent>; let fixture: ComponentFixture<LibrariesComponent>;
let component: LibrariesComponent; let component: LibrariesComponent;
let contentApi: ContentApiService; let contentApi: ContentApiService;
let alfrescoApi: AlfrescoApiService; let alfrescoApi: AlfrescoApiService;
let router: Router; let router: Router;
let page; let page;
let node; let node;
beforeEach(() => {
page = {
list: {
entries: [{ entry: { id: 1 } }, { entry: { id: 2 } }],
pagination: { data: 'data' }
}
};
node = <any>{
id: 'nodeId',
path: {
elements: []
}
};
});
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [
DataTableComponent,
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DocumentListComponent,
LibrariesComponent,
AppConfigPipe,
ExperimentalDirective
],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(LibrariesComponent);
component = fixture.componentInstance;
alfrescoApi = TestBed.get(AlfrescoApiService);
alfrescoApi.reset();
router = TestBed.get(Router);
spyOn(alfrescoApi.sitesApi, 'getSites').and.returnValue(
Promise.resolve(page)
);
spyOn(alfrescoApi.peopleApi, 'getSiteMembership').and.returnValue(
Promise.resolve({})
);
contentApi = TestBed.get(ContentApiService);
});
describe('makeLibraryTooltip()', () => {
it('maps tooltip to description', () => {
node.description = 'description';
const tooltip = component.makeLibraryTooltip(node);
expect(tooltip).toBe(node.description);
});
it('maps tooltip to description', () => {
node.title = 'title';
const tooltip = component.makeLibraryTooltip(node);
expect(tooltip).toBe(node.title);
});
it('sets tooltip to empty string', () => {
const tooltip = component.makeLibraryTooltip(node);
expect(tooltip).toBe('');
});
});
describe('makeLibraryTitle()', () => {
it('sets title with id when duplicate nodes title exists in list', () => {
node.title = 'title';
const data = new ShareDataTableAdapter(null, null);
data.setRows([
<any>{ node: { entry: { id: 'some-id', title: 'title' } } }
]);
component.documentList.data = data;
const title = component.makeLibraryTitle(node);
expect(title).toContain('nodeId');
});
it('sets title when no duplicate nodes title exists in list', () => {
node.title = 'title';
const data = new ShareDataTableAdapter(null, null);
data.setRows([
<any>{ node: { entry: { id: 'some-id', title: 'title-some-id' } } }
]);
component.documentList.data = data;
const title = component.makeLibraryTitle(node);
expect(title).toBe('title');
});
});
describe('Node navigation', () => {
let routerSpy;
beforeEach(() => { beforeEach(() => {
page = { routerSpy = spyOn(router, 'navigate');
list: {
entries: [ { entry: { id: 1 } }, { entry: { id: 2 } } ],
pagination: { data: 'data'}
}
};
node = <any> {
id: 'nodeId',
path: {
elements: []
}
};
}); });
beforeEach(() => { it('does not navigate when id is not passed', () => {
TestBed.configureTestingModule({ component.navigate(null);
imports: [ AppTestingModule ],
declarations: [
DataTableComponent,
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DocumentListComponent,
LibrariesComponent,
AppConfigPipe,
ExperimentalDirective
],
schemas: [ NO_ERRORS_SCHEMA ]
});
fixture = TestBed.createComponent(LibrariesComponent); expect(router.navigate).not.toHaveBeenCalled();
component = fixture.componentInstance;
alfrescoApi = TestBed.get(AlfrescoApiService);
alfrescoApi.reset();
router = TestBed.get(Router);
spyOn(alfrescoApi.sitesApi, 'getSites').and.returnValue((Promise.resolve(page)));
spyOn(alfrescoApi.peopleApi, 'getSiteMembership').and.returnValue((Promise.resolve({})));
contentApi = TestBed.get(ContentApiService);
}); });
describe('makeLibraryTooltip()', () => { it('navigates to node id', () => {
it('maps tooltip to description', () => { const document = { id: 'documentId' };
node.description = 'description'; spyOn(contentApi, 'getNode').and.returnValue(of({ entry: document }));
const tooltip = component.makeLibraryTooltip(node);
expect(tooltip).toBe(node.description); component.navigate(node.id);
});
it('maps tooltip to description', () => { expect(routerSpy.calls.argsFor(0)[0]).toEqual(['./', document.id]);
node.title = 'title'; });
const tooltip = component.makeLibraryTooltip(node); });
expect(tooltip).toBe(node.title); describe('navigateTo', () => {
}); it('navigates into library folder', () => {
spyOn(component, 'navigate');
it('sets tooltip to empty string', () => { const site: any = {
const tooltip = component.makeLibraryTooltip(node); entry: { guid: 'node-guid' }
};
expect(tooltip).toBe(''); component.navigateTo(site);
});
expect(component.navigate).toHaveBeenCalledWith('node-guid');
}); });
describe('makeLibraryTitle()', () => { it(' does not navigate when library is not provided', () => {
it('sets title with id when duplicate nodes title exists in list', () => { spyOn(component, 'navigate');
node.title = 'title';
const data = new ShareDataTableAdapter(null, null); component.navigateTo(null);
data.setRows([<any>{ node: { entry: { id: 'some-id', title: 'title' } } }]);
component.documentList.data = data; expect(component.navigate).not.toHaveBeenCalled();
const title = component.makeLibraryTitle(node);
expect(title).toContain('nodeId');
});
it('sets title when no duplicate nodes title exists in list', () => {
node.title = 'title';
const data = new ShareDataTableAdapter(null, null);
data.setRows([<any>{ node: { entry: { id: 'some-id', title: 'title-some-id' } } }]);
component.documentList.data = data;
const title = component.makeLibraryTitle(node);
expect(title).toBe('title');
});
});
describe('Node navigation', () => {
let routerSpy;
beforeEach(() => {
routerSpy = spyOn(router, 'navigate');
});
it('does not navigate when id is not passed', () => {
component.navigate(null);
expect(router.navigate).not.toHaveBeenCalled();
});
it('navigates to node id', () => {
const document = { id: 'documentId' };
spyOn(contentApi, 'getNode').and.returnValue(of({ entry: document }));
component.navigate(node.id);
expect(routerSpy.calls.argsFor(0)[0]).toEqual(['./', document.id]);
});
});
describe('navigateTo', () => {
it('navigates into library folder', () => {
spyOn(component, 'navigate');
const site: any = {
entry: { guid: 'node-guid' }
};
component.navigateTo(site);
expect(component.navigate).toHaveBeenCalledWith('node-guid');
});
it(' does not navigate when library is not provided', () => {
spyOn(component, 'navigate');
component.navigateTo(null);
expect(component.navigate).not.toHaveBeenCalled();
});
}); });
});
}); });

View File

@@ -38,79 +38,78 @@ import { AppExtensionService } from '../../extensions/extension.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@Component({ @Component({
templateUrl: './libraries.component.html' templateUrl: './libraries.component.html'
}) })
export class LibrariesComponent extends PageComponent implements OnInit { export class LibrariesComponent extends PageComponent implements OnInit {
isSmallScreen = false;
isSmallScreen = false; constructor(
private route: ActivatedRoute,
content: ContentManagementService,
private contentApi: ContentApiService,
store: Store<AppStore>,
extensions: AppExtensionService,
private router: Router,
private breakpointObserver: BreakpointObserver
) {
super(store, extensions, content);
}
constructor(private route: ActivatedRoute, ngOnInit() {
content: ContentManagementService, super.ngOnInit();
private contentApi: ContentApiService,
store: Store<AppStore>, this.subscriptions.push(
extensions: AppExtensionService, this.content.libraryDeleted.subscribe(() => this.reload()),
private router: Router, this.content.libraryCreated.subscribe((node: SiteEntry) => {
private breakpointObserver: BreakpointObserver) { this.navigate(node.entry.guid);
super(store, extensions, content); }),
this.breakpointObserver
.observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])
.subscribe(result => {
this.isSmallScreen = result.matches;
})
);
}
makeLibraryTooltip(library: any): string {
const { description, title } = library;
return description || title || '';
}
makeLibraryTitle(library: any): string {
const rows = this.documentList.data.getRows();
const entries = rows.map((r: ShareDataRow) => r.node.entry);
const { title, id } = library;
let isDuplicate = false;
if (entries) {
isDuplicate = entries.some((entry: any) => {
return entry.id !== id && entry.title === title;
});
} }
ngOnInit() { return isDuplicate ? `${title} (${id})` : `${title}`;
super.ngOnInit(); }
this.subscriptions.push( navigateTo(node: SiteEntry) {
this.content.libraryDeleted.subscribe(() => this.reload()), if (node && node.entry.guid) {
this.content.libraryCreated.subscribe((node: SiteEntry) => { this.navigate(node.entry.guid);
this.navigate(node.entry.guid);
}),
this.breakpointObserver
.observe([
Breakpoints.HandsetPortrait,
Breakpoints.HandsetLandscape
])
.subscribe(result => {
this.isSmallScreen = result.matches;
})
);
} }
}
makeLibraryTooltip(library: any): string { navigate(libraryId: string) {
const { description, title } = library; if (libraryId) {
this.contentApi
return description || title || ''; .getNode(libraryId, { relativePath: '/documentLibrary' })
} .pipe(map(node => node.entry))
.subscribe(documentLibrary => {
makeLibraryTitle(library: any): string { this.router.navigate(['./', documentLibrary.id], {
const rows = this.documentList.data.getRows(); relativeTo: this.route
const entries = rows.map((r: ShareDataRow) => r.node.entry); });
const { title, id } = library; });
let isDuplicate = false;
if (entries) {
isDuplicate = entries
.some((entry: any) => {
return (entry.id !== id && entry.title === title);
});
}
return isDuplicate ? `${title} (${id})` : `${title}`;
}
navigateTo(node: SiteEntry) {
if (node && node.entry.guid) {
this.navigate(node.entry.guid);
}
}
navigate(libraryId: string) {
if (libraryId) {
this.contentApi
.getNode(libraryId, { relativePath: '/documentLibrary' })
.pipe(map(node => node.entry))
.subscribe(documentLibrary => {
this.router.navigate([ './', documentLibrary.id ], { relativeTo: this.route });
});
}
} }
}
} }

View File

@@ -23,7 +23,14 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Component, Input, ChangeDetectionStrategy, OnInit, ViewEncapsulation, HostListener } from '@angular/core'; import {
Component,
Input,
ChangeDetectionStrategy,
OnInit,
ViewEncapsulation,
HostListener
} from '@angular/core';
import { PathInfo, MinimalNodeEntity } from 'alfresco-js-api'; import { PathInfo, MinimalNodeEntity } from 'alfresco-js-api';
import { Observable, BehaviorSubject, of } from 'rxjs'; import { Observable, BehaviorSubject, of } from 'rxjs';
@@ -33,140 +40,148 @@ import { NavigateToParentFolder } from '../../store/actions';
import { ContentApiService } from '../../services/content-api.service'; import { ContentApiService } from '../../services/content-api.service';
@Component({ @Component({
selector: 'aca-location-link', selector: 'aca-location-link',
template: ` template: `
<a href="" [title]="nodeLocation$ | async" (click)="goToLocation()"> <a href="" [title]="nodeLocation$ | async" (click)="goToLocation()">
{{ displayText | async }} {{ displayText | async }}
</a> </a>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { 'class': 'aca-location-link adf-location-cell' } host: { class: 'aca-location-link adf-location-cell' }
}) })
export class LocationLinkComponent implements OnInit { export class LocationLinkComponent implements OnInit {
private _path: PathInfo; private _path: PathInfo;
nodeLocation$ = new BehaviorSubject(null); nodeLocation$ = new BehaviorSubject(null);
@Input() @Input()
context: any; context: any;
@Input() @Input()
link: any[]; link: any[];
@Input() @Input()
displayText: Observable<string>; displayText: Observable<string>;
@Input() @Input()
tooltip: Observable<string>; tooltip: Observable<string>;
@HostListener('mouseenter') onMouseEnter() { @HostListener('mouseenter')
this.getTooltip(this._path); onMouseEnter() {
this.getTooltip(this._path);
}
constructor(
private store: Store<AppStore>,
private contentApi: ContentApiService
) {}
goToLocation() {
if (this.context) {
const node: MinimalNodeEntity = this.context.row.node;
this.store.dispatch(new NavigateToParentFolder(node));
} }
}
constructor( ngOnInit() {
private store: Store<AppStore>, if (this.context) {
private contentApi: ContentApiService) { const node: MinimalNodeEntity = this.context.row.node;
} if (node && node.entry && node.entry.path) {
const path = node.entry.path;
goToLocation() { if (path && path.name && path.elements) {
if (this.context) { this.displayText = this.getDisplayText(path);
const node: MinimalNodeEntity = this.context.row.node; this._path = path;
this.store.dispatch(new NavigateToParentFolder(node));
} }
}
}
}
// todo: review once 5.2.3 is out
private getDisplayText(path: PathInfo): Observable<string> {
const elements = path.elements.map(e => e.name);
// for admin users
if (elements.length === 1 && elements[0] === 'Company Home') {
return of('Personal Files');
} }
ngOnInit() { // for non-admin users
if (this.context) { if (
const node: MinimalNodeEntity = this.context.row.node; elements.length === 3 &&
if (node && node.entry && node.entry.path) { elements[0] === 'Company Home' &&
const path = node.entry.path; elements[1] === 'User Homes'
) {
return of('Personal Files');
}
if (path && path.name && path.elements) { const result = elements[elements.length - 1];
this.displayText = this.getDisplayText(path);
this._path = path; if (result === 'documentLibrary') {
} const fragment = path.elements[path.elements.length - 2];
return new Observable<string>(observer => {
this.contentApi.getNodeInfo(fragment.id).subscribe(
node => {
observer.next(
node.properties['cm:title'] || node.name || fragment.name
);
observer.complete();
},
() => {
observer.next(fragment.name);
observer.complete();
}
);
});
}
return of(result);
}
// todo: review once 5.2.3 is out
private getTooltip(path: PathInfo) {
let result: string = null;
const elements = path.elements.map(e => Object.assign({}, e));
if (elements[0].name === 'Company Home') {
elements[0].name = 'Personal Files';
if (elements.length > 2) {
if (elements[1].name === 'Sites') {
const fragment = elements[2];
this.contentApi.getNodeInfo(fragment.id).subscribe(
node => {
elements.splice(0, 2);
elements[0].name =
node.properties['cm:title'] || node.name || fragment.name;
elements.splice(1, 1);
elements.unshift({ id: null, name: 'File Libraries' });
result = elements.map(e => e.name).join('/');
this.nodeLocation$.next(result);
},
() => {
elements.splice(0, 2);
elements.unshift({ id: null, name: 'File Libraries' });
elements.splice(2, 1);
result = elements.map(e => e.name).join('/');
this.nodeLocation$.next(result);
} }
);
} }
if (elements[1].name === 'User Homes') {
elements.splice(0, 3);
elements.unshift({ id: null, name: 'Personal Files' });
}
}
} }
// todo: review once 5.2.3 is out result = elements.map(e => e.name).join('/');
private getDisplayText(path: PathInfo): Observable<string> { this.nodeLocation$.next(result);
const elements = path.elements.map(e => e.name); }
// for admin users
if (elements.length === 1 && elements[0] === 'Company Home') {
return of('Personal Files');
}
// for non-admin users
if (elements.length === 3 && elements[0] === 'Company Home' && elements[1] === 'User Homes') {
return of('Personal Files');
}
const result = elements[elements.length - 1];
if (result === 'documentLibrary') {
const fragment = path.elements[path.elements.length - 2];
return new Observable<string>(observer => {
this.contentApi.getNodeInfo(fragment.id).subscribe(
node => {
observer.next(node.properties['cm:title'] || node.name || fragment.name);
observer.complete();
},
() => {
observer.next(fragment.name);
observer.complete();
}
);
});
}
return of(result);
}
// todo: review once 5.2.3 is out
private getTooltip(path: PathInfo) {
let result: string = null;
const elements = path.elements.map(e => Object.assign({}, e));
if (elements[0].name === 'Company Home') {
elements[0].name = 'Personal Files';
if (elements.length > 2) {
if (elements[1].name === 'Sites') {
const fragment = elements[2];
this.contentApi.getNodeInfo(fragment.id).subscribe(
node => {
elements.splice(0, 2);
elements[0].name = node.properties['cm:title'] || node.name || fragment.name;
elements.splice(1, 1);
elements.unshift({ id: null, name: 'File Libraries' });
result = elements.map(e => e.name).join('/');
this.nodeLocation$.next(result);
},
() => {
elements.splice(0, 2);
elements.unshift({ id: null, name: 'File Libraries' });
elements.splice(2, 1);
result = elements.map(e => e.name).join('/');
this.nodeLocation$.next(result);
}
);
}
if (elements[1].name === 'User Homes') {
elements.splice(0, 3);
elements.unshift({ id: null, name: 'Personal Files'});
}
}
}
result = elements.map(e => e.name).join('/');
this.nodeLocation$.next(result);
}
} }

View File

@@ -27,58 +27,57 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { TestBed, ComponentFixture } from '@angular/core/testing'; import { TestBed, ComponentFixture } from '@angular/core/testing';
import { AuthenticationService, UserPreferencesService, AppConfigPipe } from '@alfresco/adf-core'; import {
AuthenticationService,
UserPreferencesService,
AppConfigPipe
} from '@alfresco/adf-core';
import { LoginComponent } from './login.component'; import { LoginComponent } from './login.component';
import { AppTestingModule } from '../../testing/app-testing.module'; import { AppTestingModule } from '../../testing/app-testing.module';
describe('LoginComponent', () => { describe('LoginComponent', () => {
let fixture: ComponentFixture<LoginComponent>; let fixture: ComponentFixture<LoginComponent>;
let router: Router; let router: Router;
let userPreference: UserPreferencesService; let userPreference: UserPreferencesService;
let auth: AuthenticationService; let auth: AuthenticationService;
let location: Location; let location: Location;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ AppTestingModule ], imports: [AppTestingModule],
declarations: [ declarations: [LoginComponent, AppConfigPipe],
LoginComponent, providers: [Location],
AppConfigPipe schemas: [NO_ERRORS_SCHEMA]
],
providers: [
Location
],
schemas: [ NO_ERRORS_SCHEMA ]
});
fixture = TestBed.createComponent(LoginComponent);
router = TestBed.get(Router);
spyOn(router, 'navigateByUrl');
location = TestBed.get(Location);
spyOn(location, 'forward');
auth = TestBed.get(AuthenticationService);
spyOn(auth, 'getRedirect').and.returnValue('/some-url');
userPreference = TestBed.get(UserPreferencesService);
spyOn(userPreference, 'setStoragePrefix');
}); });
describe('OnInit()', () => { fixture = TestBed.createComponent(LoginComponent);
it('should perform normal login when user is not logged in', () => {
spyOn(auth, 'isEcmLoggedIn').and.returnValue(false);
fixture.detectChanges();
expect(location.forward).not.toHaveBeenCalled(); router = TestBed.get(Router);
}); spyOn(router, 'navigateByUrl');
it('should redirect when user is logged in', () => { location = TestBed.get(Location);
spyOn(auth, 'isEcmLoggedIn').and.returnValue(true); spyOn(location, 'forward');
fixture.detectChanges();
expect(location.forward).toHaveBeenCalled(); auth = TestBed.get(AuthenticationService);
}); spyOn(auth, 'getRedirect').and.returnValue('/some-url');
userPreference = TestBed.get(UserPreferencesService);
spyOn(userPreference, 'setStoragePrefix');
});
describe('OnInit()', () => {
it('should perform normal login when user is not logged in', () => {
spyOn(auth, 'isEcmLoggedIn').and.returnValue(false);
fixture.detectChanges();
expect(location.forward).not.toHaveBeenCalled();
}); });
it('should redirect when user is logged in', () => {
spyOn(auth, 'isEcmLoggedIn').and.returnValue(true);
fixture.detectChanges();
expect(location.forward).toHaveBeenCalled();
});
});
}); });

View File

@@ -28,17 +28,17 @@ import { Location } from '@angular/common';
import { AuthenticationService } from '@alfresco/adf-core'; import { AuthenticationService } from '@alfresco/adf-core';
@Component({ @Component({
templateUrl: './login.component.html' templateUrl: './login.component.html'
}) })
export class LoginComponent implements OnInit { export class LoginComponent implements OnInit {
constructor( constructor(
private location: Location, private location: Location,
private auth: AuthenticationService, private auth: AuthenticationService
) {} ) {}
ngOnInit() { ngOnInit() {
if (this.auth.isEcmLoggedIn()) { if (this.auth.isEcmLoggedIn()) {
this.location.forward(); this.location.forward();
}
} }
}
} }

View File

@@ -26,31 +26,31 @@
import { PageComponent } from './page.component'; import { PageComponent } from './page.component';
class TestClass extends PageComponent { class TestClass extends PageComponent {
node: any; node: any;
constructor() { constructor() {
super(null, null, null); super(null, null, null);
} }
} }
describe('PageComponent', () => { describe('PageComponent', () => {
let component: TestClass; let component: TestClass;
beforeEach(() => { beforeEach(() => {
component = new TestClass(); component = new TestClass();
});
describe('getParentNodeId()', () => {
it('returns parent node id when node is set', () => {
component.node = { id: 'node-id' };
expect(component.getParentNodeId()).toBe('node-id');
}); });
describe('getParentNodeId()', () => { it('returns null when node is not set', () => {
it('returns parent node id when node is set', () => { component.node = null;
component.node = { id: 'node-id' };
expect(component.getParentNodeId()).toBe('node-id'); expect(component.getParentNodeId()).toBe(null);
});
it('returns null when node is not set', () => {
component.node = null;
expect(component.getParentNodeId()).toBe(null);
});
}); });
});
}); });

View File

@@ -23,7 +23,10 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { DocumentListComponent, ShareDataRow } from '@alfresco/adf-content-services'; import {
DocumentListComponent,
ShareDataRow
} from '@alfresco/adf-content-services';
import { ContentActionRef, SelectionState } from '@alfresco/adf-extensions'; import { ContentActionRef, SelectionState } from '@alfresco/adf-extensions';
import { OnDestroy, OnInit, ViewChild } from '@angular/core'; import { OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@@ -33,97 +36,109 @@ import { takeUntil } from 'rxjs/operators';
import { AppExtensionService } from '../extensions/extension.service'; import { AppExtensionService } from '../extensions/extension.service';
import { ContentManagementService } from '../services/content-management.service'; import { ContentManagementService } from '../services/content-management.service';
import { SetSelectedNodesAction, ViewFileAction } from '../store/actions'; import { SetSelectedNodesAction, ViewFileAction } from '../store/actions';
import { appSelection, currentFolder, documentDisplayMode, infoDrawerOpened, sharedUrl } from '../store/selectors/app.selectors'; import {
appSelection,
currentFolder,
documentDisplayMode,
infoDrawerOpened,
sharedUrl
} from '../store/selectors/app.selectors';
import { AppStore } from '../store/states/app.state'; import { AppStore } from '../store/states/app.state';
export abstract class PageComponent implements OnInit, OnDestroy { export abstract class PageComponent implements OnInit, OnDestroy {
onDestroy$: Subject<boolean> = new Subject<boolean>();
onDestroy$: Subject<boolean> = new Subject<boolean>(); @ViewChild(DocumentListComponent)
documentList: DocumentListComponent;
@ViewChild(DocumentListComponent) title = 'Page';
documentList: DocumentListComponent; infoDrawerOpened$: Observable<boolean>;
node: MinimalNodeEntryEntity;
selection: SelectionState;
documentDisplayMode$: Observable<string>;
sharedPreviewUrl$: Observable<string>;
actions: Array<ContentActionRef> = [];
viewerToolbarActions: Array<ContentActionRef> = [];
canUpdateNode = false;
canUpload = false;
title = 'Page'; protected subscriptions: Subscription[] = [];
infoDrawerOpened$: Observable<boolean>;
node: MinimalNodeEntryEntity;
selection: SelectionState;
documentDisplayMode$: Observable<string>;
sharedPreviewUrl$: Observable<string>;
actions: Array<ContentActionRef> = [];
viewerToolbarActions: Array<ContentActionRef> = [];
canUpdateNode = false;
canUpload = false;
protected subscriptions: Subscription[] = []; static isLockedNode(node) {
return (
node.isLocked ||
(node.properties && node.properties['cm:lockType'] === 'READ_ONLY_LOCK')
);
}
static isLockedNode(node) { constructor(
return node.isLocked || (node.properties && node.properties['cm:lockType'] === 'READ_ONLY_LOCK'); protected store: Store<AppStore>,
protected extensions: AppExtensionService,
protected content: ContentManagementService
) {}
ngOnInit() {
this.sharedPreviewUrl$ = this.store.select(sharedUrl);
this.infoDrawerOpened$ = this.store.select(infoDrawerOpened);
this.documentDisplayMode$ = this.store.select(documentDisplayMode);
this.store
.select(appSelection)
.pipe(takeUntil(this.onDestroy$))
.subscribe(selection => {
this.selection = selection;
this.actions = this.extensions.getAllowedToolbarActions();
this.viewerToolbarActions = this.extensions.getViewerToolbarActions();
this.canUpdateNode =
this.selection.count === 1 &&
this.content.canUpdateNode(selection.first);
});
this.store
.select(currentFolder)
.pipe(takeUntil(this.onDestroy$))
.subscribe(node => {
this.canUpload = node && this.content.canUploadContent(node);
});
}
ngOnDestroy() {
this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.subscriptions = [];
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
showPreview(node: MinimalNodeEntity) {
if (node && node.entry) {
const parentId = this.node ? this.node.id : null;
this.store.dispatch(new ViewFileAction(node, parentId));
} }
}
constructor( getParentNodeId(): string {
protected store: Store<AppStore>, return this.node ? this.node.id : null;
protected extensions: AppExtensionService, }
protected content: ContentManagementService) {}
ngOnInit() { imageResolver(row: ShareDataRow): string | null {
this.sharedPreviewUrl$ = this.store.select(sharedUrl); const entry: MinimalNodeEntryEntity = row.node.entry;
this.infoDrawerOpened$ = this.store.select(infoDrawerOpened);
this.documentDisplayMode$ = this.store.select(documentDisplayMode);
this.store if (PageComponent.isLockedNode(entry)) {
.select(appSelection) return 'assets/images/ic_lock_black_24dp_1x.png';
.pipe(takeUntil(this.onDestroy$))
.subscribe(selection => {
this.selection = selection;
this.actions = this.extensions.getAllowedToolbarActions();
this.viewerToolbarActions = this.extensions.getViewerToolbarActions();
this.canUpdateNode = this.selection.count === 1 && this.content.canUpdateNode(selection.first);
});
this.store.select(currentFolder)
.pipe(takeUntil(this.onDestroy$))
.subscribe(node => {
this.canUpload = node && this.content.canUploadContent(node);
});
} }
return null;
}
ngOnDestroy() { reload(): void {
this.subscriptions.forEach(subscription => subscription.unsubscribe()); if (this.documentList) {
this.subscriptions = []; this.documentList.resetSelection();
this.store.dispatch(new SetSelectedNodesAction([]));
this.onDestroy$.next(true); this.documentList.reload();
this.onDestroy$.complete();
} }
}
showPreview(node: MinimalNodeEntity) { trackByActionId(index: number, action: ContentActionRef) {
if (node && node.entry) { return action.id;
const parentId = this.node ? this.node.id : null; }
this.store.dispatch(new ViewFileAction(node, parentId));
}
}
getParentNodeId(): string {
return this.node ? this.node.id : null;
}
imageResolver(row: ShareDataRow): string | null {
const entry: MinimalNodeEntryEntity = row.node.entry;
if (PageComponent.isLockedNode(entry)) {
return 'assets/images/ic_lock_black_24dp_1x.png';
}
return null;
}
reload(): void {
if (this.documentList) {
this.documentList.resetSelection();
this.store.dispatch(new SetSelectedNodesAction([]));
this.documentList.reload();
}
}
trackByActionId(index: number, action: ContentActionRef) {
return action.id;
}
} }

View File

@@ -1,49 +1,49 @@
@mixin aca-permissions-manager-theme($theme) { @mixin aca-permissions-manager-theme($theme) {
$foreground: map-get($theme, foreground); $foreground: map-get($theme, foreground);
$accent: map-get($theme, accent); $accent: map-get($theme, accent);
aca-permissions-dialog-panel { aca-permissions-dialog-panel {
height: 400px; height: 400px;
}
.aca-node-permissions-dialog {
.mat-dialog-title {
font-size: 20px;
font-weight: 600;
font-style: normal;
font-stretch: normal;
line-height: 1.6;
margin: 0;
letter-spacing: -0.5px;
color: mat-color($foreground, text, 0.87);
} }
.aca-node-permissions-dialog { .mat-dialog-content {
.mat-dialog-title { flex: 1 1 auto;
font-size: 20px; position: relative;
font-weight: 600; overflow: auto;
font-style: normal;
font-stretch: normal;
line-height: 1.6;
margin: 0;
letter-spacing: -0.5px;
color: mat-color($foreground, text, 0.87);
}
.mat-dialog-content { adf-permission-list {
flex: 1 1 auto; display: flex;
position: relative; }
overflow: auto;
adf-permission-list {
display: flex;
}
}
.mat-dialog-actions {
flex: 0 0 auto;
padding: 8px 8px 24px 8px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
color: mat-color($foreground, text, 0.54);
button {
text-transform: uppercase;
font-weight: normal;
}
}
} }
.mat-dialog-actions {
flex: 0 0 auto;
padding: 8px 8px 24px 8px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
color: mat-color($foreground, text, 0.54);
button {
text-transform: uppercase;
font-weight: normal;
}
}
}
} }

View File

@@ -24,7 +24,10 @@
*/ */
import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { NodePermissionDialogService, PermissionListComponent } from '@alfresco/adf-content-services'; import {
NodePermissionDialogService,
PermissionListComponent
} from '@alfresco/adf-content-services';
import { MinimalNodeEntryEntity } from 'alfresco-js-api'; import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states/app.state'; import { AppStore } from '../../store/states/app.state';
@@ -34,58 +37,60 @@ import { MatDialog } from '@angular/material';
import { ContentApiService } from '../../services/content-api.service'; import { ContentApiService } from '../../services/content-api.service';
@Component({ @Component({
selector: 'aca-permissions-manager', selector: 'aca-permissions-manager',
templateUrl: './permissions-manager.component.html' templateUrl: './permissions-manager.component.html'
}) })
export class PermissionsManagerComponent implements OnInit { export class PermissionsManagerComponent implements OnInit {
@ViewChild('permissionList') @ViewChild('permissionList')
permissionList: PermissionListComponent; permissionList: PermissionListComponent;
@Input() @Input()
nodeId: string; nodeId: string;
toggleStatus = false; toggleStatus = false;
constructor( constructor(
private store: Store<AppStore>, private store: Store<AppStore>,
private dialog: MatDialog, private dialog: MatDialog,
private contentApi: ContentApiService, private contentApi: ContentApiService,
private nodePermissionDialogService: NodePermissionDialogService private nodePermissionDialogService: NodePermissionDialogService
) { ) {}
}
ngOnInit() { ngOnInit() {
this.contentApi.getNodeInfo(this.nodeId, {include: ['permissions'] }).subscribe( (currentNode: MinimalNodeEntryEntity) => { this.contentApi
this.toggleStatus = currentNode.permissions.isInheritanceEnabled; .getNodeInfo(this.nodeId, { include: ['permissions'] })
}); .subscribe((currentNode: MinimalNodeEntryEntity) => {
} this.toggleStatus = currentNode.permissions.isInheritanceEnabled;
});
}
onError(errorMessage: string) { onError(errorMessage: string) {
this.store.dispatch(new SnackbarErrorAction(errorMessage)); this.store.dispatch(new SnackbarErrorAction(errorMessage));
} }
onUpdate(event) { onUpdate(event) {
this.permissionList.reload(); this.permissionList.reload();
} }
onUpdatedPermissions(node: MinimalNodeEntryEntity) { onUpdatedPermissions(node: MinimalNodeEntryEntity) {
this.toggleStatus = node.permissions.isInheritanceEnabled; this.toggleStatus = node.permissions.isInheritanceEnabled;
this.permissionList.reload(); this.permissionList.reload();
} }
openAddPermissionDialog(event: Event) { openAddPermissionDialog(event: Event) {
this.nodePermissionDialogService.updateNodePermissionByDialog(this.nodeId) this.nodePermissionDialogService
.subscribe(() => { .updateNodePermissionByDialog(this.nodeId)
this.dialog.open(NodePermissionsDialogComponent, { .subscribe(
data: { nodeId: this.nodeId }, () => {
panelClass: 'aca-permissions-dialog-panel', this.dialog.open(NodePermissionsDialogComponent, {
width: '800px' data: { nodeId: this.nodeId },
} panelClass: 'aca-permissions-dialog-panel',
); width: '800px'
}, });
(error) => { },
this.store.dispatch(new SnackbarErrorAction(error)); error => {
} this.store.dispatch(new SnackbarErrorAction(error));
); }
} );
}
} }

View File

@@ -24,82 +24,82 @@
*/ */
import { import {
Component, Component,
Input, Input,
ComponentRef, ComponentRef,
OnInit, OnInit,
ComponentFactoryResolver, ComponentFactoryResolver,
ViewChild, ViewChild,
ViewContainerRef, ViewContainerRef,
OnDestroy, OnDestroy,
OnChanges OnChanges
} from '@angular/core'; } from '@angular/core';
import { AppExtensionService } from '../../extensions/extension.service'; import { AppExtensionService } from '../../extensions/extension.service';
import { MinimalNodeEntryEntity } from 'alfresco-js-api'; import { MinimalNodeEntryEntity } from 'alfresco-js-api';
@Component({ @Component({
selector: 'app-preview-extension', selector: 'app-preview-extension',
template: `<div #content></div>` template: `<div #content></div>`
}) })
export class PreviewExtensionComponent implements OnInit, OnChanges, OnDestroy { export class PreviewExtensionComponent implements OnInit, OnChanges, OnDestroy {
@ViewChild('content', { read: ViewContainerRef }) @ViewChild('content', { read: ViewContainerRef })
content: ViewContainerRef; content: ViewContainerRef;
@Input() @Input()
id: string; id: string;
@Input() @Input()
url: string; url: string;
@Input() @Input()
extension: string; extension: string;
@Input() @Input()
node: MinimalNodeEntryEntity; node: MinimalNodeEntryEntity;
private componentRef: ComponentRef<any>; private componentRef: ComponentRef<any>;
constructor( constructor(
private extensions: AppExtensionService, private extensions: AppExtensionService,
private componentFactoryResolver: ComponentFactoryResolver private componentFactoryResolver: ComponentFactoryResolver
) {} ) {}
ngOnInit() { ngOnInit() {
if (!this.id) { if (!this.id) {
return; return;
}
const componentType = this.extensions.getComponentById(this.id);
if (componentType) {
const factory = this.componentFactoryResolver.resolveComponentFactory(
componentType
);
if (factory) {
this.content.clear();
this.componentRef = this.content.createComponent(factory, 0);
this.updateInstance();
}
}
} }
ngOnChanges() { const componentType = this.extensions.getComponentById(this.id);
if (componentType) {
const factory = this.componentFactoryResolver.resolveComponentFactory(
componentType
);
if (factory) {
this.content.clear();
this.componentRef = this.content.createComponent(factory, 0);
this.updateInstance(); this.updateInstance();
}
} }
}
ngOnDestroy() { ngOnChanges() {
if (this.componentRef) { this.updateInstance();
this.componentRef.destroy(); }
this.componentRef = null;
} ngOnDestroy() {
if (this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
} }
}
private updateInstance() { private updateInstance() {
if (this.componentRef && this.componentRef.instance) { if (this.componentRef && this.componentRef.instance) {
const instance = this.componentRef.instance; const instance = this.componentRef.instance;
instance.node = this.node; instance.node = this.node;
instance.url = this.url; instance.url = this.url;
instance.extension = this.extension; instance.extension = this.extension;
}
} }
}
} }

View File

@@ -1,4 +1,4 @@
.app-preview { .app-preview {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,14 @@
*/ */
import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute, Router, UrlTree, UrlSegmentGroup, UrlSegment, PRIMARY_OUTLET } from '@angular/router'; import {
ActivatedRoute,
Router,
UrlTree,
UrlSegmentGroup,
UrlSegment,
PRIMARY_OUTLET
} from '@angular/router';
import { UserPreferencesService, ObjectUtils } from '@alfresco/adf-core'; import { UserPreferencesService, ObjectUtils } from '@alfresco/adf-core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states/app.state'; import { AppStore } from '../../store/states/app.state';
@@ -37,324 +44,358 @@ import { ContentActionRef, ViewerExtensionRef } from '@alfresco/adf-extensions';
import { ViewUtilService } from './view-util.service'; import { ViewUtilService } from './view-util.service';
@Component({ @Component({
selector: 'app-preview', selector: 'app-preview',
templateUrl: 'preview.component.html', templateUrl: 'preview.component.html',
styleUrls: ['preview.component.scss'], styleUrls: ['preview.component.scss'],
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { 'class': 'app-preview' } host: { class: 'app-preview' }
}) })
export class PreviewComponent extends PageComponent implements OnInit { export class PreviewComponent extends PageComponent implements OnInit {
previewLocation: string = null;
routesSkipNavigation = ['shared', 'recent-files', 'favorites'];
navigateSource: string = null;
navigationSources = [
'favorites',
'libraries',
'personal-files',
'recent-files',
'shared'
];
folderId: string = null;
nodeId: string = null;
previousNodeId: string;
nextNodeId: string;
navigateMultiple = false;
openWith: Array<ContentActionRef> = [];
contentExtensions: Array<ViewerExtensionRef> = [];
previewLocation: string = null; constructor(
routesSkipNavigation = [ 'shared', 'recent-files', 'favorites' ]; private contentApi: ContentApiService,
navigateSource: string = null; private preferences: UserPreferencesService,
navigationSources = ['favorites', 'libraries', 'personal-files', 'recent-files', 'shared']; private route: ActivatedRoute,
folderId: string = null; private router: Router,
nodeId: string = null; private viewUtils: ViewUtilService,
previousNodeId: string; store: Store<AppStore>,
nextNodeId: string; extensions: AppExtensionService,
navigateMultiple = false; content: ContentManagementService
openWith: Array<ContentActionRef> = []; ) {
contentExtensions: Array<ViewerExtensionRef> = []; super(store, extensions, content);
}
constructor( ngOnInit() {
private contentApi: ContentApiService, super.ngOnInit();
private preferences: UserPreferencesService,
private route: ActivatedRoute, this.previewLocation = this.router.url
private router: Router, .substr(0, this.router.url.indexOf('/', 1))
private viewUtils: ViewUtilService, .replace(/\//g, '');
store: Store<AppStore>,
extensions: AppExtensionService, const routeData = this.route.snapshot.data;
content: ContentManagementService) {
super(store, extensions, content); if (routeData.navigateMultiple) {
this.navigateMultiple = true;
} }
ngOnInit() { if (routeData.navigateSource) {
super.ngOnInit(); const source = routeData.navigateSource.toLowerCase();
if (this.navigationSources.includes(source)) {
this.previewLocation = this.router.url this.navigateSource = routeData.navigateSource;
.substr(0, this.router.url.indexOf('/', 1)) }
.replace(/\//g, '');
const routeData = this.route.snapshot.data;
if (routeData.navigateMultiple) {
this.navigateMultiple = true;
}
if (routeData.navigateSource) {
const source = routeData.navigateSource.toLowerCase();
if (this.navigationSources.includes(source)) {
this.navigateSource = routeData.navigateSource;
}
}
this.route.params.subscribe(params => {
this.folderId = params.folderId;
const id = params.nodeId;
if (id) {
this.displayNode(id);
}
});
this.openWith = this.extensions.openWithActions;
this.contentExtensions = this.extensions.viewerContentExtensions;
} }
/** this.route.params.subscribe(params => {
* Loads the particular node into the Viewer this.folderId = params.folderId;
* @param id Unique identifier for the Node to display const id = params.nodeId;
*/ if (id) {
async displayNode(id: string) { this.displayNode(id);
if (id) { }
try { });
this.node = await this.contentApi.getNodeInfo(id).toPromise();
this.store.dispatch(new SetSelectedNodesAction([{ entry: this.node }]));
if (this.node && this.node.isFile) { this.openWith = this.extensions.openWithActions;
const nearest = await this.getNearestNodes(this.node.id, this.node.parentId); this.contentExtensions = this.extensions.viewerContentExtensions;
}
this.previousNodeId = nearest.left; /**
this.nextNodeId = nearest.right; * Loads the particular node into the Viewer
this.nodeId = this.node.id; * @param id Unique identifier for the Node to display
return; */
} async displayNode(id: string) {
this.router.navigate([this.previewLocation, id]); if (id) {
} catch (err) { try {
if (!err || err.status !== 401) { this.node = await this.contentApi.getNodeInfo(id).toPromise();
this.router.navigate([this.previewLocation, id]); this.store.dispatch(new SetSelectedNodesAction([{ entry: this.node }]));
}
} if (this.node && this.node.isFile) {
const nearest = await this.getNearestNodes(
this.node.id,
this.node.parentId
);
this.previousNodeId = nearest.left;
this.nextNodeId = nearest.right;
this.nodeId = this.node.id;
return;
} }
this.router.navigate([this.previewLocation, id]);
} catch (err) {
if (!err || err.status !== 401) {
this.router.navigate([this.previewLocation, id]);
}
}
}
}
/**
* Handles the visibility change of the Viewer component.
* @param isVisible Indicator whether Viewer is visible or hidden.
*/
onVisibilityChanged(isVisible: boolean): void {
const shouldSkipNavigation = this.routesSkipNavigation.includes(
this.previewLocation
);
if (!isVisible) {
const route = this.getNavigationCommands(this.previewLocation);
if (!shouldSkipNavigation && this.folderId) {
route.push(this.folderId);
}
this.router.navigate(route);
}
}
/** Handles navigation to a previous document */
onNavigateBefore(): void {
if (this.previousNodeId) {
this.router.navigate(
this.getPreviewPath(this.folderId, this.previousNodeId)
);
}
}
/** Handles navigation to a next document */
onNavigateNext(): void {
if (this.nextNodeId) {
this.router.navigate(this.getPreviewPath(this.folderId, this.nextNodeId));
}
}
/**
* Generates a node preview route based on folder and node IDs.
* @param folderId Folder ID
* @param nodeId Node ID
*/
getPreviewPath(folderId: string, nodeId: string): any[] {
const route = [this.previewLocation];
if (folderId) {
route.push(folderId);
} }
/** if (nodeId) {
* Handles the visibility change of the Viewer component. route.push('preview', nodeId);
* @param isVisible Indicator whether Viewer is visible or hidden.
*/
onVisibilityChanged(isVisible: boolean): void {
const shouldSkipNavigation = this.routesSkipNavigation.includes(this.previewLocation);
if (!isVisible) {
const route = this.getNavigationCommands(this.previewLocation);
if ( !shouldSkipNavigation && this.folderId ) {
route.push(this.folderId);
}
this.router.navigate(route);
}
} }
/** Handles navigation to a previous document */ return route;
onNavigateBefore(): void { }
if (this.previousNodeId) {
this.router.navigate(
this.getPreviewPath(this.folderId, this.previousNodeId)
);
}
}
/** Handles navigation to a next document */ /**
onNavigateNext(): void { * Retrieves nearest node information for the given node and folder.
if (this.nextNodeId) { * @param nodeId Unique identifier of the document node
this.router.navigate( * @param folderId Unique identifier of the containing folder node.
this.getPreviewPath(this.folderId, this.nextNodeId) */
); async getNearestNodes(
} nodeId: string,
} folderId: string
): Promise<{ left: string; right: string }> {
const empty = {
left: null,
right: null
};
/** if (nodeId && folderId) {
* Generates a node preview route based on folder and node IDs. try {
* @param folderId Folder ID const ids = await this.getFileIds(this.navigateSource, folderId);
* @param nodeId Node ID const idx = ids.indexOf(nodeId);
*/
getPreviewPath(folderId: string, nodeId: string): any[] {
const route = [this.previewLocation];
if (folderId) { if (idx >= 0) {
route.push(folderId); return {
} left: ids[idx - 1] || null,
right: ids[idx + 1] || null
if (nodeId) { };
route.push('preview', nodeId);
}
return route;
}
/**
* Retrieves nearest node information for the given node and folder.
* @param nodeId Unique identifier of the document node
* @param folderId Unique identifier of the containing folder node.
*/
async getNearestNodes(nodeId: string, folderId: string): Promise<{ left: string, right: string }> {
const empty = {
left: null,
right: null
};
if (nodeId && folderId) {
try {
const ids = await this.getFileIds(this.navigateSource, folderId);
const idx = ids.indexOf(nodeId);
if (idx >= 0) {
return {
left: ids[idx - 1] || null,
right: ids[idx + 1] || null
};
} else {
return empty;
}
} catch {
return empty;
}
} else { } else {
return empty; return empty;
} }
} catch {
return empty;
}
} else {
return empty;
}
}
/**
* Retrieves a list of node identifiers for the folder and data source.
* @param source Data source name. Allowed values are: personal-files, libraries, favorites, shared, recent-files.
* @param folderId Containing folder node identifier for 'personal-files' and 'libraries' sources.
*/
async getFileIds(source: string, folderId?: string): Promise<string[]> {
if ((source === 'personal-files' || source === 'libraries') && folderId) {
const sortKey =
this.preferences.get('personal-files.sorting.key') || 'modifiedAt';
const sortDirection =
this.preferences.get('personal-files.sorting.direction') || 'desc';
const nodes = await this.contentApi
.getNodeChildren(folderId, {
// orderBy: `${sortKey} ${sortDirection}`,
fields: ['id', this.getRootField(sortKey)],
where: '(isFile=true)'
})
.toPromise();
const entries = nodes.list.entries.map(obj => obj.entry);
this.sort(entries, sortKey, sortDirection);
return entries.map(obj => obj.id);
} }
/** if (source === 'favorites') {
* Retrieves a list of node identifiers for the folder and data source. const nodes = await this.contentApi
* @param source Data source name. Allowed values are: personal-files, libraries, favorites, shared, recent-files. .getFavorites('-me-', {
* @param folderId Containing folder node identifier for 'personal-files' and 'libraries' sources. where: '(EXISTS(target/file))',
*/ fields: ['target']
async getFileIds(source: string, folderId?: string): Promise<string[]> { })
if ((source === 'personal-files' || source === 'libraries') && folderId) { .toPromise();
const sortKey = this.preferences.get('personal-files.sorting.key') || 'modifiedAt';
const sortDirection = this.preferences.get('personal-files.sorting.direction') || 'desc';
const nodes = await this.contentApi.getNodeChildren(folderId, {
// orderBy: `${sortKey} ${sortDirection}`,
fields: ['id', this.getRootField(sortKey)],
where: '(isFile=true)'
}).toPromise();
const entries = nodes.list.entries.map(obj => obj.entry); const sortKey =
this.sort(entries, sortKey, sortDirection); this.preferences.get('favorites.sorting.key') || 'modifiedAt';
const sortDirection =
this.preferences.get('favorites.sorting.direction') || 'desc';
const files = nodes.list.entries.map(obj => obj.entry.target.file);
this.sort(files, sortKey, sortDirection);
return entries.map(obj => obj.id); return files.map(f => f.id);
}
if (source === 'favorites') {
const nodes = await this.contentApi.getFavorites('-me-', {
where: '(EXISTS(target/file))',
fields: ['target']
}).toPromise();
const sortKey = this.preferences.get('favorites.sorting.key') || 'modifiedAt';
const sortDirection = this.preferences.get('favorites.sorting.direction') || 'desc';
const files = nodes.list.entries.map(obj => obj.entry.target.file);
this.sort(files, sortKey, sortDirection);
return files.map(f => f.id);
}
if (source === 'shared') {
const sortingKey = this.preferences.get('shared.sorting.key') || 'modifiedAt';
const sortingDirection = this.preferences.get('shared.sorting.direction') || 'desc';
const nodes = await this.contentApi.findSharedLinks({
fields: ['nodeId', this.getRootField(sortingKey)]
}).toPromise();
const entries = nodes.list.entries.map(obj => obj.entry);
this.sort(entries, sortingKey, sortingDirection);
return entries.map(obj => obj.nodeId);
}
if (source === 'recent-files') {
const person = await this.contentApi.getPerson('-me-').toPromise();
const username = person.entry.id;
const sortingKey = this.preferences.get('recent-files.sorting.key') || 'modifiedAt';
const sortingDirection = this.preferences.get('recent-files.sorting.direction') || 'desc';
const nodes = await this.contentApi.search({
query: {
query: '*',
language: 'afts'
},
filterQueries: [
{ query: `cm:modified:[NOW/DAY-30DAYS TO NOW/DAY+1DAY]` },
{ query: `cm:modifier:${username} OR cm:creator:${username}` },
{ query: `TYPE:"content" AND -TYPE:"app:filelink" AND -TYPE:"fm:post"` }
],
fields: ['id', this.getRootField(sortingKey)],
sort: [{
type: 'FIELD',
field: 'cm:modified',
ascending: false
}]
}).toPromise();
const entries = nodes.list.entries.map(obj => obj.entry);
this.sort(entries, sortingKey, sortingDirection);
return entries.map(obj => obj.id);
}
return [];
} }
private sort(items: any[], key: string, direction: string) { if (source === 'shared') {
const options: Intl.CollatorOptions = {}; const sortingKey =
this.preferences.get('shared.sorting.key') || 'modifiedAt';
const sortingDirection =
this.preferences.get('shared.sorting.direction') || 'desc';
if (key.includes('sizeInBytes') || key === 'name') { const nodes = await this.contentApi
options.numeric = true; .findSharedLinks({
} fields: ['nodeId', this.getRootField(sortingKey)]
})
.toPromise();
items.sort((a: any, b: any) => { const entries = nodes.list.entries.map(obj => obj.entry);
let left = ObjectUtils.getValue(a, key); this.sort(entries, sortingKey, sortingDirection);
if (left) {
left = (left instanceof Date) ? left.valueOf().toString() : left.toString(); return entries.map(obj => obj.nodeId);
} else { }
left = '';
if (source === 'recent-files') {
const person = await this.contentApi.getPerson('-me-').toPromise();
const username = person.entry.id;
const sortingKey =
this.preferences.get('recent-files.sorting.key') || 'modifiedAt';
const sortingDirection =
this.preferences.get('recent-files.sorting.direction') || 'desc';
const nodes = await this.contentApi
.search({
query: {
query: '*',
language: 'afts'
},
filterQueries: [
{ query: `cm:modified:[NOW/DAY-30DAYS TO NOW/DAY+1DAY]` },
{ query: `cm:modifier:${username} OR cm:creator:${username}` },
{
query: `TYPE:"content" AND -TYPE:"app:filelink" AND -TYPE:"fm:post"`
} }
],
let right = ObjectUtils.getValue(b, key); fields: ['id', this.getRootField(sortingKey)],
if (right) { sort: [
right = (right instanceof Date) ? right.valueOf().toString() : right.toString(); {
} else { type: 'FIELD',
right = ''; field: 'cm:modified',
ascending: false
} }
]
})
.toPromise();
return direction === 'asc' const entries = nodes.list.entries.map(obj => obj.entry);
? left.localeCompare(right, undefined, options) this.sort(entries, sortingKey, sortingDirection);
: right.localeCompare(left, undefined, options);
}); return entries.map(obj => obj.id);
} }
/** return [];
* Get the root field name from the property path. }
* Example: 'property1.some.child.property' => 'property1'
* @param path Property path private sort(items: any[], key: string, direction: string) {
*/ const options: Intl.CollatorOptions = {};
getRootField(path: string) {
if (path) { if (key.includes('sizeInBytes') || key === 'name') {
return path.split('.')[0]; options.numeric = true;
}
return path;
} }
printFile() { items.sort((a: any, b: any) => {
this.viewUtils.printFileGeneric(this.nodeId, this.node.content.mimeType); let left = ObjectUtils.getValue(a, key);
if (left) {
left =
left instanceof Date ? left.valueOf().toString() : left.toString();
} else {
left = '';
}
let right = ObjectUtils.getValue(b, key);
if (right) {
right =
right instanceof Date ? right.valueOf().toString() : right.toString();
} else {
right = '';
}
return direction === 'asc'
? left.localeCompare(right, undefined, options)
: right.localeCompare(left, undefined, options);
});
}
/**
* Get the root field name from the property path.
* Example: 'property1.some.child.property' => 'property1'
* @param path Property path
*/
getRootField(path: string) {
if (path) {
return path.split('.')[0];
}
return path;
}
printFile() {
this.viewUtils.printFileGeneric(this.nodeId, this.node.content.mimeType);
}
private getNavigationCommands(url: string): any[] {
const urlTree: UrlTree = this.router.parseUrl(url);
const urlSegmentGroup: UrlSegmentGroup =
urlTree.root.children[PRIMARY_OUTLET];
if (!urlSegmentGroup) {
return [url];
} }
private getNavigationCommands(url: string): any[] { const urlSegments: UrlSegment[] = urlSegmentGroup.segments;
const urlTree: UrlTree = this.router.parseUrl(url);
const urlSegmentGroup: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
if (!urlSegmentGroup) { return urlSegments.reduce(function(acc, item) {
return [url]; acc.push(item.path, item.parameters);
} return acc;
}, []);
const urlSegments: UrlSegment[] = urlSegmentGroup.segments; }
return urlSegments.reduce(function(acc, item) {
acc.push(item.path, item.parameters);
return acc;
}, []);
}
} }

View File

@@ -37,32 +37,25 @@ import { PreviewExtensionComponent } from './preview-extension.component';
import { AppToolbarModule } from '../toolbar/toolbar.module'; import { AppToolbarModule } from '../toolbar/toolbar.module';
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: PreviewComponent component: PreviewComponent
} }
]; ];
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
CoreModule.forChild(), CoreModule.forChild(),
ContentDirectiveModule, ContentDirectiveModule,
DirectivesModule, DirectivesModule,
AppInfoDrawerModule, AppInfoDrawerModule,
CoreExtensionsModule.forChild(), CoreExtensionsModule.forChild(),
AppToolbarModule AppToolbarModule
], ],
declarations: [ declarations: [PreviewComponent, PreviewExtensionComponent],
PreviewComponent, providers: [ViewUtilService],
PreviewExtensionComponent exports: [PreviewComponent]
],
providers: [
ViewUtilService
],
exports: [
PreviewComponent
]
}) })
export class PreviewModule {} export class PreviewModule {}

View File

@@ -1,183 +1,228 @@
import {Injectable} from '@angular/core'; import { Injectable } from '@angular/core';
import {AlfrescoApiService, LogService} from '@alfresco/adf-core'; import { AlfrescoApiService, LogService } from '@alfresco/adf-core';
import {RenditionEntry} from 'alfresco-js-api'; import { RenditionEntry } from 'alfresco-js-api';
import {ContentApiService} from './../../services/content-api.service'; import { ContentApiService } from './../../services/content-api.service';
@Injectable() @Injectable()
export class ViewUtilService { export class ViewUtilService {
static TARGET = '_new'; static TARGET = '_new';
/** /**
* Content groups based on categorization of files that can be viewed in the web browser. This * Content groups based on categorization of files that can be viewed in the web browser. This
* implementation or grouping is tied to the definition the ng component: ViewerComponent * implementation or grouping is tied to the definition the ng component: ViewerComponent
*/ */
public static ContentGroup = { public static ContentGroup = {
IMAGE: 'image', IMAGE: 'image',
MEDIA: 'media', MEDIA: 'media',
PDF: 'pdf', PDF: 'pdf',
TEXT: 'text' TEXT: 'text'
}; };
/** /**
* Based on ViewerComponent Implementation, this value is used to determine how many times we try * Based on ViewerComponent Implementation, this value is used to determine how many times we try
* to get the rendition of a file for preview, or printing. * to get the rendition of a file for preview, or printing.
* @type {number} * @type {number}
*/ */
maxRetries = 5; maxRetries = 5;
/** /**
* Mime-type grouping based on the ViewerComponent. * Mime-type grouping based on the ViewerComponent.
*/ */
private mimeTypes = { private mimeTypes = {
text: ['text/plain', 'text/csv', 'text/xml', 'text/html', 'application/x-javascript'], text: [
pdf: ['application/pdf'], 'text/plain',
image: ['image/png', 'image/jpeg', 'image/gif', 'image/bmp', 'image/svg+xml'], 'text/csv',
media: ['video/mp4', 'video/webm', 'video/ogg', 'audio/mpeg', 'audio/ogg', 'audio/wav'] 'text/xml',
}; 'text/html',
'application/x-javascript'
],
pdf: ['application/pdf'],
image: [
'image/png',
'image/jpeg',
'image/gif',
'image/bmp',
'image/svg+xml'
],
media: [
'video/mp4',
'video/webm',
'video/ogg',
'audio/mpeg',
'audio/ogg',
'audio/wav'
]
};
constructor(private apiService: AlfrescoApiService, constructor(
private contentApi: ContentApiService, private apiService: AlfrescoApiService,
private logService: LogService) { private contentApi: ContentApiService,
private logService: LogService
) {}
/**
* This method takes a url to trigger the print dialog against, and the type of artifact that it
* is.
* This URL should be one that can be rendered in the browser, for example PDF, Image, or Text
* @param {string} url
* @param {string} type
*/
public printFile(url: string, type: string) {
const pwa = window.open(url, ViewUtilService.TARGET);
// Because of the way chrome focus and close image window vs. pdf preview window
if (type === ViewUtilService.ContentGroup.IMAGE) {
pwa.onfocus = () => {
setTimeout(() => {
pwa.close();
}, 500);
};
pwa.onload = () => {
pwa.print();
};
} else {
pwa.onload = () => {
pwa.print();
pwa.onfocus = () => {
setTimeout(() => {
pwa.close();
}, 10);
};
};
} }
}
/** /**
* This method takes a url to trigger the print dialog against, and the type of artifact that it * Launch the File Print dialog from anywhere other than the preview service, which resolves the
* is. * rendition of the object that can be printed from a web browser.
* This URL should be one that can be rendered in the browser, for example PDF, Image, or Text * These are: images, PDF files, or PDF rendition of files.
* @param {string} url * We also force PDF rendition for TEXT type objects, otherwise the default URL is to download.
* @param {string} type * TODO there are different TEXT type objects, (HTML, plaintext, xml, etc. we should determine how these are handled)
*/ * @param {string} objectId
public printFile(url: string, type: string) { * @param {string} objectType
const pwa = window.open(url, ViewUtilService.TARGET); */
// Because of the way chrome focus and close image window vs. pdf preview window public printFileGeneric(objectId: string, mimeType: string) {
if (type === ViewUtilService.ContentGroup.IMAGE) { const nodeId = objectId;
pwa.onfocus = () => { const type: string = this.getViewerTypeByMimeType(mimeType);
setTimeout( () => {
pwa.close(); this.getRendition(nodeId, ViewUtilService.ContentGroup.PDF)
}, 500); .then(value => {
}; const url: string = this.getRenditionUrl(
pwa.onload = () => { nodeId,
pwa.print(); type,
}; value ? true : false
} else { );
pwa.onload = () => { const printType =
pwa.print(); type === ViewUtilService.ContentGroup.PDF ||
pwa.onfocus = () => { type === ViewUtilService.ContentGroup.TEXT
setTimeout( () => { ? ViewUtilService.ContentGroup.PDF
pwa.close(); : type;
}, 10); this.printFile(url, printType);
}; })
}; .catch(err => {
this.logService.error('Error with Printing');
this.logService.error(err);
});
}
public getRenditionUrl(
nodeId: string,
type: string,
renditionExists: boolean
): string {
return renditionExists && type !== ViewUtilService.ContentGroup.IMAGE
? this.apiService.contentApi.getRenditionUrl(
nodeId,
ViewUtilService.ContentGroup.PDF
)
: this.contentApi.getContentUrl(nodeId, false);
}
/**
* From ViewerComponent
* @param {string} nodeId
* @param {string} renditionId
* @param {number} retries
* @returns {Promise<AlfrescoApi.RenditionEntry>}
*/
private async waitRendition(
nodeId: string,
renditionId: string,
retries: number
): Promise<RenditionEntry> {
const rendition = await this.apiService.renditionsApi.getRendition(
nodeId,
renditionId
);
if (this.maxRetries < retries) {
const status = rendition.entry.status.toString();
if (status === 'CREATED') {
return rendition;
} else {
retries += 1;
await this.wait(1000);
return await this.waitRendition(nodeId, renditionId, retries);
}
}
}
/**
* From ViewerComponent
* @param {string} mimeType
* @returns {string}
*/
getViewerTypeByMimeType(mimeType: string) {
if (mimeType) {
mimeType = mimeType.toLowerCase();
const editorTypes = Object.keys(this.mimeTypes);
for (const type of editorTypes) {
if (this.mimeTypes[type].indexOf(mimeType) >= 0) {
return type;
} }
}
} }
return 'unknown';
}
/** /**
* Launch the File Print dialog from anywhere other than the preview service, which resolves the * From ViewerComponent
* rendition of the object that can be printed from a web browser. * @param {number} ms
* These are: images, PDF files, or PDF rendition of files. * @returns {Promise<any>}
* We also force PDF rendition for TEXT type objects, otherwise the default URL is to download. */
* TODO there are different TEXT type objects, (HTML, plaintext, xml, etc. we should determine how these are handled) public wait(ms: number): Promise<any> {
* @param {string} objectId return new Promise(resolve => setTimeout(resolve, ms));
* @param {string} objectType }
*/
public printFileGeneric(objectId: string, mimeType: string) {
const nodeId = objectId;
const type: string = this.getViewerTypeByMimeType(mimeType);
this.getRendition(nodeId, ViewUtilService.ContentGroup.PDF) /**
.then(value => { * From ViewerComponent
const url: string = this.getRenditionUrl(nodeId, type, (value ? true : false)); * @param {string} nodeId
const printType = (type === ViewUtilService.ContentGroup.PDF * @returns {string}
|| type === ViewUtilService.ContentGroup.TEXT) */
? ViewUtilService.ContentGroup.PDF : type; public async getRendition(
this.printFile(url, printType); nodeId: string,
}) renditionId: string
.catch(err => { ): Promise<RenditionEntry> {
this.logService.error('Error with Printing'); const supported = await this.apiService.renditionsApi.getRenditions(nodeId);
this.logService.error(err); let rendition = supported.list.entries.find(
}); obj => obj.entry.id.toLowerCase() === renditionId
} );
public getRenditionUrl(nodeId: string, type: string, renditionExists: boolean): string { if (rendition) {
return (renditionExists && type !== ViewUtilService.ContentGroup.IMAGE) ? const status = rendition.entry.status.toString();
this.apiService.contentApi.getRenditionUrl(nodeId, ViewUtilService.ContentGroup.PDF) :
this.contentApi.getContentUrl(nodeId, false);
}
/** if (status === 'NOT_CREATED') {
* From ViewerComponent try {
* @param {string} nodeId await this.apiService.renditionsApi.createRendition(nodeId, {
* @param {string} renditionId id: renditionId
* @param {number} retries });
* @returns {Promise<AlfrescoApi.RenditionEntry>} rendition = await this.waitRendition(nodeId, renditionId, 0);
*/ } catch (err) {
private async waitRendition(nodeId: string, renditionId: string, retries: number): Promise<RenditionEntry> { this.logService.error(err);
const rendition = await this.apiService.renditionsApi.getRendition(nodeId, renditionId);
if (this.maxRetries < retries) {
const status = rendition.entry.status.toString();
if (status === 'CREATED') {
return rendition;
} else {
retries += 1;
await this.wait(1000);
return await this.waitRendition(nodeId, renditionId, retries);
}
} }
}
} }
return new Promise(resolve => resolve(rendition));
}
/**
* From ViewerComponent
* @param {string} mimeType
* @returns {string}
*/
getViewerTypeByMimeType(mimeType: string) {
if (mimeType) {
mimeType = mimeType.toLowerCase();
const editorTypes = Object.keys(this.mimeTypes);
for (const type of editorTypes) {
if (this.mimeTypes[type].indexOf(mimeType) >= 0) {
return type;
}
}
}
return 'unknown';
}
/**
* From ViewerComponent
* @param {number} ms
* @returns {Promise<any>}
*/
public wait(ms: number): Promise<any> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* From ViewerComponent
* @param {string} nodeId
* @returns {string}
*/
public async getRendition(nodeId: string, renditionId: string): Promise<RenditionEntry> {
const supported = await this.apiService.renditionsApi.getRenditions(nodeId);
let rendition = supported.list.entries.find(obj => obj.entry.id.toLowerCase() === renditionId);
if (rendition) {
const status = rendition.entry.status.toString();
if (status === 'NOT_CREATED') {
try {
await this.apiService.renditionsApi.createRendition(nodeId, {id: renditionId});
rendition = await this.waitRendition(nodeId, renditionId, 0);
} catch (err) {
this.logService.error(err);
}
}
}
return new Promise(resolve => resolve(rendition));
}
} }

View File

@@ -26,8 +26,12 @@
import { TestBed, ComponentFixture } from '@angular/core/testing'; import { TestBed, ComponentFixture } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { import {
AlfrescoApiService, AlfrescoApiService,
TimeAgoPipe, NodeNameTooltipPipe, NodeFavoriteDirective, DataTableComponent, AppConfigPipe TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DataTableComponent,
AppConfigPipe
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services'; import { DocumentListComponent } from '@alfresco/adf-content-services';
import { ContentManagementService } from '../../services/content-management.service'; import { ContentManagementService } from '../../services/content-management.service';
@@ -37,91 +41,93 @@ import { AppTestingModule } from '../../testing/app-testing.module';
import { ExperimentalDirective } from '../../directives/experimental.directive'; import { ExperimentalDirective } from '../../directives/experimental.directive';
describe('RecentFilesComponent', () => { describe('RecentFilesComponent', () => {
let fixture: ComponentFixture<RecentFilesComponent>; let fixture: ComponentFixture<RecentFilesComponent>;
let component: RecentFilesComponent; let component: RecentFilesComponent;
let alfrescoApi: AlfrescoApiService; let alfrescoApi: AlfrescoApiService;
let contentService: ContentManagementService; let contentService: ContentManagementService;
let page; let page;
beforeEach(() => {
page = {
list: {
entries: [{ entry: { id: 1 } }, { entry: { id: 2 } }],
pagination: { data: 'data' }
}
};
});
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [
DataTableComponent,
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DocumentListComponent,
RecentFilesComponent,
AppConfigPipe,
ExperimentalDirective
],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(RecentFilesComponent);
component = fixture.componentInstance;
contentService = TestBed.get(ContentManagementService);
alfrescoApi = TestBed.get(AlfrescoApiService);
alfrescoApi.reset();
spyOn(alfrescoApi.peopleApi, 'getPerson').and.returnValue(
Promise.resolve({
entry: { id: 'personId' }
})
);
spyOn(alfrescoApi.searchApi, 'search').and.returnValue(
Promise.resolve(page)
);
});
describe('OnInit()', () => {
beforeEach(() => { beforeEach(() => {
page = { spyOn(component, 'reload').and.stub();
list: {
entries: [ { entry: { id: 1 } }, { entry: { id: 2 } } ],
pagination: { data: 'data'}
}
};
}); });
beforeEach(() => { it('should reload nodes on onDeleteNode event', () => {
TestBed.configureTestingModule({ fixture.detectChanges();
imports: [
AppTestingModule
],
declarations: [
DataTableComponent,
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DocumentListComponent,
RecentFilesComponent,
AppConfigPipe,
ExperimentalDirective
],
schemas: [ NO_ERRORS_SCHEMA ]
});
fixture = TestBed.createComponent(RecentFilesComponent); contentService.nodesDeleted.next();
component = fixture.componentInstance;
contentService = TestBed.get(ContentManagementService); expect(component.reload).toHaveBeenCalled();
alfrescoApi = TestBed.get(AlfrescoApiService);
alfrescoApi.reset();
spyOn(alfrescoApi.peopleApi, 'getPerson').and.returnValue(Promise.resolve({
entry: { id: 'personId' }
}));
spyOn(alfrescoApi.searchApi, 'search').and.returnValue(Promise.resolve(page));
}); });
describe('OnInit()', () => { it('should reload on onRestoreNode event', () => {
beforeEach(() => { fixture.detectChanges();
spyOn(component, 'reload').and.stub();
});
it('should reload nodes on onDeleteNode event', () => { contentService.nodesRestored.next();
fixture.detectChanges();
contentService.nodesDeleted.next(); expect(component.reload).toHaveBeenCalled();
expect(component.reload).toHaveBeenCalled();
});
it('should reload on onRestoreNode event', () => {
fixture.detectChanges();
contentService.nodesRestored.next();
expect(component.reload).toHaveBeenCalled();
});
it('should reload on move node event', () => {
fixture.detectChanges();
contentService.nodesMoved.next();
expect(component.reload).toHaveBeenCalled();
});
}); });
describe('refresh', () => { it('should reload on move node event', () => {
it('should call document list reload', () => { fixture.detectChanges();
spyOn(component.documentList, 'reload');
fixture.detectChanges();
component.reload(); contentService.nodesMoved.next();
expect(component.documentList.reload).toHaveBeenCalled(); expect(component.reload).toHaveBeenCalled();
});
}); });
});
describe('refresh', () => {
it('should call document list reload', () => {
spyOn(component.documentList, 'reload');
fixture.detectChanges();
component.reload();
expect(component.documentList.reload).toHaveBeenCalled();
});
});
}); });

View File

@@ -33,47 +33,44 @@ import { AppStore } from '../../store/states/app.state';
import { AppExtensionService } from '../../extensions/extension.service'; import { AppExtensionService } from '../../extensions/extension.service';
@Component({ @Component({
templateUrl: './recent-files.component.html' templateUrl: './recent-files.component.html'
}) })
export class RecentFilesComponent extends PageComponent implements OnInit { export class RecentFilesComponent extends PageComponent implements OnInit {
isSmallScreen = false; isSmallScreen = false;
constructor( constructor(
store: Store<AppStore>, store: Store<AppStore>,
extensions: AppExtensionService, extensions: AppExtensionService,
content: ContentManagementService, content: ContentManagementService,
private breakpointObserver: BreakpointObserver private breakpointObserver: BreakpointObserver
) { ) {
super(store, extensions, content); super(store, extensions, content);
} }
ngOnInit() { ngOnInit() {
super.ngOnInit(); super.ngOnInit();
this.subscriptions = this.subscriptions.concat([ this.subscriptions = this.subscriptions.concat([
this.content.nodesDeleted.subscribe(() => this.reload()), this.content.nodesDeleted.subscribe(() => this.reload()),
this.content.nodesMoved.subscribe(() => this.reload()), this.content.nodesMoved.subscribe(() => this.reload()),
this.content.nodesRestored.subscribe(() => this.reload()), this.content.nodesRestored.subscribe(() => this.reload()),
this.breakpointObserver this.breakpointObserver
.observe([ .observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])
Breakpoints.HandsetPortrait, .subscribe(result => {
Breakpoints.HandsetLandscape this.isSmallScreen = result.matches;
]) })
.subscribe(result => { ]);
this.isSmallScreen = result.matches; }
})
]); onNodeDoubleClick(node: MinimalNodeEntity) {
} if (node && node.entry) {
if (PageComponent.isLockedNode(node.entry)) {
onNodeDoubleClick(node: MinimalNodeEntity) { event.preventDefault();
if (node && node.entry) { return;
if (PageComponent.isLockedNode(node.entry)) { }
event.preventDefault();
return; this.showPreview(node);
}
this.showPreview(node);
}
} }
}
} }

View File

@@ -1,8 +1,8 @@
.adf-clear-search-icon-wrapper { .adf-clear-search-icon-wrapper {
width: 1em; width: 1em;
.mat-icon { .mat-icon {
font-size: 100%; font-size: 100%;
cursor: pointer; cursor: pointer;
} }
} }

View File

@@ -24,253 +24,299 @@
*/ */
import { ThumbnailService } from '@alfresco/adf-core'; import { ThumbnailService } from '@alfresco/adf-core';
import { animate, state, style, transition, trigger } from '@angular/animations'; import {
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, animate,
QueryList, ViewEncapsulation, ViewChild, ViewChildren, ElementRef, TemplateRef, ContentChild } from '@angular/core'; state,
style,
transition,
trigger
} from '@angular/animations';
import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
QueryList,
ViewEncapsulation,
ViewChild,
ViewChildren,
ElementRef,
TemplateRef,
ContentChild
} from '@angular/core';
import { MinimalNodeEntity, QueryBody } from 'alfresco-js-api'; import { MinimalNodeEntity, QueryBody } from 'alfresco-js-api';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { MatListItem } from '@angular/material'; import { MatListItem } from '@angular/material';
import { debounceTime, filter } from 'rxjs/operators'; import { debounceTime, filter } from 'rxjs/operators';
import { EmptySearchResultComponent, SearchComponent } from '@alfresco/adf-content-services'; import {
EmptySearchResultComponent,
SearchComponent
} from '@alfresco/adf-content-services';
@Component({ @Component({
selector: 'app-search-input-control', selector: 'app-search-input-control',
templateUrl: './search-input-control.component.html', templateUrl: './search-input-control.component.html',
styleUrls: ['./search-input-control.component.scss'], styleUrls: ['./search-input-control.component.scss'],
animations: [ animations: [
trigger('transitionMessages', [ trigger('transitionMessages', [
state('active', style({ transform: 'translateX(0%)', 'margin-left': '13px' })), state(
state('inactive', style({ transform: 'translateX(81%)'})), 'active',
state('no-animation', style({ transform: 'translateX(0%)', width: '100%' })), style({ transform: 'translateX(0%)', 'margin-left': '13px' })
transition('inactive => active', ),
animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)')), state('inactive', style({ transform: 'translateX(81%)' })),
transition('active => inactive', state(
animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)')) 'no-animation',
]) style({ transform: 'translateX(0%)', width: '100%' })
], ),
encapsulation: ViewEncapsulation.None, transition(
host: { class: 'adf-search-control' } 'inactive => active',
animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)')
),
transition(
'active => inactive',
animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)')
)
])
],
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-search-control' }
}) })
export class SearchInputControlComponent implements OnInit, OnDestroy { export class SearchInputControlComponent implements OnInit, OnDestroy {
/** Toggles whether to use an expanding search control. If false
* then a regular input is used.
*/
@Input()
expandable = true;
/** Toggles whether to use an expanding search control. If false /** Toggles highlighting of the search term in the results. */
* then a regular input is used. @Input()
*/ highlight = false;
@Input()
expandable = true;
/** Toggles highlighting of the search term in the results. */ /** Type of the input field to render, e.g. "search" or "text" (default). */
@Input() @Input()
highlight = false; inputType = 'text';
/** Type of the input field to render, e.g. "search" or "text" (default). */ /** Toggles auto-completion of the search input field. */
@Input() @Input()
inputType = 'text'; autocomplete = false;
/** Toggles auto-completion of the search input field. */ /** Toggles "find-as-you-type" suggestions for possible matches. */
@Input() @Input()
autocomplete = false; liveSearchEnabled = true;
/** Toggles "find-as-you-type" suggestions for possible matches. */ /** Maximum number of results to show in the live search. */
@Input() @Input()
liveSearchEnabled = true; liveSearchMaxResults = 5;
/** Maximum number of results to show in the live search. */ /** @deprecated in 2.1.0 */
@Input() @Input()
liveSearchMaxResults = 5; customQueryBody: QueryBody;
/** @deprecated in 2.1.0 */ /** Emitted when the search is submitted pressing ENTER button.
@Input() * The search term is provided as value of the event.
customQueryBody: QueryBody; */
@Output()
submit: EventEmitter<any> = new EventEmitter();
/** Emitted when the search is submitted pressing ENTER button. /** Emitted when the search term is changed. The search term is provided
* The search term is provided as value of the event. * in the 'value' property of the returned object. If the term is less
*/ * than three characters in length then the term is truncated to an empty
@Output() * string.
submit: EventEmitter<any> = new EventEmitter(); */
@Output()
searchChange: EventEmitter<string> = new EventEmitter();
/** Emitted when the search term is changed. The search term is provided /** Emitted when a file item from the list of "find-as-you-type" results is selected. */
* in the 'value' property of the returned object. If the term is less @Output()
* than three characters in length then the term is truncated to an empty optionClicked: EventEmitter<any> = new EventEmitter();
* string.
*/
@Output()
searchChange: EventEmitter<string> = new EventEmitter();
/** Emitted when a file item from the list of "find-as-you-type" results is selected. */ @ViewChild('search')
@Output() searchAutocomplete: SearchComponent;
optionClicked: EventEmitter<any> = new EventEmitter();
@ViewChild('search') @ViewChild('searchInput')
searchAutocomplete: SearchComponent; searchInput: ElementRef;
@ViewChild('searchInput') @ViewChildren(MatListItem)
searchInput: ElementRef; private listResultElement: QueryList<MatListItem>;
@ViewChildren(MatListItem) @ContentChild(EmptySearchResultComponent)
private listResultElement: QueryList<MatListItem>; emptySearchTemplate: EmptySearchResultComponent;
@ContentChild(EmptySearchResultComponent) searchTerm = '';
emptySearchTemplate: EmptySearchResultComponent; subscriptAnimationState: string;
noSearchResultTemplate: TemplateRef<any> = null;
skipToggle = false;
toggleDebounceTime = 200;
searchTerm = ''; private toggleSearch = new Subject<any>();
subscriptAnimationState: string; private focusSubject = new Subject<FocusEvent>();
noSearchResultTemplate: TemplateRef <any> = null;
skipToggle = false;
toggleDebounceTime = 200;
private toggleSearch = new Subject<any>(); constructor(private thumbnailService: ThumbnailService) {
private focusSubject = new Subject<FocusEvent>(); this.toggleSearch
.asObservable()
.pipe(debounceTime(this.toggleDebounceTime))
.subscribe(() => {
if (this.expandable && !this.skipToggle) {
this.subscriptAnimationState =
this.subscriptAnimationState === 'inactive' ? 'active' : 'inactive';
constructor(private thumbnailService: ThumbnailService) { if (this.subscriptAnimationState === 'inactive') {
this.searchTerm = '';
this.toggleSearch.asObservable().pipe(debounceTime(this.toggleDebounceTime)).subscribe(() => { this.searchAutocomplete.resetResults();
if (this.expandable && !this.skipToggle) { if (
this.subscriptAnimationState = this.subscriptAnimationState === 'inactive' ? 'active' : 'inactive'; document.activeElement.id === this.searchInput.nativeElement.id
) {
if (this.subscriptAnimationState === 'inactive') { this.searchInput.nativeElement.blur();
this.searchTerm = '';
this.searchAutocomplete.resetResults();
if ( document.activeElement.id === this.searchInput.nativeElement.id) {
this.searchInput.nativeElement.blur();
}
}
} }
this.skipToggle = false; }
});
}
applySearchFocus(animationDoneEvent) {
if (animationDoneEvent.toState === 'active') {
this.searchInput.nativeElement.focus();
} }
this.skipToggle = false;
});
}
applySearchFocus(animationDoneEvent) {
if (animationDoneEvent.toState === 'active') {
this.searchInput.nativeElement.focus();
}
}
ngOnInit() {
this.subscriptAnimationState = this.expandable
? 'inactive'
: 'no-animation';
this.setupFocusEventHandlers();
}
isNoSearchTemplatePresent(): boolean {
return this.emptySearchTemplate ? true : false;
}
ngOnDestroy(): void {
if (this.focusSubject) {
this.focusSubject.unsubscribe();
this.focusSubject = null;
} }
ngOnInit() { if (this.toggleSearch) {
this.subscriptAnimationState = this.expandable ? 'inactive' : 'no-animation'; this.toggleSearch.unsubscribe();
this.setupFocusEventHandlers(); this.toggleSearch = null;
}
}
searchSubmit(event: any) {
this.submit.emit(event);
this.toggleSearchBar();
}
inputChange(event: any) {
this.searchChange.emit(event);
}
getAutoComplete(): string {
return this.autocomplete ? 'on' : 'off';
}
getMimeTypeIcon(node: MinimalNodeEntity): string {
let mimeType;
if (node.entry.content && node.entry.content.mimeType) {
mimeType = node.entry.content.mimeType;
}
if (node.entry.isFolder) {
mimeType = 'folder';
} }
isNoSearchTemplatePresent(): boolean { return this.thumbnailService.getMimeTypeIcon(mimeType);
return this.emptySearchTemplate ? true : false; }
isSearchBarActive() {
return this.subscriptAnimationState === 'active' && this.liveSearchEnabled;
}
toggleSearchBar() {
if (this.toggleSearch) {
this.toggleSearch.next();
} }
}
ngOnDestroy(): void { elementClicked(item: any) {
if (this.focusSubject) { if (item.entry) {
this.focusSubject.unsubscribe(); this.optionClicked.next(item);
this.focusSubject = null; this.toggleSearchBar();
}
if (this.toggleSearch) {
this.toggleSearch.unsubscribe();
this.toggleSearch = null;
}
} }
}
searchSubmit(event: any) { onFocus($event): void {
this.submit.emit(event); this.focusSubject.next($event);
}
onBlur($event): void {
this.focusSubject.next($event);
}
activateToolbar() {
if (!this.isSearchBarActive()) {
this.toggleSearchBar();
}
}
selectFirstResult() {
if (this.listResultElement && this.listResultElement.length > 0) {
const firstElement: MatListItem = <MatListItem>(
this.listResultElement.first
);
firstElement._getHostElement().focus();
}
}
onRowArrowDown($event: KeyboardEvent): void {
const nextElement: any = this.getNextElementSibling(<Element>$event.target);
if (nextElement) {
nextElement.focus();
}
}
onRowArrowUp($event: KeyboardEvent): void {
const previousElement: any = this.getPreviousElementSibling(<Element>(
$event.target
));
if (previousElement) {
previousElement.focus();
} else {
this.searchInput.nativeElement.focus();
this.focusSubject.next(new FocusEvent('focus'));
}
}
private setupFocusEventHandlers() {
this.focusSubject
.pipe(
debounceTime(50),
filter(($event: any) => {
return (
this.isSearchBarActive() &&
($event.type === 'blur' || $event.type === 'focusout')
);
})
)
.subscribe(() => {
this.toggleSearchBar(); this.toggleSearchBar();
} });
}
inputChange(event: any) { clear(event: any) {
this.searchChange.emit(event); this.searchTerm = '';
} this.searchChange.emit('');
this.skipToggle = true;
}
getAutoComplete(): string { private getNextElementSibling(node: Element): Element {
return this.autocomplete ? 'on' : 'off'; return node.nextElementSibling;
} }
getMimeTypeIcon(node: MinimalNodeEntity): string {
let mimeType;
if (node.entry.content && node.entry.content.mimeType) {
mimeType = node.entry.content.mimeType;
}
if (node.entry.isFolder) {
mimeType = 'folder';
}
return this.thumbnailService.getMimeTypeIcon(mimeType);
}
isSearchBarActive() {
return this.subscriptAnimationState === 'active' && this.liveSearchEnabled;
}
toggleSearchBar() {
if (this.toggleSearch) {
this.toggleSearch.next();
}
}
elementClicked(item: any) {
if (item.entry) {
this.optionClicked.next(item);
this.toggleSearchBar();
}
}
onFocus($event): void {
this.focusSubject.next($event);
}
onBlur($event): void {
this.focusSubject.next($event);
}
activateToolbar() {
if (!this.isSearchBarActive()) {
this.toggleSearchBar();
}
}
selectFirstResult() {
if ( this.listResultElement && this.listResultElement.length > 0) {
const firstElement: MatListItem = <MatListItem> this.listResultElement.first;
firstElement._getHostElement().focus();
}
}
onRowArrowDown($event: KeyboardEvent): void {
const nextElement: any = this.getNextElementSibling(<Element> $event.target);
if (nextElement) {
nextElement.focus();
}
}
onRowArrowUp($event: KeyboardEvent): void {
const previousElement: any = this.getPreviousElementSibling(<Element> $event.target);
if (previousElement) {
previousElement.focus();
} else {
this.searchInput.nativeElement.focus();
this.focusSubject.next(new FocusEvent('focus'));
}
}
private setupFocusEventHandlers() {
this.focusSubject.pipe(
debounceTime(50),
filter(($event: any) => {
return this.isSearchBarActive() && ($event.type === 'blur' || $event.type === 'focusout');
})
).subscribe(() => {
this.toggleSearchBar();
});
}
clear(event: any) {
this.searchTerm = '';
this.searchChange.emit('');
this.skipToggle = true;
}
private getNextElementSibling(node: Element): Element {
return node.nextElementSibling;
}
private getPreviousElementSibling(node: Element): Element {
return node.previousElementSibling;
}
private getPreviousElementSibling(node: Element): Element {
return node.previousElementSibling;
}
} }

View File

@@ -24,65 +24,74 @@
*/ */
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, async, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; import {
TestBed,
async,
ComponentFixture,
fakeAsync,
tick
} from '@angular/core/testing';
import { SearchInputComponent } from './search-input.component'; import { SearchInputComponent } from './search-input.component';
import { AppTestingModule } from '../../../testing/app-testing.module'; import { AppTestingModule } from '../../../testing/app-testing.module';
import { Actions, ofType } from '@ngrx/effects'; import { Actions, ofType } from '@ngrx/effects';
import { NAVIGATE_FOLDER, NavigateToFolder, VIEW_FILE, ViewFileAction } from '../../../store/actions'; import {
NAVIGATE_FOLDER,
NavigateToFolder,
VIEW_FILE,
ViewFileAction
} from '../../../store/actions';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
describe('SearchInputComponent', () => { describe('SearchInputComponent', () => {
let fixture: ComponentFixture<SearchInputComponent>; let fixture: ComponentFixture<SearchInputComponent>;
let component: SearchInputComponent; let component: SearchInputComponent;
let actions$: Actions; let actions$: Actions;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [AppTestingModule],
AppTestingModule declarations: [SearchInputComponent],
], schemas: [NO_ERRORS_SCHEMA]
declarations: [ })
SearchInputComponent .compileComponents()
], .then(() => {
schemas: [ NO_ERRORS_SCHEMA ] actions$ = TestBed.get(Actions);
fixture = TestBed.createComponent(SearchInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
}));
describe('onItemClicked()', () => {
it('opens preview if node is file', fakeAsync(done => {
actions$.pipe(
ofType<ViewFileAction>(VIEW_FILE),
map(action => {
expect(action.payload.entry.id).toBe('node-id');
done();
}) })
.compileComponents() );
.then(() => {
actions$ = TestBed.get(Actions); const node = {
fixture = TestBed.createComponent(SearchInputComponent); entry: { isFile: true, id: 'node-id', parentId: 'parent-id' }
component = fixture.componentInstance; };
fixture.detectChanges();
}); component.onItemClicked(node);
tick();
})); }));
describe('onItemClicked()', () => { it('navigates if node is folder', fakeAsync(done => {
it('opens preview if node is file', fakeAsync(done => { actions$.pipe(
actions$.pipe( ofType<NavigateToFolder>(NAVIGATE_FOLDER),
ofType<ViewFileAction>(VIEW_FILE), map(action => {
map(action => { expect(action.payload.entry.id).toBe('folder-id');
expect(action.payload.entry.id).toBe('node-id'); done();
done(); })
}) );
); const node = { entry: { id: 'folder-id', isFolder: true } };
component.onItemClicked(node);
const node = { entry: { isFile: true, id: 'node-id', parentId: 'parent-id' } }; tick();
}));
component.onItemClicked(node); });
tick();
}));
it('navigates if node is folder', fakeAsync(done => {
actions$.pipe(
ofType<NavigateToFolder>(NAVIGATE_FOLDER),
map(action => {
expect(action.payload.entry.id).toBe('folder-id');
done();
})
);
const node = { entry: { id: 'folder-id', isFolder: true } };
component.onItemClicked(node);
tick();
}));
});
}); });

View File

@@ -1,37 +1,37 @@
@mixin aca-search-input-theme($theme) { @mixin aca-search-input-theme($theme) {
$background: map-get($theme, background); $background: map-get($theme, background);
.aca-search-input{ .aca-search-input {
display: flex; display: flex;
box-sizing: border-box; box-sizing: border-box;
padding: 0; padding: 0;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
white-space: nowrap; white-space: nowrap;
.adf-search-control { .adf-search-control {
color: mat-color($background, card); color: mat-color($background, card);
.mat-form-field-underline { .mat-form-field-underline {
background-color: mat-color($background, card); background-color: mat-color($background, card);
} }
.adf-input-form-field-divider { .adf-input-form-field-divider {
font-size: 14px; font-size: 14px;
} }
}
.adf-search-button.mat-icon-button {
left: -15px;
margin-left: 15px;
align-items: flex-start;
font: 400 11px system-ui;
color: mat-color($background, card);
.mat-icon {
font-size: 24px;
padding-right: 0;
}
}
} }
.adf-search-button.mat-icon-button {
left: -15px;
margin-left: 15px;
align-items: flex-start;
font: 400 11px system-ui;
color: mat-color($background, card);
.mat-icon {
font-size: 24px;
padding-right: 0;
}
}
}
} }

View File

@@ -25,127 +25,132 @@
import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { import {
NavigationEnd, PRIMARY_OUTLET, Router, RouterEvent, UrlSegment, UrlSegmentGroup, NavigationEnd,
UrlTree PRIMARY_OUTLET,
Router,
RouterEvent,
UrlSegment,
UrlSegmentGroup,
UrlTree
} from '@angular/router'; } from '@angular/router';
import { MinimalNodeEntity } from 'alfresco-js-api'; import { MinimalNodeEntity } from 'alfresco-js-api';
import { SearchInputControlComponent } from '../search-input-control/search-input-control.component'; import { SearchInputControlComponent } from '../search-input-control/search-input-control.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states/app.state'; import { AppStore } from '../../../store/states/app.state';
import { SearchByTermAction, NavigateToFolder, ViewFileAction } from '../../../store/actions'; import {
SearchByTermAction,
NavigateToFolder,
ViewFileAction
} from '../../../store/actions';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
@Component({ @Component({
selector: 'aca-search-input', selector: 'aca-search-input',
templateUrl: 'search-input.component.html', templateUrl: 'search-input.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { class: 'aca-search-input' } host: { class: 'aca-search-input' }
}) })
export class SearchInputComponent implements OnInit { export class SearchInputComponent implements OnInit {
hasOneChange = false;
hasNewChange = false;
navigationTimer: any;
enableLiveSearch = true;
hasOneChange = false; @ViewChild('searchInputControl')
hasNewChange = false; searchInputControl: SearchInputControlComponent;
navigationTimer: any;
enableLiveSearch = true;
@ViewChild('searchInputControl') constructor(private router: Router, private store: Store<AppStore>) {}
searchInputControl: SearchInputControlComponent;
constructor(private router: Router, private store: Store<AppStore>) { ngOnInit() {
} this.showInputValue();
ngOnInit() { this.router.events
this.showInputValue(); .pipe(filter(e => e instanceof RouterEvent))
.subscribe(event => {
this.router.events if (event instanceof NavigationEnd) {
.pipe(filter(e => e instanceof RouterEvent)) this.showInputValue();
.subscribe(event => {
if (event instanceof NavigationEnd) {
this.showInputValue();
}
});
}
showInputValue() {
if (this.onSearchResults) {
let searchedWord = null;
const urlTree: UrlTree = this.router.parseUrl(this.router.url);
const urlSegmentGroup: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
if (urlSegmentGroup) {
const urlSegments: UrlSegment[] = urlSegmentGroup.segments;
searchedWord = urlSegments[0].parameters['q'];
}
if (this.searchInputControl) {
this.enableLiveSearch = false;
this.searchInputControl.searchTerm = searchedWord;
this.searchInputControl.subscriptAnimationState = 'no-animation';
}
} else {
if (this.searchInputControl.subscriptAnimationState === 'no-animation') {
this.searchInputControl.subscriptAnimationState = 'active';
this.searchInputControl.searchTerm = '';
this.searchInputControl.toggleSearchBar();
}
if (!this.enableLiveSearch) {
setTimeout(() => {
this.enableLiveSearch = true;
}, this.searchInputControl.toggleDebounceTime + 100);
}
} }
} });
}
onItemClicked(node: MinimalNodeEntity) { showInputValue() {
if (node && node.entry) { if (this.onSearchResults) {
const { isFile, isFolder } = node.entry; let searchedWord = null;
if (isFile) { const urlTree: UrlTree = this.router.parseUrl(this.router.url);
this.store.dispatch(new ViewFileAction(node)); const urlSegmentGroup: UrlSegmentGroup =
} else if (isFolder) { urlTree.root.children[PRIMARY_OUTLET];
this.store.dispatch(new NavigateToFolder(node));
}
}
}
/** if (urlSegmentGroup) {
* Called when the user submits the search, e.g. hits enter or clicks submit const urlSegments: UrlSegment[] = urlSegmentGroup.segments;
* searchedWord = urlSegments[0].parameters['q'];
* @param event Parameters relating to the search }
*/
onSearchSubmit(event: KeyboardEvent) { if (this.searchInputControl) {
const searchTerm = (event.target as HTMLInputElement).value; this.enableLiveSearch = false;
this.searchInputControl.searchTerm = searchedWord;
this.searchInputControl.subscriptAnimationState = 'no-animation';
}
} else {
if (this.searchInputControl.subscriptAnimationState === 'no-animation') {
this.searchInputControl.subscriptAnimationState = 'active';
this.searchInputControl.searchTerm = '';
this.searchInputControl.toggleSearchBar();
}
if (!this.enableLiveSearch) {
setTimeout(() => {
this.enableLiveSearch = true;
}, this.searchInputControl.toggleDebounceTime + 100);
}
}
}
onItemClicked(node: MinimalNodeEntity) {
if (node && node.entry) {
const { isFile, isFolder } = node.entry;
if (isFile) {
this.store.dispatch(new ViewFileAction(node));
} else if (isFolder) {
this.store.dispatch(new NavigateToFolder(node));
}
}
}
/**
* Called when the user submits the search, e.g. hits enter or clicks submit
*
* @param event Parameters relating to the search
*/
onSearchSubmit(event: KeyboardEvent) {
const searchTerm = (event.target as HTMLInputElement).value;
if (searchTerm) {
this.store.dispatch(new SearchByTermAction(searchTerm));
}
}
onSearchChange(searchTerm: string) {
if (this.onSearchResults) {
if (this.hasOneChange) {
this.hasNewChange = true;
} else {
this.hasOneChange = true;
}
if (this.hasNewChange) {
clearTimeout(this.navigationTimer);
this.hasNewChange = false;
}
this.navigationTimer = setTimeout(() => {
if (searchTerm) { if (searchTerm) {
this.store.dispatch(new SearchByTermAction(searchTerm)); this.store.dispatch(new SearchByTermAction(searchTerm));
} }
this.hasOneChange = false;
}, 1000);
} }
}
onSearchChange(searchTerm: string) { get onSearchResults() {
if (this.onSearchResults) { return this.router.url.indexOf('/search') === 0;
}
if (this.hasOneChange) {
this.hasNewChange = true;
} else {
this.hasOneChange = true;
}
if (this.hasNewChange) {
clearTimeout(this.navigationTimer);
this.hasNewChange = false;
}
this.navigationTimer = setTimeout(() => {
if (searchTerm) {
this.store.dispatch(new SearchByTermAction(searchTerm));
}
this.hasOneChange = false;
}, 1000);
}
}
get onSearchResults() {
return this.router.url.indexOf('/search') === 0;
}
} }

View File

@@ -1,24 +1,24 @@
@import 'mixins'; @import 'mixins';
.aca-search-results-row { .aca-search-results-row {
@include flex-column; @include flex-column;
} }
.line { .line {
margin: 5px 0; margin: 5px 0;
} }
.bold { .bold {
font-weight: 400; font-weight: 400;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
.link { .link {
text-decoration: none; text-decoration: none;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
.link:hover { .link:hover {
color: #2196F3; color: #2196f3;
text-decoration: underline; text-decoration: underline;
} }

View File

@@ -23,7 +23,13 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Component, Input, OnInit, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core'; import {
Component,
Input,
OnInit,
ViewEncapsulation,
ChangeDetectionStrategy
} from '@angular/core';
import { MinimalNodeEntity } from 'alfresco-js-api'; import { MinimalNodeEntity } from 'alfresco-js-api';
import { ViewFileAction } from '../../../store/actions'; import { ViewFileAction } from '../../../store/actions';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@@ -31,85 +37,84 @@ import { AppStore } from '../../../store/states/app.state';
import { NavigateToFolder } from '../../../store/actions'; import { NavigateToFolder } from '../../../store/actions';
@Component({ @Component({
selector: 'aca-search-results-row', selector: 'aca-search-results-row',
templateUrl: './search-results-row.component.html', templateUrl: './search-results-row.component.html',
styleUrls: ['./search-results-row.component.scss'], styleUrls: ['./search-results-row.component.scss'],
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'aca-search-results-row' } host: { class: 'aca-search-results-row' }
}) })
export class SearchResultsRowComponent implements OnInit { export class SearchResultsRowComponent implements OnInit {
private node: MinimalNodeEntity; private node: MinimalNodeEntity;
@Input() context: any; @Input()
context: any;
constructor(private store: Store<AppStore>) {} constructor(private store: Store<AppStore>) {}
ngOnInit() { ngOnInit() {
this.node = this.context.row.node; this.node = this.context.row.node;
} }
get name() { get name() {
return this.getValue('name'); return this.getValue('name');
} }
get title() { get title() {
return this.getValue('properties["cm:title"]'); return this.getValue('properties["cm:title"]');
} }
get description() { get description() {
return this.getValue('properties["cm:description"]'); return this.getValue('properties["cm:description"]');
} }
get modifiedAt() { get modifiedAt() {
return this.getValue('modifiedAt'); return this.getValue('modifiedAt');
} }
get size() { get size() {
return this.getValue('content.sizeInBytes'); return this.getValue('content.sizeInBytes');
} }
get user() { get user() {
return this.getValue('modifiedByUser.displayName'); return this.getValue('modifiedByUser.displayName');
} }
get hasDescription() { get hasDescription() {
return this.description; return this.description;
} }
get hasTitle() { get hasTitle() {
return this.title; return this.title;
} }
get showTitle() { get showTitle() {
return this.name !== this.title; return this.name !== this.title;
} }
get hasSize() { get hasSize() {
return this.size; return this.size;
} }
get isFile() { get isFile() {
return this.getValue('isFile'); return this.getValue('isFile');
} }
showPreview() { showPreview() {
this.store.dispatch( this.store.dispatch(new ViewFileAction(this.node));
new ViewFileAction(this.node) }
);
}
navigate() { navigate() {
this.store.dispatch(new NavigateToFolder(this.node)); this.store.dispatch(new NavigateToFolder(this.node));
} }
private getValue(path) { private getValue(path) {
return path return path
.replace('["', '.') .replace('["', '.')
.replace('"]', '') .replace('"]', '')
.replace('[', '.') .replace('[', '.')
.replace(']', '') .replace(']', '')
.split('.') .split('.')
.reduce((acc, part) => (acc ? acc[part] : null), this.node.entry); .reduce((acc, part) => (acc ? acc[part] : null), this.node.entry);
} }
} }

View File

@@ -1,60 +1,60 @@
@import 'mixins'; @import 'mixins';
.adf-search-results { .adf-search-results {
@include flex-row;
&__facets {
display: flex;
flex-direction: row;
margin-top: 5px;
margin-bottom: 5px;
}
&__content {
@include flex-column;
border-left: 1px solid #eee;
}
&__content-header {
display: flex;
padding: 0 25px 0 25px;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #eee;
}
&--info-text {
flex: 1;
font-size: 16px;
color: rgba(0, 0, 0, 0.54);
}
.adf-search-filter {
min-width: 260px;
padding: 5px;
height: 100%;
overflow: scroll;
&--hidden {
display: none;
}
}
.text--bold {
font-weight: 600;
}
.content {
@include flex-row; @include flex-row;
flex: unset;
height: unset;
padding-top: 8px;
padding-bottom: 8px;
flex-wrap: wrap;
&__facets { &__side--left {
display: flex; @include flex-column;
flex-direction: row; height: unset;
margin-top: 5px;
margin-bottom: 5px;
}
&__content {
@include flex-column;
border-left: 1px solid #eee;
}
&__content-header {
display: flex;
padding: 0 25px 0 25px;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #eee;
}
&--info-text {
flex: 1;
font-size: 16px;
color: rgba(0, 0, 0, 0.54);
}
.adf-search-filter {
min-width: 260px;
padding: 5px;
height: 100%;
overflow: scroll;
&--hidden {
display: none;
}
}
.text--bold {
font-weight: 600;
}
.content {
@include flex-row;
flex: unset;
height: unset;
padding-top: 8px;
padding-bottom: 8px;
flex-wrap: wrap;
&__side--left {
@include flex-column;
height: unset;
}
} }
}
} }

View File

@@ -8,9 +8,8 @@ describe('SearchComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ SearchResultsComponent ] declarations: [SearchResultsComponent]
}) }).compileComponents();
.compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {

View File

@@ -26,7 +26,11 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { NodePaging, Pagination, MinimalNodeEntity } from 'alfresco-js-api'; import { NodePaging, Pagination, MinimalNodeEntity } from 'alfresco-js-api';
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute, Params } from '@angular/router';
import { SearchQueryBuilderService, SearchComponent as AdfSearchComponent, SearchFilterComponent } from '@alfresco/adf-content-services'; import {
SearchQueryBuilderService,
SearchComponent as AdfSearchComponent,
SearchFilterComponent
} from '@alfresco/adf-content-services';
import { PageComponent } from '../../page.component'; import { PageComponent } from '../../page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states/app.state'; import { AppStore } from '../../../store/states/app.state';
@@ -41,140 +45,146 @@ import { ContentManagementService } from '../../../services/content-management.s
providers: [SearchQueryBuilderService] providers: [SearchQueryBuilderService]
}) })
export class SearchResultsComponent extends PageComponent implements OnInit { export class SearchResultsComponent extends PageComponent implements OnInit {
@ViewChild('search')
search: AdfSearchComponent;
@ViewChild('search') @ViewChild('searchFilter')
search: AdfSearchComponent; searchFilter: SearchFilterComponent;
@ViewChild('searchFilter') searchedWord: string;
searchFilter: SearchFilterComponent; queryParamName = 'q';
data: NodePaging;
totalResults = 0;
hasSelectedFilters = false;
sorting = ['name', 'asc'];
isLoading = false;
searchedWord: string; constructor(
queryParamName = 'q'; private queryBuilder: SearchQueryBuilderService,
data: NodePaging; private route: ActivatedRoute,
totalResults = 0; store: Store<AppStore>,
hasSelectedFilters = false; extensions: AppExtensionService,
sorting = ['name', 'asc']; content: ContentManagementService
isLoading = false; ) {
super(store, extensions, content);
constructor( queryBuilder.paging = {
private queryBuilder: SearchQueryBuilderService, skipCount: 0,
private route: ActivatedRoute, maxItems: 25
store: Store<AppStore>, };
extensions: AppExtensionService, }
content: ContentManagementService
) {
super(store, extensions, content);
queryBuilder.paging = { ngOnInit() {
skipCount: 0, super.ngOnInit();
maxItems: 25
};
}
ngOnInit() { this.sorting = this.getSorting();
super.ngOnInit();
this.subscriptions.push(
this.queryBuilder.updated.subscribe(() => {
this.sorting = this.getSorting(); this.sorting = this.getSorting();
this.isLoading = true;
}),
this.subscriptions.push( this.queryBuilder.executed.subscribe(data => {
this.queryBuilder.updated.subscribe(() => { this.onSearchResultLoaded(data);
this.sorting = this.getSorting(); this.isLoading = false;
this.isLoading = true; })
}), );
this.queryBuilder.executed.subscribe(data => { if (this.route) {
this.onSearchResultLoaded(data); this.route.params.forEach((params: Params) => {
this.isLoading = false; this.searchedWord = params.hasOwnProperty(this.queryParamName)
}) ? params[this.queryParamName]
); : null;
const query = this.formatSearchQuery(this.searchedWord);
if (this.route) { if (query) {
this.route.params.forEach((params: Params) => { this.queryBuilder.userQuery = query;
this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null; this.queryBuilder.update();
const query = this.formatSearchQuery(this.searchedWord); } else {
this.queryBuilder.userQuery = null;
if (query) { this.queryBuilder.executed.next({
this.queryBuilder.userQuery = query; list: { pagination: { totalItems: 0 }, entries: [] }
this.queryBuilder.update(); });
} else {
this.queryBuilder.userQuery = null;
this.queryBuilder.executed.next( {list: { pagination: { totalItems: 0 }, entries: []}} );
}
});
} }
});
}
}
private formatSearchQuery(userInput: string) {
if (!userInput) {
return null;
} }
private formatSearchQuery(userInput: string) { const suffix = userInput.lastIndexOf('*') >= 0 ? '' : '*';
if (!userInput) { const query = `${userInput}${suffix} OR name:${userInput}${suffix}`;
return null;
}
const suffix = userInput.lastIndexOf('*') >= 0 ? '' : '*'; return query;
const query = `${userInput}${suffix} OR name:${userInput}${suffix}`; }
return query; onSearchResultLoaded(nodePaging: NodePaging) {
this.data = nodePaging;
this.totalResults = this.getNumberOfResults();
this.hasSelectedFilters = this.isFiltered();
}
getNumberOfResults() {
if (this.data && this.data.list && this.data.list.pagination) {
return this.data.list.pagination.totalItems;
}
return 0;
}
isFiltered(): boolean {
return (
this.searchFilter.selectedFacetQueries.length > 0 ||
this.searchFilter.selectedBuckets.length > 0 ||
this.hasCheckedCategories()
);
}
hasCheckedCategories() {
const checkedCategory = this.queryBuilder.categories.find(
category => !!this.queryBuilder.queryFragments[category.id]
);
return !!checkedCategory;
}
onPaginationChanged(pagination: Pagination) {
this.queryBuilder.paging = {
maxItems: pagination.maxItems,
skipCount: pagination.skipCount
};
this.queryBuilder.update();
}
private getSorting(): string[] {
const primary = this.queryBuilder.getPrimarySorting();
if (primary) {
return [primary.key, primary.ascending ? 'asc' : 'desc'];
} }
onSearchResultLoaded(nodePaging: NodePaging) { return ['name', 'asc'];
this.data = nodePaging; }
this.totalResults = this.getNumberOfResults();
this.hasSelectedFilters = this.isFiltered(); onNodeDoubleClick(node: MinimalNodeEntity) {
if (node && node.entry) {
if (node.entry.isFolder) {
this.store.dispatch(new NavigateToFolder(node));
return;
}
if (PageComponent.isLockedNode(node.entry)) {
event.preventDefault();
return;
}
this.showPreview(node);
} }
}
getNumberOfResults() { hideSearchFilter() {
if (this.data && this.data.list && this.data.list.pagination) { return !this.totalResults && !this.hasSelectedFilters;
return this.data.list.pagination.totalItems; }
}
return 0;
}
isFiltered(): boolean {
return this.searchFilter.selectedFacetQueries.length > 0
|| this.searchFilter.selectedBuckets.length > 0
|| this.hasCheckedCategories();
}
hasCheckedCategories() {
const checkedCategory = this.queryBuilder.categories
.find(category => !!this.queryBuilder.queryFragments[category.id]);
return !!checkedCategory;
}
onPaginationChanged(pagination: Pagination) {
this.queryBuilder.paging = {
maxItems: pagination.maxItems,
skipCount: pagination.skipCount
};
this.queryBuilder.update();
}
private getSorting(): string[] {
const primary = this.queryBuilder.getPrimarySorting();
if (primary) {
return [primary.key, primary.ascending ? 'asc' : 'desc'];
}
return ['name', 'asc'];
}
onNodeDoubleClick(node: MinimalNodeEntity) {
if (node && node.entry) {
if (node.entry.isFolder) {
this.store.dispatch(new NavigateToFolder(node));
return;
}
if (PageComponent.isLockedNode(node.entry)) {
event.preventDefault();
return;
}
this.showPreview(node);
}
}
hideSearchFilter() {
return !this.totalResults && !this.hasSelectedFilters;
}
} }

View File

@@ -1,70 +1,70 @@
@mixin aca-settings-theme($theme) { @mixin aca-settings-theme($theme) {
$background: map-get($theme, background); $background: map-get($theme, background);
$app-menu-height: 64px; $app-menu-height: 64px;
.aca-settings { .aca-settings {
.settings-input { .settings-input {
width: 50%; width: 50%;
} }
.settings-buttons { .settings-buttons {
text-align: right; text-align: right;
.mat-button { .mat-button {
text-transform: uppercase; text-transform: uppercase;
} }
} }
.app-menu { .app-menu {
height: $app-menu-height;
&.adf-toolbar {
.mat-toolbar {
background-color: inherit;
font-family: inherit;
min-height: $app-menu-height;
height: $app-menu-height;
.mat-toolbar-layout {
height: $app-menu-height; height: $app-menu-height;
&.adf-toolbar { .mat-toolbar-row {
.mat-toolbar { height: $app-menu-height;
background-color: inherit;
font-family: inherit;
min-height: $app-menu-height;
height: $app-menu-height;
.mat-toolbar-layout {
height: $app-menu-height;
.mat-toolbar-row {
height: $app-menu-height;
}
}
}
.adf-toolbar-divider {
margin-left: 5px;
margin-right: 5px;
& > div {
background-color: mat-color($background, card);
}
}
.adf-toolbar-title {
color: mat-color($background, card);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
}
.app-menu__title {
width: 100px;
height: 50px;
margin-left: 40px;
display: flex;
justify-content: center;
align-items: stretch;
&> img {
width: 100%;
object-fit: contain;
}
} }
}
} }
.adf-toolbar-divider {
margin-left: 5px;
margin-right: 5px;
& > div {
background-color: mat-color($background, card);
}
}
.adf-toolbar-title {
color: mat-color($background, card);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
}
.app-menu__title {
width: 100px;
height: 50px;
margin-left: 40px;
display: flex;
justify-content: center;
align-items: stretch;
& > img {
width: 100%;
object-fit: contain;
}
}
} }
}
} }

View File

@@ -24,86 +24,98 @@
*/ */
import { Component, ViewEncapsulation, OnInit } from '@angular/core'; import { Component, ViewEncapsulation, OnInit } from '@angular/core';
import { AppConfigService, StorageService, SettingsService } from '@alfresco/adf-core'; import {
AppConfigService,
StorageService,
SettingsService
} from '@alfresco/adf-core';
import { Validators, FormGroup, FormBuilder } from '@angular/forms'; import { Validators, FormGroup, FormBuilder } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states'; import { AppStore } from '../../store/states';
import { appLanguagePicker, selectHeaderColor, selectAppName, selectUser } from '../../store/selectors/app.selectors'; import {
appLanguagePicker,
selectHeaderColor,
selectAppName,
selectUser
} from '../../store/selectors/app.selectors';
import { MatCheckboxChange } from '@angular/material'; import { MatCheckboxChange } from '@angular/material';
import { SetLanguagePickerAction } from '../../store/actions'; import { SetLanguagePickerAction } from '../../store/actions';
import { ProfileState } from '@alfresco/adf-extensions'; import { ProfileState } from '@alfresco/adf-extensions';
@Component({ @Component({
selector: 'aca-settings', selector: 'aca-settings',
templateUrl: './settings.component.html', templateUrl: './settings.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { class: 'aca-settings' } host: { class: 'aca-settings' }
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit {
private defaultPath = '/assets/images/alfresco-logo-white.svg';
private defaultPath = '/assets/images/alfresco-logo-white.svg'; form: FormGroup;
form: FormGroup; profile$: Observable<ProfileState>;
appName$: Observable<string>;
headerColor$: Observable<string>;
languagePicker$: Observable<boolean>;
experimental: Array<{ key: string; value: boolean }> = [];
profile$: Observable<ProfileState>; constructor(
appName$: Observable<string>; private store: Store<AppStore>,
headerColor$: Observable<string>; private appConfig: AppConfigService,
languagePicker$: Observable<boolean>; private settingsService: SettingsService,
experimental: Array<{ key: string, value: boolean }> = []; private storage: StorageService,
private fb: FormBuilder
) {
this.profile$ = store.select(selectUser);
this.appName$ = store.select(selectAppName);
this.languagePicker$ = store.select(appLanguagePicker);
this.headerColor$ = store.select(selectHeaderColor);
}
constructor( get logo() {
private store: Store<AppStore>, return this.appConfig.get('application.logo', this.defaultPath);
private appConfig: AppConfigService, }
private settingsService: SettingsService,
private storage: StorageService,
private fb: FormBuilder) {
this.profile$ = store.select(selectUser);
this.appName$ = store.select(selectAppName);
this.languagePicker$ = store.select(appLanguagePicker);
this.headerColor$ = store.select(selectHeaderColor);
}
get logo() { ngOnInit() {
return this.appConfig.get('application.logo', this.defaultPath); this.form = this.fb.group({
ecmHost: [
'',
[Validators.required, Validators.pattern('^(http|https)://.*[^/]$')]
]
});
this.reset();
const settings = this.appConfig.get('experimental');
this.experimental = Object.keys(settings).map(key => {
const value = this.appConfig.get(`experimental.${key}`);
return {
key,
value: value === true || value === 'true'
};
});
}
apply(model: any, isValid: boolean) {
if (isValid) {
this.storage.setItem('ecmHost', model.ecmHost);
// window.location.reload(true);
} }
}
ngOnInit() { reset() {
this.form = this.fb.group({ this.form.reset({
ecmHost: ['', [Validators.required, Validators.pattern('^(http|https):\/\/.*[^/]$')]] ecmHost: this.storage.getItem('ecmHost') || this.settingsService.ecmHost
}); });
}
this.reset(); onLanguagePickerValueChanged(event: MatCheckboxChange) {
this.storage.setItem('languagePicker', event.checked.toString());
this.store.dispatch(new SetLanguagePickerAction(event.checked));
}
const settings = this.appConfig.get('experimental'); onToggleExperimentalFeature(key: string, event: MatCheckboxChange) {
this.experimental = Object.keys(settings).map(key => { this.storage.setItem(`experimental.${key}`, event.checked.toString());
const value = this.appConfig.get(`experimental.${key}`); }
return {
key,
value: (value === true || value === 'true')
};
});
}
apply(model: any, isValid: boolean) {
if (isValid) {
this.storage.setItem('ecmHost', model.ecmHost);
// window.location.reload(true);
}
}
reset() {
this.form.reset({
ecmHost: this.storage.getItem('ecmHost') || this.settingsService.ecmHost
});
}
onLanguagePickerValueChanged(event: MatCheckboxChange) {
this.storage.setItem('languagePicker', event.checked.toString());
this.store.dispatch(new SetLanguagePickerAction(event.checked));
}
onToggleExperimentalFeature(key: string, event: MatCheckboxChange) {
this.storage.setItem(`experimental.${key}`, event.checked.toString());
}
} }

View File

@@ -30,18 +30,14 @@ import { CommonModule } from '@angular/common';
import { CoreModule } from '@alfresco/adf-core'; import { CoreModule } from '@alfresco/adf-core';
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: SettingsComponent component: SettingsComponent
} }
]; ];
@NgModule({ @NgModule({
imports: [ imports: [CommonModule, CoreModule.forChild(), RouterModule.forChild(routes)],
CommonModule, declarations: [SettingsComponent]
CoreModule.forChild(),
RouterModule.forChild(routes)
],
declarations: [SettingsComponent]
}) })
export class AppSettingsModule {} export class AppSettingsModule {}

View File

@@ -26,8 +26,12 @@
import { TestBed, ComponentFixture } from '@angular/core/testing'; import { TestBed, ComponentFixture } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { import {
AlfrescoApiService, AlfrescoApiService,
TimeAgoPipe, NodeNameTooltipPipe, NodeFavoriteDirective, DataTableComponent, AppConfigPipe TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DataTableComponent,
AppConfigPipe
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services'; import { DocumentListComponent } from '@alfresco/adf-content-services';
import { ContentManagementService } from '../../services/content-management.service'; import { ContentManagementService } from '../../services/content-management.service';
@@ -36,86 +40,87 @@ import { AppTestingModule } from '../../testing/app-testing.module';
import { ExperimentalDirective } from '../../directives/experimental.directive'; import { ExperimentalDirective } from '../../directives/experimental.directive';
describe('SharedFilesComponent', () => { describe('SharedFilesComponent', () => {
let fixture: ComponentFixture<SharedFilesComponent>; let fixture: ComponentFixture<SharedFilesComponent>;
let component: SharedFilesComponent; let component: SharedFilesComponent;
let contentService: ContentManagementService; let contentService: ContentManagementService;
let alfrescoApi: AlfrescoApiService; let alfrescoApi: AlfrescoApiService;
let page; let page;
beforeEach(() => {
page = {
list: {
entries: [{ entry: { id: 1 } }, { entry: { id: 2 } }],
pagination: { data: 'data' }
}
};
});
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [
DataTableComponent,
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DocumentListComponent,
SharedFilesComponent,
AppConfigPipe,
ExperimentalDirective
],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(SharedFilesComponent);
component = fixture.componentInstance;
contentService = TestBed.get(ContentManagementService);
alfrescoApi = TestBed.get(AlfrescoApiService);
alfrescoApi.reset();
spyOn(alfrescoApi.sharedLinksApi, 'findSharedLinks').and.returnValue(
Promise.resolve(page)
);
});
describe('OnInit', () => {
beforeEach(() => { beforeEach(() => {
page = { spyOn(component, 'reload').and.callFake(val => val);
list: {
entries: [ { entry: { id: 1 } }, { entry: { id: 2 } } ],
pagination: { data: 'data'}
}
};
}); });
beforeEach(() => { it('should refresh on deleteNode event', () => {
TestBed fixture.detectChanges();
.configureTestingModule({
imports: [ AppTestingModule ],
declarations: [
DataTableComponent,
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DocumentListComponent,
SharedFilesComponent,
AppConfigPipe,
ExperimentalDirective
],
schemas: [ NO_ERRORS_SCHEMA ]
});
fixture = TestBed.createComponent(SharedFilesComponent); contentService.nodesDeleted.next();
component = fixture.componentInstance;
contentService = TestBed.get(ContentManagementService); expect(component.reload).toHaveBeenCalled();
alfrescoApi = TestBed.get(AlfrescoApiService);
alfrescoApi.reset();
spyOn(alfrescoApi.sharedLinksApi, 'findSharedLinks').and.returnValue(Promise.resolve(page));
}); });
describe('OnInit', () => { it('should refresh on restoreNode event', () => {
beforeEach(() => { fixture.detectChanges();
spyOn(component, 'reload').and.callFake(val => val);
});
it('should refresh on deleteNode event', () => { contentService.nodesRestored.next();
fixture.detectChanges();
contentService.nodesDeleted.next(); expect(component.reload).toHaveBeenCalled();
expect(component.reload).toHaveBeenCalled();
});
it('should refresh on restoreNode event', () => {
fixture.detectChanges();
contentService.nodesRestored.next();
expect(component.reload).toHaveBeenCalled();
});
it('should reload on move node event', () => {
fixture.detectChanges();
contentService.nodesMoved.next();
expect(component.reload).toHaveBeenCalled();
});
}); });
describe('refresh', () => { it('should reload on move node event', () => {
it('should call document list reload', () => { fixture.detectChanges();
spyOn(component.documentList, 'reload');
fixture.detectChanges();
component.reload(); contentService.nodesMoved.next();
expect(component.documentList.reload).toHaveBeenCalled(); expect(component.reload).toHaveBeenCalled();
});
}); });
});
describe('refresh', () => {
it('should call document list reload', () => {
spyOn(component.documentList, 'reload');
fixture.detectChanges();
component.reload();
expect(component.documentList.reload).toHaveBeenCalled();
});
});
}); });

View File

@@ -32,37 +32,34 @@ import { AppStore } from '../../store/states/app.state';
import { AppExtensionService } from '../../extensions/extension.service'; import { AppExtensionService } from '../../extensions/extension.service';
@Component({ @Component({
templateUrl: './shared-files.component.html' templateUrl: './shared-files.component.html'
}) })
export class SharedFilesComponent extends PageComponent implements OnInit { export class SharedFilesComponent extends PageComponent implements OnInit {
isSmallScreen = false; isSmallScreen = false;
constructor( constructor(
store: Store<AppStore>, store: Store<AppStore>,
extensions: AppExtensionService, extensions: AppExtensionService,
content: ContentManagementService, content: ContentManagementService,
private breakpointObserver: BreakpointObserver private breakpointObserver: BreakpointObserver
) { ) {
super(store, extensions, content); super(store, extensions, content);
} }
ngOnInit() { ngOnInit() {
super.ngOnInit(); super.ngOnInit();
this.subscriptions = this.subscriptions.concat([ this.subscriptions = this.subscriptions.concat([
this.content.nodesDeleted.subscribe(() => this.reload()), this.content.nodesDeleted.subscribe(() => this.reload()),
this.content.nodesMoved.subscribe(() => this.reload()), this.content.nodesMoved.subscribe(() => this.reload()),
this.content.nodesRestored.subscribe(() => this.reload()), this.content.nodesRestored.subscribe(() => this.reload()),
this.content.linksUnshared.subscribe(() => this.reload()), this.content.linksUnshared.subscribe(() => this.reload()),
this.breakpointObserver this.breakpointObserver
.observe([ .observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])
Breakpoints.HandsetPortrait, .subscribe(result => {
Breakpoints.HandsetLandscape this.isSmallScreen = result.matches;
]) })
.subscribe(result => { ]);
this.isSmallScreen = result.matches; }
})
]);
}
} }

View File

@@ -1,4 +1,4 @@
.app-shared-link-view { .app-shared-link-view {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }

View File

@@ -2,23 +2,21 @@ import { Component, ViewEncapsulation, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
@Component({ @Component({
selector: 'app-shared-link-view', selector: 'app-shared-link-view',
templateUrl: 'shared-link-view.component.html', templateUrl: 'shared-link-view.component.html',
styleUrls: [ 'shared-link-view.component.scss' ], styleUrls: ['shared-link-view.component.scss'],
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
// tslint:disable-next-line:use-host-property-decorator // tslint:disable-next-line:use-host-property-decorator
host: { 'class': 'app-shared-link-view' } host: { class: 'app-shared-link-view' }
}) })
export class SharedLinkViewComponent implements OnInit { export class SharedLinkViewComponent implements OnInit {
sharedLinkId: string = null;
sharedLinkId: string = null; constructor(private route: ActivatedRoute) {}
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.route.params.subscribe(params => {
this.sharedLinkId = params.id;
});
}
ngOnInit() {
this.route.params.subscribe(params => {
this.sharedLinkId = params.id;
});
}
} }

View File

@@ -1,51 +1,51 @@
.sidenav { .sidenav {
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
&__section:last-child {
border-bottom: 0;
}
&_action-menu {
display: flex; display: flex;
flex: 1; padding: 16px 24px;
height: 40px;
justify-content: center;
align-items: center;
}
&__section {
padding: 8px 14px;
position: relative;
}
&-menu {
display: inline-flex;
flex-direction: column; flex-direction: column;
height: 100%; padding: 0;
margin: 0;
list-style-type: none;
}
&__section:last-child { &-menu__item {
border-bottom: 0; padding: 12px 0;
} flex-direction: row;
display: flex;
align-items: center;
text-decoration: none;
text-decoration: none;
height: 24px;
}
&_action-menu { .menu__item--label {
display: flex; cursor: pointer;
padding: 16px 24px; width: 240px;
height: 40px; padding-left: 10px;
justify-content: center; }
align-items: center;
}
&__section { .menu__item--label:focus {
padding: 8px 14px; outline: none;
position: relative; }
}
&-menu {
display: inline-flex;
flex-direction: column;
padding: 0;
margin: 0;
list-style-type: none;
}
&-menu__item {
padding: 12px 0;
flex-direction: row;
display: flex;
align-items: center;
text-decoration: none;
text-decoration: none;
height: 24px;
}
.menu__item--label {
cursor: pointer;
width: 240px;
padding-left: 10px;
}
.menu__item--label:focus {
outline: none;
}
} }

View File

@@ -32,29 +32,23 @@ import { AppTestingModule } from '../../testing/app-testing.module';
import { ExperimentalDirective } from '../../directives/experimental.directive'; import { ExperimentalDirective } from '../../directives/experimental.directive';
describe('SidenavComponent', () => { describe('SidenavComponent', () => {
let fixture: ComponentFixture<SidenavComponent>; let fixture: ComponentFixture<SidenavComponent>;
let component: SidenavComponent; let component: SidenavComponent;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [AppTestingModule, EffectsModule.forRoot([NodeEffects])],
AppTestingModule, declarations: [SidenavComponent, ExperimentalDirective],
EffectsModule.forRoot([NodeEffects]) schemas: [NO_ERRORS_SCHEMA]
], })
declarations: [ .compileComponents()
SidenavComponent, .then(() => {
ExperimentalDirective fixture = TestBed.createComponent(SidenavComponent);
], component = fixture.componentInstance;
schemas: [ NO_ERRORS_SCHEMA ] });
}) }));
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(SidenavComponent);
component = fixture.componentInstance;
});
}));
it('should be created', () => { it('should be created', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@@ -1,35 +1,34 @@
@mixin sidenav-component-theme($theme) { @mixin sidenav-component-theme($theme) {
$primary: map-get($theme, primary); $primary: map-get($theme, primary);
$accent: map-get($theme, accent); $accent: map-get($theme, accent);
$foreground: map-get($theme, foreground); $foreground: map-get($theme, foreground);
$background: map-get($theme, background); $background: map-get($theme, background);
$border: 1px solid mat-color($foreground, divider, .07); $border: 1px solid mat-color($foreground, divider, 0.07);
.sidenav { .sidenav {
@include angular-material-theme($theme); @include angular-material-theme($theme);
background-color: mat-color($background, background); background-color: mat-color($background, background);
.adf-sidebar-action-menu-button { .adf-sidebar-action-menu-button {
background-color: mat-color($accent); background-color: mat-color($accent);
} }
&__section { &__section {
border-bottom: $border; border-bottom: $border;
} }
.menu__item--label:not(.menu__item--active):hover { .menu__item--label:not(.menu__item--active):hover {
color: mat-color($foreground, text); color: mat-color($foreground, text);
} }
.menu__item--active { .menu__item--active {
color: mat-color($accent); color: mat-color($accent);
} }
.menu__item--default {
color: mat-color($primary, .87);
}
.menu__item--default {
color: mat-color($primary, 0.87);
} }
} }
}

View File

@@ -33,39 +33,40 @@ import { takeUntil } from 'rxjs/operators';
import { ContentActionRef, NavBarGroupRef } from '@alfresco/adf-extensions'; import { ContentActionRef, NavBarGroupRef } from '@alfresco/adf-extensions';
@Component({ @Component({
selector: 'app-sidenav', selector: 'app-sidenav',
templateUrl: './sidenav.component.html', templateUrl: './sidenav.component.html',
styleUrls: ['./sidenav.component.scss'] styleUrls: ['./sidenav.component.scss']
}) })
export class SidenavComponent implements OnInit, OnDestroy { export class SidenavComponent implements OnInit, OnDestroy {
@Input() showLabel: boolean; @Input()
showLabel: boolean;
groups: Array<NavBarGroupRef> = []; groups: Array<NavBarGroupRef> = [];
createActions: Array<ContentActionRef> = []; createActions: Array<ContentActionRef> = [];
onDestroy$: Subject<boolean> = new Subject<boolean>(); onDestroy$: Subject<boolean> = new Subject<boolean>();
constructor( constructor(
private store: Store<AppStore>, private store: Store<AppStore>,
private extensions: AppExtensionService private extensions: AppExtensionService
) {} ) {}
ngOnInit() { ngOnInit() {
this.groups = this.extensions.getNavigationGroups(); this.groups = this.extensions.getNavigationGroups();
this.store this.store
.select(currentFolder) .select(currentFolder)
.pipe(takeUntil(this.onDestroy$)) .pipe(takeUntil(this.onDestroy$))
.subscribe(() => { .subscribe(() => {
this.createActions = this.extensions.getCreateActions(); this.createActions = this.extensions.getCreateActions();
}); });
} }
ngOnDestroy() { ngOnDestroy() {
this.onDestroy$.next(true); this.onDestroy$.next(true);
this.onDestroy$.complete(); this.onDestroy$.complete();
} }
trackById(index: number, obj: { id: string }) { trackById(index: number, obj: { id: string }) {
return obj.id; return obj.id;
} }
} }

View File

@@ -31,26 +31,25 @@ import { documentDisplayMode } from '../../../store/selectors/app.selectors';
import { ToggleDocumentDisplayMode } from '../../../store/actions'; import { ToggleDocumentDisplayMode } from '../../../store/actions';
@Component({ @Component({
selector: 'app-document-display-mode', selector: 'app-document-display-mode',
template: ` template: `
<button <button
mat-icon-button mat-icon-button
color="primary" color="primary"
(click)="onClick()"> (click)="onClick()">
<mat-icon *ngIf="(displayMode$ | async) === 'list'">view_comfy</mat-icon> <mat-icon *ngIf="(displayMode$ | async) === 'list'">view_comfy</mat-icon>
<mat-icon *ngIf="(displayMode$ | async) === 'gallery'">list</mat-icon> <mat-icon *ngIf="(displayMode$ | async) === 'gallery'">list</mat-icon>
</button> </button>
` `
}) })
export class DocumentDisplayModeComponent { export class DocumentDisplayModeComponent {
displayMode$: Observable<string>;
displayMode$: Observable<string>; constructor(private store: Store<AppStore>) {
this.displayMode$ = store.select(documentDisplayMode);
}
constructor(private store: Store<AppStore>) { onClick() {
this.displayMode$ = store.select(documentDisplayMode); this.store.dispatch(new ToggleDocumentDisplayMode());
} }
onClick() {
this.store.dispatch(new ToggleDocumentDisplayMode());
}
} }

View File

@@ -32,8 +32,8 @@ import { SelectionState } from '@alfresco/adf-extensions';
import { ContentManagementService } from '../../../services/content-management.service'; import { ContentManagementService } from '../../../services/content-management.service';
@Component({ @Component({
selector: 'app-toggle-favorite', selector: 'app-toggle-favorite',
template: ` template: `
<button <button
mat-menu-item mat-menu-item
#favorites="adfFavorite" #favorites="adfFavorite"
@@ -46,16 +46,16 @@ import { ContentManagementService } from '../../../services/content-management.s
` `
}) })
export class ToggleFavoriteComponent { export class ToggleFavoriteComponent {
selection$: Observable<SelectionState>;
selection$: Observable<SelectionState>; constructor(
private store: Store<AppStore>,
private content: ContentManagementService
) {
this.selection$ = this.store.select(appSelection);
}
constructor( onToggleEvent() {
private store: Store<AppStore>, this.content.favoriteToggle.next();
private content: ContentManagementService) { }
this.selection$ = this.store.select(appSelection);
}
onToggleEvent() {
this.content.favoriteToggle.next();
}
} }

View File

@@ -31,8 +31,8 @@ import { infoDrawerOpened } from '../../../store/selectors/app.selectors';
import { ToggleInfoDrawerAction } from '../../../store/actions'; import { ToggleInfoDrawerAction } from '../../../store/actions';
@Component({ @Component({
selector: 'app-toggle-info-drawer', selector: 'app-toggle-info-drawer',
template: ` template: `
<button <button
mat-icon-button mat-icon-button
[color]="(infoDrawerOpened$ | async) ? 'accent' : 'primary'" [color]="(infoDrawerOpened$ | async) ? 'accent' : 'primary'"
@@ -43,13 +43,13 @@ import { ToggleInfoDrawerAction } from '../../../store/actions';
` `
}) })
export class ToggleInfoDrawerComponent { export class ToggleInfoDrawerComponent {
infoDrawerOpened$: Observable<boolean>; infoDrawerOpened$: Observable<boolean>;
constructor(private store: Store<AppStore>) { constructor(private store: Store<AppStore>) {
this.infoDrawerOpened$ = this.store.select(infoDrawerOpened); this.infoDrawerOpened$ = this.store.select(infoDrawerOpened);
} }
onClick() { onClick() {
this.store.dispatch(new ToggleInfoDrawerAction()); this.store.dispatch(new ToggleInfoDrawerAction());
} }
} }

View File

@@ -24,10 +24,10 @@
*/ */
import { import {
Component, Component,
ViewEncapsulation, ViewEncapsulation,
ChangeDetectionStrategy, ChangeDetectionStrategy,
Input Input
} from '@angular/core'; } from '@angular/core';
import { AppStore } from '../../../store/states'; import { AppStore } from '../../../store/states';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@@ -35,22 +35,24 @@ import { ContentActionRef } from '@alfresco/adf-extensions';
import { AppExtensionService } from '../../../extensions/extension.service'; import { AppExtensionService } from '../../../extensions/extension.service';
@Component({ @Component({
selector: 'aca-toolbar-action', selector: 'aca-toolbar-action',
templateUrl: './toolbar-action.component.html', templateUrl: './toolbar-action.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'aca-toolbar-action' } host: { class: 'aca-toolbar-action' }
}) })
export class ToolbarActionComponent { export class ToolbarActionComponent {
@Input() type = 'icon-button'; @Input()
@Input() entry: ContentActionRef; type = 'icon-button';
@Input()
entry: ContentActionRef;
constructor( constructor(
protected store: Store<AppStore>, protected store: Store<AppStore>,
protected extensions: AppExtensionService protected extensions: AppExtensionService
) {} ) {}
trackByActionId(index: number, action: ContentActionRef) { trackByActionId(index: number, action: ContentActionRef) {
return action.id; return action.id;
} }
} }

View File

@@ -32,31 +32,33 @@ import { take } from 'rxjs/operators';
import { AppExtensionService } from '../../../extensions/extension.service'; import { AppExtensionService } from '../../../extensions/extension.service';
export enum ToolbarButtonType { export enum ToolbarButtonType {
ICON_BUTTON = 'icon-button', ICON_BUTTON = 'icon-button',
MENU_ITEM = 'menu-item' MENU_ITEM = 'menu-item'
} }
@Component({ @Component({
selector: 'app-toolbar-button', selector: 'app-toolbar-button',
templateUrl: 'toolbar-button.component.html' templateUrl: 'toolbar-button.component.html'
}) })
export class ToolbarButtonComponent { export class ToolbarButtonComponent {
@Input() type: ToolbarButtonType = ToolbarButtonType.ICON_BUTTON; @Input()
@Input() actionRef: ContentActionRef; type: ToolbarButtonType = ToolbarButtonType.ICON_BUTTON;
@Input()
actionRef: ContentActionRef;
constructor( constructor(
protected store: Store<AppStore>, protected store: Store<AppStore>,
private extensions: AppExtensionService private extensions: AppExtensionService
) {} ) {}
runAction() { runAction() {
this.store this.store
.select(appSelection) .select(appSelection)
.pipe(take(1)) .pipe(take(1))
.subscribe(selection => { .subscribe(selection => {
this.extensions.runActionById(this.actionRef.actions.click, { this.extensions.runActionById(this.actionRef.actions.click, {
selection selection
}); });
}); });
} }
} }

View File

@@ -34,23 +34,19 @@ import { ToolbarActionComponent } from './toolbar-action/toolbar-action.componen
import { ExtensionsModule } from '@alfresco/adf-extensions'; import { ExtensionsModule } from '@alfresco/adf-extensions';
export function components() { export function components() {
return [ return [
DocumentDisplayModeComponent, DocumentDisplayModeComponent,
ToggleFavoriteComponent, ToggleFavoriteComponent,
ToggleInfoDrawerComponent, ToggleInfoDrawerComponent,
ToolbarButtonComponent, ToolbarButtonComponent,
ToolbarActionComponent ToolbarActionComponent
]; ];
} }
@NgModule({ @NgModule({
imports: [ imports: [CommonModule, CoreModule.forChild(), ExtensionsModule.forChild()],
CommonModule, declarations: components(),
CoreModule.forChild(), exports: components(),
ExtensionsModule.forChild() entryComponents: components()
],
declarations: components(),
exports: components(),
entryComponents: components()
}) })
export class AppToolbarModule {} export class AppToolbarModule {}

View File

@@ -25,9 +25,12 @@
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, ComponentFixture } from '@angular/core/testing'; import { TestBed, ComponentFixture } from '@angular/core/testing';
import { import {
AlfrescoApiService, AlfrescoApiService,
TimeAgoPipe, NodeNameTooltipPipe, TimeAgoPipe,
NodeFavoriteDirective, DataTableComponent, AppConfigPipe NodeNameTooltipPipe,
NodeFavoriteDirective,
DataTableComponent,
AppConfigPipe
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services'; import { DocumentListComponent } from '@alfresco/adf-content-services';
import { ContentManagementService } from '../../services/content-management.service'; import { ContentManagementService } from '../../services/content-management.service';
@@ -36,74 +39,76 @@ import { AppTestingModule } from '../../testing/app-testing.module';
import { ExperimentalDirective } from '../../directives/experimental.directive'; import { ExperimentalDirective } from '../../directives/experimental.directive';
describe('TrashcanComponent', () => { describe('TrashcanComponent', () => {
let fixture: ComponentFixture<TrashcanComponent>; let fixture: ComponentFixture<TrashcanComponent>;
let component: TrashcanComponent; let component: TrashcanComponent;
let alfrescoApi: AlfrescoApiService; let alfrescoApi: AlfrescoApiService;
let contentService: ContentManagementService; let contentService: ContentManagementService;
let page; let page;
beforeEach(() => { beforeEach(() => {
page = { page = {
list: { list: {
entries: [ { entry: { id: 1 } }, { entry: { id: 2 } } ], entries: [{ entry: { id: 1 } }, { entry: { id: 2 } }],
pagination: { data: 'data'} pagination: { data: 'data' }
} }
}; };
});
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [
DataTableComponent,
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DocumentListComponent,
TrashcanComponent,
AppConfigPipe,
ExperimentalDirective
],
schemas: [NO_ERRORS_SCHEMA]
}); });
beforeEach(() => { fixture = TestBed.createComponent(TrashcanComponent);
TestBed.configureTestingModule({ component = fixture.componentInstance;
imports: [ AppTestingModule ],
declarations: [
DataTableComponent,
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
DocumentListComponent,
TrashcanComponent,
AppConfigPipe,
ExperimentalDirective
],
schemas: [ NO_ERRORS_SCHEMA ]
});
fixture = TestBed.createComponent(TrashcanComponent); alfrescoApi = TestBed.get(AlfrescoApiService);
component = fixture.componentInstance; alfrescoApi.reset();
contentService = TestBed.get(ContentManagementService);
alfrescoApi = TestBed.get(AlfrescoApiService); component.documentList = <any>{
alfrescoApi.reset(); reload: jasmine.createSpy('reload'),
contentService = TestBed.get(ContentManagementService); resetSelection: jasmine.createSpy('resetSelection')
};
});
component.documentList = <any> { beforeEach(() => {
reload: jasmine.createSpy('reload'), spyOn(alfrescoApi.nodesApi, 'getDeletedNodes').and.returnValue(
resetSelection: jasmine.createSpy('resetSelection') Promise.resolve(page)
}; );
});
describe('onRestoreNode()', () => {
it('should call refresh()', () => {
spyOn(component, 'reload');
fixture.detectChanges();
contentService.nodesRestored.next();
expect(component.reload).toHaveBeenCalled();
});
});
describe('refresh()', () => {
it('calls child component to reload', () => {
component.reload();
expect(component.documentList.reload).toHaveBeenCalled();
}); });
beforeEach(() => { it('calls child component to reset selection', () => {
spyOn(alfrescoApi.nodesApi, 'getDeletedNodes').and.returnValue(Promise.resolve(page)); component.reload();
}); expect(component.documentList.resetSelection).toHaveBeenCalled();
describe('onRestoreNode()', () => {
it('should call refresh()', () => {
spyOn(component, 'reload');
fixture.detectChanges();
contentService.nodesRestored.next();
expect(component.reload).toHaveBeenCalled();
});
});
describe('refresh()', () => {
it('calls child component to reload', () => {
component.reload();
expect(component.documentList.reload).toHaveBeenCalled();
});
it('calls child component to reset selection', () => {
component.reload();
expect(component.documentList.resetSelection).toHaveBeenCalled();
});
}); });
});
}); });

View File

@@ -35,36 +35,35 @@ import { Observable } from 'rxjs';
import { ProfileState } from '@alfresco/adf-extensions'; import { ProfileState } from '@alfresco/adf-extensions';
@Component({ @Component({
templateUrl: './trashcan.component.html' templateUrl: './trashcan.component.html'
}) })
export class TrashcanComponent extends PageComponent implements OnInit { export class TrashcanComponent extends PageComponent implements OnInit {
isSmallScreen = false; isSmallScreen = false;
user$: Observable<ProfileState>; user$: Observable<ProfileState>;
constructor(content: ContentManagementService, constructor(
extensions: AppExtensionService, content: ContentManagementService,
store: Store<AppStore>, extensions: AppExtensionService,
private breakpointObserver: BreakpointObserver) { store: Store<AppStore>,
super(store, extensions, content); private breakpointObserver: BreakpointObserver
this.user$ = this.store.select(selectUser); ) {
} super(store, extensions, content);
this.user$ = this.store.select(selectUser);
}
ngOnInit() { ngOnInit() {
super.ngOnInit(); super.ngOnInit();
this.subscriptions.push( this.subscriptions.push(
this.content.nodesRestored.subscribe(() => this.reload()), this.content.nodesRestored.subscribe(() => this.reload()),
this.content.nodesPurged.subscribe(() => this.reload()), this.content.nodesPurged.subscribe(() => this.reload()),
this.content.nodesRestored.subscribe(() => this.reload()), this.content.nodesRestored.subscribe(() => this.reload()),
this.breakpointObserver this.breakpointObserver
.observe([ .observe([Breakpoints.HandsetPortrait, Breakpoints.HandsetLandscape])
Breakpoints.HandsetPortrait, .subscribe(result => {
Breakpoints.HandsetLandscape this.isSmallScreen = result.matches;
]) })
.subscribe(result => { );
this.isSmallScreen = result.matches; }
}) }
);
}
}

View File

@@ -19,33 +19,35 @@ import { AbstractControl, FormControl } from '@angular/forms';
import { ContentApiService } from '../../services/content-api.service'; import { ContentApiService } from '../../services/content-api.service';
export class SiteIdValidator { export class SiteIdValidator {
static createValidator(contentApiService: ContentApiService) { static createValidator(contentApiService: ContentApiService) {
let timer; let timer;
return (control: AbstractControl) => { return (control: AbstractControl) => {
if (timer) { if (timer) {
clearTimeout(timer); clearTimeout(timer);
} }
return new Promise(resolve => { return new Promise(resolve => {
timer = setTimeout(() => { timer = setTimeout(() => {
return contentApiService return contentApiService
.getSite(control.value) .getSite(control.value)
.subscribe( .subscribe(
() => resolve({ message: 'LIBRARY.ERRORS.EXISTENT_SITE' }), () => resolve({ message: 'LIBRARY.ERRORS.EXISTENT_SITE' }),
() => resolve(null) () => resolve(null)
); );
}, 300); }, 300);
}); });
}; };
} }
} }
export function forbidSpecialCharacters({ value }: FormControl) { export function forbidSpecialCharacters({ value }: FormControl) {
const validCharacters: RegExp = /[^A-Za-z0-9-]/; const validCharacters: RegExp = /[^A-Za-z0-9-]/;
const isValid: boolean = !validCharacters.test(value); const isValid: boolean = !validCharacters.test(value);
return (isValid) ? null : { return isValid
? null
: {
message: 'LIBRARY.ERRORS.ILLEGAL_CHARACTERS' message: 'LIBRARY.ERRORS.ILLEGAL_CHARACTERS'
}; };
} }

View File

@@ -1,20 +1,19 @@
.mat-radio-group { .mat-radio-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0 0 20px 0; margin: 0 0 20px 0;
} }
.mat-radio-group .mat-radio-button { .mat-radio-group .mat-radio-button {
margin: 10px 0; margin: 10px 0;
} }
.mat-form-field { .mat-form-field {
width: 100%; width: 100%;
} }
.actions-buttons { .actions-buttons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
} }

View File

@@ -24,129 +24,139 @@ import { ContentApiService } from '../../services/content-api.service';
import { SiteIdValidator, forbidSpecialCharacters } from './form.validators'; import { SiteIdValidator, forbidSpecialCharacters } from './form.validators';
import { debounceTime } from 'rxjs/operators'; import { debounceTime } from 'rxjs/operators';
@Component({ @Component({
selector: 'app-library-dialog', selector: 'app-library-dialog',
styleUrls: ['./library.dialog.scss'], styleUrls: ['./library.dialog.scss'],
templateUrl: './library.dialog.html' templateUrl: './library.dialog.html'
}) })
export class LibraryDialogComponent implements OnInit { export class LibraryDialogComponent implements OnInit {
@Output() @Output()
error: EventEmitter<any> = new EventEmitter<any>(); error: EventEmitter<any> = new EventEmitter<any>();
@Output() @Output()
success: EventEmitter<any> = new EventEmitter<any>(); success: EventEmitter<any> = new EventEmitter<any>();
createTitle = 'LIBRARY.DIALOG.CREATE_TITLE'; createTitle = 'LIBRARY.DIALOG.CREATE_TITLE';
form: FormGroup; form: FormGroup;
visibilityOption: any; visibilityOption: any;
visibilityOptions = [ visibilityOptions = [
{ value: 'PUBLIC', label: 'LIBRARY.VISIBILITY.PUBLIC', disabled: false }, { value: 'PUBLIC', label: 'LIBRARY.VISIBILITY.PUBLIC', disabled: false },
{ value: 'PRIVATE', label: 'LIBRARY.VISIBILITY.PRIVATE', disabled: false }, { value: 'PRIVATE', label: 'LIBRARY.VISIBILITY.PRIVATE', disabled: false },
{ value: 'MODERATED', label: 'LIBRARY.VISIBILITY.MODERATED', disabled: false } {
]; value: 'MODERATED',
label: 'LIBRARY.VISIBILITY.MODERATED',
constructor( disabled: false
private formBuilder: FormBuilder,
private dialog: MatDialogRef<LibraryDialogComponent>,
private contentApi: ContentApiService
) {}
ngOnInit() {
const validators = {
id: [ Validators.required, Validators.maxLength(72), forbidSpecialCharacters ],
title: [ Validators.required, Validators.maxLength(256) ],
description: [ Validators.maxLength(512) ]
};
this.form = this.formBuilder.group({
title: ['', validators.title ],
id: [ '', validators.id, SiteIdValidator.createValidator(this.contentApi) ],
description: [ '', validators.description ],
});
this.visibilityOption = this.visibilityOptions[0].value;
this.form.controls['title'].valueChanges
.pipe(debounceTime(300))
.subscribe((titleValue: string) => {
if (!titleValue.trim().length) {
return;
}
if (!this.form.controls['id'].dirty) {
this.form.patchValue({ id: this.sanitize(titleValue.trim()) });
this.form.controls['id'].markAsTouched();
}
});
} }
];
get title(): string { constructor(
const { title } = this.form.value; private formBuilder: FormBuilder,
private dialog: MatDialogRef<LibraryDialogComponent>,
private contentApi: ContentApiService
) {}
return (title || '').trim(); ngOnInit() {
} const validators = {
id: [
Validators.required,
Validators.maxLength(72),
forbidSpecialCharacters
],
title: [Validators.required, Validators.maxLength(256)],
description: [Validators.maxLength(512)]
};
get id(): string { this.form = this.formBuilder.group({
const { id } = this.form.value; title: ['', validators.title],
id: ['', validators.id, SiteIdValidator.createValidator(this.contentApi)],
description: ['', validators.description]
});
return (id || '').trim(); this.visibilityOption = this.visibilityOptions[0].value;
}
get description(): string { this.form.controls['title'].valueChanges
const { description } = this.form.value; .pipe(debounceTime(300))
.subscribe((titleValue: string) => {
return (description || '').trim(); if (!titleValue.trim().length) {
} return;
get visibility(): string {
return this.visibilityOption || '';
}
submit() {
const { form, dialog } = this;
if (!form.valid) { return; }
this.create().subscribe(
(node: SiteEntry) => {
this.success.emit(node);
dialog.close(node);
},
(error) => this.handleError(error)
);
}
visibilityChangeHandler(event) {
this.visibilityOption = event.value;
}
private create(): Observable<SiteEntry> {
const { contentApi, title, id, description, visibility } = this;
const siteBody = <SiteBody>{
id,
title,
description,
visibility
};
return contentApi.createSite(siteBody);
}
private sanitize(input: string) {
return input
.replace(/[\s]/g, '-')
.replace(/[^A-Za-z0-9-]/g, '');
}
private handleError(error: any): any {
const { error: { statusCode } } = JSON.parse(error.message);
if (statusCode === 409) {
this.form.controls['id'].setErrors({ message: 'LIBRARY.ERRORS.CONFLICT' });
} }
return error; if (!this.form.controls['id'].dirty) {
this.form.patchValue({ id: this.sanitize(titleValue.trim()) });
this.form.controls['id'].markAsTouched();
}
});
}
get title(): string {
const { title } = this.form.value;
return (title || '').trim();
}
get id(): string {
const { id } = this.form.value;
return (id || '').trim();
}
get description(): string {
const { description } = this.form.value;
return (description || '').trim();
}
get visibility(): string {
return this.visibilityOption || '';
}
submit() {
const { form, dialog } = this;
if (!form.valid) {
return;
} }
this.create().subscribe(
(node: SiteEntry) => {
this.success.emit(node);
dialog.close(node);
},
error => this.handleError(error)
);
}
visibilityChangeHandler(event) {
this.visibilityOption = event.value;
}
private create(): Observable<SiteEntry> {
const { contentApi, title, id, description, visibility } = this;
const siteBody = <SiteBody>{
id,
title,
description,
visibility
};
return contentApi.createSite(siteBody);
}
private sanitize(input: string) {
return input.replace(/[\s]/g, '-').replace(/[^A-Za-z0-9-]/g, '');
}
private handleError(error: any): any {
const {
error: { statusCode }
} = JSON.parse(error.message);
if (statusCode === 409) {
this.form.controls['id'].setErrors({
message: 'LIBRARY.ERRORS.CONFLICT'
});
}
return error;
}
} }

View File

@@ -27,16 +27,14 @@ import { Component, Inject, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material'; import { MAT_DIALOG_DATA } from '@angular/material';
@Component({ @Component({
templateUrl: './node-permissions.dialog.html', templateUrl: './node-permissions.dialog.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { class: 'aca-node-permissions-dialog' } host: { class: 'aca-node-permissions-dialog' }
}) })
export class NodePermissionsDialogComponent { export class NodePermissionsDialogComponent {
nodeId: string; nodeId: string;
constructor( constructor(@Inject(MAT_DIALOG_DATA) data: any) {
@Inject(MAT_DIALOG_DATA) data: any, this.nodeId = data.nodeId;
) { }
this.nodeId = data.nodeId;
}
} }

View File

@@ -1,83 +1,81 @@
@mixin aca-node-versions-dialog-theme($theme) { @mixin aca-node-versions-dialog-theme($theme) {
$foreground: map-get($theme, foreground); $foreground: map-get($theme, foreground);
$accent: map-get($theme, accent); $accent: map-get($theme, accent);
.adf-version-manager-dialog-panel { .adf-version-manager-dialog-panel {
height: 400px; height: 400px;
}
.aca-node-versions-dialog {
.mat-dialog-title {
flex: 0 0 auto;
} }
.aca-node-versions-dialog { .mat-dialog-content {
.mat-dialog-title { flex: 1 1 auto;
flex: 0 0 auto; position: relative;
} overflow-y: auto;
.mat-dialog-content {
flex: 1 1 auto;
position: relative;
overflow-y: auto;
}
.mat-dialog-actions {
flex: 0 0 auto;
}
.mat-dialog-title {
font-size: 20px;
font-weight: 600;
font-style: normal;
font-stretch: normal;
line-height: 1.6;
margin: 0;
letter-spacing: -0.5px;
color: mat-color($foreground, text, 0.87);
}
.mat-dialog-actions {
padding: 8px 8px 24px 8px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
color: mat-color($foreground, text, 0.54);
button {
text-transform: uppercase;
font-weight: normal;
&:enabled {
color: mat-color($accent);
}
}
}
.adf-new-version-container {
height: 350px !important;
}
.mat-dialog-content {
max-height: 36vh;
overflow: hidden;
}
.mat-list-item-content {
padding: 0;
margin: 0 16px;
}
.adf-version-list-container {
.adf-version-list {
height: 180px;
overflow: hidden;
padding: 0;
}
.mat-list.adf-version-list {
overflow: auto;
}
}
} }
.mat-dialog-actions {
flex: 0 0 auto;
}
.mat-dialog-title {
font-size: 20px;
font-weight: 600;
font-style: normal;
font-stretch: normal;
line-height: 1.6;
margin: 0;
letter-spacing: -0.5px;
color: mat-color($foreground, text, 0.87);
}
.mat-dialog-actions {
padding: 8px 8px 24px 8px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
color: mat-color($foreground, text, 0.54);
button {
text-transform: uppercase;
font-weight: normal;
&:enabled {
color: mat-color($accent);
}
}
}
.adf-new-version-container {
height: 350px !important;
}
.mat-dialog-content {
max-height: 36vh;
overflow: hidden;
}
.mat-list-item-content {
padding: 0;
margin: 0 16px;
}
.adf-version-list-container {
.adf-version-list {
height: 180px;
overflow: hidden;
padding: 0;
}
.mat-list.adf-version-list {
overflow: auto;
}
}
}
} }

View File

@@ -31,21 +31,21 @@ import { AppStore } from '../../store/states/app.state';
import { SnackbarErrorAction } from '../../store/actions'; import { SnackbarErrorAction } from '../../store/actions';
@Component({ @Component({
templateUrl: './node-versions.dialog.html', templateUrl: './node-versions.dialog.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
host: { class: 'aca-node-versions-dialog' } host: { class: 'aca-node-versions-dialog' }
}) })
export class NodeVersionsDialogComponent { export class NodeVersionsDialogComponent {
node: MinimalNodeEntryEntity; node: MinimalNodeEntryEntity;
constructor( constructor(
@Inject(MAT_DIALOG_DATA) data: any, @Inject(MAT_DIALOG_DATA) data: any,
private store: Store<AppStore> private store: Store<AppStore>
) { ) {
this.node = data.node; this.node = data.node;
} }
uploadError(errorMessage: string) { uploadError(errorMessage: string) {
this.store.dispatch(new SnackbarErrorAction(errorMessage)); this.store.dispatch(new SnackbarErrorAction(errorMessage));
} }
} }

View File

@@ -29,15 +29,11 @@ import { DocumentListDirective } from './document-list.directive';
import { PaginationDirective } from './pagination.directive'; import { PaginationDirective } from './pagination.directive';
@NgModule({ @NgModule({
declarations: [ declarations: [
ExperimentalDirective, ExperimentalDirective,
DocumentListDirective, DocumentListDirective,
PaginationDirective PaginationDirective
], ],
exports: [ exports: [ExperimentalDirective, DocumentListDirective, PaginationDirective]
ExperimentalDirective,
DocumentListDirective,
PaginationDirective
]
}) })
export class DirectivesModule {} export class DirectivesModule {}

View File

@@ -34,133 +34,130 @@ import { SetSelectedNodesAction } from '../store/actions';
import { MinimalNodeEntryEntity } from 'alfresco-js-api'; import { MinimalNodeEntryEntity } from 'alfresco-js-api';
@Directive({ @Directive({
selector: '[acaDocumentList]' selector: '[acaDocumentList]'
}) })
export class DocumentListDirective implements OnInit, OnDestroy { export class DocumentListDirective implements OnInit, OnDestroy {
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
private isLibrary = false; private isLibrary = false;
get sortingPreferenceKey(): string { get sortingPreferenceKey(): string {
return this.route.snapshot.data.sortingPreferenceKey; return this.route.snapshot.data.sortingPreferenceKey;
}
constructor(
private store: Store<AppStore>,
private documentList: DocumentListComponent,
private preferences: UserPreferencesService,
private route: ActivatedRoute
) {}
ngOnInit() {
this.documentList.includeFields = ['isFavorite', 'aspectNames'];
this.documentList.allowDropFiles = false;
this.isLibrary = this.documentList.currentFolderId === '-mysites-';
if (this.sortingPreferenceKey) {
const current = this.documentList.sorting;
const key = this.preferences.get(
`${this.sortingPreferenceKey}.sorting.key`,
current[0]
);
const direction = this.preferences.get(
`${this.sortingPreferenceKey}.sorting.direction`,
current[1]
);
this.documentList.sorting = [key, direction];
// TODO: bug in ADF, the `sorting` binding is not updated when changed from code
this.documentList.data.setSorting({ key, direction });
} }
constructor( this.subscriptions.push(
private store: Store<AppStore>, this.documentList.ready.subscribe(() => this.onReady())
private documentList: DocumentListComponent, );
private preferences: UserPreferencesService, }
private route: ActivatedRoute
) {}
ngOnInit() { ngOnDestroy() {
this.documentList.includeFields = ['isFavorite', 'aspectNames']; this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.documentList.allowDropFiles = false; this.subscriptions = [];
this.isLibrary = this.documentList.currentFolderId === '-mysites-'; }
if (this.sortingPreferenceKey) { @HostListener('sorting-changed', ['$event'])
const current = this.documentList.sorting; onSortingChanged(event: CustomEvent) {
if (this.sortingPreferenceKey) {
const key = this.preferences.get( this.preferences.set(
`${this.sortingPreferenceKey}.sorting.key`, `${this.sortingPreferenceKey}.sorting.key`,
current[0] event.detail.key
); );
const direction = this.preferences.get( this.preferences.set(
`${this.sortingPreferenceKey}.sorting.direction`, `${this.sortingPreferenceKey}.sorting.direction`,
current[1] event.detail.direction
); );
this.documentList.sorting = [key, direction];
// TODO: bug in ADF, the `sorting` binding is not updated when changed from code
this.documentList.data.setSorting({ key, direction });
}
this.subscriptions.push(
this.documentList.ready.subscribe(() => this.onReady())
);
} }
}
ngOnDestroy() { @HostListener('node-select', ['$event'])
this.subscriptions.forEach(subscription => subscription.unsubscribe()); onNodeSelect(event: CustomEvent) {
this.subscriptions = []; if (!!event.detail && !!event.detail.node) {
const node: MinimalNodeEntryEntity = event.detail.node.entry;
if (node && this.isLockedNode(node)) {
this.unSelectLockedNodes(this.documentList);
}
this.updateSelection();
} }
}
@HostListener('sorting-changed', ['$event']) @HostListener('node-unselect')
onSortingChanged(event: CustomEvent) { onNodeUnselect() {
if (this.sortingPreferenceKey) { this.updateSelection();
this.preferences.set( }
`${this.sortingPreferenceKey}.sorting.key`,
event.detail.key
);
this.preferences.set(
`${this.sortingPreferenceKey}.sorting.direction`,
event.detail.direction
);
}
}
@HostListener('node-select', ['$event']) onReady() {
onNodeSelect(event: CustomEvent) { this.updateSelection();
if (!!event.detail && !!event.detail.node) { }
const node: MinimalNodeEntryEntity = event.detail.node.entry;
if (node && this.isLockedNode(node)) {
this.unSelectLockedNodes(this.documentList);
}
this.updateSelection(); private updateSelection() {
} const selection = this.documentList.selection.map(entry => {
} entry['isLibrary'] = this.isLibrary;
return entry;
});
@HostListener('node-unselect') this.store.dispatch(new SetSelectedNodesAction(selection));
onNodeUnselect() { }
this.updateSelection();
}
onReady() { private isLockedNode(node): boolean {
this.updateSelection(); return (
} node.isLocked ||
(node.properties && node.properties['cm:lockType'] === 'READ_ONLY_LOCK')
);
}
private updateSelection() { private isLockedRow(row): boolean {
const selection = this.documentList.selection.map(entry => { return (
entry['isLibrary'] = this.isLibrary; row.getValue('isLocked') ||
return entry; (row.getValue('properties') &&
row.getValue('properties')['cm:lockType'] === 'READ_ONLY_LOCK')
);
}
private unSelectLockedNodes(documentList: DocumentListComponent) {
documentList.selection = documentList.selection.filter(
item => !this.isLockedNode(item.entry)
);
const dataTable = documentList.dataTable;
if (dataTable && dataTable.data) {
const rows = dataTable.data.getRows();
if (rows && rows.length > 0) {
rows.forEach(r => {
if (this.isLockedRow(r)) {
r.isSelected = false;
}
}); });
}
this.store.dispatch(
new SetSelectedNodesAction(selection)
);
}
private isLockedNode(node): boolean {
return (
node.isLocked ||
(node.properties &&
node.properties['cm:lockType'] === 'READ_ONLY_LOCK')
);
}
private isLockedRow(row): boolean {
return (
row.getValue('isLocked') ||
(row.getValue('properties') &&
row.getValue('properties')['cm:lockType'] === 'READ_ONLY_LOCK')
);
}
private unSelectLockedNodes(documentList: DocumentListComponent) {
documentList.selection = documentList.selection.filter(
item => !this.isLockedNode(item.entry)
);
const dataTable = documentList.dataTable;
if (dataTable && dataTable.data) {
const rows = dataTable.data.getRows();
if (rows && rows.length > 0) {
rows.forEach(r => {
if (this.isLockedRow(r)) {
r.isSelected = false;
}
});
}
}
} }
}
} }

View File

@@ -23,73 +23,83 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Directive, TemplateRef, ViewContainerRef, Input, EmbeddedViewRef } from '@angular/core'; import {
Directive,
TemplateRef,
ViewContainerRef,
Input,
EmbeddedViewRef
} from '@angular/core';
import { AppConfigService, StorageService } from '@alfresco/adf-core'; import { AppConfigService, StorageService } from '@alfresco/adf-core';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
@Directive({ @Directive({
// tslint:disable-next-line:directive-selector // tslint:disable-next-line:directive-selector
selector: '[ifExperimental]' selector: '[ifExperimental]'
}) })
export class ExperimentalDirective { export class ExperimentalDirective {
private elseTemplateRef: TemplateRef<any>; private elseTemplateRef: TemplateRef<any>;
private elseViewRef: EmbeddedViewRef<any>; private elseViewRef: EmbeddedViewRef<any>;
private shouldRender: boolean; private shouldRender: boolean;
constructor( constructor(
private templateRef: TemplateRef<any>, private templateRef: TemplateRef<any>,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private storage: StorageService, private storage: StorageService,
private config: AppConfigService private config: AppConfigService
) {} ) {}
@Input() set ifExperimental(featureKey: string) { @Input()
const key = `experimental.${featureKey}`; set ifExperimental(featureKey: string) {
const key = `experimental.${featureKey}`;
const override = this.storage.getItem(key); const override = this.storage.getItem(key);
if (override === 'true') { if (override === 'true') {
this.shouldRender = true; this.shouldRender = true;
}
if (!environment.production) {
const value = this.config.get(key);
if (value === true || value === 'true') {
this.shouldRender = true;
}
}
if (override !== 'true' && environment.production) {
this.shouldRender = false;
}
this.updateView();
} }
@Input() set ifExperimentalElse(templateRef: TemplateRef<any>) { if (!environment.production) {
this.elseTemplateRef = templateRef; const value = this.config.get(key);
this.elseViewRef = null; if (value === true || value === 'true') {
this.updateView(); this.shouldRender = true;
}
} }
private updateView() { if (override !== 'true' && environment.production) {
if (this.shouldRender) { this.shouldRender = false;
this.viewContainerRef.clear();
this.elseViewRef = null;
if (this.templateRef) {
this.viewContainerRef.createEmbeddedView(this.templateRef);
}
} else {
if (this.elseViewRef) {
return;
}
this.viewContainerRef.clear();
if (this.elseTemplateRef) {
this.elseViewRef = this.viewContainerRef.createEmbeddedView(this.elseTemplateRef);
}
}
} }
this.updateView();
}
@Input()
set ifExperimentalElse(templateRef: TemplateRef<any>) {
this.elseTemplateRef = templateRef;
this.elseViewRef = null;
this.updateView();
}
private updateView() {
if (this.shouldRender) {
this.viewContainerRef.clear();
this.elseViewRef = null;
if (this.templateRef) {
this.viewContainerRef.createEmbeddedView(this.templateRef);
}
} else {
if (this.elseViewRef) {
return;
}
this.viewContainerRef.clear();
if (this.elseTemplateRef) {
this.elseViewRef = this.viewContainerRef.createEmbeddedView(
this.elseTemplateRef
);
}
}
}
} }

View File

@@ -25,41 +25,39 @@
import { Directive, OnInit, OnDestroy } from '@angular/core'; import { Directive, OnInit, OnDestroy } from '@angular/core';
import { import {
PaginationComponent, PaginationComponent,
UserPreferencesService, UserPreferencesService,
PaginationModel, PaginationModel,
AppConfigService AppConfigService
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
@Directive({ @Directive({
selector: '[acaPagination]' selector: '[acaPagination]'
}) })
export class PaginationDirective implements OnInit, OnDestroy { export class PaginationDirective implements OnInit, OnDestroy {
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
constructor( constructor(
private pagination: PaginationComponent, private pagination: PaginationComponent,
private preferences: UserPreferencesService, private preferences: UserPreferencesService,
private config: AppConfigService private config: AppConfigService
) {} ) {}
ngOnInit() { ngOnInit() {
this.pagination.supportedPageSizes = this.config.get( this.pagination.supportedPageSizes = this.config.get(
'pagination.supportedPageSizes' 'pagination.supportedPageSizes'
); );
this.subscriptions.push( this.subscriptions.push(
this.pagination.changePageSize.subscribe( this.pagination.changePageSize.subscribe((event: PaginationModel) => {
(event: PaginationModel) => { this.preferences.paginationSize = event.maxItems;
this.preferences.paginationSize = event.maxItems; })
} );
) }
);
}
ngOnDestroy() { ngOnDestroy() {
this.subscriptions.forEach(subscription => subscription.unsubscribe()); this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.subscriptions = []; this.subscriptions = [];
} }
} }

View File

@@ -7,15 +7,15 @@ import { MyExtensionModule } from 'my-extension';
// For any application-specific code use CoreExtensionsModule instead. // For any application-specific code use CoreExtensionsModule instead.
@NgModule({ @NgModule({
imports: [ imports: [
CodeEditorModule.forRoot({ CodeEditorModule.forRoot({
// use local Monaco installation // use local Monaco installation
baseUrl: 'assets/monaco', baseUrl: 'assets/monaco',
// use local Typings Worker // use local Typings Worker
typingsWorkerUrl: 'assets/workers/typings-worker.js' typingsWorkerUrl: 'assets/workers/typings-worker.js'
}), }),
AcaDevToolsModule, AcaDevToolsModule,
MyExtensionModule MyExtensionModule
] ]
}) })
export class AppExtensionsModule {} export class AppExtensionsModule {}

View File

@@ -39,82 +39,78 @@ import { VersionsTabComponent } from '../components/info-drawer/versions-tab/ver
import { ExtensionsModule, ExtensionService } from '@alfresco/adf-extensions'; import { ExtensionsModule, ExtensionService } from '@alfresco/adf-extensions';
export function setupExtensions(service: AppExtensionService): Function { export function setupExtensions(service: AppExtensionService): Function {
return () => service.load(); return () => service.load();
} }
@NgModule({ @NgModule({
imports: [ imports: [CommonModule, CoreModule.forChild(), ExtensionsModule.forChild()]
CommonModule,
CoreModule.forChild(),
ExtensionsModule.forChild()
]
}) })
export class CoreExtensionsModule { export class CoreExtensionsModule {
static forRoot(): ModuleWithProviders { static forRoot(): ModuleWithProviders {
return { return {
ngModule: CoreExtensionsModule, ngModule: CoreExtensionsModule,
providers: [ providers: [
AppExtensionService, AppExtensionService,
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: setupExtensions, useFactory: setupExtensions,
deps: [AppExtensionService], deps: [AppExtensionService],
multi: true multi: true
} }
] ]
}; };
} }
static forChild(): ModuleWithProviders { static forChild(): ModuleWithProviders {
return { return {
ngModule: CoreExtensionsModule ngModule: CoreExtensionsModule
}; };
} }
constructor(extensions: ExtensionService) { constructor(extensions: ExtensionService) {
extensions.setComponents({ extensions.setComponents({
'app.layout.main': LayoutComponent, 'app.layout.main': LayoutComponent,
'app.components.trashcan': TrashcanComponent, 'app.components.trashcan': TrashcanComponent,
'app.components.tabs.metadata': MetadataTabComponent, 'app.components.tabs.metadata': MetadataTabComponent,
'app.components.tabs.comments': CommentsTabComponent, 'app.components.tabs.comments': CommentsTabComponent,
'app.components.tabs.versions': VersionsTabComponent, 'app.components.tabs.versions': VersionsTabComponent,
'app.toolbar.toggleInfoDrawer': ToggleInfoDrawerComponent, 'app.toolbar.toggleInfoDrawer': ToggleInfoDrawerComponent,
'app.toolbar.toggleFavorite': ToggleFavoriteComponent 'app.toolbar.toggleFavorite': ToggleFavoriteComponent
}); });
extensions.setAuthGuards({ extensions.setAuthGuards({
'app.auth': AuthGuardEcm 'app.auth': AuthGuardEcm
}); });
extensions.setEvaluators({ extensions.setEvaluators({
'app.selection.canDelete': app.canDeleteSelection, 'app.selection.canDelete': app.canDeleteSelection,
'app.selection.canDownload': app.canDownloadSelection, 'app.selection.canDownload': app.canDownloadSelection,
'app.selection.notEmpty': app.hasSelection, 'app.selection.notEmpty': app.hasSelection,
'app.selection.canUnshare': app.canUnshareNodes, 'app.selection.canUnshare': app.canUnshareNodes,
'app.selection.canAddFavorite': app.canAddFavorite, 'app.selection.canAddFavorite': app.canAddFavorite,
'app.selection.canRemoveFavorite': app.canRemoveFavorite, 'app.selection.canRemoveFavorite': app.canRemoveFavorite,
'app.selection.first.canUpdate': app.canUpdateSelectedNode, 'app.selection.first.canUpdate': app.canUpdateSelectedNode,
'app.selection.file': app.hasFileSelected, 'app.selection.file': app.hasFileSelected,
'app.selection.file.canShare': app.canShareFile, 'app.selection.file.canShare': app.canShareFile,
'app.selection.library': app.hasLibrarySelected, 'app.selection.library': app.hasLibrarySelected,
'app.selection.folder': app.hasFolderSelected, 'app.selection.folder': app.hasFolderSelected,
'app.selection.folder.canUpdate': app.canUpdateSelectedFolder, 'app.selection.folder.canUpdate': app.canUpdateSelectedFolder,
'app.navigation.folder.canCreate': app.canCreateFolder, 'app.navigation.folder.canCreate': app.canCreateFolder,
'app.navigation.folder.canUpload': app.canUpload, 'app.navigation.folder.canUpload': app.canUpload,
'app.navigation.isTrashcan': nav.isTrashcan, 'app.navigation.isTrashcan': nav.isTrashcan,
'app.navigation.isNotTrashcan': nav.isNotTrashcan, 'app.navigation.isNotTrashcan': nav.isNotTrashcan,
'app.navigation.isLibraries': nav.isLibraries, 'app.navigation.isLibraries': nav.isLibraries,
'app.navigation.isNotLibraries': nav.isNotLibraries, 'app.navigation.isNotLibraries': nav.isNotLibraries,
'app.navigation.isSharedFiles': nav.isSharedFiles, 'app.navigation.isSharedFiles': nav.isSharedFiles,
'app.navigation.isNotSharedFiles': nav.isNotSharedFiles, 'app.navigation.isNotSharedFiles': nav.isNotSharedFiles,
'app.navigation.isFavorites': nav.isFavorites, 'app.navigation.isFavorites': nav.isFavorites,
'app.navigation.isNotFavorites': nav.isNotFavorites, 'app.navigation.isNotFavorites': nav.isNotFavorites,
'app.navigation.isRecentFiles': nav.isRecentFiles, 'app.navigation.isRecentFiles': nav.isRecentFiles,
'app.navigation.isNotRecentFiles': nav.isNotRecentFiles, 'app.navigation.isNotRecentFiles': nav.isNotRecentFiles,
'app.navigation.isSearchResults': nav.isSearchResults, 'app.navigation.isSearchResults': nav.isSearchResults,
'app.navigation.isNotSearchResults': nav.isNotSearchResults, 'app.navigation.isNotSearchResults': nav.isNotSearchResults,
'app.navigation.isPreview': nav.isPreview 'app.navigation.isPreview': nav.isPreview
}); });
} }
} }

Some files were not shown because too many files have changed in this diff Show More