SuperOffice CRM Professionals

Experienced experts in SuperOffice CRM - those primarily responsible for the setup, configuration, maintenance of SuperOffice in an organization, as well as those responsible for the implementation of applications or integrations, or provide consultancy services as to how an integration should be implemented using current industry standards.

LATEST FORUM POSTS

Technical blog posts

The TicketSentry is getting teeth

There has been a Sentry mechanism in NetServer for requests (well, the table is called ticket ) for many years. Ticket rights are not calculated in the same way as for Sales entities: instead of an owner and group system and a data access matrix, tickets belong to categories ( ej_category ) and those categories have masters and members. Finally, users belong to roles that have specific functional rights, and those functional rights are used to grant types of access to tickets. As with sale or appointment, access rights calculated for the root table will also lead to specific rights for derived tables (think appointment -> text). In the case of ticket, the ej_message table is the most interesting derived case. Think of it as category replacing the owner/group distance concept in Sales; and the functional rights correspond to the different columns in the data access right matrix in admin, for Sales entities. This TicketSentry in NetServer has, up to now, only worked with the type of access required for so-called List of tickets. In Service that means you get to see tickets in grids, but you can't change them and you can't see the messages belonging to those tickets. The List/View/Edit levels in Service don't have a direct counterpart in Sales entities, but it actually maps pretty well to table rights. List => ticket - Read , ej_message - None View => ticket - Read , ej_message - Read Edit => ticket - Full , ej_message - Full This is the mapping we'll have from now on. But, until now it was " if you have List, you get FULL on everything; otherwise you get NONE on everything; and we don't care about View or Edit ".   That wasn't really good, and it will now change to the rights mapping as outlined above. The rights calculations themselves do not change (no refactoring there!), so the various rules for members/masters/non-service-users etc are exactly the same as before. But they will now be enforced by the database layer in NetServer, and they will appear properly in the FieldRight dictionaries on TicketEntity carriers. As an example, if you perform a query that takes its first field from the ej_message table, and you do not have the right to see the ej_message part of some record in the result set, then that record will disappear completely. Even if it is joined to things that you do have access to - the ej_message part of the record will decide the outcome, because it is the first field you refer to. In other words - GetTicketMessageEntity(123) will start returning null if you don't have access to that message (calculated via its ticket, and you relationship to that). Likewise, a dot-syntax search where the first field is ej_message. will be treated in the same way - either you have access to that row, or you never see it. If you are getting hold of ej_message fields in some other way - say, as a dotsyntax query that starts with ticket - then the ticket may be there (if you have access), but fields belonging to ej_message may be blank (if you don't have access to that message). I will just reiterate that the rules have not changed. But we are starting to enforce them, within NetServer, using the standard mechanisms and behaviours there. Hopefully you're not dependent on the old, laissez-faire behaviour... it's going away.

Marek Vokáč
21-10-2022
thumb_up3 mode_comment0

From the Curiosity Cabinet: .ForEach()

Sometimes you discover somthing in a corner of the code and think, "it must have seemed like a good idea at the time". As you delve into it, you get a sense of why; and at the same time, understand why it wasn't through a somewhat deeper insight. Get a coffee and bear with me while I explain. This time it's about LINQ in .NET and how it works. I expect everyone who uses C# is familiar with it, and even loves the power. Here in SuperOffice we've generally landed on the fluent-chain-of-methods syntax rather than the sql-like one. To me that's because it saves me from a mental gear-change. But in any case, the resulting code is the same and the same thing happens. One of the core features of LINQ is deferred execution . When you say var things = myCollection.Where(x => x.ShouldKeep).Select(x => x.TheThing); then nothing is actually done - yet. myCollection is not called upon to enumerate its members, no filtering and no projection happens. Only the chain is built. Then, when you say foreach(var item in things) { ... } ... and only then ... are the operations performed. This comes from the signatures of the methods - they take an IEnumerable , and return an IEnumerable . The important aspect here is that an IEnumerable is something that can be enumerated/iterated over; but it's not an enumerator/iterator in itself and it certainly doesn't mean the thing-to-be-enumerated is an already-existing collection. It's very powerful. But this deferral of execution needs to end, sooner or later: we actually want things to happen and we want to see the result. In the case of a foreach loop, we get handed the items one at a time. Another way is to say .ToList() , or .ToArray() , .ToDictionary() (one of my favourites); there are others. Their return types are not IEnumerable - instead they return some result; and their implementations contain a foreach loop that "pulls" on the incoming IEnumerable, thereby triggering the execution of the whole pipeline. With this background, consider the following code: public static IEnumerable ForEach(this IEnumerable enumeration, Action action) { if (enumeration == null) throw new ArgumentNullException("enumeration"); if (action == null) throw new ArgumentNullException("action"); foreach (var item in enumeration) { action(item); yield return item; } } With this in place, you might think it reasonable to write listOfAppointments.ForEach(m => Assert.IsFalse(String.IsNullOrEmpty(m.AssociateName))); But... no. The catch here is that the ForEach method does have a loop iterating over the incoming elements, sure... but in turn it yields elements through its return value. Therefore, it's not going to do anything unless the next method in the chain performs an iteration. It simply won't be called, because of the special way IEnumerable works and how the compiler generates code. In the example above, there is no next method; these Asserts were simply never executed. I'm pretty sure the author of that code didn't think that was going to (not?) happen. Years ago I got yelled at by a partner because of this, and because our .ForEach was in the global namespace and masking his own (better) implementation - which took him some time to figure out. I recently removed this curious and confusing construction. While LINQ is great for data set processing, I still think that updating elements in place or complex processing can just as well be in an explicit foreach loop. It makes it more clear that this stuff is actually happening to each element. The somewhat contrived "convert foreach to LINQ" example on Microsoft Learn is actually a case of a foreach loop that yields something - obviously a LINQ candidate, but very different from a loop that computes or stores things (what I'd call an action).  Of course this is a matter of opinion. But I think everyone agrees that our little curiosity, a .ForEach that doesn't actually execute by itself, is not what you expect or want. There. Done, removed, get on with Friday :-) Marek  

Marek Vokáč
07-10-2022
thumb_up1 mode_comment0

Upcoming CRMScript / Developer improvements

Dear community, We have done some improvements to CRMScript upcoming release that we hope will be useful for you. The changes can be split into two main areas: improvements to CRMScript and improvements to the developer UI. CRMScript improvements For a few years now, we have had support for serializing/deserializing arrays and structs to JSON data. However, the support had some "gaps" in support for all basic types, and also the usage was a bit cumbersome having to go through XMLNode or JSONBuilder . To solve the latter, we have now introduced two new functions on arrays and struct : fromJsonString() and toJsonString() . The functions will allow you to serialize/deserialize in one swift code line: struct Person { String firstname; String lastname; Date dob; }; Person p; p.fromJsonString('{"firstname": "Jon", "lastname": "Doe", "dob": "1984-10-26"}'); printLine(p.toJsonString()); The observant amongst you will perhaps also notice that in the code above, we are deserializing and serializing a member of type Date. This was not supported before, but now we actually support members of all basic types: Integer, String, Bool, Float, Date, Time, DateTime, Byte, TimeSpan. For the types not supported natively by JSON, we will require/produce the following formats: Date: converted to/from string with format "YYYY-MM-DD" DateTime: converted to/from string with format "YYYY-MM-DDTHH:MI:SS" Time: converted to/from string with format "HH:MI:SS" Byte: converted to/from number. TimeSpan: converted to/from number (seconds) Support for these additional types also work for the existing fromXMLNode() and toJson() methods. Also, please note that fromJsonString() on arrays will work just like fromXMLNode() , which means it will append elements to the array and not clear it. Arrays have a separate .clear() method that can be called if you want to empty it first. Furthermore, we have introduced two new methods on arrays: buildString() and sort() .  buildString(String separator) is adding some functionality that has always existed on Vector ; the possibility to create a string (with a separator) from an array. The function will work as expected on basic types. (Complex types (e.g. an array of HTTP instances) will just result in a string with "[complex]"). The most useful example is probably to get a comma-separated array of integers: Integer[] arr; for (Integer i = 0; i In addition to basic types, we also support buildString() on arrays of structs. The default way to serialize a struct instance is to return its contents as a JSON string. However, there is now a way to override this by adding a member function with the following signature: String toString() : struct Person { String firstname; String lastname; String toString() { return this.firstname + " " + this.lastname; } }; Person[] persons; persons.fromJsonString('[{"firstname": "Mark","lastname": "Wahlberg"}]'); persons.fromJsonString('[{"firstname": "Uma","lastname": "Thurman"}]'); persons.fromJsonString('[{"firstname": "Tom","lastname": "Cruise"}]'); persons.fromJsonString('[{"firstname": "Michael J.","lastname": "Fox"}]'); printLine(persons.buildString(",")); The other new function for arrays is sort() , which will obviously sort the contents of the array. Only one-dimensional arrays are supported, and only arrays of basic types or structs. For basic types, this works as you would expect: Integer[] arr; for (Integer i = 0; i In order to be able to sort an array of structs, the struct must implement a method with the following signature: Bool compare(SameStruct s) . This will allow you to sort arrays of structs using whatever comparison you'd like: struct Person { String firstname; String lastname; String toString() { return this.firstname + " " + this.lastname; } Bool compare(Person p) { return this.toString()   Developer UI improvements This release contains lots of small and medium improvements to the developer experience in Service. Here are some of the highlights: Debugger/Tracing view When debugging in real time, or when viewing a saved script trace, we have now added a dropdown to the UI with all the source locations for the current debug/trace session. For large scripts that are using #includes, this allows you to quickly switch between the different sources. In debug mode, you can use this to e.g. set a breakpoint in another file. In tracing mode, clicking in the gutter (where the red breakpoints are shown) will now instead fast-forward the trace to that location. This can be very useful when viewing large script traces: instead of having to use the slider to try to find the frame where some particular code is executed, you can rather click next to the code and the slider will move to the correct position. Another small but welcome improvement to this view is that the width of the sidebar (containing info, variables, etc) now will be remembered in your browser and reused on subsequent views. We have also cleaned up the variable view a bit, and added a "copy value to clipboard" icon for each variable. Working with CRMScripts, Custom screens and Extra tables We have also done some changes to the UI when working with CRMScripts. First of all, the search functionality for scripts will now show the chosen result using the correct "View script" screen instead of a dump of the table contents. Also, when viewing a script, the "Run script" link now properly uses the includeId of the script if it is configured. Using the includeId-link instead of the id-link is recommended to make customizations more robust when migrating between instances of SuperOffice. When editing a CRMScript, we have now changed the output frame to use a monospaced font. This makes for instance SearchEngine.executeTextTable() look much better. One issue that has been quite annoying for some time is the complex process for creating a trace for the scripts related to a Custom screen. This has now been simplified by adding a new tab to the "View screen" screen, listing all the scripts related to this screen. Just click a script to create a trace for it. Finally, a small improvement in the view for listing extra tables. We have now added links for searching a table or creating a new record in a table from this view, so you don't have to navigate through Requests > Extra tables, and then find the same table you just edited. Together with some other bug fixes, we sincerely hope our latest version of SuperOffice will be a welcome improvement to your developer experience!

Michel Krohn-Dale
08-06-2022
thumb_up5 mode_comment3

Quote Connector REST API proposal

Take a moment to read the Quote Connector REST API proposal  

Christian Mogensen
04-05-2022
thumb_up1 mode_comment0

The future of NetServer

SuperOffice delivers multiple products. Customers see the Sales and Service applications, Customer’s customers see things like Chat, Forms, and the customer-facing side of Service; and both Customers and Partners see our programming interfaces. Those interfaces exist in several different ways. At the least-technical end we have configuration and preferences – these are “interfaces” that require no programming, but can still influence what the application does to a considerable degree. Then we have CRMScript and Configurable Screens, where a combination of programming and configuration is used to change the application. If we step into the more technical programming domain, we come to the Web Service API of NetServer, consisting of the endpoints that are strictly defined in our service model. Building NetServer web services based on service-oriented/service-agent architecture allows us to overlay (or expose) our API in various standardized ways, such as SOAP, REST, OData, and potentially other protocols based on industry trends.  These API’s are mostly accessed from programs, requiring a developer to write the code that uses them to perform some task. Up to this point, Online and Onsite installations offer the same API functionality; and they do so in a way that hides the implementation details (as well as many database details) from the caller. On a more technical level, we distribute SDK packages with the actual NetServer code, such that any public class XYZ is available to call. This gives access to lower-level code, including the ability to perform queries and updates on any table; access business logic. We also provide the ability to add logic with plugins that extend or override our functionality. It’s also possible to load a script file or DLL that our service API layer calls before and after any service API request is executed. Parallel to that, it’s possible to supply XML files that extend or override the logic in our PageBuilder user-interface engine; typically, in connection with backend plugins that provide server-side logic. These ways of accessing our code are only available Onsite , and it’s these that are the subject of the rest of this article. The world is changing – in fact it never stopped NetServer was designed quite some time ago, and I’m proud of the job the whole team did back then. While the world has progressed from .NET 1.0 up to today, the basics of NetServer have stayed the same – so much that we are almost completely compatible with code written 10 years ago. This has benefitted both us and the partners, providing a stable platform for development. But while we have held still, the world has moved. Some fashions have come and gone, some have persisted, and new ones have arrived. Now the distance between the world we designed for and the world of today has become so large that we must move with it. I am of course referring to .NET 6, but not only that, or at least not just the direct consequences. When Microsoft started the .NET Core project, one of their really fundamental decisions was to break compatibility with the platform . This means a valid .NET 4.8 program may not compile or run in .NET 6; and it may force a non-trivial rewrite. Examples of this include: When we first implemented NetServer, Windows Communication Foundation was brand new (SuperOffice was among the companies that sent a delegation to Redmond to beta test it). In .NET 6 it does not exist. NetServer was designed with “ambient variables” that keep track of identity, database, configuration and threading – as was the best practice at the time. .NET 6 is built around dependency injection and thus explicit management of these things. NetServer has a thread manager that is very conscious of when threads are created, run and destroyed; and integrates this tightly with the ambient variables. .NET 6 is totally oriented to async/await where actual threads are incidental and it’s in practice impossible to replicate the current NetServer approach. We are now at the point where Microsoft has effectively put .NET 4.8 on life support. The latest versions of the C# language are not supported (see link ). The tool chains are dying, and if you complain the answer is more and more often “hey, why are you still on 4.8?”. Finally, the demands and idioms of modern testing are not compatible with the ambient variables and “implicitness” of NetServer. It was a good pattern for its time, but that time is behind us. ‘nuff said – we have known for some time that we need to move, and we have started to do so. Compatibility – where and when The most important point I wish to make here is this: Our API endpoints – the Agents and their methods – will stay the same. If you are already integrating with us by calling them via REST, you’re fine. Read the rest if you want, or just go do something else. If you are calling us via WCF, please prepare to stop doing that and switch to REST. We will help, and we will keep WCF for a while yet, but just like .NET 4.8, WCF should be considered a dead end now. If you have NetServer in-process and are calling or extending our code directly, it’s going to be worse. It’s clear that we need to make some drastic refactoring in our code. Switching from ambient variables to explicit dependency injection is going to touch just about every class (even if much of the actual object management is done through factories, service providers and dependency resolvers at runtime). The next onsite release of NetServer will contain the first block of these changes. Startup of NetServer will no longer be implicit/lazy, but will instead use the standard startup and composition patterns from .NET 6. We have made startup methods for various environments (WinForms/desktop; IIS web; win service; …) but it doesn’t happen by itself – in fact that’s the point of having an explicit startup. It means that all integrations that run NetServer in process will have to add Startup code, suitable for their environment. But it doesn’t stop there. It doesn’t make sense for us to just make the least possible amount of changes, because that would just incur the costs without reaping the gains. There are many things that can be done once we’re on .NET 6 – think about async/await; operation cancellation; better state and identity handling. All of these will need changes to public classes: for instance, to enable cancellation all the way to the database, cancellation tokens will make an appearance and that will be reflected in various internal api’s. On top of that, there are functional refactoring's that we wish to make. There’s stuff in the userpreference table that should be elsewhere; the visiblefor table can be scrapped; user-defined fields and extra_fields should become the same thing, i.e. custom fields . There's 100.000 lines of generated code for Row field-change events that are never used. I could go on but the idea should be clear: None of these changes will break the Service API; all of them will break some class compatibility and many will break low-level database compatibility. A dark future? As always, SuperOffice strives to satisfy all parties – customers, partners and ourselves. We actually believe in our own slogan, that relations matter; and we’re not going to make life difficult for anyone on purpose. However, we both want and need to move with the rest of the industry; we can’t let our ecosystem become obsolete in the name of compatibility. That would ultimately hurt everyone. We therefore need to manage this transition as well as we possibly can. Our customers are migrating to the Cloud and in a sense that solves it, because the low-level integrations that get broken by our changes don’t exist there. We take the compatibility on the API level extremely seriously and do not plan to break it. As far as possible, we will try to give reasonable warning times when we see that a breaking change is coming; and we will do our best to have help and advice available on how to tackle it. Any such changes break our own code too, so we definitely eat our own dog food and should therefore be able to present solutions. We’d also like to involve partners to better understand what kind of changes hurt and how much; and if there are particular things we can do, avoid or retime to lessen the pain. All that said… we’ve spent more than a year on preparatory work. Migrating our code management to the cloud; rebuilding tool chains; cleaning out all sorts of old spiders and bones from our code; investigating different technologies and alternatives. Now we’re at the point where the actual transitional work has started, and it will accelerate throughout this year. While the transition may be a trifle rough, I very much look forward to when we are done. In tandem with the evolution of our Online platform, our code will be better positioned than ever to work in a Cloud environment, carrying developers, partners and customers from one era of computing to another. It's definitely not going to be boring.   Dr. Marek Vokáč Chief Engineer, SuperOffice R&D

Marek Vokáč
16-03-2022
thumb_up12 mode_comment0