I'm trying to add options to the Source dropdown on the Leads screen. The field uses the CRMSourcesAttribute class to define the existing list. After simply creating a new attribute class to add my own items, I extend the CRLead.Source CacheAttached event to use my new attribute class instead. The result is that there is no change - the new dropdown items are not shown. After doing this, if I inspect the field and select the Drop Down Values button, I do actually see the new options in the Drop Down Values popup window. Any ideas on what could be preventing the new options from displaying in the dropdown itself?
Here's how I configure it in my LeadMaint graph extension:
[PXMergeAttributes(Method = MergeMethod.Append)]
[PXRemoveBaseAttribute(typeof(CRMSourcesAttribute))]
[CRMSourcesExt] // list with old + new options
protected virtual void _(Events.CacheAttached<CRLead.source> e) { }
(v20R2)
Another option is to use code and create a new attribute class with new options, then override the DAC field to use it instead. However, as far as I know, you still have to go through the steps of activating the new options in the workflow customizations UI for each of the fields you're overriding. But then you can refer to the new options in code without having to search the list's AllowedValues at runtime.
With help from a colleague, I was able to get it working, and “without code” through screen workflow customization. I removed my code, then customized it with a new custom Workflow copied from the default. Not sure yet of the ramifications of this, like if we can access the new options in other real code or not, but I see how to create these options now. It has to also be done for both Lead and Opportunity in this scenario. It’s a bit tedious and ethereal (good word) though. I can see this easily causing problems and unexpected results for us we’ll want to watch out for.
Update 5/28/21: I added the options in the Workflow customizations for the field, then copied the default workflow to a new workflow, activated it, and activated the new options for each state/transition. I don't like it, but Acumatica tells me "that's just the way it is now". Note: you'll want to do this for other places referencing CRMSourcesAttribute as well, such as Opportunity.
Related
I'm using Acumatica 2020R1 and I'm adding side panels. Is there anyway, for example on the PO form (PO301000), that I could link a side panel to update when clicking on the Document Details?
I actually discovered a way to do this on Acumatica 2020R1. Here's how I did it:
Edit: I did this for the SOOrderEntry screen, creating a side panel that looks at the SOOrder but uses parameters from the SOLine. This method will work for any two tables with a master-detail relationship by replacing SOOrder & SOLine accordingly.
Part A:
Create virtual fields on the SOOrder DAC that correlate to the SOLine Parameters you need
Update these as lines are selecting/updated using event handlers
pass these fields as the params to your side panel actions as you normally would
While It seems like this alone would work, the side panel actually doesn’t refresh when a new SOLine is selected. We’re going to have to get creative to resolve this issue.
Part B:
Create an additional virtual field on the SOOrder DAC to keep track of which side panel was last opened (I will explain how to track this later). I use a string field to do so. This is especially important if you have more than one side panel on your screen.
Next you want to define a standard graph action that will update this field. Make it Visible = false, since you don’t actually want this button to show. We will call if from behind the scenes later. This is the method I use:
public PXAction<SOOrder> SidePanelCallback;
[PXButton]
[PXUIField(DisplayName = "SidePanelCallback", Visible = false)]
public virtual IEnumerable sidePanelCallback(PXAdapter adapter)
{
SOOrder doc = Base.Document.Current;
if (doc == null) return adapter.Get();
var docExt = doc.GetExtension<SOOrderExt>();
docExt.UsrSidePanelDetailParamsRefresh = adapter.CommandArguments;
return adapter.Get();
}
*Note: docExt.UsrSidePanelDetailParamRefresh is the variable I for the previous step
Now you need a way to trigger your code when your side panel actions are called. To do this we’ll use the following JavaScript
function commandResult(ds, context) {
var ds = px_alls['ds'];
switch (context.command)
{
case ('JustAction'):
ds.executeCallback('GraphAction', 'parameter value');
break;
}
}
You want to replace JustAction with the internal name of your side panel action. it should look like this: NavigateToLayer$DB000028
where DB000028 is the target ScreenID for your sidepanel. If you want to make sure your actions follow the same naming convention, you can check it by adding breakpoints and checking with the debugger.
You should replace GraphAction with the name of the graph action you created in the previous step. In our case SidePanelCallback
Then finally for the parameter value pass whatever you’d like to use to identify the opened side panel. This will be stored in UsrSidePanelDetailParamRefresh, or your equivalent.
You can create a case statement for each side panel you want this functionality for, so you only have to do all this once per graph!
Then finally, attach your script to the client event ‘commandPerformed’ on your data source.
Next we will define another graph action to do the actual refresh of the side panel. Mine looks like this:
public PXAction<SOOrder> SidePanelRefresh;
[PXButton]
[PXUIField(DisplayName = "SidePanelRefresh", Visible = false)]
public virtual IEnumerable sidePanelRefresh(PXAdapter adapter)
{
SOOrder doc = Base.Document.Current;
if (doc == null) return adapter.Get();
var docExt = doc.GetExtension<SOOrderExt>();
Dictionary<string, string> parameters = new Dictionary<string, string>();
string[] args = adapter.CommandArguments.Split('*');
switch (docExt.UsrSidePanelDetailParamsRefresh)
{
case ("Component Inventory"):
parameters.Add("Kit", args[0]);
parameters.Add("SiteID", args[1]);
throw new PXRedirectToGIRequiredException("Component Inventory", parameters) { Mode = PXBaseRedirectException.WindowMode.Layer };
}
return adapter.Get();
}
Note that for my use case the parameters I am passing cannot contain the character ‘*’, so I use it as a delimiter
And again, you can define as many case statements as you need for each of your dynamic side panels.
And now finally we need javascript again to call this action. Using event handlers are too slow for this type of refresh, they will always pass the old SOLine parameters. If you know a way to avoid javascript here, I’d love to hear it, as doing callbacks like this does have a heavy overhead. Because of this, be careful where you attach this script to, I actually attach it to the grid’s RowSelectorClick client event rather than AfterRowChange for this reason, and train my users to select rows in this way (by clicking the arrow to the left of each row) so we don’t impact the rest of the company for the minority that use this feature. I’d recommend this if you're working on a high traffic screen such as SO301000.
Anyway, here’s the javascript:
function Side_Panel_Refresh() {
px_alls["ds"].executeCallback("SidePanelRefresh", this.func.caller.arguments[2].row.getCell("InventoryID").getValue() + "*" + this.func.caller.arguments[2].row.getCell("SiteID").getValue());
}
SidePanelRefresh: Name of our graph action from the previous step.
InventoryID & SiteID are my parameters.
And again, using ‘*’ as a delimiter.
Anyway, I hope this helps anyone looking to make dynamic side panels. If anyone has improvements on this method I’d love to hear constructive feedback.
Happy development!
It is a new feature. Side panel were restricted to generic inquiry. In version 2020 you still need a generic inquiry to configure the side panel but they can be viewed in data entry forms too.
The feature is implemented with wizards so it's pretty limited in what it can do. Currently you are limited to the choices available in navigation parameter. It seems like it's not possible to add details entity there. Maybe there's a way but I'm not aware of it.
Check documentation reference link for User Interface: Side Panels for Data Entry Forms (content copied below):
https://help-2020r1.acumatica.com/Help?ScreenId=ShowWiki&pageid=a11aa71b-5f18-4c8f-9c69-350f2c7a89f4
Adding a Side Panel to the Data Entry Form
A customizer can add multiple side panel navigation paths to a data entry form.
To add a side panel, the customizer navigates to the Customization Projects (SM204505) form and creates a new project.
In the Customization Project Editor, the customizer selects the screen for which the side panel is planned to be added.
On the Screens > Form ID > Actions, the customizer adds an action of the Navigation:
Side panel type (1), specifies general settings (2), adds destination screen (3), and specifies parameters (4), if needed (for instance, in the screenshot below, Business Account was selected as a parameter so that the side panel displayed the cases of the specific customer selected on the data entry form).
Example of a side panel on the Customers form
To apply the changes, the customizer must publish the created customization project.
I have added a Global Button with the following code.
public override void Initialize()
{
if (!String.IsNullOrEmpty(Base.PrimaryView))
{
Type primaryViewItemType = Base.Views[Base.PrimaryView].Cache.GetItemType();
PXAction action = PXNamedAction.AddAction(Base, primaryViewItemType, "SubmitTicket", "Submit Ticket", TestClick);
}
}
public IEnumerable TestClick(PXAdapter adapter)
{
throw new PXException("Button clicked from graph" + Base.GetType().Name);
}
And it renders the button like this in each of the pages.
Now, I would like to display a popup panel, on button's click. I know I can create a popup panel on screen section. But, is there some way that I can have a general popup panel created in one place and can be displayed on each of the pages on the button's click?
Thank you.
As #HB_ACUMATICA mentioned there is no good easy way.
Providing another alternative to his post, you can create a graph and use it as a reusable popup by calling:
throw new PXPopupRedirectException(graph, string.Empty, true)
One thing I ran into was a sizing issue on the popup...
Changing the height/width when calling another graph as an in-page popup using PXPopupRedirectException
If you do copy and paste the PXSmartPanel you can create re-usable business logic by implementing the reusable business logic pattern found in this help as a starting point:
Reusing Business Logic
If I understand correctly you want to share the same PXSmartPanel control in different pages without having to copy/paste it in every screen.
In Acumatica Framework this is achieve by custom container controls like 'PXUploadDialog' which derives functionality from other controls like 'PXSmartPanel'. This is the control that is used when you attach files in all screen.
Unfortunately there seems to be no documentation on how to achieve this.
The closest I found is this SO question which is essentially unanswered:
Create custom User Control for Acumatica
Considering this, you may want to copy/paste the same smart panel in all screen.
To ease copying you can use the 'Edit ASPX' feature, make sure you backup the project before.
Edit ASPX to get to the code:
Copy paste your smart panel in the page and click 'GENERATE CUSTOMIZATION SCRIPT' to package the changes in the project:
I have created a custom module (actually I have created a handful in recent years, and this same obstacle frustrates me every time) following the Kentico documentation:
https://docs.kentico.com/display/K9/Creating+custom+modules
The problem I end up with every time, is in developing the User Interface for Parent/Child classes. I create a Vertical Tab node, and beneath it I add an edit tab and a Binding tab for the child class. This all works, and I can add and remove bindings at will, but what I can't do is ADD a new child class and bind it.
Using the Standard Edit Binding template, I am able to bind EXISTING Job Titles to the selected Category, but I cannot CREATE a new one from that page:
To solve this, I created a custom Edit Binding template, and added a New Child Class Header Action that points to a New / Edit Object child:
Which gives me a button that I can use to add a new child class (Job Title):
This approach works per se, in that I can click the New Job Title button and create a new item on the subsequent page:
But no binding is created to link the child object (Job Title) to the selected parent object (Category), An even bigger problem is that once I click Save, I am presented with the following:
The new object DOES SAVE, but the post-save navigation is somehow failing. The event log offers little in the way of diagnostics:
So I thought to create a completely custom interface to accomplish my needs here, according to the Kentico documentation:
https://docs.kentico.com/display/K9/Manually+creating+the+interface+for+custom+modules
So I change the Element Content of the New Job Title page to a custom page that I created to post a DataForm for the new object:
Taking care to assign the proper Object Types on the Properties Tab:
The intent was to programmatically create the binding upon save and also handle the correct navigation to avoid the ambiguous parameter error above, but when this page loads, the UIContext.ObjectID and UIContext.ParentObjectID are both 0:
So I cannot create the binding programmatically. I was able however to solve the error that I received by manually assigning the redirect. The experience is still lacking even with this hack, since it returns to the listing page, but the user still has to click "Add Items" to assign the binding after successfully creating it with the custom page I built.
This cannot be the proper way to do this, so any help with getting me on the right track would be greatly appreciated.
In order for the EditedObject to have a value you have to either decorate the page with the EditedObjectAtribute e.g. like this:
[EditedObject("<custom.objecttype>", "<objectid>", ...)]
or set the object yourself:
int objectId = QueryHelper.GetInteger("objectid", 0);
EditedObject = SomeInfoProvider.GetSomeInfo(objectId);
In your case, I'd recommend exploring what query parameters are available on the page and using them to fetch appropriate object(s). Also, make sure "JobCategoryId" is passed to the "New Job Title" dialog so that you can create the binding.
Btw - kudos for well asked question!
In orchard, I've added a boolean field called "IsDone" to the built in Content Menu Item content part via that Admin interface. I've then picked an item in Navigation and set the option to "yes" for the corresponding field i added.
In my custom theme, I've copied over MenuItem.cshtml.
How would I get the value of my custom "IsDone" field here?
I've tried something like
dynamic item = Model.ContentItem;
var myValue = item.MenuItem.IsDone.Value;
but I'm pretty sure my syntax is incorrect (because i get null binding errors at runtime).
thanks in advance!
First i suggest you use the shape alternate MenuItemLink-ContentMenuItem.cshtml instead of MenuItem.cshtml to target the content menu item directly.
Secondly, the field is attached to the ContentPart of the menu item. The following code retrieves the boolean field from this content part:
#using Orchard.ContentManagement;
#using System.Linq;
#{
Orchard.ContentManagement.ContentItem lContentItem = Model.Content.ContentItem;
var lBooleanField = lContentItem
.Parts
.Where(p => p.PartDefinition.Name == "ContentMenuItem") // *1
.SelectMany(p => p.Fields.Where(f => f.Name == "IsDone"))
.FirstOrDefault() as Orchard.Fields.Fields.BooleanField;
if (lBooleanField != null)
{
bool? v = lBooleanField.Value;
if (v.HasValue)
{
if (v.Value)
{
#("done")
}
else
{
#("not done")
}
}
else
{
#("not done")
}
}
}
*1
Sadly you cannot simply write lContentItem.As<Orchard.ContentManagement.ContentPart>() here as the first part in the part list is derived from this type, thus you would receive the wrong part.
While #ViRuSTriNiTy's answer is probably correct, it doesn't take advantage of the power of the dynamic objects that Orchard provides.
This is working for me but is a much shorter version:
#Model.Text
#{
bool? IsDone = Model.Content.ContentMenuItem.IsDone.Value;
var IsItDoneThough = (IsDone.HasValue ? IsDone.Value : false);
}
<p>Is it done? #IsItDoneThough</p>
You can see that in the first line I pull in the IsDone field using the dynamic nature of the Model.
For some reason (I'm sure there is a good one somewhere) the BooleanField uses a bool? as its backing value. This means that if you create the new menu item and just leave the checkbox blank it will be null when you query it. After you have saved it as checked it will be true and then if you go back and uncheck it then it will have the value false.
The second line that I've provided IsItDoneThough checks if it has a value yet. If it does then it uses that, otherwise it assumes it to be false.
Shape Alternate
#ViRuSTriNiTy's other advice, to change it to use the MenuItemLink-ContentMenuItem.cshtml instead of MenuItem.cshtml is also important.
The field doesn't exist on other menu items so it will crash if you try to access it. Just rename the .cshtml file to fix this.
Dynamic Model
Just to wrap this up with a little bit of insight as to how I got there (I'm still learning this as well) the way I figured it out is as follows:
.Content is a way of casting the current content item to dynamic, so you can use the dynamic advantages with the rest of line;
When you add the field in the admin panel it looks like it should be right there on the ContentItem, however it actually creates an invisible ContentPart to contain them and calls it whatever the ContentItem's type is.
So if you had added this field to a Page content type you would have used Model.Content.Page.IsDone.Value. If you had made a new content type called banana it would be Model.Content.Banana.IsDone.Value, etc.
Once you are inside the "invisible" part which holds the fields you can finally get at IsDone. This won't give you the actual value yet though. Each Field has its own properties which you can look up in the source code. the IsDone is actually a BooleanField and it exposes its data via the Value property.
Try doing a solution-wide search for : ContentField to see the classes for each of the fields you have available.
Hopefully this will have explained things clearly but I have actually written about using fields in a blog post and as part of my getting started with modules course over on the official docs (its way down in part 3 if you're curious).
Using built-in features instead of IsDone
This seems like a strange approach to do it this way. If you have a Content Item like a Page then you can just use the "Show on a menu" setting on the page.
Go to admin > content > open the page > down near the bottom you will find "Show on a menu":
This will automatically put it into your navigation and then you can move it around to where you want:
After it "IsDone" you can just go back and untick the "Show on a menu" option.
Setting up the alternative .cshtml
To clarify your comments about how to use the alternative, you need to
Copy the file you have at Orchard.Core/Shapes/Views/MenuItem.cshtml over to your theme's view folder so its /Views/MenuItem.cshtml
Rename the copy in your theme to MenuItem-ContentMenuItem.cshtml
Delete probably everything in it and paste in my sample at the start of this post. You don't want most of the original MenuItem.cshtml code in there as it is doing some special tricks to change itself into a different shape which isn't what you want.
Reset your original Orchard.Core/Shapes/Views/MenuItem.cshtml back to the factory default, grab it from the official Orchard repository
Understanding the view names
From your comments you asked about creating more specific views (known as alternates). You can use something call the Shape Tracer to view these. The name of them follows a certain pattern which makes them more and more specific.
You can learn about the alternates on the official docs site:
Accessing and Rendering Shapes
Alternates
To figure out what shape is being used and what alternates are available you can use the shape tracing module which is documented here:
Getting Started with Shape Tracing
The Situation: I've got a mid-sized chunk of html/javascript that contains an authentication script/input (it's a text input, radio control, and a combo box and a few buttons). What it is is less important than the concept that it's a mass of static client side code that the marketing department can pretty easily accidentally the whole thing.
The Desire: I want the users to be able to add it as a whole to a page, but not be able to modify it. When something needs to change, I want to change it in one place and have it be changed on all the pages.
What I've Tried: Widget with a default text. It works, but feels wrong. Users can edit it, and if they do when I fix it one place it doesn't propagate to all the instances. I'm a bit of a Kentico noob, but it seems like there should be a better way to do this.
Also note: I'm using portal engine if that makes a difference.
A widget is the proper usage. What you make your widget inherit from is the key in this case. I'd suggest creating a new widget based on a static HTML webpart. This way you can set the static HTML markup and hide the property from the content editor on the front end. You can do this by going to the Properties tab of the widget and setting the visibility of the field on the form. Don't delete the field, just hide it. It should be a checkbox that says hide on public form or editing form.
** Edit **
As I read through my answer and comments, I realized I meant to say clone the static HTML webpart and set its default text to your javascript. Then create a widget based on that cloned webpart. The text will reside in the web part and will allow you to update it in one place later, if needed.
I will not do it this way because you will be not able to make changes in the future. You can better create a new webpart this can be an empty webpart and then create a custom layout. In this layout you can put you're code. In this way you can always change you're code in the future and then it will be changed on all the places where the widget is placed.
I'd use a new widget based on the Static HTML webpart (make the field read only or hide it as Brenden mentioned), but store the data in a new custom setting.
no coding needed (only a macro to read the custom setting)
able to edit the script on the fly on any instance in the settings module. If you have multiple of these settings you won't need to go through all kinds of widgets to adjust their default setting but find them on a central place.
Cheers!
David
In this case I think it makes sense to create a custom web part to store all your code in it and use it that way. If you want to achieve it without creating a custom web part, you have to store the code in some non-web part and not widget specific object. I like the suggestion of creating a custom setting. You can then access this custom setting via a macro. This macro can be used as a default property of a newly created web part (inherited e.g. from the static text web part, you'd use the text property). You may as well create a widget out of it. Another approach is to use Kentico localization keys as a workaround. you can create a key in the Localization application and access it again, via a macro, e.g. {?customkey.myhtml?}. The approach with a custom setting however sounds cleaner to me.
This syntax should be working to access a custom setting value via macro:
{%Settings.CustomSettings.xxx%}
{%Settings.CustomSettings["xxx"]%}
{%Settings.CustomSettings.GetValue("xxx")%}