How to add a Multi Checkbox List to the editor of a Form-based Element? - orchardcms

I'm trying to build a custom Typeahead Form Element. I have predefined multiple datasets to draw autocomplete suggestions from. When users add my field to a form, I want them to be able to select one or more of these datasets through checkboxes.
Orchard does not seem to have any Shape like this out of the box. By looking at other Form Elements in Orchard.DynamicForms and the SelectList Shape defined in Orchard.Forms.Shapes.EditorShapes.cs, I was able to piece together this working code for the Driver:
public class TypeaheadFieldElementDriver : FormsElementDriver<TypeaheadField> {
private readonly ITokenizer _tokenizer;
private readonly IEnumerable<IDataSet> _dataSets;
public TypeaheadFieldElementDriver(IFormsBasedElementServices formsServices, ITokenizer tokenizer, IEnumerable<IDataSet> _dataSets)
: base(formsServices)
{
_tokenizer = tokenizer;
this._dataSets = _dataSets;
}
...
protected override void DescribeForm(DescribeContext context) {
context.Form("TypeaheadField", factory =>
{
var shape = (dynamic)factory;
var form = shape.Fieldset(
Id: "TypeaheadField",
_Value: shape.Textbox(
Id: "Value",
Name: "Value",
Title: "Value",
Classes: new[] { "text", "medium", "tokenized" },
Description: T("The value of this typeahead field.")),
_DataSetIds: shape.SelectList(
Id: "DataSetIds",
Name: "DataSetIds",
Title: "Remote Datasets",
Multiple: true,
Description: T("The remote datasets to fetch suggestions from.")));
foreach (var dataSet in _dataSets)
{
form._DataSetIds.Items.Add(new SelectListItem { Text = T(dataSet.Description).Text, Value = dataSet.Id });
}
return form;
});
...
}
}
This successfully renders a multiple select field and sort of works for my purposes but I'm not able to completely unselect every option because the one that was selected keeps getting posted. For this reason and just general ease of use, I would prefer to render a list of checkboxes instead but I'm not sure how to proceed.

Related

How can I exract a full sentence using Apache NLPCraft?

