diff --git a/app/common/components/device/certificate/certificate-button/certificate-button.directive.js b/app/common/components/device/certificate/certificate-button/certificate-button.directive.js index 62e15257..12f98214 100644 --- a/app/common/components/device/certificate/certificate-button/certificate-button.directive.js +++ b/app/common/components/device/certificate/certificate-button/certificate-button.directive.js @@ -1,15 +1,18 @@ -function certificateButton (dhModal) { +function certificateButton (dhModal, ResourceListSelector) { return { template: require('./certificate-button.directive.html'), restrict: 'E', - scope: { - resources: '=' - }, + scope: {}, link: $scope => { $scope.dropDownIsOpen = false - $scope.$watchCollection('resources', resources => { - $scope.allComputers = _.every(resources, {'@type': 'Computer'}) - }) + + function setView () { + $scope.resources = ResourceListSelector.getAllSelectedDevices() + $scope.allComputers = _.every($scope.resources, {'@type': 'Computer'}) + } + setView() + ResourceListSelector.callbackOnSelection(setView) + $scope.certificates = [ { title: 'Erasure', @@ -17,11 +20,11 @@ function certificateButton (dhModal) { description: 'Shows a brief and the details of the hard-drives erased.' } ] - /*{ + /* { title: 'Receipt', icon: ResourceSettings('resources:Receive').settings.fa, description: 'Generates a legal receipt for a receiver to sign.' - }*/ + } */ $scope.openDropdown = function ($event) { if ($scope.allComputers && $scope.resources.length > 0) $scope.dropDownIsOpen = true $event.stopPropagation() // https://github.com/angular-ui/bootstrap/issues/6038 diff --git a/app/common/components/elements/header-nav/header-nav.directive.sass b/app/common/components/elements/header-nav/header-nav.directive.sass index 69590129..8c61246f 100644 --- a/app/common/components/elements/header-nav/header-nav.directive.sass +++ b/app/common/components/elements/header-nav/header-nav.directive.sass @@ -7,7 +7,7 @@ header-nav user-select: none .navbar-brand float: left - padding: 2px $navbar-padding-horizontal + padding: $navbar-padding-vertical $navbar-padding-horizontal font-size: $font-size-h2 line-height: $headings-line-height height: $navbar-height @@ -22,6 +22,6 @@ header-nav .navbar-toggle padding: 5px 10px - @media (min-width: $grid-float-breakpoint) - .navbar > .container, .navbar > .container-fluid - margin-left: -$navbar-padding-horizontal + //@media (min-width: $grid-float-breakpoint) + // .navbar > .container, .navbar > .container-fluid + // margin-left: -$navbar-padding-horizontal diff --git a/app/common/components/event/manual-events-button/manual-events-button.directive.html b/app/common/components/event/manual-events-button/manual-events-button.directive.html index 6e0cd555..75c6110e 100644 --- a/app/common/components/event/manual-events-button/manual-events-button.directive.html +++ b/app/common/components/event/manual-events-button/manual-events-button.directive.html @@ -2,7 +2,7 @@ +

Components

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Range
+ + Processor{{selection.props.components.processorModel}}
+ + + Hard drive + {{::selection.props.components.totalHardDriveSize}}
+ + + RAM + {{::selection.props.components.totalRamSize}} + +
+ \ No newline at end of file diff --git a/app/common/components/resource/resource-list/resource-list-selection-summary/condition-score.html b/app/common/components/resource/resource-list/resource-list-selection-summary/condition-score.html new file mode 100644 index 00000000..8e7d914f --- /dev/null +++ b/app/common/components/resource/resource-list/resource-list-selection-summary/condition-score.html @@ -0,0 +1,55 @@ +
+ +

Condition score

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Condition
+ + Appearance + + {{::selection.props.condition.appearance.general}} +
+ + Functionality + + {{::selection.props.condition.functionality.general}} +
Total condition + + + +
+
\ No newline at end of file diff --git a/app/common/components/resource/resource-list/resource-list-selection-summary/events.html b/app/common/components/resource/resource-list/resource-list-selection-summary/events.html new file mode 100644 index 00000000..74bcdbfe --- /dev/null +++ b/app/common/components/resource/resource-list/resource-list-selection-summary/events.html @@ -0,0 +1,63 @@ +
+
+
+
+ +
+ + +
+

{{ utils.getResourceTitle(event) }}

+ +
+
+
+
+
+
+
+ +

Events

+ + + + + + + + + + + + + +
+ + + {{header}} +
+ {{ event['@type'].substr('devices:'.length) }} + + {{ event[prop]}} + + {{ event['_updated'] }} +
+
diff --git a/app/common/components/resource/resource-list/resource-list-selection-summary/lots.html b/app/common/components/resource/resource-list/resource-list-selection-summary/lots.html new file mode 100644 index 00000000..c2a7c4a3 --- /dev/null +++ b/app/common/components/resource/resource-list/resource-list-selection-summary/lots.html @@ -0,0 +1,40 @@ +
+ +

Selected lots and devices

+ +
diff --git a/app/common/components/resource/resource-list/resource-list-selection-summary/price.html b/app/common/components/resource/resource-list/resource-list-selection-summary/price.html new file mode 100644 index 00000000..1bde9252 --- /dev/null +++ b/app/common/components/resource/resource-list/resource-list-selection-summary/price.html @@ -0,0 +1,48 @@ +
+ +

Price

+
+
+

Warranty

+ + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +
%Amount
{{::roleName | humanize}}{{ selection.props.pricing[roleName][currencyOptions.val].percentage }}{{ selection.props.pricing[roleName][currencyOptions.val].amount }}
Total{{selection.props.pricing.total[currencyOptions.val] }} +
+
\ No newline at end of file diff --git a/app/common/components/resource/resource-list/resource-list-selection-summary/status.html b/app/common/components/resource/resource-list/resource-list-selection-summary/status.html new file mode 100644 index 00000000..759ddb80 --- /dev/null +++ b/app/common/components/resource/resource-list/resource-list-selection-summary/status.html @@ -0,0 +1,17 @@ +
+ +

Status

+
+
+
+
Current status
+

{{ selection.props.status }}

+
+
+
+
\ No newline at end of file diff --git a/app/common/components/resource/resource-list/resource-list-selection-summary/type.html b/app/common/components/resource/resource-list/resource-list-selection-summary/type.html new file mode 100644 index 00000000..d59319a7 --- /dev/null +++ b/app/common/components/resource/resource-list/resource-list-selection-summary/type.html @@ -0,0 +1,39 @@ +
+ +

Device

+
+
+
Type
+

{{ selection.props.type }}

+
+
+
Sub type
+

{{ selection.props.subType }}

+
+
+
+
+
Manufacturer
+

{{ selection.props.manufacturer }}

+
+
+
Model
+

{{ selection.props.model }}

+
+
+
+
+
Serial number
+

{{ selection.props.serialNumber }}

+
+
+
HID
+

{{ selection.props.hid }}

+
+
+
\ No newline at end of file diff --git a/app/common/components/resource/resource-list/resource-list-selector-big.factory.js b/app/common/components/resource/resource-list/resource-list-selector-big.factory.js deleted file mode 100644 index bd34a449..00000000 --- a/app/common/components/resource/resource-list/resource-list-selector-big.factory.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * - * @param {ResourceListSelector} ResourceSelector - * @returns {ResourceSelectorBig} - * @constructor - */ -function ResourceListSelectorBigFactory (ResourceListSelector) { - let callbacksForSelections = [] - /** - * ResourceListSelector with a way to broadcast - */ - class ResourceListSelectorBig extends ResourceListSelector { - - _control () { - super._control() - // Perform callback - _.invokeMap(callbacksForSelections, _.call, null, this.total, this.inList, this.resources) - } - - /** - * Callback to function after there have been changes in the selected resources. - * - * The callback method signature is: callback(totalSelectedResources, selectedResourcesInList, totalResources) - * @param {Function} callback - */ - static callbackOnSelection (callback) { - callbacksForSelections.push(callback) - } - } - - return ResourceListSelectorBig -} - -module.exports = ResourceListSelectorBigFactory diff --git a/app/common/components/resource/resource-list/resource-list-selector.factory.js b/app/common/components/resource/resource-list/resource-list-selector.factory.js deleted file mode 100644 index 70f0dfb3..00000000 --- a/app/common/components/resource/resource-list/resource-list-selector.factory.js +++ /dev/null @@ -1,170 +0,0 @@ -function ResourceSelectorFactory () { - /** - * Manages selecting resources in the resource-list. - * - * The user can select them through a checkbox in every row and through a 'select all on this list' / 'unselect all' - * button at the end of the table. - * - * There is the concept of *actual list* and *total*. The actual list is formed by the list of resources the user - * sees. However, as the user filters the list, resources appear and disappear. Those resources that - * the user selected and disappeared are still being held in a *total* array, and if through the filters - * those resources appear again, the user will see them selected. - */ - class ResourceListSelector { - /** - * - * @param {object} selector - A selector object with the ng-models of the checkboxes - * - 'checked': the ng-model for the 'selectAll' checkbox - * - 'checkboxes': an array of ng-models for the individual resource select checkboxes where the '_id' is the key. - * @param {array} resources - The array that will hold the resources - * @param {ResourceListGetter} resourceListGetter - An instance of ResourceListGetter - */ - constructor (selector, resources, resourceListGetter) { - this.selector = selector - this.resources = resources - - /** - * Array holding the resources that are selected in the actual list. - * @type {Array} - */ - this.inList = selector.inList = [] - /** - * Resources selected through all lists. - * @type {Array} - */ - this.total = selector.total = [] - resourceListGetter.callbackOnGetting(_.bind(this.reAddToActualList, this, _)) - this.callbacksForSelections = [] - } - - /** - * Toggles the checkbox where the resource is in, selecting (or deselecting) the device(s) - * accordingly. It can be multiple devices if *shift* is pressed. - * - * @param {object} resource - The resource to select/deselect - * @param {$event} $event - JQuery's/JQLite event object - * @param {number} $index - The index of the resource in the *resources* list. - */ - toggle (resource, $event, $index) { - if (this.isInList(resource)) { // Remove - this.remove(resource) - } else { // Add - if ($event.shiftKey) { // Add multiple - let foundPreviousSelectedOne = false - // Selects all resources until finds a previously selected resources or reaches the beginning - for (let i = $index; i >= 0 && !foundPreviousSelectedOne; i--) { - foundPreviousSelectedOne = !this.add(this.resources[i]) - } - } else { // Add one - this.add(resource) - } - } - } - - /** - * Toggles the checkbox that lets selecting all resources on the list or unselect all resources, in general. - * @param selectAll - */ - toggleSelectAll (selectAll) { - let self = this - if (selectAll) { - _.forEach(this.resources, resource => { self.add(resource) }) // Note that add can return false - } - else { - this.deselectAll() - } - } - - /** - * Deselects all devices - */ - deselectAll () { - this.inList.length = 0 - this.total.length = 0 - this.selector.checkboxes = {} - this._control() - } - - /** - * Re-populate the actual list of resources with the passed-in resources. - * - * This method is used after getting new resources, as some resources may have been - * selected in other lists (they are in the total list). - * @param resources - */ - reAddToActualList (resources) { - // We re-populate inList from the actual resources that are in total - let self = this - this.inList.length = 0 - this._control() - this.selector.checkboxes = {} // todo is ok with ng-model? - _.forEach(resources, function (resource) { - if (_.find(self.total, {_id: resource._id})) { - self.add(resource, true) // 2nd parameter -> We add it only to inList - } - }) - } - - /** - * Returns true if the resource is in the *actual* list. - * @param {object} resource - * @returns {boolean} - */ - isInList (resource) { - return _.find(this.inList, {_id: resource._id}) - } - - /** - * Adds a resource to the actual list and to the total, if it was not there before. - * @param {object} resource - The resource to add - * @param {boolean} inListOnly - True for not adding it to the total list - * @return {boolean} True if a *new* resource has been added to the list - */ - add (resource, inListOnly = false) { - if (!this.isInList(resource)) { - this.inList.push(resource) - if (!inListOnly) { - this.total.push(resource) - } else { - // Although we don't add the resource to the total list, if the result existed in the total list, we update it - const indexOfExistingResource = _.findIndex(this.total, {'_id': resource._id}) - if (indexOfExistingResource !== -1) this.total[indexOfExistingResource] = resource - } - this.selector.checkboxes[resource['_id']] = true - this._control() - return true - } else { - return false - } - } - - /** - * Removes a resources from the actual and total lists, if it was there. - * @param resource - */ - remove (resource) { - _.remove(this.inList, {'_id': resource['_id']}) - _.remove(this.total, {'_id': resource['_id']}) - this.selector.checkboxes[resource['_id']] = false - this._control() - } - - /** - * Performs common tasks after adding/removing resources on both lists. - * @private - */ - _control () { - this.selector.checked = this.inList.length === this.resources.length - _.invokeMap(this.callbacksForSelections, _.call, null, this.total, this.inList, this.resources) - } - - callbackOnSelection (callback) { - this.callbacksForSelections.push(callback) - } - - } - - return ResourceListSelector -} - -module.exports = ResourceSelectorFactory diff --git a/app/common/components/resource/resource-list/resource-list-selector.service.js b/app/common/components/resource/resource-list/resource-list-selector.service.js new file mode 100644 index 00000000..c6e31d2d --- /dev/null +++ b/app/common/components/resource/resource-list/resource-list-selector.service.js @@ -0,0 +1,286 @@ +/** + * Manages selecting resources in the resource-list. + * + * The user can select them through a checkbox in every row and through a 'select all on this list' / 'unselect all' + * button at the end of the table. + * + * There is the concept of *actual list* and *total*. The actual list is formed by the list of resources the user + * sees. However, as the user filters the list, resources appear and disappear. Those resources that + * the user selected and disappeared are still being held in a *total* array, and if through the filters + * those resources appear again, the user will see them selected. + */ +class ResourceListSelector { + /** + * + * @param {object} selector - A selector object with the ng-models of the checkboxes + * - 'checked': the ng-model for the 'selectAll' checkbox + * - 'checkboxes': an array of ng-models for the individual resource select checkboxes where the '_id' is the key. + */ + constructor () { + let callbacksForSelections = [] + let lots = [] + + /** + * Toggles the selected state of given resource, selecting (or deselecting) the device(s) + * accordingly. It can be multiple devices if *shift* is pressed. + * + * @param {object} resource - The resource to select/deselect + */ + this.toggle = (resource, parentLot) => { + if (this.isSelected(resource)) { // Remove + remove(resource) + } else { // Add + add(resource, parentLot) + } + _control() + } + + /** + * Selects all given resources + * @param resources + */ + this.selectAll = (resources, parentLot) => { + if (!parentLot) { + throw new Error('parentLot must be set') + } + _.forEach(resources, resource => { + add(resource, parentLot) + }) + _control() + } + + /** + * Deselects all given devices + */ + this.deselectAll = devices => { + _deselectAll(devices) + _control() + } + + let _deselectAll = devices => { + if (!devices || !devices.slice) { + lots.length = 0 + return + } + + devices = devices.slice() // given devices might the same array we remove devices from + devices.forEach(device => { + remove(device) + }) + } + + /** + * Re-populate the actual list of resources with the passed-in resources. + * + * This method is used after getting new resources, as some resources may have been + * selected in other lists (they are in the total list). + * @param resources + * TODO refactor to use lots + */ + this.reAddToLot = (resources, lotID) => { + // We re-populate inList from the actual resources that are in total + this.getLotByID(lotID) && (this.getLotByID(lotID).length = 0) + _control() + } + + /** + * Returns true if the resource is selected, i.e. is in one of the lots + * @param {object} resource + * @returns {boolean} + */ + this.isSelected = resource => { + let lotsList = getLotsAsList() + for (let i = 0; i < lotsList.length; i++) { + let existingResource = _.find(getSelectedDevices(lotsList[i]), {_id: resource._id}) + if (existingResource) { + return true + } + } + return false + } + + /** + * Adds given resource to all parent lots of the resource, if it was not there before. + * @param {object} resource - The resource to add + * @param {object} parentLot - Parent lot of given resource + */ + let add = (resource, parentLot) => { + if (resource['@type'] === 'Lot') { + throw new Error('tried to add lot to selection') + } + if (!parentLot || !parentLot._id) { + throw new Error('parentLot must be defined and have _id property set') + } + + resource.parentLots.forEach(_lot => { + let lot = getOrCreateLot(_lot) + + let existingResource = _.find(lot.selectedDevices, {_id: resource._id}) + if (!existingResource) { + lot.selectedDevices.push({ + _id: resource._id, + device: resource + }) + } + }) + } + + /** + * Removes a resources from the actual and total lists, if it was there. + * @param resource + */ + let remove = resource => { + resource.parentLots.forEach(lot => { + let selectedLot = this.getLotByID(lot._id) + if (selectedLot) { + _.remove(selectedLot.selectedDevices, {'_id': resource['_id']}) + if (selectedLot.selectedDevices === 0) { + deleteLotByID(lot._id) + } + } + }) + } + + this.callbackOnSelection = callback => { + callbacksForSelections.push(callback) + } + + this.getAllSelectedDevices = () => { + let devices = {} + getLotsAsList().forEach(lot => { + getSelectedDevices(lot).forEach(device => { + devices[device._id] = device.device + }) + }) + return _.values(devices) + } + + let getNonEmptyLots = () => { + return getLotsAsList() + .map((_lot) => { + let selectedDevices = getSelectedDevices(_lot).map((device) => { + return device.device + }) + let lot = {} + _.assign(lot, _lot.lot, { selectedDevices: selectedDevices }) + return lot + }).filter((lot) => { + return lot.selectedDevices.length > 0 + }) + } + + /** + * Returns non empty lots. Lots in which devices where selected have lower indexes + */ + this.getLots = () => { + return getNonEmptyLots() + } + + let getLotsAsList = () => { + return lots + } + + this.getLotByID = lotID => { + return _.find(lots, {_id: lotID}) + } + + let deleteLotByID = lotID => { + _.remove(lots, { _id: lotID }) + } + + let getSelectedDevices = lot => { + return lot.selectedDevices + } + + let getOrCreateLot = (_lot) => { + let lot = this.getLotByID(_lot._id) + if (!lot) { + lot = { + _id: _lot._id, + lot: _lot, + selectedDevices: [], + hasOriginallySelectedDevices: false + } + lots.push(lot) + } + return lot + } + + // Workaround to set labels of selected lots correctly. Necessary because API /devices doesn't include the 'label' property for device ancestors + // TODO remove as soon as API returns ancestor lots with labels set + this.nameLot = (_lot) => { + let lot = getOrCreateLot(_lot) + if (_lot.label) { + lot.lot.label = _lot.label + } + return lot + } + + // TODO move to resource-list.directive + this.VARIOUS = 'Various' + + // TODO move to resource-list.directive + this.getAggregatedPropertyOfSelected = (selectedDevices = [], pathToProp, valueIfDifferent = this.VARIOUS, postfix) => { + if (selectedDevices.length === 0) { + return null + } + let reducedDevice = selectedDevices.reduce((accumulate, device) => { + let value = accumulate[pathToProp] + if (_.has(device, pathToProp) && value !== _.get(device, pathToProp)) { + value = valueIfDifferent + postfix = '' + } + let reducedValue = {} + _.set(reducedValue, pathToProp, value) + return reducedValue + }) + + let aggregatedValue = _.get(reducedDevice, pathToProp) + + if (postfix) { + aggregatedValue += postfix + } + return aggregatedValue + } + + // TODO move to resource-list.directive + this.getRangeOfPropertyOfSelected = (selectedDevices = [], pathToProp, filter = (a) => a) => { + let min = 0 + let max = 0 + selectedDevices.forEach(device => { + if (_.has(device, pathToProp) && !min || _.get(device, pathToProp) < min) { + min = _.get(device, pathToProp) + } + if (_.has(device, pathToProp) && _.get(device, pathToProp) > max) { + max = _.get(device, pathToProp) + } + }) + if (min === max) { + return filter(min) + } + return filter(min) + ' - ' + filter(max) + } + + // TODO move to resource-list.directive + this.getAggregatedSetOfSelected = (selectedDevices = [], pathToProp, valueIdProp) => { + let set = {} + selectedDevices.forEach(device => { + _.get(device, pathToProp, []).forEach(value => { + let id = value[valueIdProp] + set[id] = value + }) + }) + return _.values(set) + } + + /** + * Performs common tasks after adding/removing resources on both lists. + * @private + */ + let _control = () => { + _.invokeMap(callbacksForSelections, _.call, null, lots) + } + } +} + +module.exports = ResourceListSelector diff --git a/app/common/components/resource/resource-list/resource-list.directive.html b/app/common/components/resource/resource-list/resource-list.directive.html index 3e4984ff..13dfa9b4 100644 --- a/app/common/components/resource/resource-list/resource-list.directive.html +++ b/app/common/components/resource/resource-list/resource-list.directive.html @@ -1,50 +1,56 @@ - -
- -
- - -
- {{selector.total.length}} items selected: -
-
- -
-
-
-
-
+
+ + + +

Lots ({{ getTotalNumberOfLots() }})

+ + +

Devices ({{ getTotalNumberOfDevices() }})

+ + +
- +
- - - -
- - -
- -
{{value | timeAgo}}
- - -
@@ -130,9 +106,81 @@
- -
diff --git a/app/common/components/resource/resource-list/resource-list.directive.js b/app/common/components/resource/resource-list/resource-list.directive.js index 0232c88c..7e82ae54 100644 --- a/app/common/components/resource/resource-list/resource-list.directive.js +++ b/app/common/components/resource/resource-list/resource-list.directive.js @@ -10,131 +10,310 @@ * @param {ResourceBreadcrumb} ResourceBreadcrumb * @param {Session} session */ -function resourceList (resourceListConfig, ResourceListGetter, ResourceListGetterBig, ResourceListSelector, - ResourceListSelectorBig, ResourceSettings, progressBar, ResourceBreadcrumb, session) { - const utils = require('./../../utils.js') - const PARENT_PATH = require('./../__init__').PATH +function resourceList (resourceListConfig, ResourceListGetter, ResourceListSelector, ResourceSettings, progressBar, ResourceBreadcrumb, session, UNIT_CODES, CONSTANTS, SearchService, $filter) { + const PATH = require('./__init__').PATH const NoMorePagesAvailableException = require('./no-more-pages-available.exception') + const selectionSummaryTemplateFolder = PATH + '/resource-list-selection-summary' return { template: require('./resource-list.directive.html'), restrict: 'E', scope: { // The parent resource. If it does not have @type, then we are the list of the main inventory view. - parentResource: '=?', - resourceType: '@', // The type of resource this list is representing. - type: '@' // Type of resource-view this list is in: big, medium, small + parentResource: '=?' }, link: { // Note that we load on 'pre' to initialize before our child (or inner) directives so they get real config values pre: ($scope, element) => { + $scope.utils = require('./../../utils.js') $scope.session = session - const resourceType = $scope.resourceType - $scope.resourceName = utils.Naming.resource(resourceType) - if (!resourceType) throw TypeError('resourceList needs a "resourceType" set, not ' + resourceType) - if (!$scope.type) throw TypeError('resourceLists needs a "type" to be "big"|"medium"|"small", not ' + $scope.type) - progressBar.start() // resourceListGetter.getResources will call this too, but doing it here we avoid delay - const config = _.cloneDeep(resourceListConfig.views[resourceType]) - if (_.isUndefined(config)) throw ReferenceError(resourceType + ' has no config.') - $scope.resources = [] // Do never directly assign (r=[]) to 'resources' as modules depend of its reference + progressBar.start() // getterDevices.getResources will call this too, but doing it here we avoid delay + const config = _.cloneDeep(resourceListConfig) + $scope.devices = [] // Do never directly assign (r=[]) to 'devices' as modules depend of its reference + $scope.lots = [] + /** - * Object to handle accessing sub resources. - * @prop {object} resource - The resource + * Gets into the resource; traverse one step into the resource hierarchy by opening the resource in the + * main window. + * @param {Object} lot - Minimum properties are @type and _id */ - const subResource = $scope.subResource = { - resource: null, // The opened subResource - isOpened: resourceId => { - return resourceId === _.get(subResource.resource, '_id') - }, - /** - * Open / close the right window. - * @param {Object} resource - */ - toggle: resource => { - subResource.resource = subResource.isOpened(resource['_id']) ? null : resource - triggerCollapse() - }, - /** - * Closes a resource if it was the opened one - * @param {string} resourceId - */ - close: resourceId => { - if (_.get(subResource.resource, '_id') === resourceId) { - subResource.resource = null - triggerCollapse() - } - }, - /** - * Gets into the resource; traverse one step into the resource hierarchy by opening the resource in the - * main window. - * @param {Object} resource - Minimum properties are @type and _id - */ - openFull: resource => { - ResourceBreadcrumb.go(resource) - } + $scope.goTo = lot => { + ResourceBreadcrumb.go(lot) } + // TODO need this? // Makes the table collapsible when window resizes // Note this method is executed too in $scope.toggleDeviceView const triggerCollapse = require('./collapse-table.js')($scope) $(window).resize(triggerCollapse) - // Note that some values of $scope are sate inside the constructors of ResourceListGetter and ResourceListSelector - let resourceListGetter - let resourceListSelector - $scope.resources.length = 0 - $scope.selector = { // resourceListSelector needs the following vars - checked: false, // ng-model for the 'selectAll' checkbox - checkboxes: {}, // ng-model for the checkboxes in the list. Only for representational purposes. - // The selected resources of the actual list. Although resourceListSelector uses its own - // list, it can optionally populate this one if passed for use to use in the template - inList: [], - // The same as inList but for the total of resources through all lists - total: [] + // Set up getters and selectors for devices + const defaultFilters = ($scope.parentResource && $scope.parentResource._id) + ? { 'dh$insideLot': $scope.parentResource._id } // TODO dh$insideLot returns devices that are in specified lot OR any sublot of specified lot + : null + const getterDevices = new ResourceListGetter('Device', $scope.devices, config, progressBar, _.cloneDeep(defaultFilters)) + const selector = $scope.selector = ResourceListSelector + getterDevices.callbackOnGetting(_.bind(selector.reAddToLot, selector, _)) + $scope.getTotalNumberOfDevices = () => { + return getterDevices.getTotalNumberResources() } - if ($scope.type === 'big') { - resourceListGetter = new ResourceListGetterBig(resourceType, $scope.resources, config, progressBar) - resourceListSelector = new ResourceListSelectorBig($scope.selector, $scope.resources, resourceListGetter) - } else { - resourceListGetter = new ResourceListGetter(resourceType, $scope.resources, config, progressBar) - resourceListSelector = new ResourceListSelector($scope.selector, $scope.resources, resourceListGetter) + + // Set up getters for lots + const getterLots = new ResourceListGetter('Lot', $scope.lots, config, progressBar, _.cloneDeep(defaultFilters)) + getterLots.updateSort('label') + // Total number of devices + $scope.getTotalNumberOfLots = () => { + return getterLots.getTotalNumberResources() } - // Search - const parentType = _.get($scope, 'parentResource.@type') - if (parentType) { - // If we are the subresource of a parent, we can only show the resources that are tied somehow with - // the parent, not all resources. We do this by setting a default parameter in search - // no need to _.clone this setting as we do not modify it - const path = 'search.subResource.' + resourceType - const defaultParam = utils.getSetting(resourceListConfig.views, ResourceSettings(parentType), path) - if (!defaultParam) throw TypeError(`${parentType} does not have default param for subResource ${resourceType}`) - config.search.defaultParams[defaultParam.key] = $scope.parentResource[defaultParam.field] + // Workaround: In root, parentResource is not set. This must be after initializing ResourceListGetter + // TODO Delete workaround as soon as API returns root with label and _id set + $scope.parentResource = $scope.parentResource || { + _id: 'NoParent', + '@type': 'Lot', + label: 'Without lot' } // Sorting $scope.sort = {} - $scope.setSort = _.bind(resourceListGetter.updateSort, resourceListGetter, _) + $scope.setSort = _.bind(getterDevices.updateSort, getterDevices, _) // Filtering - $scope.onSearchParamsChanged = _.bind(resourceListGetter.updateFiltersFromSearch, resourceListGetter, _) + $scope.onSearchParamsChanged = newFilters => { + getterDevices.updateFiltersFromSearch(newFilters) + getterLots.updateFiltersFromSearch(newFilters) // TODO update lots on filter update? + } // Selecting - $scope.toggleSelectAll = _.bind(resourceListSelector.toggleSelectAll, resourceListSelector, _) - $scope.toggleSelect = _.bind(resourceListSelector.toggle, resourceListSelector, _) - $scope.toggleSelect = (resource, $event, $index) => { - resourceListSelector.toggle(resource, $event, $index) - // Avoids the ng-click from the row () to trigger - $event.stopPropagation() + $scope.toggleSelect = (resource, $index, $event) => { + $event.stopPropagation() // Avoids the ng-click from the row () to trigger + + if ($event.shiftKey) { + let lastSelectedIndex = $scope.lastSelectedIndex || 0 + let start = Math.min(lastSelectedIndex, $index) + let end = Math.max(lastSelectedIndex, $index) + let devicesToSelect = $scope.devices.slice(start, end + 1) + selector.selectAll(devicesToSelect, $scope.parentResource) + } else if ($event.ctrlKey) { + selector.toggle(resource, $scope.parentResource) + } else { + let isSelected = selector.isSelected(resource) + if (isSelected) { + if ($scope.selection.devices.length === 1) { + selector.toggle(resource, $scope.parentResource) // remove + } else { + selector.deselectAll() + selector.toggle(resource, $scope.parentResource) // add + } + } else { + selector.deselectAll() + selector.toggle(resource, $scope.parentResource) + } + } + $scope.lastSelectedIndex = $index + } + + $scope.deselectAll = (devices) => { + $scope.selector.deselectAll(devices) + $scope.lastSelectedIndex = 0 + } + + // Workaround to set labels of selected lots correctly. Necessary because API /devices doesn't include the 'label' property for device ancestors + // TODO remove as soon as API returns ancestor lots with labels set + $scope.parentResource && selector.nameLot($scope.parentResource) + + // components, price and condition score (Must be above updateSelection) + // const manufacturerSettings = ResourceSettings('Manufacturer') + const deviceSettings = ResourceSettings('Device') + $scope.currencyOptions = { + currency: CONSTANTS.currency, + val: 'standard', + roles: ['retailer', 'platform', 'refurbisher'], + filter: (amount) => { + return $filter('currency')(amount, CONSTANTS.currency) + } } + $scope.hasExplicitPerms = session.hasExplicitPerms() + $scope.hardDriveSizeUnit = UNIT_CODES[deviceSettings.schema.totalHardDriveSize.unitCode] + $scope.ramSizeUnit = UNIT_CODES[deviceSettings.schema.totalRamSize.unitCode] + $scope.appearance = deviceSettings.schema.condition.schema.appearance.schema.general.allowed_description + $scope.functionality = deviceSettings.schema.condition.schema.functionality.schema.general.allowed_description + // const where = {parent: $scope.resource._id, '@type': {'$in': ['GraphicCard', 'Processor']}} + // deviceSettings.server.getList({where: where}).then(components => { + // $scope.graphicCard = _.find(components, {'@type': 'GraphicCard'}) + // const cpu = _.find(components, {'@type': 'Processor'}) + // if (cpu) { + // manufacturerSettings.server.findText(['label'], cpu.manufacturer.split(' ')[0], true, 1).then(manu => { + // if (manu.length) { + // $scope.processorManufacturer = manu[0] + // } + // }) + // } + // }) + + function updateSelection () { + $scope.selection = $scope.selection || {} + + let selectedDevices = $scope.selection.devices = selector.getAllSelectedDevices().slice() + $scope.selection.multiSelection = $scope.selection.devices.length > 1 + $scope.selection.lots = selector.getLots().slice() + + // mark current lot + let currentLot = _.find($scope.selection.lots, { _id: $scope.parentResource._id }) + if (currentLot) { + currentLot.current = true + } + + // selected lots pills + $scope.selection.numSelectedLotsShown = 3 // TODO get from config + $scope.selection.showDisplayMoreSelectedLotsButton = + $scope.selection.devices.length > $scope.selection.numSelectedLotsShown + $scope.selection.showMoreSelectedLots = () => { + $scope.selection.details.Lots = true + } + + // select/deselect button + $scope.selection.areAllDevicesOfCurrentLotSelected = _.difference( + $scope.devices, + selectedDevices, + (a, b) => { + return a._id === b._id + } + ).length === 0 + + // aggregated properties of selected devices + let props = { + type: selector.getAggregatedPropertyOfSelected(selectedDevices, '@type'), + subType: selector.getAggregatedPropertyOfSelected(selectedDevices, 'type'), + manufacturer: selector.getAggregatedPropertyOfSelected(selectedDevices, 'manufacturer'), + model: selector.getAggregatedPropertyOfSelected(selectedDevices, 'model'), + serialNumber: selector.getAggregatedPropertyOfSelected(selectedDevices, 'serialNumber', 'Various serial numbers'), + hid: selector.getAggregatedPropertyOfSelected(selectedDevices, 'hid', 'Various hids'), + status: selector.getAggregatedPropertyOfSelected(selectedDevices, 'status'), + condition: { + appearance: { + general: selector.getRangeOfPropertyOfSelected(selectedDevices, 'condition.appearance.general') + }, + functionality: { + general: selector.getRangeOfPropertyOfSelected(selectedDevices, 'condition.functionality.general') + }, + general: { + range: selector.getAggregatedPropertyOfSelected(selectedDevices, 'condition.general.range') + } + }, + components: { + processorModel: selector.getAggregatedPropertyOfSelected(selectedDevices, 'processorModel'), + totalHardDriveSize: selector.getAggregatedPropertyOfSelected(selectedDevices, 'totalHardDriveSize', 'Various', ' GB HardDrive'), + totalRamSize: selector.getAggregatedPropertyOfSelected(selectedDevices, 'totalRamSize', 'Various', ' MB RAM') + }, + events: selector.getAggregatedSetOfSelected(selectedDevices, 'events', '_id'), + lots: $scope.selection.lots + } + $scope.currencyOptions.roles.forEach((roleName) => { + let path = 'pricing.' + roleName + '.' + $scope.currencyOptions.val + _.set(props, + path + '.percentage', + selector.getRangeOfPropertyOfSelected(selectedDevices, path + '.percentage', (percentage) => { + return $filter('percentage')(percentage) + }) + ) + _.set(props, + path + '.amount', + selector.getRangeOfPropertyOfSelected(selectedDevices, path + '.amount', $scope.currencyOptions.filter) + ) + }) + let path = 'pricing.total.' + $scope.currencyOptions.val + _.set(props, + path, + selector.getRangeOfPropertyOfSelected(selectedDevices, path, $scope.currencyOptions.filter) + ) + $scope.selection.props = props + + // Used to determine which details pane (e.g. type, components, events, ...) to show + $scope.selection.details = {} + + // Summary for selection + let typeContentSummary + if (props.type === selector.VARIOUS) { + typeContentSummary = 'Various types' + } else if (props.type === 'Device') { + typeContentSummary = 'Placeholder' + } else if (props.subType === selector.VARIOUS) { + typeContentSummary = props.type + ' Various subtypes' + } else if (props.manufacturer === selector.VARIOUS) { + typeContentSummary = props.subType + ' Various manufacturers' + } else if (props.model === selector.VARIOUS) { + typeContentSummary = props.subType + ' ' + props.manufacturer + ' Various models' + } else { + typeContentSummary = props.subType + ' ' + props.manufacturer + ' ' + props.model + } + $scope.selection.summary = [ + { + title: 'Type, manufacturer & model', + contentSummary: typeContentSummary, + cssClass: 'type', + templateUrl: selectionSummaryTemplateFolder + '/type.html' + }, + { + title: 'Status', + contentSummary: props.status, + cssClass: 'status', + templateUrl: selectionSummaryTemplateFolder + '/status.html' + }, + { + title: 'Price', + contentSummary: props.pricing.total[$scope.currencyOptions.val], + cssClass: 'price', + templateUrl: selectionSummaryTemplateFolder + '/price.html' + }, + { + title: 'Condition score', + contentSummary: props.condition.general.range, + cssClass: 'condition-score', + templateUrl: selectionSummaryTemplateFolder + '/condition-score.html' + }, + { + title: 'Components', + contentSummary: _.values(props.components).join(' '), + cssClass: 'components', + templateUrl: selectionSummaryTemplateFolder + '/components.html' + }, + // { + // title: 'Providers', + // contentSummary: 'Donor:' + selector.getAggregatedPropertyOfSelected(allSelectedDevices, 'donor') || 'No donor' + + // 'Owner:' + selector.getAggregatedPropertyOfSelected(allSelectedDevices, 'donor') || 'No owner' + + // 'Distributor:' + selector.getAggregatedPropertyOfSelected(allSelectedDevices, 'distributor') || 'No distributor', + // content: 'Providers' + // }, + { + title: 'Events', + contentSummary: props.events.length + ' events', + cssClass: 'events', + templateUrl: selectionSummaryTemplateFolder + '/events.html' + }, + { + title: 'Lots', + contentSummary: props.lots.length + ' lots', + cssClass: 'lots', + templateUrl: selectionSummaryTemplateFolder + '/lots.html' + } + ] + } + selector.callbackOnSelection(updateSelection) + updateSelection() // Reloading // When a button succeeds in submitting info and the list needs to be reloaded in order to get the updates $scope.reload = () => { - resourceListGetter.getResources() - resourceListSelector.deselectAll() + getterDevices.getResources() + $scope.deselectAll() + } + // TODO need this? + function hardReload () { + $scope.deselectAll() + $scope.reload() } - // Pagination + // Pagination Devices // Let's avoid the user pressing multiple times the 'load more' $scope.getMoreIsBusy = false $scope.morePagesAvailable = true @@ -143,7 +322,7 @@ function resourceList (resourceListConfig, ResourceListGetter, ResourceListGette if (!$scope.getMoreIsBusy && getMoreFirstTime) { $scope.getMoreIsBusy = true try { - resourceListGetter.getResources(true, false).finally(() => { $scope.getMoreIsBusy = false }) + getterDevices.getResources(true, false).finally(() => { $scope.getMoreIsBusy = false }) } catch (err) { $scope.getMoreIsBusy = false if (!(err instanceof NoMorePagesAvailableException)) throw err @@ -152,61 +331,60 @@ function resourceList (resourceListConfig, ResourceListGetter, ResourceListGette } getMoreFirstTime = true } + + // Pagination lots + // Let's avoid the user pressing multiple times the 'load more' + $scope.getMoreLotsIsBusy = false + $scope.morePagesAvailableLots = true + $scope.getMoreLots = () => { + if (!$scope.getMoreLotsIsBusy) { + $scope.getMoreLotsIsBusy = true + try { + getterLots.getResources(true, false).finally(() => { + $scope.getMoreLotsIsBusy = false + }) + } catch (err) { + $scope.getMoreLotsIsBusy = false + if (!(err instanceof NoMorePagesAvailableException)) throw err + $scope.morePagesAvailableLots = false + } + } + } + // If we don't want to collision with tables of subResources we // need to do this when declaring the directive + // TODO need this? const $table = element.find('.fill-height-bar') - resourceListGetter.callbackOnGetting((_, __, ___, getNextPage) => { + getterDevices.callbackOnGetting((_, __, ___, getNextPage) => { if (!getNextPage) { $table.scrollTop(0) // Scroll up to the table when loading from page 0 again $scope.morePagesAvailable = true // Reset } }) - // Popover is set on the left or bottom depending screen size (xs) - const computePlacement = () => $(window).width() <= 768 ? 'bottom-left' : 'auto left' - $scope.popoverPlacement = computePlacement() - $(window).resize(() => { - const placement = computePlacement() - if (placement !== $scope.popoverPlacement) $scope.$evalAsync(() => { $scope.popoverPlacement = placement }) - }) - - $scope.popovers = {enable: false} - if ($scope.type === 'medium') { - resourceListGetter.callbackOnGetting(resources => { - $scope.popovers.enable = true - $scope.popovers.templateUrl = PARENT_PATH + '/resource-button/resource-button.popover.directive.html' - _.forEach(resources, resource => { - $scope.popovers[resource._id] = { - isOpen: false, - title: utils.getResourceTitle(resource) - } - // Extra costly todo find better way - $scope.$watch(() => $scope.popovers[resource._id].isOpen, isOpen => { - if (isOpen === false) { - $scope.subResource.close(resource._id) - } else if (isOpen === true) { // Let's close other popovers - _.forEach(resources, _resource => { - if (_resource._id !== resource._id) $scope.popovers[_resource._id].isOpen = false - }) - } - }) - }) + // Sets filter to show devices of given event + $scope.showDevicesOfEvent = (event) => { + ResourceBreadcrumb.goToRoot().then(() => { + let searchParam = { + key: 'event_id', + name: 'Has event', + // $$hashKey: 'object:7368', + description: 'Match only devices that have a specific event.', + placeholder: 'ID of event' + } + let value = event._id + SearchService.addSearchParameter(searchParam, value) }) } - function hardReload () { - $scope.checked = false - resourceListSelector.deselectAll() - $scope.reload() - } + // TODO what does next line? + ResourceSettings('Device').types.forEach(type => { $scope.$on('submitted@' + type, hardReload) }) - if ($scope.type === 'big') { - ResourceSettings(resourceType).types.forEach(type => { $scope.$on('submitted@' + type, hardReload) }) - // We register ourselves for any event type, excluding Snapshot if the list is not about devices - let eventTypes = ResourceSettings('Event').subResourcesNames - if (resourceType !== 'Device') eventTypes = _.without(eventTypes, 'devices:Snapshot', 'devices:Register') - _.forEach(eventTypes, eventType => { $scope.$on('submitted@' + eventType, hardReload) }) - } + // We register ourselves for any event type, excluding Snapshot if the list is not about devices + let eventTypes = ResourceSettings('Event').subResourcesNames + // TODO do we need next line? resourceType is always 'Device' for now + // if (resourceType !== 'Device') eventTypes = _.without(eventTypes, 'devices:Snapshot', 'devices:Register') + _.forEach(eventTypes, eventType => { $scope.$on('submitted@' + eventType, hardReload) }) // As we touch config in the init, we add it to $scope at the end to avoid $watch triggering multiple times $scope.config = config diff --git a/app/common/components/resource/resource-list/resource-list.directive.sass b/app/common/components/resource/resource-list/resource-list.directive.sass index a5d14495..4978662c 100644 --- a/app/common/components/resource/resource-list/resource-list.directive.sass +++ b/app/common/components/resource/resource-list/resource-list.directive.sass @@ -1,6 +1,45 @@ @import '_resource-list.directive' -resource-list +.breadcrumb + padding: 6px 0 + font-size: 22px + margin-bottom: 0 + background-color: transparent + group-resource-button + float: right + margin-left: 10px + .btn-primary + color: #767676 + background-color: #ffffff + border-color: #a8a8a8 + padding: 6px + border-radius: 3px + + resource-field-edit + form + display: inline-block + width: inherit + .buttons + margin-left: 10px + margin-right: 10px + li + li:before + padding: 0 5px 0 15px + resource-icon + font-size: 1em + line-height: inherit + a + color: grey + text-decoration: none + &:hover + text-decoration: none + +.resource-list + background-color: #f4f4f4 + padding: 15px + + .current-lot + padding: 15px 30px 30px 30px + #screener display: inline-block position: relative @@ -15,6 +54,8 @@ resource-list max-width: 2em #buttons + margin-top: 30px + text-align: right .btn padding-top: 0.25em padding-bottom: 0.25em @@ -25,57 +66,243 @@ resource-list outline-offset: -2px .fill-height-bar - height: calc(100vh - #{$navbar-height} - #{$breadcrumb-height} - #{$footerHeight} - #{$tabsHeight} - #{$optionsMenuHeight} - #{$searchHeight}) - @media (max-width: $screen-xs-max) - height: calc(100vh - #{$navbar-height} - #{$breadcrumb-height} - #{$footerHeight} - #{$tabsHeight}) + height: calc(100vh - #{$navbar-height} - #{$footerHeight} - #{$optionsMenuHeight}) overflow-y: auto + @media (max-width: $screen-xs-max) + height: calc(100vh - #{$navbar-height} - #{$footerHeight}) + + .resource-list-table + &.table-hover + tbody + tr + &:hover + background-color: #85bfcf + color: white + tr + * + &:first-child + white-space: nowrap + + .resource-list-checkbox + zoom: 1.5 + -moz-transform: scale(1.5) + + td + user-select: none + cursor: pointer + + th + padding-top: 0 !important + .label + display: block + +.selection + padding: 30px + border-left: 2px solid $main-nav-bg + box-shadow: -5px -3px 10px -7px #999 + background-color: #ffffff + ul + padding: 0 + +.children-lots + ul + padding: 0 + li + list-style: none + display: inline-flex + border-radius: 4px + padding: 5px + border: 1px solid + margin-bottom: 6px + background-color: white + &:not(:last-child) + margin-right: 6px + a + color: #333333 + text-decoration: none + + +.pill-lot + list-style: none + border-radius: 4px + padding: 5px + border: 1px solid #666666 + margin-bottom: 6px + background-color: white + cursor: pointer + button + background-color: transparent + padding: 0 + &.btn:hover, &.btn:focus, &.btn.focus + color: #333333 + &:not(:last-child) + margin-right: 6px + +.pill-device-flex + display: inline-flex + +.pill-device + list-style: none + padding: 5px + margin-bottom: 6px + background-color: white + cursor: pointer + button + background-color: transparent + padding: 0 + &.btn:hover, &.btn:focus, &.btn.focus + color: #333333 + &:not(:last-child) + margin-right: 6px + +.pill-lot-flex + display: inline-flex + +.selection-summary + margin-bottom: 0 + li + list-style: none + outline: none !important + &:first-child > button + border-top-left-radius: 5px + border-top-right-radius: 5px + &:last-child > button + border-bottom-left-radius: 5px + border-bottom-right-radius: 5px + &:not(:first-child) > button + border-top: none + button + background-color: white + color: #333333 + box-shadow: none + border: 1px solid #bfbfbf + + width: 100% + text-align: left + &.btn:hover, &.btn:focus, &.btn.focus, &.btn:active + box-shadow: none + outline: none + background-color: #449bb4 + border-color: white + color: white + .fa + display: block + color: white + .selection-summary-title + display: block + .selection-summary-content + font-size: 18px + .fa + float: right + font-size: 22px + display: none + + + +.selection-details-panel + border: 1px solid #bfbfbf + border-radius: 5px + position: absolute + top: 0 + width: 100% + height: 100% + background-color: white + left: 100% + transition: all 700ms + //overflow-x: hidden + //overflow-y: scroll + &.active + left: 0 + .selection-details-content-wrapper + position: relative + padding: 0 + width: 100% + height: 100% + .selection-details-content-container + position: absolute + top: 0 + width: 100% + height: 100% + padding: 0 + overflow-x: hidden + .components > .table + max-width: 650px + .condition-score + td:last-child span + display: block + .lots + .selected-lots-list + padding-top: 20px + &.multi-selection + .pill-lot-container + background-color: #e8e8e8 + border-radius: 7px + &:not(:last-child) + margin-bottom: 20px + .selected-lot + width: 75% + padding-bottom: 10px + margin-bottom: 12px + border-bottom: 1px #a6a6a6 solid + .pill-lot-container + list-style: none + padding: 8px + .selected-lot + a + color: #333333 + &:hover + color: #333333 + button + background-color: white + margin-left: 10px + border-color: grey + padding: 7px + &:hover + color: #333333 + .selected-devices-in-lot + .device + list-style: none + display: + padding: 5px + &:not(:last-child) + margin-right: 6px + .type + .descriptor + margin-bottom: 30px + .status-selector + display: -webkit-box + display: -webkit-flex + display: -ms-flexbox + display: flex + flex-wrap: wrap + margin-top: 20px + .btn + height: 100% + text-align: center + align-items: center + display: -webkit-box + display: -webkit-flex + display: -ms-flexbox + display: flex + justify-content: center + background-color: #ffffff + border-radius: 5px + border: 1px solid grey + h4 + font-size: 22px + &.btn:hover, &.btn:focus, &.btn.focus + color: #333333 + .selection-details-content + padding: 15px + .btn-hide-details + float: left + padding: 4px 10px + margin-right: 5px + border-radius: 5px + color: #333333 + &.btn:hover, &.btn:focus, &.btn.focus, &.btn:active + color: #333333 + + + - #resource-list-table tr - * - &:first-child - white-space: nowrap - - .resource-list-checkbox - zoom: 1.5 - -moz-transform: scale(1.5) - - td - user-select: none - cursor: pointer - - th - padding-top: 0 !important - .label - display: block - - #aside-subResource - padding-left: 0 - border-left: 2px solid $main-nav-bg - box-shadow: -5px -3px 10px -7px #999 - resource-view - .fill-height-bar // a subview with a search bar (ex. any resource-list) - // - 2 * (everything we had in .fill-height-bar above) but calc does not allow 2 * (parenthesis) so we get it by - // breadcrumb is represented by $line-height-computed - $breadcrumb-padding-vertical - // applying the distributive property - height: calc(100vh - #{$navbar-height} - #{$breadcrumb-height} - #{$footerHeight} - 2 * #{$optionsMenuHeight} - #{$tabsMenuHeight} - #{$tabsHeight} - 2 * #{$searchHeight} - #{$subresourceH3Height}) !important - overflow-y: auto - @media (max-width: $screen-xs-max) - height: calc(100vh - #{$navbar-height} - #{$breadcrumb-height} - #{$footerHeight} - #{$optionsMenuHeight} - #{$tabsMenuHeight} - 1 * #{$searchHeight} - #{$subresourceH3Height}) !important - - .fill-height // A subview without a search bar nor options buttons (ex. dashboard) - // We use fill-height-bar as initial, as only listing have subresources - height: calc(100vh - #{$navbar-height} - #{$breadcrumb-height} - #{$footerHeight} - #{$tabsHeight} - #{$tabsMenuHeight} - #{$searchHeight} - #{$subresourceH3Height}) !important - overflow-y: auto - - @media (max-width: $screen-xs-max) - height: calc(100vh - #{$navbar-height} - #{$breadcrumb-height} - #{$footerHeight} - #{$tabsMenuHeight} - 1 * #{$tabsHeight} - #{$subresourceH3Height}) !important - - header - background-color: $main-nav-bg - h3 - margin-top: 0 - margin-bottom: 0 - padding-top: $line-height-computed / 4 - -#device-dashboard-components td:last-child span - display: block diff --git a/app/common/components/resource/resource-search/angular-advanced-searchbox-ereuse.js b/app/common/components/resource/resource-search/angular-advanced-searchbox-ereuse.js index 1fbbe1a0..865c535e 100644 --- a/app/common/components/resource/resource-search/angular-advanced-searchbox-ereuse.js +++ b/app/common/components/resource/resource-search/angular-advanced-searchbox-ereuse.js @@ -12,7 +12,7 @@ (function () { angular.module('angular-advanced-searchbox', []) - .directive('nitAdvancedSearchbox', function (ResourceSettings) { + .directive('nitAdvancedSearchbox', function (ResourceSettings, SearchService) { return { restrict: 'E', scope: { @@ -90,6 +90,11 @@ // TODO: hide used suggestion } + function parametersAdded(param, value) { + $scope.addSearchParam(param, value) + } + SearchService.callbackOnSearchUpdate(parametersAdded) + $scope.typeaheadCanRemove = function (e) { return e.keyCode == 8 && e.target.value.length == 0 } diff --git a/app/common/components/resource/resource-search/index.js b/app/common/components/resource/resource-search/index.js index 313bcea4..455d6af0 100644 --- a/app/common/components/resource/resource-search/index.js +++ b/app/common/components/resource/resource-search/index.js @@ -6,5 +6,6 @@ module.exports = angular.module('common.components.resource.resource-search', require('./../../../config').name, 'angular-advanced-searchbox' ]) -.directive('resourceSearch', require('./resource-search.directive')) + .service('SearchService', require('./search.service')) + .directive('resourceSearch', require('./resource-search.directive')) .constant('RESOURCE_SEARCH', require('./resource-search.constant')) diff --git a/app/common/components/resource/resource-search/resource-search.directive.js b/app/common/components/resource/resource-search/resource-search.directive.js index ba3f8ac9..83791015 100644 --- a/app/common/components/resource/resource-search/resource-search.directive.js +++ b/app/common/components/resource/resource-search/resource-search.directive.js @@ -16,6 +16,8 @@ function resourceSearch (ResourceSettings) { $scope._settings = $scope.settings } $scope.params = _.clone($scope.defaultParams) + + // trigger first resource load $scope.onParamsChanged({params: $scope.params}) $scope.$watchCollection('params', function checkForChangeAndPropagate (params, oldParams) { diff --git a/app/common/components/resource/resource-search/search.service.js b/app/common/components/resource/resource-search/search.service.js new file mode 100644 index 00000000..fd13405e --- /dev/null +++ b/app/common/components/resource/resource-search/search.service.js @@ -0,0 +1,15 @@ +class DeviceSearchService { + constructor () { + let callbacksForSearchUpdate = [] + + this.addSearchParameter = (parameter, value) => { + _.invokeMap(callbacksForSearchUpdate, _.call, null, parameter, value) + } + + this.callbackOnSearchUpdate = callback => { + callbacksForSearchUpdate.push(callback) + } + } +} + +module.exports = DeviceSearchService diff --git a/app/common/components/resource/resource-settings.factory.js b/app/common/components/resource/resource-settings.factory.js index fb4a718d..80f4c03f 100644 --- a/app/common/components/resource/resource-settings.factory.js +++ b/app/common/components/resource/resource-settings.factory.js @@ -57,13 +57,14 @@ function resourceSettingsFactory (ResourceServer, schema, RESOURCE_CONFIG) { this.subResourcesNames = _.without(this.types, this.type) this.isALeaf = this.subResourcesNames.length === 0 || this.type === 'devices:EraseBasic' // todo so works redo // Get which is our root ancestor - if (this.settings._root) { // We avoid infinite recursive calls by not calling isSubResource if we are root - this.rootAncestor = this - } else { - const picked = _.pickBy(RESOURCE_CONFIG.resources, r => r._root) // Can't chain findKey after pickBy - const rootAncestorType = _.findKey(picked, (_, resourceType) => this.isSubResource(resourceType)) - this.rootAncestor = _ResourceSettingsFactory(rootAncestorType) - } + // TODO do we need root ancestor / sub resource? + // if (this.settings._root) { // We avoid infinite recursive calls by not calling isSubResource if we are root + this.rootAncestor = this + // } else { + // const picked = _.pickBy(RESOURCE_CONFIG.resources, r => r._root) // Can't chain findKey after pickBy + // const rootAncestorType = _.findKey(picked, (_, resourceType) => this.isSubResource(resourceType)) + // this.rootAncestor = _ResourceSettingsFactory(rootAncestorType) + // } } } @@ -117,6 +118,9 @@ function resourceSettingsFactory (ResourceServer, schema, RESOURCE_CONFIG) { * @private */ function _ResourceSettingsFactory (type) { + if (type === undefined) { + throw new Error('cannot get resource settings for given type. type is undefined') + } if (!(type in resourceTypes)) resourceTypes[type] = new ResourceSettings(type) return resourceTypes[type] } diff --git a/app/common/components/resource/resource-view/resource-view.directive.html b/app/common/components/resource/resource-view/resource-view.directive.html index 6fbc075b..6c1aaa15 100644 --- a/app/common/components/resource/resource-view/resource-view.directive.html +++ b/app/common/components/resource/resource-view/resource-view.directive.html @@ -1,25 +1,10 @@
-
-

- - {{resource[t.key] | humanize}} - {{resource[t.key]}} - - - {{resource[s.key] | humanize}} - {{resource[s.key]}} - - - - -

-
-
-
-
 
- +
+ +
diff --git a/app/common/components/resource/resource-view/resource-view.directive.js b/app/common/components/resource/resource-view/resource-view.directive.js index d0952dbb..03886dfd 100644 --- a/app/common/components/resource/resource-view/resource-view.directive.js +++ b/app/common/components/resource/resource-view/resource-view.directive.js @@ -1,10 +1,5 @@ -function resourceView (RecursionHelper, Subview, cerberusToView, RESOURCE_CONFIG, ResourceBreadcrumb, ResourceSettings, - $compile) { - const utils = require('./../../utils') - const BIG = 'big' - const MED = 'medium' - const SM = 'small' - const TYPES = [BIG, MED, SM] +// TODO Deprecated +function resourceView () { /** * * Represents a resource. Selects the appropriate view to show the resource, depending of its type. @@ -17,80 +12,6 @@ function resourceView (RecursionHelper, Subview, cerberusToView, RESOURCE_CONFIG scope: { resource: '=?', type: '@' - }, - compile: element => { - return RecursionHelper.compile(element, ($scope, iElement) => { - let tabs // We keep a reference of the tabs to be able to $destroy and replace them when changing resource - if (!_.includes(TYPES, $scope.type)) { - throw TypeError('ResourceView only accepts big, medium and small as types.') - } - - $scope.setActive = tabToActivate => { - _.forOwn($scope.tabs, tab => { tab.isActive = false }) - tabToActivate.isActive = true - } - - $scope.$on('changeTab', () => $scope.setActive()) - - $scope.$watch('resource._id', (newResourceId, oldResourceId) => { - // We create the tabs with the subviews embedded - // Note that resourceView is re-used to hold different types of resources at different times - if (newResourceId !== oldResourceId) generateViewAndSubview() - }) - - $scope.goTo = resource => ResourceBreadcrumb.go(resource) - - function generateViewAndSubview () { - if ($scope.type === SM) { - $scope.srefUiParams = { - resourceName: utils.Naming.resource($scope.resource['@type']), - id: $scope.resource._id - } - // todo integrate this 3 lines with Subview class - const rSettings = ResourceSettings($scope.resource['@type']) - let view = angular.element(Subview.view(rSettings.getSetting('subviewSmall'))) - $compile(view)($scope) - if (!_.isEmpty($scope.resource)) $scope.model = cerberusToView.parse($scope.resource) - iElement.find('#resource-view-body').html(view) - } else { - // first at all ensure variables in $scope subviews will use are ready - if (!_.isEmpty($scope.resource)) $scope.model = cerberusToView.parse($scope.resource) - // Gets the info for the view - const resourceType = _.get($scope, 'resource.@type') - if (!_.isUndefined(resourceType)) $scope.view = Subview.getSetting(resourceType, 'view') - // Generates the subviews - $scope.tabs = {} // Stores for each tab if it is the active one - $scope.tabs.active = 0 // Let's make the first tab the active one initially - // We need to manually destory the tabs and their children because we are replacing them through - // jquery (.html() function) and not through an angular way - if (tabs) tabs.isolateScope().$destroy() - tabs = Subview.generate($scope, resourceType) - iElement.find('#resource-view-body').html(tabs) - // If small we use the resourceName - - if ($scope.type === BIG) { - const subviewsConfig = resourceType - ? Subview.getSetting(resourceType, 'subviews') - : RESOURCE_CONFIG.inventory.subviews - const num = subviewsConfig.length - const AVOID_INPUTS = new Set(['text', 'textarea', 'number', 'email', 'month', 'password', 'tel', 'time', - 'url', 'date', 'datetime-local', 'search']) // We want to e.has to be fast - /** - * Sets the subviews tabs by pressing a number from the keyboard. - */ - const keyPress = e => { - if (!AVOID_INPUTS.has(e.target.type) && _.inRange(e.which, 49, 49 + num)) { // 49 is the keyCode for '1' - $scope.$evalAsync(() => { $scope.tabs.active = e.which - 49 }) - } - } - $(document).keypress(keyPress) - $scope.$on('$destroy', () => $(document).off('keypress', null, keyPress)) - } - } - } - - generateViewAndSubview() - }) } } } diff --git a/app/common/components/resource/resource-view/subview.factory.js b/app/common/components/resource/resource-view/subview.factory.js deleted file mode 100644 index ba66b61a..00000000 --- a/app/common/components/resource/resource-view/subview.factory.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * - * @param {ResourceSettings} ResourceSettings - * @param RESOURCE_CONFIG - * @param {ResourceViewGenerator} ResourceViewGeneratorFactory - * @returns {Subview} - * @constructor - */ -function SubviewFactory (ResourceViewGenerator, RESOURCE_CONFIG) { - class Subview extends ResourceViewGenerator { - - /** - * Generates a subview - * @param resourceType - * @param $scope - * @returns {Array} - */ - static generate ($scope, resourceType = null) { - let subviewsConfig = resourceType ? this.getSetting(resourceType, 'subviews') : RESOURCE_CONFIG.inventory.subviews - return this.compile(subviewsConfig, $scope) - } - - static _template (subviewsConfig, $scope) { - let body = `` - _.forEach(subviewsConfig, (option) => { - let uid = option.view + option.resourceType - $scope.tabs[uid] = {isActive: false} - let icon = option.fa - ? _.map(_.isArray(option.fa) ? option.fa : [option.fa], fa => ``).join('') - : `` - body += ` - - - ${icon} - ${option.name} - -
-
${this.view(option)}
-
-
- ` - }) - return body + '
' - } - - static view (option) { - return ` - <${option.view} resource-type="${option.resourceType}" - type="{{type}}" - parent-resource="resource" - resource="resource" - model="model" - class="${option.class} col-xs-12" - > - - ` - } - - } - - return Subview -} - -module.exports = SubviewFactory diff --git a/app/common/components/resource/resource/resource.directive.html b/app/common/components/resource/resource/resource.directive.html index 4c0a368d..c5ca7f2c 100644 --- a/app/common/components/resource/resource/resource.directive.html +++ b/app/common/components/resource/resource/resource.directive.html @@ -2,4 +2,5 @@ We will only pass the resource after getting it from server (the ng-if) this is needed when entering directly to a view (viewing a resource by entering through its URL) --> - + + diff --git a/app/common/components/resource/resource/resource.directive.js b/app/common/components/resource/resource/resource.directive.js index 692f58c1..aa08ec74 100644 --- a/app/common/components/resource/resource/resource.directive.js +++ b/app/common/components/resource/resource/resource.directive.js @@ -1,6 +1,7 @@ /** * Loads and shows a resource */ +// eslint-disable-next-line no-unused-vars function resource (RecursionHelper, ResourceSettings) { return { template: require('./resource.directive.html'), @@ -12,10 +13,29 @@ function resource (RecursionHelper, ResourceSettings) { compile: element => { return RecursionHelper.compile(element, $scope => { $scope.resource = $scope.resource || {} + // const fakeResource = { + // '_updated': '2018-04-13T10:35:39', + // '_created': '2018-04-12T18:29:30', + // 'label': 'BDR', + // '@type': 'Lot', + // 'ancestors': [], + // '_id': 'NCZ0iW0mC', + // '_links': { + // 'self': {'href': 'db1/lots/n98tkSWa', 'title': 'Lot'}, + // 'collection': {'href': 'db1/lots', 'title': 'lots'}, + // 'parent': {'href': '/', 'title': 'home'} + // }, + // 'children': {'lots': ['aEduoh01']}, + // 'perms': [{'account': '5acfa595a0961e1b7f28c456', 'perm': 'r'}], + // 'byUser': '5acfa56aa0961e1b7f28c34a', + // 'sharedWith': ['5acfa595a0961e1b7f28c456'] + // } + // _.assign($scope.resource, fakeResource) // So the value it is already there + + // TODO comment in when removing fake resource let rSettings = ResourceSettings($scope.resource['@type']) - $scope.isEvent = rSettings.isSubResource('Event') // Get the resource - if (rSettings.authorized) { + if (rSettings.authorized) { // TODO why authorized setting? rSettings.server.one($scope.resource._id).get().then(resource => { _.assign($scope.resource, resource) // So the value it is already there }).catch(error => { diff --git a/app/common/constants/CONSTANTS.js b/app/common/constants/CONSTANTS.js index 48a86e3e..fb6b9e7a 100644 --- a/app/common/constants/CONSTANTS.js +++ b/app/common/constants/CONSTANTS.js @@ -1,6 +1,6 @@ const CONSTANTS = { appName: 'DeviceHub', - url: 'http://127.0.0.1:5000', + url: 'http://devicehub.ereuse.net', deviceInventoryUrl: 'https://github.com/eReuse/device-inventory/releases/latest', androidAppUrl: 'https://play.google.com/store/apps/details?id=org.ereuse.scanner', siteLogo: 'common/assets/devicehub-logo.svg', diff --git a/app/common/styles/_variables.sass b/app/common/styles/_variables.sass index 3a7ddc6a..39e496fe 100644 --- a/app/common/styles/_variables.sass +++ b/app/common/styles/_variables.sass @@ -7,6 +7,8 @@ $navbar-default-bg: $body-bg $main-nav-bg: #EFEFEF !default $icon-font-path: './fonts/' $navbar-height: 42px !default +$navbar-padding-vertical: 10px !default +$navbar-padding-horizontal: 10px !default $tabsHeight: $navbar-height !default $nav-pills-bg: lighten($main-nav-bg, 4) !default diff --git a/app/common/styles/app.sass b/app/common/styles/app.sass index 6d3a94bb..7d91d9fd 100644 --- a/app/common/styles/app.sass +++ b/app/common/styles/app.sass @@ -87,10 +87,6 @@ button[ng-click], a, small[ng-click] a.text-success:hover color: $brand-success -.breadcrumb - margin-bottom: 0 - - h3, .h3 margin-top: $line-height-computed / 2 margin-bottom: ($line-height-computed / 2) diff --git a/app/views/inventory/inventory.controller.html b/app/views/inventory/inventory.controller.html index 9f23b6a2..3cf7a50c 100644 --- a/app/views/inventory/inventory.controller.html +++ b/app/views/inventory/inventory.controller.html @@ -1,5 +1,4 @@
-