diff --git a/grunt_config.json b/grunt_config.json index 260c6a11..9c0ccd44 100644 --- a/grunt_config.json +++ b/grunt_config.json @@ -24,6 +24,10 @@ "src": "<%= dirs.css.src %>/style-polymer.css", "dest": "<%= dirs.js.dest %>/src/components/style-polymer.css" }, + { + "src": "<%= dirs.js.node_modules %>/filepond/dist/filepond.css", + "dest": "<%= dirs.css.dest %>/filepond.css" + }, { "src": "<%= dirs.js.src %>/select2/select2.css", "dest": "<%= dirs.css.dest %>/select2.css" @@ -39,12 +43,12 @@ "<%= dirs.js.node_modules %>/luxon/build/global/luxon.min.js", "<%= dirs.js.node_modules %>/clipboard/dist/clipboard.min.js", "<%= dirs.js.node_modules %>/favico.js/favico-0.3.10.min.js", - "<%= dirs.js.node_modules %>/dropzone/dist/min/dropzone.min.js", "<%= dirs.js.node_modules %>/sweetalert2/dist/sweetalert2.min.js", "<%= dirs.js.node_modules %>/sticky-sidebar/dist/sticky-sidebar.min.js", "<%= dirs.js.node_modules %>/sticky-sidebar/dist/jquery.sticky-sidebar.min.js", "<%= dirs.js.node_modules %>/waypoints/lib/noframework.waypoints.min.js", "<%= dirs.js.node_modules %>/waypoints/lib/jquery.waypoints.min.js", + "<%= dirs.js.node_modules %>/filepond/dist/filepond.min.js", "<%= dirs.js.src %>/logging.js", "<%= dirs.js.src %>/bootstrap.js", "<%= dirs.js.src %>/i18n_utils.js", diff --git a/package-lock.json b/package-lock.json index 9838d53e..9f79e30b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,9 +26,9 @@ "clipboard": "^2.0.1", "copy-webpack-plugin": "^4.4.2", "css-loader": "^0.28.11", - "dropzone": "^5.5.0", "exports-loader": "^0.6.4", "favico.js": "^0.3.10", + "filepond": "^4.32.10", "grunt": "^0.4.5", "grunt-cli": "^1.4.3", "grunt-contrib-concat": "^0.5.1", @@ -51,7 +51,7 @@ "raw-loader": "1.0.0-beta.0", "sticky-sidebar": "3.3.1", "style-loader": "^0.21.0", - "sweetalert2": "^9.10.12", + "sweetalert2": "^11.26.3", "ts-loader": "^1.3.3", "waypoints": "4.0.1", "webpack": "4.23.1", @@ -3284,12 +3284,6 @@ "domelementtype": "1" } }, - "node_modules/dropzone": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.9.3.tgz", - "integrity": "sha512-Azk8kD/2/nJIuVPK+zQ9sjKMRIpRvNyqn9XwbBHNq+iNuSccbJS6hwm1Woy0pMST0erSo0u4j+KJaodndDk4vA==", - "dev": true - }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -3916,6 +3910,12 @@ "dev": true, "optional": true }, + "node_modules/filepond": { + "version": "4.32.10", + "resolved": "https://registry.npmjs.org/filepond/-/filepond-4.32.10.tgz", + "integrity": "sha512-uflpIL+rfAgsYk7HgXugh7ELlHNHLTPtIHfvuq0gv3x2qNfVxdGA6T7RPxMh9Vji8ysXstXkDHVXQ4hY38MPTQ==", + "dev": true + }, "node_modules/fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -10334,12 +10334,13 @@ } }, "node_modules/sweetalert2": { - "version": "9.17.4", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-9.17.4.tgz", - "integrity": "sha512-T6Js/kjtGmpekE1tPT+drdgsedEnaet03mMadKUMvtBlFzVLcYy8kvIuchaQJ09rhG5bdmRRMQ2sEK0ZnHi1vg==", + "version": "11.26.3", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.26.3.tgz", + "integrity": "sha512-VU0hGw/WfI9h7Mh+SCsDlWgtxDwWZ6ccqS7QcO8zEeWnwplN1GptcLstq76OluUBSLUza6ldvKd3558OhjpJ9A==", "dev": true, "funding": { - "url": "https://sweetalert2.github.io/#donations" + "type": "individual", + "url": "https://github.com/sponsors/limonte" } }, "node_modules/tapable": { @@ -14789,12 +14790,6 @@ "domelementtype": "1" } }, - "dropzone": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.9.3.tgz", - "integrity": "sha512-Azk8kD/2/nJIuVPK+zQ9sjKMRIpRvNyqn9XwbBHNq+iNuSccbJS6hwm1Woy0pMST0erSo0u4j+KJaodndDk4vA==", - "dev": true - }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -15324,6 +15319,12 @@ "dev": true, "optional": true }, + "filepond": { + "version": "4.32.10", + "resolved": "https://registry.npmjs.org/filepond/-/filepond-4.32.10.tgz", + "integrity": "sha512-uflpIL+rfAgsYk7HgXugh7ELlHNHLTPtIHfvuq0gv3x2qNfVxdGA6T7RPxMh9Vji8ysXstXkDHVXQ4hY38MPTQ==", + "dev": true + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -20525,9 +20526,9 @@ } }, "sweetalert2": { - "version": "9.17.4", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-9.17.4.tgz", - "integrity": "sha512-T6Js/kjtGmpekE1tPT+drdgsedEnaet03mMadKUMvtBlFzVLcYy8kvIuchaQJ09rhG5bdmRRMQ2sEK0ZnHi1vg==", + "version": "11.26.3", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.26.3.tgz", + "integrity": "sha512-VU0hGw/WfI9h7Mh+SCsDlWgtxDwWZ6ccqS7QcO8zEeWnwplN1GptcLstq76OluUBSLUza6ldvKd3558OhjpJ9A==", "dev": true }, "tapable": { diff --git a/package.json b/package.json index 85240e78..7904667d 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,9 @@ "clipboard": "^2.0.1", "copy-webpack-plugin": "^4.4.2", "css-loader": "^0.28.11", - "dropzone": "^5.5.0", "exports-loader": "^0.6.4", "favico.js": "^0.3.10", + "filepond": "^4.32.10", "grunt": "^0.4.5", "grunt-cli": "^1.4.3", "grunt-contrib-concat": "^0.5.1", @@ -51,7 +51,7 @@ "raw-loader": "1.0.0-beta.0", "sticky-sidebar": "3.3.1", "style-loader": "^0.21.0", - "sweetalert2": "^9.10.12", + "sweetalert2": "^11.26.3", "ts-loader": "^1.3.3", "waypoints": "4.0.1", "webpack": "4.23.1", diff --git a/rhodecode/apps/repository/__init__.py b/rhodecode/apps/repository/__init__.py index 82538c6d..5bb15c71 100644 --- a/rhodecode/apps/repository/__init__.py +++ b/rhodecode/apps/repository/__init__.py @@ -203,7 +203,6 @@ def includeme(config): route_name="repo_commit_comment_attachment_upload", request_method="POST", renderer="json_ext", - xhr=True, ) config.add_route( diff --git a/rhodecode/apps/repository/views/repo_commits.py b/rhodecode/apps/repository/views/repo_commits.py index 23cefd7f..d6b5e087 100644 --- a/rhodecode/apps/repository/views/repo_commits.py +++ b/rhodecode/apps/repository/views/repo_commits.py @@ -588,7 +588,7 @@ class RepoCommitsView(RepoAppView): ".xlsx", ".zip", ] - max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js + max_file_size = 10 * 1024 * 1024 # 10MB try: f_store = store_utils.get_filestore_backend(self.request.registry.settings) diff --git a/rhodecode/apps/repository/views/repo_files.py b/rhodecode/apps/repository/views/repo_files.py index 8505fabb..76178e35 100644 --- a/rhodecode/apps/repository/views/repo_files.py +++ b/rhodecode/apps/repository/views/repo_files.py @@ -1453,6 +1453,10 @@ class RepoFilesView(RepoAppView): nodes = {} for file_obj in r_post.getall("files_upload") or []: + if file_obj == "{}": + # filepond sends the field with the same name as the file field with metadata which in our case is empty + continue + content = file_obj.file filename = file_obj.filename diff --git a/rhodecode/public/css/filepond.less b/rhodecode/public/css/filepond.less new file mode 100644 index 00000000..104f6e9b --- /dev/null +++ b/rhodecode/public/css/filepond.less @@ -0,0 +1,23 @@ +.filepond--root .filepond--file-info, +.filepond--root .filepond--file-info-main, +.filepond--root .filepond--file-info-sub { + pointer-events: auto !important; +} + +/* Make the “sub” line look like the main line (brighter) */ +.filepond--root .filepond--file-info .filepond--file-info-sub { + opacity: 1 !important; /* remove faded look */ + color: #ffffff; /* or whatever contrasts your green */ + font-size: 0.75em; /* same as main, if you like */ +} + +/* Style the link itself */ +.filepond--root .filepond--file-info .filepond--file-info-sub a { + color: #ffffff; + text-decoration: underline; + cursor: pointer; +} + +.filepond--root .filepond--drop-label .filepond--label-action { + font-family: @text-regular; /* or a different font */ +} diff --git a/rhodecode/public/css/main.less b/rhodecode/public/css/main.less index fab77fea..1dcbead1 100644 --- a/rhodecode/public/css/main.less +++ b/rhodecode/public/css/main.less @@ -28,6 +28,7 @@ @import 'deform'; @import 'tooltips'; @import 'sweetalert2'; +@import 'filepond'; //--- BASE ------------------// @@ -2957,27 +2958,11 @@ form.markup-form { } -.dropzone-wrapper { +.filepond-wrapper { border: 1px solid @grey5; padding: 20px; } -.dropzone, -.dropzone-pure { - border: 2px dashed @grey5; - border-radius: 5px; - background: white; - min-height: 200px; - padding: 54px; - - .dz-message { - font-weight: 700; - text-align: center; - margin: 2em 0; - } - -} - .dz-preview { margin: 10px 0 !important; position: relative; diff --git a/rhodecode/public/js/src/plugins/jquery.timeago-extension.js b/rhodecode/public/js/src/plugins/jquery.timeago-extension.js index ab314d66..ecf7227b 100644 --- a/rhodecode/public/js/src/plugins/jquery.timeago-extension.js +++ b/rhodecode/public/js/src/plugins/jquery.timeago-extension.js @@ -36,7 +36,7 @@ var AgeModule = (function () { day: days, hour: hours, minute: minutes, - second: seconds, + second: Math.round(seconds), }; }; diff --git a/rhodecode/public/js/src/rhodecode/comments.js b/rhodecode/public/js/src/rhodecode/comments.js index 5150d3b5..fd256e2d 100644 --- a/rhodecode/public/js/src/rhodecode/comments.js +++ b/rhodecode/public/js/src/rhodecode/comments.js @@ -928,7 +928,6 @@ var CommentsController = function() { }, 100); } - // add dropzone support var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) { var renderer = templateContext.visual.default_renderer; if (renderer == 'rst') { @@ -949,74 +948,115 @@ var CommentsController = function() { return false; }; - //see: https://www.dropzonejs.com/#configuration - var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload', + let storeUrl = pyroutes.url('repo_commit_comment_attachment_upload', {'repo_name': templateContext.repo_name, 'commit_id': templateContext.commit_data.commit_id}) - var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0); + let previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0); if (previewTmpl !== undefined){ - var selectLink = $(formElement).find('.pick-attachment').get(0); - $(formElement).find('.comment-attachment-uploader').dropzone({ - url: storeUrl, - headers: {"X-CSRF-Token": CSRF_TOKEN}, - paramName: function () { - return "attachment" - }, // The name that will be used to transfer the file - clickable: selectLink, - parallelUploads: 1, - maxFiles: 10, - maxFilesize: templateContext.attachment_store.max_file_size_mb, - uploadMultiple: false, - autoProcessQueue: true, // if false queue will not be processed automatically. - createImageThumbnails: false, - previewTemplate: previewTmpl.innerHTML, + let $uploader = $(formElement).find('.comment-attachment-uploader'); + let inputElement = $uploader.find('.comment-attachment-input')[0]; + let selectLink = $(formElement).find('.pick-attachment').get(0); - accept: function (file, done) { - done(); + let pond = FilePond.create(inputElement, { + name: 'attachment', + credits: false, + allowMultiple: true, + allowRevert: false, + maxFiles: 10, + maxParallelUploads: 1, + maxFileSize: templateContext.attachment_store.max_file_size_mb + 'MB', + + // custom upload so we can send CSRF header etc + server: { + process: function (fieldName, file, metadata, load, error, progress, abort) { + let xhr = new XMLHttpRequest(); + xhr.open('POST', storeUrl); + + xhr.setRequestHeader('X-CSRF-Token', CSRF_TOKEN); + + let formData = new FormData(); + formData.append('attachment', file, file.name); + + xhr.upload.onprogress = function (e) { + if (e.lengthComputable) { + progress(e.loaded, e.loaded, e.total); + } + }; + + xhr.onreadystatechange = function () { + if (xhr.readyState !== XMLHttpRequest.DONE) { + return; + } + + pond.removeFile(file.id); //removes status + + if (xhr.status >= 200 && xhr.status < 300) { + load(xhr.responseText); + } else { + const parsed_resp = JSON.parse(xhr.responseText || '{}'); + if (parsed_resp.error === '') return; + + const message = (parsed_resp.error || parsed_resp.main || parsed_resp.message || "Unknown error"); + + $.Topic('/notifications').publish({ + message: { + message: message, + level: "error", + force: true + } + }); + } + }; + + xhr.send(formData); + + return { + abort: function () { + xhr.abort(); + abort(); + } + }; }, - init: function () { + }, - this.on("sending", function (file, xhr, formData) { - $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide(); - $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show(); - }); + // get rid of "tap to undo" etc + labelTapToUndo: '', + labelTapToRetry: '', + labelTapToCancel: '' + }); - this.on("success", function (file, response) { - $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show(); - $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide(); - - var isRendered = false; - var ext = file.name.split('.').pop(); - var imageExts = templateContext.attachment_store.image_ext; - if (imageExts.indexOf(ext) !== -1){ - isRendered = true; - } - - insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered) - }); - - this.on("error", function (file, errorMessage, xhr) { - $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide(); - - var error = null; - - if (xhr !== undefined){ - var httpStatus = xhr.status + " " + xhr.statusText; - if (xhr !== undefined && xhr.status >= 500) { - error = httpStatus; - } - } - - if (error === null) { - error = errorMessage.error || errorMessage || httpStatus; - } - $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error)); - - }); - } + if (selectLink) { + $(selectLink).off('click').on('click', function (e) { + e.preventDefault(); + pond.browse(); }); } + + pond.on('processfile', function (errorObj, file) { + if (errorObj) return; + + // success: parse server response + let response; + try { + response = JSON.parse(file.serverId || '{}'); + } catch (e) { + response = null; + } + if (!response) { + return; + } + + let filename = file.filename || (file.file && file.file.name) || file.name || ''; + let ext = filename.split('.').pop(); + let imageExts = templateContext.attachment_store.image_ext || []; + let isRendered = imageExts.indexOf(ext) !== -1; + + pond.removeFile(file.id); + + insertAttachmentText(cm, filename, response.repo_fqn_access_path, isRendered); + }); + } return commentForm; }; @@ -1318,6 +1358,7 @@ var CommentsController = function() { }; this.createComment = function(node, f_path, line_no, resolutionComment) { + self.edit = false; var $node = $(node); var $td = $node.closest('td'); diff --git a/rhodecode/templates/base/root.mako b/rhodecode/templates/base/root.mako index 3f6fbd7c..5ef599fc 100644 --- a/rhodecode/templates/base/root.mako +++ b/rhodecode/templates/base/root.mako @@ -63,8 +63,9 @@ c.template_context['attachment_store'] = { ## CSS definitions <%def name="css()"> - + + ## EXTRA FOR CSS ${self.css_extra()} diff --git a/rhodecode/templates/changeset/changeset_file_comment.mako b/rhodecode/templates/changeset/changeset_file_comment.mako index df9cadfd..d4b84ea1 100644 --- a/rhodecode/templates/changeset/changeset_file_comment.mako +++ b/rhodecode/templates/changeset/changeset_file_comment.mako @@ -468,21 +468,14 @@ diff --git a/rhodecode/templates/codeblocks/diffs.mako b/rhodecode/templates/codeblocks/diffs.mako index 4d7e5c01..1e3ecd1c 100644 --- a/rhodecode/templates/codeblocks/diffs.mako +++ b/rhodecode/templates/codeblocks/diffs.mako @@ -1279,7 +1279,7 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number): }; var animateText = function (fPath, anchorId) { - fPath = Select2.util.escapeMarkup(fPath); + fPath = jQuery.fn.select2.defaults.defaults.escapeMarkup(fPath); $('.fpath-placeholder-text').html(contextPrefix + '' + fPath + '') }; diff --git a/rhodecode/templates/commits/changelog.mako b/rhodecode/templates/commits/changelog.mako index eabb1121..68223d18 100644 --- a/rhodecode/templates/commits/changelog.mako +++ b/rhodecode/templates/commits/changelog.mako @@ -282,57 +282,104 @@ window.location = pyroutes.url('repo_commits', filter); }); - $("#branch_filter").select2({ - 'dropdownAutoWidth': true, - 'width': 'resolve', - 'placeholder': "${c.selected_name or _('Branch filter')}", - containerCssClass: "drop-menu", - dropdownCssClass: "drop-menu-dropdown", - query: function(query){ - var key = 'cache'; - var cached = cache[key] ; - if(cached) { - var data = {results: []}; - //filter results - $.each(cached.results, function(){ - var section = this.text; - var children = []; - $.each(this.children, function(){ - if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){ - children.push({'id': this.id, 'text': this.text, 'type': this.type}) - } - }); - data.results.push({'text': section, 'children': children}); - query.callback({results: data.results}); + // make sure we have a cache object + window.cache = window.cache || {}; + var branchCacheKey = 'cache'; + + function buildBranchFilterData(term, cached) { + var data = { results: [] }; + term = term || ''; + + $.each(cached.results, function () { + var section = this.text; + var children = []; + + $.each(this.children, function () { + if (!term.length || this.text.toUpperCase().indexOf(term.toUpperCase()) >= 0) { + children.push({ + id: this.id, + text: this.text, + type: this.type + }); + } }); - }else{ - $.ajax({ + + data.results.push({ + text: section, + children: children + }); + }); + + return data; + } + + function ensureBranchFilterData(term, callback) { + var cached = cache[branchCacheKey]; + + if (cached) { + callback(buildBranchFilterData(term, cached)); + } else { + $.ajax({ url: pyroutes.url('repo_refs_changelog_data', {'repo_name': '${c.repo_name}'}), data: {}, dataType: 'json', type: 'GET', - success: function(data) { - cache[key] = data; - query.callback({results: data.results}); + success: function (data) { + cache[branchCacheKey] = data; + callback(buildBranchFilterData(term, data)); } - }) - } + }); } - }); - $('#branch_filter').on('change', function(e){ - var data = $('#branch_filter').select2('data'); - //type: branch_closed - var selected = data.text; - var filter = {'repo_name': '${c.repo_name}'}; - if(data.type == 'branch' || data.type == 'branch_closed'){ - filter["branch"] = selected; - if (data.type == 'branch_closed') { - filter["evolve"] = '1'; + } + + var $branchFilter = $("#branch_filter"); + + var preselectedId = $branchFilter.val(); + var preselectedText = "${c.selected_name or ''}"; + + if (preselectedId && preselectedText) { + var option = new Option(preselectedText, preselectedId, true, true); + $branchFilter.append(option).trigger('change'); + } + + $branchFilter.select2({ + dropdownAutoWidth: true, + width: "100%", + placeholder: "${c.selected_name or _('Branch filter')}", + containerCssClass: "drop-menu", + dropdownCssClass: "drop-menu-dropdown", + + ajax: { + transport: function (params, success, failure) { + var term = (params.data && params.data.term) ? params.data.term : ''; + ensureBranchFilterData(term, function (data) { + success(data); // already {results: [...]} + }); + }, + processResults: function (data/*, params*/) { + return data; // passthrough } } - else if (data.type == 'book'){ + }); + + $branchFilter.on('select2:select', function (e) { + var data = e.params && e.params.data; + if (!data) { + return; + } + + var selected = data.text; + var filter = {'repo_name': '${c.repo_name}'}; + + if (data.type === 'branch' || data.type === 'branch_closed') { + filter["branch"] = selected; + if (data.type === 'branch_closed') { + filter["evolve"] = '1'; + } + } else if (data.type === 'book') { filter["bookmark"] = selected; } + window.location = pyroutes.url('repo_commits', filter); }); diff --git a/rhodecode/templates/files/files.mako b/rhodecode/templates/files/files.mako index bf6829f7..5593e90b 100644 --- a/rhodecode/templates/files/files.mako +++ b/rhodecode/templates/files/files.mako @@ -44,6 +44,24 @@ + +