In my model file I am using a macro with a regex extract any space-separated alpha-numeric words to capture an user-input sentence i.e.
macros:
- name: "<GENERIC_INPUT>"
macro: "{//[a-zA-Z0-9 ]+//}"
Then I am trying to capture it as following in the element:
elements:
- id: "prop:title"
description: Set title
synonyms:
- "{set|add} title <GENERIC_INPUT>"
The intent term is as following:
intents:
- "intent=myIntent term(createStory)~{tok_id() == 'prop:createStory'} term(title)~{tok_id() == 'prop:title'}?"
In the Java Model I am correctly capturing the title property:
public NCResult onMatch(
NCIntentMatch ctx,
#NCIntentTerm("createStory") NCToken createStory,
#NCIntentTerm("title") Optional<NCToken> titleList,
{
...
When I run a query against the REST API service the probe is deployed in, I only get the first word of the last element <GENERIC_INPUT> (the regular expression) of the synonym defined as {set|add} title <GENERIC_INPUT> i.e.
HTTP 200 [235ms]
{
"status": "API_OK",
"state": {
"resType": "json",
"mdlId": "Create Story",
"txt": "set title this is my story",
"resMeta": {},
"srvReqId": "GKDY-QLBM-B6TQ-7KYO-KMR8",
"status": "QRY_READY",
"resBody": {
"title": "set title this",
"createStory": true,
},
"usrId": 1,
"intentId": "myIntent"
}
}
In the resBody.title I get set title this rather than the whole string as it should be allowed by the regex i.e. set title this is my story
Any idea why? How can I get it to extract the whole title?
Many thanks
Regex <GENERIC_INPUT> can catch individual token, but not group of tokens.
Please try such way
elements:
- id: "prop:title"
description: "Set title"
synonyms:
- "{set|add} title"
- id: "prop:any"
description: "Set any"
synonyms:
- "//[a-zA-Z0-9 ]+//"
intents:
- "intent=test term(title)={# == 'prop:title'} term(any)={# == 'prop:any'}*"
Callback
#NCIntentRef("test")
#NCIntentSample({
"Set title 1 2",
"Set title a b c"
})
NCResult x(
NCIntentMatch ctx,
#NCIntentTerm("title") NCToken title,
#NCIntentTerm("any") List<NCToken> any) {
System.out.println("title=" + title.getNormalizedText());
System.out.println("any=" + any.stream().map(NCToken::getNormalizedText).collect(Collectors.joining("|")));
return NCResult.text("OK");
}
It should work.
But also please try to drop regex here. It can work too slow and you will have many garbage variants.
You can use one element in intent and extract following words in the callback
Model:
elements:
- id: "prop:title"
description: "Set title"
synonyms:
- "{set|add} title"
intents:
- "intent=test term(title)={# == 'prop:title'}"
Callback:
#NCIntentRef("test")
#NCIntentSample({
"Set title 1 2",
"Set title a b c"
})
NCResult x(
NCIntentMatch ctx,
#NCIntentTerm("title") NCToken title) {
System.out.println("title=" + title.getNormalizedText());
System.out.println("any after=" +
Stream.concat(
ctx.getVariant().getFreeTokens().stream(),
ctx.getVariant().getStopWordTokens().stream()
).sorted(Comparator.comparingInt(NCToken::getStartCharIndex)).
filter(p -> p.getStartCharIndex() > title.getStartCharIndex()).
map(NCToken::getNormalizedText).
collect(Collectors.joining("|"))
);
return NCResult.text("OK");
}
Same result, but without regex.
do you know if the apache nlpcraft provides a built-in method to extract as >>well quoted sentences i.e. 'some sentence like this one'?
There are few workarounds for such request, some of the seem like hacks.
I guess that most straight solution is following:
Make NCCustomParser
public class QuotedSentenceParser implements NCCustomParser {
#Override
public List<NCCustomElement> parse(NCRequest req, NCModelView mdl, List<NCCustomWord> words, List<NCCustomElement> elements) {
String txt = req.getNormalizedText();
if (
txt.charAt(0) == '\'' &&
txt.charAt(txt.length() - 1) == '\'' &&
!txt.substring(1, txt.length() - 1).contains("'")
)
return words.stream().map(
w -> new NCCustomElement() {
#Override
public String getElementId() {
return "qElem";
}
#Override
public List<NCCustomWord> getWords() {
return Collections.singletonList(w);
}
#Override
public Map<String, Object> getMetadata() {
return Collections.emptyMap();
}
}
).collect(Collectors.toList());
return null;
}
}
add configuration (Note, that you have to add qElem dummy element here.. It seems like some bug or unclear feature, I am pretty sure that dynamic definition of this element ID in QuotedSentenceParser must be enough)
elements:
- id: "qElem"
description: "Set title"
synonyms:
- "-"
intents:
- "intent=test term(qElem)={# == 'qElem'}*"
parsers:
- "org.apache.nlpcraft.examples.lightswitch.QuotedSentenceParser"
Usage
#NCIntentRef("test")
#NCIntentSample({
"'Set title a b c'"
})
NCResult x(NCIntentMatch ctx, #NCIntentTerm("qElem") List<NCToken> qElems) {
System.out.println(qElems.stream().map(p -> p.getNormalizedText()).collect(Collectors.joining("|")));
return NCResult.text("OK");
}

GraphiQL on Orchard-Core: "where" and "filter" does not work

