Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GDB-10692 - Change toggle behavior of import checkbox #1690

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
12 changes: 9 additions & 3 deletions src/css/import-resource-tree.css
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,18 @@
margin-right: .5rem;
}

.import-resource-tree .import-resources-actions .import-resource-status-dropdown .import-resource-status-checkbox {
pointer-events: none;
.import-resource-tree .import-resources-actions .import-resource-status-dropdown {
padding-left: 5px;
gap: 5px;
margin-right: 10px;
}

.import-resource-tree .import-resources-actions .import-resource-status-dropdown #importSelectCheckboxInput {
cursor: pointer;
}

.import-resource-tree .import-resources-actions .import-resource-status-dropdown .dropdown-toggle {
padding-left: 0.5rem;
margin-right: 0;
}

.import-resource-tree .import-resources-actions .import-resource-status-dropdown .dropdown-toggle:not(.selected) {
Expand Down
92 changes: 39 additions & 53 deletions src/js/angular/import/directives/import-resource-tree.directive.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {SortingType} from "../../models/import/sorting-type";
import {ImportResourceTreeElement} from "../../models/import/import-resource-tree-element";
import {TABS} from "../services/import-context.service";
import {ImportResourceTreeService} from "../services/import-resource-tree.service";
import {ResourceListUtil} from "../../models/import/resource-list-wrapper";
import {convertToBytes} from "../../utils/size-util";

const TYPE_FILTER_OPTIONS = {
Expand Down Expand Up @@ -64,13 +65,12 @@ function importResourceTreeDirective($timeout, ImportContextService) {
// =========================
$scope.resources = new ImportResourceTreeElement();
$scope.displayResources = [];
$scope.resourceListUtil = undefined;
$scope.TYPE_FILTER_OPTIONS = TYPE_FILTER_OPTIONS;
$scope.filterByType = TYPE_FILTER_OPTIONS.ALL;
$scope.filterByFileName = '';
$scope.STATUS_OPTIONS = STATUS_OPTIONS;
$scope.selectedByStatus = undefined;
$scope.areAllDisplayedImportResourcesSelected = false;
$scope.areAllDisplayedImportResourcesPartialSelected = false;
$scope.selectedByStatus = STATUS_OPTIONS.NONE;
$scope.ImportResourceStatus = ImportResourceStatus;
$scope.canRemoveResource = angular.isDefined(attrs.onRemove);
$scope.canResetSelectedResources = false;
Expand Down Expand Up @@ -104,7 +104,13 @@ function importResourceTreeDirective($timeout, ImportContextService) {
} else if (STATUS_OPTIONS.IMPORTED === $scope.selectedByStatus) {
$scope.resources.selectAllWithStatus([ImportResourceStatus.DONE]);
} else if (STATUS_OPTIONS.NOT_IMPORTED === $scope.selectedByStatus) {
$scope.resources.selectAllWithStatus([ImportResourceStatus.IMPORTING, ImportResourceStatus.NONE, ImportResourceStatus.ERROR, ImportResourceStatus.PENDING, ImportResourceStatus.INTERRUPTING]);
$scope.resources.selectAllWithStatus([
ImportResourceStatus.IMPORTING,
ImportResourceStatus.NONE,
ImportResourceStatus.ERROR,
ImportResourceStatus.PENDING,
ImportResourceStatus.INTERRUPTING
]);
}

updateListedImportResources();
Expand All @@ -113,11 +119,13 @@ function importResourceTreeDirective($timeout, ImportContextService) {

$scope.filterByTypeChanged = (newType) => {
$scope.filterByType = newType;
$scope.resourceListUtil.setFilterByType(newType);
updateListedImportResources();
};

$scope.filterByFileNameChanged = (filterByFileName) => {
$scope.filterByFileName = filterByFileName;
$scope.resourceListUtil.setFilterByName(filterByFileName);
debounce(updateListedImportResources, 100);
};

Expand Down Expand Up @@ -180,6 +188,14 @@ function importResourceTreeDirective($timeout, ImportContextService) {
$scope.onEditResource({resource});
};

$scope.toggleSelectAll = () => {
$scope.resources.setSelection(!($scope.resourceListUtil.areAllDisplayedImportResourcesPartialSelected ||
$scope.resourceListUtil.areAllDisplayedImportResourcesSelected));

updateListedImportResources();
setCanResetResourcesFlag();
};

// =========================
// Private functions
// =========================
Expand All @@ -195,10 +211,9 @@ function importResourceTreeDirective($timeout, ImportContextService) {
const updateListedImportResources = () => {
$scope.resources.getRoot().updateSelectionState();
sortResources();
$scope.displayResources = $scope.resources.toList()
.filter(filterByType)
.filter(filterByName);

// Update utility class with sorted resources
$scope.resourceListUtil.setResourceList($scope.resources);
$scope.displayResources = $scope.resourceListUtil.getFilteredResources();
updateHasSelection();
updateSelectByStateDropdownModel();
};
Expand All @@ -210,85 +225,55 @@ function importResourceTreeDirective($timeout, ImportContextService) {
};

const updateSelectByStateDropdownModel = () => {
const hasUnselectedDisplayedImportResource = $scope.displayResources.some((resource) => !resource.selected);
const hasSelectedDisplayedImportResource = $scope.displayResources.some((resource) => resource.selected);
$scope.areAllDisplayedImportResourcesSelected = hasSelectedDisplayedImportResource && !hasUnselectedDisplayedImportResource;
$scope.areAllDisplayedImportResourcesPartialSelected = hasSelectedDisplayedImportResource && hasUnselectedDisplayedImportResource;
const mainCheckbox = element[0].querySelector('#importSelectCheckboxInput');

if (mainCheckbox) {
mainCheckbox.checked = $scope.resourceListUtil.areAllDisplayedResourcesSelectedOrPartial();
}
};

const sortResources = () => {
if (SortingType.NAME === $scope.sortedBy) {
$scope.resources.sort(compareByName($scope.sortAsc));
$scope.resources.sort(nameComparator($scope.sortAsc));
} else if (SortingType.SIZE === $scope.sortedBy) {
$scope.resources.sort(compareBySize($scope.sortAsc));
$scope.resources.sort(sizeComparator($scope.sortAsc));
} else if (SortingType.MODIFIED === $scope.sortedBy) {
$scope.resources.sort(compareByModified($scope.sortAsc));
$scope.resources.sort(modifiedByComparator($scope.sortAsc));
} else if (SortingType.IMPORTED === $scope.sortedBy) {
$scope.resources.sort(compareByImportedOn($scope.sortAsc));
$scope.resources.sort(importedOnComparator($scope.sortAsc));
} else if (SortingType.CONTEXT === $scope.sortedBy) {
$scope.resources.sort(compareByContext($scope.sortAsc));
$scope.resources.sort(contextComparator($scope.sortAsc));
}
};

const compareByName = (acs) => (r1, r2) => {
const nameComparator = (acs) => (r1, r2) => {
return acs ? r1.importResource.name.localeCompare(r2.importResource.name) : r2.importResource.name.localeCompare(r1.importResource.name);
};

const compareBySize = (acs) => (r1, r2) => {
const sizeComparator = (acs) => (r1, r2) => {
// The format of size returned by the backend has changed, but we need to keep the old format for backward compatibility.
// Therefore, we convert the size to always be in bytes.
const r1Size = convertToBytes(r1.importResource.size);
const r2Size = convertToBytes(r2.importResource.size);
return acs ? r1Size - r2Size : r2Size - r1Size;
};

const compareByModified = (acs) => (r1, r2) => {
const modifiedByComparator = (acs) => (r1, r2) => {
const r1ModifiedOn = r1.importResource.modifiedOn || Number.MAX_VALUE;
const r2ModifiedOn = r2.importResource.modifiedOn || Number.MAX_VALUE;
return acs ? r1ModifiedOn - r2ModifiedOn : r2ModifiedOn - r1ModifiedOn;
};

const compareByImportedOn = (acs) => (r1, r2) => {
const importedOnComparator = (acs) => (r1, r2) => {
const r1ImportedOn = r1.importResource.importedOn || Number.MAX_VALUE;
const r2ImportedOn = r2.importResource.importedOn || Number.MAX_VALUE;
return acs ? r1ImportedOn - r2ImportedOn : r2ImportedOn - r1ImportedOn;
};

const compareByContext = (acs) => (r1, r2) => {
const contextComparator = (acs) => (r1, r2) => {
return acs ? r1.importResource.context.localeCompare(r2.importResource.context) : r2.importResource.context.localeCompare(r1.importResource.context);
};

const filterByType = (resource) => {
if (TYPE_FILTER_OPTIONS.ALL === $scope.filterByType) {
return true;
}

if ($scope.filterByType === TYPE_FILTER_OPTIONS.FILE) {
return resource.isFile();
}

if ($scope.filterByType === TYPE_FILTER_OPTIONS.DIRECTORY) {
return resource.isDirectory();
}

return false;
};

const filterByName = (resource) => {
if (!$scope.filterByFileName) {
return true;
}

if ($scope.filterByType === TYPE_FILTER_OPTIONS.DIRECTORY) {
return resource.hasTextInDirectoriesName($scope.filterByFileName);
}

if ($scope.filterByType === TYPE_FILTER_OPTIONS.FILE) {
return resource.hasTextInFilesName($scope.filterByFileName);
}
return resource.hasTextInResourcesName($scope.filterByFileName);
};

let debounceTimeout;
const debounce = (func, delay) => {
// Clear previous timeout
Expand All @@ -314,6 +299,7 @@ function importResourceTreeDirective($timeout, ImportContextService) {
} else {
ImportResourceTreeService.mergeResourceTree($scope.resources, newResources, isUserImport);
}
$scope.resourceListUtil = new ResourceListUtil($scope.resources, $scope.filterByType, $scope.filterByFileName);
ImportResourceTreeService.calculateElementIndent($scope.resources);
ImportResourceTreeService.setupAfterTreeInitProperties($scope.resources);
updateListedImportResources();
Expand Down
9 changes: 4 additions & 5 deletions src/js/angular/import/templates/import-resource-tree.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
<div class="import-resource-tree mt-2" ng-if="!showLoader && resources && !resources.isEmpty()">
<div class="import-resource-tree-header">
<div class="import-resources-actions">
<div class="import-resource-status-dropdown btn-group" uib-dropdown>
<div class="import-resource-status-dropdown btn-group btn-secondary" uib-dropdown>
<input type="checkbox" id="importSelectCheckboxInput" ng-class="{'selected': selectedByStatus}"
prop-indeterminate="resourceListUtil.areAllDisplayedImportResourcesPartialSelected"
ng-click="toggleSelectAll()">
<button type="button" uib-dropdown-toggle class="btn btn-secondary dropdown-toggle"
ng-class="{'selected': selectedByStatus}">
<input type="checkbox" prop-indeterminate="areAllDisplayedImportResourcesPartialSelected"
ng-model="areAllDisplayedImportResourcesSelected">
{{selectedByStatus ? (('import.import_resource_tree.status.' + selectedByStatus) | translate) : '
'}}
</button>
<ul class="dropdown-menu" role="menu">
<li>
Expand Down
73 changes: 73 additions & 0 deletions src/js/angular/models/import/resource-list-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
export class ResourceListUtil {
constructor(resources, filterByTypeOption, filterByFileName) {
this.resourceList = resources.toList() || [];
this.filterByTypeOption = filterByTypeOption;
this.filterByFileName = filterByFileName;
}

get areAllDisplayedImportResourcesSelected() {
const filteredResources = this.getFilteredResources();
return filteredResources.every((resource) => resource.selected);
}

get areAllDisplayedImportResourcesPartialSelected() {
const filteredResources = this.getFilteredResources();
const hasUnselected = filteredResources.some((resource) => !resource.selected);
const hasSelected = filteredResources.some((resource) => resource.selected);
return hasSelected && hasUnselected;
}

areAllDisplayedResourcesSelectedOrPartial() {
return !!(this.areAllDisplayedImportResourcesSelected || this.areAllDisplayedImportResourcesPartialSelected);
}

setFilterByType(filterByType) {
this.filterByTypeOption = filterByType;
}

setFilterByName(filterByFileName) {
this.filterByFileName = filterByFileName;
}

setResourceList(resources) {
this.resourceList = resources.toList() || [];
}

filterByType(resource) {
switch (this.filterByTypeOption) {
case TYPE_FILTER_OPTIONS.ALL:
return true;
case TYPE_FILTER_OPTIONS.FILE:
return resource.isFile();
case TYPE_FILTER_OPTIONS.DIRECTORY:
return resource.isDirectory();
default:
return false;
}
}

filterByName(resource) {
if (!this.filterByFileName) {
return true;
}

if (this.filterByTypeOption === TYPE_FILTER_OPTIONS.DIRECTORY) {
return resource.hasTextInDirectoriesName(this.filterByFileName);
}

if (this.filterByTypeOption === TYPE_FILTER_OPTIONS.FILE) {
return resource.hasTextInFilesName(this.filterByFileName);
}

return resource.hasTextInResourcesName(this.filterByFileName);
}

getFilteredResources() {
return this.resourceList.filter((resource) => this.filterByType(resource) && this.filterByName(resource));
}
}
const TYPE_FILTER_OPTIONS = {
'FILE': 'FILE',
'DIRECTORY': 'DIRECTORY',
'ALL': 'ALL'
};
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,44 @@ describe('Import user data: File upload', () => {
ImportUserDataSteps.getResources().should('have.length', 1);
ImportUserDataSteps.checkImportedResource(0, 'invalid-format.json', 'RDF Parse Error: Invalid file format');
});

it('should be able to select files via individual and main checkboxes', () => {
// Given I upload a file
ImportUserDataSteps.selectFile(ImportUserDataSteps.createFile(testFiles[0], bnodes));
ImportSettingsDialogSteps.import();
ImportUserDataSteps.selectFile(ImportUserDataSteps.createFile(testFiles[1], jsonld));
ImportSettingsDialogSteps.import();
ImportUserDataSteps.getResources().should('have.length', 2);
// When I check the main checkbox
ImportUserDataSteps.checkMainCheckbox();
// Then I expect all the list items to be selected
ImportUserDataSteps.getSelectedResources().should('have.length', 2);
// When I uncheck the main checkbox
ImportUserDataSteps.checkMainCheckbox();
// Then I expect all the list items to be unchecked
ImportUserDataSteps.getSelectedResources().should('have.length', 0);
ImportUserDataSteps.getMainCheckbox().should('not.be.checked');
// When I check one item from the list
ImportUserDataSteps.selectFileByIndex(0);
// Then I expect the main checkbox to be indeterminate
ImportUserDataSteps.getMainCheckbox().should('have.prop', 'indeterminate', true);
// When I check the indeterminate checkbox
ImportUserDataSteps.checkMainCheckbox();
// Then I expect all checkboxes to be unchecked
ImportUserDataSteps.getMainCheckbox().should('not.be.checked');
ImportUserDataSteps.getResource(0).find('.select-checkbox').should('not.be.checked');
ImportUserDataSteps.getResource(1).find('.select-checkbox').should('not.be.checked');
// When I reset a file's status
ImportUserDataSteps.resetFileStatus(1);
// Then I select only imported files from the dropdown
ImportUserDataSteps.selectImportedResources();
// And I expect an indeterminate main checkbox
ImportUserDataSteps.getMainCheckbox().should('have.prop', 'indeterminate', true);
// When I check the indeterminate checkbox
ImportUserDataSteps.checkMainCheckbox();
// Then I expect all checkboxes to be unchecked
ImportUserDataSteps.getMainCheckbox().should('not.be.checked');
ImportUserDataSteps.getResource(0).find('.select-checkbox').should('not.be.checked');
ImportUserDataSteps.getResource(1).find('.select-checkbox').should('not.be.checked');
});
});
8 changes: 8 additions & 0 deletions test-cypress/steps/import/import-steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ class ImportSteps {
return this.getResourcesTable().find('.row.title-row');
}

static checkMainCheckbox() {
this.getMainCheckbox().click();
}

static getMainCheckbox() {
return this.getResourcesTable().find('#importSelectCheckboxInput');
}

static getSelectedResources() {
return this.getResources().find('.select-checkbox:checked');
}
Expand Down