@@ -4751,28 +4751,180 @@ const cloneDockerFolderFromMenu = async (id) => {
47514751 if ( ! nextName ) {
47524752 return ;
47534753 }
4754- const clonePayload = {
4755- name : nextName ,
4756- icon : String ( source ?. icon || '' ) ,
4757- parentId : normalizeFolderParentId ( source ?. parentId || source ?. parent_id || '' ) ,
4758- settings : JSON . parse ( JSON . stringify ( ( source ?. settings && typeof source . settings === 'object' ) ? source . settings : { } ) ) ,
4759- regex : String ( source ?. regex || '' ) ,
4760- containers : Array . isArray ( source ?. containers ) ? [ ...source . containers ] : [ ] ,
4761- actions : Array . isArray ( source ?. actions ) ? JSON . parse ( JSON . stringify ( source . actions ) ) : [ ]
4762- } ;
4754+ const clonePayload = buildDockerFolderClonePayload ( source , { name : nextName } ) ;
47634755 $ ( 'div.spinner.fixed' ) . show ( 'slow' ) ;
47644756 try {
4765- await $ . post ( '/plugins/folderview.plus/server/create.php' , {
4757+ await persistDockerFolderClonePayload ( clonePayload ) ;
4758+ await $ . post ( '/plugins/folderview.plus/server/sync_order.php' , { type : 'docker' } ) . promise ( ) ;
4759+ loadlist ( ) ;
4760+ } finally {
4761+ $ ( 'div.spinner.fixed' ) . hide ( 'slow' ) ;
4762+ }
4763+ } , {
4764+ userMessage : getDockerMenuLabel ( 'clone-folder-failed' , 'Failed to clone folder.' ) ,
4765+ userVisible : true
4766+ } ) ;
4767+ } ;
4768+
4769+ const buildDockerFolderClonePayload = ( source , overrides = { } ) => {
4770+ const sourceName = String ( source ?. name || '' ) . trim ( ) || 'Folder' ;
4771+ const sourceParentId = normalizeFolderParentId ( source ?. parentId || source ?. parent_id || '' ) ;
4772+ const overrideName = overrides && Object . prototype . hasOwnProperty . call ( overrides , 'name' )
4773+ ? overrides . name
4774+ : undefined ;
4775+ const overrideParentId = overrides && Object . prototype . hasOwnProperty . call ( overrides , 'parentId' )
4776+ ? overrides . parentId
4777+ : undefined ;
4778+ const resolvedName = String ( overrideName ?? sourceName ) . trim ( ) || 'Folder' ;
4779+ const resolvedParentId = normalizeFolderParentId ( overrideParentId ?? sourceParentId ) ;
4780+ return {
4781+ name : resolvedName ,
4782+ icon : String ( source ?. icon || '' ) ,
4783+ parentId : resolvedParentId ,
4784+ settings : JSON . parse ( JSON . stringify ( ( source ?. settings && typeof source . settings === 'object' ) ? source . settings : { } ) ) ,
4785+ regex : String ( source ?. regex || '' ) ,
4786+ containers : Array . isArray ( source ?. containers ) ? [ ...source . containers ] : [ ] ,
4787+ actions : Array . isArray ( source ?. actions ) ? JSON . parse ( JSON . stringify ( source . actions ) ) : [ ]
4788+ } ;
4789+ } ;
4790+
4791+ const generateDockerFolderCloneId = ( reservedIds = new Set ( ) ) => {
4792+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' ;
4793+ const reserved = reservedIds instanceof Set ? reservedIds : new Set ( ) ;
4794+ const cryptoObject = window . crypto || window . msCrypto || null ;
4795+ for ( let attempt = 0 ; attempt < 16 ; attempt += 1 ) {
4796+ let nextId = '' ;
4797+ if ( cryptoObject && typeof cryptoObject . getRandomValues === 'function' ) {
4798+ const bytes = new Uint8Array ( 20 ) ;
4799+ cryptoObject . getRandomValues ( bytes ) ;
4800+ nextId = Array . from ( bytes , ( value ) => alphabet . charAt ( value % alphabet . length ) ) . join ( '' ) ;
4801+ } else {
4802+ nextId = Array . from ( { length : 20 } , ( ) => alphabet . charAt ( Math . floor ( Math . random ( ) * alphabet . length ) ) ) . join ( '' ) ;
4803+ }
4804+ if ( ! reserved . has ( nextId ) && ! Object . prototype . hasOwnProperty . call ( globalFolders , nextId ) ) {
4805+ return nextId ;
4806+ }
4807+ }
4808+ return `fvclone${ Date . now ( ) . toString ( 36 ) } ${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 10 ) } ` . slice ( 0 , 20 ) ;
4809+ } ;
4810+
4811+ const getDockerFolderBranchCloneOrder = ( rootId ) => {
4812+ const orderedIds = [ ] ;
4813+ const seen = new Set ( ) ;
4814+ const visit = ( folderId ) => {
4815+ const safeFolderId = String ( folderId || '' ) . trim ( ) ;
4816+ if ( ! safeFolderId || seen . has ( safeFolderId ) || ! globalFolders [ safeFolderId ] ) {
4817+ return ;
4818+ }
4819+ seen . add ( safeFolderId ) ;
4820+ orderedIds . push ( safeFolderId ) ;
4821+ getFolderChildren ( safeFolderId ) . forEach ( visit ) ;
4822+ } ;
4823+ visit ( rootId ) ;
4824+ return orderedIds ;
4825+ } ;
4826+
4827+ const persistDockerFolderClonePayload = async ( payload , folderId = '' ) => {
4828+ const safeFolderId = String ( folderId || '' ) . trim ( ) ;
4829+ const request = {
4830+ type : 'docker' ,
4831+ content : JSON . stringify ( payload )
4832+ } ;
4833+ if ( safeFolderId ) {
4834+ request . id = safeFolderId ;
4835+ }
4836+ await $ . post (
4837+ safeFolderId
4838+ ? '/plugins/folderview.plus/server/update.php'
4839+ : '/plugins/folderview.plus/server/create.php' ,
4840+ request
4841+ ) . promise ( ) ;
4842+ } ;
4843+
4844+ const rollbackClonedDockerFolders = async ( createdIds = [ ] ) => {
4845+ const ids = Array . isArray ( createdIds ) ? createdIds . filter ( ( entry ) => String ( entry || '' ) . trim ( ) !== '' ) : [ ] ;
4846+ for ( const createdId of ids . slice ( ) . reverse ( ) ) {
4847+ try {
4848+ await $ . post ( '/plugins/folderview.plus/server/delete.php' , {
47664849 type : 'docker' ,
4767- content : JSON . stringify ( clonePayload )
4850+ id : createdId
47684851 } ) . promise ( ) ;
4852+ } catch ( _error ) {
4853+ // Best-effort rollback only.
4854+ }
4855+ }
4856+ if ( ids . length > 0 ) {
4857+ try {
4858+ await $ . post ( '/plugins/folderview.plus/server/sync_order.php' , { type : 'docker' } ) . promise ( ) ;
4859+ } catch ( _error ) {
4860+ // Best-effort rollback only.
4861+ }
4862+ }
4863+ } ;
4864+
4865+ const cloneDockerFolderBranchFromMenu = async ( id ) => {
4866+ await runDockerGuardedAction ( 'clone-branch' , async ( ) => {
4867+ if ( ! ensureDockerFolderUnlocked ( id , 'Clone branch' ) ) {
4868+ return ;
4869+ }
4870+ const source = globalFolders [ id ] ;
4871+ if ( ! source || typeof source !== 'object' ) {
4872+ return ;
4873+ }
4874+ const branchIds = getDockerFolderBranchCloneOrder ( id ) ;
4875+ if ( branchIds . length <= 1 ) {
4876+ await cloneDockerFolderFromMenu ( id ) ;
4877+ return ;
4878+ }
4879+ const defaultName = `${ String ( source ?. name || 'Folder' ) . trim ( ) || 'Folder' } (Copy)` ;
4880+ const nextName = String ( window . prompt ( 'Clone branch root name' , defaultName ) || '' ) . trim ( ) ;
4881+ if ( ! nextName ) {
4882+ return ;
4883+ }
4884+ const sourceParentId = normalizeFolderParentId ( source ?. parentId || source ?. parent_id || '' ) ;
4885+ const reservedIds = new Set ( Object . keys ( globalFolders ) ) ;
4886+ const cloneIdMap = new Map ( ) ;
4887+ branchIds . forEach ( ( sourceId ) => {
4888+ const cloneId = generateDockerFolderCloneId ( reservedIds ) ;
4889+ reservedIds . add ( cloneId ) ;
4890+ cloneIdMap . set ( sourceId , cloneId ) ;
4891+ } ) ;
4892+ const createdIds = [ ] ;
4893+ $ ( 'div.spinner.fixed' ) . show ( 'slow' ) ;
4894+ try {
4895+ for ( const sourceId of branchIds ) {
4896+ const sourceFolder = globalFolders [ sourceId ] ;
4897+ if ( ! sourceFolder || typeof sourceFolder !== 'object' ) {
4898+ continue ;
4899+ }
4900+ const rawParentId = normalizeFolderParentId ( sourceFolder ?. parentId || sourceFolder ?. parent_id || '' ) ;
4901+ const clonedParentId = sourceId === id
4902+ ? sourceParentId
4903+ : String ( cloneIdMap . get ( rawParentId ) || '' ) . trim ( ) ;
4904+ if ( sourceId !== id && ! clonedParentId ) {
4905+ throw new Error ( `Clone branch failed because parent mapping was missing for nested folder "${ sourceFolder ?. name || sourceId } ".` ) ;
4906+ }
4907+ const clonePayload = buildDockerFolderClonePayload ( sourceFolder , {
4908+ name : sourceId === id ? nextName : String ( sourceFolder ?. name || '' ) . trim ( ) || 'Folder' ,
4909+ parentId : clonedParentId
4910+ } ) ;
4911+ const cloneId = String ( cloneIdMap . get ( sourceId ) || '' ) . trim ( ) ;
4912+ if ( ! cloneId ) {
4913+ throw new Error ( `Clone branch failed because a clone id was not generated for folder "${ sourceFolder ?. name || sourceId } ".` ) ;
4914+ }
4915+ await persistDockerFolderClonePayload ( clonePayload , cloneId ) ;
4916+ createdIds . push ( cloneId ) ;
4917+ }
47694918 await $ . post ( '/plugins/folderview.plus/server/sync_order.php' , { type : 'docker' } ) . promise ( ) ;
47704919 loadlist ( ) ;
4920+ } catch ( error ) {
4921+ await rollbackClonedDockerFolders ( createdIds ) ;
4922+ throw error ;
47714923 } finally {
47724924 $ ( 'div.spinner.fixed' ) . hide ( 'slow' ) ;
47734925 }
47744926 } , {
4775- userMessage : getDockerMenuLabel ( 'clone-folder -failed' , 'Failed to clone folder .' ) ,
4927+ userMessage : getDockerMenuLabel ( 'clone-branch -failed' , 'Failed to clone branch .' ) ,
47764928 userVisible : true
47774929 } ) ;
47784930} ;
@@ -5360,13 +5512,30 @@ const addDockerFolderContext = (id) => {
53605512 action : ( evt ) => { evt . preventDefault ( ) ; editFolder ( id ) ; }
53615513 } ) ;
53625514
5515+ const cloneSubMenu = [
5516+ {
5517+ text : getDockerMenuLabel ( 'clone-folder' , 'Clone folder' ) ,
5518+ icon : 'fa-clone' ,
5519+ action : ( evt ) => {
5520+ evt . preventDefault ( ) ;
5521+ cloneDockerFolderFromMenu ( id ) ;
5522+ }
5523+ }
5524+ ] ;
5525+ if ( hasChildren ) {
5526+ cloneSubMenu . push ( {
5527+ text : getDockerMenuLabel ( 'clone-branch' , 'Clone branch' ) ,
5528+ icon : 'fa-sitemap' ,
5529+ action : ( evt ) => {
5530+ evt . preventDefault ( ) ;
5531+ cloneDockerFolderBranchFromMenu ( id ) ;
5532+ }
5533+ } ) ;
5534+ }
53635535 opts . push ( {
5364- text : getDockerMenuLabel ( 'clone-folder ' , 'Clone folder ' ) ,
5536+ text : getDockerMenuLabel ( 'clone-menu ' , 'Clone' ) ,
53655537 icon : 'fa-clone' ,
5366- action : ( evt ) => {
5367- evt . preventDefault ( ) ;
5368- cloneDockerFolderFromMenu ( id ) ;
5369- }
5538+ subMenu : cloneSubMenu
53705539 } ) ;
53715540
53725541 opts . push ( {
0 commit comments