From 19e7ca00e9d76b01c77d9e445bc94ffdfa3f6801 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Wed, 23 Feb 2022 18:12:07 -0600 Subject: [PATCH 01/47] Bare Minimum Data Table Functionality - removed Buttons for new File, etc. --- .../app/javascript/packs/files/datatable.js | 359 +++++-- .../app/javascript/packs/files/sweet_alert.js | 35 + .../app/views/files/_inline_js.html.erb | 879 ------------------ apps/dashboard/app/views/files/index.html.erb | 29 +- .../app/views/files/index.json.jbuilder | 1 - .../app/views/layouts/files.html.erb | 2 +- 6 files changed, 312 insertions(+), 993 deletions(-) create mode 100755 apps/dashboard/app/javascript/packs/files/sweet_alert.js diff --git a/apps/dashboard/app/javascript/packs/files/datatable.js b/apps/dashboard/app/javascript/packs/files/datatable.js index 42555216e9..d3050bd0f1 100755 --- a/apps/dashboard/app/javascript/packs/files/datatable.js +++ b/apps/dashboard/app/javascript/packs/files/datatable.js @@ -1,104 +1,250 @@ -function update_datatables_status(api){ - // from "function info ( api )" of https://cdn.datatables.net/select/1.3.1/js/dataTables.select.js - let rows = api.rows( { selected: true } ).flatten().length, - page_info = api.page.info(), - msg = page_info.recordsTotal == page_info.recordsDisplay ? `Showing ${page_info.recordsDisplay} rows` : `Showing ${page_info.recordsDisplay} of ${page_info.recordsTotal} rows`; +import 'datatables.net'; +import 'datatables.net-bs4/js/dataTables.bootstrap4'; +import 'datatables.net-select'; +import 'datatables.net-select-bs4'; +import Handlebars from 'handlebars'; +import { Swal } from './sweet_alert.js'; - $('.datatables-status').html(`${msg} - ${rows} rows selected`); -} +let table = null; -var table = $('#directory-contents').on('xhr.dt', function ( e, settings, json, xhr ) { - // new ajax request for new data so update date/time - if(json && json.time){ - history.replaceState(_.merge({}, history.state, {currentDirectoryUpdatedAt: json.time}), null); - } -}).DataTable({ - autoWidth: false, - language: { - search: 'Filter:', - }, - order: [[1, "asc"], [2, "asc"]], - rowId: 'id', - paging:false, - scrollCollapse: true, - select: { - style: 'os', - className: 'selected', - toggleable: true, - // don't trigger select checkbox column as select - // if you need to omit more columns, use a "selectable" class on the columns you want to support selection - selector: 'td:not(:first-child)' - }, - // https://datatables.net/reference/option/dom - // dom: '', dataTables_info nowrap - // - // put breadcrmbs below filter!!! - dom: "<'row'<'col-sm-12'f>>" + // normally <'row'<'col-sm-6'l><'col-sm-6'f>> but we disabled pagination so l is not needed (dropdown for selecting # rows) - "<'row'<'col-sm-12'<'dt-status-bar'<'datatables-status float-right'><'transfers-status'>>>>"+ - "<'row'<'col-sm-12'tr>>", // normally this is <'row'<'col-sm-5'i><'col-sm-7'p>> but we disabled pagination so have info take whole row - columns: [ - { - data: null, - orderable: false, - defaultContent: '', - render: function(data, type, row, meta) { - var api = new $.fn.dataTable.Api( meta.settings ); - let selected = api.rows(meta.row, { selected: true }).count() > 0; - return ` ${selected ? 'checked' : ''}`; - } - }, - { data: 'type', render: (data, type, row, meta) => data == 'd' ? ' dir' : ' file' }, // type - { name: 'name', data: 'name', className: 'text-break', render: (data, type, row, meta) => `${Handlebars.escapeExpression(data)}` }, // name - { name: 'actions', orderable: false, data: null, render: (data, type, row, meta) => actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, // FIXME: pass row index or something needed for finding item - { data: 'size', - render: (data, type, row, meta) => { - return type == "display" ? row.human_size : data; - } - }, // human_size - { data: 'modified_at', render: (data, type, row, meta) => { - if(type == "display"){ - let date = new Date(data * 1000) - // Return formatted date "3/23/2021 10:52:28 AM" - return isNaN(data) ? 'Invalid Date' : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` - } - else{ - return data; - } - }}, // modified_at - { name: 'owner', data: 'owner', visible: getShowOwnerMode() }, // owner - { name: 'mode', data: 'mode', visible: getShowOwnerMode(), render: (data, type, row, meta) => { +export { actionsBtnTemplate, reportTransferTemplate, table, dataFromJsonResponse, getEmptyDirs, getFilesAndDirectoriesFromDirectory, getShowDotFiles, getShowOwnerMode, update_datatables_status }; +let actionsBtnTemplate = null; +let reportTransferTemplate = null; - // mode after base conversion is a string such as "100755" - let mode = data.toString(8) +window.dataFromJsonResponse = dataFromJsonResponse; +window.reloadTable = reloadTable; - // only care about the last 3 bits (755) - let chmodDisplay = mode.substring(mode.length - 3) +$(document).ready(function() { + + reportTransferTemplate = (function(){ + let template_str = $('#transfer-template').html(); + return Handlebars.compile(template_str); + })(); + + actionsBtnTemplate = (function(){ + let template_str = $('#actions-btn-template').html(); + return Handlebars.compile(template_str); + })(); + + table = $('#directory-contents').on('xhr.dt', function ( e, settings, json, xhr ) { + // new ajax request for new data so update date/time + // if(json && json.time){ + if(json && json.time){ + history.replaceState(_.merge({}, history.state, {currentDirectoryUpdatedAt: json.time}), null); + } + }).DataTable({ + autoWidth: false, + language: { + search: 'Filter:', + }, + order: [[1, "asc"], [2, "asc"]], + rowId: 'id', + paging:false, + scrollCollapse: true, + select: { + style: 'os', + className: 'selected', + toggleable: true, + // don't trigger select checkbox column as select + // if you need to omit more columns, use a "selectable" class on the columns you want to support selection + selector: 'td:not(:first-child)' + }, + // https://datatables.net/reference/option/dom + // dom: '', dataTables_info nowrap + // + // put breadcrmbs below filter!!! + dom: "<'row'<'col-sm-12'f>>" + // normally <'row'<'col-sm-6'l><'col-sm-6'f>> but we disabled pagination so l is not needed (dropdown for selecting # rows) + "<'row'<'col-sm-12'<'dt-status-bar'<'datatables-status float-right'><'transfers-status'>>>>"+ + "<'row'<'col-sm-12'tr>>", // normally this is <'row'<'col-sm-5'i><'col-sm-7'p>> but we disabled pagination so have info take whole row + columns: [ + { + data: null, + orderable: false, + defaultContent: '', + render: function(data, type, row, meta) { + var api = new $.fn.dataTable.Api( meta.settings ); + let selected = api.rows(meta.row, { selected: true }).count() > 0; + return ` ${selected ? 'checked' : ''}`; + } + }, + { data: 'type', render: (data, type, row, meta) => data == 'd' ? ' dir' : ' file' }, // type + { name: 'name', data: 'name', className: 'text-break', render: (data, type, row, meta) => `${Handlebars.escapeExpression(data)}` }, // name + { name: 'actions', orderable: false, data: null, render: (data, type, row, meta) => actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, // FIXME: pass row index or something needed for finding item + { data: 'size', + render: (data, type, row, meta) => { + return type == "display" ? row.human_size : data; + } + }, // human_size + { data: 'modified_at', render: (data, type, row, meta) => { + if(type == "display"){ + let date = new Date(data * 1000) + + // Return formatted date "3/23/2021 10:52:28 AM" + return isNaN(data) ? 'Invalid Date' : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` + } + else{ + return data; + } + }}, // modified_at + { name: 'owner', data: 'owner', visible: getShowOwnerMode() }, // owner + { name: 'mode', data: 'mode', visible: getShowOwnerMode(), render: (data, type, row, meta) => { + + // mode after base conversion is a string such as "100755" + let mode = data.toString(8) + + // only care about the last 3 bits (755) + let chmodDisplay = mode.substring(mode.length - 3) + + return chmodDisplay + }} // mode + ] + }); - return chmodDisplay - }} // mode - ] -}); -$.fn.dataTable.ext.search.push( + $.fn.dataTable.ext.search.push( function( settings, data, dataIndex ) { return getShowDotFiles() || ! data[2].startsWith('.'); } -) + ) + + $('#directory-contents tbody').on('keydown', 'input, a', function(e){ + if(e.key == "ArrowDown"){ + e.preventDefault(); + + // let tr = this.closest('tr').nextSibling; + let tr = $(this.closest('tr')).next('tr').get(0); + if(tr){ + tr.querySelector('input[type=checkbox]').focus(); + + // deselect if not holding shift key to work + // like native file browsers + if(! e.shiftKey){ + table.rows().deselect(); + } + + // select if moving down + table.row(tr).select(); + } + } + else if(e.key == "ArrowUp"){ + e.preventDefault(); + + let tr = $(this.closest('tr')).prev('tr').get(0); + if(tr){ + tr.querySelector('input[type=checkbox]').focus(); + + // deselect if not holding shift key to work + // like native file browsers + if(! e.shiftKey){ + table.rows().deselect(); + } + + // select if moving up + table.row(tr).select(); + } + } + }); + + table.on( 'deselect', function ( e, dt, type, indexes ) { + dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', false)); + }); + + table.on( 'select', function ( e, dt, type, indexes ) { + dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', true)); + }); + + $('#directory-contents tbody').on('click', 'tr td:first-child input[type=checkbox]', function(){ + // input checkbox checked or not + + if($(this).is(':checked')){ + // select row + table.row(this.closest('tr')).select(); + } + else{ + // deselect row + table.row(this.closest('tr')).deselect(); + } + + this.focus(); + }); -let actionsBtnTemplate = (function(){ - let template_str = $('#actions-btn-template').html(); - return Handlebars.compile(template_str); -})(); + // if only 1 selected item, do not allow to de-select + table.on('user-select', function ( e, dt, type, cell, originalEvent ) { + var selected_rows = dt.rows( { selected: true } ); -// prepend show dotfiles checkbox to search box -$('#directory-contents_filter').prepend(``) -$('#directory-contents_filter').prepend(``) + if(originalEvent.target.closest('.actions-btn-group')){ + // dont do user select event when opening or working with actions btn dropdown + e.preventDefault(); + } + else if(selected_rows.count() == 1 && cell.index().row == selected_rows.indexes()[0] ){ + // dont do user select because already selected + e.preventDefault(); + } + else{ + // row need to find the checkbox to give it the focus + cell.node().closest('tr').querySelector('input[type=checkbox]').focus(); + } + }); -table.on('draw.dtSelect.dt select.dtSelect.dt deselect.dtSelect.dt info.dt', function () { - update_datatables_status(table); }); +function dataFromJsonResponse(response){ + return new Promise((resolve, reject) => { + Promise.resolve(response) + .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) + .then(response => response.json()) + .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) + .catch((e) => reject(e)) + }); +} + + +function getEmptyDirs(entry){ + return new Promise((resolve) => { + if(entry.isFile){ + resolve([]); + } + else{ + // getFilesAndDirectoriesFromDirectory has no return value, so turn this into a promise + getFilesAndDirectoriesFromDirectory(entry.createReader(), [], function(error){ console.error(error)}, { + onSuccess: (entries) => { + if(entries.length == 0){ + // this is an empty directory + resolve([entry]); + } + else{ + Promise.all(entries.map(e => getEmptyDirs(e))).then((dirs) => resolve(_.flattenDeep(dirs))); + } + } + }) + } + }); +} + + +function getFilesAndDirectoriesFromDirectory (directoryReader, oldEntries, logDropError, { onSuccess }) { + directoryReader.readEntries( + (entries) => { + const newEntries = [...oldEntries, ...entries] + // According to the FileSystem API spec, getFilesAndDirectoriesFromDirectory() must be called until it calls the onSuccess with an empty array. + if (entries.length) { + setTimeout(() => { + getFilesAndDirectoriesFromDirectory(directoryReader, newEntries, logDropError, { onSuccess }) + }, 0) + // Done iterating this particular directory + } else { + onSuccess(newEntries) + } + }, + // Make sure we resolve on error anyway, it's fine if only one directory couldn't be parsed! + (error) => { + logDropError(error) + onSuccess(oldEntries) + } + ) +} + function getShowOwnerMode() { return localStorage.getItem('show-owner-mode') == 'true' @@ -108,22 +254,37 @@ function getShowDotFiles() { return localStorage.getItem('show-dotfiles') == 'true' } -function setShowOwnerMode(visible) { - localStorage.setItem('show-owner-mode', new Boolean(visible)); -} +function update_datatables_status(api){ + // from "function info ( api )" of https://cdn.datatables.net/select/1.3.1/js/dataTables.select.js + let rows = api.rows( { selected: true } ).flatten().length, + page_info = api.page.info(), + msg = page_info.recordsTotal == page_info.recordsDisplay ? `Showing ${page_info.recordsDisplay} rows` : `Showing ${page_info.recordsDisplay} of ${page_info.recordsTotal} rows`; -function setShowDotFiles(visible) { - localStorage.setItem('show-dotfiles', new Boolean(visible)); + $('.datatables-status').html(`${msg} - ${rows} rows selected`); } -function updateDotFileVisibility() { - table.draw(); -} +function reloadTable(url) { + var request_url = url || history.state.currentDirectoryUrl; -function updateShowOwnerModeVisibility() { - let visible = getShowOwnerMode(); + return fetch(request_url, {headers: {'Accept':'application/json'}}) + .then(response => dataFromJsonResponse(response)) + .then(function(data) { + + $('#shell-wrapper').replaceWith((data.shell_dropdown_html)) - table.column('owner:name').visible(visible); - table.column('mode:name').visible(visible); -} + table.clear(); + table.rows.add(data.files); + table.draw(); + $('#open-in-terminal-btn').attr('href', data.shell_url); + $('#open-in-terminal-btn').removeClass('disabled'); + + return Promise.resolve(data); + }) + .catch((e) => { + Swal.fire(e.message, `Error occurred when attempting to access ${request_url}`, 'error'); + + $('#open-in-terminal-btn').addClass('disabled'); + return Promise.reject(e); + }); +} diff --git a/apps/dashboard/app/javascript/packs/files/sweet_alert.js b/apps/dashboard/app/javascript/packs/files/sweet_alert.js new file mode 100755 index 0000000000..447f116d3f --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/sweet_alert.js @@ -0,0 +1,35 @@ +import Swal from 'sweetalert2' + +export { Swal, alertError, loading, doneLoading }; + +window.Swal = Swal.mixin({ + showClass: { + popup: 'swal2-noanimation', + backdrop: 'swal2-noanimation' + }, + hideClass: { + popup: '', + backdrop: '' + } +}); + +$(document).ready(function(){ + +}); + +function alertError(error_title, error_message){ + Swal.fire(error_title, error_message, 'error'); +} + +function loading(title){ + Swal.fire({ + title: title, + allowOutsideClick: false, + showConfirmButton: false, + willOpen: () => { Swal.showLoading() } + }); +} + +function doneLoading(){ + Swal.close(); +} diff --git a/apps/dashboard/app/views/files/_inline_js.html.erb b/apps/dashboard/app/views/files/_inline_js.html.erb index 483d468509..df3d5dfc3f 100755 --- a/apps/dashboard/app/views/files/_inline_js.html.erb +++ b/apps/dashboard/app/views/files/_inline_js.html.erb @@ -1,16 +1,6 @@ diff --git a/apps/dashboard/app/views/files/index.html.erb b/apps/dashboard/app/views/files/index.html.erb index f32c25d666..5e1b81de3d 100644 --- a/apps/dashboard/app/views/files/index.html.erb +++ b/apps/dashboard/app/views/files/index.html.erb @@ -1,28 +1,31 @@ <%# z-index:999 cause dropdowns are 1000 default sticky-top is 1020 https://getbootstrap.com/docs/4.6/layout/overview/#z-index %> +
<% if Configuration.files_enable_shell_button %> <%= render partial: 'shell_dropdown' %> <% end %> - - - - - - +

- <%= render partial: "favorites" %> - <%= render partial: "copy_move_popup" %> - <%= render partial: "file_action_menu" %> - <%= render partial: "default_label_error_messages" %> + <%= render partial: "favorites", :locals => { :files => @files } %> + <%= render partial: "copy_move_popup", :locals => { :files => @files } %> + <%= render partial: "file_action_menu", :locals => { :files => @files } %> + <%= render partial: "default_label_error_messages", :locals => { :files => @files } %>
- <%= render partial: "files_table" %> + <%= render partial: "files_table", :locals => { :files => @files } %>
-<%= render partial: "buttons" %> -<%= render partial: "inline_js" %> +<%= render partial: "buttons", :locals => { :files => @files } %> +<%= render partial: "inline_js", :locals => { :files => @files } %> diff --git a/apps/dashboard/app/views/files/index.json.jbuilder b/apps/dashboard/app/views/files/index.json.jbuilder index da9260e68c..4168655cc1 100644 --- a/apps/dashboard/app/views/files/index.json.jbuilder +++ b/apps/dashboard/app/views/files/index.json.jbuilder @@ -1,6 +1,5 @@ json.path @path.to_s json.url files_path(@path).to_s - #TODO: support array of shell urls, along with the default shell url which could be above json.shell_url OodAppkit.shell.url(path: @path.to_s).to_s diff --git a/apps/dashboard/app/views/layouts/files.html.erb b/apps/dashboard/app/views/layouts/files.html.erb index a0f6b54040..255c7da1b5 100644 --- a/apps/dashboard/app/views/layouts/files.html.erb +++ b/apps/dashboard/app/views/layouts/files.html.erb @@ -1,6 +1,6 @@ <% content_for :head do - javascript_pack_tag 'files/index', nonce: true + javascript_pack_tag 'files/datatable', nonce: true end %> From 4e4f714cd08e67fcc41162c89404dc2d9dd1be9a Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Mon, 28 Feb 2022 11:14:29 -0600 Subject: [PATCH 02/47] Updated code to reflect global instead of window and only where necessary --- apps/dashboard/app/javascript/packs/files/datatable.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/datatable.js b/apps/dashboard/app/javascript/packs/files/datatable.js index d3050bd0f1..2a9237424f 100755 --- a/apps/dashboard/app/javascript/packs/files/datatable.js +++ b/apps/dashboard/app/javascript/packs/files/datatable.js @@ -8,12 +8,15 @@ import { Swal } from './sweet_alert.js'; let table = null; -export { actionsBtnTemplate, reportTransferTemplate, table, dataFromJsonResponse, getEmptyDirs, getFilesAndDirectoriesFromDirectory, getShowDotFiles, getShowOwnerMode, update_datatables_status }; +export { + actionsBtnTemplate, reportTransferTemplate, table, dataFromJsonResponse, getEmptyDirs, getFilesAndDirectoriesFromDirectory, + getShowDotFiles, getShowOwnerMode, reloadTable, update_datatables_status +}; + let actionsBtnTemplate = null; let reportTransferTemplate = null; -window.dataFromJsonResponse = dataFromJsonResponse; -window.reloadTable = reloadTable; +global.reloadTable = reloadTable; // Required to be marked as global since we are using this in the template. $(document).ready(function() { From d4731877514fc5fe81e9f14d38617a1880b417c7 Mon Sep 17 00:00:00 2001 From: Gerald Byrket <88058640+gbsoftwaresolutions@users.noreply.github.com> Date: Wed, 2 Mar 2022 12:03:11 -0600 Subject: [PATCH 03/47] Feature/refactor copy path (#1876) * Completed New File/Folder functionality migration * Completed Upload Functionality * Completed Download Functionality * Completed Copy Path Functionality Co-authored-by: Gerald Byrket --- .../app/javascript/packs/files/clipboard.js | 41 +++-- .../app/javascript/packs/files/datatable.js | 7 +- .../app/javascript/packs/files/fileops.js | 146 +++++++++++++++ .../app/javascript/packs/files/sweet_alert.js | 2 +- .../app/javascript/packs/files/uppy.js | 172 ++++++++++++++++++ .../app/views/files/_buttons.html.erb | 125 +------------ .../app/views/files/_inline_js.html.erb | 5 +- apps/dashboard/app/views/files/index.html.erb | 2 +- 8 files changed, 355 insertions(+), 145 deletions(-) create mode 100755 apps/dashboard/app/javascript/packs/files/fileops.js create mode 100755 apps/dashboard/app/javascript/packs/files/uppy.js diff --git a/apps/dashboard/app/javascript/packs/files/clipboard.js b/apps/dashboard/app/javascript/packs/files/clipboard.js index 2605934432..fc5b5c5b1e 100755 --- a/apps/dashboard/app/javascript/packs/files/clipboard.js +++ b/apps/dashboard/app/javascript/packs/files/clipboard.js @@ -1,17 +1,33 @@ import ClipboardJS from 'clipboard' +import {Handlebars} from './datatable.js'; +export {ClipboardJS, clipboardjs, clearClipboard, updateClipboardFromSelection, updateViewForClipboard }; -window.ClipboardJS = ClipboardJS +global.ClipboardJS = ClipboardJS -var clipboardjs = new ClipboardJS('#copy-path'); +var clipboardjs = null; -clipboardjs.on('success', function(e) { - $(e.trigger).tooltip({title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom'}).tooltip('show'); - setTimeout(() => $(e.trigger).tooltip('hide'), 2000); - e.clearSelection(); -}); -clipboardjs.on('error', function(e) { - e.clearSelection(); +$(document).ready(function(){ + + clipboardjs = new ClipboardJS('#copy-path'); + + clipboardjs.on('success', function(e) { + //FIXME: for some reason the jQuery function tooltip is not being recognized. Will need to figure out why or move on to new tooltip plugin. + + // $(e.trigger).tooltip({title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom'}).tooltip('show'); + // setTimeout(() => $(e.trigger).tooltip('hide'), 2000); + e.clearSelection(); + }); + clipboardjs.on('error', function(e) { + e.clearSelection(); + }); + + //FIXME: so need to handle updateViewForClipboard based on EVENTS emitted by local storage modifications + updateViewForClipboard(); + global.addEventListener('storage', () => { + updateViewForClipboard(); + }); + }); function updateClipboardFromSelection(){ @@ -105,10 +121,3 @@ function updateViewForClipboard(){ } }); } - -//FIXME: so need to handle updateViewForClipboard based on EVENTS emitted by local storage modifications -updateViewForClipboard(); -window.addEventListener('storage', () => { - updateViewForClipboard(); -}); - diff --git a/apps/dashboard/app/javascript/packs/files/datatable.js b/apps/dashboard/app/javascript/packs/files/datatable.js index 2a9237424f..e867cd8be4 100755 --- a/apps/dashboard/app/javascript/packs/files/datatable.js +++ b/apps/dashboard/app/javascript/packs/files/datatable.js @@ -4,13 +4,16 @@ import 'datatables.net-select'; import 'datatables.net-select-bs4'; import Handlebars from 'handlebars'; import { Swal } from './sweet_alert.js'; +import {} from './fileops.js'; +import {} from './uppy.js'; +import {} from './clipboard.js'; let table = null; export { actionsBtnTemplate, reportTransferTemplate, table, dataFromJsonResponse, getEmptyDirs, getFilesAndDirectoriesFromDirectory, - getShowDotFiles, getShowOwnerMode, reloadTable, update_datatables_status + getShowDotFiles, getShowOwnerMode, Handlebars, reloadTable, update_datatables_status }; let actionsBtnTemplate = null; @@ -19,7 +22,7 @@ let reportTransferTemplate = null; global.reloadTable = reloadTable; // Required to be marked as global since we are using this in the template. $(document).ready(function() { - + reportTransferTemplate = (function(){ let template_str = $('#transfer-template').html(); return Handlebars.compile(template_str); diff --git a/apps/dashboard/app/javascript/packs/files/fileops.js b/apps/dashboard/app/javascript/packs/files/fileops.js new file mode 100755 index 0000000000..382c72a6b9 --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/fileops.js @@ -0,0 +1,146 @@ +import {Swal, alertError, doneLoading, loading} from './sweet_alert.js'; +import {dataFromJsonResponse, table} from './datatable.js'; + +export {getEmptyDirs, downloadDirectory, downloadFile}; + +$(document).ready(function(){ + + $('#new-file-btn').on("click", () => { + Swal.fire({ + title: 'New File', + input: 'text', + inputLabel: 'Filename', + showCancelButton: true, + inputValidator: (value) => { + if (! value ) { + // TODO: validate filenames against listing + return 'Provide a non-empty filename.' + } + else if (value.includes("/")) { + // TODO: validate filenames against listing + return 'Illegal character (/) not allowed in filename.' + } + } + }) + .then((result) => result.isConfirmed ? Promise.resolve(result.value) : Promise.reject('cancelled')) + .then((filename) => newFile(filename)); + }); + + $('#new-dir-btn').on("click", () => { + Swal.fire({ + title: 'New Directory', + input: 'text', + inputLabel: 'Directory name', + inputAttributes: { + spellcheck: 'false', + }, + showCancelButton: true, + inputValidator: (value) => { + if (! value || value.includes("/")) { + // TODO: validate filenames against listing + return 'Provide a directory name that does not have / in it' + } + }, + }) + .then((result) => result.isConfirmed ? Promise.resolve(result.value) : Promise.reject('cancelled')) + .then((filename) => newDirectory(filename)); + }); + + $('#download-btn').on("click", () => { + let selection = table.rows({ selected: true }).data(); + if(selection.length == 0) { + Swal.fire('Select a file, files, or directory to download', 'You have selected none.', 'error'); + } + selection.toArray().forEach( (f) => { + if(f.type == 'd') { + downloadDirectory(f) + } + else if(f.type == 'f') { + downloadFile(f) + } + }) + }); + +}); + +function newDirectory(filename){ + fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) + .then(response => dataFromJsonResponse(response)) + .then(() => reloadTable()) + .catch(e => alertError('Error occurred when attempting to create new directory', e.message)); +} + +function newFile(filename){ + fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?touch=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) + .then(response => dataFromJsonResponse(response)) + .then(() => reloadTable()) + .catch(e => alertError('Error occurred when attempting to create new file', e.message)); +} + +function getEmptyDirs(entry){ + return new Promise((resolve) => { + if(entry.isFile){ + resolve([]); + } + else{ + // getFilesAndDirectoriesFromDirectory has no return value, so turn this into a promise + getFilesAndDirectoriesFromDirectory(entry.createReader(), [], function(error){ console.error(error)}, { + onSuccess: (entries) => { + if(entries.length == 0){ + // this is an empty directory + resolve([entry]); + } + else{ + Promise.all(entries.map(e => getEmptyDirs(e))).then((dirs) => resolve(_.flattenDeep(dirs))); + } + } + }) + } + }); +} + +function downloadDirectory(file) { + let filename = $($.parseHTML(file.name)).text(), + canDownloadReq = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?can_download=${Date.now().toString()}` + + loading('preparing to download directory: ' + file.name) + + fetch(canDownloadReq, { + method: 'GET', + headers: { + 'X-CSRF-Token': csrf_token, + 'Accept': 'application/json' + } + }) + .then(response => dataFromJsonResponse(response)) + .then(data => { + if (data.can_download) { + doneLoading(); + downloadFile(file) + } else { + Swal.fire(dashboard_files_directory_download_error_modal_title, data.error_message, 'error') + } + }) + .catch(e => { + Swal.fire(dashboard_files_directory_download_error_modal_title, e.message, 'error') + }) +} + +function downloadFile(file) { + // creating the temporary iframe is exactly what the CloudCmd does + // so this just repeats the status quo + + let filename = $($.parseHTML(file.name)).text(), + downloadUrl = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?download=${Date.now().toString()}`, + iframe = document.createElement('iframe'), + TIME = 30 * 1000; + + iframe.setAttribute('class', 'd-none'); + iframe.setAttribute('src', downloadUrl); + + document.body.appendChild(iframe); + + setTimeout(function() { + document.body.removeChild(iframe); + }, TIME); +} diff --git a/apps/dashboard/app/javascript/packs/files/sweet_alert.js b/apps/dashboard/app/javascript/packs/files/sweet_alert.js index 447f116d3f..3c523b0d0a 100755 --- a/apps/dashboard/app/javascript/packs/files/sweet_alert.js +++ b/apps/dashboard/app/javascript/packs/files/sweet_alert.js @@ -2,7 +2,7 @@ import Swal from 'sweetalert2' export { Swal, alertError, loading, doneLoading }; -window.Swal = Swal.mixin({ +global.Swal = Swal.mixin({ showClass: { popup: 'swal2-noanimation', backdrop: 'swal2-noanimation' diff --git a/apps/dashboard/app/javascript/packs/files/uppy.js b/apps/dashboard/app/javascript/packs/files/uppy.js new file mode 100755 index 0000000000..f664d67ce3 --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/uppy.js @@ -0,0 +1,172 @@ +import { Uppy, BasePlugin } from '@uppy/core' +import Dashboard from '@uppy/dashboard' +import XHRUpload from '@uppy/xhr-upload' +import _ from 'lodash'; + +export {BasePlugin, closeAndResetUppyModal, Dashboard, uppy, Uppy, XHRUpload}; + +// Uppy = Uppy; +// BasePlugin = BasePlugin; +// Dashboard = Dashboard; +// XHRUpload = XHRUpload; + +let uppy = null; + +$(document).ready(function(){ + class EmptyDirCreator extends BasePlugin { + constructor (uppy, opts){ + super(uppy, opts) + this.id = this.opts.id || 'EmptyDirUploaderCatcher'; + this.type = 'acquirer'; + + this.empty_dirs = []; + this.last_entries = []; + + this.handleRootDrop = this.handleRootDrop.bind(this); + this.createEmptyDirs = this.createEmptyDirs.bind(this); + + this.uppy = uppy; + } + + handleRootDrop (e) { + // from https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/getDroppedFiles/index.js + if (e.dataTransfer.items && e.dataTransfer.items[0] && 'webkitGetAsEntry' in e.dataTransfer.items[0]) { + // toArray https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/toArray.js#L4 + let items = Array.prototype.slice.call(e.dataTransfer.items || [], 0); + let entries = items.map(i => i.webkitGetAsEntry()).filter(i => i); + + return Promise.all(entries.map(i => getEmptyDirs(i))).then((dirs) => { + this.empty_dirs = this.empty_dirs.concat(_.flattenDeep(dirs)); + + console.log(this.empty_dirs); + }); + } + //else we don't have access to directory information + } + + createEmptyDirs (ids) { + if(! this.uppy.getState().error){ // avoid creating empty dirs if error occurred during upload + + //TODO: error checking and reporting + return Promise.all(this.empty_dirs.map((d) => { + // "fullPath" should actually be the path relative to the current directory + let filename = _.trimStart(d.fullPath, '/'); + + return fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) + //TODO: parse json response verify if there was an error creating directory and handle error + + })).then(() => this.empty_dirs = []); + } + } + + install () { + this.uppy.addPostProcessor(this.createEmptyDirs); + } + + uninstall () { + this.uppy.removePostProcessor(this.createEmptyDirs); + } + } + + uppy = new Uppy({ + restrictions: { + maxFileSize: maxFileSize, + } + }); + + uppy.use(EmptyDirCreator); + uppy.use(Dashboard, { + trigger: '#upload-btn', + fileManagerSelectionType: 'both', + disableThumbnailGenerator: true, + showLinkToFileUploadResult: false, + closeModalOnClickOutside: true, + closeAfterFinish: true, + allowMultipleUploads: false, + onRequestCloseModal: () => closeAndResetUppyModal(uppy), + note: 'Empty directories will be included in the upload only when a directory upload is initiated via drag and drop. This is because the File and Directory Entries API is available only on a drop event, not during an input change event.' + }); + uppy.use(XHRUpload, { + endpoint: filesUploadPath, + withCredentials: true, + fieldName: 'file', + limit: 1, + headers: { 'X-CSRF-Token': csrf_token }, + timeout: 128 * 1000, + }); + + uppy.on('file-added', (file) => { + uppy.setFileMeta(file.id, { parent: history.state.currentDirectory }); + if(file.meta.relativePath == null && file.data.webkitRelativePath){ + uppy.setFileMeta(file.id, { relativePath: file.data.webkitRelativePath }); + } + }); + + uppy.on('complete', (result) => { + if(result.successful.length > 0){ + reloadTable(); + } + }); + + // https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file + window.addEventListener("dragover",function(e){ + e = e || event; + e.preventDefault(); + },false); + window.addEventListener("drop",function(e){ + e = e || event; + e.preventDefault(); + },false); + + $('#directory-contents').on('drop', function(e){ + this.classList.remove('dragover'); + console.log('File(s) dropped'); + // Prevent default behavior (Prevent file from being opened) + + // pass drop event to uppy dashboard + uppy.getPlugin('Dashboard').openModal().then(() => uppy.getPlugin('Dashboard').handleDrop(e.originalEvent)) + }); + + $('#directory-contents').on('dragover', function(e){ + this.classList.add('dragover'); + + // Prevent default behavior (Prevent file from being opened) + e.preventDefault(); + + // specifies what feedback will be shown to the user by setting the dropEffect attribute of the DataTransfer associated with the event + // too bad we can't show an indicator (no dragstart/end when dragging from OS to browser) + e.originalEvent.dataTransfer.dropEffect = 'copy'; + }); + + $('#directory-contents').on('dragleave', function(e){ + this.classList.remove('dragover'); + }); + +}); + +function closeAndResetUppyModal(uppy){ + uppy.getPlugin('Dashboard').closeModal(); + uppy.reset(); +} + +function getEmptyDirs(entry){ + return new Promise((resolve) => { + if(entry.isFile){ + resolve([]); + } + else{ + // getFilesAndDirectoriesFromDirectory has no return value, so turn this into a promise + getFilesAndDirectoriesFromDirectory(entry.createReader(), [], function(error){ console.error(error)}, { + onSuccess: (entries) => { + if(entries.length == 0){ + // this is an empty directory + resolve([entry]); + } + else{ + Promise.all(entries.map(e => getEmptyDirs(e))).then((dirs) => resolve(_.flattenDeep(dirs))); + } + } + }) + } + }); +} diff --git a/apps/dashboard/app/views/files/_buttons.html.erb b/apps/dashboard/app/views/files/_buttons.html.erb index 5d69fe120c..5d0456dcf7 100755 --- a/apps/dashboard/app/views/files/_buttons.html.erb +++ b/apps/dashboard/app/views/files/_buttons.html.erb @@ -31,130 +31,7 @@ $('#path-breadcrumbs').on('click', '#goto-btn', function(){ } }) .then((result) => result.isConfirmed ? Promise.resolve(result.value) : Promise.reject('cancelled')) - .then((pathname) => goto('<%= files_path('/') %>' + pathname)) + .then((pathname) => goto(filesPath + pathname)) }); -$('#new-file-btn').on("click", () => { - Swal.fire({ - title: 'New File', - input: 'text', - inputLabel: 'Filename', - showCancelButton: true, - inputValidator: (value) => { - if (! value ) { - // TODO: validate filenames against listing - return 'Provide a non-empty filename.' - } - else if (value.includes("/")) { - // TODO: validate filenames against listing - return 'Illegal character (/) not allowed in filename.' - } - } - }) - .then((result) => result.isConfirmed ? Promise.resolve(result.value) : Promise.reject('cancelled')) - .then((filename) => newFile(filename)); -}); - -$('#new-dir-btn').on("click", () => { - Swal.fire({ - title: 'New Directory', - input: 'text', - inputLabel: 'Directory name', - inputAttributes: { - spellcheck: 'false', - }, - showCancelButton: true, - inputValidator: (value) => { - if (! value || value.includes("/")) { - // TODO: validate filenames against listing - return 'Provide a directory name that does not have / in it' - } - }, - }) - .then((result) => result.isConfirmed ? Promise.resolve(result.value) : Promise.reject('cancelled')) - .then((filename) => newDirectory(filename)); -}); - -function downloadDirectory(file) { - let filename = $($.parseHTML(file.name)).text(), - canDownloadReq = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?can_download=${Date.now().toString()}` - - loading('preparing to download directory: ' + file.name) - - fetch(canDownloadReq, { - method: 'GET', - headers: { - 'X-CSRF-Token': csrf_token, - 'Accept': 'application/json' - } - }) - .then(response => dataFromJsonResponse(response)) - .then(data => { - if (data.can_download) { - doneLoading(); - downloadFile(file) - } else { - Swal.fire('<%= t('dashboard.files_directory_download_error_modal_title') %>', data.error_message, 'error') - } - }) - .catch(e => { - Swal.fire('<%= t('dashboard.files_directory_download_error_modal_title') %>', e.message, 'error') - }) -} - -function downloadFile(file) { - // creating the temporary iframe is exactly what the CloudCmd does - // so this just repeats the status quo - - let filename = $($.parseHTML(file.name)).text(), - downloadUrl = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?download=${Date.now().toString()}`, - iframe = document.createElement('iframe'), - TIME = 30 * 1000; - - iframe.setAttribute('class', 'd-none'); - iframe.setAttribute('src', downloadUrl); - - document.body.appendChild(iframe); - - setTimeout(function() { - document.body.removeChild(iframe); - }, TIME); -} - -$('#download-btn').on("click", () => { - let selection = table.rows({ selected: true }).data(); - if(selection.length == 0) { - Swal.fire('Select a file, files, or directory to download', 'You have selected none.', 'error'); - } - selection.toArray().forEach( (f) => { - if(f.type == 'd') { - downloadDirectory(f) - } - else if(f.type == 'f') { - downloadFile(f) - } - }) -}); - -function getEmptyDirs(entry){ - return new Promise((resolve) => { - if(entry.isFile){ - resolve([]); - } - else{ - // getFilesAndDirectoriesFromDirectory has no return value, so turn this into a promise - getFilesAndDirectoriesFromDirectory(entry.createReader(), [], function(error){ console.error(error)}, { - onSuccess: (entries) => { - if(entries.length == 0){ - // this is an empty directory - resolve([entry]); - } - else{ - Promise.all(entries.map(e => getEmptyDirs(e))).then((dirs) => resolve(_.flattenDeep(dirs))); - } - } - }) - } - }); -} \ No newline at end of file diff --git a/apps/dashboard/app/views/files/_inline_js.html.erb b/apps/dashboard/app/views/files/_inline_js.html.erb index df3d5dfc3f..b066f36d3f 100755 --- a/apps/dashboard/app/views/files/_inline_js.html.erb +++ b/apps/dashboard/app/views/files/_inline_js.html.erb @@ -1,6 +1,9 @@ \ No newline at end of file diff --git a/apps/dashboard/app/views/files/index.html.erb b/apps/dashboard/app/views/files/index.html.erb index d91f5fee55..0d74cc45e2 100644 --- a/apps/dashboard/app/views/files/index.html.erb +++ b/apps/dashboard/app/views/files/index.html.erb @@ -9,7 +9,6 @@ - @@ -26,5 +25,4 @@ <%= render partial: "files_table", :locals => { :files => @files } %> -<%= render partial: "buttons", :locals => { :files => @files } %> <%= render partial: "inline_js", :locals => { :files => @files } %> From 8589f1dac632a38c8589aa188c7738b0baa13cee Mon Sep 17 00:00:00 2001 From: Gerald Byrket <88058640+gbsoftwaresolutions@users.noreply.github.com> Date: Thu, 3 Mar 2022 10:09:43 -0600 Subject: [PATCH 08/47] Implemented refactored show Owner/Perms/Dotfiles (#1884) Co-authored-by: Gerald Byrket --- .../app/javascript/packs/files/datatable.js | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/apps/dashboard/app/javascript/packs/files/datatable.js b/apps/dashboard/app/javascript/packs/files/datatable.js index 0e33e530e6..4d5fdb2c7b 100755 --- a/apps/dashboard/app/javascript/packs/files/datatable.js +++ b/apps/dashboard/app/javascript/packs/files/datatable.js @@ -242,8 +242,48 @@ $(document).ready(function() { } }); + // prepend show dotfiles checkbox to search box + $('#directory-contents_filter').prepend(``) + $('#directory-contents_filter').prepend(``) + + + $('#show-dotfiles').on('change', () => { + let visible = $('#show-dotfiles').is(':checked'); + + setShowDotFiles(visible); + updateDotFileVisibility(); + }); + + $('#show-owner-mode').on('change', () => { + let visible = $('#show-owner-mode').is(':checked'); + + setShowOwnerMode(visible); + updateShowOwnerModeVisibility(); + }); + + }); +function setShowOwnerMode(visible) { + localStorage.setItem('show-owner-mode', new Boolean(visible)); +} + +function setShowDotFiles(visible) { + localStorage.setItem('show-dotfiles', new Boolean(visible)); +} + +function updateDotFileVisibility() { + table.draw(); +} + +function updateShowOwnerModeVisibility() { + let visible = getShowOwnerMode(); + + table.column('owner:name').visible(visible); + table.column('mode:name').visible(visible); +} + + function goto(url, pushState = true, show_processing_indicator = true) { if(url == history.state.currentDirectoryUrl) pushState = false; From 5af731d429122f04a4a3b8f0e9c12f794bbea5de Mon Sep 17 00:00:00 2001 From: Gerald Byrket <88058640+gbsoftwaresolutions@users.noreply.github.com> Date: Thu, 3 Mar 2022 11:37:54 -0600 Subject: [PATCH 09/47] Fixed ToolTip issue (#1886) Co-authored-by: Gerald Byrket --- .../app/javascript/packs/files/clipboard.js | 11 +- .../app/javascript/packs/files/datatable.js | 15 ++- .../app/javascript/packs/files/index.js | 106 ------------------ .../app/javascript/packs/files/uppy.js | 5 +- .../app/views/files/_inline_js.html.erb | 14 +++ 5 files changed, 34 insertions(+), 117 deletions(-) delete mode 100644 apps/dashboard/app/javascript/packs/files/index.js diff --git a/apps/dashboard/app/javascript/packs/files/clipboard.js b/apps/dashboard/app/javascript/packs/files/clipboard.js index dc137c8bbc..642123625e 100755 --- a/apps/dashboard/app/javascript/packs/files/clipboard.js +++ b/apps/dashboard/app/javascript/packs/files/clipboard.js @@ -11,16 +11,19 @@ $(document).ready(function(){ clipboardjs = new ClipboardJS('#copy-path'); + /* + NOTE: Had to move the following functionality to the template (_inline_js.html.erb) so the tooltip would be recognized. + clipboardjs.on('success', function(e) { - //FIXME: for some reason the jQuery function tooltip is not being recognized. Will need to figure out why or move on to new tooltip plugin. - - // $(e.trigger).tooltip({title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom'}).tooltip('show'); - // setTimeout(() => $(e.trigger).tooltip('hide'), 2000); + //FIXME: for some reason the jQuery function tooltip is not being recognized. Will need to figure out why or move on to new tooltip plugin. - gerald + $(e.trigger).popover({title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom'}).tooltip('show'); + setTimeout(() => $(e.trigger).tooltip('hide'), 2000); e.clearSelection(); }); clipboardjs.on('error', function(e) { e.clearSelection(); }); + */ //FIXME: so need to handle updateViewForClipboard based on EVENTS emitted by local storage modifications updateViewForClipboard(); diff --git a/apps/dashboard/app/javascript/packs/files/datatable.js b/apps/dashboard/app/javascript/packs/files/datatable.js index 4d5fdb2c7b..aedc812940 100755 --- a/apps/dashboard/app/javascript/packs/files/datatable.js +++ b/apps/dashboard/app/javascript/packs/files/datatable.js @@ -9,16 +9,15 @@ import {} from './uppy.js'; import {} from './clipboard.js'; let table = null; - +let onpopstate = null; +let actionsBtnTemplate = null; +let reportTransferTemplate = null; export { actionsBtnTemplate, reportTransferTemplate, table, dataFromJsonResponse, getEmptyDirs, getFilesAndDirectoriesFromDirectory, - getShowDotFiles, getShowOwnerMode, Handlebars, reloadTable, update_datatables_status + getShowDotFiles, getShowOwnerMode, Handlebars, onpopstate, reloadTable, update_datatables_status }; -let actionsBtnTemplate = null; -let reportTransferTemplate = null; - global.reloadTable = reloadTable; // Required to be marked as global since we are using this in the template. $(document).ready(function() { @@ -261,6 +260,12 @@ $(document).ready(function() { updateShowOwnerModeVisibility(); }); + onpopstate = function(event){ + // FIXME: handle edge case if state ! exist + setTimeout(() => { + goto(event.state.currentDirectoryUrl, false); + }, 0); + }; }); diff --git a/apps/dashboard/app/javascript/packs/files/index.js b/apps/dashboard/app/javascript/packs/files/index.js deleted file mode 100644 index b4b87eb321..0000000000 --- a/apps/dashboard/app/javascript/packs/files/index.js +++ /dev/null @@ -1,106 +0,0 @@ -import ClipboardJS from 'clipboard' -import Swal from 'sweetalert2' -import { Uppy, BasePlugin } from '@uppy/core' -import Dashboard from '@uppy/dashboard' -import XHRUpload from '@uppy/xhr-upload' -import Handlebars from 'handlebars'; -import _ from 'lodash'; -import 'datatables.net'; -import 'datatables.net-bs4/js/dataTables.bootstrap4'; -import 'datatables.net-select'; -import 'datatables.net-select-bs4'; - -window.ClipboardJS = ClipboardJS -window.Uppy = Uppy -window.BasePlugin = BasePlugin -window.Dashboard = Dashboard -window.XHRUpload = XHRUpload -window.Swal = Swal.mixin({ - showClass: { - popup: 'swal2-noanimation', - backdrop: 'swal2-noanimation' - }, - hideClass: { - popup: '', - backdrop: '' - } -}); -window.alertError = alertError; -window.dataFromJsonResponse = dataFromJsonResponse; -window.newFile = newFile; -window.newDirectory = newDirectory; -window.reloadTable = reloadTable; -window.goto = goto; -window.loading = loading; -window.doneLoading = doneLoading; -window.$ = $; -window.jQuery = jQuery; -window._ = _; -window.Handlebars = Handlebars; - -function alertError(error_title, error_message){ - Swal.fire(error_title, error_message, 'error'); -} - -function dataFromJsonResponse(response){ - return new Promise((resolve, reject) => { - Promise.resolve(response) - .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) - .then(response => response.json()) - .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) - .catch((e) => reject(e)) - }); -} - -function newFile(filename){ - fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?touch=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) - .then(response => dataFromJsonResponse(response)) - .then(() => reloadTable()) - .catch(e => alertError('Error occurred when attempting to create new file', e.message)); -} - -function newDirectory(filename){ - fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) - .then(response => dataFromJsonResponse(response)) - .then(() => reloadTable()) - .catch(e => alertError('Error occurred when attempting to create new directory', e.message)); -} - -function reloadTable(url){ - var request_url = url || history.state.currentDirectoryUrl; - - return fetch(request_url, {headers: {'Accept':'application/json'}}) - .then(response => dataFromJsonResponse(response)) - .then(function(data) { - $('#shell-wrapper').replaceWith((data.shell_dropdown_html)) - - table.clear(); - table.rows.add(data.files); - table.draw(); - - $('#open-in-terminal-btn').attr('href', data.shell_url); - $('#open-in-terminal-btn').removeClass('disabled'); - - return Promise.resolve(data); - }) - .catch((e) => { - Swal.fire(e.message, `Error occurred when attempting to access ${request_url}`, 'error'); - - $('#open-in-terminal-btn').addClass('disabled'); - return Promise.reject(e); - }); -} - - -function loading(title){ - Swal.fire({ - title: title, - allowOutsideClick: false, - showConfirmButton: false, - willOpen: () => { Swal.showLoading() } - }); -} - -function doneLoading(){ - Swal.close(); -} diff --git a/apps/dashboard/app/javascript/packs/files/uppy.js b/apps/dashboard/app/javascript/packs/files/uppy.js index f664d67ce3..285f96d81b 100755 --- a/apps/dashboard/app/javascript/packs/files/uppy.js +++ b/apps/dashboard/app/javascript/packs/files/uppy.js @@ -109,11 +109,12 @@ $(document).ready(function(){ }); // https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file - window.addEventListener("dragover",function(e){ + global.addEventListener("dragover",function(e){ e = e || event; e.preventDefault(); },false); - window.addEventListener("drop",function(e){ + + global.addEventListener("drop",function(e){ e = e || event; e.preventDefault(); },false); diff --git a/apps/dashboard/app/views/files/_inline_js.html.erb b/apps/dashboard/app/views/files/_inline_js.html.erb index 444b5f34e8..a247f49547 100755 --- a/apps/dashboard/app/views/files/_inline_js.html.erb +++ b/apps/dashboard/app/views/files/_inline_js.html.erb @@ -17,4 +17,18 @@ history.replaceState({ reloadTable(); <% end %> +$(document).ready(function(){ + clipboardjs = new ClipboardJS('#copy-path'); + clipboardjs.on('success', function(e) { + //FIXME: Have to put this in the template so it will recognize tooltip. + + $(e.trigger).tooltip({title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom'}).tooltip('show'); + setTimeout(() => $(e.trigger).tooltip('hide'), 2000); + e.clearSelection(); + }); + clipboardjs.on('error', function(e) { + e.clearSelection(); + }); +}); + From 916e088d6228961de76f22de4fea80843f3257af Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Tue, 15 Mar 2022 12:32:23 -0400 Subject: [PATCH 10/47] Extracted Basic Datatable and removed all dependencies from datatable.js --- .../app/javascript/packs/files/datatable.js | 322 +++++++++--------- .../app/javascript/packs/files/index.js | 32 ++ .../app/views/files/_inline_js.html.erb | 9 +- .../app/views/layouts/files.html.erb | 4 +- 4 files changed, 202 insertions(+), 165 deletions(-) create mode 100644 apps/dashboard/app/javascript/packs/files/index.js diff --git a/apps/dashboard/app/javascript/packs/files/datatable.js b/apps/dashboard/app/javascript/packs/files/datatable.js index aedc812940..37a70f0a87 100755 --- a/apps/dashboard/app/javascript/packs/files/datatable.js +++ b/apps/dashboard/app/javascript/packs/files/datatable.js @@ -1,26 +1,95 @@ +/** + * everything in this file only depends upon the Datatable. Nothing else. This could run alone without any of the files app functionality. +**/ + import 'datatables.net'; import 'datatables.net-bs4/js/dataTables.bootstrap4'; import 'datatables.net-select'; import 'datatables.net-select-bs4'; import Handlebars from 'handlebars'; -import { Swal } from './sweet_alert.js'; -import {} from './fileops.js'; -import {} from './uppy.js'; -import {} from './clipboard.js'; let table = null; let onpopstate = null; let actionsBtnTemplate = null; let reportTransferTemplate = null; -export { - actionsBtnTemplate, reportTransferTemplate, table, dataFromJsonResponse, getEmptyDirs, getFilesAndDirectoriesFromDirectory, - getShowDotFiles, getShowOwnerMode, Handlebars, onpopstate, reloadTable, update_datatables_status -}; +export { reloadTable, table, update_datatables_status }; + -global.reloadTable = reloadTable; // Required to be marked as global since we are using this in the template. +// global.reloadTable = reloadTable; // Required to be marked as global since we are using this in the template. $(document).ready(function() { + showTable(); + + // handle arrow keys. + $('#directory-contents tbody').on('keydown', 'input, a', function(e) { + if(e.key == "ArrowDown") { + e.preventDefault(); + let tr = $(this.closest('tr')).next('tr').get(0); + if(tr){ + tr.querySelector('input[type=checkbox]').focus(); + + // deselect if not holding shift key to work + // like native file browsers + if(! e.shiftKey){ + table.rows().deselect(); + } + + // select if moving down + table.row(tr).select(); + } + } else if(e.key == "ArrowUp") { + e.preventDefault(); + + let tr = $(this.closest('tr')).prev('tr').get(0); + if(tr) { + tr.querySelector('input[type=checkbox]').focus(); + + // deselect if not holding shift key to work + // like native file browsers + if(! e.shiftKey){ + table.rows().deselect(); + } + + // select if moving up + table.row(tr).select(); + } + } + }); + + // Selecting a checkbox in the table + table.on( 'select', function ( e, dt, type, indexes ) { + dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', true)); + }); + + // Deselecting a checkbox in the table + table.on( 'deselect', function ( e, dt, type, indexes ) { + dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', false)); + }); + + // Update the Table Information (Number of items selected, items pers page, pages, etc.) + table.on('draw.dtSelect.dt select.dtSelect.dt deselect.dtSelect.dt info.dt', function () { + update_datatables_status(table); + }); + + // if only 1 selected item, do not allow to de-select + table.on('user-select', function ( e, dt, type, cell, originalEvent ) { + var selected_rows = dt.rows( { selected: true } ); + + if(originalEvent.target.closest('.actions-btn-group')){ + // dont do user select event when opening or working with actions btn dropdown + e.preventDefault(); + } else if(selected_rows.count() == 1 && cell.index().row == selected_rows.indexes()[0] ){ + // dont do user select because already selected + e.preventDefault(); + } else{ + // row need to find the checkbox to give it the focus + cell.node().closest('tr').querySelector('input[type=checkbox]').focus(); + } + }); + + + reportTransferTemplate = (function(){ let template_str = $('#transfer-template').html(); @@ -32,80 +101,6 @@ $(document).ready(function() { return Handlebars.compile(template_str); })(); - table = $('#directory-contents').on('xhr.dt', function ( e, settings, json, xhr ) { - // new ajax request for new data so update date/time - // if(json && json.time){ - if(json && json.time){ - history.replaceState(_.merge({}, history.state, {currentDirectoryUpdatedAt: json.time}), null); - } - }).DataTable({ - autoWidth: false, - language: { - search: 'Filter:', - }, - order: [[1, "asc"], [2, "asc"]], - rowId: 'id', - paging:false, - scrollCollapse: true, - select: { - style: 'os', - className: 'selected', - toggleable: true, - // don't trigger select checkbox column as select - // if you need to omit more columns, use a "selectable" class on the columns you want to support selection - selector: 'td:not(:first-child)' - }, - // https://datatables.net/reference/option/dom - // dom: '', dataTables_info nowrap - // - // put breadcrmbs below filter!!! - dom: "<'row'<'col-sm-12'f>>" + // normally <'row'<'col-sm-6'l><'col-sm-6'f>> but we disabled pagination so l is not needed (dropdown for selecting # rows) - "<'row'<'col-sm-12'<'dt-status-bar'<'datatables-status float-right'><'transfers-status'>>>>"+ - "<'row'<'col-sm-12'tr>>", // normally this is <'row'<'col-sm-5'i><'col-sm-7'p>> but we disabled pagination so have info take whole row - columns: [ - { - data: null, - orderable: false, - defaultContent: '', - render: function(data, type, row, meta) { - var api = new $.fn.dataTable.Api( meta.settings ); - let selected = api.rows(meta.row, { selected: true }).count() > 0; - return ` ${selected ? 'checked' : ''}`; - } - }, - { data: 'type', render: (data, type, row, meta) => data == 'd' ? ' dir' : ' file' }, // type - { name: 'name', data: 'name', className: 'text-break', render: (data, type, row, meta) => `${Handlebars.escapeExpression(data)}` }, // name - { name: 'actions', orderable: false, data: null, render: (data, type, row, meta) => actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, // FIXME: pass row index or something needed for finding item - { data: 'size', - render: (data, type, row, meta) => { - return type == "display" ? row.human_size : data; - } - }, // human_size - { data: 'modified_at', render: (data, type, row, meta) => { - if(type == "display"){ - let date = new Date(data * 1000) - - // Return formatted date "3/23/2021 10:52:28 AM" - return isNaN(data) ? 'Invalid Date' : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` - } - else{ - return data; - } - }}, // modified_at - { name: 'owner', data: 'owner', visible: getShowOwnerMode() }, // owner - { name: 'mode', data: 'mode', visible: getShowOwnerMode(), render: (data, type, row, meta) => { - - // mode after base conversion is a string such as "100755" - let mode = data.toString(8) - - // only care about the last 3 bits (755) - let chmodDisplay = mode.substring(mode.length - 3) - - return chmodDisplay - }} // mode - ] - }); - $.fn.dataTable.ext.search.push( function( settings, data, dataIndex ) { @@ -113,55 +108,7 @@ $(document).ready(function() { } ) - $('#directory-contents tbody').on('keydown', 'input, a', function(e){ - if(e.key == "ArrowDown"){ - e.preventDefault(); - - // let tr = this.closest('tr').nextSibling; - let tr = $(this.closest('tr')).next('tr').get(0); - if(tr){ - tr.querySelector('input[type=checkbox]').focus(); - - // deselect if not holding shift key to work - // like native file browsers - if(! e.shiftKey){ - table.rows().deselect(); - } - - // select if moving down - table.row(tr).select(); - } - } - else if(e.key == "ArrowUp"){ - e.preventDefault(); - - let tr = $(this.closest('tr')).prev('tr').get(0); - if(tr){ - tr.querySelector('input[type=checkbox]').focus(); - - // deselect if not holding shift key to work - // like native file browsers - if(! e.shiftKey){ - table.rows().deselect(); - } - - // select if moving up - table.row(tr).select(); - } - } - }); - table.on( 'deselect', function ( e, dt, type, indexes ) { - dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', false)); - }); - - table.on( 'select', function ( e, dt, type, indexes ) { - dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', true)); - }); - - table.on('draw.dtSelect.dt select.dtSelect.dt deselect.dtSelect.dt info.dt', function () { - update_datatables_status(table); - }); $('#directory-contents tbody').on('click', 'tr td:first-child input[type=checkbox]', function(){ // input checkbox checked or not @@ -178,23 +125,6 @@ $(document).ready(function() { this.focus(); }); - // if only 1 selected item, do not allow to de-select - table.on('user-select', function ( e, dt, type, cell, originalEvent ) { - var selected_rows = dt.rows( { selected: true } ); - - if(originalEvent.target.closest('.actions-btn-group')){ - // dont do user select event when opening or working with actions btn dropdown - e.preventDefault(); - } - else if(selected_rows.count() == 1 && cell.index().row == selected_rows.indexes()[0] ){ - // dont do user select because already selected - e.preventDefault(); - } - else{ - // row need to find the checkbox to give it the focus - cell.node().closest('tr').querySelector('input[type=checkbox]').focus(); - } - }); $('#show-dotfiles').on('change', () => { let visible = $('#show-dotfiles').is(':checked'); @@ -385,15 +315,6 @@ function getFilesAndDirectoriesFromDirectory (directoryReader, oldEntries, logDr ) } - -function getShowOwnerMode() { - return localStorage.getItem('show-owner-mode') == 'true' -} - -function getShowDotFiles() { - return localStorage.getItem('show-dotfiles') == 'true' -} - function update_datatables_status(api){ // from "function info ( api )" of https://cdn.datatables.net/select/1.3.1/js/dataTables.select.js let rows = api.rows( { selected: true } ).flatten().length, @@ -428,3 +349,90 @@ function reloadTable(url) { return Promise.reject(e); }); } + + +function showTable(tableOptions) +{ + + table = $('#directory-contents').on('xhr.dt', function ( e, settings, json, xhr ) { + // new ajax request for new data so update date/time + // if(json && json.time){ + if(json && json.time){ + history.replaceState(_.merge({}, history.state, {currentDirectoryUpdatedAt: json.time}), null); + } + }).DataTable({ + autoWidth: false, + language: { + search: 'Filter:', + }, + order: [[1, "asc"], [2, "asc"]], + rowId: 'id', + paging:false, + scrollCollapse: true, + select: { + style: 'os', + className: 'selected', + toggleable: true, + // don't trigger select checkbox column as select + // if you need to omit more columns, use a "selectable" class on the columns you want to support selection + selector: 'td:not(:first-child)' + }, + // https://datatables.net/reference/option/dom + // dom: '', dataTables_info nowrap + // + // put breadcrmbs below filter!!! + dom: "<'row'<'col-sm-12'f>>" + // normally <'row'<'col-sm-6'l><'col-sm-6'f>> but we disabled pagination so l is not needed (dropdown for selecting # rows) + "<'row'<'col-sm-12'<'dt-status-bar'<'datatables-status float-right'><'transfers-status'>>>>"+ + "<'row'<'col-sm-12'tr>>", // normally this is <'row'<'col-sm-5'i><'col-sm-7'p>> but we disabled pagination so have info take whole row + columns: [ + { + data: null, + orderable: false, + defaultContent: '', + render: function(data, type, row, meta) { + var api = new $.fn.dataTable.Api( meta.settings ); + let selected = api.rows(meta.row, { selected: true }).count() > 0; + return ` ${selected ? 'checked' : ''}`; + } + }, + { data: 'type', render: (data, type, row, meta) => data == 'd' ? ' dir' : ' file' }, // type + { name: 'name', data: 'name', className: 'text-break', render: (data, type, row, meta) => `${Handlebars.escapeExpression(data)}` }, // name + { name: 'actions', orderable: false, data: null, render: (data, type, row, meta) => actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, // FIXME: pass row index or something needed for finding item + { data: 'size', + render: (data, type, row, meta) => { + return type == "display" ? row.human_size : data; + } + }, // human_size + { data: 'modified_at', render: (data, type, row, meta) => { + if(type == "display"){ + let date = new Date(data * 1000) + + // Return formatted date "3/23/2021 10:52:28 AM" + return isNaN(data) ? 'Invalid Date' : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` + } + else{ + return data; + } + }}, // modified_at + { name: 'owner', data: 'owner', visible: getShowOwnerMode() }, // owner + { name: 'mode', data: 'mode', visible: getShowOwnerMode(), render: (data, type, row, meta) => { + + // mode after base conversion is a string such as "100755" + let mode = data.toString(8) + + // only care about the last 3 bits (755) + let chmodDisplay = mode.substring(mode.length - 3) + + return chmodDisplay + }} // mode + ] + }); +} + +function getShowOwnerMode() { + return localStorage.getItem('show-owner-mode') == 'true' +} + +function getShowDotFiles() { + return localStorage.getItem('show-dotfiles') == 'true' +} \ No newline at end of file diff --git a/apps/dashboard/app/javascript/packs/files/index.js b/apps/dashboard/app/javascript/packs/files/index.js new file mode 100644 index 0000000000..8ab0119b6c --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/index.js @@ -0,0 +1,32 @@ +import { Swal } from './sweet_alert.js'; +import { reloadTable } from './datatable.js'; +/* +import {} from './fileops.js'; +import {} from './uppy.js'; +import {} from './clipboard.js'; +*/ + +export { Swal }; + +$(document).ready(function() { + /* + BEGIN Basic table functionality. + This section is the minimum functionality required to show the table. + + var tableOptions = { + 'functionality': [ + 'download', 'upload', 'terminal', 'new-file','new-directory','copy-move','delete' + ], + }; + + */ + + if(! alert) { + reloadTable(); + } + // Options represents the functionality of the Files App that you want to include. + + /* END Basic table functionality. */ + +}); + \ No newline at end of file diff --git a/apps/dashboard/app/views/files/_inline_js.html.erb b/apps/dashboard/app/views/files/_inline_js.html.erb index a247f49547..8af291d1cf 100755 --- a/apps/dashboard/app/views/files/_inline_js.html.erb +++ b/apps/dashboard/app/views/files/_inline_js.html.erb @@ -5,6 +5,7 @@ const filesPath = '<%= files_path('/') %>'; const maxFileSize = '<%= Configuration.file_upload_max %>'; const filesUploadPath = '<%= files_upload_path %>'; const transfersPath = '<%= transfers_path(format: "json") %>'; +const alert = '<%= alert %>'; history.replaceState({ currentDirectory: '<%= @path %>', @@ -12,11 +13,7 @@ history.replaceState({ currentDirectoryUpdatedAt: '<%= Time.now.to_i %>' }, null); -<% unless alert %> -// initial data load -reloadTable(); -<% end %> - +/* $(document).ready(function(){ clipboardjs = new ClipboardJS('#copy-path'); clipboardjs.on('success', function(e) { @@ -30,5 +27,5 @@ $(document).ready(function(){ e.clearSelection(); }); }); - +*/ diff --git a/apps/dashboard/app/views/layouts/files.html.erb b/apps/dashboard/app/views/layouts/files.html.erb index 255c7da1b5..e19e7c0cce 100644 --- a/apps/dashboard/app/views/layouts/files.html.erb +++ b/apps/dashboard/app/views/layouts/files.html.erb @@ -1,7 +1,7 @@ <% content_for :head do - javascript_pack_tag 'files/datatable', nonce: true + javascript_pack_tag 'files/index', nonce: true end %> -<%= render template: "layouts/application", locals: { layout_container_class: 'container-fluid' } %> +<%= render template: "layouts/application", locals: { layout_container_class: 'container-fluid' } %> \ No newline at end of file From 238ce5f27e271eec2dd02b3923274dace0850c4a Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Thu, 17 Mar 2022 17:33:31 -0400 Subject: [PATCH 11/47] Completed Create File Functionality --- .../app/javascript/packs/files/DataTable.js | 140 ++++++++++++++++++ .../app/javascript/packs/files/FileOps.js | 66 +++++++++ .../app/javascript/packs/files/index.js | 58 ++++++-- 3 files changed, 250 insertions(+), 14 deletions(-) create mode 100644 apps/dashboard/app/javascript/packs/files/DataTable.js create mode 100644 apps/dashboard/app/javascript/packs/files/FileOps.js diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js new file mode 100644 index 0000000000..4abba274ab --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -0,0 +1,140 @@ +import 'datatables.net'; +import 'datatables.net-bs4/js/dataTables.bootstrap4'; +import 'datatables.net-select'; +import 'datatables.net-select-bs4'; +import Handlebars from 'handlebars'; + +export { DataTable }; + +class DataTable { + _table = null; + + constructor() { + this.loadDataTable(); + } + + getTable() { + return this._table; + } + + loadDataTable() { + this._table = $('#directory-contents').on('xhr.dt', function ( e, settings, json, xhr ) { + // new ajax request for new data so update date/time + // if(json && json.time){ + if(json && json.time){ + history.replaceState(_.merge({}, history.state, {currentDirectoryUpdatedAt: json.time}), null); + } + }).DataTable({ + autoWidth: false, + language: { + search: 'Filter:', + }, + order: [[1, "asc"], [2, "asc"]], + rowId: 'id', + paging:false, + scrollCollapse: true, + select: { + style: 'os', + className: 'selected', + toggleable: true, + // don't trigger select checkbox column as select + // if you need to omit more columns, use a "selectable" class on the columns you want to support selection + selector: 'td:not(:first-child)' + }, + // https://datatables.net/reference/option/dom + // dom: '', dataTables_info nowrap + // + // put breadcrmbs below filter!!! + dom: "<'row'<'col-sm-12'f>>" + // normally <'row'<'col-sm-6'l><'col-sm-6'f>> but we disabled pagination so l is not needed (dropdown for selecting # rows) + "<'row'<'col-sm-12'<'dt-status-bar'<'datatables-status float-right'><'transfers-status'>>>>"+ + "<'row'<'col-sm-12'tr>>", // normally this is <'row'<'col-sm-5'i><'col-sm-7'p>> but we disabled pagination so have info take whole row + columns: [ + { + data: null, + orderable: false, + defaultContent: '', + render: function(data, type, row, meta) { + var api = new $.fn.dataTable.Api( meta.settings ); + let selected = api.rows(meta.row, { selected: true }).count() > 0; + return ` ${selected ? 'checked' : ''}`; + } + }, + { data: 'type', render: (data, type, row, meta) => data == 'd' ? ' dir' : ' file' }, // type + { name: 'name', data: 'name', className: 'text-break', render: (data, type, row, meta) => `${Handlebars.escapeExpression(data)}` }, // name + { name: 'actions', orderable: false, data: null, render: (data, type, row, meta) => this.actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, // FIXME: pass row index or something needed for finding item + { data: 'size', + render: (data, type, row, meta) => { + return type == "display" ? row.human_size : data; + } + }, // human_size + { data: 'modified_at', render: (data, type, row, meta) => { + if(type == "display"){ + let date = new Date(data * 1000) + + // Return formatted date "3/23/2021 10:52:28 AM" + return isNaN(data) ? 'Invalid Date' : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` + } + else{ + return data; + } + }}, // modified_at + { name: 'owner', data: 'owner', visible: this.getShowOwnerMode() }, // owner + { name: 'mode', data: 'mode', visible: this.getShowOwnerMode(), render: (data, type, row, meta) => { + + // mode after base conversion is a string such as "100755" + let mode = data.toString(8) + + // only care about the last 3 bits (755) + let chmodDisplay = mode.substring(mode.length - 3) + + return chmodDisplay + }} // mode + ] + }); + + } + + async reloadTable(url) { + var request_url = url || history.state.currentDirectoryUrl; + + try { + const response = await fetch(request_url, { headers: { 'Accept': 'application/json' } }); + const data = await this.dataFromJsonResponse(response); + $('#shell-wrapper').replaceWith((data.shell_dropdown_html)); + this._table.clear(); + this._table.rows.add(data.files); + this._table.draw(); + + $('#open-in-terminal-btn').attr('href', data.shell_url); + $('#open-in-terminal-btn').removeClass('disabled'); + return await Promise.resolve(data); + } catch (e) { + Swal.fire(e.message, `Error occurred when attempting to access ${request_url}`, 'error'); + + $('#open-in-terminal-btn').addClass('disabled'); + return await Promise.reject(e); + } + } + + getShowOwnerMode() { + return localStorage.getItem('show-owner-mode') == 'true' + } + + dataFromJsonResponse(response){ + var results = new Promise((resolve, reject) => { + Promise.resolve(response) + .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) + .then(response => response.json()) + .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) + .catch((e) => reject(e)) + }); + + return results; + } + + actionsBtnTemplate () { + let template_str = $('#actions-btn-template').html(); + return Handlebars.compile(template_str); + } + +} \ No newline at end of file diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js new file mode 100644 index 0000000000..9cdb98028c --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/FileOps.js @@ -0,0 +1,66 @@ +import {triggerReloadTable, Swal } from './index.js'; +import {alertError} from './sweet_alert.js'; + +export { FileOps }; + +class FileOps { + _table = null; + _swal = Swal; + + constructor(table) { + this._table = table; + } + + newFilePrompt() { + this._swal.fire({ + title: 'New File', + input: 'text', + inputLabel: 'Filename', + showCancelButton: true, + inputValidator: (value) => { + if (! value ) { + // TODO: validate filenames against listing + return 'Provide a non-empty filename.' + } + else if (value.includes("/")) { + // TODO: validate filenames against listing + return 'Illegal character (/) not allowed in filename.' + } + } + }) + .then (function(result){ + if(result.isConfirmed) { + const eventData = { + type: "createFile", + fileName: result.value + }; + + $("#directory-contents").trigger('table_request', eventData); + + } else { + triggerReloadTable(); + } + }) + .then((filename) => this.newFile(filename)); + } + + newFile(filename) { + fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?touch=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) + .then( function(response) { + const eventData = { + type: "getDataFromJsonResponse", + response: response + }; + + $("#directory-contents").trigger('table_request', eventData); + + }) + .then( function() { + $("#directory-contents").trigger('table_request', {type: "reloadTable" }); + }) + .catch( function(e) { + alertError('Error occurred when attempting to create new file', e.message); + }); + } + +} \ No newline at end of file diff --git a/apps/dashboard/app/javascript/packs/files/index.js b/apps/dashboard/app/javascript/packs/files/index.js index 8ab0119b6c..0317227745 100644 --- a/apps/dashboard/app/javascript/packs/files/index.js +++ b/apps/dashboard/app/javascript/packs/files/index.js @@ -1,32 +1,62 @@ import { Swal } from './sweet_alert.js'; -import { reloadTable } from './datatable.js'; +import { DataTable } from './DataTable.js'; +import { FileOps } from './FileOps.js'; /* import {} from './fileops.js'; import {} from './uppy.js'; import {} from './clipboard.js'; */ -export { Swal }; +export { Swal, triggerReloadTable }; + $(document).ready(function() { - /* - BEGIN Basic table functionality. - This section is the minimum functionality required to show the table. - - var tableOptions = { - 'functionality': [ - 'download', 'upload', 'terminal', 'new-file','new-directory','copy-move','delete' - ], + let dataTable = new DataTable(); + let fileOps = null; + + $("#new-file-btn").on("click", function() { + const eventData = { + type: "newFile" }; - */ + $("#directory-contents").trigger('table_request', eventData); + }); - if(! alert) { - reloadTable(); + if( ! alert ) { + dataTable.reloadTable(); } + + $("#directory-contents").on("table_request", function(event, options) { + switch (options.type) { + case 'reloadTable': + dataTable.reloadTable(); + break; + case 'newFile': + fileOps = new FileOps(dataTable); + fileOps.newFilePrompt(); + break; + case 'createFile': + fileOps = new FileOps(dataTable); + fileOps.newFile(options.fileName); + triggerReloadTable(); + break; + case 'getDataFromJsonResponse': + dataTable.dataFromJsonResponse(options.response); + break; + default: + triggerReloadTable(); + break; + } + + + }); + // Options represents the functionality of the Files App that you want to include. /* END Basic table functionality. */ }); - \ No newline at end of file + +function triggerReloadTable() { + $("#directory-contents").trigger('table-request', { type: 'reloadTable' }); +} From fa04216641ae971f5bdebbb2f51780eeaea0ebaf Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Mon, 21 Mar 2022 14:56:21 -0400 Subject: [PATCH 12/47] Completed New File with Pub/Sub --- .../app/javascript/packs/files/DataTable.js | 157 ++++--- .../app/javascript/packs/files/FileOps.js | 124 ++--- .../app/javascript/packs/files/SweetAlert.js | 72 +++ .../app/javascript/packs/files/clipboard.js | 74 --- .../app/javascript/packs/files/datatable.js | 438 ------------------ .../app/javascript/packs/files/fileops.js | 377 --------------- .../app/javascript/packs/files/index.js | 62 --- .../app/javascript/packs/files/sweet_alert.js | 35 -- .../app/javascript/packs/files/uppy.js | 173 ------- .../app/views/layouts/files.html.erb | 2 +- 10 files changed, 228 insertions(+), 1286 deletions(-) create mode 100644 apps/dashboard/app/javascript/packs/files/SweetAlert.js delete mode 100755 apps/dashboard/app/javascript/packs/files/clipboard.js delete mode 100755 apps/dashboard/app/javascript/packs/files/datatable.js delete mode 100755 apps/dashboard/app/javascript/packs/files/fileops.js delete mode 100644 apps/dashboard/app/javascript/packs/files/index.js delete mode 100755 apps/dashboard/app/javascript/packs/files/sweet_alert.js delete mode 100755 apps/dashboard/app/javascript/packs/files/uppy.js diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js index 4abba274ab..a3ff366881 100644 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -3,14 +3,31 @@ import 'datatables.net-bs4/js/dataTables.bootstrap4'; import 'datatables.net-select'; import 'datatables.net-select-bs4'; import Handlebars from 'handlebars'; +import {} from './SweetAlert.js'; +import {} from './FileOps.js'; -export { DataTable }; -class DataTable { +let table = null; +$(document).ready(function () { + table = new DataTable(); + + // listen for new file button click then trigger new file functionality. + $("#new-file-btn").on("click", function () { + $("#directory-contents").trigger('newFile'); + }); + + $("#directory-contents").on("reloadTable", function () { + table.reloadTable(); + }); + +}); + +class DataTable { _table = null; constructor() { this.loadDataTable(); + this.reloadTable(); } getTable() { @@ -18,20 +35,20 @@ class DataTable { } loadDataTable() { - this._table = $('#directory-contents').on('xhr.dt', function ( e, settings, json, xhr ) { + this._table = $('#directory-contents').on('xhr.dt', function (e, settings, json, xhr) { // new ajax request for new data so update date/time // if(json && json.time){ - if(json && json.time){ - history.replaceState(_.merge({}, history.state, {currentDirectoryUpdatedAt: json.time}), null); + if (json && json.time) { + history.replaceState(_.merge({}, history.state, { currentDirectoryUpdatedAt: json.time }), null); } - }).DataTable({ + }).DataTable({ autoWidth: false, language: { search: 'Filter:', }, order: [[1, "asc"], [2, "asc"]], rowId: 'id', - paging:false, + paging: false, scrollCollapse: true, select: { style: 'os', @@ -46,57 +63,62 @@ class DataTable { // // put breadcrmbs below filter!!! dom: "<'row'<'col-sm-12'f>>" + // normally <'row'<'col-sm-6'l><'col-sm-6'f>> but we disabled pagination so l is not needed (dropdown for selecting # rows) - "<'row'<'col-sm-12'<'dt-status-bar'<'datatables-status float-right'><'transfers-status'>>>>"+ + "<'row'<'col-sm-12'<'dt-status-bar'<'datatables-status float-right'><'transfers-status'>>>>" + "<'row'<'col-sm-12'tr>>", // normally this is <'row'<'col-sm-5'i><'col-sm-7'p>> but we disabled pagination so have info take whole row columns: [ { - data: null, - orderable: false, - defaultContent: '', - render: function(data, type, row, meta) { - var api = new $.fn.dataTable.Api( meta.settings ); - let selected = api.rows(meta.row, { selected: true }).count() > 0; - return ` ${selected ? 'checked' : ''}`; - } + data: null, + orderable: false, + defaultContent: '', + render: function (data, type, row, meta) { + var api = new $.fn.dataTable.Api(meta.settings); + let selected = api.rows(meta.row, { selected: true }).count() > 0; + return ` ${selected ? 'checked' : ''}`; + } }, { data: 'type', render: (data, type, row, meta) => data == 'd' ? ' dir' : ' file' }, // type - { name: 'name', data: 'name', className: 'text-break', render: (data, type, row, meta) => `${Handlebars.escapeExpression(data)}` }, // name - { name: 'actions', orderable: false, data: null, render: (data, type, row, meta) => this.actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, // FIXME: pass row index or something needed for finding item - { data: 'size', - render: (data, type, row, meta) => { - return type == "display" ? row.human_size : data; - } + { name: 'name', data: 'name', className: 'text-break', render: (data, type, row, meta) => `${Handlebars.escapeExpression(data)}` }, // name + { name: 'actions', orderable: false, data: null, render: (data, type, row, meta) => this.actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, // FIXME: pass row index or something needed for finding item + { + data: 'size', + render: (data, type, row, meta) => { + return type == "display" ? row.human_size : data; + } }, // human_size - { data: 'modified_at', render: (data, type, row, meta) => { - if(type == "display"){ - let date = new Date(data * 1000) - - // Return formatted date "3/23/2021 10:52:28 AM" - return isNaN(data) ? 'Invalid Date' : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` - } - else{ - return data; - } - }}, // modified_at + { + data: 'modified_at', render: (data, type, row, meta) => { + if (type == "display") { + let date = new Date(data * 1000) + + // Return formatted date "3/23/2021 10:52:28 AM" + return isNaN(data) ? 'Invalid Date' : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` + } + else { + return data; + } + } + }, // modified_at { name: 'owner', data: 'owner', visible: this.getShowOwnerMode() }, // owner - { name: 'mode', data: 'mode', visible: this.getShowOwnerMode(), render: (data, type, row, meta) => { - - // mode after base conversion is a string such as "100755" - let mode = data.toString(8) - - // only care about the last 3 bits (755) - let chmodDisplay = mode.substring(mode.length - 3) - - return chmodDisplay - }} // mode + { + name: 'mode', data: 'mode', visible: this.getShowOwnerMode(), render: (data, type, row, meta) => { + + // mode after base conversion is a string such as "100755" + let mode = data.toString(8) + + // only care about the last 3 bits (755) + let chmodDisplay = mode.substring(mode.length - 3) + + return chmodDisplay + } + } // mode ] - }); - + }); + } async reloadTable(url) { var request_url = url || history.state.currentDirectoryUrl; - + try { const response = await fetch(request_url, { headers: { 'Accept': 'application/json' } }); const data = await this.dataFromJsonResponse(response); @@ -109,32 +131,35 @@ class DataTable { $('#open-in-terminal-btn').removeClass('disabled'); return await Promise.resolve(data); } catch (e) { - Swal.fire(e.message, `Error occurred when attempting to access ${request_url}`, 'error'); + const eventData = { + 'title': `Error occurred when attempting to access ${request_url}`, + 'message': e.message, + }; + + $("#directory-contents").trigger('swalShowError', eventData); $('#open-in-terminal-btn').addClass('disabled'); return await Promise.reject(e); } - } + } - getShowOwnerMode() { + getShowOwnerMode() { return localStorage.getItem('show-owner-mode') == 'true' - } - - dataFromJsonResponse(response){ - var results = new Promise((resolve, reject) => { - Promise.resolve(response) - .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) - .then(response => response.json()) - .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) - .catch((e) => reject(e)) + } + + dataFromJsonResponse(response) { + return new Promise((resolve, reject) => { + Promise.resolve(response) + .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) + .then(response => response.json()) + .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) + .catch((e) => reject(e)) }); + } - return results; - } - - actionsBtnTemplate () { - let template_str = $('#actions-btn-template').html(); + actionsBtnTemplate() { + let template_str = $('#actions-btn-template').html(); return Handlebars.compile(template_str); - } - -} \ No newline at end of file + } + +} diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js index 9cdb98028c..6ec3e42d11 100644 --- a/apps/dashboard/app/javascript/packs/files/FileOps.js +++ b/apps/dashboard/app/javascript/packs/files/FileOps.js @@ -1,66 +1,70 @@ -import {triggerReloadTable, Swal } from './index.js'; -import {alertError} from './sweet_alert.js'; +let fileOps = null; -export { FileOps }; +$(document).ready(function () { + fileOps = new FileOps(); + $("#directory-contents").on("newFile", function () { + fileOps.newFilePrompt(); + }); -class FileOps { - _table = null; - _swal = Swal; + $("#directory-contents").on("fileOpsCreateFile", function (e, options) { + fileOps.newFile(options.value); + }); - constructor(table) { - this._table = table; - } - - newFilePrompt() { - this._swal.fire({ - title: 'New File', - input: 'text', - inputLabel: 'Filename', - showCancelButton: true, - inputValidator: (value) => { - if (! value ) { - // TODO: validate filenames against listing - return 'Provide a non-empty filename.' - } - else if (value.includes("/")) { - // TODO: validate filenames against listing - return 'Illegal character (/) not allowed in filename.' - } - } - }) - .then (function(result){ - if(result.isConfirmed) { - const eventData = { - type: "createFile", - fileName: result.value - }; - - $("#directory-contents").trigger('table_request', eventData); - - } else { - triggerReloadTable(); - } - }) - .then((filename) => this.newFile(filename)); - } - - newFile(filename) { - fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?touch=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) - .then( function(response) { - const eventData = { - type: "getDataFromJsonResponse", - response: response - }; - - $("#directory-contents").trigger('table_request', eventData); +}); - }) - .then( function() { - $("#directory-contents").trigger('table_request', {type: "reloadTable" }); - }) - .catch( function(e) { - alertError('Error occurred when attempting to create new file', e.message); - }); +class FileOps { + constructor() { + } + + + newFilePrompt() { + + const eventData = { + action: 'fileOpsCreateFile', + 'inputOptions': { + title: 'New File', + input: 'text', + inputLabel: 'Filename', + showCancelButton: true, + inputValidator: (value) => { + if (!value) { + // TODO: validate filenames against listing + return 'Provide a non-empty filename.' + } + else if (value.includes("/")) { + // TODO: validate filenames against listing + return 'Illegal character (/) not allowed in filename.' + } + } } - + }; + + $("#directory-contents").trigger('swalShowInput', eventData); + + } + + newFile(filename) { + fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?touch=true`, { method: 'put', headers: { 'X-CSRF-Token': csrf_token } }) + .then(function (response) { + const eventData = { + type: "getDataFromJsonResponse", + response: response + }; + + $("#directory-contents").trigger('table_request', eventData); + + }) + .then(function () { + $("#directory-contents").trigger('reloadTable'); + }) + .catch(function (e) { + const eventData = { + 'title': 'Error occurred when attempting to create new file', + 'message': e.message, + }; + + $("#directory-contents").trigger('swalShowError', eventData); + + }); + } } \ No newline at end of file diff --git a/apps/dashboard/app/javascript/packs/files/SweetAlert.js b/apps/dashboard/app/javascript/packs/files/SweetAlert.js new file mode 100644 index 0000000000..1a6546f672 --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/SweetAlert.js @@ -0,0 +1,72 @@ +import Swal from 'sweetalert2' + +let sweetAlert = null; + +$(document).ready(function(){ + sweetAlert = new SweetAlert(); + $("#directory-contents").on("swalShowError", function(e,options) { + sweetAlert.alertError(options.title, options.message); + }); + + $("#directory-contents").on("swalShowPrompt", function(e,options) { + sweetAlert.alertError(options.title, options.message); + }); + + $("#directory-contents").on("swalShowInput", function(e,options) { + sweetAlert.input(options); + }); + +}); + +class SweetAlert { + _swal = null; + + constructor() { + this._swal = Swal; + this.setMixin(); + } + + input(options) { + this._swal.fire(options.inputOptions) + .then (function(result){ + if(result.isConfirmed) { + $("#directory-contents").trigger(options.action, result); + } else { + $("#directory-contents").trigger('reloadTable'); + } + }); + } + + setMixin() { + this._swal.mixIn = ({ + showClass: { + popup: 'swal2-noanimation', + backdrop: 'swal2-noanimation' + }, + hideClass: { + popup: '', + backdrop: '' + } + }); + } + + alertError(error_title, error_message) { + this._swal.fire(error_title, error_message, 'error'); + } + + loading(title) { + this._swal.fire({ + title: title, + allowOutsideClick: false, + showConfirmButton: false, + willOpen: () => { this._swal.showLoading() } + }); + } + + doneLoading() { + this._swal.close(); + } +} + + + diff --git a/apps/dashboard/app/javascript/packs/files/clipboard.js b/apps/dashboard/app/javascript/packs/files/clipboard.js deleted file mode 100755 index 642123625e..0000000000 --- a/apps/dashboard/app/javascript/packs/files/clipboard.js +++ /dev/null @@ -1,74 +0,0 @@ -import ClipboardJS from 'clipboard' -import {Handlebars, table} from './datatable.js'; - -export {ClipboardJS, clipboardjs, clearClipboard, updateClipboardFromSelection, updateViewForClipboard }; - -global.ClipboardJS = ClipboardJS - -var clipboardjs = null; - -$(document).ready(function(){ - - clipboardjs = new ClipboardJS('#copy-path'); - - /* - NOTE: Had to move the following functionality to the template (_inline_js.html.erb) so the tooltip would be recognized. - - clipboardjs.on('success', function(e) { - //FIXME: for some reason the jQuery function tooltip is not being recognized. Will need to figure out why or move on to new tooltip plugin. - gerald - $(e.trigger).popover({title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom'}).tooltip('show'); - setTimeout(() => $(e.trigger).tooltip('hide'), 2000); - e.clearSelection(); - }); - clipboardjs.on('error', function(e) { - e.clearSelection(); - }); - */ - - //FIXME: so need to handle updateViewForClipboard based on EVENTS emitted by local storage modifications - updateViewForClipboard(); - global.addEventListener('storage', () => { - updateViewForClipboard(); - }); - - - $('#copy-move-btn').on("click", () => { - updateClipboardFromSelection(); - updateViewForClipboard(); - }); -}); - -function updateClipboardFromSelection(){ - let selection = table.rows({selected: true}).data(); - if(selection.length == 0){ - clearClipboard(); - } - else { - let clipboardData = { - from: history.state.currentDirectory, - files: selection.toArray().map((f) => { - return { directory: f.type == 'd', name: f.name }; - }) - }; - - localStorage.setItem('filesClipboard', JSON.stringify(clipboardData)); - } -} - -function clearClipboard(){ - localStorage.removeItem('filesClipboard'); -} - -function updateViewForClipboard(){ - let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || '{}'), - template_str = $('#clipboard-template').html(), - template = Handlebars.compile(template_str); - - $('#clipboard').html(template(clipboard)); - - $('#clipboard-clear').on("click", () => { - clearClipboard(); - updateViewForClipboard(); - }); - -} diff --git a/apps/dashboard/app/javascript/packs/files/datatable.js b/apps/dashboard/app/javascript/packs/files/datatable.js deleted file mode 100755 index 37a70f0a87..0000000000 --- a/apps/dashboard/app/javascript/packs/files/datatable.js +++ /dev/null @@ -1,438 +0,0 @@ -/** - * everything in this file only depends upon the Datatable. Nothing else. This could run alone without any of the files app functionality. -**/ - -import 'datatables.net'; -import 'datatables.net-bs4/js/dataTables.bootstrap4'; -import 'datatables.net-select'; -import 'datatables.net-select-bs4'; -import Handlebars from 'handlebars'; - -let table = null; -let onpopstate = null; -let actionsBtnTemplate = null; -let reportTransferTemplate = null; - -export { reloadTable, table, update_datatables_status }; - - -// global.reloadTable = reloadTable; // Required to be marked as global since we are using this in the template. - -$(document).ready(function() { - showTable(); - - // handle arrow keys. - $('#directory-contents tbody').on('keydown', 'input, a', function(e) { - if(e.key == "ArrowDown") { - e.preventDefault(); - let tr = $(this.closest('tr')).next('tr').get(0); - if(tr){ - tr.querySelector('input[type=checkbox]').focus(); - - // deselect if not holding shift key to work - // like native file browsers - if(! e.shiftKey){ - table.rows().deselect(); - } - - // select if moving down - table.row(tr).select(); - } - } else if(e.key == "ArrowUp") { - e.preventDefault(); - - let tr = $(this.closest('tr')).prev('tr').get(0); - if(tr) { - tr.querySelector('input[type=checkbox]').focus(); - - // deselect if not holding shift key to work - // like native file browsers - if(! e.shiftKey){ - table.rows().deselect(); - } - - // select if moving up - table.row(tr).select(); - } - } - }); - - // Selecting a checkbox in the table - table.on( 'select', function ( e, dt, type, indexes ) { - dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', true)); - }); - - // Deselecting a checkbox in the table - table.on( 'deselect', function ( e, dt, type, indexes ) { - dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', false)); - }); - - // Update the Table Information (Number of items selected, items pers page, pages, etc.) - table.on('draw.dtSelect.dt select.dtSelect.dt deselect.dtSelect.dt info.dt', function () { - update_datatables_status(table); - }); - - // if only 1 selected item, do not allow to de-select - table.on('user-select', function ( e, dt, type, cell, originalEvent ) { - var selected_rows = dt.rows( { selected: true } ); - - if(originalEvent.target.closest('.actions-btn-group')){ - // dont do user select event when opening or working with actions btn dropdown - e.preventDefault(); - } else if(selected_rows.count() == 1 && cell.index().row == selected_rows.indexes()[0] ){ - // dont do user select because already selected - e.preventDefault(); - } else{ - // row need to find the checkbox to give it the focus - cell.node().closest('tr').querySelector('input[type=checkbox]').focus(); - } - }); - - - - - reportTransferTemplate = (function(){ - let template_str = $('#transfer-template').html(); - return Handlebars.compile(template_str); - })(); - - actionsBtnTemplate = (function(){ - let template_str = $('#actions-btn-template').html(); - return Handlebars.compile(template_str); - })(); - - - $.fn.dataTable.ext.search.push( - function( settings, data, dataIndex ) { - return getShowDotFiles() || ! data[2].startsWith('.'); - } - ) - - - - $('#directory-contents tbody').on('click', 'tr td:first-child input[type=checkbox]', function(){ - // input checkbox checked or not - - if($(this).is(':checked')){ - // select row - table.row(this.closest('tr')).select(); - } - else{ - // deselect row - table.row(this.closest('tr')).deselect(); - } - - this.focus(); - }); - - - $('#show-dotfiles').on('change', () => { - let visible = $('#show-dotfiles').is(':checked'); - - setShowDotFiles(visible); - updateDotFileVisibility(); - }); - - $('#show-owner-mode').on('change', () => { - let visible = $('#show-owner-mode').is(':checked'); - - setShowOwnerMode(visible); - updateShowOwnerModeVisibility(); - }); - - $('#path-breadcrumbs').on('click', '#goto-btn', function(){ - Swal.fire({ - title: 'Change Directory', - input: 'text', - inputLabel: 'Path', - inputValue: history.state.currentDirectory, - inputAttributes: { - spellcheck: 'false', - }, - showCancelButton: true, - inputValidator: (value) => { - if (! value || ! value.startsWith('/')) { - // TODO: validate filenames against listing - return 'Provide an absolute pathname' - } - } - }) - .then((result) => result.isConfirmed ? Promise.resolve(result.value) : Promise.reject('cancelled')) - .then((pathname) => goto(filesPath + pathname)) - }); - - $('#directory-contents tbody, #path-breadcrumbs, #favorites').on('click', 'a.d', function(){ - if(clickEventIsSignificant(event)){ - event.preventDefault(); - event.cancelBubble = true; - if(event.stopPropagation) event.stopPropagation(); - - goto(this.getAttribute("href")); - } - }); - - // prepend show dotfiles checkbox to search box - $('#directory-contents_filter').prepend(``) - $('#directory-contents_filter').prepend(``) - - - $('#show-dotfiles').on('change', () => { - let visible = $('#show-dotfiles').is(':checked'); - - setShowDotFiles(visible); - updateDotFileVisibility(); - }); - - $('#show-owner-mode').on('change', () => { - let visible = $('#show-owner-mode').is(':checked'); - - setShowOwnerMode(visible); - updateShowOwnerModeVisibility(); - }); - - onpopstate = function(event){ - // FIXME: handle edge case if state ! exist - setTimeout(() => { - goto(event.state.currentDirectoryUrl, false); - }, 0); - }; - -}); - -function setShowOwnerMode(visible) { - localStorage.setItem('show-owner-mode', new Boolean(visible)); -} - -function setShowDotFiles(visible) { - localStorage.setItem('show-dotfiles', new Boolean(visible)); -} - -function updateDotFileVisibility() { - table.draw(); -} - -function updateShowOwnerModeVisibility() { - let visible = getShowOwnerMode(); - - table.column('owner:name').visible(visible); - table.column('mode:name').visible(visible); -} - - -function goto(url, pushState = true, show_processing_indicator = true) { - if(url == history.state.currentDirectoryUrl) - pushState = false; - - reloadTable(url) - .then((data) => { - $('#path-breadcrumbs').html(data.breadcrumbs_html); - - if(pushState) { - // Clear search query when moving to another directory. - table.search('').draw(); - - history.pushState({ - currentDirectory: data.path, - currentDirectoryUrl: data.url - }, data.name, data.url); - } - }) - .finally(() => { - //TODO: after processing is available via ActiveJobs merge - // if(show_processing_indicator) - // table.processing(false) - }); -} - - -// borrowed from Turbolinks -// event: MouseEvent -function clickEventIsSignificant(event) { - return !( - // (event.target && (event.target as any).isContentEditable) - event.defaultPrevented - || event.which > 1 - || event.altKey - || event.ctrlKey - || event.metaKey - || event.shiftKey - ) -} - -function dataFromJsonResponse(response){ - return new Promise((resolve, reject) => { - Promise.resolve(response) - .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) - .then(response => response.json()) - .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) - .catch((e) => reject(e)) - }); -} - - -function getEmptyDirs(entry){ - return new Promise((resolve) => { - if(entry.isFile){ - resolve([]); - } - else{ - // getFilesAndDirectoriesFromDirectory has no return value, so turn this into a promise - getFilesAndDirectoriesFromDirectory(entry.createReader(), [], function(error){ console.error(error)}, { - onSuccess: (entries) => { - if(entries.length == 0){ - // this is an empty directory - resolve([entry]); - } - else{ - Promise.all(entries.map(e => getEmptyDirs(e))).then((dirs) => resolve(_.flattenDeep(dirs))); - } - } - }) - } - }); -} - - -function getFilesAndDirectoriesFromDirectory (directoryReader, oldEntries, logDropError, { onSuccess }) { - directoryReader.readEntries( - (entries) => { - const newEntries = [...oldEntries, ...entries] - // According to the FileSystem API spec, getFilesAndDirectoriesFromDirectory() must be called until it calls the onSuccess with an empty array. - if (entries.length) { - setTimeout(() => { - getFilesAndDirectoriesFromDirectory(directoryReader, newEntries, logDropError, { onSuccess }) - }, 0) - // Done iterating this particular directory - } else { - onSuccess(newEntries) - } - }, - // Make sure we resolve on error anyway, it's fine if only one directory couldn't be parsed! - (error) => { - logDropError(error) - onSuccess(oldEntries) - } - ) -} - -function update_datatables_status(api){ - // from "function info ( api )" of https://cdn.datatables.net/select/1.3.1/js/dataTables.select.js - let rows = api.rows( { selected: true } ).flatten().length, - page_info = api.page.info(), - msg = page_info.recordsTotal == page_info.recordsDisplay ? `Showing ${page_info.recordsDisplay} rows` : `Showing ${page_info.recordsDisplay} of ${page_info.recordsTotal} rows`; - - $('.datatables-status').html(`${msg} - ${rows} rows selected`); -} - -function reloadTable(url) { - var request_url = url || history.state.currentDirectoryUrl; - - return fetch(request_url, {headers: {'Accept':'application/json'}}) - .then(response => dataFromJsonResponse(response)) - .then(function(data) { - - $('#shell-wrapper').replaceWith((data.shell_dropdown_html)) - - table.clear(); - table.rows.add(data.files); - table.draw(); - - $('#open-in-terminal-btn').attr('href', data.shell_url); - $('#open-in-terminal-btn').removeClass('disabled'); - - return Promise.resolve(data); - }) - .catch((e) => { - Swal.fire(e.message, `Error occurred when attempting to access ${request_url}`, 'error'); - - $('#open-in-terminal-btn').addClass('disabled'); - return Promise.reject(e); - }); -} - - -function showTable(tableOptions) -{ - - table = $('#directory-contents').on('xhr.dt', function ( e, settings, json, xhr ) { - // new ajax request for new data so update date/time - // if(json && json.time){ - if(json && json.time){ - history.replaceState(_.merge({}, history.state, {currentDirectoryUpdatedAt: json.time}), null); - } - }).DataTable({ - autoWidth: false, - language: { - search: 'Filter:', - }, - order: [[1, "asc"], [2, "asc"]], - rowId: 'id', - paging:false, - scrollCollapse: true, - select: { - style: 'os', - className: 'selected', - toggleable: true, - // don't trigger select checkbox column as select - // if you need to omit more columns, use a "selectable" class on the columns you want to support selection - selector: 'td:not(:first-child)' - }, - // https://datatables.net/reference/option/dom - // dom: '', dataTables_info nowrap - // - // put breadcrmbs below filter!!! - dom: "<'row'<'col-sm-12'f>>" + // normally <'row'<'col-sm-6'l><'col-sm-6'f>> but we disabled pagination so l is not needed (dropdown for selecting # rows) - "<'row'<'col-sm-12'<'dt-status-bar'<'datatables-status float-right'><'transfers-status'>>>>"+ - "<'row'<'col-sm-12'tr>>", // normally this is <'row'<'col-sm-5'i><'col-sm-7'p>> but we disabled pagination so have info take whole row - columns: [ - { - data: null, - orderable: false, - defaultContent: '', - render: function(data, type, row, meta) { - var api = new $.fn.dataTable.Api( meta.settings ); - let selected = api.rows(meta.row, { selected: true }).count() > 0; - return ` ${selected ? 'checked' : ''}`; - } - }, - { data: 'type', render: (data, type, row, meta) => data == 'd' ? ' dir' : ' file' }, // type - { name: 'name', data: 'name', className: 'text-break', render: (data, type, row, meta) => `${Handlebars.escapeExpression(data)}` }, // name - { name: 'actions', orderable: false, data: null, render: (data, type, row, meta) => actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, // FIXME: pass row index or something needed for finding item - { data: 'size', - render: (data, type, row, meta) => { - return type == "display" ? row.human_size : data; - } - }, // human_size - { data: 'modified_at', render: (data, type, row, meta) => { - if(type == "display"){ - let date = new Date(data * 1000) - - // Return formatted date "3/23/2021 10:52:28 AM" - return isNaN(data) ? 'Invalid Date' : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` - } - else{ - return data; - } - }}, // modified_at - { name: 'owner', data: 'owner', visible: getShowOwnerMode() }, // owner - { name: 'mode', data: 'mode', visible: getShowOwnerMode(), render: (data, type, row, meta) => { - - // mode after base conversion is a string such as "100755" - let mode = data.toString(8) - - // only care about the last 3 bits (755) - let chmodDisplay = mode.substring(mode.length - 3) - - return chmodDisplay - }} // mode - ] - }); -} - -function getShowOwnerMode() { - return localStorage.getItem('show-owner-mode') == 'true' -} - -function getShowDotFiles() { - return localStorage.getItem('show-dotfiles') == 'true' -} \ No newline at end of file diff --git a/apps/dashboard/app/javascript/packs/files/fileops.js b/apps/dashboard/app/javascript/packs/files/fileops.js deleted file mode 100755 index fb2d53c3d3..0000000000 --- a/apps/dashboard/app/javascript/packs/files/fileops.js +++ /dev/null @@ -1,377 +0,0 @@ -import {Swal, alertError, doneLoading, loading} from './sweet_alert.js'; -import {dataFromJsonResponse,Handlebars, table} from './datatable.js'; -import {clearClipboard, updateViewForClipboard } from './clipboard.js'; - -export {copyFiles, downloadDirectory, downloadFile, getEmptyDirs, reportTransferTemplate}; - -var reportTransferTemplate = null; - -$(document).ready(function(){ - - reportTransferTemplate = (function(){ - let template_str = $('#transfer-template').html(); - return Handlebars.compile(template_str); - })(); - - $('#new-file-btn').on("click", () => { - Swal.fire({ - title: 'New File', - input: 'text', - inputLabel: 'Filename', - showCancelButton: true, - inputValidator: (value) => { - if (! value ) { - // TODO: validate filenames against listing - return 'Provide a non-empty filename.' - } - else if (value.includes("/")) { - // TODO: validate filenames against listing - return 'Illegal character (/) not allowed in filename.' - } - } - }) - .then((result) => result.isConfirmed ? Promise.resolve(result.value) : Promise.reject('cancelled')) - .then((filename) => newFile(filename)); - }); - - $('#new-dir-btn').on("click", () => { - Swal.fire({ - title: 'New Directory', - input: 'text', - inputLabel: 'Directory name', - inputAttributes: { - spellcheck: 'false', - }, - showCancelButton: true, - inputValidator: (value) => { - if (! value || value.includes("/")) { - // TODO: validate filenames against listing - return 'Provide a directory name that does not have / in it' - } - }, - }) - .then((result) => result.isConfirmed ? Promise.resolve(result.value) : Promise.reject('cancelled')) - .then((filename) => newDirectory(filename)); - }); - - $('#download-btn').on("click", () => { - let selection = table.rows({ selected: true }).data(); - if(selection.length == 0) { - Swal.fire('Select a file, files, or directory to download', 'You have selected none.', 'error'); - } - selection.toArray().forEach( (f) => { - if(f.type == 'd') { - downloadDirectory(f) - } - else if(f.type == 'f') { - downloadFile(f) - } - }) - }); - - $('#clipboard-move-to-dir').on("click", () => { - let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || 'null'); - - if(clipboard){ - clipboard.to = history.state.currentDirectory; - - if(clipboard.from == clipboard.to){ - console.error('clipboard from and to are identical') - // TODO: - } - else{ - let files = {}; - clipboard.files.forEach((f) => { - files[`${clipboard.from}/${f.name}`] = `${history.state.currentDirectory}/${f.name}` - }); - - moveFiles(files, csrf_token); - } - } - else{ - console.error('files clipboard is empty'); - } - }); - - $('#clipboard-copy-to-dir').on("click", () => { - let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || 'null'); - - if(clipboard){ - clipboard.to = history.state.currentDirectory; - - if(clipboard.from == clipboard.to){ - console.error('clipboard from and to are identical') - - // TODO: we want to support this use case - // copy and paste as a new filename - // but lots of edge cases - // (overwrite or rename duplicates) - // _copy - // _copy_2 - // _copy_3 - // _copy_4 - } - else{ - // [{"/from/file/path":"/to/file/path" }] - let files = {}; - clipboard.files.forEach((f) => { - files[`${clipboard.from}/${f.name}`] = `${history.state.currentDirectory}/${f.name}` - }); - - copyFiles(files, csrf_token); - } - } - else{ - console.error('files clipboard is empty'); - } - }); - - $('#delete-btn').on("click", () => { - let files = table.rows({selected: true}).data().toArray().map((f) => f.name); - deleteFiles(files); - }); - - $('#directory-contents tbody').on('click', '.rename-file', function(e){ - e.preventDefault(); - - let row = table.row(this.dataset.rowIndex).data(); - - // if there was some other attribute that just had the name... - let filename = $($.parseHTML(row.name)).text(); - - Swal.fire({ - title: 'Rename', - input: 'text', - inputLabel: 'Filename', - inputValue: filename, - inputAttributes: { - spellcheck: 'false', - }, - showCancelButton: true, - inputValidator: (value) => { - if (! value) { - // TODO: validate filenames against listing - return 'Provide a filename to rename this to'; - } - else if (value.includes('/') || value.includes('..')){ - return 'Filename cannot include / or ..'; - } - } - }) - .then((result) => result.isConfirmed ? Promise.resolve(result.value) : Promise.reject('cancelled')) - .then((new_filename) => renameFile(filename, new_filename)) - }); - - $('#directory-contents tbody').on('click', '.delete-file', function(e){ - e.preventDefault(); - - let row = table.row(this.dataset.rowIndex).data(); - deleteFiles([row.name]); - }); - -}); - -function newDirectory(filename){ - fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) - .then(response => dataFromJsonResponse(response)) - .then(() => reloadTable()) - .catch(e => alertError('Error occurred when attempting to create new directory', e.message)); -} - -function newFile(filename){ - fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?touch=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) - .then(response => dataFromJsonResponse(response)) - .then(() => reloadTable()) - .catch(e => alertError('Error occurred when attempting to create new file', e.message)); -} - -function getEmptyDirs(entry){ - return new Promise((resolve) => { - if(entry.isFile){ - resolve([]); - } - else{ - // getFilesAndDirectoriesFromDirectory has no return value, so turn this into a promise - getFilesAndDirectoriesFromDirectory(entry.createReader(), [], function(error){ console.error(error)}, { - onSuccess: (entries) => { - if(entries.length == 0){ - // this is an empty directory - resolve([entry]); - } - else{ - Promise.all(entries.map(e => getEmptyDirs(e))).then((dirs) => resolve(_.flattenDeep(dirs))); - } - } - }) - } - }); -} - -function downloadDirectory(file) { - let filename = $($.parseHTML(file.name)).text(), - canDownloadReq = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?can_download=${Date.now().toString()}` - - loading('preparing to download directory: ' + file.name) - - fetch(canDownloadReq, { - method: 'GET', - headers: { - 'X-CSRF-Token': csrf_token, - 'Accept': 'application/json' - } - }) - .then(response => dataFromJsonResponse(response)) - .then(data => { - if (data.can_download) { - doneLoading(); - downloadFile(file) - } else { - Swal.fire(dashboard_files_directory_download_error_modal_title, data.error_message, 'error') - } - }) - .catch(e => { - Swal.fire(dashboard_files_directory_download_error_modal_title, e.message, 'error') - }) -} - -function downloadFile(file) { - // creating the temporary iframe is exactly what the CloudCmd does - // so this just repeats the status quo - - let filename = $($.parseHTML(file.name)).text(), - downloadUrl = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?download=${Date.now().toString()}`, - iframe = document.createElement('iframe'), - TIME = 30 * 1000; - - iframe.setAttribute('class', 'd-none'); - iframe.setAttribute('src', downloadUrl); - - document.body.appendChild(iframe); - - setTimeout(function() { - document.body.removeChild(iframe); - }, TIME); -} - -function transferFiles(files, action, summary){ - loading(_.startCase(summary)); - - return fetch(transfersPath, { - method: 'post', - body: JSON.stringify({ - command: action, - files: files - }), - headers: { 'X-CSRF-Token': csrf_token } - }) - .then(response => dataFromJsonResponse(response)) - .then((data) => { - - if(! data.completed){ - // was async, gotta report on progress and start polling - reportTransfer(data); - } - else { - if(data.target_dir == history.state.currentDirectory){ - reloadTable(); - } - } - - if(action == 'mv' || action == 'cp'){ - clearClipboard(); - updateViewForClipboard(); - } - }) - .then(() => doneLoading()) - .catch(e => alertError('Error occurred when attempting to ' + summary, e.message)) -} - -function copyFiles(files){ - transferFiles(files, "cp", "copy files") -} - -function reportTransfer(data){ - // 1. add the transfer label - findAndUpdateTransferStatus(data); - - let attempts = 0 - - // 2. poll for the updates - var poll = function() { - $.getJSON(data.show_json_url, function (newdata) { - findAndUpdateTransferStatus(newdata); - - if(newdata.completed) { - if(! newdata.error_message) { - if(newdata.target_dir == history.state.currentDirectory) { - reloadTable(); - } - - // 3. fade out after 5 seconds - fadeOutTransferStatus(newdata) - } - } - else { - // not completed yet, so poll again - setTimeout(poll, 1000); - } - }).fail(function() { - if (attempts >= 3) { - Swal.fire('Operation may not have happened', 'Failed to retrieve file operation status.', 'error'); - } else { - setTimeout(poll, 1000); - attempts++; - } - }); - } - - poll(); -} - -function renameFile(filename, new_filename){ - let files = {}; - files[`${history.state.currentDirectory}/${filename}`] = `${history.state.currentDirectory}/${new_filename}`; - transferFiles(files, "mv", "rename file") -} - -function moveFiles(files, summary = "move files"){ - transferFiles(files, "mv", "move files") -} - -function removeFiles(files){ - transferFiles(files, "rm", "remove files") -} - -function findAndUpdateTransferStatus(data){ - let id = `#${data.id}`; - - if($(id).length){ - $(id).replaceWith(reportTransferTemplate(data)); - } - else{ - $('.transfers-status').append(reportTransferTemplate(data)); - } -} - -function fadeOutTransferStatus(data){ - let id = `#${data.id}`; - $(id).fadeOut(4000); -} - -function deleteFiles(files){ - if(! files.length > 0){ - return; - } - - Swal.fire({ - title: files.length == 1 ? `Delete ${files[0]}?` : `Delete ${files.length} selected files?`, - text: 'Are you sure you want to delete the files: ' + files.join(', '), - showCancelButton: true, - }) - .then((result) => { - if(result.isConfirmed){ - loading('Deleting files...'); - removeFiles(files.map(f => [history.state.currentDirectory, f].join('/')), csrf_token); - } - }) -} diff --git a/apps/dashboard/app/javascript/packs/files/index.js b/apps/dashboard/app/javascript/packs/files/index.js deleted file mode 100644 index 0317227745..0000000000 --- a/apps/dashboard/app/javascript/packs/files/index.js +++ /dev/null @@ -1,62 +0,0 @@ -import { Swal } from './sweet_alert.js'; -import { DataTable } from './DataTable.js'; -import { FileOps } from './FileOps.js'; -/* -import {} from './fileops.js'; -import {} from './uppy.js'; -import {} from './clipboard.js'; -*/ - -export { Swal, triggerReloadTable }; - - -$(document).ready(function() { - let dataTable = new DataTable(); - let fileOps = null; - - $("#new-file-btn").on("click", function() { - const eventData = { - type: "newFile" - }; - - $("#directory-contents").trigger('table_request', eventData); - }); - - if( ! alert ) { - dataTable.reloadTable(); - } - - $("#directory-contents").on("table_request", function(event, options) { - switch (options.type) { - case 'reloadTable': - dataTable.reloadTable(); - break; - case 'newFile': - fileOps = new FileOps(dataTable); - fileOps.newFilePrompt(); - break; - case 'createFile': - fileOps = new FileOps(dataTable); - fileOps.newFile(options.fileName); - triggerReloadTable(); - break; - case 'getDataFromJsonResponse': - dataTable.dataFromJsonResponse(options.response); - break; - default: - triggerReloadTable(); - break; - } - - - }); - - // Options represents the functionality of the Files App that you want to include. - - /* END Basic table functionality. */ - -}); - -function triggerReloadTable() { - $("#directory-contents").trigger('table-request', { type: 'reloadTable' }); -} diff --git a/apps/dashboard/app/javascript/packs/files/sweet_alert.js b/apps/dashboard/app/javascript/packs/files/sweet_alert.js deleted file mode 100755 index 3c523b0d0a..0000000000 --- a/apps/dashboard/app/javascript/packs/files/sweet_alert.js +++ /dev/null @@ -1,35 +0,0 @@ -import Swal from 'sweetalert2' - -export { Swal, alertError, loading, doneLoading }; - -global.Swal = Swal.mixin({ - showClass: { - popup: 'swal2-noanimation', - backdrop: 'swal2-noanimation' - }, - hideClass: { - popup: '', - backdrop: '' - } -}); - -$(document).ready(function(){ - -}); - -function alertError(error_title, error_message){ - Swal.fire(error_title, error_message, 'error'); -} - -function loading(title){ - Swal.fire({ - title: title, - allowOutsideClick: false, - showConfirmButton: false, - willOpen: () => { Swal.showLoading() } - }); -} - -function doneLoading(){ - Swal.close(); -} diff --git a/apps/dashboard/app/javascript/packs/files/uppy.js b/apps/dashboard/app/javascript/packs/files/uppy.js deleted file mode 100755 index 285f96d81b..0000000000 --- a/apps/dashboard/app/javascript/packs/files/uppy.js +++ /dev/null @@ -1,173 +0,0 @@ -import { Uppy, BasePlugin } from '@uppy/core' -import Dashboard from '@uppy/dashboard' -import XHRUpload from '@uppy/xhr-upload' -import _ from 'lodash'; - -export {BasePlugin, closeAndResetUppyModal, Dashboard, uppy, Uppy, XHRUpload}; - -// Uppy = Uppy; -// BasePlugin = BasePlugin; -// Dashboard = Dashboard; -// XHRUpload = XHRUpload; - -let uppy = null; - -$(document).ready(function(){ - class EmptyDirCreator extends BasePlugin { - constructor (uppy, opts){ - super(uppy, opts) - this.id = this.opts.id || 'EmptyDirUploaderCatcher'; - this.type = 'acquirer'; - - this.empty_dirs = []; - this.last_entries = []; - - this.handleRootDrop = this.handleRootDrop.bind(this); - this.createEmptyDirs = this.createEmptyDirs.bind(this); - - this.uppy = uppy; - } - - handleRootDrop (e) { - // from https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/getDroppedFiles/index.js - if (e.dataTransfer.items && e.dataTransfer.items[0] && 'webkitGetAsEntry' in e.dataTransfer.items[0]) { - // toArray https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/toArray.js#L4 - let items = Array.prototype.slice.call(e.dataTransfer.items || [], 0); - let entries = items.map(i => i.webkitGetAsEntry()).filter(i => i); - - return Promise.all(entries.map(i => getEmptyDirs(i))).then((dirs) => { - this.empty_dirs = this.empty_dirs.concat(_.flattenDeep(dirs)); - - console.log(this.empty_dirs); - }); - } - //else we don't have access to directory information - } - - createEmptyDirs (ids) { - if(! this.uppy.getState().error){ // avoid creating empty dirs if error occurred during upload - - //TODO: error checking and reporting - return Promise.all(this.empty_dirs.map((d) => { - // "fullPath" should actually be the path relative to the current directory - let filename = _.trimStart(d.fullPath, '/'); - - return fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) - //TODO: parse json response verify if there was an error creating directory and handle error - - })).then(() => this.empty_dirs = []); - } - } - - install () { - this.uppy.addPostProcessor(this.createEmptyDirs); - } - - uninstall () { - this.uppy.removePostProcessor(this.createEmptyDirs); - } - } - - uppy = new Uppy({ - restrictions: { - maxFileSize: maxFileSize, - } - }); - - uppy.use(EmptyDirCreator); - uppy.use(Dashboard, { - trigger: '#upload-btn', - fileManagerSelectionType: 'both', - disableThumbnailGenerator: true, - showLinkToFileUploadResult: false, - closeModalOnClickOutside: true, - closeAfterFinish: true, - allowMultipleUploads: false, - onRequestCloseModal: () => closeAndResetUppyModal(uppy), - note: 'Empty directories will be included in the upload only when a directory upload is initiated via drag and drop. This is because the File and Directory Entries API is available only on a drop event, not during an input change event.' - }); - uppy.use(XHRUpload, { - endpoint: filesUploadPath, - withCredentials: true, - fieldName: 'file', - limit: 1, - headers: { 'X-CSRF-Token': csrf_token }, - timeout: 128 * 1000, - }); - - uppy.on('file-added', (file) => { - uppy.setFileMeta(file.id, { parent: history.state.currentDirectory }); - if(file.meta.relativePath == null && file.data.webkitRelativePath){ - uppy.setFileMeta(file.id, { relativePath: file.data.webkitRelativePath }); - } - }); - - uppy.on('complete', (result) => { - if(result.successful.length > 0){ - reloadTable(); - } - }); - - // https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file - global.addEventListener("dragover",function(e){ - e = e || event; - e.preventDefault(); - },false); - - global.addEventListener("drop",function(e){ - e = e || event; - e.preventDefault(); - },false); - - $('#directory-contents').on('drop', function(e){ - this.classList.remove('dragover'); - console.log('File(s) dropped'); - // Prevent default behavior (Prevent file from being opened) - - // pass drop event to uppy dashboard - uppy.getPlugin('Dashboard').openModal().then(() => uppy.getPlugin('Dashboard').handleDrop(e.originalEvent)) - }); - - $('#directory-contents').on('dragover', function(e){ - this.classList.add('dragover'); - - // Prevent default behavior (Prevent file from being opened) - e.preventDefault(); - - // specifies what feedback will be shown to the user by setting the dropEffect attribute of the DataTransfer associated with the event - // too bad we can't show an indicator (no dragstart/end when dragging from OS to browser) - e.originalEvent.dataTransfer.dropEffect = 'copy'; - }); - - $('#directory-contents').on('dragleave', function(e){ - this.classList.remove('dragover'); - }); - -}); - -function closeAndResetUppyModal(uppy){ - uppy.getPlugin('Dashboard').closeModal(); - uppy.reset(); -} - -function getEmptyDirs(entry){ - return new Promise((resolve) => { - if(entry.isFile){ - resolve([]); - } - else{ - // getFilesAndDirectoriesFromDirectory has no return value, so turn this into a promise - getFilesAndDirectoriesFromDirectory(entry.createReader(), [], function(error){ console.error(error)}, { - onSuccess: (entries) => { - if(entries.length == 0){ - // this is an empty directory - resolve([entry]); - } - else{ - Promise.all(entries.map(e => getEmptyDirs(e))).then((dirs) => resolve(_.flattenDeep(dirs))); - } - } - }) - } - }); -} diff --git a/apps/dashboard/app/views/layouts/files.html.erb b/apps/dashboard/app/views/layouts/files.html.erb index e19e7c0cce..5465ddd34a 100644 --- a/apps/dashboard/app/views/layouts/files.html.erb +++ b/apps/dashboard/app/views/layouts/files.html.erb @@ -1,6 +1,6 @@ <% content_for :head do - javascript_pack_tag 'files/index', nonce: true + javascript_pack_tag 'files/DataTable', nonce: true end %> From 96a8544d1e250a55f0ebf345ee7eba15f84ba682 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Mon, 21 Mar 2022 15:34:19 -0400 Subject: [PATCH 13/47] Completed Create Folder --- .../app/javascript/packs/files/DataTable.js | 10 ++- .../app/javascript/packs/files/FileOps.js | 61 ++++++++++++++++++- apps/dashboard/app/views/files/index.html.erb | 2 +- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js index a3ff366881..17d54abe54 100644 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -13,12 +13,20 @@ $(document).ready(function () { // listen for new file button click then trigger new file functionality. $("#new-file-btn").on("click", function () { - $("#directory-contents").trigger('newFile'); + $("#directory-contents").trigger('fileOpsNewFile'); + }); + + $("#new-folder-btn").on("click", function () { + $("#directory-contents").trigger('fileOpsNewFolder'); }); $("#directory-contents").on("reloadTable", function () { table.reloadTable(); }); + + $("#directory-contents").on("getDataFromJsonResponse", function (e, options) { + table.dataFromJsonResponse(options.response); + }); }); diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js index 6ec3e42d11..31584ed3ba 100644 --- a/apps/dashboard/app/javascript/packs/files/FileOps.js +++ b/apps/dashboard/app/javascript/packs/files/FileOps.js @@ -2,14 +2,22 @@ let fileOps = null; $(document).ready(function () { fileOps = new FileOps(); - $("#directory-contents").on("newFile", function () { + $("#directory-contents").on("fileOpsNewFile", function () { fileOps.newFilePrompt(); }); + $("#directory-contents").on("fileOpsNewFolder", function () { + fileOps.newFolderPrompt(); + }); + $("#directory-contents").on("fileOpsCreateFile", function (e, options) { fileOps.newFile(options.value); }); + $("#directory-contents").on("fileOpsCreateFolder", function (e, options) { + fileOps.newFolder(options.value); + }); + }); class FileOps { @@ -51,7 +59,7 @@ class FileOps { response: response }; - $("#directory-contents").trigger('table_request', eventData); + $("#directory-contents").trigger('getDataFromJsonResponse', eventData); }) .then(function () { @@ -67,4 +75,53 @@ class FileOps { }); } + + newFolderPrompt() { + + const eventData = { + action: 'fileOpsCreateFolder', + 'inputOptions': { + title: 'New Folder', + input: 'text', + inputLabel: 'Folder name', + showCancelButton: true, + inputValidator: (value) => { + if (!value || value.includes("/")) { + // TODO: validate filenames against listing + return 'Provide a directory name that does not have / in it' + } + } + } + }; + + $("#directory-contents").trigger('swalShowInput', eventData); + + } + + newFolder(filename) { + fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) + .then(function (response) { + const eventData = { + type: "getDataFromJsonResponse", + response: response + }; + + $("#directory-contents").trigger('getDataFromJsonResponse', eventData); + + }) + .then(function () { + $("#directory-contents").trigger('reloadTable'); + }) + .catch(function (e) { + const eventData = { + 'title': 'Error occurred when attempting to create new folder', + 'message': e.message, + }; + + $("#directory-contents").trigger('swalShowError', eventData); + + }); + } + + } \ No newline at end of file diff --git a/apps/dashboard/app/views/files/index.html.erb b/apps/dashboard/app/views/files/index.html.erb index 0d74cc45e2..5f9cc757fc 100644 --- a/apps/dashboard/app/views/files/index.html.erb +++ b/apps/dashboard/app/views/files/index.html.erb @@ -7,7 +7,7 @@ <% end %> - + From 9256cd146a4b1bf6fc37ffe64cf1e6fa473aee0f Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Mon, 21 Mar 2022 15:53:52 -0400 Subject: [PATCH 14/47] Completed upload functionality --- .../app/javascript/packs/files/DataTable.js | 13 +- .../app/javascript/packs/files/FileOps.js | 4 + .../app/javascript/packs/files/UppyOps.js | 172 ++++++++++++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100755 apps/dashboard/app/javascript/packs/files/UppyOps.js diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js index 17d54abe54..4cb7d7b9f5 100644 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -5,13 +5,14 @@ import 'datatables.net-select-bs4'; import Handlebars from 'handlebars'; import {} from './SweetAlert.js'; import {} from './FileOps.js'; +import {} from './UppyOps.js'; let table = null; $(document).ready(function () { table = new DataTable(); - // listen for new file button click then trigger new file functionality. + /* BUTTON ACTIONS */ $("#new-file-btn").on("click", function () { $("#directory-contents").trigger('fileOpsNewFile'); }); @@ -20,6 +21,14 @@ $(document).ready(function () { $("#directory-contents").trigger('fileOpsNewFolder'); }); + // $("#upload-btn").on("click", function () { + // $("#directory-contents").trigger('uppyOpsUpload'); + // }); + + /* END BUTTON ACTIONS */ + + /* TABLE ACTIONS */ + $("#directory-contents").on("reloadTable", function () { table.reloadTable(); }); @@ -27,6 +36,8 @@ $(document).ready(function () { $("#directory-contents").on("getDataFromJsonResponse", function (e, options) { table.dataFromJsonResponse(options.response); }); + + /* END TABLE ACTIONS */ }); diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js index 31584ed3ba..80efd63a13 100644 --- a/apps/dashboard/app/javascript/packs/files/FileOps.js +++ b/apps/dashboard/app/javascript/packs/files/FileOps.js @@ -18,6 +18,10 @@ $(document).ready(function () { fileOps.newFolder(options.value); }); + $("#directory-contents").on("fileOpsUpload", function (e, options) { + fileOps.newFolder(options.value); + }); + }); class FileOps { diff --git a/apps/dashboard/app/javascript/packs/files/UppyOps.js b/apps/dashboard/app/javascript/packs/files/UppyOps.js new file mode 100755 index 0000000000..7055ce88d1 --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/UppyOps.js @@ -0,0 +1,172 @@ +import { Uppy, BasePlugin } from '@uppy/core' +import Dashboard from '@uppy/dashboard' +import XHRUpload from '@uppy/xhr-upload' +import _ from 'lodash'; + + +let uppy = null; + +$(document).ready(function() { + + class EmptyDirCreator extends BasePlugin { + constructor (uppy, opts){ + super(uppy, opts) + this.id = this.opts.id || 'EmptyDirUploaderCatcher'; + this.type = 'acquirer'; + + this.empty_dirs = []; + this.last_entries = []; + + this.handleRootDrop = this.handleRootDrop.bind(this); + this.createEmptyDirs = this.createEmptyDirs.bind(this); + + this.uppy = uppy; + } + + + + handleRootDrop (e) { + // from https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/getDroppedFiles/index.js + if (e.dataTransfer.items && e.dataTransfer.items[0] && 'webkitGetAsEntry' in e.dataTransfer.items[0]) { + // toArray https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/toArray.js#L4 + let items = Array.prototype.slice.call(e.dataTransfer.items || [], 0); + let entries = items.map(i => i.webkitGetAsEntry()).filter(i => i); + + return Promise.all(entries.map(i => getEmptyDirs(i))).then((dirs) => { + this.empty_dirs = this.empty_dirs.concat(_.flattenDeep(dirs)); + + }); + } + //else we don't have access to directory information + } + + createEmptyDirs (ids) { + if(! this.uppy.getState().error){ // avoid creating empty dirs if error occurred during upload + + //TODO: error checking and reporting + return Promise.all(this.empty_dirs.map((d) => { + // "fullPath" should actually be the path relative to the current directory + let filename = _.trimStart(d.fullPath, '/'); + + return fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) + //TODO: parse json response verify if there was an error creating directory and handle error + + })).then(() => this.empty_dirs = []); + } + } + + install () { + this.uppy.addPostProcessor(this.createEmptyDirs); + } + + uninstall () { + this.uppy.removePostProcessor(this.createEmptyDirs); + } + } + + uppy = new Uppy({ + restrictions: { + maxFileSize: maxFileSize, + } + }); + + uppy.use(EmptyDirCreator); + uppy.use(Dashboard, { + trigger: '#upload-btn', + fileManagerSelectionType: 'both', + disableThumbnailGenerator: true, + showLinkToFileUploadResult: false, + closeModalOnClickOutside: true, + closeAfterFinish: true, + allowMultipleUploads: false, + onRequestCloseModal: () => closeAndResetUppyModal(uppy), + note: 'Empty directories will be included in the upload only when a directory upload is initiated via drag and drop. This is because the File and Directory Entries API is available only on a drop event, not during an input change event.' + }); + uppy.use(XHRUpload, { + endpoint: filesUploadPath, + withCredentials: true, + fieldName: 'file', + limit: 1, + headers: { 'X-CSRF-Token': csrf_token }, + timeout: 128 * 1000, + }); + + uppy.on('file-added', (file) => { + uppy.setFileMeta(file.id, { parent: history.state.currentDirectory }); + if(file.meta.relativePath == null && file.data.webkitRelativePath){ + uppy.setFileMeta(file.id, { relativePath: file.data.webkitRelativePath }); + } + }); + + uppy.on('complete', (result) => { + if(result.successful.length > 0){ + reloadTable(); + } + }); + + // https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file + global.addEventListener("dragover",function(e){ + e = e || event; + e.preventDefault(); + },false); + + global.addEventListener("drop",function(e){ + e = e || event; + e.preventDefault(); + },false); + + $('#directory-contents').on('drop', function(e){ + this.classList.remove('dragover'); + // Prevent default behavior (Prevent file from being opened) + + // pass drop event to uppy dashboard + uppy.getPlugin('Dashboard').openModal().then(() => uppy.getPlugin('Dashboard').handleDrop(e.originalEvent)) + }); + + $('#directory-contents').on('dragover', function(e){ + this.classList.add('dragover'); + + // Prevent default behavior (Prevent file from being opened) + e.preventDefault(); + + // specifies what feedback will be shown to the user by setting the dropEffect attribute of the DataTransfer associated with the event + // too bad we can't show an indicator (no dragstart/end when dragging from OS to browser) + e.originalEvent.dataTransfer.dropEffect = 'copy'; + }); + + $('#directory-contents').on('dragleave', function(e){ + this.classList.remove('dragover'); + }); + +}); + +function closeAndResetUppyModal(uppy){ + uppy.getPlugin('Dashboard').closeModal(); + uppy.reset(); +} + +function getEmptyDirs(entry){ + return new Promise((resolve) => { + if(entry.isFile){ + resolve([]); + } + else{ + // getFilesAndDirectoriesFromDirectory has no return value, so turn this into a promise + getFilesAndDirectoriesFromDirectory(entry.createReader(), [], function(error){ console.error(error)}, { + onSuccess: (entries) => { + if(entries.length == 0){ + // this is an empty directory + resolve([entry]); + } + else{ + Promise.all(entries.map(e => getEmptyDirs(e))).then((dirs) => resolve(_.flattenDeep(dirs))); + } + } + }) + } + }); +} + +function reloadTable() { + $("#directory-contents").trigger('reloadTable'); +} \ No newline at end of file From cfc914f0715cb31423170cac583802f91a0e5e0a Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Mon, 21 Mar 2022 16:09:01 -0400 Subject: [PATCH 15/47] Completed upload functionality --- apps/dashboard/app/javascript/packs/files/DataTable.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js index 4cb7d7b9f5..91522171db 100644 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -21,8 +21,10 @@ $(document).ready(function () { $("#directory-contents").trigger('fileOpsNewFolder'); }); + // Will have to work on this one later. Not so straight forward. + // // $("#upload-btn").on("click", function () { - // $("#directory-contents").trigger('uppyOpsUpload'); + // $("#directory-contents").trigger('uppyShowUploadPrompt'); // }); /* END BUTTON ACTIONS */ From 424b117f540fb1b28ff293e1fe307085d6af971a Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Tue, 22 Mar 2022 15:35:12 -0500 Subject: [PATCH 16/47] Completed File/Folder Download --- .../app/javascript/packs/files/DataTable.js | 130 ++++++++++++++++++ .../app/javascript/packs/files/FileOps.js | 107 +++++++++++--- .../app/javascript/packs/files/SweetAlert.js | 12 +- 3 files changed, 229 insertions(+), 20 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js index 91522171db..e8a99dc4da 100644 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -21,6 +21,26 @@ $(document).ready(function () { $("#directory-contents").trigger('fileOpsNewFolder'); }); + $("#download-btn").on("click", function () { + let selection = table.getTable().rows({ selected: true }).data(); + if(selection.length == 0) { + const eventData = { + 'title': 'Select a file, files, or directory to download', + 'message': 'You have selected none.', + }; + + $("#directory-contents").trigger('swalShowError', eventData); + + } else { + const eventData = { + selection: selection + }; + + $("#directory-contents").trigger('fileOpsDownload', eventData); + } + + }); + // Will have to work on this one later. Not so straight forward. // // $("#upload-btn").on("click", function () { @@ -41,6 +61,99 @@ $(document).ready(function () { /* END TABLE ACTIONS */ + /* DATATABLE LISTENERS */ + // prepend show dotfiles checkbox to search box + + table.getTable().on('draw.dtSelect.dt select.dtSelect.dt deselect.dtSelect.dt info.dt', function () { + table.updateDatatablesStatus(); + }); + + // if only 1 selected item, do not allow to de-select + table.getTable().on('user-select', function ( e, dt, type, cell, originalEvent ) { + var selected_rows = dt.rows( { selected: true } ); + + if(originalEvent.target.closest('.actions-btn-group')){ + // dont do user select event when opening or working with actions btn dropdown + e.preventDefault(); + } + else if(selected_rows.count() == 1 && cell.index().row == selected_rows.indexes()[0] ){ + // dont do user select because already selected + e.preventDefault(); + } + else{ + // row need to find the checkbox to give it the focus + cell.node().closest('tr').querySelector('input[type=checkbox]').focus(); + } + }); + + table.getTable().on( 'deselect', function ( e, dt, type, indexes ) { + dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', false)); + }); + + table.getTable().on( 'select', function ( e, dt, type, indexes ) { + dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', true)); + }); + + $('#directory-contents tbody').on('click', 'tr td:first-child input[type=checkbox]', function(){ + // input checkbox checked or not + + if($(this).is(':checked')){ + // select row + table.getTable().row(this.closest('tr')).select(); + } + else{ + // deselect row + table.getTable().row(this.closest('tr')).deselect(); + } + + this.focus(); + }); + + $('#directory-contents tbody').on('keydown', 'input, a', function(e){ + if(e.key == "ArrowDown"){ + e.preventDefault(); + + // let tr = this.closest('tr').nextSibling; + let tr = $(this.closest('tr')).next('tr').get(0); + if(tr){ + tr.querySelector('input[type=checkbox]').focus(); + + // deselect if not holding shift key to work + // like native file browsers + if(! e.shiftKey){ + table.getTable().rows().deselect(); + } + + // select if moving down + table.getTable().row(tr).select(); + } + } + else if(e.key == "ArrowUp"){ + e.preventDefault(); + + let tr = $(this.closest('tr')).prev('tr').get(0); + if(tr){ + tr.querySelector('input[type=checkbox]').focus(); + + // deselect if not holding shift key to work + // like native file browsers + if(! e.shiftKey){ + table.getTable().rows().deselect(); + } + + // select if moving up + table.getTable().row(tr).select(); + } + } + }); + + $.fn.dataTable.ext.search.push( + function( settings, data, dataIndex ) { + return table.getShowDotFiles() || ! data[2].startsWith('.'); + } + ) + + /* END DATATABLE LISTENERS */ }); class DataTable { @@ -135,6 +248,9 @@ class DataTable { ] }); + $('#directory-contents_filter').prepend(``) + $('#directory-contents_filter').prepend(``) + } async reloadTable(url) { @@ -164,6 +280,10 @@ class DataTable { } } + getShowDotFiles() { + return localStorage.getItem('show-dotfiles') == 'true' + } + getShowOwnerMode() { return localStorage.getItem('show-owner-mode') == 'true' } @@ -183,4 +303,14 @@ class DataTable { return Handlebars.compile(template_str); } + updateDatatablesStatus(){ + // from "function info ( api )" of https://cdn.datatables.net/select/1.3.1/js/dataTables.select.js + let api = this._table; + let rows = api.rows( { selected: true } ).flatten().length, + page_info = api.page.info(), + msg = page_info.recordsTotal == page_info.recordsDisplay ? `Showing ${page_info.recordsDisplay} rows` : `Showing ${page_info.recordsDisplay} of ${page_info.recordsTotal} rows`; + + $('.datatables-status').html(`${msg} - ${rows} rows selected`); + } + } diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js index 80efd63a13..bbbbeadfec 100644 --- a/apps/dashboard/app/javascript/packs/files/FileOps.js +++ b/apps/dashboard/app/javascript/packs/files/FileOps.js @@ -22,6 +22,10 @@ $(document).ready(function () { fileOps.newFolder(options.value); }); + $("#directory-contents").on("fileOpsDownload", function (e, options) { + fileOps.download(options.selection); + }); + }); class FileOps { @@ -57,15 +61,7 @@ class FileOps { newFile(filename) { fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?touch=true`, { method: 'put', headers: { 'X-CSRF-Token': csrf_token } }) - .then(function (response) { - const eventData = { - type: "getDataFromJsonResponse", - response: response - }; - - $("#directory-contents").trigger('getDataFromJsonResponse', eventData); - - }) + .then(response => this.dataFromJsonResponse(response)) .then(function () { $("#directory-contents").trigger('reloadTable'); }) @@ -104,15 +100,7 @@ class FileOps { newFolder(filename) { fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) - .then(function (response) { - const eventData = { - type: "getDataFromJsonResponse", - response: response - }; - - $("#directory-contents").trigger('getDataFromJsonResponse', eventData); - - }) + .then(response => this.dataFromJsonResponse(response)) .then(function () { $("#directory-contents").trigger('reloadTable'); }) @@ -127,5 +115,88 @@ class FileOps { }); } + download(selection) { + selection.toArray().forEach( (f) => { + if(f.type == 'd') { + this.downloadDirectory(f); + } else if (f.type == 'f') { + this.downloadFile(f); + } + }); + } + + downloadDirectory(file) { + let filename = $($.parseHTML(file.name)).text(), + canDownloadReq = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?can_download=${Date.now().toString()}` + + const eventData = { + 'message': 'preparing to download directory: ' + file.name, + }; + + $("#directory-contents").trigger('swalShowLoading', eventData); + + fetch(canDownloadReq, { + method: 'GET', + headers: { + 'X-CSRF-Token': csrf_token, + 'Accept': 'application/json' + } + }) + .then(response => this.dataFromJsonResponse(response)) + .then(data => { + if (data.can_download) { + $("#directory-contents").trigger('swalClose'); + this.downloadFile(file) + } else { + const eventData = { + 'title': 'Error while downloading', + 'message': data.error_message, + }; + + $("#directory-contents").trigger('swalClose'); + $("#directory-contents").trigger('showError', eventData); + } + }) + .catch(e => { + const eventData = { + 'title': 'Error while downloading', + 'message': e.message, + }; + + $("#directory-contents").trigger('swalClose'); + $("#directory-contents").trigger('showError', eventData); + }) + } + + + downloadFile(file) { + // creating the temporary iframe is exactly what the CloudCmd does + // so this just repeats the status quo + + let filename = $($.parseHTML(file.name)).text(), + downloadUrl = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?download=${Date.now().toString()}`, + iframe = document.createElement('iframe'), + TIME = 30 * 1000; + + iframe.setAttribute('class', 'd-none'); + iframe.setAttribute('src', downloadUrl); + + document.body.appendChild(iframe); + + setTimeout(function() { + document.body.removeChild(iframe); + }, TIME); + } + + dataFromJsonResponse(response) { + return new Promise((resolve, reject) => { + Promise.resolve(response) + .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) + .then(response => response.json()) + .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) + .catch((e) => reject(e)) + }); + } + } \ No newline at end of file diff --git a/apps/dashboard/app/javascript/packs/files/SweetAlert.js b/apps/dashboard/app/javascript/packs/files/SweetAlert.js index 1a6546f672..4dbbf23fea 100644 --- a/apps/dashboard/app/javascript/packs/files/SweetAlert.js +++ b/apps/dashboard/app/javascript/packs/files/SweetAlert.js @@ -16,6 +16,14 @@ $(document).ready(function(){ sweetAlert.input(options); }); + $("#directory-contents").on("swalShowLoading", function(e,options) { + sweetAlert.loading(options.message); + }); + + $("#directory-contents").on("swalClose", function() { + sweetAlert.close(); + }); + }); class SweetAlert { @@ -54,7 +62,7 @@ class SweetAlert { this._swal.fire(error_title, error_message, 'error'); } - loading(title) { + async loading(title) { this._swal.fire({ title: title, allowOutsideClick: false, @@ -63,7 +71,7 @@ class SweetAlert { }); } - doneLoading() { + close() { this._swal.close(); } } From 976e64f0910fca5435d0334504978a7f98806fa0 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Wed, 23 Mar 2022 11:34:44 -0500 Subject: [PATCH 17/47] Completed Delete Functionality --- .../app/javascript/packs/files/DataTable.js | 20 +++ .../app/javascript/packs/files/FileOps.js | 150 ++++++++++++++++++ .../app/javascript/packs/files/SweetAlert.js | 7 +- 3 files changed, 176 insertions(+), 1 deletion(-) diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js index e8a99dc4da..a7cce51af5 100644 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -41,6 +41,26 @@ $(document).ready(function () { }); + $("#delete-btn").on("click", function () { + let files = table.getTable().rows({selected: true}).data().toArray().map((f) => f.name); + if(files.length == 0) { + const eventData = { + 'title': 'Select a file, files, or directory to delete.', + 'message': 'You have selected none.', + }; + + $("#directory-contents").trigger('swalShowError', eventData); + + } else { + const eventData = { + files: files + }; + + $("#directory-contents").trigger('fileOpsDeletePrompt', eventData); + } + + }); + // Will have to work on this one later. Not so straight forward. // // $("#upload-btn").on("click", function () { diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js index bbbbeadfec..2a2f978639 100644 --- a/apps/dashboard/app/javascript/packs/files/FileOps.js +++ b/apps/dashboard/app/javascript/packs/files/FileOps.js @@ -1,3 +1,5 @@ +import Handlebars from 'handlebars'; + let fileOps = null; $(document).ready(function () { @@ -26,10 +28,22 @@ $(document).ready(function () { fileOps.download(options.selection); }); + $("#directory-contents").on("fileOpsDeletePrompt", function (e, options) { + fileOps.deletePrompt(options.files); + }); + + $("#directory-contents").on("fileOpsDelete", function (e, options) { + fileOps.delete(options.files); + }); + + }); class FileOps { + _handleBars = null; + constructor() { + this._handleBars = Handlebars; } @@ -198,5 +212,141 @@ class FileOps { .catch((e) => reject(e)) }); } + + deletePrompt(files) { + const eventData = { + action: 'fileOpsDelete', + files: files, + 'inputOptions': { + title: files.length == 1 ? `Delete ${files[0]}?` : `Delete ${files.length} selected files?`, + text: 'Are you sure you want to delete the files: ' + files.join(', '), + showCancelButton: true, + } + }; + + $("#directory-contents").trigger('swalShowInput', eventData); + + } + + + removeFiles(files){ + this.transferFiles(files, "rm", "remove files") + $("#directory-contents").trigger('swalClose'); + $("#directory-contents").trigger('reloadTable'); + + } + + delete(files) { + const eventData = { + 'message': 'Deleting files...: ', + }; + + $("#directory-contents").trigger('swalShowLoading', eventData); + + this.removeFiles(files.map(f => [history.state.currentDirectory, f].join('/')), csrf_token); + } + + async transferFiles(files, action, summary) { + const eventData = { + 'message': _.startCase(summary), + }; + + $("#directory-contents").trigger('swalShowLoading', eventData); + + try { + const response = await fetch(transfersPath, { + method: 'post', + body: JSON.stringify({ + command: action, + files: files + }), + headers: { 'X-CSRF-Token': csrf_token } + }); + const data = await this.dataFromJsonResponse(response); + if (!data.completed) { + // was async, gotta report on progress and start polling + this.reportTransfer(data); + } + else { + if (data.target_dir == history.state.currentDirectory) { + $("#directory-contents").trigger('reloadTable'); + } + } + + if (action == 'mv' || action == 'cp') { + // clearClipboard(); + // updateViewForClipboard(); + } + + return; + } catch (e) { + const eventData = { + 'title': 'Error occurred when attempting to ' + summary, + 'message': e.message, + }; + + $("#directory-contents").trigger('showError', eventData); + } + } + + reportTransfer(data) { + // 1. add the transfer label + findAndUpdateTransferStatus(data); + + let attempts = 0 + + // 2. poll for the updates + var poll = function() { + $.getJSON(data.show_json_url, function (newdata) { + findAndUpdateTransferStatus(newdata); + + if(newdata.completed) { + if(! newdata.error_message) { + if(newdata.target_dir == history.state.currentDirectory) { + // reloadTable(); + } + + // 3. fade out after 5 seconds + fadeOutTransferStatus(newdata) + } + } + else { + // not completed yet, so poll again + setTimeout(poll, 1000); + } + }).fail(function() { + if (attempts >= 3) { + // Swal.fire('Operation may not have happened', 'Failed to retrieve file operation status.', 'error'); + } else { + setTimeout(poll, 1000); + attempts++; + } + }); + } + + poll(); + + } + + findAndUpdateTransferStatus(data) { + let id = `#${data.id}`; + + if($(id).length){ + $(id).replaceWith(this.reportTransferTemplate(data)); + } + else{ + $('.transfers-status').append(this.reportTransferTemplate(data)); + } + } + + fadeOutTransferStatus(data){ + let id = `#${data.id}`; + $(id).fadeOut(4000); + } + + reportTransferTemplate = (function(){ + let template_str = $('#transfer-template').html(); + return Handlebars.compile(template_str); + })(); } \ No newline at end of file diff --git a/apps/dashboard/app/javascript/packs/files/SweetAlert.js b/apps/dashboard/app/javascript/packs/files/SweetAlert.js index 4dbbf23fea..38d5f233e8 100644 --- a/apps/dashboard/app/javascript/packs/files/SweetAlert.js +++ b/apps/dashboard/app/javascript/packs/files/SweetAlert.js @@ -38,7 +38,12 @@ class SweetAlert { this._swal.fire(options.inputOptions) .then (function(result){ if(result.isConfirmed) { - $("#directory-contents").trigger(options.action, result); + const eventData = { + result: result, + files: options.files ? options.files : null + }; + + $("#directory-contents").trigger(options.action, eventData); } else { $("#directory-contents").trigger('reloadTable'); } From 702194fcf351d541ed175d089e25c32fe37429bf Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Wed, 23 Mar 2022 16:20:38 -0500 Subject: [PATCH 18/47] WIP Copy/Move --- .../app/javascript/packs/files/ClipBoard.js | 155 ++++++++++++++++++ .../app/javascript/packs/files/DataTable.js | 25 +++ .../app/javascript/packs/files/FileOps.js | 37 +++-- 3 files changed, 207 insertions(+), 10 deletions(-) create mode 100755 apps/dashboard/app/javascript/packs/files/ClipBoard.js diff --git a/apps/dashboard/app/javascript/packs/files/ClipBoard.js b/apps/dashboard/app/javascript/packs/files/ClipBoard.js new file mode 100755 index 0000000000..40e3c11b07 --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/ClipBoard.js @@ -0,0 +1,155 @@ +import ClipboardJS from 'clipboard' +import Handlebars from 'handlebars'; + +$(document).ready(function() { + + var clipBoard = new ClipBoard(); + + $("#directory-contents").on('success', function(e) { + $(e.trigger).tooltip({title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom'}).tooltip('show'); + setTimeout(() => $(e.trigger).tooltip('hide'), 2000); + e.clearSelection(); + }); + + $("#directory-contents").on('error', function(e) { + e.clearSelection(); + }); + + $("#directory-contents").on("clipboardClear", function (e, options) { + clipBoard.clearClipboard(); + clipBoard.updateViewForClipboard(); + }); + + $("#directory-contents").on("updateClipboardFromSelection", function (e, options) { + clipBoard.updateClipboardFromSelection(options.selection); + clipBoard.updateViewForClipboard(); + }); + + $("#directory-contents").on("updateViewForClipboard", function (e, options) { + clipBoard.updateViewForClipboard(); + }); + + +}); + +class ClipBoard { + _clipBoard = null; + _handleBars = null; + + constructor() { + this._clipBoard = new ClipboardJS('#copy-path'); + this._handleBars = Handlebars; + } + + getClipBoard() { + return this._clipBoard; + } + + getHandleBars() { + return this._handleBars; + } + + clearClipboard() { + localStorage.removeItem('filesClipboard'); + } + + updateClipboardFromSelection(selection) { + + if(selection.length == 0){ + this.clearClipboard(); + } else { + let clipboardData = { + from: history.state.currentDirectory, + files: selection.toArray().map((f) => { + return { directory: f.type == 'd', name: f.name }; + }) + }; + + localStorage.setItem('filesClipboard', JSON.stringify(clipboardData)); + } + } + + + updateViewForClipboard() { + let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || '{}'), + template_str = $('#clipboard-template').html(), + template = this._handleBars.compile(template_str); + + $('#clipboard').html(template(clipboard)); + + $('#clipboard-clear').on("click", () => { + this.clearClipboard(); + this.updateViewForClipboard(); + }); + + $('#clipboard-move-to-dir').on("click", () => { + let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || 'null'); + if(clipboard){ + clipboard.to = history.state.currentDirectory; + + if(clipboard.from == clipboard.to){ + console.error('clipboard from and to are identical') + // TODO: + } + else{ + let files = {}; + clipboard.files.forEach((f) => { + files[`${clipboard.from}/${f.name}`] = `${history.state.currentDirectory}/${f.name}` + }); + + const eventData = { + 'files': files, + 'token': csrf_token + }; + + $("#directory-contents").trigger('fileOpsMove', eventData); + } + } + else{ + console.error('files clipboard is empty'); + } + }); + + + $('#clipboard-copy-to-dir').on("click", () => { + let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || 'null'); + + if(clipboard){ + clipboard.to = history.state.currentDirectory; + + if(clipboard.from == clipboard.to){ + console.error('clipboard from and to are identical') + + // TODO: we want to support this use case + // copy and paste as a new filename + // but lots of edge cases + // (overwrite or rename duplicates) + // _copy + // _copy_2 + // _copy_3 + // _copy_4 + } + else{ + // [{"/from/file/path":"/to/file/path" }] + let files = {}; + clipboard.files.forEach((f) => { + files[`${clipboard.from}/${f.name}`] = `${history.state.currentDirectory}/${f.name}` + }); + + const eventData = { + 'files': files, + 'token': csrf_token + }; + + $("#directory-contents").trigger('fileOpsCopy', eventData); + } + } + else{ + console.error('files clipboard is empty'); + } + }); + + } + + +} diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js index a7cce51af5..9833052dbc 100644 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -6,6 +6,7 @@ import Handlebars from 'handlebars'; import {} from './SweetAlert.js'; import {} from './FileOps.js'; import {} from './UppyOps.js'; +import {} from './ClipBoard.js'; let table = null; @@ -61,6 +62,28 @@ $(document).ready(function () { }); + $("#copy-move-btn").on("click", function () { + let selection = table.getTable().rows({selected: true}).data(); + if(selection.length == 0) { + const eventData = { + 'title': 'Select a file, files, or directory to copy or move.', + 'message': 'You have selected none.', + }; + + $("#directory-contents").trigger('swalShowError', eventData); + $("#directory-contents").trigger('clipboardClear', eventData); + + } else { + const eventData = { + selection: selection + }; + + $("#directory-contents").trigger('updateClipboardFromSelection', eventData); + } + + }); + + // Will have to work on this one later. Not so straight forward. // // $("#upload-btn").on("click", function () { @@ -286,6 +309,8 @@ class DataTable { $('#open-in-terminal-btn').attr('href', data.shell_url); $('#open-in-terminal-btn').removeClass('disabled'); + + $("#directory-contents").trigger('updateViewForClipboard'); return await Promise.resolve(data); } catch (e) { const eventData = { diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js index 2a2f978639..88888dbce0 100644 --- a/apps/dashboard/app/javascript/packs/files/FileOps.js +++ b/apps/dashboard/app/javascript/packs/files/FileOps.js @@ -13,16 +13,16 @@ $(document).ready(function () { }); $("#directory-contents").on("fileOpsCreateFile", function (e, options) { - fileOps.newFile(options.value); + fileOps.newFile(options.result.value); }); $("#directory-contents").on("fileOpsCreateFolder", function (e, options) { - fileOps.newFolder(options.value); + fileOps.newFolder(options.result.value); }); - $("#directory-contents").on("fileOpsUpload", function (e, options) { - fileOps.newFolder(options.value); - }); + // $("#directory-contents").on("fileOpsUpload", function (e, options) { + // fileOps.newFolder(options.value); + // }); $("#directory-contents").on("fileOpsDownload", function (e, options) { fileOps.download(options.selection); @@ -36,6 +36,13 @@ $(document).ready(function () { fileOps.delete(options.files); }); + $("#directory-contents").on("fileOpsMove", function (e, options) { + fileOps.move(options.files, options.token); + }); + + $("#directory-contents").on("fileOpsCopy", function (e, options) { + fileOps.copy(options.files, options.token); + }); }); @@ -274,8 +281,8 @@ class FileOps { } if (action == 'mv' || action == 'cp') { - // clearClipboard(); - // updateViewForClipboard(); + $("#directory-contents").trigger('clipboardClear'); + $("#directory-contents").trigger('reloadTable'); } return; @@ -298,7 +305,7 @@ class FileOps { // 2. poll for the updates var poll = function() { $.getJSON(data.show_json_url, function (newdata) { - findAndUpdateTransferStatus(newdata); + this.findAndUpdateTransferStatus(newdata); if(newdata.completed) { if(! newdata.error_message) { @@ -307,7 +314,7 @@ class FileOps { } // 3. fade out after 5 seconds - fadeOutTransferStatus(newdata) + this.fadeOutTransferStatus(newdata) } } else { @@ -344,9 +351,19 @@ class FileOps { $(id).fadeOut(4000); } + move(files, token) { + this.transferFiles(files, 'mv', 'move files'); + $("#directory-contents").trigger('swalClose'); + } + + copy(files, token) { + this.transferFiles(files, 'cp', 'copy files'); + $("#directory-contents").trigger('swalClose'); + } + reportTransferTemplate = (function(){ let template_str = $('#transfer-template').html(); return Handlebars.compile(template_str); })(); - + } \ No newline at end of file From 5092c6b11fae19fc9eca01b09ee0170dab5f6c03 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Fri, 25 Mar 2022 16:27:34 -0400 Subject: [PATCH 19/47] WIP --- .../app/javascript/packs/files/FileOps.js | 219 +++++++++--------- .../app/views/files/_inline_js.html.erb | 2 +- 2 files changed, 112 insertions(+), 109 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js index 88888dbce0..e0d17df33f 100644 --- a/apps/dashboard/app/javascript/packs/files/FileOps.js +++ b/apps/dashboard/app/javascript/packs/files/FileOps.js @@ -2,8 +2,17 @@ import Handlebars from 'handlebars'; let fileOps = null; +let reportTransferTemplate = null; + $(document).ready(function () { fileOps = new FileOps(); + + reportTransferTemplate = (function(){ + let template_str = $('#transfer-template').html(); + return Handlebars.compile(template_str); + })(); + + $("#directory-contents").on("fileOpsNewFile", function () { fileOps.newFilePrompt(); }); @@ -48,7 +57,8 @@ $(document).ready(function () { class FileOps { _handleBars = null; - + _timeout = 2000; + constructor() { this._handleBars = Handlebars; } @@ -84,16 +94,10 @@ class FileOps { fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?touch=true`, { method: 'put', headers: { 'X-CSRF-Token': csrf_token } }) .then(response => this.dataFromJsonResponse(response)) .then(function () { - $("#directory-contents").trigger('reloadTable'); + this.reloadTable(); }) .catch(function (e) { - const eventData = { - 'title': 'Error occurred when attempting to create new file', - 'message': e.message, - }; - - $("#directory-contents").trigger('swalShowError', eventData); - + this.alertError('Error occurred when attempting to create new file', e.message); }); } @@ -123,16 +127,10 @@ class FileOps { fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) .then(response => this.dataFromJsonResponse(response)) .then(function () { - $("#directory-contents").trigger('reloadTable'); + this.reloadTable(); }) .catch(function (e) { - const eventData = { - 'title': 'Error occurred when attempting to create new folder', - 'message': e.message, - }; - - $("#directory-contents").trigger('swalShowError', eventData); - + this.alertError('Error occurred when attempting to create new folder', e.message); }); } @@ -149,12 +147,8 @@ class FileOps { downloadDirectory(file) { let filename = $($.parseHTML(file.name)).text(), canDownloadReq = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?can_download=${Date.now().toString()}` - - const eventData = { - 'message': 'preparing to download directory: ' + file.name, - }; - $("#directory-contents").trigger('swalShowLoading', eventData); + this.showSwalLoading('preparing to download directory: ' + file.name); fetch(canDownloadReq, { method: 'GET', @@ -166,17 +160,11 @@ class FileOps { .then(response => this.dataFromJsonResponse(response)) .then(data => { if (data.can_download) { - $("#directory-contents").trigger('swalClose'); + this.doneLoading(); this.downloadFile(file) } else { - const eventData = { - 'title': 'Error while downloading', - 'message': data.error_message, - }; - - $("#directory-contents").trigger('swalClose'); - $("#directory-contents").trigger('showError', eventData); - + this.doneLoading(); + this.alertError('Error while downloading', data.error_message); } }) .catch(e => { @@ -185,8 +173,8 @@ class FileOps { 'message': e.message, }; - $("#directory-contents").trigger('swalClose'); - $("#directory-contents").trigger('showError', eventData); + this.doneLoading(); + this.alertError('Error while downloading', data.error_message); }) } @@ -238,132 +226,147 @@ class FileOps { removeFiles(files){ this.transferFiles(files, "rm", "remove files") - $("#directory-contents").trigger('swalClose'); - $("#directory-contents").trigger('reloadTable'); - + this.doneLoading(); + this.reloadTable(); } delete(files) { - const eventData = { - 'message': 'Deleting files...: ', - }; - - $("#directory-contents").trigger('swalShowLoading', eventData); + this.showSwalLoading('Deleting files...: '); this.removeFiles(files.map(f => [history.state.currentDirectory, f].join('/')), csrf_token); } - async transferFiles(files, action, summary) { - const eventData = { - 'message': _.startCase(summary), - }; + transferFiles(files, action, summary){ - $("#directory-contents").trigger('swalShowLoading', eventData); + this.showSwalLoading(_.startCase(summary)); - try { - const response = await fetch(transfersPath, { - method: 'post', - body: JSON.stringify({ - command: action, - files: files - }), - headers: { 'X-CSRF-Token': csrf_token } - }); - const data = await this.dataFromJsonResponse(response); - if (!data.completed) { + return fetch(transfersPath, { + method: 'post', + body: JSON.stringify({ + command: action, + files: files + }), + headers: { 'X-CSRF-Token': csrf_token } + }) + .then(response => dataFromJsonResponse(response)) + .then((data) => { + + if(! data.completed){ // was async, gotta report on progress and start polling this.reportTransfer(data); } else { - if (data.target_dir == history.state.currentDirectory) { - $("#directory-contents").trigger('reloadTable'); + if(data.target_dir == history.state.currentDirectory){ + this.reloadTable(); } } - - if (action == 'mv' || action == 'cp') { - $("#directory-contents").trigger('clipboardClear'); - $("#directory-contents").trigger('reloadTable'); + + if(action == 'mv' || action == 'cp') { + this.clearClipboard(); } - - return; - } catch (e) { - const eventData = { - 'title': 'Error occurred when attempting to ' + summary, - 'message': e.message, - }; - - $("#directory-contents").trigger('showError', eventData); + }) + .then( + () => this.doneLoading() + ) + .catch(e => this.alertError('Error occurred when attempting to ' + summary, e.message)) + } + + findAndUpdateTransferStatus(data){ + let id = `#${data.id}`; + + if($(id).length){ + $(id).replaceWith(this.reportTransferTemplate(data)); } + else{ + $('.transfers-status').append(this.reportTransferTemplate(data)); + } + } + + fadeOutTransferStatus(data){ + let id = `#${data.id}`; + $(id).fadeOut(4000); } - reportTransfer(data) { + + reportTransfer(data){ // 1. add the transfer label findAndUpdateTransferStatus(data); - + let attempts = 0 - + // 2. poll for the updates var poll = function() { $.getJSON(data.show_json_url, function (newdata) { - this.findAndUpdateTransferStatus(newdata); - + findAndUpdateTransferStatus(newdata); + if(newdata.completed) { if(! newdata.error_message) { if(newdata.target_dir == history.state.currentDirectory) { - // reloadTable(); + this.reloadTable(); } - + // 3. fade out after 5 seconds this.fadeOutTransferStatus(newdata) } } else { // not completed yet, so poll again - setTimeout(poll, 1000); + setTimeout(function(){ + attempts++; + }, this._timeout); } }).fail(function() { if (attempts >= 3) { - // Swal.fire('Operation may not have happened', 'Failed to retrieve file operation status.', 'error'); + this.alertError('Operation may not have happened', 'Failed to retrieve file operation status.'); } else { - setTimeout(poll, 1000); - attempts++; + setTimeout(function(){ + attempts++; + }, this._timeout); } }); } - - poll(); - - } - - findAndUpdateTransferStatus(data) { - let id = `#${data.id}`; - if($(id).length){ - $(id).replaceWith(this.reportTransferTemplate(data)); - } - else{ - $('.transfers-status').append(this.reportTransferTemplate(data)); - } - } - - fadeOutTransferStatus(data){ - let id = `#${data.id}`; - $(id).fadeOut(4000); - } + poll(); + } move(files, token) { this.transferFiles(files, 'mv', 'move files'); - $("#directory-contents").trigger('swalClose'); + this.doneLoading(); } copy(files, token) { this.transferFiles(files, 'cp', 'copy files'); + this.doneLoading(); + } + + alertError(title, message) { + const eventData = { + 'title': title, + 'message': message, + }; + + $("#directory-contents").trigger('showError', eventData); + + } + + doneLoading() { $("#directory-contents").trigger('swalClose'); } - reportTransferTemplate = (function(){ - let template_str = $('#transfer-template').html(); - return Handlebars.compile(template_str); - })(); - + clearClipboard() { + $("#directory-contents").trigger('clipboardClear'); + } + + reloadTable() { + $("#directory-contents").trigger('reloadTable'); + } + + showSwalLoading (message) { + const eventData = { + 'message': message, + }; + + $("#directory-contents").trigger('swalShowLoading', eventData); + + } } \ No newline at end of file diff --git a/apps/dashboard/app/views/files/_inline_js.html.erb b/apps/dashboard/app/views/files/_inline_js.html.erb index 8af291d1cf..787be489eb 100755 --- a/apps/dashboard/app/views/files/_inline_js.html.erb +++ b/apps/dashboard/app/views/files/_inline_js.html.erb @@ -1,3 +1,4 @@ +

<%= transfers_path(format: "json") %> diff --git a/apps/dashboard/app/views/files/_inline_js.html.erb b/apps/dashboard/app/views/files/_inline_js.html.erb index 3c84e18ad9..ee60b31e2d 100755 --- a/apps/dashboard/app/views/files/_inline_js.html.erb +++ b/apps/dashboard/app/views/files/_inline_js.html.erb @@ -12,19 +12,4 @@ history.replaceState({ currentDirectoryUpdatedAt: '<%= Time.now.to_i %>' }, null); -/* -$(document).ready(function(){ - clipboardjs = new ClipboardJS('#copy-path'); - clipboardjs.on('success', function(e) { - //FIXME: Have to put this in the template so it will recognize tooltip. - - $(e.trigger).tooltip({title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom'}).tooltip('show'); - setTimeout(() => $(e.trigger).tooltip('hide'), 2000); - e.clearSelection(); - }); - clipboardjs.on('error', function(e) { - e.clearSelection(); - }); -}); -*/ From b9c508d22debe3611738387237eeae61f4b40ce2 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Wed, 30 Mar 2022 17:44:44 -0400 Subject: [PATCH 23/47] Completed Datatable file operations --- .../app/javascript/packs/files/DataTable.js | 65 ++++++++++++++++ .../app/javascript/packs/files/FileOps.js | 76 +++++++++++++++++-- 2 files changed, 135 insertions(+), 6 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js index d5dcda5adc..1d7d2a5c7a 100644 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -10,6 +10,7 @@ import {} from './ClipBoard.js'; let table = null; + $(document).ready(function () { table = new DataTable(); @@ -102,6 +103,49 @@ $(document).ready(function () { table.dataFromJsonResponse(options.response); }); + $(document).on('click','.rename-file', function(e) { + e.preventDefault(); + let rowId = e.currentTarget.dataset.rowIndex; + let row = table.getTable().row(rowId).data(); + let fileName = $($.parseHTML(row.name)).text(); + + const eventData = { + file: fileName, + }; + + $("#directory-contents").trigger('fileOpsRenameFilePrompt', eventData); + + }); + + $(document).on('click','.delete-file', function(e) { + e.preventDefault(); + let rowId = e.currentTarget.dataset.rowIndex; + let row = table.getTable().row(rowId).data(); + let fileName = $($.parseHTML(row.name)).text(); + + const eventData = { + files: [fileName] + }; + + $("#directory-contents").trigger('fileOpsDeletePrompt', eventData); + + }); + + $('#show-dotfiles').on('change', () => { + let visible = $('#show-dotfiles').is(':checked'); + + table.setShowDotFiles(visible); + table.updateDotFileVisibility(); + }); + + $('#show-owner-mode').on('change', () => { + let visible = $('#show-owner-mode').is(':checked'); + + table.setShowOwnerMode(visible); + table.updateShowOwnerModeVisibility(); + }); + + /* END TABLE ACTIONS */ /* DATATABLE LISTENERS */ @@ -325,6 +369,27 @@ class DataTable { } } + updateDotFileVisibility() { + this.reloadTable(); + } + + updateShowOwnerModeVisibility() { + let visible = this.getShowOwnerMode(); + + this._table.column('owner:name').visible(visible); + this._table.column('mode:name').visible(visible); + } + + + setShowOwnerMode(visible) { + localStorage.setItem('show-owner-mode', new Boolean(visible)); + } + + setShowDotFiles(visible) { + localStorage.setItem('show-dotfiles', new Boolean(visible)); + } + + getShowDotFiles() { return localStorage.getItem('show-dotfiles') == 'true' } diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js index 7a42757e5c..595ba1cb6b 100644 --- a/apps/dashboard/app/javascript/packs/files/FileOps.js +++ b/apps/dashboard/app/javascript/packs/files/FileOps.js @@ -15,6 +15,14 @@ $(document).ready(function () { fileOps.newFolderPrompt(); }); + $("#directory-contents").on("fileOpsRenameFilePrompt", function (e, options) { + fileOps.renameFilePrompt(options.file); + }); + + $("#directory-contents").on("fileOpsRenameFile", function (e, options) { + fileOps.renameFile(options.files, options.result.value); + }); + $("#directory-contents").on("fileOpsCreateFile", function (e, options) { fileOps.newFile(options.result.value); }); @@ -59,6 +67,60 @@ class FileOps { } + deleteFile(fileName, newFileName) { + let files = {}; + files[`${history.state.currentDirectory}/${fileName}`] = `${history.state.currentDirectory}/${newFileName}`; + this.transferFiles(files, "mv", "rename file") + } + + deleteFilePrompt(files) { + const eventData = { + action: 'fileOpsDeleteFile', + title: files.length == 1 ? `Delete ${files[0]}?` : `Delete ${files.length} selected files?`, + text: 'Are you sure you want to delete the files: ' + files.join(', '), + showCancelButton: true, + } + + $("#directory-contents").trigger('swalShowPrompt', eventData); + } + + + renameFile(fileName, newFileName) { + let files = {}; + files[`${history.state.currentDirectory}/${fileName}`] = `${history.state.currentDirectory}/${newFileName}`; + this.transferFiles(files, "mv", "rename file") + } + + renameFilePrompt(fileName) { + const eventData = { + action: 'fileOpsRenameFile', + files: fileName, + 'inputOptions': { + title: 'Rename', + input: 'text', + inputLabel: 'Filename', + inputValue: fileName, + inputAttributes: { + spellcheck: 'false', + }, + showCancelButton: true, + inputValidator: (value) => { + if (! value) { + // TODO: validate filenames against listing + return 'Provide a filename to rename this to'; + } else if (value.includes('/') || value.includes('..')) { + return 'Filename cannot include / or ..'; + } + } + } + }; + + $("#directory-contents").trigger('swalShowInput', eventData); + + } + + + newFilePrompt() { const eventData = { @@ -86,13 +148,14 @@ class FileOps { } newFile(filename) { + let myFileOp = new FileOps(); fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?touch=true`, { method: 'put', headers: { 'X-CSRF-Token': csrf_token } }) .then(response => this.dataFromJsonResponse(response)) .then(function () { - this.reloadTable(); + myFileOp.reloadTable(); }) .catch(function (e) { - this.alertError('Error occurred when attempting to create new file', e.message); + myFileOp.alertError('Error occurred when attempting to create new file', e.message); }); } @@ -119,13 +182,14 @@ class FileOps { } newFolder(filename) { + let myFileOp = new FileOps(); fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) .then(response => this.dataFromJsonResponse(response)) .then(function () { - this.reloadTable(); + myFileOp.reloadTable(); }) .catch(function (e) { - this.alertError('Error occurred when attempting to create new folder', e.message); + myFileOp.alertError('Error occurred when attempting to create new folder', e.message); }); } @@ -253,7 +317,7 @@ class FileOps { } else { // if(data.target_dir == history.state.currentDirectory){ // } - this.findAndUpdateTransferStatus(data); + // this.findAndUpdateTransferStatus(data); } if(action == 'mv' || action == 'cp') { @@ -261,9 +325,9 @@ class FileOps { this.clearClipboard(); } + this.fadeOutTransferStatus(data); this.doneLoading(); this.reloadTable(); - this.fadeOutTransferStatus(data); }) .then(() => this.doneLoading()) From 530bf2bddea78460fdbf0d5630a91324b510f7ff Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Wed, 30 Mar 2022 18:47:20 -0400 Subject: [PATCH 24/47] Completed JS Refactor. Ready for Code Review --- .../app/javascript/packs/files/DataTable.js | 7 ++- .../app/javascript/packs/files/FileOps.js | 52 +++++++++++++++++-- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js index 1d7d2a5c7a..aee847696f 100644 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -84,6 +84,9 @@ $(document).ready(function () { }); + $("#goto-btn").on("click", function () { + $("#directory-contents").trigger('changeDirectoryPrompt'); + }); // Will have to work on this one later. Not so straight forward. // @@ -95,8 +98,8 @@ $(document).ready(function () { /* TABLE ACTIONS */ - $("#directory-contents").on("reloadTable", function () { - table.reloadTable(); + $("#directory-contents").on("reloadTable", function (e, options) { + table.reloadTable(options.url); }); $("#directory-contents").on("getDataFromJsonResponse", function (e, options) { diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js index 595ba1cb6b..0e3cb66d26 100644 --- a/apps/dashboard/app/javascript/packs/files/FileOps.js +++ b/apps/dashboard/app/javascript/packs/files/FileOps.js @@ -55,17 +55,55 @@ $(document).ready(function () { fileOps.copy(options.files, options.token); }); + $("#directory-contents").on("changeDirectoryPrompt", function () { + fileOps.changeDirectoryPrompt(); + }); + + $("#directory-contents").on("changeDirectory", function (e, options) { + fileOps.changeDirectory(options.result.value); + }); + }); class FileOps { _handleBars = null; _timeout = 2000; _attempts = 0; - + _filesPath = filesPath; + constructor() { this._handleBars = Handlebars; } + changeDirectory(path) { + this.goto(filesPath + path); + } + + changeDirectoryPrompt() { + const eventData = { + action: 'changeDirectory', + 'inputOptions': { + title: 'Change Directory', + input: 'text', + inputLabel: 'Path', + inputValue: history.state.currentDirectory, + inputAttributes: { + spellcheck: 'false', + }, + showCancelButton: true, + inputValidator: (value) => { + if (! value || ! value.startsWith('/')) { + // TODO: validate filenames against listing + return 'Provide an absolute pathname' + } + } + } + }; + + $("#directory-contents").trigger('swalShowInput', eventData); + + } + deleteFile(fileName, newFileName) { let files = {}; @@ -420,8 +458,12 @@ class FileOps { $("#directory-contents").trigger('clipboardClear'); } - reloadTable() { - $("#directory-contents").trigger('reloadTable'); + reloadTable(url) { + const eventData = { + 'url': url, + }; + + $("#directory-contents").trigger('reloadTable', eventData); } showSwalLoading (message) { @@ -432,4 +474,8 @@ class FileOps { $("#directory-contents").trigger('swalShowLoading', eventData); } + + goto(url) { + window.open(url,"_self"); + } } \ No newline at end of file From 3917327ab8e0fcdd6d8fefccafcb37f76c9b5067 Mon Sep 17 00:00:00 2001 From: Gerald Byrket <88058640+gbsoftwaresolutions@users.noreply.github.com> Date: Thu, 31 Mar 2022 14:54:46 -0500 Subject: [PATCH 25/47] Move what was in document.ready to outside --- .../app/javascript/packs/files/UppyOps.js | 212 +++++++++--------- 1 file changed, 103 insertions(+), 109 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/UppyOps.js b/apps/dashboard/app/javascript/packs/files/UppyOps.js index 7055ce88d1..9a0bfcf2ab 100755 --- a/apps/dashboard/app/javascript/packs/files/UppyOps.js +++ b/apps/dashboard/app/javascript/packs/files/UppyOps.js @@ -4,140 +4,134 @@ import XHRUpload from '@uppy/xhr-upload' import _ from 'lodash'; -let uppy = null; +class EmptyDirCreator extends BasePlugin { + constructor (uppy, opts){ + super(uppy, opts) + this.id = this.opts.id || 'EmptyDirUploaderCatcher'; + this.type = 'acquirer'; -$(document).ready(function() { + this.empty_dirs = []; + this.last_entries = []; - class EmptyDirCreator extends BasePlugin { - constructor (uppy, opts){ - super(uppy, opts) - this.id = this.opts.id || 'EmptyDirUploaderCatcher'; - this.type = 'acquirer'; + this.handleRootDrop = this.handleRootDrop.bind(this); + this.createEmptyDirs = this.createEmptyDirs.bind(this); - this.empty_dirs = []; - this.last_entries = []; - - this.handleRootDrop = this.handleRootDrop.bind(this); - this.createEmptyDirs = this.createEmptyDirs.bind(this); - - this.uppy = uppy; - } + this.uppy = uppy; + } - handleRootDrop (e) { - // from https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/getDroppedFiles/index.js - if (e.dataTransfer.items && e.dataTransfer.items[0] && 'webkitGetAsEntry' in e.dataTransfer.items[0]) { - // toArray https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/toArray.js#L4 - let items = Array.prototype.slice.call(e.dataTransfer.items || [], 0); - let entries = items.map(i => i.webkitGetAsEntry()).filter(i => i); + handleRootDrop (e) { + // from https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/getDroppedFiles/index.js + if (e.dataTransfer.items && e.dataTransfer.items[0] && 'webkitGetAsEntry' in e.dataTransfer.items[0]) { + // toArray https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/toArray.js#L4 + let items = Array.prototype.slice.call(e.dataTransfer.items || [], 0); + let entries = items.map(i => i.webkitGetAsEntry()).filter(i => i); - return Promise.all(entries.map(i => getEmptyDirs(i))).then((dirs) => { - this.empty_dirs = this.empty_dirs.concat(_.flattenDeep(dirs)); + return Promise.all(entries.map(i => getEmptyDirs(i))).then((dirs) => { + this.empty_dirs = this.empty_dirs.concat(_.flattenDeep(dirs)); - }); - } - //else we don't have access to directory information + }); } + //else we don't have access to directory information + } - createEmptyDirs (ids) { - if(! this.uppy.getState().error){ // avoid creating empty dirs if error occurred during upload + createEmptyDirs (ids) { + if(! this.uppy.getState().error){ // avoid creating empty dirs if error occurred during upload - //TODO: error checking and reporting - return Promise.all(this.empty_dirs.map((d) => { - // "fullPath" should actually be the path relative to the current directory - let filename = _.trimStart(d.fullPath, '/'); + //TODO: error checking and reporting + return Promise.all(this.empty_dirs.map((d) => { + // "fullPath" should actually be the path relative to the current directory + let filename = _.trimStart(d.fullPath, '/'); - return fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) - //TODO: parse json response verify if there was an error creating directory and handle error + return fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) + //TODO: parse json response verify if there was an error creating directory and handle error - })).then(() => this.empty_dirs = []); - } + })).then(() => this.empty_dirs = []); } + } - install () { - this.uppy.addPostProcessor(this.createEmptyDirs); - } + install () { + this.uppy.addPostProcessor(this.createEmptyDirs); + } - uninstall () { - this.uppy.removePostProcessor(this.createEmptyDirs); - } + uninstall () { + this.uppy.removePostProcessor(this.createEmptyDirs); } +} - uppy = new Uppy({ - restrictions: { - maxFileSize: maxFileSize, - } - }); - - uppy.use(EmptyDirCreator); - uppy.use(Dashboard, { - trigger: '#upload-btn', - fileManagerSelectionType: 'both', - disableThumbnailGenerator: true, - showLinkToFileUploadResult: false, - closeModalOnClickOutside: true, - closeAfterFinish: true, - allowMultipleUploads: false, - onRequestCloseModal: () => closeAndResetUppyModal(uppy), - note: 'Empty directories will be included in the upload only when a directory upload is initiated via drag and drop. This is because the File and Directory Entries API is available only on a drop event, not during an input change event.' - }); - uppy.use(XHRUpload, { - endpoint: filesUploadPath, - withCredentials: true, - fieldName: 'file', - limit: 1, - headers: { 'X-CSRF-Token': csrf_token }, - timeout: 128 * 1000, - }); +uppy = new Uppy({ + restrictions: { + maxFileSize: maxFileSize, + } +}); - uppy.on('file-added', (file) => { - uppy.setFileMeta(file.id, { parent: history.state.currentDirectory }); - if(file.meta.relativePath == null && file.data.webkitRelativePath){ - uppy.setFileMeta(file.id, { relativePath: file.data.webkitRelativePath }); - } - }); +uppy.use(EmptyDirCreator); +uppy.use(Dashboard, { + trigger: '#upload-btn', + fileManagerSelectionType: 'both', + disableThumbnailGenerator: true, + showLinkToFileUploadResult: false, + closeModalOnClickOutside: true, + closeAfterFinish: true, + allowMultipleUploads: false, + onRequestCloseModal: () => closeAndResetUppyModal(uppy), + note: 'Empty directories will be included in the upload only when a directory upload is initiated via drag and drop. This is because the File and Directory Entries API is available only on a drop event, not during an input change event.' +}); +uppy.use(XHRUpload, { + endpoint: filesUploadPath, + withCredentials: true, + fieldName: 'file', + limit: 1, + headers: { 'X-CSRF-Token': csrf_token }, + timeout: 128 * 1000, +}); - uppy.on('complete', (result) => { - if(result.successful.length > 0){ - reloadTable(); - } - }); +uppy.on('file-added', (file) => { + uppy.setFileMeta(file.id, { parent: history.state.currentDirectory }); + if(file.meta.relativePath == null && file.data.webkitRelativePath){ + uppy.setFileMeta(file.id, { relativePath: file.data.webkitRelativePath }); + } +}); - // https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file - global.addEventListener("dragover",function(e){ - e = e || event; - e.preventDefault(); - },false); - - global.addEventListener("drop",function(e){ - e = e || event; - e.preventDefault(); - },false); - - $('#directory-contents').on('drop', function(e){ - this.classList.remove('dragover'); - // Prevent default behavior (Prevent file from being opened) - - // pass drop event to uppy dashboard - uppy.getPlugin('Dashboard').openModal().then(() => uppy.getPlugin('Dashboard').handleDrop(e.originalEvent)) - }); +uppy.on('complete', (result) => { + if(result.successful.length > 0){ + reloadTable(); + } +}); - $('#directory-contents').on('dragover', function(e){ - this.classList.add('dragover'); +// https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file +global.addEventListener("dragover",function(e){ + e = e || event; + e.preventDefault(); +},false); - // Prevent default behavior (Prevent file from being opened) - e.preventDefault(); +global.addEventListener("drop",function(e){ + e = e || event; + e.preventDefault(); +},false); - // specifies what feedback will be shown to the user by setting the dropEffect attribute of the DataTransfer associated with the event - // too bad we can't show an indicator (no dragstart/end when dragging from OS to browser) - e.originalEvent.dataTransfer.dropEffect = 'copy'; - }); +$('#directory-contents').on('drop', function(e){ + this.classList.remove('dragover'); + // Prevent default behavior (Prevent file from being opened) - $('#directory-contents').on('dragleave', function(e){ - this.classList.remove('dragover'); - }); + // pass drop event to uppy dashboard + uppy.getPlugin('Dashboard').openModal().then(() => uppy.getPlugin('Dashboard').handleDrop(e.originalEvent)) +}); + +$('#directory-contents').on('dragover', function(e){ + this.classList.add('dragover'); + // Prevent default behavior (Prevent file from being opened) + e.preventDefault(); + + // specifies what feedback will be shown to the user by setting the dropEffect attribute of the DataTransfer associated with the event + // too bad we can't show an indicator (no dragstart/end when dragging from OS to browser) + e.originalEvent.dataTransfer.dropEffect = 'copy'; +}); + +$('#directory-contents').on('dragleave', function(e){ + this.classList.remove('dragover'); }); function closeAndResetUppyModal(uppy){ @@ -169,4 +163,4 @@ function getEmptyDirs(entry){ function reloadTable() { $("#directory-contents").trigger('reloadTable'); -} \ No newline at end of file +} From 08deeb9dd73d1ddee98068c7e19b078cda53e8a3 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Thu, 31 Mar 2022 16:01:59 -0400 Subject: [PATCH 26/47] Undo last commit --- .../app/javascript/packs/files/UppyOps.js | 212 +++++++++--------- 1 file changed, 109 insertions(+), 103 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/UppyOps.js b/apps/dashboard/app/javascript/packs/files/UppyOps.js index 9a0bfcf2ab..7055ce88d1 100755 --- a/apps/dashboard/app/javascript/packs/files/UppyOps.js +++ b/apps/dashboard/app/javascript/packs/files/UppyOps.js @@ -4,134 +4,140 @@ import XHRUpload from '@uppy/xhr-upload' import _ from 'lodash'; -class EmptyDirCreator extends BasePlugin { - constructor (uppy, opts){ - super(uppy, opts) - this.id = this.opts.id || 'EmptyDirUploaderCatcher'; - this.type = 'acquirer'; +let uppy = null; - this.empty_dirs = []; - this.last_entries = []; - - this.handleRootDrop = this.handleRootDrop.bind(this); - this.createEmptyDirs = this.createEmptyDirs.bind(this); - - this.uppy = uppy; - } +$(document).ready(function() { + class EmptyDirCreator extends BasePlugin { + constructor (uppy, opts){ + super(uppy, opts) + this.id = this.opts.id || 'EmptyDirUploaderCatcher'; + this.type = 'acquirer'; + this.empty_dirs = []; + this.last_entries = []; - handleRootDrop (e) { - // from https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/getDroppedFiles/index.js - if (e.dataTransfer.items && e.dataTransfer.items[0] && 'webkitGetAsEntry' in e.dataTransfer.items[0]) { - // toArray https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/toArray.js#L4 - let items = Array.prototype.slice.call(e.dataTransfer.items || [], 0); - let entries = items.map(i => i.webkitGetAsEntry()).filter(i => i); + this.handleRootDrop = this.handleRootDrop.bind(this); + this.createEmptyDirs = this.createEmptyDirs.bind(this); - return Promise.all(entries.map(i => getEmptyDirs(i))).then((dirs) => { - this.empty_dirs = this.empty_dirs.concat(_.flattenDeep(dirs)); - - }); + this.uppy = uppy; } - //else we don't have access to directory information - } - createEmptyDirs (ids) { - if(! this.uppy.getState().error){ // avoid creating empty dirs if error occurred during upload - //TODO: error checking and reporting - return Promise.all(this.empty_dirs.map((d) => { - // "fullPath" should actually be the path relative to the current directory - let filename = _.trimStart(d.fullPath, '/'); - return fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) - //TODO: parse json response verify if there was an error creating directory and handle error + handleRootDrop (e) { + // from https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/getDroppedFiles/index.js + if (e.dataTransfer.items && e.dataTransfer.items[0] && 'webkitGetAsEntry' in e.dataTransfer.items[0]) { + // toArray https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/toArray.js#L4 + let items = Array.prototype.slice.call(e.dataTransfer.items || [], 0); + let entries = items.map(i => i.webkitGetAsEntry()).filter(i => i); - })).then(() => this.empty_dirs = []); + return Promise.all(entries.map(i => getEmptyDirs(i))).then((dirs) => { + this.empty_dirs = this.empty_dirs.concat(_.flattenDeep(dirs)); + + }); + } + //else we don't have access to directory information } - } - install () { - this.uppy.addPostProcessor(this.createEmptyDirs); - } + createEmptyDirs (ids) { + if(! this.uppy.getState().error){ // avoid creating empty dirs if error occurred during upload - uninstall () { - this.uppy.removePostProcessor(this.createEmptyDirs); - } -} + //TODO: error checking and reporting + return Promise.all(this.empty_dirs.map((d) => { + // "fullPath" should actually be the path relative to the current directory + let filename = _.trimStart(d.fullPath, '/'); -uppy = new Uppy({ - restrictions: { - maxFileSize: maxFileSize, - } -}); + return fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) + //TODO: parse json response verify if there was an error creating directory and handle error -uppy.use(EmptyDirCreator); -uppy.use(Dashboard, { - trigger: '#upload-btn', - fileManagerSelectionType: 'both', - disableThumbnailGenerator: true, - showLinkToFileUploadResult: false, - closeModalOnClickOutside: true, - closeAfterFinish: true, - allowMultipleUploads: false, - onRequestCloseModal: () => closeAndResetUppyModal(uppy), - note: 'Empty directories will be included in the upload only when a directory upload is initiated via drag and drop. This is because the File and Directory Entries API is available only on a drop event, not during an input change event.' -}); -uppy.use(XHRUpload, { - endpoint: filesUploadPath, - withCredentials: true, - fieldName: 'file', - limit: 1, - headers: { 'X-CSRF-Token': csrf_token }, - timeout: 128 * 1000, -}); + })).then(() => this.empty_dirs = []); + } + } -uppy.on('file-added', (file) => { - uppy.setFileMeta(file.id, { parent: history.state.currentDirectory }); - if(file.meta.relativePath == null && file.data.webkitRelativePath){ - uppy.setFileMeta(file.id, { relativePath: file.data.webkitRelativePath }); - } -}); + install () { + this.uppy.addPostProcessor(this.createEmptyDirs); + } -uppy.on('complete', (result) => { - if(result.successful.length > 0){ - reloadTable(); + uninstall () { + this.uppy.removePostProcessor(this.createEmptyDirs); + } } -}); -// https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file -global.addEventListener("dragover",function(e){ - e = e || event; - e.preventDefault(); -},false); + uppy = new Uppy({ + restrictions: { + maxFileSize: maxFileSize, + } + }); + + uppy.use(EmptyDirCreator); + uppy.use(Dashboard, { + trigger: '#upload-btn', + fileManagerSelectionType: 'both', + disableThumbnailGenerator: true, + showLinkToFileUploadResult: false, + closeModalOnClickOutside: true, + closeAfterFinish: true, + allowMultipleUploads: false, + onRequestCloseModal: () => closeAndResetUppyModal(uppy), + note: 'Empty directories will be included in the upload only when a directory upload is initiated via drag and drop. This is because the File and Directory Entries API is available only on a drop event, not during an input change event.' + }); + uppy.use(XHRUpload, { + endpoint: filesUploadPath, + withCredentials: true, + fieldName: 'file', + limit: 1, + headers: { 'X-CSRF-Token': csrf_token }, + timeout: 128 * 1000, + }); -global.addEventListener("drop",function(e){ - e = e || event; - e.preventDefault(); -},false); + uppy.on('file-added', (file) => { + uppy.setFileMeta(file.id, { parent: history.state.currentDirectory }); + if(file.meta.relativePath == null && file.data.webkitRelativePath){ + uppy.setFileMeta(file.id, { relativePath: file.data.webkitRelativePath }); + } + }); -$('#directory-contents').on('drop', function(e){ - this.classList.remove('dragover'); - // Prevent default behavior (Prevent file from being opened) + uppy.on('complete', (result) => { + if(result.successful.length > 0){ + reloadTable(); + } + }); - // pass drop event to uppy dashboard - uppy.getPlugin('Dashboard').openModal().then(() => uppy.getPlugin('Dashboard').handleDrop(e.originalEvent)) -}); + // https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file + global.addEventListener("dragover",function(e){ + e = e || event; + e.preventDefault(); + },false); + + global.addEventListener("drop",function(e){ + e = e || event; + e.preventDefault(); + },false); + + $('#directory-contents').on('drop', function(e){ + this.classList.remove('dragover'); + // Prevent default behavior (Prevent file from being opened) + + // pass drop event to uppy dashboard + uppy.getPlugin('Dashboard').openModal().then(() => uppy.getPlugin('Dashboard').handleDrop(e.originalEvent)) + }); -$('#directory-contents').on('dragover', function(e){ - this.classList.add('dragover'); + $('#directory-contents').on('dragover', function(e){ + this.classList.add('dragover'); - // Prevent default behavior (Prevent file from being opened) - e.preventDefault(); + // Prevent default behavior (Prevent file from being opened) + e.preventDefault(); - // specifies what feedback will be shown to the user by setting the dropEffect attribute of the DataTransfer associated with the event - // too bad we can't show an indicator (no dragstart/end when dragging from OS to browser) - e.originalEvent.dataTransfer.dropEffect = 'copy'; -}); + // specifies what feedback will be shown to the user by setting the dropEffect attribute of the DataTransfer associated with the event + // too bad we can't show an indicator (no dragstart/end when dragging from OS to browser) + e.originalEvent.dataTransfer.dropEffect = 'copy'; + }); + + $('#directory-contents').on('dragleave', function(e){ + this.classList.remove('dragover'); + }); -$('#directory-contents').on('dragleave', function(e){ - this.classList.remove('dragover'); }); function closeAndResetUppyModal(uppy){ @@ -163,4 +169,4 @@ function getEmptyDirs(entry){ function reloadTable() { $("#directory-contents").trigger('reloadTable'); -} +} \ No newline at end of file From 59be7f532d279fc61a45451314ba194f6712e6b6 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Fri, 1 Apr 2022 12:52:48 -0400 Subject: [PATCH 27/47] Modified document.ready to jQuery(function() { --- apps/dashboard/app/javascript/packs/files/ClipBoard.js | 2 +- apps/dashboard/app/javascript/packs/files/DataTable.js | 6 +----- apps/dashboard/app/javascript/packs/files/FileOps.js | 2 +- apps/dashboard/app/javascript/packs/files/SweetAlert.js | 2 +- apps/dashboard/app/javascript/packs/files/UppyOps.js | 2 +- apps/dashboard/app/javascript/packs/files/index.js | 6 ++++++ apps/dashboard/app/views/layouts/files.html.erb | 2 +- 7 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 apps/dashboard/app/javascript/packs/files/index.js diff --git a/apps/dashboard/app/javascript/packs/files/ClipBoard.js b/apps/dashboard/app/javascript/packs/files/ClipBoard.js index 40e3c11b07..8de2f10f52 100755 --- a/apps/dashboard/app/javascript/packs/files/ClipBoard.js +++ b/apps/dashboard/app/javascript/packs/files/ClipBoard.js @@ -1,7 +1,7 @@ import ClipboardJS from 'clipboard' import Handlebars from 'handlebars'; -$(document).ready(function() { +jQuery(function() { var clipBoard = new ClipBoard(); diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js index aee847696f..6a4f0757c9 100644 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -3,15 +3,11 @@ import 'datatables.net-bs4/js/dataTables.bootstrap4'; import 'datatables.net-select'; import 'datatables.net-select-bs4'; import Handlebars from 'handlebars'; -import {} from './SweetAlert.js'; -import {} from './FileOps.js'; -import {} from './UppyOps.js'; -import {} from './ClipBoard.js'; let table = null; -$(document).ready(function () { +jQuery(function() { table = new DataTable(); /* BUTTON ACTIONS */ diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js index 0e3cb66d26..c5be348680 100644 --- a/apps/dashboard/app/javascript/packs/files/FileOps.js +++ b/apps/dashboard/app/javascript/packs/files/FileOps.js @@ -4,7 +4,7 @@ let fileOps = null; let reportTransferTemplate = null; -$(document).ready(function () { +jQuery(function() { fileOps = new FileOps(); $("#directory-contents").on("fileOpsNewFile", function () { diff --git a/apps/dashboard/app/javascript/packs/files/SweetAlert.js b/apps/dashboard/app/javascript/packs/files/SweetAlert.js index 38d5f233e8..6638ef7ca8 100644 --- a/apps/dashboard/app/javascript/packs/files/SweetAlert.js +++ b/apps/dashboard/app/javascript/packs/files/SweetAlert.js @@ -2,7 +2,7 @@ import Swal from 'sweetalert2' let sweetAlert = null; -$(document).ready(function(){ +jQuery(function() { sweetAlert = new SweetAlert(); $("#directory-contents").on("swalShowError", function(e,options) { sweetAlert.alertError(options.title, options.message); diff --git a/apps/dashboard/app/javascript/packs/files/UppyOps.js b/apps/dashboard/app/javascript/packs/files/UppyOps.js index 7055ce88d1..3cdf5e2e69 100755 --- a/apps/dashboard/app/javascript/packs/files/UppyOps.js +++ b/apps/dashboard/app/javascript/packs/files/UppyOps.js @@ -6,7 +6,7 @@ import _ from 'lodash'; let uppy = null; -$(document).ready(function() { +Query(function() { class EmptyDirCreator extends BasePlugin { constructor (uppy, opts){ diff --git a/apps/dashboard/app/javascript/packs/files/index.js b/apps/dashboard/app/javascript/packs/files/index.js new file mode 100644 index 0000000000..d8af2f7ea1 --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/index.js @@ -0,0 +1,6 @@ +import {} from './ClipBoard.js'; +import {} from './DataTable.js'; +import {} from './FileOps.js'; +import {} from './SweetAlert.js'; +import {} from './UppyOps.js'; + diff --git a/apps/dashboard/app/views/layouts/files.html.erb b/apps/dashboard/app/views/layouts/files.html.erb index 5465ddd34a..e19e7c0cce 100644 --- a/apps/dashboard/app/views/layouts/files.html.erb +++ b/apps/dashboard/app/views/layouts/files.html.erb @@ -1,6 +1,6 @@ <% content_for :head do - javascript_pack_tag 'files/DataTable', nonce: true + javascript_pack_tag 'files/index', nonce: true end %> From 07eedb8c9d8e7d6e9bb6e239b5b758c2fab8f633 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Mon, 4 Apr 2022 14:55:07 -0400 Subject: [PATCH 28/47] Implemented Constants --- .../app/javascript/packs/files/ClipBoard.js | 23 +++---- .../app/javascript/packs/files/DataTable.js | 69 ++++++++++++++----- .../app/javascript/packs/files/FileOps.js | 55 +++++++-------- .../app/javascript/packs/files/SweetAlert.js | 32 ++++----- 4 files changed, 98 insertions(+), 81 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/ClipBoard.js b/apps/dashboard/app/javascript/packs/files/ClipBoard.js index 8de2f10f52..3ee93db4a9 100755 --- a/apps/dashboard/app/javascript/packs/files/ClipBoard.js +++ b/apps/dashboard/app/javascript/packs/files/ClipBoard.js @@ -1,31 +1,32 @@ import ClipboardJS from 'clipboard' import Handlebars from 'handlebars'; +import {CONTENTID, TRIGGERID} from './DataTable.js'; jQuery(function() { var clipBoard = new ClipBoard(); - $("#directory-contents").on('success', function(e) { + $(CONTENTID.table).on('success', function(e) { $(e.trigger).tooltip({title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom'}).tooltip('show'); setTimeout(() => $(e.trigger).tooltip('hide'), 2000); e.clearSelection(); }); - $("#directory-contents").on('error', function(e) { + $(CONTENTID.table).on('error', function(e) { e.clearSelection(); }); - $("#directory-contents").on("clipboardClear", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.clearClipboard, function (e, options) { clipBoard.clearClipboard(); clipBoard.updateViewForClipboard(); }); - $("#directory-contents").on("updateClipboardFromSelection", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.updateClipboard, function (e, options) { clipBoard.updateClipboardFromSelection(options.selection); clipBoard.updateViewForClipboard(); }); - $("#directory-contents").on("updateViewForClipboard", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.updateClipboardView, function (e, options) { clipBoard.updateViewForClipboard(); }); @@ -34,21 +35,15 @@ jQuery(function() { class ClipBoard { _clipBoard = null; - _handleBars = null; constructor() { this._clipBoard = new ClipboardJS('#copy-path'); - this._handleBars = Handlebars; } getClipBoard() { return this._clipBoard; } - getHandleBars() { - return this._handleBars; - } - clearClipboard() { localStorage.removeItem('filesClipboard'); } @@ -73,7 +68,7 @@ class ClipBoard { updateViewForClipboard() { let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || '{}'), template_str = $('#clipboard-template').html(), - template = this._handleBars.compile(template_str); + template = Handlebars.compile(template_str); $('#clipboard').html(template(clipboard)); @@ -102,7 +97,7 @@ class ClipBoard { 'token': csrf_token }; - $("#directory-contents").trigger('fileOpsMove', eventData); + $(CONTENTID.table).trigger(TRIGGERID.moveFile, eventData); } } else{ @@ -141,7 +136,7 @@ class ClipBoard { 'token': csrf_token }; - $("#directory-contents").trigger('fileOpsCopy', eventData); + $(CONTENTID.table).trigger(TRIGGERID.copyFile, eventData); } } else{ diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js index 6a4f0757c9..6bdc7c636c 100644 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -3,7 +3,37 @@ import 'datatables.net-bs4/js/dataTables.bootstrap4'; import 'datatables.net-select'; import 'datatables.net-select-bs4'; import Handlebars from 'handlebars'; - +export {CONTENTID, TRIGGERID}; + +const TRIGGERID = { + changeDirectory: 'changeDirectory', + changeDirectoryPrompt: 'changeDirectoryPrompt', + clearClipboard: 'clearClipboard', + closeSwal: 'closeSwal', + copyFile: 'copyFile', + createFile: 'createFile', + createFolder: 'createFolder', + deleteFile: 'deleteFile', + deletePrompt: 'deletePrompt', + download: 'download', + getJsonResponse: 'getJsonResponse', + moveFile: 'moveFile', + newFile: 'newFile', + newFolder: 'newFolder', + reloadTable: 'reloadTable', + renameFile: 'renameFile', + renameFilePrompt: 'renameFilePrompt', + showError: 'showError', + showInput: 'showInput', + showLoading: 'showLoading', + showPrompt: 'showPrompt', + updateClipboard: 'updateClipboard', + updateClipboardView: 'updateClipboardView', +}; + +const CONTENTID = { + table: '#directory-contents', +}; let table = null; @@ -12,11 +42,11 @@ jQuery(function() { /* BUTTON ACTIONS */ $("#new-file-btn").on("click", function () { - $("#directory-contents").trigger('fileOpsNewFile'); + $(CONTENTID.table).trigger(TRIGGERID.newFile); }); $("#new-folder-btn").on("click", function () { - $("#directory-contents").trigger('fileOpsNewFolder'); + $(CONTENTID.table).trigger(TRIGGERID.newFolder); }); $("#download-btn").on("click", function () { @@ -27,14 +57,14 @@ jQuery(function() { 'message': 'You have selected none.', }; - $("#directory-contents").trigger('swalShowError', eventData); + $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); } else { const eventData = { selection: selection }; - $("#directory-contents").trigger('fileOpsDownload', eventData); + $(CONTENTID.table).trigger(TRIGGERID.download, eventData); } }); @@ -47,14 +77,14 @@ jQuery(function() { 'message': 'You have selected none.', }; - $("#directory-contents").trigger('swalShowError', eventData); + $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); } else { const eventData = { files: files }; - $("#directory-contents").trigger('fileOpsDeletePrompt', eventData); + $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); } }); @@ -67,38 +97,39 @@ jQuery(function() { 'message': 'You have selected none.', }; - $("#directory-contents").trigger('swalShowError', eventData); - $("#directory-contents").trigger('clipboardClear', eventData); + $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); + $(CONTENTID.table).trigger(TRIGGERID.clearClipbaord, eventData); } else { const eventData = { selection: selection }; - $("#directory-contents").trigger('updateClipboardFromSelection', eventData); + $(CONTENTID.table).trigger(TRIGGERID.updateClipboard, eventData); } }); $("#goto-btn").on("click", function () { - $("#directory-contents").trigger('changeDirectoryPrompt'); + $(CONTENTID.table).trigger(TRIGGERID.changeDirectoryPrompt); }); + // TODO: // Will have to work on this one later. Not so straight forward. // // $("#upload-btn").on("click", function () { - // $("#directory-contents").trigger('uppyShowUploadPrompt'); + // $(CONTENTID.table).trigger('uppyShowUploadPrompt'); // }); /* END BUTTON ACTIONS */ /* TABLE ACTIONS */ - $("#directory-contents").on("reloadTable", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.reloadTable, function (e, options) { table.reloadTable(options.url); }); - $("#directory-contents").on("getDataFromJsonResponse", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.getJsonResponse, function (e, options) { table.dataFromJsonResponse(options.response); }); @@ -112,7 +143,7 @@ jQuery(function() { file: fileName, }; - $("#directory-contents").trigger('fileOpsRenameFilePrompt', eventData); + $(CONTENTID.table).trigger(TRIGGERID.renameFilePrompt, eventData); }); @@ -126,7 +157,7 @@ jQuery(function() { files: [fileName] }; - $("#directory-contents").trigger('fileOpsDeletePrompt', eventData); + $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); }); @@ -255,7 +286,7 @@ class DataTable { } loadDataTable() { - this._table = $('#directory-contents').on('xhr.dt', function (e, settings, json, xhr) { + this._table = $(CONTENTID.table).on('xhr.dt', function (e, settings, json, xhr) { // new ajax request for new data so update date/time // if(json && json.time){ if (json && json.time) { @@ -353,7 +384,7 @@ class DataTable { $('#open-in-terminal-btn').attr('href', data.shell_url); $('#open-in-terminal-btn').removeClass('disabled'); - $("#directory-contents").trigger('updateViewForClipboard'); + $(CONTENTID.table).trigger(TRIGGERID.updateClipboardView); return await Promise.resolve(data); } catch (e) { const eventData = { @@ -361,7 +392,7 @@ class DataTable { 'message': e.message, }; - $("#directory-contents").trigger('swalShowError', eventData); + $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); $('#open-in-terminal-btn').addClass('disabled'); return await Promise.reject(e); diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js index c5be348680..4dbe332825 100644 --- a/apps/dashboard/app/javascript/packs/files/FileOps.js +++ b/apps/dashboard/app/javascript/packs/files/FileOps.js @@ -1,4 +1,5 @@ import Handlebars from 'handlebars'; +import {CONTENTID, TRIGGERID} from './DataTable.js'; let fileOps = null; @@ -7,72 +8,66 @@ let reportTransferTemplate = null; jQuery(function() { fileOps = new FileOps(); - $("#directory-contents").on("fileOpsNewFile", function () { + $(CONTENTID.table).on(TRIGGERID.newFilePrompt, function () { fileOps.newFilePrompt(); }); - $("#directory-contents").on("fileOpsNewFolder", function () { + $(CONTENTID.table).on(TRIGGERID.newFolderPrompt, function () { fileOps.newFolderPrompt(); }); - $("#directory-contents").on("fileOpsRenameFilePrompt", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.renameFilePrompt, function (e, options) { fileOps.renameFilePrompt(options.file); }); - $("#directory-contents").on("fileOpsRenameFile", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.renameFile, function (e, options) { fileOps.renameFile(options.files, options.result.value); }); - $("#directory-contents").on("fileOpsCreateFile", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.createFile, function (e, options) { fileOps.newFile(options.result.value); }); - $("#directory-contents").on("fileOpsCreateFolder", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.createFolder, function (e, options) { fileOps.newFolder(options.result.value); }); - // $("#directory-contents").on("fileOpsUpload", function (e, options) { - // fileOps.newFolder(options.value); - // }); - - $("#directory-contents").on("fileOpsDownload", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.download, function (e, options) { fileOps.download(options.selection); }); - $("#directory-contents").on("fileOpsDeletePrompt", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.deletePrompt, function (e, options) { fileOps.deletePrompt(options.files); }); - $("#directory-contents").on("fileOpsDelete", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.deleteFile, function (e, options) { fileOps.delete(options.files); }); - $("#directory-contents").on("fileOpsMove", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.moveFile, function (e, options) { fileOps.move(options.files, options.token); }); - $("#directory-contents").on("fileOpsCopy", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.copyFile, function (e, options) { fileOps.copy(options.files, options.token); }); - $("#directory-contents").on("changeDirectoryPrompt", function () { + $(CONTENTID.table).on(TRIGGERID.changeDirectoryPrompt, function () { fileOps.changeDirectoryPrompt(); }); - $("#directory-contents").on("changeDirectory", function (e, options) { + $(CONTENTID.table).on(TRIGGERID.changeDirectory, function (e, options) { fileOps.changeDirectory(options.result.value); }); }); class FileOps { - _handleBars = null; _timeout = 2000; _attempts = 0; _filesPath = filesPath; constructor() { - this._handleBars = Handlebars; } changeDirectory(path) { @@ -100,7 +95,7 @@ class FileOps { } }; - $("#directory-contents").trigger('swalShowInput', eventData); + $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); } @@ -119,7 +114,7 @@ class FileOps { showCancelButton: true, } - $("#directory-contents").trigger('swalShowPrompt', eventData); + $(CONTENTID.table).trigger(TRIGGERID.showPrompt, eventData); } @@ -153,7 +148,7 @@ class FileOps { } }; - $("#directory-contents").trigger('swalShowInput', eventData); + $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); } @@ -181,7 +176,7 @@ class FileOps { } }; - $("#directory-contents").trigger('swalShowInput', eventData); + $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); } @@ -215,7 +210,7 @@ class FileOps { } }; - $("#directory-contents").trigger('swalShowInput', eventData); + $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); } @@ -316,7 +311,7 @@ class FileOps { } }; - $("#directory-contents").trigger('swalShowInput', eventData); + $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); } @@ -446,16 +441,16 @@ class FileOps { 'message': message, }; - $("#directory-contents").trigger('showError', eventData); + $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); } doneLoading() { - $("#directory-contents").trigger('swalClose'); + $(CONTENTID.table).trigger(TRIGGERID.closeSwal); } clearClipboard() { - $("#directory-contents").trigger('clipboardClear'); + $(CONTENTID.table).trigger(TRIGGERID.clearClipboard); } reloadTable(url) { @@ -463,7 +458,7 @@ class FileOps { 'url': url, }; - $("#directory-contents").trigger('reloadTable', eventData); + $(CONTENTID.table).trigger(TRIGGERID.reloadTable, eventData); } showSwalLoading (message) { @@ -471,7 +466,7 @@ class FileOps { 'message': message, }; - $("#directory-contents").trigger('swalShowLoading', eventData); + $(CONTENTID.table).trigger(TRIGGERID.showLoading, eventData); } diff --git a/apps/dashboard/app/javascript/packs/files/SweetAlert.js b/apps/dashboard/app/javascript/packs/files/SweetAlert.js index 6638ef7ca8..9a3167f117 100644 --- a/apps/dashboard/app/javascript/packs/files/SweetAlert.js +++ b/apps/dashboard/app/javascript/packs/files/SweetAlert.js @@ -1,41 +1,40 @@ import Swal from 'sweetalert2' +import {CONTENTID, TRIGGERID} from './DataTable.js'; let sweetAlert = null; jQuery(function() { sweetAlert = new SweetAlert(); - $("#directory-contents").on("swalShowError", function(e,options) { + $(CONTENTID.table).on(TRIGGERID.showError, function(e,options) { sweetAlert.alertError(options.title, options.message); }); - $("#directory-contents").on("swalShowPrompt", function(e,options) { + $(CONTENTID.table).on(TRIGGERID.showPrompt, function(e,options) { sweetAlert.alertError(options.title, options.message); }); - $("#directory-contents").on("swalShowInput", function(e,options) { + $(CONTENTID.table).on(TRIGGERID.showInput, function(e,options) { sweetAlert.input(options); }); - $("#directory-contents").on("swalShowLoading", function(e,options) { + $(CONTENTID.table).on(TRIGGERID.showLoading, function(e,options) { sweetAlert.loading(options.message); }); - $("#directory-contents").on("swalClose", function() { + $(CONTENTID.table).on(TRIGGERID.closeSwal, function() { sweetAlert.close(); }); }); class SweetAlert { - _swal = null; constructor() { - this._swal = Swal; this.setMixin(); } input(options) { - this._swal.fire(options.inputOptions) + Swal.fire(options.inputOptions) .then (function(result){ if(result.isConfirmed) { const eventData = { @@ -43,15 +42,15 @@ class SweetAlert { files: options.files ? options.files : null }; - $("#directory-contents").trigger(options.action, eventData); + $(CONTENTID.table).trigger(options.action, eventData); } else { - $("#directory-contents").trigger('reloadTable'); + $(CONTENTID.table).trigger(TRIGGERID.reloadTable); } }); } setMixin() { - this._swal.mixIn = ({ + Swal.mixIn = ({ showClass: { popup: 'swal2-noanimation', backdrop: 'swal2-noanimation' @@ -64,22 +63,19 @@ class SweetAlert { } alertError(error_title, error_message) { - this._swal.fire(error_title, error_message, 'error'); + Swal.fire(error_title, error_message, 'error'); } async loading(title) { - this._swal.fire({ + Swal.fire({ title: title, allowOutsideClick: false, showConfirmButton: false, - willOpen: () => { this._swal.showLoading() } + willOpen: () => { Swal.showLoading() } }); } close() { - this._swal.close(); + Swal.close(); } } - - - From 365eb107f9d42399de314fb9d278890ffe15eacc Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Tue, 5 Apr 2022 11:43:56 -0400 Subject: [PATCH 29/47] Moved 'Data Validation' to receiver. --- .../app/javascript/packs/files/ClipBoard.js | 15 +++- .../app/javascript/packs/files/DataTable.js | 61 ++++---------- .../app/javascript/packs/files/FileOps.js | 82 ++++++++++--------- .../app/javascript/packs/files/UppyOps.js | 2 +- 4 files changed, 71 insertions(+), 89 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/ClipBoard.js b/apps/dashboard/app/javascript/packs/files/ClipBoard.js index 3ee93db4a9..3f5972a0c1 100755 --- a/apps/dashboard/app/javascript/packs/files/ClipBoard.js +++ b/apps/dashboard/app/javascript/packs/files/ClipBoard.js @@ -22,8 +22,19 @@ jQuery(function() { }); $(CONTENTID.table).on(TRIGGERID.updateClipboard, function (e, options) { - clipBoard.updateClipboardFromSelection(options.selection); - clipBoard.updateViewForClipboard(); + if(options.selection.length == 0) { + const eventData = { + 'title': 'Select a file, files, or directory to copy or move.', + 'message': 'You have selected none.', + }; + + $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); + $(CONTENTID.table).trigger(TRIGGERID.clearClipbaord, eventData); + + } else { + clipBoard.updateClipboardFromSelection(options.selection); + clipBoard.updateViewForClipboard(); + } }); $(CONTENTID.table).on(TRIGGERID.updateClipboardView, function (e, options) { diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js index 6bdc7c636c..52ea92f29c 100644 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -51,62 +51,31 @@ jQuery(function() { $("#download-btn").on("click", function () { let selection = table.getTable().rows({ selected: true }).data(); - if(selection.length == 0) { - const eventData = { - 'title': 'Select a file, files, or directory to download', - 'message': 'You have selected none.', - }; - - $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); - - } else { - const eventData = { - selection: selection - }; - - $(CONTENTID.table).trigger(TRIGGERID.download, eventData); - } + const eventData = { + selection: selection + }; + + $(CONTENTID.table).trigger(TRIGGERID.download, eventData); }); $("#delete-btn").on("click", function () { let files = table.getTable().rows({selected: true}).data().toArray().map((f) => f.name); - if(files.length == 0) { - const eventData = { - 'title': 'Select a file, files, or directory to delete.', - 'message': 'You have selected none.', - }; - - $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); - - } else { - const eventData = { - files: files - }; - - $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); - } + const eventData = { + files: files + }; + + $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); }); $("#copy-move-btn").on("click", function () { let selection = table.getTable().rows({selected: true}).data(); - if(selection.length == 0) { - const eventData = { - 'title': 'Select a file, files, or directory to copy or move.', - 'message': 'You have selected none.', - }; - - $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); - $(CONTENTID.table).trigger(TRIGGERID.clearClipbaord, eventData); - - } else { - const eventData = { - selection: selection - }; - - $(CONTENTID.table).trigger(TRIGGERID.updateClipboard, eventData); - } + const eventData = { + selection: selection + }; + + $(CONTENTID.table).trigger(TRIGGERID.updateClipboard, eventData); }); diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js index 4dbe332825..298635f52a 100644 --- a/apps/dashboard/app/javascript/packs/files/FileOps.js +++ b/apps/dashboard/app/javascript/packs/files/FileOps.js @@ -33,14 +33,34 @@ jQuery(function() { }); $(CONTENTID.table).on(TRIGGERID.download, function (e, options) { - fileOps.download(options.selection); + if(options.selection.length == 0) { + const eventData = { + 'title': 'Select a file, files, or directory to download', + 'message': 'You have selected none.', + }; + + $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); + + } else { + fileOps.download(options.selection); + } }); $(CONTENTID.table).on(TRIGGERID.deletePrompt, function (e, options) { - fileOps.deletePrompt(options.files); + if(options.files.length == 0) { + const eventData = { + 'title': 'Select a file, files, or directory to delete.', + 'message': 'You have selected none.', + }; + + $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); + + } else { + fileOps.deletePrompt(options.files); + } }); - $(CONTENTID.table).on(TRIGGERID.deleteFile, function (e, options) { + $(CONTENTID.table).on(TRIGGERID.deleteFile, function (e, options) { fileOps.delete(options.files); }); @@ -99,24 +119,25 @@ class FileOps { } - - deleteFile(fileName, newFileName) { - let files = {}; - files[`${history.state.currentDirectory}/${fileName}`] = `${history.state.currentDirectory}/${newFileName}`; - this.transferFiles(files, "mv", "rename file") - } - - deleteFilePrompt(files) { + deletePrompt(files) { const eventData = { - action: 'fileOpsDeleteFile', - title: files.length == 1 ? `Delete ${files[0]}?` : `Delete ${files.length} selected files?`, - text: 'Are you sure you want to delete the files: ' + files.join(', '), - showCancelButton: true, - } + action: TRIGGERID.deleteFile, + files: files, + 'inputOptions': { + title: files.length == 1 ? `Delete ${files[0]}?` : `Delete ${files.length} selected files?`, + text: 'Are you sure you want to delete the files: ' + files.join(', '), + showCancelButton: true, + } + }; + + $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); - $(CONTENTID.table).trigger(TRIGGERID.showPrompt, eventData); } + + removeFiles(files) { + this.transferFiles(files, "rm", "remove files") + } renameFile(fileName, newFileName) { let files = {}; @@ -126,7 +147,7 @@ class FileOps { renameFilePrompt(fileName) { const eventData = { - action: 'fileOpsRenameFile', + action: TRIGGERID.renameFile, files: fileName, 'inputOptions': { title: 'Rename', @@ -157,7 +178,7 @@ class FileOps { newFilePrompt() { const eventData = { - action: 'fileOpsCreateFile', + action: TRIGGERID.createFile, 'inputOptions': { title: 'New File', input: 'text', @@ -195,7 +216,7 @@ class FileOps { newFolderPrompt() { const eventData = { - action: 'fileOpsCreateFolder', + action: TRIGGERID.createFolder, 'inputOptions': { title: 'New Folder', input: 'text', @@ -299,26 +320,7 @@ class FileOps { .catch((e) => reject(e)) }); } - - deletePrompt(files) { - const eventData = { - action: 'fileOpsDelete', - files: files, - 'inputOptions': { - title: files.length == 1 ? `Delete ${files[0]}?` : `Delete ${files.length} selected files?`, - text: 'Are you sure you want to delete the files: ' + files.join(', '), - showCancelButton: true, - } - }; - - $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); - - } - - - removeFiles(files) { - this.transferFiles(files, "rm", "remove files") - } + delete(files) { this.showSwalLoading('Deleting files...: '); diff --git a/apps/dashboard/app/javascript/packs/files/UppyOps.js b/apps/dashboard/app/javascript/packs/files/UppyOps.js index 3cdf5e2e69..66306fe51e 100755 --- a/apps/dashboard/app/javascript/packs/files/UppyOps.js +++ b/apps/dashboard/app/javascript/packs/files/UppyOps.js @@ -6,7 +6,7 @@ import _ from 'lodash'; let uppy = null; -Query(function() { +jQuery(function() { class EmptyDirCreator extends BasePlugin { constructor (uppy, opts){ From b5de72eac0351df4e9a0295909a8ecba4674e68a Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Wed, 6 Apr 2022 11:58:36 -0400 Subject: [PATCH 30/47] Fixed formatting --- .../app/javascript/packs/files/DataTable.js | 236 +++++++++--------- 1 file changed, 118 insertions(+), 118 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js index 52ea92f29c..bacaf4189d 100644 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ b/apps/dashboard/app/javascript/packs/files/DataTable.js @@ -3,32 +3,32 @@ import 'datatables.net-bs4/js/dataTables.bootstrap4'; import 'datatables.net-select'; import 'datatables.net-select-bs4'; import Handlebars from 'handlebars'; -export {CONTENTID, TRIGGERID}; +export { CONTENTID, TRIGGERID }; const TRIGGERID = { - changeDirectory: 'changeDirectory', - changeDirectoryPrompt: 'changeDirectoryPrompt', - clearClipboard: 'clearClipboard', - closeSwal: 'closeSwal', + changeDirectory: 'changeDirectory', + changeDirectoryPrompt: 'changeDirectoryPrompt', + clearClipboard: 'clearClipboard', + closeSwal: 'closeSwal', copyFile: 'copyFile', - createFile: 'createFile', - createFolder: 'createFolder', - deleteFile: 'deleteFile', - deletePrompt: 'deletePrompt', - download: 'download', - getJsonResponse: 'getJsonResponse', - moveFile: 'moveFile', - newFile: 'newFile', - newFolder: 'newFolder', - reloadTable: 'reloadTable', - renameFile: 'renameFile', - renameFilePrompt: 'renameFilePrompt', + createFile: 'createFile', + createFolder: 'createFolder', + deleteFile: 'deleteFile', + deletePrompt: 'deletePrompt', + download: 'download', + getJsonResponse: 'getJsonResponse', + moveFile: 'moveFile', + newFile: 'newFile', + newFolder: 'newFolder', + reloadTable: 'reloadTable', + renameFile: 'renameFile', + renameFilePrompt: 'renameFilePrompt', showError: 'showError', - showInput: 'showInput', - showLoading: 'showLoading', - showPrompt: 'showPrompt', - updateClipboard: 'updateClipboard', - updateClipboardView: 'updateClipboardView', + showInput: 'showInput', + showLoading: 'showLoading', + showPrompt: 'showPrompt', + updateClipboard: 'updateClipboard', + updateClipboardView: 'updateClipboardView', }; const CONTENTID = { @@ -37,7 +37,7 @@ const CONTENTID = { let table = null; -jQuery(function() { +jQuery(function () { table = new DataTable(); /* BUTTON ACTIONS */ @@ -54,29 +54,29 @@ jQuery(function() { const eventData = { selection: selection }; - - $(CONTENTID.table).trigger(TRIGGERID.download, eventData); - + + $(CONTENTID.table).trigger(TRIGGERID.download, eventData); + }); $("#delete-btn").on("click", function () { - let files = table.getTable().rows({selected: true}).data().toArray().map((f) => f.name); + let files = table.getTable().rows({ selected: true }).data().toArray().map((f) => f.name); const eventData = { files: files }; - - $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); - + + $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); + }); $("#copy-move-btn").on("click", function () { - let selection = table.getTable().rows({selected: true}).data(); + let selection = table.getTable().rows({ selected: true }).data(); const eventData = { selection: selection }; - + $(CONTENTID.table).trigger(TRIGGERID.updateClipboard, eventData); - + }); $("#goto-btn").on("click", function () { @@ -102,7 +102,7 @@ jQuery(function() { table.dataFromJsonResponse(options.response); }); - $(document).on('click','.rename-file', function(e) { + $(document).on('click', '.rename-file', function (e) { e.preventDefault(); let rowId = e.currentTarget.dataset.rowIndex; let row = table.getTable().row(rowId).data(); @@ -111,12 +111,12 @@ jQuery(function() { const eventData = { file: fileName, }; - - $(CONTENTID.table).trigger(TRIGGERID.renameFilePrompt, eventData); - }); + $(CONTENTID.table).trigger(TRIGGERID.renameFilePrompt, eventData); + + }); - $(document).on('click','.delete-file', function(e) { + $(document).on('click', '.delete-file', function (e) { e.preventDefault(); let rowId = e.currentTarget.dataset.rowIndex; let row = table.getTable().row(rowId).data(); @@ -125,28 +125,28 @@ jQuery(function() { const eventData = { files: [fileName] }; - - $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); - }); + $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); + + }); - $('#show-dotfiles').on('change', () => { + $('#show-dotfiles').on('change', () => { let visible = $('#show-dotfiles').is(':checked'); - + table.setShowDotFiles(visible); table.updateDotFileVisibility(); - }); - - $('#show-owner-mode').on('change', () => { + }); + + $('#show-owner-mode').on('change', () => { let visible = $('#show-owner-mode').is(':checked'); - + table.setShowOwnerMode(visible); table.updateShowOwnerModeVisibility(); - }); - + }); + /* END TABLE ACTIONS */ - + /* DATATABLE LISTENERS */ // prepend show dotfiles checkbox to search box @@ -155,87 +155,87 @@ jQuery(function() { }); // if only 1 selected item, do not allow to de-select - table.getTable().on('user-select', function ( e, dt, type, cell, originalEvent ) { - var selected_rows = dt.rows( { selected: true } ); + table.getTable().on('user-select', function (e, dt, type, cell, originalEvent) { + var selected_rows = dt.rows({ selected: true }); - if(originalEvent.target.closest('.actions-btn-group')){ - // dont do user select event when opening or working with actions btn dropdown - e.preventDefault(); + if (originalEvent.target.closest('.actions-btn-group')) { + // dont do user select event when opening or working with actions btn dropdown + e.preventDefault(); } - else if(selected_rows.count() == 1 && cell.index().row == selected_rows.indexes()[0] ){ - // dont do user select because already selected - e.preventDefault(); + else if (selected_rows.count() == 1 && cell.index().row == selected_rows.indexes()[0]) { + // dont do user select because already selected + e.preventDefault(); } - else{ - // row need to find the checkbox to give it the focus - cell.node().closest('tr').querySelector('input[type=checkbox]').focus(); + else { + // row need to find the checkbox to give it the focus + cell.node().closest('tr').querySelector('input[type=checkbox]').focus(); } }); - table.getTable().on( 'deselect', function ( e, dt, type, indexes ) { + table.getTable().on('deselect', function (e, dt, type, indexes) { dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', false)); - }); + }); - table.getTable().on( 'select', function ( e, dt, type, indexes ) { + table.getTable().on('select', function (e, dt, type, indexes) { dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', true)); }); - $('#directory-contents tbody').on('click', 'tr td:first-child input[type=checkbox]', function(){ + $('#directory-contents tbody').on('click', 'tr td:first-child input[type=checkbox]', function () { // input checkbox checked or not - if($(this).is(':checked')){ - // select row - table.getTable().row(this.closest('tr')).select(); + if ($(this).is(':checked')) { + // select row + table.getTable().row(this.closest('tr')).select(); } - else{ - // deselect row - table.getTable().row(this.closest('tr')).deselect(); + else { + // deselect row + table.getTable().row(this.closest('tr')).deselect(); } this.focus(); }); - $('#directory-contents tbody').on('keydown', 'input, a', function(e){ - if(e.key == "ArrowDown"){ - e.preventDefault(); + $('#directory-contents tbody').on('keydown', 'input, a', function (e) { + if (e.key == "ArrowDown") { + e.preventDefault(); - // let tr = this.closest('tr').nextSibling; - let tr = $(this.closest('tr')).next('tr').get(0); - if(tr){ - tr.querySelector('input[type=checkbox]').focus(); + // let tr = this.closest('tr').nextSibling; + let tr = $(this.closest('tr')).next('tr').get(0); + if (tr) { + tr.querySelector('input[type=checkbox]').focus(); - // deselect if not holding shift key to work - // like native file browsers - if(! e.shiftKey){ - table.getTable().rows().deselect(); - } + // deselect if not holding shift key to work + // like native file browsers + if (!e.shiftKey) { + table.getTable().rows().deselect(); + } - // select if moving down - table.getTable().row(tr).select(); - } + // select if moving down + table.getTable().row(tr).select(); + } } - else if(e.key == "ArrowUp"){ - e.preventDefault(); + else if (e.key == "ArrowUp") { + e.preventDefault(); - let tr = $(this.closest('tr')).prev('tr').get(0); - if(tr){ - tr.querySelector('input[type=checkbox]').focus(); + let tr = $(this.closest('tr')).prev('tr').get(0); + if (tr) { + tr.querySelector('input[type=checkbox]').focus(); - // deselect if not holding shift key to work - // like native file browsers - if(! e.shiftKey){ - table.getTable().rows().deselect(); - } + // deselect if not holding shift key to work + // like native file browsers + if (!e.shiftKey) { + table.getTable().rows().deselect(); + } - // select if moving up - table.getTable().row(tr).select(); - } + // select if moving up + table.getTable().row(tr).select(); + } } }); $.fn.dataTable.ext.search.push( - function( settings, data, dataIndex ) { - return table.getShowDotFiles() || ! data[2].startsWith('.'); + function (settings, data, dataIndex) { + return table.getShowDotFiles() || !data[2].startsWith('.'); } ) @@ -298,7 +298,7 @@ class DataTable { }, { data: 'type', render: (data, type, row, meta) => data == 'd' ? ' dir' : ' file' }, // type { name: 'name', data: 'name', className: 'text-break', render: (data, type, row, meta) => `${Handlebars.escapeExpression(data)}` }, // name - { name: 'actions', orderable: false, data: null, render: (data, type, row, meta) => this.actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, + { name: 'actions', orderable: false, data: null, render: (data, type, row, meta) => this.actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, { data: 'size', render: (data, type, row, meta) => { @@ -334,9 +334,9 @@ class DataTable { ] }); - $('#directory-contents_filter').prepend(``) - $('#directory-contents_filter').prepend(``) - + $('#directory-contents_filter').prepend(``) + $('#directory-contents_filter').prepend(``) + } async reloadTable(url) { @@ -352,7 +352,7 @@ class DataTable { $('#open-in-terminal-btn').attr('href', data.shell_url); $('#open-in-terminal-btn').removeClass('disabled'); - + $(CONTENTID.table).trigger(TRIGGERID.updateClipboardView); return await Promise.resolve(data); } catch (e) { @@ -371,23 +371,23 @@ class DataTable { updateDotFileVisibility() { this.reloadTable(); } - + updateShowOwnerModeVisibility() { let visible = this.getShowOwnerMode(); - + this._table.column('owner:name').visible(visible); this._table.column('mode:name').visible(visible); } - - + + setShowOwnerMode(visible) { localStorage.setItem('show-owner-mode', new Boolean(visible)); } - + setShowDotFiles(visible) { localStorage.setItem('show-dotfiles', new Boolean(visible)); } - + getShowDotFiles() { return localStorage.getItem('show-dotfiles') == 'true' @@ -411,17 +411,17 @@ class DataTable { let template_str = $('#actions-btn-template').html(); let compiled = Handlebars.compile(template_str); let results = compiled(options); - return results; + return results; } - updateDatatablesStatus(){ + updateDatatablesStatus() { // from "function info ( api )" of https://cdn.datatables.net/select/1.3.1/js/dataTables.select.js let api = this._table; - let rows = api.rows( { selected: true } ).flatten().length, + let rows = api.rows({ selected: true }).flatten().length, page_info = api.page.info(), msg = page_info.recordsTotal == page_info.recordsDisplay ? `Showing ${page_info.recordsDisplay} rows` : `Showing ${page_info.recordsDisplay} of ${page_info.recordsTotal} rows`; - + $('.datatables-status').html(`${msg} - ${rows} rows selected`); - } - + } + } From 20ac0a7d181288db0e8174dde55bf262033f27d7 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Thu, 7 Apr 2022 15:49:41 -0400 Subject: [PATCH 31/47] Rename js files --- .gitignore | 3 + .../app/javascript/packs/files/ClipBoard.js | 161 ------ .../app/javascript/packs/files/DataTable.js | 427 ---------------- .../app/javascript/packs/files/FileOps.js | 478 ------------------ .../app/javascript/packs/files/SweetAlert.js | 81 --- .../app/javascript/packs/files/UppyOps.js | 172 ------- .../app/javascript/packs/files/index.js | 10 +- 7 files changed, 8 insertions(+), 1324 deletions(-) delete mode 100755 apps/dashboard/app/javascript/packs/files/ClipBoard.js delete mode 100644 apps/dashboard/app/javascript/packs/files/DataTable.js delete mode 100644 apps/dashboard/app/javascript/packs/files/FileOps.js delete mode 100644 apps/dashboard/app/javascript/packs/files/SweetAlert.js delete mode 100755 apps/dashboard/app/javascript/packs/files/UppyOps.js diff --git a/.gitignore b/.gitignore index 355d436602..e615d528da 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,6 @@ tags # Ignore copies of .rubocop.yml .rubocop.yml !/.rubocop.yml + +# Ignore .vscode +.vscode diff --git a/apps/dashboard/app/javascript/packs/files/ClipBoard.js b/apps/dashboard/app/javascript/packs/files/ClipBoard.js deleted file mode 100755 index 3f5972a0c1..0000000000 --- a/apps/dashboard/app/javascript/packs/files/ClipBoard.js +++ /dev/null @@ -1,161 +0,0 @@ -import ClipboardJS from 'clipboard' -import Handlebars from 'handlebars'; -import {CONTENTID, TRIGGERID} from './DataTable.js'; - -jQuery(function() { - - var clipBoard = new ClipBoard(); - - $(CONTENTID.table).on('success', function(e) { - $(e.trigger).tooltip({title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom'}).tooltip('show'); - setTimeout(() => $(e.trigger).tooltip('hide'), 2000); - e.clearSelection(); - }); - - $(CONTENTID.table).on('error', function(e) { - e.clearSelection(); - }); - - $(CONTENTID.table).on(TRIGGERID.clearClipboard, function (e, options) { - clipBoard.clearClipboard(); - clipBoard.updateViewForClipboard(); - }); - - $(CONTENTID.table).on(TRIGGERID.updateClipboard, function (e, options) { - if(options.selection.length == 0) { - const eventData = { - 'title': 'Select a file, files, or directory to copy or move.', - 'message': 'You have selected none.', - }; - - $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); - $(CONTENTID.table).trigger(TRIGGERID.clearClipbaord, eventData); - - } else { - clipBoard.updateClipboardFromSelection(options.selection); - clipBoard.updateViewForClipboard(); - } - }); - - $(CONTENTID.table).on(TRIGGERID.updateClipboardView, function (e, options) { - clipBoard.updateViewForClipboard(); - }); - - -}); - -class ClipBoard { - _clipBoard = null; - - constructor() { - this._clipBoard = new ClipboardJS('#copy-path'); - } - - getClipBoard() { - return this._clipBoard; - } - - clearClipboard() { - localStorage.removeItem('filesClipboard'); - } - - updateClipboardFromSelection(selection) { - - if(selection.length == 0){ - this.clearClipboard(); - } else { - let clipboardData = { - from: history.state.currentDirectory, - files: selection.toArray().map((f) => { - return { directory: f.type == 'd', name: f.name }; - }) - }; - - localStorage.setItem('filesClipboard', JSON.stringify(clipboardData)); - } - } - - - updateViewForClipboard() { - let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || '{}'), - template_str = $('#clipboard-template').html(), - template = Handlebars.compile(template_str); - - $('#clipboard').html(template(clipboard)); - - $('#clipboard-clear').on("click", () => { - this.clearClipboard(); - this.updateViewForClipboard(); - }); - - $('#clipboard-move-to-dir').on("click", () => { - let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || 'null'); - if(clipboard){ - clipboard.to = history.state.currentDirectory; - - if(clipboard.from == clipboard.to){ - console.error('clipboard from and to are identical') - // TODO: - } - else{ - let files = {}; - clipboard.files.forEach((f) => { - files[`${clipboard.from}/${f.name}`] = `${history.state.currentDirectory}/${f.name}` - }); - - const eventData = { - 'files': files, - 'token': csrf_token - }; - - $(CONTENTID.table).trigger(TRIGGERID.moveFile, eventData); - } - } - else{ - console.error('files clipboard is empty'); - } - }); - - - $('#clipboard-copy-to-dir').on("click", () => { - let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || 'null'); - - if(clipboard){ - clipboard.to = history.state.currentDirectory; - - if(clipboard.from == clipboard.to){ - console.error('clipboard from and to are identical') - - // TODO: we want to support this use case - // copy and paste as a new filename - // but lots of edge cases - // (overwrite or rename duplicates) - // _copy - // _copy_2 - // _copy_3 - // _copy_4 - } - else{ - // [{"/from/file/path":"/to/file/path" }] - let files = {}; - clipboard.files.forEach((f) => { - files[`${clipboard.from}/${f.name}`] = `${history.state.currentDirectory}/${f.name}` - }); - - const eventData = { - 'files': files, - 'token': csrf_token - }; - - $(CONTENTID.table).trigger(TRIGGERID.copyFile, eventData); - } - } - else{ - console.error('files clipboard is empty'); - } - }); - - } - - -} diff --git a/apps/dashboard/app/javascript/packs/files/DataTable.js b/apps/dashboard/app/javascript/packs/files/DataTable.js deleted file mode 100644 index bacaf4189d..0000000000 --- a/apps/dashboard/app/javascript/packs/files/DataTable.js +++ /dev/null @@ -1,427 +0,0 @@ -import 'datatables.net'; -import 'datatables.net-bs4/js/dataTables.bootstrap4'; -import 'datatables.net-select'; -import 'datatables.net-select-bs4'; -import Handlebars from 'handlebars'; -export { CONTENTID, TRIGGERID }; - -const TRIGGERID = { - changeDirectory: 'changeDirectory', - changeDirectoryPrompt: 'changeDirectoryPrompt', - clearClipboard: 'clearClipboard', - closeSwal: 'closeSwal', - copyFile: 'copyFile', - createFile: 'createFile', - createFolder: 'createFolder', - deleteFile: 'deleteFile', - deletePrompt: 'deletePrompt', - download: 'download', - getJsonResponse: 'getJsonResponse', - moveFile: 'moveFile', - newFile: 'newFile', - newFolder: 'newFolder', - reloadTable: 'reloadTable', - renameFile: 'renameFile', - renameFilePrompt: 'renameFilePrompt', - showError: 'showError', - showInput: 'showInput', - showLoading: 'showLoading', - showPrompt: 'showPrompt', - updateClipboard: 'updateClipboard', - updateClipboardView: 'updateClipboardView', -}; - -const CONTENTID = { - table: '#directory-contents', -}; - -let table = null; - -jQuery(function () { - table = new DataTable(); - - /* BUTTON ACTIONS */ - $("#new-file-btn").on("click", function () { - $(CONTENTID.table).trigger(TRIGGERID.newFile); - }); - - $("#new-folder-btn").on("click", function () { - $(CONTENTID.table).trigger(TRIGGERID.newFolder); - }); - - $("#download-btn").on("click", function () { - let selection = table.getTable().rows({ selected: true }).data(); - const eventData = { - selection: selection - }; - - $(CONTENTID.table).trigger(TRIGGERID.download, eventData); - - }); - - $("#delete-btn").on("click", function () { - let files = table.getTable().rows({ selected: true }).data().toArray().map((f) => f.name); - const eventData = { - files: files - }; - - $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); - - }); - - $("#copy-move-btn").on("click", function () { - let selection = table.getTable().rows({ selected: true }).data(); - const eventData = { - selection: selection - }; - - $(CONTENTID.table).trigger(TRIGGERID.updateClipboard, eventData); - - }); - - $("#goto-btn").on("click", function () { - $(CONTENTID.table).trigger(TRIGGERID.changeDirectoryPrompt); - }); - - // TODO: - // Will have to work on this one later. Not so straight forward. - // - // $("#upload-btn").on("click", function () { - // $(CONTENTID.table).trigger('uppyShowUploadPrompt'); - // }); - - /* END BUTTON ACTIONS */ - - /* TABLE ACTIONS */ - - $(CONTENTID.table).on(TRIGGERID.reloadTable, function (e, options) { - table.reloadTable(options.url); - }); - - $(CONTENTID.table).on(TRIGGERID.getJsonResponse, function (e, options) { - table.dataFromJsonResponse(options.response); - }); - - $(document).on('click', '.rename-file', function (e) { - e.preventDefault(); - let rowId = e.currentTarget.dataset.rowIndex; - let row = table.getTable().row(rowId).data(); - let fileName = $($.parseHTML(row.name)).text(); - - const eventData = { - file: fileName, - }; - - $(CONTENTID.table).trigger(TRIGGERID.renameFilePrompt, eventData); - - }); - - $(document).on('click', '.delete-file', function (e) { - e.preventDefault(); - let rowId = e.currentTarget.dataset.rowIndex; - let row = table.getTable().row(rowId).data(); - let fileName = $($.parseHTML(row.name)).text(); - - const eventData = { - files: [fileName] - }; - - $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); - - }); - - $('#show-dotfiles').on('change', () => { - let visible = $('#show-dotfiles').is(':checked'); - - table.setShowDotFiles(visible); - table.updateDotFileVisibility(); - }); - - $('#show-owner-mode').on('change', () => { - let visible = $('#show-owner-mode').is(':checked'); - - table.setShowOwnerMode(visible); - table.updateShowOwnerModeVisibility(); - }); - - - /* END TABLE ACTIONS */ - - /* DATATABLE LISTENERS */ - // prepend show dotfiles checkbox to search box - - table.getTable().on('draw.dtSelect.dt select.dtSelect.dt deselect.dtSelect.dt info.dt', function () { - table.updateDatatablesStatus(); - }); - - // if only 1 selected item, do not allow to de-select - table.getTable().on('user-select', function (e, dt, type, cell, originalEvent) { - var selected_rows = dt.rows({ selected: true }); - - if (originalEvent.target.closest('.actions-btn-group')) { - // dont do user select event when opening or working with actions btn dropdown - e.preventDefault(); - } - else if (selected_rows.count() == 1 && cell.index().row == selected_rows.indexes()[0]) { - // dont do user select because already selected - e.preventDefault(); - } - else { - // row need to find the checkbox to give it the focus - cell.node().closest('tr').querySelector('input[type=checkbox]').focus(); - } - }); - - table.getTable().on('deselect', function (e, dt, type, indexes) { - dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', false)); - }); - - table.getTable().on('select', function (e, dt, type, indexes) { - dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', true)); - }); - - $('#directory-contents tbody').on('click', 'tr td:first-child input[type=checkbox]', function () { - // input checkbox checked or not - - if ($(this).is(':checked')) { - // select row - table.getTable().row(this.closest('tr')).select(); - } - else { - // deselect row - table.getTable().row(this.closest('tr')).deselect(); - } - - this.focus(); - }); - - $('#directory-contents tbody').on('keydown', 'input, a', function (e) { - if (e.key == "ArrowDown") { - e.preventDefault(); - - // let tr = this.closest('tr').nextSibling; - let tr = $(this.closest('tr')).next('tr').get(0); - if (tr) { - tr.querySelector('input[type=checkbox]').focus(); - - // deselect if not holding shift key to work - // like native file browsers - if (!e.shiftKey) { - table.getTable().rows().deselect(); - } - - // select if moving down - table.getTable().row(tr).select(); - } - } - else if (e.key == "ArrowUp") { - e.preventDefault(); - - let tr = $(this.closest('tr')).prev('tr').get(0); - if (tr) { - tr.querySelector('input[type=checkbox]').focus(); - - // deselect if not holding shift key to work - // like native file browsers - if (!e.shiftKey) { - table.getTable().rows().deselect(); - } - - // select if moving up - table.getTable().row(tr).select(); - } - } - }); - - $.fn.dataTable.ext.search.push( - function (settings, data, dataIndex) { - return table.getShowDotFiles() || !data[2].startsWith('.'); - } - ) - - /* END DATATABLE LISTENERS */ -}); - -class DataTable { - _table = null; - - constructor() { - this.loadDataTable(); - this.reloadTable(); - } - - getTable() { - return this._table; - } - - loadDataTable() { - this._table = $(CONTENTID.table).on('xhr.dt', function (e, settings, json, xhr) { - // new ajax request for new data so update date/time - // if(json && json.time){ - if (json && json.time) { - history.replaceState(_.merge({}, history.state, { currentDirectoryUpdatedAt: json.time }), null); - } - }).DataTable({ - autoWidth: false, - language: { - search: 'Filter:', - }, - order: [[1, "asc"], [2, "asc"]], - rowId: 'id', - paging: false, - scrollCollapse: true, - select: { - style: 'os', - className: 'selected', - toggleable: true, - // don't trigger select checkbox column as select - // if you need to omit more columns, use a "selectable" class on the columns you want to support selection - selector: 'td:not(:first-child)' - }, - // https://datatables.net/reference/option/dom - // dom: '', dataTables_info nowrap - // - // put breadcrmbs below filter!!! - dom: "<'row'<'col-sm-12'f>>" + // normally <'row'<'col-sm-6'l><'col-sm-6'f>> but we disabled pagination so l is not needed (dropdown for selecting # rows) - "<'row'<'col-sm-12'<'dt-status-bar'<'datatables-status float-right'><'transfers-status'>>>>" + - "<'row'<'col-sm-12'tr>>", // normally this is <'row'<'col-sm-5'i><'col-sm-7'p>> but we disabled pagination so have info take whole row - columns: [ - { - data: null, - orderable: false, - defaultContent: '', - render: function (data, type, row, meta) { - var api = new $.fn.dataTable.Api(meta.settings); - let selected = api.rows(meta.row, { selected: true }).count() > 0; - return ` ${selected ? 'checked' : ''}`; - } - }, - { data: 'type', render: (data, type, row, meta) => data == 'd' ? ' dir' : ' file' }, // type - { name: 'name', data: 'name', className: 'text-break', render: (data, type, row, meta) => `${Handlebars.escapeExpression(data)}` }, // name - { name: 'actions', orderable: false, data: null, render: (data, type, row, meta) => this.actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, - { - data: 'size', - render: (data, type, row, meta) => { - return type == "display" ? row.human_size : data; - } - }, // human_size - { - data: 'modified_at', render: (data, type, row, meta) => { - if (type == "display") { - let date = new Date(data * 1000) - - // Return formatted date "3/23/2021 10:52:28 AM" - return isNaN(data) ? 'Invalid Date' : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` - } - else { - return data; - } - } - }, // modified_at - { name: 'owner', data: 'owner', visible: this.getShowOwnerMode() }, // owner - { - name: 'mode', data: 'mode', visible: this.getShowOwnerMode(), render: (data, type, row, meta) => { - - // mode after base conversion is a string such as "100755" - let mode = data.toString(8) - - // only care about the last 3 bits (755) - let chmodDisplay = mode.substring(mode.length - 3) - - return chmodDisplay - } - } // mode - ] - }); - - $('#directory-contents_filter').prepend(``) - $('#directory-contents_filter').prepend(``) - - } - - async reloadTable(url) { - var request_url = url || history.state.currentDirectoryUrl; - - try { - const response = await fetch(request_url, { headers: { 'Accept': 'application/json' } }); - const data = await this.dataFromJsonResponse(response); - $('#shell-wrapper').replaceWith((data.shell_dropdown_html)); - this._table.clear(); - this._table.rows.add(data.files); - this._table.draw(); - - $('#open-in-terminal-btn').attr('href', data.shell_url); - $('#open-in-terminal-btn').removeClass('disabled'); - - $(CONTENTID.table).trigger(TRIGGERID.updateClipboardView); - return await Promise.resolve(data); - } catch (e) { - const eventData = { - 'title': `Error occurred when attempting to access ${request_url}`, - 'message': e.message, - }; - - $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); - - $('#open-in-terminal-btn').addClass('disabled'); - return await Promise.reject(e); - } - } - - updateDotFileVisibility() { - this.reloadTable(); - } - - updateShowOwnerModeVisibility() { - let visible = this.getShowOwnerMode(); - - this._table.column('owner:name').visible(visible); - this._table.column('mode:name').visible(visible); - } - - - setShowOwnerMode(visible) { - localStorage.setItem('show-owner-mode', new Boolean(visible)); - } - - setShowDotFiles(visible) { - localStorage.setItem('show-dotfiles', new Boolean(visible)); - } - - - getShowDotFiles() { - return localStorage.getItem('show-dotfiles') == 'true' - } - - getShowOwnerMode() { - return localStorage.getItem('show-owner-mode') == 'true' - } - - dataFromJsonResponse(response) { - return new Promise((resolve, reject) => { - Promise.resolve(response) - .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) - .then(response => response.json()) - .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) - .catch((e) => reject(e)) - }); - } - - actionsBtnTemplate(options) { - let template_str = $('#actions-btn-template').html(); - let compiled = Handlebars.compile(template_str); - let results = compiled(options); - return results; - } - - updateDatatablesStatus() { - // from "function info ( api )" of https://cdn.datatables.net/select/1.3.1/js/dataTables.select.js - let api = this._table; - let rows = api.rows({ selected: true }).flatten().length, - page_info = api.page.info(), - msg = page_info.recordsTotal == page_info.recordsDisplay ? `Showing ${page_info.recordsDisplay} rows` : `Showing ${page_info.recordsDisplay} of ${page_info.recordsTotal} rows`; - - $('.datatables-status').html(`${msg} - ${rows} rows selected`); - } - -} diff --git a/apps/dashboard/app/javascript/packs/files/FileOps.js b/apps/dashboard/app/javascript/packs/files/FileOps.js deleted file mode 100644 index 298635f52a..0000000000 --- a/apps/dashboard/app/javascript/packs/files/FileOps.js +++ /dev/null @@ -1,478 +0,0 @@ -import Handlebars from 'handlebars'; -import {CONTENTID, TRIGGERID} from './DataTable.js'; - -let fileOps = null; - -let reportTransferTemplate = null; - -jQuery(function() { - fileOps = new FileOps(); - - $(CONTENTID.table).on(TRIGGERID.newFilePrompt, function () { - fileOps.newFilePrompt(); - }); - - $(CONTENTID.table).on(TRIGGERID.newFolderPrompt, function () { - fileOps.newFolderPrompt(); - }); - - $(CONTENTID.table).on(TRIGGERID.renameFilePrompt, function (e, options) { - fileOps.renameFilePrompt(options.file); - }); - - $(CONTENTID.table).on(TRIGGERID.renameFile, function (e, options) { - fileOps.renameFile(options.files, options.result.value); - }); - - $(CONTENTID.table).on(TRIGGERID.createFile, function (e, options) { - fileOps.newFile(options.result.value); - }); - - $(CONTENTID.table).on(TRIGGERID.createFolder, function (e, options) { - fileOps.newFolder(options.result.value); - }); - - $(CONTENTID.table).on(TRIGGERID.download, function (e, options) { - if(options.selection.length == 0) { - const eventData = { - 'title': 'Select a file, files, or directory to download', - 'message': 'You have selected none.', - }; - - $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); - - } else { - fileOps.download(options.selection); - } - }); - - $(CONTENTID.table).on(TRIGGERID.deletePrompt, function (e, options) { - if(options.files.length == 0) { - const eventData = { - 'title': 'Select a file, files, or directory to delete.', - 'message': 'You have selected none.', - }; - - $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); - - } else { - fileOps.deletePrompt(options.files); - } - }); - - $(CONTENTID.table).on(TRIGGERID.deleteFile, function (e, options) { - fileOps.delete(options.files); - }); - - $(CONTENTID.table).on(TRIGGERID.moveFile, function (e, options) { - fileOps.move(options.files, options.token); - }); - - $(CONTENTID.table).on(TRIGGERID.copyFile, function (e, options) { - fileOps.copy(options.files, options.token); - }); - - $(CONTENTID.table).on(TRIGGERID.changeDirectoryPrompt, function () { - fileOps.changeDirectoryPrompt(); - }); - - $(CONTENTID.table).on(TRIGGERID.changeDirectory, function (e, options) { - fileOps.changeDirectory(options.result.value); - }); - -}); - -class FileOps { - _timeout = 2000; - _attempts = 0; - _filesPath = filesPath; - - constructor() { - } - - changeDirectory(path) { - this.goto(filesPath + path); - } - - changeDirectoryPrompt() { - const eventData = { - action: 'changeDirectory', - 'inputOptions': { - title: 'Change Directory', - input: 'text', - inputLabel: 'Path', - inputValue: history.state.currentDirectory, - inputAttributes: { - spellcheck: 'false', - }, - showCancelButton: true, - inputValidator: (value) => { - if (! value || ! value.startsWith('/')) { - // TODO: validate filenames against listing - return 'Provide an absolute pathname' - } - } - } - }; - - $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); - - } - - deletePrompt(files) { - const eventData = { - action: TRIGGERID.deleteFile, - files: files, - 'inputOptions': { - title: files.length == 1 ? `Delete ${files[0]}?` : `Delete ${files.length} selected files?`, - text: 'Are you sure you want to delete the files: ' + files.join(', '), - showCancelButton: true, - } - }; - - $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); - - } - - - removeFiles(files) { - this.transferFiles(files, "rm", "remove files") - } - - renameFile(fileName, newFileName) { - let files = {}; - files[`${history.state.currentDirectory}/${fileName}`] = `${history.state.currentDirectory}/${newFileName}`; - this.transferFiles(files, "mv", "rename file") - } - - renameFilePrompt(fileName) { - const eventData = { - action: TRIGGERID.renameFile, - files: fileName, - 'inputOptions': { - title: 'Rename', - input: 'text', - inputLabel: 'Filename', - inputValue: fileName, - inputAttributes: { - spellcheck: 'false', - }, - showCancelButton: true, - inputValidator: (value) => { - if (! value) { - // TODO: validate filenames against listing - return 'Provide a filename to rename this to'; - } else if (value.includes('/') || value.includes('..')) { - return 'Filename cannot include / or ..'; - } - } - } - }; - - $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); - - } - - - - newFilePrompt() { - - const eventData = { - action: TRIGGERID.createFile, - 'inputOptions': { - title: 'New File', - input: 'text', - inputLabel: 'Filename', - showCancelButton: true, - inputValidator: (value) => { - if (!value) { - // TODO: validate filenames against listing - return 'Provide a non-empty filename.' - } - else if (value.includes("/")) { - // TODO: validate filenames against listing - return 'Illegal character (/) not allowed in filename.' - } - } - } - }; - - $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); - - } - - newFile(filename) { - let myFileOp = new FileOps(); - fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?touch=true`, { method: 'put', headers: { 'X-CSRF-Token': csrf_token } }) - .then(response => this.dataFromJsonResponse(response)) - .then(function () { - myFileOp.reloadTable(); - }) - .catch(function (e) { - myFileOp.alertError('Error occurred when attempting to create new file', e.message); - }); - } - - newFolderPrompt() { - - const eventData = { - action: TRIGGERID.createFolder, - 'inputOptions': { - title: 'New Folder', - input: 'text', - inputLabel: 'Folder name', - showCancelButton: true, - inputValidator: (value) => { - if (!value || value.includes("/")) { - // TODO: validate filenames against listing - return 'Provide a directory name that does not have / in it' - } - } - } - }; - - $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); - - } - - newFolder(filename) { - let myFileOp = new FileOps(); - fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) - .then(response => this.dataFromJsonResponse(response)) - .then(function () { - myFileOp.reloadTable(); - }) - .catch(function (e) { - myFileOp.alertError('Error occurred when attempting to create new folder', e.message); - }); - } - - download(selection) { - selection.toArray().forEach( (f) => { - if(f.type == 'd') { - this.downloadDirectory(f); - } else if (f.type == 'f') { - this.downloadFile(f); - } - }); - } - - downloadDirectory(file) { - let filename = $($.parseHTML(file.name)).text(), - canDownloadReq = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?can_download=${Date.now().toString()}` - - this.showSwalLoading('preparing to download directory: ' + file.name); - - fetch(canDownloadReq, { - method: 'GET', - headers: { - 'X-CSRF-Token': csrf_token, - 'Accept': 'application/json' - } - }) - .then(response => this.dataFromJsonResponse(response)) - .then(data => { - if (data.can_download) { - this.doneLoading(); - this.downloadFile(file) - } else { - this.doneLoading(); - this.alertError('Error while downloading', data.error_message); - } - }) - .catch(e => { - const eventData = { - 'title': 'Error while downloading', - 'message': e.message, - }; - - this.doneLoading(); - this.alertError('Error while downloading', data.error_message); - }) - } - - - downloadFile(file) { - // creating the temporary iframe is exactly what the CloudCmd does - // so this just repeats the status quo - - let filename = $($.parseHTML(file.name)).text(), - downloadUrl = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?download=${Date.now().toString()}`, - iframe = document.createElement('iframe'), - TIME = 30 * 1000; - - iframe.setAttribute('class', 'd-none'); - iframe.setAttribute('src', downloadUrl); - - document.body.appendChild(iframe); - - setTimeout(function() { - document.body.removeChild(iframe); - }, TIME); - } - - dataFromJsonResponse(response) { - return new Promise((resolve, reject) => { - Promise.resolve(response) - .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) - .then(response => response.json()) - .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) - .catch((e) => reject(e)) - }); - } - - - delete(files) { - this.showSwalLoading('Deleting files...: '); - - this.removeFiles(files.map(f => [history.state.currentDirectory, f].join('/')), csrf_token); - } - - transferFiles(files, action, summary){ - - this._attempts = 0; - - this.showSwalLoading(_.startCase(summary)); - - return fetch(transfersPath, { - method: 'post', - body: JSON.stringify({ - command: action, - files: files - }), - headers: { 'X-CSRF-Token': csrf_token } - }) - .then(response => this.dataFromJsonResponse(response)) - .then((data) => { - - if(! data.completed){ - // was async, gotta report on progress and start polling - this.reportTransfer(data); - this.findAndUpdateTransferStatus(data); - } else { - // if(data.target_dir == history.state.currentDirectory){ - // } - // this.findAndUpdateTransferStatus(data); - } - - if(action == 'mv' || action == 'cp') { - this.reloadTable(); - this.clearClipboard(); - } - - this.fadeOutTransferStatus(data); - this.doneLoading(); - this.reloadTable(); - - }) - .then(() => this.doneLoading()) - .catch(e => this.alertError('Error occurred when attempting to ' + summary, e.message)) - } - - findAndUpdateTransferStatus(data) { - let id = `#${data.id}`; - - if($(id).length){ - $(id).replaceWith(this.reportTransferTemplate(data)); - } else{ - $('.transfers-status').append(this.reportTransferTemplate(data)); - } - } - - fadeOutTransferStatus(data){ - let id = `#${data.id}`; - $(id).fadeOut(4000); - } - - reportTransferTemplate = (function(){ - let template_str = $('#transfer-template').html(); - return Handlebars.compile(template_str); - })(); - - poll(data) { - $.getJSON(data.show_json_url, function (newdata) { - // because of getJSON not being an actual piece of the object, we need to instantiate an instance FileOps for this section of code. - let myFileOp = new FileOps(); - myFileOp.findAndUpdateTransferStatus(newdata); - - if(newdata.completed) { - if(! newdata.error_message) { - if(newdata.target_dir == history.state.currentDirectory) { - myFileOp.reloadTable(); - } - - // 3. fade out after 5 seconds - myFileOp.fadeOutTransferStatus(newdata) - } - } - else { - // not completed yet, so poll again - setTimeout(function(){ - myFileOp._attempts++; - }, myFileOp._timeout); - } - }).fail(function() { - if (myFileOp._attempts >= 3) { - myFileOp.alertError('Operation may not have happened', 'Failed to retrieve file operation status.'); - } else { - setTimeout(function(){ - tmyFileOphis._attempts++; - }, myFileOp._timeout); - } - }); - } - - - reportTransfer(data) { - // 1. add the transfer label - this.findAndUpdateTransferStatus(data); - this.poll(data); - } - - move(files, token) { - this.transferFiles(files, 'mv', 'move files'); - } - - copy(files, token) { - this.transferFiles(files, 'cp', 'copy files'); - } - - alertError(title, message) { - const eventData = { - 'title': title, - 'message': message, - }; - - $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); - - } - - doneLoading() { - $(CONTENTID.table).trigger(TRIGGERID.closeSwal); - } - - clearClipboard() { - $(CONTENTID.table).trigger(TRIGGERID.clearClipboard); - } - - reloadTable(url) { - const eventData = { - 'url': url, - }; - - $(CONTENTID.table).trigger(TRIGGERID.reloadTable, eventData); - } - - showSwalLoading (message) { - const eventData = { - 'message': message, - }; - - $(CONTENTID.table).trigger(TRIGGERID.showLoading, eventData); - - } - - goto(url) { - window.open(url,"_self"); - } -} \ No newline at end of file diff --git a/apps/dashboard/app/javascript/packs/files/SweetAlert.js b/apps/dashboard/app/javascript/packs/files/SweetAlert.js deleted file mode 100644 index 9a3167f117..0000000000 --- a/apps/dashboard/app/javascript/packs/files/SweetAlert.js +++ /dev/null @@ -1,81 +0,0 @@ -import Swal from 'sweetalert2' -import {CONTENTID, TRIGGERID} from './DataTable.js'; - -let sweetAlert = null; - -jQuery(function() { - sweetAlert = new SweetAlert(); - $(CONTENTID.table).on(TRIGGERID.showError, function(e,options) { - sweetAlert.alertError(options.title, options.message); - }); - - $(CONTENTID.table).on(TRIGGERID.showPrompt, function(e,options) { - sweetAlert.alertError(options.title, options.message); - }); - - $(CONTENTID.table).on(TRIGGERID.showInput, function(e,options) { - sweetAlert.input(options); - }); - - $(CONTENTID.table).on(TRIGGERID.showLoading, function(e,options) { - sweetAlert.loading(options.message); - }); - - $(CONTENTID.table).on(TRIGGERID.closeSwal, function() { - sweetAlert.close(); - }); - -}); - -class SweetAlert { - - constructor() { - this.setMixin(); - } - - input(options) { - Swal.fire(options.inputOptions) - .then (function(result){ - if(result.isConfirmed) { - const eventData = { - result: result, - files: options.files ? options.files : null - }; - - $(CONTENTID.table).trigger(options.action, eventData); - } else { - $(CONTENTID.table).trigger(TRIGGERID.reloadTable); - } - }); - } - - setMixin() { - Swal.mixIn = ({ - showClass: { - popup: 'swal2-noanimation', - backdrop: 'swal2-noanimation' - }, - hideClass: { - popup: '', - backdrop: '' - } - }); - } - - alertError(error_title, error_message) { - Swal.fire(error_title, error_message, 'error'); - } - - async loading(title) { - Swal.fire({ - title: title, - allowOutsideClick: false, - showConfirmButton: false, - willOpen: () => { Swal.showLoading() } - }); - } - - close() { - Swal.close(); - } -} diff --git a/apps/dashboard/app/javascript/packs/files/UppyOps.js b/apps/dashboard/app/javascript/packs/files/UppyOps.js deleted file mode 100755 index 66306fe51e..0000000000 --- a/apps/dashboard/app/javascript/packs/files/UppyOps.js +++ /dev/null @@ -1,172 +0,0 @@ -import { Uppy, BasePlugin } from '@uppy/core' -import Dashboard from '@uppy/dashboard' -import XHRUpload from '@uppy/xhr-upload' -import _ from 'lodash'; - - -let uppy = null; - -jQuery(function() { - - class EmptyDirCreator extends BasePlugin { - constructor (uppy, opts){ - super(uppy, opts) - this.id = this.opts.id || 'EmptyDirUploaderCatcher'; - this.type = 'acquirer'; - - this.empty_dirs = []; - this.last_entries = []; - - this.handleRootDrop = this.handleRootDrop.bind(this); - this.createEmptyDirs = this.createEmptyDirs.bind(this); - - this.uppy = uppy; - } - - - - handleRootDrop (e) { - // from https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/getDroppedFiles/index.js - if (e.dataTransfer.items && e.dataTransfer.items[0] && 'webkitGetAsEntry' in e.dataTransfer.items[0]) { - // toArray https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/toArray.js#L4 - let items = Array.prototype.slice.call(e.dataTransfer.items || [], 0); - let entries = items.map(i => i.webkitGetAsEntry()).filter(i => i); - - return Promise.all(entries.map(i => getEmptyDirs(i))).then((dirs) => { - this.empty_dirs = this.empty_dirs.concat(_.flattenDeep(dirs)); - - }); - } - //else we don't have access to directory information - } - - createEmptyDirs (ids) { - if(! this.uppy.getState().error){ // avoid creating empty dirs if error occurred during upload - - //TODO: error checking and reporting - return Promise.all(this.empty_dirs.map((d) => { - // "fullPath" should actually be the path relative to the current directory - let filename = _.trimStart(d.fullPath, '/'); - - return fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) - //TODO: parse json response verify if there was an error creating directory and handle error - - })).then(() => this.empty_dirs = []); - } - } - - install () { - this.uppy.addPostProcessor(this.createEmptyDirs); - } - - uninstall () { - this.uppy.removePostProcessor(this.createEmptyDirs); - } - } - - uppy = new Uppy({ - restrictions: { - maxFileSize: maxFileSize, - } - }); - - uppy.use(EmptyDirCreator); - uppy.use(Dashboard, { - trigger: '#upload-btn', - fileManagerSelectionType: 'both', - disableThumbnailGenerator: true, - showLinkToFileUploadResult: false, - closeModalOnClickOutside: true, - closeAfterFinish: true, - allowMultipleUploads: false, - onRequestCloseModal: () => closeAndResetUppyModal(uppy), - note: 'Empty directories will be included in the upload only when a directory upload is initiated via drag and drop. This is because the File and Directory Entries API is available only on a drop event, not during an input change event.' - }); - uppy.use(XHRUpload, { - endpoint: filesUploadPath, - withCredentials: true, - fieldName: 'file', - limit: 1, - headers: { 'X-CSRF-Token': csrf_token }, - timeout: 128 * 1000, - }); - - uppy.on('file-added', (file) => { - uppy.setFileMeta(file.id, { parent: history.state.currentDirectory }); - if(file.meta.relativePath == null && file.data.webkitRelativePath){ - uppy.setFileMeta(file.id, { relativePath: file.data.webkitRelativePath }); - } - }); - - uppy.on('complete', (result) => { - if(result.successful.length > 0){ - reloadTable(); - } - }); - - // https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file - global.addEventListener("dragover",function(e){ - e = e || event; - e.preventDefault(); - },false); - - global.addEventListener("drop",function(e){ - e = e || event; - e.preventDefault(); - },false); - - $('#directory-contents').on('drop', function(e){ - this.classList.remove('dragover'); - // Prevent default behavior (Prevent file from being opened) - - // pass drop event to uppy dashboard - uppy.getPlugin('Dashboard').openModal().then(() => uppy.getPlugin('Dashboard').handleDrop(e.originalEvent)) - }); - - $('#directory-contents').on('dragover', function(e){ - this.classList.add('dragover'); - - // Prevent default behavior (Prevent file from being opened) - e.preventDefault(); - - // specifies what feedback will be shown to the user by setting the dropEffect attribute of the DataTransfer associated with the event - // too bad we can't show an indicator (no dragstart/end when dragging from OS to browser) - e.originalEvent.dataTransfer.dropEffect = 'copy'; - }); - - $('#directory-contents').on('dragleave', function(e){ - this.classList.remove('dragover'); - }); - -}); - -function closeAndResetUppyModal(uppy){ - uppy.getPlugin('Dashboard').closeModal(); - uppy.reset(); -} - -function getEmptyDirs(entry){ - return new Promise((resolve) => { - if(entry.isFile){ - resolve([]); - } - else{ - // getFilesAndDirectoriesFromDirectory has no return value, so turn this into a promise - getFilesAndDirectoriesFromDirectory(entry.createReader(), [], function(error){ console.error(error)}, { - onSuccess: (entries) => { - if(entries.length == 0){ - // this is an empty directory - resolve([entry]); - } - else{ - Promise.all(entries.map(e => getEmptyDirs(e))).then((dirs) => resolve(_.flattenDeep(dirs))); - } - } - }) - } - }); -} - -function reloadTable() { - $("#directory-contents").trigger('reloadTable'); -} \ No newline at end of file diff --git a/apps/dashboard/app/javascript/packs/files/index.js b/apps/dashboard/app/javascript/packs/files/index.js index d8af2f7ea1..0af8c8f5ae 100644 --- a/apps/dashboard/app/javascript/packs/files/index.js +++ b/apps/dashboard/app/javascript/packs/files/index.js @@ -1,6 +1,6 @@ -import {} from './ClipBoard.js'; -import {} from './DataTable.js'; -import {} from './FileOps.js'; -import {} from './SweetAlert.js'; -import {} from './UppyOps.js'; +import {} from './clip_board.js'; +import {} from './data_table.js'; +import {} from './file_ops.js'; +import {} from './sweet_alert.js'; +import {} from './uppy_ops.js'; From 60211f1808c715820ad26848432a24fa662d0d89 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Thu, 7 Apr 2022 15:49:50 -0400 Subject: [PATCH 32/47] Rename js files --- .../app/javascript/packs/files/clip_board.js | 161 ++++++ .../app/javascript/packs/files/data_table.js | 429 ++++++++++++++++ .../app/javascript/packs/files/file_ops.js | 478 ++++++++++++++++++ .../app/javascript/packs/files/sweet_alert.js | 81 +++ .../app/javascript/packs/files/uppy_ops.js | 172 +++++++ 5 files changed, 1321 insertions(+) create mode 100755 apps/dashboard/app/javascript/packs/files/clip_board.js create mode 100644 apps/dashboard/app/javascript/packs/files/data_table.js create mode 100644 apps/dashboard/app/javascript/packs/files/file_ops.js create mode 100644 apps/dashboard/app/javascript/packs/files/sweet_alert.js create mode 100755 apps/dashboard/app/javascript/packs/files/uppy_ops.js diff --git a/apps/dashboard/app/javascript/packs/files/clip_board.js b/apps/dashboard/app/javascript/packs/files/clip_board.js new file mode 100755 index 0000000000..6f62912bf2 --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/clip_board.js @@ -0,0 +1,161 @@ +import ClipboardJS from 'clipboard' +import Handlebars from 'handlebars'; +import {CONTENTID, TRIGGERID} from './data_table.js'; + +jQuery(function() { + + var clipBoard = new ClipBoard(); + + $(CONTENTID.table).on('success', function(e) { + $(e.trigger).tooltip({title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom'}).tooltip('show'); + setTimeout(() => $(e.trigger).tooltip('hide'), 2000); + e.clearSelection(); + }); + + $(CONTENTID.table).on('error', function(e) { + e.clearSelection(); + }); + + $(CONTENTID.table).on(TRIGGERID.clearClipboard, function (e, options) { + clipBoard.clearClipboard(); + clipBoard.updateViewForClipboard(); + }); + + $(CONTENTID.table).on(TRIGGERID.updateClipboard, function (e, options) { + if(options.selection.length == 0) { + const eventData = { + 'title': 'Select a file, files, or directory to copy or move.', + 'message': 'You have selected none.', + }; + + $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); + $(CONTENTID.table).trigger(TRIGGERID.clearClipbaord, eventData); + + } else { + clipBoard.updateClipboardFromSelection(options.selection); + clipBoard.updateViewForClipboard(); + } + }); + + $(CONTENTID.table).on(TRIGGERID.updateClipboardView, function (e, options) { + clipBoard.updateViewForClipboard(); + }); + + +}); + +class ClipBoard { + _clipBoard = null; + + constructor() { + this._clipBoard = new ClipboardJS('#copy-path'); + } + + getClipBoard() { + return this._clipBoard; + } + + clearClipboard() { + localStorage.removeItem('filesClipboard'); + } + + updateClipboardFromSelection(selection) { + + if(selection.length == 0){ + this.clearClipboard(); + } else { + let clipboardData = { + from: history.state.currentDirectory, + files: selection.toArray().map((f) => { + return { directory: f.type == 'd', name: f.name }; + }) + }; + + localStorage.setItem('filesClipboard', JSON.stringify(clipboardData)); + } + } + + + updateViewForClipboard() { + let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || '{}'), + template_str = $('#clipboard-template').html(), + template = Handlebars.compile(template_str); + + $('#clipboard').html(template(clipboard)); + + $('#clipboard-clear').on("click", () => { + this.clearClipboard(); + this.updateViewForClipboard(); + }); + + $('#clipboard-move-to-dir').on("click", () => { + let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || 'null'); + if(clipboard){ + clipboard.to = history.state.currentDirectory; + + if(clipboard.from == clipboard.to){ + console.error('clipboard from and to are identical') + // TODO: + } + else{ + let files = {}; + clipboard.files.forEach((f) => { + files[`${clipboard.from}/${f.name}`] = `${history.state.currentDirectory}/${f.name}` + }); + + const eventData = { + 'files': files, + 'token': csrf_token + }; + + $(CONTENTID.table).trigger(TRIGGERID.moveFile, eventData); + } + } + else{ + console.error('files clipboard is empty'); + } + }); + + + $('#clipboard-copy-to-dir').on("click", () => { + let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || 'null'); + + if(clipboard){ + clipboard.to = history.state.currentDirectory; + + if(clipboard.from == clipboard.to){ + console.error('clipboard from and to are identical') + + // TODO: we want to support this use case + // copy and paste as a new filename + // but lots of edge cases + // (overwrite or rename duplicates) + // _copy + // _copy_2 + // _copy_3 + // _copy_4 + } + else{ + // [{"/from/file/path":"/to/file/path" }] + let files = {}; + clipboard.files.forEach((f) => { + files[`${clipboard.from}/${f.name}`] = `${history.state.currentDirectory}/${f.name}` + }); + + const eventData = { + 'files': files, + 'token': csrf_token + }; + + $(CONTENTID.table).trigger(TRIGGERID.copyFile, eventData); + } + } + else{ + console.error('files clipboard is empty'); + } + }); + + } + + +} diff --git a/apps/dashboard/app/javascript/packs/files/data_table.js b/apps/dashboard/app/javascript/packs/files/data_table.js new file mode 100644 index 0000000000..d43e8bf999 --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/data_table.js @@ -0,0 +1,429 @@ +import 'datatables.net'; +import 'datatables.net-bs4/js/dataTables.bootstrap4'; +import 'datatables.net-select'; +import 'datatables.net-select-bs4'; +import Handlebars from 'handlebars'; +export { CONTENTID, TRIGGERID }; + +const TRIGGERID = { + changeDirectory: 'changeDirectory', + changeDirectoryPrompt: 'changeDirectoryPrompt', + clearClipboard: 'clearClipboard', + closeSwal: 'closeSwal', + copyFile: 'copyFile', + createFile: 'createFile', + createFolder: 'createFolder', + deleteFile: 'deleteFile', + deletePrompt: 'deletePrompt', + download: 'download', + getJsonResponse: 'getJsonResponse', + moveFile: 'moveFile', + newFile: 'newFile', + newFilePrompt: 'newFilePrompt', + newFolderPrompt: 'newFolderPrompt', + newFolder: 'newFolder', + reloadTable: 'reloadTable', + renameFile: 'renameFile', + renameFilePrompt: 'renameFilePrompt', + showError: 'showError', + showInput: 'showInput', + showLoading: 'showLoading', + showPrompt: 'showPrompt', + updateClipboard: 'updateClipboard', + updateClipboardView: 'updateClipboardView', +}; + +const CONTENTID = { + table: '#directory-contents', +}; + +let table = null; + +jQuery(function () { + table = new DataTable(); + + /* BUTTON ACTIONS */ + $("#new-file-btn").on("click", function () { + $(CONTENTID.table).trigger(TRIGGERID.newFilePrompt); + }); + + $("#new-folder-btn").on("click", function () { + $(CONTENTID.table).trigger(TRIGGERID.newFolderPrompt); + }); + + $("#download-btn").on("click", function () { + let selection = table.getTable().rows({ selected: true }).data(); + const eventData = { + selection: selection + }; + + $(CONTENTID.table).trigger(TRIGGERID.download, eventData); + + }); + + $("#delete-btn").on("click", function () { + let files = table.getTable().rows({ selected: true }).data().toArray().map((f) => f.name); + const eventData = { + files: files + }; + + $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); + + }); + + $("#copy-move-btn").on("click", function () { + let selection = table.getTable().rows({ selected: true }).data(); + const eventData = { + selection: selection + }; + + $(CONTENTID.table).trigger(TRIGGERID.updateClipboard, eventData); + + }); + + $("#goto-btn").on("click", function () { + $(CONTENTID.table).trigger(TRIGGERID.changeDirectoryPrompt); + }); + + // TODO: + // Will have to work on this one later. Not so straight forward. + // + // $("#upload-btn").on("click", function () { + // $(CONTENTID.table).trigger('uppyShowUploadPrompt'); + // }); + + /* END BUTTON ACTIONS */ + + /* TABLE ACTIONS */ + + $(CONTENTID.table).on(TRIGGERID.reloadTable, function (e, options) { + table.reloadTable(options.url); + }); + + $(CONTENTID.table).on(TRIGGERID.getJsonResponse, function (e, options) { + table.dataFromJsonResponse(options.response); + }); + + $(document).on('click', '.rename-file', function (e) { + e.preventDefault(); + let rowId = e.currentTarget.dataset.rowIndex; + let row = table.getTable().row(rowId).data(); + let fileName = $($.parseHTML(row.name)).text(); + + const eventData = { + file: fileName, + }; + + $(CONTENTID.table).trigger(TRIGGERID.renameFilePrompt, eventData); + + }); + + $(document).on('click', '.delete-file', function (e) { + e.preventDefault(); + let rowId = e.currentTarget.dataset.rowIndex; + let row = table.getTable().row(rowId).data(); + let fileName = $($.parseHTML(row.name)).text(); + + const eventData = { + files: [fileName] + }; + + $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); + + }); + + $('#show-dotfiles').on('change', () => { + let visible = $('#show-dotfiles').is(':checked'); + + table.setShowDotFiles(visible); + table.updateDotFileVisibility(); + }); + + $('#show-owner-mode').on('change', () => { + let visible = $('#show-owner-mode').is(':checked'); + + table.setShowOwnerMode(visible); + table.updateShowOwnerModeVisibility(); + }); + + + /* END TABLE ACTIONS */ + + /* DATATABLE LISTENERS */ + // prepend show dotfiles checkbox to search box + + table.getTable().on('draw.dtSelect.dt select.dtSelect.dt deselect.dtSelect.dt info.dt', function () { + table.updateDatatablesStatus(); + }); + + // if only 1 selected item, do not allow to de-select + table.getTable().on('user-select', function (e, dt, type, cell, originalEvent) { + var selected_rows = dt.rows({ selected: true }); + + if (originalEvent.target.closest('.actions-btn-group')) { + // dont do user select event when opening or working with actions btn dropdown + e.preventDefault(); + } + else if (selected_rows.count() == 1 && cell.index().row == selected_rows.indexes()[0]) { + // dont do user select because already selected + e.preventDefault(); + } + else { + // row need to find the checkbox to give it the focus + cell.node().closest('tr').querySelector('input[type=checkbox]').focus(); + } + }); + + table.getTable().on('deselect', function (e, dt, type, indexes) { + dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', false)); + }); + + table.getTable().on('select', function (e, dt, type, indexes) { + dt.rows(indexes).nodes().toArray().forEach(e => $(e).find('input[type=checkbox]').prop('checked', true)); + }); + + $('#directory-contents tbody').on('click', 'tr td:first-child input[type=checkbox]', function () { + // input checkbox checked or not + + if ($(this).is(':checked')) { + // select row + table.getTable().row(this.closest('tr')).select(); + } + else { + // deselect row + table.getTable().row(this.closest('tr')).deselect(); + } + + this.focus(); + }); + + $('#directory-contents tbody').on('keydown', 'input, a', function (e) { + if (e.key == "ArrowDown") { + e.preventDefault(); + + // let tr = this.closest('tr').nextSibling; + let tr = $(this.closest('tr')).next('tr').get(0); + if (tr) { + tr.querySelector('input[type=checkbox]').focus(); + + // deselect if not holding shift key to work + // like native file browsers + if (!e.shiftKey) { + table.getTable().rows().deselect(); + } + + // select if moving down + table.getTable().row(tr).select(); + } + } + else if (e.key == "ArrowUp") { + e.preventDefault(); + + let tr = $(this.closest('tr')).prev('tr').get(0); + if (tr) { + tr.querySelector('input[type=checkbox]').focus(); + + // deselect if not holding shift key to work + // like native file browsers + if (!e.shiftKey) { + table.getTable().rows().deselect(); + } + + // select if moving up + table.getTable().row(tr).select(); + } + } + }); + + $.fn.dataTable.ext.search.push( + function (settings, data, dataIndex) { + return table.getShowDotFiles() || !data[2].startsWith('.'); + } + ) + + /* END DATATABLE LISTENERS */ +}); + +class DataTable { + _table = null; + + constructor() { + this.loadDataTable(); + this.reloadTable(); + } + + getTable() { + return this._table; + } + + loadDataTable() { + this._table = $(CONTENTID.table).on('xhr.dt', function (e, settings, json, xhr) { + // new ajax request for new data so update date/time + // if(json && json.time){ + if (json && json.time) { + history.replaceState(_.merge({}, history.state, { currentDirectoryUpdatedAt: json.time }), null); + } + }).DataTable({ + autoWidth: false, + language: { + search: 'Filter:', + }, + order: [[1, "asc"], [2, "asc"]], + rowId: 'id', + paging: false, + scrollCollapse: true, + select: { + style: 'os', + className: 'selected', + toggleable: true, + // don't trigger select checkbox column as select + // if you need to omit more columns, use a "selectable" class on the columns you want to support selection + selector: 'td:not(:first-child)' + }, + // https://datatables.net/reference/option/dom + // dom: '', dataTables_info nowrap + // + // put breadcrmbs below filter!!! + dom: "<'row'<'col-sm-12'f>>" + // normally <'row'<'col-sm-6'l><'col-sm-6'f>> but we disabled pagination so l is not needed (dropdown for selecting # rows) + "<'row'<'col-sm-12'<'dt-status-bar'<'datatables-status float-right'><'transfers-status'>>>>" + + "<'row'<'col-sm-12'tr>>", // normally this is <'row'<'col-sm-5'i><'col-sm-7'p>> but we disabled pagination so have info take whole row + columns: [ + { + data: null, + orderable: false, + defaultContent: '', + render: function (data, type, row, meta) { + var api = new $.fn.dataTable.Api(meta.settings); + let selected = api.rows(meta.row, { selected: true }).count() > 0; + return ` ${selected ? 'checked' : ''}`; + } + }, + { data: 'type', render: (data, type, row, meta) => data == 'd' ? ' dir' : ' file' }, // type + { name: 'name', data: 'name', className: 'text-break', render: (data, type, row, meta) => `${Handlebars.escapeExpression(data)}` }, // name + { name: 'actions', orderable: false, data: null, render: (data, type, row, meta) => this.actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, + { + data: 'size', + render: (data, type, row, meta) => { + return type == "display" ? row.human_size : data; + } + }, // human_size + { + data: 'modified_at', render: (data, type, row, meta) => { + if (type == "display") { + let date = new Date(data * 1000) + + // Return formatted date "3/23/2021 10:52:28 AM" + return isNaN(data) ? 'Invalid Date' : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` + } + else { + return data; + } + } + }, // modified_at + { name: 'owner', data: 'owner', visible: this.getShowOwnerMode() }, // owner + { + name: 'mode', data: 'mode', visible: this.getShowOwnerMode(), render: (data, type, row, meta) => { + + // mode after base conversion is a string such as "100755" + let mode = data.toString(8) + + // only care about the last 3 bits (755) + let chmodDisplay = mode.substring(mode.length - 3) + + return chmodDisplay + } + } // mode + ] + }); + + $('#directory-contents_filter').prepend(``) + $('#directory-contents_filter').prepend(``) + + } + + async reloadTable(url) { + var request_url = url || history.state.currentDirectoryUrl; + + try { + const response = await fetch(request_url, { headers: { 'Accept': 'application/json' } }); + const data = await this.dataFromJsonResponse(response); + $('#shell-wrapper').replaceWith((data.shell_dropdown_html)); + this._table.clear(); + this._table.rows.add(data.files); + this._table.draw(); + + $('#open-in-terminal-btn').attr('href', data.shell_url); + $('#open-in-terminal-btn').removeClass('disabled'); + + $(CONTENTID.table).trigger(TRIGGERID.updateClipboardView); + return await Promise.resolve(data); + } catch (e) { + const eventData = { + 'title': `Error occurred when attempting to access ${request_url}`, + 'message': e.message, + }; + + $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); + + $('#open-in-terminal-btn').addClass('disabled'); + return await Promise.reject(e); + } + } + + updateDotFileVisibility() { + this.reloadTable(); + } + + updateShowOwnerModeVisibility() { + let visible = this.getShowOwnerMode(); + + this._table.column('owner:name').visible(visible); + this._table.column('mode:name').visible(visible); + } + + + setShowOwnerMode(visible) { + localStorage.setItem('show-owner-mode', new Boolean(visible)); + } + + setShowDotFiles(visible) { + localStorage.setItem('show-dotfiles', new Boolean(visible)); + } + + + getShowDotFiles() { + return localStorage.getItem('show-dotfiles') == 'true' + } + + getShowOwnerMode() { + return localStorage.getItem('show-owner-mode') == 'true' + } + + dataFromJsonResponse(response) { + return new Promise((resolve, reject) => { + Promise.resolve(response) + .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) + .then(response => response.json()) + .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) + .catch((e) => reject(e)) + }); + } + + actionsBtnTemplate(options) { + let template_str = $('#actions-btn-template').html(); + let compiled = Handlebars.compile(template_str); + let results = compiled(options); + return results; + } + + updateDatatablesStatus() { + // from "function info ( api )" of https://cdn.datatables.net/select/1.3.1/js/dataTables.select.js + let api = this._table; + let rows = api.rows({ selected: true }).flatten().length, + page_info = api.page.info(), + msg = page_info.recordsTotal == page_info.recordsDisplay ? `Showing ${page_info.recordsDisplay} rows` : `Showing ${page_info.recordsDisplay} of ${page_info.recordsTotal} rows`; + + $('.datatables-status').html(`${msg} - ${rows} rows selected`); + } + +} diff --git a/apps/dashboard/app/javascript/packs/files/file_ops.js b/apps/dashboard/app/javascript/packs/files/file_ops.js new file mode 100644 index 0000000000..9576adfa43 --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/file_ops.js @@ -0,0 +1,478 @@ +import Handlebars from 'handlebars'; +import {CONTENTID, TRIGGERID} from './data_table.js'; + +let fileOps = null; + +let reportTransferTemplate = null; + +jQuery(function() { + fileOps = new FileOps(); + + $(CONTENTID.table).on(TRIGGERID.newFilePrompt, function () { + fileOps.newFilePrompt(); + }); + + $(CONTENTID.table).on(TRIGGERID.newFolderPrompt, function () { + fileOps.newFolderPrompt(); + }); + + $(CONTENTID.table).on(TRIGGERID.renameFilePrompt, function (e, options) { + fileOps.renameFilePrompt(options.file); + }); + + $(CONTENTID.table).on(TRIGGERID.renameFile, function (e, options) { + fileOps.renameFile(options.files, options.result.value); + }); + + $(CONTENTID.table).on(TRIGGERID.createFile, function (e, options) { + fileOps.newFile(options.result.value); + }); + + $(CONTENTID.table).on(TRIGGERID.createFolder, function (e, options) { + fileOps.newFolder(options.result.value); + }); + + $(CONTENTID.table).on(TRIGGERID.download, function (e, options) { + if(options.selection.length == 0) { + const eventData = { + 'title': 'Select a file, files, or directory to download', + 'message': 'You have selected none.', + }; + + $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); + + } else { + fileOps.download(options.selection); + } + }); + + $(CONTENTID.table).on(TRIGGERID.deletePrompt, function (e, options) { + if(options.files.length == 0) { + const eventData = { + 'title': 'Select a file, files, or directory to delete.', + 'message': 'You have selected none.', + }; + + $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); + + } else { + fileOps.deletePrompt(options.files); + } + }); + + $(CONTENTID.table).on(TRIGGERID.deleteFile, function (e, options) { + fileOps.delete(options.files); + }); + + $(CONTENTID.table).on(TRIGGERID.moveFile, function (e, options) { + fileOps.move(options.files, options.token); + }); + + $(CONTENTID.table).on(TRIGGERID.copyFile, function (e, options) { + fileOps.copy(options.files, options.token); + }); + + $(CONTENTID.table).on(TRIGGERID.changeDirectoryPrompt, function () { + fileOps.changeDirectoryPrompt(); + }); + + $(CONTENTID.table).on(TRIGGERID.changeDirectory, function (e, options) { + fileOps.changeDirectory(options.result.value); + }); + +}); + +class FileOps { + _timeout = 2000; + _attempts = 0; + _filesPath = filesPath; + + constructor() { + } + + changeDirectory(path) { + this.goto(filesPath + path); + } + + changeDirectoryPrompt() { + const eventData = { + action: 'changeDirectory', + 'inputOptions': { + title: 'Change Directory', + input: 'text', + inputLabel: 'Path', + inputValue: history.state.currentDirectory, + inputAttributes: { + spellcheck: 'false', + }, + showCancelButton: true, + inputValidator: (value) => { + if (! value || ! value.startsWith('/')) { + // TODO: validate filenames against listing + return 'Provide an absolute pathname' + } + } + } + }; + + $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); + + } + + deletePrompt(files) { + const eventData = { + action: TRIGGERID.deleteFile, + files: files, + 'inputOptions': { + title: files.length == 1 ? `Delete ${files[0]}?` : `Delete ${files.length} selected files?`, + text: 'Are you sure you want to delete the files: ' + files.join(', '), + showCancelButton: true, + } + }; + + $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); + + } + + + removeFiles(files) { + this.transferFiles(files, "rm", "remove files") + } + + renameFile(fileName, newFileName) { + let files = {}; + files[`${history.state.currentDirectory}/${fileName}`] = `${history.state.currentDirectory}/${newFileName}`; + this.transferFiles(files, "mv", "rename file") + } + + renameFilePrompt(fileName) { + const eventData = { + action: TRIGGERID.renameFile, + files: fileName, + 'inputOptions': { + title: 'Rename', + input: 'text', + inputLabel: 'Filename', + inputValue: fileName, + inputAttributes: { + spellcheck: 'false', + }, + showCancelButton: true, + inputValidator: (value) => { + if (! value) { + // TODO: validate filenames against listing + return 'Provide a filename to rename this to'; + } else if (value.includes('/') || value.includes('..')) { + return 'Filename cannot include / or ..'; + } + } + } + }; + + $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); + + } + + + + newFilePrompt() { + + const eventData = { + action: TRIGGERID.createFile, + 'inputOptions': { + title: 'New File', + input: 'text', + inputLabel: 'Filename', + showCancelButton: true, + inputValidator: (value) => { + if (!value) { + // TODO: validate filenames against listing + return 'Provide a non-empty filename.' + } + else if (value.includes("/")) { + // TODO: validate filenames against listing + return 'Illegal character (/) not allowed in filename.' + } + } + } + }; + + $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); + + } + + newFile(filename) { + let myFileOp = new FileOps(); + fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?touch=true`, { method: 'put', headers: { 'X-CSRF-Token': csrf_token } }) + .then(response => this.dataFromJsonResponse(response)) + .then(function () { + myFileOp.reloadTable(); + }) + .catch(function (e) { + myFileOp.alertError('Error occurred when attempting to create new file', e.message); + }); + } + + newFolderPrompt() { + + const eventData = { + action: TRIGGERID.createFolder, + 'inputOptions': { + title: 'New Folder', + input: 'text', + inputLabel: 'Folder name', + showCancelButton: true, + inputValidator: (value) => { + if (!value || value.includes("/")) { + // TODO: validate filenames against listing + return 'Provide a directory name that does not have / in it' + } + } + } + }; + + $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); + + } + + newFolder(filename) { + let myFileOp = new FileOps(); + fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) + .then(response => this.dataFromJsonResponse(response)) + .then(function () { + myFileOp.reloadTable(); + }) + .catch(function (e) { + myFileOp.alertError('Error occurred when attempting to create new folder', e.message); + }); + } + + download(selection) { + selection.toArray().forEach( (f) => { + if(f.type == 'd') { + this.downloadDirectory(f); + } else if (f.type == 'f') { + this.downloadFile(f); + } + }); + } + + downloadDirectory(file) { + let filename = $($.parseHTML(file.name)).text(), + canDownloadReq = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?can_download=${Date.now().toString()}` + + this.showSwalLoading('preparing to download directory: ' + file.name); + + fetch(canDownloadReq, { + method: 'GET', + headers: { + 'X-CSRF-Token': csrf_token, + 'Accept': 'application/json' + } + }) + .then(response => this.dataFromJsonResponse(response)) + .then(data => { + if (data.can_download) { + this.doneLoading(); + this.downloadFile(file) + } else { + this.doneLoading(); + this.alertError('Error while downloading', data.error_message); + } + }) + .catch(e => { + const eventData = { + 'title': 'Error while downloading', + 'message': e.message, + }; + + this.doneLoading(); + this.alertError('Error while downloading', data.error_message); + }) + } + + + downloadFile(file) { + // creating the temporary iframe is exactly what the CloudCmd does + // so this just repeats the status quo + + let filename = $($.parseHTML(file.name)).text(), + downloadUrl = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?download=${Date.now().toString()}`, + iframe = document.createElement('iframe'), + TIME = 30 * 1000; + + iframe.setAttribute('class', 'd-none'); + iframe.setAttribute('src', downloadUrl); + + document.body.appendChild(iframe); + + setTimeout(function() { + document.body.removeChild(iframe); + }, TIME); + } + + dataFromJsonResponse(response) { + return new Promise((resolve, reject) => { + Promise.resolve(response) + .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) + .then(response => response.json()) + .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) + .catch((e) => reject(e)) + }); + } + + + delete(files) { + this.showSwalLoading('Deleting files...: '); + + this.removeFiles(files.map(f => [history.state.currentDirectory, f].join('/')), csrf_token); + } + + transferFiles(files, action, summary){ + + this._attempts = 0; + + this.showSwalLoading(_.startCase(summary)); + + return fetch(transfersPath, { + method: 'post', + body: JSON.stringify({ + command: action, + files: files + }), + headers: { 'X-CSRF-Token': csrf_token } + }) + .then(response => this.dataFromJsonResponse(response)) + .then((data) => { + + if(! data.completed){ + // was async, gotta report on progress and start polling + this.reportTransfer(data); + this.findAndUpdateTransferStatus(data); + } else { + // if(data.target_dir == history.state.currentDirectory){ + // } + // this.findAndUpdateTransferStatus(data); + } + + if(action == 'mv' || action == 'cp') { + this.reloadTable(); + this.clearClipboard(); + } + + this.fadeOutTransferStatus(data); + this.doneLoading(); + this.reloadTable(); + + }) + .then(() => this.doneLoading()) + .catch(e => this.alertError('Error occurred when attempting to ' + summary, e.message)) + } + + findAndUpdateTransferStatus(data) { + let id = `#${data.id}`; + + if($(id).length){ + $(id).replaceWith(this.reportTransferTemplate(data)); + } else{ + $('.transfers-status').append(this.reportTransferTemplate(data)); + } + } + + fadeOutTransferStatus(data){ + let id = `#${data.id}`; + $(id).fadeOut(4000); + } + + reportTransferTemplate = (function(){ + let template_str = $('#transfer-template').html(); + return Handlebars.compile(template_str); + })(); + + poll(data) { + $.getJSON(data.show_json_url, function (newdata) { + // because of getJSON not being an actual piece of the object, we need to instantiate an instance FileOps for this section of code. + let myFileOp = new FileOps(); + myFileOp.findAndUpdateTransferStatus(newdata); + + if(newdata.completed) { + if(! newdata.error_message) { + if(newdata.target_dir == history.state.currentDirectory) { + myFileOp.reloadTable(); + } + + // 3. fade out after 5 seconds + myFileOp.fadeOutTransferStatus(newdata) + } + } + else { + // not completed yet, so poll again + setTimeout(function(){ + myFileOp._attempts++; + }, myFileOp._timeout); + } + }).fail(function() { + if (myFileOp._attempts >= 3) { + myFileOp.alertError('Operation may not have happened', 'Failed to retrieve file operation status.'); + } else { + setTimeout(function(){ + tmyFileOphis._attempts++; + }, myFileOp._timeout); + } + }); + } + + + reportTransfer(data) { + // 1. add the transfer label + this.findAndUpdateTransferStatus(data); + this.poll(data); + } + + move(files, token) { + this.transferFiles(files, 'mv', 'move files'); + } + + copy(files, token) { + this.transferFiles(files, 'cp', 'copy files'); + } + + alertError(title, message) { + const eventData = { + 'title': title, + 'message': message, + }; + + $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); + + } + + doneLoading() { + $(CONTENTID.table).trigger(TRIGGERID.closeSwal); + } + + clearClipboard() { + $(CONTENTID.table).trigger(TRIGGERID.clearClipboard); + } + + reloadTable(url) { + const eventData = { + 'url': url, + }; + + $(CONTENTID.table).trigger(TRIGGERID.reloadTable, eventData); + } + + showSwalLoading (message) { + const eventData = { + 'message': message, + }; + + $(CONTENTID.table).trigger(TRIGGERID.showLoading, eventData); + + } + + goto(url) { + window.open(url,"_self"); + } +} \ No newline at end of file diff --git a/apps/dashboard/app/javascript/packs/files/sweet_alert.js b/apps/dashboard/app/javascript/packs/files/sweet_alert.js new file mode 100644 index 0000000000..b5817e8f6c --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/sweet_alert.js @@ -0,0 +1,81 @@ +import Swal from 'sweetalert2' +import {CONTENTID, TRIGGERID} from './data_table.js'; + +let sweetAlert = null; + +jQuery(function() { + sweetAlert = new SweetAlert(); + $(CONTENTID.table).on(TRIGGERID.showError, function(e,options) { + sweetAlert.alertError(options.title, options.message); + }); + + $(CONTENTID.table).on(TRIGGERID.showPrompt, function(e,options) { + sweetAlert.alertError(options.title, options.message); + }); + + $(CONTENTID.table).on(TRIGGERID.showInput, function(e,options) { + sweetAlert.input(options); + }); + + $(CONTENTID.table).on(TRIGGERID.showLoading, function(e,options) { + sweetAlert.loading(options.message); + }); + + $(CONTENTID.table).on(TRIGGERID.closeSwal, function() { + sweetAlert.close(); + }); + +}); + +class SweetAlert { + + constructor() { + this.setMixin(); + } + + input(options) { + Swal.fire(options.inputOptions) + .then (function(result){ + if(result.isConfirmed) { + const eventData = { + result: result, + files: options.files ? options.files : null + }; + + $(CONTENTID.table).trigger(options.action, eventData); + } else { + $(CONTENTID.table).trigger(TRIGGERID.reloadTable); + } + }); + } + + setMixin() { + Swal.mixIn = ({ + showClass: { + popup: 'swal2-noanimation', + backdrop: 'swal2-noanimation' + }, + hideClass: { + popup: '', + backdrop: '' + } + }); + } + + alertError(error_title, error_message) { + Swal.fire(error_title, error_message, 'error'); + } + + async loading(title) { + Swal.fire({ + title: title, + allowOutsideClick: false, + showConfirmButton: false, + willOpen: () => { Swal.showLoading() } + }); + } + + close() { + Swal.close(); + } +} diff --git a/apps/dashboard/app/javascript/packs/files/uppy_ops.js b/apps/dashboard/app/javascript/packs/files/uppy_ops.js new file mode 100755 index 0000000000..66306fe51e --- /dev/null +++ b/apps/dashboard/app/javascript/packs/files/uppy_ops.js @@ -0,0 +1,172 @@ +import { Uppy, BasePlugin } from '@uppy/core' +import Dashboard from '@uppy/dashboard' +import XHRUpload from '@uppy/xhr-upload' +import _ from 'lodash'; + + +let uppy = null; + +jQuery(function() { + + class EmptyDirCreator extends BasePlugin { + constructor (uppy, opts){ + super(uppy, opts) + this.id = this.opts.id || 'EmptyDirUploaderCatcher'; + this.type = 'acquirer'; + + this.empty_dirs = []; + this.last_entries = []; + + this.handleRootDrop = this.handleRootDrop.bind(this); + this.createEmptyDirs = this.createEmptyDirs.bind(this); + + this.uppy = uppy; + } + + + + handleRootDrop (e) { + // from https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/getDroppedFiles/index.js + if (e.dataTransfer.items && e.dataTransfer.items[0] && 'webkitGetAsEntry' in e.dataTransfer.items[0]) { + // toArray https://github.com/transloadit/uppy/blob/7ce58beeb620df3df0640cb369f5d71e3d3f751f/packages/%40uppy/utils/src/toArray.js#L4 + let items = Array.prototype.slice.call(e.dataTransfer.items || [], 0); + let entries = items.map(i => i.webkitGetAsEntry()).filter(i => i); + + return Promise.all(entries.map(i => getEmptyDirs(i))).then((dirs) => { + this.empty_dirs = this.empty_dirs.concat(_.flattenDeep(dirs)); + + }); + } + //else we don't have access to directory information + } + + createEmptyDirs (ids) { + if(! this.uppy.getState().error){ // avoid creating empty dirs if error occurred during upload + + //TODO: error checking and reporting + return Promise.all(this.empty_dirs.map((d) => { + // "fullPath" should actually be the path relative to the current directory + let filename = _.trimStart(d.fullPath, '/'); + + return fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) + //TODO: parse json response verify if there was an error creating directory and handle error + + })).then(() => this.empty_dirs = []); + } + } + + install () { + this.uppy.addPostProcessor(this.createEmptyDirs); + } + + uninstall () { + this.uppy.removePostProcessor(this.createEmptyDirs); + } + } + + uppy = new Uppy({ + restrictions: { + maxFileSize: maxFileSize, + } + }); + + uppy.use(EmptyDirCreator); + uppy.use(Dashboard, { + trigger: '#upload-btn', + fileManagerSelectionType: 'both', + disableThumbnailGenerator: true, + showLinkToFileUploadResult: false, + closeModalOnClickOutside: true, + closeAfterFinish: true, + allowMultipleUploads: false, + onRequestCloseModal: () => closeAndResetUppyModal(uppy), + note: 'Empty directories will be included in the upload only when a directory upload is initiated via drag and drop. This is because the File and Directory Entries API is available only on a drop event, not during an input change event.' + }); + uppy.use(XHRUpload, { + endpoint: filesUploadPath, + withCredentials: true, + fieldName: 'file', + limit: 1, + headers: { 'X-CSRF-Token': csrf_token }, + timeout: 128 * 1000, + }); + + uppy.on('file-added', (file) => { + uppy.setFileMeta(file.id, { parent: history.state.currentDirectory }); + if(file.meta.relativePath == null && file.data.webkitRelativePath){ + uppy.setFileMeta(file.id, { relativePath: file.data.webkitRelativePath }); + } + }); + + uppy.on('complete', (result) => { + if(result.successful.length > 0){ + reloadTable(); + } + }); + + // https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file + global.addEventListener("dragover",function(e){ + e = e || event; + e.preventDefault(); + },false); + + global.addEventListener("drop",function(e){ + e = e || event; + e.preventDefault(); + },false); + + $('#directory-contents').on('drop', function(e){ + this.classList.remove('dragover'); + // Prevent default behavior (Prevent file from being opened) + + // pass drop event to uppy dashboard + uppy.getPlugin('Dashboard').openModal().then(() => uppy.getPlugin('Dashboard').handleDrop(e.originalEvent)) + }); + + $('#directory-contents').on('dragover', function(e){ + this.classList.add('dragover'); + + // Prevent default behavior (Prevent file from being opened) + e.preventDefault(); + + // specifies what feedback will be shown to the user by setting the dropEffect attribute of the DataTransfer associated with the event + // too bad we can't show an indicator (no dragstart/end when dragging from OS to browser) + e.originalEvent.dataTransfer.dropEffect = 'copy'; + }); + + $('#directory-contents').on('dragleave', function(e){ + this.classList.remove('dragover'); + }); + +}); + +function closeAndResetUppyModal(uppy){ + uppy.getPlugin('Dashboard').closeModal(); + uppy.reset(); +} + +function getEmptyDirs(entry){ + return new Promise((resolve) => { + if(entry.isFile){ + resolve([]); + } + else{ + // getFilesAndDirectoriesFromDirectory has no return value, so turn this into a promise + getFilesAndDirectoriesFromDirectory(entry.createReader(), [], function(error){ console.error(error)}, { + onSuccess: (entries) => { + if(entries.length == 0){ + // this is an empty directory + resolve([entry]); + } + else{ + Promise.all(entries.map(e => getEmptyDirs(e))).then((dirs) => resolve(_.flattenDeep(dirs))); + } + } + }) + } + }); +} + +function reloadTable() { + $("#directory-contents").trigger('reloadTable'); +} \ No newline at end of file From 02d52742be51f122c7dc2122be2f8ca20b3760d2 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Fri, 8 Apr 2022 14:30:36 -0400 Subject: [PATCH 33/47] Refactored EventName to be defined in appropriate JS Class --- .../app/javascript/packs/files/clip_board.js | 108 ++++++++++-------- .../app/javascript/packs/files/data_table.js | 57 +++------ .../app/javascript/packs/files/file_ops.js | 83 +++++++++----- .../app/javascript/packs/files/sweet_alert.js | 24 ++-- .../app/javascript/packs/files/uppy_ops.js | 4 +- 5 files changed, 150 insertions(+), 126 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/clip_board.js b/apps/dashboard/app/javascript/packs/files/clip_board.js index 6f62912bf2..e5c55c10f6 100755 --- a/apps/dashboard/app/javascript/packs/files/clip_board.js +++ b/apps/dashboard/app/javascript/packs/files/clip_board.js @@ -1,35 +1,45 @@ import ClipboardJS from 'clipboard' import Handlebars from 'handlebars'; -import {CONTENTID, TRIGGERID} from './data_table.js'; +import {CONTENTID} from './data_table.js'; +import {EVENTNAME as SWAL_EVENTNAME} from './sweet_alert.js'; +import {EVENTNAME as FILEOPS_EVENTNAME} from './file_ops.js'; + +export {EVENTNAME}; + +const EVENTNAME = { + clearClipboard: 'clearClipboard', + updateClipboard: 'updateClipboard', + updateClipboardView: 'updateClipboardView', +} + +jQuery(function () { -jQuery(function() { - var clipBoard = new ClipBoard(); - $(CONTENTID.table).on('success', function(e) { - $(e.trigger).tooltip({title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom'}).tooltip('show'); + $(CONTENTID.table).on('success', function (e) { + $(e.trigger).tooltip({ title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom' }).tooltip('show'); setTimeout(() => $(e.trigger).tooltip('hide'), 2000); e.clearSelection(); }); - - $(CONTENTID.table).on('error', function(e) { + + $(CONTENTID.table).on('error', function (e) { e.clearSelection(); }); - $(CONTENTID.table).on(TRIGGERID.clearClipboard, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.clearClipboard, function (e, options) { clipBoard.clearClipboard(); clipBoard.updateViewForClipboard(); }); - $(CONTENTID.table).on(TRIGGERID.updateClipboard, function (e, options) { - if(options.selection.length == 0) { + $(CONTENTID.table).on(EVENTNAME.updateClipboard, function (e, options) { + if (options.selection.length == 0) { const eventData = { - 'title': 'Select a file, files, or directory to copy or move.', - 'message': 'You have selected none.', + 'title': 'Select a file, files, or directory to copy or move.', + 'message': 'You have selected none.', }; - $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); - $(CONTENTID.table).trigger(TRIGGERID.clearClipbaord, eventData); + $(CONTENTID.table).trigger(SWAL_EVENTNAME.showError, eventData); + $(CONTENTID.table).trigger(EVENTNAME.clearClipbaord, eventData); } else { clipBoard.updateClipboardFromSelection(options.selection); @@ -37,7 +47,7 @@ jQuery(function() { } }); - $(CONTENTID.table).on(TRIGGERID.updateClipboardView, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.updateClipboardView, function (e, options) { clipBoard.updateViewForClipboard(); }); @@ -60,72 +70,72 @@ class ClipBoard { } updateClipboardFromSelection(selection) { - - if(selection.length == 0){ + + if (selection.length == 0) { this.clearClipboard(); } else { let clipboardData = { from: history.state.currentDirectory, files: selection.toArray().map((f) => { - return { directory: f.type == 'd', name: f.name }; + return { directory: f.type == 'd', name: f.name }; }) }; - + localStorage.setItem('filesClipboard', JSON.stringify(clipboardData)); } } - - + + updateViewForClipboard() { let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || '{}'), - template_str = $('#clipboard-template').html(), - template = Handlebars.compile(template_str); - + template_str = $('#clipboard-template').html(), + template = Handlebars.compile(template_str); + $('#clipboard').html(template(clipboard)); - + $('#clipboard-clear').on("click", () => { - this.clearClipboard(); - this.updateViewForClipboard(); + this.clearClipboard(); + this.updateViewForClipboard(); }); - + $('#clipboard-move-to-dir').on("click", () => { let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || 'null'); - if(clipboard){ + if (clipboard) { clipboard.to = history.state.currentDirectory; - - if(clipboard.from == clipboard.to){ + + if (clipboard.from == clipboard.to) { console.error('clipboard from and to are identical') // TODO: } - else{ + else { let files = {}; clipboard.files.forEach((f) => { files[`${clipboard.from}/${f.name}`] = `${history.state.currentDirectory}/${f.name}` }); - + const eventData = { 'files': files, 'token': csrf_token }; - - $(CONTENTID.table).trigger(TRIGGERID.moveFile, eventData); + + $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.moveFile, eventData); } } - else{ + else { console.error('files clipboard is empty'); } }); - - + + $('#clipboard-copy-to-dir').on("click", () => { let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || 'null'); - - if(clipboard){ + + if (clipboard) { clipboard.to = history.state.currentDirectory; - - if(clipboard.from == clipboard.to){ + + if (clipboard.from == clipboard.to) { console.error('clipboard from and to are identical') - + // TODO: we want to support this use case // copy and paste as a new filename // but lots of edge cases @@ -135,7 +145,7 @@ class ClipBoard { // _copy_3 // _copy_4 } - else{ + else { // [{"/from/file/path":"/to/file/path" }] let files = {}; clipboard.files.forEach((f) => { @@ -146,16 +156,16 @@ class ClipBoard { 'files': files, 'token': csrf_token }; - - $(CONTENTID.table).trigger(TRIGGERID.copyFile, eventData); + + $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.copyFile, eventData); } } - else{ + else { console.error('files clipboard is empty'); } }); - + } - + } diff --git a/apps/dashboard/app/javascript/packs/files/data_table.js b/apps/dashboard/app/javascript/packs/files/data_table.js index d43e8bf999..9e65c676ca 100644 --- a/apps/dashboard/app/javascript/packs/files/data_table.js +++ b/apps/dashboard/app/javascript/packs/files/data_table.js @@ -3,34 +3,15 @@ import 'datatables.net-bs4/js/dataTables.bootstrap4'; import 'datatables.net-select'; import 'datatables.net-select-bs4'; import Handlebars from 'handlebars'; -export { CONTENTID, TRIGGERID }; - -const TRIGGERID = { - changeDirectory: 'changeDirectory', - changeDirectoryPrompt: 'changeDirectoryPrompt', - clearClipboard: 'clearClipboard', - closeSwal: 'closeSwal', - copyFile: 'copyFile', - createFile: 'createFile', - createFolder: 'createFolder', - deleteFile: 'deleteFile', - deletePrompt: 'deletePrompt', - download: 'download', +import {EVENTNAME as CLIPBOARD_EVENTNAME} from './clip_board.js'; +import {EVENTNAME as FILEOPS_EVENTNAME} from './file_ops.js'; +import {EVENTNAME as SWAL_EVENTNAME} from './sweet_alert.js'; + +export { CONTENTID, EVENTNAME }; + +const EVENTNAME = { getJsonResponse: 'getJsonResponse', - moveFile: 'moveFile', - newFile: 'newFile', - newFilePrompt: 'newFilePrompt', - newFolderPrompt: 'newFolderPrompt', - newFolder: 'newFolder', reloadTable: 'reloadTable', - renameFile: 'renameFile', - renameFilePrompt: 'renameFilePrompt', - showError: 'showError', - showInput: 'showInput', - showLoading: 'showLoading', - showPrompt: 'showPrompt', - updateClipboard: 'updateClipboard', - updateClipboardView: 'updateClipboardView', }; const CONTENTID = { @@ -44,11 +25,11 @@ jQuery(function () { /* BUTTON ACTIONS */ $("#new-file-btn").on("click", function () { - $(CONTENTID.table).trigger(TRIGGERID.newFilePrompt); + $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.newFilePrompt); }); $("#new-folder-btn").on("click", function () { - $(CONTENTID.table).trigger(TRIGGERID.newFolderPrompt); + $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.newFolderPrompt); }); $("#download-btn").on("click", function () { @@ -57,7 +38,7 @@ jQuery(function () { selection: selection }; - $(CONTENTID.table).trigger(TRIGGERID.download, eventData); + $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.download, eventData); }); @@ -67,7 +48,7 @@ jQuery(function () { files: files }; - $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); + $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.deletePrompt, eventData); }); @@ -77,12 +58,12 @@ jQuery(function () { selection: selection }; - $(CONTENTID.table).trigger(TRIGGERID.updateClipboard, eventData); + $(CONTENTID.table).trigger(CLIPBOARD_EVENTNAME.updateClipboard, eventData); }); $("#goto-btn").on("click", function () { - $(CONTENTID.table).trigger(TRIGGERID.changeDirectoryPrompt); + $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.changeDirectoryPrompt); }); // TODO: @@ -96,11 +77,11 @@ jQuery(function () { /* TABLE ACTIONS */ - $(CONTENTID.table).on(TRIGGERID.reloadTable, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.reloadTable, function (e, options) { table.reloadTable(options.url); }); - $(CONTENTID.table).on(TRIGGERID.getJsonResponse, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.getJsonResponse, function (e, options) { table.dataFromJsonResponse(options.response); }); @@ -114,7 +95,7 @@ jQuery(function () { file: fileName, }; - $(CONTENTID.table).trigger(TRIGGERID.renameFilePrompt, eventData); + $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.renameFilePrompt, eventData); }); @@ -128,7 +109,7 @@ jQuery(function () { files: [fileName] }; - $(CONTENTID.table).trigger(TRIGGERID.deletePrompt, eventData); + $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.deletePrompt, eventData); }); @@ -355,7 +336,7 @@ class DataTable { $('#open-in-terminal-btn').attr('href', data.shell_url); $('#open-in-terminal-btn').removeClass('disabled'); - $(CONTENTID.table).trigger(TRIGGERID.updateClipboardView); + $(CONTENTID.table).trigger(CLIPBOARD_EVENTNAME.updateClipboardView); return await Promise.resolve(data); } catch (e) { const eventData = { @@ -363,7 +344,7 @@ class DataTable { 'message': e.message, }; - $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); + $(CONTENTID.table).trigger(SWAL_EVENTNAME.showError, eventData); $('#open-in-terminal-btn').addClass('disabled'); return await Promise.reject(e); diff --git a/apps/dashboard/app/javascript/packs/files/file_ops.js b/apps/dashboard/app/javascript/packs/files/file_ops.js index 9576adfa43..27d952239f 100644 --- a/apps/dashboard/app/javascript/packs/files/file_ops.js +++ b/apps/dashboard/app/javascript/packs/files/file_ops.js @@ -1,5 +1,28 @@ import Handlebars from 'handlebars'; -import {CONTENTID, TRIGGERID} from './data_table.js'; +import {CONTENTID, EVENTNAME as DATATABLE_EVENTNAME} from './data_table.js'; +import {EVENTNAME as CLIPBOARD_EVENTNAME} from './clip_board.js'; +import {EVENTNAME as SWAL_EVENTNAME} from './sweet_alert.js'; + +export {EVENTNAME}; + +const EVENTNAME = { + changeDirectory: 'changeDirectory', + changeDirectoryPrompt: 'changeDirectoryPrompt', + copyFile: 'copyFile', + createFile: 'createFile', + createFolder: 'createFolder', + deleteFile: 'deleteFile', + deletePrompt: 'deletePrompt', + download: 'download', + moveFile: 'moveFile', + newFile: 'newFile', + newFilePrompt: 'newFilePrompt', + newFolderPrompt: 'newFolderPrompt', + newFolder: 'newFolder', + renameFile: 'renameFile', + renameFilePrompt: 'renameFilePrompt', +} + let fileOps = null; @@ -8,75 +31,75 @@ let reportTransferTemplate = null; jQuery(function() { fileOps = new FileOps(); - $(CONTENTID.table).on(TRIGGERID.newFilePrompt, function () { + $(CONTENTID.table).on(EVENTNAME.newFilePrompt, function () { fileOps.newFilePrompt(); }); - $(CONTENTID.table).on(TRIGGERID.newFolderPrompt, function () { + $(CONTENTID.table).on(EVENTNAME.newFolderPrompt, function () { fileOps.newFolderPrompt(); }); - $(CONTENTID.table).on(TRIGGERID.renameFilePrompt, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.renameFilePrompt, function (e, options) { fileOps.renameFilePrompt(options.file); }); - $(CONTENTID.table).on(TRIGGERID.renameFile, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.renameFile, function (e, options) { fileOps.renameFile(options.files, options.result.value); }); - $(CONTENTID.table).on(TRIGGERID.createFile, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.createFile, function (e, options) { fileOps.newFile(options.result.value); }); - $(CONTENTID.table).on(TRIGGERID.createFolder, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.createFolder, function (e, options) { fileOps.newFolder(options.result.value); }); - $(CONTENTID.table).on(TRIGGERID.download, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.download, function (e, options) { if(options.selection.length == 0) { const eventData = { 'title': 'Select a file, files, or directory to download', 'message': 'You have selected none.', }; - $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); + $(CONTENTID.table).trigger(SWAL_EVENTNAME.showError, eventData); } else { fileOps.download(options.selection); } }); - $(CONTENTID.table).on(TRIGGERID.deletePrompt, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.deletePrompt, function (e, options) { if(options.files.length == 0) { const eventData = { 'title': 'Select a file, files, or directory to delete.', 'message': 'You have selected none.', }; - $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); + $(CONTENTID.table).trigger(SWAL_EVENTNAME.showError, eventData); } else { fileOps.deletePrompt(options.files); } }); - $(CONTENTID.table).on(TRIGGERID.deleteFile, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.deleteFile, function (e, options) { fileOps.delete(options.files); }); - $(CONTENTID.table).on(TRIGGERID.moveFile, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.moveFile, function (e, options) { fileOps.move(options.files, options.token); }); - $(CONTENTID.table).on(TRIGGERID.copyFile, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.copyFile, function (e, options) { fileOps.copy(options.files, options.token); }); - $(CONTENTID.table).on(TRIGGERID.changeDirectoryPrompt, function () { + $(CONTENTID.table).on(EVENTNAME.changeDirectoryPrompt, function () { fileOps.changeDirectoryPrompt(); }); - $(CONTENTID.table).on(TRIGGERID.changeDirectory, function (e, options) { + $(CONTENTID.table).on(EVENTNAME.changeDirectory, function (e, options) { fileOps.changeDirectory(options.result.value); }); @@ -115,13 +138,13 @@ class FileOps { } }; - $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); + $(CONTENTID.table).trigger(SWAL_EVENTNAME.showInput, eventData); } deletePrompt(files) { const eventData = { - action: TRIGGERID.deleteFile, + action: EVENTNAME.deleteFile, files: files, 'inputOptions': { title: files.length == 1 ? `Delete ${files[0]}?` : `Delete ${files.length} selected files?`, @@ -130,7 +153,7 @@ class FileOps { } }; - $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); + $(CONTENTID.table).trigger(SWAL_EVENTNAME.showInput, eventData); } @@ -147,7 +170,7 @@ class FileOps { renameFilePrompt(fileName) { const eventData = { - action: TRIGGERID.renameFile, + action: EVENTNAME.renameFile, files: fileName, 'inputOptions': { title: 'Rename', @@ -169,7 +192,7 @@ class FileOps { } }; - $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); + $(CONTENTID.table).trigger(SWAL_EVENTNAME.showInput, eventData); } @@ -178,7 +201,7 @@ class FileOps { newFilePrompt() { const eventData = { - action: TRIGGERID.createFile, + action: EVENTNAME.createFile, 'inputOptions': { title: 'New File', input: 'text', @@ -197,7 +220,7 @@ class FileOps { } }; - $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); + $(CONTENTID.table).trigger(SWAL_EVENTNAME.showInput, eventData); } @@ -216,7 +239,7 @@ class FileOps { newFolderPrompt() { const eventData = { - action: TRIGGERID.createFolder, + action: EVENTNAME.createFolder, 'inputOptions': { title: 'New Folder', input: 'text', @@ -231,7 +254,7 @@ class FileOps { } }; - $(CONTENTID.table).trigger(TRIGGERID.showInput, eventData); + $(CONTENTID.table).trigger(SWAL_EVENTNAME.showInput, eventData); } @@ -443,16 +466,16 @@ class FileOps { 'message': message, }; - $(CONTENTID.table).trigger(TRIGGERID.showError, eventData); + $(CONTENTID.table).trigger(SWAL_EVENTNAME.showError, eventData); } doneLoading() { - $(CONTENTID.table).trigger(TRIGGERID.closeSwal); + $(CONTENTID.table).trigger(SWAL_EVENTNAME.closeSwal); } clearClipboard() { - $(CONTENTID.table).trigger(TRIGGERID.clearClipboard); + $(CONTENTID.table).trigger(CLIPBOARD_EVENTNAME.clearClipboard); } reloadTable(url) { @@ -460,7 +483,7 @@ class FileOps { 'url': url, }; - $(CONTENTID.table).trigger(TRIGGERID.reloadTable, eventData); + $(CONTENTID.table).trigger(DATATABLE_EVENTNAME.reloadTable, eventData); } showSwalLoading (message) { @@ -468,7 +491,7 @@ class FileOps { 'message': message, }; - $(CONTENTID.table).trigger(TRIGGERID.showLoading, eventData); + $(CONTENTID.table).trigger(SWAL_EVENTNAME.showLoading, eventData); } diff --git a/apps/dashboard/app/javascript/packs/files/sweet_alert.js b/apps/dashboard/app/javascript/packs/files/sweet_alert.js index b5817e8f6c..0efedd7a83 100644 --- a/apps/dashboard/app/javascript/packs/files/sweet_alert.js +++ b/apps/dashboard/app/javascript/packs/files/sweet_alert.js @@ -1,27 +1,37 @@ import Swal from 'sweetalert2' -import {CONTENTID, TRIGGERID} from './data_table.js'; +import {CONTENTID, EVENTNAME as DATATABLE_EVENTNAME} from './data_table.js'; + +export {EVENTNAME}; + +const EVENTNAME = { + showError: 'showError', + showInput: 'showInput', + showLoading: 'showLoading', + showPrompt: 'showPrompt', + closeSwal: 'closeSwal', +} let sweetAlert = null; jQuery(function() { sweetAlert = new SweetAlert(); - $(CONTENTID.table).on(TRIGGERID.showError, function(e,options) { + $(CONTENTID.table).on(EVENTNAME.showError, function(e,options) { sweetAlert.alertError(options.title, options.message); }); - $(CONTENTID.table).on(TRIGGERID.showPrompt, function(e,options) { + $(CONTENTID.table).on(EVENTNAME.showPrompt, function(e,options) { sweetAlert.alertError(options.title, options.message); }); - $(CONTENTID.table).on(TRIGGERID.showInput, function(e,options) { + $(CONTENTID.table).on(EVENTNAME.showInput, function(e,options) { sweetAlert.input(options); }); - $(CONTENTID.table).on(TRIGGERID.showLoading, function(e,options) { + $(CONTENTID.table).on(EVENTNAME.showLoading, function(e,options) { sweetAlert.loading(options.message); }); - $(CONTENTID.table).on(TRIGGERID.closeSwal, function() { + $(CONTENTID.table).on(EVENTNAME.closeSwal, function() { sweetAlert.close(); }); @@ -44,7 +54,7 @@ class SweetAlert { $(CONTENTID.table).trigger(options.action, eventData); } else { - $(CONTENTID.table).trigger(TRIGGERID.reloadTable); + $(CONTENTID.table).trigger(DATATABLE_EVENTNAME.reloadTable); } }); } diff --git a/apps/dashboard/app/javascript/packs/files/uppy_ops.js b/apps/dashboard/app/javascript/packs/files/uppy_ops.js index 66306fe51e..2970802e3c 100755 --- a/apps/dashboard/app/javascript/packs/files/uppy_ops.js +++ b/apps/dashboard/app/javascript/packs/files/uppy_ops.js @@ -2,7 +2,7 @@ import { Uppy, BasePlugin } from '@uppy/core' import Dashboard from '@uppy/dashboard' import XHRUpload from '@uppy/xhr-upload' import _ from 'lodash'; - +import {CONTENTID, EVENTNAME as DATATABLE_EVENTNAME} from './data_table.js'; let uppy = null; @@ -168,5 +168,5 @@ function getEmptyDirs(entry){ } function reloadTable() { - $("#directory-contents").trigger('reloadTable'); + $(CONTENTID.table).trigger(DATATABLE_EVENTNAME.reloadTable,{}); } \ No newline at end of file From fc3e6c6e3d406e33e3630068b1ca6de9dc3263ab Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Mon, 11 Apr 2022 16:25:28 -0500 Subject: [PATCH 34/47] Fixed page reload issue --- .../app/javascript/packs/files/data_table.js | 31 +++++++++++ .../app/javascript/packs/files/file_ops.js | 51 +++++++++++++++++-- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/data_table.js b/apps/dashboard/app/javascript/packs/files/data_table.js index 9e65c676ca..f11322f7ed 100644 --- a/apps/dashboard/app/javascript/packs/files/data_table.js +++ b/apps/dashboard/app/javascript/packs/files/data_table.js @@ -12,6 +12,7 @@ export { CONTENTID, EVENTNAME }; const EVENTNAME = { getJsonResponse: 'getJsonResponse', reloadTable: 'reloadTable', + goto: 'goto', }; const CONTENTID = { @@ -85,6 +86,10 @@ jQuery(function () { table.dataFromJsonResponse(options.response); }); + $(CONTENTID.table).on(EVENTNAME.goto, function (e, options) { + table.goto(options.path) + }); + $(document).on('click', '.rename-file', function (e) { e.preventDefault(); let rowId = e.currentTarget.dataset.rowIndex; @@ -329,6 +334,7 @@ class DataTable { const response = await fetch(request_url, { headers: { 'Accept': 'application/json' } }); const data = await this.dataFromJsonResponse(response); $('#shell-wrapper').replaceWith((data.shell_dropdown_html)); + this._table.clear(); this._table.rows.add(data.files); this._table.draw(); @@ -407,4 +413,29 @@ class DataTable { $('.datatables-status').html(`${msg} - ${rows} rows selected`); } + goto(url, pushState = true, show_processing_indicator = true) { + if(url == history.state.currentDirectoryUrl) + pushState = false; + + this.reloadTable(url) + .then((data) => { + $('#path-breadcrumbs').html(data.breadcrumbs_html); + + if(pushState) { + // Clear search query when moving to another directory. + this._table.search('').draw(); + + history.pushState({ + currentDirectory: data.path, + currentDirectoryUrl: data.url + }, data.name, data.url); + } + }) + .finally(() => { + //TODO: after processing is available via ActiveJobs merge + // if(show_processing_indicator) + // table.processing(false) + }); + } + } diff --git a/apps/dashboard/app/javascript/packs/files/file_ops.js b/apps/dashboard/app/javascript/packs/files/file_ops.js index 27d952239f..b4a819a33b 100644 --- a/apps/dashboard/app/javascript/packs/files/file_ops.js +++ b/apps/dashboard/app/javascript/packs/files/file_ops.js @@ -29,8 +29,35 @@ let fileOps = null; let reportTransferTemplate = null; jQuery(function() { - fileOps = new FileOps(); + fileOps = new FileOps(); + + $('#directory-contents tbody, #path-breadcrumbs, #favorites').on('click', 'a.d', function(event){ + if(fileOps.clickEventIsSignificant(event)){ + event.preventDefault(); + event.cancelBubble = true; + if(event.stopPropagation) event.stopPropagation(); + + const eventData = { + 'path': this.getAttribute("href"), + }; + + $(CONTENTID.table).trigger(DATATABLE_EVENTNAME.goto, eventData); + + } + }); + + $('#directory-contents tbody').on('dblclick', 'tr td:not(:first-child)', function(){ + // handle doubleclick + let a = this.parentElement.querySelector('a'); + if(a.classList.contains('d')) { + const eventData = { + 'path': a.getAttribute("href"), + }; + $(CONTENTID.table).trigger(DATATABLE_EVENTNAME.goto, eventData); + } + }); + $(CONTENTID.table).on(EVENTNAME.newFilePrompt, function () { fileOps.newFilePrompt(); }); @@ -113,8 +140,25 @@ class FileOps { constructor() { } + clickEventIsSignificant(event) { + return !( + // (event.target && (event.target as any).isContentEditable) + event.defaultPrevented + || event.which > 1 + || event.altKey + || event.ctrlKey + || event.metaKey + || event.shiftKey + ) + } + changeDirectory(path) { - this.goto(filesPath + path); + const eventData = { + 'path': filesPath + path, + }; + + $(CONTENTID.table).trigger(DATATABLE_EVENTNAME.goto, eventData); + } changeDirectoryPrompt() { @@ -495,7 +539,4 @@ class FileOps { } - goto(url) { - window.open(url,"_self"); - } } \ No newline at end of file From b8273ec25c6d528c887614ab5ff42d7be60a5de3 Mon Sep 17 00:00:00 2001 From: Gerald Byrket <88058640+gbsoftwaresolutions@users.noreply.github.com> Date: Mon, 11 Apr 2022 16:27:15 -0500 Subject: [PATCH 35/47] added newline at eof --- apps/dashboard/app/javascript/packs/files/uppy_ops.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dashboard/app/javascript/packs/files/uppy_ops.js b/apps/dashboard/app/javascript/packs/files/uppy_ops.js index 2970802e3c..3e13437583 100755 --- a/apps/dashboard/app/javascript/packs/files/uppy_ops.js +++ b/apps/dashboard/app/javascript/packs/files/uppy_ops.js @@ -169,4 +169,4 @@ function getEmptyDirs(entry){ function reloadTable() { $(CONTENTID.table).trigger(DATATABLE_EVENTNAME.reloadTable,{}); -} \ No newline at end of file +} From e772ede3a82479ee263db36712d8c2e332ed79d7 Mon Sep 17 00:00:00 2001 From: Gerald Byrket <88058640+gbsoftwaresolutions@users.noreply.github.com> Date: Mon, 11 Apr 2022 16:27:54 -0500 Subject: [PATCH 36/47] added newline at eof --- apps/dashboard/app/views/layouts/files.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dashboard/app/views/layouts/files.html.erb b/apps/dashboard/app/views/layouts/files.html.erb index e19e7c0cce..a0f6b54040 100644 --- a/apps/dashboard/app/views/layouts/files.html.erb +++ b/apps/dashboard/app/views/layouts/files.html.erb @@ -4,4 +4,4 @@ content_for :head do end %> -<%= render template: "layouts/application", locals: { layout_container_class: 'container-fluid' } %> \ No newline at end of file +<%= render template: "layouts/application", locals: { layout_container_class: 'container-fluid' } %> From 31e2c3f2ee424ea0bd7ec78fbef5a00c62ad2aad Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Tue, 12 Apr 2022 10:57:54 -0400 Subject: [PATCH 37/47] Modified render syntax to remove '.html.erb' --- apps/dashboard/app/views/files/index.json.jbuilder | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/app/views/files/index.json.jbuilder b/apps/dashboard/app/views/files/index.json.jbuilder index 4168655cc1..57b0a10478 100644 --- a/apps/dashboard/app/views/files/index.json.jbuilder +++ b/apps/dashboard/app/views/files/index.json.jbuilder @@ -19,7 +19,7 @@ json.files @files do |f| json.mode f[:mode] end -json.breadcrumbs_html render partial: 'breadcrumb.html.erb', collection: @path.descend, as: :file, locals: { file_count: @path.descend.count, full_path: @path } -json.shell_dropdown_html render partial: 'shell_dropdown.html.erb' +json.breadcrumbs_html render partial: 'breadcrumb', formats: [:html, :erb], collection: @path.descend, as: :file, locals: {file_count: @path.descend.count, full_path: @path } +json.shell_dropdown_html render partial: 'shell_dropdown', formats: [:html, :erb] json.time Time.now.to_i json.error_message alert if alert From 5bdee6d2ed91e8db64636142a6e8e52cb037749d Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Mon, 18 Apr 2022 12:53:27 -0400 Subject: [PATCH 38/47] Moved to button clicks to the appropriate java file --- .../app/javascript/packs/files/clip_board.js | 31 +++-- .../app/javascript/packs/files/data_table.js | 99 ++------------ .../app/javascript/packs/files/file_ops.js | 123 +++++++++++++----- .../app/javascript/packs/files/sweet_alert.js | 14 +- .../app/javascript/packs/files/uppy_ops.js | 2 +- 5 files changed, 133 insertions(+), 136 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/clip_board.js b/apps/dashboard/app/javascript/packs/files/clip_board.js index e5c55c10f6..2f7617f73c 100755 --- a/apps/dashboard/app/javascript/packs/files/clip_board.js +++ b/apps/dashboard/app/javascript/packs/files/clip_board.js @@ -16,30 +16,43 @@ jQuery(function () { var clipBoard = new ClipBoard(); - $(CONTENTID.table).on('success', function (e) { + $("#copy-move-btn").on("click", function () { + let table = $(CONTENTID).DataTable(); + let selection = table.rows({ selected: true }).data(); + + const eventData = { + selection: selection + }; + + $(CONTENTID).trigger(EVENTNAME.updateClipboard, eventData); + + }); + + + $(CONTENTID).on('success', function (e) { $(e.trigger).tooltip({ title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom' }).tooltip('show'); setTimeout(() => $(e.trigger).tooltip('hide'), 2000); e.clearSelection(); }); - $(CONTENTID.table).on('error', function (e) { + $(CONTENTID).on('error', function (e) { e.clearSelection(); }); - $(CONTENTID.table).on(EVENTNAME.clearClipboard, function (e, options) { + $(CONTENTID).on(EVENTNAME.clearClipboard, function (e, options) { clipBoard.clearClipboard(); clipBoard.updateViewForClipboard(); }); - $(CONTENTID.table).on(EVENTNAME.updateClipboard, function (e, options) { + $(CONTENTID).on(EVENTNAME.updateClipboard, function (e, options) { if (options.selection.length == 0) { const eventData = { 'title': 'Select a file, files, or directory to copy or move.', 'message': 'You have selected none.', }; - $(CONTENTID.table).trigger(SWAL_EVENTNAME.showError, eventData); - $(CONTENTID.table).trigger(EVENTNAME.clearClipbaord, eventData); + $(CONTENTID).trigger(SWAL_EVENTNAME.showError, eventData); + $(CONTENTID).trigger(EVENTNAME.clearClipbaord, eventData); } else { clipBoard.updateClipboardFromSelection(options.selection); @@ -47,7 +60,7 @@ jQuery(function () { } }); - $(CONTENTID.table).on(EVENTNAME.updateClipboardView, function (e, options) { + $(CONTENTID).on(EVENTNAME.updateClipboardView, function (e, options) { clipBoard.updateViewForClipboard(); }); @@ -118,7 +131,7 @@ class ClipBoard { 'token': csrf_token }; - $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.moveFile, eventData); + $(CONTENTID).trigger(FILEOPS_EVENTNAME.moveFile, eventData); } } else { @@ -157,7 +170,7 @@ class ClipBoard { 'token': csrf_token }; - $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.copyFile, eventData); + $(CONTENTID).trigger(FILEOPS_EVENTNAME.copyFile, eventData); } } else { diff --git a/apps/dashboard/app/javascript/packs/files/data_table.js b/apps/dashboard/app/javascript/packs/files/data_table.js index f11322f7ed..6836e447e6 100644 --- a/apps/dashboard/app/javascript/packs/files/data_table.js +++ b/apps/dashboard/app/javascript/packs/files/data_table.js @@ -4,7 +4,6 @@ import 'datatables.net-select'; import 'datatables.net-select-bs4'; import Handlebars from 'handlebars'; import {EVENTNAME as CLIPBOARD_EVENTNAME} from './clip_board.js'; -import {EVENTNAME as FILEOPS_EVENTNAME} from './file_ops.js'; import {EVENTNAME as SWAL_EVENTNAME} from './sweet_alert.js'; export { CONTENTID, EVENTNAME }; @@ -15,109 +14,31 @@ const EVENTNAME = { goto: 'goto', }; -const CONTENTID = { - table: '#directory-contents', -}; +const CONTENTID = '#directory-contents'; let table = null; jQuery(function () { table = new DataTable(); - /* BUTTON ACTIONS */ - $("#new-file-btn").on("click", function () { - $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.newFilePrompt); - }); - - $("#new-folder-btn").on("click", function () { - $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.newFolderPrompt); - }); - - $("#download-btn").on("click", function () { - let selection = table.getTable().rows({ selected: true }).data(); - const eventData = { - selection: selection - }; - - $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.download, eventData); - - }); - - $("#delete-btn").on("click", function () { - let files = table.getTable().rows({ selected: true }).data().toArray().map((f) => f.name); - const eventData = { - files: files - }; - - $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.deletePrompt, eventData); - - }); - - $("#copy-move-btn").on("click", function () { - let selection = table.getTable().rows({ selected: true }).data(); - const eventData = { - selection: selection - }; - - $(CONTENTID.table).trigger(CLIPBOARD_EVENTNAME.updateClipboard, eventData); - - }); - - $("#goto-btn").on("click", function () { - $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.changeDirectoryPrompt); - }); - - // TODO: - // Will have to work on this one later. Not so straight forward. - // - // $("#upload-btn").on("click", function () { - // $(CONTENTID.table).trigger('uppyShowUploadPrompt'); - // }); /* END BUTTON ACTIONS */ /* TABLE ACTIONS */ - - $(CONTENTID.table).on(EVENTNAME.reloadTable, function (e, options) { - table.reloadTable(options.url); + + $(CONTENTID).on(EVENTNAME.reloadTable, function (e, options) { + let url = options.url ? options.url : ''; + table.reloadTable(url); }); - $(CONTENTID.table).on(EVENTNAME.getJsonResponse, function (e, options) { + $(CONTENTID).on(EVENTNAME.getJsonResponse, function (e, options) { table.dataFromJsonResponse(options.response); }); - $(CONTENTID.table).on(EVENTNAME.goto, function (e, options) { + $(CONTENTID).on(EVENTNAME.goto, function (e, options) { table.goto(options.path) }); - $(document).on('click', '.rename-file', function (e) { - e.preventDefault(); - let rowId = e.currentTarget.dataset.rowIndex; - let row = table.getTable().row(rowId).data(); - let fileName = $($.parseHTML(row.name)).text(); - - const eventData = { - file: fileName, - }; - - $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.renameFilePrompt, eventData); - - }); - - $(document).on('click', '.delete-file', function (e) { - e.preventDefault(); - let rowId = e.currentTarget.dataset.rowIndex; - let row = table.getTable().row(rowId).data(); - let fileName = $($.parseHTML(row.name)).text(); - - const eventData = { - files: [fileName] - }; - - $(CONTENTID.table).trigger(FILEOPS_EVENTNAME.deletePrompt, eventData); - - }); - $('#show-dotfiles').on('change', () => { let visible = $('#show-dotfiles').is(':checked'); @@ -243,7 +164,7 @@ class DataTable { } loadDataTable() { - this._table = $(CONTENTID.table).on('xhr.dt', function (e, settings, json, xhr) { + this._table = $(CONTENTID).on('xhr.dt', function (e, settings, json, xhr) { // new ajax request for new data so update date/time // if(json && json.time){ if (json && json.time) { @@ -342,7 +263,7 @@ class DataTable { $('#open-in-terminal-btn').attr('href', data.shell_url); $('#open-in-terminal-btn').removeClass('disabled'); - $(CONTENTID.table).trigger(CLIPBOARD_EVENTNAME.updateClipboardView); + $(CONTENTID).trigger(CLIPBOARD_EVENTNAME.updateClipboardView); return await Promise.resolve(data); } catch (e) { const eventData = { @@ -350,7 +271,7 @@ class DataTable { 'message': e.message, }; - $(CONTENTID.table).trigger(SWAL_EVENTNAME.showError, eventData); + $(CONTENTID).trigger(SWAL_EVENTNAME.showError, eventData); $('#open-in-terminal-btn').addClass('disabled'); return await Promise.reject(e); diff --git a/apps/dashboard/app/javascript/packs/files/file_ops.js b/apps/dashboard/app/javascript/packs/files/file_ops.js index b4a819a33b..27ad5dbd20 100644 --- a/apps/dashboard/app/javascript/packs/files/file_ops.js +++ b/apps/dashboard/app/javascript/packs/files/file_ops.js @@ -26,8 +26,6 @@ const EVENTNAME = { let fileOps = null; -let reportTransferTemplate = null; - jQuery(function() { fileOps = new FileOps(); @@ -41,7 +39,7 @@ jQuery(function() { 'path': this.getAttribute("href"), }; - $(CONTENTID.table).trigger(DATATABLE_EVENTNAME.goto, eventData); + $(CONTENTID).trigger(DATATABLE_EVENTNAME.goto, eventData); } }); @@ -54,79 +52,144 @@ jQuery(function() { 'path': a.getAttribute("href"), }; - $(CONTENTID.table).trigger(DATATABLE_EVENTNAME.goto, eventData); + $(CONTENTID).trigger(DATATABLE_EVENTNAME.goto, eventData); } }); - $(CONTENTID.table).on(EVENTNAME.newFilePrompt, function () { + $("#new-file-btn").on("click", function () { + $(CONTENTID).trigger(EVENTNAME.newFilePrompt); + }); + + $("#new-folder-btn").on("click", function () { + $(CONTENTID).trigger(EVENTNAME.newFolderPrompt); + }); + + $("#download-btn").on("click", function () { + let table = $(CONTENTID).DataTable(); + let selection = table.rows({ selected: true }).data(); + const eventData = { + selection: selection + }; + + $(CONTENTID).trigger(EVENTNAME.download, eventData); + + }); + + $("#delete-btn").on("click", function () { + + let table = $(CONTENTID).DataTable(); + let files = table.rows({ selected: true }).data().toArray().map((f) => f.name); + const eventData = { + files: files + }; + + $(CONTENTID).trigger(EVENTNAME.deletePrompt, eventData); + + }); + + $("#goto-btn").on("click", function () { + $(CONTENTID).trigger(EVENTNAME.changeDirectoryPrompt); + }); + + $(document).on('click', '.rename-file', function (e) { + e.preventDefault(); + let table = $(CONTENTID).DataTable(); + let rowId = e.currentTarget.dataset.rowIndex; + let row = table.row(rowId).data(); + let fileName = $($.parseHTML(row.name)).text(); + + const eventData = { + file: fileName, + }; + + $(CONTENTID).trigger(EVENTNAME.renameFilePrompt, eventData); + + }); + + $(document).on('click', '.delete-file', function (e) { + e.preventDefault(); + let table = $(CONTENTID).DataTable(); + let rowId = e.currentTarget.dataset.rowIndex; + let row = table.row(rowId).data(); + let fileName = $($.parseHTML(row.name)).text(); + + const eventData = { + files: [fileName] + }; + + $(CONTENTID).trigger(EVENTNAME.deletePrompt, eventData); + + }); + + $(CONTENTID).on(EVENTNAME.newFilePrompt, function () { fileOps.newFilePrompt(); }); - $(CONTENTID.table).on(EVENTNAME.newFolderPrompt, function () { + $(CONTENTID).on(EVENTNAME.newFolderPrompt, function () { fileOps.newFolderPrompt(); }); - $(CONTENTID.table).on(EVENTNAME.renameFilePrompt, function (e, options) { + $(CONTENTID).on(EVENTNAME.renameFilePrompt, function (e, options) { fileOps.renameFilePrompt(options.file); }); - $(CONTENTID.table).on(EVENTNAME.renameFile, function (e, options) { + $(CONTENTID).on(EVENTNAME.renameFile, function (e, options) { fileOps.renameFile(options.files, options.result.value); }); - $(CONTENTID.table).on(EVENTNAME.createFile, function (e, options) { + $(CONTENTID).on(EVENTNAME.createFile, function (e, options) { fileOps.newFile(options.result.value); }); - $(CONTENTID.table).on(EVENTNAME.createFolder, function (e, options) { + $(CONTENTID).on(EVENTNAME.createFolder, function (e, options) { fileOps.newFolder(options.result.value); }); - $(CONTENTID.table).on(EVENTNAME.download, function (e, options) { + $(CONTENTID).on(EVENTNAME.download, function (e, options) { if(options.selection.length == 0) { const eventData = { 'title': 'Select a file, files, or directory to download', 'message': 'You have selected none.', }; - $(CONTENTID.table).trigger(SWAL_EVENTNAME.showError, eventData); + $(CONTENTID).trigger(SWAL_EVENTNAME.showError, eventData); } else { fileOps.download(options.selection); } }); - $(CONTENTID.table).on(EVENTNAME.deletePrompt, function (e, options) { + $(CONTENTID).on(EVENTNAME.deletePrompt, function (e, options) { if(options.files.length == 0) { const eventData = { 'title': 'Select a file, files, or directory to delete.', 'message': 'You have selected none.', }; - $(CONTENTID.table).trigger(SWAL_EVENTNAME.showError, eventData); + $(CONTENTID).trigger(SWAL_EVENTNAME.showError, eventData); } else { fileOps.deletePrompt(options.files); } }); - $(CONTENTID.table).on(EVENTNAME.deleteFile, function (e, options) { + $(CONTENTID).on(EVENTNAME.deleteFile, function (e, options) { fileOps.delete(options.files); }); - $(CONTENTID.table).on(EVENTNAME.moveFile, function (e, options) { + $(CONTENTID).on(EVENTNAME.moveFile, function (e, options) { fileOps.move(options.files, options.token); }); - $(CONTENTID.table).on(EVENTNAME.copyFile, function (e, options) { + $(CONTENTID).on(EVENTNAME.copyFile, function (e, options) { fileOps.copy(options.files, options.token); }); - $(CONTENTID.table).on(EVENTNAME.changeDirectoryPrompt, function () { + $(CONTENTID).on(EVENTNAME.changeDirectoryPrompt, function () { fileOps.changeDirectoryPrompt(); }); - $(CONTENTID.table).on(EVENTNAME.changeDirectory, function (e, options) { + $(CONTENTID).on(EVENTNAME.changeDirectory, function (e, options) { fileOps.changeDirectory(options.result.value); }); @@ -157,7 +220,7 @@ class FileOps { 'path': filesPath + path, }; - $(CONTENTID.table).trigger(DATATABLE_EVENTNAME.goto, eventData); + $(CONTENTID).trigger(DATATABLE_EVENTNAME.goto, eventData); } @@ -182,7 +245,7 @@ class FileOps { } }; - $(CONTENTID.table).trigger(SWAL_EVENTNAME.showInput, eventData); + $(CONTENTID).trigger(SWAL_EVENTNAME.showInput, eventData); } @@ -197,7 +260,7 @@ class FileOps { } }; - $(CONTENTID.table).trigger(SWAL_EVENTNAME.showInput, eventData); + $(CONTENTID).trigger(SWAL_EVENTNAME.showInput, eventData); } @@ -236,7 +299,7 @@ class FileOps { } }; - $(CONTENTID.table).trigger(SWAL_EVENTNAME.showInput, eventData); + $(CONTENTID).trigger(SWAL_EVENTNAME.showInput, eventData); } @@ -264,7 +327,7 @@ class FileOps { } }; - $(CONTENTID.table).trigger(SWAL_EVENTNAME.showInput, eventData); + $(CONTENTID).trigger(SWAL_EVENTNAME.showInput, eventData); } @@ -298,7 +361,7 @@ class FileOps { } }; - $(CONTENTID.table).trigger(SWAL_EVENTNAME.showInput, eventData); + $(CONTENTID).trigger(SWAL_EVENTNAME.showInput, eventData); } @@ -510,16 +573,16 @@ class FileOps { 'message': message, }; - $(CONTENTID.table).trigger(SWAL_EVENTNAME.showError, eventData); + $(CONTENTID).trigger(SWAL_EVENTNAME.showError, eventData); } doneLoading() { - $(CONTENTID.table).trigger(SWAL_EVENTNAME.closeSwal); + $(CONTENTID).trigger(SWAL_EVENTNAME.closeSwal); } clearClipboard() { - $(CONTENTID.table).trigger(CLIPBOARD_EVENTNAME.clearClipboard); + $(CONTENTID).trigger(CLIPBOARD_EVENTNAME.clearClipboard); } reloadTable(url) { @@ -527,7 +590,7 @@ class FileOps { 'url': url, }; - $(CONTENTID.table).trigger(DATATABLE_EVENTNAME.reloadTable, eventData); + $(CONTENTID).trigger(DATATABLE_EVENTNAME.reloadTable, eventData); } showSwalLoading (message) { @@ -535,7 +598,7 @@ class FileOps { 'message': message, }; - $(CONTENTID.table).trigger(SWAL_EVENTNAME.showLoading, eventData); + $(CONTENTID).trigger(SWAL_EVENTNAME.showLoading, eventData); } diff --git a/apps/dashboard/app/javascript/packs/files/sweet_alert.js b/apps/dashboard/app/javascript/packs/files/sweet_alert.js index 0efedd7a83..04aebb5144 100644 --- a/apps/dashboard/app/javascript/packs/files/sweet_alert.js +++ b/apps/dashboard/app/javascript/packs/files/sweet_alert.js @@ -15,23 +15,23 @@ let sweetAlert = null; jQuery(function() { sweetAlert = new SweetAlert(); - $(CONTENTID.table).on(EVENTNAME.showError, function(e,options) { + $(CONTENTID).on(EVENTNAME.showError, function(e,options) { sweetAlert.alertError(options.title, options.message); }); - $(CONTENTID.table).on(EVENTNAME.showPrompt, function(e,options) { + $(CONTENTID).on(EVENTNAME.showPrompt, function(e,options) { sweetAlert.alertError(options.title, options.message); }); - $(CONTENTID.table).on(EVENTNAME.showInput, function(e,options) { + $(CONTENTID).on(EVENTNAME.showInput, function(e,options) { sweetAlert.input(options); }); - $(CONTENTID.table).on(EVENTNAME.showLoading, function(e,options) { + $(CONTENTID).on(EVENTNAME.showLoading, function(e,options) { sweetAlert.loading(options.message); }); - $(CONTENTID.table).on(EVENTNAME.closeSwal, function() { + $(CONTENTID).on(EVENTNAME.closeSwal, function() { sweetAlert.close(); }); @@ -52,9 +52,9 @@ class SweetAlert { files: options.files ? options.files : null }; - $(CONTENTID.table).trigger(options.action, eventData); + $(CONTENTID).trigger(options.action, eventData); } else { - $(CONTENTID.table).trigger(DATATABLE_EVENTNAME.reloadTable); + $(CONTENTID).trigger(DATATABLE_EVENTNAME.reloadTable); } }); } diff --git a/apps/dashboard/app/javascript/packs/files/uppy_ops.js b/apps/dashboard/app/javascript/packs/files/uppy_ops.js index 3e13437583..87e91d55d3 100755 --- a/apps/dashboard/app/javascript/packs/files/uppy_ops.js +++ b/apps/dashboard/app/javascript/packs/files/uppy_ops.js @@ -168,5 +168,5 @@ function getEmptyDirs(entry){ } function reloadTable() { - $(CONTENTID.table).trigger(DATATABLE_EVENTNAME.reloadTable,{}); + $(CONTENTID).trigger(DATATABLE_EVENTNAME.reloadTable,{}); } From e07c3693234e6d92fa55147159947e0a0082b076 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Mon, 18 Apr 2022 14:45:28 -0400 Subject: [PATCH 39/47] Renamed new-dir-btn to new-folder-btn --- apps/dashboard/test/system/files_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dashboard/test/system/files_test.rb b/apps/dashboard/test/system/files_test.rb index 32bb3a8da5..78292a589b 100644 --- a/apps/dashboard/test/system/files_test.rb +++ b/apps/dashboard/test/system/files_test.rb @@ -36,7 +36,7 @@ class FilesTest < ApplicationSystemTestCase test "adding a new directory" do Dir.mktmpdir do |dir| visit files_url(dir) - find('#new-dir-btn').click + find('#new-folder-btn').click find('#swal2-input').set('bar') find('.swal2-confirm').click assert_selector 'tbody a.d', exact_text: 'bar', wait: 10 From 3b52501823234647b1c4fdbad25c88f80341cd08 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Mon, 18 Apr 2022 23:16:43 -0400 Subject: [PATCH 40/47] Changed folder to dir --- .../app/javascript/packs/files/file_ops.js | 32 +++++++++---------- apps/dashboard/app/views/files/index.html.erb | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/file_ops.js b/apps/dashboard/app/javascript/packs/files/file_ops.js index 27ad5dbd20..1683b6801b 100644 --- a/apps/dashboard/app/javascript/packs/files/file_ops.js +++ b/apps/dashboard/app/javascript/packs/files/file_ops.js @@ -10,15 +10,15 @@ const EVENTNAME = { changeDirectoryPrompt: 'changeDirectoryPrompt', copyFile: 'copyFile', createFile: 'createFile', - createFolder: 'createFolder', + createDirectory: 'createDirectory', deleteFile: 'deleteFile', deletePrompt: 'deletePrompt', download: 'download', moveFile: 'moveFile', newFile: 'newFile', newFilePrompt: 'newFilePrompt', - newFolderPrompt: 'newFolderPrompt', - newFolder: 'newFolder', + newDirectoryPrompt: 'newDirectoryPrompt', + newDirectory: 'newDirectory', renameFile: 'renameFile', renameFilePrompt: 'renameFilePrompt', } @@ -60,8 +60,8 @@ jQuery(function() { $(CONTENTID).trigger(EVENTNAME.newFilePrompt); }); - $("#new-folder-btn").on("click", function () { - $(CONTENTID).trigger(EVENTNAME.newFolderPrompt); + $("#new-dir-btn").on("click", function () { + $(CONTENTID).trigger(EVENTNAME.newDirectoryPrompt); }); $("#download-btn").on("click", function () { @@ -125,8 +125,8 @@ jQuery(function() { fileOps.newFilePrompt(); }); - $(CONTENTID).on(EVENTNAME.newFolderPrompt, function () { - fileOps.newFolderPrompt(); + $(CONTENTID).on(EVENTNAME.newDirectoryPrompt, function () { + fileOps.newDirectoryPrompt(); }); $(CONTENTID).on(EVENTNAME.renameFilePrompt, function (e, options) { @@ -141,8 +141,8 @@ jQuery(function() { fileOps.newFile(options.result.value); }); - $(CONTENTID).on(EVENTNAME.createFolder, function (e, options) { - fileOps.newFolder(options.result.value); + $(CONTENTID).on(EVENTNAME.createDirectory, function (e, options) { + fileOps.newDirectory(options.result.value); }); $(CONTENTID).on(EVENTNAME.download, function (e, options) { @@ -343,14 +343,14 @@ class FileOps { }); } - newFolderPrompt() { + newDirectoryPrompt() { const eventData = { - action: EVENTNAME.createFolder, + action: EVENTNAME.createDirectory, 'inputOptions': { - title: 'New Folder', + title: 'New Directory', input: 'text', - inputLabel: 'Folder name', + inputLabel: 'Directory name', showCancelButton: true, inputValidator: (value) => { if (!value || value.includes("/")) { @@ -365,7 +365,7 @@ class FileOps { } - newFolder(filename) { + newDirectory(filename) { let myFileOp = new FileOps(); fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrf_token }}) .then(response => this.dataFromJsonResponse(response)) @@ -373,7 +373,7 @@ class FileOps { myFileOp.reloadTable(); }) .catch(function (e) { - myFileOp.alertError('Error occurred when attempting to create new folder', e.message); + myFileOp.alertError('Error occurred when attempting to create new directory', e.message); }); } @@ -602,4 +602,4 @@ class FileOps { } -} \ No newline at end of file +} diff --git a/apps/dashboard/app/views/files/index.html.erb b/apps/dashboard/app/views/files/index.html.erb index 5f9cc757fc..0d74cc45e2 100644 --- a/apps/dashboard/app/views/files/index.html.erb +++ b/apps/dashboard/app/views/files/index.html.erb @@ -7,7 +7,7 @@ <% end %> - + From 60fdcbeba76919b024003953b8acf5405fd11e58 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Mon, 18 Apr 2022 23:36:13 -0400 Subject: [PATCH 41/47] updated test to reflect updated id --- apps/dashboard/app/javascript/packs/files/data_table.js | 2 +- apps/dashboard/test/system/files_test.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/data_table.js b/apps/dashboard/app/javascript/packs/files/data_table.js index 6836e447e6..b214b86dc9 100644 --- a/apps/dashboard/app/javascript/packs/files/data_table.js +++ b/apps/dashboard/app/javascript/packs/files/data_table.js @@ -27,7 +27,7 @@ jQuery(function () { /* TABLE ACTIONS */ $(CONTENTID).on(EVENTNAME.reloadTable, function (e, options) { - let url = options.url ? options.url : ''; + let url = $.isEmptyObject(options) ? '' : options.url; table.reloadTable(url); }); diff --git a/apps/dashboard/test/system/files_test.rb b/apps/dashboard/test/system/files_test.rb index 78292a589b..32bb3a8da5 100644 --- a/apps/dashboard/test/system/files_test.rb +++ b/apps/dashboard/test/system/files_test.rb @@ -36,7 +36,7 @@ class FilesTest < ApplicationSystemTestCase test "adding a new directory" do Dir.mktmpdir do |dir| visit files_url(dir) - find('#new-folder-btn').click + find('#new-dir-btn').click find('#swal2-input').set('bar') find('.swal2-confirm').click assert_selector 'tbody a.d', exact_text: 'bar', wait: 10 From 16c607593d0cda31068de363b1df588168b5e50a Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Tue, 19 Apr 2022 11:03:04 -0400 Subject: [PATCH 42/47] Removed clipboard dependency from datatable --- apps/dashboard/app/javascript/packs/files/clip_board.js | 2 ++ apps/dashboard/app/javascript/packs/files/data_table.js | 2 -- apps/dashboard/app/javascript/packs/files/file_ops.js | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/clip_board.js b/apps/dashboard/app/javascript/packs/files/clip_board.js index 2f7617f73c..1c807605f0 100755 --- a/apps/dashboard/app/javascript/packs/files/clip_board.js +++ b/apps/dashboard/app/javascript/packs/files/clip_board.js @@ -100,6 +100,8 @@ class ClipBoard { updateViewForClipboard() { + console.log("updateViewForClipboard"); + let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || '{}'), template_str = $('#clipboard-template').html(), template = Handlebars.compile(template_str); diff --git a/apps/dashboard/app/javascript/packs/files/data_table.js b/apps/dashboard/app/javascript/packs/files/data_table.js index b214b86dc9..d132e738f4 100644 --- a/apps/dashboard/app/javascript/packs/files/data_table.js +++ b/apps/dashboard/app/javascript/packs/files/data_table.js @@ -3,7 +3,6 @@ import 'datatables.net-bs4/js/dataTables.bootstrap4'; import 'datatables.net-select'; import 'datatables.net-select-bs4'; import Handlebars from 'handlebars'; -import {EVENTNAME as CLIPBOARD_EVENTNAME} from './clip_board.js'; import {EVENTNAME as SWAL_EVENTNAME} from './sweet_alert.js'; export { CONTENTID, EVENTNAME }; @@ -263,7 +262,6 @@ class DataTable { $('#open-in-terminal-btn').attr('href', data.shell_url); $('#open-in-terminal-btn').removeClass('disabled'); - $(CONTENTID).trigger(CLIPBOARD_EVENTNAME.updateClipboardView); return await Promise.resolve(data); } catch (e) { const eventData = { diff --git a/apps/dashboard/app/javascript/packs/files/file_ops.js b/apps/dashboard/app/javascript/packs/files/file_ops.js index 1683b6801b..2c93bb1a17 100644 --- a/apps/dashboard/app/javascript/packs/files/file_ops.js +++ b/apps/dashboard/app/javascript/packs/files/file_ops.js @@ -488,6 +488,7 @@ class FileOps { if(action == 'mv' || action == 'cp') { this.reloadTable(); this.clearClipboard(); + this.updateClipboard(); } this.fadeOutTransferStatus(data); @@ -602,4 +603,8 @@ class FileOps { } + updateClipboard() { + $(CONTENTID).trigger(CLIPBOARD_EVENTNAME.updateClipboardView); + } + } From 06089c54e5ac9cff74fe4a7dc4f84a1fc8bce180 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Tue, 19 Apr 2022 11:20:54 -0400 Subject: [PATCH 43/47] removed local references in template --- apps/dashboard/app/views/files/index.html.erb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/dashboard/app/views/files/index.html.erb b/apps/dashboard/app/views/files/index.html.erb index 0d74cc45e2..c7c7974a09 100644 --- a/apps/dashboard/app/views/files/index.html.erb +++ b/apps/dashboard/app/views/files/index.html.erb @@ -18,11 +18,11 @@
- <%= render partial: "favorites", :locals => { :files => @files } %> - <%= render partial: "copy_move_popup", :locals => { :files => @files } %> - <%= render partial: "file_action_menu", :locals => { :files => @files } %> - <%= render partial: "default_label_error_messages", :locals => { :files => @files } %> + <%= render partial: "favorites" %> + <%= render partial: "copy_move_popup" %> + <%= render partial: "file_action_menu" %> + <%= render partial: "default_label_error_messages" %>
- <%= render partial: "files_table", :locals => { :files => @files } %> + <%= render partial: "files_table" %>
-<%= render partial: "inline_js", :locals => { :files => @files } %> +<%= render partial: "inline_js" %> From 7a6c13ff7a15456f2e719cc3ea8691dfc4a71af3 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Tue, 19 Apr 2022 11:28:51 -0400 Subject: [PATCH 44/47] Fixed 'change directory' bug in listener --- apps/dashboard/app/javascript/packs/files/clip_board.js | 2 -- apps/dashboard/app/javascript/packs/files/file_ops.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/clip_board.js b/apps/dashboard/app/javascript/packs/files/clip_board.js index 1c807605f0..2f7617f73c 100755 --- a/apps/dashboard/app/javascript/packs/files/clip_board.js +++ b/apps/dashboard/app/javascript/packs/files/clip_board.js @@ -100,8 +100,6 @@ class ClipBoard { updateViewForClipboard() { - console.log("updateViewForClipboard"); - let clipboard = JSON.parse(localStorage.getItem('filesClipboard') || '{}'), template_str = $('#clipboard-template').html(), template = Handlebars.compile(template_str); diff --git a/apps/dashboard/app/javascript/packs/files/file_ops.js b/apps/dashboard/app/javascript/packs/files/file_ops.js index 2c93bb1a17..05dd1ea584 100644 --- a/apps/dashboard/app/javascript/packs/files/file_ops.js +++ b/apps/dashboard/app/javascript/packs/files/file_ops.js @@ -87,7 +87,7 @@ jQuery(function() { }); - $("#goto-btn").on("click", function () { + $(document).on("click", '#goto-btn', function () { $(CONTENTID).trigger(EVENTNAME.changeDirectoryPrompt); }); From bbea4a0802eed47cce6b9f40840577a389f82121 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Tue, 19 Apr 2022 14:23:55 -0400 Subject: [PATCH 45/47] Cleaned up code causing JS Errors --- .../app/javascript/packs/files/data_table.js | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/apps/dashboard/app/javascript/packs/files/data_table.js b/apps/dashboard/app/javascript/packs/files/data_table.js index d132e738f4..d4c8c71ebd 100644 --- a/apps/dashboard/app/javascript/packs/files/data_table.js +++ b/apps/dashboard/app/javascript/packs/files/data_table.js @@ -272,7 +272,9 @@ class DataTable { $(CONTENTID).trigger(SWAL_EVENTNAME.showError, eventData); $('#open-in-terminal-btn').addClass('disabled'); - return await Promise.reject(e); + + // Removed this as it was causing a JS Error and there is no reprocution from removing it. + // return await Promise.reject(e); } } @@ -338,16 +340,17 @@ class DataTable { this.reloadTable(url) .then((data) => { - $('#path-breadcrumbs').html(data.breadcrumbs_html); - - if(pushState) { - // Clear search query when moving to another directory. - this._table.search('').draw(); - - history.pushState({ - currentDirectory: data.path, - currentDirectoryUrl: data.url - }, data.name, data.url); + if(data) { + $('#path-breadcrumbs').html(data.breadcrumbs_html); + if(pushState) { + // Clear search query when moving to another directory. + this._table.search('').draw(); + + history.pushState({ + currentDirectory: data.path, + currentDirectoryUrl: data.url + }, data.name, data.url); + } } }) .finally(() => { From 3aca1158366d27eb4fcfaad83fc501d334b96f38 Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Wed, 20 Apr 2022 16:22:58 -0500 Subject: [PATCH 46/47] Added call to update clipboard view on clipboard instantiation --- apps/dashboard/app/javascript/packs/files/clip_board.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/dashboard/app/javascript/packs/files/clip_board.js b/apps/dashboard/app/javascript/packs/files/clip_board.js index 2f7617f73c..3632ac0cf8 100755 --- a/apps/dashboard/app/javascript/packs/files/clip_board.js +++ b/apps/dashboard/app/javascript/packs/files/clip_board.js @@ -72,6 +72,7 @@ class ClipBoard { constructor() { this._clipBoard = new ClipboardJS('#copy-path'); + this.updateViewForClipboard(); } getClipBoard() { From 7ed0141ccc64cdf94047d0401f9ac9472b50e62d Mon Sep 17 00:00:00 2001 From: Gerald Byrket Date: Wed, 20 Apr 2022 16:31:36 -0500 Subject: [PATCH 47/47] Fixed testing --- apps/dashboard/test/system/files_test.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/dashboard/test/system/files_test.rb b/apps/dashboard/test/system/files_test.rb index 32bb3a8da5..b55829ec9c 100644 --- a/apps/dashboard/test/system/files_test.rb +++ b/apps/dashboard/test/system/files_test.rb @@ -63,7 +63,8 @@ class FilesTest < ApplicationSystemTestCase find('#clipboard-copy-to-dir').click # if this fails it is due to the directory table not reloading - assert_selector '#directory-contents tbody tr', count: 3, wait: 10 + # assert_selector '#directory-contents tbody tr', count: 3, wait: 10 + assert_selector 'tbody a', exact_text: 'app', wait: 10 assert_equal "", `diff -rq #{File.join(dir, 'app')} #{Rails.root.join('app').to_s}`.strip, "failed to recursively copy app dir" assert_equal "", `diff -rq #{File.join(dir, 'config')} #{Rails.root.join('config').to_s}`.strip, "failed to recursively copy config dir"