Compare commits

...

13 Commits

Author SHA1 Message Date
Johannes Zellner
1de93ab10e Poor man's quoting fix for auth proxy in french 2021-03-23 21:01:52 +01:00
Johannes Zellner
7e8381f2fa filemanager: Fix deep copying 2021-03-23 20:53:00 +01:00
Johannes Zellner
e70ab4a4f2 Avoid rendering native select widget 2021-03-23 20:52:44 +01:00
Johannes Zellner
922c46e9be Fix radio button alignment like checkboxes 2021-03-23 20:52:42 +01:00
Johannes Zellner
9db128b5e2 filemanager: Fix state issue with editor 2021-03-23 20:51:37 +01:00
Johannes Zellner
46588c7cca filemanager: find a new unique name when file copy clashes 2021-03-23 20:50:49 +01:00
Johannes Zellner
59b37af931 filemanager: Skip keyboard actions if modal dialogs are open 2021-03-23 20:50:35 +01:00
Johannes Zellner
4221ac9595 filemanager: Select entry on context menu button clicked 2021-03-23 20:50:24 +01:00
Johannes Zellner
500da2df25 Some cleanups for showing popular and section names depending on category and search
(cherry picked from commit 5d5c712f1c)
2021-03-22 14:35:43 -07:00
Johannes Zellner
611462ffd7 Show popular apps first when no category is selected
(cherry picked from commit 050ea48e3e)
2021-03-22 14:35:36 -07:00
Girish Ramakrishnan
d689066298 Fix display of user management/dashboard visiblity for email apps
(cherry picked from commit 613ac16601)
2021-03-22 14:35:15 -07:00
Girish Ramakrishnan
68b8d936e7 Do not show user management string for sogo
(cherry picked from commit 84cf5809a0)
2021-03-22 14:35:08 -07:00
Girish Ramakrishnan
e8b985a266 renewCerts: do not pass domain
(cherry picked from commit 168636e493)
2021-03-22 14:34:52 -07:00
9 changed files with 106 additions and 28 deletions

View File

