[AAE-10777] Move services from Core in Content the right place (#8242)

Clean core services and directive
This commit is contained in:
Eugenio Romano
2023-03-10 09:28:24 +01:00
committed by GitHub
parent 112e272ce7
commit 2590e7d0a9
263 changed files with 884 additions and 3393 deletions
@@ -25,7 +25,7 @@ import { AspectListDialogComponentData } from './aspect-list-dialog-data.interfa
import { AspectListService } from './services/aspect-list.service';
import { delay } from 'rxjs/operators';
import { AspectEntry, MinimalNode } from '@alfresco/js-api';
import { NodesApiService } from '@alfresco/adf-core';
import { NodesApiService } from '../common/services/nodes-api.service';
const aspectListMock: AspectEntry[] = [{
entry: {
@@ -16,7 +16,8 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NodesApiService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { NodesApiService } from '../common/services/nodes-api.service';
import { ContentTestingModule } from '../testing/content.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { AspectListComponent } from './aspect-list.component';
@@ -16,7 +16,7 @@
*/
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { NodesApiService } from '@alfresco/adf-core';
import { NodesApiService } from '../common/services/nodes-api.service';
import { Observable, Subject, zip } from 'rxjs';
import { concatMap, map, takeUntil, tap } from 'rxjs/operators';
import { AspectListService } from './services/aspect-list.service';
@@ -18,7 +18,8 @@
import { MinimalNode } from '@alfresco/js-api';
import { TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { NodesApiService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { NodesApiService } from '../../common/services/nodes-api.service';
import { EMPTY, of } from 'rxjs';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { NodeAspectService } from './node-aspect.service';
@@ -16,9 +16,9 @@
*/
import { Injectable } from '@angular/core';
import { NodesApiService } from '@alfresco/adf-core';
import { DialogAspectListService } from './dialog-aspect-list.service';
import { CardViewContentUpdateService } from '../../common/services/card-view-content-update.service';
import { NodesApiService } from '../../common/services/nodes-api.service';
@Injectable({
providedIn: 'root'
@@ -33,8 +33,7 @@ export class CategoryTreeDatasourceService extends TreeService<CategoryNode> {
public getSubNodes(parentNodeId: string, skipCount?: number, maxItems?: number, name?: string): Observable<TreeResponse<CategoryNode>> {
return !name ? this.categoryService.getSubcategories(parentNodeId, skipCount, maxItems).pipe(map((response: CategoryPaging) => {
const parentNode: CategoryNode = this.getParentNode(parentNodeId);
const nodesList: CategoryNode[] = response.list.entries.map((entry: CategoryEntry) => {
return {
const nodesList: CategoryNode[] = response.list.entries.map((entry: CategoryEntry) => ({
id: entry.entry.id,
nodeName: entry.entry.name,
parentId: entry.entry.parentId,
@@ -42,8 +41,7 @@ export class CategoryTreeDatasourceService extends TreeService<CategoryNode> {
level: parentNode ? parentNode.level + 1 : 0,
isLoading: false,
nodeType: TreeNodeType.RegularNode
};
});
}));
if (response.list.pagination.hasMoreItems && parentNode) {
const loadMoreNode: CategoryNode = {
id: 'loadMore',
@@ -0,0 +1,52 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FileModel, FileUploadStatus } from '../models/file.model';
export class FileUploadEvent {
constructor(
public readonly file: FileModel,
public readonly status: FileUploadStatus = FileUploadStatus.Pending,
public readonly error: any = null) {
}
}
export class FileUploadCompleteEvent extends FileUploadEvent {
constructor(file: FileModel, public totalComplete: number = 0, public data?: any, public totalAborted: number = 0) {
super(file, FileUploadStatus.Complete);
}
}
export class FileUploadDeleteEvent extends FileUploadEvent {
constructor(file: FileModel, public totalComplete: number = 0) {
super(file, FileUploadStatus.Deleted);
}
}
export class FileUploadErrorEvent extends FileUploadEvent {
constructor(file: FileModel, public error: any, public totalError: number = 0) {
super(file, FileUploadStatus.Error);
}
}
@@ -0,0 +1,150 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PersonEntry, Person, PersonPaging } from '@alfresco/js-api';
export const fakeEcmUser = {
id: 'fake-id',
firstName: 'fake-ecm-first-name',
lastName: 'fake-ecm-last-name',
description: 'i am a fake user for test',
avatarId: 'fake-avatar-id',
email: 'fakeEcm@ecmUser.com',
skypeId: 'fake-skype-id',
googleId: 'fake-googleId-id',
instantMessageId: 'fake-instantMessageId-id',
company: null,
jobTitle: 'job-ecm-test',
location: 'fake location',
mobile: '000000000',
telephone: '11111111',
statusUpdatedAt: 'fake-date',
userStatus: 'active',
enabled: true,
emailNotificationsEnabled: true
};
export const fakeEcmAdminUser = {
...fakeEcmUser,
capabilities: {
isAdmin: true
}
};
export const fakeEcmUser2 = {
id: 'another-fake-id',
firstName: 'another-fake-first-name',
lastName: 'another',
displayName: 'admin.adf User',
email: 'admin.adf@alfresco.com',
company: null,
enabled: true,
emailNotificationsEnabled: true
};
export const fakeEcmUserNoImage = {
id: 'fake-id',
firstName: 'fake-first-name',
lastName: 'fake-last-name',
description: 'i am a fake user for test',
avatarId: null,
email: 'fakeEcm@ecmUser.com',
skypeId: 'fake-skype-id',
googleId: 'fake-googleId-id',
instantMessageId: 'fake-instantMessageId-id',
company: null,
jobTitle: null,
location: 'fake location',
mobile: '000000000',
telephone: '11111111',
statusUpdatedAt: 'fake-date',
userStatus: 'active',
enabled: true,
emailNotificationsEnabled: true
};
export const fakeEcmEditedUser = {
id: 'fake-id',
firstName: null,
lastName: 'fake-last-name',
description: 'i am a fake user for test',
avatarId: 'fake-avatar-id',
email: 'fakeEcm@ecmUser.com',
skypeId: 'fake-skype-id',
googleId: 'fake-googleId-id',
instantMessageId: 'fake-instantMessageId-id',
company: null,
jobTitle: 'test job',
location: 'fake location',
mobile: '000000000',
telephone: '11111111',
statusUpdatedAt: 'fake-date',
userStatus: 'active',
enabled: true,
emailNotificationsEnabled: true
};
export const fakeEcmUserList = new PersonPaging({
list: {
pagination: {
count: 2,
hasMoreItems: false,
totalItems: 2,
skipCount: 0,
maxItems: 100
},
entries: [
{
entry: fakeEcmUser
},
{
entry: fakeEcmUser2
}
]
}
});
export const createNewPersonMock = {
id: 'fake-id',
firstName: 'fake-ecm-first-name',
lastName: 'fake-ecm-last-name',
description: 'i am a fake user for test',
password: 'fake-avatar-id',
email: 'fakeEcm@ecmUser.com'
};
export const getFakeUserWithContentAdminCapability = (): PersonEntry => {
const fakeEcmUserWithAdminCapabilities = {
...fakeEcmUser,
capabilities: {
isAdmin: true
}
};
const mockPerson = new Person(fakeEcmUserWithAdminCapabilities);
return { entry: mockPerson };
};
export const getFakeUserWithContentUserCapability = (): PersonEntry => {
const fakeEcmUserWithAdminCapabilities = {
...fakeEcmUser,
capabilities: {
isAdmin: false
}
};
const mockPerson = new Person(fakeEcmUserWithAdminCapabilities);
return { entry: mockPerson };
};
@@ -0,0 +1,30 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* spellchecker: disable */
export class AllowableOperationsEnum extends String {
static DELETE: string = 'delete';
static UPDATE: string = 'update';
static CREATE: string = 'create';
static COPY: string = 'copy';
static LOCK: string = 'lock';
static UPDATEPERMISSIONS: string = 'updatePermissions';
static NOT_DELETE: string = '!delete';
static NOT_UPDATE: string = '!update';
static NOT_CREATE: string = '!create';
static NOT_UPDATEPERMISSIONS: string = '!updatePermissions';
}
@@ -0,0 +1,27 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class EcmCompanyModel {
organization: string;
address1: string;
address2: string;
address3: string;
postcode: string;
telephone: string;
fax: string;
email: string;
}
@@ -0,0 +1,72 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Capabilities } from '@alfresco/js-api';
import { EcmCompanyModel } from './ecm-company.model';
export class EcmUserModel {
id: string;
firstName: string;
lastName?: string;
displayName?: string;
description?: string;
avatarId?: string;
email: string;
skypeId?: string;
googleId?: string;
instantMessageId?: string;
jobTitle?: string;
location?: string;
company: EcmCompanyModel;
mobile?: string;
telephone?: string;
statusUpdatedAt?: Date;
userStatus?: string;
enabled: boolean;
emailNotificationsEnabled?: boolean;
aspectNames?: string[];
properties?: { [key: string]: string };
capabilities?: Capabilities;
constructor(obj?: any) {
this.id = obj && obj.id || null;
this.firstName = obj && obj.firstName;
this.lastName = obj && obj.lastName;
this.description = obj && obj.description || null;
this.avatarId = obj && obj.avatarId || null;
this.email = obj && obj.email || null;
this.skypeId = obj && obj.skypeId;
this.googleId = obj && obj.googleId;
this.instantMessageId = obj && obj.instantMessageId;
this.jobTitle = obj && obj.jobTitle || null;
this.location = obj && obj.location || null;
this.company = obj && obj.company;
this.mobile = obj && obj.mobile;
this.telephone = obj && obj.telephone;
this.statusUpdatedAt = obj && obj.statusUpdatedAt;
this.userStatus = obj && obj.userStatus;
this.enabled = obj && obj.enabled;
this.emailNotificationsEnabled = obj && obj.emailNotificationsEnabled;
this.aspectNames = obj && obj.aspectNames;
this.properties = obj && obj.properties;
this.capabilities = obj && obj.capabilities;
}
isAdmin(): boolean {
return this.capabilities?.isAdmin;
}
}
@@ -0,0 +1,48 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FileModel } from './file.model';
describe('FileModel', () => {
describe('extension', () => {
it('should return the extension if file has it', () => {
const file = new FileModel({ name: 'tyrion-lannister.doc' } as File);
expect(file.extension).toBe('doc');
});
it('should return the empty string if file has NOT got it', () => {
const file = new FileModel({ name: 'daenerys-targaryen' } as File);
expect(file.extension).toBe('');
});
it('should return the empty string if file is starting with . and doesn\'t have extension', () => {
const file = new FileModel({ name: '.white-walkers' } as File);
expect(file.extension).toBe('');
});
it('should return the last extension string if file contains many dot', () => {
const file = new FileModel({ name: 'you.know.nothing.jon.snow.exe' } as File);
expect(file.extension).toBe('exe');
});
});
});
@@ -0,0 +1,123 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AssocChildBody, AssociationBody } from '@alfresco/js-api';
export interface FileUploadProgress {
loaded: number;
total: number;
percent: number;
}
export class FileUploadOptions {
/**
* Add a version comment which will appear in version history.
* Setting this parameter also enables versioning of this node, if it is not already versioned.
*/
comment?: string;
/**
* Overwrite the content of the node with a new version.
*/
newVersion?: boolean;
/**
* If true, then created node will be version 1.0 MAJOR. If false, then created node will be version 0.1 MINOR.
*/
majorVersion?: boolean;
/**
* Root folder id.
*/
parentId?: string;
/**
* Defines the **relativePath** value.
* The relativePath specifies the folder structure to create relative to the node nodeId.
* Folders in the relativePath that do not exist are created before the node is created.
*/
path?: string;
/**
* You can use the nodeType field to create a specific type. The default is **cm:content**.
*/
nodeType?: string;
/**
* You can set multi-value properties when you create a new node which supports properties of type multiple.
*/
properties?: any;
/**
* If the content model allows then it is also possible to create primary children with a different assoc type.
*/
association?: any;
/**
* You can optionally specify an array of **secondaryChildren** to create one or more secondary child associations,
* such that the newly created node acts as a parent node.
*/
secondaryChildren?: AssocChildBody[];
/**
* You can optionally specify an array of **targets** to create one or more peer associations such that the newly created node acts as a source node.
*/
targets?: AssociationBody[];
/**
* If true, then created node will be versioned. If false, then created node will be unversioned and auto-versioning disabled.
*/
versioningEnabled?: boolean;
}
// eslint-disable-next-line no-shadow
export enum FileUploadStatus {
Pending = 0,
Complete = 1,
Starting = 2,
Progress = 3,
Cancelled = 4,
Aborted = 5,
Error = 6,
Deleted = 7
}
export class FileModel {
readonly name: string;
readonly size: number;
readonly file: File;
id: string;
status: FileUploadStatus = FileUploadStatus.Pending;
errorCode: number;
progress: FileUploadProgress;
options: FileUploadOptions;
data: any;
constructor(file: File, options?: FileUploadOptions, id?: string) {
this.file = file;
this.id = id;
this.name = file.name;
this.size = file.size;
this.data = null;
this.errorCode = null;
this.progress = {
loaded: 0,
total: 0,
percent: 0
};
this.options = Object.assign({}, {
newVersion: false
}, options);
}
get extension(): string {
return this.name.slice((Math.max(0, this.name.lastIndexOf('.')) || Infinity) + 1);
}
}
@@ -0,0 +1,26 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class NodeMetadata {
metadata: any;
nodeType: string;
constructor(metadata: any, nodeType: string) {
this.metadata = metadata;
this.nodeType = nodeType;
}
}
@@ -0,0 +1,32 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* spellchecker: disable */
export class PermissionsEnum extends String {
static CONTRIBUTOR: string = 'Contributor';
static CONSUMER: string = 'Consumer';
static COLLABORATOR: string = 'Collaborator';
static MANAGER: string = 'Manager';
static EDITOR: string = 'Editor';
static COORDINATOR: string = 'Coordinator';
static NOT_CONTRIBUTOR: string = '!Contributor';
static NOT_CONSUMER: string = '!Consumer';
static NOT_COLLABORATOR: string = '!Collaborator';
static NOT_MANAGER: string = '!Manager';
static NOT_EDITOR: string = '!Editor';
static NOT_COORDINATOR: string = '!Coordinator';
}
@@ -18,5 +18,22 @@
export * from './services/favorites-api.service';
export * from './services/card-view-content-update.service';
export * from './services/sites.service';
export * from './services/rendition.service';
export * from './services/upload.service';
export * from './services/nodes-api.service';
export * from './services/discovery-api.service';
export * from './services/people-content.service';
export * from './services/content.service';
export * from './events/file.event';
export * from './models/ecm-user.model';
export * from './models/ecm-company.model';
export * from './models/file.model';
export * from './models/node-metadata.model';
export * from './models/permissions.enum';
export * from './models/allowable-operations.enum';
export * from './interfaces/search-configuration.interface';
export * from './mocks/ecm-user.service.mock';
@@ -0,0 +1,164 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestBed } from '@angular/core/testing';
import { ContentService } from './content.service';
import { AppConfigService, AuthenticationService, StorageService, setupTestBed, CoreTestingModule } from '@alfresco/adf-core';
import { Node } from '@alfresco/js-api';
import { TranslateModule } from '@ngx-translate/core';
declare let jasmine: any;
describe('ContentService', () => {
let contentService: ContentService;
let authService: AuthenticationService;
let storage: StorageService;
let node: any;
const nodeId = 'fake-node-id';
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
authService = TestBed.inject(AuthenticationService);
contentService = TestBed.inject(ContentService);
storage = TestBed.inject(StorageService);
storage.clear();
node = {
entry: {
id: nodeId
}
};
jasmine.Ajax.install();
const appConfig: AppConfigService = TestBed.inject(AppConfigService);
appConfig.config = {
ecmHost: 'http://localhost:9876/ecm',
provider: 'ECM'
};
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
it('should return a valid content URL', (done) => {
authService.login('fake-username', 'fake-password').subscribe(() => {
expect(contentService.getContentUrl(node)).toContain('/ecm/alfresco/api/' +
'-default-/public/alfresco/versions/1/nodes/fake-node-id/content?attachment=false&alf_ticket=fake-post-ticket');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 201,
contentType: 'application/json',
responseText: JSON.stringify({ entry: { id: 'fake-post-ticket', userId: 'admin' } })
});
});
describe('AllowableOperations', () => {
it('should hasAllowableOperations be false if allowableOperation is not present in the node', () => {
const permissionNode = new Node({});
expect(contentService.hasAllowableOperations(permissionNode, 'create')).toBeFalsy();
});
it('should hasAllowableOperations be true if allowableOperation is present and you have the permission for the request operation', () => {
const permissionNode = new Node({ allowableOperations: ['delete', 'update', 'create', 'updatePermissions'] });
expect(contentService.hasAllowableOperations(permissionNode, 'create')).toBeTruthy();
});
it('should hasAllowableOperations be false if allowableOperation is present but you don\'t have the permission for the request operation', () => {
const permissionNode = new Node({ allowableOperations: ['delete', 'update', 'updatePermissions'] });
expect(contentService.hasAllowableOperations(permissionNode, 'create')).toBeFalsy();
});
it('should hasAllowableOperations works in the opposite way with negate value', () => {
const permissionNode = new Node({ allowableOperations: ['delete', 'update', 'updatePermissions'] });
expect(contentService.hasAllowableOperations(permissionNode, '!create')).toBeTruthy();
});
it('should hasAllowableOperations return false if no permission parameter are passed', () => {
const permissionNode = new Node({ allowableOperations: ['delete', 'update', 'updatePermissions'] });
expect(contentService.hasAllowableOperations(permissionNode, null)).toBeFalsy();
});
it('should havePermission return true if permission parameter is copy', () => {
const permissionNode = null;
expect(contentService.hasAllowableOperations(permissionNode, 'copy')).toBeTruthy();
});
});
describe('Permissions', () => {
it('should havePermission be false if allowableOperation is not present in the node', () => {
const permissionNode = new Node({});
expect(contentService.hasPermissions(permissionNode, 'manager')).toBeFalsy();
});
it('should havePermission be true if permissions is present and you have the permission for the request operation', () => {
const permissionNode = new Node({ permissions: { locallySet: [{ name: 'manager', authorityId: 'user1' }, { name: 'collaborator', authorityId: 'user2' }, { name: 'consumer', authorityId: 'user3' }] } });
expect(contentService.hasPermissions(permissionNode, 'manager', 'user1')).toBeTruthy();
});
it('should havePermission be false if permissions is present but you don\'t have the permission for the request operation', () => {
const permissionNode = new Node({ permissions: { locallySet: [{ name: 'collaborator', authorityId: 'user1' }, { name: 'consumer', authorityId: 'user2' }] } });
expect(contentService.hasPermissions(permissionNode, 'manager', 'user1')).toBeFalsy();
});
it('should havePermission works in the opposite way with negate value', () => {
const permissionNode = new Node({ permissions: { locallySet: [{ name: 'collaborator', authorityId: 'user1' }, { name: 'consumer', authorityId: 'user2' }] } });
expect(contentService.hasPermissions(permissionNode, '!manager', 'user1')).toBeTruthy();
});
it('should havePermission return false if no permission parameter are passed', () => {
const permissionNode = new Node({ permissions: { locallySet: [{ name: 'collaborator', authorityId: 'user1' }, { name: 'consumer', authorityId: 'user2' }] } });
expect(contentService.hasPermissions(permissionNode, null, 'user1')).toBeFalsy();
});
it('should havePermission return true if the permissions is empty and the permission to check is Consumer', () => {
const permissionNode = new Node({ permissions: [] });
expect(contentService.hasPermissions(permissionNode, 'Consumer', 'user1')).toBeTruthy();
});
it('should havePermission return false if the permissions is empty and the permission to check is not Consumer', () => {
const permissionNode = new Node({ permissions: [] });
expect(contentService.hasPermissions(permissionNode, '!Consumer', 'user1')).toBeFalsy();
});
it('should havePermission be true if inherited permissions is present and you have the permission for the request operation', () => {
const permissionNode = new Node({ permissions: { inherited: [{ name: 'manager', authorityId: 'user1' }, { name: 'collaborator', authorityId: 'user2' } ] } });
expect(contentService.hasPermissions(permissionNode, 'manager', 'user1')).toBeTruthy();
});
it('should take current logged user id if userId undefined ', () => {
spyOn(authService, 'getEcmUsername').and.returnValue('user1');
const permissionNode = new Node({ permissions: { inherited: [{ name: 'manager', authorityId: 'user1' }, { name: 'collaborator', authorityId: 'user2' } ] } });
expect(contentService.hasPermissions(permissionNode, 'manager')).toBeTruthy();
});
});
});
@@ -0,0 +1,153 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { ContentApi, MinimalNode, Node, NodeEntry } from '@alfresco/js-api';
import { Subject } from 'rxjs';
import { AlfrescoApiService, AuthenticationService } from '@alfresco/adf-core';
import { PermissionsEnum } from '../models/permissions.enum';
import { AllowableOperationsEnum } from '../models/allowable-operations.enum';
export interface FolderCreatedEvent {
name: string;
relativePath?: string;
parentId?: string;
node?: NodeEntry;
}
@Injectable({
providedIn: 'root'
})
export class ContentService {
folderCreated: Subject<FolderCreatedEvent> = new Subject<FolderCreatedEvent>();
folderCreate: Subject<MinimalNode> = new Subject<MinimalNode>();
folderEdit: Subject<MinimalNode> = new Subject<MinimalNode>();
private _contentApi: ContentApi;
get contentApi(): ContentApi {
this._contentApi = this._contentApi ?? new ContentApi(this.apiService.getInstance());
return this._contentApi;
}
constructor(public authService: AuthenticationService,
public apiService: AlfrescoApiService) {
}
/**
* Gets a content URL for the given node.
*
* @param node Node or Node ID to get URL for.
* @param attachment Toggles whether to retrieve content as an attachment for download
* @param ticket Custom ticket to use for authentication
* @returns URL string or `null`
*/
getContentUrl(node: NodeEntry | string, attachment?: boolean, ticket?: string): string {
if (node) {
let nodeId: string;
if (typeof node === 'string') {
nodeId = node;
} else if (node.entry) {
nodeId = node.entry.id;
}
return this.contentApi.getContentUrl(nodeId, attachment, ticket);
}
return null;
}
getDocumentThumbnailUrl(nodeId: string, attachment?: boolean, ticket?: string): string {
return this.contentApi.getDocumentThumbnailUrl(nodeId, attachment, ticket);
}
/**
* Checks if the user has permission on that node
*
* @param node Node to check permissions
* @param permission Required permission type
* @param userId Optional current user id will be taken by default
* @returns True if the user has the required permissions, false otherwise
*/
hasPermissions(node: Node, permission: PermissionsEnum | string, userId?: string): boolean {
let hasPermissions = false;
userId = userId ?? this.authService.getEcmUsername();
const permissions = [...(node.permissions?.locallySet || []), ...(node.permissions?.inherited || [])]
.filter((currentPermission) => currentPermission.authorityId === userId);
if (permissions.length) {
if (permission && permission.startsWith('!')) {
hasPermissions = !permissions.find((currentPermission) => currentPermission.name === permission.replace('!', ''));
} else {
hasPermissions = !!permissions.find((currentPermission) => currentPermission.name === permission);
}
} else {
if (permission === PermissionsEnum.CONSUMER) {
hasPermissions = true;
} else if (permission === PermissionsEnum.NOT_CONSUMER) {
hasPermissions = false;
} else if (permission && permission.startsWith('!')) {
hasPermissions = true;
}
}
return hasPermissions;
}
/**
* Checks if the user has permissions on that node
*
* @param node Node to check allowableOperations
* @param allowableOperation Create, delete, update, updatePermissions, !create, !delete, !update, !updatePermissions
* @returns True if the user has the required permissions, false otherwise
*/
hasAllowableOperations(node: Node, allowableOperation: AllowableOperationsEnum | string): boolean {
let hasAllowableOperations = false;
if (node && node.allowableOperations) {
if (allowableOperation && allowableOperation.startsWith('!')) {
hasAllowableOperations = !node.allowableOperations.find((currentOperation) => currentOperation === allowableOperation.replace('!', ''));
} else {
hasAllowableOperations = !!node.allowableOperations.find((currentOperation) => currentOperation === allowableOperation);
}
} else {
if (allowableOperation && allowableOperation.startsWith('!')) {
hasAllowableOperations = true;
}
}
if (allowableOperation === AllowableOperationsEnum.COPY) {
hasAllowableOperations = true;
}
if (allowableOperation === AllowableOperationsEnum.LOCK) {
hasAllowableOperations = node.isFile;
if (node.isLocked && node.allowableOperations) {
hasAllowableOperations = !!~node.allowableOperations.indexOf('updatePermissions');
}
}
return hasAllowableOperations;
}
}
@@ -0,0 +1,95 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { from, Observable, throwError, Subject } from 'rxjs';
import { catchError, map, switchMap, filter, take } from 'rxjs/operators';
import { RepositoryInfo, SystemPropertiesRepresentation } from '@alfresco/js-api';
import { BpmProductVersionModel, AlfrescoApiService, AuthenticationService } from '@alfresco/adf-core';
import { ApiClientsService } from '@alfresco/adf-core/api';
@Injectable({
providedIn: 'root'
})
export class DiscoveryApiService {
/**
* Gets product information for Content Services.
*/
ecmProductInfo$ = new Subject<RepositoryInfo>();
constructor(
private apiService: AlfrescoApiService,
private authenticationService: AuthenticationService,
private apiClientsService: ApiClientsService
) {
this.authenticationService.onLogin
.pipe(
filter(() => this.apiService.getInstance()?.isEcmLoggedIn()),
take(1),
switchMap(() => this.getEcmProductInfo())
)
.subscribe((info) => this.ecmProductInfo$.next(info));
}
/**
* Gets product information for Content Services.
*
* @returns ProductVersionModel containing product details
*/
getEcmProductInfo(): Observable<RepositoryInfo> {
const discoveryApi = this.apiClientsService.get('DiscoveryClient.discovery');
return from(discoveryApi.getRepositoryInformation())
.pipe(
map((res) => res.entry.repository),
catchError((err) => throwError(err))
);
}
/**
* Gets product information for Process Services.
*
* @returns ProductVersionModel containing product details
*/
getBpmProductInfo(): Observable<BpmProductVersionModel> {
const aboutApi = this.apiClientsService.get('ActivitiClient.about');
return from(aboutApi.getAppVersion())
.pipe(
map((res) => new BpmProductVersionModel(res)),
catchError((err) => throwError(err))
);
}
getBPMSystemProperties(): Observable<SystemPropertiesRepresentation> {
const systemPropertiesApi = this.apiClientsService.get('ActivitiClient.system-properties');
return from(systemPropertiesApi.getProperties())
.pipe(
map((res: any) => {
if ('string' === typeof (res)) {
throw new Error('Not valid response');
}
return res;
}),
catchError((err) => throwError(err.error))
);
}
}
@@ -0,0 +1,258 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { MinimalNode, NodeEntry, NodePaging, NodesApi, TrashcanApi, Node } from '@alfresco/js-api';
import { Subject, from, Observable, throwError } from 'rxjs';
import { AlfrescoApiService, UserPreferencesService } from '@alfresco/adf-core';
import { catchError, map } from 'rxjs/operators';
import { NodeMetadata } from '../models/node-metadata.model';
@Injectable({
providedIn: 'root'
})
export class NodesApiService {
/**
* Publish/subscribe to events related to node updates.
*/
nodeUpdated = new Subject<Node>();
private _trashcanApi: TrashcanApi;
get trashcanApi(): TrashcanApi {
this._trashcanApi = this._trashcanApi ?? new TrashcanApi(this.apiService.getInstance());
return this._trashcanApi;
}
private _nodesApi: NodesApi;
get nodesApi(): NodesApi {
this._nodesApi = this._nodesApi ?? new NodesApi(this.apiService.getInstance());
return this._nodesApi;
}
constructor(private apiService: AlfrescoApiService,
private preferences: UserPreferencesService) {
}
private getEntryFromEntity(entity: NodeEntry) {
return entity.entry;
}
/**
* Gets the stored information about a node.
*
* @param nodeId ID of the target node
* @param options Optional parameters supported by JS-API
* @returns Node information
*/
getNode(nodeId: string, options: any = {}): Observable<MinimalNode> {
const defaults = {
include: ['path', 'properties', 'allowableOperations', 'permissions']
};
const queryOptions = Object.assign(defaults, options);
return from(this.nodesApi.getNode(nodeId, queryOptions)).pipe(
map(this.getEntryFromEntity),
catchError((err) => throwError(err))
);
}
/**
* Gets the items contained in a folder node.
*
* @param nodeId ID of the target node
* @param options Optional parameters supported by JS-API
* @returns List of child items from the folder
*/
getNodeChildren(nodeId: string, options: any = {}): Observable<NodePaging> {
const defaults = {
maxItems: this.preferences.paginationSize,
skipCount: 0,
include: ['path', 'properties', 'allowableOperations', 'permissions']
};
const queryOptions = Object.assign(defaults, options);
return from(this.nodesApi.listNodeChildren(nodeId, queryOptions)).pipe(
catchError((err) => throwError(err))
);
}
/**
* Creates a new document node inside a folder.
*
* @param parentNodeId ID of the parent folder node
* @param nodeBody Data for the new node
* @param options Optional parameters supported by JS-API
* @returns Details of the new node
*/
createNode(parentNodeId: string, nodeBody: any, options: any = {}): Observable<MinimalNode> {
return from(this.nodesApi.createNode(parentNodeId, nodeBody, options)).pipe(
map(this.getEntryFromEntity),
catchError((err) => throwError(err))
);
}
/**
* Creates a new folder node inside a parent folder.
*
* @param parentNodeId ID of the parent folder node
* @param nodeBody Data for the new folder
* @param options Optional parameters supported by JS-API
* @returns Details of the new folder
*/
createFolder(parentNodeId: string, nodeBody: any, options: any = {}): Observable<MinimalNode> {
const body = Object.assign({ nodeType: 'cm:folder' }, nodeBody);
return this.createNode(parentNodeId, body, options);
}
/**
* Updates the information about a node.
*
* @param nodeId ID of the target node
* @param nodeBody New data for the node
* @param options Optional parameters supported by JS-API
* @returns Updated node information
*/
updateNode(nodeId: string, nodeBody: any, options: any = {}): Observable<MinimalNode> {
const defaults = {
include: ['path', 'properties', 'allowableOperations', 'permissions', 'definition']
};
const queryOptions = Object.assign(defaults, options);
return from(this.nodesApi.updateNode(nodeId, nodeBody, queryOptions)).pipe(
map(this.getEntryFromEntity),
catchError((err) => throwError(err))
);
}
/**
* Moves a node to the trashcan.
*
* @param nodeId ID of the target node
* @param options Optional parameters supported by JS-API
* @returns Empty result that notifies when the deletion is complete
*/
deleteNode(nodeId: string, options: any = {}): Observable<any> {
return from(this.nodesApi.deleteNode(nodeId, options)).pipe(
catchError((err) => throwError(err))
);
}
/**
* Restores a node previously moved to the trashcan.
*
* @param nodeId ID of the node to restore
* @returns Details of the restored node
*/
restoreNode(nodeId: string): Observable<MinimalNode> {
return from(this.trashcanApi.restoreDeletedNode(nodeId)).pipe(
map(this.getEntryFromEntity),
catchError((err) => throwError(err))
);
}
/**
* Get the metadata and the nodeType for a nodeId cleaned by the prefix.
*
* @param nodeId ID of the target node
* @returns Node metadata
*/
public getNodeMetadata(nodeId: string): Observable<NodeMetadata> {
return from(this.nodesApi.getNode(nodeId))
.pipe(map(this.cleanMetadataFromSemicolon));
}
/**
* Create a new Node from form metadata.
*
* @param path Path to the node
* @param nodeType Node type
* @param name Node name
* @param nameSpace Namespace for properties
* @param data Property data to store in the node under namespace
* @returns The created node
*/
public createNodeMetadata(nodeType: string, nameSpace: any, data: any, path: string, name?: string): Observable<NodeEntry> {
const properties = {};
for (const key in data) {
if (data[key]) {
properties[nameSpace + ':' + key] = data[key];
}
}
return this.createNodeInsideRoot(name || this.generateUuid(), nodeType, properties, path);
}
/**
* Gets content for the given node.
*
* @param nodeId ID of the target node
* @returns Content data
*/
getNodeContent(nodeId: string): Observable<any> {
return from(this.nodesApi.getNodeContent(nodeId))
.pipe(
catchError((err) => throwError(err))
);
}
/**
* Create a new Node inside `-root-` folder
*
* @param name Node name
* @param nodeType Node type
* @param properties Node body properties
* @param path Path to the node
* @returns The created node
*/
public createNodeInsideRoot(name: string, nodeType: string, properties: any, path: string): Observable<NodeEntry> {
const body = {
name,
nodeType,
properties,
relativePath: path
};
return from(this.nodesApi.createNode('-root-', body, {}));
}
private generateUuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
private cleanMetadataFromSemicolon(nodeEntry: NodeEntry): NodeMetadata {
const metadata = {};
if (nodeEntry && nodeEntry.entry.properties) {
for (const key in nodeEntry.entry.properties) {
if (key) {
if (key.indexOf(':') !== -1) {
metadata [key.split(':')[1]] = nodeEntry.entry.properties[key];
} else {
metadata [key] = nodeEntry.entry.properties[key];
}
}
}
}
return new NodeMetadata(metadata, nodeEntry.entry.nodeType);
}
}
@@ -0,0 +1,138 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { fakeEcmUserList, createNewPersonMock, fakeEcmUser, fakeEcmAdminUser } from '../mocks/ecm-user.service.mock';
import { AuthenticationService, AlfrescoApiService, AlfrescoApiServiceMock, CoreTestingModule, LogService } from '@alfresco/adf-core';
import { PeopleContentService, PeopleContentQueryRequestModel } from './people-content.service';
import { TranslateModule } from '@ngx-translate/core';
import { TestBed } from '@angular/core/testing';
describe('PeopleContentService', () => {
let peopleContentService: PeopleContentService;
let logService: LogService;
let authenticationService: AuthenticationService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
providers: [
{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }
]
});
authenticationService = TestBed.inject(AuthenticationService);
peopleContentService = TestBed.inject(PeopleContentService);
logService = TestBed.inject(LogService);
});
it('should be able to fetch person details based on id', async () => {
spyOn(peopleContentService.peopleApi, 'getPerson').and.returnValue(Promise.resolve({ entry: fakeEcmUser } as any));
const person = await peopleContentService.getPerson('fake-id').toPromise();
expect(person.id).toEqual('fake-id');
expect(person.email).toEqual('fakeEcm@ecmUser.com');
});
it('should be able to list people', async () => {
spyOn(peopleContentService.peopleApi, 'listPeople').and.returnValue(Promise.resolve(fakeEcmUserList));
const response = await peopleContentService.listPeople().toPromise();
const people = response.entries;
const pagination = response.pagination;
expect(people).toBeDefined();
expect(people[0].id).toEqual('fake-id');
expect(people[1].id).toEqual('another-fake-id');
expect(pagination.count).toEqual(2);
expect(pagination.totalItems).toEqual(2);
expect(pagination.hasMoreItems).toBeFalsy();
expect(pagination.skipCount).toEqual(0);
});
it('should call listPeople api with requested sorting params', async () => {
const listPeopleSpy = spyOn(peopleContentService.peopleApi, 'listPeople').and.returnValue(Promise.resolve(fakeEcmUserList));
const requestQueryParams: PeopleContentQueryRequestModel = {
skipCount: 10,
maxItems: 20,
sorting: { orderBy: 'firstName', direction: 'asc' }
};
const expectedValue = { skipCount: 10, maxItems: 20, orderBy: ['firstName ASC'] } as any;
await peopleContentService.listPeople(requestQueryParams).toPromise();
expect(listPeopleSpy).toHaveBeenCalledWith(expectedValue);
});
it('should not call listPeople api with sorting params if sorting is not defined', async () => {
const listPeopleSpy = spyOn(peopleContentService.peopleApi, 'listPeople').and.returnValue(Promise.resolve(fakeEcmUserList));
const requestQueryParams: PeopleContentQueryRequestModel = { skipCount: 10, maxItems: 20, sorting: undefined };
const expectedValue = { skipCount: 10, maxItems: 20 };
await peopleContentService.listPeople(requestQueryParams).toPromise();
expect(listPeopleSpy).toHaveBeenCalledWith(expectedValue);
});
it('should be able to create new person', async () => {
spyOn(peopleContentService.peopleApi, 'createPerson').and.returnValue(Promise.resolve({ entry: fakeEcmUser } as any));
const newUser = await peopleContentService.createPerson(createNewPersonMock).toPromise();
expect(newUser.id).toEqual('fake-id');
expect(newUser.email).toEqual('fakeEcm@ecmUser.com');
});
it('should be able to throw an error if createPerson api failed', (done) => {
spyOn(peopleContentService.peopleApi, 'createPerson').and.returnValue(Promise.reject('failed to create new person'));
const logErrorSpy = spyOn(logService, 'error');
peopleContentService.createPerson(createNewPersonMock).subscribe(
() => {
},
(error) => {
expect(logErrorSpy).toHaveBeenCalledWith('failed to create new person');
expect(error).toEqual('failed to create new person');
done();
}
);
});
it('Should make the api call to check if the user is a content admin only once', async () => {
const getCurrentPersonSpy = spyOn(peopleContentService.peopleApi, 'getPerson').and.returnValue(Promise.resolve({ entry: fakeEcmAdminUser } as any));
const user = await peopleContentService.getCurrentUserInfo().toPromise();
expect(user.id).toEqual('fake-id');
expect(peopleContentService.isCurrentUserAdmin()).toBe(true);
expect(getCurrentPersonSpy.calls.count()).toEqual(1);
await peopleContentService.getCurrentUserInfo().toPromise();
expect(await peopleContentService.isCurrentUserAdmin()).toBe(true);
expect(getCurrentPersonSpy.calls.count()).toEqual(1);
});
it('should reset the admin cache upon logout', async () => {
spyOn(peopleContentService.peopleApi, 'getPerson').and.returnValue(Promise.resolve({ entry: fakeEcmAdminUser } as any));
const user = await peopleContentService.getCurrentUserInfo().toPromise();
expect(user.id).toEqual('fake-id');
expect(peopleContentService.isCurrentUserAdmin()).toBe(true);
authenticationService.onLogout.next(true);
expect(peopleContentService.isCurrentUserAdmin()).toBe(false);
});
});
@@ -0,0 +1,186 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Observable, from, throwError, of } from 'rxjs';
import { AuthenticationService, AlfrescoApiService, LogService } from '@alfresco/adf-core';
import { catchError, map, tap } from 'rxjs/operators';
import { PeopleApi, PersonBodyCreate, Pagination, PersonBodyUpdate } from '@alfresco/js-api';
import { EcmUserModel } from '../models/ecm-user.model';
import { ContentService } from './content.service';
// eslint-disable-next-line no-shadow
export enum ContentGroups {
ALFRESCO_ADMINISTRATORS = 'ALFRESCO_ADMINISTRATORS'
}
export interface PeopleContentQueryResponse {
pagination: Pagination;
entries: EcmUserModel[];
}
export interface PeopleContentSortingModel {
orderBy: string;
direction: string;
}
export interface PeopleContentQueryRequestModel {
skipCount?: number;
maxItems?: number;
sorting?: PeopleContentSortingModel;
}
@Injectable({
providedIn: 'root'
})
export class PeopleContentService {
private currentUser: EcmUserModel;
private _peopleApi: PeopleApi;
get peopleApi(): PeopleApi {
this._peopleApi = this._peopleApi ?? new PeopleApi(this.apiService.getInstance());
return this._peopleApi;
}
constructor(
private apiService: AlfrescoApiService,
authenticationService: AuthenticationService,
private logService: LogService,
private contentService: ContentService
) {
authenticationService.onLogout.subscribe(() => {
this.resetLocalCurrentUser();
});
}
/**
* Gets information about a user identified by their username.
*
* @param personId ID of the target user
* @returns User information
*/
getPerson(personId: string): Observable<EcmUserModel> {
return from(this.peopleApi.getPerson(personId))
.pipe(
map((personEntry) => new EcmUserModel(personEntry.entry)),
tap(user => this.currentUser = user),
catchError((error) => this.handleError(error)));
}
getCurrentPerson(): Observable<EcmUserModel> {
return this.getCurrentUserInfo();
}
/**
* Gets information about the current user alias -me-
*
* @returns User information
*/
getCurrentUserInfo(): Observable<EcmUserModel> {
if (this.currentUser) {
return of(this.currentUser);
}
return this.getPerson('-me-');
}
/**
* Used to know if the current user has the admin capability
*
* @returns true or false
*/
isCurrentUserAdmin(): boolean {
return this.currentUser?.isAdmin() ?? false;
}
/**
* Reset the local current user object
*/
resetLocalCurrentUser() {
this.currentUser = undefined;
}
/**
* Gets a list of people.
*
* @param requestQuery maxItems and skipCount parameters supported by JS-API
* @returns Response containing pagination and list of entries
*/
listPeople(requestQuery?: PeopleContentQueryRequestModel): Observable<PeopleContentQueryResponse> {
const requestQueryParams = {skipCount: requestQuery?.skipCount, maxItems: requestQuery?.maxItems};
const orderBy = this.buildOrderArray(requestQuery?.sorting);
if (orderBy.length) {
requestQueryParams['orderBy'] = orderBy;
}
const promise = this.peopleApi.listPeople(requestQueryParams);
return from(promise).pipe(
map(response => ({
pagination: response.list.pagination,
entries: response.list.entries.map((person) => person.entry as EcmUserModel)
})),
catchError((err) => this.handleError(err))
);
}
/**
* Creates new person.
*
* @param newPerson Object containing the new person details.
* @param opts Optional parameters
* @returns Created new person
*/
createPerson(newPerson: PersonBodyCreate, opts?: any): Observable<EcmUserModel> {
return from(this.peopleApi.createPerson(newPerson, opts)).pipe(
map((res) => res?.entry as EcmUserModel),
catchError((error) => this.handleError(error))
);
}
/**
* Updates the person details
*
* @param personId The identifier of a person
* @param details The person details
* @param opts Optional parameters
* @returns Updated person model
*/
updatePerson(personId: string, details: PersonBodyUpdate, opts?: any): Observable<EcmUserModel> {
return from(this.peopleApi.updatePerson(personId, details, opts)).pipe(
map((res) => res?.entry as EcmUserModel),
catchError((error) => this.handleError(error))
);
}
/**
* Returns a profile image as a URL.
*
* @param avatarId Target avatar
* @returns Image URL
*/
getUserProfileImage(avatarId: string): string {
return this.contentService.getContentUrl(avatarId);
}
private buildOrderArray(sorting: PeopleContentSortingModel): string[] {
return sorting?.orderBy && sorting?.direction ? [`${sorting.orderBy} ${sorting.direction.toUpperCase()}`] : [];
}
private handleError(error: any) {
this.logService.error(error);
return throwError(error || 'Server error');
}
}
@@ -22,7 +22,7 @@ import { AlfrescoApiService , LogService, Track,TranslationService, ViewUtilServ
@Injectable({
providedIn: 'root'
})
export class RenditionViewerService {
export class RenditionService {
static TARGET = '_new';
@@ -82,8 +82,8 @@ export class RenditionViewerService {
getRenditionUrl(nodeId: string, type: string, renditionExists: boolean): string {
return (renditionExists && type !== RenditionViewerService.ContentGroup.IMAGE) ?
this.contentApi.getRenditionUrl(nodeId, RenditionViewerService.ContentGroup.PDF) :
return (renditionExists && type !== RenditionService.ContentGroup.IMAGE) ?
this.contentApi.getRenditionUrl(nodeId, RenditionService.ContentGroup.PDF) :
this.contentApi.getContentUrl(nodeId, false);
}
@@ -230,20 +230,20 @@ export class RenditionViewerService {
}
async generateMediaTracksRendition(nodeId: string): Promise<Track[]> {
return this.isRenditionAvailable(nodeId, RenditionViewerService.SUBTITLES_RENDITION_NAME)
return this.isRenditionAvailable(nodeId, RenditionService.SUBTITLES_RENDITION_NAME)
.then((value) => {
const tracks = [];
if (value) {
tracks.push({
kind: 'subtitles',
src: this.contentApi.getRenditionUrl(nodeId, RenditionViewerService.SUBTITLES_RENDITION_NAME),
src: this.contentApi.getRenditionUrl(nodeId, RenditionService.SUBTITLES_RENDITION_NAME),
label: this.translateService.instant('ADF_VIEWER.SUBTITLES')
});
}
return tracks;
})
.catch((err) => {
this.logService.error('Error while retrieving ' + RenditionViewerService.SUBTITLES_RENDITION_NAME + ' rendition');
this.logService.error('Error while retrieving ' + RenditionService.SUBTITLES_RENDITION_NAME + ' rendition');
this.logService.error(err);
return [];
});
@@ -262,10 +262,10 @@ export class RenditionViewerService {
* This URL should be one that can be rendered in the browser, for example PDF, Image, or Text
*/
printFile(url: string, type: string): void {
const pwa = window.open(url, RenditionViewerService.TARGET);
const pwa = window.open(url, RenditionService.TARGET);
if (pwa) {
// Because of the way chrome focus and close image window vs. pdf preview window
if (type === RenditionViewerService.ContentGroup.IMAGE) {
if (type === RenditionService.ContentGroup.IMAGE) {
pwa.onfocus = () => {
setTimeout(() => {
pwa.close();
@@ -290,12 +290,12 @@ export class RenditionViewerService {
const nodeId = objectId;
const type: string = this.viewUtilsService.getViewerTypeByMimeType(mimeType);
this.getRendition(nodeId, RenditionViewerService.ContentGroup.PDF)
this.getRendition(nodeId, RenditionService.ContentGroup.PDF)
.then((value) => {
const url: string = this.getRenditionUrl(nodeId, type, (!!value));
const printType = (type === RenditionViewerService.ContentGroup.PDF
|| type === RenditionViewerService.ContentGroup.TEXT)
? RenditionViewerService.ContentGroup.PDF : type;
const printType = (type === RenditionService.ContentGroup.PDF
|| type === RenditionService.ContentGroup.TEXT)
? RenditionService.ContentGroup.PDF : type;
this.printFile(url, printType);
})
.catch((err) => {
@@ -0,0 +1,561 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { AppConfigModule, AppConfigService, setupTestBed, CoreTestingModule } from '@alfresco/adf-core';
import { UploadService } from './upload.service';
import { RepositoryInfo } from '@alfresco/js-api';
import { TranslateModule } from '@ngx-translate/core';
import { BehaviorSubject } from 'rxjs';
import { DiscoveryApiService } from '../../common/services/discovery-api.service';
import { FileModel, FileUploadStatus } from '../../common/models/file.model';
declare let jasmine: any;
describe('UploadService', () => {
let service: UploadService;
let appConfigService: AppConfigService;
let uploadFileSpy: jasmine.Spy;
const mockProductInfo = new BehaviorSubject<RepositoryInfo>(null);
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule,
AppConfigModule
],
providers: [
{
provide: DiscoveryApiService,
useValue: {
ecmProductInfo$: mockProductInfo
}
}
]
});
beforeEach(() => {
appConfigService = TestBed.inject(AppConfigService);
appConfigService.config = {
ecmHost: 'http://localhost:9876/ecm',
files: {
excluded: ['.DS_Store', 'desktop.ini', '.git', '*.git', '*.SWF'],
'match-options': {
/* cspell:disable-next-line */
nocase: true
}
},
folders: {
excluded: ['ROLLINGPANDA'],
'match-options': {
/* cspell:disable-next-line */
nocase: true
}
}
};
service = TestBed.inject(UploadService);
service.queue = [];
service.clearCache();
uploadFileSpy = spyOn(service.uploadApi, 'uploadFile').and.callThrough();
jasmine.Ajax.install();
mockProductInfo.next({ status: { isThumbnailGenerationEnabled: true } } as RepositoryInfo);
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
it('should return an empty queue if no elements are added', () => {
expect(service.getQueue().length).toEqual(0);
});
it('should add an element in the queue and returns it', () => {
const filesFake = new FileModel({ name: 'fake-name', size: 10 } as File);
service.addToQueue(filesFake);
expect(service.getQueue().length).toEqual(1);
});
it('should add two elements in the queue and returns them', () => {
const filesFake = [
new FileModel({ name: 'fake-name', size: 10 } as File),
new FileModel({ name: 'fake-name2', size: 20 } as File)
];
service.addToQueue(...filesFake);
expect(service.getQueue().length).toEqual(2);
});
it('should not have the queue uploading if all files are complete, cancelled, aborted, errored or deleted', () => {
const file1 = new FileModel({ name: 'fake-file-1', size: 10 } as File);
const file2 = new FileModel({ name: 'fake-file-2', size: 20 } as File);
const file3 = new FileModel({ name: 'fake-file-3', size: 30 } as File);
const file4 = new FileModel({ name: 'fake-file-4', size: 40 } as File);
const file5 = new FileModel({ name: 'fake-file-5', size: 50 } as File);
file1.status = FileUploadStatus.Complete;
file2.status = FileUploadStatus.Cancelled;
file3.status = FileUploadStatus.Aborted;
file4.status = FileUploadStatus.Error;
file5.status = FileUploadStatus.Deleted;
service.addToQueue(file1, file2, file3, file4, file5);
expect(service.isUploading()).toBe(false);
});
it('should have the queue still uploading if some files are still pending, starting or in progress', () => {
const file1 = new FileModel({ name: 'fake-file-1', size: 10 } as File);
const file2 = new FileModel({ name: 'fake-file-2', size: 20 } as File);
service.addToQueue(file1, file2);
file1.status = FileUploadStatus.Complete;
file2.status = FileUploadStatus.Pending;
expect(service.isUploading()).toBe(true);
file2.status = FileUploadStatus.Starting;
expect(service.isUploading()).toBe(true);
file2.status = FileUploadStatus.Progress;
expect(service.isUploading()).toBe(true);
});
it('should skip hidden macOS files', () => {
const file1 = new FileModel(new File([''], '.git'));
const file2 = new FileModel(new File([''], 'readme.md'));
const result = service.addToQueue(file1, file2);
expect(result.length).toBe(1);
expect(result[0]).toBe(file2);
});
it('should match the extension in case insensitive way', () => {
const file1 = new FileModel(new File([''], 'test.swf'));
const file2 = new FileModel(new File([''], 'readme.md'));
const result = service.addToQueue(file1, file2);
expect(result.length).toBe(1);
expect(result[0]).toBe(file2);
});
it('should make XHR done request after the file is added in the queue', (done) => {
const emitter = new EventEmitter();
const emitterDisposable = emitter.subscribe((e) => {
expect(e.value).toBe('File uploaded');
emitterDisposable.unsubscribe();
done();
});
const fileFake = new FileModel(
{ name: 'fake-name', size: 10 } as File,
{ parentId: '-root-', path: 'fake-dir' }
);
service.addToQueue(fileFake);
service.uploadFilesInTheQueue(emitter);
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toBe('http://localhost:9876/ecm/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?autoRename=true&include=allowableOperations');
expect(request.method).toBe('POST');
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'text/plain',
responseText: 'File uploaded'
});
});
it('should make XHR error request after an error occur', (done) => {
const emitter = new EventEmitter();
const emitterDisposable = emitter.subscribe((e) => {
expect(e.value).toBe('Error file uploaded');
emitterDisposable.unsubscribe();
done();
});
const fileFake = new FileModel(
{ name: 'fake-name', size: 10 } as File,
{ parentId: '-root-' }
);
service.addToQueue(fileFake);
service.uploadFilesInTheQueue(null, emitter);
expect(jasmine.Ajax.requests.mostRecent().url)
.toBe('http://localhost:9876/ecm/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?autoRename=true&include=allowableOperations');
jasmine.Ajax.requests.mostRecent().respondWith({
status: 404,
contentType: 'text/plain',
responseText: 'Error file uploaded'
});
});
it('should abort file only if it is safe to abort', (done) => {
const emitter = new EventEmitter();
const emitterDisposable = emitter.subscribe((event) => {
expect(event.value).toEqual('File aborted');
emitterDisposable.unsubscribe();
done();
});
const fileFake = new FileModel({ name: 'fake-name', size: 10000000 } as File);
service.addToQueue(fileFake);
service.uploadFilesInTheQueue(emitter);
const file = service.getQueue();
service.cancelUpload(...file);
});
it('should let file complete and then delete node if it is not safe to abort', (done) => {
const emitter = new EventEmitter();
const emitterDisposable = emitter.subscribe((event) => {
expect(event.value).toEqual('File deleted');
emitterDisposable.unsubscribe();
const deleteRequest = jasmine.Ajax.requests.mostRecent();
expect(deleteRequest.url).toBe('http://localhost:9876/ecm/alfresco/api/-default-/public/alfresco/versions/1/nodes/myNodeId?permanent=true');
expect(deleteRequest.method).toBe('DELETE');
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'text/plain',
responseText: 'File deleted'
});
done();
});
const fileFake = new FileModel({ name: 'fake-name', size: 10 } as File);
service.addToQueue(fileFake);
service.uploadFilesInTheQueue(emitter);
const file = service.getQueue();
service.cancelUpload(...file);
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toBe('http://localhost:9876/ecm/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?autoRename=true&include=allowableOperations');
expect(request.method).toBe('POST');
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: {
entry: {
id: 'myNodeId'
}
}
});
});
it('should delete node version when cancelling the upload of the new file version', (done) => {
const emitter = new EventEmitter();
const emitterDisposable = emitter.subscribe((event) => {
expect(event.value).toEqual('File deleted');
emitterDisposable.unsubscribe();
const deleteRequest = jasmine.Ajax.requests.mostRecent();
expect(deleteRequest.url).toBe('http://localhost:9876/ecm/alfresco/api/-default-/public/alfresco/versions/1/nodes/myNodeId/versions/1.1');
expect(deleteRequest.method).toBe('DELETE');
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'text/plain',
responseText: 'File deleted'
});
done();
});
const fileFake = new FileModel({name: 'fake-name', size: 10} as File, null, 'fakeId');
service.addToQueue(fileFake);
service.uploadFilesInTheQueue(emitter);
const file = service.getQueue();
service.cancelUpload(...file);
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toBe('http://localhost:9876/ecm/alfresco/api/-default-/public/alfresco/versions/1/nodes/fakeId/content?include=allowableOperations');
expect(request.method).toBe('PUT');
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: {
entry: {
id: 'myNodeId',
properties: {
'cm:versionLabel': '1.1'
}
}
}
});
});
it('If newVersion is set, name should be a param', () => {
const emitter = new EventEmitter();
const filesFake = new FileModel(
{ name: 'fake-name', size: 10 } as File,
{ newVersion: true }
);
service.addToQueue(filesFake);
service.uploadFilesInTheQueue(emitter);
expect(uploadFileSpy).toHaveBeenCalledWith(
{
name: 'fake-name',
size: 10
},
undefined,
undefined,
{ newVersion: true },
{
renditions: 'doclib',
include: ['allowableOperations'],
overwrite: true,
majorVersion: undefined,
comment: undefined,
name: 'fake-name'
}
);
});
it('should use custom root folder ID given to the service', (done) => {
const emitter = new EventEmitter();
const emitterDisposable = emitter.subscribe((e) => {
expect(e.value).toBe('File uploaded');
emitterDisposable.unsubscribe();
done();
});
const filesFake = new FileModel(
{ name: 'fake-file-name', size: 10 } as File,
{ parentId: '123', path: 'fake-dir' }
);
service.addToQueue(filesFake);
service.uploadFilesInTheQueue(emitter);
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toBe('http://localhost:9876/ecm/alfresco/api/-default-/public/alfresco/versions/1/nodes/123/children?autoRename=true&include=allowableOperations');
expect(request.method).toBe('POST');
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'text/plain',
responseText: 'File uploaded'
});
});
describe('versioningEnabled', () => {
it('should upload with "versioningEnabled" parameter taken from file options', () => {
const model = new FileModel(
{ name: 'file-name', size: 10 } as File,
{ versioningEnabled: true }
);
service.addToQueue(model);
service.uploadFilesInTheQueue();
expect(uploadFileSpy).toHaveBeenCalledWith(
{
name: 'file-name',
size: 10
},
undefined,
undefined,
{ newVersion: false },
{
include: [ 'allowableOperations' ],
renditions: 'doclib',
versioningEnabled: true,
autoRename: true
}
);
});
it('should not use "versioningEnabled" if not explicitly provided', () => {
const model = new FileModel(
{ name: 'file-name', size: 10 } as File,
{}
);
service.addToQueue(model);
service.uploadFilesInTheQueue();
expect(uploadFileSpy).toHaveBeenCalledWith(
{
name: 'file-name',
size: 10
},
undefined,
undefined,
{ newVersion: false },
{
include: [ 'allowableOperations' ],
renditions: 'doclib',
autoRename: true
}
);
});
});
it('should append the extra upload options to the request', () => {
const filesFake = new FileModel(
{ name: 'fake-name', size: 10 } as File,
{
parentId: '123',
path: 'fake-dir',
secondaryChildren: [{ assocType: 'assoc-1', childId: 'child-id' }],
association: { assocType: 'fake-assoc' },
targets: [{ assocType: 'target-assoc', targetId: 'fake-target-id' }]
});
service.addToQueue(filesFake);
service.uploadFilesInTheQueue();
expect(uploadFileSpy).toHaveBeenCalledWith(
{
name: 'fake-name',
size: 10
},
'fake-dir',
'123',
{
newVersion: false,
parentId: '123',
path: 'fake-dir',
secondaryChildren: [ { assocType: 'assoc-1', childId: 'child-id' }],
association: { assocType: 'fake-assoc' },
targets: [{ assocType: 'target-assoc', targetId: 'fake-target-id' }]
},
{
renditions: 'doclib',
include: ['allowableOperations'],
autoRename: true
}
);
});
it('should start downloading the next one if a file of the list is aborted', (done) => {
service.fileUploadAborted.subscribe((e) => {
expect(e).not.toBeNull();
});
service.fileUploadCancelled.subscribe((e) => {
expect(e).not.toBeNull();
done();
});
const fileFake1 = new FileModel({ name: 'fake-name1', size: 10 } as File);
const fileFake2 = new FileModel({ name: 'fake-name2', size: 10 } as File);
const fileList = [fileFake1, fileFake2];
service.addToQueue(...fileList);
service.uploadFilesInTheQueue();
const file = service.getQueue();
service.cancelUpload(...file);
});
it('should remove from the queue all the files in the excluded list', () => {
const file1 = new FileModel(new File([''], '.git'));
const file2 = new FileModel(new File([''], '.DS_Store'));
const file3 = new FileModel(new File([''], 'desktop.ini'));
const file4 = new FileModel(new File([''], 'readme.md'));
const file5 = new FileModel(new File([''], 'test.git'));
const result = service.addToQueue(file1, file2, file3, file4, file5);
expect(result.length).toBe(1);
expect(result[0]).toBe(file4);
});
it('should skip files if they are in an excluded folder', () => {
const file1: any = { name: 'readmetoo.md', file : { webkitRelativePath: '/rollingPanda/' }};
const file2: any = { name: 'readme.md', file : { webkitRelativePath: '/test/' }};
const result = service.addToQueue(file1, file2);
expect(result.length).toBe(1);
expect(result[0]).toBe(file2);
});
it('should match the folder in case insensitive way', () => {
const file1: any = { name: 'readmetoo.md', file : { webkitRelativePath: '/rollingPanda/' }};
const file2: any = { name: 'readme.md', file : { webkitRelativePath: '/test/' }};
const result = service.addToQueue(file1, file2);
expect(result.length).toBe(1);
expect(result[0]).toBe(file2);
});
it('should skip files if they are in an excluded folder when path is in options', () => {
const file1: any = { name: 'readmetoo.md', file : {}, options: { path: '/rollingPanda/'}};
const file2: any = { name: 'readme.md', file : { webkitRelativePath: '/test/' }};
const result = service.addToQueue(file1, file2);
expect(result.length).toBe(1);
expect(result[0]).toBe(file2);
});
it('should call onUploadDeleted if file was deleted', () => {
const file = { status: FileUploadStatus.Deleted } as FileModel;
spyOn(service.fileUploadDeleted, 'next');
service.cancelUpload(file);
expect(service.fileUploadDeleted.next).toHaveBeenCalled();
});
it('should call fileUploadError if file has error status', () => {
const file = { status: FileUploadStatus.Error } as FileModel;
spyOn(service.fileUploadError, 'next');
service.cancelUpload(file);
expect(service.fileUploadError.next).toHaveBeenCalled();
});
it('should call fileUploadCancelled if file is in pending', () => {
const file = { status: FileUploadStatus.Pending } as FileModel;
spyOn(service.fileUploadCancelled, 'next');
service.cancelUpload(file);
expect(service.fileUploadCancelled.next).toHaveBeenCalled();
});
it('Should not pass rendition if it is disabled', () => {
mockProductInfo.next({ status: { isThumbnailGenerationEnabled: false } } as RepositoryInfo);
const filesFake = new FileModel(
{ name: 'fake-name', size: 10 } as File,
{ newVersion: true}
);
service.addToQueue(filesFake);
service.uploadFilesInTheQueue();
expect(uploadFileSpy).toHaveBeenCalledWith(
{
name: 'fake-name',
size: 10
},
undefined,
undefined,
{ newVersion: true },
{
include: ['allowableOperations'],
overwrite: true,
majorVersion: undefined,
comment: undefined,
name: 'fake-name'
}
);
});
});
@@ -0,0 +1,475 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter, Injectable } from '@angular/core';
import { Minimatch } from 'minimatch';
import { Subject } from 'rxjs';
import {
FileUploadCompleteEvent,
FileUploadDeleteEvent,
FileUploadErrorEvent,
FileUploadEvent
} from '../events/file.event';
import { FileModel, FileUploadProgress, FileUploadStatus } from '../models/file.model';
import { AppConfigService, AlfrescoApiService } from '@alfresco/adf-core';
import { filter } from 'rxjs/operators';
import { DiscoveryApiService } from '../../common/services/discovery-api.service';
import { NodesApi, UploadApi, VersionsApi } from '@alfresco/js-api';
const MIN_CANCELLABLE_FILE_SIZE = 1000000;
const MAX_CANCELLABLE_FILE_PERCENTAGE = 50;
@Injectable({
providedIn: 'root'
})
export class UploadService {
queue: FileModel[] = [];
queueChanged: Subject<FileModel[]> = new Subject<FileModel[]>();
fileUpload: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadStarting: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadCancelled: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadProgress: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadAborted: Subject<FileUploadEvent> = new Subject<FileUploadEvent>();
fileUploadError: Subject<FileUploadErrorEvent> = new Subject<FileUploadErrorEvent>();
fileUploadComplete: Subject<FileUploadCompleteEvent> = new Subject<FileUploadCompleteEvent>();
fileUploadDeleted: Subject<FileUploadDeleteEvent> = new Subject<FileUploadDeleteEvent>();
fileDeleted: Subject<string> = new Subject<string>();
private cache: { [key: string]: any } = {};
private totalComplete: number = 0;
private totalAborted: number = 0;
private totalError: number = 0;
private excludedFileList: string[] = [];
private excludedFoldersList: string[] = [];
private matchingOptions: any = null;
private folderMatchingOptions: any = null;
private abortedFile: string;
private isThumbnailGenerationEnabled: boolean;
private _uploadApi: UploadApi;
get uploadApi(): UploadApi {
this._uploadApi = this._uploadApi ?? new UploadApi(this.apiService.getInstance());
return this._uploadApi;
}
private _nodesApi: NodesApi;
get nodesApi(): NodesApi {
this._nodesApi = this._nodesApi ?? new NodesApi(this.apiService.getInstance());
return this._nodesApi;
}
private _versionsApi: VersionsApi;
get versionsApi(): VersionsApi {
this._versionsApi = this._versionsApi ?? new VersionsApi(this.apiService.getInstance());
return this._versionsApi;
}
constructor(
protected apiService: AlfrescoApiService,
private appConfigService: AppConfigService,
private discoveryApiService: DiscoveryApiService) {
this.discoveryApiService.ecmProductInfo$.pipe(filter(info => !!info))
.subscribe(({status}) => {
this.isThumbnailGenerationEnabled = status.isThumbnailGenerationEnabled;
});
}
clearCache() {
this.cache = {};
}
/**
* Returns the number of concurrent threads for uploading.
*
* @returns Number of concurrent threads (default 1)
*/
getThreadsCount(): number {
return this.appConfigService.get<number>('upload.threads', 1);
}
/**
* Checks whether the service still has files uploading or awaiting upload.
*
* @returns True if files in the queue are still uploading, false otherwise
*/
isUploading(): boolean {
const finishedFileStates = [FileUploadStatus.Complete, FileUploadStatus.Cancelled, FileUploadStatus.Aborted, FileUploadStatus.Error, FileUploadStatus.Deleted];
return this.queue.reduce((stillUploading: boolean, currentFile: FileModel) => stillUploading || finishedFileStates.indexOf(currentFile.status) === -1, false);
}
/**
* Gets the file Queue
*
* @returns Array of files that form the queue
*/
getQueue(): FileModel[] {
return this.queue;
}
/**
* Adds files to the uploading queue to be uploaded
*
* @param files One or more separate parameters or an array of files to queue
* @returns Array of files that were not blocked from upload by the ignore list
*/
addToQueue(...files: FileModel[]): FileModel[] {
const allowedFiles = files.filter((currentFile) =>
this.filterElement(currentFile)
);
this.queue = this.queue.concat(allowedFiles);
this.queueChanged.next(this.queue);
return allowedFiles;
}
/**
* Finds all the files in the queue that are not yet uploaded and uploads them into the directory folder.
*
* @param successEmitter Emitter to invoke on file success status change
* @param errorEmitter Emitter to invoke on file error status change
*/
uploadFilesInTheQueue(successEmitter?: EventEmitter<any>, errorEmitter?: EventEmitter<any>): void {
const files = this.getFilesToUpload();
if (files && files.length > 0) {
for (const file of files) {
this.onUploadStarting(file);
const promise = this.beginUpload(file, successEmitter, errorEmitter);
this.cache[file.name] = promise;
const next = () => {
setTimeout(() => this.uploadFilesInTheQueue(successEmitter, errorEmitter), 100);
};
promise.next = next;
promise.then(
() => next(),
() => next()
);
}
}
}
/**
* Cancels uploading of files.
* If the file is smaller than 1 MB the file will be uploaded and then the node deleted
* to prevent having files that were aborted but still uploaded.
*
* @param files One or more separate parameters or an array of files specifying uploads to cancel
*/
cancelUpload(...files: FileModel[]) {
files.forEach((file) => {
const promise = this.cache[file.name];
if (promise) {
if (this.isSaveToAbortFile(file)) {
promise.abort();
} else {
this.abortedFile = file.name;
}
delete this.cache[file.name];
promise.next();
} else {
const performAction = this.getAction(file);
if (performAction) {
performAction();
}
}
});
}
/** Clears the upload queue */
clearQueue() {
this.queue = [];
this.totalComplete = 0;
this.totalAborted = 0;
this.totalError = 0;
}
/**
* Gets an upload promise for a file.
*
* @param file The target file
* @returns Promise that is resolved if the upload is successful or error otherwise
*/
getUploadPromise(file: FileModel): any {
const opts: any = {
include: ['allowableOperations']
};
if (this.isThumbnailGenerationEnabled) {
opts.renditions = 'doclib';
}
if (file.options && file.options.versioningEnabled !== undefined) {
opts.versioningEnabled = file.options.versioningEnabled;
}
if (file.options.newVersion === true) {
opts.overwrite = true;
opts.majorVersion = file.options.majorVersion;
opts.comment = file.options.comment;
opts.name = file.name;
} else {
opts.autoRename = true;
}
if (file.options.nodeType) {
opts.nodeType = file.options.nodeType;
}
if (file.id) {
return this.nodesApi.updateNodeContent(file.id, file.file as any, opts);
} else {
const nodeBody = {...file.options};
delete nodeBody['versioningEnabled'];
return this.uploadApi.uploadFile(
file.file,
file.options.path,
file.options.parentId,
nodeBody,
opts
);
}
}
private getFilesToUpload(): FileModel[] {
const cached = Object.keys(this.cache);
const threadsCount = this.getThreadsCount();
if (cached.length >= threadsCount) {
return [];
}
const files = this.queue
.filter(toUpload => !cached.includes(toUpload.name) && toUpload.status === FileUploadStatus.Pending)
.slice(0, threadsCount);
return files;
}
private beginUpload(file: FileModel, successEmitter?: EventEmitter<any>, errorEmitter?: EventEmitter<any>): any {
const promise = this.getUploadPromise(file);
promise
.on('progress', (progress: FileUploadProgress) => {
this.onUploadProgress(file, progress);
})
.on('abort', () => {
this.onUploadAborted(file);
if (successEmitter) {
successEmitter.emit({value: 'File aborted'});
}
})
.on('error', (err) => {
this.onUploadError(file, err);
if (errorEmitter) {
errorEmitter.emit({value: 'Error file uploaded'});
}
})
.on('success', (data) => {
if (this.abortedFile === file.name) {
this.onUploadAborted(file);
if (file.id === undefined) {
this.deleteAbortedNode(data.entry.id);
} else {
this.deleteAbortedNodeVersion(data.entry.id, data.entry.properties['cm:versionLabel']);
}
if (successEmitter) {
successEmitter.emit({value: 'File deleted'});
}
} else {
this.onUploadComplete(file, data);
if (successEmitter) {
successEmitter.emit({value: data});
}
}
})
.catch(() => {
});
return promise;
}
private onUploadStarting(file: FileModel): void {
if (file) {
file.status = FileUploadStatus.Starting;
const event = new FileUploadEvent(file, FileUploadStatus.Starting);
this.fileUpload.next(event);
this.fileUploadStarting.next(event);
}
}
private onUploadProgress(
file: FileModel,
progress: FileUploadProgress
): void {
if (file) {
file.progress = progress;
file.status = FileUploadStatus.Progress;
const event = new FileUploadEvent(file, FileUploadStatus.Progress);
this.fileUpload.next(event);
this.fileUploadProgress.next(event);
}
}
private onUploadError(file: FileModel, error: any): void {
if (file) {
file.errorCode = (error || {}).status;
file.status = FileUploadStatus.Error;
this.totalError++;
const promise = this.cache[file.name];
if (promise) {
delete this.cache[file.name];
}
const event = new FileUploadErrorEvent(
file,
error,
this.totalError
);
this.fileUpload.next(event);
this.fileUploadError.next(event);
}
}
private onUploadComplete(file: FileModel, data: any): void {
if (file) {
file.status = FileUploadStatus.Complete;
file.data = data;
this.totalComplete++;
const promise = this.cache[file.name];
if (promise) {
delete this.cache[file.name];
}
const event = new FileUploadCompleteEvent(
file,
this.totalComplete,
data,
this.totalAborted
);
this.fileUpload.next(event);
this.fileUploadComplete.next(event);
}
}
private onUploadAborted(file: FileModel): void {
if (file) {
file.status = FileUploadStatus.Aborted;
this.totalAborted++;
const event = new FileUploadEvent(file, FileUploadStatus.Aborted);
this.fileUpload.next(event);
this.fileUploadAborted.next(event);
}
}
private onUploadCancelled(file: FileModel): void {
if (file) {
file.status = FileUploadStatus.Cancelled;
const event = new FileUploadEvent(file, FileUploadStatus.Cancelled);
this.fileUpload.next(event);
this.fileUploadCancelled.next(event);
}
}
private onUploadDeleted(file: FileModel): void {
if (file) {
file.status = FileUploadStatus.Deleted;
this.totalComplete--;
const event = new FileUploadDeleteEvent(file, this.totalComplete);
this.fileUpload.next(event);
this.fileUploadDeleted.next(event);
}
}
private getAction(file: FileModel) {
const actions = {
[FileUploadStatus.Pending]: () => this.onUploadCancelled(file),
[FileUploadStatus.Deleted]: () => this.onUploadDeleted(file),
[FileUploadStatus.Error]: () => this.onUploadError(file, null)
};
return actions[file.status];
}
private deleteAbortedNode(nodeId: string) {
this.nodesApi.deleteNode(nodeId, {permanent: true})
.then(() => (this.abortedFile = undefined));
}
private deleteAbortedNodeVersion(nodeId: string, versionId: string) {
this.versionsApi.deleteVersion(nodeId, versionId)
.then(() => (this.abortedFile = undefined));
}
private isSaveToAbortFile(file: FileModel): boolean {
return (
file.size > MIN_CANCELLABLE_FILE_SIZE &&
file.progress.percent < MAX_CANCELLABLE_FILE_PERCENTAGE
);
}
private filterElement(file: FileModel) {
this.excludedFileList = this.appConfigService.get<string[]>('files.excluded');
this.excludedFoldersList = this.appConfigService.get<string[]>('folders.excluded');
let isAllowed = true;
if (this.excludedFileList) {
this.matchingOptions = this.appConfigService.get('files.match-options');
isAllowed = this.isFileNameAllowed(file);
}
if (isAllowed && this.excludedFoldersList) {
this.folderMatchingOptions = this.appConfigService.get('folders.match-options');
isAllowed = this.isParentFolderAllowed(file);
}
return isAllowed;
}
private isParentFolderAllowed(file: FileModel): boolean {
let isAllowed: boolean = true;
const currentFile: any = file.file;
const fileRelativePath = currentFile.webkitRelativePath ? currentFile.webkitRelativePath : file.options.path;
if (currentFile && fileRelativePath) {
isAllowed =
this.excludedFoldersList.filter((folderToExclude) => fileRelativePath
.split('/')
.some((pathElement) => {
const minimatch = new Minimatch(folderToExclude, this.folderMatchingOptions);
return minimatch.match(pathElement);
})).length === 0;
}
return isAllowed;
}
private isFileNameAllowed(file: FileModel): boolean {
return (
this.excludedFileList.filter((pattern) => {
const minimatch = new Minimatch(pattern, this.matchingOptions);
return minimatch.match(file.name);
}).length === 0
);
}
}
@@ -20,12 +20,13 @@ import { By } from '@angular/platform-browser';
import { Node } from '@alfresco/js-api';
import { ContentMetadataCardComponent } from './content-metadata-card.component';
import { ContentMetadataComponent } from '../content-metadata/content-metadata.component';
import { setupTestBed, AllowableOperationsEnum } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { SimpleChange } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { NodeAspectService } from '../../../aspect-list/services/node-aspect.service';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { AllowableOperationsEnum } from '../../../common/models/allowable-operations.enum';
import { of } from 'rxjs';
describe('ContentMetadataCardComponent', () => {
@@ -17,10 +17,12 @@
import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Node } from '@alfresco/js-api';
import { ContentService, AllowableOperationsEnum } from '@alfresco/adf-core';
import { NodeAspectService } from '../../../aspect-list/services/node-aspect.service';
import { PresetConfig } from '../../interfaces/content-metadata.interfaces';
import { VersionCompatibilityService } from '../../../version-compatibility/version-compatibility.service';
import { ContentService } from '../../../common/services/content.service';
import { AllowableOperationsEnum } from '../../../common/models/allowable-operations.enum';
@Component({
selector: 'adf-content-metadata-card',
templateUrl: './content-metadata-card.component.html',
@@ -22,9 +22,10 @@ import { ClassesApi, MinimalNode, Node } from '@alfresco/js-api';
import { ContentMetadataComponent } from './content-metadata.component';
import { ContentMetadataService } from '../../services/content-metadata.service';
import {
CardViewBaseItemModel, CardViewComponent, NodesApiService,
CardViewBaseItemModel, CardViewComponent,
LogService, setupTestBed, AppConfigService
} from '@alfresco/adf-core';
import { NodesApiService } from '../../../common/services/nodes-api.service';
import { throwError, of } from 'rxjs';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { mockGroupProperties } from './mock-data';
@@ -20,7 +20,6 @@ import { Node } from '@alfresco/js-api';
import { Observable, Subject, of, zip } from 'rxjs';
import {
CardViewItem,
NodesApiService,
LogService,
TranslationService,
AppConfigService,
@@ -31,6 +30,7 @@ import { ContentMetadataService } from '../../services/content-metadata.service'
import { CardViewGroup, PresetConfig } from '../../interfaces/content-metadata.interfaces';
import { takeUntil, debounceTime, catchError, map } from 'rxjs/operators';
import { CardViewContentUpdateService } from '../../../common/services/card-view-content-update.service';
import { NodesApiService } from '../../../common/services/nodes-api.service';
const DEFAULT_SEPARATOR = ', ';
@@ -17,9 +17,11 @@
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { EventEmitter, Injectable, Output } from '@angular/core';
import { ContentService, ThumbnailService, TranslationService, AllowableOperationsEnum } from '@alfresco/adf-core';
import { ThumbnailService, TranslationService } from '@alfresco/adf-core';
import { Subject, Observable, throwError } from 'rxjs';
import { ShareDataRow } from '../document-list/data/share-data-row.model';
import { AllowableOperationsEnum } from '../common/models/allowable-operations.enum';
import { ContentService } from '../common/services/content.service';
import { Node, NodeEntry, SitePaging } from '@alfresco/js-api';
import { DocumentListService } from '../document-list/services/document-list.service';
import { ContentNodeSelectorComponent } from './content-node-selector.component';
@@ -28,7 +28,7 @@ import {
SiteEntry,
SitePaging
} from '@alfresco/js-api';
import { NodesApiService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { of } from 'rxjs';
import { ContentNodeSelectorPanelComponent } from './content-node-selector-panel.component';
import { ContentTestingModule } from '../testing/content.testing.module';
@@ -40,6 +40,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { SearchQueryBuilderService } from '../search';
import { mockQueryBody } from '../mock/search-query.mock';
import { SitesService } from '../common/services/sites.service';
import { NodesApiService } from '../common/services/nodes-api.service';
const fakeResultSetPaging: ResultSetPaging = {
list: {
@@ -30,17 +30,17 @@ import {
} from '@alfresco/js-api';
import {
AppConfigService,
FileModel,
FileUploadStatus,
NodesApiService,
setupTestBed,
UploadService,
FileUploadCompleteEvent,
DataRow,
ThumbnailService,
ContentService,
DataColumn
} from '@alfresco/adf-core';
import { ContentService } from '../common/services/content.service';
import { UploadService } from '../common/services/upload.service';
import { NodesApiService } from '../common/services/nodes-api.service';
import { FileModel, FileUploadStatus } from '../common/models/file.model';
import { FileUploadCompleteEvent } from '../common/events/file.event';
import { of, throwError } from 'rxjs';
import { DropdownBreadcrumbComponent } from '../breadcrumb';
import { ContentNodeSelectorPanelComponent } from './content-node-selector-panel.component';
@@ -31,14 +31,13 @@ import {
UserPreferencesService,
UserPreferenceValues,
InfinitePaginationComponent, PaginatedComponent,
NodesApiService,
UploadService,
FileUploadCompleteEvent,
FileUploadDeleteEvent,
AppConfigService,
DataSorting,
ShowHeaderMode
} from '@alfresco/adf-core';
import { NodesApiService } from '../common/services/nodes-api.service';
import { UploadService } from '../common/services/upload.service';
import { FileUploadCompleteEvent, FileUploadDeleteEvent } from '../common/events/file.event';
import { UntypedFormControl } from '@angular/forms';
import { Node, NodePaging, Pagination, SiteEntry, SitePaging, NodeEntry, QueryBody, RequestScope } from '@alfresco/js-api';
import { DocumentListComponent } from '../document-list/components/document-list.component';
@@ -21,7 +21,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ContentNodeSelectorComponent } from './content-node-selector.component';
import { Node, NodeEntry, SitePaging } from '@alfresco/js-api';
import { By } from '@angular/platform-browser';
import { ContentService, UploadService, FileModel, FileUploadEvent } from '@alfresco/adf-core';
import { FileModel } from '../common/models/file.model';
import { FileUploadEvent } from '../common/events/file.event';
import { UploadService } from '../common/services/upload.service';
import { of } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
import { DocumentListService } from '../document-list/services/document-list.service';
@@ -31,6 +34,8 @@ import { UploadModule } from '../upload';
import { ContentNodeSelectorPanelComponent } from './content-node-selector-panel.component';
import { NodeAction } from '../document-list/models/node-action.enum';
import { SitesService } from '../common/services/sites.service';
import { NodesApiService } from '../common/services/nodes-api.service';
import { ContentService } from '../common/services/content.service';
describe('ContentNodeSelectorComponent', () => {
let component: ContentNodeSelectorComponent;
@@ -103,7 +108,8 @@ describe('ContentNodeSelectorComponent', () => {
}
});
spyOn(contentService, 'getNode').and.returnValue(of(fakeFolderNodeWithPermission));
const nodesApiService = TestBed.inject(NodesApiService);
spyOn(nodesApiService, 'getNode').and.returnValue(of(fakeFolderNodeWithPermission.entry));
component.data.showLocalUploadButton = true;
component.hasAllowableOperations = true;
@@ -17,9 +17,11 @@
import { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { TranslationService, NotificationService, AllowableOperationsEnum, ContentService, UploadService } from '@alfresco/adf-core';
import { TranslationService, NotificationService} from '@alfresco/adf-core';
import { Node } from '@alfresco/js-api';
import { AllowableOperationsEnum } from '../common/models/allowable-operations.enum';
import { ContentService } from '../common/services/content.service';
import { UploadService } from '../common/services/upload.service';
import { ContentNodeSelectorComponentData } from './content-node-selector.component-data.interface';
import { NodeEntryEvent } from '../document-list/components/node.event';
import { NodeAction } from '../document-list/models/node-action.enum';
@@ -17,14 +17,15 @@
import { TestBed, fakeAsync, ComponentFixture, tick } from '@angular/core/testing';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog';
import { of, empty } from 'rxjs';
import { of } from 'rxjs';
import {
setupTestBed,
NodesApiService,
NotificationService,
RenditionsService,
AppConfigService
} from '@alfresco/adf-core';
import { NodesApiService } from '../common/services/nodes-api.service';
import { RenditionService } from '../common/services/rendition.service';
import { SharedLinksApiService } from './services/shared-links-api.service';
import { ShareDialogComponent } from './content-node-share.dialog';
import moment from 'moment';
@@ -38,7 +39,7 @@ describe('ShareDialogComponent', () => {
openSnackMessage: jasmine.createSpy('openSnackMessage')
};
let sharedLinksApiService: SharedLinksApiService;
let renditionService: RenditionsService;
let renditionService: RenditionService;
let nodesApiService: NodesApiService;
let fixture: ComponentFixture<ShareDialogComponent>;
let component: ShareDialogComponent;
@@ -50,9 +51,14 @@ describe('ShareDialogComponent', () => {
ContentTestingModule
],
providers: [
{ provide: NotificationService, useValue: notificationServiceMock },
{ provide: MatDialogRef, useValue: { close: () => {}} },
{ provide: MAT_DIALOG_DATA, useValue: {} }
{provide: NotificationService, useValue: notificationServiceMock},
{
provide: MatDialogRef, useValue: {
close: () => {
}
}
},
{provide: MAT_DIALOG_DATA, useValue: {}}
]
});
@@ -63,7 +69,7 @@ describe('ShareDialogComponent', () => {
matDialog = TestBed.inject(MatDialog);
sharedLinksApiService = TestBed.inject(SharedLinksApiService);
renditionService = TestBed.inject(RenditionsService);
renditionService = TestBed.inject(RenditionService);
nodesApiService = TestBed.inject(NodesApiService);
appConfigService = TestBed.inject(AppConfigService);
@@ -117,9 +123,9 @@ describe('ShareDialogComponent', () => {
it(`should toggle share action when property 'sharedId' does not exists`, () => {
spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of({
entry: { id: 'sharedId', sharedId: 'sharedId' }
entry: {id: 'sharedId', sharedId: 'sharedId'}
}));
spyOn(renditionService, 'generateRenditionForNode').and.returnValue(empty());
spyOn(renditionService, 'getNodeRendition').and.returnValue(Promise.resolve({url: '', mimeType: ''}));
component.data = {
node,
@@ -129,16 +135,16 @@ describe('ShareDialogComponent', () => {
fixture.detectChanges();
expect(sharedLinksApiService.createSharedLinks).toHaveBeenCalled();
expect(renditionService.generateRenditionForNode).toHaveBeenCalled();
expect(renditionService.getNodeRendition).toHaveBeenCalled();
expect(fixture.nativeElement.querySelector('input[formcontrolname="sharedUrl"]').value).toBe('some-url/sharedId');
expect(fixture.nativeElement.querySelector('.mat-slide-toggle').classList).toContain('mat-checked');
});
it(`should not toggle share action when file has 'sharedId' property`, async () => {
spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of({
entry: { id: 'sharedId', sharedId: 'sharedId' }
entry: {id: 'sharedId', sharedId: 'sharedId'}
}));
spyOn(renditionService, 'generateRenditionForNode').and.returnValue(empty());
spyOn(renditionService, 'getNodeRendition').and.returnValue(Promise.resolve({url: '', mimeType: ''}));
node.entry.properties['qshare:sharedId'] = 'sharedId';
@@ -158,7 +164,7 @@ describe('ShareDialogComponent', () => {
});
it('should open a confirmation dialog when unshare button is triggered', () => {
spyOn(matDialog, 'open').and.returnValue({ beforeClosed: () => of(false) } as any);
spyOn(matDialog, 'open').and.returnValue({beforeClosed: () => of(false)} as any);
spyOn(sharedLinksApiService, 'deleteSharedLink').and.callThrough();
node.entry.properties['qshare:sharedId'] = 'sharedId';
@@ -179,7 +185,7 @@ describe('ShareDialogComponent', () => {
});
it('should unshare file when confirmation dialog returns true', fakeAsync(() => {
spyOn(matDialog, 'open').and.returnValue({ beforeClosed: () => of(true) } as any);
spyOn(matDialog, 'open').and.returnValue({beforeClosed: () => of(true)} as any);
spyOn(sharedLinksApiService, 'deleteSharedLink').and.returnValue(of({}));
node.entry.properties['qshare:sharedId'] = 'sharedId';
@@ -199,7 +205,7 @@ describe('ShareDialogComponent', () => {
}));
it('should not unshare file when confirmation dialog returns false', fakeAsync(() => {
spyOn(matDialog, 'open').and.returnValue({ beforeClosed: () => of(false) } as any);
spyOn(matDialog, 'open').and.returnValue({beforeClosed: () => of(false)} as any);
spyOn(sharedLinksApiService, 'deleteSharedLink').and.callThrough();
node.entry.properties['qshare:sharedId'] = 'sharedId';
@@ -258,7 +264,7 @@ describe('ShareDialogComponent', () => {
await fixture.whenStable();
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
properties: { 'qshare:expiryDate': null }
properties: {'qshare:expiryDate': null}
});
expect(
@@ -297,10 +303,10 @@ describe('ShareDialogComponent', () => {
fixture.detectChanges();
fixture.nativeElement
.querySelector(
'mat-slide-toggle[data-automation-id="adf-expire-toggle"] label'
)
.dispatchEvent(new MouseEvent('click'));
.querySelector(
'mat-slide-toggle[data-automation-id="adf-expire-toggle"] label'
)
.dispatchEvent(new MouseEvent('click'));
fixture.componentInstance.form.controls['time'].setValue(date);
fixture.detectChanges();
@@ -308,7 +314,7 @@ describe('ShareDialogComponent', () => {
tick(100);
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
properties: { 'qshare:expiryDate': date.utc().format() }
properties: {'qshare:expiryDate': date.utc().format()}
});
}));
@@ -336,7 +342,7 @@ describe('ShareDialogComponent', () => {
tick(500);
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
properties: { 'qshare:expiryDate': date.endOf('day').utc().format() }
properties: {'qshare:expiryDate': date.endOf('day').utc().format()}
});
}));
@@ -354,7 +360,7 @@ describe('ShareDialogComponent', () => {
tick(100);
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
properties: { 'qshare:expiryDate': date.utc().format() }
properties: {'qshare:expiryDate': date.utc().format()}
});
}));
});
@@ -28,17 +28,18 @@ import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { UntypedFormGroup, UntypedFormControl, AbstractControl } from '@angular/forms';
import { Observable, Subject } from 'rxjs';
import {
NodesApiService,
ContentService,
RenditionsService,
AppConfigService
} from '@alfresco/adf-core';
import { NodesApiService } from '../common/services/nodes-api.service';
import { ContentService } from '../common/services/content.service';
import { SharedLinksApiService } from './services/shared-links-api.service';
import { SharedLinkEntry, Node } from '@alfresco/js-api';
import { ConfirmDialogComponent } from '../dialogs/confirm.dialog';
import moment from 'moment';
import { ContentNodeShareSettings } from './content-node-share.settings';
import { takeUntil, debounceTime } from 'rxjs/operators';
import { RenditionService } from '../common/services/rendition.service';
type DatePickerType = 'date' | 'time' | 'month' | 'datetime';
@@ -46,7 +47,7 @@ type DatePickerType = 'date' | 'time' | 'month' | 'datetime';
selector: 'adf-share-dialog',
templateUrl: './content-node-share.dialog.html',
styleUrls: ['./content-node-share.dialog.scss'],
host: { class: 'adf-share-dialog' },
host: {class: 'adf-share-dialog'},
encapsulation: ViewEncapsulation.None
})
export class ShareDialogComponent implements OnInit, OnDestroy {
@@ -59,15 +60,15 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
isDisabled: boolean = false;
form: UntypedFormGroup = new UntypedFormGroup({
sharedUrl: new UntypedFormControl(''),
time: new UntypedFormControl({ value: '', disabled: true })
time: new UntypedFormControl({value: '', disabled: true})
});
type: DatePickerType = 'datetime';
maxDebounceTime = 500;
@ViewChild('slideToggleExpirationDate', { static: true })
@ViewChild('slideToggleExpirationDate', {static: true})
slideToggleExpirationDate;
@ViewChild('dateTimePickerInput', { static: true })
@ViewChild('dateTimePickerInput', {static: true})
dateTimePickerInput;
private onDestroy$ = new Subject<boolean>();
@@ -79,9 +80,10 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
private dialog: MatDialog,
private nodesApiService: NodesApiService,
private contentService: ContentService,
private renditionService: RenditionsService,
private renditionService: RenditionService,
@Inject(MAT_DIALOG_DATA) public data: ContentNodeShareSettings
) {}
) {
}
ngOnInit() {
this.type = this.appConfigService.get<DatePickerType>('sharedLinkDateTimePickerType', 'datetime');
@@ -133,7 +135,7 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
}
get canUpdate() {
const { entry } = this.data.node;
const {entry} = this.data.node;
if (entry && entry.allowableOperations) {
return this.contentService.hasAllowableOperations(entry, 'update');
@@ -199,9 +201,9 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
}
this.isDisabled = false;
this.isFileShared = true;
this.renditionService
.generateRenditionForNode(this.data.node.entry.id)
.subscribe(() => {});
// eslint-disable-next-line
this.renditionService.getNodeRendition(this.data.node.entry.id);
this.updateForm();
}
@@ -219,19 +221,19 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
this.sharedLinksApiService
.deleteSharedLink(sharedId)
.subscribe((response: any) => {
if (response instanceof Error) {
this.isDisabled = false;
this.isFileShared = true;
this.handleError(response);
} else {
if (this.data.node.entry.properties) {
this.data.node.entry.properties['qshare:sharedId'] = null;
this.data.node.entry.properties['qshare:expiryDate'] = null;
if (response instanceof Error) {
this.isDisabled = false;
this.isFileShared = true;
this.handleError(response);
} else {
if (this.data.node.entry.properties) {
this.data.node.entry.properties['qshare:sharedId'] = null;
this.data.node.entry.properties['qshare:expiryDate'] = null;
}
this.dialogRef.close(false);
}
this.dialogRef.close(false);
}
}
);
);
}
private handleError(error: Error) {
@@ -240,7 +242,8 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
try {
statusCode = JSON.parse(error.message).error.statusCode;
} catch {}
} catch {
}
if (statusCode === 403) {
message = 'SHARE.UNSHARE_PERMISSION_ERROR';
@@ -253,7 +256,7 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
}
private updateForm() {
const { entry } = this.data.node;
const {entry} = this.data.node;
let expiryDate = null;
if (entry && entry.properties) {
@@ -285,7 +288,7 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
}
private updateEntryExpiryDate(date: moment.Moment) {
const { properties } = this.data.node.entry;
const {properties} = this.data.node.entry;
if (properties) {
properties['qshare:expiryDate'] = date
@@ -17,9 +17,6 @@
import {
CoreTestingModule,
fakeEcmEditedUser,
fakeEcmUser,
fakeEcmUserNoImage,
IdentityUserModel,
InitialUsernamePipe,
setupTestBed,
@@ -29,6 +26,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatMenuModule } from '@angular/material/menu';
import { By, DomSanitizer } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { fakeEcmEditedUser, fakeEcmUser, fakeEcmUserNoImage } from '../common/mocks/ecm-user.service.mock';
import { ContentTestingModule } from '../testing/content.testing.module';
import { ContentUserInfoComponent } from './content-user-info.component';
@@ -238,7 +236,6 @@ describe('ContentUserInfoComponent', () => {
fixture.detectChanges();
const pipe = new InitialUsernamePipe(new FakeSanitizer());
const expected = pipe.transform({
id: 13,
firstName: 'Wilbur',
lastName: 'Adams',
email: 'wilbur@app.com'
@@ -15,10 +15,12 @@
* limitations under the License.
*/
import { EcmUserModel, IdentityUserModel, PeopleContentService, UserInfoMode } from '@alfresco/adf-core';
import { IdentityUserModel, UserInfoMode } from '@alfresco/adf-core';
import { Component, Input, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core';
import { MatMenuTrigger, MenuPositionX, MenuPositionY } from '@angular/material/menu';
import { Subject } from 'rxjs';
import { EcmUserModel } from '../common/models/ecm-user.model';
import { PeopleContentService } from '../common/services/people-content.service';
@Component({
selector: 'adf-content-user-info',
@@ -28,6 +28,7 @@ import { MatDatetimepickerModule } from '@mat-datetimepicker/core';
import { MatMomentDatetimeModule } from '@mat-datetimepicker/moment';
import { LibraryDialogComponent } from './library/library.dialog';
import { ContentDirectiveModule } from '../directives';
import { DownloadZipDialogModule } from './download-zip/download-zip.dialog.module';
@NgModule({
imports: [
@@ -38,7 +39,8 @@ import { ContentDirectiveModule } from '../directives';
ReactiveFormsModule,
MatMomentDatetimeModule,
MatDatetimepickerModule,
ContentDirectiveModule
ContentDirectiveModule,
DownloadZipDialogModule
],
declarations: [
FolderDialogComponent,
@@ -0,0 +1,9 @@
<h1 matDialogTitle>{{ 'CORE.DIALOG.DOWNLOAD_ZIP.TITLE' | translate }}</h1>
<div mat-dialog-content>
<mat-progress-bar color="primary" mode="indeterminate"></mat-progress-bar>
</div>
<mat-dialog-actions align="end">
<button mat-button color="primary" id="cancel-button" (click)="cancelDownload()">
{{ 'CORE.DIALOG.DOWNLOAD_ZIP.ACTIONS.CANCEL' | translate }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,39 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NgModule } from '@angular/core';
import { DownloadZipDialogComponent } from './download-zip.dialog';
import { PipeModule } from '@alfresco/adf-core';
import { MatDialogModule } from '@angular/material/dialog';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatButtonModule } from '@angular/material/button';
import { TranslateModule } from '@ngx-translate/core';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [DownloadZipDialogComponent],
imports: [
CommonModule,
PipeModule,
MatDialogModule,
MatProgressBarModule,
MatButtonModule,
TranslateModule
],
exports: [DownloadZipDialogComponent]
})
export class DownloadZipDialogModule {}
@@ -0,0 +1,3 @@
.adf-download-zip-dialog .mat-dialog-actions .mat-button-wrapper {
text-transform: uppercase;
}
@@ -0,0 +1,159 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { DownloadZipDialogComponent } from './download-zip.dialog';
import { CoreTestingModule, setupTestBed } from '@alfresco/adf-core';
import { DownloadZipService } from './services/download-zip.service';
import { Observable } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
describe('DownloadZipDialogComponent', () => {
let fixture: ComponentFixture<DownloadZipDialogComponent>;
let component: DownloadZipDialogComponent;
let element: HTMLElement;
let downloadZipService: DownloadZipService;
const dialogRef = {
close: jasmine.createSpy('close')
};
const dataMock = {
nodeIds: [
'123'
]
};
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
],
providers: [
{ provide: MatDialogRef, useValue: dialogRef },
{ provide: MAT_DIALOG_DATA, useValue: dataMock }
]
});
beforeEach(() => {
dialogRef.close.calls.reset();
fixture = TestBed.createComponent(DownloadZipDialogComponent);
downloadZipService = TestBed.inject(DownloadZipService);
component = fixture.componentInstance;
element = fixture.nativeElement;
});
afterEach(() => {
fixture.destroy();
});
it('should call downloadZip when it is not cancelled', () => {
component.cancelled = false;
spyOn(component, 'downloadZip');
component.ngOnInit();
expect(component.downloadZip).toHaveBeenCalledWith(['123']);
});
it('should not call downloadZip when it is cancelled', () => {
component.cancelled = true;
spyOn(component, 'downloadZip');
component.ngOnInit();
expect(component.downloadZip).not.toHaveBeenCalled();
});
it('should not call downloadZip when it contains zero nodeIds', () => {
component.data = {
nodeIds: []
};
spyOn(component, 'downloadZip');
component.ngOnInit();
expect(component.downloadZip).not.toHaveBeenCalled();
});
it('should call cancelDownload when CANCEL button is clicked', () => {
spyOn(downloadZipService, 'createDownload').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
fixture.detectChanges();
spyOn(component, 'cancelDownload');
const cancelButton = element.querySelector<HTMLButtonElement>('#cancel-button');
cancelButton.click();
expect(component.cancelDownload).toHaveBeenCalled();
});
it('should call createDownload when component is initialize', () => {
const createDownloadSpy = spyOn(downloadZipService, 'createDownload').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
fixture.detectChanges();
expect(createDownloadSpy).toHaveBeenCalled();
});
it('should close dialog when download is completed', () => {
spyOn(downloadZipService, 'createDownload').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
component.download('fakeUrl', 'fileName');
spyOn(component, 'cancelDownload');
fixture.detectChanges();
expect(dialogRef.close).toHaveBeenCalled();
});
it('should close dialog when download is cancelled', () => {
spyOn(downloadZipService, 'createDownload').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
fixture.detectChanges();
component.download('url', 'filename');
spyOn(downloadZipService, 'cancelDownload');
component.cancelDownload();
expect(dialogRef.close).toHaveBeenCalled();
});
it('should interrupt download when cancel button is clicked', () => {
spyOn(component, 'downloadZip');
spyOn(component, 'download');
spyOn(component, 'cancelDownload');
fixture.detectChanges();
expect(component.downloadZip).toHaveBeenCalled();
const cancelButton = element.querySelector<HTMLButtonElement>('#cancel-button');
cancelButton.click();
expect(component.cancelDownload).toHaveBeenCalled();
expect(component.download).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,57 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { DownloadZipDialogComponent } from './download-zip.dialog';
import { zipNode, downloadEntry } from './mock/download-zip-data.mock';
@Component({
selector: 'adf-download-zip-dialog-storybook',
template: `<button mat-raised-button (click)="openDialog()">Open dialog</button>`
})
export class DownloadZipDialogStorybookComponent implements OnInit, OnChanges {
@Input()
showLoading: boolean;
constructor(private dialog: MatDialog) {}
ngOnInit(): void {
this.setEntryStatus(this.showLoading);
}
ngOnChanges(): void {
this.setEntryStatus(this.showLoading);
}
setEntryStatus(isLoading: boolean){
if (!isLoading) {
downloadEntry.entry.status = 'DONE';
} else {
downloadEntry.entry.status = 'PACKING';
}
}
openDialog() {
this.dialog.open(DownloadZipDialogComponent, {
minWidth: '50%',
data: {
nodeIds: [zipNode.entry.id]
}
});
}
}
@@ -0,0 +1,83 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Meta, moduleMetadata, Story } from '@storybook/angular';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { MatButtonModule } from '@angular/material/button';
import { DownloadZipDialogStorybookComponent } from './download-zip.dialog.stories.component';
import {
AlfrescoApiServiceMock,
ContentApiMock,
DownloadZipMockService,
NodesApiMock
} from './mock/download-zip-service.mock';
import { DownloadZipDialogModule } from './download-zip.dialog.module';
import { DownloadZipService } from './services/download-zip.service';
import { ContentService } from '../../common/services/content.service';
import { NodesApiService } from '../../common/services/nodes-api.service';
export default {
component: DownloadZipDialogStorybookComponent,
title: 'Core/Dialog/Download ZIP Dialog',
decorators: [
moduleMetadata({
imports: [
DownloadZipDialogModule,
MatButtonModule
],
providers: [
{
provide: AlfrescoApiService,
useClass: AlfrescoApiServiceMock
},
{
provide: DownloadZipService,
useClass: DownloadZipMockService
},
{
provide: ContentService,
useClass: ContentApiMock
},
{
provide: NodesApiService,
useClass: NodesApiMock
}
]
})
],
argTypes: {
showLoading: {
control: {
type: 'boolean'
},
table: {
category: 'Story controls',
type: {
summary: 'boolean'
}
},
defaultValue: false
}
}
} as Meta;
export const downloadZIPDialog: Story<DownloadZipDialogStorybookComponent> = (
args: DownloadZipDialogStorybookComponent
) => ({
props: args
});
downloadZIPDialog.parameters = { layout: 'centered' };
@@ -0,0 +1,114 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DownloadEntry, MinimalNode } from '@alfresco/js-api';
import { LogService } from '@alfresco/adf-core';
import { NodesApiService } from '../../common/services/nodes-api.service';
import { DownloadZipService } from './services/download-zip.service';
import { ContentService } from '../../common/services/content.service';
@Component({
selector: 'adf-download-zip-dialog',
templateUrl: './download-zip.dialog.html',
styleUrls: ['./download-zip.dialog.scss'],
host: { class: 'adf-download-zip-dialog' },
encapsulation: ViewEncapsulation.None
})
export class DownloadZipDialogComponent implements OnInit {
// flag for async threads
cancelled = false;
downloadId: string;
constructor(private dialogRef: MatDialogRef<DownloadZipDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: any,
private logService: LogService,
private downloadZipService: DownloadZipService,
private nodeService: NodesApiService,
private contentService: ContentService) {
}
ngOnInit() {
if (this.data && this.data.nodeIds && this.data.nodeIds.length > 0) {
if (!this.cancelled) {
this.downloadZip(this.data.nodeIds);
} else {
this.logService.log('Cancelled');
}
}
}
cancelDownload() {
this.cancelled = true;
this.downloadZipService.cancelDownload(this.downloadId);
this.dialogRef.close(false);
}
downloadZip(nodeIds: string[]) {
if (nodeIds && nodeIds.length > 0) {
this.downloadZipService.createDownload({ nodeIds }).subscribe((data: DownloadEntry) => {
if (data && data.entry && data.entry.id) {
const url = this.contentService.getContentUrl(data.entry.id, true);
this.nodeService.getNode(data.entry.id).subscribe((downloadNode: MinimalNode) => {
this.logService.log(downloadNode);
const fileName = downloadNode.name;
this.downloadId = data.entry.id;
this.waitAndDownload(data.entry.id, url, fileName);
});
}
});
}
}
waitAndDownload(downloadId: string, url: string, fileName: string) {
if (this.cancelled) {
return;
}
this.downloadZipService.getDownload(downloadId).subscribe((downloadEntry: DownloadEntry) => {
if (downloadEntry.entry) {
if (downloadEntry.entry.status === 'DONE') {
this.download(url, fileName);
} else {
setTimeout(() => {
this.waitAndDownload(downloadId, url, fileName);
}, 1000);
}
}
});
}
download(url: string, fileName: string) {
if (url && fileName) {
const link = document.createElement('a');
link.style.display = 'none';
link.download = fileName;
link.href = url;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
this.dialogRef.close(true);
}
}
@@ -0,0 +1,33 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DownloadEntry } from '@alfresco/js-api';
export const zipNode = {
entry: {
name: 'files.zip',
contentUrl: './assets/files.zip',
id: 'files_in_zip'
}
};
export const downloadEntry: DownloadEntry = {
entry: {
id: 'entryId',
status: 'DONE'
}
};
@@ -0,0 +1,101 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DownloadBodyCreate, DownloadEntry } from '@alfresco/js-api';
import { from, Observable, of, ReplaySubject, Subject } from 'rxjs';
import { catchError } from 'rxjs/internal/operators/catchError';
import { zipNode, downloadEntry } from './download-zip-data.mock';
export class AlfrescoApiServiceMock {
nodeUpdated = new Subject<Node>();
alfrescoApiInitialized: ReplaySubject<boolean> = new ReplaySubject(1);
alfrescoApi = new AlfrescoApiCompatibilityMock();
load() {}
getInstance = () => this.alfrescoApi;
}
class AlfrescoApiCompatibilityMock {
core = new CoreMock();
content = new ContentApiMock();
isOauthConfiguration = () => true;
isLoggedIn = () => true;
isEcmConfiguration = () => true;
isEcmLoggedIn = () => true;
}
export class ContentApiMock {
getContentUrl = (_: string, _1?: boolean, _2?: string): string =>
zipNode.entry.contentUrl;
}
class CoreMock {
downloadsApi = new DownloadsApiMock();
nodesApi = new NodesApiMock();
}
export class NodesApiMock {
getNode = (_: string, _2?: any): any => of(zipNode.entry);
}
class DownloadsApiMock {
createDownload = (
_: DownloadBodyCreate,
_2?: any
): Promise<DownloadEntry> => Promise.resolve(downloadEntry);
getDownload = (_: string, _2?: any): Promise<DownloadEntry> =>
Promise.resolve(downloadEntry);
cancelDownload(_: string) {}
}
export class DownloadZipMockService {
private _downloadsApi: DownloadsApiMock;
get downloadsApi(): DownloadsApiMock {
this._downloadsApi = this._downloadsApi ?? new DownloadsApiMock();
return this._downloadsApi;
}
createDownload(payload: DownloadBodyCreate): Observable<DownloadEntry> {
return from(this.downloadsApi.createDownload(payload)).pipe(
catchError((err) => of(err))
);
}
getDownload(downloadId: string): Observable<DownloadEntry> {
return from(this.downloadsApi.getDownload(downloadId));
}
cancelDownload(downloadId: string) {
this.downloadsApi.cancelDownload(downloadId);
}
download(url: string, fileName: string) {
if (url && fileName) {
const link = document.createElement('a');
link.style.display = 'none';
link.download = fileName;
link.href = url;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
}
@@ -0,0 +1,74 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DownloadEntry, DownloadBodyCreate, DownloadsApi } from '@alfresco/js-api';
import { Injectable } from '@angular/core';
import { Observable, from, throwError } from 'rxjs';
import { AlfrescoApiService, LogService } from '@alfresco/adf-core';
import { catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DownloadZipService {
private _downloadsApi: DownloadsApi;
get downloadsApi(): DownloadsApi {
this._downloadsApi = this._downloadsApi ?? new DownloadsApi(this.apiService.getInstance());
return this._downloadsApi;
}
constructor(private apiService: AlfrescoApiService,
private logService: LogService) {
}
/**
* Creates a new download.
*
* @param payload Object containing the node IDs of the items to add to the ZIP file
* @returns Status object for the download
*/
createDownload(payload: DownloadBodyCreate): Observable<DownloadEntry> {
return from(this.downloadsApi.createDownload(payload)).pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Gets status information for a download node.
*
* @param downloadId ID of the download node
* @returns Status object for the download
*/
getDownload(downloadId: string): Observable<DownloadEntry> {
return from(this.downloadsApi.getDownload(downloadId));
}
/**
* Cancels a download.
*
* @param downloadId ID of the target download node
*/
cancelDownload(downloadId: string) {
this.downloadsApi.cancelDownload(downloadId);
}
private handleError(error: any) {
this.logService.error(error);
return throwError(error || 'Server error');
}
}
@@ -17,7 +17,9 @@
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { MatDialogRef } from '@angular/material/dialog';
import { NodesApiService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { NodesApiService } from '../common/services/nodes-api.service';
import { FolderDialogComponent } from './folder.dialog';
import { of, throwError } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
@@ -22,7 +22,8 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Node } from '@alfresco/js-api';
import { NodesApiService, TranslationService } from '@alfresco/adf-core';
import { TranslationService } from '@alfresco/adf-core';
import { NodesApiService } from '../common/services/nodes-api.service';
import { forbidEndingDot, forbidOnlySpaces, forbidSpecialCharacters } from './folder-name.validators';
@@ -22,4 +22,8 @@ export * from './confirm.dialog';
export * from './dialog.module';
export * from './library/library.dialog';
export * from './download-zip/download-zip.dialog';
export * from './download-zip/download-zip.dialog.module';
export * from './folder-name.validators';
@@ -16,7 +16,8 @@
*/
import { ChangeDetectorRef, Component, ElementRef, SimpleChange } from '@angular/core';
import { ContentService, CoreTestingModule, setupTestBed } from '@alfresco/adf-core';
import { CoreTestingModule, setupTestBed } from '@alfresco/adf-core';
import { ContentService } from '../common/services/content.service';
import { CheckAllowableOperationDirective } from './check-allowable-operation.directive';
import { TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
@@ -19,7 +19,8 @@
import { ChangeDetectorRef, Directive, ElementRef, Host, Inject, Input, OnChanges, Optional, Renderer2, SimpleChanges } from '@angular/core';
import { NodeEntry } from '@alfresco/js-api';
import { ContentService, EXTENDIBLE_COMPONENT } from '@alfresco/adf-core';
import { EXTENDIBLE_COMPONENT } from '@alfresco/adf-core';
import { ContentService } from '../common/services/content.service';
import { NodeAllowableOperationSubject } from '../interfaces/node-allowable-operation-subject.interface';
@Directive({
@@ -29,6 +29,7 @@ import { LibraryMembershipDirective } from './library-membership.directive';
import { NodeDeleteDirective } from './node-delete.directive';
import { NodeFavoriteDirective } from './node-favorite.directive';
import { NodeRestoreDirective } from './node-restore.directive';
import { NodeDownloadDirective } from './node-download.directive';
@NgModule({
imports: [
@@ -46,7 +47,8 @@ import { NodeRestoreDirective } from './node-restore.directive';
LibraryMembershipDirective,
NodeDeleteDirective,
NodeFavoriteDirective,
NodeRestoreDirective
NodeRestoreDirective,
NodeDownloadDirective
],
exports: [
NodeLockDirective,
@@ -57,7 +59,8 @@ import { NodeRestoreDirective } from './node-restore.directive';
LibraryMembershipDirective,
NodeDeleteDirective,
NodeFavoriteDirective,
NodeRestoreDirective
NodeRestoreDirective,
NodeDownloadDirective
]
})
export class ContentDirectiveModule {
@@ -0,0 +1,190 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { MatDialog } from '@angular/material/dialog';
import { Component, DebugElement, ViewChild } from '@angular/core';
import { setupTestBed, AlfrescoApiService, CoreTestingModule } from '@alfresco/adf-core';
import { NodeDownloadDirective } from './node-download.directive';
import { TranslateModule } from '@ngx-translate/core';
import { ContentDirectiveModule } from '@alfresco/adf-content-services';
@Component({
template: '<div [adfNodeDownload]="selection" [version]="version"></div>'
})
class TestComponent {
@ViewChild(NodeDownloadDirective, { static: true })
downloadDirective: NodeDownloadDirective;
selection;
version;
}
describe('NodeDownloadDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let element: DebugElement;
let dialog: MatDialog;
let apiService: AlfrescoApiService;
let contentService;
let dialogSpy;
const mockOauth2Auth: any = {
oauth2Auth: {
callCustomApi: () => Promise.resolve()
},
isEcmLoggedIn: jasmine.createSpy('isEcmLoggedIn'),
reply: jasmine.createSpy('reply')
};
setupTestBed({
imports: [
ContentDirectiveModule,
TranslateModule.forRoot(),
CoreTestingModule
],
declarations: [
TestComponent
]
});
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
element = fixture.debugElement.query(By.directive(NodeDownloadDirective));
dialog = TestBed.inject(MatDialog);
apiService = TestBed.inject(AlfrescoApiService);
contentService = component.downloadDirective['contentApi'];
dialogSpy = spyOn(dialog, 'open');
});
it('should not download node when selection is empty', () => {
spyOn(apiService, 'getInstance').and.returnValue(mockOauth2Auth);
component.selection = [];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(apiService.getInstance).not.toHaveBeenCalled();
});
it('should not download zip when selection has no nodes', () => {
component.selection = [];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(dialogSpy).not.toHaveBeenCalled();
});
it('should download selected node as file', () => {
spyOn(contentService, 'getContentUrl');
const node = { entry: { id: 'node-id', isFile: true } };
component.selection = [node];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(contentService.getContentUrl).toHaveBeenCalledWith(node.entry.id, true);
});
it('should download selected node version as file', () => {
component.version = {
entry: {
id: '1.0'
}
};
spyOn(contentService, 'getVersionContentUrl');
const node = {entry: {id: 'node-id', isFile: true}};
component.selection = [node];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(contentService.getVersionContentUrl).toHaveBeenCalledWith(node.entry.id, '1.0', true);
});
it('should download selected shared node as file', () => {
spyOn(contentService, 'getContentUrl');
const node = { entry: { nodeId: 'shared-node-id', isFile: true } };
component.selection = [node];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(contentService.getContentUrl).toHaveBeenCalledWith(node.entry.nodeId, true);
});
it('should download selected files nodes as zip', () => {
const node1 = { entry: { id: 'node-1' } };
const node2 = { entry: { id: 'node-2' } };
component.selection = [node1, node2];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(dialogSpy.calls.argsFor(0)[1].data).toEqual({ nodeIds: [ 'node-1', 'node-2' ] });
});
it('should download selected shared files nodes as zip', () => {
const node1 = { entry: { nodeId: 'shared-node-1' } };
const node2 = { entry: { nodeId: 'shared-node-2' } };
component.selection = [node1, node2];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(dialogSpy.calls.argsFor(0)[1].data).toEqual({ nodeIds: [ 'shared-node-1', 'shared-node-2' ] });
});
it('should download selected folder node as zip', () => {
const node = { entry: { isFolder: true, id: 'node-id' } };
component.selection = [node];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(dialogSpy.calls.argsFor(0)[1].data).toEqual({ nodeIds: [ 'node-id' ] });
});
it('should create link element to download file node', () => {
const dummyLinkElement: any = {
download: null,
href: null,
click: () => null,
style: {
display: null
}
};
const node = { entry: { name: 'dummy', isFile: true, id: 'node-id' } };
spyOn(contentService, 'getContentUrl').and.returnValue('somewhere-over-the-rainbow');
spyOn(document, 'createElement').and.returnValue(dummyLinkElement);
spyOn(document.body, 'appendChild').and.stub();
spyOn(document.body, 'removeChild').and.stub();
component.selection = [node];
fixture.detectChanges();
element.triggerEventHandler('click', null);
expect(document.createElement).toHaveBeenCalled();
expect(dummyLinkElement.download).toBe('dummy');
expect(dummyLinkElement.href).toContain('somewhere-over-the-rainbow');
});
});
@@ -0,0 +1,142 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Directive, Input, HostListener } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { AlfrescoApiService, DownloadService } from '@alfresco/adf-core';
import { DownloadZipDialogComponent } from '../dialogs/download-zip/download-zip.dialog';
import { ContentApi, NodeEntry, VersionEntry } from '@alfresco/js-api';
/**
* Directive selectors without adf- prefix will be deprecated on 3.0.0
*/
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: '[adfNodeDownload]'
})
export class NodeDownloadDirective {
_contentApi: ContentApi;
get contentApi(): ContentApi {
this._contentApi = this._contentApi ?? new ContentApi(this.apiService.getInstance());
return this._contentApi;
}
/** Nodes to download. */
@Input('adfNodeDownload')
nodes: NodeEntry | NodeEntry[];
/** Node's version to download. */
@Input()
version: VersionEntry;
@HostListener('click')
onClick() {
this.downloadNodes(this.nodes);
}
constructor(
private apiService: AlfrescoApiService,
private downloadService: DownloadService,
private dialog: MatDialog) {
}
/**
* Downloads multiple selected nodes.
* Packs result into a .ZIP archive if there is more than one node selected.
*
* @param selection Multiple selected nodes to download
*/
downloadNodes(selection: NodeEntry | Array<NodeEntry>) {
if (!this.isSelectionValid(selection)) {
return;
}
if (selection instanceof Array) {
if (selection.length === 1) {
this.downloadNode(selection[0]);
} else {
this.downloadZip(selection);
}
} else {
this.downloadNode(selection);
}
}
/**
* Downloads a single node.
* Packs result into a .ZIP archive is the node is a Folder.
*
* @param node Node to download
*/
downloadNode(node: NodeEntry) {
if (node && node.entry) {
const entry = node.entry;
if (entry.isFile) {
this.downloadFile(node);
}
if (entry.isFolder) {
this.downloadZip([node]);
}
// Check if there's nodeId for Shared Files
if (!entry.isFile && !entry.isFolder && (entry as any).nodeId) {
this.downloadFile(node);
}
}
}
private isSelectionValid(selection: NodeEntry | Array<NodeEntry>) {
return selection || (selection instanceof Array && selection.length > 0);
}
private downloadFile(node: NodeEntry) {
if (node && node.entry) {
// nodeId for Shared node
const id = (node.entry as any).nodeId || node.entry.id;
let url;
let fileName;
if (this.version) {
url = this.contentApi.getVersionContentUrl(id, this.version.entry.id, true);
fileName = this.version.entry.name;
} else {
url = this.contentApi.getContentUrl(id, true);
fileName = node.entry.name;
}
this.downloadService.downloadUrl(url, fileName);
}
}
private downloadZip(selection: Array<NodeEntry>) {
if (selection && selection.length > 0) {
// nodeId for Shared node
const nodeIds = selection.map((node: any) => (node.entry.nodeId || node.entry.id));
this.dialog.open(DownloadZipDialogComponent, {
width: '600px',
disableClose: true,
data: {
nodeIds
}
});
}
}
}
@@ -19,7 +19,8 @@
import { Directive, ElementRef, Renderer2, HostListener, Input, AfterViewInit } from '@angular/core';
import { Node } from '@alfresco/js-api';
import { AllowableOperationsEnum, ContentService } from '@alfresco/adf-core';
import { ContentService } from '../common/services/content.service';
import { AllowableOperationsEnum } from '../common/models/allowable-operations.enum';
import { ContentNodeDialogService } from '../content-node-selector/content-node-dialog.service';
@Directive({
@@ -25,3 +25,4 @@ export * from './library-membership.directive';
export * from './node-delete.directive';
export * from './node-favorite.directive';
export * from './node-restore.directive';
export * from './node-download.directive';
@@ -17,7 +17,7 @@
import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange, EventEmitter } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ContentService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { FileNode } from '../../../mock';
import { ContentActionModel } from './../../models/content-action.model';
import { DocumentActionsService } from './../../services/document-actions.service';
@@ -28,6 +28,7 @@ import { ContentActionListComponent } from './content-action-list.component';
import { ContentActionComponent } from './content-action.component';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { ContentService } from '../../../common/services/content.service';
describe('ContentAction', () => {
let documentList: DocumentListComponent;
@@ -27,9 +27,10 @@ import {
DataTableModule,
ObjectDataTableAdapter,
ShowHeaderMode,
ThumbnailService,
ContentService
ThumbnailService
} from '@alfresco/adf-core';
import { ContentService } from '../../common/services/content.service';
import { Subject, of, throwError } from 'rxjs';
import {
FileNode,
@@ -22,9 +22,9 @@ import {
AfterContentInit, Component, ContentChild, ElementRef, EventEmitter, HostListener, Input,
OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewChild, ViewEncapsulation
} from '@angular/core';
import { ContentService } from '../../common/services/content.service';
import {
ContentService,
DataCellEvent,
DataColumn,
DataRowActionEvent,
@@ -46,9 +46,9 @@ import {
AlfrescoApiService,
UserPreferenceValues,
DataRow,
DataTableService,
NodesApiService
DataTableService
} from '@alfresco/adf-core';
import { NodesApiService } from '../../common/services/nodes-api.service';
import { Node, NodeEntry, NodePaging, NodesApi, Pagination } from '@alfresco/js-api';
import { Subject, BehaviorSubject, of } from 'rxjs';
@@ -26,7 +26,8 @@ import {
} from '@angular/core';
import { NodeEntry, Site } from '@alfresco/js-api';
import { ShareDataRow } from '../../data/share-data-row.model';
import { NodesApiService } from '@alfresco/adf-core';
import { NodesApiService } from '../../../common/services/nodes-api.service';
import { BehaviorSubject, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@@ -24,10 +24,10 @@ import {
OnDestroy
} from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { NodesApiService } from '@alfresco/adf-core';
import { SiteEntry, Site } from '@alfresco/js-api';
import { ShareDataRow } from '../../data/share-data-row.model';
import { takeUntil } from 'rxjs/operators';
import { NodesApiService } from '../../../common/services/nodes-api.service';
@Component({
selector: 'adf-library-role-column',
@@ -16,7 +16,7 @@
*/
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { NodesApiService } from '@alfresco/adf-core';
import { NodesApiService } from '../../../common/services/nodes-api.service';
import { BehaviorSubject, Subject } from 'rxjs';
import { Site, SiteEntry } from '@alfresco/js-api';
import { ShareDataRow } from '../../data/share-data-row.model';
@@ -26,7 +26,7 @@ import {
} from '@angular/core';
import { NodeEntry } from '@alfresco/js-api';
import { BehaviorSubject, Subject } from 'rxjs';
import { NodesApiService } from '@alfresco/adf-core';
import { NodesApiService } from '../../../common/services/nodes-api.service';
import { ShareDataRow } from '../../data/share-data-row.model';
import { takeUntil } from 'rxjs/operators';
@@ -15,9 +15,10 @@
* limitations under the License.
*/
import { DataRow, ObjectUtils, ThumbnailService, ContentService } from '@alfresco/adf-core';
import { DataRow, ObjectUtils, ThumbnailService } from '@alfresco/adf-core';
import { MinimalNode, NodeEntry } from '@alfresco/js-api';
import { PermissionStyleModel } from './../models/permissions-style.model';
import { ContentService } from './../../common/services/content.service';
export const ERR_OBJECT_NOT_FOUND: string = 'Object source not found';
@@ -15,13 +15,14 @@
* limitations under the License.
*/
import { DataColumn, DataRow, DataSorting, ContentService, ThumbnailService, setupTestBed } from '@alfresco/adf-core';
import { DataColumn, DataRow, DataSorting, ThumbnailService, setupTestBed } from '@alfresco/adf-core';
import { FileNode, FolderNode, SmartFolderNode, RuleFolderNode, LinkFolderNode } from './../../mock';
import { ERR_OBJECT_NOT_FOUND, ShareDataRow } from './share-data-row.model';
import { ERR_COL_NOT_FOUND, ERR_ROW_NOT_FOUND, ShareDataTableAdapter } from './share-datatable-adapter';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { ContentService } from '../../common/services/content.service';
describe('ShareDataTableAdapter', () => {
@@ -41,7 +42,7 @@ describe('ShareDataTableAdapter', () => {
contentService = TestBed.inject(ContentService);
thumbnailService = TestBed.inject(ThumbnailService);
spyOn(thumbnailService, 'getDocumentThumbnailUrl').and.returnValue(imageUrl);
spyOn(contentService, 'getDocumentThumbnailUrl').and.returnValue(imageUrl);
});
it('should use client sorting by default', () => {
@@ -267,7 +268,6 @@ describe('ShareDataTableAdapter', () => {
const value = adapter.getValue(row, col);
expect(value).toBe(imageUrl);
expect(thumbnailService.getDocumentThumbnailUrl).toHaveBeenCalledWith(file);
});
it('should resolve fallback file icon for unknown node', () => {
@@ -20,13 +20,13 @@ import {
DataRow,
DataSorting,
DataTableAdapter,
ThumbnailService,
ContentService
ThumbnailService
} from '@alfresco/adf-core';
import { NodePaging, NodeEntry } from '@alfresco/js-api';
import { PermissionStyleModel } from './../models/permissions-style.model';
import { ShareDataRow } from './share-data-row.model';
import { RowFilter } from './row-filter.model';
import { ContentService } from '../../common/services/content.service';
export const ERR_ROW_NOT_FOUND: string = 'Row not found';
export const ERR_COL_NOT_FOUND: string = 'Column not found';
@@ -122,7 +122,7 @@ export class ShareDataTableAdapter implements DataTableAdapter {
if (node.entry.isFile) {
if (this.thumbnails) {
return this.thumbnailService.getDocumentThumbnailUrl(node);
return this.getDocumentThumbnailUrl(node);
}
}
@@ -149,6 +149,33 @@ export class ShareDataTableAdapter implements DataTableAdapter {
return dataRow.cacheValue(col.key, value);
}
/**
* Gets a thumbnail URL for the given document node.
*
* @param node Node or Node ID to get URL for.
* @param attachment Toggles whether to retrieve content as an attachment for download
* @param ticket Custom ticket to use for authentication
* @returns URL string
*/
private getDocumentThumbnailUrl(node: NodeEntry, attachment?: boolean, ticket?: string): string {
let resultUrl: string;
if (node) {
let nodeId: string;
if (typeof node === 'string') {
nodeId = node;
} else if (node.entry) {
nodeId = node.entry.id;
}
resultUrl = this.contentService.getDocumentThumbnailUrl(nodeId, attachment, ticket);
}
return resultUrl || this.thumbnailService.getMimeTypeIcon(node.entry.content.mimeType);
}
getSorting(): DataSorting {
return this.sorting;
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { AllowableOperationsEnum } from '@alfresco/adf-core';
import { AllowableOperationsEnum } from '../../common/models/allowable-operations.enum';
export class PermissionStyleModel {
css: string;
@@ -15,7 +15,8 @@
* limitations under the License.
*/
import { ContentService, TranslationService } from '@alfresco/adf-core';
import { TranslationService } from '@alfresco/adf-core';
import { ContentService } from '../../common/services/content.service';
import { Injectable } from '@angular/core';
import { NodeEntry } from '@alfresco/js-api';
import { Observable, Subject, throwError, of } from 'rxjs';
@@ -15,12 +15,11 @@
* limitations under the License.
*/
import {
AlfrescoApiService, ContentService, LogService, PaginationModel
} from '@alfresco/adf-core';
import { AlfrescoApiService, LogService, PaginationModel } from '@alfresco/adf-core';
import { NodesApiService } from '../../common/services/nodes-api.service';
import { Injectable } from '@angular/core';
import { NodeEntry, NodePaging, NodesApi } from '@alfresco/js-api';
import { MinimalNode, NodeEntry, NodePaging, NodesApi } from '@alfresco/js-api';
import { DocumentLoaderNode } from '../models/document-folder.model';
import { Observable, from, throwError, forkJoin } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@@ -40,7 +39,7 @@ export class DocumentListService implements DocumentListLoader {
return this._nodesApi;
}
constructor(private contentService: ContentService,
constructor(private nodesApiService: NodesApiService,
private apiService: AlfrescoApiService,
private logService: LogService,
private customResourcesService: CustomResourcesService) {
@@ -135,7 +134,7 @@ export class DocumentListService implements DocumentListLoader {
* @param includeFields Extra information to include (available options are "aspectNames", "isLink" and "association")
* @returns Details of the folder
*/
getNode(nodeId: string, includeFields: string[] = []): Observable<NodeEntry> {
getNode(nodeId: string, includeFields: string[] = []): Observable<MinimalNode> {
const includeFieldsRequest = ['path', 'properties', 'allowableOperations', 'permissions', 'definition', ...includeFields]
.filter((element, index, array) => index === array.indexOf(element));
@@ -144,7 +143,7 @@ export class DocumentListService implements DocumentListLoader {
include: includeFieldsRequest
};
return this.contentService.getNode(nodeId, opts);
return this.nodesApiService.getNode(nodeId, opts);
}
/**
@@ -15,7 +15,8 @@
* limitations under the License.
*/
import { ContentService, TranslationService } from '@alfresco/adf-core';
import { TranslationService } from '@alfresco/adf-core';
import { ContentService } from '../../common/services/content.service';
import { Injectable } from '@angular/core';
import { NodeEntry } from '@alfresco/js-api';
import { Observable, Subject, throwError, of } from 'rxjs';
@@ -18,8 +18,10 @@
import { Injectable, Output, EventEmitter } from '@angular/core';
import { Node, NodeEntry } from '@alfresco/js-api';
import { Subject } from 'rxjs';
import { AlfrescoApiService, ContentService, NodeDownloadDirective, DownloadService } from '@alfresco/adf-core';
import { AlfrescoApiService, DownloadService } from '@alfresco/adf-core';
import { MatDialog } from '@angular/material/dialog';
import { ContentService } from '../../common/services/content.service';
import { NodeDownloadDirective } from '../../directives/node-download.directive';
import { DocumentListService } from './document-list.service';
import { ContentNodeDialogService } from '../../content-node-selector/content-node-dialog.service';
@@ -21,11 +21,12 @@ import { MatDialog } from '@angular/material/dialog';
import { By } from '@angular/platform-browser';
import { Subject, of } from 'rxjs';
import { ContentService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { FolderCreateDirective } from './folder-create.directive';
import { Node } from '@alfresco/js-api';
import { ContentTestingModule } from '../testing/content.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { ContentService } from '../common/services/content.service';
@Component({
template: `
@@ -21,7 +21,7 @@ import { Directive, HostListener, Input, Output, EventEmitter } from '@angular/c
import { MatDialog } from '@angular/material/dialog';
import { Node } from '@alfresco/js-api';
import { FolderDialogComponent } from '../dialogs/folder.dialog';
import { ContentService } from '@alfresco/adf-core';
import { ContentService } from '../common/services/content.service';
const DEFAULT_FOLDER_PARENT_ID = '-my-';
const DIALOG_WIDTH: number = 400;
@@ -21,11 +21,12 @@ import { MatDialog } from '@angular/material/dialog';
import { By } from '@angular/platform-browser';
import { Subject, of } from 'rxjs';
import { ContentService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { FolderEditDirective } from './folder-edit.directive';
import { Node } from '@alfresco/js-api';
import { ContentTestingModule } from '../testing/content.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { ContentService } from '../common/services/content.service';
@Component({
template: '<div [adf-edit-folder]="folder" (success)="success($event)" title="edit-title"></div>'
@@ -21,7 +21,7 @@ import { Directive, ElementRef, HostListener, Input, Output, EventEmitter } from
import { MatDialog } from '@angular/material/dialog';
import { Node } from '@alfresco/js-api';
import { FolderDialogComponent } from '../dialogs/folder.dialog';
import { ContentService } from '@alfresco/adf-core';
import { ContentService } from '../common/services/content.service';
const DIALOG_WIDTH: number = 400;
@@ -15,12 +15,12 @@
* limitations under the License.
*/
import { ContentService } from '@alfresco/adf-core';
import { Component, EventEmitter, Output } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
import { BehaviorSubject, of, Subject } from 'rxjs';
import { ContentService } from '../common/services/content.service';
import { mockFile, mockNewVersionUploaderData, mockNode } from '../mock';
import { ContentTestingModule } from '../testing/content.testing.module';
import {
@@ -17,7 +17,8 @@
import { Injectable } from '@angular/core';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { AlfrescoApiService, ContentService } from '@alfresco/adf-core';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { ContentService } from '../common/services/content.service';
import { NewVersionUploaderDialogComponent } from './new-version-uploader.dialog';
import { VersionPaging, VersionsApi } from '@alfresco/js-api';
@@ -15,7 +15,9 @@
* limitations under the License.
*/
import { CommentModel, EcmCompanyModel, EcmUserModel } from '@alfresco/adf-core';
import { CommentModel } from '@alfresco/adf-core';
import { EcmCompanyModel } from '../../common/models/ecm-company.model';
import { EcmUserModel } from '../../common/models/ecm-user.model';
export const fakeUser1 = {
enabled: true,
@@ -15,11 +15,17 @@
* limitations under the License.
*/
import { AlfrescoApiService, LogService, CommentModel, CommentsService, PeopleContentService } from '@alfresco/adf-core';
import {
AlfrescoApiService,
LogService,
CommentModel,
CommentsService
} from '@alfresco/adf-core';
import { CommentEntry, CommentsApi, Comment } from '@alfresco/js-api';
import { Injectable } from '@angular/core';
import { Observable, from, throwError } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { ContentService } from '../../common/services/content.service';
@Injectable({
providedIn: 'root'
@@ -35,7 +41,7 @@ export class NodeCommentsService implements CommentsService {
constructor(
private apiService: AlfrescoApiService,
private logService: LogService,
private peopleContentService: PeopleContentService
private contentService: ContentService
) {}
/**
@@ -101,7 +107,7 @@ export class NodeCommentsService implements CommentsService {
return throwError(error || 'Server error');
}
getUserImage(user: string): string {
return this.peopleContentService.getUserProfileImage(user);
getUserImage(avatarId: string): string {
return this.contentService.getContentUrl(avatarId);
}
}
@@ -15,11 +15,12 @@
* limitations under the License.
*/
import { AllowableOperationsEnum, ContentService } from '@alfresco/adf-core';
import { Node, NodeEntry, PermissionElement } from '@alfresco/js-api';
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { NodePermissionService } from '../../services/node-permission.service';
import { RoleModel } from '../../models/role.model';
import { ContentService } from '../../../common/services/content.service';
import { AllowableOperationsEnum } from '../../../common/models/allowable-operations.enum';
@Component({
selector: 'adf-add-permission',
@@ -17,10 +17,11 @@
import { SimpleInheritedPermissionTestComponent } from '../../mock/inherited-permission.component.mock';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NodesApiService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { of } from 'rxjs';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { NodesApiService } from '../../common/services/nodes-api.service';
const fakeNodeWithInherit: any = { id: 'fake-id', permissions : {isInheritanceEnabled : true}, allowableOperations: ['updatePermissions']};
const fakeNodeNoInherit: any = { id: 'fake-id', permissions : {isInheritanceEnabled : false}, allowableOperations: ['updatePermissions']};
@@ -17,8 +17,10 @@
/* eslint-disable @angular-eslint/no-input-rename */
import { Directive, Input, Output, EventEmitter } from '@angular/core';
import { NodesApiService, ContentService, AllowableOperationsEnum } from '@alfresco/adf-core';
import { Node } from '@alfresco/js-api';
import { ContentService } from '../../common/services/content.service';
import { NodesApiService } from '../../common/services/nodes-api.service';
import { AllowableOperationsEnum } from '../../common/models/allowable-operations.enum';
@Directive({
selector: 'button[adf-inherit-permission], mat-button-toggle[adf-inherit-permission]',
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { NodesApiService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
@@ -36,6 +36,7 @@ import {
} from '../../../mock/permission-list.component.mock';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { MinimalNode } from '@alfresco/js-api';
import { NodesApiService } from '../../../common/services/nodes-api.service';
describe('PermissionListComponent', () => {
@@ -15,7 +15,8 @@
* limitations under the License.
*/
import { NodesApiService, NotificationService, setupTestBed } from '@alfresco/adf-core';
import { NotificationService, setupTestBed } from '@alfresco/adf-core';
import { NodesApiService } from '../../../common/services/nodes-api.service';
import { TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { of, throwError } from 'rxjs';
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { AllowableOperationsEnum, ContentService, NodesApiService, NotificationService } from '@alfresco/adf-core';
import { NotificationService } from '@alfresco/adf-core';
import { Node, PermissionElement } from '@alfresco/js-api';
import { EventEmitter, Injectable } from '@angular/core';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
@@ -26,6 +26,9 @@ import { PermissionDisplayModel } from '../../models/permission.model';
import { NodePermissionsModel } from '../../models/member.model';
import { NodePermissionService } from '../../services/node-permission.service';
import { NodePermissionDialogService } from '../../services/node-permission-dialog.service';
import { NodesApiService } from '../../../common/services/nodes-api.service';
import { ContentService } from '../../../common/services/content.service';
import { AllowableOperationsEnum } from '../../../common/models/allowable-operations.enum';
const SITE_MANAGER_ROLE = 'SiteManager';
@@ -19,7 +19,7 @@ import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Group, NodeEntry } from '@alfresco/js-api';
import { NodePermissionService } from '../../services/node-permission.service';
import { EcmUserModel } from '@alfresco/adf-core';
import { EcmUserModel } from '../../../common/models/ecm-user.model';
@Component({
selector: 'adf-user-name-column',
@@ -18,7 +18,7 @@
import { Group, Node, NodeEntry, PermissionElement } from '@alfresco/js-api';
import { PermissionDisplayModel } from './permission.model';
import { RoleModel } from './role.model';
import { EcmUserModel } from '@alfresco/adf-core';
import { EcmUserModel } from '../../common/models/ecm-user.model';
export interface NodePermissionsModel {
node: Node;
@@ -15,7 +15,6 @@
* limitations under the License.
*/
import { AllowableOperationsEnum, ContentService } from '@alfresco/adf-core';
import { Node, PermissionElement } from '@alfresco/js-api';
import { MatDialog } from '@angular/material/dialog';
import { Injectable } from '@angular/core';
@@ -25,6 +24,8 @@ import { NodePermissionService } from './node-permission.service';
import { AddPermissionDialogComponent } from '../components/add-permission/add-permission-dialog.component';
import { AddPermissionDialogData } from '../components/add-permission/add-permission-dialog-data.interface';
import { RoleModel } from '../models/role.model';
import { ContentService } from '../../common/services/content.service';
import { AllowableOperationsEnum } from '../../common/models/allowable-operations.enum';
@Injectable({
providedIn: 'root'
@@ -17,7 +17,7 @@
import { TestBed } from '@angular/core/testing';
import { NodePermissionService } from './node-permission.service';
import { NodesApiService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { SearchService } from '../../search/services/search.service';
import { Node, PermissionElement } from '@alfresco/js-api';
import { of, throwError } from 'rxjs';
@@ -26,6 +26,7 @@ import { fakeEmptyResponse, fakeNodeWithOnlyLocally, fakeSiteRoles, fakeSiteNode
import { fakeAuthorityResults } from '../../mock/add-permission.component.mock';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { NodesApiService } from '../../common/services/nodes-api.service';
describe('NodePermissionService', () => {
@@ -17,10 +17,10 @@
import {
AlfrescoApiService,
NodesApiService,
TranslationService,
EcmUserModel
TranslationService
} from '@alfresco/adf-core';
import { NodesApiService } from '../../common/services/nodes-api.service';
import { EcmUserModel } from '../../common/models/ecm-user.model';
import {
Group,
GroupMemberEntry,
@@ -16,7 +16,7 @@
*/
import { Injectable } from '@angular/core';
import { AlfrescoApiService, AppConfigService, NodesApiService, DataSorting } from '@alfresco/adf-core';
import { AlfrescoApiService, AppConfigService, DataSorting } from '@alfresco/adf-core';
import { SearchConfiguration } from '../models/search-configuration.interface';
import { BaseQueryBuilderService } from './base-query-builder.service';
import { SearchCategory } from '../models/search-category.interface';
@@ -25,6 +25,7 @@ import { filter } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { SearchSortingDefinition } from '../models/search-sorting-definition.interface';
import { FilterSearch } from '../models/filter-search.interface';
import { NodesApiService } from '../../common/services/nodes-api.service';
@Injectable({
providedIn: 'root'
@@ -15,7 +15,8 @@
* limitations under the License.
*/
import { setupTestBed, NodesApiService } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { NodesApiService } from '../../common/services/nodes-api.service';
import { TreeViewService } from './tree-view.service';
import { TestBed } from '@angular/core/testing';
import { ContentTestingModule } from '../../testing/content.testing.module';
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { NodesApiService } from '@alfresco/adf-core';
import { NodesApiService } from '../../common/services/nodes-api.service';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { TreeBaseNode } from '../models/tree-view.model';
@@ -17,12 +17,15 @@
import { Component, NgZone } from '@angular/core';
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { TranslationService, UploadService, setupTestBed, FileModel, FileUploadErrorEvent } from '@alfresco/adf-core';
import { TranslationService, setupTestBed } from '@alfresco/adf-core';
import { UploadBase } from './upload-base';
import { UploadFilesEvent } from '../upload-files.event';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { mockUploadSuccessPromise } from '../../../mock/upload.service.mock';
import { UploadService } from '../../../common/services/upload.service';
import { FileModel } from '../../../common/models/file.model';
import { FileUploadErrorEvent } from '../../../common/events/file.event';
@Component({
selector: 'adf-upload-button-test',
@@ -15,7 +15,10 @@
* limitations under the License.
*/
import { FileModel, FileInfo, UploadService, TranslationService, FileUploadErrorEvent } from '@alfresco/adf-core';
import { FileInfo, TranslationService } from '@alfresco/adf-core';
import { FileUploadErrorEvent } from '../../../common/events/file.event';
import { FileModel } from '../../../common/models/file.model';
import { UploadService } from '../../../common/services/upload.service';
import { EventEmitter, Input, Output, OnInit, OnDestroy, NgZone, Directive } from '@angular/core';
import { Subject } from 'rxjs';
import { UploadFilesEvent } from '../upload-files.event';
@@ -18,12 +18,15 @@
import { EventEmitter } from '@angular/core';
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import {
FileModel, FileUploadCompleteEvent, FileUploadErrorEvent, UploadService, setupTestBed, UserPreferencesService
setupTestBed, UserPreferencesService
} from '@alfresco/adf-core';
import { UploadModule } from '../upload.module';
import { FileUploadingDialogComponent } from './file-uploading-dialog.component';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { UploadService } from '../../common/services/upload.service';
import { FileModel } from '../../common/models/file.model';
import { FileUploadCompleteEvent, FileUploadErrorEvent } from '../../common/events/file.event';
describe('FileUploadingDialogComponent', () => {
let fixture: ComponentFixture<FileUploadingDialogComponent>;

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