import { Injectable } from '@angular/core' ; import { MatDialog } from '@angular/material/dialog' ; import { Observable , Subject , of , zip , from } from 'rxjs' ; import { AlfrescoApiService , ContentService , DataColumn , TranslationService , ThumbnailService } from '@alfresco/adf-core' ; import { DocumentListService , ContentNodeSelectorComponent , ContentNodeSelectorComponentData , ContentNodeDialogService , ShareDataRow , NodeAction } from '@alfresco/adf-content-services' ; import { MinimalNodeEntity , MinimalNodeEntryEntity , SitePaging , NodeChildAssociationPaging , NodeChildAssociationEntry , NodesApi } from '@alfresco/js-api' ; import { ContentApiService } from '@alfresco/aca-shared' ; import { catchError , map , mergeMap } from 'rxjs/operators' ; type BatchOperationType = Extract < NodeAction , NodeAction . COPY | NodeAction . MOVE > ; @ Injectable ( { providedIn : 'root' } ) export class NodeActionsService { contentCopied : Subject < MinimalNodeEntity [ ] > = new Subject < MinimalNodeEntity [ ] > ( ) ; contentMoved : Subject < any > = new Subject < any > ( ) ; moveDeletedEntries : any [ ] = [ ] ; isSitesDestinationAvailable = false ; _nodesApi : NodesApi ; get nodesApi ( ) : NodesApi { this . _nodesApi = this . _nodesApi ?? new NodesApi ( this . apiService . getInstance ( ) ) ; return this . _nodesApi ; } constructor ( private contentService , private contentApi , private dialog , private documentListService , private apiService , private translation , private thumbnailService ) { } param contentEntities param permission copyNodes ( contentEntities , permission ? ) { return this . doBatchOperation ( NodeAction . COPY , contentEntities , permission ) ; } param contentEntities param permission moveNodes ( contentEntities , permission ? ) { return this . doBatchOperation ( NodeAction . MOVE , contentEntities , permission ) ; } param action param contentEntities param permission doBatchOperation ( action , contentEntities , permission ? ) { const observable = new Subject < string > ( ) ; if ( ! this . isEntryEntitiesArray ( contentEntities ) ) { observable . error ( new Error ( JSON . stringify ( { error : { statusCode : 400 } } ) ) ) ; } else if ( this . checkPermission ( action , contentEntities , permission ) ) { const destinationSelection = this . getContentNodeSelection ( action , contentEntities ) ; destinationSelection . subscribe ( ( selections ) => { const contentEntry = contentEntities [ 0 ] . entry ; const contentEntryId = contentEntry . nodeId || contentEntry . id ; const type = contentEntry . isFolder ? 'folder' : 'content' ; const batch = [ ] ; const selection = selections [ 0 ] ; let action$ ; if ( action === NodeAction . MOVE && contentEntities . length === 1 && type === 'content' ) { action$ = this . documentListService . moveNode ( contentEntryId , selection . id ) ; } else { contentEntities . forEach ( ( node ) => { batch . push ( this [ `${ action . toLowerCase ( ) }NodeAction` ] ( node . entry , selection . id ) ) ; } ) ; action$ = zip ( ... batch ) ; } action$ . subscribe ( ( newContent ) => { observable . next ( `OPERATION.SUCCESS.${ type . toUpperCase ( ) }.${ action . toUpperCase ( ) }` ) ; const processedData = this . processResponse ( newContent ) ; if ( action === NodeAction . COPY ) { this . contentCopied . next ( processedData . succeeded ) ; } else if ( action === NodeAction . MOVE ) { this . contentMoved . next ( processedData ) ; } } , observable . error . bind ( observable ) ) ; } ) ; } else { observable . error ( new Error ( JSON . stringify ( { error : { statusCode : 403 } } ) ) ) ; } return observable ; } isEntryEntitiesArray ( contentEntities ) { if ( contentEntities && contentEntities . length ) { const nonEntryNode = contentEntities . find ( ( node ) => ! node || ! node . entry || ! ( node . entry . nodeId || node . entry . id ) ) ; return ! nonEntryNode ; } return false ; } checkPermission ( action , contentEntities , permission ? ) { const notAllowedNode = contentEntities . find ( ( node ) => ! this . isActionAllowed ( action , node . entry , permission ) ) ; return ! notAllowedNode ; } getEntryParentId ( nodeEntry ) { let entryParentId = '' ; if ( nodeEntry . parentId ) { entryParentId = nodeEntry . parentId ; } else if ( nodeEntry . path && nodeEntry . path . elements && nodeEntry . path . elements . length ) { entryParentId = nodeEntry . path . elements [ nodeEntry . path . elements . length - 1 ] . id ; } return entryParentId ; } getContentNodeSelection ( action , contentEntities ) { const currentParentFolderId = this . getEntryParentId ( contentEntities [ 0 ] . entry ) ; const customDropdown = new SitePaging ( { list : { entries : [ { entry : { guid : '-my-' , title : this . translation . instant ( 'APP.BROWSE.PERSONAL.SIDENAV_LINK.LABEL' ) } } , { entry : { guid : '-mysites-' , title : this . translation . instant ( 'APP.BROWSE.LIBRARIES.MENU.MY_LIBRARIES.SIDENAV_LINK.LABEL' ) } } ] } } ) ; const title = this . getTitleTranslation ( action , contentEntities ) ; this . isSitesDestinationAvailable = false ; const data = { selectionMode : 'single' , title : title , currentFolderId : currentParentFolderId , actionName : action , dropdownHideMyFiles : true , dropdownSiteList : customDropdown , rowFilter : this . rowFilter . bind ( this ) , imageResolver : this . imageResolver . bind ( this ) , isSelectionValid : this . canCopyMoveInsideIt . bind ( this ) , breadcrumbTransform : this . customizeBreadcrumb . bind ( this ) , select : new Subject < MinimalNodeEntryEntity [ ] > ( ) , excludeSiteContent : ContentNodeDialogService . nonDocumentSiteContent } ; this . dialog . open ( ContentNodeSelectorComponent , { data , panelClass : 'adf-content-node-selector-dialog' , width : '630px' } ) ; data . select . subscribe ( { complete : this . close . bind ( this ) } ) ; return data . select ; } getTitleTranslation ( action , nodes = [ ] ) { let keyPrefix = 'ITEMS' ; let name = '' ; if ( nodes . length === 1 && nodes [ 0 ] . entry . name ) { name = nodes [ 0 ] . entry . name ; keyPrefix = 'ITEM' ; } const number = nodes . length ; return this . translation . instant ( `NODE_SELECTOR.${ action . toUpperCase ( ) }_${ keyPrefix }` , { name , number } ) ; } private canCopyMoveInsideIt ( entry ) { return this . hasEntityCreatePermission ( entry ) && ! this . isSite ( entry ) ; } private hasEntityCreatePermission ( entry ) { return this . contentService . hasAllowableOperations ( entry , 'create' ) ; } private isSite ( entry ) { return ! ! entry . guid || entry . nodeType === 'st:site' || entry . nodeType === 'st:sites' ; } close ( ) { this . dialog . closeAll ( ) ; } private customizeBreadcrumb ( node ) { if ( node && node . path && node . path . elements ) { const elements = node . path . elements ; if ( elements . length > 1 ) { if ( elements [ 1 ] . name === 'User Homes' ) { elements . splice ( 0 , 2 ) ; if ( elements [ 0 ] ) { elements [ 0 ] . name = this . translation . instant ( 'APP.BROWSE.PERSONAL.TITLE' ) ; elements [ 0 ] . id = '-my-' ; } else { node . name = this . translation . instant ( 'APP.BROWSE.PERSONAL.TITLE' ) ; } } else if ( elements [ 1 ] . name === 'Sites' ) { this . normalizeSitePath ( node ) ; } } else if ( elements . length === 1 ) { if ( node . name === 'Sites' ) { node . name = this . translation . instant ( 'APP.BROWSE.LIBRARIES.MENU.MY_LIBRARIES.TITLE' ) ; elements . splice ( 0 , 1 ) ; } } } else if ( node === null && this . isSitesDestinationAvailable ) { node = { name : this . translation . instant ( 'APP.BROWSE.LIBRARIES.MENU.MY_LIBRARIES.TITLE' ) , path : { elements : [ ] } } as any ; } return node ; } private normalizeSitePath ( node ) { const elements = node . path . elements ; elements . splice ( 0 , 1 ) ; elements [ 0 ] . name = this . translation . instant ( 'APP.BROWSE.LIBRARIES.MENU.MY_LIBRARIES.TITLE' ) ; elements [ 0 ] . id = '-mysites-' ; if ( this . isSiteContainer ( node ) ) { node . name = elements [ 1 ] . name ; elements . splice ( 1 , 1 ) ; } else { const docLib = elements . findIndex ( ( el ) => el . name === 'documentLibrary' ) ; if ( docLib > - 1 ) { elements . splice ( docLib , 1 ) ; } } } isSiteContainer ( node ) { if ( node && node . aspectNames && node . aspectNames . length > 0 ) { return node . aspectNames . indexOf ( 'st:siteContainer' ) >= 0 ; } return false ; } copyNodeAction ( nodeEntry , selectionId ) { if ( nodeEntry . isFolder ) { return this . copyFolderAction ( nodeEntry , selectionId ) ; } else { return this . copyContentAction ( nodeEntry , selectionId ) ; } } copyContentAction ( contentEntry , selectionId , oldName ? ) { const _oldName = oldName || contentEntry . name ; const contentEntryId = contentEntry . nodeId || contentEntry . id ; return this . copyNode ( contentEntryId , selectionId , _oldName ) . pipe ( catchError ( ( err ) => { let errStatusCode ; try { const { error : { statusCode } } = JSON . parse ( err . message ) ; errStatusCode = statusCode ; } catch ( e ) { } if ( errStatusCode && errStatusCode === 409 ) { return this . copyContentAction ( contentEntry , selectionId , this . getNewNameFrom ( _oldName , contentEntry . name ) ) ; } else { return of ( err || 'Server error' ) ; } } ) ) ; } copyFolderAction ( contentEntry , selectionId ) { const contentEntryId = contentEntry . nodeId || contentEntry . id ; let $destinationFolder ; let $childrenToCopy ; let newDestinationFolder ; return this . copyNode ( contentEntryId , selectionId , contentEntry . name ) . pipe ( catchError ( ( err ) => { let errStatusCode ; try { const { error : { statusCode } } = JSON . parse ( err . message ) ; errStatusCode = statusCode ; } catch { } if ( errStatusCode && errStatusCode === 409 ) { $destinationFolder = this . getChildByName ( selectionId , contentEntry . name ) ; $childrenToCopy = this . getNodeChildren ( contentEntryId ) ; return $destinationFolder . pipe ( mergeMap ( ( destination ) => { newDestinationFolder = destination ; return $childrenToCopy ; } ) , mergeMap ( ( nodesToCopy ) => { const batch = [ ] ; nodesToCopy . list . entries . forEach ( ( node ) => { if ( node . entry . isFolder ) { batch . push ( this . copyFolderAction ( node . entry , newDestinationFolder . entry . id ) ) ; } else { batch . push ( this . copyContentAction ( node . entry , newDestinationFolder . entry . id ) ) ; } } ) ; if ( ! batch . length ) { return of ( { } ) ; } return zip ( ... batch ) ; } ) ) ; } else { return of ( err || 'Server error' ) ; } } ) ) ; } moveNodeAction ( nodeEntry , selectionId ) { this . moveDeletedEntries = [ ] ; if ( nodeEntry . isFolder ) { const initialParentId = nodeEntry . parentId ; return this . moveFolderAction ( nodeEntry , selectionId ) . pipe ( mergeMap ( ( newContent ) => { if ( initialParentId === selectionId ) { return of ( newContent ) ; } const flattenResponse = this . flatten ( newContent ) ; const processedData = this . processResponse ( flattenResponse ) ; if ( processedData . failed . length === 0 ) { return this . getChildByName ( initialParentId , nodeEntry . name ) . pipe ( mergeMap ( ( folderOnInitialLocation ) => { if ( folderOnInitialLocation ) { const nodeEntryId = nodeEntry . nodeId || nodeEntry . id ; return this . contentApi . deleteNode ( nodeEntryId ) . pipe ( mergeMap ( ( ) => { this . moveDeletedEntries . push ( nodeEntry ) ; return of ( newContent ) ; } ) ) ; } return of ( newContent ) ; } ) ) ; } return of ( newContent ) ; } ) ) ; } else { return this . moveContentAction ( nodeEntry , selectionId ) ; } } moveFolderAction ( contentEntry , selectionId ) { const contentEntryId = contentEntry . nodeId || contentEntry . id ; const initialParentId = this . getEntryParentId ( contentEntry ) ; let $destinationFolder ; let $childrenToMove ; let newDestinationFolder ; return this . documentListService . moveNode ( contentEntryId , selectionId ) . pipe ( map ( ( itemMoved ) => { return { itemMoved , initialParentId } ; } ) , catchError ( ( err ) => { let errStatusCode ; try { const { error : { statusCode } } = JSON . parse ( err . message ) ; errStatusCode = statusCode ; } catch ( e ) { } if ( errStatusCode && errStatusCode === 409 ) { $destinationFolder = this . getChildByName ( selectionId , contentEntry . name ) ; $childrenToMove = this . getNodeChildren ( contentEntryId ) ; return $destinationFolder . pipe ( mergeMap ( ( destination ) => { newDestinationFolder = destination ; return $childrenToMove ; } ) , mergeMap ( ( childrenToMove ) => { const batch = [ ] ; childrenToMove . list . entries . forEach ( ( node ) => { if ( node . entry . isFolder ) { batch . push ( this . moveFolderAction ( node . entry , newDestinationFolder . entry . id ) ) ; } else { batch . push ( this . moveContentAction ( node . entry , newDestinationFolder . entry . id ) ) ; } } ) ; if ( ! batch . length ) { return of ( batch ) ; } return zip ( ... batch ) ; } ) ) ; } else { return of ( err ) ; } } ) ) ; } moveContentAction ( contentEntry , selectionId ) { const contentEntryId = contentEntry . nodeId || contentEntry . id ; const initialParentId = this . getEntryParentId ( contentEntry ) ; return this . documentListService . moveNode ( contentEntryId , selectionId ) . pipe ( map ( ( itemMoved ) => { return { itemMoved , initialParentId } ; } ) , catchError ( ( err ) => { return of ( err ) ; } ) ) ; } getChildByName ( parentId , name ) { const matchedNodes = new Subject < any > ( ) ; this . getNodeChildren ( parentId ) . subscribe ( ( childrenNodes ) => { const result = childrenNodes . list . entries . find ( ( node ) => node . entry . name === name ) ; if ( result ) { matchedNodes . next ( result ) ; } else { matchedNodes . next ( null ) ; } } , ( err ) => { return matchedNodes . error ( err ) ; } ) ; return matchedNodes ; } private isActionAllowed ( action , node , permission ? ) { if ( action === NodeAction . COPY ) { return true ; } return this . contentService . hasAllowableOperations ( node , permission ) ; } private rowFilter ( row ) { const node = row . node . entry ; this . isSitesDestinationAvailable = ! ! node [ 'guid' ] ; return ! node . isFile && node . nodeType !== 'app:folderlink' ; } private imageResolver ( row , _ ) { const entry = row . node . entry ; if ( ! this . contentService . hasAllowableOperations ( entry , 'update' ) ) { return this . thumbnailService . getMimeTypeIcon ( 'disable/folder' ) ; } return null ; } public getNewNameFrom ( name , baseName ? ) { const extensionMatch = name . match ( /\.[^/.]+$/ ) ; const fileExtension = extensionMatch ? extensionMatch [ 0 ] : '' ; let extensionFree = extensionMatch ? name . slice ( 0 , extensionMatch . index ) : name ; let prefixNumber = 1 ; let baseExtensionFree ; if ( baseName ) { const baseExtensionMatch = baseName . match ( /\.[^/.]+$/ ) ; baseExtensionFree = baseExtensionMatch ? baseName . slice ( 0 , baseExtensionMatch . index ) : baseName ; } if ( ! baseExtensionFree || baseExtensionFree !== extensionFree ) { const oldPrefix = extensionFree . match ( '-[0-9]+$' ) ; if ( oldPrefix ) { const oldPrefixNumber = parseInt ( oldPrefix [ 0 ] . slice ( 1 ) , 10 ) ; if ( oldPrefixNumber . toString ( ) === oldPrefix [ 0 ] . slice ( 1 ) ) { extensionFree = extensionFree . slice ( 0 , oldPrefix . index ) ; prefixNumber = oldPrefixNumber + 1 ; } } } return extensionFree + '-' + prefixNumber + fileExtension ; } param nodeId param params getNodeChildren ( nodeId , params ? ) { return from ( this . nodesApi . listNodeChildren ( nodeId , params ) ) ; } param nodeId param targetParentId param name copyNode ( nodeId , targetParentId , name ? ) { return from ( this . nodesApi . copyNode ( nodeId , { targetParentId , name } ) ) ; } public flatten ( nDimArray ) { if ( ! Array . isArray ( nDimArray ) ) { return nDimArray ; } const nodeQueue = nDimArray . slice ( 0 ) ; const resultingArray = [ ] ; do { nodeQueue . forEach ( ( node ) => { if ( Array . isArray ( node ) ) { nodeQueue . push ( ... node ) ; } else { resultingArray . push ( node ) ; } const nodeIndex = nodeQueue . indexOf ( node ) ; nodeQueue . splice ( nodeIndex , 1 ) ; } ) ; } while ( nodeQueue . length ) ; return resultingArray ; } processResponse ( data ) { const moveStatus = { succeeded : [ ] , failed : [ ] , partiallySucceeded : [ ] } ; if ( Array . isArray ( data ) ) { return data . reduce ( ( acc , next ) => { if ( next instanceof Error ) { acc . failed . push ( next ) ; } else if ( Array . isArray ( next ) ) { const folderMoveResponseData = this . flatten ( next ) ; const foundError = folderMoveResponseData . find ( ( node ) => node instanceof Error ) ; const foundEntry = folderMoveResponseData . find ( ( node ) => ( node . itemMoved && node . itemMoved . entry ) || ( node && node . entry ) ) ; if ( ! foundError ) { acc . succeeded . push ( next ) ; } else if ( ! foundEntry ) { acc . failed . push ( next ) ; } else { acc . partiallySucceeded . push ( next ) ; } } else { acc . succeeded . push ( next ) ; } return acc ; } , moveStatus ) ; } else { if ( ( data . itemMoved && data . itemMoved . entry ) || ( data && data . entry ) ) { moveStatus . succeeded . push ( data ) ; } else { moveStatus . failed . push ( data ) ; } return moveStatus ; } } }