TinyMCE 5: Autocomplete that can be ignored so you can just type your own entry - tinymce

I want to help the user author handlebars/mustache templates so when they type a { character, an autocomplete of known template values comes up to assist the user. But the user may not want to choose one of the suggested options, they might just want to continue typing a new template value and terminate it with a }. In the example in the code below, the options "Order Number" and "Delivery Date" are the known autocomplete options, but I want the user to be able to type {Invoice Number} for example.
I've succeeded in make this work in one way. As a user is typing {Invoice Number} I add it to the list of allowed options in the autocompleter fetch function. So the user can get what they want into the document if the click on the suggested option. Clicking fires the onAction function. I want onAction to fire as soon as the user types the closing handlebars character i.e. }.
Here is the code I have tried. I am using TinyMCE 5.
let template = (function () {
'use strict';
tinymce.PluginManager.add("template", function (editor, url) {
const properties = [
{value: "Order Number", text: "Order Number"},
{value: "Delivery Date", text: "Delivery Date"}
];
const insertNewProperty = function(value) {
let property = {value: value, text: value};
properties.push(property);
return property;
};
editor.ui.registry.addAutocompleter('autocompleter-template', {
ch: '{',
minChars: 0,
columns: 1,
fetch: function (pattern) {
return new tinymce.util.Promise(function (resolve) {
let filteredProperties = pattern ? properties.filter(p => p.text.indexOf(pattern) > -1) : properties;
if (filteredProperties.length > 0) {
resolve(filteredProperties);
} else {
resolve([{value: pattern, text: pattern}]);
}
});
},
onAction: function (autocompleteApi, rng, value) {
let property = properties.find(p => p.value === value);
if (!property) {
property = insertNewProperty(value)
}
let content = `{${property.text}}`;
editor.selection.setRng(rng);
editor.insertContent(content);
autocompleteApi.hide();
}
});
return {
getMetadata: function () {
return {
name: "Learning",
url: "https://stackoverflow.com"
};
}
};
});
}());
})();```

You can add a matches function to your callbacks, along with your onAction, this help you do it.
matches: function (rng, text, pattern) {
if(pattern.endsWith('}')){
pattern = pattern.replace('}', '');
/**
* Check if pattern does not match an existing
* variable and do what you want
*/
return true;
}
},
And make sure the function always returns true.

Related

Trying to store SuiteScript search result as a lookup parameter

I have the following code which returns the internal ID of a sales order by looking it up from a support case record.
So the order of events is:
A support case is received via email
The free text message body field contains a reference to a sales order transaction number. This is identified by the use of the number convention of 'SO1547878'
A workflow is triggered on case creation from the email case creation feature. The sales order number is extracted and stored in a custom field.
The internal ID of the record is looked up and written to the console (log debug) using the workflow action script below:
*#NApiVersion 2.x
*#NScriptType WorkflowActionScript
* #param {Object} context
define(["N/search", "N/record"], function (search, record) {
function onAction(context) {
var recordObj = context.newRecord;
var oc_number = recordObj.getValue({ fieldId: "custevent_case_creation" });
var s = search
.create({
type: "salesorder",
filters: [
search.createFilter({
name: "tranid",
operator: search.Operator.IS,
values: [oc_number],
}),
],
columns: ["internalid"],
})
.run()
.getRange({
start: 0,
end: 1,
});
log.debug("result set", s);
return s[0];
}
return {
onAction: onAction,
};
});
I am trying to return the resulting internal ID as a parameter so I can create a link to the record on the case record.
I'm getting stuck trying to work out how I would do this?
Is there a way to store the value on the case record, of the internal ID, that is looked up? (i.e.the one currently on the debug logs)?
I am very new to JS and Suitescript so am not sure at what point in this process, this value would need to be stored in the support case record.
At the moment. the workflow action script (which is the part of the workflow the above script relates to) is set to trigger after submit
Thanks
Edit: Thanks to Bknights, I have a solution that works.
The workflow:
The new revised script is as follows:
*#NApiVersion 2.x
*#NScriptType WorkflowActionScript
* #param {Object} context
*/
define(["N/search", "N/record"], function (search, record) {
function onAction(context) {
var recordObj = context.newRecord;
var oc_number = recordObj.getValue({ fieldId: "custevent_case_creation" });
var s = search
.create({
type: "salesorder",
filters: [
search.createFilter({
name: "tranid",
operator: search.Operator.IS,
values: [oc_number],
}),
],
columns: ["internalid"],
})
.run()
.getRange({
start: 0,
end: 1,
});
log.debug("result set", s[0].id);
return s[0].id;
}
return {
onAction: onAction,
};
});
On the script record for the workflow action script, set the type of return you expect. In this case, it would be a sales order record:
This would allow you to use a list/record field to store the value from the 'search message' workflow action created by the script
the result
Edit 2: A variation of this
/**
*#NApiVersion 2.x
*#NScriptType WorkflowActionScript
* #param {Object} context
*/
define(["N/search", "N/record"], function (search, record) {
function onAction(context) {
try {
var recordObj = context.newRecord;
var oc_number = recordObj.getValue({
fieldId: "custevent_case_creation",
});
var s = search
.create({
type: "salesorder",
filters: [
search.createFilter({
name: "tranid",
operator: search.Operator.IS,
values: [oc_number],
}),
],
columns: ["internalid","department"],
})
.run()
.getRange({
start: 0,
end: 1,
});
log.debug("result set", s[0]);
recordObj.setValue({fieldId:'custevent_case_sales_order', value:s[0].id});
// return s[0]
} catch (error) {
log.debug(
error.name,
"recordObjId: " +
recordObj.id +
", oc_number:" +
oc_number +
", message: " +
error.message
);
}
}
return {
onAction: onAction,
};
});
Depending on what you want to do with the order link you can do a couple of things.
If you want to reference the Sales Order record from the Support Case record you'd want to add a custom List/Record field to support cases that references transactions. (ex custevent_case_order)
Then move this script to a beforeSubmit UserEvent script and instead of returning extend it like:
recordObj.setValue({fieldId:'custevent_case_order', value:s[0].id});
For performance you'll probably want to test whether you are in a create/update event and that the custom order field is not yet filled in.
If this is part of a larger workflow you may still want to look up the Sales Order in the user event script and then start you workflow when that field has been populated.
If you want to keep the workflow intact your current code could return s[0].id to a workflow or workflow action custom field and then apply it to the case with a Set Field Value action.

Ag-grid set column text filter to range of letters

The goal is to set a column's text filter to be a range of letters that values start with. For example in a customer name column setting the filter to be "starts with" and a range of a-m. The user will enter the two letters that define the start and end of the range (eg. "a" and "m").
Ag-grid's filter docs state that "in range" filtering is only supported for date and number data types. Looking at ag-grid's multi-filter example, multiple filters are combined with an OR condition in the filter model:
{
athlete: {
filterType: "multi",
filterModels: [
{
filterType: "text",
operator: "OR",
condition1: {
filterType: "text",
type: "startsWith",
filter: "a"
},
condition2: {
filterType: "text",
type: "startsWith",
filter: "b"
}
},
null
]
}
}
It looks like the solution is to programmatically find all letters in the range specified by the user, then include a condition for each letter in the "filterModels" array. Is there a more efficient way to do this?
A custom filter was the best solution for this scenario.
I was looking to support an optional range of letters, along with optional additional text in the filter field. Using a regular expression that matched this pattern inside the doesFilterPass method is working as expected.
Example using Vue and lodash:
doesFilterPass(params) {
// Regex matches "[A-M]", "[n-z]", "[E-p] additionaltext", etc
const nameFilterRegex = /^(?<nameStartRange>\[[a-z]{1}-[a-z]{1}\])?(?:\s+)?(?<additionalText>[a-z]+)?(?:\s+)?$/i;
const regexResult = nameFilterRegex.exec(params.data.name);
if (!isNil(regexResult)) {
const nameRange = regexResult.groups.nameStartRange;
const additionalText = regexResult.groups.additionalText;
if (!isEmpty(nameRange)) {
try {
const lastNameRegex = new RegExp(nameRange, "gi");
const matchedChar = params.data.name[0].match(lastNameRegex);
if (isEmpty(matchedChar)) {
return false;
}
} catch {
return false;
}
}
if (!isEmpty(additionalText)) {
if (!params.data.filterValue.includes(additionalText)) {
return false;
}
}
}
return true;
};

Mongodb will not create document within if/else statement

New to MongoDB. I'm trying to add a new document to an existing collection only if a document with the same name does not already exist within that collection. I'm using an if/else statement first to check whether there is a document with the same name as the new entry, and if not, an else statement that will create the new entry. Whatever I try, the new document is not getting added, and instead it returns an empty array. I'd be grateful for any help.
I've tried switching the if/else statements; checking for null and undefined values upon return of if statement
app.post('/cocktails/new', isLoggedIn, (req, res) => {
// add to DB then show item
let name = req.body.name;
let style = req.body.style;
let spirit = req.body.spirit;
let image = req.body.image;
let description = req.body.description;
let newCocktail = {name: name, style: style, base: spirit, image:
image, description: description}
Cocktail.find({name}, function (err, existCocktail) {
if(existCocktail){
console.log(existCocktail)
}
else {Cocktail.create(newCocktail, (err, cocktail) => {
console.log(cocktail)
if (err) {console.log(err)}
else {
res.redirect('/cocktails/' + cocktail._id)}
})
}
})
})
In the event document is not found with the if function, else statements will execute, leading to new document being created with the newCocktail object.
You should use findOne instead of find.
find returns an empty array when no docs found.
The following expression returns true, so your existCocktail condition becomes true, causing your new data not added.
[] ? true : false
Also I refactor your code a bit, you can use destructuring your req.body.
app.post("/cocktails/new", isLoggedIn, (req, res) => {
// add to DB then show item
const { name, style, spirit, image, description } = red.body;
let newCocktail = {
name,
style,
base: spirit,
image,
description
};
Cocktail.findOne({ name }, function(err, existCocktail) {
if (existCocktail) {
console.log(existCocktail);
res.status(400).json({ error: "Name already exists" });
} else {
Cocktail.create(newCocktail, (err, cocktail) => {
console.log(cocktail);
if (err) {
console.log(err);
res.status(500).json({ error: "Something went bad" });
} else {
res.redirect("/cocktails/" + cocktail._id);
}
});
}
});
});

React-Native - Dynamically generating a form with tcomb-form using an Array of objects

I'm creating a tcomb-form through an array of objects but I don't have a lot of experience with it and honestly I'm struggling a little bit to get hang of it.
This is the array structure that we are going to use:
export const AUDIT_CONTENT =
[
{type: "label", style: "title", group: "4.1", text: "field text here"},
{type: "label", style: "label", group: "4.1", text: "field text here"},
{type: "multi-checkbox", style: "checkbox", group: "4.1", text: "field text here"},
{type: "multi-checkbox", style: "checkbox", group: "4.1", text: "field text here"},
{type: "multi-checkbox", style: "checkbox", group: "4.1", text: "field text here"},
{type: "label", style: "label", group: "4.1", text: "field text here"},
{type: "multi-checkbox", style: "checkbox", group: "4.1", text: "field text here"}
]
The fields with type: label are objects that are going to store fields type: multi-checkbox, and these fields are the ones that are going to be validated. I'm grouping those fields by group, so all fields with group 4.1 are inside an array, the fields with group 4.1 as well and so on.
I managed to dynamically generate those fields by doing the following:
myFields = () => {
for (var c = 0; c < groupedFields.length; c++) {
for (var i = 0; i < groupedFields[c].length; i++ ) {
if (groupedFields[c][i].type === 'multi-checkbox') {
fields[groupedFields[c][i].text] = t.maybe(t.enums({
OPTION_1 : "OPTION 1 Label",
OPTION_2 : "OPTION 2 Label",
OPTION_3 : "OPTION 3 Label",
OPTION_4 : "OPTION 4 Label"
}));
}
}
}
}
var fields = {};
myFields()
var myFormType = t.struct(fields);
Now my problem starts here. I'm only generation the fields that receive a value, in this case the ones with type: multi-checkbox, but, I also want to dynamically render in my form the fields with type: label in the same order as my AUDIT_CONTENT array with those being objects so the result will be something like this:
"Field with style title": {
"Field with style label": [
{"Field with style multi-checkbox": "OPTION_1"},
{"Field with style multi-checkbox": "OPTION_3"},
],
"Other field with style label": [
{"Field with style multi-checkbox": "OPTION_4"},
{"Field with style multi-checkbox": "OPTION_2"},
]
}
This result will be stored in Mongo.
Hope someone can help me out with this and thanks in advance.
It would be better if you provide a visual representation of what you want but i think that you want to render and update a nested structure. For this i recommend recursive map methods for the array.
/*
To render a structure like this you can use map and assign types to the objects to decide what to render
But you should render it all.
Maybe you can use something like this:
*/
renderInputs(array){
return array.map((obj) => {
/* You can generate even nested forms if you want */
if (obj.children) {
return <div> {this.renderInputs()}</div>
} else {
return renderType(obj)
}
})
}
renderType(obj){
switch (obj.type) {
case 'label':
return <Element {...objProps} />
case 'multi-checkbox':
return <Element2 {...objProps} />
/*You even can generate other methods for nested objects*/
case 'other-case':
return <div>{this.OtherSpecialSubRenderMethodIfYoUwANT()}</div>
}
}
/**You will need an recursive method to update your state also and each object is recomended to have an unique id*/
updateState(array = this.state.array, newValue, id){
return array.map((obj) => {
if (obj.children) {
return updateState(obj.children, newValue, id)
}
if (obj.id == id) {
return { ...obj, value: newValue }
}
return obj;
})
}

Password Validation on Dynamically Generated Form

So I'm just learning Angular and I have basic routing setup and a partial for setting up a basic page with a form (theoretically any form), and based on the controller I load it loads that form from a fields array in the controller.
One of those forms is a registration form where I want to verify that the passwords match.
So in the partial I have (a mode complicated version of) this
<input ng-repeat="field in fields" ng-model="field.model" mustEqual="fields.mustEqual">
I found a directive which does password comparison:
taskDivApp.directive('mustEqual', function() {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, elem, attrs, ngModel)
{
if(!ngModel) return; // do nothing if no ng-model
// watch own value and re-validate on change
scope.$watch(attrs.ngModel, function()
{
validate();
});
// observe the other value and re-validate on change
attrs.$observe('equals', function (val)
{
validate();
});
var validate = function()
{
// values
var val1 = ngModel.$viewValue;
var val2 = attrs.mustEqual;
// set validity
ngModel.$setValidity('equals', val1 === val2);
};
}
}
});
So basically all I want to do is be able to load a literal string into ng-model from the controller so that I can pair up the model and the must equal, so the data for the form would look like:
$scope.fields = [
{
'type':"Email",
'placeholder':"Email Address",
'req': true,
'focus':true
},
{
'type':"Password",
'placeholder':"Password",
'model': "password",
'mustEqual': "passwordConf"
},
{
'type':"Password",
'placeholder':"Comfirm Password",
'model':"passwordConf",
'mustEqual': "password"
}
];
However what happens now is that the main password field gets bound to the "model" field of its index in the array and similar for passwordConf (ie inital values literally "password" and "passwordConf")
The fact that this isn't easy makes me think I have the wrong mindset - is this not a good way to do forms, should I just have them hard coded?
If this is okay then any ideas on how I could accomplish it would be appreciated!