@@ -272,7 +272,7 @@
<p class="text-bold text-danger">{{ 'filemanager.textEditorCloseDialog.details' | tr }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" ng-click="textEditor.close()">{{ 'filemanager.textEditorCloseDialog.dontSave' | tr }}</button>
<button type="button" class="btn btn-default" ng-click="textEditor.onClose()">{{ 'filemanager.textEditorCloseDialog.dontSave' | tr }}</button>
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
<button type="button" class="btn btn-success" ng-click="textEditor.saveAndClose()"><i class="fa fa-circle-notch fa-spin" ng-show="textEditor.busy"></i> {{ 'main.dialog.save' | tr }}</button>
</div>
@@ -362,7 +362,7 @@
<td style="width: 80px" class="elide-table-cell" ng-mousedown="select($event, entry)" ng-dblclick="open(entry)">{{ entry.size | prettyByteSize }}</td>
<td style="width:100px" class="elide-table-cell" ng-mousedown="select($event, entry)" ng-dblclick="open(entry)">{{ entry.uid | prettyOwner }}</td>
<td style="width: 45px; padding: 7px;">
<button type="button" class="btn btn-xs btn-default context-menu-action" context-menu="menuOptions" model="entry" context-menu-on="click"><i class="fas fa-ellipsis-h"></i></button>
<button type="button" class="btn btn-xs btn-default context-menu-action" context-menu="menuOptions" model="entry" context-menu-on="click" ng-click="onEntryContextMenu($event, entry)"><i class="fas fa-ellipsis-h"></i></button>
</td>
</tr>
</tbody>

View File

@@ -2180,8 +2180,8 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
});
};
Client.prototype.renewCerts = function (domain, callback) {
post('/api/v1/cloudron/renew_certs', { domain: domain || null }, null, function (error, data, status) {
Client.prototype.renewCerts = function (callback) {
post('/api/v1/cloudron/renew_certs', {}, null, function (error, data, status) {
if (error) return callback(error);
if (status !== 202) return callback(new ClientError(status, data));
@@ -2830,9 +2830,12 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
};
Client.prototype.filesCopy = function (id, type, path, newPath, callback) {
var that = this;
var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id;
put('/api/v1/' + objpath + '/files/' + path, { action: 'copy', newFilePath: decodeURIComponent(newPath) }, {}, function (error, data, status) {
if (error && error.statusCode === 409) return that.filesCopy(id, type, path, newPath + '-copy', callback);
if (error) return callback(error);
if (status !== 200) return callback(new ClientError(status, data));

View File

@@ -423,6 +423,11 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
}
};
$scope.onEntryContextMenu = function ($event, entry) {
if ($scope.selected.indexOf(entry) !== -1) return;
$scope.selected.push(entry);
};
$scope.actionCut = function () {
$scope.clipboard = $scope.selected.slice();
$scope.clipboard.forEach(function (entry) {
@@ -435,11 +440,14 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
$scope.clipboard = $scope.selected.slice();
$scope.clipboard.forEach(function (entry) {
entry.fullFilePath = sanitize($scope.cwd + '/' + entry.fileName);
entry.pathFrom = $scope.cwd; // we stash the original path for pasting
});
$scope.clipboardCut = false;
};
function collectFiles(entry, callback) {
var pathFrom = entry.pathFrom;
if (entry.isDirectory) {
Client.filesGet($scope.id, $scope.type, entry.fullFilePath, 'data', function (error, result) {
if (error) return callback(error);
@@ -448,6 +456,7 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
// amend fullFilePath
result.entries.forEach(function (e) {
e.fullFilePath = sanitize(entry.fullFilePath + '/' + e.fileName);
e.pathFrom = pathFrom; // we stash the original path for pasting
});
var collectedFiles = [];
@@ -505,10 +514,11 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
if (error) return Client.error(error);
async.eachLimit(collectedFiles, 5, function (entry, callback) {
var newFilePath = sanitize($scope.cwd + '/' + ((destinationEntry && destinationEntry.isDirectory) ? destinationEntry.fileName : '') + '/' + entry.fullFilePath);
var newFilePath = sanitize($scope.cwd + '/' + ((destinationEntry && destinationEntry.isDirectory) ? destinationEntry.fileName : '') + '/' + entry.fullFilePath.slice(entry.pathFrom.length));
// TODO this will NOT overwrite files in destination!
Client.filesCopy($scope.id, $scope.type, entry.fullFilePath, newFilePath, callback);
// This will NOT overwrite but finds a unique new name to copy to
// we prefix with a / to ensure we don't do relative target paths
Client.filesCopy($scope.id, $scope.type, entry.fullFilePath, '/' + newFilePath, callback);
}, function (error) {
if (error) return Client.error(error);
@@ -853,14 +863,20 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
$('#textEditorCloseModal').modal('hide');
},
onClose: function () {
$scope.view = 'fileTree';
location.hash = $scope.cwd;
$('#textEditorCloseModal').modal('hide');
},
saveAndClose: function () {
$scope.textEditor.save(function () {
$scope.textEditor.close();
$scope.textEditor.onClose();
});
},
maybeClose: function () {
if (!$scope.textEditor.unsaved) return $scope.textEditor.close();
if (!$scope.textEditor.unsaved) return $scope.textEditor.onClose();
$('#textEditorCloseModal').modal('show');
},
};
@@ -1013,6 +1029,10 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
});
});
function isModalVisible() {
return !!document.getElementsByClassName('modal in').length;
}
// handle save shortcuts
window.addEventListener('keydown', function (event) {
if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 's') {
@@ -1023,23 +1043,27 @@ app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Cl
} else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'c') {
if ($scope.view === 'textEditor') return;
if ($scope.selected.length === 0) return;
if (isModalVisible()) return;
event.preventDefault();
$scope.$apply($scope.actionCopy);
} else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'x') {
if ($scope.view === 'textEditor') return;
if ($scope.selected.length === 0) return;
if (isModalVisible()) return;
event.preventDefault();
$scope.$apply($scope.actionCut);
} else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'v') {
if ($scope.view === 'textEditor') return;
if ($scope.clipboard.length === 0) return;
if (isModalVisible()) return;
event.preventDefault();
$scope.$apply($scope.actionPaste);
} else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'a') {
if ($scope.view === 'textEditor') return;
if (isModalVisible()) return;
event.preventDefault();
$scope.$apply($scope.actionSelectAll);

View File

@@ -230,7 +230,7 @@
body: JSON.stringify({ username: username, password: password, totpToken: totpToken })
}).then(function (response) {
if (response.status === 401 || response.status === 403) {
document.getElementById('message').innerText = '{{ login.errorIncorrectCredentials }}';
document.getElementById('message').innerText = "{{ login.errorIncorrectCredentials }}"; // FIXME this needs proper escaping for translated strings, single quotes break easily!
return;
}

View File

@@ -108,7 +108,18 @@ $state-danger-border: $brand-danger;
color: white;
}
input[type="checkbox"] {
select.form-control {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg fill='black' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
background-repeat: no-repeat;
background-position-x: 100%;
background-position-y: 5px;
}
input[type="checkbox"], input[type="radio"] {
margin-top: 2px;
}
@@ -796,6 +807,11 @@ multiselect {
margin-top: 65px;
width: calc(100% - 30px); // -30px to offset .row margins
max-width: 1200px;
h2 {
font-size: 20px;
margin-top: 10px;
}
}
.appstore-item {
@@ -1854,7 +1870,6 @@ tag-input {
}
}
// ----------------------------
// Dark mode overrides
// ----------------------------
@@ -1869,6 +1884,10 @@ tag-input {
background-color: $backgroundLight;
}
select.form-control {
background-image: url("data:image/svg+xml;utf8,<svg fill='#959595' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
}
.navbar-default {
background-color: $backgroundDark;
border-color: $backgroundDark;

View File

@@ -622,14 +622,21 @@
<div class="col-md-12">
<form role="form" name="accessForm" ng-submit="access.submit()" autocomplete="off">
<div class="form-group">
<div ng-show="access.ssoAuth">
<div class="form-group" ng-show="app.manifest.addons.email">
<label class="control-label">{{ 'app.accessControl.userManagement.title' | tr }}</label>
<p>{{ 'appstore.installDialog.userManagementMailbox' | tr }}
<span ng-bind-html="'appstore.installDialog.configuredForCloudronEmail' | tr:{ emailDocsLink: 'https://docs.cloudron.io/email/' }">
</p>
</div>
<div ng-show="access.ssoAuth && !app.manifest.addons.email">
<label class="control-label">{{ 'app.accessControl.userManagement.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#access-restriction" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<p>{{ 'app.accessControl.userManagement.description' | tr }} <span class="text-small text-warning" ng-show="access.ftp">{{ 'app.accessControl.userManagement.descriptionSftp' | tr }}</span></p>
</div>
<div ng-show="!access.ssoAuth">
<div ng-show="!access.ssoAuth || app.manifest.addons.email">
<label class="control-label">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<p ng-show="!app.manifest.addons.email" class="text-small">{{ 'appstore.installDialog.userManagementNone' | tr }} <span ng-show="access.ftp">{{ 'app.accessControl.userManagement.sftpAccessControl' | tr }}</span></p>
<p ng-show="app.manifest.addons.email" class="text-small" ng-bind-html="'appstore.installDialog.configuredForCloudronEmail' | tr:{ emailDocsLink: 'https://docs.cloudron.io/email/' }"></p>
<p ng-show="!app.manifest.addons.email">{{ 'appstore.installDialog.userManagementNone' | tr }} <span ng-show="access.ftp">{{ 'app.accessControl.userManagement.sftpAccessControl' | tr }}</span></p>
</div>
<div class="radio">

View File

@@ -55,14 +55,14 @@
<div class="form-group" ng-show="appInstall.app.manifest.addons.email">
<label class="control-label">{{ 'appstore.installDialog.userManagement' | tr }}</label>
<p>{{ 'appstore.installDialog.userManagementMailbox' | tr }}</p>
<p class="text-info" ng-bind-html="'appstore.installDialog.configuredForCloudronEmail' | tr:{ emailDocsLink: 'https://docs.cloudron.io/email/' }"></p>
<p>{{ 'appstore.installDialog.userManagementMailbox' | tr }}
<span ng-bind-html="'appstore.installDialog.configuredForCloudronEmail' | tr:{ emailDocsLink: 'https://docs.cloudron.io/email/' }">
</p>
</div>
<div class="form-group">
<label class="control-label" ng-show="!appInstall.customAuth">{{ 'appstore.installDialog.userManagement' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#access-restriction" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label" ng-show="!appInstall.customAuth && !appInstall.app.manifest.addons.email">{{ 'appstore.installDialog.userManagement' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#access-restriction" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label" ng-show="appInstall.customAuth || appInstall.app.manifest.addons.email">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<p ng-show="appInstall.customAuth && !appInstall.app.manifest.addons.email">{{ 'appstore.installDialog.userManagementNone' | tr }}</p>
<div class="radio" ng-show="appInstall.optionalSso">
<label>
@@ -265,10 +265,9 @@
<div ng-show="ready && validSubscription" class="ng-cloak appstore-toolbar">
<div class="appstore-toolbar-content">
<button class="btn" type="button" ng-click="showCategory('');" ng-class="{ 'btn-primary': '' === category }">{{ 'appstore.category.all' | tr }}</button>
<button class="btn" type="button" ng-click="showCategory('featured');" ng-class="{ 'btn-primary': 'featured' === category }">{{ 'appstore.category.popular' | tr }}</button>
<button class="btn" type="button" ng-click="showCategory('new');" ng-class="{ 'btn-primary': 'new' === category }">{{ 'appstore.category.newApps' | tr }}</button>
<div class="dropdown">
<button class="btn dropdown-toggle" type="button" data-toggle="dropdown" ng-class="{ 'btn-primary': '' !== category && 'recent' !== category && 'featured' !== category && 'new' !== category }">
<button class="btn dropdown-toggle" type="button" data-toggle="dropdown" ng-class="{ 'btn-primary': '' !== category && 'recent' !== category && 'new' !== category }">
{{ categoryButtonLabel(category) }}
<span class="caret"></span>
</button>
@@ -290,7 +289,33 @@
<br/>
<a href="https://forum.cloudron.io/category/5/app-requests" target="_blank">{{ 'appstore.appMissing' | tr }}</a>
</div>
<div class="col-md-12" ng-show="category === '' && popularApps.length">
<div class="row-no-margin">
<div class="col-sm-12">
<h2>{{ 'appstore.category.popular' | tr }}</h2>
</div>
</div>
<div class="row-no-margin">
<div class="col-sm-1 appstore-item" ng-repeat="app in popularApps">
<div class="appstore-item-content highlight" ng-click="gotoApp(app)" ng-class="{ 'appstore-item-content-testing': app.releaseState === 'unstable' }">
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.releaseState === 'unstable'">{{ 'appstore.unstable' | tr }}</span>
<div class="appstore-item-content-icon col-same-height">
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
</div>
<div class="appstore-item-content-description col-same-height">
<h4 class="appstore-item-content-title">{{ app.manifest.title }}</h4>
<div class="appstore-item-content-tagline text-muted">{{ app.manifest.tagline }}</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-12" ng-show="apps.length">
<div class="row-no-margin" ng-show="!category && !searchString">
<div class="col-sm-12">
<h2>{{ 'appstore.category.all' | tr }}</h2>
</div>
</div>
<div class="row-no-margin">
<div class="col-sm-1 appstore-item" ng-repeat="app in apps">
<div class="appstore-item-content highlight" ng-click="gotoApp(app)" ng-class="{ 'appstore-item-content-testing': app.releaseState === 'unstable' }">

View File

@@ -14,6 +14,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
$scope.ready = false;
$scope.apps = [];
$scope.popularApps = [];
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
$scope.users = [];
@@ -79,7 +80,6 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
if (category === 'new') return categoryLabel;
if (category === 'recent') return categoryLabel;
if (category === 'featured') return categoryLabel;
var tmp = $scope.categories.find(function (c) { return c.id === category; });
if (tmp) return tmp.label;
@@ -425,6 +425,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
var token = $scope.searchString.toUpperCase();
$scope.popularApps = [];
$scope.apps = apps.filter(function (app) {
if (app.manifest.id.toUpperCase().indexOf(token) !== -1) return true;
if (app.manifest.title.toUpperCase().indexOf(token) !== -1) return true;
@@ -432,7 +433,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
if (app.manifest.tags.join().toUpperCase().indexOf(token) !== -1) return true;
if (app.manifest.description.toUpperCase().indexOf(token) !== -1) return true;
return false;
});
});
});
};
@@ -471,9 +472,8 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
if (error) return $timeout($scope.showCategory.bind(null, category), 1000);
if (!$scope.category) {
$scope.apps = apps.slice(0).sort(function (a1, a2) { return a1.manifest.title.localeCompare(a2.manifest.title); });
} else if ($scope.category === 'featured') {
$scope.apps = apps.filter(function (app) { return app.featured; }).sort(function (a1, a2) { return a2.ranking - a1.ranking; }); // reverse sort
$scope.apps = apps.slice(0).filter(function (app) { return !app.featured; }).sort(function (a1, a2) { return a1.manifest.title.localeCompare(a2.manifest.title); });
$scope.popularApps = apps.slice(0).filter(function (app) { return app.featured; }).sort(function (a1, a2) { return a2.ranking - a1.ranking; });
} else if ($scope.category === 'new') {
$scope.apps = filterForNewApps(apps);
} else if ($scope.category === 'recent') {

View File

@@ -425,7 +425,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
$scope.renewCerts.message = '';
$scope.renewCerts.errorMessage = '';
Client.renewCerts(null /* all domains */, function (error, taskId) {
Client.renewCerts(function (error, taskId) {
if (error) {
console.error(error);
$scope.renewCerts.errorMessage = error.message;