Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 143 additions & 6 deletions dist/milo_ui.bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ function MLFoldTree$toggleItem(id, opened) {

},{}],8:[function(require,module,exports){
'use strict';

const async = require('async');
const FORMLIST_CHANGE_MESSAGE = 'mlformlistchange';

const MLFormList = module.exports = milo.createComponentClass({
Expand Down Expand Up @@ -744,7 +744,9 @@ const MLFormList = module.exports = milo.createComponentClass({
init: MLFormList$init,
moveItem: MLFormList$moveItem,
setItemSchema: MLFormList$setItemSchema,
destroy: MLFormList$destroy
destroy: MLFormList$destroy,
validateModel: MLFormList$validateModel,
clearSubSchemaValidation: MLFormList$clearSubSchemaValidation
}
});

Expand All @@ -769,6 +771,7 @@ function handleClick (type, event) {
function MLFormList$init () {
MLFormList.super.init.apply(this, arguments);
this.once('childrenbound', onChildrenBound);
this._invalidFormControls = {};
}

function MLFormList$setItemSchema (schema) {
Expand Down Expand Up @@ -844,7 +847,95 @@ function _triggerExternalPropagation () {
showHidePrepend.call(this);
}

},{}],9:[function(require,module,exports){
function MLFormList$clearSubSchemaValidation () {
this._invalidFormControls = {};
}

function MLFormList$validateModel (callback, invalidControls) {
const validations = [];
const self = this;
this._dataValidations = { fromModel: {} };
(this.model.m().get() || []).forEach((data, index) => {
this._subFormSchema.items.forEach((item) => {
if (item.validate && item.validate.fromModel && item.validate.fromModel[0] === 'required') {
this._dataValidations.fromModel[`${index}${item.modelPath}`] = [validateRequired];
}
});
});

_.eachKey(this._dataValidations.fromModel, function (validators, modelPath) {
const [index, path] = modelPath.split('.');
const data = (this.model.m().get() || [])[index][path];
validators = Array.isArray(validators) ? validators : [validators];

if (validators && validators.length) {
validations.push({
modelPath: modelPath,
data: data,
validators: validators
});
}
}, this);


let allValid = true;
async.each(validations,
function (validation, nextValidation) {
let lastResponse;
async.every(validation.validators,
function (validator, next) {
validator(validation.data, function (err, response) {
lastResponse = response || {};
next(err, lastResponse.valid);
});
},
function (err, valid) {
lastResponse.path = validation.modelPath;
lastResponse.valid = valid;
handleValidatedComponents.call(self, lastResponse, invalidControls);
if (!valid) allValid = false;
nextValidation(null);
}
);
},
function (err) {
invalidControls = Object.assign({}, invalidControls, self._invalidFormControls);
callback && callback({allValid, invalidControls});
}
);
}

function validateRequired(data, callback) {
const valid = typeof data != 'undefined'
&& (typeof data != 'string' || data.trim() != '');
const response = MLForm$$validatorResponse(valid, 'please enter a value', 'REQUIRED');
callback(null, response);
}

function MLForm$$validatorResponse(valid, reason, reasonCode) {
return valid
? { valid: true }
: { valid: false, reason: reason, reasonCode: reasonCode };
}

function handleValidatedComponents(response) {
if (response.valid) {
delete this._invalidFormControls[response.path];
} else {
const [index, modelPath] = response.path.split('.');
let reason = {
label: `List Item ${Number(index)+1}. ${modelPath}`,
reason: response.reason,
reasonCode: response.reasonCode
};
this._invalidFormControls[response.path] = {
reason: reason
};
}
}


},{"async":34}],9:[function(require,module,exports){
'use strict';

const componentsRegistry = milo.registry.components;
Expand Down Expand Up @@ -2280,6 +2371,8 @@ function defaultFilter(text, option) {
function _updateOptionsAndAddButton(text, filteredArr) {
if (!text) {
this.toggleAddButton(false, { preserveState: true });
// If the input is empty, then there's nothing to add; don't show the "Add" button
this.__showAddOnClick = false;
setSelected.call(this, filteredArr[0]);
} else {
if (filteredArr.length && _.find(filteredArr, isExactMatch)) {
Expand Down Expand Up @@ -2398,10 +2491,32 @@ function _onMouseLeave() {
});
return; // should stop as before
}
this.toggleAddButton(false, { preserveState: true });
// DM-2718: keep the "Create new ...?" prompt + Add button visible when the mouse
// leaves and the dropdown collapses, as long as the typed text is a valid new entry.
// Deriving visibility from the live text (rather than blanking it) matches onInputClick.
_refreshAddButton.call(this);
}


/**
* DM-2718: Recompute the "add item" button visibility from the current input text.
* The previous approach relied on the `__showAddOnClick` flag, which is derived from
* `_isAddButtonShown` at mouse-leave time and could get stuck `false` after repeated
* mouse-leave/click cycles, leaving the "Create new ...?" prompt permanently hidden even
* though the typed text is a valid new entry. Deriving from the live text is robust.
*/
function _refreshAddButton() {
if (!this._addItemPrompt) return;
var text = this._comboInput.el.value && this._comboInput.el.value.trim();
if (!text) return;
var options = this._optionsData || [];
var hasExactMatch = options.some(function (option) {
return option.label && option.label.toLowerCase() === text.toLowerCase();
});
if (hasExactMatch) this.toggleAddButton(false, { preserveState: true });
else this.toggleAddButton(options.length > 1 || this._optionsURL);
}

/**
* Input click handler
*
Expand All @@ -2411,7 +2526,7 @@ function _onMouseLeave() {
function onInputClick(type, event) {
this.showOptions();
this._comboInput.el.setSelectionRange(0, this._comboInput.el.value.length);
if (this.__showAddOnClick) this.toggleAddButton(true);
_refreshAddButton.call(this);
}


Expand All @@ -2435,7 +2550,13 @@ function onEnterKey(type, event) {
* @param {Event} event
*/
function onAddBtn (type, event) {
var data = { label: this._comboInput.el.value };
var trimmedLabel = this._comboInput.el.value.trim();
if (!trimmedLabel) {
this.toggleAddButton(false, { preserveState: true });
this.__showAddOnClick = false;
return;
}
var data = { label: trimmedLabel };
this.postMessage('additem', data);
this.events.postMessage('milo_supercomboadditem', data);
this.toggleAddButton(false, { preserveState: true });
Expand Down Expand Up @@ -3507,6 +3628,7 @@ _.extendProto(MLForm, {
viewPathSchema: MLForm$viewPathSchema,
getModelPath: MLForm$getModelPath,
getViewPath: MLForm$getViewPath,
getSubSchemas: MLForm$getSubSchemas,
destroy: MLForm$destroy,
});

Expand Down Expand Up @@ -3911,6 +4033,21 @@ function MLForm$viewPathComponent(viewPath) {
return viewPathObj && viewPathObj.component;
}

/**
* Returns subSchemas of type formList
*
* @return {Schemas}
*/
function MLForm$getSubSchemas() {
let subSchemas = [];
for(const value in this._formViewPaths) {
if(Object.hasOwn(this._formViewPaths, value) && this._formViewPaths[value].schema && this._formViewPaths[value].schema.type === "formlist") {
subSchemas.push(this._formViewPaths[value]);
}
}
return subSchemas;
}


/**
* Returns form schema for a given view path item (path as defined in Data facet)
Expand Down
2 changes: 1 addition & 1 deletion dist/milo_ui.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/milo_ui.min.js.map

Large diffs are not rendered by default.

36 changes: 33 additions & 3 deletions lib/components/SuperCombo.js
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,8 @@ function defaultFilter(text, option) {
function _updateOptionsAndAddButton(text, filteredArr) {
if (!text) {
this.toggleAddButton(false, { preserveState: true });
// If the input is empty, then there's nothing to add; don't show the "Add" button
this.__showAddOnClick = false;
setSelected.call(this, filteredArr[0]);
} else {
if (filteredArr.length && _.find(filteredArr, isExactMatch)) {
Expand Down Expand Up @@ -613,10 +615,32 @@ function _onMouseLeave() {
});
return; // should stop as before
}
this.toggleAddButton(false, { preserveState: true });
// DM-2718: keep the "Create new ...?" prompt + Add button visible when the mouse
// leaves and the dropdown collapses, as long as the typed text is a valid new entry.
// Deriving visibility from the live text (rather than blanking it) matches onInputClick.
_refreshAddButton.call(this);
}


/**
* DM-2718: Recompute the "add item" button visibility from the current input text.
* The previous approach relied on the `__showAddOnClick` flag, which is derived from
* `_isAddButtonShown` at mouse-leave time and could get stuck `false` after repeated
* mouse-leave/click cycles, leaving the "Create new ...?" prompt permanently hidden even
* though the typed text is a valid new entry. Deriving from the live text is robust.
*/
function _refreshAddButton() {
if (!this._addItemPrompt) return;
var text = this._comboInput.el.value && this._comboInput.el.value.trim();
if (!text) return;
var options = this._optionsData || [];
var hasExactMatch = options.some(function (option) {
return option.label && option.label.toLowerCase() === text.toLowerCase();
});
if (hasExactMatch) this.toggleAddButton(false, { preserveState: true });
else this.toggleAddButton(options.length > 1 || this._optionsURL);
}

/**
* Input click handler
*
Expand All @@ -626,7 +650,7 @@ function _onMouseLeave() {
function onInputClick(type, event) {
this.showOptions();
this._comboInput.el.setSelectionRange(0, this._comboInput.el.value.length);
if (this.__showAddOnClick) this.toggleAddButton(true);
_refreshAddButton.call(this);
}


Expand All @@ -650,7 +674,13 @@ function onEnterKey(type, event) {
* @param {Event} event
*/
function onAddBtn (type, event) {
var data = { label: this._comboInput.el.value };
var trimmedLabel = this._comboInput.el.value.trim();
if (!trimmedLabel) {
this.toggleAddButton(false, { preserveState: true });
this.__showAddOnClick = false;
return;
}
var data = { label: trimmedLabel };
this.postMessage('additem', data);
this.events.postMessage('milo_supercomboadditem', data);
this.toggleAddButton(false, { preserveState: true });
Expand Down