diff --git a/angular.json b/angular.json index f871fe16d..908491f7f 100644 --- a/angular.json +++ b/angular.json @@ -94,6 +94,7 @@ "styles": [ "src/assets/fonts/material-icons/material-icons.css", "src/assets/fonts/muli/muli.css", + "node_modules/cropperjs/dist/cropper.min.css", "src/styles.scss" ], "scripts": [ diff --git a/package-lock.json b/package-lock.json index ce12b229b..07b94b6ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,9 +38,9 @@ } }, "@alfresco/adf-core": { - "version": "4.4.0-32262", - "resolved": "https://registry.npmjs.org/@alfresco/adf-core/-/adf-core-4.4.0-32262.tgz", - "integrity": "sha512-CXOaGnRbPMliRcTNUeztNc4Bc0zHWdQ2vp1JucplMQY6vaDrmKXBXasFTunA9y3v0WR/YHTqDCP2r+5y5LYZTg==", + "version": "4.4.0-32367", + "resolved": "https://registry.npmjs.org/@alfresco/adf-core/-/adf-core-4.4.0-32367.tgz", + "integrity": "sha512-SWyrLWyaoosCyZycSkw6mfysVdAYARGQrHIqcYM1AnfmXyqz/evW9x+88/IG/Pyh/iL/vfgutkUTf3ZTxjsBhA==", "requires": { "tslib": "^2.0.0" } @@ -8806,6 +8806,15 @@ "integrity": "sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ==", "dev": true }, + "jasmine-marbles": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/jasmine-marbles/-/jasmine-marbles-0.6.0.tgz", + "integrity": "sha512-1uzgjEesEeCb+r+v46qn5x326TiGqk5SUZa+A3O+XnMCjG/pGcUOhL9Xsg5L7gLC6RFHyWGTkB5fei4rcvIOiQ==", + "dev": true, + "requires": { + "lodash": "^4.5.0" + } + }, "jasmine-spec-reporter": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-6.0.0.tgz", diff --git a/package.json b/package.json index e4c4fb1bd..be3af5025 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "private": true, "dependencies": { "@alfresco/adf-content-services": "4.4.0-32262", - "@alfresco/adf-core": "4.4.0-32262", + "@alfresco/adf-core": "4.4.0-32367", "@alfresco/adf-extensions": "4.4.0-32262", "@alfresco/js-api": "4.4.0-3371", "@angular/animations": "10.0.4", @@ -85,6 +85,7 @@ "husky": "^5.1.1", "inquirer": "^7.3.3", "jasmine-core": "~3.7.1", + "jasmine-marbles": "0.6.0", "jasmine-spec-reporter": "~6.0.0", "karma": "^6.3.1", "karma-chrome-launcher": "^3.1.0", diff --git a/projects/aca-shared/store/src/actions/upload.actions.ts b/projects/aca-shared/store/src/actions/upload.actions.ts index 05e8e163a..fcb8282fd 100644 --- a/projects/aca-shared/store/src/actions/upload.actions.ts +++ b/projects/aca-shared/store/src/actions/upload.actions.ts @@ -28,7 +28,8 @@ import { Action } from '@ngrx/store'; export enum UploadActionTypes { UploadFiles = 'UPLOAD_FILES', UploadFolder = 'UPLOAD_FOLDER', - UploadFileVersion = 'UPLOAD_FILE_VERSION' + UploadFileVersion = 'UPLOAD_FILE_VERSION', + UploadImage = 'UPLOAD_IMAGE' } export class UploadFilesAction implements Action { @@ -43,6 +44,12 @@ export class UploadFolderAction implements Action { constructor(public payload: any) {} } +export class UploadNewImageAction implements Action { + readonly type = UploadActionTypes.UploadImage; + + constructor(public payload: any) {} +} + export class UploadFileVersionAction implements Action { readonly type = UploadActionTypes.UploadFileVersion; diff --git a/src/app/components/layout/app-layout/app-layout.component.scss b/src/app/components/layout/app-layout/app-layout.component.scss index ec355913d..f80819974 100644 --- a/src/app/components/layout/app-layout/app-layout.component.scss +++ b/src/app/components/layout/app-layout/app-layout.component.scss @@ -15,6 +15,12 @@ right: 0; background-color: white; } + + adf-file-uploading-dialog { + z-index: 1100; + } + + } @media screen and (max-width: 599px) { diff --git a/src/app/components/viewer/viewer.component.html b/src/app/components/viewer/viewer.component.html index f62a0a2ff..2b5da1866 100644 --- a/src/app/components/viewer/viewer.component.html +++ b/src/app/components/viewer/viewer.component.html @@ -14,6 +14,8 @@ [allowDownload]="false" [allowFullScreen]="false" [overlayMode]="true" + [readOnly]="!canUpdateNode" + (fileSubmit)="onFileSubmit($event)" (showViewerChange)="onViewerVisibilityChanged()" [canNavigateBefore]="previousNodeId" [canNavigateNext]="nextNodeId" diff --git a/src/app/components/viewer/viewer.component.ts b/src/app/components/viewer/viewer.component.ts index 33d67c525..ffce1de01 100644 --- a/src/app/components/viewer/viewer.component.ts +++ b/src/app/components/viewer/viewer.component.ts @@ -33,6 +33,7 @@ import { ReloadDocumentListAction, SetCurrentNodeVersionAction, SetSelectedNodesAction, + UploadNewImageAction, ViewerActionTypes, ViewNodeAction } from '@alfresco/aca-shared/store'; @@ -45,6 +46,7 @@ import { Store } from '@ngrx/store'; import { from, Observable, Subject } from 'rxjs'; import { debounceTime, takeUntil } from 'rxjs/operators'; import { Actions, ofType } from '@ngrx/effects'; +import { ContentManagementService } from '../../services/content-management.service'; @Component({ selector: 'app-viewer', @@ -64,6 +66,7 @@ export class AppViewerComponent implements OnInit, OnDestroy { selection: SelectionState; infoDrawerOpened$: Observable; + canUpdateNode = false; showRightSide = false; openWith: ContentActionRef[] = []; toolbarActions: ContentActionRef[] = []; @@ -112,7 +115,8 @@ export class AppViewerComponent implements OnInit, OnDestroy { private preferences: UserPreferencesService, private apiService: AlfrescoApiService, private uploadService: UploadService, - private appHookService: AppHookService + private appHookService: AppHookService, + private content: ContentManagementService ) {} ngOnInit() { @@ -208,6 +212,7 @@ export class AppViewerComponent implements OnInit, OnDestroy { if (nodeId) { try { this.node = await this.contentApi.getNodeInfo(nodeId).toPromise(); + this.canUpdateNode = this.content.canUpdateNode(this.node); this.store.dispatch(new SetSelectedNodesAction([{ entry: this.node }])); if (this.node && this.node.isFile) { @@ -248,6 +253,11 @@ export class AppViewerComponent implements OnInit, OnDestroy { this.store.dispatch(new ViewNodeAction(this.nextNodeId, { location })); } + onFileSubmit(newBlob: Blob) { + const newImageFile: File = new File([newBlob], this?.node?.name, { type: this?.node?.content?.mimeType }); + this.store.dispatch(new UploadNewImageAction(newImageFile)); + } + /** * Retrieves nearest node information for the given node and folder. * @param nodeId Unique identifier of the document node diff --git a/src/app/store/effects/upload.effects.spec.ts b/src/app/store/effects/upload.effects.spec.ts index 794eef627..85577b68b 100644 --- a/src/app/store/effects/upload.effects.spec.ts +++ b/src/app/store/effects/upload.effects.spec.ts @@ -25,13 +25,23 @@ import { Store } from '@ngrx/store'; import { TestBed } from '@angular/core/testing'; -import { EffectsModule } from '@ngrx/effects'; +import { Actions, EffectsModule } from '@ngrx/effects'; import { UploadEffects } from './upload.effects'; import { AppTestingModule } from '../../testing/app-testing.module'; import { NgZone } from '@angular/core'; import { UploadService, FileUploadCompleteEvent, FileModel } from '@alfresco/adf-core'; -import { UnlockWriteAction, UploadFileVersionAction } from '@alfresco/aca-shared/store'; +import { + SnackbarErrorAction, + SnackbarInfoAction, + UnlockWriteAction, + UploadFileVersionAction, + UploadNewImageAction +} from '@alfresco/aca-shared/store'; import { ContentManagementService } from '../../services/content-management.service'; +import { Observable, of, throwError } from 'rxjs'; +import { MinimalNodeEntryEntity } from '@alfresco/js-api'; +import { cold, hot } from 'jasmine-marbles'; +import { provideMockActions } from '@ngrx/effects/testing'; describe('UploadEffects', () => { let store: Store; @@ -39,10 +49,35 @@ describe('UploadEffects', () => { let effects: UploadEffects; let zone: NgZone; let contentManagementService: ContentManagementService; + let actions$: Observable = of(); + const fakeNode: MinimalNodeEntryEntity = { + createdAt: undefined, + modifiedAt: undefined, + modifiedByUser: undefined, + isFile: true, + createdByUser: { + id: 'admin.adf@alfresco.com', + displayName: 'Administrator' + }, + nodeType: 'cm:content', + content: { + mimeType: 'image/jpeg', + mimeTypeName: 'JPEG Image', + sizeInBytes: 175540, + encoding: 'UTF-8' + }, + parentId: 'dff2bc1e-d092-42ac-82d1-87c82f6e56cb', + isFolder: false, + name: 'GoqZhm.jpg', + id: '1bf8a8f7-18ac-4eef-919d-61d952eaa179', + allowableOperations: ['delete', 'update', 'updatePermissions'], + isFavorite: false + }; beforeEach(() => { TestBed.configureTestingModule({ - imports: [AppTestingModule, EffectsModule.forRoot([UploadEffects])] + imports: [AppTestingModule, EffectsModule.forRoot([UploadEffects])], + providers: [provideMockActions(() => actions$)] }); zone = TestBed.inject(NgZone); @@ -56,11 +91,6 @@ describe('UploadEffects', () => { effects = TestBed.inject(UploadEffects); }); - beforeEach(() => { - spyOn(effects['fileVersionInput'], 'click'); - spyOn(effects, 'uploadVersion').and.callThrough(); - }); - describe('uploadAndUnlock()', () => { it('should not upload and unlock file if param not provided', () => { effects.uploadAndUnlock(null); @@ -151,8 +181,17 @@ describe('UploadEffects', () => { }); describe('upload file version', () => { + beforeEach(() => { + actions$ = TestBed.inject(Actions); + }); + it('should trigger upload file from context menu', () => { - store.dispatch({ type: 'UPLOAD_FILE_VERSION' }); + spyOn(effects['fileVersionInput'], 'click'); + actions$ = hot('a', { + a: new UploadFileVersionAction(undefined) + }); + const expected = cold('b', {}); + expect(effects.uploadVersion$).toBeObservable(expected); expect(effects['fileVersionInput'].click).toHaveBeenCalled(); }); @@ -206,8 +245,50 @@ describe('UploadEffects', () => { } } }); - store.dispatch(new UploadFileVersionAction(fakeEvent)); + actions$ = hot('a', { + a: new UploadFileVersionAction(fakeEvent) + }); + + const expected = cold('b', {}); + expect(effects.uploadVersion$).toBeObservable(expected); + expect(contentManagementService.versionUpdateDialog).toHaveBeenCalledWith(fakeEvent.detail.data.node.entry, fakeEvent.detail.files[0].file); }); }); + + describe('image versioning', () => { + beforeEach(() => { + actions$ = TestBed.inject(Actions); + }); + + it('should trigger upload file version from viewer', () => { + spyOn(contentManagementService, 'getNodeInfo').and.returnValue(of(fakeNode)); + const data = atob('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='); + const fakeBlob = new Blob([data], { type: 'image/png' }); + const newImageFile: File = new File([fakeBlob], 'GoqZhm.jpg'); + actions$ = hot('a', { + a: new UploadNewImageAction(newImageFile) + }); + + const expected = cold('b', { + b: new SnackbarInfoAction('APP.MESSAGES.UPLOAD.SUCCESS.MEDIA_MANAGEMENT') + }); + expect(effects.uploadNewImage$).toBeObservable(expected); + }); + + it('should display snackbar if can`t retrieve node details', () => { + spyOn(contentManagementService, 'getNodeInfo').and.returnValue(throwError(fakeNode)); + const data = atob('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='); + const fakeBlob = new Blob([data], { type: 'image/png' }); + const newImageFile: File = new File([fakeBlob], 'GoqZhm.jpg'); + actions$ = hot('a', { + a: new UploadNewImageAction(newImageFile) + }); + + const expected = cold('b', { + b: new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.GENERIC') + }); + expect(effects.uploadNewImage$).toBeObservable(expected); + }); + }); }); diff --git a/src/app/store/effects/upload.effects.ts b/src/app/store/effects/upload.effects.ts index ec4adfe26..999bc8448 100644 --- a/src/app/store/effects/upload.effects.ts +++ b/src/app/store/effects/upload.effects.ts @@ -31,14 +31,16 @@ import { UploadFilesAction, UploadFileVersionAction, UploadFolderAction, - getCurrentFolder + getCurrentFolder, + UploadNewImageAction, + SnackbarInfoAction } from '@alfresco/aca-shared/store'; import { FileModel, FileUtils, UploadService } from '@alfresco/adf-core'; import { Injectable, NgZone, RendererFactory2 } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { of } from 'rxjs'; -import { catchError, map, take } from 'rxjs/operators'; +import { catchError, map, mergeMap, switchMap, take } from 'rxjs/operators'; import { ContentManagementService } from '../../services/content-management.service'; import { MinimalNodeEntryEntity } from '@alfresco/js-api'; @@ -101,17 +103,47 @@ export class UploadEffects { }) ); + @Effect({ dispatch: false }) + uploadNewImage$ = this.actions$.pipe( + ofType(UploadActionTypes.UploadImage), + switchMap((action) => { + return this.contentService.getNodeInfo().pipe( + mergeMap((node) => { + if (node?.id) { + const newFile = new FileModel( + action?.payload, + { + majorVersion: false, + newVersion: true, + parentId: node?.parentId, + nodeType: node?.content?.mimeType + }, + node?.id + ); + this.uploadQueue([newFile]); + return of(new SnackbarInfoAction('APP.MESSAGES.UPLOAD.SUCCESS.MEDIA_MANAGEMENT')); + } + return of(null); + }), + catchError(() => { + return of(new SnackbarErrorAction('APP.MESSAGES.UPLOAD.ERROR.GENERIC')); + }) + ); + }) + ); + @Effect({ dispatch: false }) uploadVersion$ = this.actions$.pipe( ofType(UploadActionTypes.UploadFileVersion), - map((action) => { - if (action && action.payload) { - const node = action.payload.detail.data.node.entry; - const file: any = action.payload.detail.files[0].file; - this.contentService.versionUpdateDialog(node, file); + mergeMap((action) => { + if (action?.payload) { + const node = action?.payload?.detail?.data?.node?.entry; + const file: any = action?.payload?.detail?.files[0]?.file; + return of(this.contentService.versionUpdateDialog(node, file)); } else if (!action.payload) { - this.fileVersionInput.click(); + return of(this.fileVersionInput.click()); } + return of(null); }) ); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 24ebd82ce..c4e3125b0 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -301,6 +301,9 @@ "504": "The server timed out, try again or contact IT support [504]", "403": "Insufficient permissions to upload in this location [403]", "404": "Upload location no longer exists [404]" + }, + "SUCCESS": { + "MEDIA_MANAGEMENT_SUCCESS": "Version updated successfully" } }, "INFO": {