[SOLVED] JavaScript editor does not show auto completion results

I have…

  • Read the following guideline: Troubleshooting and Support | Squidex. I understand that my support request might get deleted if I do not follow the guideline.
  • Used code blocks with ``` to format my code examples like JSON or logs properly.

I’m submitting a…

  • Regression (a behavior that stopped working in a new release)
  • Bug report
  • Performance issue
  • Documentation issue or request

Current behavior

Hard to do much scripting with a textbox.

Expected behavior

With a larger text input field I could possibly write a proper JavaScript.

Minimal reproduction of the problem

  1. Login to admin UI.
  2. Access one of your Apps.
  3. Click Rules on the vertical tab bar.
  4. Click New Rule.
  5. Select a rule condition, such as in my case, If … Content changed.
  6. Minimally configure the rule condition, such as in my case, including 'pages' when event.type == 'Created' || event.type == 'Updated'.
  7. Select the rule predicate then … Script (Runs a custom Javascript).
  8. Observe the problem: Script is an tag rather than a or proper script editor. I would perhaps think it best to have an incorporated VS Code web editor but anything that would let me edit it would be better than a text box.

Environment

App Name: dasher-supply

  • Self hosted with docker
  • Self hosted with IIS
  • Self hosted with other version
  • Cloud version

Version: 7.14.0.0

Browser:

  • Chrome (desktop)
  • Chrome (Android)
  • Chrome (iOS)
  • Firefox
  • Safari (desktop)
  • Safari (iOS)
  • IE
  • Edge

Others:

I mean unless you intended for me to refer to a Javascript file somewhere. In which case, I need some documentation for how I can go about ensuring this can run.

As a workaround I could put a JavaScript in a Node.js express server and push it to a webhook and then have the express server API put what I want in the slugs collection. I need it because I have several schemas with slugs and I want to cache my slugs for faster lookups, as they all share the same global URL namespace on my frontend and I need to perform one lookup to figure out how to handle the route so I can avoid querying several tables looking for a slug match. I guess there may be other ways to solve the problem of caching the data, but it would be just as easy perhaps to add a script. Also some documentation on what objects are made available at runtime would be much appreciated. I’m mostly guessing when it comes to these event-handling scripts.

I noticed that I didn’t put some of my <code/> into the correct fields and it truncated my complaint. The script input are <input type="text" /> instead of <textarea />.

Hi,

it is actually not a normal text field, e.g. you get colors there:

There is actually a API which provides a list of autocompletion fields:

[
    {
        "path": "characterCount(text)",
        "type": "Function",
        "description": "Counts the number of characters in a text. Useful in combination with html2Text or markdown2Text."
    },
    {
        "path": "contentAction",
        "type": "Function",
        "description": "The status of the content."
    },
    {
        "path": "contentUrl",
        "type": "Function",
        "description": "The URL to the content in the UI."
    },
    {
        "path": "deleteJSON(url, callback, headers?, ignoreError?)",
        "type": "Function",
        "description": "Makes a DELETE request to the defined URL and parses the result as JSON. Headers are optional."
    },
    {
        "path": "event",
        "type": "Object",
        "description": "The actual event."
    },
    {
        "path": "event.actor",
        "type": "Object",
        "description": "The user or client that triggered the event or command."
    },
    {
        "path": "event.actor.identifier",
        "type": "String",
        "description": "The ID of this actor."
    },
    {
        "path": "event.actor.type",
        "type": "String",
        "description": "The type of this actor."
    },
    {
        "path": "event.appId",
        "type": "Object",
        "description": "The ID of the current app."
    },
    {
        "path": "event.appId.id",
        "type": "String",
        "description": "The ID part of this ID."
    },
    {
        "path": "event.appId.name",
        "type": "String",
        "description": "The name part of this ID."
    },
    {
        "path": "event.created",
        "type": "String",
        "description": "The timestamp when the object was created."
    },
    {
        "path": "event.createdBy",
        "type": "Object",
        "description": "The user who created the object."
    },
    {
        "path": "event.createdBy.identifier",
        "type": "String",
        "description": "The ID of this actor."
    },
    {
        "path": "event.createdBy.type",
        "type": "String",
        "description": "The type of this actor."
    },
    {
        "path": "event.data",
        "type": "Object",
        "description": "The data of the content."
    },
    {
        "path": "event.data.description",
        "type": "Object",
        "description": "Description field."
    },
    {
        "path": "event.data.description.iv",
        "type": "String",
        "description": "Description field (Invariant)."
    },
    {
        "path": "event.data.slug",
        "type": "Object",
        "description": "Slug field."
    },
    {
        "path": "event.data.slug.iv",
        "type": "String",
        "description": "Slug field (Invariant)."
    },
    {
        "path": "event.data.Teaser",
        "type": "Object",
        "description": "Teaser field."
    },
    {
        "path": "event.data.Teaser.iv",
        "type": "String",
        "description": "Teaser field (Invariant)."
    },
    {
        "path": "event.data.text",
        "type": "Object",
        "description": "Text field."
    },
    {
        "path": "event.data.text.iv",
        "type": "String",
        "description": "Text field (Invariant)."
    },
    {
        "path": "event.data.title",
        "type": "Object",
        "description": "Title field."
    },
    {
        "path": "event.data.title.iv",
        "type": "String",
        "description": "Title field (Invariant)."
    },
    {
        "path": "event.dataOld",
        "type": "Object",
        "description": "The previous data of the content."
    },
    {
        "path": "event.dataOld.description",
        "type": "Object",
        "description": "Description field."
    },
    {
        "path": "event.dataOld.description.iv",
        "type": "String",
        "description": "Description field (Invariant)."
    },
    {
        "path": "event.dataOld.slug",
        "type": "Object",
        "description": "Slug field."
    },
    {
        "path": "event.dataOld.slug.iv",
        "type": "String",
        "description": "Slug field (Invariant)."
    },
    {
        "path": "event.dataOld.Teaser",
        "type": "Object",
        "description": "Teaser field."
    },
    {
        "path": "event.dataOld.Teaser.iv",
        "type": "String",
        "description": "Teaser field (Invariant)."
    },
    {
        "path": "event.dataOld.text",
        "type": "Object",
        "description": "Text field."
    },
    {
        "path": "event.dataOld.text.iv",
        "type": "String",
        "description": "Text field (Invariant)."
    },
    {
        "path": "event.dataOld.title",
        "type": "Object",
        "description": "Title field."
    },
    {
        "path": "event.dataOld.title.iv",
        "type": "String",
        "description": "Title field (Invariant)."
    },
    {
        "path": "event.id",
        "type": "String",
        "description": "The ID of the object (usually GUID)."
    },
    {
        "path": "event.lastModified",
        "type": "String",
        "description": "The timestamp when the object was updated the last time."
    },
    {
        "path": "event.lastModifiedBy",
        "type": "Object",
        "description": "The user who updated the object the last time."
    },
    {
        "path": "event.lastModifiedBy.identifier",
        "type": "String",
        "description": "The ID of this actor."
    },
    {
        "path": "event.lastModifiedBy.type",
        "type": "String",
        "description": "The type of this actor."
    },
    {
        "path": "event.name",
        "type": "String",
        "description": "The name of the event."
    },
    {
        "path": "event.newStatus",
        "type": "String",
        "description": "The new status of the content."
    },
    {
        "path": "event.schemaId",
        "type": "Object",
        "description": "The version of the objec."
    },
    {
        "path": "event.schemaId.id",
        "type": "String",
        "description": "The ID part of this ID."
    },
    {
        "path": "event.schemaId.name",
        "type": "String",
        "description": "The name part of this ID."
    },
    {
        "path": "event.status",
        "type": "String",
        "description": "The status of the content."
    },
    {
        "path": "event.timestamp",
        "type": "String",
        "description": "The event timestamp."
    },
    {
        "path": "event.type",
        "type": "String",
        "description": "The type of the event.",
        "allowedValues": [
            "Created",
            "Deleted",
            "Published",
            "StatusChanged",
            "Updated",
            "Unpublished",
            "ReferenceUpdated"
        ]
    },
    {
        "path": "event.user",
        "type": "Object",
        "description": "Information about the current user."
    },
    {
        "path": "event.user.claims",
        "type": "Object",
        "description": "The additional properties of this user."
    },
    {
        "path": "event.user.claims.name",
        "type": "Array",
        "description": "The list of additional properties that have the name 'name'."
    },
    {
        "path": "event.user.email",
        "type": "String",
        "description": "The email address ofthis  user."
    },
    {
        "path": "event.user.id",
        "type": "String",
        "description": "The ID of this user."
    },
    {
        "path": "event.user.isClient",
        "type": "Boolean",
        "description": "True when this user is a client, which is typically the case when the request is made from the API."
    },
    {
        "path": "event.user.isUser",
        "type": "Boolean",
        "description": "True when this user is a user, which is typically the case when the request is made in the UI."
    },
    {
        "path": "event.version",
        "type": "Number",
        "description": "The version of the objec."
    },
    {
        "path": "formatDate(data, pattern)",
        "type": "Function",
        "description": "Formats a JavaScript date object using the specified pattern."
    },
    {
        "path": "formatTime(text)",
        "type": "Function",
        "description": "Formats a JavaScript date object using the specified pattern."
    },
    {
        "path": "generate(prompt, callback?",
        "type": "Function",
        "description": "Uses OpenAI or other machine learning platforms to generate content from a prompt."
    },
    {
        "path": "getAsset(ids, callback)",
        "type": "Function",
        "description": "Queries the asset with the specified ID and invokes the callback with an array of assets.",
        "deprecationReason": "Please use getAssetV2, which returns a single asset and not an array."
    },
    {
        "path": "getAssetBlurHash(asset, callback, x?, y?)",
        "type": "Function",
        "description": "Gets the blur hash of an asset if it is an image or null otherwise."
    },
    {
        "path": "getAssets(ids, callback)",
        "type": "Function",
        "description": "Queries the assets with the specified IDs and invokes the callback with an array of assets."
    },
    {
        "path": "getAssetText(asset, callback, encoding?)",
        "type": "Function",
        "description": "Get the text of an asset. Encodings: base64,ascii,unicode,utf8"
    },
    {
        "path": "getAssetV2(ids, callback)",
        "type": "Function",
        "description": "Queries the asset with the specified ID and invokes the callback with the found asset or null otherwise."
    },
    {
        "path": "getJSON(url, callback, headers?, ignoreError?)",
        "type": "Function",
        "description": "Makes a GET request to the defined URL and parses the result as JSON. Headers are optional."
    },
    {
        "path": "getReference(id, callback)",
        "type": "Function",
        "description": "Queries the content item with the specified ID and invokes the callback with an array of contents.",
        "deprecationReason": "Please use getReferenceV2, which returns a single content item and not an array."
    },
    {
        "path": "getReferences(ids, callback)",
        "type": "Function",
        "description": "Queries the content items with the specified IDs and invokes the callback with an array of contents."
    },
    {
        "path": "getReferenceV2(id, callback)",
        "type": "Function",
        "description": "Queries the content item with the specified ID and invokes the callback with the found content item or null otherwise."
    },
    {
        "path": "guid()",
        "type": "Function",
        "description": "Generates a guid."
    },
    {
        "path": "html2Text(text)",
        "type": "Function",
        "description": "Converts a HTML string to plain text."
    },
    {
        "path": "incrementCounter(name)",
        "type": "Function",
        "description": "Increments the counter with the given name and returns the value (OBSOLETE)."
    },
    {
        "path": "incrementCounterV2(name, callback?)",
        "type": "Function",
        "description": "Increments the counter with the given name and returns the value."
    },
    {
        "path": "markdown2Text(text)",
        "type": "Function",
        "description": "Converts a markdown string to plain text."
    },
    {
        "path": "md5(text)",
        "type": "Function",
        "description": "Calculate the MD5 hash from a given string. Use this method for hashing passwords, when backwards compatibility is important."
    },
    {
        "path": "patchJSON(url, body, callback, headers?, ignoreError?)",
        "type": "Function",
        "description": "Makes a PATCH request to the defined URL and parses the result as JSON. Headers are optional."
    },
    {
        "path": "postJSON(url, body, callback, headers?, ignoreError?)",
        "type": "Function",
        "description": "Makes a POST request to the defined URL and parses the result as JSON. Headers are optional."
    },
    {
        "path": "putJSON(url, body, callback, headers?, ignoreError?)",
        "type": "Function",
        "description": "Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional."
    },
    {
        "path": "resetCounter(name, callback?, value?)",
        "type": "Function",
        "description": "Resets the counter with the given name to zero."
    },
    {
        "path": "resetCounter(name, value?)",
        "type": "Function",
        "description": "Resets the counter with the given name to zero (OBSOLETE)."
    },
    {
        "path": "sha256(text)",
        "type": "Function",
        "description": "Calculate the SHA256 hash from a given string. Use this method for hashing passwords."
    },
    {
        "path": "sha512(text)",
        "type": "Function",
        "description": "Calculate the SHA256 hash from a given string. Use this method for hashing passwords."
    },
    {
        "path": "slugify(text)",
        "type": "Function",
        "description": "Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs."
    },
    {
        "path": "toCamelCase(text)",
        "type": "Function",
        "description": "Converts a text to camelCase."
    },
    {
        "path": "toPascalCase(text)",
        "type": "Function",
        "description": "Converts a text to PascalCase"
    },
    {
        "path": "translate(text, language, callback, sourceLanguage?",
        "type": "Function",
        "description": "Translate from the source text to the specified language."
    },
    {
        "path": "updateAsset(asset, metadata)",
        "type": "Function",
        "description": "Update the metadata of the asset."
    },
    {
        "path": "wordCount(text)",
        "type": "Function",
        "description": "Counts the number of words in a text. Useful in combination with html2Text or markdown2Text."
    }
]

I have to check whether this is also supposed to work with the conditions. Might be a bug. There are a few after the migration to the latest angular version.

Oh okay, so you are assuming the only script I might want to run in a rule is a predefined script? Since it’s not the design of it, you may consider this not a bug.

I will comment that it wouldn’t be much harder to just let someone type in a whole script. It might be more dangerous depending on if you trust the user. I trust me. There wouldn’t be anything to prevent a longer script from calling into a shorter one, so you would retain 100% of the design and permit more use cases. In Node.js, I use vm’s for running my evals.

My use case for this rule was more complicated as evidenced by my feature request as well. I’ve moved that functionality into my frontend startup routine and I will also put a backdoor webhook on the internal Docker network in so that the webhook can update the backend according to any changes in the schema projections.

I don’t understand how you concluded that. The auto completion provides basic snippets about available fields and so on. It is not a full scripting environment and server and it was never meant like that. I don’t say that it cannot be changed, but this would be a huge effort. Because you need…

  1. Dedicated instances so that scripts cannot bring your whole server down (as you mentioned):
  2. Monitoring and logging features to keep track of errors and stuff like that.
  3. Typescript support and type definitions.

It would be a lot of effort. Squidex is not built with NodeJS, it uses a script engine called Jint: GitHub - sebastienros/jint: Javascript Interpreter for .NET. It is written in .NET and not as fast as V8. The advantage is that it is easier to build APIs for existing .NET features. This is probably not a good foundation for complicated scripts. Therefore I would probably use deno (because of the security features) and write a microservice that executes the scripts in a Javascript / Environment. Of course this has some other disadvantages, because you have to make HTTP calls. Therefore not be a good idea to run small scripts like conditions in this microservice, because of the high costs of the extra API call, which eats all the performance gains of V8 vs Jint.

I see your point. JavaScript has grown into quite an ecosystem of things. And the order of importance of those things changes frequently.

Perhaps I still don’t fully understand the Run Script action feature, and that’s okay. Because I don’t think I’m going to use it. I have a higher productivity inside my own code base and truly a custom solution is probably best in my case. I would not want to compromise the response time of the API server a single bit.

I do use custom JavaScript on the collections to ensure my slugs are formatted the way I want them when saving (creating/updating) and to copy data from references as needed, and that’s hard to get right as well with no type hinting language server helping. But action-reaction triggers are hard to get right within a config interface because there are like infinity uses for them. As long as we can do webhooks it isn’t even important to complicate the .NET code space at all.

As for the deno (Rust) Node.js (C) alternative, bun (Zig) comes to my mind first lately. I’m using Astro on my frontend and I am not sure if everything will work correctly in bun, so I’m sticking to Node for now, but I’ve got bun on my mind. I am impressed with the simplicity of bun from a dev perspective–it kind of cuts out a lot of the necessary boilerplate to just run TypeScript. While bun is written in Zig, you can possibly cross into Zig world from .NET with Zig-SDK. If you do decide to integrate with deno, I’ve also heard that Zig makes it easier to integrate Rust into other languages as well.

Again, I really like what Squidex does. I’m suddenly a fan boy. I originally started modelling my app in Strapi because it was close to what I was using on the frontend, but I found that I needed more flexible models, like MongoDB, instead of the constraints of the traditional relational model. Once in a while, I find myself craving some of the relational paradigms.

I’m done with this report if you are. Not a bug. Thanks for your help! :slight_smile:

That’s the big advantage of webhooks. In one of my webhooks I have 10+ webhooks and they are super easy to test.

For me the runtime is not that important. it should be a separate process and it needs the ability to scale independently from Squidex, so I would probably use https://www.openfaas.com/ or something like that.

Thank you :slight_smile:

The problem with autocompletion is definitely a bug. Ace Editor has changed something with the plugin loading mechanism and we have not found it automatically. Need more tests.