Skip to content

Commit 1b0596f

Browse files
authored
Merge branch 'master' into OLMIS-8181-doses-input-disabled
2 parents 10dd855 + 3d30842 commit 1b0596f

12 files changed

Lines changed: 227 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
Upcoming Version (WIP)
1+
7.2.17-SNAPSHOT (WIP)
2+
==================
3+
4+
7.2.16 / 2026-06-09
25
==================
36

47
Improvements:
58
* [OLMIS-8181](https://openlmis.atlassian.net/browse/OLMIS-8181): Disable doses input when net content is one.
9+
* [OLMIS-8118](https://openlmis.atlassian.net/browse/OLMIS-8118): Validate max value on positive integer inputs.
610
* [SELV3-748](https://openlmis.atlassian.net/browse/SELV3-748) Resolved cookie issue causing untranslated warning messages
11+
* [OLMIS-8192](https://openlmis.atlassian.net/browse/OLMIS-8192): Editable text inputs inside table cells now grow with their content.
12+
13+
Bug fixes:
14+
* [OLMIS-8165](https://openlmis.atlassian.net/browse/OLMIS-8165): Fix modals (React) by enabling vertical scrolling when their content exceeds the max height.
15+
716

817
7.2.15 / 2026-02-05
918
=================

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
version: "2"
22
services:
33
ui-components:
4-
image: openlmis/dev-ui:9.0.9-SNAPSHOT
4+
image: openlmis/dev-ui:9.0.8
55
ports:
66
- "9000:9000"
77
- "9876:9876"

project.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version=7.2.16-SNAPSHOT
1+
version=7.2.17-SNAPSHOT
22
projectName=OpenLMIS UI Components
33
projectKey=ui-components
44
transifexProject=openlmis-ui-components

src/common/_variables.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,6 @@ $res-xxl: 1400px;
8080

8181
$sidebar-width: 16em !default;
8282
$popover-width: 20em;
83+
84+
// Z-index
85+
$z-index-header-sticky: 1030 !default; // above content, below modals

src/openlmis-form/positive-integer.directive.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,26 @@
2323
* @name openlmis-form.directive:positiveInteger
2424
*
2525
* @description
26-
* Restricts the ngModel to only allow positive integers.
26+
* Restricts the ngModel to only allow positive integers. Values larger than the maximum safe
27+
* integer in JavaScript are rejected (the 'max' validity is set to false), because parseInt
28+
* would otherwise silently overflow and send a wrong value. When the java-integer attribute is
29+
* present the limit is lowered to the maximum Java Integer value.
2730
*
2831
* @example
2932
* Extend the input element to force it to only accept positive integers as values.
3033
* ```
3134
* <input ng-model="someModel" positive-integer>
3235
* ```
36+
* Add the java-integer attribute to additionally reject values exceeding the Java Integer range.
37+
* ```
38+
* <input ng-model="someModel" positive-integer java-integer>
39+
* ```
3340
*/
41+
var JAVA_INTEGER_MAX = Math.pow(2, 31) - 1;
42+
3443
angular
3544
.module('openlmis-form')
45+
.constant('JAVA_INTEGER_MAX', JAVA_INTEGER_MAX)
3646
.directive('positiveInteger', positiveInteger);
3747

3848
function positiveInteger() {
@@ -59,7 +69,14 @@
5969
modelCtrl.$render();
6070
}
6171

62-
return transformedInput ? parseInt(transformedInput) : null;
72+
var parsed = transformedInput ? parseInt(transformedInput) : null;
73+
74+
var maxValue = attrs.hasOwnProperty('javaInteger')
75+
? JAVA_INTEGER_MAX
76+
: Number.MAX_SAFE_INTEGER;
77+
modelCtrl.$setValidity('max', !parsed || parsed <= maxValue);
78+
79+
return parsed;
6380
});
6481
}
6582
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* This program is part of the OpenLMIS logistics management information system platform software.
3+
* Copyright © 2017 VillageReach
4+
*
5+
* This program is free software: you can redistribute it and/or modify it under the terms
6+
* of the GNU Affero General Public License as published by the Free Software Foundation, either
7+
* version 3 of the License, or (at your option) any later version.
8+
*  
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
10+
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
11+
* See the GNU Affero General Public License for more details. You should have received a copy of
12+
* the GNU Affero General Public License along with this program. If not, see
13+
* http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org. 
14+
*/
15+
describe('openlmisPositiveInteger', function() {
16+
17+
beforeEach(function() {
18+
module('openlmis-form');
19+
20+
inject(function($injector) {
21+
this.$compile = $injector.get('$compile');
22+
this.$rootScope = $injector.get('$rootScope');
23+
});
24+
25+
this.$scope = this.$rootScope.$new();
26+
27+
this.compileElement = function(extraAttrs) {
28+
this.element = this.$compile(
29+
'<form name="form_test">' +
30+
'<input name="input_test" positive-integer ' + (extraAttrs || '') +
31+
' ng-model="example"/></form>'
32+
)(this.$scope);
33+
angular.element('body').append(this.element);
34+
this.$rootScope.$apply();
35+
};
36+
37+
this.compileElement();
38+
});
39+
40+
it('should add number class', function() {
41+
expect(this.$scope.form_test.input_test.$$element.hasClass('number')).toBe(true);
42+
});
43+
44+
it('should strip non-numeric characters', function() {
45+
this.$scope.form_test.input_test.$setViewValue('12abc3');
46+
this.$scope.$digest();
47+
48+
expect(this.$scope.example).toEqual(123);
49+
});
50+
51+
it('should parse value as integer', function() {
52+
this.$scope.form_test.input_test.$setViewValue('42');
53+
this.$scope.$digest();
54+
55+
expect(this.$scope.example).toEqual(42);
56+
});
57+
58+
it('should return null for empty input', function() {
59+
this.$scope.form_test.input_test.$setViewValue('');
60+
this.$scope.$digest();
61+
62+
expect(this.$scope.example).toBeNull();
63+
});
64+
65+
describe('without java-integer attribute', function() {
66+
67+
it('should be valid for value within the safe integer range', function() {
68+
this.$scope.form_test.input_test.$setViewValue('9999999999');
69+
this.$scope.$digest();
70+
71+
expect(this.$scope.form_test.input_test.$error.max).toBeFalsy();
72+
});
73+
74+
it('should be invalid for value exceeding the max safe integer', function() {
75+
this.$scope.form_test.input_test.$setViewValue('99999999999999999');
76+
this.$scope.$digest();
77+
78+
expect(this.$scope.form_test.input_test.$error.max).toBe(true);
79+
});
80+
});
81+
82+
describe('with java-integer attribute', function() {
83+
84+
beforeEach(function() {
85+
this.compileElement('java-integer');
86+
});
87+
88+
it('should be valid for value within Java Integer range', function() {
89+
this.$scope.form_test.input_test.$setViewValue('2147483647');
90+
this.$scope.$digest();
91+
92+
expect(this.$scope.form_test.input_test.$error.max).toBeFalsy();
93+
expect(this.$scope.form_test.input_test.$valid).toBe(true);
94+
});
95+
96+
it('should be invalid for value exceeding Java Integer max', function() {
97+
this.$scope.form_test.input_test.$setViewValue('2147483648');
98+
this.$scope.$digest();
99+
100+
expect(this.$scope.form_test.input_test.$error.max).toBe(true);
101+
expect(this.$scope.example).toBe(2147483648);
102+
});
103+
104+
it('should be valid for empty value', function() {
105+
this.$scope.form_test.input_test.$setViewValue('');
106+
this.$scope.$digest();
107+
108+
expect(this.$scope.form_test.input_test.$error.max).toBeFalsy();
109+
});
110+
111+
it('should be valid for typical small quantity', function() {
112+
this.$scope.form_test.input_test.$setViewValue('100');
113+
this.$scope.$digest();
114+
115+
expect(this.$scope.form_test.input_test.$error.max).toBeFalsy();
116+
});
117+
});
118+
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"openlmisQuantityUnitToggle.packs": "Packs",
33
"openlmisQuantityUnitToggle.doses": "Doses",
4-
"openlmisInputDosesPacks.DosesBracket":" doses )"
4+
"openlmisInputDosesPacks.DosesBracket":" )"
55
}

src/openlmis-quantity-unit-toggle/quantity-unit.service.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@
6161
var packs = (stockOnHand > 0) ? Math.floor(stockOnHand / netContent) : Math.ceil(stockOnHand / netContent);
6262
var remainderDoses = stockOnHand % netContent;
6363

64-
return packs + ' ( +' + remainderDoses + messageService.get('openlmisInputDosesPacks.DosesBracket');
64+
var doseSign = remainderDoses < 0 ? '' : '+';
65+
var bracket = messageService.get('openlmisInputDosesPacks.DosesBracket');
66+
return packs + ' ( ' + doseSign + remainderDoses + bracket;
6567
}
6668

6769
/**

src/openlmis-table/tr-focused.directive.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
.module('openlmis-table')
3333
.directive('tr', directive);
3434

35-
function directive() {
35+
directive.$inject = ['$rootScope'];
36+
37+
function directive($rootScope) {
3638
var focusInRow, focusedRow, rows = [];
3739

3840
return {
@@ -43,7 +45,8 @@
4345
function link(scope, element) {
4446
if (!rows.length) {
4547
angular.element('body')
46-
.on('focusin', setSelectedRow);
48+
.on('focusin', setSelectedRow)
49+
.on('focusout', clearSelectedRowOnLeave);
4750
}
4851

4952
if (rows.indexOf(element) === -1) {
@@ -76,7 +79,8 @@
7679

7780
if (!rows.length) {
7881
angular.element('body')
79-
.off('focusin', setSelectedRow);
82+
.off('focusin', setSelectedRow)
83+
.off('focusout', clearSelectedRowOnLeave);
8084
}
8185
}
8286
}
@@ -97,6 +101,25 @@
97101
focusInRow = undefined;
98102
}
99103

104+
function clearSelectedRowOnLeave(event) {
105+
var related = event.relatedTarget ||
106+
(event.originalEvent && event.originalEvent.relatedTarget);
107+
108+
// When focus moves to another focusable element a focusin follows and
109+
// setSelectedRow handles the change. Only act when focus leaves to a
110+
// non-focusable target (e.g. clicking outside the table), where no
111+
// focusin fires and the row would otherwise keep the is-focused class.
112+
if (related) {
113+
return;
114+
}
115+
116+
if (focusedRow) {
117+
focusedRow.removeClass('is-focused');
118+
focusedRow = undefined;
119+
$rootScope.$evalAsync();
120+
}
121+
}
122+
100123
}
101124

102125
})();

src/openlmis-table/tr-focused.directive.spec.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,40 @@ describe('TR Focused Directive', function() {
5959
expect(tr.hasClass('is-focused')).toBe(false);
6060
});
6161

62+
it('removes is-focused when focus leaves the row to a non-focusable target', function() {
63+
var tr = this.table.find('tr:first');
64+
65+
this.table.find('input:first').focus();
66+
this.$rootScope.$apply();
67+
68+
expect(tr.hasClass('is-focused')).toBe(true);
69+
70+
angular.element('body').triggerHandler({
71+
type: 'focusout',
72+
relatedTarget: null
73+
});
74+
this.$rootScope.$apply();
75+
76+
expect(tr.hasClass('is-focused')).toBe(false);
77+
});
78+
79+
it('keeps is-focused when focus moves to another focusable element', function() {
80+
var tr = this.table.find('tr:first');
81+
82+
this.table.find('input:first').focus();
83+
this.$rootScope.$apply();
84+
85+
expect(tr.hasClass('is-focused')).toBe(true);
86+
87+
// A focusout with a relatedTarget is followed by a focusin that
88+
// setSelectedRow handles, so the focusout handler must not clear here.
89+
angular.element('body').triggerHandler({
90+
type: 'focusout',
91+
relatedTarget: this.table.find('input:last')[0]
92+
});
93+
this.$rootScope.$apply();
94+
95+
expect(tr.hasClass('is-focused')).toBe(true);
96+
});
97+
6298
});

0 commit comments

Comments
 (0)