Custom App in Freshdesk: Button Only Works Occasionally

Hello everyone,

I built a custom app for our company that adds a button to the top navigation bar in Freshdesk when viewing a ticket. When this button is clicked, a dialog opens with a form. Based on the values entered in the form, appropriate time entries are then created.

The app generally works well, but we often experience the issue that the dialog does not open, or it opens but the button inside it cannot be clicked. This happens randomly and without any recognizable pattern. It occurs for almost all users. It also sometimes happens that when two users are using the app at the same time, one can use the function while the other cannot.
There are no error messages in the console logs either. One time there was a message saying that app.js could not be loaded, but I couldn’t reproduce this message.

Does anyone have an idea what could be causing this? What input would you need from me to help troubleshoot this issue?

Thanks in advance!
Daniel

Here is my manifest.json:

{
  "platform-version": "3.0",
  "modules": {
    "common": {
      "requests": {
        "createTimeEntryForTicket": {}
      }
    },
    "support_ticket": {
      "location": {
        "ticket_top_navigation": {
          "url": "dialog.html",
          "icon": "styles/images/stopwatch.svg"
        }
      }
    }
  },
  "engines": {
    "node": "18.20.3",
    "fdk": "9.3.1"
  },
  "permissions": {
    "ticket_time_entry": {
      "create": true
    }
  }
}

Hi @Daniel_Bohme,

Can you also share the HTML file content that is relevant to this problem to check if it’s properly used?

Have you used the app lifecycle methods properly to listen to the DOM events only after the app is activated?

Hi @Raviraj,

here are my html and js files:

dialog.html

<!DOCTYPE html>

<html lang="de">

<head>
  <title>Time Protocol Helper</title>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <script async src="{{{appclient}}}"></script>
  <script async src="scripts/dialog.js"></script>
  <link rel="stylesheet" type="text/css" href="styles/style.css" />
</head>

<body>
  <div class='container-fluid'>
    <form>
      <fw-input id="input_worked_time" type="number" min="0" label="Geleistete Zeit (in Minuten)" required></fw-input>
      <fw-input id="input_billable_time" type="number" min="0" label="Abrechenbare Zeit (in Minuten)"
        required></fw-input>
      <fw-textarea id="textarea_description" rows="4" max-rows="10" label="Was wurde gemacht? (Beschreibung)"
        required></fw-textarea>
      </br>
      <fw-button id="submit_button" color="primary">Protokoll anlegen</fw-button>
    </form>
  </div>
</body>

<script defer src="scripts/app.js"></script>
<script async type="module"
  src="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/dist/crayons/crayons.esm.js"></script>
<script async nomodule src="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/dist/crayons/crayons.js"></script>

</html>

dialog.js

document.addEventListener("DOMContentLoaded", async function () {
    const client = await app.initialized();

    const workedTimeInput = document.getElementById("input_worked_time");
    const billableTimeInput = document.getElementById("input_billable_time");
    const descriptionTextarea = document.getElementById("textarea_description");
    const submitButton = document.getElementById("submit_button");

    // Add an event listener that copies the worked time into the billable time field
    workedTimeInput.addEventListener("fwBlur", function () {
        const workedTimeValue = workedTimeInput.value;
        billableTimeInput.value = workedTimeValue;
        billableTimeInput.setAttribute("max", workedTimeValue);
    });

    // Add event listener for the form submit
    submitButton.addEventListener("click", async function (event) {
        event.preventDefault();

        // Reset error states
        resetForm(workedTimeInput, billableTimeInput, descriptionTextarea);

        // Validate Form Input
        const isValid = validateForm(workedTimeInput, billableTimeInput, descriptionTextarea);

        // Create Entries if Input is valid
        if (isValid) {
            try {
                // Parse TimeEntries
                const billableTimeMinutes = parseInt(billableTimeInput.value);
                const workedTimeMinutes = parseInt(workedTimeInput.value);

                // Create billable TimeEntry
                await createTimeEntryForTicket(
                    client,
                    descriptionTextarea.value,
                    billableTimeMinutes,
                    true
                );

                // If workedTime is greater then billableTime create a second TimeEntry
                if (workedTimeMinutes > billableTimeMinutes) {

                    // Calculate how much time is left for workedTime Entry
                    const calculatedTimeMinutes = workedTimeMinutes - billableTimeMinutes;

                    // Create worked TimeEntry
                    await createTimeEntryForTicket(
                        client,
                        descriptionTextarea.value,
                        calculatedTimeMinutes,
                        false
                    );
                }
            } catch (error) {
                // Log Error
                console.error(error);
            }

            // Close Dialog
            await client.instance.close();
        }
    });
});

// Convert Minute TimeFormat in HH:MM
function convertMinutesToHHMM(minutes) {
    const hrs = Math.floor(minutes / 60);
    const mins = minutes % 60;
    const paddedHrs = String(hrs).padStart(2, '0');
    const paddedMins = String(mins).padStart(2, '0');
    return `${paddedHrs}:${paddedMins}`;
}

// Invoke a TimeEntry Request with given Parameters 
async function createTimeEntryForTicket(client, note, timeSpent, billable) {
    // Get agentId from loggedInUser
    const loggedInAgent = await client.data.get("loggedInUser");
    const agentId = loggedInAgent.loggedInUser.id;

    // Get ticketId from opened Ticket
    const ticketData = await client.data.get("ticket");
    const ticketId = ticketData.ticket.id;

    // Convert spent time
    const timeSpentHHMM = convertMinutesToHHMM(timeSpent);

    // Create request body
    const body = {
        "note": note,
        "time_spent": timeSpentHHMM,
        "agent_id": agentId,
        "billable": billable
    }

    // Invoke API Request for TimeEntry
    await client.request.invokeTemplate("createTimeEntryForTicket", {
        context: {
            ticketId: ticketId
        },
        body: JSON.stringify(body)
    });
}