I am trying to understand how to query with GraphiQL on Orchard-Core and I am facing some problems which I cannot fix:
I am trying to implement an extremelly simple query:
query MyQuery ($myDesc: String) {
product (where: {description: $myDesc}) {
description
}
}
When I try to play the query I receive the following error:
{
"errors": [
{
"message": "Argument \"where\" has invalid value {description: $myDesc}.\nIn field \"description\": Unknown field.",
"locations": [
{
"line": 17,
"column": 11
}
],
"extensions": {
"code": "5.3.3.1"
}
}
]
}
It says that 'Field "description" is not defined by type "productWhereInput"'
I need to apply to the products a bunch of filters to give me the result I want, but none of them are working.
I have also tried to use "filter" instead of "where", but then I get another error:
'Unknown argument "filter" on field "Query.product".'
Looking to some documentations it doesn't seem to be so hard to use GraphiQL, but when I try to write the code in the way I find it on the docs it give me errors after errors here on Orchard-Core.
There are only limited number of default implemented where filters. By the error you get you didn't implement WhereInputObjectGraphType for your part.
One way is to create yessql MapIndex on fields you want to be able to query.
I'll assume Description is TextField.
So first create ProductPartIndex class as YesSQL MapIndex, that will map Description of every ContentItem with ProductPart.
After that you need to connect index with GraphQL WhereInputObjectType.
public class ProductInputObjectType : WhereInputObjectGraphType<ProductPart>
{
public ProductInputObjectType(IStringLocalizer<ProductInputObjectType> S)
{
AddScalarFilterFields<StringGraphType>("description", S["product description"]);
}
}
public class ProductPartIndexAliasProvider : IIndexAliasProvider
{
private static readonly IndexAlias[] _aliases = new[]
{
new IndexAlias
{
Alias = "ProductPart",
Index = nameof(ProductPartIndex),
IndexType = typeof(ProductPartIndex)
}
};
public IEnumerable<IndexAlias> GetAliases()
{
return _aliases;
}
}
[RequireFeatures("OrchardCore.Apis.GraphQL")]
public class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddInputObjectGraphType<ProductPart, ProductInputObjectType>();
services.AddTransient<IIndexAliasProvider, ProductPartIndexAliasProvider>();
services.AddWhereInputIndexPropertyProvider<ProductPartIndex>();
}
}
This is adhoc code, didn't test it. Everything in here is altered version of AliasPart from source code.

How do I pass Javascript Object to Bixby's Action Output?

I am building an app that search for anime quotes. So, I have the following data, which is 331 Objects (which I call them 'Quote Cards') long and so it will also be updated in future as I add more and more quotes. The problem I having is in creating concepts for JS's array items such as keywords, and imageTag. and also the the character names which are listed as property. I am also willing to change the output as long as I can keep category, and keywords array items. Those are necessary for my quote cards
[
{
$id: "Cold_Souls_1",
animeTitle: "Fullmetal Alchemist Brotherhood",
animePoster:{
referenceImage: 'https://qph.fs.quoracdn.net/main-qimg-da58c837c7197acf364cb2ada34fc5fb.webp',
imageTags: ["Grey","Yellow","Blue","Metal Body","Machine", "Robot","Yellow Hair Boy"],
},
animeCharacters:{
"Edward Elric": [
{
quote: "A lesson without pain is meaningless. For you cannot gain something without sacrificing something else in return. But once you have recovered it and made it your own... You will gain an irreplaceable Fullmetal heart.",
keywords: ["lesson", "pain", "return", "meaningless", "gain","sacrificing", "recover"],
category: "Life Lesson"
}
]
}
},....................
]
In Bixby, you would model a structure that represents the JSON response.
structure (Anime) {
description (The output of your action)
property (title) {
type(viv.core.Text)
visibility (Private)
}
property (poster){
type(AnimePoster)
visibility (Private)
}
property (characters) {
type (AnimeCharacter)
max (Many)
visibility (Private)
}
}
structure (AnimePoster) {
property (referenceImage) {
type (viv.core.Text)
visibility (Private)
}
property (imageTags) {
type (viv.core.Text)
max (Many)
visibility (Private)
}
}
structure (AnimeCharacter) {
property (name) {
type (viv.core.Text)
visibility (Private)
}
property (quote) {
type (viv.core.Text)
visibility (Private)
}
property (keywords) {
type (viv.core.Text)
max (Many)
visibility (Private)
}
property (category) {
type (viv.core.Text)
visibility (Private)
}
}
In your javascript file, you process the JSON structure of animes
// listOfAnimes is the JSON object described in the question
var animes = [];
listOfAnimes.forEach((anime) => {
var characterNames = Object.keys(anime.animeCharacters);
var characters = [];
Object.keys(anime.animeCharacters).forEach((key) => {
characters.push({
name: key,
quote: anime.animeCharacters[key][0].quote, // * warning, can be many
category: anime.animeCharacters[key][0].category// * warning, can be many
});
});
animes.push( {
$id: anime.$id,
title: anime.title,
characters: characters,
poster: {
referenceImage: anime.animePoster.referenceImage,
imageTags: anime.animePoster.imageTags
},
});
});

