浏览代码

Merge pull request #71 from vineeth-vk11/#9

Adding ability to undo actions
Nariman Jelveh 1 年之前
父节点
当前提交
e1c44d91c5
共有 6 个文件被更改,包括 415 次插入105 次删除
  1. 10 0
      src/UI/UIDesktop.js
  2. 1 98
      src/UI/UIItem.js
  3. 10 0
      src/UI/UIWindow.js
  4. 1 0
      src/globals.js
  5. 385 7
      src/helpers.js
  6. 8 0
      src/initgui.js

+ 10 - 0
src/UI/UIDesktop.js

@@ -726,6 +726,16 @@ async function UIDesktop(options){
                         }
                     },
                     // -------------------------------------------
+                    // Undo
+                    // -------------------------------------------
+                    {
+                        html: "Undo",
+                        disabled: actions_history.length > 0 ? false : true,
+                        onClick: function(){
+                            undo_last_action();
+                        }
+                    },
+                    // -------------------------------------------
                     // Upload Here
                     // -------------------------------------------
                     {

+ 1 - 98
src/UI/UIItem.js

@@ -627,104 +627,7 @@ function UIItem(options){
         $(el_item_name_editor).removeClass('item-name-editor-active');
 
         // Perform rename request
-        puter.fs.rename({
-            uid: options.uid === 'null' ? null : options.uid,
-            new_name: new_name,
-            excludeSocketID: window.socket.id,
-            success: async (fsentry)=>{
-                // Has the extension changed? in that case update options.sugggested_apps
-                const old_extension = path.extname(old_name); 
-                const new_extension = path.extname(new_name);
-                if(old_extension !== new_extension){
-                    suggest_apps_for_fsentry({
-                        uid: options.uid,
-                        onSuccess: function(suggested_apps){
-                            options.suggested_apps = suggested_apps;
-                        }
-                    });
-                }
-
-                // Set new item name
-                $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name`).html(html_encode(truncate_filename(new_name, TRUNCATE_LENGTH)).replaceAll(' ', ' '));
-                $(el_item_name).show();
-
-                // Hide item name editor
-                $(el_item_name_editor).hide();
-
-                // Set new icon
-                const new_icon = (options.is_dir ? window.icons['folder.svg'] : (await item_icon(fsentry)).image);
-                $(el_item_icon).find('.item-icon-icon').attr('src', new_icon);
-
-                // Set new data-name
-                options.name = new_name;
-                $(el_item).attr('data-name', html_encode(new_name));
-                $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('data-name', html_encode(new_name));
-                $(`.window-${options.uid}`).attr('data-name', html_encode(new_name));
-
-                // Set new title attribute
-                $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('title', html_encode(new_name));
-                $(`.window-${options.uid}`).attr('title', html_encode(new_name));
-
-                // Set new value for item-name-editor
-                $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name-editor`).val(html_encode(new_name));
-                $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name`).attr('title', html_encode(new_name));
-
-                // Set new data-path
-                options.path = path.join( path.dirname(options.path), options.name);
-                const new_path = options.path;
-                $(el_item).attr('data-path', new_path);
-                $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('data-path', new_path);
-                $(`.window-${options.uid}`).attr('data-path', new_path);
-
-                // Update all elements that have matching paths
-                $(`[data-path="${html_encode(old_path)}" i]`).each(function(){
-                    $(this).attr('data-path', new_path)
-                    if($(this).hasClass('window-navbar-path-dirname'))
-                        $(this).text(new_name);
-                });
-
-                // Update the paths of all elements whose paths start with old_path
-                $(`[data-path^="${html_encode(old_path) + '/'}"]`).each(function(){
-                    const new_el_path = _.replace($(this).attr('data-path'), old_path + '/', new_path+'/');
-                    $(this).attr('data-path', new_el_path);
-                });
-
-                // Update the 'Sites Cache'
-                if($(el_item).attr('data-has_website') === '1')
-                    await update_sites_cache();
-
-                // Update website_url
-                website_url = determine_website_url(new_path);
-                $(el_item).attr('data-website_url', website_url);
-
-                // Update all exact-matching windows
-                $(`.window-${options.uid}`).each(function(){
-                    update_window_path(this, options.path);
-                })
-
-                // Set new name for corresponding open windows
-                $(`.window-${options.uid} .window-head-title`).text(new_name);
-
-                // Re-sort all matching item containers
-                $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).parent('.item-container').each(function(){
-                    sort_items(this, $(el_item).closest('.item-container').attr('data-sort_by'), $(el_item).closest('.item-container').attr('data-sort_order'));
-                })
-            },
-            error: function (err){
-                // reset to old name
-                $(el_item_name).text(truncate_filename(options.name, TRUNCATE_LENGTH));
-                $(el_item_name).show();
-
-                // hide item name editor
-                $(el_item_name_editor).hide();
-                $(el_item_name_editor).val(html_encode($(el_item).attr('data-name')));
-
-                //show error
-                if(err.message){
-                    UIAlert(err.message)
-                }
-            },
-        });
+        rename_file(options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url);
     }
     
     // --------------------------------------------------------

+ 10 - 0
src/UI/UIWindow.js

@@ -1917,6 +1917,16 @@ async function UIWindow(options) {
                             }
                         },
                         // -------------------------------------------
+                        // Undo
+                        // -------------------------------------------
+                        {
+                            html: "Undo",
+                            disabled: actions_history.length > 0 ? false : true,
+                            onClick: function(){
+                                undo_last_action();
+                            }
+                        },
+                        // -------------------------------------------
                         // Upload Here
                         // -------------------------------------------
                         {

+ 1 - 0
src/globals.js

@@ -19,6 +19,7 @@
 
 window.clipboard_op = '';
 window.clipboard = [];
+window.actions_history = [];
 window.window_nav_history = {};
 window.window_nav_history_current_position = {};
 window.progress_tracker = [];

+ 385 - 7
src/helpers.js

@@ -1434,9 +1434,15 @@ window.create_folder = async(basedir, appendto_element)=>{
             overwrite: false,
             success: function (data){
                 const el_created_dir = $(appendto_element).find('.item[data-path="'+html_encode(dirname)+'/'+html_encode(data.name)+'"]');
-                if(el_created_dir.length > 0)
+                if(el_created_dir.length > 0){
                     activate_item_name_editor(el_created_dir);
 
+                    // Add action to actions_history for undo ability
+                    actions_history.push({
+                        operation: 'create_folder',
+                        data: el_created_dir
+                    });
+                }
                 clearTimeout(progwin_timeout);
 
                 // done
@@ -1472,6 +1478,12 @@ window.create_file = async(options)=>{
                 const created_file = $(appendto_element).find('.item[data-path="'+html_encode(dirname)+'/'+html_encode(data.name)+'"]');
                 if(created_file.length > 0){
                     activate_item_name_editor(created_file);
+
+                    // Add action to actions_history for undo ability
+                    actions_history.push({
+                        operation: 'create_file',
+                        data: created_file
+                    });
                 }
             }
         });
@@ -1515,6 +1527,8 @@ window.copy_clipboard_items = async function(dest_path, dest_container_element){
             progwin = await UIWindowCopyProgress({operation_id: copy_op_id});
         }, 2000);
 
+        const copied_item_paths = []
+
         for(let i=0; i<clipboard.length; i++){
             let copy_path = clipboard[i].path;
             let item_with_same_name_already_exists = true;
@@ -1523,20 +1537,24 @@ window.copy_clipboard_items = async function(dest_path, dest_container_element){
             do{
                 if(overwrite)
                     item_with_same_name_already_exists = false;
-                
+
                 // cancelled?
                 if(operation_cancelled[copy_op_id])
                     return;
 
                 // perform copy
                 try{
-                    await puter.fs.copy({
+                    let resp = await puter.fs.copy({
                             source: copy_path,
                             destination: dest_path,
                             overwrite: overwrite || overwrite_all,
                             // if user is copying an item to where its source is, change the name so there is no conflict
                             dedupeName: dest_path === path.dirname(copy_path),
                     });
+
+                    // copy new path for undo copy
+                    copied_item_paths.push(resp[0].path);
+
                     // skips next loop iteration
                     break;
                 }catch(err){
@@ -1569,6 +1587,12 @@ window.copy_clipboard_items = async function(dest_path, dest_container_element){
         }
 
         // done
+        // Add action to actions_history for undo ability
+        actions_history.push({
+            operation: 'copy',
+            data: copied_item_paths
+        });
+
         clearTimeout(progwin_timeout);
 
         let copy_duration = (Date.now() - copy_progress_window_init_ts);
@@ -1602,6 +1626,8 @@ window.copy_items = function(el_items, dest_path){
             progwin = await UIWindowCopyProgress({operation_id: copy_op_id});
         }, 2000);
 
+        const copied_item_paths = []
+
         for(let i=0; i < el_items.length; i++){
             let copy_path = $(el_items[i]).attr('data-path');
             let item_with_same_name_already_exists = true;
@@ -1615,7 +1641,7 @@ window.copy_items = function(el_items, dest_path){
                 if(operation_cancelled[copy_op_id])
                     return;
                 try{
-                    await puter.fs.copy({
+                    let resp = await puter.fs.copy({
                             source: copy_path,
                             destination: dest_path,
                             overwrite: overwrite || overwrite_all,
@@ -1623,6 +1649,9 @@ window.copy_items = function(el_items, dest_path){
                             dedupeName: dest_path === path.dirname(copy_path),
                     })
 
+                    // copy new path for undo copy
+                    copied_item_paths.push(resp[0].path);
+
                     // skips next loop iteration
                     item_with_same_name_already_exists = false;
                 }catch(err){
@@ -1658,6 +1687,12 @@ window.copy_items = function(el_items, dest_path){
         }
 
         // done
+        // Add action to actions_history for undo ability
+        actions_history.push({
+            operation: 'copy',
+            data: copied_item_paths
+        });
+
         clearTimeout(progwin_timeout);
 
         let copy_duration = (Date.now() - copy_progress_window_init_ts);
@@ -2270,7 +2305,7 @@ window.new_context_menu_item = function(dirname, append_to_element){
  * @param {string} dest_path - The destination path to move the items to
  * @returns {Promise<void>} 
  */
-window.move_items = async function(el_items, dest_path){
+window.move_items = async function(el_items, dest_path, is_undo = false){
     let move_op_id = operation_id++;
     operation_cancelled[move_op_id] = false;
 
@@ -2304,6 +2339,9 @@ window.move_items = async function(el_items, dest_path){
         progwin = await UIWindowMoveProgress({operation_id: move_op_id});
     }, 2000);
 
+    // storing moved items for undo ability
+    const moved_items = []
+
     // Go through each item and try to move it
     for(let i=0; i<el_items.length; i++){
         // get current item
@@ -2543,7 +2581,7 @@ window.move_items = async function(el_items, dest_path){
                 fsentry.name = metadata?.original_name || fsentry.name;
 
                 // create new item on matching containers
-                UIItem({
+                const options = {
                     appendTo: $(`.item-container[data-path="${html_encode(dest_path)}" i]`),
                     immutable: fsentry.immutable,
                     associated_app_name: fsentry.associated_app?.name,
@@ -2563,7 +2601,9 @@ window.move_items = async function(el_items, dest_path){
                     has_website: $(el_item).attr('data-has_website') === '1',
                     metadata: fsentry.metadata ?? '',
                     suggested_apps: fsentry.suggested_apps,
-                });
+                }
+                UIItem(options);
+                moved_items.push({'options': options, 'original_path': $(el_item).attr('data-path')});
 
                 // this operation may have created some missing directories, 
                 // see if any of the directories in the path of this file is new AND
@@ -2657,6 +2697,19 @@ window.move_items = async function(el_items, dest_path){
     // -----------------------------------------------------------------------
     // DONE! close progress window with delay to allow user to see 100% progress
     // -----------------------------------------------------------------------
+    // Add action to actions_history for undo ability
+    if(!is_undo && dest_path !== trash_path){
+        actions_history.push({
+            operation: 'move',
+            data: moved_items,
+        });
+    }else if(!is_undo && dest_path === trash_path){
+        actions_history.push({
+            operation: 'delete',
+            data: moved_items,
+        });
+    }
+
     if(progwin){
         setTimeout(() => {
             $(progwin).close();   
@@ -2936,6 +2989,20 @@ window.upload_items = async function(items, dest_path){
             // success
             success: async function(items){
                 // DONE
+                // Add action to actions_history for undo ability
+                const files = []
+                if(typeof items[Symbol.iterator] === 'function'){
+                    for(const item of items){
+                        files.push(item.path)
+                    }
+                }else{
+                    files.push(items.path)
+                }
+
+                actions_history.push({
+                    operation: 'upload',
+                    data: files
+                });
                 // close progress window after a bit of delay for a better UX
                 setTimeout(() => {
                     setTimeout(() => {
@@ -3318,3 +3385,314 @@ window.unzipItem = async function(itemPath) {
         }, Math.max(0, copy_progress_hide_delay - (Date.now() - start_ts)));
     })
 }
+
+window.rename_file = async(options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url, is_undo = false)=>{
+    puter.fs.rename({
+        uid: options.uid === 'null' ? null : options.uid,
+        new_name: new_name,
+        excludeSocketID: window.socket.id,
+        success: async (fsentry)=>{
+            // Add action to actions_history for undo ability
+            if (!is_undo)
+                actions_history.push({
+                    operation: 'rename',
+                    data: {options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url}
+                });
+            
+            // Has the extension changed? in that case update options.sugggested_apps
+            const old_extension = path.extname(old_name); 
+            const new_extension = path.extname(new_name);
+            if(old_extension !== new_extension){
+                suggest_apps_for_fsentry({
+                    uid: options.uid,
+                    onSuccess: function(suggested_apps){
+                        options.suggested_apps = suggested_apps;
+                    }
+                });
+            }
+
+            // Set new item name
+            $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name`).html(html_encode(truncate_filename(new_name, TRUNCATE_LENGTH)).replaceAll(' ', '&nbsp;'));
+            $(el_item_name).show();
+
+            // Hide item name editor
+            $(el_item_name_editor).hide();
+
+            // Set new icon
+            const new_icon = (options.is_dir ? window.icons['folder.svg'] : (await item_icon(fsentry)).image);
+            $(el_item_icon).find('.item-icon-icon').attr('src', new_icon);
+
+            // Set new data-name
+            options.name = new_name;
+            $(el_item).attr('data-name', html_encode(new_name));
+            $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('data-name', html_encode(new_name));
+            $(`.window-${options.uid}`).attr('data-name', html_encode(new_name));
+
+            // Set new title attribute
+            $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('title', html_encode(new_name));
+            $(`.window-${options.uid}`).attr('title', html_encode(new_name));
+
+            // Set new value for item-name-editor
+            $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name-editor`).val(html_encode(new_name));
+            $(`.item[data-uid='${$(el_item).attr('data-uid')}'] .item-name`).attr('title', html_encode(new_name));
+
+            // Set new data-path
+            options.path = path.join( path.dirname(options.path), options.name);
+            const new_path = options.path;
+            $(el_item).attr('data-path', new_path);
+            $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('data-path', new_path);
+            $(`.window-${options.uid}`).attr('data-path', new_path);
+
+            // Update all elements that have matching paths
+            $(`[data-path="${html_encode(old_path)}" i]`).each(function(){
+                $(this).attr('data-path', new_path)
+                if($(this).hasClass('window-navbar-path-dirname'))
+                    $(this).text(new_name);
+            });
+
+            // Update the paths of all elements whose paths start with old_path
+            $(`[data-path^="${html_encode(old_path) + '/'}"]`).each(function(){
+                const new_el_path = _.replace($(this).attr('data-path'), old_path + '/', new_path+'/');
+                $(this).attr('data-path', new_el_path);
+            });
+
+            // Update the 'Sites Cache'
+            if($(el_item).attr('data-has_website') === '1')
+                await update_sites_cache();
+
+            // Update website_url
+            website_url = determine_website_url(new_path);
+            $(el_item).attr('data-website_url', website_url);
+
+            // Update all exact-matching windows
+            $(`.window-${options.uid}`).each(function(){
+                update_window_path(this, options.path);
+            })
+
+            // Set new name for corresponding open windows
+            $(`.window-${options.uid} .window-head-title`).text(new_name);
+
+            // Re-sort all matching item containers
+            $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).parent('.item-container').each(function(){
+                sort_items(this, $(el_item).closest('.item-container').attr('data-sort_by'), $(el_item).closest('.item-container').attr('data-sort_order'));
+            })
+        },
+        error: function (err){
+            // reset to old name
+            $(el_item_name).text(truncate_filename(options.name, TRUNCATE_LENGTH));
+            $(el_item_name).show();
+
+            // hide item name editor
+            $(el_item_name_editor).hide();
+            $(el_item_name_editor).val(html_encode($(el_item).attr('data-name')));
+
+            //show error
+            if(err.message){
+                UIAlert(err.message)
+            }
+        },
+    });
+}
+
+/**
+ * Deletes the given item with path.
+ * 
+ * @param {string} path - path of the item to delete 
+ * @returns {Promise<void>}
+ */
+window.delete_item_with_path = async function(path){
+    try{
+        await puter.fs.delete({
+            paths: path,
+            descendantsOnly: false,
+            recursive: true,
+        });
+    }catch(err){
+        UIAlert(err.responseText);
+    }
+}
+
+window.undo_last_action = async()=>{
+    if (actions_history.length > 0) {
+        const last_action = actions_history.pop();
+
+        // Undo the create file action
+        if (last_action.operation === 'create_file' || last_action.operation === 'create_folder') {
+            const lastCreatedItem = last_action.data;
+            undo_create_file_or_folder(lastCreatedItem); 
+        } else if(last_action.operation === 'rename') {
+            const {options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url}  = last_action.data;
+            rename_file(options, old_name, new_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url, true); 
+        } else if(last_action.operation === 'upload') {
+            const files = last_action.data;
+            undo_upload(files);
+        } else if(last_action.operation === 'copy') {
+            const files = last_action.data;
+            undo_copy(files);
+        } else if(last_action.operation === 'move') {
+            const items = last_action.data;
+            undo_move(items);
+        } else if(last_action.operation === 'delete') {
+            const items = last_action.data;
+            undo_delete(items);
+        }
+    }
+}
+
+window.undo_create_file_or_folder = async(item)=>{
+    await window.delete_item(item);
+}
+
+window.undo_upload = async(files)=>{
+    for (const file of files) {
+        await window.delete_item_with_path(file);
+    }
+}
+
+window.undo_copy = async(files)=>{
+    for (const file of files) {
+        await window.delete_item_with_path(file);
+    }
+}
+
+window.undo_move = async(items)=>{
+    for (const item of items) {
+        const el = await get_html_element_from_options(item.options);
+        console.log(item.original_path)
+        move_items([el], path.dirname(item.original_path), true);
+    }
+}
+
+window.undo_delete = async(items)=>{
+    for (const item of items) {
+        const el = await get_html_element_from_options(item.options);
+        let metadata = $(el).attr('data-metadata') === '' ? {} : JSON.parse($(el).attr('data-metadata'))
+        move_items([el], path.dirname(metadata.original_path), true);
+    }
+}
+
+
+window.get_html_element_from_options = async function(options){
+    const item_id = global_element_id++;
+    
+    options.disabled = options.disabled ?? false;
+    options.visible = options.visible ?? 'visible'; // one of 'visible', 'revealed', 'hidden'
+    options.is_dir = options.is_dir ?? false;
+    options.is_selected = options.is_selected ?? false;
+    options.is_shared = options.is_shared ?? false;
+    options.is_shortcut = options.is_shortcut ?? 0;
+    options.is_trash = options.is_trash ?? false;
+    options.metadata = options.metadata ?? '';
+    options.multiselectable = options.multiselectable ?? true;
+    options.shortcut_to = options.shortcut_to ?? '';
+    options.shortcut_to_path = options.shortcut_to_path ?? '';
+    options.immutable = (options.immutable === false || options.immutable === 0 || options.immutable === undefined ? 0 : 1);
+    options.sort_container_after_append = (options.sort_container_after_append !== undefined ? options.sort_container_after_append : false);
+    const is_shared_with_me = (options.path !== '/'+window.user.username && !options.path.startsWith('/'+window.user.username+'/'));
+
+    let website_url = determine_website_url(options.path);
+
+    // do a quick check to see if the target parent has any file type restrictions
+    const appendto_allowed_file_types = $(options.appendTo).attr('data-allowed_file_types')
+    if(!window.check_fsentry_against_allowed_file_types_string({is_dir: options.is_dir, name:options.name, type:options.type}, appendto_allowed_file_types))
+        options.disabled = true;
+
+    // --------------------------------------------------------
+    // HTML for Item
+    // --------------------------------------------------------
+    let h = '';
+    h += `<div  id="item-${item_id}" 
+                class="item${options.is_selected ? ' item-selected':''} ${options.disabled ? 'item-disabled':''} item-${options.visible}" 
+                data-id="${item_id}" 
+                data-name="${html_encode(options.name)}" 
+                data-metadata="${html_encode(options.metadata)}" 
+                data-uid="${options.uid}" 
+                data-is_dir="${options.is_dir ? 1 : 0}" 
+                data-is_trash="${options.is_trash ? 1 : 0}"
+                data-has_website="${options.has_website ? 1 : 0 }" 
+                data-website_url = "${website_url ? html_encode(website_url) : ''}"
+                data-immutable="${options.immutable}" 
+                data-is_shortcut = "${options.is_shortcut}"
+                data-shortcut_to = "${html_encode(options.shortcut_to)}"
+                data-shortcut_to_path = "${html_encode(options.shortcut_to_path)}"
+                data-sortable = "${options.sortable ?? 'true'}"
+                data-sort_by = "${html_encode(options.sort_by) ?? 'name'}"
+                data-size = "${options.size ?? ''}"
+                data-type = "${html_encode(options.type) ?? ''}"
+                data-modified = "${options.modified ?? ''}"
+                data-associated_app_name = "${html_encode(options.associated_app_name) ?? ''}"
+                data-path="${html_encode(options.path)}">`;
+
+        // spinner
+        h += `<div class="item-spinner">`;
+        h += `</div>`;
+        // modified
+        h += `<div class="item-attr item-attr--modified">`;
+            h += `<span>${options.modified === 0 ? '-' : timeago.format(options.modified*1000)}</span>`;
+        h += `</div>`;
+        // size
+        h += `<div class="item-attr item-attr--size">`;
+            h += `<span>${options.size ? byte_format(options.size) : '-'}</span>`;
+        h += `</div>`;
+        // type
+        h += `<div class="item-attr item-attr--type">`;
+            if(options.is_dir)
+                h += `<span>Folder</span>`;
+            else
+                h += `<span>${options.type ? html_encode(options.type) : '-'}</span>`;
+        h += `</div>`;
+
+
+        // icon
+        h += `<div class="item-icon">`;
+            h += `<img src="${html_encode(options.icon.image)}" class="item-icon-${options.icon.type}" data-item-id="${item_id}">`;
+        h += `</div>`;
+        // badges
+        h += `<div class="item-badges">`;
+            // website badge
+            h += `<img  class="item-badge item-has-website-badge long-hover" 
+                        style="${options.has_website ? 'display:block;' : ''}" 
+                        src="${html_encode(window.icons['world.svg'])}" 
+                        data-item-id="${item_id}"
+                    >`;
+            // link badge
+            h += `<img  class="item-badge item-has-website-url-badge" 
+                        style="${website_url ? 'display:block;' : ''}" 
+                        src="${html_encode(window.icons['link.svg'])}" 
+                        data-item-id="${item_id}"
+                    >`;
+
+            // shared badge
+            h += `<img  class="item-badge item-badge-has-permission" 
+                        style="display: ${ is_shared_with_me ? 'block' : 'none'};
+                            background-color: #ffffff;
+                            padding: 2px;" src="${html_encode(window.icons['shared.svg'])}" 
+                        data-item-id="${item_id}"
+                        title="A user has shared this item with you.">`;
+            // owner-shared badge
+            h += `<img  class="item-badge item-is-shared" 
+                        style="background-color: #ffffff; padding: 2px; ${!is_shared_with_me && options.is_shared ? 'display:block;' : ''}" 
+                        src="${html_encode(window.icons['owner-shared.svg'])}" 
+                        data-item-id="${item_id}"
+                        data-item-uid="${options.uid}"
+                        data-item-path="${html_encode(options.path)}"
+                        title="You have shared this item with at least one other user."
+                    >`;
+            // shortcut badge
+            h += `<img  class="item-badge item-shortcut" 
+                        style="background-color: #ffffff; padding: 2px; ${options.is_shortcut !== 0 ? 'display:block;' : ''}" 
+                        src="${html_encode(window.icons['shortcut.svg'])}" 
+                        data-item-id="${item_id}"
+                        title="Shortcut"
+                    >`;
+
+        h += `</div>`;
+
+        // name
+        h += `<span class="item-name" data-item-id="${item_id}" title="${html_encode(options.name)}">${html_encode(truncate_filename(options.name, TRUNCATE_LENGTH)).replaceAll(' ', '&nbsp;')}</span>`
+        // name editor
+        h += `<textarea class="item-name-editor hide-scrollbar" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off" data-gramm_editor="false">${html_encode(options.name)}</textarea>`
+    h += `</div>`;
+
+    return h;
+}

+ 8 - 0
src/initgui.js

@@ -1677,6 +1677,14 @@ window.initgui = async function(){
             }
             return false;
         }
+        //-----------------------------------------------------------------------------
+        // Undo
+        // ctrl/command + z, will undo last action
+        //-----------------------------------------------------------------------------
+        if((e.ctrlKey || e.metaKey) && e.which === 90){
+            undo_last_action();
+            return false;
+        }
     });
 
     $(document).on('click', '.remove-permission-link', async function(e){