// Validates Form Input
function validateForm(workedTimeInput, billableTimeInput, descriptionTextarea) {
    let isValid = true;

    // Check if worked time is not empty
    if (!workedTimeInput.value) {
        isValid = false;
        workedTimeInput.setAttribute("state", "error");
        workedTimeInput.setAttribute("error-text", "Dieses Feld ist erforderlich.");
    }

    // Check if billable time is not empty and is not greater then worked time
    if (!billableTimeInput.value || parseInt(billableTimeInput.value) > parseInt(workedTimeInput.value)) {
        billableTimeInput.setAttribute("state", "error");
        billableTimeInput.setAttribute("error-text", "Dieses Feld ist erforderlich und darf nicht größer als die geleistete Zeit sein.");
    }

    // Check if description is not empty
    if (!descriptionTextarea.value) {
        isValid = false;
        descriptionTextarea.setAttribute("state", "error");
        descriptionTextarea.setAttribute("error-text", "Dieses Feld ist erforderlich.");
    }

    return isValid;
}

// Reset the Form
function resetForm(workedTimeInput, billableTimeInput, descriptionTextarea) {
    workedTimeInput.removeAttribute("state");
    workedTimeInput.removeAttribute("error-text");
    billableTimeInput.removeAttribute("state");
    billableTimeInput.removeAttribute("error-text");
    descriptionTextarea.removeAttribute("state");
    descriptionTextarea.removeAttribute("error-text");
}

app.js

init();

async function init() {
  const client = await app.initialized();
  client.events.on('app.activated', () => {
    showTimeEntryDialog(client);
  });
}

function showTimeEntryDialog(client) {
  try {
    client.interface.trigger('showDialog', {
      title: 'Arbeitszeit protokollieren',
      template: 'dialog.html'
    });
  } catch (error) {
    console.error(error);
  }
}

@Daniel_Bohme I see that your app’s main code and the modal dialog share the same HTML file.

For the ticket_top_navigation, there’s no real estate available to display some elements. But, it should have its own HTML file, and all the JavaScript logic will work with all the data, events, and interface methods available in the page.

Since the app opens a dialog immediately, you will have to add a dedicated HTML file for the app and a separate one for the dialog. The app.js and dialog.js should not be used together, either.
Can you please try this method and see if it works fine without any occasional issues?

If it keeps happening, please follow up on the support ticket that you have raised. The respective product team will check it to confirm any issues and fix them. Since the product team is not available here, a support ticket will be the next action.

Hi @Raviraj,

i´ve seperated the dialog.html in 2 single files, the Problem still exists. I have this error message in the Console:

index.html

<!DOCTYPE html>

<html lang="de">

<head>
    <title>Time Protocol Helper</title>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script async src="{{{appclient}}}"></script>
    <link rel="stylesheet" type="text/css" href="styles/style.css" />
</head>

<body>
</body>

<script defer src="scripts/app.js"></script>
<script async type="module"
    src="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/dist/crayons/crayons.esm.js"></script>
<script async nomodule src="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/dist/crayons/crayons.js"></script>

</html>

dialog.html

<!DOCTYPE html>

<html lang="de">

<head>
  <title>Time Protocol Helper</title>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <script async src="{{{appclient}}}"></script>
  <script async src="scripts/dialog.js"></script>
  <link rel="stylesheet" type="text/css" href="styles/style.css" />
</head>

<body>
  <div class='container-fluid'>
    <form>
      <fw-input id="input_worked_time" type="number" min="0" label="Geleistete Zeit (in Minuten)" required></fw-input>
      <fw-input id="input_billable_time" type="number" min="0" label="Abrechenbare Zeit (in Minuten)"
        required></fw-input>
      <fw-textarea id="textarea_description" rows="4" max-rows="10" label="Was wurde gemacht? (Beschreibung)"
        required></fw-textarea>
      </br>
      <fw-button id="submit_button" color="primary">Protokoll anlegen</fw-button>
    </form>
  </div>
</body>

<script async type="module"
  src="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/dist/crayons/crayons.esm.js"></script>
<script async nomodule src="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/dist/crayons/crayons.js"></script>

</html>

Hi @Daniel_Bohme,

The order of the script loading matters. The frontend methods depends on the appclient script. If using Crayons components, they should also be loaded first before use.

The async keyword in the script loading does not ensure the order of loading. So, can you avoid it and use the keyword defer instead and reorder according to the need as follows in your dialog.html file?
The module scripts don’t need defer as it’s deferred by default.

<head>
  <!-- 1. appclient -->
  <script defer src="{{{appclient}}}"></script>

  <!-- 2. crayons.esm.js -->
  <script type="module" src="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/dist/crayons/crayons.esm.js"></script>

  <!-- 3. crayons.js -->
  <script nomodule defer src="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/dist/crayons/crayons.js"></script>

  <!-- 4. dialog.js -->
  <script defer src="dialog.js"></script>
</head>

Can you try and confirm if it works or still the issue repeats?

It looks like the issue is resolved. I’ll keep an eye on it and get back to you if anything comes up.

Thanks!

1 Like

This topic was automatically closed 6 days after the last reply. New replies are no longer allowed.