jquery-jable: How to display a field as read-only in the edit form?

I have a table pre-populated with the company LAN IP addresses with fields for associated data, status, etc. The (jquery-)jtable fields collection is configured like this.
fields: {
id: { title: 'ID'},
ip: { title: 'IP address, edit: false }
more: { ... }
}
This works but the problem is that when the edit dialog pops up the user can't see the ip address of the record being edited as jtable's edit form doesn't show the field.
I've read through the documentation but can't see any way to display a field as read-only in the edit form. Any ideas?
You don't need to hack the jTable library asset, this just leads to pains when you want to update to a later version. All you need to do is create a custom input via the jTable field option "input", see an example field setup to accomplish what you need here:
JobId: {
title: 'JobId',
create: true,
edit: true,
list: true,
input: function (data) {
if (data.value) {
return '<input type="text" readonly class="jtable-input-readonly" name="JobId" value="' + data.value + '"/>';
} else {
//nothing to worry about here for your situation, data.value is undefined so the else is for the create/add new record user interaction, create is false for your usage so this else is not needed but shown just so you know when it would be entered
}
},
width: '5%',
visibility: 'hidden'
},
And simple style class:
.jtable-input-readonly{
background-color:lightgray;
}
I have simple solution:
formCreated: function (event, data)
{
if(data.formType=='edit') {
$('#Edit-ip').prop('readonly', true);
$('#Edit-ip').addClass('jtable-input-readonly');
}
},
For dropdown make other options disabled except the current one:
$('#Edit-country option:not(:selected)').attr('disabled', true);
And simple style class:
.jtable-input-readonly{
background-color:lightgray;
}
I had to hack jtable.js. Start around line 2427. Changed lines are marked with '*'.
//Do not create element for non-editable fields
if (field.edit == false) {
//Label hack part 1: Unless 'hidden' we want to show fields even though they can't be edited. Disable the 'continue'.
* //continue;
}
//Hidden field
if (field.type == 'hidden') {
$editForm.append(self._createInputForHidden(fieldName, fieldValue));
continue;
}
//Create a container div for this input field and add to form
var $fieldContainer = $('<div class="jtable-input-field-container"></div>').appendTo($editForm);
//Create a label for input
$fieldContainer.append(self._createInputLabelForRecordField(fieldName));
//Label hack part 2: Create a label containing the field value.
* if (field.edit == false) {
* $fieldContainer.append(self._myCreateLabelWithText(fieldValue));
* continue; //Label hack: Unless 'hidden' we want to show fields even though they can't be edited.
* }
//Create input element with it's current value
After _createInputLabelForRecordField add in this function (around line 1430):
/* Hack part 3: Creates label containing non-editable field value.
*************************************************************************/
_myCreateLabelWithText: function (txt) {
return $('<div />')
.addClass('jtable-input-label')
.html(txt);
},
With the Metro theme both the field name and value will be grey colour.
Be careful with your update script that you're passing back to. No value will be passed back for the //edit: false// fields so don't include them in your update query.
A more simple version for dropdowns
$('#Edit-country').prop('disabled',true);
No need to disable all the options :)

Symfony2 Entity form type with a specific query_buider

