Skip to content

Commit

Permalink
GDB-9969 import view help messages (#1328)
Browse files Browse the repository at this point in the history
* Reimplemented help message.
* Add import view persistence service backed by the local storage.
* Add upload limit file size info alert.
* Removed the watcher for the opened tab and replaced with explicit call to a new function `openTab(tabId)` which now handles the tab initialization.
* Fix fat button text and icons align when on lower resolutions.
* Split tests for user data tab.
* Implemented additional tests for the help messages.
* Implemented persistence for the help message on the empty state. Added more tests.
* Add more code documentation
* Skip copy to clipboard check test
  • Loading branch information
svilenvelikov authored Mar 27, 2024
1 parent 72d4cf0 commit 6f3ddc4
Show file tree
Hide file tree
Showing 24 changed files with 1,478 additions and 806 deletions.
2 changes: 2 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'angular/core/interceptors/unauthorized.interceptor';
import 'angular/core/interceptors/authentication.interceptor';
import 'angular/core/directives/rdfresourcesearch/rdf-resource-search.directive';
import 'angular/core/directives/languageselector/language-selector.directive';
import 'angular/core/directives/copy-to-clipboard/copy-to-clipboard.directive';
import 'angular/core/directives/angulartooltips/angular-tooltips.js';
import 'angular/core/directives/uppercased.directive';
import 'angular/core/directives/operations-statuses-monitor/operations-statuses-monitor.directive';
Expand All @@ -33,6 +34,7 @@ const modules = [
'graphdb.framework.core.interceptors.authentication',
'graphdb.framework.core.directives.rdfresourcesearch.rdfresourcesearch',
'graphdb.framework.core.directives.languageselector.languageselector',
'graphdb.framework.core.directives.copytoclipboard.copytoclipboard',
'graphdb.framework.core.directives.angular-tooltips',
'graphdb.framework.core.directives.uppercased',
'graphdb.framework.guides.services',
Expand Down
26 changes: 25 additions & 1 deletion src/css/import.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
flex-flow: row wrap;
}

.upload-buttons div.btn {
.upload-buttons .btn {
display: flex;
height: 100%;
max-height: 100px;
white-space: normal;
Expand All @@ -14,6 +15,7 @@

.grid-container {
display: flex;
align-items: center;
}

.text {
Expand All @@ -37,3 +39,25 @@
.impex-popover pre {
white-space: pre-wrap;
}

.file-size-prop,
.server-import-directory-prop {
border-bottom: 1px dotted var(--secondary-color);
}

.copy-btn {
border: none;
background-color: transparent;
cursor: pointer;
padding: 0;
color: var(--secondary-color);
}

.copy-btn:hover {
transform: scale(1.1);
transition: all 0.1s ease-out;
}

.copy-btn:focus {
outline: none;
}
226 changes: 127 additions & 99 deletions src/i18n/locale-en.json

Large diffs are not rendered by default.

228 changes: 128 additions & 100 deletions src/i18n/locale-fr.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* @ngdoc directive
* @name copyToClipboard
* @module graphdb.framework.core.directives.copytoclipboard.copytoclipboard
* @restrict E
*
* @description
* `copyToClipboard` is a directive that creates a button which, when clicked, copies a specific text to the clipboard.
* The directive uses an isolated scope with a single property, `tooltipText`, which is bound to the `tooltipText` attribute of the element where the directive is used.
* The tooltip text is translated using the `$translate` service.
* The button also has an `ng-click` directive that triggers the `copyToClipboard` function when the button is clicked.
*
* @param {string} tooltipText - The text to be displayed as a tooltip when hovering over the button.
*
* @example
* <copy-to-clipboard tooltip-text="Your tooltip text here"></copy-to-clipboard>
*/
angular
.module('graphdb.framework.core.directives.copytoclipboard.copytoclipboard', [])
.directive('copyToClipboard', copyToClipboard);

copyToClipboard.$inject = ['$translate', 'toastr'];

/**
* @ngdoc method
* @name copyToClipboard
* @methodOf graphdb.framework.core.directives.copytoclipboard.copytoclipboard
*
* @description
* The `copyToClipboard` function is the link function of the directive.
* It defines a `copyToClipboard` function in the directive's scope that gets the text from the element with the class `copyable` that is a child of the directive's parent element.
* It then uses the `navigator.clipboard.writeText` function to write this text to the clipboard.
* If the text is successfully written to the clipboard, a success message is displayed using the `toastr` service.
* If an error occurs, the error is logged to the console.
*
* @param {Object} $scope - The directive's isolated scope.
* @param {Object} element - The jqLite-wrapped element that this directive matches.
*/
function copyToClipboard($translate, toastr) {
return {
template: '<button class="copy-btn" gdb-tooltip="{{tooltipText | translate}}" ng-click="copyToClipboard()"><span class="icon-copy"></span></button>',
restrict: 'E',
scope: {
tooltipText: '@'
},
link: function ($scope, element) {
$scope.copyToClipboard = function() {
const textToCopy = element.parent().find('.copyable').text();
navigator.clipboard.writeText(textToCopy).then(() => {
toastr.success($translate.instant('import.help.messages.copied_to_clipboard'));
}, (err) => {
console.error('Could not copy text: ', err);
});
};
}
};
}
121 changes: 84 additions & 37 deletions src/js/angular/import/controllers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'angular/core/services';
import 'angular/utils/uri-utils';
import 'angular/rest/import.rest.service';
import 'angular/rest/upload.rest.service';
import 'angular/import/import-context.service';
import 'angular/import/import-view-storage.service';
import {FILE_STATUS} from "../models/import/file-status";
import {FileFormats} from "../models/import/file-formats";
import * as stringUtils from "../utils/string-utils";
Expand All @@ -12,10 +14,14 @@ const modules = [
'ui.bootstrap',
'toastr',
'graphdb.framework.core.services.repositories',
'graphdb.framework.utils.localstorageadapter',
'graphdb.framework.utils.uriutils',
'graphdb.framework.guides.services',
'graphdb.framework.rest.import.service',
'graphdb.framework.rest.upload.service'
'graphdb.framework.rest.upload.service',
'graphdb.framework.core.directives',
'graphdb.framework.importcontext.service',
'graphdb.framework.import.importviewstorageservice'
];

const importViewModule = angular.module('graphdb.framework.impex.import.controllers', modules);
Expand All @@ -31,13 +37,14 @@ const USER_DATA_TYPE = {
URL: 'url'
};

importViewModule.controller('ImportViewCtrl', ['$scope', 'toastr', '$interval', '$repositories', '$uibModal', '$filter', '$jwtAuth', '$location', '$translate', 'LicenseRestService', 'GuidesService', 'ModalService', 'ImportRestService',
function ($scope, toastr, $interval, $repositories, $uibModal, $filter, $jwtAuth, $location, $translate, LicenseRestService, GuidesService, ModalService, ImportRestService) {
importViewModule.controller('ImportViewCtrl', ['$scope', 'toastr', '$interval', '$repositories', '$uibModal', '$filter', '$jwtAuth', '$location', '$translate', 'LicenseRestService', 'GuidesService', 'ModalService', 'ImportRestService', 'ImportContextService', 'ImportViewStorageService',
function ($scope, toastr, $interval, $repositories, $uibModal, $filter, $jwtAuth, $location, $translate, LicenseRestService, GuidesService, ModalService, ImportRestService, ImportContextService, ImportViewStorageService) {

// =========================
// Private variables
// =========================

const subscriptions = [];
let listPollingHandler = null;
const LIST_POLLING_INTERVAL = 4000;

Expand All @@ -53,6 +60,7 @@ importViewModule.controller('ImportViewCtrl', ['$scope', 'toastr', '$interval',
$scope.fileFormatsExtended = FileFormats.getFileFormatsExtended();
$scope.fileFormatsHuman = FileFormats.getFileFormatsHuman() + $translate.instant('import.gz.zip');
$scope.textFileFormatsHuman = FileFormats.getTextFileFormatsHuman();
$scope.maxUploadFileSizeMB = 0;

// =========================
// Public functions
Expand Down Expand Up @@ -300,7 +308,7 @@ importViewModule.controller('ImportViewCtrl', ['$scope', 'toastr', '$interval',
if (!$($scope.tabId).is(':visible')) {
return;
}
updateListHttp(force);
$scope.updateListHttp(force);
};

// This should be public because it's used by the upload and import controllers
Expand All @@ -318,11 +326,13 @@ importViewModule.controller('ImportViewCtrl', ['$scope', 'toastr', '$interval',
// Private functions
// =========================

const updateListHttp = (force) => {
const loader = $scope.viewUrl === OPERATION.UPLOAD ? ImportRestService.getUploadedFiles : ImportRestService.getServerFiles;
loader($repositories.getActiveRepository()).success(function (data) {
// TODO: temporary exposed in the scope because it is called via scope.parent from the child TabsCtrl which should be changed
$scope.updateListHttp = (force) => {
const filesLoader = $scope.viewUrl === OPERATION.UPLOAD ? ImportRestService.getUploadedFiles : ImportRestService.getServerFiles;
filesLoader($repositories.getActiveRepository()).success(function (data) {
if ($scope.files.length === 0 || force) {
$scope.files = data;
ImportContextService.updateFiles($scope.files);
$scope.files.forEach(function (f) {
if (!f.type) {
f.type = $scope.defaultType;
Expand All @@ -345,16 +355,16 @@ importViewModule.controller('ImportViewCtrl', ['$scope', 'toastr', '$interval',
$scope.files = _.filter($scope.files, function (f) {
return f.status !== undefined;
});
ImportContextService.updateFiles($scope.files);
}
$scope.showClearStatuses = _.filter($scope.files, function (file) {
return file.status === FILE_STATUS.DONE || file.status === FILE_STATUS.ERROR;
}).length > 0;

$scope.savedSettings = _.mapKeys(_.filter($scope.files, 'parserSettings'), 'name');

$scope.loader = false;
}).error(function (data) {
toastr.warning($translate.instant('import.error.could.not.get.files', {data: getError(data)}));
}).finally(() => {
$scope.loader = false;
});
};
Expand All @@ -369,7 +379,7 @@ importViewModule.controller('ImportViewCtrl', ['$scope', 'toastr', '$interval',
value: data[i].value
};
}
$scope.maxUploadFileSizeMB = $scope.appData.properties['graphdb.workbench.maxUploadSize'].value / (1024 * 1024);
$scope.maxUploadFileSizeMB = FileUtils.convertBytesToMegabytes($scope.appData.properties['graphdb.workbench.maxUploadSize'].value);
}).error(function (data) {
const msg = getError(data);
toastr.error(msg, $translate.instant('common.error'));
Expand All @@ -380,38 +390,25 @@ importViewModule.controller('ImportViewCtrl', ['$scope', 'toastr', '$interval',
subscriptions.forEach((subscription) => subscription());
};

const isTabOpened = () => {
return $($scope.tabId).is(':visible');
const initPersistence = () => {
ImportViewStorageService.initImportViewSettings();
};

const onTabOpened = () => {
if (isTabOpened()) {
updateListHttp(false);
$location.hash($scope.viewType);
}
const initSubscribtions = () => {
subscriptions.push($scope.$on('repositoryIsSet', $scope.onRepositoryChange));
subscriptions.push($scope.$on('$destroy', () => $interval.cancel(listPollingHandler)));
$scope.$on('$destroy', removeAllListeners);
};

// =========================
// Watchers and event handlers
// =========================

const subscriptions = [];

subscriptions.push($scope.$on('repositoryIsSet', $scope.onRepositoryChange));

// update the list instantly when the tab is changed
subscriptions.push($scope.$watch(isTabOpened, onTabOpened));

subscriptions.push($scope.$on('$destroy', () => $interval.cancel(listPollingHandler)));

$scope.$on('$destroy', removeAllListeners);

// =========================
// Initialization
// =========================

// TODO: Beware that this init is called tree times due to the child controllers which extends this one. We should refactor this.
const init = () => {
initSubscribtions();
getAppData();
initPersistence();
};
init();
}]);
Expand All @@ -425,8 +422,9 @@ importViewModule.controller('ImportCtrl', ['$scope', 'toastr', '$controller', '$
// =========================
// Public variables
// =========================
angular.extend(this, $controller('ImportViewCtrl', {$scope: $scope}));

$scope.loader = true;
angular.extend(this, $controller('ImportViewCtrl', {$scope: $scope}));
$scope.viewUrl = OPERATION.SERVER;
$scope.defaultType = 'server';
$scope.tabId = '#import-server';
Expand Down Expand Up @@ -487,8 +485,8 @@ importViewModule.controller('UploadCtrl', ['$scope', 'toastr', '$controller', '$
// Public variables
// =========================

angular.extend(this, $controller('ImportViewCtrl', {$scope: $scope}));
$scope.loader = true;
angular.extend(this, $controller('ImportViewCtrl', {$scope: $scope}));
$scope.viewUrl = OPERATION.UPLOAD;
$scope.defaultType = USER_DATA_TYPE.FILE;
$scope.tabId = '#import-user';
Expand Down Expand Up @@ -780,29 +778,76 @@ importViewModule.controller('TextCtrl', ['$scope', '$uibModalInstance', 'text',
};
}]);

importViewModule.controller('TabCtrl', ['$scope', '$location', function ($scope, $location) {
importViewModule.controller('TabCtrl', ['$scope', '$location', 'ImportViewStorageService', 'ImportContextService', function ($scope, $location, ImportViewStorageService, ImportContextService) {

// =========================
// Private variables
// =========================

// flag to reset help visibility on empty state in initial load of the view
let shouldResetHelpVisibility = true;

// =========================
// Public variables
// =========================

$scope.viewType = $location.hash();
$scope.isCollapsed = false;
$scope.commonUrl = 'js/angular/import/templates/commonInfo.html';
$scope.isHelpVisible = true;
$scope.fileSizeLimitInfoTemplateUrl = 'js/angular/import/templates/fileSizeLimitInfo.html';

// =========================
// Public functions
// =========================

$scope.openTab = (tab) => {
$scope.viewType = tab;
$scope.$parent.viewUrl = tab === 'server' ? OPERATION.SERVER : OPERATION.UPLOAD;
$location.hash($scope.viewType);
$scope.updateListHttp(true);
};

$scope.changeHelpTemplate = function (templateFile) {
$scope.templateUrl = 'js/angular/import/templates/' + templateFile;
};

$scope.toggleHelp = () => {
const viewPersistance = ImportViewStorageService.getImportViewSettings();
ImportViewStorageService.toggleHelpVisibility();
$scope.isHelpVisible = !viewPersistance.isHelpVisible;
};

// =========================
// Private functions
// =========================

const onFilesUpdated = (files) => {
// reset help visibility on empty state in initial load
if (shouldResetHelpVisibility && files.length === 0) {
ImportViewStorageService.setHelpVisibility(true);
shouldResetHelpVisibility = false;
}
const viewPersistance = ImportViewStorageService.getImportViewSettings();
let isVisible = viewPersistance.isHelpVisible;
if (files.length === 0 && viewPersistance.isHelpVisible) {
isVisible = true;
} else if (files.length === 0 && !viewPersistance.isHelpVisible) {
isVisible = false;
} else if (viewPersistance.isHelpVisible) {
isVisible = true;
} else if (!viewPersistance.isHelpVisible) {
isVisible = false;
}
ImportViewStorageService.setHelpVisibility(isVisible);
$scope.isHelpVisible = isVisible;
};

// =========================
// Initialization
// =========================

const init = function () {
ImportContextService.onFilesUpdated(onFilesUpdated);

if ($scope.viewType !== 'user' && $scope.viewType !== 'server') {
$scope.viewType = 'user';
}
Expand All @@ -812,6 +857,8 @@ importViewModule.controller('TabCtrl', ['$scope', '$location', function ($scope,
} else {
$scope.templateUrl = 'js/angular/import/templates/importInfo.html';
}

$scope.openTab($scope.viewType || 'user');
};
init();
}]);
Expand Down
Loading

0 comments on commit 6f3ddc4

Please sign in to comment.