Context
In my case, I've some orders with "discount vouchers" (discount). A discount can be use on under different conditions. For instance, discounts have an expired date, can be used by a limited number of customers, can be dedicated to a user, ...
Each discount can be attached to several order.
In my backoffice, I want to add to order create form a field "Discount" with a list of discount available but only right discounts.
What I made
An entity "order" with a field manyToMany
/**
* #ORM\ManyToMany(targetEntity="PATH\MyBundle\Entity\Discount", inversedBy="orders")
* #ORM\JoinTable(name="shop_discounts_orders",
* joinColumns={#ORM\JoinColumn(name="order_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="discount_id", referencedColumnName="id")}
* )
*/
private $discounts;
An entity "discounts" with a field manyToMany
/**
* #ORM\ManyToMany(targetEntity="PATH\MyBundle\Entity\Order", mappedBy="discounts")
*/
private $orders;
A form OrderType with a field discounts
$builder->add('discounts', 'entity',
array( 'label' => 'Discount vouchers',
'required' => false,
'expanded' => true,
'class' => 'PATH\MyBundle\Entity\Discount',
'property' => 'title',
'multiple' => true,
'query_builder' => function(EntityRepository $er) use ($params) {
return $er->getQuerySelectType($params);
},
));
With this solution, I can return specific discount defined by my request in my entity repository. It's good for expired date condition for instance.
What I would like
I'd like to filter results in the checkbox list. In fact, I want limit usage of the discount to a dedicated user, limit to a list of products, or limit the number of usage... And these condition cannot be done by a simple sql request.
I try to create special Type. My idea is to have an array of entities Discount and load a choice list... After that, I create a dataTransformer but It doesn't work !
Thank's for your ideas !
You could use the $options from public function buildForm(FormBuilderInterface $builder, array $options) to pass your user and product for instance. With those 2 informations you could refine your list of discount (in your query)
if you do so you need to add them in the setDefaultValue
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'user_discount' => null,
'product_discount' => null,
));
}
and in your controller:
$form = $this->formFactory->create(new YourFormType(), $entity, array(
'user_discount' => $this->getUser(),
'product_discount' => $product,
));
I found a solution and explain it if someone have the same issue as me.
Create a custom Type
My custom type is inspired by Symfony\Bridge\Doctrine\Form\Type\DoctrineType
class DiscountOrderType extends AbstractType
{
// overide choiceList callback
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$choiceListCache =& $this->choiceListCache;
$type = $this;
$choiceList = function (Options $options) use (&$choiceListCache, &$time, $container) {
[[ Copy paste same as Doctrine type ]]
// Create your own choiceList class (EntityChoiceList)
if (!isset($choiceListCache[$hash])) {
$choiceListCache[$hash] = new DiscountChoiceList(
$options['em'],
$options['class'],
$options['property'],
$options['loader'],
$options['choices'],
$options['group_by']
);
// If you want add container
$choiceListCache[$hash]->setContainer($container);
}
return $choiceListCache[$hash];
};
$resolver->setDefaults(array(
'choice_list' => $choiceList,
));
}
Create a custom EntityChoiceList
My custom type is inspired by Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList
class EntityChoiceList extends ObjectChoiceList
{
protected function load()
{
if ($this->entityLoader) {
$entities = $this->entityLoader->getEntities();
} else {
$entities = $this->em->getRepository($this->class)->findAll();
}
// You have access to the entities in the choice list
// Add your custom code here to manipulate the choice list
// you can do some check not properly possible with sql request (http requests on each result, ...) before add it in choice list
// you can add some custom cache rules, ...
// if you use gedmon and want apply a "join" with translate table, you can add $query->setHint(\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, 'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'); before playing request...
// Possibilities are infinite
// FOR INSTANCE : you already want unset first entity of the result
if (isset($entities[0])) {
unset($entities[0]);
}
// END OF CUSTOM CODE
try {
// The second parameter $labels is ignored by ObjectChoiceList
// The third parameter $preferredChoices is currently not supported
parent::initialize($entities, array(), array());
} catch (StringCastException $e) {
throw new StringCastException(str_replace('argument $labelPath', 'option "property"', $e->getMessage()), null, $e);
}
$this->loaded = true;
}
Of course you can try to extend symfony class for beautyfull code ;).
Thank's to #maxwell2022 for your help !

Resources